let d = import "adr-defaults.ncl" in d.make_adr { id = "adr-010", title = "Protocol Migration System — Progressive NCL Checks for Consumer Project Upgrades", status = 'Accepted, date = "2026-03-28", context = "As the ontoref protocol evolved (manifest.ncl self-interrogation, typed ADR checks, CLAUDE.md agent entry-point, justfile convention), the adoption tooling relied on static prompt templates with manual {placeholder} substitution. An agent or developer adopting ontoref had no machine-queryable way to know which protocol features were missing from their project, nor how to apply them in a safe, ordered sequence. The template approach produced four separate documents that drifted out of sync with the actual protocol state and required human judgement to determine which ones applied. There was no idempotency guarantee and no check mechanism — a project that had already applied a change would re-read instructions that no longer applied.", decision = "Protocol upgrades for consumer projects are expressed as ordered NCL migration files in reflection/migrations/NNN-slug.ncl. Each migration declares: id (zero-padded 4-digit string), slug, description, a typed check record (FileExists | Grep | NuCmd), and an instructions string interpolated at runtime with project_root and project_name. Applied state is determined solely by whether the check passes — there is no state file. This makes migrations fully idempotent: running `migrate list` on an already-compliant project shows all applied with no side effects. NuCmd checks must be valid Nushell (no bash &&, $env.VAR not $VAR, no bash redirects). Grep checks targeting ADR files must use the glob pattern adrs/adr-[0-9][0-9][0-9]-*.ncl to exclude infrastructure files (adr-schema.ncl, adr-constraints.ncl, _template.ncl) that legitimately contain deprecated field names as schema definitions. The system is exposed via `ontoref migrate list`, `migrate pending`, and `migrate show ` — wired into the interactive group dispatch and help system. Migrations are advisory: the system reports state, never applies changes automatically.", rationale = [ { claim = "Check-as-source-of-truth eliminates state file drift", detail = "Any state file recording 'migration 0003 applied' becomes stale the moment someone reverts a change, changes branches, or clones a fresh repo. The check IS the state: if the condition is satisfied, the migration is applied; if not, it is pending. This is the same principle used by database migration tools that check for a schema version column — except here the 'column' is a Nushell assertion over the project's file system. No synchronization required.", }, { claim = "Typed checks (FileExists | Grep | NuCmd) cover the full protocol surface", detail = "FileExists covers structural requirements (.ontology/manifest.ncl present). Grep covers content requirements (pattern present or absent in specific files). NuCmd covers semantic requirements that require evaluation — nickel export succeeds, capabilities[] is non-empty, justfile validates. The three types compose the full assertion space without requiring a general-purpose script language in the migration definition itself.", }, { claim = "Ordered numbering enables dependency reasoning without a dependency graph", detail = "Migration 0003 (manifest self-interrogation) requires migration 0001 (manifest.ncl present) to have been applied. Rather than declaring explicit depends_on edges (which require a DAG evaluator), the numeric ordering encodes the implicit prerequisite sequence. An agent applying pending migrations in order will always satisfy prerequisites before dependent checks.", }, ], consequences = { positive = [ "`migrate pending` gives agents and developers a single authoritative list of what is missing — no manual comparison against protocol documentation", "Migrations are idempotent and safe to re-run: `migrate list` on a fully-adopted project is a no-op read", "Instructions are interpolated at runtime with project_root and project_name — no manual placeholder substitution", "New protocol features arrive as numbered migrations without touching existing template files", "NuCmd checks encode the same typed check logic used by ADR constraints in validate.nu — consistent assertion model across the protocol", ], negative = [ "NuCmd checks must be single-line Nushell (nu -c) — complex multi-step checks become dense; readability degrades for non-trivial assertions", "Grep checks require knowing which files to exclude (infrastructure vs instance files); the adr-[0-9][0-9][0-9]-*.ncl pattern is a convention that authors must follow", "Migration ordering encodes implicit dependencies — a migration that genuinely depends on two prior migrations has no way to express that formally beyond numeric sequence", ], }, alternatives_considered = [ { option = "Single monolithic adoption prompt template with {placeholder} substitution", why_rejected = "Produced four separate documents (project-full-adoption-prompt.md, update-ontology-prompt.md, manifest-self-interrogation-prompt.md, vendor-frontend-assets-prompt.md) that drifted out of sync. Required manual judgement to determine which applied to a given project. No idempotency, no machine-queryable state, no ordered application guarantee. Each new protocol feature required updating multiple templates.", }, { option = "State file recording applied migration IDs", why_rejected = "State files become stale on branch switches, cherry-picks, and fresh clones. They require commit discipline to keep in sync. A project where someone manually applied the changes without running the migration tool would show the migration as pending despite being satisfied — false negatives. The check-as-truth model has no false negatives by construction.", }, { option = "Jinja2/j2 templating for instruction rendering", why_rejected = "The ontoref runtime already runs Nushell for all automation. Adding a j2 dependency for template rendering introduces a new tool to install, configure, and maintain. Runtime string interpolation in Nushell (str replace --all) is sufficient for the two substitution values needed (project_root, project_name) and keeps the migration runner dependency-free.", }, ], constraints = [ { id = "protocol-changes-require-migration", claim = "Any change to templates/, reflection/schemas/*.ncl, .claude/CLAUDE.md, or consumer-facing reflection/modes/ that consumer projects need to adopt must be accompanied by a new migration in reflection/migrations/", scope = "all commits that touch templates/, reflection/schemas/, .claude/CLAUDE.md, or consumer-facing reflection/modes/", severity = 'Hard, check = { tag = 'Grep, pattern = "Protocol Evolution", paths = [".claude/CLAUDE.md"], must_be_empty = false }, rationale = "Migrations are the sole propagation mechanism for protocol changes. A change without a migration only applies to ontoref itself — consumer projects have no machine-queryable way to discover the change via `migrate pending`.", }, { id = "nucmd-checks-must-be-nushell", claim = "NuCmd check cmd fields must be valid Nushell — no bash operators (&&, ||, 2>/dev/null), no $VARNAME (must be $env.VARNAME)", scope = "reflection/migrations/*.ncl (any migration with tag = 'NuCmd)", severity = 'Hard, check = { tag = 'Grep, pattern = "&&|\\$[A-Z_]+[^)]", paths = ["reflection/migrations/"], must_be_empty = true }, rationale = "The migration runner executes checks via `nu -c $check.cmd`. Bash syntax in a Nu script produces parser errors that surface as false-negative check results — the migration appears pending due to a runner error, not because the condition is unmet.", }, { id = "grep-checks-use-instance-glob", claim = "Grep checks targeting ADR files must scope to adrs/adr-[0-9][0-9][0-9]-*.ncl, not adrs/ or adrs/adr-*.ncl", scope = "reflection/migrations/*.ncl (any migration with tag = 'Grep and paths containing 'adrs')", severity = 'Soft, check = { tag = 'Grep, pattern = "\"adrs/\"", paths = ["reflection/migrations/"], must_be_empty = true }, rationale = "adr-schema.ncl, adr-constraints.ncl, adr-defaults.ncl, and _template.ncl are infrastructure files that legitimately contain deprecated field names as schema definitions. Scanning all of adrs/ produces false positives in ontoref's own repo and in any consumer project that vendors the ADR schema files.", }, ], related_adrs = ["adr-001", "adr-006"], ontology_check = { decision_string = "Protocol migrations expressed as ordered NCL files with typed idempotent checks; applied state determined by check result not state file; NuCmd checks must be valid Nushell; Grep checks on ADR files must use instance-only glob", invariants_at_risk = ["no-enforcement", "self-describing"], verdict = 'Safe, }, }