2026-03-13 00:21:04 +00:00
|
|
|
#!/usr/bin/env nu
|
|
|
|
|
# reflection/modules/generator.nu — project output generators.
|
|
|
|
|
# Composes full documentation from ontology, ADRs, modes, crates, and scenarios.
|
|
|
|
|
# Output formats: md (human), json (agent/MCP), mdbook (publishable HTML).
|
|
|
|
|
#
|
|
|
|
|
# This is the implementation behind the generate-docs and generate-mdbook modes.
|
|
|
|
|
# Modes declare steps; this module does the actual work.
|
|
|
|
|
|
|
|
|
|
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 }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
use ../modules/store.nu [daemon-export-safe]
|
|
|
|
|
|
|
|
|
|
def nickel-import-path [root: string]: nothing -> string {
|
|
|
|
|
let entries = [
|
|
|
|
|
$"($root)/.ontology"
|
|
|
|
|
$"($root)/adrs"
|
|
|
|
|
$"($root)/.ontoref/ontology/schemas"
|
|
|
|
|
$"($root)/.ontoref/adrs"
|
|
|
|
|
$"($root)/.onref"
|
|
|
|
|
$root
|
|
|
|
|
$"($env.ONTOREF_ROOT)/ontology"
|
|
|
|
|
$"($env.ONTOREF_ROOT)/ontology/schemas"
|
|
|
|
|
$"($env.ONTOREF_ROOT)/adrs"
|
|
|
|
|
$env.ONTOREF_ROOT
|
|
|
|
|
]
|
|
|
|
|
let valid = ($entries | where { |p| $p | path exists } | uniq)
|
|
|
|
|
let existing = ($env.NICKEL_IMPORT_PATH? | default "")
|
|
|
|
|
if ($existing | is-not-empty) {
|
|
|
|
|
($valid | append $existing) | str join ":"
|
|
|
|
|
} else {
|
|
|
|
|
$valid | str join ":"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def ncl-export-safe [root: string, file: string]: nothing -> record {
|
|
|
|
|
let ip = (nickel-import-path $root)
|
|
|
|
|
daemon-export-safe $file --import-path $ip | default {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ── Data extraction ──────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
def extract-identity [root: string]: nothing -> record {
|
|
|
|
|
let cargo = $"($root)/Cargo.toml"
|
|
|
|
|
mut name = ($root | path basename)
|
|
|
|
|
mut version = ""
|
|
|
|
|
mut description = ""
|
|
|
|
|
mut crates = []
|
|
|
|
|
|
|
|
|
|
if ($cargo | path exists) {
|
|
|
|
|
let cargo_data = (open $cargo)
|
|
|
|
|
if ($cargo_data | get -o package.name | is-not-empty) {
|
|
|
|
|
$name = ($cargo_data | get package.name)
|
|
|
|
|
$version = ($cargo_data | get -o package.version | default "")
|
|
|
|
|
$description = ($cargo_data | get -o package.description | default "")
|
|
|
|
|
}
|
|
|
|
|
# Workspace members
|
|
|
|
|
let members = ($cargo_data | get -o workspace.members | default [])
|
|
|
|
|
if ($members | is-not-empty) {
|
|
|
|
|
$crates = ($members | each { |m|
|
|
|
|
|
let member_cargo = $"($root)/($m)/Cargo.toml"
|
|
|
|
|
# Expand globs: crates/* → actual directories
|
|
|
|
|
if ($m | str contains "*") {
|
|
|
|
|
glob $"($root)/($m)/Cargo.toml" | each { |f|
|
|
|
|
|
let crate_dir = ($f | path dirname)
|
|
|
|
|
let crate_data = (open $f)
|
|
|
|
|
{
|
|
|
|
|
name: ($crate_data | get -o package.name | default ($crate_dir | path basename)),
|
|
|
|
|
path: ($crate_dir | str replace $"($root)/" ""),
|
|
|
|
|
description: ($crate_data | get -o package.description | default ""),
|
|
|
|
|
version: ($crate_data | get -o package.version | default ""),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if ($member_cargo | path exists) {
|
|
|
|
|
let crate_data = (open $member_cargo)
|
|
|
|
|
[{
|
|
|
|
|
name: ($crate_data | get -o package.name | default ($m | path basename)),
|
|
|
|
|
path: $m,
|
|
|
|
|
description: ($crate_data | get -o package.description | default ""),
|
|
|
|
|
version: ($crate_data | get -o package.version | default ""),
|
|
|
|
|
}]
|
|
|
|
|
} else { [] }
|
|
|
|
|
} | flatten)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{ name: $name, version: $version, description: $description, crates: $crates, root: $root }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def extract-ontology [root: string]: nothing -> record {
|
|
|
|
|
let core_file = $"($root)/.ontology/core.ncl"
|
|
|
|
|
if not ($core_file | path exists) { return { nodes: [], edges: [] } }
|
|
|
|
|
let core = (ncl-export-safe $root $core_file)
|
|
|
|
|
if ($core | is-empty) { return { nodes: [], edges: [] } }
|
|
|
|
|
{
|
|
|
|
|
nodes: ($core.nodes? | default []),
|
|
|
|
|
edges: ($core.edges? | default []),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def extract-state [root: string]: nothing -> list<record> {
|
|
|
|
|
let state_file = $"($root)/.ontology/state.ncl"
|
|
|
|
|
if not ($state_file | path exists) { return [] }
|
|
|
|
|
let state = (ncl-export-safe $root $state_file)
|
|
|
|
|
if ($state | is-empty) { return [] }
|
|
|
|
|
$state.dimensions? | default []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def extract-gates [root: string]: nothing -> list<record> {
|
|
|
|
|
let gate_file = $"($root)/.ontology/gate.ncl"
|
|
|
|
|
if not ($gate_file | path exists) { return [] }
|
|
|
|
|
let gate = (ncl-export-safe $root $gate_file)
|
|
|
|
|
if ($gate | is-empty) { return [] }
|
|
|
|
|
$gate.gates? | default [] | where { |g| ($g.active? | default false) == true }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def extract-adrs [root: string]: nothing -> list<record> {
|
|
|
|
|
let files = (glob $"($root)/adrs/adr-*.ncl")
|
|
|
|
|
$files | each { |f|
|
|
|
|
|
let data = (ncl-export-safe $root $f)
|
|
|
|
|
if ($data | is-not-empty) {
|
|
|
|
|
{
|
|
|
|
|
id: ($data.id? | default ""),
|
|
|
|
|
title: ($data.title? | default ""),
|
|
|
|
|
status: ($data.status? | default ""),
|
|
|
|
|
date: ($data.date? | default ""),
|
|
|
|
|
decision: ($data.decision? | default ""),
|
|
|
|
|
constraints: ($data.constraints? | default []),
|
|
|
|
|
}
|
|
|
|
|
} else { null }
|
|
|
|
|
} | compact
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def extract-modes [root: string]: nothing -> list<record> {
|
|
|
|
|
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_files = ($ontoref_modes | append $project_modes | uniq)
|
|
|
|
|
|
|
|
|
|
$all_files | each { |f|
|
|
|
|
|
let data = (ncl-export-safe $root $f)
|
|
|
|
|
if ($data | is-not-empty) and ($data.id? | is-not-empty) {
|
|
|
|
|
{
|
|
|
|
|
id: ($data.id? | default ""),
|
|
|
|
|
trigger: ($data.trigger? | default ""),
|
|
|
|
|
steps: ($data.steps? | default [] | length),
|
|
|
|
|
source: (if ($f | str starts-with $root) { $f | str replace $"($root)/" "" } else { $f | str replace $"($env.ONTOREF_ROOT)/" "onref/" }),
|
|
|
|
|
}
|
|
|
|
|
} else { null }
|
|
|
|
|
} | compact
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def extract-scenarios [root: string]: nothing -> list<record> {
|
|
|
|
|
let scenarios_dir = $"($root)/reflection/scenarios"
|
|
|
|
|
if not ($scenarios_dir | path exists) { return [] }
|
|
|
|
|
let dirs = (ls $scenarios_dir | where type == dir | get name)
|
|
|
|
|
$dirs | each { |d|
|
|
|
|
|
let scenario_ncl = $"($d)/scenario.ncl"
|
|
|
|
|
let meta = if ($scenario_ncl | path exists) {
|
|
|
|
|
ncl-export-safe $root $scenario_ncl
|
|
|
|
|
} else { {} }
|
|
|
|
|
let files = (ls $d | where type == file | get name | each { |f| $f | path basename })
|
|
|
|
|
{
|
|
|
|
|
category: ($d | path basename),
|
|
|
|
|
path: ($d | str replace $"($root)/" ""),
|
|
|
|
|
files: $files,
|
|
|
|
|
actor: ($meta.actor? | default ""),
|
|
|
|
|
purpose: ($meta.purpose? | default ""),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ── Compose full document ────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
def compose-doc-data [root: string]: nothing -> record {
|
|
|
|
|
let identity = (extract-identity $root)
|
|
|
|
|
let ontology = (extract-ontology $root)
|
|
|
|
|
let state = (extract-state $root)
|
|
|
|
|
let gates = (extract-gates $root)
|
|
|
|
|
let adrs = (extract-adrs $root)
|
|
|
|
|
let modes = (extract-modes $root)
|
|
|
|
|
let scenarios = (extract-scenarios $root)
|
|
|
|
|
|
|
|
|
|
# Classify ontology nodes by level.
|
|
|
|
|
let nodes = ($ontology.nodes? | default [])
|
|
|
|
|
let axioms = ($nodes | where { |n| ($n.invariant? | default false) == true })
|
|
|
|
|
let tensions = ($nodes | where { |n| ($n.level? | default "") == "Tension" })
|
|
|
|
|
let practices = ($nodes | where { |n| let l = ($n.level? | default ""); $l == "Practice" or $l == "Spiral" })
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
identity: $identity,
|
|
|
|
|
architecture: {
|
|
|
|
|
axioms: ($axioms | each { |n| { id: $n.id, name: ($n.name? | default ""), description: ($n.description? | default ""), artifact_paths: ($n.artifact_paths? | default []) } }),
|
|
|
|
|
tensions: ($tensions | each { |n| { id: $n.id, name: ($n.name? | default ""), description: ($n.description? | default "") } }),
|
|
|
|
|
practices: ($practices | each { |n| { id: $n.id, name: ($n.name? | default ""), description: ($n.description? | default ""), artifact_paths: ($n.artifact_paths? | default []) } }),
|
|
|
|
|
edges: ($ontology.edges? | default []),
|
|
|
|
|
},
|
|
|
|
|
state: ($state | each { |d| { id: ($d.id? | default ""), name: ($d.name? | default ""), current: ($d.current_state? | default ""), desired: ($d.desired_state? | default "") } }),
|
|
|
|
|
gates: ($gates | each { |g| { id: ($g.id? | default ""), protects: ($g.protects? | default []), condition: ($g.opening_condition? | default "") } }),
|
|
|
|
|
decisions: ($adrs | where { |a| $a.status == "Accepted" }),
|
|
|
|
|
decisions_all: $adrs,
|
|
|
|
|
modes: $modes,
|
|
|
|
|
scenarios: $scenarios,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ── Format: Markdown ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
def render-md [data: record]: nothing -> string {
|
|
|
|
|
let id = $data.identity
|
|
|
|
|
mut lines = [$"# ($id.name)"]
|
|
|
|
|
|
|
|
|
|
if ($id.version | is-not-empty) { $lines = ($lines | append $"**Version**: ($id.version)") }
|
|
|
|
|
if ($id.description | is-not-empty) {
|
|
|
|
|
$lines = ($lines | append "")
|
|
|
|
|
$lines = ($lines | append $id.description)
|
|
|
|
|
}
|
|
|
|
|
$lines = ($lines | append "")
|
|
|
|
|
|
|
|
|
|
# Crates
|
|
|
|
|
if ($id.crates | is-not-empty) {
|
|
|
|
|
$lines = ($lines | append "## Crates")
|
|
|
|
|
$lines = ($lines | append "")
|
|
|
|
|
$lines = ($lines | append "| Crate | Description |")
|
|
|
|
|
$lines = ($lines | append "|-------|-------------|")
|
|
|
|
|
for c in $id.crates {
|
|
|
|
|
$lines = ($lines | append $"| `($c.name)` | ($c.description) |")
|
|
|
|
|
}
|
|
|
|
|
$lines = ($lines | append "")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Architecture — Axioms
|
|
|
|
|
if ($data.architecture.axioms | is-not-empty) {
|
|
|
|
|
$lines = ($lines | append "## Invariants")
|
|
|
|
|
$lines = ($lines | append "")
|
|
|
|
|
for a in $data.architecture.axioms {
|
|
|
|
|
$lines = ($lines | append $"### ($a.name)")
|
|
|
|
|
$lines = ($lines | append "")
|
|
|
|
|
$lines = ($lines | append $a.description)
|
|
|
|
|
if ($a.artifact_paths | is-not-empty) {
|
|
|
|
|
let paths = ($a.artifact_paths | each { |p| $"`($p)`" } | str join ", ")
|
|
|
|
|
$lines = ($lines | append $"Artifacts: ($paths)")
|
|
|
|
|
}
|
|
|
|
|
$lines = ($lines | append "")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Architecture — Tensions
|
|
|
|
|
if ($data.architecture.tensions | is-not-empty) {
|
|
|
|
|
$lines = ($lines | append "## Tensions")
|
|
|
|
|
$lines = ($lines | append "")
|
|
|
|
|
for t in $data.architecture.tensions {
|
|
|
|
|
let poles = if ($t.poles | is-not-empty) {
|
|
|
|
|
let pole_strs = ($t.poles | each { |p| $"($p.pole? | default '')" })
|
|
|
|
|
let joined = ($pole_strs | str join " ↔ ")
|
|
|
|
|
$" (char lparen)($joined)(char rparen)"
|
|
|
|
|
} else { "" }
|
|
|
|
|
$lines = ($lines | append $"- **($t.name)**($poles): ($t.description)")
|
|
|
|
|
}
|
|
|
|
|
$lines = ($lines | append "")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Architecture — Systems/Practices
|
|
|
|
|
if ($data.architecture.practices | is-not-empty) {
|
|
|
|
|
$lines = ($lines | append "## Systems")
|
|
|
|
|
$lines = ($lines | append "")
|
|
|
|
|
for p in $data.architecture.practices {
|
|
|
|
|
$lines = ($lines | append $"### ($p.name)")
|
|
|
|
|
$lines = ($lines | append "")
|
|
|
|
|
$lines = ($lines | append $p.description)
|
|
|
|
|
if ($p.artifact_paths | is-not-empty) {
|
|
|
|
|
$lines = ($lines | append "")
|
|
|
|
|
for ap in $p.artifact_paths { $lines = ($lines | append $"- `($ap)`") }
|
|
|
|
|
}
|
|
|
|
|
$lines = ($lines | append "")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# State dimensions
|
|
|
|
|
if ($data.state | is-not-empty) {
|
|
|
|
|
$lines = ($lines | append "## State")
|
|
|
|
|
$lines = ($lines | append "")
|
|
|
|
|
$lines = ($lines | append "| Dimension | Current | Desired |")
|
|
|
|
|
$lines = ($lines | append "|-----------|---------|---------|")
|
|
|
|
|
for d in $data.state {
|
|
|
|
|
let marker = if $d.current == $d.desired { " ✓" } else { "" }
|
|
|
|
|
$lines = ($lines | append $"| ($d.name) | `($d.current)` | `($d.desired)`($marker) |")
|
|
|
|
|
}
|
|
|
|
|
$lines = ($lines | append "")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Gates
|
|
|
|
|
if ($data.gates | is-not-empty) {
|
|
|
|
|
$lines = ($lines | append "## Active Gates")
|
|
|
|
|
$lines = ($lines | append "")
|
|
|
|
|
for g in $data.gates {
|
|
|
|
|
$lines = ($lines | append $"- **($g.id)**: protects `($g.protects)` — ($g.condition)")
|
|
|
|
|
}
|
|
|
|
|
$lines = ($lines | append "")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Decisions
|
|
|
|
|
if ($data.decisions | is-not-empty) {
|
|
|
|
|
$lines = ($lines | append "## Decisions (ADRs)")
|
|
|
|
|
$lines = ($lines | append "")
|
|
|
|
|
for a in $data.decisions {
|
|
|
|
|
$lines = ($lines | append $"### ($a.id): ($a.title)")
|
|
|
|
|
$lines = ($lines | append "")
|
|
|
|
|
$lines = ($lines | append $a.decision)
|
|
|
|
|
if ($a.constraints | is-not-empty) {
|
|
|
|
|
let hard = ($a.constraints | where { |c| ($c.severity? | default "") == "Hard" })
|
|
|
|
|
if ($hard | is-not-empty) {
|
|
|
|
|
$lines = ($lines | append "")
|
|
|
|
|
$lines = ($lines | append "**Hard constraints:**")
|
|
|
|
|
$lines = ($lines | append "")
|
|
|
|
|
for c in $hard {
|
|
|
|
|
$lines = ($lines | append $"- ($c.description? | default $c.scope)")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
$lines = ($lines | append "")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Modes
|
|
|
|
|
if ($data.modes | is-not-empty) {
|
|
|
|
|
$lines = ($lines | append "## Operational Modes")
|
|
|
|
|
$lines = ($lines | append "")
|
|
|
|
|
for m in $data.modes {
|
|
|
|
|
let step_count = $"(char lparen)($m.steps) steps(char rparen)"
|
|
|
|
|
$lines = ($lines | append $"- **($m.id)** ($step_count): ($m.trigger)")
|
|
|
|
|
}
|
|
|
|
|
$lines = ($lines | append "")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Scenarios
|
|
|
|
|
if ($data.scenarios | is-not-empty) {
|
|
|
|
|
$lines = ($lines | append "## Scenarios")
|
|
|
|
|
$lines = ($lines | append "")
|
|
|
|
|
for s in $data.scenarios {
|
|
|
|
|
let purpose_str = if ($s.purpose | is-not-empty) { $" [($s.purpose)]" } else { "" }
|
|
|
|
|
let file_count = $"(char lparen)($s.files | length) files(char rparen)"
|
|
|
|
|
$lines = ($lines | append $"- **($s.category)**($purpose_str): `($s.path)/` ($file_count)")
|
|
|
|
|
}
|
|
|
|
|
$lines = ($lines | append "")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$lines | str join "\n"
|
|
|
|
|
}
|
|
|
|
|
|
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 helpers ────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
def read-crate-module-doc [crate_full_path: string]: nothing -> string {
|
|
|
|
|
for entry in [$"($crate_full_path)/src/lib.rs", $"($crate_full_path)/src/main.rs"] {
|
|
|
|
|
if not ($entry | path exists) { continue }
|
|
|
|
|
let lines = (open $entry | lines)
|
|
|
|
|
let past_attrs = (
|
|
|
|
|
$lines
|
|
|
|
|
| skip while { |l|
|
|
|
|
|
let t = ($l | str trim)
|
|
|
|
|
($t | is-empty) or ($t | str starts-with "#!")
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
let doc_lines = (
|
|
|
|
|
$past_attrs
|
|
|
|
|
| take while { |l|
|
|
|
|
|
let t = ($l | str trim)
|
|
|
|
|
($t | is-empty) or ($t | str starts-with "//!")
|
|
|
|
|
}
|
|
|
|
|
| where { |l| $l | str trim | str starts-with "//!" }
|
|
|
|
|
| each { |l| $l | str trim | str replace --regex '^//! ?' "" }
|
|
|
|
|
)
|
|
|
|
|
return ($doc_lines | str join "\n")
|
|
|
|
|
}
|
|
|
|
|
""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def count-pub-coverage [crate_full_path: string]: nothing -> record {
|
|
|
|
|
let src_dir = $"($crate_full_path)/src"
|
|
|
|
|
if not ($src_dir | path exists) { return { total: 0, documented: 0, percent: 0 } }
|
|
|
|
|
let rs_files = (glob $"($src_dir)/**/*.rs")
|
|
|
|
|
if ($rs_files | is-empty) { return { total: 0, documented: 0, percent: 0 } }
|
|
|
|
|
mut total = 0
|
|
|
|
|
mut documented = 0
|
|
|
|
|
for rs in $rs_files {
|
|
|
|
|
let lines = (open $rs | lines)
|
|
|
|
|
let indexed = ($lines | enumerate)
|
|
|
|
|
for row in $indexed {
|
|
|
|
|
let t = ($row.item | str trim)
|
|
|
|
|
let is_pub = (
|
|
|
|
|
($t | str starts-with "pub fn ") or ($t | str starts-with "pub struct ") or
|
|
|
|
|
($t | str starts-with "pub enum ") or ($t | str starts-with "pub trait ") or
|
|
|
|
|
($t | str starts-with "pub type ") or ($t | str starts-with "pub const ") or
|
|
|
|
|
($t | str starts-with "pub mod ") or ($t | str starts-with "pub async fn ")
|
|
|
|
|
)
|
|
|
|
|
if not $is_pub { continue }
|
|
|
|
|
$total = ($total + 1)
|
|
|
|
|
if $row.index > 0 {
|
|
|
|
|
let prev = ($indexed | where { |r| $r.index < $row.index } | reverse | where { |r| ($r.item | str trim | is-not-empty) } | if ($in | is-not-empty) { first } else { null })
|
|
|
|
|
if ($prev != null) {
|
|
|
|
|
let pt = ($prev.item | str trim)
|
|
|
|
|
if ($pt | str starts-with "///") {
|
|
|
|
|
$documented = ($documented + 1)
|
|
|
|
|
} else if ($pt | str starts-with "#[") {
|
|
|
|
|
let before = ($indexed | where { |r| $r.index < $prev.index } | reverse | where { |r| ($r.item | str trim | is-not-empty) } | if ($in | is-not-empty) { first } else { null })
|
|
|
|
|
if ($before != null) and (($before.item | str trim) | str starts-with "///") {
|
|
|
|
|
$documented = ($documented + 1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let pct = if $total > 0 { ($documented * 100 / $total | math round) } else { 100 }
|
|
|
|
|
{ total: $total, documented: $documented, percent: $pct }
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 00:21:04 +00:00
|
|
|
# ── Format: mdBook ───────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
def render-mdbook [data: record, root: string] {
|
|
|
|
|
let docs_src = $"($root)/docs/src"
|
|
|
|
|
mkdir $docs_src
|
|
|
|
|
mkdir $"($docs_src)/architecture"
|
|
|
|
|
mkdir $"($docs_src)/decisions"
|
|
|
|
|
mkdir $"($docs_src)/modes"
|
|
|
|
|
|
|
|
|
|
# README / intro
|
|
|
|
|
let intro = ([
|
|
|
|
|
$"# ($data.identity.name)"
|
|
|
|
|
""
|
|
|
|
|
($data.identity.description)
|
|
|
|
|
""
|
|
|
|
|
$"Generated from project ontology and reflection data."
|
|
|
|
|
] | str join "\n")
|
|
|
|
|
$intro | save -f $"($docs_src)/README.md"
|
|
|
|
|
|
|
|
|
|
# Architecture page
|
|
|
|
|
let arch_data = {
|
|
|
|
|
identity: $data.identity,
|
|
|
|
|
architecture: $data.architecture,
|
|
|
|
|
state: $data.state,
|
|
|
|
|
gates: $data.gates,
|
|
|
|
|
decisions: [],
|
|
|
|
|
decisions_all: [],
|
|
|
|
|
modes: [],
|
|
|
|
|
scenarios: [],
|
|
|
|
|
}
|
|
|
|
|
let arch_md = (render-md $arch_data)
|
|
|
|
|
$arch_md | save -f $"($docs_src)/architecture/overview.md"
|
|
|
|
|
|
|
|
|
|
# Individual ADR pages
|
|
|
|
|
for adr in $data.decisions_all {
|
|
|
|
|
let adr_data = {
|
|
|
|
|
identity: { name: "", version: "", description: "", crates: [], root: "" },
|
|
|
|
|
architecture: { axioms: [], tensions: [], practices: [], edges: [] },
|
|
|
|
|
state: [],
|
|
|
|
|
gates: [],
|
|
|
|
|
decisions: (if $adr.status == "Accepted" { [$adr] } else { [] }),
|
|
|
|
|
decisions_all: [$adr],
|
|
|
|
|
modes: [],
|
|
|
|
|
scenarios: [],
|
|
|
|
|
}
|
|
|
|
|
let status_badge = match $adr.status {
|
|
|
|
|
"Accepted" => "✅ Accepted",
|
|
|
|
|
"Proposed" => "📝 Proposed",
|
|
|
|
|
"Superseded" => "🔄 Superseded",
|
|
|
|
|
_ => $adr.status,
|
|
|
|
|
}
|
|
|
|
|
let content = ([
|
|
|
|
|
$"# ($adr.id): ($adr.title)"
|
|
|
|
|
""
|
|
|
|
|
$"**Status**: ($status_badge) **Date**: ($adr.date)"
|
|
|
|
|
""
|
|
|
|
|
$adr.decision
|
|
|
|
|
] | str join "\n")
|
|
|
|
|
$content | save -f $"($docs_src)/decisions/($adr.id).md"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ADR index
|
|
|
|
|
let adr_index_lines = (["# Decisions (ADRs)" ""] | append (
|
|
|
|
|
$data.decisions_all | each { |a|
|
|
|
|
|
let link = $"(char lparen)./($a.id).md(char rparen)"
|
|
|
|
|
$"- [($a.id): ($a.title)]($link) — ($a.status)"
|
|
|
|
|
}
|
|
|
|
|
))
|
|
|
|
|
($adr_index_lines | str join "\n") | save -f $"($docs_src)/decisions/README.md"
|
|
|
|
|
|
|
|
|
|
# Modes page
|
|
|
|
|
let modes_lines = (["# Operational Modes" ""] | append (
|
|
|
|
|
$data.modes | each { |m|
|
|
|
|
|
let step_count = $"(char lparen)($m.steps) steps(char rparen)"
|
|
|
|
|
$"- **($m.id)** ($step_count): ($m.trigger)"
|
|
|
|
|
}
|
|
|
|
|
))
|
|
|
|
|
($modes_lines | str join "\n") | save -f $"($docs_src)/modes/README.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
|
|
|
# Crates chapter — one page per workspace member, sourced from //! docs
|
|
|
|
|
mut crate_pages = []
|
|
|
|
|
if ($data.identity.crates | is-not-empty) {
|
|
|
|
|
mkdir $"($docs_src)/crates"
|
|
|
|
|
for c in $data.identity.crates {
|
|
|
|
|
let full_path = $"($root)/($c.path)"
|
|
|
|
|
let module_doc = (read-crate-module-doc $full_path)
|
|
|
|
|
let coverage = (count-pub-coverage $full_path)
|
|
|
|
|
|
|
|
|
|
# Which practice nodes list this crate as first artifact_path?
|
|
|
|
|
let c_norm = ($c.path | str replace --regex '/$' "")
|
|
|
|
|
let implementing = (
|
|
|
|
|
$data.architecture.practices
|
|
|
|
|
| where { |p|
|
|
|
|
|
let first = ($p.artifact_paths? | default [] | if ($in | is-not-empty) { first } else { "" } | str replace --regex '/$' "")
|
|
|
|
|
$first == $c_norm
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Cargo feature flags from Cargo.toml
|
|
|
|
|
let cargo_file = $"($full_path)/Cargo.toml"
|
|
|
|
|
let features = if ($cargo_file | path exists) {
|
|
|
|
|
let cargo_data = (open $cargo_file)
|
|
|
|
|
let feat_map = ($cargo_data | get -o features | default {})
|
|
|
|
|
$feat_map | transpose key value | where { |r| $r.key != "default" } | each { |r|
|
|
|
|
|
let deps = ($r.value | each { |d| $" - `($d)`" } | str join "\n")
|
|
|
|
|
if ($deps | is-not-empty) {
|
|
|
|
|
$"- `($r.key)` — enables:\n($deps)"
|
|
|
|
|
} else {
|
|
|
|
|
$"- `($r.key)`"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else { [] }
|
|
|
|
|
|
|
|
|
|
let coverage_badge = if $coverage.percent >= 80 {
|
|
|
|
|
$"✅ ($coverage.percent)% \(($coverage.documented)/($coverage.total) pub items\)"
|
|
|
|
|
} else if $coverage.percent >= 50 {
|
|
|
|
|
$"⚠️ ($coverage.percent)% \(($coverage.documented)/($coverage.total) pub items\)"
|
|
|
|
|
} else {
|
|
|
|
|
$"❌ ($coverage.percent)% \(($coverage.documented)/($coverage.total) pub items\)"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mut page_lines = [$"# ($c.name)" ""]
|
|
|
|
|
if ($module_doc | is-not-empty) {
|
|
|
|
|
$page_lines = ($page_lines | append [$module_doc ""])
|
|
|
|
|
} else {
|
|
|
|
|
$page_lines = ($page_lines | append [$"> ⚠️ No `//!` module doc found in `src/lib.rs`." ""])
|
|
|
|
|
}
|
|
|
|
|
$page_lines = ($page_lines | append [$"**Doc coverage:** ($coverage_badge)" ""])
|
|
|
|
|
if ($features | is-not-empty) {
|
|
|
|
|
$page_lines = ($page_lines | append (["## Feature Flags" ""] | append $features | append ""))
|
|
|
|
|
}
|
|
|
|
|
if ($implementing | is-not-empty) {
|
|
|
|
|
let practice_links = ($implementing | each { |p| $"- **($p.id)** — ($p.name)" })
|
|
|
|
|
$page_lines = ($page_lines | append (["## Implements" ""] | append $practice_links | append ""))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let slug = ($c.name | str replace "--" "-")
|
|
|
|
|
let page_path = $"($docs_src)/crates/($slug).md"
|
|
|
|
|
($page_lines | str join "\n") | save -f $page_path
|
|
|
|
|
$crate_pages = ($crate_pages | append { name: $c.name, slug: $slug })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Crates index
|
|
|
|
|
let crates_index = (["# Crates" ""] | append (
|
|
|
|
|
$crate_pages | each { |cp| $"- [($cp.name)](($cp.slug).md)" }
|
|
|
|
|
))
|
|
|
|
|
($crates_index | str join "\n") | save -f $"($docs_src)/crates/README.md"
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 00:21:04 +00:00
|
|
|
# SUMMARY.md
|
|
|
|
|
mut summary = [
|
|
|
|
|
"# Summary"
|
|
|
|
|
""
|
|
|
|
|
"[Introduction](README.md)"
|
|
|
|
|
""
|
|
|
|
|
"# Architecture"
|
|
|
|
|
""
|
|
|
|
|
"- [Overview](architecture/overview.md)"
|
|
|
|
|
""
|
|
|
|
|
"# Decisions"
|
|
|
|
|
""
|
|
|
|
|
]
|
|
|
|
|
for adr in $data.decisions_all {
|
|
|
|
|
$summary = ($summary | append $"- [($adr.id)](decisions/($adr.id).md)")
|
|
|
|
|
}
|
|
|
|
|
$summary = ($summary | append ["" "# Operations" "" "- [Modes](modes/README.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
|
|
|
if ($crate_pages | is-not-empty) {
|
|
|
|
|
$summary = ($summary | append ["" "# Crates" "" "- [Overview](crates/README.md)"])
|
|
|
|
|
for cp in $crate_pages {
|
|
|
|
|
$summary = ($summary | append $" - [($cp.name)](crates/($cp.slug).md)")
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-13 00:21:04 +00:00
|
|
|
($summary | str join "\n") | save -f $"($docs_src)/SUMMARY.md"
|
|
|
|
|
|
|
|
|
|
# book.toml
|
|
|
|
|
let book_toml = $"[book]
|
|
|
|
|
authors = [\"Generated by ontoref\"]
|
|
|
|
|
language = \"en\"
|
|
|
|
|
multilingual = false
|
|
|
|
|
src = \"src\"
|
|
|
|
|
title = \"($data.identity.name) Documentation\"
|
|
|
|
|
|
|
|
|
|
[output.html]
|
|
|
|
|
default-theme = \"navy\"
|
|
|
|
|
preferred-dark-theme = \"navy\"
|
|
|
|
|
"
|
|
|
|
|
let book_file = $"($root)/docs/book.toml"
|
|
|
|
|
if not ($book_file | path exists) {
|
|
|
|
|
$book_toml | save -f $book_file
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
print $" (ansi green)Generated:(ansi reset) ($docs_src)/SUMMARY.md"
|
|
|
|
|
print $" (ansi green)Generated:(ansi reset) ($docs_src)/README.md"
|
|
|
|
|
print $" (ansi green)Generated:(ansi reset) ($docs_src)/architecture/overview.md"
|
|
|
|
|
let adr_count = ($data.decisions_all | length)
|
|
|
|
|
print $" (ansi green)Generated:(ansi reset) ($adr_count) ADR pages in ($docs_src)/decisions/"
|
|
|
|
|
print $" (ansi green)Generated:(ansi reset) ($docs_src)/modes/README.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
|
|
|
if ($crate_pages | is-not-empty) {
|
|
|
|
|
let crate_count = ($crate_pages | length)
|
|
|
|
|
print $" (ansi green)Generated:(ansi reset) ($crate_count) crate pages in ($docs_src)/crates/"
|
|
|
|
|
}
|
2026-03-13 00:21:04 +00:00
|
|
|
|
|
|
|
|
# Build if mdbook is available
|
|
|
|
|
let has_mdbook = (do { ^which mdbook } | complete | get exit_code) == 0
|
|
|
|
|
if $has_mdbook {
|
|
|
|
|
print ""
|
|
|
|
|
print $" (ansi cyan)Building mdBook...(ansi reset)"
|
|
|
|
|
let result = (do { ^mdbook build $"($root)/docs/" } | complete)
|
|
|
|
|
if $result.exit_code == 0 {
|
|
|
|
|
print $" (ansi green)Book built:(ansi reset) ($root)/docs/book/"
|
|
|
|
|
} else {
|
|
|
|
|
print $" (ansi yellow)Build failed:(ansi reset) ($result.stderr | str trim)"
|
|
|
|
|
print $" (ansi dark_gray)Run manually: mdbook build docs/(ansi reset)"
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
print ""
|
|
|
|
|
print $" (ansi dark_gray)mdbook not found. Install: cargo install mdbook(ansi reset)"
|
|
|
|
|
print $" (ansi dark_gray)Then: mdbook build docs/(ansi reset)"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ── Public API ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export def "docs generate" [
|
|
|
|
|
--fmt (-f): string = "", # Output format: md | json | yaml | mdbook (short: m j y)
|
|
|
|
|
]: nothing -> nothing {
|
|
|
|
|
let root = (project-root)
|
|
|
|
|
let actor = ($env.ONTOREF_ACTOR? | default "developer")
|
|
|
|
|
let raw_fmt = if ($fmt | is-not-empty) { $fmt } else if $actor == "agent" { "json" } else { "md" }
|
|
|
|
|
let f = match $raw_fmt {
|
|
|
|
|
"j" => "json",
|
|
|
|
|
"y" => "yaml",
|
|
|
|
|
"m" => "md",
|
|
|
|
|
_ => $raw_fmt,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let data = (compose-doc-data $root)
|
|
|
|
|
|
|
|
|
|
match $f {
|
|
|
|
|
"json" => { print ($data | to json) },
|
|
|
|
|
"yaml" => { print ($data | to yaml) },
|
|
|
|
|
"md" => { print (render-md $data) },
|
|
|
|
|
"mdbook" => { render-mdbook $data $root },
|
|
|
|
|
_ => {
|
|
|
|
|
print $" Unknown format: ($f). Available: md | json | yaml | mdbook"
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export def "docs formats" []: nothing -> nothing {
|
|
|
|
|
print ""
|
|
|
|
|
print " Available documentation formats:"
|
|
|
|
|
print ""
|
|
|
|
|
print $" (ansi cyan)md(ansi reset) Markdown document to stdout (default for humans)"
|
|
|
|
|
print $" (ansi cyan)json(ansi reset) Structured JSON to stdout (default for agents)"
|
|
|
|
|
print $" (ansi cyan)yaml(ansi reset) YAML to stdout"
|
|
|
|
|
print $" (ansi cyan)mdbook(ansi reset) Generates docs/src/ + SUMMARY.md, builds if mdbook installed"
|
|
|
|
|
print ""
|
|
|
|
|
print $" (ansi dark_gray)Usage: strat docs generate --fmt <format>(ansi reset)"
|
|
|
|
|
print $" (ansi dark_gray)Short: strat docs generate -f j(ansi reset)"
|
|
|
|
|
print ""
|
|
|
|
|
}
|