ontoref/adrs/adr-010-protocol-migration-system.ncl

93 lines
9.0 KiB
Plaintext
Raw Permalink Normal View History

2026-03-29 00:19:56 +00:00
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 <id>` — 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 = [
feat: mode guards, convergence, manifest coverage, doc authoring pattern ## Mode guards and convergence loops (ADR-011) - `Guard` and `Converge` types added to `reflection/schema.ncl` and `reflection/defaults.ncl`. Guards run pre-flight checks (Block/Warn); converge loops iterate until a condition is met (RetryFailed/RetryAll). - `sync-ontology.ncl`: 3 guards + converge (zero-drift condition, max 2 iter). - `coder-workflow.ncl`: guard (coder-dir-exists) + `novelty-check` step. - Rust types in `ontoref-reflection/src/mode.rs`; executor in `executor.rs` evaluates guards before steps and convergence loop after. - `adrs/adr-011-mode-guards-and-convergence.ncl` added. ## Manifest capability completeness - `.ontology/manifest.ncl`: 3 → 19 declared capabilities covering the full action surface (daemon API, modes, Task Composer, QA, bookmarks, etc.). - `sync.nu`: `audit-manifest-coverage` + `sync manifest-check` command. - `validate-project.ncl`: 6th category `manifest-cov`. - Pre-commit hook `manifest-coverage` added. - Migrations `0010-manifest-capability-completeness`, `0011-manifest-coverage-hooks`. ## Rust doc authoring pattern — canonical `///` convention - `#[onto_api]`: `description = "..."` optional when `///` doc comment exists above handler — first line used as fallback. `#[derive(OntologyNode)]` same. - `ontoref-daemon/src/api.rs`: 42 handlers migrated to `///` doc comments; `description = "..."` removed from all `#[onto_api]` blocks. - `sync diff --docs --fail-on-drift`: exits 1 on crate `//!` drift; used by new `docs-drift` pre-commit hook. `docs-links` hook checks rustdoc broken links. - `generator.nu`: mdBook `crates/` chapter — per-crate page from `//!` doc, coverage badge, feature flags, implementing practice nodes. - `.claude/CLAUDE.md`: `### Documentation Authoring (Rust)` section added. - Migration `0012-rust-doc-authoring-pattern`. ## OntologyNode derive fixes - `#[derive(OntologyNode)]`: `name` and `paths` attributes supported; `///` doc fallback for `description`; `artifact_paths` correctly populated. - `Core::from_value` calls `merge_contributors()` behind `#[cfg(feature = "derive")]`. ## Bug fixes - `sync.nu` drift check: exact crate path match (not `str starts-with`); first-path-only rule; split on `. ` not `.` to avoid `.ontology/` truncation. - `find-unclaimed-artifacts`: fixed absolute vs relative path comparison. - Rustdoc broken intra-doc links fixed across all three crates. - `ci-docs` recipe now sets `RUSTDOCFLAGS` and actually fails on errors. mode guards/converge, manifest coverage validation, 19 capabilities (ADR-011) Extend the mode schema with Guard (pre-flight Block/Warn checks) and Converge (RetryFailed/RetryAll post-execution loops) — protocol pushes back on invalid state and iterates until convergence. ADR-011 records the decision to extend modes rather than create a separate action subsystem. Manifest expanded from 3 to 19 capabilities covering the full action surface (compose, plans, backlog graduation, notifications, coder pipeline, forms, templates, drift, quick actions, migrations, config, onboarding). New audit-manifest-coverage validator + pre-commit hook + SessionStart hook ensure agents always see complete project self-description. Bug fix: find-unclaimed-artifacts absolute vs relative path comparison — 19 phantom MISSING items resolved. Health 43% → 100%. Anti-slop: coder novelty-check step (Jaccard overlap against published+QA) inserted between triage and publish in coder-workflow. Justfile restructured into 5 modules (build/test/dev/ci/assets). Migrations 0010-0011 propagate requirements to consumer projects.
2026-03-30 19:08:25 +01:00
{
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`.",
},
2026-03-29 00:19:56 +00:00
{
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,
},
}