1705 lines
64 KiB
Plaintext
Raw Normal View History

2026-03-13 00:21:04 +00:00
#!/usr/bin/env nu
# reflection/modules/sync.nu — ontology↔code synchronization.
# Detects drift between .ontology/ declarations and actual project artifacts.
# Scans: crates, scenarios, agents, CI, forms, modes, justfiles, .claude/.
# Operates on $env.ONTOREF_PROJECT_ROOT (set by onref wrapper).
use store.nu [daemon-export, daemon-export-safe]
# Resolve project root: ONTOREF_PROJECT_ROOT if set and different from ONTOREF_ROOT, else ONTOREF_ROOT.
def project-root []: nothing -> string {
let pr = ($env.ONTOREF_PROJECT_ROOT? | default "")
if ($pr | is-not-empty) and ($pr != $env.ONTOREF_ROOT) { $pr } else { $env.ONTOREF_ROOT }
}
# ── scan ──────────────────────────────────────────────────────────────────────
# Analyze a project's real structure: crates, scenarios, agents, CI, forms, modes, justfiles, claude.
export def "sync scan" [
--level: string = "auto", # Analysis level: structural | full | auto (detect nightly)
]: nothing -> record {
let root = (project-root)
let crates = (scan-crates $root $level)
let scenarios = (scan-scenarios $root)
let agents = (scan-agents $root)
let ci = (scan-ci $root)
let forms = (scan-forms $root)
let modes = (scan-modes $root)
let justfiles = (scan-justfiles $root)
let claude = (scan-claude $root)
let api_level = if ($crates | any { |c| ($c.pub_api | length) > 0 }) { "full" } else { "structural" }
{
crates: $crates,
scenarios: $scenarios,
agents: $agents,
ci: $ci,
forms: $forms,
modes: $modes,
justfiles: $justfiles,
claude: $claude,
api_level: $api_level,
}
}
# ── diff ──────────────────────────────────────────────────────────────────────
# Compare scan against ontology, producing drift report.
export def "sync diff" [
--- feat: API catalog surface, protocol v2 tooling, MCP expansion, on+re update ## Summary Session 2026-03-23. Closes the loop between handler code and discoverability across all three surfaces (browser, CLI, MCP agent) via compile-time inventory registration. Adds protocol v2 update tooling, extends MCP from 21 to 29 tools, and brings the self-description up to date. ## API Catalog Surface (#[onto_api] proc-macro) - crates/ontoref-derive: new proc-macro crate; `#[onto_api(method, path, description, auth, actors, params, tags)]` emits `inventory::submit!(ApiRouteEntry{...})` at link time - crates/ontoref-daemon/src/api_catalog.rs: `catalog()` — pure fn over `inventory::iter::<ApiRouteEntry>()`, zero runtime allocation - GET /api/catalog: returns full annotated HTTP surface as JSON - templates/pages/api_catalog.html: new page with client-side filtering by method, auth, path/description; detail panel per route (params table, feature flag); linked from dashboard card and nav - UI nav: "API" link (</> icon) added to mobile dropdown and desktop bar - inventory = "0.3" added to workspace.dependencies (MIT, zero transitive deps) ## Protocol Update Mode - reflection/modes/update_ontoref.ncl: 9-step DAG (5 detect parallel, 2 update idempotent, 2 validate, 1 report) — brings any project from protocol v1 to v2 by adding manifest.ncl and connections.ncl if absent, scanning ADRs for deprecated check_hint, validating with nickel export - reflection/templates/update-ontology-prompt.md: 8-phase reusable prompt for agent-driven ontology enrichment (infrastructure → audit → core.ncl → state.ncl → manifest.ncl → connections.ncl → ADR migration → validation) ## CLI — describe group extensions - reflection/bin/ontoref.nu: `describe diff [--fmt] [--file]` and `describe api [--actor] [--tag] [--auth] [--fmt]` registered as canonical subcommands with log-action; aliases `df` and `da` added; QUICK REFERENCE and ALIASES sections updated ## MCP — two new tools (21 → 29 total) - ontoref_api_catalog: filters catalog() output by actor/tag/auth; returns { routes, total } — no HTTP roundtrip, calls inventory directly - ontoref_file_versions: reads ProjectContext.file_versions DashMap per slug; returns BTreeMap<filename, u64> reload counters - insert_mcp_ctx: audited and updated from 15 to 28 entries in 6 groups - HelpTool JSON: 8 new entries (validate_adrs, validate, impact, guides, bookmark_list, bookmark_add, api_catalog, file_versions) - ServerHandler::get_info instructions updated to mention new tools ## Web UI — dashboard additions - Dashboard: "API Catalog" card (9th); "Ontology File Versions" section showing per-file reload counters from file_versions DashMap - dashboard_mp: builds BTreeMap<String, u64> from ctx.file_versions and injects into Tera context ## on+re update - .ontology/core.ncl: describe-query-layer and adopt-ontoref-tooling descriptions updated; ontoref-daemon updated ("11 pages", "29 tools", API catalog, per-file versioning, #[onto_api]); new node api-catalog-surface (Yang/Practice) with 3 edges; artifact_paths extended across 3 nodes - .ontology/state.ncl: protocol-maturity blocker updated (protocol v2 complete); self-description-coverage catalyst updated with session 2026-03-23 additions - ADR-007: "API Surface Discoverability via #[onto_api] Proc-Macro" — Accepted ## Documentation - README.md: crates table updated (11 pages, 29 MCP tools, ontoref-derive row); MCP representative table expanded; API Catalog, Semantic Diff, Per-File Versioning paragraphs added; update_ontoref onboarding section added - CHANGELOG.md: [Unreleased] section with 4 change groups - assets/web/src/index.html: tool counts 19→29 (EN+ES), page counts 12→11 (EN+ES), daemon description paragraph updated with API catalog + #[onto_api]
2026-03-23 00:58:27 +01:00
--quick, # Skip nickel exports; parse NCL text directly for speed
--level: string = "", # Extra checks: "full" adds ADR violations, content assets, connection health
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
--docs, # Check for drift between crate //! doc comments and ontology node descriptions
--fail-on-drift, # Exit 1 if any DRIFT items are found (for pre-commit use)
2026-03-13 00:21:04 +00:00
]: nothing -> table {
let root = (project-root)
let scan = (sync scan --level structural)
let ontology = if $quick {
load-ontology-quick $root
} else {
load-ontology $root
}
if ($ontology | is-empty) {
print " No .ontology/core.ncl found — nothing to diff."
return []
}
let nodes = ($ontology.nodes? | default [])
let edges = ($ontology.edges? | default [])
let node_ids = ($nodes | each { |n| $n | get -o id | default "" } | where { $in | is-not-empty })
# Build set of all existing paths from scan
let existing_paths = (collect-existing-paths $scan $root)
mut report = []
# Check each node: does at least one artifact_path exist on disk?
for node in $nodes {
let id = $node.id
let paths = ($node.artifact_paths? | default [])
if ($paths | is-empty) {
# Tensions and abstract concepts have no artifact_paths — always OK
$report = ($report | append { status: "OK", id: $id, artifact_path: "", detail: "No artifact_paths declared (abstract node)" })
} else {
# Check if at least one declared path exists in the project
let found = ($paths | any { |p|
let full = $"($root)/($p)"
($full | path exists) or ($existing_paths | any { |ep| $ep starts-with $p or $p starts-with $ep })
})
if $found {
$report = ($report | append { status: "OK", id: $id, artifact_path: ($paths | first), detail: "" })
} else {
$report = ($report | append {
status: "STALE",
id: $id,
artifact_path: ($paths | str join ", "),
detail: $"Declared paths not found: ($paths | str join ', ')",
})
}
}
}
# Check scan artifacts that no node claims via artifact_paths
let all_node_paths = ($nodes | each { |n| $n.artifact_paths? | default [] } | flatten)
let unclaimed = (find-unclaimed-artifacts $scan $root $all_node_paths)
for art in $unclaimed {
$report = ($report | append {
status: "MISSING",
id: $art.id,
artifact_path: $art.path,
detail: $"Artifact exists but no ontology node claims it: ($art.kind) at ($art.path)",
})
}
# Check edge integrity
for edge in $edges {
if not ($edge.from in $node_ids) {
$report = ($report | append {
status: "BROKEN",
id: $"edge:($edge.from)->($edge.to)",
artifact_path: "",
detail: $"Edge source '($edge.from)' not found in nodes",
})
}
if not ($edge.to in $node_ids) {
$report = ($report | append {
status: "BROKEN",
id: $"edge:($edge.from)->($edge.to)",
artifact_path: "",
detail: $"Edge target '($edge.to)' not found in nodes",
})
}
}
--- feat: API catalog surface, protocol v2 tooling, MCP expansion, on+re update ## Summary Session 2026-03-23. Closes the loop between handler code and discoverability across all three surfaces (browser, CLI, MCP agent) via compile-time inventory registration. Adds protocol v2 update tooling, extends MCP from 21 to 29 tools, and brings the self-description up to date. ## API Catalog Surface (#[onto_api] proc-macro) - crates/ontoref-derive: new proc-macro crate; `#[onto_api(method, path, description, auth, actors, params, tags)]` emits `inventory::submit!(ApiRouteEntry{...})` at link time - crates/ontoref-daemon/src/api_catalog.rs: `catalog()` — pure fn over `inventory::iter::<ApiRouteEntry>()`, zero runtime allocation - GET /api/catalog: returns full annotated HTTP surface as JSON - templates/pages/api_catalog.html: new page with client-side filtering by method, auth, path/description; detail panel per route (params table, feature flag); linked from dashboard card and nav - UI nav: "API" link (</> icon) added to mobile dropdown and desktop bar - inventory = "0.3" added to workspace.dependencies (MIT, zero transitive deps) ## Protocol Update Mode - reflection/modes/update_ontoref.ncl: 9-step DAG (5 detect parallel, 2 update idempotent, 2 validate, 1 report) — brings any project from protocol v1 to v2 by adding manifest.ncl and connections.ncl if absent, scanning ADRs for deprecated check_hint, validating with nickel export - reflection/templates/update-ontology-prompt.md: 8-phase reusable prompt for agent-driven ontology enrichment (infrastructure → audit → core.ncl → state.ncl → manifest.ncl → connections.ncl → ADR migration → validation) ## CLI — describe group extensions - reflection/bin/ontoref.nu: `describe diff [--fmt] [--file]` and `describe api [--actor] [--tag] [--auth] [--fmt]` registered as canonical subcommands with log-action; aliases `df` and `da` added; QUICK REFERENCE and ALIASES sections updated ## MCP — two new tools (21 → 29 total) - ontoref_api_catalog: filters catalog() output by actor/tag/auth; returns { routes, total } — no HTTP roundtrip, calls inventory directly - ontoref_file_versions: reads ProjectContext.file_versions DashMap per slug; returns BTreeMap<filename, u64> reload counters - insert_mcp_ctx: audited and updated from 15 to 28 entries in 6 groups - HelpTool JSON: 8 new entries (validate_adrs, validate, impact, guides, bookmark_list, bookmark_add, api_catalog, file_versions) - ServerHandler::get_info instructions updated to mention new tools ## Web UI — dashboard additions - Dashboard: "API Catalog" card (9th); "Ontology File Versions" section showing per-file reload counters from file_versions DashMap - dashboard_mp: builds BTreeMap<String, u64> from ctx.file_versions and injects into Tera context ## on+re update - .ontology/core.ncl: describe-query-layer and adopt-ontoref-tooling descriptions updated; ontoref-daemon updated ("11 pages", "29 tools", API catalog, per-file versioning, #[onto_api]); new node api-catalog-surface (Yang/Practice) with 3 edges; artifact_paths extended across 3 nodes - .ontology/state.ncl: protocol-maturity blocker updated (protocol v2 complete); self-description-coverage catalyst updated with session 2026-03-23 additions - ADR-007: "API Surface Discoverability via #[onto_api] Proc-Macro" — Accepted ## Documentation - README.md: crates table updated (11 pages, 29 MCP tools, ontoref-derive row); MCP representative table expanded; API Catalog, Semantic Diff, Per-File Versioning paragraphs added; update_ontoref onboarding section added - CHANGELOG.md: [Unreleased] section with 4 change groups - assets/web/src/index.html: tool counts 19→29 (EN+ES), page counts 12→11 (EN+ES), daemon description paragraph updated with API catalog + #[onto_api]
2026-03-23 00:58:27 +01:00
# ── Full level: ADR violations, content assets, connection health ────────────
if $level == "full" {
# ADR violations: run validate check-all and capture failures
let adr_result = do { nu --no-config-file -c "use reflection/modules/validate.nu *; validate check-all --fmt json" } | complete
if $adr_result.exit_code != 0 {
let violations = do { $adr_result.stdout | from json } | complete
if $violations.exit_code == 0 {
let failed = ($violations.stdout | where { |r| ($r.passed? | default true) == false })
for v in $failed {
$report = ($report | append {
status: "BROKEN",
id: ($v.constraint_id? | default "adr-constraint"),
artifact_path: "",
detail: $"ADR constraint failed: ($v.description? | default '')",
})
}
}
}
# Content assets: verify source_path exists on disk
let manifest_file = $"($root)/.ontology/manifest.ncl"
if ($manifest_file | path exists) {
let manifest_result = do { nickel export --format json $manifest_file } | complete
if $manifest_result.exit_code == 0 {
let manifest = ($manifest_result.stdout | from json)
let assets = ($manifest.content_assets? | default [])
for asset in $assets {
let src = ($asset.source_path? | default "")
if ($src | is-not-empty) and not ($"($root)/($src)" | path exists) {
$report = ($report | append {
status: "MISSING",
id: ($asset.id? | default $src),
artifact_path: $src,
detail: $"content_asset source_path not found on disk: ($src)",
})
}
}
}
}
# Connection health: check declared project slugs exist in daemon registry
let conn_file = $"($root)/.ontology/connections.ncl"
if ($conn_file | path exists) {
let conn_result = do { nickel export --format json $conn_file } | complete
if $conn_result.exit_code == 0 {
let connections = ($conn_result.stdout | from json)
let daemon_url = ($env.ONTOREF_DAEMON_URL? | default "http://127.0.0.1:7891")
let projects_result = do { http get $"($daemon_url)/projects" } | complete
if $projects_result.exit_code == 0 {
let registered = ($projects_result.stdout | from json | get slugs? | default [])
for direction in ["upstream", "downstream", "peers"] {
let conns = ($connections | get -o $direction | default [])
for conn in $conns {
let target = ($conn.project? | default "")
if ($target | is-not-empty) and not ($target in $registered) {
$report = ($report | append {
status: "BROKEN",
id: $"connection:($target)",
artifact_path: "",
detail: $"connection ($direction) references unregistered project: ($target)",
})
}
}
}
}
}
}
}
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
if $docs {
let doc_scan = (sync scan --level structural)
let doc_drifts = (check-crate-doc-drift $root $nodes $doc_scan)
$report = ($report | append $doc_drifts)
}
let sorted = ($report | sort-by status id)
if $fail_on_drift {
let drifts = ($sorted | where status == "DRIFT")
if ($drifts | is-not-empty) {
print $"(ansi red)✗ ($drifts | length) crate\(s\) have doc drift:(ansi reset)"
for d in $drifts {
print $" ($d.id) ($d.artifact_path)"
print $" ($d.detail)"
}
exit 1
}
}
$sorted
2026-03-13 00:21:04 +00:00
}
# ── propose ───────────────────────────────────────────────────────────────────
# Generate NCL patches for drift items.
export def "sync propose" []: nothing -> string {
let diff = (sync diff)
let missing = ($diff | where status == "MISSING")
let stale = ($diff | where status == "STALE")
let broken = ($diff | where status == "BROKEN")
mut lines = []
if ($missing | is-not-empty) {
$lines = ($lines | append "# ── New nodes (MISSING artifacts found) ──")
$lines = ($lines | append "")
for item in $missing {
let id = $item.id
let path = $item.artifact_path
let kind = (detect-artifact-kind $path)
$lines = ($lines | append (generate-node-ncl $id $path $kind))
$lines = ($lines | append "")
}
}
if ($stale | is-not-empty) {
$lines = ($lines | append "# ── Stale nodes (no artifact found) — remove or update ──")
$lines = ($lines | append "")
for item in $stale {
$lines = ($lines | append $"# REMOVE: ($item.id) — ($item.detail)")
}
$lines = ($lines | append "")
}
if ($broken | is-not-empty) {
$lines = ($lines | append "# ── Broken edges — remove ──")
$lines = ($lines | append "")
for item in $broken {
$lines = ($lines | append $"# REMOVE EDGE: ($item.id) — ($item.detail)")
}
$lines = ($lines | append "")
}
if ($lines | is-empty) {
"# No drift detected — ontology is in sync."
} else {
$lines | str join "\n"
}
}
# ── apply ─────────────────────────────────────────────────────────────────────
# Apply proposed changes: insert new nodes, remove stale nodes from core.ncl.
export def "sync apply" []: nothing -> nothing {
let diff = (sync diff)
let missing = ($diff | where status == "MISSING")
let stale = ($diff | where status == "STALE")
let broken = ($diff | where status == "BROKEN")
if ($missing | is-empty) and ($stale | is-empty) and ($broken | is-empty) {
print " Ontology is in sync — nothing to apply."
return
}
let root = (project-root)
let core_ncl = $"($root)/.ontology/core.ncl"
if not ($core_ncl | path exists) {
print $" Error: ($core_ncl) not found."
return
}
print ""
print "Proposed changes:"
print "──────────────────────────────────────────────────────────────────"
if ($missing | is-not-empty) {
print $" ADD: ($missing | length) new nodes"
for item in $missing { print $" + ($item.id) ← ($item.artifact_path)" }
}
if ($stale | is-not-empty) {
print $" REMOVE: ($stale | length) stale nodes"
for item in $stale { print $" - ($item.id)" }
}
if ($broken | is-not-empty) {
print $" FIX: ($broken | length) broken edges"
for item in $broken { print $" ! ($item.id)" }
}
print ""
let confirm = (input "Apply changes? [y/N] " | str trim | str downcase)
if $confirm != "y" {
print " Aborted."
return
}
mut lines = (open $core_ncl --raw | lines | each { |l| $l })
# Insert new nodes before the '],\n\n edges' boundary
if ($missing | is-not-empty) {
# Find the line index of ' edges = ['
let edges_match = ($lines | enumerate | where { |row| ($row.item | str trim) == "edges = [" })
if ($edges_match | is-empty) {
print " Cannot find 'edges = [' in core.ncl — file format not recognized. Skipping node insertion."
} else {
let edges_idx = ($edges_match | first | get index)
# Find the top-level '],' closing the nodes array before edges.
# Top-level = exactly 2 leading spaces (same indent as 'edges = [').
mut insert_idx = $edges_idx - 1
while $insert_idx > 0 {
let line = ($lines | get $insert_idx)
let trimmed = ($line | str trim)
# Top-level array close: line is " ]," (2-space indent, not nested deeper)
if $trimmed == "]," and ($line | str starts-with " ") and (not ($line | str starts-with " ")) { break }
$insert_idx = $insert_idx - 1
}
if $insert_idx == 0 {
print " Cannot find nodes array closing bracket in core.ncl. Skipping node insertion."
} else {
mut new_lines = []
for item in $missing {
let kind = (detect-artifact-kind $item.artifact_path)
let block = (generate-node-ncl $item.id $item.artifact_path $kind)
$new_lines = ($new_lines | append "")
$new_lines = ($new_lines | append ($block | lines))
}
let before = ($lines | first $insert_idx)
let after = ($lines | skip $insert_idx)
$lines = ($before | append $new_lines | append $after)
print $" Inserted ($missing | length) new nodes."
} # insert_idx guard
} # edges_match guard
}
# Remove stale nodes
for item in $stale {
let id = $item.id
let id_pattern = $"\"($id)\""
let id_matches = ($lines | enumerate | where { |row| ($row.item | str contains $id_pattern) and ($row.item | str contains "id ") })
if ($id_matches | is-not-empty) {
let id_line_idx = ($id_matches | first | get index)
# Walk back to find 'd.make_node'
mut start_idx = $id_line_idx
while $start_idx > 0 {
if ($lines | get $start_idx | str contains "d.make_node") { break }
$start_idx = $start_idx - 1
}
# Walk forward to find '},' or lone '}' (last node in array has no trailing comma)
mut end_idx = $id_line_idx
let total = ($lines | length)
while $end_idx < ($total - 1) {
let trimmed_line = ($lines | get $end_idx | str trim)
if ($trimmed_line | str starts-with "},") or ($trimmed_line == "}") { break }
$end_idx = $end_idx + 1
}
# Guard: only remove if we actually found the closing brace
let end_line_trimmed = ($lines | get $end_idx | str trim)
if not (($end_line_trimmed | str starts-with "},") or ($end_line_trimmed == "}")) {
print $" warn: could not find closing brace for node '($id)' — skipping removal"
continue
}
# Remove lines [start_idx..end_idx] inclusive
let before = ($lines | first $start_idx)
let after = ($lines | skip ($end_idx + 1))
# Skip leading blank line in after
let trimmed_after = if ($after | is-not-empty) and (($after | first) | str trim | is-empty) {
$after | skip 1
} else {
$after
}
$lines = ($before | append $trimmed_after)
print $" Removed node '($id)'."
}
}
# Remove broken edges (handles both single-line and multi-line make_edge blocks)
# Deduplicate: an edge with both from+to broken produces two BROKEN entries with the same id.
let broken = ($broken | uniq-by id)
for item in $broken {
let edge_id = $item.id
let parts = ($edge_id | str replace "edge:" "" | split row "->")
if ($parts | length) == 2 {
let src = ($parts | first)
let tgt = ($parts | last)
let from_pattern = $"from = \"($src)\""
let to_pattern = $"to = \"($tgt)\""
# Try single-line match first (from and to on same line)
let single_matches = ($lines | enumerate | where { |row|
($row.item | str contains $from_pattern) and ($row.item | str contains $to_pattern)
})
if ($single_matches | is-not-empty) {
let edge_idx = ($single_matches | first | get index)
let before = ($lines | first $edge_idx)
let after = ($lines | skip ($edge_idx + 1))
$lines = ($before | append $after)
print $" Removed edge ($src) → ($tgt)."
} else {
# Multi-line: find the make_edge block containing both from and to
let from_lines = ($lines | enumerate | where { |row| $row.item | str contains $from_pattern })
if ($from_lines | is-not-empty) {
let from_idx = ($from_lines | first | get index)
# Search for matching to within ±5 lines
let search_start = ([$from_idx - 5, 0] | math max)
let search_end = ([($from_idx + 5), (($lines | length) - 1)] | math min)
let nearby = ($lines | skip $search_start | first ($search_end - $search_start + 1))
let has_to = ($nearby | any { |l| $l | str contains $to_pattern })
if $has_to {
# Walk back to find d.make_edge
mut start = $from_idx
while $start > 0 and (not ($lines | get $start | str contains "d.make_edge")) {
$start = $start - 1
}
# Walk forward to find the closing }
mut end = $from_idx
while $end < (($lines | length) - 1) {
let l = ($lines | get $end | str trim)
if ($l == "}," or $l == "}") and $end > $from_idx { break }
$end = $end + 1
}
let before = ($lines | first $start)
let after = ($lines | skip ($end + 1))
$lines = ($before | append $after)
print $" Removed multi-line edge ($src) → ($tgt)."
}
}
}
}
}
$lines | str join "\n" | $in + "\n" | save --force $core_ncl
print ""
print " Changes written to .ontology/core.ncl"
print " Run 'sync diff' to verify."
}
# ── state ─────────────────────────────────────────────────────────────────────
# Compare state.ncl dimensions against project reality.
export def "sync state" []: nothing -> table {
let root = (project-root)
let state_ncl = $"($root)/.ontology/state.ncl"
if not ($state_ncl | path exists) {
print " No .ontology/state.ncl found."
return []
}
let state = (daemon-export-safe $state_ncl)
if $state == null {
print " Failed to export state.ncl"
return []
}
let dims = ($state.dimensions? | default [])
mut report = []
for dim in $dims {
let current = ($dim.current_state? | default "unknown")
let desired = ($dim.desired_state? | default "unknown")
let status = if $current == $desired { "REACHED" } else { "ACTIVE" }
$report = ($report | append {
id: $dim.id,
current: $current,
desired: $desired,
status: $status,
horizon: ($dim.horizon? | default ""),
})
}
$report
}
# ── audit ─────────────────────────────────────────────────────────────────────
# Full audit: scan + diff + ADR constraints + gate conditions + NCL integrity.
export def "sync audit" [
--fmt: string = "", # Output format: table* | json | silent
--strict, # Exit with error on MISSING/STALE/BROKEN
--quick, # Skip expensive operations (no nickel exports for API surface)
]: nothing -> record {
let root = (project-root)
let actor = ($env.ONTOREF_ACTOR? | default "developer")
let f = if ($fmt | is-not-empty) { $fmt } else if $actor == "agent" { "json" } else { "table" }
# Node drift
let diff = if $quick { sync diff --quick } else { sync diff }
let ok_count = ($diff | where status == "OK" | length)
let missing_count = ($diff | where status == "MISSING" | length)
let stale_count = ($diff | where status == "STALE" | length)
let broken_count = ($diff | where status == "BROKEN" | length)
let total_nodes = ($ok_count + $missing_count + $stale_count + $broken_count)
# ADR constraint checks
let adr_results = if $quick { [] } else { audit-adr-constraints $root }
# Gate status (pass diff so breach detection can check protected nodes)
let gate_results = if $quick { [] } else { audit-gates $root $diff }
# State dimensions
let state_report = if $quick { [] } else { sync state }
# Justfile + .claude + tools audits (manifest-driven when available)
let scan = if $quick { { justfiles: { exists: false }, claude: { exists: false } } } else {
sync scan --level structural
}
let manifest = (load-manifest-safe $root)
let justfile_results = if $quick { [] } else { audit-justfiles ($scan.justfiles? | default { exists: false }) $manifest }
let claude_results = if $quick { [] } else { audit-claude ($scan.claude? | default { exists: false }) $manifest }
let tools_results = if $quick { [] } else { audit-tools $manifest }
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
let manifest_cov_results = if $quick { [] } else { audit-manifest-coverage $root }
2026-03-13 00:21:04 +00:00
# Health score (0-100) — pooled weighted model
#
# Each check earns points proportional to its architectural weight:
# node OK = 1pt (ontology completeness)
# ADR Hard PASS = 3pt (architectural guardrail — violation is severe)
# ADR Soft PASS = 1pt (advisory constraint)
# infra PASS = 1pt (tooling / .claude / justfiles)
# Penalties (subtracted from earned points):
# broken edge = -2pt (graph integrity)
# gate BREACH = -5pt (active membrana violated)
#
# health = earned / max_possible * 100, floored at 0
let adr_pass = ($adr_results | where status == "PASS" | length)
let adr_total = ($adr_results | length)
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
let all_infra = ($justfile_results | append $claude_results | append $tools_results | append $manifest_cov_results)
2026-03-13 00:21:04 +00:00
let jc_pass = ($all_infra | where status == "PASS" | length)
let jc_total = ($all_infra | length)
let node_earned = ($ok_count | into float)
let node_max = ($total_nodes | into float)
# ADR severity weighting: Hard = 3pt, Soft = 1pt
let adr_earned_list = ($adr_results | where status == "PASS" | each { |r|
if ($r.severity? | default "Hard") == "Hard" { 3 } else { 1 }
})
let adr_earned = if ($adr_earned_list | is-empty) { 0.0 } else { $adr_earned_list | math sum | into float }
let adr_max_list = ($adr_results | each { |r|
if ($r.severity? | default "Hard") == "Hard" { 3 } else { 1 }
})
let adr_max = if ($adr_max_list | is-empty) { 0.0 } else { $adr_max_list | math sum | into float }
let infra_earned = ($jc_pass | into float)
let infra_max = ($jc_total | into float)
let breach_count = ($gate_results | where status == "BREACH" | length)
let penalty = (($broken_count * 2 + $breach_count * 5) | into float)
let max_possible = ($node_max + $adr_max + $infra_max)
let earned = ($node_earned + $adr_earned + $infra_earned - $penalty)
let health = if $max_possible == 0.0 { 100.0 } else {
let raw = ($earned / $max_possible * 100.0)
if $raw < 0.0 { 0.0 } else if $raw > 100.0 { 100.0 } else { $raw }
} | math round --precision 1
let report = {
nodes: {
ok: $ok_count,
missing: $missing_count,
stale: $stale_count,
broken_edges: $broken_count,
details: $diff,
},
adrs: $adr_results,
gates: $gate_results,
state: $state_report,
justfiles: $justfile_results,
claude: $claude_results,
tools: $tools_results,
health: $health,
}
if $f == "json" {
print ($report | to json)
} else if $f == "silent" {
# No output — caller uses return value only
} else {
print ""
print $"Ontology Audit — ($root | path basename)"
print "──────────────────────────────────────────────────────────────────"
print $" Nodes: ($ok_count) OK / ($missing_count) MISSING / ($stale_count) STALE"
print $" Edges: ($broken_count) broken"
if ($adr_results | is-not-empty) {
let adr_fail = ($adr_results | where status == "FAIL" | length)
let hard_fail = ($adr_results | where status == "FAIL" and severity == "Hard" | length)
let soft_fail = ($adr_results | where status == "FAIL" and severity == "Soft" | length)
let severity_detail = if ($hard_fail + $soft_fail) > 0 {
$" \(($hard_fail)H ($soft_fail)S\)"
} else { "" }
print $" ADR constraints: ($adr_pass) PASS / ($adr_fail) FAIL($severity_detail)"
}
if ($gate_results | is-not-empty) {
let active_gates = ($gate_results | where status == "ACTIVE" | length)
let breach_gates = ($gate_results | where status == "BREACH" | length)
print $" Gates: ($active_gates) ACTIVE / ($breach_gates) BREACH"
}
if ($state_report | is-not-empty) {
let reached = ($state_report | where status == "REACHED" | length)
let active_dims = ($state_report | where status == "ACTIVE" | length)
print $" State: ($reached) REACHED / ($active_dims) ACTIVE"
}
if ($justfile_results | is-not-empty) {
let jf_pass = ($justfile_results | where status == "PASS" | length)
let jf_miss = ($justfile_results | where status == "MISSING" | length)
print $" Justfiles: ($jf_pass) PASS / ($jf_miss) MISSING"
}
if ($claude_results | is-not-empty) {
let cl_pass = ($claude_results | where status == "PASS" | length)
let cl_miss = ($claude_results | where status == "MISSING" | length)
print $" .claude/: ($cl_pass) PASS / ($cl_miss) MISSING"
}
if ($tools_results | is-not-empty) {
let tl_pass = ($tools_results | where status == "PASS" | length)
let tl_miss = ($tools_results | where status == "MISSING" | length)
let tl_opt = ($tools_results | where status == "OPTIONAL" | length)
print $" Tools: ($tl_pass) PASS / ($tl_miss) MISSING / ($tl_opt) optional"
}
print $" Health: ($health)%"
print ""
# Show problem items
let problems = ($diff | where status != "OK")
if ($problems | is-not-empty) {
print " Issues:"
for p in $problems {
print $" [($p.status)] ($p.id) ($p.detail)"
}
print ""
}
# Show justfile/claude/tools issues
let jc_issues = ($justfile_results | append $claude_results | append $tools_results | where status == "MISSING")
if ($jc_issues | is-not-empty) {
print " Infrastructure gaps:"
for p in $jc_issues {
print $" [MISSING] ($p.check): ($p.detail)"
}
print ""
}
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
# Show manifest capability coverage issues
let manifest_issues = ($manifest_cov_results | where { |r| $r.status != "PASS" })
if ($manifest_issues | is-not-empty) {
print " Manifest capability gaps:"
for p in $manifest_issues {
print $" [($p.status)] ($p.check): ($p.detail)"
}
print ""
}
2026-03-13 00:21:04 +00:00
}
if $strict and (($missing_count + $stale_count + $broken_count) > 0) {
error make { msg: $"Audit failed: ($missing_count) MISSING, ($stale_count) STALE, ($broken_count) BROKEN" }
}
$report
}
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
# ── manifest-check ───────────────────────────────────────────────────────────
# Quick manifest capability coverage check — used by pre-commit hooks and CI.
# Exits 0 if no Hard failures. Prints warnings but does not block on Soft issues.
export def "sync manifest-check" [
--strict, # Also fail on WARN (Soft) issues
]: nothing -> nothing {
let root = (project-root)
let results = (audit-manifest-coverage $root)
let fails = ($results | where status == "FAIL")
let warns = ($results | where status == "WARN")
if ($fails | is-not-empty) {
for f in $fails {
print $"(ansi red)✗ ($f.detail)(ansi reset)"
}
error make { msg: $"Manifest coverage: ($fails | length) Hard failure\(s\)" }
}
if $strict and ($warns | is-not-empty) {
for w in $warns {
print $"(ansi yellow)⚠ ($w.check): ($w.detail)(ansi reset)"
}
error make { msg: $"Manifest coverage: ($warns | length) warnings \(strict mode\)" }
}
if ($warns | is-not-empty) {
print $"(ansi yellow)⚠ ($warns | length) manifest coverage warnings — run `ontoref sync audit` for details(ansi reset)"
}
let pass = ($results | where status == "PASS")
if ($pass | is-not-empty) {
print $"(ansi green)✓ ($pass | first | get detail)(ansi reset)"
}
}
2026-03-13 00:21:04 +00:00
# ── watch ─────────────────────────────────────────────────────────────────────
# Launch bacon in headless mode with ontology-watch job, monitor drift file.
export def "sync watch" []: nothing -> nothing {
let root = (project-root)
let bacon_toml = $"($root)/bacon.toml"
if not ($bacon_toml | path exists) {
print " No bacon.toml found — cannot watch."
print " Run setup_reflection.nu to install bacon configuration."
return
}
# Verify ontology-watch job exists in bacon.toml
let bacon_content = (open $bacon_toml --raw)
if not ($bacon_content | str contains "[jobs.ontology-watch]") {
print " bacon.toml found but missing [jobs.ontology-watch] job."
print " Re-run setup_reflection.nu to enable the ontology-reflection layer."
return
}
let has_bacon = (which bacon | is-not-empty)
if not $has_bacon {
print " bacon not found — install with: cargo install bacon"
return
}
print " Starting ontology drift watch via bacon..."
print " Press Ctrl+C to stop."
^bacon --headless -j ontology-watch
}
# ── Internal helpers ──────────────────────────────────────────────────────────
def scan-crates [root: string, level: string]: nothing -> list {
let cargo_toml = $"($root)/Cargo.toml"
if not ($cargo_toml | path exists) { return [] }
let cargo = (open $cargo_toml)
let workspace_members = ($cargo | get -o workspace.members | default [])
let crate_paths = if ($workspace_members | is-empty) { ["."] } else { $workspace_members }
mut crates = []
for member in $crate_paths {
let crate_dir = $"($root)/($member)"
let crate_toml_path = $"($crate_dir)/Cargo.toml"
if ($crate_toml_path | path exists) {
$crates = ($crates | append (parse-single-crate $crate_dir $level $root))
} else {
# Try globbing (e.g., "crates/*")
let expanded = (glob $"($root)/($member)/Cargo.toml")
for ct in $expanded {
$crates = ($crates | append (parse-single-crate ($ct | path dirname) $level $root))
}
}
}
$crates
}
def parse-single-crate [crate_dir: string, level: string, root: string]: nothing -> record {
let crate_toml = $"($crate_dir)/Cargo.toml"
let cargo = (open $crate_toml)
let name = ($cargo | get -o package.name | default ($crate_dir | path basename))
let features = ($cargo | get -o features | default {} | columns)
let deps_count = ($cargo | get -o dependencies | default {} | columns | length)
let src_dir = $"($crate_dir)/src"
let src_modules = if ($src_dir | path exists) {
glob $"($src_dir)/**/*.rs" | each { |f| $f | path relative-to $crate_dir }
} else { [] }
let test_dir = $"($crate_dir)/tests"
let test_count = if ($test_dir | path exists) {
glob $"($test_dir)/**/*.rs" | length
} else { 0 }
let do_full = match $level {
"full" => true,
"structural" => false,
_ => { (which "cargo" | is-not-empty) and ((do { ^cargo +nightly --version } | complete).exit_code == 0) },
}
let pub_api = if $do_full {
extract-pub-api $crate_dir $name
} else { [] }
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
let crate_doc = (extract-crate-doc $crate_dir)
2026-03-13 00:21:04 +00:00
{
name: $name,
path: ($crate_dir | path relative-to $root),
features: $features,
deps_count: $deps_count,
src_modules: $src_modules,
test_count: $test_count,
pub_api: $pub_api,
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
crate_doc: $crate_doc,
2026-03-13 00:21:04 +00:00
}
}
def extract-pub-api [crate_dir: string, crate_name: string]: nothing -> list {
let result = do {
cd $crate_dir
^cargo +nightly rustdoc -p $crate_name -- -Z unstable-options --output-format json
} | complete
if $result.exit_code != 0 { return [] }
# Find the generated JSON doc
let doc_name = ($crate_name | str replace "-" "_")
let doc_json = $"($crate_dir)/target/doc/($doc_name).json"
if not ($doc_json | path exists) { return [] }
let doc = (open $doc_json)
let index = ($doc | get -o index | default {})
$index | transpose k v | where {|item|
let vis = ($item.v | get -o visibility | default "")
$vis == "public"
} | each { |item|
let inner = ($item.v | get -o inner | default {})
let kind = ($inner | columns | first | default "unknown")
{
kind: $kind,
path: ($item.v | get -o name | default ""),
signature: ($item.v | get -o docs | default "" | str substring 0..120),
}
}
}
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
def extract-crate-doc [crate_dir: string]: nothing -> string {
let candidates = [$"($crate_dir)/src/lib.rs", $"($crate_dir)/src/main.rs"]
let existing = ($candidates | where { |f| $f | path exists })
if ($existing | is-empty) { return "" }
open --raw ($existing | first)
| lines
| skip while { |l|
let t = ($l | str trim)
($t | is-empty) or ($t | str starts-with "#![")
}
| take while { |l|
let t = ($l | str trim)
($t | is-empty) or ($t | str starts-with "//!")
}
| where { |l| $l | str starts-with "//!" }
| each { |l| $l | str replace --regex '^//!\s?' "" }
| str join "\n"
| str trim
}
def word-overlap [a: string, b: string]: nothing -> float {
let stop = ["that", "this", "with", "from", "have", "will", "been", "when", "they", "each"]
let words_a = ($a | str downcase | split row --regex '\W+'
| where { |w| ($w | str length) > 3 and not ($w in $stop) } | sort | uniq)
let words_b = ($b | str downcase | split row --regex '\W+'
| where { |w| ($w | str length) > 3 and not ($w in $stop) } | sort | uniq)
if ($words_a | is-empty) or ($words_b | is-empty) { return 0.0 }
let intersection = ($words_a | where { |w| $w in $words_b } | length)
let union_size = ($words_a | append $words_b | sort | uniq | length)
($intersection | into float) / ($union_size | into float)
}
def check-crate-doc-drift [root: string, nodes: list<record>, scan: record]: nothing -> list<record> {
let crates = ($scan.crates? | default [])
if ($crates | is-empty) { return [] }
mut drifts = []
for node in $nodes {
let paths = ($node.artifact_paths? | default [])
if ($paths | is-empty) { continue }
let node_desc = ($node.description? | default "" | str trim)
# First sentence of description for summary-level comparison.
# Split on ". " (period + space) to avoid breaking on path fragments like ".ontology/".
let node_summary = ($node_desc | split row ". " | where { |s| not ($s | str trim | is-empty) } | if ($in | is-not-empty) { first | str trim } else { "" })
# Only check the FIRST artifact_path for crate-doc drift.
# A crate appearing mid-list means "this crate implements the concept" —
# different abstraction level from the concept description itself.
let first_path = ($paths | if ($in | is-not-empty) { first } else { "" })
let p_norm = ($first_path | str replace --regex '/$' "")
let matched = ($crates | where { |c| $c.path == $p_norm })
if ($matched | is-not-empty) {
let cr = ($matched | first)
let crate_doc = ($cr.crate_doc? | default "" | str trim)
if ($crate_doc | is-not-empty) {
let crate_first = ($crate_doc | lines | where { |l| $l | is-not-empty } | if ($in | is-not-empty) { first } else { "" })
if ($node_summary | is-empty) {
$drifts = ($drifts | append {
status: "DRIFT",
id: $node.id,
artifact_path: $cr.path,
detail: $"Node has no description. Crate //! doc: \"($crate_first | str substring 0..120)\"",
})
} else {
# Compare first sentences: both are "summary" granularity regardless of total length.
let overlap = (word-overlap $node_summary $crate_first)
if $overlap < 0.20 {
$drifts = ($drifts | append {
status: "DRIFT",
id: $node.id,
artifact_path: $cr.path,
detail: $"Doc mismatch \(overlap: ($overlap * 100 | math round)%\). Node: \"($node_summary | str substring 0..80)\" | Crate: \"($crate_first | str substring 0..80)\"",
})
}
}
}
}
}
$drifts
}
2026-03-13 00:21:04 +00:00
def scan-scenarios [root: string]: nothing -> list {
let scenarios_dir = $"($root)/reflection/scenarios"
if not ($scenarios_dir | path exists) {
# Fallback: check examples/
let examples_dir = $"($root)/examples"
if not ($examples_dir | path exists) { return [] }
ls $examples_dir | where type == "dir" | each { |d|
{
category: ($d.name | path basename),
path: ($d.name | path relative-to $root),
description: "",
files: (glob $"($d.name)/*" | length),
actor: "developer",
}
}
} else {
ls $scenarios_dir | where type == "dir" | each { |d|
let meta_file = $"($d.name)/scenario.ncl"
let meta = if ($meta_file | path exists) {
daemon-export-safe $meta_file | default {}
} else { {} }
{
category: ($d.name | path basename),
path: ($d.name | path relative-to $root),
description: ($meta | get -o purpose | default ""),
files: (glob $"($d.name)/*" | length),
actor: ($meta | get -o actor | default "developer"),
validates: ($meta | get -o validates | default []),
}
}
}
}
def scan-agents [root: string]: nothing -> list {
let agent_files = (glob $"($root)/**/*.agent.mdx")
$agent_files | each { |f|
{
name: ($f | path basename | str replace ".agent.mdx" ""),
path: ($f | path relative-to $root),
format: "mdx",
}
}
}
def scan-ci [root: string]: nothing -> list {
mut providers = []
if ($"($root)/.github/workflows" | path exists) {
let wf = (glob $"($root)/.github/workflows/*.yml" | append (glob $"($root)/.github/workflows/*.yaml"))
for f in $wf {
$providers = ($providers | append { provider: "github-actions", path: ($f | path relative-to $root) })
}
}
if ($"($root)/.woodpecker" | path exists) {
let wf = (glob $"($root)/.woodpecker/*.yml" | append (glob $"($root)/.woodpecker/*.yaml"))
for f in $wf {
$providers = ($providers | append { provider: "woodpecker", path: ($f | path relative-to $root) })
}
}
$providers
}
def scan-forms [root: string]: nothing -> list {
let forms_dir = $"($root)/reflection/forms"
if not ($forms_dir | path exists) { return [] }
glob $"($forms_dir)/*.ncl" | each { |f|
{ name: ($f | path basename | str replace ".ncl" ""), path: ($f | path relative-to $root) }
}
}
def scan-modes [root: string]: nothing -> list {
# Collect modes from both ONTOREF_ROOT and project root
let ontoref_modes = (glob $"($env.ONTOREF_ROOT)/reflection/modes/*.ncl")
let project_modes = if ($root != $env.ONTOREF_ROOT) {
glob $"($root)/reflection/modes/*.ncl"
} else { [] }
let all = ($ontoref_modes | append $project_modes | uniq)
$all | each { |f|
{ name: ($f | path basename | str replace ".ncl" ""), path: $f }
}
}
# ── Justfile scanner ────────────────────────────────────────────────────────
# Detects justfile modules, recipes, module system (import/mod), and variables.
def scan-justfiles [root: string]: nothing -> record {
let justfile_path = $"($root)/justfile"
if not ($justfile_path | path exists) {
return { exists: false, system: "", modules: [], recipes: [], variables: [] }
}
let raw = (open $justfile_path --raw)
let lines = ($raw | lines)
# Detect module system: import vs mod
let has_import = ($lines | any { |l| ($l | str trim | str starts-with "import ") or ($l | str trim | str starts-with "import?") })
let has_mod = ($lines | any { |l| ($l | str trim | str starts-with "mod ") or ($l | str trim | str starts-with "mod?") })
let system = if $has_import and $has_mod { "hybrid" } else if $has_import { "import" } else if $has_mod { "mod" } else { "flat" }
# Extract module declarations
let mod_lines = ($lines | where { |l|
let t = ($l | str trim)
($t | str starts-with "import ") or ($t | str starts-with "import? ") or ($t | str starts-with "mod ") or ($t | str starts-with "mod? ")
})
let modules = ($mod_lines | each { |l|
let t = ($l | str trim)
let optional = ($t | str starts-with "import?") or ($t | str starts-with "mod?")
let path_match = ($t | parse --regex "'([^']+)'|\"([^\"]+)\"")
let mod_path = if ($path_match | is-not-empty) {
let m = ($path_match | first)
let c0 = ($m | get -o capture0 | default "")
let c1 = ($m | get -o capture1 | default "")
if ($c0 | is-not-empty) { $c0 } else { $c1 }
} else { "" }
let name_match = ($t | parse --regex '(?:mod\??\s+)(\w+)')
let name = if ($name_match | is-not-empty) {
$name_match | first | get capture0
} else {
$mod_path | path basename | str replace ".just" ""
}
{ name: $name, path: $mod_path, optional: $optional }
})
# Scan justfiles/ directory for .just files
let justfiles_dir = $"($root)/justfiles"
let just_files = if ($justfiles_dir | path exists) {
glob $"($justfiles_dir)/*.just" | each { |f| $f | path relative-to $root }
} else { [] }
# Extract top-level variables (VAR := "value" or VAR := `command`)
let variables = ($lines | where { |l|
let t = ($l | str trim)
($t =~ '^\w+\s*:=') and (not ($t | str starts-with "#"))
} | each { |l|
let parts = ($l | str trim | split row ":=" | each { |p| $p | str trim })
if ($parts | length) >= 2 { $parts | first } else { "" }
} | where { |v| $v | is-not-empty })
# Extract recipe names (lines matching "name:", "@name:", "name PARAM:", "name PARAM="val":")
let recipes = ($lines | where { |l|
let t = ($l | str trim)
($t =~ '^@?\w[\w-]*(\s.*)?:') and (not ($t | str starts-with "#")) and (not ($t =~ '^\w+\s*:='))
} | each { |l|
let t = ($l | str trim | str replace "@" "")
$t | split row " " | first | str replace ":" ""
})
{
exists: true,
system: $system,
modules: $modules,
module_files: $just_files,
recipes: $recipes,
variables: $variables,
}
}
# ── .claude/ scanner ───────────────────────────────────────────────────────
# Inventories .claude/ capabilities: guidelines, commands, hooks, agents, skills.
def scan-claude [root: string]: nothing -> record {
let claude_dir = $"($root)/.claude"
if not ($claude_dir | path exists) {
return { exists: false, has_claude_md: false, guidelines: [], commands: [], hooks: [], settings: [] }
}
let has_claude_md = ($"($claude_dir)/CLAUDE.md" | path exists)
# Guidelines: check each language subdirectory
let guidelines_dir = $"($claude_dir)/guidelines"
let guidelines = if ($guidelines_dir | path exists) {
ls $guidelines_dir | where type == "dir" | each { |d|
let lang = ($d.name | path basename)
let files = (glob $"($d.name)/*.md" | each { |f| $f | path basename })
{ language: $lang, files: $files }
}
} else { [] }
# Commands
let commands_dir = $"($claude_dir)/commands"
let commands = if ($commands_dir | path exists) {
glob $"($commands_dir)/*.md" | each { |f|
let name = ($f | path basename | str replace ".md" "")
let is_symlink = ((do { ^test -L $f } | complete).exit_code == 0)
{ name: $name, symlink: $is_symlink }
}
} else { [] }
# Hooks (check settings.json for hook definitions)
let settings_path = $"($claude_dir)/settings.json"
let hooks = if ($settings_path | path exists) {
let settings = (open $settings_path)
let hook_events = ($settings | get -o hooks | default {} | columns)
$hook_events
} else { [] }
# Settings files
let settings = (glob $"($claude_dir)/settings*.json" | each { |f| $f | path basename })
# Layout conventions
let has_layout = ($"($claude_dir)/layout_conventions.md" | path exists)
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
# Session hook — check both legacy and current locations
let has_session_hook = (
($"($claude_dir)/hooks/session-context.sh" | path exists)
or ($"($claude_dir)/ontoref-session-start.sh" | path exists)
)
2026-03-13 00:21:04 +00:00
{
exists: true,
has_claude_md: $has_claude_md,
has_layout_conventions: $has_layout,
has_session_hook: $has_session_hook,
guidelines: $guidelines,
commands: $commands,
hooks: $hooks,
settings: $settings,
}
}
# Load ontology via daemon-export (full fidelity, includes all fields).
def load-ontology [root: string]: nothing -> record {
let core_ncl = $"($root)/.ontology/core.ncl"
if not ($core_ncl | path exists) { return {} }
let data = (daemon-export-safe $core_ncl)
if $data == null {
print " Warning: failed to export core.ncl"
return {}
}
$data
}
# Load ontology by parsing NCL text directly — no nickel binary needed.
# Extracts id and artifact_paths from node blocks, and from/to from edge blocks.
# Intentionally lossy: sufficient for drift detection in pre-commit/quick mode.
def load-ontology-quick [root: string]: nothing -> record {
let core_ncl = $"($root)/.ontology/core.ncl"
if not ($core_ncl | path exists) { return {} }
let raw = (open $core_ncl --raw)
let lines = ($raw | lines)
mut nodes = []
mut edges = []
mut in_node = false
mut in_paths = false
mut in_edge_section = false
mut in_edge = false
mut edge_from = ""
mut edge_to = ""
mut current_id = ""
mut current_paths = []
for line in $lines {
let trimmed = ($line | str trim)
# Detect section boundaries
if ($trimmed | str starts-with "nodes = [") {
$in_edge_section = false
} else if ($trimmed | str starts-with "edges = [") {
$in_edge_section = true
}
# Parse node blocks
if not $in_edge_section {
if ($trimmed | str contains "d.make_node") {
$in_node = true
$in_paths = false
$current_id = ""
$current_paths = []
} else if $in_node and ($trimmed | str starts-with "},") {
if ($current_id | is-not-empty) {
$nodes = ($nodes | append { id: $current_id, artifact_paths: $current_paths })
}
$in_node = false
$in_paths = false
} else if $in_node {
# Extract id
if ($trimmed | str starts-with "id ") {
let id_match = ($trimmed | parse --regex 'id\s+=\s+"([^"]+)"')
if ($id_match | is-not-empty) {
$current_id = ($id_match | first | get capture0)
}
}
# Extract artifact_paths (handles multiline arrays)
if ($trimmed | str starts-with "artifact_paths") {
$in_paths = true
let paths_match = ($trimmed | parse --regex '"([^"]+)"')
if ($paths_match | is-not-empty) {
$current_paths = ($current_paths | append ($paths_match | get capture0))
}
} else if $in_paths {
if ($trimmed | str starts-with "],") or ($trimmed == "]") {
$in_paths = false
} else {
let paths_match = ($trimmed | parse --regex '"([^"]+)"')
if ($paths_match | is-not-empty) {
$current_paths = ($current_paths | append ($paths_match | get capture0))
}
}
}
}
}
# Parse edge blocks (single-line or multi-line d.make_edge)
if $in_edge_section and ($trimmed | str contains "d.make_edge") {
$in_edge = true
$edge_from = ""
$edge_to = ""
# Check if single-line (from + to on same line as d.make_edge)
let from_match = ($trimmed | parse --regex 'from\s*=\s*"([^"]+)"')
let to_match = ($trimmed | parse --regex 'to\s*=\s*"([^"]+)"')
if ($from_match | is-not-empty) { $edge_from = ($from_match | first | get capture0) }
if ($to_match | is-not-empty) { $edge_to = ($to_match | first | get capture0) }
if ($edge_from | is-not-empty) and ($edge_to | is-not-empty) {
$edges = ($edges | append { from: $edge_from, to: $edge_to })
$in_edge = false
}
} else if $in_edge {
# Multi-line: accumulate from/to across lines
let from_match = ($trimmed | parse --regex 'from\s*=\s*"([^"]+)"')
let to_match = ($trimmed | parse --regex 'to\s*=\s*"([^"]+)"')
if ($from_match | is-not-empty) { $edge_from = ($from_match | first | get capture0) }
if ($to_match | is-not-empty) { $edge_to = ($to_match | first | get capture0) }
if ($trimmed == "}," or $trimmed == "}") {
if ($edge_from | is-not-empty) and ($edge_to | is-not-empty) {
$edges = ($edges | append { from: $edge_from, to: $edge_to })
}
$in_edge = false
}
}
}
{ nodes: $nodes, edges: $edges }
}
# Collect all paths that exist in the scan output (relative to root).
def collect-existing-paths [scan: record, root: string]: nothing -> list<string> {
mut paths = []
for c in $scan.crates {
$paths = ($paths | append $c.path)
}
for s in $scan.scenarios {
$paths = ($paths | append $s.path)
}
for a in $scan.agents {
$paths = ($paths | append $a.path)
}
for ci in $scan.ci {
$paths = ($paths | append $ci.path)
}
# Forms
for f in ($scan.forms? | default []) {
$paths = ($paths | append $f.path)
}
# Modes
for m in ($scan.modes? | default []) {
$paths = ($paths | append $m.path)
}
# Justfile module files
if ($scan.justfiles?.module_files? | default [] | is-not-empty) {
for jf in $scan.justfiles.module_files {
$paths = ($paths | append $jf)
}
}
# Entry points
if ($"($root)/onref" | path exists) { $paths = ($paths | append "onref") }
if ($"($root)/onref" | path exists) { $paths = ($paths | append "onref") }
$paths
}
# Find scan artifacts not claimed by any node's artifact_paths.
def find-unclaimed-artifacts [scan: record, root: string, node_paths: list<string>]: nothing -> list {
mut unclaimed = []
for c in $scan.crates {
let crate_rel = $c.path
let claimed = ($node_paths | any { |np| $crate_rel starts-with $np or $np starts-with $crate_rel })
if not $claimed {
$unclaimed = ($unclaimed | append { id: $c.name, path: $crate_rel, kind: "crate" })
}
}
for s in $scan.scenarios {
let scen_rel = $s.path
let claimed = ($node_paths | any { |np| $scen_rel starts-with $np or $np starts-with $scen_rel })
if not $claimed {
$unclaimed = ($unclaimed | append { id: $"scenario-($s.category)", path: $scen_rel, kind: "scenario" })
}
}
for a in $scan.agents {
let agent_rel = $a.path
let claimed = ($node_paths | any { |np| $agent_rel starts-with $np or $np starts-with $agent_rel })
if not $claimed {
$unclaimed = ($unclaimed | append { id: $"agent-($a.name)", path: $agent_rel, kind: "agent" })
}
}
for ci in $scan.ci {
let ci_rel = $ci.path
let claimed = ($node_paths | any { |np| $ci_rel starts-with $np or $np starts-with $ci_rel })
if not $claimed {
$unclaimed = ($unclaimed | append { id: $"ci-($ci.provider)", path: $ci_rel, kind: "ci" })
}
}
for f in ($scan.forms? | default []) {
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
# form.path may be absolute — normalize to relative for comparison
let form_rel = if ($f.path | str starts-with $root) {
$f.path | path relative-to $root
} else { $f.path }
2026-03-13 00:21:04 +00:00
let claimed = ($node_paths | any { |np| $form_rel starts-with $np or $np starts-with $form_rel })
if not $claimed {
$unclaimed = ($unclaimed | append { id: $"form-($f.name)", path: $form_rel, kind: "form" })
}
}
for m in ($scan.modes? | default []) {
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
# mode.path may be absolute — normalize to relative for comparison
let mode_rel = if ($m.path | str starts-with $root) {
$m.path | path relative-to $root
} else { $m.path }
2026-03-13 00:21:04 +00:00
let claimed = ($node_paths | any { |np| $mode_rel starts-with $np or $np starts-with $mode_rel })
if not $claimed {
$unclaimed = ($unclaimed | append { id: $"mode-($m.name)", path: $mode_rel, kind: "mode" })
}
}
$unclaimed
}
def detect-artifact-kind [path: string]: nothing -> string {
if ($path | str contains "crates/") {
"crate"
} else if ($path | str contains "scenarios/") or ($path | str contains "examples/") {
"scenario"
} else if ($path | str contains ".agent.mdx") {
"agent"
} else if ($path | str contains ".github/") or ($path | str contains ".woodpecker/") {
"ci"
} else if ($path | str contains "justfiles/") or ($path == "justfile") {
"justfile"
} else if ($path | str contains ".claude/") {
"claude"
} else if ($path | str contains "reflection/modules/") {
"module"
} else if ($path | str contains "reflection/modes/") {
"mode"
} else {
"unknown"
}
}
def generate-node-ncl [id: string, path: string, kind: string]: nothing -> string {
let level = match $kind {
"crate" => "'Project",
"scenario" => "'Moment",
"agent" => "'Practice",
_ => "'Project",
}
[
$" d.make_node {",
$" id = \"($id)\",",
$" name = \"($id | str replace -a '-' ' ' | split words | each { |w| ($w | str capitalize) } | str join ' ')\",",
$" pole = 'Yang,",
$" level = ($level),",
$" description = \"Detected from ($kind) at ($path)\",",
$" invariant = false,",
$" artifact_paths = [\"($path)\"],",
$" },",
] | str join "\n"
}
def audit-adr-constraints [root: string]: nothing -> list {
let adr_files = (glob $"($root)/adrs/adr-*.ncl")
mut results = []
for f in $adr_files {
let adr = (daemon-export-safe $f)
if $adr == null { continue }
if ($adr.status? | default "") != "Accepted" { continue }
let constraints = ($adr.constraints? | default [])
for c in $constraints {
let severity = ($c.severity? | default "")
if $severity not-in ["Hard", "Soft"] { continue }
let hint = ($c.check_hint? | default "")
if ($hint | is-empty) { continue }
# Guard: only execute check_hints that start with a known safe command prefix.
# Prevents arbitrary shell injection from ADR constraint fields.
# Only read-only commands that do NOT accept code execution flags.
# Excluded: nu (accepts -c), cargo (accepts run), nickel (accepts eval),
# cat/jq (can be piped into shell via bash -c).
let hint_trimmed = ($hint | str trim)
let parts = ($hint_trimmed | split row " ")
let cmd = ($parts | first)
let args = ($parts | skip 1)
let safe_commands = ["rg", "grep", "test", "[", "ls", "wc", "file", "stat"]
if $cmd not-in $safe_commands {
$results = ($results | append {
adr: ($adr.id? | default ""),
constraint: ($c.id? | default ""),
severity: $severity,
status: "SKIP",
detail: $"check_hint command not in whitelist: ($cmd)",
})
continue
}
# Block flags that allow code execution via whitelisted commands
# (e.g., rg --pre <script> executes an external command per file).
let dangerous_flags = ["--pre", "--pre-glob", "--exec", "--exec-batch"]
let has_dangerous = ($args | any { |a| $a in $dangerous_flags })
if $has_dangerous {
$results = ($results | append {
adr: ($adr.id? | default ""),
constraint: ($c.id? | default ""),
severity: $severity,
status: "SKIP",
detail: $"check_hint contains blocked flag: ($args | str join ' ')",
})
continue
}
let check = do { cd $root; run-external $cmd ...$args } | complete
let status = if $check.exit_code == 0 { "PASS" } else { "FAIL" }
$results = ($results | append {
adr: ($adr.id? | default ""),
constraint: ($c.id? | default ""),
severity: $severity,
status: $status,
detail: ($c.claim? | default ""),
})
}
}
$results
}
# Load manifest safely — returns empty record if not available.
def load-manifest-safe [root: string]: nothing -> record {
let manifest_ncl = $"($root)/.ontology/manifest.ncl"
if not ($manifest_ncl | path exists) { return {} }
let ontoref_ontology = if ($env.ONTOREF_ROOT? | is-not-empty) { $"($env.ONTOREF_ROOT)/ontology" } else { "" }
let import_path = ([$"($root)/.ontology", $ontoref_ontology, ($env.NICKEL_IMPORT_PATH? | default "")] | where { $in | is-not-empty } | str join ":")
daemon-export-safe $manifest_ncl --import-path $import_path | default {}
}
# Verify tools declared in manifest are available on PATH.
def audit-tools [manifest: record]: nothing -> list {
let tools = ($manifest.tools? | default [])
if ($tools | is-empty) { return [] }
$tools | each { |t|
let name = $t.name
let required = ($t.required? | default true)
let found = (which $name | is-not-empty)
let status = if $found { "PASS" } else if $required { "MISSING" } else { "OPTIONAL" }
let method = ($t.install_method? | default "Builtin")
{ check: $"tool-($name)", status: $status, detail: $"($name) [($method)]" }
}
}
# Check justfile completeness against manifest convention or canonical defaults.
def audit-justfiles [justfiles: record, manifest: record]: nothing -> list {
if not ($justfiles.exists? | default false) { return [] }
mut results = []
let jf_conv = ($manifest.justfile? | default {})
let canonical_required = ($jf_conv.required_modules? | default ["build" "test" "dev" "ci"])
let canonical_recipes = ($jf_conv.required_recipes? | default ["default" "help"])
let module_names = ($justfiles.modules? | default [] | get name)
for req in $canonical_required {
let found = ($module_names | any { |n| $n == $req })
let status = if $found { "PASS" } else { "MISSING" }
$results = ($results | append { check: $"justfile-module-($req)", status: $status, detail: $"Canonical module '($req)'" })
}
let recipe_names = ($justfiles.recipes? | default [])
for req in $canonical_recipes {
let found = ($recipe_names | any { |r| $r == $req })
let status = if $found { "PASS" } else { "MISSING" }
$results = ($results | append { check: $"justfile-recipe-($req)", status: $status, detail: $"Required recipe '($req)'" })
}
$results
}
# Check .claude/ completeness against manifest baseline or defaults.
def audit-claude [claude: record, manifest: record]: nothing -> list {
if not ($claude.exists? | default false) {
return [{ check: "claude-dir", status: "MISSING", detail: ".claude/ directory not found" }]
}
mut results = []
let checks = [
["claude-md" ($claude.has_claude_md? | default false) "CLAUDE.md"]
["layout-conventions" ($claude.has_layout_conventions? | default false) "layout_conventions.md"]
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
["session-hook" ($claude.has_session_hook? | default false) "hooks/session-context.sh"]
2026-03-13 00:21:04 +00:00
]
for c in $checks {
let status = if $c.1 { "PASS" } else { "MISSING" }
$results = ($results | append { check: $c.0, status: $status, detail: $c.2 })
}
# Check expected guideline languages (from manifest or defaults)
let guideline_langs = ($claude.guidelines? | default [] | get language)
let cl_baseline = ($manifest.claude? | default {})
let expected_langs = ($cl_baseline.guidelines? | default ["bash" "nushell"])
for lang in $expected_langs {
let found = ($guideline_langs | any { |l| $l == $lang })
let status = if $found { "PASS" } else { "MISSING" }
$results = ($results | append { check: $"guideline-($lang)", status: $status, detail: $"Guidelines for ($lang)" })
}
$results
}
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
# Audit manifest capability completeness.
# Cross-references Practice nodes, reflection modes, and daemon UI pages
# against declared capabilities[].artifacts and capabilities[].nodes to find
# undeclared functionality that agents and sessions will never see.
def audit-manifest-coverage [root: string]: nothing -> list {
let manifest = (load-manifest-safe $root)
let caps = ($manifest.capabilities? | default [])
if ($caps | is-empty) {
return [{
check: "manifest-capabilities",
status: "FAIL",
detail: "No capabilities declared in manifest.ncl — project functionality invisible to agents",
severity: "Hard",
}]
}
let all_cap_nodes = ($caps | each { |c| $c.nodes? | default [] } | flatten | uniq)
let all_cap_artifacts = ($caps | each { |c| $c.artifacts? | default [] } | flatten)
mut results = []
# Check 1: Practice nodes without any capability referencing them
let core_ncl = $"($root)/.ontology/core.ncl"
if ($core_ncl | path exists) {
let core = (daemon-export-safe $core_ncl)
if $core != null {
let practices = ($core.nodes? | default [] | where { |n| ($n.level? | default "") == "Practice" })
let uncovered = ($practices | where { |p|
not ($all_cap_nodes | any { |n| $n == $p.id })
})
for p in $uncovered {
$results = ($results | append {
check: "practice-node-uncovered",
status: "WARN",
detail: $"Practice node '($p.id)' \(($p.name)\) has no capability referencing it in nodes[]",
severity: "Soft",
})
}
}
}
# Check 2: Reflection modes not mentioned in any capability's artifacts
let modes_dir = $"($root)/reflection/modes"
if ($modes_dir | path exists) {
let mode_files = (glob $"($modes_dir)/*.ncl" | each { |f| $f | path basename | str replace ".ncl" "" })
for mode_id in $mode_files {
let mode_path = $"reflection/modes/($mode_id).ncl"
let referenced = ($all_cap_artifacts | any { |a|
($a == $mode_path) or ($a | str starts-with "reflection/modes/")
})
if not $referenced {
$results = ($results | append {
check: "mode-uncovered",
status: "WARN",
detail: $"Reflection mode '($mode_id)' not referenced by any capability — invisible to agents",
severity: "Soft",
})
}
}
}
# Check 3: Capability quality — summaries too thin or missing artifacts
for c in $caps {
let summary = ($c.summary? | default "")
if ($summary | str length) < 30 {
$results = ($results | append {
check: "capability-thin-summary",
status: "WARN",
detail: $"Capability '($c.id)' summary under 30 chars — too thin for agent comprehension",
severity: "Soft",
})
}
let arts = ($c.artifacts? | default [])
if ($arts | is-empty) {
$results = ($results | append {
check: "capability-no-artifacts",
status: "WARN",
detail: $"Capability '($c.id)' declares no artifacts — cannot verify existence",
severity: "Soft",
})
}
}
if ($results | is-empty) {
[{
check: "manifest-coverage",
status: "PASS",
detail: $"All ($caps | length) capabilities verified: practice nodes covered, modes referenced, summaries adequate",
severity: "Hard",
}]
} else {
$results
}
}
2026-03-13 00:21:04 +00:00
def audit-gates [root: string, diff: list]: nothing -> list {
let gate_ncl = $"($root)/.ontology/gate.ncl"
if not ($gate_ncl | path exists) { return [] }
let gates = (daemon-export-safe $gate_ncl)
if $gates == null { return [] }
let membranes = ($gates.membranes? | default [])
# Collect node ids that are not OK (MISSING, STALE, BROKEN)
let bad_nodes = ($diff | where status != "OK" | get id)
$membranes | each { |m|
let active = ($m.active? | default false)
let protects = ($m.protects? | default [])
# An active gate is in BREACH when any node it protects is not OK
let breached_nodes = if $active and ($protects | is-not-empty) {
$protects | where { |p| $bad_nodes | any { |b| $b == $p } }
} else { [] }
let status = if not $active {
"INACTIVE"
} else if ($breached_nodes | is-not-empty) {
"BREACH"
} else {
"ACTIVE"
}
{
id: ($m.id? | default ""),
status: $status,
protects: $protects,
breached_nodes: $breached_nodes,
}
}
}