From 326404721f03fe3f6c838d23953baecbecf69ccc Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 7 Jun 2026 05:47:05 +0200 Subject: [PATCH 01/22] Plan the downstream consumer boundary (12.1.1) Draft ExecPlan for roadmap task 12.1.1, "Track the downstream consumer boundary". The plan delivers a TOML manifest, a generated Markdown matrix, an ADR 007 vocabulary section, and a referential-integrity gate in a new weaver-docs-gate workspace member so every command- contract task records whether it consumes OrthoConfig, wraps it temporarily, depends on a not-yet-decided upstream contract, or deliberately diverges via ADR 007. Revised after a Logisphere pre-implementation review. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...-1-1-track-downstream-consumer-boundary.md | 1035 +++++++++++++++++ 1 file changed, 1035 insertions(+) create mode 100644 docs/execplans/12-1-1-track-downstream-consumer-boundary.md diff --git a/docs/execplans/12-1-1-track-downstream-consumer-boundary.md b/docs/execplans/12-1-1-track-downstream-consumer-boundary.md new file mode 100644 index 00000000..d29493b8 --- /dev/null +++ b/docs/execplans/12-1-1-track-downstream-consumer-boundary.md @@ -0,0 +1,1035 @@ +# Track the downstream consumer boundary (roadmap 12.1.1) + +This ExecPlan (execution plan) is a living document. The sections +`Constraints`, `Tolerances`, `Risks`, `Progress`, `Surprises & Discoveries`, +`Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work +proceeds. + +Status: DRAFT + +This document must be maintained in accordance with `AGENTS.md` at the +repository root. + +## Purpose / big picture + +Weaver consumes generic command-contract machinery from OrthoConfig and owns +only Weaver-specific semantic code-edit metadata (capabilities, routing, +selectors, safety). Today that split is captured in two places: the OrthoConfig +dependency table and the temporary-adapter removal policy in +`docs/adr-007-agent-native-command-surface.md`. Neither place can answer the +question "for every command-contract task in the live Weaver roadmap, what is +its relationship to OrthoConfig?" — there is no single matrix, no machine +check, and no fixed vocabulary. + +Roadmap item 12.1.1 closes that gap. The success criterion in +`docs/roadmap.md` line 46 is: + +> Success: every command-contract task says whether it consumes OrthoConfig, +> wraps it temporarily, or records a deliberate divergence in ADR 007. + +After this change a contributor can open the roadmap, follow a back-link to a +single boundary matrix, and read for every command-contract task: the +classification state (`consumes`, `wraps`, or `divergent`), the upstream +OrthoConfig task it pairs with, and either the removal gate or the ADR 007 +divergence section that governs it. A test in the workspace fails when a new +command-contract task lands without a matrix entry, when a matrix entry +references a missing roadmap task, when a `wraps` entry has no removal gate, or +when a `divergent` entry has no ADR 007 anchor. + +Observable behaviour after this change: + +- `docs/orthoconfig-consumer-boundary.md` exists and lists every Weaver + command-contract task with its classification state and upstream pairing. +- `docs/adr-007-agent-native-command-surface.md` defines the three states in a + dedicated "Boundary classification" section and back-links to the matrix. +- `docs/roadmap.md` carries an inline back-link near each command-contract task + group to the matrix entry. +- `docs/developers-guide.md` documents the workflow for adding a new + command-contract task and updating the matrix. +- `docs/users-guide.md` notes the temporary divergences that users may see + while OrthoConfig contracts ship. +- `cargo test --workspace` exercises a new dedicated + `crates/weaver-docs-gate` integration test that parses the boundary + manifest (`docs/orthoconfig-consumer-boundary.toml`), the roadmap, and + the boundary matrix and asserts referential integrity. +- `make check-fmt && make lint && make test && make markdownlint && make nixie` + all pass. + +Downstream tasks 12.1.2 through 12.1.5 and every subsequent command-contract +task can record their boundary classification in one place instead of +reinventing the ADR cross-reference. + +## Why this matters now + +Phase 12 is the unavoidable foundation that prevents every later slice from +re-asking the same dependency question. Without a classification mechanism, +each command-contract task in phases 13 through 19 has to argue from first +principles whether the surface belongs upstream. With one, each task either +points at the matrix entry it inherits or proposes a single delta to it. + +The work is documentation-led, but the test gate is not optional. Without it +the matrix decays the moment a roadmap task is added or renamed and the +boundary becomes review-only again. + +## Constraints + +1. **OrthoConfig 5.2.3 may not have shipped.** The roadmap notes the + dependency, but 5.2.3 itself is documentation-only and can be tracked + independently. The matrix must rely on already-published OrthoConfig + decisions (the OrthoConfig `§2.1` consumer-boundary statement and + OrthoConfig ADR-003 schema ownership) rather than the unshipped bullets of + 5.2.3. The matrix is forward-compatible with 5.2.3 once it lands. +2. **No new runtime crate dependencies.** Documentation and tests use crates + already in the workspace (`camino`, `serde`, `toml`, `rstest`, + `pretty_assertions`, `googletest`). The test must compile under + `--workspace --all-targets --all-features`. No new workspace member adds + a runtime binary; `crates/weaver-docs-gate` exists only to host the + integration test and the small manifest parser it shares with itself. +3. **Do not duplicate the OrthoConfig roadmap.** Each matrix row references an + OrthoConfig task ID, not its content. If OrthoConfig renames a task, the + matrix needs a one-line update, not a re-derivation. +4. **No new commands, schemas, or runtime crates.** This task does not extend + `crates/weaver-cli/src/command_surface.rs` types nor introduce a new + adapter. Its job is to classify, not to refactor. +5. **No removal of ADR 007 content.** The existing OrthoConfig dependency + table and temporary-adapter removal policy stay where they are. The new + "Boundary classification" section sits alongside them and the matrix doc + references both. +6. **400-line file limit.** Every Rust source file added or modified must + stay below 400 lines. The matrix Markdown is exempt by repository policy + for prose, but the manifest TOML should remain well below that limit by + design. +7. **en-GB Oxford spelling.** All new prose follows `en-GB-oxendict` + conventions and `docs/documentation-style-guide.md`. +8. **No prototype command grammar reintroduction.** The matrix references + resource-first commands only. Prototype `observe`, `act`, `verify`, + provider-first commands, and root `--output` remain archive provenance. +9. **Stable task IDs.** The manifest keys roadmap task IDs as strings + (`"12.1.2"`). Roadmap renumbering is out of scope; ID stability is + asserted by the test gate. +10. **Strict Clippy.** New code must compile under + `cargo clippy --workspace --all-targets --all-features -- -D warnings`. + +## Tolerances (exception triggers) + +- **Scope.** If implementation modifies more than twelve documentation + files or six Rust source files (excluding the new + `crates/weaver-docs-gate` member's `Cargo.toml`, `src/lib.rs`, + `examples/render_boundary_matrix.rs`, and the test file), stop and + escalate. The new workspace member is expected; growth beyond it is + not. +- **Roadmap renames.** If classifying the existing tasks requires renumbering + a roadmap entry or moving a task between phases, stop and escalate. +- **ADR 007 rewrites.** If the boundary classification cannot fit alongside + the existing OrthoConfig dependency table and temporary-adapter removal + policy without rewriting either, stop and escalate. +- **Dependencies.** If the test gate requires a new external crate + dependency, stop and escalate. +- **Divergent classifications.** If two or more command-contract tasks must + be classified `divergent` without an existing ADR 007 anchor, stop and + open a follow-up ADR-007 amendment before continuing. +- **Iterations.** If the test gate fails after three attempts at adjustment, + stop and escalate. +- **Time.** If any single stage (A through E) takes more than four working + hours of focused effort, stop, record progress, and escalate. + +## Risks + +- Risk: OrthoConfig renumbers a roadmap task and the matrix points at a stale + ID. Severity: medium. Likelihood: medium. Mitigation: store the OrthoConfig + task ID plus a short stable phrase (for example, + `"5.2.3 — Record consumer dependency boundaries"`). The developers' guide + documents a quarterly cross-repo reconciliation step that compares the + manifest phrases against the OrthoConfig roadmap when that repository is + available locally. The CI gate intentionally does not check OrthoConfig + out; cross-repo drift is caught by the reconciliation rite, not by an + always-on probe. +- Risk: A command-contract task is missed during classification. Severity: + high. Likelihood: medium. Mitigation: the candidate set is an explicit + opt-in registry, not a topic-keyword heuristic. Every roadmap task ID + that should be classified appears in + `docs/orthoconfig-consumer-boundary.toml` under a top-level + `managed_tasks` array. The test gate fails when a roadmap task heading + matches an ID in `managed_tasks` but has no matching `[[task]]` row, and + also when a `[[task]]` row references an ID outside `managed_tasks`. + Adding a new command-contract task therefore requires editing the + registry first; default-fail prevents silent omission. +- Risk: Contributors classify a new task as `consumes` even though the + upstream OrthoConfig contract has not shipped yet. Severity: medium. + Likelihood: medium. Mitigation: the manifest's `consumes` state requires + a non-empty `shipped_in` field naming the upstream release (or commit + SHA) that landed the contract, plus the upstream task ID. The test gate + rejects `consumes` rows without `shipped_in`. +- Risk: A roadmap task depends on an OrthoConfig contract that is not yet + decided. Severity: high. Likelihood: high (phases 17–20 in particular). + Mitigation: a fourth state `pending` captures exactly this case. A + `pending` row carries the upstream task ID with `shipped_in = None` and + a non-empty `next_review_by` field (ISO-8601 date). The test gate fails + when a `pending` row's `next_review_by` is more than 270 days in the past + to prevent the matrix from decaying into a parking lot. +- Risk: The matrix doc and the manifest TOML drift. Severity: high. + Likelihood: medium. Mitigation: the matrix is generated from the manifest + by a Rust helper that the test calls; the test asserts byte-for-byte + equality with the committed matrix Markdown. There is no second snapshot + layer to maintain. +- Risk: Boundary classification is treated as a one-off audit rather than a + living contract. Severity: high. Likelihood: medium. Mitigation: + documentation in `docs/developers-guide.md` describes the workflow and + links to the matrix; the test gate runs on every CI build; every row + carries a `last_reviewed` ISO-8601 date and the gate warns (without + failing) when any row is more than 270 days stale, prompting a refresh. +- Risk: A `divergent` state is used as a parking lot for "we have not + decided". Severity: medium. Likelihood: low (now that `pending` exists). + Mitigation: every `divergent` row must carry an `adr_anchor` field + pointing to a specific section heading in + `docs/adr-007-agent-native-command-surface.md` and the test asserts the + anchor exists. Anchors may be shared by multiple rows (many-to-one), but + the anchor itself must be present. +- Risk: Wafflecat's lighter alternative (inline roadmap annotations with a + small lint, no separate manifest) would satisfy the same prose-level + success criterion at lower surface cost. Severity: low (this is a scope + trade-off, not a correctness risk). Likelihood: n/a. Mitigation: the + Decision Log captures the explicit choice to take the heavier matrix + approach for downstream queryability. If team capacity becomes the + binding constraint, the alternative remains available as a follow-up + redesign. + +## Progress + +Each item carries a UTC timestamp once it completes. The list reflects actual +state, not the intended sequence. + +- [ ] Stage A: ratify the boundary vocabulary and matrix shape. +- [ ] Stage B: produce the boundary manifest, generator, and matrix doc. +- [ ] Stage C: cross-link the matrix from ADR 007, the roadmap, and the + developers' guide. +- [ ] Stage D: add the referential-integrity test gate and snapshot. +- [ ] Stage E: refresh the users' guide, run the full quality gates, and run + `coderabbit review --agent`. + +## Surprises & discoveries + +Recorded as they occur during implementation. Format: + +- Observation: … + Evidence: … + Impact: … + +## Decision log + +Recorded for any decision that future work must respect. + +- Decision: classify boundary state with a closed four-value vocabulary + (`consumes`, `wraps`, `pending`, `divergent`) rather than free-form prose. + Rationale: the three-value vocabulary (consumes/wraps/divergent) borrowed + from OpenAPI Generator and IETF BCP 9 compliance reports does not + distinguish "upstream contract exists but unshipped" (`wraps`, with a + removal gate) from "upstream contract not yet decided" (`pending`, with + a `next_review_by` date). Without the fourth state the matrix would + pollute `wraps` and `divergent` with non-decisions, exactly the parking + lot the gate is meant to prevent. Date/Author: 2026-06-07, Logisphere + review by Telefono and Doggylump. +- Decision: hold the classification as a single TOML manifest at + `docs/orthoconfig-consumer-boundary.toml`, render the Markdown matrix from + it, and gate referential integrity from a Rust integration test. + Rationale: keeps the source of truth machine-readable, avoids + hand-maintained tables drifting, and follows the `xtask`-style drift gate + pattern (matklad/cargo-xtask, `trycmd`) used in the wider Rust ecosystem. + Date/Author: 2026-06-07, planning. +- Decision: host the test gate in a new tiny workspace member + `crates/weaver-docs-gate`, not in `crates/weaver-build-util`. + Rationale: `weaver-build-util` exists for build-script helpers (manual + page dates, capability-based `Dir` access). Adding documentation- + governance integration tests there mixes concerns and gives the + build-helper crate a misleading public surface. A dedicated workspace + member is honest about scope and keeps `weaver-build-util` boring. + Date/Author: 2026-06-07, Logisphere review by Pandalump. +- Decision: identify upstream contract availability with a + `shipped_in: Option` field (an OrthoConfig version tag such as + `"0.9.0"` or a fallback commit short SHA when no release exists yet), + not a Boolean `shipped` flag. Rationale: Boolean is too coarse for an + evolving dependency; a version string survives the `wraps → consumes` + transition audibly and lets the matrix encode minimum-known-good + versions. Date/Author: 2026-06-07, Logisphere review by Telefono. +- Decision: maintain the candidate set as an explicit `managed_tasks` + array in the manifest, not as a topic-keyword heuristic over the + roadmap prose. Rationale: heuristics produce silent false negatives + when a new task uses unfamiliar terminology; the explicit list defaults + to fail-closed and forces classification on every new command-contract + task. Date/Author: 2026-06-07, Logisphere review by Buzzy Bee. +- Decision: do not wire `rstest-bdd` scenarios for the matrix gate. + Rationale: the candidate behavioural scenarios are direct restatements + of the integration-test invariants. `rstest-bdd` would add ceremony + without adding behavioural coverage. The integration test stays as a + parameterized `rstest` suite. Date/Author: 2026-06-07, Logisphere + review by Dinolump. +- Decision: assert matrix freshness by byte-for-byte equality with the + committed Markdown file, not via an `insta` snapshot. Rationale: the + manifest is the source of truth and the renderer is deterministic; a + second snapshot review layer would duplicate the equality check + without adding signal. Date/Author: 2026-06-07, Logisphere review by + Dinolump. +- Decision: do not generate the OrthoConfig dependency table in ADR 007 + from the manifest. Rationale: ADR 007 is a stable, human-readable + architectural record. The manifest is the per-task index that points + back to the ADR; a cycle would couple them too tightly. Date/Author: + 2026-06-07, planning. +- Decision considered and deferred: switch entirely to inline roadmap + annotations (Wafflecat's lighter alternative) with no separate manifest + or matrix doc. Rationale for deferring: the heavier matrix approach + enables cross-phase queryability and a single discoverable artefact for + contributors and reviewers. If team capacity proves limiting, the + lighter alternative remains a viable follow-up. Date/Author: 2026-06-07, + Logisphere review by Wafflecat; deferred. + +## Outcomes & retrospective + +Recorded at completion. Includes whether the gate caught a regression in +practice, how often the manifest needed updates during subsequent tasks, and +any vocabulary or schema adjustments. + +## Context and orientation + +A novice opening this plan needs the following landmarks before reading the +"Plan of work" section. + +### Repository landmarks + +- `docs/roadmap.md` — the live forward roadmap. Tasks 12 through 20 belong to + the post-ADR-007 grammar. Task 12.1.1 lives at lines 38 to 47. +- `docs/adr-007-agent-native-command-surface.md` — the ADR that introduced + the agent-native command surface, the OrthoConfig dependency table (lines + 104 to 122), and the temporary-adapter removal policy (lines 129 to 147). +- `crates/weaver-cli/src/command_surface.rs` — the only current temporary + adapter, holding `CommandSurfaceRecord`, `READ_ONLY_COMMANDS`, and + `find_read_only_command` for the pilot `definitions get` and + `references list` family. Its module documentation already names the + OrthoConfig replacement tasks. This file is not modified by 12.1.1. +- `crates/weaver-build-util/src/lib.rs` — a shared build-time helper + crate (manual-page dates, capability-based `Dir` access). This plan + does *not* touch it; the boundary gate has nothing to do with build + scripts. +- `crates/weaver-docs-gate/` — new workspace member introduced by this + plan. It hosts the manifest parser (`load_manifest`), the matrix + renderer (`render_matrix`), the `cargo run --example` regenerator, + and the `boundary_gate` integration test. It has no runtime + consumers; its only role is to make documentation governance + testable. +- `docs/developers-guide.md` — the long-form contributor guide. A new + subsection ("Tracking the OrthoConfig consumer boundary") sits next to the + existing ADR guidance. +- `docs/users-guide.md` — the long-form user guide. A new short paragraph + records that some `--json` and human-renderer details may temporarily + diverge from upstream until OrthoConfig contracts ship. + +### Upstream landmarks (read once for context) + +- OrthoConfig roadmap, task 5.2.3: documentation-only commitment to record + consumer dependency boundaries for Weaver and Netsuke. The hard + dependencies are whole-CLI introspection, strict vocabulary policy, + agent-context IR, and localized help generation. The soft dependencies are + profiles, delivery, feedback, skill manifests, and execution ledgers. +- OrthoConfig `docs/agent-native-cli-design.md` §2.1: states that OrthoConfig + owns schemas, command metadata, vocabulary, renderer metadata, generated + help and man pages, policy linting, and optional primitives for profiles, + delivery, feedback, skill manifests, and execution ledgers. Weaver owns + semantic execution, capability routing, providers, sandboxing, Double-Lock + safety, edits, jobs, and provider-specific idempotency. +- OrthoConfig `docs/adr-003-define-schema-ownership-for-agent-native-contracts.md`: + defines the three contract crates and the schema-version constants + (`ORTHO_AGENT_CONTEXT_SCHEMA_VERSION`, + `ORTHO_POLICY_REPORT_SCHEMA_VERSION`). + +### Terms of art + +- **Command-contract task.** A roadmap task whose body or success criterion + touches at least one of: command surface metadata, public CLI grammar, + resource path or canonical verb, renderer contract (`--json`, human + renderer, `--plain`, `--color`, `--width`, paging), exit-code taxonomy, + bounded-list output, mutation policy (`--dry-run`, `--force`, + idempotency), structured error schemas, enumerating diagnostics, selector + forms (`--uri`, `--position`, `--query`, `--from-stdin`), capability + discoverability (`context --json`, `capabilities list`, `help`, manpages, + completions, skills), profiles, delivery, feedback, jobs, execution + ledger, or drift gates. The canonical candidate list is built into the + manifest. +- **Boundary state.** One of `consumes`, `wraps`, `pending`, or + `divergent`. + - `consumes` means the Weaver task imports or follows an OrthoConfig + contract that has already shipped. The manifest row carries the + upstream task ID *and* a non-empty `shipped_in` field naming the + OrthoConfig release tag (for example `"0.9.0"`) or a fallback short + commit SHA. The row carries no `removal_gate`. + - `wraps` means the Weaver task creates or reuses a temporary local + adapter while waiting for OrthoConfig to ship a contract whose shape + is committed upstream but whose implementation has not yet landed. + The manifest row carries the upstream task ID, an empty + `shipped_in`, and a non-empty `removal_gate`. + - `pending` means the Weaver task depends on an OrthoConfig contract + whose shape is not yet decided upstream. The row carries the upstream + task ID, an empty `shipped_in`, no `removal_gate`, and a non-empty + `next_review_by` ISO-8601 date naming when the row will be + re-evaluated. + - `divergent` means the Weaver task deliberately deviates from an + OrthoConfig contract. The row carries a non-empty `adr_anchor` + pointing to a heading slug in + `docs/adr-007-agent-native-command-surface.md`. +- **Removal gate.** The condition under which a `wraps` row must be + retired: usually "the upstream OrthoConfig task is implemented" or a + composite of several upstream tasks. The gate text matches the language + already used in the ADR 007 removal policy. +- **`managed_tasks` registry.** A top-level array in the manifest listing + every Weaver roadmap task ID whose body sits inside the boundary + surface. The registry is the candidate set; the gate fails when a + registry entry has no `[[task]]` row, when a `[[task]]` row references + an ID outside the registry, or when a candidate task heading appears + in the roadmap but is missing from both. + +### Hexagonal architecture relevance + +Boundary classification is a dependency-rule artefact, not a runtime port. +The matrix records, for each Weaver task, which side of the +domain-versus-adapter line its contract sits on. `consumes` means Weaver +imports a port owned by OrthoConfig; `wraps` means Weaver maintains a local +adapter that will be replaced by the upstream port; `divergent` means +Weaver's domain deliberately defines its own port because the upstream port +does not fit. Treat the matrix as a guard against accidentally inverting the +dependency rule when a new task lands. + +## Plan of work + +The work runs in five short stages. Each stage ends with a validation step +that must pass before the next stage starts. + +### Stage A: ratify the boundary vocabulary and matrix shape + +Stage A is documentation-led. No code lands. + +1. Open `docs/adr-007-agent-native-command-surface.md` and add a new section + titled "Boundary classification" before "Consequences". The section + defines the four states (`consumes`, `wraps`, `pending`, `divergent`), + the evidence each state requires, and the location of the matrix + (`docs/orthoconfig-consumer-boundary.md`). It does not duplicate the + existing "OrthoConfig dependencies" or "Temporary adapter removal + policy" sections; it points at them. It states explicitly that + `pending` exists to keep `wraps` honest about "shape committed + upstream" versus "shape not yet decided upstream". +2. Choose canonical column names for the matrix: `Roadmap task`, `Gist`, + `State`, `Upstream OrthoConfig task`, `Shipped in`, + `Removal gate or divergence`, `Next review by`, `Last reviewed`. + Document them in the new ADR section. +3. Open `docs/documentation-style-guide.md` only if a style entry is needed + for the badge or symbol convention used in the matrix (a leading + `✓`, `~`, or `×` character paired with the textual state name); if no + entry is needed, do not modify the style guide. + +Validation: `make markdownlint` passes, `make nixie` passes, and the new +section in ADR 007 reads as a self-contained explanation that a contributor +without prior context can use to classify a new task. + +### Stage B: produce the boundary manifest, generator, and matrix doc + +Stage B introduces a single source of truth and the renderer that derives +the human-readable matrix from it. + +1. Create `docs/orthoconfig-consumer-boundary.toml`. The file has a + top-level `managed_tasks` array listing every Weaver roadmap task ID + that falls inside the boundary surface, and one `[[task]]` row per + entry: + + ```toml + schema_version = 1 + + managed_tasks = [ + "12.1.1", "12.1.2", "12.1.3", "12.1.4", "12.1.5", + "13.1.1", "13.1.2", "13.1.3", + "13.2.1", "13.2.2", "13.2.3", + "13.3.1", "13.3.2", "13.3.3", "13.3.4", + # … through phases 14–20 (see Stage B step 2). + ] + + [[task]] + id = "13.1.2" + gist = "Implement the Weaver command-surface adapter for one read-only command family." + state = "wraps" + upstream = [ + { task = "OrthoConfig 5.2.3", role = "boundary" }, + { task = "OrthoConfig 6.1", role = "metadata" }, + { task = "OrthoConfig 7.2.7", role = "capability_provenance" }, + ] + shipped_in = "" + removal_gate = """ + Replace `CommandSurfaceRecord` once OrthoConfig 6.1 and 7.2.7 ship the \ + recursive command and capability/provenance metadata. + """ + adr_anchor = "" + next_review_by = "" + last_reviewed = "2026-06-07" + ``` + + The fields encode each state's evidence requirement: + + - `consumes` — `shipped_in` is non-empty and names an OrthoConfig + release tag or short commit SHA; `removal_gate`, `adr_anchor`, and + `next_review_by` are empty. + - `wraps` — `shipped_in` is empty; `removal_gate` is non-empty; + `adr_anchor` is empty; `next_review_by` may be empty. + - `pending` — `shipped_in` and `removal_gate` are empty; + `next_review_by` is non-empty (ISO-8601 date); `adr_anchor` may be + empty. + - `divergent` — `adr_anchor` is non-empty and names a stable heading + slug in `docs/adr-007-agent-native-command-surface.md` (anchors may + be shared by multiple rows); `shipped_in` and `removal_gate` are + empty. + + The `role` field on each upstream entry is a closed enum: + `boundary`, `metadata`, `capability_provenance`, `vocabulary`, + `renderer`, `profile`, `delivery`, `feedback`, `execution_ledger`. + The test gate rejects unknown roles. +2. Populate the manifest with the candidate set derived from + `docs/roadmap.md` and ADR 007. Use the topic-keyword scan described in + the Risks section. At minimum the seed set must include: + - 12.1.1 through 12.1.5; + - 13.1.1, 13.1.2, 13.1.3, 13.2.1, 13.2.2, 13.2.3, 13.3.1, 13.3.2, + 13.3.3, 13.3.4; + - 14.1.1 through 14.1.4, 14.2.1 through 14.2.3, 14.3.1, 14.3.2; + - 15.1.1, 15.1.2, 15.3.2, 15.3.3, 15.3.5, 15.3.6; + - 16.1.1, 16.1.2, 16.1.3, 16.2.1, 16.2.2, 16.2.3, 16.3.3, 16.3.5; + - 17.1.5, 17.2.2; + - 18.2.1; + - 19.1.1, 19.1.2, 19.1.3, 19.1.4, 19.2.2; + - 20.1.1, 20.1.2, 20.3.3. + + Each row carries its classification, upstream pairing, and (where + applicable) removal gate language derived from ADR 007's existing + table. Tasks classified `consumes` must reference an upstream that has + already shipped; tasks classified `wraps` must reference a removal + gate; tasks classified `divergent` must reference an ADR 007 anchor. +3. Create a new tiny workspace member at `crates/weaver-docs-gate` that + exposes the manifest parser and the matrix renderer to the test + harness: + + ```toml + # crates/weaver-docs-gate/Cargo.toml + [package] + name = "weaver-docs-gate" + version = "0.0.0" + publish = false + edition = "2024" + ``` + + The crate has no runtime dependencies it does not need: only + `serde`, `toml`, `camino`, and `thiserror` (all already in the + workspace). It is not a library that downstream code links; it is a + test fixture in crate form. The crate's `src/lib.rs` exposes: + + ```rust + pub fn load_manifest(path: &Utf8Path) -> Result; + pub fn render_matrix(manifest: &BoundaryManifest) -> String; + ``` + + `BoundaryManifest` holds the `schema_version`, the ordered + `managed_tasks` array, and a `Vec` keyed by task ID. + `BoundaryTask` holds `id`, `gist`, `state` (an enum with four + variants), `upstream` (a `Vec`), `shipped_in`, + `removal_gate`, `adr_anchor`, `next_review_by`, and `last_reviewed`. +4. Generate `docs/orthoconfig-consumer-boundary.md` from the manifest. The + matrix uses pipe-delimited Markdown tables grouped by roadmap phase + (12, 13, 14, 15, 16, 17, 18, 19, 20). Each table row links the task ID + back to its roadmap anchor, displays the state with a leading symbol + (`✓` for `consumes`, `~` for `wraps`, `?` for `pending`, `×` for + `divergent`), and lists the upstream task IDs joined by commas. The + `Shipped in` column carries the OrthoConfig version tag for + `consumes` rows and is empty otherwise. Long removal-gate text wraps + in the cell; the gate is the single source of truth and is not + abbreviated. + +Validation: `cargo build --workspace` succeeds (the new +`weaver-docs-gate` crate compiles). `make markdownlint` and `make nixie` +pass. The generated matrix matches the committed +`docs/orthoconfig-consumer-boundary.md` byte for byte. + +### Stage C: cross-link the matrix from ADR 007, the roadmap, and the developers' guide + +Stage C adds the back-links and a contributor workflow that prevents +silent drift. + +1. In `docs/adr-007-agent-native-command-surface.md`, finish the + "Boundary classification" section by linking to the matrix and the + manifest. The existing OrthoConfig dependency table stays as-is. +2. In `docs/roadmap.md`, immediately under the "12.1.1" item, add a single + sentence linking the matrix: "See the OrthoConfig consumer boundary + matrix for the per-task classification." Add the same link near the + headings of phases 13 through 20 so contributors editing a later phase + discover the matrix without scrolling. +3. In `docs/developers-guide.md`, add a subsection titled "Tracking the + OrthoConfig consumer boundary". It describes the workflow: + - When adding a roadmap task that touches a command-contract topic, + first add its ID to the `managed_tasks` array, then add a + `[[task]]` row in `docs/orthoconfig-consumer-boundary.toml`. + - Choose the matching classification state. If the upstream + OrthoConfig contract has not yet been decided, use `pending` and + set `next_review_by`. + - Run `cargo test -p weaver-docs-gate --test boundary_gate` to + regenerate `docs/orthoconfig-consumer-boundary.md` and assert + referential integrity. + - When OrthoConfig ships an upstream contract, flip the manifest + row from `wraps` or `pending` to `consumes`, set `shipped_in` to + the OrthoConfig release tag (or short commit SHA), and drop the + removal gate or review date. + - For a `divergent` row, capture the rationale in a named ADR 007 + section anchor and reference it in the manifest. Anchors may be + shared by multiple rows. + - Run the quarterly cross-repo reconciliation step (a short + ritual): when an OrthoConfig checkout is locally available, scan + the manifest's upstream phrases against the OrthoConfig roadmap + and fix any drifted IDs by hand. This ritual lives in the + developers' guide because the CI gate intentionally does not + check OrthoConfig out. +4. In `docs/contents.md`, add a new entry for + `docs/orthoconfig-consumer-boundary.md` so the new doc is discoverable + from the index. + +Validation: `make markdownlint` passes; the cross-links resolve when the +repository is browsed locally; the developers' guide entry is reachable +from `docs/contents.md`. + +### Stage D: add the referential-integrity test gate and snapshot + +Stage D enforces the matrix at CI time. + +1. In `crates/weaver-docs-gate/Cargo.toml`, add `rstest`, + `pretty_assertions`, and `googletest` to `[dev-dependencies]` (each + already lives in the workspace). The crate does not gain new runtime + dependencies and does not depend on `insta` or `rstest-bdd`. +2. Add `crates/weaver-docs-gate/tests/boundary_gate.rs`. It loads the + manifest, generates the matrix Markdown, and asserts the following + invariants. Each invariant is parameterized with `rstest` so the test + reports the specific row or task ID at fault. + + The invariants are: + + - **Required fields.** Every `[[task]]` row has a non-empty `id`, + `gist`, `state`, and `last_reviewed` field, and `state` is one of + `consumes`, `wraps`, `pending`, `divergent`. + - **`consumes` evidence.** Rows with `state = "consumes"` carry a + non-empty `shipped_in`, at least one upstream entry, an empty + `removal_gate`, an empty `adr_anchor`, and an empty + `next_review_by`. + - **`wraps` evidence.** Rows with `state = "wraps"` carry at least + one upstream entry, an empty `shipped_in`, a non-empty + `removal_gate`, and an empty `adr_anchor`. + - **`pending` evidence.** Rows with `state = "pending"` carry at + least one upstream entry, an empty `shipped_in`, an empty + `removal_gate`, and a non-empty `next_review_by` ISO-8601 date. + - **`divergent` evidence.** Rows with `state = "divergent"` carry a + non-empty `adr_anchor`, an empty `shipped_in`, and an empty + `removal_gate`; the anchor occurs as a heading slug in + `docs/adr-007-agent-native-command-surface.md`. + - **Registry symmetry.** Every ID in `managed_tasks` has a matching + `[[task]]` row; every `[[task]]` row's `id` appears in + `managed_tasks`; every `managed_tasks` ID appears as a task + heading in `docs/roadmap.md`. + - **`role` enum.** Each upstream entry's `role` is one of + `boundary`, `metadata`, `capability_provenance`, `vocabulary`, + `renderer`, `profile`, `delivery`, `feedback`, `execution_ledger`. + - **Date formats.** `last_reviewed` and `next_review_by` parse as + ISO-8601 dates (`YYYY-MM-DD`). + - **Staleness budget (warn).** When any row's `last_reviewed` is + more than 270 days behind the build's `SOURCE_DATE_EPOCH` (or wall + clock if the variable is unset), the test prints a warning naming + the row but does not fail. + - **`pending` decay budget (fail).** When a `pending` row's + `next_review_by` is more than 270 days *behind* the same + reference date, the test fails. This stops `pending` rows from + becoming a parking lot. + - **Matrix freshness.** The Markdown produced by `render_matrix` + equals the committed `docs/orthoconfig-consumer-boundary.md` + byte for byte. + +3. **Failure-message format.** Each invariant emits a diagnostic in a + fixed shape so contributors can resolve it without reading the test + source. The shape is: + + ```plaintext + boundary-gate: + roadmap_task: + manifest_path: docs/orthoconfig-consumer-boundary.toml + details: + remediation: + see: docs/developers-guide.md#tracking-the-orthoconfig-consumer-boundary + ``` + + Concrete examples that the test must produce verbatim: + + ```plaintext + boundary-gate: registry-symmetry + roadmap_task: 13.2.4 + manifest_path: docs/orthoconfig-consumer-boundary.toml + details: managed_tasks lists "13.2.4" but no [[task]] row matches. + remediation: add a [[task]] row for "13.2.4" with the matching state. + see: docs/developers-guide.md#tracking-the-orthoconfig-consumer-boundary + ``` + + ```plaintext + boundary-gate: consumes-evidence + roadmap_task: 14.1.1 + manifest_path: docs/orthoconfig-consumer-boundary.toml + details: state = "consumes" but shipped_in is empty. + remediation: set shipped_in to the OrthoConfig release tag, or change state to "wraps" or "pending". + see: docs/developers-guide.md#tracking-the-orthoconfig-consumer-boundary + ``` + + The integration test produces these strings through a small + helper. `pretty_assertions::assert_eq!` reports the message verbatim + on failure. + +Validation: the new test fails before the manifest exists (Red), passes +once Stage B is in place (Green), and remains green after Stage E. + +### Stage E: refresh the users' guide, run the full quality gates, and run CodeRabbit + +1. In `docs/users-guide.md`, add a short paragraph in the appropriate + chapter (the user-facing JSON or output-mode discussion) noting that + some `--json` fields, exit classes, and error codes may be provisional + until the OrthoConfig contracts ship. The paragraph links to the + matrix and notes that field names and exit classes remain stable + inside a Weaver minor release. +2. Run `make check-fmt` and capture the output under + `/tmp/check-fmt-weaver-12-1-1-track-downstream-consumer-boundary.out`. +3. Run `make lint`, `make test`, `make markdownlint`, and `make nixie`, + each capturing to a log file with the same naming convention. Verify + each completes successfully. +4. Run `coderabbit review --agent` and resolve every comment before the + plan is marked COMPLETE. + +Validation: every command completes successfully and `coderabbit review +--agent` raises no outstanding concerns. The roadmap entry for 12.1.1 is +ticked off. + +## Concrete steps + +The following sequence assumes a clean working tree on branch +`12-1-1-track-downstream-consumer-boundary`. All commands run from the +repository root. + +### Stage A commands + +```sh +${EDITOR:-vi} docs/adr-007-agent-native-command-surface.md +make markdownlint 2>&1 | tee /tmp/markdownlint-weaver-12-1-1-track-downstream-consumer-boundary.out +make nixie 2>&1 | tee /tmp/nixie-weaver-12-1-1-track-downstream-consumer-boundary.out +git add docs/adr-007-agent-native-command-surface.md +git commit -m "Add OrthoConfig boundary classification section to ADR 007" +``` + +### Stage B commands + +```sh +${EDITOR:-vi} docs/orthoconfig-consumer-boundary.toml +${EDITOR:-vi} crates/weaver-docs-gate/Cargo.toml +${EDITOR:-vi} crates/weaver-docs-gate/src/lib.rs +${EDITOR:-vi} Cargo.toml # add the new workspace member +cargo build --workspace 2>&1 | tee /tmp/build-weaver-12-1-1-track-downstream-consumer-boundary.out +# Regenerate the matrix using the renderer exposed through cargo run --example. +cargo run -p weaver-docs-gate --example render_boundary_matrix \ + -- docs/orthoconfig-consumer-boundary.toml \ + docs/orthoconfig-consumer-boundary.md +make markdownlint 2>&1 | tee /tmp/markdownlint-weaver-12-1-1-track-downstream-consumer-boundary.out +git add crates/weaver-docs-gate docs/orthoconfig-consumer-boundary.toml \ + docs/orthoconfig-consumer-boundary.md Cargo.toml +git commit -m "Add OrthoConfig consumer boundary manifest, crate, and matrix" +``` + +The renderer is exposed through `cargo run --example` rather than a +`[[bin]]` entry; the example shares the same parser the test calls so +the test cannot rot behind the generator. + +### Stage C commands + +```sh +${EDITOR:-vi} docs/roadmap.md +${EDITOR:-vi} docs/developers-guide.md +${EDITOR:-vi} docs/contents.md +make markdownlint 2>&1 | tee /tmp/markdownlint-weaver-12-1-1-track-downstream-consumer-boundary.out +git add docs/roadmap.md docs/developers-guide.md docs/contents.md +git commit -m "Cross-link the OrthoConfig consumer boundary matrix" +``` + +### Stage D commands + +```sh +${EDITOR:-vi} crates/weaver-docs-gate/Cargo.toml +${EDITOR:-vi} crates/weaver-docs-gate/tests/boundary_gate.rs +make check-fmt 2>&1 | tee /tmp/check-fmt-weaver-12-1-1-track-downstream-consumer-boundary.out +make lint 2>&1 | tee /tmp/lint-weaver-12-1-1-track-downstream-consumer-boundary.out +make test 2>&1 | tee /tmp/test-weaver-12-1-1-track-downstream-consumer-boundary.out +git add crates/weaver-docs-gate +git commit -m "Gate the OrthoConfig boundary matrix with a referential-integrity test" +``` + +### Stage E commands + +```sh +${EDITOR:-vi} docs/users-guide.md +make check-fmt 2>&1 | tee /tmp/check-fmt-weaver-12-1-1-track-downstream-consumer-boundary.out +make lint 2>&1 | tee /tmp/lint-weaver-12-1-1-track-downstream-consumer-boundary.out +make test 2>&1 | tee /tmp/test-weaver-12-1-1-track-downstream-consumer-boundary.out +make markdownlint 2>&1 | tee /tmp/markdownlint-weaver-12-1-1-track-downstream-consumer-boundary.out +make nixie 2>&1 | tee /tmp/nixie-weaver-12-1-1-track-downstream-consumer-boundary.out +coderabbit review --agent +git add docs/users-guide.md +git commit -m "Note OrthoConfig boundary in the user's guide" +``` + +After Stage E, mark `12.1.1` complete in `docs/roadmap.md` in a final +commit. + +## Validation and acceptance + +Quality criteria define what "done" means. + +- **Behaviour.** A contributor adding a new command-contract roadmap task + without a manifest row sees `cargo test --workspace` fail with a + `boundary-gate: registry-symmetry` diagnostic naming the missing task. + A `wraps` row without a removal gate, a `consumes` row without + `shipped_in`, a `pending` row whose `next_review_by` is more than 270 + days in the past, or a `divergent` row without an ADR 007 anchor each + fail the same test with the matching diagnostic shape from Stage D. + The generated matrix Markdown matches the committed copy. +- **Tests.** `cargo test --workspace` passes including the new + `boundary_gate` integration test in `crates/weaver-docs-gate`. The + test fails for the expected reason before Stage B is in place (Red), + passes after Stage B and Stage D land (Green), and the matrix + regeneration is idempotent (Refactor). +- **Lint and typecheck.** `make check-fmt`, `make lint`, `make markdownlint`, + and `make nixie` succeed. New code adds no `#[allow]` or `#[expect]` + attributes beyond what existing files use. +- **Documentation.** `docs/contents.md` lists the new boundary matrix; + `docs/users-guide.md` mentions provisional contracts where users may + notice them; `docs/developers-guide.md` carries the contributor + workflow; ADR 007 carries the boundary classification section. +- **CodeRabbit.** `coderabbit review --agent` raises no outstanding + concerns at completion. + +### Red-Green-Refactor evidence + +- **Red.** Before Stage B and Stage D land, + `cargo test -p weaver-docs-gate --test boundary_gate` fails with the + exact `boundary-gate: registry-symmetry` diagnostic shape shown in + Stage D. Capture the failing transcript in `Progress` under Stage D. +- **Green.** After Stage B (manifest, generator) and Stage D (test gate) + land, the same command passes. Capture the passing transcript. +- **Refactor.** Re-render the matrix with the `cargo run --example` + invocation in Stage B and rerun the test; both remain green. Capture + the output. + +## Interfaces and dependencies + +The Rust helper carries a deliberately small public surface. + +```rust +//! Tooling for the OrthoConfig consumer boundary matrix. + +use camino::Utf8Path; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BoundaryState { + Consumes, + Wraps, + Pending, + Divergent, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UpstreamRole { + Boundary, + Metadata, + CapabilityProvenance, + Vocabulary, + Renderer, + Profile, + Delivery, + Feedback, + ExecutionLedger, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UpstreamRef { + pub task: String, + pub role: UpstreamRole, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BoundaryTask { + pub id: String, + pub gist: String, + pub state: BoundaryState, + pub upstream: Vec, + pub shipped_in: Option, + pub removal_gate: Option, + pub adr_anchor: Option, + pub next_review_by: Option, + pub last_reviewed: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BoundaryManifest { + pub schema_version: u32, + pub managed_tasks: Vec, + pub tasks: Vec, +} + +#[derive(Debug, thiserror::Error)] +pub enum BoundaryError { + #[error("manifest file not found: {0}")] + NotFound(camino::Utf8PathBuf), + #[error("invalid TOML: {0}")] + InvalidToml(String), + #[error("invalid manifest row: {0}")] + InvalidRow(String), + #[error("unknown upstream role: {0}")] + UnknownRole(String), +} + +pub fn load_manifest(path: &Utf8Path) -> Result; +pub fn render_matrix(manifest: &BoundaryManifest) -> String; +``` + +The renderer takes no global state, accepts a parsed manifest, and +returns a `String`. The test gate calls `load_manifest` and +`render_matrix` and compares the rendered output against the committed +matrix Markdown byte for byte. + +The integration test depends only on existing workspace dependencies +(`rstest`, `pretty_assertions`, `googletest`, `toml`, `camino`, +`thiserror`). It does not use `insta` or `rstest-bdd`. + +## Boundary classification diagram + +A high-level view of the boundary states and their evidence: + +```mermaid +flowchart TD + Task[Command-contract roadmap task] + Task --> Classify{Boundary state} + Classify -- consumes --> Shipped[shipped_in: OrthoConfig release] + Classify -- wraps --> Gate[removal_gate text] + Classify -- pending --> Review[next_review_by date] + Classify -- divergent --> Anchor[ADR 007 anchor] + Shipped --> Matrix[docs/orthoconfig-consumer-boundary.md] + Gate --> Matrix + Review --> Matrix + Anchor --> Matrix + Matrix --> Test[weaver-docs-gate boundary_gate] + Test --> CI[make test] +``` + +The diagram captures the steady state once the matrix is in place. Each +new command-contract roadmap task adds a row to the manifest; the test +gate stops a commit that omits the row or carries the wrong evidence. + +## Idempotence and recovery + +- Re-running the matrix generator must produce byte-identical output. The + generator does not depend on wall-clock time. The `last_reviewed` field + is set explicitly per row by the contributor when they touch the row. +- Re-running the test gate is safe: the test reads the manifest, the + roadmap, the ADR, and the matrix, and asserts equality. No filesystem + mutation occurs. +- If the manifest and matrix drift, the test fails and the contributor + regenerates the matrix from the manifest. The manifest stays + authoritative. +- A partially completed plan stays in `Progress` so the next contributor + resumes from the last completed stage. Each stage's commit is reversible + by `git revert`. + +## Artefacts and notes + +The plan's working artefacts live under `/tmp` per `AGENTS.md`. They are +captured during execution and referenced from `Progress` once each stage +completes. Examples (filled in as work proceeds): + +```plaintext +/tmp/check-fmt-weaver-12-1-1-track-downstream-consumer-boundary.out +/tmp/lint-weaver-12-1-1-track-downstream-consumer-boundary.out +/tmp/test-weaver-12-1-1-track-downstream-consumer-boundary.out +/tmp/markdownlint-weaver-12-1-1-track-downstream-consumer-boundary.out +/tmp/nixie-weaver-12-1-1-track-downstream-consumer-boundary.out +``` + +## Revision note + +2026-06-07. Revised after a Logisphere pre-implementation design review. +Changes applied: (1) host crate moved from `weaver-build-util` to a new +`weaver-docs-gate` workspace member; (2) classification vocabulary +expanded to four states (`consumes`, `wraps`, `pending`, `divergent`) +to absorb "upstream contract not yet decided" cleanly; (3) the +`shipped: bool` flag replaced with a `shipped_in: Option` field +carrying the OrthoConfig release tag; (4) the topic-keyword candidate +heuristic replaced with an explicit `managed_tasks` registry that fails +closed when a new ID is missing; (5) the test stack pruned (dropped the +`rstest-bdd` feature file and the `insta` snapshot in favour of one +byte-for-byte equality check); (6) the test gate's failure-message +shape pinned with concrete examples; (7) staleness rules added +(`last_reviewed` warn-at-270-days, `pending.next_review_by` +fail-at-270-days); (8) the `role` field tightened into a closed enum; +(9) the developers' guide adds a quarterly cross-repo reconciliation +ritual. The remaining unresolved alternative (inline roadmap +annotations) is recorded in the Decision Log as deferred. + +## Relevant skills and documentation + +The contributor implementing this plan should load the following skills +and consult the following documents. + +Skills: + +- `execplans` — to keep this plan a living document. +- `hexagonal-architecture` — to interpret the boundary as a dependency-rule + artefact, not a runtime port. +- `arch-decision-records` — to amend ADR 007 cleanly when a `divergent` + row needs an anchor. +- `rust-unit-testing` — for the `rstest` parameterization and the + `pretty_assertions`/`googletest` shape of the gate. The plan + intentionally does *not* use `rstest-bdd` or `insta` because their + scenarios and snapshots would duplicate the integration test's + invariants. +- `arch-crate-design` — to keep `crates/weaver-docs-gate` minimal and + honest about its single test-fixture purpose. +- `commit-message` and `pr-creation` — for the final commits and the draft + pull request. +- `en-gb-oxendict` — for the prose updates. + +Documents: + +- `docs/roadmap.md` +- `docs/adr-007-agent-native-command-surface.md` +- `docs/documentation-style-guide.md` +- `docs/developers-guide.md` +- `docs/users-guide.md` +- `docs/rust-testing-with-rstest-fixtures.md` +- `docs/rust-doctest-dry-guide.md` +- `docs/reliable-testing-in-rust-via-dependency-injection.md` +- `docs/complexity-antipatterns-and-refactoring-strategies.md` +- `docs/ortho-config-users-guide.md` +- `docs/rstest-bdd-users-guide.md` + +Upstream references (read-only context): + +- OrthoConfig roadmap, task 5.2.3 (in the OrthoConfig repository). +- OrthoConfig `docs/agent-native-cli-design.md` §2.1. +- OrthoConfig `docs/adr-003-define-schema-ownership-for-agent-native-contracts.md`. + +Prior art consulted for the gate design: + +- The `cargo-vet` audit-criteria pattern: a per-row state with a + configurable vocabulary, gated at CI time. +- The OpenAPI Generator per-generator support matrix: a three-state + classification rendered into a single discoverable table. +- The `clap_mangen` + `clap_complete` + `trycmd` drift pattern: a + CI-enforced gate that asserts each generated artefact matches the + manifest. +- The Strangler-Fig retirement pattern (Martin Fowler): an ADR + superseded-by link is the right home for a `divergent` row that + later becomes `consumes`. From b98785226b47da92e1685e5ef1910f882e99de3e Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 14 Jun 2026 05:03:37 +0200 Subject: [PATCH 02/22] Document OrthoConfig boundary states Add the ADR 007 boundary classification section with the fixed state vocabulary and matrix column contract for downstream command-contract tasks. Mark the 12.1.1 ExecPlan as active and record Stage A validation, CodeRabbit review, and the temporary OrthoConfig SHA pin decision. --- docs/adr-007-agent-native-command-surface.md | 44 +++++++++++++++++++ ...-1-1-track-downstream-consumer-boundary.md | 22 +++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/docs/adr-007-agent-native-command-surface.md b/docs/adr-007-agent-native-command-surface.md index 62f98838..732f56c2 100644 --- a/docs/adr-007-agent-native-command-surface.md +++ b/docs/adr-007-agent-native-command-surface.md @@ -146,6 +146,50 @@ Any local helper that survives after those OrthoConfig tasks are available must record a permanent divergence in this ADR before it can remain in the implementation. +## Boundary classification + +Every live roadmap task that touches the command contract must be classified in +the OrthoConfig consumer boundary matrix at +[`docs/orthoconfig-consumer-boundary.md`](orthoconfig-consumer-boundary.md). +The machine-readable source of truth is +[`docs/orthoconfig-consumer-boundary.toml`](orthoconfig-consumer-boundary.toml). +The matrix complements the dependency table and removal policy above; it does +not replace either one. + +The matrix uses these columns: + +- `Roadmap task` links to the Weaver roadmap task being classified. +- `Gist` summarizes the task in one reviewable sentence. +- `State` is one of `consumes`, `wraps`, `pending`, or `divergent`. +- `Upstream OrthoConfig task` names the upstream task IDs the Weaver task + depends on. +- `Shipped in` names the OrthoConfig release tag or pinned commit SHA that + landed the contract for `consumes` rows. +- `Removal gate or divergence` records either the replacement condition for a + temporary wrapper or the ADR 007 section that owns a deliberate divergence. +- `Next review by` records the next review date for unresolved upstream + contracts. +- `Last reviewed` records the last date the row was checked. + +The four boundary states have fixed evidence requirements: + +- `consumes` means Weaver follows an OrthoConfig contract that has already + shipped. The row must name the upstream task and the OrthoConfig release tag + or pinned commit SHA in `Shipped in`. +- `wraps` means Weaver keeps a narrow temporary adapter while waiting for an + upstream contract whose shape is already committed. The row must name the + upstream task and the removal gate. +- `pending` means Weaver depends on an upstream contract whose shape has not + yet been decided. The row must name the upstream task and a `Next review by` + date. This state exists so `wraps` remains reserved for contracts whose + upstream shape is already committed. +- `divergent` means Weaver deliberately keeps a different contract. The row + must point to the ADR 007 section that explains the divergence. + +The matrix may display the states with symbols for scanning, but the textual +state is the contract. Reviewers should treat the TOML row as the authority and +the Markdown matrix as generated documentation. + ## Human renderer contract The default renderer is for humans. It emits localized, readable output with diff --git a/docs/execplans/12-1-1-track-downstream-consumer-boundary.md b/docs/execplans/12-1-1-track-downstream-consumer-boundary.md index d29493b8..ecd298f5 100644 --- a/docs/execplans/12-1-1-track-downstream-consumer-boundary.md +++ b/docs/execplans/12-1-1-track-downstream-consumer-boundary.md @@ -5,7 +5,7 @@ This ExecPlan (execution plan) is a living document. The sections `Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work proceeds. -Status: DRAFT +Status: IN PROGRESS This document must be maintained in accordance with `AGENTS.md` at the repository root. @@ -199,7 +199,14 @@ boundary becomes review-only again. Each item carries a UTC timestamp once it completes. The list reflects actual state, not the intended sequence. -- [ ] Stage A: ratify the boundary vocabulary and matrix shape. +- [ ] 2026-06-14T02:44:33Z: Execution approved by the user and started on + branch `12-1-1-track-downstream-consumer-boundary`. +- [x] 2026-06-14T02:44:33Z: Stage A ratified the boundary vocabulary and + matrix columns in ADR 007. Validation passed with + `make markdownlint` (`Summary: 0 error(s)`) and `make nixie` + (`All diagrams validated successfully!`). Logs: + `/tmp/markdownlint-weaver-12-1-1-track-downstream-consumer-boundary.out` + and `/tmp/nixie-weaver-12-1-1-track-downstream-consumer-boundary.out`. - [ ] Stage B: produce the boundary manifest, generator, and matrix doc. - [ ] Stage C: cross-link the matrix from ADR 007, the roadmap, and the developers' guide. @@ -219,6 +226,17 @@ Recorded as they occur during implementation. Format: Recorded for any decision that future work must respect. +- Decision: execute this plan despite the document's previous draft status. + Rationale: the user explicitly requested implementation of this plan on + 2026-06-14, satisfying the ExecPlan approval gate. Date/Author: + 2026-06-14, implementation. +- Decision: pin `ortho_config` to commit + `4339a6f3c61dc4fed86493d99ffb05230bee2a1b` until the project agrees an + OrthoConfig v0.9.0 release. + Rationale: the user requested a pinned SHA rather than a release version for + now; `git ls-remote https://github.com/leynos/ortho-config.git HEAD` + resolved that revision for the current upstream main branch. Date/Author: + 2026-06-14, implementation. - Decision: classify boundary state with a closed four-value vocabulary (`consumes`, `wraps`, `pending`, `divergent`) rather than free-form prose. Rationale: the three-value vocabulary (consumes/wraps/divergent) borrowed From 76af1171df85345a20b5e3b18c589470a9638f66 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 14 Jun 2026 06:04:40 +0200 Subject: [PATCH 03/22] Add OrthoConfig consumer boundary matrix Add a private docs-gate crate that parses the consumer-boundary manifest and renders the committed Markdown matrix from it. Pin `ortho_config` to the requested upstream SHA until the project settles an OrthoConfig v0.9.0 release, and record the Stage B gate and CodeRabbit evidence in the ExecPlan. --- Cargo.lock | 20 +- Cargo.toml | 4 +- crates/weaver-docs-gate/Cargo.toml | 16 + .../examples/render_boundary_matrix.rs | 38 ++ crates/weaver-docs-gate/src/lib.rs | 234 +++++++ crates/weaver-docs-gate/src/renderer.rs | 281 ++++++++ ...-1-1-track-downstream-consumer-boundary.md | 32 +- docs/orthoconfig-consumer-boundary.md | 106 +++ docs/orthoconfig-consumer-boundary.toml | 622 ++++++++++++++++++ 9 files changed, 1346 insertions(+), 7 deletions(-) create mode 100644 crates/weaver-docs-gate/Cargo.toml create mode 100644 crates/weaver-docs-gate/examples/render_boundary_matrix.rs create mode 100644 crates/weaver-docs-gate/src/lib.rs create mode 100644 crates/weaver-docs-gate/src/renderer.rs create mode 100644 docs/orthoconfig-consumer-boundary.md create mode 100644 docs/orthoconfig-consumer-boundary.toml diff --git a/Cargo.lock b/Cargo.lock index 1a97cfd1..1abcd37f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -299,6 +299,7 @@ version = "4.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7281235d6e96d3544ca18bba9049be92f4190f8d923e3caef1b5f66cfa752608" dependencies = [ + "camino", "cap-primitives 4.0.2", "io-extras 0.19.0", "io-lifetimes 3.0.1", @@ -1462,11 +1463,10 @@ dependencies = [ [[package]] name = "ortho_config" version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9314c7a4be184287f3f9a66fcbcec054ac215d4975152df86b7aadeae8d5e019" +source = "git+https://github.com/leynos/ortho-config.git?rev=4339a6f3c61dc4fed86493d99ffb05230bee2a1b#4339a6f3c61dc4fed86493d99ffb05230bee2a1b" dependencies = [ "camino", - "cap-std 3.4.5", + "cap-std 4.0.2", "clap", "clap-dispatch", "directories", @@ -1489,8 +1489,7 @@ dependencies = [ [[package]] name = "ortho_config_macros" version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3941ab7c592a0b93aadf12ce30e0ef3d01af87b46c8f363d8157dc33273ba83" +source = "git+https://github.com/leynos/ortho-config.git?rev=4339a6f3c61dc4fed86493d99ffb05230bee2a1b#4339a6f3c61dc4fed86493d99ffb05230bee2a1b" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -3253,6 +3252,17 @@ dependencies = [ "serde", ] +[[package]] +name = "weaver-docs-gate" +version = "0.1.0" +dependencies = [ + "camino", + "cap-std 4.0.2", + "serde", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", +] + [[package]] name = "weaver-e2e" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 9966f22a..5e6794ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ members = [ "crates/sempai-yaml", "crates/sempai", "crates/weaver-cards", + "crates/weaver-docs-gate", ] resolver = "2" @@ -38,7 +39,7 @@ lru = "0.12" lsp-types = "0.97" mockall = "0.14.0" once_cell = "1.19" -ortho_config = "0.8.0" +ortho_config = { git = "https://github.com/leynos/ortho-config.git", rev = "4339a6f3c61dc4fed86493d99ffb05230bee2a1b" } predicates = "3.1" proptest = "1.5" rstest = "0.26.1" @@ -53,6 +54,7 @@ saphyr = "0.0.6" tempfile = "3.10" thiserror = "2.0" time = { version = "0.3", features = ["formatting"] } +toml = "0.9.12" tracing = "0.1" tree-sitter = "0.25.10" tree-sitter-python = "0.25.0" diff --git a/crates/weaver-docs-gate/Cargo.toml b/crates/weaver-docs-gate/Cargo.toml new file mode 100644 index 00000000..b83081cf --- /dev/null +++ b/crates/weaver-docs-gate/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "weaver-docs-gate" +edition.workspace = true +version.workspace = true +rust-version.workspace = true +publish = false + +[dependencies] +cap-std = { workspace = true } +camino = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } +toml = { workspace = true } + +[lints] +workspace = true diff --git a/crates/weaver-docs-gate/examples/render_boundary_matrix.rs b/crates/weaver-docs-gate/examples/render_boundary_matrix.rs new file mode 100644 index 00000000..c439e729 --- /dev/null +++ b/crates/weaver-docs-gate/examples/render_boundary_matrix.rs @@ -0,0 +1,38 @@ +//! Regenerate the `OrthoConfig` consumer boundary matrix. + +use std::env; + +use camino::Utf8Path; +use cap_std::{ambient_authority, fs::Dir}; +use weaver_docs_gate::{load_manifest, render_matrix}; + +fn main() -> Result<(), String> { + let mut args = env::args().skip(1); + let manifest_path = args + .next() + .ok_or_else(|| "usage: render_boundary_matrix ".to_owned())?; + let output_path = args + .next() + .ok_or_else(|| "usage: render_boundary_matrix ".to_owned())?; + + if args.next().is_some() { + return Err("usage: render_boundary_matrix ".to_owned()); + } + + let manifest = + load_manifest(Utf8Path::new(&manifest_path)).map_err(|error| error.to_string())?; + write_output(Utf8Path::new(&output_path), render_matrix(&manifest)) +} + +fn write_output(path: &Utf8Path, content: String) -> Result<(), String> { + let parent = path + .parent() + .ok_or_else(|| format!("invalid output path: {path}"))?; + let file_name = path + .file_name() + .ok_or_else(|| format!("invalid output path: {path}"))?; + let dir = + Dir::open_ambient_dir(parent, ambient_authority()).map_err(|error| error.to_string())?; + dir.write(file_name, content) + .map_err(|error| error.to_string()) +} diff --git a/crates/weaver-docs-gate/src/lib.rs b/crates/weaver-docs-gate/src/lib.rs new file mode 100644 index 00000000..6575f5d8 --- /dev/null +++ b/crates/weaver-docs-gate/src/lib.rs @@ -0,0 +1,234 @@ +//! Tooling for the `OrthoConfig` consumer boundary matrix. + +use std::io::{self, ErrorKind}; + +use camino::{Utf8Path, Utf8PathBuf}; +use cap_std::{ambient_authority, fs::Dir}; +use serde::{Deserialize, Deserializer}; + +mod renderer; +pub use renderer::render_matrix; + +/// One boundary classification state for a Weaver roadmap task. +/// +/// # Examples +/// ``` +/// use weaver_docs_gate::BoundaryState; +/// +/// assert_eq!(BoundaryState::Wraps.as_str(), "wraps"); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum BoundaryState { + /// Weaver follows an `OrthoConfig` contract that has shipped. + Consumes, + /// Weaver uses a temporary local adapter with a removal gate. + Wraps, + /// Weaver waits for an upstream contract whose shape is undecided. + Pending, + /// Weaver deliberately keeps a different contract. + Divergent, +} + +impl BoundaryState { + /// Return the manifest spelling for the state. + /// + /// # Examples + /// ``` + /// use weaver_docs_gate::BoundaryState; + /// + /// assert_eq!(BoundaryState::Consumes.as_str(), "consumes"); + /// ``` + #[must_use] + pub const fn as_str(self) -> &'static str { + match self { + Self::Consumes => "consumes", + Self::Wraps => "wraps", + Self::Pending => "pending", + Self::Divergent => "divergent", + } + } +} + +/// The upstream `OrthoConfig` role that a Weaver task consumes or waits for. +/// +/// # Examples +/// ``` +/// use weaver_docs_gate::UpstreamRole; +/// +/// assert_eq!(UpstreamRole::Renderer.as_str(), "renderer"); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum UpstreamRole { + /// Consumer-boundary ownership and governance. + Boundary, + /// Recursive command metadata. + Metadata, + /// Capability and provider provenance metadata. + CapabilityProvenance, + /// Canonical command vocabulary. + Vocabulary, + /// Human and machine renderer contracts. + Renderer, + /// Profile parsing, precedence, and redaction. + Profile, + /// Delivery sink contracts. + Delivery, + /// Feedback command contracts. + Feedback, + /// Execution ledger contracts. + ExecutionLedger, +} + +impl UpstreamRole { + /// Return the manifest spelling for the role. + /// + /// # Examples + /// ``` + /// use weaver_docs_gate::UpstreamRole; + /// + /// assert_eq!(UpstreamRole::Boundary.as_str(), "boundary"); + /// ``` + #[must_use] + pub const fn as_str(self) -> &'static str { + match self { + Self::Boundary => "boundary", + Self::Metadata => "metadata", + Self::CapabilityProvenance => "capability_provenance", + Self::Vocabulary => "vocabulary", + Self::Renderer => "renderer", + Self::Profile => "profile", + Self::Delivery => "delivery", + Self::Feedback => "feedback", + Self::ExecutionLedger => "execution_ledger", + } + } +} + +/// A single upstream `OrthoConfig` task reference. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +pub struct UpstreamRef { + /// The upstream roadmap task or stable design section. + pub task: String, + /// The role that upstream reference plays for the Weaver task. + pub role: UpstreamRole, +} + +/// One classified Weaver roadmap task. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +pub struct BoundaryTask { + /// Weaver roadmap task ID, such as `13.1.2`. + pub id: String, + /// One-sentence task summary. + pub gist: String, + /// Boundary classification state. + pub state: BoundaryState, + /// Upstream `OrthoConfig` task references. + pub upstream: Vec, + /// `OrthoConfig` release tag or pinned SHA for shipped contracts. + #[serde(deserialize_with = "empty_string_as_none")] + pub shipped_in: Option, + /// Replacement condition for temporary wrappers. + #[serde(deserialize_with = "empty_string_as_none")] + pub removal_gate: Option, + /// ADR 007 heading slug for deliberate divergences. + #[serde(deserialize_with = "empty_string_as_none")] + pub adr_anchor: Option, + /// ISO-8601 review date for pending contracts. + #[serde(deserialize_with = "empty_string_as_none")] + pub next_review_by: Option, + /// ISO-8601 date when the row was last reviewed. + pub last_reviewed: String, +} + +/// The complete boundary manifest. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +pub struct BoundaryManifest { + /// Manifest schema version. + pub schema_version: u32, + /// Ordered registry of Weaver roadmap task IDs governed by the matrix. + pub managed_tasks: Vec, + /// Classified task rows. + #[serde(rename = "task")] + pub tasks: Vec, +} + +/// Errors returned while loading the boundary manifest. +#[derive(Debug, thiserror::Error)] +pub enum BoundaryError { + /// The manifest path does not exist. + #[error("manifest file not found: {0}")] + NotFound(Utf8PathBuf), + /// The manifest path cannot be opened through a parent directory handle. + #[error("invalid manifest path: {0}")] + InvalidPath(Utf8PathBuf), + /// The file could not be read. + #[error("manifest file could not be read: {path}: {source}")] + Read { + /// Manifest path. + path: Utf8PathBuf, + /// Underlying I/O error. + source: Box, + }, + /// The file is not valid TOML for the boundary schema. + #[error("invalid TOML in {path}: {source}")] + InvalidToml { + /// Manifest path. + path: Utf8PathBuf, + /// TOML parse error. + source: Box, + }, +} + +/// Load the boundary manifest from disk. +/// +/// # Errors +/// +/// Returns [`BoundaryError`] when the manifest is missing, unreadable, or not +/// valid TOML for the boundary schema. +/// +/// # Examples +/// ```no_run +/// use camino::Utf8Path; +/// use weaver_docs_gate::load_manifest; +/// +/// let manifest = load_manifest(Utf8Path::new("docs/orthoconfig-consumer-boundary.toml"))?; +/// assert_eq!(manifest.schema_version, 1); +/// # Ok::<(), weaver_docs_gate::BoundaryError>(()) +/// ``` +pub fn load_manifest(path: &Utf8Path) -> Result { + let parent = path.parent().unwrap_or_else(|| Utf8Path::new(".")); + let file_name = path + .file_name() + .ok_or_else(|| BoundaryError::InvalidPath(path.to_path_buf()))?; + let dir = Dir::open_ambient_dir(parent, ambient_authority()) + .map_err(|source| read_error(path, source))?; + + let contents = dir + .read_to_string(file_name) + .map_err(|source| read_error(path, source))?; + toml::from_str(&contents).map_err(|source| BoundaryError::InvalidToml { + path: path.to_path_buf(), + source: Box::new(source), + }) +} + +fn read_error(path: &Utf8Path, source: io::Error) -> BoundaryError { + if source.kind() == ErrorKind::NotFound { + BoundaryError::NotFound(path.to_path_buf()) + } else { + BoundaryError::Read { + path: path.to_path_buf(), + source: Box::new(source), + } + } +} + +fn empty_string_as_none<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let value = String::deserialize(deserializer)?; + Ok((!value.is_empty()).then_some(value)) +} diff --git a/crates/weaver-docs-gate/src/renderer.rs b/crates/weaver-docs-gate/src/renderer.rs new file mode 100644 index 00000000..609214e9 --- /dev/null +++ b/crates/weaver-docs-gate/src/renderer.rs @@ -0,0 +1,281 @@ +//! Markdown rendering for the `OrthoConfig` consumer boundary matrix. + +use crate::{BoundaryManifest, BoundaryState, BoundaryTask}; + +const NA: &str = "n/a"; + +struct MatrixRow { + roadmap_task: String, + gist: String, + state: String, + upstream: String, + shipped_in: String, + gate_or_divergence: String, + next_review_by: String, + last_reviewed: String, +} + +struct ColumnWidths { + roadmap_task: usize, + gist: usize, + state: usize, + upstream: usize, + shipped_in: usize, + gate_or_divergence: usize, + next_review_by: usize, + last_reviewed: usize, +} + +impl ColumnWidths { + fn for_rows(rows: &[MatrixRow]) -> Self { + let mut widths = Self::headers(); + for row in rows { + widths.include(row); + } + widths + } + + const fn headers() -> Self { + Self { + roadmap_task: 12, + gist: 4, + state: 5, + upstream: 25, + shipped_in: 10, + gate_or_divergence: 27, + next_review_by: 14, + last_reviewed: 13, + } + } + + fn include(&mut self, row: &MatrixRow) { + self.roadmap_task = self.roadmap_task.max(cell_width(&row.roadmap_task)); + self.gist = self.gist.max(cell_width(&row.gist)); + self.state = self.state.max(cell_width(&row.state)); + self.upstream = self.upstream.max(cell_width(&row.upstream)); + self.shipped_in = self.shipped_in.max(cell_width(&row.shipped_in)); + self.gate_or_divergence = self + .gate_or_divergence + .max(cell_width(&row.gate_or_divergence)); + self.next_review_by = self.next_review_by.max(cell_width(&row.next_review_by)); + self.last_reviewed = self.last_reviewed.max(cell_width(&row.last_reviewed)); + } +} + +/// Render the human-readable boundary matrix from the manifest. +/// +/// # Examples +/// ``` +/// use weaver_docs_gate::{BoundaryManifest, BoundaryState, BoundaryTask, render_matrix}; +/// +/// let manifest = BoundaryManifest { +/// schema_version: 1, +/// managed_tasks: vec!["12.1.1".into()], +/// tasks: vec![BoundaryTask { +/// id: "12.1.1".into(), +/// gist: "Track the downstream consumer boundary.".into(), +/// state: BoundaryState::Consumes, +/// upstream: Vec::new(), +/// shipped_in: Some("4339a6f3".into()), +/// removal_gate: None, +/// adr_anchor: None, +/// next_review_by: None, +/// last_reviewed: "2026-06-14".into(), +/// }], +/// }; +/// +/// assert!(render_matrix(&manifest).contains("12.1.1")); +/// ``` +#[must_use] +pub fn render_matrix(manifest: &BoundaryManifest) -> String { + let mut rendered = String::from(concat!( + "# OrthoConfig consumer boundary\n\n", + "\n\n", + "This matrix is generated from ", + "`docs/orthoconfig-consumer-boundary.toml`. Do not edit the table\n", + "by hand; update the manifest and regenerate it with `cargo run -p\n", + "weaver-docs-gate --example render_boundary_matrix --\n", + "docs/orthoconfig-consumer-boundary.toml ", + "docs/orthoconfig-consumer-boundary.md`.\n\n", + "The matrix tracks every live Weaver command-contract roadmap task ", + "that consumes\n", + "OrthoConfig, wraps it temporarily, waits on upstream shape, or ", + "deliberately\n", + "diverges under ADR 007.\n\n", + )); + + for phase in grouped_phases(manifest) { + push_phase(&mut rendered, phase, manifest); + } + + while rendered.ends_with("\n\n") { + rendered.pop(); + } + rendered.push_str("\n"); + + rendered +} + +fn grouped_phases(manifest: &BoundaryManifest) -> Vec<&str> { + let mut phases = Vec::new(); + for task in &manifest.tasks { + let phase = task.id.split('.').next().unwrap_or_default(); + if !phases.contains(&phase) { + phases.push(phase); + } + } + phases +} + +fn push_phase(rendered: &mut String, phase: &str, manifest: &BoundaryManifest) { + let rows = manifest + .tasks + .iter() + .filter(|task| task.id.starts_with(phase)) + .map(MatrixRow::from) + .collect::>(); + let widths = ColumnWidths::for_rows(&rows); + + rendered.push_str("## Phase "); + rendered.push_str(phase); + rendered.push_str("\n\n"); + push_header(rendered, &widths); + push_separator(rendered, &widths); + for row in &rows { + push_row(rendered, row, &widths); + } + rendered.push('\n'); +} + +fn push_header(rendered: &mut String, widths: &ColumnWidths) { + push_cells( + rendered, + widths, + &MatrixRow { + roadmap_task: "Roadmap task".into(), + gist: "Gist".into(), + state: "State".into(), + upstream: "Upstream OrthoConfig task".into(), + shipped_in: "Shipped in".into(), + gate_or_divergence: "Removal gate or divergence".into(), + next_review_by: "Next review by".into(), + last_reviewed: "Last reviewed".into(), + }, + ); +} + +fn push_separator(rendered: &mut String, widths: &ColumnWidths) { + push_cells( + rendered, + widths, + &MatrixRow { + roadmap_task: "-".repeat(widths.roadmap_task), + gist: "-".repeat(widths.gist), + state: "-".repeat(widths.state), + upstream: "-".repeat(widths.upstream), + shipped_in: "-".repeat(widths.shipped_in), + gate_or_divergence: "-".repeat(widths.gate_or_divergence), + next_review_by: "-".repeat(widths.next_review_by), + last_reviewed: "-".repeat(widths.last_reviewed), + }, + ); +} + +fn push_row(rendered: &mut String, row: &MatrixRow, widths: &ColumnWidths) { + push_cells(rendered, widths, row); +} + +fn push_cells(rendered: &mut String, widths: &ColumnWidths, row: &MatrixRow) { + rendered.push_str("| "); + rendered.push_str(&padded(&row.roadmap_task, widths.roadmap_task)); + rendered.push_str(" | "); + rendered.push_str(&padded(&row.gist, widths.gist)); + rendered.push_str(" | "); + rendered.push_str(&padded(&row.state, widths.state)); + rendered.push_str(" | "); + rendered.push_str(&padded(&row.upstream, widths.upstream)); + rendered.push_str(" | "); + rendered.push_str(&padded(&row.shipped_in, widths.shipped_in)); + rendered.push_str(" | "); + rendered.push_str(&padded(&row.gate_or_divergence, widths.gate_or_divergence)); + rendered.push_str(" | "); + rendered.push_str(&padded(&row.next_review_by, widths.next_review_by)); + rendered.push_str(" | "); + rendered.push_str(&padded(&row.last_reviewed, widths.last_reviewed)); + rendered.push_str(" |\n"); +} + +impl From<&BoundaryTask> for MatrixRow { + fn from(task: &BoundaryTask) -> Self { + Self { + roadmap_task: format!("[{}]({})", escape_cell(&task.id), roadmap_anchor(&task.id)), + gist: escape_cell(&task.gist), + state: state_label(task.state).into(), + upstream: upstream_tasks(task), + shipped_in: optional_cell(task.shipped_in.as_deref()), + gate_or_divergence: gate_or_divergence(task), + next_review_by: optional_cell(task.next_review_by.as_deref()), + last_reviewed: escape_cell(&task.last_reviewed), + } + } +} + +fn upstream_tasks(task: &BoundaryTask) -> String { + let upstream = task + .upstream + .iter() + .map(|reference| reference.task.as_str()) + .collect::>() + .join(", "); + optional_cell(Some(&upstream)) +} + +fn gate_or_divergence(task: &BoundaryTask) -> String { + if let Some(gate) = task.removal_gate.as_deref() { + return escape_cell(gate); + } + + task.adr_anchor.as_deref().map_or_else( + || NA.into(), + |anchor| format!("[ADR 007](adr-007-agent-native-command-surface.md#{anchor})"), + ) +} + +fn optional_cell(value: Option<&str>) -> String { + value + .filter(|inner| !inner.is_empty()) + .map_or_else(|| NA.into(), escape_cell) +} + +const fn state_label(state: BoundaryState) -> &'static str { + match state { + BoundaryState::Consumes => "✓ consumes", + BoundaryState::Wraps => "~ wraps", + BoundaryState::Pending => "? pending", + BoundaryState::Divergent => "× divergent", + } +} + +fn roadmap_anchor(task_id: &str) -> &'static str { + match task_id.split('.').next().unwrap_or_default() { + "12" => "roadmap.md#121-confirm-reusable-contracts-that-weaver-must-not-duplicate", + "13" => "roadmap.md#13-command-contract-proving-slice", + "14" => "roadmap.md#14-code-reading-loop-slice", + "15" => "roadmap.md#15-sempai-selector-to-context-slice", + "16" => "roadmap.md#16-safe-change-loop-slice", + "17" => "roadmap.md#17-impact-and-history-slice", + "18" => "roadmap.md#18-provider-ecosystem-slice", + "19" => "roadmap.md#19-agent-workflow-and-assurance-slice", + "20" => "roadmap.md#20-deferred-extensions-after-the-core-product-promise", + _ => "roadmap.md", + } +} + +fn escape_cell(value: &str) -> String { value.replace('|', "\\|").replace('\n', "
") } + +fn padded(value: &str, width: usize) -> String { + let padding = width.saturating_sub(cell_width(value)); + format!("{value}{}", " ".repeat(padding)) +} + +fn cell_width(value: &str) -> usize { value.chars().count() } diff --git a/docs/execplans/12-1-1-track-downstream-consumer-boundary.md b/docs/execplans/12-1-1-track-downstream-consumer-boundary.md index ecd298f5..f9aa73c0 100644 --- a/docs/execplans/12-1-1-track-downstream-consumer-boundary.md +++ b/docs/execplans/12-1-1-track-downstream-consumer-boundary.md @@ -207,7 +207,17 @@ state, not the intended sequence. (`All diagrams validated successfully!`). Logs: `/tmp/markdownlint-weaver-12-1-1-track-downstream-consumer-boundary.out` and `/tmp/nixie-weaver-12-1-1-track-downstream-consumer-boundary.out`. -- [ ] Stage B: produce the boundary manifest, generator, and matrix doc. +- [x] 2026-06-14T03:28:00Z: Stage B produced + `docs/orthoconfig-consumer-boundary.toml`, generated + `docs/orthoconfig-consumer-boundary.md`, added + `crates/weaver-docs-gate`, and pinned `ortho_config` to + `4339a6f3c61dc4fed86493d99ffb05230bee2a1b`. Validation passed with + `cargo build --workspace`, `make fmt`, `make check-fmt`, `make lint`, + `make test`, `make markdownlint`, and `make nixie`. The first + CodeRabbit pass found one valid generic `OrthoConfig 9.2` reference + issue; after fixing the manifest to use `OrthoConfig 9.2.1` and + `OrthoConfig 9.2.2`, regenerating the matrix, and rerunning gates, the + repeat CodeRabbit review completed with `findings: 0`. - [ ] Stage C: cross-link the matrix from ADR 007, the roadmap, and the developers' guide. - [ ] Stage D: add the referential-integrity test gate and snapshot. @@ -221,6 +231,26 @@ Recorded as they occur during implementation. Format: - Observation: … Evidence: … Impact: … +- Observation: `toml` was already present in `Cargo.lock` but was not a + workspace dependency. + Evidence: repository search showed transitive lockfile entries and no + workspace dependency entry before Stage B. + Impact: Stage B added `toml = "0.9.12"` to `[workspace.dependencies]` so + the private docs-gate crate can use the parser explicitly. +- Observation: `mdtablefix` rewrites generated Markdown tables and can break + rows that contain empty cells. + Evidence: the first `make fmt` attempt failed with `MD056` after wrapping + empty table cells in `docs/orthoconfig-consumer-boundary.md`. + Impact: the renderer now emits formatter-stable aligned tables and displays + absent values as `n/a`; the TOML manifest remains the evidence source for + genuinely empty fields. +- Observation: delivery and feedback OrthoConfig references need versioned + leaf task identifiers in shared upstream lists. + Evidence: `coderabbit review --agent` flagged the `12.1.5` manifest row + while later rows already used `OrthoConfig 9.2.1` and + `OrthoConfig 9.2.2`. + Impact: the manifest now uses the leaf task identifiers consistently before + generated matrix publication. ## Decision log diff --git a/docs/orthoconfig-consumer-boundary.md b/docs/orthoconfig-consumer-boundary.md new file mode 100644 index 00000000..5c9ac46c --- /dev/null +++ b/docs/orthoconfig-consumer-boundary.md @@ -0,0 +1,106 @@ +# OrthoConfig consumer boundary + + + +This matrix is generated from `docs/orthoconfig-consumer-boundary.toml`. Do not +edit the table by hand; update the manifest and regenerate it with +`cargo run -p weaver-docs-gate --example render_boundary_matrix -- docs/orthoconfig-consumer-boundary.toml docs/orthoconfig-consumer-boundary.md`. + +The matrix tracks every live Weaver command-contract roadmap task that consumes +OrthoConfig, wraps it temporarily, waits on upstream shape, or deliberately +diverges under ADR 007. + +## Phase 12 + +| Roadmap task | Gist | State | Upstream OrthoConfig task | Shipped in | Removal gate or divergence | Next review by | Last reviewed | +| ---------------------------------------------------------------------------------- | ------------------------------------------------------ | ---------- | ---------------------------------------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------- | ------------- | +| [12.1.1](roadmap.md#121-confirm-reusable-contracts-that-weaver-must-not-duplicate) | Track the downstream consumer boundary. | ✓ consumes | OrthoConfig 5.2.3 / design §2.1 | 4339a6f3c61dc4fed86493d99ffb05230bee2a1b | n/a | n/a | 2026-06-14 | +| [12.1.2](roadmap.md#121-confirm-reusable-contracts-that-weaver-must-not-duplicate) | Consume recursive command metadata. | ~ wraps | OrthoConfig 6.1 | n/a | Replace local recursive command metadata once OrthoConfig 6.1 ships command and subcommand records that can drive help, manpages, completions, and context output. | n/a | 2026-06-14 | +| [12.1.3](roadmap.md#121-confirm-reusable-contracts-that-weaver-must-not-duplicate) | Consume compact context and skill metadata. | ~ wraps | OrthoConfig 6.2, OrthoConfig 6.3 | n/a | Replace Weaver-local context and skill metadata once OrthoConfig 6.2 and 6.3 ship reusable context and skill shapes that Weaver can extend. | n/a | 2026-06-14 | +| [12.1.4](roadmap.md#121-confirm-reusable-contracts-that-weaver-must-not-duplicate) | Consume canonical vocabulary and behavioural metadata. | ~ wraps | OrthoConfig 7.1, OrthoConfig 7.2, OrthoConfig 8.1 | n/a | Replace Weaver-local vocabulary, renderer, JSON, exit-code, bounded-list, mutation, and provenance checks once OrthoConfig 7.1, 7.2, and 8.1 ship those contracts. | n/a | 2026-06-14 | +| [12.1.5](roadmap.md#121-confirm-reusable-contracts-that-weaver-must-not-duplicate) | Consume compounding primitive contracts. | ? pending | OrthoConfig 9.1, OrthoConfig 9.2.1, OrthoConfig 9.2.2, OrthoConfig 9.3 | n/a | n/a | 2026-12-31 | 2026-06-14 | + +## Phase 13 + +| Roadmap task | Gist | State | Upstream OrthoConfig task | Shipped in | Removal gate or divergence | Next review by | Last reviewed | +| ------------------------------------------------------ | ------------------------------------------------------------------------------ | ----------- | ------------------------------------------------------------------------ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -------------- | ------------- | +| [13.1.1](roadmap.md#13-command-contract-proving-slice) | Record the agent-native command-surface reset as ADR 007. | × divergent | OrthoConfig 5.2.3 | n/a | [ADR 007](adr-007-agent-native-command-surface.md#decision-outcome) | n/a | 2026-06-14 | +| [13.1.2](roadmap.md#13-command-contract-proving-slice) | Implement the Weaver command-surface adapter for one read-only command family. | ~ wraps | OrthoConfig 5.2.3, OrthoConfig 6.1, OrthoConfig 7.2.7 | n/a | Replace CommandSurfaceRecord once OrthoConfig 6.1 and 7.2.7 ship recursive command and capability/provenance metadata for Weaver resource paths. | n/a | 2026-06-14 | +| [13.1.3](roadmap.md#13-command-contract-proving-slice) | Define the temporary-adapter removal policy. | ~ wraps | OrthoConfig 6.1, OrthoConfig 7.2.7 | n/a | Retire local generic helpers once their named OrthoConfig replacements are available, unless ADR 007 records a permanent divergence. | n/a | 2026-06-14 | +| [13.2.1](roadmap.md#13-command-contract-proving-slice) | Implement the localized human renderer for definitions get. | ~ wraps | OrthoConfig 7.2.2 | n/a | Replace Weaver-local human renderer metadata once OrthoConfig 7.2.2 ships reusable human-renderer controls and terminal behaviour metadata. | n/a | 2026-06-14 | +| [13.2.2](roadmap.md#13-command-contract-proving-slice) | Implement universal JSON and structured errors for definitions get. | ~ wraps | OrthoConfig 7.2.3, OrthoConfig 7.2.4, OrthoConfig 7.2.5, OrthoConfig 8.1 | n/a | Replace Weaver-local JSON, stderr error, and exit-class definitions once OrthoConfig 7.2.3 through 7.2.5 and 8.1 ship stable renderer contracts. | n/a | 2026-06-14 | +| [13.2.3](roadmap.md#13-command-contract-proving-slice) | Implement enumerating errors and bounded responses for the pilot. | ~ wraps | OrthoConfig 7.2.6 | n/a | Replace local bounded-list and enumerating-error metadata once OrthoConfig 7.2.6 ships the reusable contract. | n/a | 2026-06-14 | +| [13.3.1](roadmap.md#13-command-contract-proving-slice) | Implement weaver context JSON for the pilot command family. | ~ wraps | OrthoConfig 6.2 | n/a | Replace local context payload metadata once OrthoConfig 6.2 ships compact agent-context generation and schema stability. | n/a | 2026-06-14 | +| [13.3.2](roadmap.md#13-command-contract-proving-slice) | Implement capabilities list JSON from runtime provider state. | ~ wraps | OrthoConfig 7.2.7 | n/a | Replace local capability/provenance envelope metadata once OrthoConfig 7.2.7 ships the reusable capability contract. | n/a | 2026-06-14 | +| [13.3.3](roadmap.md#13-command-contract-proving-slice) | Generate help, manpage input, completions, and skill paths. | ~ wraps | OrthoConfig 6.3, OrthoConfig 8.1 | n/a | Replace local generated-reference metadata once OrthoConfig 6.3 and 8.1 ship skill and reference-CLI contracts. | n/a | 2026-06-14 | +| [13.3.4](roadmap.md#13-command-contract-proving-slice) | Add command-surface drift gates for the pilot. | ~ wraps | OrthoConfig 6.1, OrthoConfig 7.1, OrthoConfig 8.1 | n/a | Replace local drift checks for generic command metadata, vocabulary, and renderer surfaces once OrthoConfig ships reusable policy gates. | n/a | 2026-06-14 | + +## Phase 14 + +| Roadmap task | Gist | State | Upstream OrthoConfig task | Shipped in | Removal gate or divergence | Next review by | Last reviewed | +| ----------------------------------------------- | --------------------------------------------------------------------------- | --------- | ---------------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------- | ------------- | +| [14.1.1](roadmap.md#14-code-reading-loop-slice) | Implement weaver definitions get. | ~ wraps | OrthoConfig 6.1, OrthoConfig 7.2 | n/a | Replace generic command and renderer metadata for definitions get once OrthoConfig 6.1 and 7.2 can express the resource command. | n/a | 2026-06-14 | +| [14.1.2](roadmap.md#14-code-reading-loop-slice) | Implement weaver references list. | ~ wraps | OrthoConfig 6.1, OrthoConfig 7.2.6 | n/a | Replace generic command and bounded-list metadata for references list once OrthoConfig 6.1 and 7.2.6 ship the reusable contracts. | n/a | 2026-06-14 | +| [14.1.3](roadmap.md#14-code-reading-loop-slice) | Implement weaver diagnostics list. | ~ wraps | OrthoConfig 6.1, OrthoConfig 7.2.5 | n/a | Replace generic command and structured-error metadata for diagnostics list once OrthoConfig ships the reusable contracts. | n/a | 2026-06-14 | +| [14.1.4](roadmap.md#14-code-reading-loop-slice) | Add combinatorial read-command end-to-end coverage. | ~ wraps | OrthoConfig 7.2, OrthoConfig 8.1 | n/a | Retire local generic renderer assertions once OrthoConfig reference-CLI and renderer conformance gates cover the shared contract. | n/a | 2026-06-14 | +| [14.2.1](roadmap.md#14-code-reading-loop-slice) | Implement weaver cards get for position references. | ~ wraps | OrthoConfig 6.1, OrthoConfig 7.2 | n/a | Replace generic command, JSON, and human-renderer metadata for cards get once OrthoConfig can express the resource command. | n/a | 2026-06-14 | +| [14.2.2](roadmap.md#14-code-reading-loop-slice) | Add qualified symbol selectors to cards get. | ~ wraps | OrthoConfig 7.2.6 | n/a | Replace generic enumerating-error metadata for ambiguous selectors once OrthoConfig 7.2.6 ships the reusable contract. | n/a | 2026-06-14 | +| [14.2.3](roadmap.md#14-code-reading-loop-slice) | Add one-hop relation summaries to cards. | ~ wraps | OrthoConfig 7.2.6 | n/a | Replace generic bounded-output metadata for relation summaries once OrthoConfig 7.2.6 ships the reusable contract. | n/a | 2026-06-14 | +| [14.3.1](roadmap.md#14-code-reading-loop-slice) | Prototype symbols list pattern search over weaver-syntax and optional srgn. | ? pending | OrthoConfig 6.1, OrthoConfig 7.1 | n/a | n/a | 2026-12-31 | 2026-06-14 | +| [14.3.2](roadmap.md#14-code-reading-loop-slice) | Decide whether the static-search pilot graduates. | ? pending | OrthoConfig 7.1 | n/a | n/a | 2026-12-31 | 2026-06-14 | + +## Phase 15 + +| Roadmap task | Gist | State | Upstream OrthoConfig task | Shipped in | Removal gate or divergence | Next review by | Last reviewed | +| -------------------------------------------------------- | -------------------------------------------------------------------------------- | ------- | ---------------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------- | ------------- | +| [15.1.1](roadmap.md#15-sempai-selector-to-context-slice) | Implement one-liner tokenization and Pratt parsing for positive symbol patterns. | ~ wraps | OrthoConfig 7.2.5 | n/a | Replace generic diagnostic envelope and error-class metadata once OrthoConfig structured-error contracts are available. | n/a | 2026-06-14 | +| [15.1.2](roadmap.md#15-sempai-selector-to-context-slice) | Define selector record schemas for one-liner matches. | ~ wraps | OrthoConfig 7.2.7 | n/a | Replace generic selector provenance fields once OrthoConfig capability/provenance metadata can carry them. | n/a | 2026-06-14 | +| [15.3.2](roadmap.md#15-sempai-selector-to-context-slice) | Add weaver symbols list query inputs. | ~ wraps | OrthoConfig 6.1, OrthoConfig 7.2.5 | n/a | Replace generic command, input-combination, and error metadata once OrthoConfig can express the symbols list query contract. | n/a | 2026-06-14 | +| [15.3.3](roadmap.md#15-sempai-selector-to-context-slice) | Define stable JSONL request and response schemas for Sempai query operations. | ~ wraps | OrthoConfig 7.2.4 | n/a | Replace generic machine-renderer field and schema metadata once OrthoConfig 7.2.4 ships the reusable contract. | n/a | 2026-06-14 | +| [15.3.5](roadmap.md#15-sempai-selector-to-context-slice) | Implement actuation handoff contract using focus-first selection. | ~ wraps | OrthoConfig 7.2.7 | n/a | Replace generic selector handoff provenance once OrthoConfig capability/provenance metadata can carry downstream mutation inputs. | n/a | 2026-06-14 | +| [15.3.6](roadmap.md#15-sempai-selector-to-context-slice) | Add diagnostics conformance suites for Sempai error categories. | ~ wraps | OrthoConfig 7.2.5, OrthoConfig 8.1 | n/a | Replace generic structured-error conformance once OrthoConfig reference-CLI and renderer gates cover the shared error contract. | n/a | 2026-06-14 | + +## Phase 16 + +| Roadmap task | Gist | State | Upstream OrthoConfig task | Shipped in | Removal gate or divergence | Next review by | Last reviewed | +| ---------------------------------------------- | ------------------------------------------------------------------- | ------- | ---------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------------- | ------------- | +| [16.1.1](roadmap.md#16-safe-change-loop-slice) | Implement weaver patches apply. | ~ wraps | OrthoConfig 6.1, OrthoConfig 7.2.1 | n/a | Replace generic mutation command and preview metadata once OrthoConfig 6.1 and 7.2.1 ship reusable mutation contracts. | n/a | 2026-06-14 | +| [16.1.2](roadmap.md#16-safe-change-loop-slice) | Add idempotency keys, transaction IDs, and retry matching. | ~ wraps | OrthoConfig 7.2.1 | n/a | Replace generic idempotency and retry metadata once OrthoConfig 7.2.1 ships mutation behavioural metadata. | n/a | 2026-06-14 | +| [16.1.3](roadmap.md#16-safe-change-loop-slice) | Standardize mutation dry-run, force, and safety metadata. | ~ wraps | OrthoConfig 7.2.1 | n/a | Replace local mutation flag and safety metadata once OrthoConfig 7.2.1 ships the reusable mutation policy contract. | n/a | 2026-06-14 | +| [16.2.1](roadmap.md#16-safe-change-loop-slice) | Implement weaver symbols rename for position references. | ~ wraps | OrthoConfig 6.1, OrthoConfig 7.2.7 | n/a | Replace generic command and capability/provenance metadata once OrthoConfig can express symbols rename. | n/a | 2026-06-14 | +| [16.2.2](roadmap.md#16-safe-change-loop-slice) | Add direct Sempai selector support to symbols rename. | ~ wraps | OrthoConfig 7.2.6 | n/a | Replace generic ambiguous-mutation and enumerating-error metadata once OrthoConfig 7.2.6 ships the reusable contract. | n/a | 2026-06-14 | +| [16.2.3](roadmap.md#16-safe-change-loop-slice) | Add from-stdin selector stream consumption to mutation commands. | ~ wraps | OrthoConfig 7.2.4 | n/a | Replace generic stream input and JSON renderer metadata once OrthoConfig 7.2.4 ships reusable machine contracts. | n/a | 2026-06-14 | +| [16.3.3](roadmap.md#16-safe-change-loop-slice) | Add symbols move command contract and discovery output. | ~ wraps | OrthoConfig 7.2.7 | n/a | Replace generic command discovery and capability/provenance metadata once OrthoConfig 7.2.7 can express symbols move and extricate-symbol. | n/a | 2026-06-14 | +| [16.3.5](roadmap.md#16-safe-change-loop-slice) | Extend refusal diagnostics and rollback guarantees for extrication. | ~ wraps | OrthoConfig 7.2.5 | n/a | Replace generic refusal diagnostic and error-code metadata once OrthoConfig structured-error contracts cover the shared surface. | n/a | 2026-06-14 | + +## Phase 17 + +| Roadmap task | Gist | State | Upstream OrthoConfig task | Shipped in | Removal gate or divergence | Next review by | Last reviewed | +| ------------------------------------------------ | --------------------------------------------------------------------- | ------- | ---------------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------- | -------------- | ------------- | +| [17.1.5](roadmap.md#17-impact-and-history-slice) | Implement budgeted traversal and graph-slice command integration. | ~ wraps | OrthoConfig 6.1, OrthoConfig 7.2.6 | n/a | Replace generic command and bounded-output metadata once OrthoConfig can express graph-slices get. | n/a | 2026-06-14 | +| [17.2.2](roadmap.md#17-impact-and-history-slice) | Implement slice reconstruction per commit with data-quality metadata. | ~ wraps | OrthoConfig 7.2.4 | n/a | Replace generic machine-output field metadata once OrthoConfig renderer contracts cover history slice payloads. | n/a | 2026-06-14 | + +## Phase 18 + +| Roadmap task | Gist | State | Upstream OrthoConfig task | Shipped in | Removal gate or divergence | Next review by | Last reviewed | +| ------------------------------------------------ | ---------------------------------------------------------------- | ------- | ------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------- | -------------- | ------------- | +| [18.2.1](roadmap.md#18-provider-ecosystem-slice) | Wire provider summaries into capabilities list and context JSON. | ~ wraps | OrthoConfig 7.2.7 | n/a | Replace local provider summary envelope metadata once OrthoConfig 7.2.7 ships reusable capability/provenance metadata. | n/a | 2026-06-14 | + +## Phase 19 + +| Roadmap task | Gist | State | Upstream OrthoConfig task | Shipped in | Removal gate or divergence | Next review by | Last reviewed | +| ---------------------------------------------------------- | ----------------------------------------------------------------- | --------- | ---------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------- | -------------- | ------------- | +| [19.1.1](roadmap.md#19-agent-workflow-and-assurance-slice) | Implement profile storage, redaction, and root profile selection. | ? pending | OrthoConfig 9.1 | n/a | n/a | 2026-12-31 | 2026-06-14 | +| [19.1.2](roadmap.md#19-agent-workflow-and-assurance-slice) | Implement durable job ledger support. | ? pending | OrthoConfig 9.3 | n/a | n/a | 2026-12-31 | 2026-06-14 | +| [19.1.3](roadmap.md#19-agent-workflow-and-assurance-slice) | Implement wait, jobs, and delivery sinks. | ? pending | OrthoConfig 9.2.1, OrthoConfig 9.3 | n/a | n/a | 2026-12-31 | 2026-06-14 | +| [19.1.4](roadmap.md#19-agent-workflow-and-assurance-slice) | Implement feedback create, list, and send. | ? pending | OrthoConfig 9.2.2 | n/a | n/a | 2026-12-31 | 2026-06-14 | +| [19.2.2](roadmap.md#19-agent-workflow-and-assurance-slice) | Implement explicit interactive review as an opt-in workflow. | ~ wraps | OrthoConfig 7.2.1 | n/a | Replace generic non-interactive and mutation-policy metadata once OrthoConfig 7.2.1 ships reusable interaction contracts. | n/a | 2026-06-14 | + +## Phase 20 + +| Roadmap task | Gist | State | Upstream OrthoConfig task | Shipped in | Removal gate or divergence | Next review by | Last reviewed | +| -------------------------------------------------------------------------- | ------------------------------------------------------------------------- | --------- | ------------------------- | ---------- | -------------------------- | -------------- | ------------- | +| [20.1.1](roadmap.md#20-deferred-extensions-after-the-core-product-promise) | Decide whether to generate MCP descriptions from context JSON. | ? pending | OrthoConfig 10.1.1 | n/a | n/a | 2026-12-31 | 2026-06-14 | +| [20.1.2](roadmap.md#20-deferred-extensions-after-the-core-product-promise) | Decide whether SDK or OpenAPI-shaped runtime explorers are in scope. | ? pending | OrthoConfig 10.1.2 | n/a | n/a | 2026-12-31 | 2026-06-14 | +| [20.3.3](roadmap.md#20-deferred-extensions-after-the-core-product-promise) | Decide whether weaver daemon status JSON belongs in the command contract. | ? pending | OrthoConfig 6.2.3 | n/a | n/a | 2026-12-31 | 2026-06-14 | + diff --git a/docs/orthoconfig-consumer-boundary.toml b/docs/orthoconfig-consumer-boundary.toml new file mode 100644 index 00000000..a253a816 --- /dev/null +++ b/docs/orthoconfig-consumer-boundary.toml @@ -0,0 +1,622 @@ +schema_version = 1 + +managed_tasks = [ + "12.1.1", "12.1.2", "12.1.3", "12.1.4", "12.1.5", + "13.1.1", "13.1.2", "13.1.3", "13.2.1", "13.2.2", "13.2.3", + "13.3.1", "13.3.2", "13.3.3", "13.3.4", + "14.1.1", "14.1.2", "14.1.3", "14.1.4", "14.2.1", "14.2.2", + "14.2.3", "14.3.1", "14.3.2", + "15.1.1", "15.1.2", "15.3.2", "15.3.3", "15.3.5", "15.3.6", + "16.1.1", "16.1.2", "16.1.3", "16.2.1", "16.2.2", "16.2.3", + "16.3.3", "16.3.5", + "17.1.5", "17.2.2", + "18.2.1", + "19.1.1", "19.1.2", "19.1.3", "19.1.4", "19.2.2", + "20.1.1", "20.1.2", "20.3.3", +] + +[[task]] +id = "12.1.1" +gist = "Track the downstream consumer boundary." +state = "consumes" +upstream = [{ task = "OrthoConfig 5.2.3 / design §2.1", role = "boundary" }] +shipped_in = "4339a6f3c61dc4fed86493d99ffb05230bee2a1b" +removal_gate = "" +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "12.1.2" +gist = "Consume recursive command metadata." +state = "wraps" +upstream = [{ task = "OrthoConfig 6.1", role = "metadata" }] +shipped_in = "" +removal_gate = "Replace local recursive command metadata once OrthoConfig 6.1 ships command and subcommand records that can drive help, manpages, completions, and context output." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "12.1.3" +gist = "Consume compact context and skill metadata." +state = "wraps" +upstream = [ + { task = "OrthoConfig 6.2", role = "metadata" }, + { task = "OrthoConfig 6.3", role = "metadata" }, +] +shipped_in = "" +removal_gate = "Replace Weaver-local context and skill metadata once OrthoConfig 6.2 and 6.3 ship reusable context and skill shapes that Weaver can extend." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "12.1.4" +gist = "Consume canonical vocabulary and behavioural metadata." +state = "wraps" +upstream = [ + { task = "OrthoConfig 7.1", role = "vocabulary" }, + { task = "OrthoConfig 7.2", role = "renderer" }, + { task = "OrthoConfig 8.1", role = "renderer" }, +] +shipped_in = "" +removal_gate = "Replace Weaver-local vocabulary, renderer, JSON, exit-code, bounded-list, mutation, and provenance checks once OrthoConfig 7.1, 7.2, and 8.1 ship those contracts." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "12.1.5" +gist = "Consume compounding primitive contracts." +state = "pending" +upstream = [ + { task = "OrthoConfig 9.1", role = "profile" }, + { task = "OrthoConfig 9.2.1", role = "delivery" }, + { task = "OrthoConfig 9.2.2", role = "feedback" }, + { task = "OrthoConfig 9.3", role = "execution_ledger" }, +] +shipped_in = "" +removal_gate = "" +adr_anchor = "" +next_review_by = "2026-12-31" +last_reviewed = "2026-06-14" + +[[task]] +id = "13.1.1" +gist = "Record the agent-native command-surface reset as ADR 007." +state = "divergent" +upstream = [{ task = "OrthoConfig 5.2.3", role = "boundary" }] +shipped_in = "" +removal_gate = "" +adr_anchor = "decision-outcome" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "13.1.2" +gist = "Implement the Weaver command-surface adapter for one read-only command family." +state = "wraps" +upstream = [ + { task = "OrthoConfig 5.2.3", role = "boundary" }, + { task = "OrthoConfig 6.1", role = "metadata" }, + { task = "OrthoConfig 7.2.7", role = "capability_provenance" }, +] +shipped_in = "" +removal_gate = "Replace CommandSurfaceRecord once OrthoConfig 6.1 and 7.2.7 ship recursive command and capability/provenance metadata for Weaver resource paths." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "13.1.3" +gist = "Define the temporary-adapter removal policy." +state = "wraps" +upstream = [ + { task = "OrthoConfig 6.1", role = "metadata" }, + { task = "OrthoConfig 7.2.7", role = "capability_provenance" }, +] +shipped_in = "" +removal_gate = "Retire local generic helpers once their named OrthoConfig replacements are available, unless ADR 007 records a permanent divergence." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "13.2.1" +gist = "Implement the localized human renderer for definitions get." +state = "wraps" +upstream = [{ task = "OrthoConfig 7.2.2", role = "renderer" }] +shipped_in = "" +removal_gate = "Replace Weaver-local human renderer metadata once OrthoConfig 7.2.2 ships reusable human-renderer controls and terminal behaviour metadata." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "13.2.2" +gist = "Implement universal JSON and structured errors for definitions get." +state = "wraps" +upstream = [ + { task = "OrthoConfig 7.2.3", role = "renderer" }, + { task = "OrthoConfig 7.2.4", role = "renderer" }, + { task = "OrthoConfig 7.2.5", role = "renderer" }, + { task = "OrthoConfig 8.1", role = "renderer" }, +] +shipped_in = "" +removal_gate = "Replace Weaver-local JSON, stderr error, and exit-class definitions once OrthoConfig 7.2.3 through 7.2.5 and 8.1 ship stable renderer contracts." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "13.2.3" +gist = "Implement enumerating errors and bounded responses for the pilot." +state = "wraps" +upstream = [{ task = "OrthoConfig 7.2.6", role = "renderer" }] +shipped_in = "" +removal_gate = "Replace local bounded-list and enumerating-error metadata once OrthoConfig 7.2.6 ships the reusable contract." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "13.3.1" +gist = "Implement weaver context JSON for the pilot command family." +state = "wraps" +upstream = [{ task = "OrthoConfig 6.2", role = "metadata" }] +shipped_in = "" +removal_gate = "Replace local context payload metadata once OrthoConfig 6.2 ships compact agent-context generation and schema stability." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "13.3.2" +gist = "Implement capabilities list JSON from runtime provider state." +state = "wraps" +upstream = [{ task = "OrthoConfig 7.2.7", role = "capability_provenance" }] +shipped_in = "" +removal_gate = "Replace local capability/provenance envelope metadata once OrthoConfig 7.2.7 ships the reusable capability contract." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "13.3.3" +gist = "Generate help, manpage input, completions, and skill paths." +state = "wraps" +upstream = [ + { task = "OrthoConfig 6.3", role = "metadata" }, + { task = "OrthoConfig 8.1", role = "renderer" }, +] +shipped_in = "" +removal_gate = "Replace local generated-reference metadata once OrthoConfig 6.3 and 8.1 ship skill and reference-CLI contracts." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "13.3.4" +gist = "Add command-surface drift gates for the pilot." +state = "wraps" +upstream = [ + { task = "OrthoConfig 6.1", role = "metadata" }, + { task = "OrthoConfig 7.1", role = "vocabulary" }, + { task = "OrthoConfig 8.1", role = "renderer" }, +] +shipped_in = "" +removal_gate = "Replace local drift checks for generic command metadata, vocabulary, and renderer surfaces once OrthoConfig ships reusable policy gates." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "14.1.1" +gist = "Implement weaver definitions get." +state = "wraps" +upstream = [ + { task = "OrthoConfig 6.1", role = "metadata" }, + { task = "OrthoConfig 7.2", role = "renderer" }, +] +shipped_in = "" +removal_gate = "Replace generic command and renderer metadata for definitions get once OrthoConfig 6.1 and 7.2 can express the resource command." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "14.1.2" +gist = "Implement weaver references list." +state = "wraps" +upstream = [ + { task = "OrthoConfig 6.1", role = "metadata" }, + { task = "OrthoConfig 7.2.6", role = "renderer" }, +] +shipped_in = "" +removal_gate = "Replace generic command and bounded-list metadata for references list once OrthoConfig 6.1 and 7.2.6 ship the reusable contracts." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "14.1.3" +gist = "Implement weaver diagnostics list." +state = "wraps" +upstream = [ + { task = "OrthoConfig 6.1", role = "metadata" }, + { task = "OrthoConfig 7.2.5", role = "renderer" }, +] +shipped_in = "" +removal_gate = "Replace generic command and structured-error metadata for diagnostics list once OrthoConfig ships the reusable contracts." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "14.1.4" +gist = "Add combinatorial read-command end-to-end coverage." +state = "wraps" +upstream = [ + { task = "OrthoConfig 7.2", role = "renderer" }, + { task = "OrthoConfig 8.1", role = "renderer" }, +] +shipped_in = "" +removal_gate = "Retire local generic renderer assertions once OrthoConfig reference-CLI and renderer conformance gates cover the shared contract." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "14.2.1" +gist = "Implement weaver cards get for position references." +state = "wraps" +upstream = [ + { task = "OrthoConfig 6.1", role = "metadata" }, + { task = "OrthoConfig 7.2", role = "renderer" }, +] +shipped_in = "" +removal_gate = "Replace generic command, JSON, and human-renderer metadata for cards get once OrthoConfig can express the resource command." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "14.2.2" +gist = "Add qualified symbol selectors to cards get." +state = "wraps" +upstream = [{ task = "OrthoConfig 7.2.6", role = "renderer" }] +shipped_in = "" +removal_gate = "Replace generic enumerating-error metadata for ambiguous selectors once OrthoConfig 7.2.6 ships the reusable contract." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "14.2.3" +gist = "Add one-hop relation summaries to cards." +state = "wraps" +upstream = [{ task = "OrthoConfig 7.2.6", role = "renderer" }] +shipped_in = "" +removal_gate = "Replace generic bounded-output metadata for relation summaries once OrthoConfig 7.2.6 ships the reusable contract." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "14.3.1" +gist = "Prototype symbols list pattern search over weaver-syntax and optional srgn." +state = "pending" +upstream = [ + { task = "OrthoConfig 6.1", role = "metadata" }, + { task = "OrthoConfig 7.1", role = "vocabulary" }, +] +shipped_in = "" +removal_gate = "" +adr_anchor = "" +next_review_by = "2026-12-31" +last_reviewed = "2026-06-14" + +[[task]] +id = "14.3.2" +gist = "Decide whether the static-search pilot graduates." +state = "pending" +upstream = [{ task = "OrthoConfig 7.1", role = "vocabulary" }] +shipped_in = "" +removal_gate = "" +adr_anchor = "" +next_review_by = "2026-12-31" +last_reviewed = "2026-06-14" + +[[task]] +id = "15.1.1" +gist = "Implement one-liner tokenization and Pratt parsing for positive symbol patterns." +state = "wraps" +upstream = [{ task = "OrthoConfig 7.2.5", role = "renderer" }] +shipped_in = "" +removal_gate = "Replace generic diagnostic envelope and error-class metadata once OrthoConfig structured-error contracts are available." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "15.1.2" +gist = "Define selector record schemas for one-liner matches." +state = "wraps" +upstream = [{ task = "OrthoConfig 7.2.7", role = "capability_provenance" }] +shipped_in = "" +removal_gate = "Replace generic selector provenance fields once OrthoConfig capability/provenance metadata can carry them." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "15.3.2" +gist = "Add weaver symbols list query inputs." +state = "wraps" +upstream = [ + { task = "OrthoConfig 6.1", role = "metadata" }, + { task = "OrthoConfig 7.2.5", role = "renderer" }, +] +shipped_in = "" +removal_gate = "Replace generic command, input-combination, and error metadata once OrthoConfig can express the symbols list query contract." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "15.3.3" +gist = "Define stable JSONL request and response schemas for Sempai query operations." +state = "wraps" +upstream = [{ task = "OrthoConfig 7.2.4", role = "renderer" }] +shipped_in = "" +removal_gate = "Replace generic machine-renderer field and schema metadata once OrthoConfig 7.2.4 ships the reusable contract." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "15.3.5" +gist = "Implement actuation handoff contract using focus-first selection." +state = "wraps" +upstream = [{ task = "OrthoConfig 7.2.7", role = "capability_provenance" }] +shipped_in = "" +removal_gate = "Replace generic selector handoff provenance once OrthoConfig capability/provenance metadata can carry downstream mutation inputs." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "15.3.6" +gist = "Add diagnostics conformance suites for Sempai error categories." +state = "wraps" +upstream = [ + { task = "OrthoConfig 7.2.5", role = "renderer" }, + { task = "OrthoConfig 8.1", role = "renderer" }, +] +shipped_in = "" +removal_gate = "Replace generic structured-error conformance once OrthoConfig reference-CLI and renderer gates cover the shared error contract." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "16.1.1" +gist = "Implement weaver patches apply." +state = "wraps" +upstream = [ + { task = "OrthoConfig 6.1", role = "metadata" }, + { task = "OrthoConfig 7.2.1", role = "renderer" }, +] +shipped_in = "" +removal_gate = "Replace generic mutation command and preview metadata once OrthoConfig 6.1 and 7.2.1 ship reusable mutation contracts." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "16.1.2" +gist = "Add idempotency keys, transaction IDs, and retry matching." +state = "wraps" +upstream = [{ task = "OrthoConfig 7.2.1", role = "renderer" }] +shipped_in = "" +removal_gate = "Replace generic idempotency and retry metadata once OrthoConfig 7.2.1 ships mutation behavioural metadata." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "16.1.3" +gist = "Standardize mutation dry-run, force, and safety metadata." +state = "wraps" +upstream = [{ task = "OrthoConfig 7.2.1", role = "renderer" }] +shipped_in = "" +removal_gate = "Replace local mutation flag and safety metadata once OrthoConfig 7.2.1 ships the reusable mutation policy contract." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "16.2.1" +gist = "Implement weaver symbols rename for position references." +state = "wraps" +upstream = [ + { task = "OrthoConfig 6.1", role = "metadata" }, + { task = "OrthoConfig 7.2.7", role = "capability_provenance" }, +] +shipped_in = "" +removal_gate = "Replace generic command and capability/provenance metadata once OrthoConfig can express symbols rename." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "16.2.2" +gist = "Add direct Sempai selector support to symbols rename." +state = "wraps" +upstream = [{ task = "OrthoConfig 7.2.6", role = "renderer" }] +shipped_in = "" +removal_gate = "Replace generic ambiguous-mutation and enumerating-error metadata once OrthoConfig 7.2.6 ships the reusable contract." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "16.2.3" +gist = "Add from-stdin selector stream consumption to mutation commands." +state = "wraps" +upstream = [{ task = "OrthoConfig 7.2.4", role = "renderer" }] +shipped_in = "" +removal_gate = "Replace generic stream input and JSON renderer metadata once OrthoConfig 7.2.4 ships reusable machine contracts." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "16.3.3" +gist = "Add symbols move command contract and discovery output." +state = "wraps" +upstream = [{ task = "OrthoConfig 7.2.7", role = "capability_provenance" }] +shipped_in = "" +removal_gate = "Replace generic command discovery and capability/provenance metadata once OrthoConfig 7.2.7 can express symbols move and extricate-symbol." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "16.3.5" +gist = "Extend refusal diagnostics and rollback guarantees for extrication." +state = "wraps" +upstream = [{ task = "OrthoConfig 7.2.5", role = "renderer" }] +shipped_in = "" +removal_gate = "Replace generic refusal diagnostic and error-code metadata once OrthoConfig structured-error contracts cover the shared surface." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "17.1.5" +gist = "Implement budgeted traversal and graph-slice command integration." +state = "wraps" +upstream = [ + { task = "OrthoConfig 6.1", role = "metadata" }, + { task = "OrthoConfig 7.2.6", role = "renderer" }, +] +shipped_in = "" +removal_gate = "Replace generic command and bounded-output metadata once OrthoConfig can express graph-slices get." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "17.2.2" +gist = "Implement slice reconstruction per commit with data-quality metadata." +state = "wraps" +upstream = [{ task = "OrthoConfig 7.2.4", role = "renderer" }] +shipped_in = "" +removal_gate = "Replace generic machine-output field metadata once OrthoConfig renderer contracts cover history slice payloads." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "18.2.1" +gist = "Wire provider summaries into capabilities list and context JSON." +state = "wraps" +upstream = [{ task = "OrthoConfig 7.2.7", role = "capability_provenance" }] +shipped_in = "" +removal_gate = "Replace local provider summary envelope metadata once OrthoConfig 7.2.7 ships reusable capability/provenance metadata." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "19.1.1" +gist = "Implement profile storage, redaction, and root profile selection." +state = "pending" +upstream = [{ task = "OrthoConfig 9.1", role = "profile" }] +shipped_in = "" +removal_gate = "" +adr_anchor = "" +next_review_by = "2026-12-31" +last_reviewed = "2026-06-14" + +[[task]] +id = "19.1.2" +gist = "Implement durable job ledger support." +state = "pending" +upstream = [{ task = "OrthoConfig 9.3", role = "execution_ledger" }] +shipped_in = "" +removal_gate = "" +adr_anchor = "" +next_review_by = "2026-12-31" +last_reviewed = "2026-06-14" + +[[task]] +id = "19.1.3" +gist = "Implement wait, jobs, and delivery sinks." +state = "pending" +upstream = [ + { task = "OrthoConfig 9.2.1", role = "delivery" }, + { task = "OrthoConfig 9.3", role = "execution_ledger" }, +] +shipped_in = "" +removal_gate = "" +adr_anchor = "" +next_review_by = "2026-12-31" +last_reviewed = "2026-06-14" + +[[task]] +id = "19.1.4" +gist = "Implement feedback create, list, and send." +state = "pending" +upstream = [{ task = "OrthoConfig 9.2.2", role = "feedback" }] +shipped_in = "" +removal_gate = "" +adr_anchor = "" +next_review_by = "2026-12-31" +last_reviewed = "2026-06-14" + +[[task]] +id = "19.2.2" +gist = "Implement explicit interactive review as an opt-in workflow." +state = "wraps" +upstream = [{ task = "OrthoConfig 7.2.1", role = "renderer" }] +shipped_in = "" +removal_gate = "Replace generic non-interactive and mutation-policy metadata once OrthoConfig 7.2.1 ships reusable interaction contracts." +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-14" + +[[task]] +id = "20.1.1" +gist = "Decide whether to generate MCP descriptions from context JSON." +state = "pending" +upstream = [{ task = "OrthoConfig 10.1.1", role = "metadata" }] +shipped_in = "" +removal_gate = "" +adr_anchor = "" +next_review_by = "2026-12-31" +last_reviewed = "2026-06-14" + +[[task]] +id = "20.1.2" +gist = "Decide whether SDK or OpenAPI-shaped runtime explorers are in scope." +state = "pending" +upstream = [{ task = "OrthoConfig 10.1.2", role = "metadata" }] +shipped_in = "" +removal_gate = "" +adr_anchor = "" +next_review_by = "2026-12-31" +last_reviewed = "2026-06-14" + +[[task]] +id = "20.3.3" +gist = "Decide whether weaver daemon status JSON belongs in the command contract." +state = "pending" +upstream = [{ task = "OrthoConfig 6.2.3", role = "metadata" }] +shipped_in = "" +removal_gate = "" +adr_anchor = "" +next_review_by = "2026-12-31" +last_reviewed = "2026-06-14" From bfecd96e8a295dfdd3bddd968b2b3dc1d7123a05 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 14 Jun 2026 06:27:41 +0200 Subject: [PATCH 04/22] Link OrthoConfig boundary matrix from docs Cross-link the generated consumer boundary matrix from the documentation index, developer workflow, and each manifest-managed roadmap task group. Record the Stage C validation and CodeRabbit result in the ExecPlan so the next implementation slice can continue from the reviewed state. --- docs/contents.md | 4 ++ docs/developers-guide.md | 17 ++++++ ...-1-1-track-downstream-consumer-boundary.md | 8 ++- docs/roadmap.md | 57 +++++++++++++++---- 4 files changed, 72 insertions(+), 14 deletions(-) diff --git a/docs/contents.md b/docs/contents.md index 80e04d1f..47009f69 100644 --- a/docs/contents.md +++ b/docs/contents.md @@ -32,6 +32,10 @@ readers can find the source of truth without scanning the whole tree. surface](adr-007-agent-native-command-surface.md) - Decision record for the human-friendly, agent-native 0.1.0 command contract and OrthoConfig dependency boundary. +- [OrthoConfig consumer boundary matrix](orthoconfig-consumer-boundary.md) + - Generated matrix classifying every command-contract roadmap task as an + OrthoConfig consumer, temporary wrapper, pending dependency, or deliberate + divergence. - [Archived prototype roadmap](archive/prototype-roadmap.md) - Historical completed and superseded roadmap tasks from the pre-ADR-007 command grammar, preserving task numbers `1` through `11`. diff --git a/docs/developers-guide.md b/docs/developers-guide.md index 08e9870d..cc05c015 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -16,6 +16,23 @@ from OrthoConfig-backed command metadata plus Weaver-owned semantic adapters. Do not add new public command grammar by hand in `clap`, daemon routing, manual pages, help text, and docs independently. +The [OrthoConfig consumer boundary matrix](orthoconfig-consumer-boundary.md) is +the source of truth for command-contract roadmap tasks. Before adding, +renaming, or reclassifying a public command task, update +`docs/orthoconfig-consumer-boundary.toml` and regenerate +`docs/orthoconfig-consumer-boundary.md` with: + +```sh +cargo run -p weaver-docs-gate --example render_boundary_matrix -- \ + docs/orthoconfig-consumer-boundary.toml docs/orthoconfig-consumer-boundary.md +``` + +Use `consumes` only when the upstream OrthoConfig contract has shipped and the +row can name the release or pinned SHA in `shipped_in`. Use `wraps` for a +temporary Weaver adapter with a concrete removal gate, `pending` for an +upstream contract that is not decided or shipped yet, and `divergent` only when +ADR 007 records the deliberate Weaver-owned divergence. + When adding or renaming a public command: 1. Update the OrthoConfig-backed command metadata or the Weaver semantic diff --git a/docs/execplans/12-1-1-track-downstream-consumer-boundary.md b/docs/execplans/12-1-1-track-downstream-consumer-boundary.md index f9aa73c0..b96862d5 100644 --- a/docs/execplans/12-1-1-track-downstream-consumer-boundary.md +++ b/docs/execplans/12-1-1-track-downstream-consumer-boundary.md @@ -218,8 +218,12 @@ state, not the intended sequence. issue; after fixing the manifest to use `OrthoConfig 9.2.1` and `OrthoConfig 9.2.2`, regenerating the matrix, and rerunning gates, the repeat CodeRabbit review completed with `findings: 0`. -- [ ] Stage C: cross-link the matrix from ADR 007, the roadmap, and the - developers' guide. +- [x] 2026-06-14T04:52:00Z: Stage C cross-linked the generated matrix from + `docs/contents.md`, `docs/developers-guide.md`, and every + manifest-managed roadmap task group. ADR 007 already linked both the + matrix and the TOML source of truth from Stage A. Validation passed with + `make fmt`, `make markdownlint`, `make nixie`, and CodeRabbit + `findings: 0`. - [ ] Stage D: add the referential-integrity test gate and snapshot. - [ ] Stage E: refresh the users' guide, run the full quality gates, and run `coderabbit review --agent`. diff --git a/docs/roadmap.md b/docs/roadmap.md index 9b72cd5f..a75d3ecd 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -40,6 +40,8 @@ from rediscovering the same dependency and migration questions. This step answers which generic command-contract pieces come from OrthoConfig and which temporary Weaver adapters are allowed. Its outcome informs every command-surface and renderer task. See ADR 007 and the OrthoConfig roadmap. +Boundary classifications for this step are tracked in the +[OrthoConfig consumer boundary matrix](orthoconfig-consumer-boundary.md). - [ ] 12.1.1. Track the downstream consumer boundary. - Depends on OrthoConfig 5.2.3. @@ -106,7 +108,9 @@ introspection, and agent-grade CLI hardening into one acceptance surface. This step answers whether Weaver-specific semantic metadata can sit on top of OrthoConfig command contracts without duplicating them. See ADR 007 and -`docs/weaver-design.md` §§2.1.1-2.1.4. +`docs/weaver-design.md` §§2.1.1-2.1.4. Boundary classifications for these tasks +are tracked in the +[OrthoConfig consumer boundary matrix](orthoconfig-consumer-boundary.md). - [x] 13.1.1. Record the agent-native command-surface reset as ADR 007. - Records the forward OrthoConfig dependency boundary that 12.1 validates. @@ -131,7 +135,8 @@ OrthoConfig command contracts without duplicating them. See ADR 007 and This step answers whether the same command contract can serve humans and agents without forking command behaviour. It migrates prototype archive work 3.2.2 through 3.3.4 and 11.3.1 through 11.3.4. See `docs/weaver-design.md` §§2.1.3, -2.1.7, 2.1.10, and 2.1.11. +2.1.7, 2.1.10, and 2.1.11. Boundary classifications for these tasks are tracked +in the [OrthoConfig consumer boundary matrix](orthoconfig-consumer-boundary.md). - [ ] 13.2.1. Implement the localized human renderer for `definitions get`. - Requires 13.1.2 and depends on OrthoConfig 7.2.2. @@ -158,7 +163,9 @@ through 3.3.4 and 11.3.1 through 11.3.4. See `docs/weaver-design.md` §§2.1.3, This step answers whether humans and agents can discover the command contract from generated surfaces instead of hard-coded catalogues. It migrates prototype archive work 3.2.3 through 3.2.6, 5.7.1 through 5.7.5, and 11.3.2. See -`docs/weaver-design.md` §§2.1.4 and 6.1. +`docs/weaver-design.md` §§2.1.4 and 6.1. Boundary classifications for these +tasks are tracked in the +[OrthoConfig consumer boundary matrix](orthoconfig-consumer-boundary.md). - [ ] 13.3.1. Implement `weaver context --json` for the pilot command family. - Requires 13.2.3 and depends on OrthoConfig 6.2.1 through 6.2.3. @@ -242,6 +249,8 @@ cards-first context, same-file graph slices, and agent-grade compact output. This step answers whether existing semantic backends fit the resource-first surface without provider-specific commands. It migrates prototype archive work 10.1.1 through 10.3.2. See `docs/weaver-design.md` §§2.2, 3.1, and 6.1. +Boundary classifications for these tasks are tracked in the +[OrthoConfig consumer boundary matrix](orthoconfig-consumer-boundary.md). - [ ] 14.1.1. Implement `weaver definitions get`. - Requires phase 13. @@ -266,7 +275,9 @@ surface without provider-specific commands. It migrates prototype archive work This step answers whether compact symbol cards are enough to guide a user or agent to the next useful read. It migrates prototype archive work 7.1.1 through 7.1.4, 9.2.1 through 9.2.3, and 10.2.1 through 10.2.2. See -`docs/jacquard-card-first-symbol-graph-design.md` §§5-11. +`docs/jacquard-card-first-symbol-graph-design.md` §§5-11. Boundary +classifications for these tasks are tracked in the +[OrthoConfig consumer boundary matrix](orthoconfig-consumer-boundary.md). - [ ] 14.2.1. Implement `weaver cards get` for position references. - Requires 14.1.1 and reuses prototype archive work 7.1.1 through 7.1.4. @@ -286,6 +297,8 @@ agent to the next useful read. It migrates prototype archive work 7.1.1 through This step answers whether structural grep should graduate now or wait for the Sempai selector slice. It migrates the product intent of prototype archive work 10.4.1 through 10.4.2 and 5.5.1 without forcing provider-specific commands. +Boundary classifications for these tasks are tracked in the +[OrthoConfig consumer boundary matrix](orthoconfig-consumer-boundary.md). - [ ] 14.3.1. Prototype `weaver symbols list --pattern` over `weaver-syntax` and optional `srgn`. @@ -315,7 +328,9 @@ cards, and the Sempai-to-Jacquard vertical slice. This step answers whether the target one-liner grammar can select real symbols without overbuilding the full query engine. It migrates prototype archive work 4.1.6 through 4.1.7, 4.3.3, and 9.1.1. See -`docs/sempai-query-language-design.md` §§3-6. +`docs/sempai-query-language-design.md` §§3-6. Boundary classifications for +these tasks are tracked in the +[OrthoConfig consumer boundary matrix](orthoconfig-consumer-boundary.md). - [ ] 15.1.1. Implement one-liner tokenization and Pratt parsing for positive symbol patterns. @@ -407,7 +422,9 @@ This step answers whether the backend can be exposed through Weaver's resource-first command surface with stable schemas, cache behaviour, diagnostics, quality gates, and default-enablement rules. It preserves prototype archive work 4.3.1 through 4.3.9 under the new public command shape. -See `docs/weaver-design.md` §2.1.2. +See `docs/weaver-design.md` §2.1.2. Boundary classifications for these tasks +are tracked in the +[OrthoConfig consumer boundary matrix](orthoconfig-consumer-boundary.md). - [ ] 15.3.1. Add Sempai execution routing in `weaverd` for selector-backed `symbols list`. @@ -494,6 +511,8 @@ the symbol-relocation tasks below. This step answers whether the completed patch and Double-Lock foundations fit the resource-first grammar. It migrates prototype archive work 6.1.1 through 6.1.4 and 11.4.1 through 11.4.2. See `docs/weaver-design.md` §§4.2-4.3. +Boundary classifications for these tasks are tracked in the +[OrthoConfig consumer boundary matrix](orthoconfig-consumer-boundary.md). - [ ] 16.1.1. Implement `weaver patches apply`. - Requires phase 13 and reuses prototype archive work 6.1.1 through 6.1.4. @@ -516,6 +535,8 @@ the resource-first grammar. It migrates prototype archive work 6.1.1 through This step answers whether provider-hidden actuators can mutate safely from both direct references and Sempai streams. It migrates prototype archive work 5.2.1 through 5.2.6, 10.5.1 through 10.5.2, and 4.3.5. See ADR 001 and ADR 004. +Boundary classifications for these tasks are tracked in the +[OrthoConfig consumer boundary matrix](orthoconfig-consumer-boundary.md). - [ ] 16.2.1. Implement `weaver symbols rename` for position references. - Requires 16.1.3 and reuses prototype archive work 5.2.1 through 5.2.5. @@ -656,6 +677,8 @@ This step answers whether graph traversal gives enough extra value to justify its complexity. It preserves the completed prototype archive schema work 7.2.1 and migrates prototype archive work 7.2.2 through 7.2.5, 11.1.1, and 11.2.2. See `docs/jacquard-card-first-symbol-graph-design.md` §12.1 through §12.3. +Boundary classifications for these tasks are tracked in the +[OrthoConfig consumer boundary matrix](orthoconfig-consumer-boundary.md). - [ ] 17.1.1. Implement a two-pass Tree-sitter extraction pipeline for graph slices. @@ -694,7 +717,8 @@ and migrates prototype archive work 7.2.2 through 7.2.5, 11.1.1, and 11.2.2. See This step answers whether historical graph reconstruction is stable enough to support change-risk narratives. It migrates prototype archive work 7.3.1 through 7.3.5. See `docs/jacquard-card-first-symbol-graph-design.md` §13.1, -§13.2, and §22. +§13.2, and §22. Boundary classifications for these tasks are tracked in the +[OrthoConfig consumer boundary matrix](orthoconfig-consumer-boundary.md). - [ ] 17.2.1. Implement git-backed blob loading for historical revisions without checkout. @@ -852,7 +876,9 @@ prototype archive work 5.5.1, 5.6.1, and 5.8.1. See ADR 001, ADR 004, and ADR This step answers whether humans and agents can debug provider selection when needed without making provider names normal command syntax. It migrates prototype archive work 5.7.1 through 5.7.5 and 5.2.6. See -`docs/weaver-design.md` §6.1. +`docs/weaver-design.md` §6.1. Boundary classifications for these tasks are +tracked in the +[OrthoConfig consumer boundary matrix](orthoconfig-consumer-boundary.md). - [ ] 18.2.1. Wire provider summaries into `capabilities list` and `context --json`. @@ -886,7 +912,9 @@ formal verification, output delivery, feedback, profiles, and jobs. This step answers whether profiles, jobs, and delivery reduce repeated agent turns without making human usage worse. It migrates the product intent of ADR 007 compounding primitives and prototype archive work 6.2.1. See -`docs/weaver-design.md` §§2.1.5-2.1.6. +`docs/weaver-design.md` §§2.1.5-2.1.6. Boundary classifications for these tasks +are tracked in the +[OrthoConfig consumer boundary matrix](orthoconfig-consumer-boundary.md). - [ ] 19.1.1. Implement profile storage, redaction, and root `--profile`. - Requires 13.3.1 and depends on OrthoConfig 9.1. @@ -912,7 +940,9 @@ turns without making human usage worse. It migrates the product intent of ADR This step answers whether richer human workflows can exist without weakening the non-interactive agent contract. It migrates prototype archive work 6.2.1 -through 6.2.3. See `docs/weaver-design.md` §§5-6. +through 6.2.3. See `docs/weaver-design.md` §§5-6. Boundary classifications for +these tasks are tracked in the +[OrthoConfig consumer boundary matrix](orthoconfig-consumer-boundary.md). - [ ] 19.2.1. Recast project onboarding as a composition of resource-first commands. @@ -970,7 +1000,8 @@ the core Weaver promise. This step answers whether external integration surfaces are wrappers over the same command metadata or a distracting second product. See ADR 007 and -OrthoConfig 10.1. +OrthoConfig 10.1. Boundary classifications for these tasks are tracked in the +[OrthoConfig consumer boundary matrix](orthoconfig-consumer-boundary.md). - [ ] 20.1.1. Decide whether to generate MCP descriptions from `context --json`. @@ -1011,7 +1042,9 @@ This step answers whether optional observability surfaces have earned a new design after the local-first RFC 0001 contract exists. It keeps metrics, distributed tracing, retained diagnostics, and status expansion out of the core promise until their privacy, retention, endpoint, and command-latency costs are -explicit. See RFC 0001: +explicit. Boundary classifications for these tasks are tracked in the +[OrthoConfig consumer boundary matrix](orthoconfig-consumer-boundary.md). See +RFC 0001: - [Open questions](rfcs/0001-o11y.md#open-questions) - [Local request correlation](rfcs/0001-o11y.md#local-request-correlation) From 6dda698ea2737404ad74e1c85fdd86adc0be154e Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 14 Jun 2026 07:19:48 +0200 Subject: [PATCH 05/22] Gate OrthoConfig boundary documentation Add integration tests that validate the boundary manifest against the roadmap, ADR 007 anchors, state-specific evidence fields, and the committed Markdown matrix. Keep the renderer aligned with the formatter-stable matrix output and record Stage D validation plus CodeRabbit follow-up in the ExecPlan. --- crates/weaver-docs-gate/src/renderer.rs | 8 +- .../tests/boundary_manifest.rs | 305 ++++++++++++++++++ ...-1-1-track-downstream-consumer-boundary.md | 19 +- 3 files changed, 327 insertions(+), 5 deletions(-) create mode 100644 crates/weaver-docs-gate/tests/boundary_manifest.rs diff --git a/crates/weaver-docs-gate/src/renderer.rs b/crates/weaver-docs-gate/src/renderer.rs index 609214e9..6eab7f11 100644 --- a/crates/weaver-docs-gate/src/renderer.rs +++ b/crates/weaver-docs-gate/src/renderer.rs @@ -42,7 +42,7 @@ impl ColumnWidths { state: 5, upstream: 25, shipped_in: 10, - gate_or_divergence: 27, + gate_or_divergence: 26, next_review_by: 14, last_reviewed: 13, } @@ -92,9 +92,9 @@ pub fn render_matrix(manifest: &BoundaryManifest) -> String { "# OrthoConfig consumer boundary\n\n", "\n\n", "This matrix is generated from ", - "`docs/orthoconfig-consumer-boundary.toml`. Do not edit the table\n", - "by hand; update the manifest and regenerate it with `cargo run -p\n", - "weaver-docs-gate --example render_boundary_matrix --\n", + "`docs/orthoconfig-consumer-boundary.toml`. Do not\n", + "edit the table by hand; update the manifest and regenerate it with\n", + "`cargo run -p weaver-docs-gate --example render_boundary_matrix -- ", "docs/orthoconfig-consumer-boundary.toml ", "docs/orthoconfig-consumer-boundary.md`.\n\n", "The matrix tracks every live Weaver command-contract roadmap task ", diff --git a/crates/weaver-docs-gate/tests/boundary_manifest.rs b/crates/weaver-docs-gate/tests/boundary_manifest.rs new file mode 100644 index 00000000..816864c8 --- /dev/null +++ b/crates/weaver-docs-gate/tests/boundary_manifest.rs @@ -0,0 +1,305 @@ +//! Integration tests for the `OrthoConfig` consumer boundary manifest. + +use std::collections::BTreeSet; + +use camino::{Utf8Path, Utf8PathBuf}; +use cap_std::{ambient_authority, fs::Dir}; +use weaver_docs_gate::{ + BoundaryManifest, + BoundaryState, + BoundaryTask, + load_manifest, + render_matrix, +}; + +const ADR_007: &str = "docs/adr-007-agent-native-command-surface.md"; +const MANIFEST: &str = "docs/orthoconfig-consumer-boundary.toml"; +const MATRIX: &str = "docs/orthoconfig-consumer-boundary.md"; +const ROADMAP: &str = "docs/roadmap.md"; + +type TestResult = Result; + +#[test] +fn manifest_registry_matches_rows_and_roadmap_tasks() -> TestResult { + let manifest = manifest().map_err(|error| format!("load boundary manifest: {error}"))?; + let row_ids = manifest + .tasks + .iter() + .map(|task| task.id.as_str()) + .collect::>(); + let managed_ids = manifest + .managed_tasks + .iter() + .map(String::as_str) + .collect::>(); + + ensure_equal(&managed_ids, &row_ids, "managed_tasks must match task rows")?; + ensure_unique(&managed_ids, "managed_tasks")?; + + let roadmap = read_doc(ROADMAP).map_err(|error| format!("read {ROADMAP}: {error}"))?; + let roadmap_ids = roadmap_task_ids(&roadmap); + for task_id in managed_ids { + ensure( + roadmap_ids.contains(task_id), + format!("manifest task {task_id} is missing from {ROADMAP}"), + )?; + } + + Ok(()) +} + +#[test] +fn boundary_state_rows_have_required_evidence() -> TestResult { + let manifest = manifest().map_err(|error| format!("load boundary manifest: {error}"))?; + + for task in &manifest.tasks { + validate_date(&task.last_reviewed, "last_reviewed", task)?; + ensure( + !task.upstream.is_empty(), + format!("task {} must name at least one upstream reference", task.id), + )?; + + match task.state { + BoundaryState::Consumes => validate_consumes_evidence(task), + BoundaryState::Wraps => validate_wraps_evidence(task), + BoundaryState::Pending => validate_pending_evidence(task), + BoundaryState::Divergent => validate_divergence_evidence(task), + }?; + } + + Ok(()) +} + +#[test] +fn divergent_rows_reference_existing_adr_007_anchors() -> TestResult { + let manifest = manifest().map_err(|error| format!("load boundary manifest: {error}"))?; + let adr = read_doc(ADR_007).map_err(|error| format!("read {ADR_007}: {error}"))?; + let anchors = heading_anchors(&adr); + + for task in manifest + .tasks + .iter() + .filter(|task| task.state == BoundaryState::Divergent) + { + let anchor = task + .adr_anchor + .as_deref() + .ok_or_else(|| format!("divergent task {} has no ADR anchor", task.id))?; + ensure( + anchors.contains(anchor), + format!( + "task {} references missing ADR 007 anchor {anchor}", + task.id + ), + )?; + } + + Ok(()) +} + +#[test] +fn committed_matrix_matches_manifest_rendering() -> TestResult { + let manifest = manifest().map_err(|error| format!("load boundary manifest: {error}"))?; + let expected = render_matrix(&manifest); + let actual = read_doc(MATRIX).map_err(|error| format!("read {MATRIX}: {error}"))?; + + ensure_equal( + &expected, + &actual, + format!("{MATRIX} is not generated from {MANIFEST}"), + ) +} + +fn manifest() -> TestResult { + load_manifest(&repo_path(MANIFEST)?).map_err(|error| error.to_string()) +} + +fn read_doc(doc_path: &str) -> TestResult { + let resolved_path = repo_path(doc_path)?; + let parent = resolved_path.parent().unwrap_or_else(|| Utf8Path::new(".")); + let file_name = resolved_path + .file_name() + .ok_or_else(|| format!("invalid doc path: {resolved_path}"))?; + let dir = Dir::open_ambient_dir(parent, ambient_authority()) + .map_err(|error| format!("open {parent}: {error}"))?; + dir.read_to_string(file_name) + .map_err(|error| format!("read {resolved_path}: {error}")) +} + +fn repo_path(path: &str) -> TestResult { + let crate_dir = Utf8Path::new(env!("CARGO_MANIFEST_DIR")); + let repo_root = crate_dir + .parent() + .and_then(Utf8Path::parent) + .ok_or_else(|| format!("cannot resolve repository root from {crate_dir}"))?; + Ok(repo_root.join(path)) +} + +fn roadmap_task_ids(roadmap: &str) -> BTreeSet<&str> { + roadmap + .lines() + .filter_map(|line| line.trim_start().split_once("] ").map(|(_, task)| task)) + .filter_map(|task| task.split_once(". ").map(|(task_id, _)| task_id)) + .filter(|task_id| { + task_id + .chars() + .all(|char| char.is_ascii_digit() || char == '.') + }) + .collect() +} + +fn heading_anchors(document: &str) -> BTreeSet { + document + .lines() + .filter_map(|line| line.trim_start().strip_prefix("## ")) + .map(markdown_anchor) + .collect() +} + +fn markdown_anchor(heading: &str) -> String { + let mut anchor = String::new(); + let mut previous_was_dash = false; + + for char in heading.chars().flat_map(char::to_lowercase) { + if char.is_ascii_alphanumeric() { + anchor.push(char); + previous_was_dash = false; + } else if char.is_whitespace() || char == '-' { + push_dash(&mut anchor, &mut previous_was_dash); + } + } + + anchor.trim_matches('-').to_owned() +} + +fn push_dash(anchor: &mut String, previous_was_dash: &mut bool) { + if !*previous_was_dash && !anchor.is_empty() { + anchor.push('-'); + *previous_was_dash = true; + } +} + +fn ensure(condition: bool, message: String) -> TestResult { + if condition { Ok(()) } else { Err(message) } +} + +fn ensure_equal(left: &T, right: &T, message: impl Into) -> TestResult +where + T: std::fmt::Debug + PartialEq, +{ + if left == right { + Ok(()) + } else { + Err(format!( + "{}\nleft: {left:?}\nright: {right:?}", + message.into() + )) + } +} + +fn ensure_unique(values: &[&str], label: &str) -> TestResult { + let mut seen = BTreeSet::new(); + for value in values { + ensure( + seen.insert(*value), + format!("{label} contains duplicate value {value}"), + )?; + } + Ok(()) +} + +fn validate_consumes_evidence(task: &BoundaryTask) -> TestResult { + ensure( + task.shipped_in.is_some(), + format!("consumes task {} must name shipped_in", task.id), + )?; + ensure( + task.removal_gate.is_none(), + format!("consumes task {} must not carry removal_gate", task.id), + )?; + ensure( + task.adr_anchor.is_none(), + format!("consumes task {} must not carry adr_anchor", task.id), + )?; + ensure( + task.next_review_by.is_none(), + format!("consumes task {} must not carry next_review_by", task.id), + ) +} + +fn validate_wraps_evidence(task: &BoundaryTask) -> TestResult { + ensure( + task.removal_gate.is_some(), + format!("wraps task {} must name a removal gate", task.id), + )?; + ensure( + task.shipped_in.is_none(), + format!("wraps task {} must not carry shipped_in", task.id), + )?; + ensure( + task.adr_anchor.is_none(), + format!("wraps task {} must not carry adr_anchor", task.id), + )?; + ensure( + task.next_review_by.is_none(), + format!("wraps task {} must not carry next_review_by", task.id), + ) +} + +fn validate_pending_evidence(task: &BoundaryTask) -> TestResult { + let next_review_by = task + .next_review_by + .as_deref() + .ok_or_else(|| format!("pending task {} must provide next_review_by", task.id))?; + validate_date(next_review_by, "next_review_by", task)?; + ensure( + task.shipped_in.is_none(), + format!("pending task {} must not carry shipped_in", task.id), + )?; + ensure( + task.removal_gate.is_none(), + format!("pending task {} must not carry removal_gate", task.id), + )?; + ensure( + task.adr_anchor.is_none(), + format!("pending task {} must not carry adr_anchor", task.id), + ) +} + +fn validate_divergence_evidence(task: &BoundaryTask) -> TestResult { + ensure( + task.adr_anchor.is_some(), + format!("divergent task {} must name an ADR 007 anchor", task.id), + )?; + ensure( + task.shipped_in.is_none(), + format!("divergent task {} must not carry shipped_in", task.id), + )?; + ensure( + task.removal_gate.is_none(), + format!("divergent task {} must not carry removal_gate", task.id), + )?; + ensure( + task.next_review_by.is_none(), + format!("divergent task {} must not carry next_review_by", task.id), + ) +} + +fn validate_date(value: &str, field: &str, task: &BoundaryTask) -> TestResult { + ensure( + is_iso_date(value), + format!("task {} has invalid {field} date {value:?}", task.id), + ) +} + +fn is_iso_date(value: &str) -> bool { + value.len() == 10 + && value + .chars() + .enumerate() + .all(|(index, char)| matches!(index, 4 | 7) == (char == '-')) + && value + .chars() + .filter(|char| *char != '-') + .all(|char| char.is_ascii_digit()) +} diff --git a/docs/execplans/12-1-1-track-downstream-consumer-boundary.md b/docs/execplans/12-1-1-track-downstream-consumer-boundary.md index b96862d5..60f10c37 100644 --- a/docs/execplans/12-1-1-track-downstream-consumer-boundary.md +++ b/docs/execplans/12-1-1-track-downstream-consumer-boundary.md @@ -224,7 +224,16 @@ state, not the intended sequence. matrix and the TOML source of truth from Stage A. Validation passed with `make fmt`, `make markdownlint`, `make nixie`, and CodeRabbit `findings: 0`. -- [ ] Stage D: add the referential-integrity test gate and snapshot. +- [x] 2026-06-14T05:36:00Z: Stage D added + `crates/weaver-docs-gate/tests/boundary_manifest.rs`, covering + manifest registry order, roadmap task references, state-specific + evidence fields, ADR 007 divergence anchors, and byte-for-byte matrix + regeneration. Validation passed with `cargo test -p weaver-docs-gate`, + `make fmt`, `make check-fmt`, `make lint`, `make test`, + `make markdownlint`, and `make nixie`. The first CodeRabbit pass found + five valid test-diagnostic issues; after refactoring the tests to return + `Result<(), String>` with explicit validation errors, the repeat review + completed with `findings: 0`. - [ ] Stage E: refresh the users' guide, run the full quality gates, and run `coderabbit review --agent`. @@ -255,6 +264,14 @@ Recorded as they occur during implementation. Format: `OrthoConfig 9.2.2`. Impact: the manifest now uses the leaf task identifiers consistently before generated matrix publication. +- Observation: the matrix drift test must compare against the Markdown shape + after repository formatting, not only the raw renderer's first draft. + Evidence: the first Stage D focused test run passed integrity checks but + failed `committed_matrix_matches_manifest_rendering` because the renderer + emitted pre-`mdtablefix` paragraph wrapping and a one-character wider final + table column. + Impact: the renderer now emits the formatter-stable matrix text that is + committed, making future manifest or renderer drift deterministic. ## Decision log From 6e7f307f61c5b6a3e4a02c6889ec0ce9b7f1d7a7 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 14 Jun 2026 07:43:11 +0200 Subject: [PATCH 06/22] Explain OrthoConfig boundary states to users Document the temporary wrapper and pending-contract states users may encounter before the 0.1.0 command-surface compatibility promise. Record the final Stage E gates and clean CodeRabbit review in the ExecPlan. --- .../12-1-1-track-downstream-consumer-boundary.md | 8 ++++++-- docs/users-guide.md | 8 ++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/execplans/12-1-1-track-downstream-consumer-boundary.md b/docs/execplans/12-1-1-track-downstream-consumer-boundary.md index 60f10c37..36609e74 100644 --- a/docs/execplans/12-1-1-track-downstream-consumer-boundary.md +++ b/docs/execplans/12-1-1-track-downstream-consumer-boundary.md @@ -234,8 +234,12 @@ state, not the intended sequence. five valid test-diagnostic issues; after refactoring the tests to return `Result<(), String>` with explicit validation errors, the repeat review completed with `findings: 0`. -- [ ] Stage E: refresh the users' guide, run the full quality gates, and run - `coderabbit review --agent`. +- [x] 2026-06-14T06:28:00Z: Stage E refreshed + `docs/users-guide.md` with the operator-facing boundary note for + temporary wrappers and pending OrthoConfig contracts. Validation passed + with `make fmt`, `make check-fmt`, `make lint`, `make test`, + `make markdownlint`, and `make nixie` before the final CodeRabbit + review. Final CodeRabbit completed with `findings: 0`. ## Surprises & discoveries diff --git a/docs/users-guide.md b/docs/users-guide.md index 3f76612f..f73d7799 100644 --- a/docs/users-guide.md +++ b/docs/users-guide.md @@ -31,6 +31,14 @@ agent usage reliable: skill, and execution-ledger contracts depend on OrthoConfig where the OrthoConfig roadmap already owns the generic machinery. +The [OrthoConfig consumer boundary matrix](orthoconfig-consumer-boundary.md) +tracks which target command-contract tasks already consume OrthoConfig, which +ones use temporary Weaver wrappers, and which ones are pending upstream +OrthoConfig contracts. While those pending and wrapper rows remain open, users +may see Weaver-owned help, output, profile, delivery, feedback, or job-ledger +behaviour that is expected to converge on the named OrthoConfig contract before +the 0.1.0 compatibility promise is made. + Representative target commands look like this: ```sh From d747396f551b31d8a8d41102faea9eba3e207e1f Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 14 Jun 2026 18:01:41 +0200 Subject: [PATCH 07/22] Type boundary manifest test helpers Replace path-string helper parameters with Utf8Path inputs and field-name labels with a small enum so the docs-gate tests model those arguments with more precise types. --- .../tests/boundary_manifest.rs | 50 ++++++++++++++----- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/crates/weaver-docs-gate/tests/boundary_manifest.rs b/crates/weaver-docs-gate/tests/boundary_manifest.rs index 816864c8..df952148 100644 --- a/crates/weaver-docs-gate/tests/boundary_manifest.rs +++ b/crates/weaver-docs-gate/tests/boundary_manifest.rs @@ -19,6 +19,23 @@ const ROADMAP: &str = "docs/roadmap.md"; type TestResult = Result; +#[derive(Clone, Copy)] +enum FieldName { + LastReviewed, + NextReviewBy, + ManagedTasks, +} + +impl FieldName { + const fn as_str(self) -> &'static str { + match self { + Self::LastReviewed => "last_reviewed", + Self::NextReviewBy => "next_review_by", + Self::ManagedTasks => "managed_tasks", + } + } +} + #[test] fn manifest_registry_matches_rows_and_roadmap_tasks() -> TestResult { let manifest = manifest().map_err(|error| format!("load boundary manifest: {error}"))?; @@ -34,9 +51,10 @@ fn manifest_registry_matches_rows_and_roadmap_tasks() -> TestResult { .collect::>(); ensure_equal(&managed_ids, &row_ids, "managed_tasks must match task rows")?; - ensure_unique(&managed_ids, "managed_tasks")?; + ensure_unique(&managed_ids, FieldName::ManagedTasks)?; - let roadmap = read_doc(ROADMAP).map_err(|error| format!("read {ROADMAP}: {error}"))?; + let roadmap = + read_doc(Utf8Path::new(ROADMAP)).map_err(|error| format!("read {ROADMAP}: {error}"))?; let roadmap_ids = roadmap_task_ids(&roadmap); for task_id in managed_ids { ensure( @@ -53,7 +71,7 @@ fn boundary_state_rows_have_required_evidence() -> TestResult { let manifest = manifest().map_err(|error| format!("load boundary manifest: {error}"))?; for task in &manifest.tasks { - validate_date(&task.last_reviewed, "last_reviewed", task)?; + validate_date(&task.last_reviewed, FieldName::LastReviewed, task)?; ensure( !task.upstream.is_empty(), format!("task {} must name at least one upstream reference", task.id), @@ -73,7 +91,8 @@ fn boundary_state_rows_have_required_evidence() -> TestResult { #[test] fn divergent_rows_reference_existing_adr_007_anchors() -> TestResult { let manifest = manifest().map_err(|error| format!("load boundary manifest: {error}"))?; - let adr = read_doc(ADR_007).map_err(|error| format!("read {ADR_007}: {error}"))?; + let adr = + read_doc(Utf8Path::new(ADR_007)).map_err(|error| format!("read {ADR_007}: {error}"))?; let anchors = heading_anchors(&adr); for task in manifest @@ -101,7 +120,8 @@ fn divergent_rows_reference_existing_adr_007_anchors() -> TestResult { fn committed_matrix_matches_manifest_rendering() -> TestResult { let manifest = manifest().map_err(|error| format!("load boundary manifest: {error}"))?; let expected = render_matrix(&manifest); - let actual = read_doc(MATRIX).map_err(|error| format!("read {MATRIX}: {error}"))?; + let actual = + read_doc(Utf8Path::new(MATRIX)).map_err(|error| format!("read {MATRIX}: {error}"))?; ensure_equal( &expected, @@ -111,10 +131,10 @@ fn committed_matrix_matches_manifest_rendering() -> TestResult { } fn manifest() -> TestResult { - load_manifest(&repo_path(MANIFEST)?).map_err(|error| error.to_string()) + load_manifest(&repo_path(Utf8Path::new(MANIFEST))?).map_err(|error| error.to_string()) } -fn read_doc(doc_path: &str) -> TestResult { +fn read_doc(doc_path: &Utf8Path) -> TestResult { let resolved_path = repo_path(doc_path)?; let parent = resolved_path.parent().unwrap_or_else(|| Utf8Path::new(".")); let file_name = resolved_path @@ -126,7 +146,7 @@ fn read_doc(doc_path: &str) -> TestResult { .map_err(|error| format!("read {resolved_path}: {error}")) } -fn repo_path(path: &str) -> TestResult { +fn repo_path(path: &Utf8Path) -> TestResult { let crate_dir = Utf8Path::new(env!("CARGO_MANIFEST_DIR")); let repo_root = crate_dir .parent() @@ -197,12 +217,12 @@ where } } -fn ensure_unique(values: &[&str], label: &str) -> TestResult { +fn ensure_unique(values: &[&str], label: FieldName) -> TestResult { let mut seen = BTreeSet::new(); for value in values { ensure( seen.insert(*value), - format!("{label} contains duplicate value {value}"), + format!("{} contains duplicate value {value}", label.as_str()), )?; } Ok(()) @@ -251,7 +271,7 @@ fn validate_pending_evidence(task: &BoundaryTask) -> TestResult { .next_review_by .as_deref() .ok_or_else(|| format!("pending task {} must provide next_review_by", task.id))?; - validate_date(next_review_by, "next_review_by", task)?; + validate_date(next_review_by, FieldName::NextReviewBy, task)?; ensure( task.shipped_in.is_none(), format!("pending task {} must not carry shipped_in", task.id), @@ -285,10 +305,14 @@ fn validate_divergence_evidence(task: &BoundaryTask) -> TestResult { ) } -fn validate_date(value: &str, field: &str, task: &BoundaryTask) -> TestResult { +fn validate_date(value: &str, field: FieldName, task: &BoundaryTask) -> TestResult { ensure( is_iso_date(value), - format!("task {} has invalid {field} date {value:?}", task.id), + format!( + "task {} has invalid {} date {value:?}", + task.id, + field.as_str() + ), ) } From 3a3cca93f7acf43ae8f53719d114defe7a08bca9 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 14 Jun 2026 18:04:35 +0200 Subject: [PATCH 08/22] Extract boundary field constraint checks Share the repeated optional-field validation used by the boundary manifest state evidence tests, and let the basic assertion helper accept string-like messages directly. --- .../tests/boundary_manifest.rs | 97 ++++++++++--------- 1 file changed, 51 insertions(+), 46 deletions(-) diff --git a/crates/weaver-docs-gate/tests/boundary_manifest.rs b/crates/weaver-docs-gate/tests/boundary_manifest.rs index df952148..6fa70a7e 100644 --- a/crates/weaver-docs-gate/tests/boundary_manifest.rs +++ b/crates/weaver-docs-gate/tests/boundary_manifest.rs @@ -199,8 +199,12 @@ fn push_dash(anchor: &mut String, previous_was_dash: &mut bool) { } } -fn ensure(condition: bool, message: String) -> TestResult { - if condition { Ok(()) } else { Err(message) } +fn ensure(condition: bool, message: impl Into) -> TestResult { + if condition { + Ok(()) + } else { + Err(message.into()) + } } fn ensure_equal(left: &T, right: &T, message: impl Into) -> TestResult @@ -228,41 +232,48 @@ fn ensure_unique(values: &[&str], label: FieldName) -> TestResult { Ok(()) } -fn validate_consumes_evidence(task: &BoundaryTask) -> TestResult { - ensure( - task.shipped_in.is_some(), - format!("consumes task {} must name shipped_in", task.id), - )?; - ensure( - task.removal_gate.is_none(), - format!("consumes task {} must not carry removal_gate", task.id), - )?; +fn validate_field_constraints( + task: &BoundaryTask, + state: &str, + required: (&str, bool), + forbidden: &[(&str, bool)], +) -> TestResult { ensure( - task.adr_anchor.is_none(), - format!("consumes task {} must not carry adr_anchor", task.id), + required.1, + format!("{state} task {} must name {}", task.id, required.0), )?; - ensure( - task.next_review_by.is_none(), - format!("consumes task {} must not carry next_review_by", task.id), + for &(field, is_some) in forbidden { + ensure( + !is_some, + format!("{state} task {} must not carry {field}", task.id), + )?; + } + Ok(()) +} + +fn validate_consumes_evidence(task: &BoundaryTask) -> TestResult { + validate_field_constraints( + task, + "consumes", + ("shipped_in", task.shipped_in.is_some()), + &[ + ("removal_gate", task.removal_gate.is_some()), + ("adr_anchor", task.adr_anchor.is_some()), + ("next_review_by", task.next_review_by.is_some()), + ], ) } fn validate_wraps_evidence(task: &BoundaryTask) -> TestResult { - ensure( - task.removal_gate.is_some(), - format!("wraps task {} must name a removal gate", task.id), - )?; - ensure( - task.shipped_in.is_none(), - format!("wraps task {} must not carry shipped_in", task.id), - )?; - ensure( - task.adr_anchor.is_none(), - format!("wraps task {} must not carry adr_anchor", task.id), - )?; - ensure( - task.next_review_by.is_none(), - format!("wraps task {} must not carry next_review_by", task.id), + validate_field_constraints( + task, + "wraps", + ("removal_gate", task.removal_gate.is_some()), + &[ + ("shipped_in", task.shipped_in.is_some()), + ("adr_anchor", task.adr_anchor.is_some()), + ("next_review_by", task.next_review_by.is_some()), + ], ) } @@ -287,21 +298,15 @@ fn validate_pending_evidence(task: &BoundaryTask) -> TestResult { } fn validate_divergence_evidence(task: &BoundaryTask) -> TestResult { - ensure( - task.adr_anchor.is_some(), - format!("divergent task {} must name an ADR 007 anchor", task.id), - )?; - ensure( - task.shipped_in.is_none(), - format!("divergent task {} must not carry shipped_in", task.id), - )?; - ensure( - task.removal_gate.is_none(), - format!("divergent task {} must not carry removal_gate", task.id), - )?; - ensure( - task.next_review_by.is_none(), - format!("divergent task {} must not carry next_review_by", task.id), + validate_field_constraints( + task, + "divergent", + ("adr_anchor", task.adr_anchor.is_some()), + &[ + ("shipped_in", task.shipped_in.is_some()), + ("removal_gate", task.removal_gate.is_some()), + ("next_review_by", task.next_review_by.is_some()), + ], ) } From c73e831826e49fcfd1b1cee07950ff6146e05990 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 14 Jun 2026 22:49:53 +0200 Subject: [PATCH 09/22] Consolidate boundary state evidence checks Replace the duplicated state-specific wrapper functions in the docs-gate boundary manifest tests with a single dispatcher that preserves the existing field constraint rules for each state. --- .../tests/boundary_manifest.rs | 74 +++++++++---------- 1 file changed, 33 insertions(+), 41 deletions(-) diff --git a/crates/weaver-docs-gate/tests/boundary_manifest.rs b/crates/weaver-docs-gate/tests/boundary_manifest.rs index 6fa70a7e..cafbc882 100644 --- a/crates/weaver-docs-gate/tests/boundary_manifest.rs +++ b/crates/weaver-docs-gate/tests/boundary_manifest.rs @@ -77,12 +77,7 @@ fn boundary_state_rows_have_required_evidence() -> TestResult { format!("task {} must name at least one upstream reference", task.id), )?; - match task.state { - BoundaryState::Consumes => validate_consumes_evidence(task), - BoundaryState::Wraps => validate_wraps_evidence(task), - BoundaryState::Pending => validate_pending_evidence(task), - BoundaryState::Divergent => validate_divergence_evidence(task), - }?; + validate_state_evidence(task)?; } Ok(()) @@ -251,32 +246,6 @@ fn validate_field_constraints( Ok(()) } -fn validate_consumes_evidence(task: &BoundaryTask) -> TestResult { - validate_field_constraints( - task, - "consumes", - ("shipped_in", task.shipped_in.is_some()), - &[ - ("removal_gate", task.removal_gate.is_some()), - ("adr_anchor", task.adr_anchor.is_some()), - ("next_review_by", task.next_review_by.is_some()), - ], - ) -} - -fn validate_wraps_evidence(task: &BoundaryTask) -> TestResult { - validate_field_constraints( - task, - "wraps", - ("removal_gate", task.removal_gate.is_some()), - &[ - ("shipped_in", task.shipped_in.is_some()), - ("adr_anchor", task.adr_anchor.is_some()), - ("next_review_by", task.next_review_by.is_some()), - ], - ) -} - fn validate_pending_evidence(task: &BoundaryTask) -> TestResult { let next_review_by = task .next_review_by @@ -297,17 +266,40 @@ fn validate_pending_evidence(task: &BoundaryTask) -> TestResult { ) } -fn validate_divergence_evidence(task: &BoundaryTask) -> TestResult { - validate_field_constraints( - task, - "divergent", - ("adr_anchor", task.adr_anchor.is_some()), - &[ +fn validate_state_evidence(task: &BoundaryTask) -> TestResult { + match task.state { + BoundaryState::Consumes => validate_field_constraints( + task, + "consumes", ("shipped_in", task.shipped_in.is_some()), + &[ + ("removal_gate", task.removal_gate.is_some()), + ("adr_anchor", task.adr_anchor.is_some()), + ("next_review_by", task.next_review_by.is_some()), + ], + ), + BoundaryState::Wraps => validate_field_constraints( + task, + "wraps", ("removal_gate", task.removal_gate.is_some()), - ("next_review_by", task.next_review_by.is_some()), - ], - ) + &[ + ("shipped_in", task.shipped_in.is_some()), + ("adr_anchor", task.adr_anchor.is_some()), + ("next_review_by", task.next_review_by.is_some()), + ], + ), + BoundaryState::Divergent => validate_field_constraints( + task, + "divergent", + ("adr_anchor", task.adr_anchor.is_some()), + &[ + ("shipped_in", task.shipped_in.is_some()), + ("removal_gate", task.removal_gate.is_some()), + ("next_review_by", task.next_review_by.is_some()), + ], + ), + BoundaryState::Pending => validate_pending_evidence(task), + } } fn validate_date(value: &str, field: FieldName, task: &BoundaryTask) -> TestResult { From 291977acb8d2e741580fd5c007c32f30faa6de86 Mon Sep 17 00:00:00 2001 From: leynos Date: Tue, 16 Jun 2026 01:05:37 +0200 Subject: [PATCH 10/22] Record boundary implementation completion Mark roadmap task 12.1.1 complete and update the ExecPlan with the final implementation status, follow-up test refactors, validation evidence, and retrospective findings. --- ...-1-1-track-downstream-consumer-boundary.md | 51 +++++++++++++++++-- docs/roadmap.md | 9 +++- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/docs/execplans/12-1-1-track-downstream-consumer-boundary.md b/docs/execplans/12-1-1-track-downstream-consumer-boundary.md index 36609e74..1391390c 100644 --- a/docs/execplans/12-1-1-track-downstream-consumer-boundary.md +++ b/docs/execplans/12-1-1-track-downstream-consumer-boundary.md @@ -5,7 +5,7 @@ This ExecPlan (execution plan) is a living document. The sections `Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work proceeds. -Status: IN PROGRESS +Status: COMPLETE This document must be maintained in accordance with `AGENTS.md` at the repository root. @@ -240,6 +240,15 @@ state, not the intended sequence. with `make fmt`, `make check-fmt`, `make lint`, `make test`, `make markdownlint`, and `make nixie` before the final CodeRabbit review. Final CodeRabbit completed with `findings: 0`. +- [x] 2026-06-15T23:02:41Z: Follow-up code-quality refinements addressed + the `crates/weaver-docs-gate/tests/boundary_manifest.rs` biomarker + findings without changing the manifest gate's behaviour. The first + refactor typed path helper arguments as `&Utf8Path` and field labels + as `FieldName`; the second extracted shared optional-field constraint + validation; the third consolidated the state-specific wrappers into + `validate_state_evidence`. Validation passed after each commit with + `cargo test -p weaver-docs-gate`, `make check-fmt`, `make lint`, and + `make test`. ## Surprises & discoveries @@ -276,6 +285,14 @@ Recorded as they occur during implementation. Format: table column. Impact: the renderer now emits the formatter-stable matrix text that is committed, making future manifest or renderer drift deterministic. +- Observation: the final documentation gate tests were correct but initially + too string-heavy and locally repetitive. + Evidence: three follow-up commits changed helper signatures from path + strings to `&Utf8Path`, introduced the `FieldName` enum for manifest field + labels, extracted `validate_field_constraints`, and consolidated the + state-specific evidence wrappers into `validate_state_evidence`. + Impact: the test file now keeps the same assertions and diagnostics while + making the argument roles clearer and reducing duplication markers. ## Decision log @@ -354,12 +371,38 @@ Recorded for any decision that future work must respect. contributors and reviewers. If team capacity proves limiting, the lighter alternative remains a viable follow-up. Date/Author: 2026-06-07, Logisphere review by Wafflecat; deferred. +- Decision: keep the boundary gate refactors as test-internal helper changes + rather than changing the manifest schema or generated matrix. + Rationale: the requested refinements addressed maintainability biomarkers + in `crates/weaver-docs-gate/tests/boundary_manifest.rs`; the observable + contract remains the TOML manifest, generated Markdown matrix, and + referential-integrity assertions. Date/Author: 2026-06-15, + implementation. ## Outcomes & retrospective -Recorded at completion. Includes whether the gate caught a regression in -practice, how often the manifest needed updates during subsequent tasks, and -any vocabulary or schema adjustments. +Roadmap 12.1.1 is complete. The branch added the OrthoConfig consumer +boundary vocabulary to ADR 007, introduced the TOML boundary manifest and +generated Markdown matrix, linked the matrix from the roadmap and contributor +documentation, documented user-facing temporary wrapper and pending-contract +states, and added the `weaver-docs-gate` parser, renderer, regenerator, and +integration tests. + +The gate already caught useful drift during implementation. CodeRabbit found +generic `OrthoConfig 9.2` upstream references before publication of the +matrix, and the matrix rendering test caught formatter-sensitive Markdown +output before the generated file was committed as stable. Subsequent +code-quality refinements kept the same behaviour while improving the +test-helper API and eliminating duplicate state-evidence wrappers. + +The final implementation uses a four-state vocabulary: `consumes`, `wraps`, +`pending`, and `divergent`. The fourth state, `pending`, is an intentional +schema adjustment from the roadmap's original three-state success wording; it +prevents undecided upstream contracts from being recorded as either temporary +wrappers or deliberate divergences. All final gates passed: +`cargo test -p weaver-docs-gate`, `make check-fmt`, `make lint`, `make test`, +`make markdownlint`, and `make nixie`. The final CodeRabbit review for the +main implementation completed with `findings: 0`. ## Context and orientation diff --git a/docs/roadmap.md b/docs/roadmap.md index a75d3ecd..701707ad 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -43,8 +43,15 @@ command-surface and renderer task. See ADR 007 and the OrthoConfig roadmap. Boundary classifications for this step are tracked in the [OrthoConfig consumer boundary matrix](orthoconfig-consumer-boundary.md). -- [ ] 12.1.1. Track the downstream consumer boundary. +- [x] 12.1.1. Track the downstream consumer boundary. - Depends on OrthoConfig 5.2.3. + - Implemented by ADR 007 boundary vocabulary, + `docs/orthoconfig-consumer-boundary.toml`, + `docs/orthoconfig-consumer-boundary.md`, and the + `weaver-docs-gate` manifest tests. + - Follow-up test refactors typed path and field-label helper arguments and + consolidated state evidence validation without changing the boundary + contract. - Success: every command-contract task says whether it consumes OrthoConfig, wraps it temporarily, or records a deliberate divergence in ADR 007. - [ ] 12.1.2. Consume recursive command metadata. From 48814d4f6d209ee609541c445e59f7a87fdae4f4 Mon Sep 17 00:00:00 2001 From: leynos Date: Tue, 16 Jun 2026 01:23:03 +0200 Subject: [PATCH 11/22] Enforce pending boundary review expiry Parse pending next_review_by dates in the docs-gate boundary manifest test and fail rows whose review date is more than 270 days behind the build date, using SOURCE_DATE_EPOCH when present for reproducible builds. Record the post-completion gate fix in the ExecPlan. --- Cargo.lock | 1 + Cargo.toml | 2 +- crates/weaver-docs-gate/Cargo.toml | 1 + .../tests/boundary_manifest.rs | 41 +++++----- .../tests/support/pending_review_date.rs | 77 +++++++++++++++++++ ...-1-1-track-downstream-consumer-boundary.md | 24 ++++++ 6 files changed, 126 insertions(+), 20 deletions(-) create mode 100644 crates/weaver-docs-gate/tests/support/pending_review_date.rs diff --git a/Cargo.lock b/Cargo.lock index 1abcd37f..2e6757c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3260,6 +3260,7 @@ dependencies = [ "cap-std 4.0.2", "serde", "thiserror 2.0.18", + "time", "toml 0.9.12+spec-1.1.0", ] diff --git a/Cargo.toml b/Cargo.toml index 5e6794ab..bf1244dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ sha2 = "0.10" saphyr = "0.0.6" tempfile = "3.10" thiserror = "2.0" -time = { version = "0.3", features = ["formatting"] } +time = { version = "0.3", features = ["formatting", "macros", "parsing"] } toml = "0.9.12" tracing = "0.1" tree-sitter = "0.25.10" diff --git a/crates/weaver-docs-gate/Cargo.toml b/crates/weaver-docs-gate/Cargo.toml index b83081cf..0de28bd4 100644 --- a/crates/weaver-docs-gate/Cargo.toml +++ b/crates/weaver-docs-gate/Cargo.toml @@ -10,6 +10,7 @@ cap-std = { workspace = true } camino = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } +time = { workspace = true } toml = { workspace = true } [lints] diff --git a/crates/weaver-docs-gate/tests/boundary_manifest.rs b/crates/weaver-docs-gate/tests/boundary_manifest.rs index cafbc882..02f6e09e 100644 --- a/crates/weaver-docs-gate/tests/boundary_manifest.rs +++ b/crates/weaver-docs-gate/tests/boundary_manifest.rs @@ -1,5 +1,8 @@ //! Integration tests for the `OrthoConfig` consumer boundary manifest. +#[path = "support/pending_review_date.rs"] +mod pending_review_date; + use std::collections::BTreeSet; use camino::{Utf8Path, Utf8PathBuf}; @@ -252,6 +255,7 @@ fn validate_pending_evidence(task: &BoundaryTask) -> TestResult { .as_deref() .ok_or_else(|| format!("pending task {} must provide next_review_by", task.id))?; validate_date(next_review_by, FieldName::NextReviewBy, task)?; + pending_review_date::validate_pending_review_date(next_review_by, &task.id)?; ensure( task.shipped_in.is_none(), format!("pending task {} must not carry shipped_in", task.id), @@ -267,49 +271,48 @@ fn validate_pending_evidence(task: &BoundaryTask) -> TestResult { } fn validate_state_evidence(task: &BoundaryTask) -> TestResult { - match task.state { - BoundaryState::Consumes => validate_field_constraints( - task, + let (state, required, forbidden) = match task.state { + BoundaryState::Consumes => ( "consumes", ("shipped_in", task.shipped_in.is_some()), - &[ + [ ("removal_gate", task.removal_gate.is_some()), ("adr_anchor", task.adr_anchor.is_some()), ("next_review_by", task.next_review_by.is_some()), ], ), - BoundaryState::Wraps => validate_field_constraints( - task, + BoundaryState::Wraps => ( "wraps", ("removal_gate", task.removal_gate.is_some()), - &[ + [ ("shipped_in", task.shipped_in.is_some()), ("adr_anchor", task.adr_anchor.is_some()), ("next_review_by", task.next_review_by.is_some()), ], ), - BoundaryState::Divergent => validate_field_constraints( - task, + BoundaryState::Divergent => ( "divergent", ("adr_anchor", task.adr_anchor.is_some()), - &[ + [ ("shipped_in", task.shipped_in.is_some()), ("removal_gate", task.removal_gate.is_some()), ("next_review_by", task.next_review_by.is_some()), ], ), - BoundaryState::Pending => validate_pending_evidence(task), - } + BoundaryState::Pending => return validate_pending_evidence(task), + }; + validate_field_constraints(task, state, required, &forbidden) } fn validate_date(value: &str, field: FieldName, task: &BoundaryTask) -> TestResult { - ensure( - is_iso_date(value), - format!( - "task {} has invalid {} date {value:?}", - task.id, - field.as_str() - ), + ensure(is_iso_date(value), invalid_date_message(value, field, task)) +} + +fn invalid_date_message(value: &str, field: FieldName, task: &BoundaryTask) -> String { + format!( + "task {} has invalid {} date {value:?}", + task.id, + field.as_str() ) } diff --git a/crates/weaver-docs-gate/tests/support/pending_review_date.rs b/crates/weaver-docs-gate/tests/support/pending_review_date.rs new file mode 100644 index 00000000..828b8e8a --- /dev/null +++ b/crates/weaver-docs-gate/tests/support/pending_review_date.rs @@ -0,0 +1,77 @@ +//! Pending dependency review-date expiry checks for the boundary manifest gate. + +use time::{Date, OffsetDateTime, format_description::well_known::Iso8601}; + +const PENDING_REVIEW_WINDOW_DAYS: i64 = 270; + +type TestResult = Result; + +fn ensure(condition: bool, message: impl Into) -> TestResult { + if condition { + Ok(()) + } else { + Err(message.into()) + } +} + +pub(crate) fn validate_pending_review_date(value: &str, task_id: &str) -> TestResult { + let review_date = parse_review_date(value, task_id)?; + let build_date = build_date()?; + ensure( + is_review_current(review_date, build_date), + format!( + "task {task_id} has stale next_review_by date {value:?}; pending reviews must be no \ + more than {PENDING_REVIEW_WINDOW_DAYS} days behind the build date", + ), + ) +} + +fn parse_review_date(value: &str, task_id: &str) -> TestResult { + Date::parse(value, &Iso8601::DATE).map_err(|_| invalid_date_message(value, task_id)) +} + +fn build_date() -> TestResult { + if let Ok(epoch) = std::env::var("SOURCE_DATE_EPOCH") { + let seconds = epoch + .parse::() + .map_err(|error| format!("SOURCE_DATE_EPOCH must be Unix seconds: {error}"))?; + OffsetDateTime::from_unix_timestamp(seconds) + .map(OffsetDateTime::date) + .map_err(|error| format!("SOURCE_DATE_EPOCH is outside supported range: {error}")) + } else { + Ok(OffsetDateTime::now_utc().date()) + } +} + +fn invalid_date_message(value: &str, task_id: &str) -> String { + format!("task {task_id} has invalid next_review_by date {value:?}") +} + +fn is_review_current(review_date: Date, build_date: Date) -> bool { + (build_date - review_date).whole_days() <= PENDING_REVIEW_WINDOW_DAYS +} + +#[cfg(test)] +mod tests { + //! Unit tests for pending review age-window comparisons. + + use time::macros::date; + + use super::is_review_current; + + #[test] + fn review_window_accepts_exact_boundary() { + assert!(is_review_current( + date!(2026 - 01 - 01), + date!(2026 - 09 - 28) + )); + } + + #[test] + fn review_window_rejects_expired_pending_review() { + assert!(!is_review_current( + date!(2026 - 01 - 01), + date!(2026 - 09 - 29) + )); + } +} diff --git a/docs/execplans/12-1-1-track-downstream-consumer-boundary.md b/docs/execplans/12-1-1-track-downstream-consumer-boundary.md index 1391390c..353ac27a 100644 --- a/docs/execplans/12-1-1-track-downstream-consumer-boundary.md +++ b/docs/execplans/12-1-1-track-downstream-consumer-boundary.md @@ -249,6 +249,12 @@ state, not the intended sequence. `validate_state_evidence`. Validation passed after each commit with `cargo test -p weaver-docs-gate`, `make check-fmt`, `make lint`, and `make test`. +- [x] 2026-06-15T23:20:03Z: The pending review expiry check now parses + `next_review_by` as a calendar date and fails pending rows that are + more than 270 days behind the build date. The build date comes from + `SOURCE_DATE_EPOCH` when set and from the wall clock otherwise. + Validation passed with `cargo test -p weaver-docs-gate`, + `make check-fmt`, `make lint`, and `make test`. ## Surprises & discoveries @@ -293,6 +299,13 @@ Recorded as they occur during implementation. Format: state-specific evidence wrappers into `validate_state_evidence`. Impact: the test file now keeps the same assertions and diagnostics while making the argument roles clearer and reducing duplication markers. +- Observation: the documented pending review staleness rule was not enforced + by the original Stage D test. + Evidence: `validate_pending_evidence` checked that `next_review_by` looked + like `YYYY-MM-DD` but did not compare it with the build date. + Impact: `crates/weaver-docs-gate/tests/support/pending_review_date.rs` now + parses the date with the `time` crate and rejects pending reviews more than + 270 days behind `SOURCE_DATE_EPOCH` or the wall-clock date. ## Decision log @@ -378,6 +391,12 @@ Recorded for any decision that future work must respect. contract remains the TOML manifest, generated Markdown matrix, and referential-integrity assertions. Date/Author: 2026-06-15, implementation. +- Decision: use the existing workspace `time` crate for pending review date + parsing and build-date comparison. + Rationale: manual calendar arithmetic conflicts with the repository's + strict integer-division and remainder lints, while `time::Date` and + `OffsetDateTime` express the policy directly and keep the gate portable. + Date/Author: 2026-06-15, implementation. ## Outcomes & retrospective @@ -404,6 +423,11 @@ wrappers or deliberate divergences. All final gates passed: `make markdownlint`, and `make nixie`. The final CodeRabbit review for the main implementation completed with `findings: 0`. +A post-completion fix closed the documented pending-review expiry gap. Pending +rows now fail once `next_review_by` is more than 270 days behind the build +date, using `SOURCE_DATE_EPOCH` for reproducible builds and the wall clock +otherwise. + ## Context and orientation A novice opening this plan needs the following landmarks before reading the From 9371c421a13bf634ef554c81b797f0d9190152e2 Mon Sep 17 00:00:00 2001 From: leynos Date: Sat, 20 Jun 2026 16:44:38 +0200 Subject: [PATCH 12/22] Harden docs gate boundary coverage Separate TOML deserialization into a private adapter layer so the public boundary vocabulary remains plain domain data. Optimise matrix rendering by grouping rows once, and add loader, renderer, property, snapshot, and example coverage for the docs gate review findings. Document the `weaver-docs-gate` public API in the developer guide so future command-contract changes know which types and functions own the boundary. --- Cargo.lock | 3 + crates/weaver-docs-gate/Cargo.toml | 5 + crates/weaver-docs-gate/src/lib.rs | 80 +++++-- .../weaver-docs-gate/src/manifest_adapter.rs | 129 ++++++++++ crates/weaver-docs-gate/src/renderer.rs | 153 ++++++++++-- .../weaver-docs-gate/tests/load_manifest.rs | 225 ++++++++++++++++++ docs/developers-guide.md | 37 +++ 7 files changed, 585 insertions(+), 47 deletions(-) create mode 100644 crates/weaver-docs-gate/src/manifest_adapter.rs create mode 100644 crates/weaver-docs-gate/tests/load_manifest.rs diff --git a/Cargo.lock b/Cargo.lock index 2e6757c1..96634594 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3258,7 +3258,10 @@ version = "0.1.0" dependencies = [ "camino", "cap-std 4.0.2", + "insta", + "proptest", "serde", + "tempfile", "thiserror 2.0.18", "time", "toml 0.9.12+spec-1.1.0", diff --git a/crates/weaver-docs-gate/Cargo.toml b/crates/weaver-docs-gate/Cargo.toml index 0de28bd4..bc5a40d2 100644 --- a/crates/weaver-docs-gate/Cargo.toml +++ b/crates/weaver-docs-gate/Cargo.toml @@ -13,5 +13,10 @@ thiserror = { workspace = true } time = { workspace = true } toml = { workspace = true } +[dev-dependencies] +insta = { workspace = true } +proptest = { workspace = true } +tempfile = { workspace = true } + [lints] workspace = true diff --git a/crates/weaver-docs-gate/src/lib.rs b/crates/weaver-docs-gate/src/lib.rs index 6575f5d8..16d64497 100644 --- a/crates/weaver-docs-gate/src/lib.rs +++ b/crates/weaver-docs-gate/src/lib.rs @@ -4,8 +4,8 @@ use std::io::{self, ErrorKind}; use camino::{Utf8Path, Utf8PathBuf}; use cap_std::{ambient_authority, fs::Dir}; -use serde::{Deserialize, Deserializer}; +mod manifest_adapter; mod renderer; pub use renderer::render_matrix; @@ -17,8 +17,7 @@ pub use renderer::render_matrix; /// /// assert_eq!(BoundaryState::Wraps.as_str(), "wraps"); /// ``` -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] -#[serde(rename_all = "snake_case")] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BoundaryState { /// Weaver follows an `OrthoConfig` contract that has shipped. Consumes, @@ -58,8 +57,7 @@ impl BoundaryState { /// /// assert_eq!(UpstreamRole::Renderer.as_str(), "renderer"); /// ``` -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] -#[serde(rename_all = "snake_case")] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum UpstreamRole { /// Consumer-boundary ownership and governance. Boundary, @@ -107,7 +105,18 @@ impl UpstreamRole { } /// A single upstream `OrthoConfig` task reference. -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +/// +/// # Examples +/// ``` +/// use weaver_docs_gate::{UpstreamRef, UpstreamRole}; +/// +/// let upstream = UpstreamRef { +/// task: "ortho-config:renderer-contract".into(), +/// role: UpstreamRole::Renderer, +/// }; +/// assert_eq!(upstream.role.as_str(), "renderer"); +/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] pub struct UpstreamRef { /// The upstream roadmap task or stable design section. pub task: String, @@ -116,7 +125,25 @@ pub struct UpstreamRef { } /// One classified Weaver roadmap task. -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +/// +/// # Examples +/// ``` +/// use weaver_docs_gate::{BoundaryState, BoundaryTask}; +/// +/// let task = BoundaryTask { +/// id: "12.1.1".into(), +/// gist: "Track the downstream consumer boundary.".into(), +/// state: BoundaryState::Pending, +/// upstream: Vec::new(), +/// shipped_in: None, +/// removal_gate: None, +/// adr_anchor: None, +/// next_review_by: Some("2026-12-31".into()), +/// last_reviewed: "2026-06-20".into(), +/// }; +/// assert_eq!(task.state.as_str(), "pending"); +/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] pub struct BoundaryTask { /// Weaver roadmap task ID, such as `13.1.2`. pub id: String, @@ -127,30 +154,37 @@ pub struct BoundaryTask { /// Upstream `OrthoConfig` task references. pub upstream: Vec, /// `OrthoConfig` release tag or pinned SHA for shipped contracts. - #[serde(deserialize_with = "empty_string_as_none")] pub shipped_in: Option, /// Replacement condition for temporary wrappers. - #[serde(deserialize_with = "empty_string_as_none")] pub removal_gate: Option, /// ADR 007 heading slug for deliberate divergences. - #[serde(deserialize_with = "empty_string_as_none")] pub adr_anchor: Option, /// ISO-8601 review date for pending contracts. - #[serde(deserialize_with = "empty_string_as_none")] pub next_review_by: Option, /// ISO-8601 date when the row was last reviewed. pub last_reviewed: String, } /// The complete boundary manifest. -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +/// +/// # Examples +/// ``` +/// use weaver_docs_gate::BoundaryManifest; +/// +/// let manifest = BoundaryManifest { +/// schema_version: 1, +/// managed_tasks: Vec::new(), +/// tasks: Vec::new(), +/// }; +/// assert!(manifest.tasks.is_empty()); +/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] pub struct BoundaryManifest { /// Manifest schema version. pub schema_version: u32, /// Ordered registry of Weaver roadmap task IDs governed by the matrix. pub managed_tasks: Vec, /// Classified task rows. - #[serde(rename = "task")] pub tasks: Vec, } @@ -208,10 +242,14 @@ pub fn load_manifest(path: &Utf8Path) -> Result let contents = dir .read_to_string(file_name) .map_err(|source| read_error(path, source))?; - toml::from_str(&contents).map_err(|source| BoundaryError::InvalidToml { - path: path.to_path_buf(), - source: Box::new(source), - }) + let manifest = + toml::from_str::(&contents).map_err(|source| { + BoundaryError::InvalidToml { + path: path.to_path_buf(), + source: Box::new(source), + } + })?; + Ok(manifest.into()) } fn read_error(path: &Utf8Path, source: io::Error) -> BoundaryError { @@ -224,11 +262,3 @@ fn read_error(path: &Utf8Path, source: io::Error) -> BoundaryError { } } } - -fn empty_string_as_none<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let value = String::deserialize(deserializer)?; - Ok((!value.is_empty()).then_some(value)) -} diff --git a/crates/weaver-docs-gate/src/manifest_adapter.rs b/crates/weaver-docs-gate/src/manifest_adapter.rs new file mode 100644 index 00000000..0e8d367e --- /dev/null +++ b/crates/weaver-docs-gate/src/manifest_adapter.rs @@ -0,0 +1,129 @@ +//! TOML adapter for the `OrthoConfig` consumer boundary manifest. + +use serde::{Deserialize, Deserializer}; + +use crate::{BoundaryManifest, BoundaryState, BoundaryTask, UpstreamRef, UpstreamRole}; + +#[derive(Debug, Deserialize)] +pub(super) struct BoundaryManifestDto { + pub(super) schema_version: u32, + pub(super) managed_tasks: Vec, + #[serde(rename = "task")] + pub(super) tasks: Vec, +} + +#[derive(Debug, Deserialize)] +pub(super) struct BoundaryTaskDto { + pub(super) id: String, + pub(super) gist: String, + pub(super) state: BoundaryStateDto, + pub(super) upstream: Vec, + #[serde(deserialize_with = "empty_string_as_none")] + pub(super) shipped_in: Option, + #[serde(deserialize_with = "empty_string_as_none")] + pub(super) removal_gate: Option, + #[serde(deserialize_with = "empty_string_as_none")] + pub(super) adr_anchor: Option, + #[serde(deserialize_with = "empty_string_as_none")] + pub(super) next_review_by: Option, + pub(super) last_reviewed: String, +} + +#[derive(Debug, Deserialize)] +pub(super) struct UpstreamRefDto { + pub(super) task: String, + pub(super) role: UpstreamRoleDto, +} + +#[derive(Debug, Clone, Copy, Deserialize)] +#[serde(rename_all = "snake_case")] +pub(super) enum BoundaryStateDto { + Consumes, + Wraps, + Pending, + Divergent, +} + +#[derive(Debug, Clone, Copy, Deserialize)] +#[serde(rename_all = "snake_case")] +pub(super) enum UpstreamRoleDto { + Boundary, + Metadata, + CapabilityProvenance, + Vocabulary, + Renderer, + Profile, + Delivery, + Feedback, + ExecutionLedger, +} + +impl From for BoundaryManifest { + fn from(dto: BoundaryManifestDto) -> Self { + Self { + schema_version: dto.schema_version, + managed_tasks: dto.managed_tasks, + tasks: dto.tasks.into_iter().map(BoundaryTask::from).collect(), + } + } +} + +impl From for BoundaryTask { + fn from(dto: BoundaryTaskDto) -> Self { + Self { + id: dto.id, + gist: dto.gist, + state: BoundaryState::from(dto.state), + upstream: dto.upstream.into_iter().map(UpstreamRef::from).collect(), + shipped_in: dto.shipped_in, + removal_gate: dto.removal_gate, + adr_anchor: dto.adr_anchor, + next_review_by: dto.next_review_by, + last_reviewed: dto.last_reviewed, + } + } +} + +impl From for UpstreamRef { + fn from(dto: UpstreamRefDto) -> Self { + Self { + task: dto.task, + role: UpstreamRole::from(dto.role), + } + } +} + +impl From for BoundaryState { + fn from(dto: BoundaryStateDto) -> Self { + match dto { + BoundaryStateDto::Consumes => Self::Consumes, + BoundaryStateDto::Wraps => Self::Wraps, + BoundaryStateDto::Pending => Self::Pending, + BoundaryStateDto::Divergent => Self::Divergent, + } + } +} + +impl From for UpstreamRole { + fn from(dto: UpstreamRoleDto) -> Self { + match dto { + UpstreamRoleDto::Boundary => Self::Boundary, + UpstreamRoleDto::Metadata => Self::Metadata, + UpstreamRoleDto::CapabilityProvenance => Self::CapabilityProvenance, + UpstreamRoleDto::Vocabulary => Self::Vocabulary, + UpstreamRoleDto::Renderer => Self::Renderer, + UpstreamRoleDto::Profile => Self::Profile, + UpstreamRoleDto::Delivery => Self::Delivery, + UpstreamRoleDto::Feedback => Self::Feedback, + UpstreamRoleDto::ExecutionLedger => Self::ExecutionLedger, + } + } +} + +fn empty_string_as_none<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let value = String::deserialize(deserializer)?; + Ok((!value.is_empty()).then_some(value)) +} diff --git a/crates/weaver-docs-gate/src/renderer.rs b/crates/weaver-docs-gate/src/renderer.rs index 6eab7f11..58989b0a 100644 --- a/crates/weaver-docs-gate/src/renderer.rs +++ b/crates/weaver-docs-gate/src/renderer.rs @@ -1,5 +1,7 @@ //! Markdown rendering for the `OrthoConfig` consumer boundary matrix. +use std::collections::BTreeMap; + use crate::{BoundaryManifest, BoundaryState, BoundaryTask}; const NA: &str = "n/a"; @@ -104,8 +106,8 @@ pub fn render_matrix(manifest: &BoundaryManifest) -> String { "diverges under ADR 007.\n\n", )); - for phase in grouped_phases(manifest) { - push_phase(&mut rendered, phase, manifest); + for (phase, rows) in grouped_rows(manifest) { + push_phase(&mut rendered, phase, &rows); } while rendered.ends_with("\n\n") { @@ -116,32 +118,27 @@ pub fn render_matrix(manifest: &BoundaryManifest) -> String { rendered } -fn grouped_phases(manifest: &BoundaryManifest) -> Vec<&str> { - let mut phases = Vec::new(); +fn grouped_rows(manifest: &BoundaryManifest) -> BTreeMap<&str, Vec> { + let mut phases = BTreeMap::new(); for task in &manifest.tasks { let phase = task.id.split('.').next().unwrap_or_default(); - if !phases.contains(&phase) { - phases.push(phase); - } + phases + .entry(phase) + .or_insert_with(Vec::new) + .push(MatrixRow::from(task)); } phases } -fn push_phase(rendered: &mut String, phase: &str, manifest: &BoundaryManifest) { - let rows = manifest - .tasks - .iter() - .filter(|task| task.id.starts_with(phase)) - .map(MatrixRow::from) - .collect::>(); - let widths = ColumnWidths::for_rows(&rows); +fn push_phase(rendered: &mut String, phase: &str, rows: &[MatrixRow]) { + let widths = ColumnWidths::for_rows(rows); rendered.push_str("## Phase "); rendered.push_str(phase); rendered.push_str("\n\n"); push_header(rendered, &widths); push_separator(rendered, &widths); - for row in &rows { + for row in rows { push_row(rendered, row, &widths); } rendered.push('\n'); @@ -221,12 +218,13 @@ impl From<&BoundaryTask> for MatrixRow { } fn upstream_tasks(task: &BoundaryTask) -> String { - let upstream = task - .upstream - .iter() - .map(|reference| reference.task.as_str()) - .collect::>() - .join(", "); + let mut upstream = String::new(); + for reference in &task.upstream { + if !upstream.is_empty() { + upstream.push_str(", "); + } + upstream.push_str(&reference.task); + } optional_cell(Some(&upstream)) } @@ -279,3 +277,114 @@ fn padded(value: &str, width: usize) -> String { } fn cell_width(value: &str) -> usize { value.chars().count() } + +#[cfg(test)] +mod tests { + //! Unit, snapshot, and property coverage for Markdown matrix rendering. + + use proptest::prelude::*; + + use super::*; + use crate::{BoundaryManifest, UpstreamRef, UpstreamRole}; + + #[test] + fn groups_rows_by_phase_without_prefix_collisions() { + let manifest = manifest_with_tasks(vec![task("1.1.1"), task("12.1.1")]); + + let rows = grouped_rows(&manifest); + + assert_eq!(rows.get("1").map(Vec::len), Some(1)); + assert_eq!(rows.get("12").map(Vec::len), Some(1)); + } + + #[test] + fn computes_column_widths_from_escaped_cells() { + let rows = vec![MatrixRow { + roadmap_task: "[12.1.1](roadmap.md#task)".into(), + gist: "contains \\| pipe".into(), + state: "✓ consumes".into(), + upstream: "upstream contract".into(), + shipped_in: "4339a6f3".into(), + gate_or_divergence: "n/a".into(), + next_review_by: "n/a".into(), + last_reviewed: "2026-06-20".into(), + }]; + + let widths = ColumnWidths::for_rows(&rows); + + assert_eq!(widths.gist, "contains \\| pipe".chars().count()); + assert_eq!(widths.last_reviewed, "Last reviewed".chars().count()); + } + + #[test] + fn snapshots_rendered_matrix_shape() { + let manifest = manifest_with_tasks(vec![task("12.1.1")]); + + insta::assert_snapshot!(render_matrix(&manifest), @r###" + # OrthoConfig consumer boundary + + + + This matrix is generated from `docs/orthoconfig-consumer-boundary.toml`. Do not + edit the table by hand; update the manifest and regenerate it with + `cargo run -p weaver-docs-gate --example render_boundary_matrix -- docs/orthoconfig-consumer-boundary.toml docs/orthoconfig-consumer-boundary.md`. + + The matrix tracks every live Weaver command-contract roadmap task that consumes + OrthoConfig, wraps it temporarily, waits on upstream shape, or deliberately + diverges under ADR 007. + + ## Phase 12 + + | Roadmap task | Gist | State | Upstream OrthoConfig task | Shipped in | Removal gate or divergence | Next review by | Last reviewed | + | ---------------------------------------------------------------------------------- | ------------------- | ---------- | ------------------------- | ---------- | -------------------------- | -------------- | ------------- | + | [12.1.1](roadmap.md#121-confirm-reusable-contracts-that-weaver-must-not-duplicate) | Review \| renderer. | ✓ consumes | renderer-contract | 4339a6f3 | n/a | n/a | 2026-06-20 | + + "###); + } + + proptest! { + #[test] + fn rendered_task_cells_never_emit_raw_table_pipes( + gist in "[A-Za-z0-9 |\\n]{0,64}", + upstream in "[A-Za-z0-9 |\\n]{0,64}", + ) { + let mut boundary_task = task("12.1.1"); + boundary_task.gist = gist; + boundary_task.upstream = vec![UpstreamRef { + task: upstream, + role: UpstreamRole::Renderer, + }]; + + let rendered = render_matrix(&manifest_with_tasks(vec![boundary_task])); + for line in rendered.lines().filter(|line| line.starts_with("| [")) { + prop_assert!(line.matches(" | ").count() == 7); + } + } + } + + fn manifest_with_tasks(tasks: Vec) -> BoundaryManifest { + let managed_tasks = tasks.iter().map(|task| task.id.clone()).collect(); + BoundaryManifest { + schema_version: 1, + managed_tasks, + tasks, + } + } + + fn task(id: &str) -> BoundaryTask { + BoundaryTask { + id: id.into(), + gist: "Review | renderer.".into(), + state: BoundaryState::Consumes, + upstream: vec![UpstreamRef { + task: "renderer-contract".into(), + role: UpstreamRole::Renderer, + }], + shipped_in: Some("4339a6f3".into()), + removal_gate: None, + adr_anchor: None, + next_review_by: None, + last_reviewed: "2026-06-20".into(), + } + } +} diff --git a/crates/weaver-docs-gate/tests/load_manifest.rs b/crates/weaver-docs-gate/tests/load_manifest.rs new file mode 100644 index 00000000..9c661496 --- /dev/null +++ b/crates/weaver-docs-gate/tests/load_manifest.rs @@ -0,0 +1,225 @@ +//! Unit-level coverage for loading boundary manifests from disk. + +use std::process::Command; + +use camino::{Utf8Path, Utf8PathBuf}; +use cap_std::{ambient_authority, fs::Dir}; +use tempfile::TempDir; +use weaver_docs_gate::{BoundaryError, BoundaryState, load_manifest}; + +type TestResult = Result; + +const VALID_MANIFEST: &str = r#" +schema_version = 1 +managed_tasks = ["12.1.1"] + +[[task]] +id = "12.1.1" +gist = "Track the downstream consumer boundary." +state = "consumes" +shipped_in = "4339a6f3" +removal_gate = "" +adr_anchor = "" +next_review_by = "" +last_reviewed = "2026-06-20" + +[[task.upstream]] +task = "renderer-contract" +role = "renderer" +"#; + +#[test] +fn load_manifest_reports_missing_files() -> TestResult { + let temp = temp_dir()?; + let missing = temp.path().join("missing.toml"); + + let error = load_manifest(&missing).err().ok_or("missing file loaded")?; + + assert_boundary_error(&error, |boundary_error| { + matches!(boundary_error, BoundaryError::NotFound(_)) + }) +} + +#[test] +fn load_manifest_reports_invalid_paths() -> TestResult { + let error = load_manifest(Utf8Path::new("/")) + .err() + .ok_or("root path loaded as manifest")?; + + assert_boundary_error(&error, |boundary_error| { + matches!(boundary_error, BoundaryError::InvalidPath(_)) + }) +} + +#[test] +fn load_manifest_reports_read_errors() -> TestResult { + let temp = temp_dir()?; + let manifest_dir = temp.path().join("manifest.toml"); + Dir::open_ambient_dir(temp.path(), ambient_authority()) + .map_err(|error| format!("open temp dir: {error}"))? + .create_dir("manifest.toml") + .map_err(|error| format!("create manifest directory: {error}"))?; + + let error = load_manifest(&manifest_dir) + .err() + .ok_or("directory loaded as manifest")?; + + assert_boundary_error(&error, |boundary_error| { + matches!(boundary_error, BoundaryError::Read { .. }) + }) +} + +#[test] +fn load_manifest_reports_invalid_toml() -> TestResult { + let temp = temp_dir()?; + let manifest_path = temp.path().join("manifest.toml"); + write_file(temp.path(), "manifest.toml", "schema_version = ")? + .map_err(|error| format!("write invalid manifest: {error}"))?; + + let error = load_manifest(&manifest_path) + .err() + .ok_or("invalid TOML loaded as manifest")?; + + assert_boundary_error(&error, |boundary_error| { + matches!(boundary_error, BoundaryError::InvalidToml { .. }) + }) +} + +#[test] +fn load_manifest_maps_toml_dto_to_domain_types() -> TestResult { + let temp = temp_dir()?; + let manifest_path = temp.path().join("manifest.toml"); + write_file(temp.path(), "manifest.toml", VALID_MANIFEST)? + .map_err(|error| format!("write valid manifest: {error}"))?; + + let manifest = load_manifest(&manifest_path).map_err(|error| error.to_string())?; + + ensure_equal(&manifest.schema_version, &1, "schema version should load")?; + ensure_equal( + &manifest.tasks.first().map(|task| task.state), + &Some(BoundaryState::Consumes), + "state should map from TOML DTO", + )?; + ensure_equal( + &manifest + .tasks + .first() + .and_then(|task| task.next_review_by.as_ref()), + &None, + "empty optional strings should map to None", + )?; + Ok(()) +} + +#[test] +fn render_boundary_matrix_example_writes_output() -> TestResult { + let temp = temp_dir()?; + let manifest_path = temp.path().join("manifest.toml"); + let output_path = temp.path().join("matrix.md"); + write_file(temp.path(), "manifest.toml", VALID_MANIFEST)? + .map_err(|error| format!("write valid manifest: {error}"))?; + + let output = Command::new(env!("CARGO")) + .args([ + "run", + "-p", + "weaver-docs-gate", + "--example", + "render_boundary_matrix", + "--", + manifest_path.as_str(), + output_path.as_str(), + ]) + .current_dir(repo_root()?) + .output() + .map_err(|error| format!("run render_boundary_matrix example: {error}"))?; + + if !output.status.success() { + return Err(format!( + "example failed\nstdout: {}\nstderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + )); + } + + let rendered = read_file(temp.path(), "matrix.md")? + .map_err(|error| format!("read rendered matrix: {error}"))?; + ensure( + rendered.contains("[12.1.1]"), + "rendered output should name task", + ) +} + +fn temp_dir() -> TestResult { + let temp = tempfile::tempdir().map_err(|error| format!("create temp dir: {error}"))?; + let path = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()) + .map_err(|path| format!("temp path is not UTF-8: {}", path.display()))?; + Ok(Utf8TempDir { _temp: temp, path }) +} + +fn write_file( + dir_path: &Utf8Path, + file_name: &str, + content: &str, +) -> TestResult> { + let dir = Dir::open_ambient_dir(dir_path, ambient_authority()) + .map_err(|error| format!("open {dir_path}: {error}"))?; + Ok(dir.write(file_name, content)) +} + +fn read_file(dir_path: &Utf8Path, file_name: &str) -> TestResult> { + let dir = Dir::open_ambient_dir(dir_path, ambient_authority()) + .map_err(|error| format!("open {dir_path}: {error}"))?; + Ok(dir.read_to_string(file_name)) +} + +fn repo_root() -> TestResult { + let crate_dir = Utf8Path::new(env!("CARGO_MANIFEST_DIR")); + crate_dir + .parent() + .and_then(Utf8Path::parent) + .map(Utf8Path::to_path_buf) + .ok_or_else(|| format!("cannot resolve repository root from {crate_dir}")) +} + +fn ensure(condition: bool, message: impl Into) -> TestResult { + if condition { + Ok(()) + } else { + Err(message.into()) + } +} + +fn ensure_equal(left: &T, right: &T, message: impl Into) -> TestResult +where + T: std::fmt::Debug + PartialEq, +{ + if left == right { + Ok(()) + } else { + Err(format!( + "{}\nleft: {left:?}\nright: {right:?}", + message.into() + )) + } +} + +fn assert_boundary_error( + error: &BoundaryError, + predicate: impl FnOnce(&BoundaryError) -> bool, +) -> TestResult { + if predicate(error) { + Ok(()) + } else { + Err(format!("unexpected boundary error: {error:?}")) + } +} + +struct Utf8TempDir { + _temp: TempDir, + path: Utf8PathBuf, +} + +impl Utf8TempDir { + fn path(&self) -> &Utf8Path { &self.path } +} diff --git a/docs/developers-guide.md b/docs/developers-guide.md index cc05c015..ea33a4fe 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -33,6 +33,43 @@ temporary Weaver adapter with a concrete removal gate, `pending` for an upstream contract that is not decided or shipped yet, and `divergent` only when ADR 007 records the deliberate Weaver-owned divergence. +### `weaver-docs-gate` boundary API + +The private `weaver-docs-gate` workspace member owns the generated OrthoConfig +consumer boundary matrix and the referential-integrity checks that keep +command-contract roadmap tasks classified. The crate is intentionally a +developer tool, not a runtime dependency for `weaver` or `weaverd`. + +The public library API exposes plain domain types. TOML deserialization is kept +inside the crate's private adapter layer so manifest framework concerns do not +leak into the boundary vocabulary: + +- `BoundaryState` is the four-state classification vocabulary: `consumes`, + `wraps`, `pending`, and `divergent`. Use `as_str()` when diagnostics or + generated text need the canonical manifest spelling. +- `UpstreamRole` names the kind of OrthoConfig contract a Weaver task consumes + or waits for, such as `renderer`, `profile`, `delivery`, or + `execution_ledger`. +- `UpstreamRef` pairs an upstream roadmap or design reference with an + `UpstreamRole`. +- `BoundaryTask` is one classified Weaver roadmap task row. Its optional + evidence fields are state-specific: `shipped_in` for `consumes`, + `removal_gate` for `wraps`, `next_review_by` for `pending`, and `adr_anchor` + for `divergent`. +- `BoundaryManifest` is the complete source-of-truth document. Its + `managed_tasks` registry must match the ordered `tasks` rows so new + command-contract roadmap items fail closed until explicitly classified. +- `BoundaryError` reports manifest loading failures. Match it when a test or + developer tool must distinguish a missing file, invalid path, read failure, + or TOML schema error. + +Use `load_manifest(path)` to read `docs/orthoconfig-consumer-boundary.toml` +into a `BoundaryManifest`. Use `render_matrix(&manifest)` to produce the +Markdown contents for `docs/orthoconfig-consumer-boundary.md`. The renderer +escapes table cells, groups rows by roadmap phase, and sizes columns from the +escaped cell content, so callers should pass domain values rather than +preformatted Markdown rows. + When adding or renaming a public command: 1. Update the OrthoConfig-backed command metadata or the Weaver semantic From 7a89d00d7fedb2f720a03849f5dc0341277378e3 Mon Sep 17 00:00:00 2001 From: leynos Date: Sat, 20 Jun 2026 21:39:18 +0200 Subject: [PATCH 13/22] Hide docs gate adapter details Keep manifest load failures domain-facing by replacing public parser and I/O source payloads with stable `Unreadable` and `InvalidSchema` variants. Move renderer tests under the renderer module and document docs-gate helper functions so the boundary crate has enough documented surface for review coverage. --- .../examples/render_boundary_matrix.rs | 2 + crates/weaver-docs-gate/src/lib.rs | 55 +++++--- .../weaver-docs-gate/src/manifest_adapter.rs | 11 ++ .../src/{renderer.rs => renderer/mod.rs} | 130 +++--------------- crates/weaver-docs-gate/src/renderer/tests.rs | 113 +++++++++++++++ .../tests/boundary_manifest.rs | 21 +++ .../weaver-docs-gate/tests/load_manifest.rs | 19 ++- .../tests/support/pending_review_date.rs | 8 ++ docs/developers-guide.md | 4 +- 9 files changed, 231 insertions(+), 132 deletions(-) rename crates/weaver-docs-gate/src/{renderer.rs => renderer/mod.rs} (68%) create mode 100644 crates/weaver-docs-gate/src/renderer/tests.rs diff --git a/crates/weaver-docs-gate/examples/render_boundary_matrix.rs b/crates/weaver-docs-gate/examples/render_boundary_matrix.rs index c439e729..dc3e304a 100644 --- a/crates/weaver-docs-gate/examples/render_boundary_matrix.rs +++ b/crates/weaver-docs-gate/examples/render_boundary_matrix.rs @@ -6,6 +6,7 @@ use camino::Utf8Path; use cap_std::{ambient_authority, fs::Dir}; use weaver_docs_gate::{load_manifest, render_matrix}; +/// Regenerate a boundary matrix from a manifest path and output path. fn main() -> Result<(), String> { let mut args = env::args().skip(1); let manifest_path = args @@ -24,6 +25,7 @@ fn main() -> Result<(), String> { write_output(Utf8Path::new(&output_path), render_matrix(&manifest)) } +/// Write the regenerated matrix through a capability-oriented directory handle. fn write_output(path: &Utf8Path, content: String) -> Result<(), String> { let parent = path .parent() diff --git a/crates/weaver-docs-gate/src/lib.rs b/crates/weaver-docs-gate/src/lib.rs index 16d64497..d43793b0 100644 --- a/crates/weaver-docs-gate/src/lib.rs +++ b/crates/weaver-docs-gate/src/lib.rs @@ -189,6 +189,22 @@ pub struct BoundaryManifest { } /// Errors returned while loading the boundary manifest. +/// +/// # Examples +/// ``` +/// use camino::Utf8PathBuf; +/// use weaver_docs_gate::BoundaryError; +/// +/// let error = BoundaryError::InvalidSchema { +/// path: Utf8PathBuf::from("docs/orthoconfig-consumer-boundary.toml"), +/// detail: "missing field `schema_version`".into(), +/// }; +/// assert!( +/// error +/// .to_string() +/// .contains("invalid boundary manifest schema") +/// ); +/// ``` #[derive(Debug, thiserror::Error)] pub enum BoundaryError { /// The manifest path does not exist. @@ -197,21 +213,21 @@ pub enum BoundaryError { /// The manifest path cannot be opened through a parent directory handle. #[error("invalid manifest path: {0}")] InvalidPath(Utf8PathBuf), - /// The file could not be read. - #[error("manifest file could not be read: {path}: {source}")] - Read { + /// The manifest exists but cannot be read as file contents. + #[error("boundary manifest cannot be read: {path}: {detail}")] + Unreadable { /// Manifest path. path: Utf8PathBuf, - /// Underlying I/O error. - source: Box, + /// Stable human-readable read failure detail. + detail: String, }, - /// The file is not valid TOML for the boundary schema. - #[error("invalid TOML in {path}: {source}")] - InvalidToml { + /// The manifest contents do not match the boundary manifest schema. + #[error("invalid boundary manifest schema in {path}: {detail}")] + InvalidSchema { /// Manifest path. path: Utf8PathBuf, - /// TOML parse error. - source: Box, + /// Stable human-readable schema failure detail. + detail: String, }, } @@ -219,8 +235,8 @@ pub enum BoundaryError { /// /// # Errors /// -/// Returns [`BoundaryError`] when the manifest is missing, unreadable, or not -/// valid TOML for the boundary schema. +/// Returns [`BoundaryError`] when the manifest is missing, unreadable, or does +/// not match the boundary schema. /// /// # Examples /// ```no_run @@ -237,28 +253,29 @@ pub fn load_manifest(path: &Utf8Path) -> Result .file_name() .ok_or_else(|| BoundaryError::InvalidPath(path.to_path_buf()))?; let dir = Dir::open_ambient_dir(parent, ambient_authority()) - .map_err(|source| read_error(path, source))?; + .map_err(|source| read_error(path, &source))?; let contents = dir .read_to_string(file_name) - .map_err(|source| read_error(path, source))?; + .map_err(|source| read_error(path, &source))?; let manifest = toml::from_str::(&contents).map_err(|source| { - BoundaryError::InvalidToml { + BoundaryError::InvalidSchema { path: path.to_path_buf(), - source: Box::new(source), + detail: source.to_string(), } })?; Ok(manifest.into()) } -fn read_error(path: &Utf8Path, source: io::Error) -> BoundaryError { +/// Convert filesystem failures into stable manifest loading errors. +fn read_error(path: &Utf8Path, source: &io::Error) -> BoundaryError { if source.kind() == ErrorKind::NotFound { BoundaryError::NotFound(path.to_path_buf()) } else { - BoundaryError::Read { + BoundaryError::Unreadable { path: path.to_path_buf(), - source: Box::new(source), + detail: source.to_string(), } } } diff --git a/crates/weaver-docs-gate/src/manifest_adapter.rs b/crates/weaver-docs-gate/src/manifest_adapter.rs index 0e8d367e..ae7e3afe 100644 --- a/crates/weaver-docs-gate/src/manifest_adapter.rs +++ b/crates/weaver-docs-gate/src/manifest_adapter.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Deserializer}; use crate::{BoundaryManifest, BoundaryState, BoundaryTask, UpstreamRef, UpstreamRole}; +/// Deserialization shape for the complete TOML manifest. #[derive(Debug, Deserialize)] pub(super) struct BoundaryManifestDto { pub(super) schema_version: u32, @@ -12,6 +13,7 @@ pub(super) struct BoundaryManifestDto { pub(super) tasks: Vec, } +/// Deserialization shape for one `[[task]]` TOML row. #[derive(Debug, Deserialize)] pub(super) struct BoundaryTaskDto { pub(super) id: String, @@ -29,12 +31,14 @@ pub(super) struct BoundaryTaskDto { pub(super) last_reviewed: String, } +/// Deserialization shape for one `[[task.upstream]]` entry. #[derive(Debug, Deserialize)] pub(super) struct UpstreamRefDto { pub(super) task: String, pub(super) role: UpstreamRoleDto, } +/// Serde-backed spelling of the public boundary state vocabulary. #[derive(Debug, Clone, Copy, Deserialize)] #[serde(rename_all = "snake_case")] pub(super) enum BoundaryStateDto { @@ -44,6 +48,7 @@ pub(super) enum BoundaryStateDto { Divergent, } +/// Serde-backed spelling of upstream role values in the manifest. #[derive(Debug, Clone, Copy, Deserialize)] #[serde(rename_all = "snake_case")] pub(super) enum UpstreamRoleDto { @@ -59,6 +64,7 @@ pub(super) enum UpstreamRoleDto { } impl From for BoundaryManifest { + /// Convert adapter-owned TOML data into the public domain manifest. fn from(dto: BoundaryManifestDto) -> Self { Self { schema_version: dto.schema_version, @@ -69,6 +75,7 @@ impl From for BoundaryManifest { } impl From for BoundaryTask { + /// Convert one TOML task row into a public domain task. fn from(dto: BoundaryTaskDto) -> Self { Self { id: dto.id, @@ -85,6 +92,7 @@ impl From for BoundaryTask { } impl From for UpstreamRef { + /// Convert one TOML upstream reference into a public domain reference. fn from(dto: UpstreamRefDto) -> Self { Self { task: dto.task, @@ -94,6 +102,7 @@ impl From for UpstreamRef { } impl From for BoundaryState { + /// Convert the adapter state vocabulary into the public state enum. fn from(dto: BoundaryStateDto) -> Self { match dto { BoundaryStateDto::Consumes => Self::Consumes, @@ -105,6 +114,7 @@ impl From for BoundaryState { } impl From for UpstreamRole { + /// Convert the adapter role vocabulary into the public role enum. fn from(dto: UpstreamRoleDto) -> Self { match dto { UpstreamRoleDto::Boundary => Self::Boundary, @@ -120,6 +130,7 @@ impl From for UpstreamRole { } } +/// Treat empty TOML strings as absent optional manifest evidence. fn empty_string_as_none<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, diff --git a/crates/weaver-docs-gate/src/renderer.rs b/crates/weaver-docs-gate/src/renderer/mod.rs similarity index 68% rename from crates/weaver-docs-gate/src/renderer.rs rename to crates/weaver-docs-gate/src/renderer/mod.rs index 58989b0a..03e37de2 100644 --- a/crates/weaver-docs-gate/src/renderer.rs +++ b/crates/weaver-docs-gate/src/renderer/mod.rs @@ -6,6 +6,7 @@ use crate::{BoundaryManifest, BoundaryState, BoundaryTask}; const NA: &str = "n/a"; +/// One fully formatted Markdown table row before padding is applied. struct MatrixRow { roadmap_task: String, gist: String, @@ -17,6 +18,7 @@ struct MatrixRow { last_reviewed: String, } +/// Widths for each generated Markdown table column. struct ColumnWidths { roadmap_task: usize, gist: usize, @@ -29,6 +31,7 @@ struct ColumnWidths { } impl ColumnWidths { + /// Compute table widths from headers and escaped row cells. fn for_rows(rows: &[MatrixRow]) -> Self { let mut widths = Self::headers(); for row in rows { @@ -37,6 +40,7 @@ impl ColumnWidths { widths } + /// Return the minimum widths required by the header row. const fn headers() -> Self { Self { roadmap_task: 12, @@ -50,6 +54,7 @@ impl ColumnWidths { } } + /// Expand widths so the given row fits without changing table shape. fn include(&mut self, row: &MatrixRow) { self.roadmap_task = self.roadmap_task.max(cell_width(&row.roadmap_task)); self.gist = self.gist.max(cell_width(&row.gist)); @@ -118,6 +123,7 @@ pub fn render_matrix(manifest: &BoundaryManifest) -> String { rendered } +/// Group rendered rows by roadmap phase in deterministic phase order. fn grouped_rows(manifest: &BoundaryManifest) -> BTreeMap<&str, Vec> { let mut phases = BTreeMap::new(); for task in &manifest.tasks { @@ -130,6 +136,7 @@ fn grouped_rows(manifest: &BoundaryManifest) -> BTreeMap<&str, Vec> { phases } +/// Append one phase heading and table to the rendered matrix. fn push_phase(rendered: &mut String, phase: &str, rows: &[MatrixRow]) { let widths = ColumnWidths::for_rows(rows); @@ -144,6 +151,7 @@ fn push_phase(rendered: &mut String, phase: &str, rows: &[MatrixRow]) { rendered.push('\n'); } +/// Append a Markdown table header using the computed column widths. fn push_header(rendered: &mut String, widths: &ColumnWidths) { push_cells( rendered, @@ -161,6 +169,7 @@ fn push_header(rendered: &mut String, widths: &ColumnWidths) { ); } +/// Append the Markdown separator row for a generated table. fn push_separator(rendered: &mut String, widths: &ColumnWidths) { push_cells( rendered, @@ -178,10 +187,12 @@ fn push_separator(rendered: &mut String, widths: &ColumnWidths) { ); } +/// Append one data row to a generated table. fn push_row(rendered: &mut String, row: &MatrixRow, widths: &ColumnWidths) { push_cells(rendered, widths, row); } +/// Append all cells for a table row with stable Markdown separators. fn push_cells(rendered: &mut String, widths: &ColumnWidths, row: &MatrixRow) { rendered.push_str("| "); rendered.push_str(&padded(&row.roadmap_task, widths.roadmap_task)); @@ -203,6 +214,7 @@ fn push_cells(rendered: &mut String, widths: &ColumnWidths, row: &MatrixRow) { } impl From<&BoundaryTask> for MatrixRow { + /// Convert one manifest task into escaped matrix cell strings. fn from(task: &BoundaryTask) -> Self { Self { roadmap_task: format!("[{}]({})", escape_cell(&task.id), roadmap_anchor(&task.id)), @@ -217,6 +229,7 @@ impl From<&BoundaryTask> for MatrixRow { } } +/// Format upstream task references as a comma-separated matrix cell. fn upstream_tasks(task: &BoundaryTask) -> String { let mut upstream = String::new(); for reference in &task.upstream { @@ -228,6 +241,7 @@ fn upstream_tasks(task: &BoundaryTask) -> String { optional_cell(Some(&upstream)) } +/// Format the state-specific removal gate or ADR divergence cell. fn gate_or_divergence(task: &BoundaryTask) -> String { if let Some(gate) = task.removal_gate.as_deref() { return escape_cell(gate); @@ -239,12 +253,14 @@ fn gate_or_divergence(task: &BoundaryTask) -> String { ) } +/// Format an optional manifest field as either escaped text or `n/a`. fn optional_cell(value: Option<&str>) -> String { value .filter(|inner| !inner.is_empty()) .map_or_else(|| NA.into(), escape_cell) } +/// Return the visible matrix label for a boundary state. const fn state_label(state: BoundaryState) -> &'static str { match state { BoundaryState::Consumes => "✓ consumes", @@ -254,6 +270,7 @@ const fn state_label(state: BoundaryState) -> &'static str { } } +/// Return the roadmap section anchor for a task ID. fn roadmap_anchor(task_id: &str) -> &'static str { match task_id.split('.').next().unwrap_or_default() { "12" => "roadmap.md#121-confirm-reusable-contracts-that-weaver-must-not-duplicate", @@ -269,122 +286,17 @@ fn roadmap_anchor(task_id: &str) -> &'static str { } } +/// Escape Markdown table metacharacters in a cell value. fn escape_cell(value: &str) -> String { value.replace('|', "\\|").replace('\n', "
") } +/// Pad a rendered cell to a target display width. fn padded(value: &str, width: usize) -> String { let padding = width.saturating_sub(cell_width(value)); format!("{value}{}", " ".repeat(padding)) } +/// Count the displayed width used by the matrix renderer. fn cell_width(value: &str) -> usize { value.chars().count() } #[cfg(test)] -mod tests { - //! Unit, snapshot, and property coverage for Markdown matrix rendering. - - use proptest::prelude::*; - - use super::*; - use crate::{BoundaryManifest, UpstreamRef, UpstreamRole}; - - #[test] - fn groups_rows_by_phase_without_prefix_collisions() { - let manifest = manifest_with_tasks(vec![task("1.1.1"), task("12.1.1")]); - - let rows = grouped_rows(&manifest); - - assert_eq!(rows.get("1").map(Vec::len), Some(1)); - assert_eq!(rows.get("12").map(Vec::len), Some(1)); - } - - #[test] - fn computes_column_widths_from_escaped_cells() { - let rows = vec![MatrixRow { - roadmap_task: "[12.1.1](roadmap.md#task)".into(), - gist: "contains \\| pipe".into(), - state: "✓ consumes".into(), - upstream: "upstream contract".into(), - shipped_in: "4339a6f3".into(), - gate_or_divergence: "n/a".into(), - next_review_by: "n/a".into(), - last_reviewed: "2026-06-20".into(), - }]; - - let widths = ColumnWidths::for_rows(&rows); - - assert_eq!(widths.gist, "contains \\| pipe".chars().count()); - assert_eq!(widths.last_reviewed, "Last reviewed".chars().count()); - } - - #[test] - fn snapshots_rendered_matrix_shape() { - let manifest = manifest_with_tasks(vec![task("12.1.1")]); - - insta::assert_snapshot!(render_matrix(&manifest), @r###" - # OrthoConfig consumer boundary - - - - This matrix is generated from `docs/orthoconfig-consumer-boundary.toml`. Do not - edit the table by hand; update the manifest and regenerate it with - `cargo run -p weaver-docs-gate --example render_boundary_matrix -- docs/orthoconfig-consumer-boundary.toml docs/orthoconfig-consumer-boundary.md`. - - The matrix tracks every live Weaver command-contract roadmap task that consumes - OrthoConfig, wraps it temporarily, waits on upstream shape, or deliberately - diverges under ADR 007. - - ## Phase 12 - - | Roadmap task | Gist | State | Upstream OrthoConfig task | Shipped in | Removal gate or divergence | Next review by | Last reviewed | - | ---------------------------------------------------------------------------------- | ------------------- | ---------- | ------------------------- | ---------- | -------------------------- | -------------- | ------------- | - | [12.1.1](roadmap.md#121-confirm-reusable-contracts-that-weaver-must-not-duplicate) | Review \| renderer. | ✓ consumes | renderer-contract | 4339a6f3 | n/a | n/a | 2026-06-20 | - - "###); - } - - proptest! { - #[test] - fn rendered_task_cells_never_emit_raw_table_pipes( - gist in "[A-Za-z0-9 |\\n]{0,64}", - upstream in "[A-Za-z0-9 |\\n]{0,64}", - ) { - let mut boundary_task = task("12.1.1"); - boundary_task.gist = gist; - boundary_task.upstream = vec![UpstreamRef { - task: upstream, - role: UpstreamRole::Renderer, - }]; - - let rendered = render_matrix(&manifest_with_tasks(vec![boundary_task])); - for line in rendered.lines().filter(|line| line.starts_with("| [")) { - prop_assert!(line.matches(" | ").count() == 7); - } - } - } - - fn manifest_with_tasks(tasks: Vec) -> BoundaryManifest { - let managed_tasks = tasks.iter().map(|task| task.id.clone()).collect(); - BoundaryManifest { - schema_version: 1, - managed_tasks, - tasks, - } - } - - fn task(id: &str) -> BoundaryTask { - BoundaryTask { - id: id.into(), - gist: "Review | renderer.".into(), - state: BoundaryState::Consumes, - upstream: vec![UpstreamRef { - task: "renderer-contract".into(), - role: UpstreamRole::Renderer, - }], - shipped_in: Some("4339a6f3".into()), - removal_gate: None, - adr_anchor: None, - next_review_by: None, - last_reviewed: "2026-06-20".into(), - } - } -} +mod tests; diff --git a/crates/weaver-docs-gate/src/renderer/tests.rs b/crates/weaver-docs-gate/src/renderer/tests.rs new file mode 100644 index 00000000..1e488fe8 --- /dev/null +++ b/crates/weaver-docs-gate/src/renderer/tests.rs @@ -0,0 +1,113 @@ +//! Unit, snapshot, and property coverage for Markdown matrix rendering. + +use proptest::prelude::*; + +use super::*; +use crate::{BoundaryManifest, UpstreamRef, UpstreamRole}; + +/// Prove numeric phase grouping does not confuse `1.*` with `12.*` tasks. +#[test] +fn groups_rows_by_phase_without_prefix_collisions() { + let manifest = manifest_with_tasks(vec![task("1.1.1"), task("12.1.1")]); + + let rows = grouped_rows(&manifest); + + assert_eq!(rows.get("1").map(Vec::len), Some(1)); + assert_eq!(rows.get("12").map(Vec::len), Some(1)); +} + +/// Prove column widths are based on escaped cell text. +#[test] +fn computes_column_widths_from_escaped_cells() { + let rows = vec![MatrixRow { + roadmap_task: "[12.1.1](roadmap.md#task)".into(), + gist: "contains \\| pipe".into(), + state: "✓ consumes".into(), + upstream: "upstream contract".into(), + shipped_in: "4339a6f3".into(), + gate_or_divergence: "n/a".into(), + next_review_by: "n/a".into(), + last_reviewed: "2026-06-20".into(), + }]; + + let widths = ColumnWidths::for_rows(&rows); + + assert_eq!(widths.gist, "contains \\| pipe".chars().count()); + assert_eq!(widths.last_reviewed, "Last reviewed".chars().count()); +} + +/// Pin the stable generated Markdown shape for one representative task. +#[test] +fn snapshots_rendered_matrix_shape() { + let manifest = manifest_with_tasks(vec![task("12.1.1")]); + + insta::assert_snapshot!(render_matrix(&manifest), @r###" + # OrthoConfig consumer boundary + + + + This matrix is generated from `docs/orthoconfig-consumer-boundary.toml`. Do not + edit the table by hand; update the manifest and regenerate it with + `cargo run -p weaver-docs-gate --example render_boundary_matrix -- docs/orthoconfig-consumer-boundary.toml docs/orthoconfig-consumer-boundary.md`. + + The matrix tracks every live Weaver command-contract roadmap task that consumes + OrthoConfig, wraps it temporarily, waits on upstream shape, or deliberately + diverges under ADR 007. + + ## Phase 12 + + | Roadmap task | Gist | State | Upstream OrthoConfig task | Shipped in | Removal gate or divergence | Next review by | Last reviewed | + | ---------------------------------------------------------------------------------- | ------------------- | ---------- | ------------------------- | ---------- | -------------------------- | -------------- | ------------- | + | [12.1.1](roadmap.md#121-confirm-reusable-contracts-that-weaver-must-not-duplicate) | Review \| renderer. | ✓ consumes | renderer-contract | 4339a6f3 | n/a | n/a | 2026-06-20 | + + "###); +} + +proptest! { + #[test] + /// Prove arbitrary task text cannot add Markdown table columns. + fn rendered_task_cells_never_emit_raw_table_pipes( + gist in "[A-Za-z0-9 |\\n]{0,64}", + upstream in "[A-Za-z0-9 |\\n]{0,64}", + ) { + let mut boundary_task = task("12.1.1"); + boundary_task.gist = gist; + boundary_task.upstream = vec![UpstreamRef { + task: upstream, + role: UpstreamRole::Renderer, + }]; + + let rendered = render_matrix(&manifest_with_tasks(vec![boundary_task])); + for line in rendered.lines().filter(|line| line.starts_with("| [")) { + prop_assert!(line.matches(" | ").count() == 7); + } + } +} + +/// Build a manifest containing the supplied task rows. +fn manifest_with_tasks(tasks: Vec) -> BoundaryManifest { + let managed_tasks = tasks.iter().map(|task| task.id.clone()).collect(); + BoundaryManifest { + schema_version: 1, + managed_tasks, + tasks, + } +} + +/// Build a representative consumed boundary task. +fn task(id: &str) -> BoundaryTask { + BoundaryTask { + id: id.into(), + gist: "Review | renderer.".into(), + state: BoundaryState::Consumes, + upstream: vec![UpstreamRef { + task: "renderer-contract".into(), + role: UpstreamRole::Renderer, + }], + shipped_in: Some("4339a6f3".into()), + removal_gate: None, + adr_anchor: None, + next_review_by: None, + last_reviewed: "2026-06-20".into(), + } +} diff --git a/crates/weaver-docs-gate/tests/boundary_manifest.rs b/crates/weaver-docs-gate/tests/boundary_manifest.rs index 02f6e09e..32eabb1f 100644 --- a/crates/weaver-docs-gate/tests/boundary_manifest.rs +++ b/crates/weaver-docs-gate/tests/boundary_manifest.rs @@ -30,6 +30,7 @@ enum FieldName { } impl FieldName { + /// Return the manifest spelling for a validated field. const fn as_str(self) -> &'static str { match self { Self::LastReviewed => "last_reviewed", @@ -39,6 +40,7 @@ impl FieldName { } } +/// Prove the explicit managed-task registry matches rows and roadmap tasks. #[test] fn manifest_registry_matches_rows_and_roadmap_tasks() -> TestResult { let manifest = manifest().map_err(|error| format!("load boundary manifest: {error}"))?; @@ -69,6 +71,7 @@ fn manifest_registry_matches_rows_and_roadmap_tasks() -> TestResult { Ok(()) } +/// Prove every row carries the evidence required by its state. #[test] fn boundary_state_rows_have_required_evidence() -> TestResult { let manifest = manifest().map_err(|error| format!("load boundary manifest: {error}"))?; @@ -86,6 +89,7 @@ fn boundary_state_rows_have_required_evidence() -> TestResult { Ok(()) } +/// Prove divergent rows link to existing ADR 007 sections. #[test] fn divergent_rows_reference_existing_adr_007_anchors() -> TestResult { let manifest = manifest().map_err(|error| format!("load boundary manifest: {error}"))?; @@ -114,6 +118,7 @@ fn divergent_rows_reference_existing_adr_007_anchors() -> TestResult { Ok(()) } +/// Prove the checked-in Markdown matrix is generated from the manifest. #[test] fn committed_matrix_matches_manifest_rendering() -> TestResult { let manifest = manifest().map_err(|error| format!("load boundary manifest: {error}"))?; @@ -128,10 +133,12 @@ fn committed_matrix_matches_manifest_rendering() -> TestResult { ) } +/// Load the boundary manifest from the repository docs. fn manifest() -> TestResult { load_manifest(&repo_path(Utf8Path::new(MANIFEST))?).map_err(|error| error.to_string()) } +/// Read a repository documentation file as UTF-8 text. fn read_doc(doc_path: &Utf8Path) -> TestResult { let resolved_path = repo_path(doc_path)?; let parent = resolved_path.parent().unwrap_or_else(|| Utf8Path::new(".")); @@ -144,6 +151,7 @@ fn read_doc(doc_path: &Utf8Path) -> TestResult { .map_err(|error| format!("read {resolved_path}: {error}")) } +/// Resolve a repository-relative path from the test crate directory. fn repo_path(path: &Utf8Path) -> TestResult { let crate_dir = Utf8Path::new(env!("CARGO_MANIFEST_DIR")); let repo_root = crate_dir @@ -153,6 +161,7 @@ fn repo_path(path: &Utf8Path) -> TestResult { Ok(repo_root.join(path)) } +/// Extract roadmap task IDs from Markdown task headings. fn roadmap_task_ids(roadmap: &str) -> BTreeSet<&str> { roadmap .lines() @@ -166,6 +175,7 @@ fn roadmap_task_ids(roadmap: &str) -> BTreeSet<&str> { .collect() } +/// Extract GitHub-style heading anchors from a Markdown document. fn heading_anchors(document: &str) -> BTreeSet { document .lines() @@ -174,6 +184,7 @@ fn heading_anchors(document: &str) -> BTreeSet { .collect() } +/// Convert a heading into the anchor form used by GitHub Markdown. fn markdown_anchor(heading: &str) -> String { let mut anchor = String::new(); let mut previous_was_dash = false; @@ -190,6 +201,7 @@ fn markdown_anchor(heading: &str) -> String { anchor.trim_matches('-').to_owned() } +/// Append a single collapsed dash while building an anchor. fn push_dash(anchor: &mut String, previous_was_dash: &mut bool) { if !*previous_was_dash && !anchor.is_empty() { anchor.push('-'); @@ -197,6 +209,7 @@ fn push_dash(anchor: &mut String, previous_was_dash: &mut bool) { } } +/// Return an error when a test invariant is false. fn ensure(condition: bool, message: impl Into) -> TestResult { if condition { Ok(()) @@ -205,6 +218,7 @@ fn ensure(condition: bool, message: impl Into) -> TestResult { } } +/// Return a diff-friendly error when two values differ. fn ensure_equal(left: &T, right: &T, message: impl Into) -> TestResult where T: std::fmt::Debug + PartialEq, @@ -219,6 +233,7 @@ where } } +/// Ensure a manifest field contains no duplicate values. fn ensure_unique(values: &[&str], label: FieldName) -> TestResult { let mut seen = BTreeSet::new(); for value in values { @@ -230,6 +245,7 @@ fn ensure_unique(values: &[&str], label: FieldName) -> TestResult { Ok(()) } +/// Validate required and forbidden evidence fields for one state. fn validate_field_constraints( task: &BoundaryTask, state: &str, @@ -249,6 +265,7 @@ fn validate_field_constraints( Ok(()) } +/// Validate the additional freshness evidence required for pending rows. fn validate_pending_evidence(task: &BoundaryTask) -> TestResult { let next_review_by = task .next_review_by @@ -270,6 +287,7 @@ fn validate_pending_evidence(task: &BoundaryTask) -> TestResult { ) } +/// Validate state-specific evidence for one manifest row. fn validate_state_evidence(task: &BoundaryTask) -> TestResult { let (state, required, forbidden) = match task.state { BoundaryState::Consumes => ( @@ -304,10 +322,12 @@ fn validate_state_evidence(task: &BoundaryTask) -> TestResult { validate_field_constraints(task, state, required, &forbidden) } +/// Validate an ISO-like date field in one manifest row. fn validate_date(value: &str, field: FieldName, task: &BoundaryTask) -> TestResult { ensure(is_iso_date(value), invalid_date_message(value, field, task)) } +/// Build a diagnostic for an invalid date field. fn invalid_date_message(value: &str, field: FieldName, task: &BoundaryTask) -> String { format!( "task {} has invalid {} date {value:?}", @@ -316,6 +336,7 @@ fn invalid_date_message(value: &str, field: FieldName, task: &BoundaryTask) -> S ) } +/// Return whether a value is shaped as an ISO `YYYY-MM-DD` date. fn is_iso_date(value: &str) -> bool { value.len() == 10 && value diff --git a/crates/weaver-docs-gate/tests/load_manifest.rs b/crates/weaver-docs-gate/tests/load_manifest.rs index 9c661496..3b21e51b 100644 --- a/crates/weaver-docs-gate/tests/load_manifest.rs +++ b/crates/weaver-docs-gate/tests/load_manifest.rs @@ -28,6 +28,7 @@ task = "renderer-contract" role = "renderer" "#; +/// Prove missing manifest files return the not-found variant. #[test] fn load_manifest_reports_missing_files() -> TestResult { let temp = temp_dir()?; @@ -40,6 +41,7 @@ fn load_manifest_reports_missing_files() -> TestResult { }) } +/// Prove paths without a file name return the invalid-path variant. #[test] fn load_manifest_reports_invalid_paths() -> TestResult { let error = load_manifest(Utf8Path::new("/")) @@ -51,6 +53,7 @@ fn load_manifest_reports_invalid_paths() -> TestResult { }) } +/// Prove non-file manifest paths return the unreadable variant. #[test] fn load_manifest_reports_read_errors() -> TestResult { let temp = temp_dir()?; @@ -65,10 +68,11 @@ fn load_manifest_reports_read_errors() -> TestResult { .ok_or("directory loaded as manifest")?; assert_boundary_error(&error, |boundary_error| { - matches!(boundary_error, BoundaryError::Read { .. }) + matches!(boundary_error, BoundaryError::Unreadable { .. }) }) } +/// Prove malformed manifest contents return the schema variant. #[test] fn load_manifest_reports_invalid_toml() -> TestResult { let temp = temp_dir()?; @@ -81,10 +85,11 @@ fn load_manifest_reports_invalid_toml() -> TestResult { .ok_or("invalid TOML loaded as manifest")?; assert_boundary_error(&error, |boundary_error| { - matches!(boundary_error, BoundaryError::InvalidToml { .. }) + matches!(boundary_error, BoundaryError::InvalidSchema { .. }) }) } +/// Prove adapter DTOs map manifest values into plain domain types. #[test] fn load_manifest_maps_toml_dto_to_domain_types() -> TestResult { let temp = temp_dir()?; @@ -111,6 +116,7 @@ fn load_manifest_maps_toml_dto_to_domain_types() -> TestResult { Ok(()) } +/// Prove the checked example binary writes a generated matrix. #[test] fn render_boundary_matrix_example_writes_output() -> TestResult { let temp = temp_dir()?; @@ -150,6 +156,7 @@ fn render_boundary_matrix_example_writes_output() -> TestResult { ) } +/// Create a UTF-8 temp directory guard for filesystem tests. fn temp_dir() -> TestResult { let temp = tempfile::tempdir().map_err(|error| format!("create temp dir: {error}"))?; let path = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()) @@ -157,6 +164,7 @@ fn temp_dir() -> TestResult { Ok(Utf8TempDir { _temp: temp, path }) } +/// Write a file through a capability-oriented directory handle. fn write_file( dir_path: &Utf8Path, file_name: &str, @@ -167,12 +175,14 @@ fn write_file( Ok(dir.write(file_name, content)) } +/// Read a file through a capability-oriented directory handle. fn read_file(dir_path: &Utf8Path, file_name: &str) -> TestResult> { let dir = Dir::open_ambient_dir(dir_path, ambient_authority()) .map_err(|error| format!("open {dir_path}: {error}"))?; Ok(dir.read_to_string(file_name)) } +/// Resolve the repository root from the test crate directory. fn repo_root() -> TestResult { let crate_dir = Utf8Path::new(env!("CARGO_MANIFEST_DIR")); crate_dir @@ -182,6 +192,7 @@ fn repo_root() -> TestResult { .ok_or_else(|| format!("cannot resolve repository root from {crate_dir}")) } +/// Return an error when a test invariant is false. fn ensure(condition: bool, message: impl Into) -> TestResult { if condition { Ok(()) @@ -190,6 +201,7 @@ fn ensure(condition: bool, message: impl Into) -> TestResult { } } +/// Return a diff-friendly error when two values differ. fn ensure_equal(left: &T, right: &T, message: impl Into) -> TestResult where T: std::fmt::Debug + PartialEq, @@ -204,6 +216,7 @@ where } } +/// Check a boundary error variant while preserving diagnostics on mismatch. fn assert_boundary_error( error: &BoundaryError, predicate: impl FnOnce(&BoundaryError) -> bool, @@ -215,11 +228,13 @@ fn assert_boundary_error( } } +/// Temp directory wrapper that exposes a UTF-8 path. struct Utf8TempDir { _temp: TempDir, path: Utf8PathBuf, } impl Utf8TempDir { + /// Return the UTF-8 temp directory path. fn path(&self) -> &Utf8Path { &self.path } } diff --git a/crates/weaver-docs-gate/tests/support/pending_review_date.rs b/crates/weaver-docs-gate/tests/support/pending_review_date.rs index 828b8e8a..44a4c69a 100644 --- a/crates/weaver-docs-gate/tests/support/pending_review_date.rs +++ b/crates/weaver-docs-gate/tests/support/pending_review_date.rs @@ -6,6 +6,7 @@ const PENDING_REVIEW_WINDOW_DAYS: i64 = 270; type TestResult = Result; +/// Return an error when a pending-review invariant is false. fn ensure(condition: bool, message: impl Into) -> TestResult { if condition { Ok(()) @@ -14,6 +15,7 @@ fn ensure(condition: bool, message: impl Into) -> TestResult { } } +/// Validate that a pending row's review date is not stale. pub(crate) fn validate_pending_review_date(value: &str, task_id: &str) -> TestResult { let review_date = parse_review_date(value, task_id)?; let build_date = build_date()?; @@ -26,10 +28,12 @@ pub(crate) fn validate_pending_review_date(value: &str, task_id: &str) -> TestRe ) } +/// Parse a pending-review date for one manifest task. fn parse_review_date(value: &str, task_id: &str) -> TestResult { Date::parse(value, &Iso8601::DATE).map_err(|_| invalid_date_message(value, task_id)) } +/// Return the build date used for deterministic staleness checks. fn build_date() -> TestResult { if let Ok(epoch) = std::env::var("SOURCE_DATE_EPOCH") { let seconds = epoch @@ -43,10 +47,12 @@ fn build_date() -> TestResult { } } +/// Build a diagnostic for an invalid pending-review date. fn invalid_date_message(value: &str, task_id: &str) -> String { format!("task {task_id} has invalid next_review_by date {value:?}") } +/// Return whether the review date is within the allowed review window. fn is_review_current(review_date: Date, build_date: Date) -> bool { (build_date - review_date).whole_days() <= PENDING_REVIEW_WINDOW_DAYS } @@ -59,6 +65,7 @@ mod tests { use super::is_review_current; + /// Prove the review window accepts the exact maximum age. #[test] fn review_window_accepts_exact_boundary() { assert!(is_review_current( @@ -67,6 +74,7 @@ mod tests { )); } + /// Prove the review window rejects stale pending reviews. #[test] fn review_window_rejects_expired_pending_review() { assert!(!is_review_current( diff --git a/docs/developers-guide.md b/docs/developers-guide.md index ea33a4fe..ceae6cd9 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -60,8 +60,8 @@ leak into the boundary vocabulary: `managed_tasks` registry must match the ordered `tasks` rows so new command-contract roadmap items fail closed until explicitly classified. - `BoundaryError` reports manifest loading failures. Match it when a test or - developer tool must distinguish a missing file, invalid path, read failure, - or TOML schema error. + developer tool must distinguish a missing file, invalid path, unreadable + manifest, or invalid boundary schema. Use `load_manifest(path)` to read `docs/orthoconfig-consumer-boundary.toml` into a `BoundaryManifest`. Use `render_matrix(&manifest)` to produce the From 221e32303605b0d2320a8e33608a47092c48d00d Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 21 Jun 2026 12:38:37 +0200 Subject: [PATCH 14/22] Make boundary review dates explicit Pass the manifest build date into pending-review validation instead of reading `SOURCE_DATE_EPOCH` or the wall clock from the helper. This keeps the staleness gate deterministic and makes the time dependency visible at the call site. Add inline snapshots for `BoundaryError` display strings so the public error messages have stable regression coverage. --- .../tests/boundary_manifest.rs | 8 ++++- .../weaver-docs-gate/tests/load_manifest.rs | 29 +++++++++++++++++ .../tests/support/pending_review_date.rs | 23 ++++---------- ...-1-1-track-downstream-consumer-boundary.md | 31 ++++++++++++------- 4 files changed, 61 insertions(+), 30 deletions(-) diff --git a/crates/weaver-docs-gate/tests/boundary_manifest.rs b/crates/weaver-docs-gate/tests/boundary_manifest.rs index 32eabb1f..32d8ab68 100644 --- a/crates/weaver-docs-gate/tests/boundary_manifest.rs +++ b/crates/weaver-docs-gate/tests/boundary_manifest.rs @@ -7,6 +7,7 @@ use std::collections::BTreeSet; use camino::{Utf8Path, Utf8PathBuf}; use cap_std::{ambient_authority, fs::Dir}; +use time::{Date, macros::date}; use weaver_docs_gate::{ BoundaryManifest, BoundaryState, @@ -17,6 +18,7 @@ use weaver_docs_gate::{ const ADR_007: &str = "docs/adr-007-agent-native-command-surface.md"; const MANIFEST: &str = "docs/orthoconfig-consumer-boundary.toml"; +const MANIFEST_BUILD_DATE: Date = date!(2026 - 06 - 21); const MATRIX: &str = "docs/orthoconfig-consumer-boundary.md"; const ROADMAP: &str = "docs/roadmap.md"; @@ -272,7 +274,11 @@ fn validate_pending_evidence(task: &BoundaryTask) -> TestResult { .as_deref() .ok_or_else(|| format!("pending task {} must provide next_review_by", task.id))?; validate_date(next_review_by, FieldName::NextReviewBy, task)?; - pending_review_date::validate_pending_review_date(next_review_by, &task.id)?; + pending_review_date::validate_pending_review_date( + next_review_by, + &task.id, + MANIFEST_BUILD_DATE, + )?; ensure( task.shipped_in.is_none(), format!("pending task {} must not carry shipped_in", task.id), diff --git a/crates/weaver-docs-gate/tests/load_manifest.rs b/crates/weaver-docs-gate/tests/load_manifest.rs index 3b21e51b..119ad676 100644 --- a/crates/weaver-docs-gate/tests/load_manifest.rs +++ b/crates/weaver-docs-gate/tests/load_manifest.rs @@ -89,6 +89,35 @@ fn load_manifest_reports_invalid_toml() -> TestResult { }) } +/// Snapshot the stable display strings for public boundary errors. +#[test] +fn boundary_error_display_messages_are_stable() { + insta::assert_snapshot!( + BoundaryError::NotFound(Utf8PathBuf::from("docs/missing.toml")).to_string(), + @"manifest file not found: docs/missing.toml" + ); + insta::assert_snapshot!( + BoundaryError::InvalidPath(Utf8PathBuf::from("/")).to_string(), + @"invalid manifest path: /" + ); + insta::assert_snapshot!( + BoundaryError::Unreadable { + path: Utf8PathBuf::from("docs/manifest.toml"), + detail: "permission denied".into(), + } + .to_string(), + @"boundary manifest cannot be read: docs/manifest.toml: permission denied" + ); + insta::assert_snapshot!( + BoundaryError::InvalidSchema { + path: Utf8PathBuf::from("docs/manifest.toml"), + detail: "missing field `schema_version`".into(), + } + .to_string(), + @"invalid boundary manifest schema in docs/manifest.toml: missing field `schema_version`" + ); +} + /// Prove adapter DTOs map manifest values into plain domain types. #[test] fn load_manifest_maps_toml_dto_to_domain_types() -> TestResult { diff --git a/crates/weaver-docs-gate/tests/support/pending_review_date.rs b/crates/weaver-docs-gate/tests/support/pending_review_date.rs index 44a4c69a..ccb6a629 100644 --- a/crates/weaver-docs-gate/tests/support/pending_review_date.rs +++ b/crates/weaver-docs-gate/tests/support/pending_review_date.rs @@ -1,6 +1,6 @@ //! Pending dependency review-date expiry checks for the boundary manifest gate. -use time::{Date, OffsetDateTime, format_description::well_known::Iso8601}; +use time::{Date, format_description::well_known::Iso8601}; const PENDING_REVIEW_WINDOW_DAYS: i64 = 270; @@ -16,9 +16,12 @@ fn ensure(condition: bool, message: impl Into) -> TestResult { } /// Validate that a pending row's review date is not stale. -pub(crate) fn validate_pending_review_date(value: &str, task_id: &str) -> TestResult { +pub(crate) fn validate_pending_review_date( + value: &str, + task_id: &str, + build_date: Date, +) -> TestResult { let review_date = parse_review_date(value, task_id)?; - let build_date = build_date()?; ensure( is_review_current(review_date, build_date), format!( @@ -33,20 +36,6 @@ fn parse_review_date(value: &str, task_id: &str) -> TestResult { Date::parse(value, &Iso8601::DATE).map_err(|_| invalid_date_message(value, task_id)) } -/// Return the build date used for deterministic staleness checks. -fn build_date() -> TestResult { - if let Ok(epoch) = std::env::var("SOURCE_DATE_EPOCH") { - let seconds = epoch - .parse::() - .map_err(|error| format!("SOURCE_DATE_EPOCH must be Unix seconds: {error}"))?; - OffsetDateTime::from_unix_timestamp(seconds) - .map(OffsetDateTime::date) - .map_err(|error| format!("SOURCE_DATE_EPOCH is outside supported range: {error}")) - } else { - Ok(OffsetDateTime::now_utc().date()) - } -} - /// Build a diagnostic for an invalid pending-review date. fn invalid_date_message(value: &str, task_id: &str) -> String { format!("task {task_id} has invalid next_review_by date {value:?}") diff --git a/docs/execplans/12-1-1-track-downstream-consumer-boundary.md b/docs/execplans/12-1-1-track-downstream-consumer-boundary.md index 353ac27a..07c7e31f 100644 --- a/docs/execplans/12-1-1-track-downstream-consumer-boundary.md +++ b/docs/execplans/12-1-1-track-downstream-consumer-boundary.md @@ -251,10 +251,16 @@ state, not the intended sequence. `make test`. - [x] 2026-06-15T23:20:03Z: The pending review expiry check now parses `next_review_by` as a calendar date and fails pending rows that are - more than 270 days behind the build date. The build date comes from - `SOURCE_DATE_EPOCH` when set and from the wall clock otherwise. + more than 270 days behind the explicit manifest build date supplied by + the test gate. Validation passed with `cargo test -p weaver-docs-gate`, `make check-fmt`, `make lint`, and `make test`. +- [x] 2026-06-21T00:00:00Z: Review feedback removed the implicit time source + from the pending-review staleness helper. The helper now accepts the + build date as a parameter, and the manifest integration test passes a + fixed `MANIFEST_BUILD_DATE` so validation does not depend on + `SOURCE_DATE_EPOCH` or the wall clock. The same follow-up added inline + snapshots for stable `BoundaryError` display strings. ## Surprises & discoveries @@ -305,7 +311,7 @@ Recorded as they occur during implementation. Format: like `YYYY-MM-DD` but did not compare it with the build date. Impact: `crates/weaver-docs-gate/tests/support/pending_review_date.rs` now parses the date with the `time` crate and rejects pending reviews more than - 270 days behind `SOURCE_DATE_EPOCH` or the wall-clock date. + 270 days behind the explicit build date provided by the manifest gate. ## Decision log @@ -394,8 +400,8 @@ Recorded for any decision that future work must respect. - Decision: use the existing workspace `time` crate for pending review date parsing and build-date comparison. Rationale: manual calendar arithmetic conflicts with the repository's - strict integer-division and remainder lints, while `time::Date` and - `OffsetDateTime` express the policy directly and keep the gate portable. + strict integer-division and remainder lints, while `time::Date` expresses + the explicit-date policy directly and keeps the gate portable. Date/Author: 2026-06-15, implementation. ## Outcomes & retrospective @@ -425,8 +431,9 @@ main implementation completed with `findings: 0`. A post-completion fix closed the documented pending-review expiry gap. Pending rows now fail once `next_review_by` is more than 270 days behind the build -date, using `SOURCE_DATE_EPOCH` for reproducible builds and the wall clock -otherwise. +date supplied by the manifest gate. A later review follow-up made that build +date an explicit parameter so the gate no longer depends on +`SOURCE_DATE_EPOCH` or the wall clock. ## Context and orientation @@ -776,12 +783,12 @@ Stage D enforces the matrix at CI time. - **Date formats.** `last_reviewed` and `next_review_by` parse as ISO-8601 dates (`YYYY-MM-DD`). - **Staleness budget (warn).** When any row's `last_reviewed` is - more than 270 days behind the build's `SOURCE_DATE_EPOCH` (or wall - clock if the variable is unset), the test prints a warning naming - the row but does not fail. + more than 270 days behind the explicit build date supplied by the + manifest gate, the test prints a warning naming the row but does not + fail. - **`pending` decay budget (fail).** When a `pending` row's - `next_review_by` is more than 270 days *behind* the same - reference date, the test fails. This stops `pending` rows from + `next_review_by` is more than 270 days *behind* the same explicit + build date, the test fails. This stops `pending` rows from becoming a parking lot. - **Matrix freshness.** The Markdown produced by `render_matrix` equals the committed `docs/orthoconfig-consumer-boundary.md` From 67159222709a82deebe043786e1c26207142e254 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 21 Jun 2026 16:11:31 +0200 Subject: [PATCH 15/22] Separate boundary manifest parsing Make `load_manifest` parse from a `Read` input and move filesystem path handling into `load_manifest_file`. This keeps the manifest domain API free of path-resolution concerns while preserving the capability-oriented file adapter used by the regeneration example. Expand module documentation for the parser, TOML adapter, renderer, and test support relationships. Add proptest coverage for renderer column widths, cell escaping, phase grouping, and pending-review date windows. --- .../examples/render_boundary_matrix.rs | 4 +- crates/weaver-docs-gate/src/lib.rs | 120 +++++++++++++++--- .../weaver-docs-gate/src/manifest_adapter.rs | 7 + crates/weaver-docs-gate/src/renderer/mod.rs | 6 + crates/weaver-docs-gate/src/renderer/tests.rs | 70 ++++++++++ .../tests/boundary_manifest.rs | 4 +- .../weaver-docs-gate/tests/load_manifest.rs | 90 +++++++++---- .../tests/support/pending_review_date.rs | 39 +++++- docs/developers-guide.md | 24 ++-- ...-1-1-track-downstream-consumer-boundary.md | 40 +++--- 10 files changed, 327 insertions(+), 77 deletions(-) diff --git a/crates/weaver-docs-gate/examples/render_boundary_matrix.rs b/crates/weaver-docs-gate/examples/render_boundary_matrix.rs index dc3e304a..01f1566d 100644 --- a/crates/weaver-docs-gate/examples/render_boundary_matrix.rs +++ b/crates/weaver-docs-gate/examples/render_boundary_matrix.rs @@ -4,7 +4,7 @@ use std::env; use camino::Utf8Path; use cap_std::{ambient_authority, fs::Dir}; -use weaver_docs_gate::{load_manifest, render_matrix}; +use weaver_docs_gate::{load_manifest_file, render_matrix}; /// Regenerate a boundary matrix from a manifest path and output path. fn main() -> Result<(), String> { @@ -21,7 +21,7 @@ fn main() -> Result<(), String> { } let manifest = - load_manifest(Utf8Path::new(&manifest_path)).map_err(|error| error.to_string())?; + load_manifest_file(Utf8Path::new(&manifest_path)).map_err(|error| error.to_string())?; write_output(Utf8Path::new(&output_path), render_matrix(&manifest)) } diff --git a/crates/weaver-docs-gate/src/lib.rs b/crates/weaver-docs-gate/src/lib.rs index d43793b0..022ef284 100644 --- a/crates/weaver-docs-gate/src/lib.rs +++ b/crates/weaver-docs-gate/src/lib.rs @@ -1,6 +1,14 @@ //! Tooling for the `OrthoConfig` consumer boundary matrix. +//! +//! This crate keeps the boundary manifest pipeline in three small pieces: +//! public domain types live in this root module, `manifest_adapter` owns the +//! TOML and Serde conversion layer, and `renderer` turns a validated +//! [`BoundaryManifest`] into the checked-in Markdown matrix. Filesystem access +//! is intentionally limited to [`load_manifest_file`]; callers that already +//! have manifest bytes should use [`load_manifest`] so parsing stays independent +//! of path handling. -use std::io::{self, ErrorKind}; +use std::io::{self, ErrorKind, Read}; use camino::{Utf8Path, Utf8PathBuf}; use cap_std::{ambient_authority, fs::Dir}; @@ -188,15 +196,13 @@ pub struct BoundaryManifest { pub tasks: Vec, } -/// Errors returned while loading the boundary manifest. +/// Errors returned while parsing the boundary manifest. /// /// # Examples /// ``` -/// use camino::Utf8PathBuf; /// use weaver_docs_gate::BoundaryError; /// /// let error = BoundaryError::InvalidSchema { -/// path: Utf8PathBuf::from("docs/orthoconfig-consumer-boundary.toml"), /// detail: "missing field `schema_version`".into(), /// }; /// assert!( @@ -207,6 +213,39 @@ pub struct BoundaryManifest { /// ``` #[derive(Debug, thiserror::Error)] pub enum BoundaryError { + /// Manifest bytes could not be read from the supplied reader. + #[error("boundary manifest cannot be read: {detail}")] + Unreadable { + /// Stable human-readable read failure detail. + detail: String, + }, + /// The manifest contents do not match the boundary manifest schema. + #[error("invalid boundary manifest schema: {detail}")] + InvalidSchema { + /// Stable human-readable schema failure detail. + detail: String, + }, +} + +/// Errors returned while loading a boundary manifest from a file. +/// +/// # Examples +/// ``` +/// use camino::Utf8PathBuf; +/// use weaver_docs_gate::BoundaryFileError; +/// +/// let error = BoundaryFileError::InvalidSchema { +/// path: Utf8PathBuf::from("docs/orthoconfig-consumer-boundary.toml"), +/// detail: "missing field `schema_version`".into(), +/// }; +/// assert!( +/// error +/// .to_string() +/// .contains("invalid boundary manifest schema") +/// ); +/// ``` +#[derive(Debug, thiserror::Error)] +pub enum BoundaryFileError { /// The manifest path does not exist. #[error("manifest file not found: {0}")] NotFound(Utf8PathBuf), @@ -231,37 +270,72 @@ pub enum BoundaryError { }, } +/// Parse the boundary manifest from a byte reader. +/// +/// # Errors +/// +/// Returns [`BoundaryError`] when the reader fails or when the bytes do not +/// match the boundary schema. +/// +/// # Examples +/// ``` +/// use weaver_docs_gate::load_manifest; +/// +/// let manifest = load_manifest( +/// br#"schema_version = 1 +/// managed_tasks = [] +/// task = [] +/// "# +/// .as_slice(), +/// )?; +/// assert_eq!(manifest.schema_version, 1); +/// # Ok::<(), weaver_docs_gate::BoundaryError>(()) +/// ``` +pub fn load_manifest(mut reader: impl Read) -> Result { + let mut contents = String::new(); + reader + .read_to_string(&mut contents) + .map_err(|source| BoundaryError::Unreadable { + detail: source.to_string(), + })?; + parse_manifest(&contents) +} + /// Load the boundary manifest from disk. /// /// # Errors /// -/// Returns [`BoundaryError`] when the manifest is missing, unreadable, or does -/// not match the boundary schema. +/// Returns [`BoundaryFileError`] when the manifest is missing, unreadable, or +/// does not match the boundary schema. /// /// # Examples /// ```no_run /// use camino::Utf8Path; -/// use weaver_docs_gate::load_manifest; +/// use weaver_docs_gate::load_manifest_file; /// -/// let manifest = load_manifest(Utf8Path::new("docs/orthoconfig-consumer-boundary.toml"))?; +/// let manifest = load_manifest_file(Utf8Path::new("docs/orthoconfig-consumer-boundary.toml"))?; /// assert_eq!(manifest.schema_version, 1); -/// # Ok::<(), weaver_docs_gate::BoundaryError>(()) +/// # Ok::<(), weaver_docs_gate::BoundaryFileError>(()) /// ``` -pub fn load_manifest(path: &Utf8Path) -> Result { +pub fn load_manifest_file(path: &Utf8Path) -> Result { let parent = path.parent().unwrap_or_else(|| Utf8Path::new(".")); let file_name = path .file_name() - .ok_or_else(|| BoundaryError::InvalidPath(path.to_path_buf()))?; + .ok_or_else(|| BoundaryFileError::InvalidPath(path.to_path_buf()))?; let dir = Dir::open_ambient_dir(parent, ambient_authority()) .map_err(|source| read_error(path, &source))?; let contents = dir .read_to_string(file_name) .map_err(|source| read_error(path, &source))?; + parse_manifest(&contents).map_err(|error| file_error(path, error)) +} + +/// Parse TOML manifest text into the public domain manifest. +fn parse_manifest(contents: &str) -> Result { let manifest = - toml::from_str::(&contents).map_err(|source| { + toml::from_str::(contents).map_err(|source| { BoundaryError::InvalidSchema { - path: path.to_path_buf(), detail: source.to_string(), } })?; @@ -269,13 +343,27 @@ pub fn load_manifest(path: &Utf8Path) -> Result } /// Convert filesystem failures into stable manifest loading errors. -fn read_error(path: &Utf8Path, source: &io::Error) -> BoundaryError { +fn read_error(path: &Utf8Path, source: &io::Error) -> BoundaryFileError { if source.kind() == ErrorKind::NotFound { - BoundaryError::NotFound(path.to_path_buf()) + BoundaryFileError::NotFound(path.to_path_buf()) } else { - BoundaryError::Unreadable { + BoundaryFileError::Unreadable { path: path.to_path_buf(), detail: source.to_string(), } } } + +/// Attach file context to domain parser errors. +fn file_error(path: &Utf8Path, error: BoundaryError) -> BoundaryFileError { + match error { + BoundaryError::Unreadable { detail } => BoundaryFileError::Unreadable { + path: path.to_path_buf(), + detail, + }, + BoundaryError::InvalidSchema { detail } => BoundaryFileError::InvalidSchema { + path: path.to_path_buf(), + detail, + }, + } +} diff --git a/crates/weaver-docs-gate/src/manifest_adapter.rs b/crates/weaver-docs-gate/src/manifest_adapter.rs index ae7e3afe..616a09bd 100644 --- a/crates/weaver-docs-gate/src/manifest_adapter.rs +++ b/crates/weaver-docs-gate/src/manifest_adapter.rs @@ -1,4 +1,11 @@ //! TOML adapter for the `OrthoConfig` consumer boundary manifest. +//! +//! This module is the crate's outbound deserialization adapter. It owns every +//! Serde derive and TOML spelling detail, then converts those DTOs into the +//! plain domain structs exported by `lib.rs`. `load_manifest` and +//! `load_manifest_file` parse text through `BoundaryManifestDto`; the renderer +//! never sees these adapter types and consumes only the resulting +//! `BoundaryManifest`. use serde::{Deserialize, Deserializer}; diff --git a/crates/weaver-docs-gate/src/renderer/mod.rs b/crates/weaver-docs-gate/src/renderer/mod.rs index 03e37de2..25c2acdb 100644 --- a/crates/weaver-docs-gate/src/renderer/mod.rs +++ b/crates/weaver-docs-gate/src/renderer/mod.rs @@ -1,4 +1,10 @@ //! Markdown rendering for the `OrthoConfig` consumer boundary matrix. +//! +//! The renderer is the final stage of the docs-gate pipeline. It accepts the +//! domain `BoundaryManifest` produced by `load_manifest` or `load_manifest_file`, +//! groups its task rows by roadmap phase, escapes Markdown table cells, computes +//! stable column widths from the escaped data, and returns the full Markdown +//! document that is committed as `docs/orthoconfig-consumer-boundary.md`. use std::collections::BTreeMap; diff --git a/crates/weaver-docs-gate/src/renderer/tests.rs b/crates/weaver-docs-gate/src/renderer/tests.rs index 1e488fe8..342b14ca 100644 --- a/crates/weaver-docs-gate/src/renderer/tests.rs +++ b/crates/weaver-docs-gate/src/renderer/tests.rs @@ -64,6 +64,64 @@ fn snapshots_rendered_matrix_shape() { } proptest! { + #[test] + /// Prove computed widths accommodate every generated escaped cell. + fn column_widths_accommodate_all_generated_cells( + roadmap_task in "[A-Za-z0-9 .|\\n]{0,32}", + gist in "[A-Za-z0-9 .|\\n]{0,64}", + upstream in "[A-Za-z0-9 .|\\n]{0,64}", + gate_or_divergence in "[A-Za-z0-9 .|\\n]{0,64}", + ) { + let row = MatrixRow { + roadmap_task: escape_cell(&roadmap_task), + gist: escape_cell(&gist), + state: "✓ consumes".into(), + upstream: escape_cell(&upstream), + shipped_in: "4339a6f3".into(), + gate_or_divergence: escape_cell(&gate_or_divergence), + next_review_by: "n/a".into(), + last_reviewed: "2026-06-20".into(), + }; + + let widths = ColumnWidths::for_rows(&[row]); + + prop_assert!(widths.roadmap_task >= cell_width(&escape_cell(&roadmap_task))); + prop_assert!(widths.gist >= cell_width(&escape_cell(&gist))); + prop_assert!(widths.upstream >= cell_width(&escape_cell(&upstream))); + prop_assert!(widths.gate_or_divergence >= cell_width(&escape_cell(&gate_or_divergence))); + } + + #[test] + /// Prove cell escaping removes raw newlines and unescaped table pipes. + fn escaped_cells_do_not_contain_table_breaks(value in "[A-Za-z0-9 .|\\n]{0,128}") { + let escaped = escape_cell(&value); + + prop_assert!(!escaped.contains('\n')); + prop_assert!(!has_unescaped_pipe(&escaped)); + } + + #[test] + /// Prove arbitrary numeric phase IDs are grouped exactly once. + fn phase_grouping_preserves_arbitrary_phase_ids(phases in proptest::collection::vec(1u16..=999, 1..32)) { + let tasks = phases + .iter() + .enumerate() + .map(|(index, phase)| task(&format!("{phase}.{index}.1"))) + .collect::>(); + let manifest = manifest_with_tasks(tasks); + let rows = grouped_rows(&manifest); + let expected = phases + .iter() + .map(u16::to_string) + .collect::>(); + + prop_assert_eq!(rows.keys().map(|phase| (*phase).to_owned()).collect::>(), expected); + for (phase, grouped) in rows { + let prefix = format!("[{phase}."); + prop_assert!(grouped.iter().all(|row| row.roadmap_task.starts_with(&prefix))); + } + } + #[test] /// Prove arbitrary task text cannot add Markdown table columns. fn rendered_task_cells_never_emit_raw_table_pipes( @@ -84,6 +142,18 @@ proptest! { } } +/// Return whether a rendered cell contains a pipe not escaped for Markdown. +fn has_unescaped_pipe(value: &str) -> bool { + let mut previous_was_escape = false; + for character in value.chars() { + if character == '|' && !previous_was_escape { + return true; + } + previous_was_escape = character == '\\' && !previous_was_escape; + } + false +} + /// Build a manifest containing the supplied task rows. fn manifest_with_tasks(tasks: Vec) -> BoundaryManifest { let managed_tasks = tasks.iter().map(|task| task.id.clone()).collect(); diff --git a/crates/weaver-docs-gate/tests/boundary_manifest.rs b/crates/weaver-docs-gate/tests/boundary_manifest.rs index 32d8ab68..87a88f43 100644 --- a/crates/weaver-docs-gate/tests/boundary_manifest.rs +++ b/crates/weaver-docs-gate/tests/boundary_manifest.rs @@ -137,7 +137,9 @@ fn committed_matrix_matches_manifest_rendering() -> TestResult { /// Load the boundary manifest from the repository docs. fn manifest() -> TestResult { - load_manifest(&repo_path(Utf8Path::new(MANIFEST))?).map_err(|error| error.to_string()) + let contents = + read_doc(Utf8Path::new(MANIFEST)).map_err(|error| format!("read {MANIFEST}: {error}"))?; + load_manifest(contents.as_bytes()).map_err(|error| error.to_string()) } /// Read a repository documentation file as UTF-8 text. diff --git a/crates/weaver-docs-gate/tests/load_manifest.rs b/crates/weaver-docs-gate/tests/load_manifest.rs index 119ad676..8203675f 100644 --- a/crates/weaver-docs-gate/tests/load_manifest.rs +++ b/crates/weaver-docs-gate/tests/load_manifest.rs @@ -1,11 +1,22 @@ -//! Unit-level coverage for loading boundary manifests from disk. +//! Unit-level coverage for boundary manifest parsing and file loading. +//! +//! The tests in this module deliberately exercise both entry points in the +//! docs-gate crate. `load_manifest` receives in-memory manifest bytes and +//! proves the path-free parser behaviour; `load_manifest_file` receives UTF-8 +//! paths and proves the filesystem adapter's error mapping. use std::process::Command; use camino::{Utf8Path, Utf8PathBuf}; use cap_std::{ambient_authority, fs::Dir}; use tempfile::TempDir; -use weaver_docs_gate::{BoundaryError, BoundaryState, load_manifest}; +use weaver_docs_gate::{ + BoundaryError, + BoundaryFileError, + BoundaryState, + load_manifest, + load_manifest_file, +}; type TestResult = Result; @@ -34,22 +45,24 @@ fn load_manifest_reports_missing_files() -> TestResult { let temp = temp_dir()?; let missing = temp.path().join("missing.toml"); - let error = load_manifest(&missing).err().ok_or("missing file loaded")?; + let error = load_manifest_file(&missing) + .err() + .ok_or("missing file loaded")?; - assert_boundary_error(&error, |boundary_error| { - matches!(boundary_error, BoundaryError::NotFound(_)) + assert_boundary_file_error(&error, |boundary_error| { + matches!(boundary_error, BoundaryFileError::NotFound(_)) }) } /// Prove paths without a file name return the invalid-path variant. #[test] fn load_manifest_reports_invalid_paths() -> TestResult { - let error = load_manifest(Utf8Path::new("/")) + let error = load_manifest_file(Utf8Path::new("/")) .err() .ok_or("root path loaded as manifest")?; - assert_boundary_error(&error, |boundary_error| { - matches!(boundary_error, BoundaryError::InvalidPath(_)) + assert_boundary_file_error(&error, |boundary_error| { + matches!(boundary_error, BoundaryFileError::InvalidPath(_)) }) } @@ -63,24 +76,19 @@ fn load_manifest_reports_read_errors() -> TestResult { .create_dir("manifest.toml") .map_err(|error| format!("create manifest directory: {error}"))?; - let error = load_manifest(&manifest_dir) + let error = load_manifest_file(&manifest_dir) .err() .ok_or("directory loaded as manifest")?; - assert_boundary_error(&error, |boundary_error| { - matches!(boundary_error, BoundaryError::Unreadable { .. }) + assert_boundary_file_error(&error, |boundary_error| { + matches!(boundary_error, BoundaryFileError::Unreadable { .. }) }) } /// Prove malformed manifest contents return the schema variant. #[test] fn load_manifest_reports_invalid_toml() -> TestResult { - let temp = temp_dir()?; - let manifest_path = temp.path().join("manifest.toml"); - write_file(temp.path(), "manifest.toml", "schema_version = ")? - .map_err(|error| format!("write invalid manifest: {error}"))?; - - let error = load_manifest(&manifest_path) + let error = load_manifest(b"schema_version = ".as_slice()) .err() .ok_or("invalid TOML loaded as manifest")?; @@ -89,19 +97,38 @@ fn load_manifest_reports_invalid_toml() -> TestResult { }) } -/// Snapshot the stable display strings for public boundary errors. +/// Snapshot the stable display strings for public parser errors. #[test] fn boundary_error_display_messages_are_stable() { insta::assert_snapshot!( - BoundaryError::NotFound(Utf8PathBuf::from("docs/missing.toml")).to_string(), + BoundaryError::Unreadable { + detail: "stream closed".into(), + } + .to_string(), + @"boundary manifest cannot be read: stream closed" + ); + insta::assert_snapshot!( + BoundaryError::InvalidSchema { + detail: "missing field `schema_version`".into(), + } + .to_string(), + @"invalid boundary manifest schema: missing field `schema_version`" + ); +} + +/// Snapshot the stable display strings for public file-adapter errors. +#[test] +fn boundary_file_error_display_messages_are_stable() { + insta::assert_snapshot!( + BoundaryFileError::NotFound(Utf8PathBuf::from("docs/missing.toml")).to_string(), @"manifest file not found: docs/missing.toml" ); insta::assert_snapshot!( - BoundaryError::InvalidPath(Utf8PathBuf::from("/")).to_string(), + BoundaryFileError::InvalidPath(Utf8PathBuf::from("/")).to_string(), @"invalid manifest path: /" ); insta::assert_snapshot!( - BoundaryError::Unreadable { + BoundaryFileError::Unreadable { path: Utf8PathBuf::from("docs/manifest.toml"), detail: "permission denied".into(), } @@ -109,7 +136,7 @@ fn boundary_error_display_messages_are_stable() { @"boundary manifest cannot be read: docs/manifest.toml: permission denied" ); insta::assert_snapshot!( - BoundaryError::InvalidSchema { + BoundaryFileError::InvalidSchema { path: Utf8PathBuf::from("docs/manifest.toml"), detail: "missing field `schema_version`".into(), } @@ -121,12 +148,7 @@ fn boundary_error_display_messages_are_stable() { /// Prove adapter DTOs map manifest values into plain domain types. #[test] fn load_manifest_maps_toml_dto_to_domain_types() -> TestResult { - let temp = temp_dir()?; - let manifest_path = temp.path().join("manifest.toml"); - write_file(temp.path(), "manifest.toml", VALID_MANIFEST)? - .map_err(|error| format!("write valid manifest: {error}"))?; - - let manifest = load_manifest(&manifest_path).map_err(|error| error.to_string())?; + let manifest = load_manifest(VALID_MANIFEST.as_bytes()).map_err(|error| error.to_string())?; ensure_equal(&manifest.schema_version, &1, "schema version should load")?; ensure_equal( @@ -257,6 +279,18 @@ fn assert_boundary_error( } } +/// Check a boundary file error variant while preserving diagnostics on mismatch. +fn assert_boundary_file_error( + error: &BoundaryFileError, + predicate: impl FnOnce(&BoundaryFileError) -> bool, +) -> TestResult { + if predicate(error) { + Ok(()) + } else { + Err(format!("unexpected boundary file error: {error:?}")) + } +} + /// Temp directory wrapper that exposes a UTF-8 path. struct Utf8TempDir { _temp: TempDir, diff --git a/crates/weaver-docs-gate/tests/support/pending_review_date.rs b/crates/weaver-docs-gate/tests/support/pending_review_date.rs index ccb6a629..e310c673 100644 --- a/crates/weaver-docs-gate/tests/support/pending_review_date.rs +++ b/crates/weaver-docs-gate/tests/support/pending_review_date.rs @@ -1,4 +1,10 @@ //! Pending dependency review-date expiry checks for the boundary manifest gate. +//! +//! This support module is compiled only by the boundary manifest integration +//! test. It keeps the time-window rule isolated from file parsing and document +//! rendering: callers provide the build date explicitly, and this module only +//! parses `next_review_by` values and checks them against the 270-day pending +//! review budget. use time::{Date, format_description::well_known::Iso8601}; @@ -50,9 +56,10 @@ fn is_review_current(review_date: Date, build_date: Date) -> bool { mod tests { //! Unit tests for pending review age-window comparisons. - use time::macros::date; + use proptest::prelude::*; + use time::{Duration, macros::date}; - use super::is_review_current; + use super::{PENDING_REVIEW_WINDOW_DAYS, is_review_current, validate_pending_review_date}; /// Prove the review window accepts the exact maximum age. #[test] @@ -71,4 +78,32 @@ mod tests { date!(2026 - 09 - 29) )); } + + proptest! { + #[test] + /// Prove all generated dates inside the review window are accepted. + fn review_window_accepts_generated_in_range(age in 0i64..=PENDING_REVIEW_WINDOW_DAYS) { + let build_date = date!(2026 - 12 - 31); + let review_date = build_date - Duration::days(age); + + prop_assert!(validate_pending_review_date( + &review_date.to_string(), + "12.1.5", + build_date, + ).is_ok()); + } + + #[test] + /// Prove generated dates older than the review window are rejected. + fn review_window_rejects_generated_stale_dates(age in (PENDING_REVIEW_WINDOW_DAYS + 1)..=720) { + let build_date = date!(2026 - 12 - 31); + let review_date = build_date - Duration::days(age); + + prop_assert!(validate_pending_review_date( + &review_date.to_string(), + "12.1.5", + build_date, + ).is_err()); + } + } } diff --git a/docs/developers-guide.md b/docs/developers-guide.md index ceae6cd9..f3594b05 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -59,16 +59,20 @@ leak into the boundary vocabulary: - `BoundaryManifest` is the complete source-of-truth document. Its `managed_tasks` registry must match the ordered `tasks` rows so new command-contract roadmap items fail closed until explicitly classified. -- `BoundaryError` reports manifest loading failures. Match it when a test or - developer tool must distinguish a missing file, invalid path, unreadable - manifest, or invalid boundary schema. - -Use `load_manifest(path)` to read `docs/orthoconfig-consumer-boundary.toml` -into a `BoundaryManifest`. Use `render_matrix(&manifest)` to produce the -Markdown contents for `docs/orthoconfig-consumer-boundary.md`. The renderer -escapes table cells, groups rows by roadmap phase, and sizes columns from the -escaped cell content, so callers should pass domain values rather than -preformatted Markdown rows. +- `BoundaryError` reports path-free manifest parser failures. Match it when a + test or developer tool reads manifest bytes from an in-memory fixture, a + network response, or another non-filesystem source. +- `BoundaryFileError` reports filesystem adapter failures. Match it when a + tool must distinguish a missing file, invalid path, unreadable manifest file, + or invalid boundary schema with file context. + +Use `load_manifest(reader)` when the caller already owns manifest bytes. Use +`load_manifest_file(path)` when a tool needs the crate's capability-oriented +filesystem adapter to read `docs/orthoconfig-consumer-boundary.toml`. Use +`render_matrix(&manifest)` to produce the Markdown contents for +`docs/orthoconfig-consumer-boundary.md`. The renderer escapes table cells, +groups rows by roadmap phase, and sizes columns from the escaped cell content, +so callers should pass domain values rather than preformatted Markdown rows. When adding or renaming a public command: diff --git a/docs/execplans/12-1-1-track-downstream-consumer-boundary.md b/docs/execplans/12-1-1-track-downstream-consumer-boundary.md index 07c7e31f..3ca5a09d 100644 --- a/docs/execplans/12-1-1-track-downstream-consumer-boundary.md +++ b/docs/execplans/12-1-1-track-downstream-consumer-boundary.md @@ -261,6 +261,12 @@ state, not the intended sequence. fixed `MANIFEST_BUILD_DATE` so validation does not depend on `SOURCE_DATE_EPOCH` or the wall clock. The same follow-up added inline snapshots for stable `BoundaryError` display strings. +- [x] 2026-06-21T00:00:00Z: Review feedback split manifest parsing from the + filesystem adapter. `load_manifest` now accepts a `Read` input, + `load_manifest_file` owns capability-oriented path access, module docs + describe the parser/adapter/renderer relationships, and proptest + coverage now exercises column widths, cell escaping, phase grouping, and + date-window ranges. ## Surprises & discoveries @@ -457,11 +463,11 @@ A novice opening this plan needs the following landmarks before reading the does *not* touch it; the boundary gate has nothing to do with build scripts. - `crates/weaver-docs-gate/` — new workspace member introduced by this - plan. It hosts the manifest parser (`load_manifest`), the matrix - renderer (`render_matrix`), the `cargo run --example` regenerator, - and the `boundary_gate` integration test. It has no runtime - consumers; its only role is to make documentation governance - testable. + plan. It hosts the path-free manifest parser (`load_manifest`), the + filesystem adapter (`load_manifest_file`), the matrix renderer + (`render_matrix`), the `cargo run --example` regenerator, and the + `boundary_gate` integration test. It has no runtime consumers; its + only role is to make documentation governance testable. - `docs/developers-guide.md` — the long-form contributor guide. A new subsection ("Tracking the OrthoConfig consumer boundary") sits next to the existing ADR guidance. @@ -671,7 +677,8 @@ the human-readable matrix from it. test fixture in crate form. The crate's `src/lib.rs` exposes: ```rust - pub fn load_manifest(path: &Utf8Path) -> Result; + pub fn load_manifest(reader: impl Read) -> Result; + pub fn load_manifest_file(path: &Utf8Path) -> Result; pub fn render_matrix(manifest: &BoundaryManifest) -> String; ``` @@ -1029,24 +1036,21 @@ pub struct BoundaryManifest { #[derive(Debug, thiserror::Error)] pub enum BoundaryError { - #[error("manifest file not found: {0}")] - NotFound(camino::Utf8PathBuf), - #[error("invalid TOML: {0}")] - InvalidToml(String), - #[error("invalid manifest row: {0}")] - InvalidRow(String), - #[error("unknown upstream role: {0}")] - UnknownRole(String), + #[error("boundary manifest cannot be read: {detail}")] + Unreadable { detail: String }, + #[error("invalid boundary manifest schema: {detail}")] + InvalidSchema { detail: String }, } -pub fn load_manifest(path: &Utf8Path) -> Result; +pub fn load_manifest(reader: impl Read) -> Result; +pub fn load_manifest_file(path: &Utf8Path) -> Result; pub fn render_matrix(manifest: &BoundaryManifest) -> String; ``` The renderer takes no global state, accepts a parsed manifest, and -returns a `String`. The test gate calls `load_manifest` and -`render_matrix` and compares the rendered output against the committed -matrix Markdown byte for byte. +returns a `String`. The test gate reads the manifest file, calls +`load_manifest` and `render_matrix`, and compares the rendered output +against the committed matrix Markdown byte for byte. The integration test depends only on existing workspace dependencies (`rstest`, `pretty_assertions`, `googletest`, `toml`, `camino`, From da482ce07becdda8497a83470a6fc9568f366dff Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 21 Jun 2026 17:24:00 +0200 Subject: [PATCH 16/22] Deduplicate boundary error assertions Replace the separate parser and file-adapter error assertion helpers with one generic helper that preserves debug diagnostics on mismatch. Keep the load-manifest test cases and fixture data unchanged. --- .../weaver-docs-gate/tests/load_manifest.rs | 40 +++++-------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/crates/weaver-docs-gate/tests/load_manifest.rs b/crates/weaver-docs-gate/tests/load_manifest.rs index 8203675f..1ad3a9af 100644 --- a/crates/weaver-docs-gate/tests/load_manifest.rs +++ b/crates/weaver-docs-gate/tests/load_manifest.rs @@ -49,9 +49,7 @@ fn load_manifest_reports_missing_files() -> TestResult { .err() .ok_or("missing file loaded")?; - assert_boundary_file_error(&error, |boundary_error| { - matches!(boundary_error, BoundaryFileError::NotFound(_)) - }) + assert_error_variant(&error, |e| matches!(e, BoundaryFileError::NotFound(_))) } /// Prove paths without a file name return the invalid-path variant. @@ -61,9 +59,7 @@ fn load_manifest_reports_invalid_paths() -> TestResult { .err() .ok_or("root path loaded as manifest")?; - assert_boundary_file_error(&error, |boundary_error| { - matches!(boundary_error, BoundaryFileError::InvalidPath(_)) - }) + assert_error_variant(&error, |e| matches!(e, BoundaryFileError::InvalidPath(_))) } /// Prove non-file manifest paths return the unreadable variant. @@ -80,8 +76,8 @@ fn load_manifest_reports_read_errors() -> TestResult { .err() .ok_or("directory loaded as manifest")?; - assert_boundary_file_error(&error, |boundary_error| { - matches!(boundary_error, BoundaryFileError::Unreadable { .. }) + assert_error_variant(&error, |e| { + matches!(e, BoundaryFileError::Unreadable { .. }) }) } @@ -92,9 +88,7 @@ fn load_manifest_reports_invalid_toml() -> TestResult { .err() .ok_or("invalid TOML loaded as manifest")?; - assert_boundary_error(&error, |boundary_error| { - matches!(boundary_error, BoundaryError::InvalidSchema { .. }) - }) + assert_error_variant(&error, |e| matches!(e, BoundaryError::InvalidSchema { .. })) } /// Snapshot the stable display strings for public parser errors. @@ -267,27 +261,15 @@ where } } -/// Check a boundary error variant while preserving diagnostics on mismatch. -fn assert_boundary_error( - error: &BoundaryError, - predicate: impl FnOnce(&BoundaryError) -> bool, -) -> TestResult { - if predicate(error) { - Ok(()) - } else { - Err(format!("unexpected boundary error: {error:?}")) - } -} - -/// Check a boundary file error variant while preserving diagnostics on mismatch. -fn assert_boundary_file_error( - error: &BoundaryFileError, - predicate: impl FnOnce(&BoundaryFileError) -> bool, -) -> TestResult { +/// Check an error variant while preserving full debug diagnostics on mismatch. +fn assert_error_variant(error: &E, predicate: impl FnOnce(&E) -> bool) -> TestResult +where + E: std::fmt::Debug, +{ if predicate(error) { Ok(()) } else { - Err(format!("unexpected boundary file error: {error:?}")) + Err(format!("unexpected error variant: {error:?}")) } } From 92b02cc6ff8a4cc7dba9cf59a8a1d3101395c51a Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 21 Jun 2026 17:36:20 +0200 Subject: [PATCH 17/22] Expose boundary manifest gate diagnostics Add component-level docs for the renderer tests and matrix regeneration example. Make the boundary manifest gate emit structured failure messages, trace validation decisions, and increment a validation-failure metric while keeping the observability crates as docs-gate dev-dependencies. Run the boundary manifest gate as a named CI step and document the remediation path in the developer guide, roadmap, and ExecPlan. --- .github/workflows/ci.yml | 2 + Cargo.lock | 27 +++++++++++ Cargo.toml | 1 + crates/weaver-docs-gate/Cargo.toml | 2 + .../examples/render_boundary_matrix.rs | 5 +++ crates/weaver-docs-gate/src/renderer/tests.rs | 5 +++ .../tests/boundary_manifest.rs | 40 ++++++++++++++--- .../tests/support/pending_review_date.rs | 5 +++ docs/developers-guide.md | 16 +++++++ ...-1-1-track-downstream-consumer-boundary.md | 45 ++++++++++++++++--- docs/roadmap.md | 3 ++ 11 files changed, 141 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 09f037ba..edb8e193 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,8 @@ jobs: run: uv tool install nixie-cli==1.0.0 - name: Mermaid lint run: make nixie + - name: Boundary manifest gate + run: cargo test -p weaver-docs-gate --test boundary_manifest -- --nocapture - name: Install Whitaker run: | cargo install --locked \ diff --git a/Cargo.lock b/Cargo.lock index 96634594..b87f5118 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1341,6 +1341,16 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "metrics" +version = "0.24.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89550ee9f79e88fef3119de263694973a8adb26c21d75322164fb8c493039fe2" +dependencies = [ + "portable-atomic", + "rapidhash", +] + [[package]] name = "mockall" version = "0.14.0" @@ -1625,6 +1635,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + [[package]] name = "potential_utf" version = "0.1.5" @@ -1834,6 +1850,15 @@ dependencies = [ "rand_core", ] +[[package]] +name = "rapidhash" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59" +dependencies = [ + "rustversion", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -3259,12 +3284,14 @@ dependencies = [ "camino", "cap-std 4.0.2", "insta", + "metrics", "proptest", "serde", "tempfile", "thiserror 2.0.18", "time", "toml 0.9.12+spec-1.1.0", + "tracing", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index bf1244dd..64f6e2fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ insta = "1.41" lru = "0.12" lsp-types = "0.97" mockall = "0.14.0" +metrics = "0.24" once_cell = "1.19" ortho_config = { git = "https://github.com/leynos/ortho-config.git", rev = "4339a6f3c61dc4fed86493d99ffb05230bee2a1b" } predicates = "3.1" diff --git a/crates/weaver-docs-gate/Cargo.toml b/crates/weaver-docs-gate/Cargo.toml index bc5a40d2..526eee0a 100644 --- a/crates/weaver-docs-gate/Cargo.toml +++ b/crates/weaver-docs-gate/Cargo.toml @@ -15,8 +15,10 @@ toml = { workspace = true } [dev-dependencies] insta = { workspace = true } +metrics = { workspace = true } proptest = { workspace = true } tempfile = { workspace = true } +tracing = { workspace = true } [lints] workspace = true diff --git a/crates/weaver-docs-gate/examples/render_boundary_matrix.rs b/crates/weaver-docs-gate/examples/render_boundary_matrix.rs index 01f1566d..89cf0820 100644 --- a/crates/weaver-docs-gate/examples/render_boundary_matrix.rs +++ b/crates/weaver-docs-gate/examples/render_boundary_matrix.rs @@ -1,4 +1,9 @@ //! Regenerate the `OrthoConfig` consumer boundary matrix. +//! +//! This example is the documented command-line adapter for the crate's public +//! APIs. It loads `docs/orthoconfig-consumer-boundary.toml` with +//! `load_manifest_file`, renders the Markdown matrix with `render_matrix`, and +//! writes the result to the output path supplied by the caller. use std::env; diff --git a/crates/weaver-docs-gate/src/renderer/tests.rs b/crates/weaver-docs-gate/src/renderer/tests.rs index 342b14ca..812c3cf4 100644 --- a/crates/weaver-docs-gate/src/renderer/tests.rs +++ b/crates/weaver-docs-gate/src/renderer/tests.rs @@ -1,4 +1,9 @@ //! Unit, snapshot, and property coverage for Markdown matrix rendering. +//! +//! These tests exercise the private helpers behind `renderer::render_matrix`. +//! They prove that manifest rows are grouped by roadmap phase, escaped into +//! Markdown table cells, padded with stable column widths, and rendered into +//! the generated matrix shape consumed by the integration gate. use proptest::prelude::*; diff --git a/crates/weaver-docs-gate/tests/boundary_manifest.rs b/crates/weaver-docs-gate/tests/boundary_manifest.rs index 87a88f43..08c9bac0 100644 --- a/crates/weaver-docs-gate/tests/boundary_manifest.rs +++ b/crates/weaver-docs-gate/tests/boundary_manifest.rs @@ -7,7 +7,9 @@ use std::collections::BTreeSet; use camino::{Utf8Path, Utf8PathBuf}; use cap_std::{ambient_authority, fs::Dir}; +use metrics::counter; use time::{Date, macros::date}; +use tracing::{debug, warn}; use weaver_docs_gate::{ BoundaryManifest, BoundaryState, @@ -21,6 +23,9 @@ const MANIFEST: &str = "docs/orthoconfig-consumer-boundary.toml"; const MANIFEST_BUILD_DATE: Date = date!(2026 - 06 - 21); const MATRIX: &str = "docs/orthoconfig-consumer-boundary.md"; const ROADMAP: &str = "docs/roadmap.md"; +const REMEDIATE_MANIFEST: &str = "update docs/orthoconfig-consumer-boundary.toml, regenerate \ + docs/orthoconfig-consumer-boundary.md, then rerun cargo test -p \ + weaver-docs-gate"; type TestResult = Result; @@ -79,6 +84,12 @@ fn boundary_state_rows_have_required_evidence() -> TestResult { let manifest = manifest().map_err(|error| format!("load boundary manifest: {error}"))?; for task in &manifest.tasks { + debug!( + target: "weaver_docs_gate::boundary_manifest", + task_id = %task.id, + state = ?task.state, + "validating boundary manifest row", + ); validate_date(&task.last_reviewed, FieldName::LastReviewed, task)?; ensure( !task.upstream.is_empty(), @@ -218,7 +229,8 @@ fn ensure(condition: bool, message: impl Into) -> TestResult { if condition { Ok(()) } else { - Err(message.into()) + let detail = message.into(); + Err(gate_failure("invariant_failed", &detail)) } } @@ -230,13 +242,31 @@ where if left == right { Ok(()) } else { - Err(format!( - "{}\nleft: {left:?}\nright: {right:?}", - message.into() - )) + let detail = format!("{}\nleft: {left:?}\nright: {right:?}", message.into()); + Err(gate_failure("equality_mismatch", &detail)) } } +/// Build the structured failure message emitted by the manifest gate. +fn gate_failure(code: &'static str, message: &str) -> String { + counter!( + "weaver_docs_gate_boundary_validation_failures_total", + "code" => code, + ) + .increment(1); + warn!( + target: "weaver_docs_gate::boundary_manifest", + code, + remediation = REMEDIATE_MANIFEST, + message = %message, + "boundary manifest validation failed", + ); + format!( + "boundary_manifest_gate failure code={code}\nmessage: {message}\nremediation: \ + {REMEDIATE_MANIFEST}" + ) +} + /// Ensure a manifest field contains no duplicate values. fn ensure_unique(values: &[&str], label: FieldName) -> TestResult { let mut seen = BTreeSet::new(); diff --git a/crates/weaver-docs-gate/tests/support/pending_review_date.rs b/crates/weaver-docs-gate/tests/support/pending_review_date.rs index e310c673..1829e4d9 100644 --- a/crates/weaver-docs-gate/tests/support/pending_review_date.rs +++ b/crates/weaver-docs-gate/tests/support/pending_review_date.rs @@ -80,6 +80,11 @@ mod tests { } proptest! { + #![proptest_config(ProptestConfig { + failure_persistence: None, + ..ProptestConfig::default() + })] + #[test] /// Prove all generated dates inside the review window are accepted. fn review_window_accepts_generated_in_range(age in 0i64..=PENDING_REVIEW_WINDOW_DAYS) { diff --git a/docs/developers-guide.md b/docs/developers-guide.md index f3594b05..32278ee7 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -33,6 +33,22 @@ temporary Weaver adapter with a concrete removal gate, `pending` for an upstream contract that is not decided or shipped yet, and `divergent` only when ADR 007 records the deliberate Weaver-owned divergence. +The CI workflow runs the boundary manifest gate as a named +`Boundary manifest gate` step before the wider coverage action. A failed +invariant emits a structured message shaped as: + +```text +boundary_manifest_gate failure code= +message: +remediation: update docs/orthoconfig-consumer-boundary.toml, regenerate +docs/orthoconfig-consumer-boundary.md, then rerun cargo test -p weaver-docs-gate +``` + +Treat the `remediation` line as the recovery checklist. Update the TOML source +of truth first, regenerate the Markdown matrix with the command above, and rerun +`cargo test -p weaver-docs-gate --test boundary_manifest -- --nocapture` +locally when you need the same visible failure output that CI reports. + ### `weaver-docs-gate` boundary API The private `weaver-docs-gate` workspace member owns the generated OrthoConfig diff --git a/docs/execplans/12-1-1-track-downstream-consumer-boundary.md b/docs/execplans/12-1-1-track-downstream-consumer-boundary.md index 3ca5a09d..8e32e1d1 100644 --- a/docs/execplans/12-1-1-track-downstream-consumer-boundary.md +++ b/docs/execplans/12-1-1-track-downstream-consumer-boundary.md @@ -79,11 +79,14 @@ boundary becomes review-only again. decisions (the OrthoConfig `§2.1` consumer-boundary statement and OrthoConfig ADR-003 schema ownership) rather than the unshipped bullets of 5.2.3. The matrix is forward-compatible with 5.2.3 once it lands. -2. **No new runtime crate dependencies.** Documentation and tests use crates - already in the workspace (`camino`, `serde`, `toml`, `rstest`, - `pretty_assertions`, `googletest`). The test must compile under - `--workspace --all-targets --all-features`. No new workspace member adds - a runtime binary; `crates/weaver-docs-gate` exists only to host the +2. **No new product runtime crate dependencies.** Documentation and tests use + workspace dependencies (`camino`, `serde`, `toml`, `rstest`, + `pretty_assertions`, `googletest`, `tracing`, and `metrics`). The + observability follow-up keeps `metrics` and `tracing` as + `weaver-docs-gate` dev-dependencies so `weaver` and `weaverd` do not gain a + runtime dependency. The test must compile under + `--workspace --all-targets --all-features`. No new workspace member adds a + runtime binary; `crates/weaver-docs-gate` exists only to host the integration test and the small manifest parser it shares with itself. 3. **Do not duplicate the OrthoConfig roadmap.** Each matrix row references an OrthoConfig task ID, not its content. If OrthoConfig renames a task, the @@ -267,6 +270,14 @@ state, not the intended sequence. describe the parser/adapter/renderer relationships, and proptest coverage now exercises column widths, cell escaping, phase grouping, and date-window ranges. +- [x] 2026-06-21T00:00:00Z: Review feedback expanded module-level + documentation for renderer tests and the matrix regeneration example. + The boundary manifest gate now emits structured failure messages with a + stable code, human message, and remediation line, records validation + decisions through `tracing`, increments a metrics counter for validation + failures, and runs as a named CI step before wider workspace coverage. + `docs/developers-guide.md` documents the visible failure format and + local rerun command. ## Surprises & discoveries @@ -318,6 +329,16 @@ Recorded as they occur during implementation. Format: Impact: `crates/weaver-docs-gate/tests/support/pending_review_date.rs` now parses the date with the `time` crate and rejects pending reviews more than 270 days behind the explicit build date provided by the manifest gate. +- Observation: review output needed the boundary gate to be easier to triage + in CI. + Evidence: the gate was covered by the workspace test suite, but CI did not + name the boundary-manifest test separately and failure messages did not + carry a stable machine-readable code plus remediation line. + Impact: `.github/workflows/ci.yml` now runs + `cargo test -p weaver-docs-gate --test boundary_manifest -- --nocapture` + as `Boundary manifest gate`; gate failures use the + `boundary_manifest_gate failure code=...` format documented in + `docs/developers-guide.md`. ## Decision log @@ -409,6 +430,13 @@ Recorded for any decision that future work must respect. strict integer-division and remainder lints, while `time::Date` expresses the explicit-date policy directly and keeps the gate portable. Date/Author: 2026-06-15, implementation. +- Decision: keep boundary-gate observability local to the docs-gate + validation path. + Rationale: the gate is a documentation-governance check, not runtime + product behaviour. Structured failure strings, `tracing` events, and a + metrics counter give CI and local runs inspectable decision points without + changing the manifest schema, generated matrix, or public CLI behaviour. + Date/Author: 2026-06-21, implementation. ## Outcomes & retrospective @@ -441,6 +469,13 @@ date supplied by the manifest gate. A later review follow-up made that build date an explicit parameter so the gate no longer depends on `SOURCE_DATE_EPOCH` or the wall clock. +A later review follow-up made the Stage D gate operationally visible. CI now +runs the boundary manifest integration test as its own named step, and +validation failures include a stable failure code, a human-readable message, +and the remediation command sequence. The gate emits `tracing` events at row +validation decision points and increments +`weaver_docs_gate_boundary_validation_failures_total` when an invariant fails. + ## Context and orientation A novice opening this plan needs the following landmarks before reading the diff --git a/docs/roadmap.md b/docs/roadmap.md index 701707ad..91bd548c 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -52,6 +52,9 @@ Boundary classifications for this step are tracked in the - Follow-up test refactors typed path and field-label helper arguments and consolidated state evidence validation without changing the boundary contract. + - Follow-up observability work added a named CI boundary-manifest gate, + structured remediation messages, and trace/metric events for validation + failures. - Success: every command-contract task says whether it consumes OrthoConfig, wraps it temporarily, or records a deliberate divergence in ADR 007. - [ ] 12.1.2. Consume recursive command metadata. From 26e93441280a49e93f79ab9247df28d961a879fe Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 21 Jun 2026 21:44:14 +0200 Subject: [PATCH 18/22] Add boundary manifest load observability Emit `tracing` events and load-outcome metrics from the docs-gate manifest reader and file adapter so callers can diagnose read, path, and schema failures outside the integration tests. Include remediation guidance in generated parser/file errors, remove the renderer test helper clone path flagged by review, and update the roadmap, developer guide, and ExecPlan with the operational boundary. --- crates/weaver-docs-gate/Cargo.toml | 4 +- crates/weaver-docs-gate/src/lib.rs | 158 ++++++++++++++++-- crates/weaver-docs-gate/src/renderer/tests.rs | 28 +++- docs/developers-guide.md | 10 ++ ...-1-1-track-downstream-consumer-boundary.md | 37 +++- docs/roadmap.md | 4 +- 6 files changed, 204 insertions(+), 37 deletions(-) diff --git a/crates/weaver-docs-gate/Cargo.toml b/crates/weaver-docs-gate/Cargo.toml index 526eee0a..8bb31b7d 100644 --- a/crates/weaver-docs-gate/Cargo.toml +++ b/crates/weaver-docs-gate/Cargo.toml @@ -8,17 +8,17 @@ publish = false [dependencies] cap-std = { workspace = true } camino = { workspace = true } +metrics = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } time = { workspace = true } toml = { workspace = true } +tracing = { workspace = true } [dev-dependencies] insta = { workspace = true } -metrics = { workspace = true } proptest = { workspace = true } tempfile = { workspace = true } -tracing = { workspace = true } [lints] workspace = true diff --git a/crates/weaver-docs-gate/src/lib.rs b/crates/weaver-docs-gate/src/lib.rs index 022ef284..8237d5c7 100644 --- a/crates/weaver-docs-gate/src/lib.rs +++ b/crates/weaver-docs-gate/src/lib.rs @@ -12,11 +12,19 @@ use std::io::{self, ErrorKind, Read}; use camino::{Utf8Path, Utf8PathBuf}; use cap_std::{ambient_authority, fs::Dir}; +use metrics::counter; +use tracing::{debug, warn}; mod manifest_adapter; mod renderer; pub use renderer::render_matrix; +const MANIFEST_REMEDIATION: &str = "update docs/orthoconfig-consumer-boundary.toml, regenerate \ + docs/orthoconfig-consumer-boundary.md, then rerun cargo test \ + -p weaver-docs-gate"; +const METRIC_LOAD_TOTAL: &str = "weaver_docs_gate_boundary_manifest_load_total"; +const OBSERVABILITY_TARGET: &str = "weaver_docs_gate::boundary_manifest"; + /// One boundary classification state for a Weaver roadmap task. /// /// # Examples @@ -292,13 +300,26 @@ pub enum BoundaryFileError { /// # Ok::<(), weaver_docs_gate::BoundaryError>(()) /// ``` pub fn load_manifest(mut reader: impl Read) -> Result { + debug!( + target: OBSERVABILITY_TARGET, + source = "reader", + "loading boundary manifest", + ); let mut contents = String::new(); - reader - .read_to_string(&mut contents) - .map_err(|source| BoundaryError::Unreadable { - detail: source.to_string(), - })?; + reader.read_to_string(&mut contents).map_err(|source| { + let detail = unreadable_detail(&source.to_string()); + record_load_failure("reader", "unreadable", &detail); + BoundaryError::Unreadable { detail } + })?; parse_manifest(&contents) + .inspect(record_load_success) + .inspect_err(|error| { + record_load_failure( + "reader", + boundary_failure_outcome(error), + &error.to_string(), + ); + }) } /// Load the boundary manifest from disk. @@ -318,26 +339,40 @@ pub fn load_manifest(mut reader: impl Read) -> Result(()) /// ``` pub fn load_manifest_file(path: &Utf8Path) -> Result { + debug!( + target: OBSERVABILITY_TARGET, + source = "file", + path = %path, + "loading boundary manifest", + ); let parent = path.parent().unwrap_or_else(|| Utf8Path::new(".")); - let file_name = path - .file_name() - .ok_or_else(|| BoundaryFileError::InvalidPath(path.to_path_buf()))?; - let dir = Dir::open_ambient_dir(parent, ambient_authority()) - .map_err(|source| read_error(path, &source))?; + let file_name = path.file_name().ok_or_else(|| invalid_path_error(path))?; + let dir = Dir::open_ambient_dir(parent, ambient_authority()).map_err(|source| { + let error = read_error(path, &source); + record_file_load_failure(path, &error); + error + })?; - let contents = dir - .read_to_string(file_name) - .map_err(|source| read_error(path, &source))?; - parse_manifest(&contents).map_err(|error| file_error(path, error)) + let contents = dir.read_to_string(file_name).map_err(|source| { + let error = read_error(path, &source); + record_file_load_failure(path, &error); + error + })?; + let manifest = parse_manifest(&contents).map_err(|error| { + let file_error = file_error(path, error); + record_file_load_failure(path, &file_error); + file_error + })?; + record_file_load_success(path, &manifest); + Ok(manifest) } /// Parse TOML manifest text into the public domain manifest. fn parse_manifest(contents: &str) -> Result { let manifest = toml::from_str::(contents).map_err(|source| { - BoundaryError::InvalidSchema { - detail: source.to_string(), - } + let detail = schema_detail(&source.to_string()); + BoundaryError::InvalidSchema { detail } })?; Ok(manifest.into()) } @@ -349,7 +384,7 @@ fn read_error(path: &Utf8Path, source: &io::Error) -> BoundaryFileError { } else { BoundaryFileError::Unreadable { path: path.to_path_buf(), - detail: source.to_string(), + detail: unreadable_detail(&source.to_string()), } } } @@ -367,3 +402,90 @@ fn file_error(path: &Utf8Path, error: BoundaryError) -> BoundaryFileError { }, } } + +/// Build an invalid-path error and emit the matching operational event. +fn invalid_path_error(path: &Utf8Path) -> BoundaryFileError { + let error = BoundaryFileError::InvalidPath(path.to_path_buf()); + record_file_load_failure(path, &error); + error +} + +/// Add remediation context to read failures. +fn unreadable_detail(source: &str) -> String { + format!("{source}; remediation: {MANIFEST_REMEDIATION}") +} + +/// Add remediation context to schema failures. +fn schema_detail(source: &str) -> String { + format!("{source}; remediation: {MANIFEST_REMEDIATION}") +} + +/// Record a successful manifest parse with low-cardinality labels. +fn record_load_success(manifest: &BoundaryManifest) { + counter!(METRIC_LOAD_TOTAL, "source" => "reader", "outcome" => "success").increment(1); + debug!( + target: OBSERVABILITY_TARGET, + task_count = manifest.tasks.len(), + managed_task_count = manifest.managed_tasks.len(), + "loaded boundary manifest", + ); +} + +/// Record a successful file-backed manifest load. +fn record_file_load_success(path: &Utf8Path, manifest: &BoundaryManifest) { + counter!(METRIC_LOAD_TOTAL, "source" => "file", "outcome" => "success").increment(1); + debug!( + target: OBSERVABILITY_TARGET, + source = "file", + path = %path, + task_count = manifest.tasks.len(), + managed_task_count = manifest.managed_tasks.len(), + "loaded boundary manifest", + ); +} + +/// Record a path-free manifest load failure. +fn record_load_failure(source: &'static str, outcome: &'static str, detail: &str) { + counter!(METRIC_LOAD_TOTAL, "source" => source, "outcome" => outcome).increment(1); + warn!( + target: OBSERVABILITY_TARGET, + source, + outcome, + remediation = MANIFEST_REMEDIATION, + detail, + "boundary manifest load failed", + ); +} + +/// Record a file-backed manifest load failure. +fn record_file_load_failure(path: &Utf8Path, error: &BoundaryFileError) { + let outcome = file_failure_outcome(error); + counter!(METRIC_LOAD_TOTAL, "source" => "file", "outcome" => outcome).increment(1); + warn!( + target: OBSERVABILITY_TARGET, + source = "file", + outcome, + path = %path, + remediation = MANIFEST_REMEDIATION, + error = %error, + "boundary manifest load failed", + ); +} + +/// Return the metric outcome label for a file-backed error. +const fn file_failure_outcome(error: &BoundaryFileError) -> &'static str { + match error { + BoundaryFileError::NotFound(_) => "not_found", + BoundaryFileError::InvalidPath(_) => "invalid_path", + BoundaryFileError::Unreadable { .. } => "unreadable", + BoundaryFileError::InvalidSchema { .. } => "invalid_schema", + } +} + +/// Return the metric outcome label for a path-free parser error. +const fn boundary_failure_outcome(error: &BoundaryError) -> &'static str { + match error { + BoundaryError::Unreadable { .. } => "unreadable", + BoundaryError::InvalidSchema { .. } => "invalid_schema", + } +} diff --git a/crates/weaver-docs-gate/src/renderer/tests.rs b/crates/weaver-docs-gate/src/renderer/tests.rs index 812c3cf4..408b8960 100644 --- a/crates/weaver-docs-gate/src/renderer/tests.rs +++ b/crates/weaver-docs-gate/src/renderer/tests.rs @@ -13,7 +13,7 @@ use crate::{BoundaryManifest, UpstreamRef, UpstreamRole}; /// Prove numeric phase grouping does not confuse `1.*` with `12.*` tasks. #[test] fn groups_rows_by_phase_without_prefix_collisions() { - let manifest = manifest_with_tasks(vec![task("1.1.1"), task("12.1.1")]); + let manifest = manifest_with_task_ids(vec!["1.1.1".into(), "12.1.1".into()]); let rows = grouped_rows(&manifest); @@ -44,7 +44,7 @@ fn computes_column_widths_from_escaped_cells() { /// Pin the stable generated Markdown shape for one representative task. #[test] fn snapshots_rendered_matrix_shape() { - let manifest = manifest_with_tasks(vec![task("12.1.1")]); + let manifest = manifest_with_task_ids(vec!["12.1.1".into()]); insta::assert_snapshot!(render_matrix(&manifest), @r###" # OrthoConfig consumer boundary @@ -108,12 +108,12 @@ proptest! { #[test] /// Prove arbitrary numeric phase IDs are grouped exactly once. fn phase_grouping_preserves_arbitrary_phase_ids(phases in proptest::collection::vec(1u16..=999, 1..32)) { - let tasks = phases + let task_ids = phases .iter() .enumerate() - .map(|(index, phase)| task(&format!("{phase}.{index}.1"))) + .map(|(index, phase)| format!("{phase}.{index}.1")) .collect::>(); - let manifest = manifest_with_tasks(tasks); + let manifest = manifest_with_task_ids(task_ids); let rows = grouped_rows(&manifest); let expected = phases .iter() @@ -140,7 +140,11 @@ proptest! { role: UpstreamRole::Renderer, }]; - let rendered = render_matrix(&manifest_with_tasks(vec![boundary_task])); + let rendered = render_matrix(&BoundaryManifest { + schema_version: 1, + managed_tasks: vec!["12.1.1".into()], + tasks: vec![boundary_task], + }); for line in rendered.lines().filter(|line| line.starts_with("| [")) { prop_assert!(line.matches(" | ").count() == 7); } @@ -159,9 +163,15 @@ fn has_unescaped_pipe(value: &str) -> bool { false } -/// Build a manifest containing the supplied task rows. -fn manifest_with_tasks(tasks: Vec) -> BoundaryManifest { - let managed_tasks = tasks.iter().map(|task| task.id.clone()).collect(); +/// Build a manifest containing representative rows for the supplied task IDs. +fn manifest_with_task_ids(task_ids: Vec) -> BoundaryManifest { + let (managed_tasks, tasks) = task_ids + .into_iter() + .map(|id| { + let boundary_task = task(&id); + (id, boundary_task) + }) + .unzip(); BoundaryManifest { schema_version: 1, managed_tasks, diff --git a/docs/developers-guide.md b/docs/developers-guide.md index 32278ee7..2649fba1 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -90,6 +90,16 @@ filesystem adapter to read `docs/orthoconfig-consumer-boundary.toml`. Use groups rows by roadmap phase, and sizes columns from the escaped cell content, so callers should pass domain values rather than preformatted Markdown rows. +`load_manifest` and `load_manifest_file` emit `tracing` events on load +attempts, successes, and failures using the +`weaver_docs_gate::boundary_manifest` target. They also increment the +`weaver_docs_gate_boundary_manifest_load_total` counter with bounded `source` +and `outcome` labels. Libraries must not install a global subscriber or metrics +recorder; callers that need diagnostics should install those at the application +or CI harness boundary. Parser and file-adapter errors include the remediation +step used by the CI gate so local developer tools and test output point at the +same recovery path. + When adding or renaming a public command: 1. Update the OrthoConfig-backed command metadata or the Weaver semantic diff --git a/docs/execplans/12-1-1-track-downstream-consumer-boundary.md b/docs/execplans/12-1-1-track-downstream-consumer-boundary.md index 8e32e1d1..3b80c503 100644 --- a/docs/execplans/12-1-1-track-downstream-consumer-boundary.md +++ b/docs/execplans/12-1-1-track-downstream-consumer-boundary.md @@ -278,6 +278,13 @@ state, not the intended sequence. failures, and runs as a named CI step before wider workspace coverage. `docs/developers-guide.md` documents the visible failure format and local rerun command. +- [x] 2026-06-21T00:00:00Z: Review feedback moved boundary manifest + observability into the docs-gate library entry points. `load_manifest` + and `load_manifest_file` now emit structured `tracing` events, increment + `weaver_docs_gate_boundary_manifest_load_total` with bounded + source/outcome labels, and include remediation guidance in generated + read/schema errors. The renderer test helper no longer uses the flagged + `task.id.clone()` pattern. ## Surprises & discoveries @@ -339,6 +346,14 @@ Recorded as they occur during implementation. Format: as `Boundary manifest gate`; gate failures use the `boundary_manifest_gate failure code=...` format documented in `docs/developers-guide.md`. +- Observation: test-only observability did not help callers using the + docs-gate parser or filesystem adapter directly. + Evidence: review feedback noted that `load_manifest` and + `load_manifest_file` had no structured logging or metrics at their read and + schema-failure decision points. + Impact: those public entry points now record success and failure outcomes + through `tracing` and `metrics`, while leaving subscriber and recorder + installation to the application or CI harness boundary. ## Decision log @@ -430,12 +445,13 @@ Recorded for any decision that future work must respect. strict integer-division and remainder lints, while `time::Date` expresses the explicit-date policy directly and keeps the gate portable. Date/Author: 2026-06-15, implementation. -- Decision: keep boundary-gate observability local to the docs-gate - validation path. - Rationale: the gate is a documentation-governance check, not runtime - product behaviour. Structured failure strings, `tracing` events, and a - metrics counter give CI and local runs inspectable decision points without - changing the manifest schema, generated matrix, or public CLI behaviour. +- Decision: keep boundary observability inside the docs-gate crate without + installing global telemetry infrastructure. + Rationale: the gate is documentation-governance behaviour, but callers also + use the parser and filesystem adapter directly. Emitting `tracing` events and + `metrics` counters from the library makes failures diagnosable, while + deferring subscriber and recorder setup keeps the crate reusable and avoids + global side effects. Date/Author: 2026-06-21, implementation. ## Outcomes & retrospective @@ -476,6 +492,15 @@ and the remediation command sequence. The gate emits `tracing` events at row validation decision points and increments `weaver_docs_gate_boundary_validation_failures_total` when an invariant fails. +Another review follow-up moved the operational signal into production library +paths. `load_manifest` and `load_manifest_file` now emit structured events on +load attempts, successes, read failures, invalid paths, missing files, and +schema failures. They increment +`weaver_docs_gate_boundary_manifest_load_total` with low-cardinality +`source` and `outcome` labels and attach the same remediation guidance to +generated read and schema errors that the CI gate prints for validation +failures. + ## Context and orientation A novice opening this plan needs the following landmarks before reading the diff --git a/docs/roadmap.md b/docs/roadmap.md index 91bd548c..9291c7e9 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -53,8 +53,8 @@ Boundary classifications for this step are tracked in the consolidated state evidence validation without changing the boundary contract. - Follow-up observability work added a named CI boundary-manifest gate, - structured remediation messages, and trace/metric events for validation - failures. + structured remediation messages, and library trace/metric events for + manifest load outcomes. - Success: every command-contract task says whether it consumes OrthoConfig, wraps it temporarily, or records a deliberate divergence in ADR 007. - [ ] 12.1.2. Consume recursive command metadata. From 59484ab9cc4cc4ce9a0f9ea01187c9d5c1dce3c2 Mon Sep 17 00:00:00 2001 From: leynos Date: Mon, 22 Jun 2026 00:17:30 +0200 Subject: [PATCH 19/22] Expand boundary manifest review coverage Document the `load_manifest` and `load_manifest_file` tracing and metrics side effects in public Rustdoc so downstream callers see the operational contract at the API boundary. Add example CLI error-path coverage, property checks for ISO date and state/evidence invariants, and a `trybuild` pass test that compiles a small downstream public API fixture. --- Cargo.lock | 46 +++++ Cargo.toml | 1 + crates/weaver-docs-gate/Cargo.toml | 1 + crates/weaver-docs-gate/src/lib.rs | 20 +++ .../tests/boundary_manifest.rs | 2 + crates/weaver-docs-gate/tests/compile_time.rs | 12 ++ .../weaver-docs-gate/tests/load_manifest.rs | 109 ++++++++++-- .../tests/support/boundary_properties.rs | 167 ++++++++++++++++++ .../weaver-docs-gate/tests/ui/public_api.rs | 64 +++++++ docs/developers-guide.md | 12 +- ...-1-1-track-downstream-consumer-boundary.md | 13 ++ docs/roadmap.md | 3 + 12 files changed, 434 insertions(+), 16 deletions(-) create mode 100644 crates/weaver-docs-gate/tests/compile_time.rs create mode 100644 crates/weaver-docs-gate/tests/support/boundary_properties.rs create mode 100644 crates/weaver-docs-gate/tests/ui/public_api.rs diff --git a/Cargo.lock b/Cargo.lock index b87f5118..a744ea6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2536,6 +2536,12 @@ dependencies = [ "libc", ] +[[package]] +name = "target-triple" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" + [[package]] name = "tempfile" version = "3.27.0" @@ -2549,6 +2555,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "termtree" version = "0.5.1" @@ -2693,6 +2708,21 @@ dependencies = [ "winnow 0.7.15", ] +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.2", +] + [[package]] name = "toml_datetime" version = "0.6.11" @@ -2892,6 +2922,21 @@ dependencies = [ "tree-sitter-language", ] +[[package]] +name = "trybuild" +version = "1.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c635f0191bd3a2941013e5062667100969f8c4e9cd787c14f977265d73616e" +dependencies = [ + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml 1.1.2+spec-1.1.0", +] + [[package]] name = "type-map" version = "0.5.1" @@ -3292,6 +3337,7 @@ dependencies = [ "time", "toml 0.9.12+spec-1.1.0", "tracing", + "trybuild", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 64f6e2fc..6532652e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ thiserror = "2.0" time = { version = "0.3", features = ["formatting", "macros", "parsing"] } toml = "0.9.12" tracing = "0.1" +trybuild = "1.0" tree-sitter = "0.25.10" tree-sitter-python = "0.25.0" tree-sitter-rust = "0.24.0" diff --git a/crates/weaver-docs-gate/Cargo.toml b/crates/weaver-docs-gate/Cargo.toml index 8bb31b7d..bae98ce1 100644 --- a/crates/weaver-docs-gate/Cargo.toml +++ b/crates/weaver-docs-gate/Cargo.toml @@ -19,6 +19,7 @@ tracing = { workspace = true } insta = { workspace = true } proptest = { workspace = true } tempfile = { workspace = true } +trybuild = { workspace = true } [lints] workspace = true diff --git a/crates/weaver-docs-gate/src/lib.rs b/crates/weaver-docs-gate/src/lib.rs index 8237d5c7..7074220b 100644 --- a/crates/weaver-docs-gate/src/lib.rs +++ b/crates/weaver-docs-gate/src/lib.rs @@ -280,6 +280,16 @@ pub enum BoundaryFileError { /// Parse the boundary manifest from a byte reader. /// +/// # Observability +/// +/// Emits `tracing` events with target +/// `weaver_docs_gate::boundary_manifest` when loading starts, succeeds, or +/// fails. Increments the +/// `weaver_docs_gate_boundary_manifest_load_total` metrics counter with +/// `source = "reader"` and an `outcome` label of `success`, `unreadable`, or +/// `invalid_schema`. The function does not install a tracing subscriber or +/// metrics recorder. +/// /// # Errors /// /// Returns [`BoundaryError`] when the reader fails or when the bytes do not @@ -324,6 +334,16 @@ pub fn load_manifest(mut reader: impl Read) -> Result TestResult { write_file(temp.path(), "manifest.toml", VALID_MANIFEST)? .map_err(|error| format!("write valid manifest: {error}"))?; - let output = Command::new(env!("CARGO")) - .args([ - "run", - "-p", - "weaver-docs-gate", - "--example", - "render_boundary_matrix", - "--", - manifest_path.as_str(), - output_path.as_str(), - ]) - .current_dir(repo_root()?) - .output() - .map_err(|error| format!("run render_boundary_matrix example: {error}"))?; + let output = run_render_boundary_matrix(&[manifest_path.as_str(), output_path.as_str()])?; if !output.status.success() { return Err(format!( @@ -201,6 +188,59 @@ fn render_boundary_matrix_example_writes_output() -> TestResult { ) } +/// Prove the example reports a missing manifest path. +#[test] +fn render_boundary_matrix_example_reports_missing_manifest() -> TestResult { + let temp = temp_dir()?; + let manifest_path = temp.path().join("missing.toml"); + let output_path = temp.path().join("matrix.md"); + + let output = run_render_boundary_matrix(&[manifest_path.as_str(), output_path.as_str()])?; + + assert_example_failure(&output, "manifest file not found") +} + +/// Prove the example reports a missing output argument. +#[test] +fn render_boundary_matrix_example_reports_missing_output_argument() -> TestResult { + let temp = temp_dir()?; + let manifest_path = temp.path().join("manifest.toml"); + write_file(temp.path(), "manifest.toml", VALID_MANIFEST)? + .map_err(|error| format!("write valid manifest: {error}"))?; + + let output = run_render_boundary_matrix(&[manifest_path.as_str()])?; + + assert_example_failure(&output, "usage: render_boundary_matrix ") +} + +/// Prove the example reports unexpected extra arguments. +#[test] +fn render_boundary_matrix_example_reports_extra_arguments() -> TestResult { + let temp = temp_dir()?; + let manifest_path = temp.path().join("manifest.toml"); + let output_path = temp.path().join("matrix.md"); + write_file(temp.path(), "manifest.toml", VALID_MANIFEST)? + .map_err(|error| format!("write valid manifest: {error}"))?; + + let output = + run_render_boundary_matrix(&[manifest_path.as_str(), output_path.as_str(), "extra"])?; + + assert_example_failure(&output, "usage: render_boundary_matrix ") +} + +/// Prove the example reports an output path that cannot name a file. +#[test] +fn render_boundary_matrix_example_reports_invalid_output_path() -> TestResult { + let temp = temp_dir()?; + let manifest_path = temp.path().join("manifest.toml"); + write_file(temp.path(), "manifest.toml", VALID_MANIFEST)? + .map_err(|error| format!("write valid manifest: {error}"))?; + + let output = run_render_boundary_matrix(&[manifest_path.as_str(), "/"])?; + + assert_example_failure(&output, "invalid output path: /") +} + /// Create a UTF-8 temp directory guard for filesystem tests. fn temp_dir() -> TestResult { let temp = tempfile::tempdir().map_err(|error| format!("create temp dir: {error}"))?; @@ -227,6 +267,45 @@ fn read_file(dir_path: &Utf8Path, file_name: &str) -> TestResult TestResult { + Command::new(env!("CARGO")) + .args([ + "run", + "-p", + "weaver-docs-gate", + "--example", + "render_boundary_matrix", + "--", + ]) + .args(args) + .current_dir(repo_root()?) + .output() + .map_err(|error| format!("run render_boundary_matrix example: {error}")) +} + +/// Check that the example failed and wrote the expected diagnostic. +fn assert_example_failure(output: &Output, expected_stderr: &str) -> TestResult { + ensure( + !output.status.success(), + "example should exit with failure status", + )?; + ensure( + output.stdout.is_empty(), + format!( + "example failure should not write stdout\nstdout: {}", + String::from_utf8_lossy(&output.stdout), + ), + )?; + ensure( + String::from_utf8_lossy(&output.stderr).contains(expected_stderr), + format!( + "example stderr should contain {expected_stderr:?}\nstderr: {}", + String::from_utf8_lossy(&output.stderr), + ), + ) +} + /// Resolve the repository root from the test crate directory. fn repo_root() -> TestResult { let crate_dir = Utf8Path::new(env!("CARGO_MANIFEST_DIR")); diff --git a/crates/weaver-docs-gate/tests/support/boundary_properties.rs b/crates/weaver-docs-gate/tests/support/boundary_properties.rs new file mode 100644 index 00000000..7e3a38bf --- /dev/null +++ b/crates/weaver-docs-gate/tests/support/boundary_properties.rs @@ -0,0 +1,167 @@ +//! Property tests for boundary-manifest validation helpers. +//! +//! These tests exercise the pure validation rules used by the integration +//! gate. They keep generated ISO date shapes and state/evidence combinations +//! separate from filesystem and Markdown rendering concerns. + +use proptest::prelude::*; +use weaver_docs_gate::{BoundaryState, BoundaryTask, UpstreamRef, UpstreamRole}; + +use super::{is_iso_date, validate_state_evidence}; + +/// Generated state-specific evidence for one manifest row. +struct Evidence { + shipped_in: Option, + removal_gate: Option, + adr_anchor: Option, + next_review_by: Option, +} + +/// Presence flags used to generate and check state evidence. +#[derive(Clone, Copy)] +struct EvidenceFlags(u8); + +const SHIPPED_IN: u8 = 0b0001; +const REMOVAL_GATE: u8 = 0b0010; +const ADR_ANCHOR: u8 = 0b0100; +const NEXT_REVIEW_BY: u8 = 0b1000; + +impl Evidence { + /// Build optional evidence fields from generated presence flags. + fn from_flags(flags: EvidenceFlags) -> Self { + Self { + shipped_in: flags.has(SHIPPED_IN).then(|| "4339a6f3".into()), + removal_gate: flags + .has(REMOVAL_GATE) + .then(|| "remove once upstream ships".into()), + adr_anchor: flags + .has(ADR_ANCHOR) + .then(|| "intentional-divergence".into()), + next_review_by: flags.has(NEXT_REVIEW_BY).then(|| "2026-06-30".into()), + } + } +} + +impl EvidenceFlags { + /// Return whether the named evidence bit is present. + const fn has(self, mask: u8) -> bool { self.0 & mask != 0 } +} + +proptest! { + #![proptest_config(ProptestConfig { + failure_persistence: None, + ..ProptestConfig::default() + })] + + #[test] + /// Prove formatted calendar dates satisfy the ISO date-shape check. + fn iso_date_accepts_generated_yyyy_mm_dd( + year in 0u32..=9999, + month in 1u32..=12, + day in 1u32..=28, + ) { + let date = format!("{year:04}-{month:02}-{day:02}"); + + prop_assert!(is_iso_date(&date)); + } + + #[test] + /// Prove non-ten-character strings cannot satisfy the ISO date-shape check. + fn iso_date_rejects_invalid_lengths(value in invalid_length_date_text()) { + prop_assert!(!is_iso_date(&value)); + } + + #[test] + /// Prove malformed separators and non-digits are rejected. + fn iso_date_rejects_invalid_formats(value in invalid_format_date_text()) { + prop_assert!(!is_iso_date(&value)); + } + + #[test] + /// Prove state/evidence validation accepts exactly the required field set. + fn state_evidence_constraints_match_boundary_state( + state in boundary_state(), + evidence_bits in 0u8..16, + ) { + let flags = EvidenceFlags(evidence_bits); + let task = task_with_evidence(state, Evidence::from_flags(flags)); + let expected = expected_state_evidence(state, flags); + + prop_assert_eq!(validate_state_evidence(&task).is_ok(), expected); + } +} + +/// Generate strings whose length alone rules out an ISO `YYYY-MM-DD` shape. +fn invalid_length_date_text() -> impl Strategy { + prop_oneof!["[0-9-]{0,9}", "[0-9-]{11,20}",] +} + +/// Generate ten-character strings that violate separator or digit positions. +fn invalid_format_date_text() -> impl Strategy { + prop_oneof![ + (0u32..=9999, 1u32..=12, 1u32..=28) + .prop_map(|(year, month, day)| format!("{year:04}/{month:02}-{day:02}")), + (0u32..=9999, 1u32..=12, 1u32..=28) + .prop_map(|(year, month, day)| format!("{year:04}-{month:02}/{day:02}")), + (0u32..=9999, 1u32..=12, 1u32..=28) + .prop_map(|(year, month, day)| format!("A{year:03}-{month:02}-{day:02}")), + ] +} + +/// Generate each boundary state with equal intent. +fn boundary_state() -> impl Strategy { + prop_oneof![ + Just(BoundaryState::Consumes), + Just(BoundaryState::Wraps), + Just(BoundaryState::Pending), + Just(BoundaryState::Divergent), + ] +} + +/// Build one task with generated state-specific evidence fields. +fn task_with_evidence(state: BoundaryState, evidence: Evidence) -> BoundaryTask { + BoundaryTask { + id: "12.1.1".into(), + gist: "Track the downstream consumer boundary.".into(), + state, + upstream: vec![UpstreamRef { + task: "renderer-contract".into(), + role: UpstreamRole::Renderer, + }], + shipped_in: evidence.shipped_in, + removal_gate: evidence.removal_gate, + adr_anchor: evidence.adr_anchor, + next_review_by: evidence.next_review_by, + last_reviewed: "2026-06-20".into(), + } +} + +/// Return whether the generated evidence set is valid for a state. +const fn expected_state_evidence(state: BoundaryState, flags: EvidenceFlags) -> bool { + match state { + BoundaryState::Consumes => { + flags.has(SHIPPED_IN) + && !flags.has(REMOVAL_GATE) + && !flags.has(ADR_ANCHOR) + && !flags.has(NEXT_REVIEW_BY) + } + BoundaryState::Wraps => { + flags.has(REMOVAL_GATE) + && !flags.has(SHIPPED_IN) + && !flags.has(ADR_ANCHOR) + && !flags.has(NEXT_REVIEW_BY) + } + BoundaryState::Pending => { + flags.has(NEXT_REVIEW_BY) + && !flags.has(SHIPPED_IN) + && !flags.has(REMOVAL_GATE) + && !flags.has(ADR_ANCHOR) + } + BoundaryState::Divergent => { + flags.has(ADR_ANCHOR) + && !flags.has(SHIPPED_IN) + && !flags.has(REMOVAL_GATE) + && !flags.has(NEXT_REVIEW_BY) + } + } +} diff --git a/crates/weaver-docs-gate/tests/ui/public_api.rs b/crates/weaver-docs-gate/tests/ui/public_api.rs new file mode 100644 index 00000000..3c9dd705 --- /dev/null +++ b/crates/weaver-docs-gate/tests/ui/public_api.rs @@ -0,0 +1,64 @@ +//! Downstream compile-time usage of the `weaver-docs-gate` public API. + +use std::{error::Error, fmt::Debug}; + +use camino::Utf8Path; +use weaver_docs_gate::{ + BoundaryError, + BoundaryFileError, + BoundaryManifest, + BoundaryState, + BoundaryTask, + UpstreamRef, + UpstreamRole, + load_manifest, + load_manifest_file, + render_matrix, +}; + +fn assert_error_type() +where + E: Error + Debug + Send + Sync + 'static, +{ +} + +fn main() -> Result<(), Box> { + assert_error_type::(); + assert_error_type::(); + + let manifest = BoundaryManifest { + schema_version: 1, + managed_tasks: vec!["12.1.1".into()], + tasks: vec![BoundaryTask { + id: "12.1.1".into(), + gist: "Track the downstream consumer boundary.".into(), + state: BoundaryState::Consumes, + upstream: vec![UpstreamRef { + task: "renderer-contract".into(), + role: UpstreamRole::Renderer, + }], + shipped_in: Some("4339a6f3".into()), + removal_gate: None, + adr_anchor: None, + next_review_by: None, + last_reviewed: "2026-06-20".into(), + }], + }; + + let _state = BoundaryState::Consumes.as_str(); + let _role = UpstreamRole::Renderer.as_str(); + let _matrix = render_matrix(&manifest); + + let _loaded = load_manifest( + br#"schema_version = 1 +managed_tasks = [] +task = [] +"# + .as_slice(), + )?; + + let _file_result: Result = + load_manifest_file(Utf8Path::new("docs/orthoconfig-consumer-boundary.toml")); + + Ok(()) +} diff --git a/docs/developers-guide.md b/docs/developers-guide.md index 2649fba1..d563b075 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -98,7 +98,17 @@ and `outcome` labels. Libraries must not install a global subscriber or metrics recorder; callers that need diagnostics should install those at the application or CI harness boundary. Parser and file-adapter errors include the remediation step used by the CI gate so local developer tools and test output point at the -same recovery path. +same recovery path. The public Rustdoc for both load functions documents these +observable side effects; downstream callers that require silent parsing should +isolate calls behind their own adapter and install no subscriber or metrics +recorder. + +The docs-gate test suite also treats the matrix regeneration example as an +externally observable interface. `tests/load_manifest.rs` covers the example's +success path and argument/error paths, `tests/support/boundary_properties.rs` +covers generated date and state/evidence invariants, and +`tests/compile_time.rs` uses `trybuild` to prove downstream callers can compile +against the exported API and error trait surface. When adding or renaming a public command: diff --git a/docs/execplans/12-1-1-track-downstream-consumer-boundary.md b/docs/execplans/12-1-1-track-downstream-consumer-boundary.md index 3b80c503..d5557721 100644 --- a/docs/execplans/12-1-1-track-downstream-consumer-boundary.md +++ b/docs/execplans/12-1-1-track-downstream-consumer-boundary.md @@ -285,6 +285,12 @@ state, not the intended sequence. source/outcome labels, and include remediation guidance in generated read/schema errors. The renderer test helper no longer uses the flagged `task.id.clone()` pattern. +- [x] 2026-06-22T00:00:00Z: Review feedback made the load-function + observability contract explicit in public Rustdoc, added E2E coverage + for the `render_boundary_matrix` example's missing-manifest, + missing-output, extra-argument, and invalid-output failures, added + property coverage for ISO date-shape and state/evidence invariants, and + added a `trybuild` compile-time check for downstream public API usage. ## Surprises & discoveries @@ -501,6 +507,13 @@ schema failures. They increment generated read and schema errors that the CI gate prints for validation failures. +The 2026-06-22 review follow-up made that operational contract visible in the +public API documentation and widened the test boundary. The example binary now +has error-path coverage for argument and output-path failures, generated +properties exercise date-shape and state/evidence invariants, and a +`trybuild` pass test proves the exported domain types, load functions, +renderer, and error trait implementations remain usable by downstream callers. + ## Context and orientation A novice opening this plan needs the following landmarks before reading the diff --git a/docs/roadmap.md b/docs/roadmap.md index 9291c7e9..9390572d 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -55,6 +55,9 @@ Boundary classifications for this step are tracked in the - Follow-up observability work added a named CI boundary-manifest gate, structured remediation messages, and library trace/metric events for manifest load outcomes. + - Follow-up review coverage documents those observable load side effects, + adds example CLI error-path tests, property tests for date/evidence + invariants, and trybuild public API checks. - Success: every command-contract task says whether it consumes OrthoConfig, wraps it temporarily, or records a deliberate divergence in ADR 007. - [ ] 12.1.2. Consume recursive command metadata. From 3bf5657f12d9f54da29f26ba6ab9852006726435 Mon Sep 17 00:00:00 2001 From: leynos Date: Mon, 22 Jun 2026 00:30:09 +0200 Subject: [PATCH 20/22] Consolidate boundary manifest success recording Replace the separate reader and file success helpers with one `record_success` helper that keeps the source label and optional path field explicit while sharing the success metric and trace event shape. --- crates/weaver-docs-gate/src/lib.rs | 46 ++++++++++++++---------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/crates/weaver-docs-gate/src/lib.rs b/crates/weaver-docs-gate/src/lib.rs index 7074220b..7d583c7f 100644 --- a/crates/weaver-docs-gate/src/lib.rs +++ b/crates/weaver-docs-gate/src/lib.rs @@ -322,7 +322,7 @@ pub fn load_manifest(mut reader: impl Read) -> Result Result String { format!("{source}; remediation: {MANIFEST_REMEDIATION}") } -/// Record a successful manifest parse with low-cardinality labels. -fn record_load_success(manifest: &BoundaryManifest) { - counter!(METRIC_LOAD_TOTAL, "source" => "reader", "outcome" => "success").increment(1); - debug!( - target: OBSERVABILITY_TARGET, - task_count = manifest.tasks.len(), - managed_task_count = manifest.managed_tasks.len(), - "loaded boundary manifest", - ); -} - -/// Record a successful file-backed manifest load. -fn record_file_load_success(path: &Utf8Path, manifest: &BoundaryManifest) { - counter!(METRIC_LOAD_TOTAL, "source" => "file", "outcome" => "success").increment(1); - debug!( - target: OBSERVABILITY_TARGET, - source = "file", - path = %path, - task_count = manifest.tasks.len(), - managed_task_count = manifest.managed_tasks.len(), - "loaded boundary manifest", - ); +fn record_success(source: &'static str, path: Option<&Utf8Path>, manifest: &BoundaryManifest) { + counter!(METRIC_LOAD_TOTAL, "source" => source, "outcome" => "success").increment(1); + if let Some(manifest_path) = path { + debug!( + target: OBSERVABILITY_TARGET, + source, + path = %manifest_path, + task_count = manifest.tasks.len(), + managed_task_count = manifest.managed_tasks.len(), + "loaded boundary manifest", + ); + } else { + debug!( + target: OBSERVABILITY_TARGET, + source, + task_count = manifest.tasks.len(), + managed_task_count = manifest.managed_tasks.len(), + "loaded boundary manifest", + ); + } } /// Record a path-free manifest load failure. From 8e8490eec6213a923098af2c3bca3f968da5d406 Mon Sep 17 00:00:00 2001 From: leynos Date: Mon, 22 Jun 2026 00:33:16 +0200 Subject: [PATCH 21/22] Simplify boundary evidence property oracle Replace the state-specific compound boolean checks with a direct required-bit comparison so the property oracle matches the four-bit evidence model without exceeding the complexity threshold. --- .../tests/support/boundary_properties.rs | 33 ++++--------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/crates/weaver-docs-gate/tests/support/boundary_properties.rs b/crates/weaver-docs-gate/tests/support/boundary_properties.rs index 7e3a38bf..072d8e14 100644 --- a/crates/weaver-docs-gate/tests/support/boundary_properties.rs +++ b/crates/weaver-docs-gate/tests/support/boundary_properties.rs @@ -138,30 +138,11 @@ fn task_with_evidence(state: BoundaryState, evidence: Evidence) -> BoundaryTask /// Return whether the generated evidence set is valid for a state. const fn expected_state_evidence(state: BoundaryState, flags: EvidenceFlags) -> bool { - match state { - BoundaryState::Consumes => { - flags.has(SHIPPED_IN) - && !flags.has(REMOVAL_GATE) - && !flags.has(ADR_ANCHOR) - && !flags.has(NEXT_REVIEW_BY) - } - BoundaryState::Wraps => { - flags.has(REMOVAL_GATE) - && !flags.has(SHIPPED_IN) - && !flags.has(ADR_ANCHOR) - && !flags.has(NEXT_REVIEW_BY) - } - BoundaryState::Pending => { - flags.has(NEXT_REVIEW_BY) - && !flags.has(SHIPPED_IN) - && !flags.has(REMOVAL_GATE) - && !flags.has(ADR_ANCHOR) - } - BoundaryState::Divergent => { - flags.has(ADR_ANCHOR) - && !flags.has(SHIPPED_IN) - && !flags.has(REMOVAL_GATE) - && !flags.has(NEXT_REVIEW_BY) - } - } + let required = match state { + BoundaryState::Consumes => SHIPPED_IN, + BoundaryState::Wraps => REMOVAL_GATE, + BoundaryState::Pending => NEXT_REVIEW_BY, + BoundaryState::Divergent => ADR_ANCHOR, + }; + flags.0 == required } From e5f263145b8832bea2e77146030726cc1defa56a Mon Sep 17 00:00:00 2001 From: leynos Date: Mon, 22 Jun 2026 00:41:58 +0200 Subject: [PATCH 22/22] Inline boundary manifest deserialization Remove the mechanical manifest DTO adapter and deserialize the boundary manifest directly into the public domain types. Keep the TOML spelling contract explicit with serde renames and empty-string handling for optional evidence fields. Expand the property tests around the manifest gate so generated inputs cover registry symmetry, arbitrary state/evidence rows, ADR anchor membership, and Markdown heading-anchor shape. --- crates/weaver-docs-gate/src/lib.rs | 51 +- .../weaver-docs-gate/src/manifest_adapter.rs | 147 ---- .../tests/boundary_manifest.rs | 59 +- .../weaver-docs-gate/tests/load_manifest.rs | 6 +- .../tests/support/boundary_properties.rs | 171 +++- docs/developers-guide.md | 8 +- ...-1-1-track-downstream-consumer-boundary.md | 778 +++++++++--------- 7 files changed, 615 insertions(+), 605 deletions(-) delete mode 100644 crates/weaver-docs-gate/src/manifest_adapter.rs diff --git a/crates/weaver-docs-gate/src/lib.rs b/crates/weaver-docs-gate/src/lib.rs index 7d583c7f..22ed0a09 100644 --- a/crates/weaver-docs-gate/src/lib.rs +++ b/crates/weaver-docs-gate/src/lib.rs @@ -1,21 +1,20 @@ //! Tooling for the `OrthoConfig` consumer boundary matrix. //! -//! This crate keeps the boundary manifest pipeline in three small pieces: -//! public domain types live in this root module, `manifest_adapter` owns the -//! TOML and Serde conversion layer, and `renderer` turns a validated -//! [`BoundaryManifest`] into the checked-in Markdown matrix. Filesystem access -//! is intentionally limited to [`load_manifest_file`]; callers that already -//! have manifest bytes should use [`load_manifest`] so parsing stays independent -//! of path handling. +//! This crate keeps the boundary manifest pipeline in two small pieces: public +//! domain types and TOML deserialization live in this root module, while +//! `renderer` turns a validated [`BoundaryManifest`] into the checked-in +//! Markdown matrix. Filesystem access is intentionally limited to +//! [`load_manifest_file`]; callers that already have manifest bytes should use +//! [`load_manifest`] so parsing stays independent of path handling. use std::io::{self, ErrorKind, Read}; use camino::{Utf8Path, Utf8PathBuf}; use cap_std::{ambient_authority, fs::Dir}; use metrics::counter; +use serde::{Deserialize, Deserializer}; use tracing::{debug, warn}; -mod manifest_adapter; mod renderer; pub use renderer::render_matrix; @@ -33,7 +32,8 @@ const OBSERVABILITY_TARGET: &str = "weaver_docs_gate::boundary_manifest"; /// /// assert_eq!(BoundaryState::Wraps.as_str(), "wraps"); /// ``` -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "snake_case")] pub enum BoundaryState { /// Weaver follows an `OrthoConfig` contract that has shipped. Consumes, @@ -73,7 +73,8 @@ impl BoundaryState { /// /// assert_eq!(UpstreamRole::Renderer.as_str(), "renderer"); /// ``` -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "snake_case")] pub enum UpstreamRole { /// Consumer-boundary ownership and governance. Boundary, @@ -132,7 +133,7 @@ impl UpstreamRole { /// }; /// assert_eq!(upstream.role.as_str(), "renderer"); /// ``` -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub struct UpstreamRef { /// The upstream roadmap task or stable design section. pub task: String, @@ -159,7 +160,7 @@ pub struct UpstreamRef { /// }; /// assert_eq!(task.state.as_str(), "pending"); /// ``` -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub struct BoundaryTask { /// Weaver roadmap task ID, such as `13.1.2`. pub id: String, @@ -170,12 +171,16 @@ pub struct BoundaryTask { /// Upstream `OrthoConfig` task references. pub upstream: Vec, /// `OrthoConfig` release tag or pinned SHA for shipped contracts. + #[serde(deserialize_with = "empty_string_as_none")] pub shipped_in: Option, /// Replacement condition for temporary wrappers. + #[serde(deserialize_with = "empty_string_as_none")] pub removal_gate: Option, /// ADR 007 heading slug for deliberate divergences. + #[serde(deserialize_with = "empty_string_as_none")] pub adr_anchor: Option, /// ISO-8601 review date for pending contracts. + #[serde(deserialize_with = "empty_string_as_none")] pub next_review_by: Option, /// ISO-8601 date when the row was last reviewed. pub last_reviewed: String, @@ -194,13 +199,14 @@ pub struct BoundaryTask { /// }; /// assert!(manifest.tasks.is_empty()); /// ``` -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub struct BoundaryManifest { /// Manifest schema version. pub schema_version: u32, /// Ordered registry of Weaver roadmap task IDs governed by the matrix. pub managed_tasks: Vec, /// Classified task rows. + #[serde(rename = "task")] pub tasks: Vec, } @@ -389,12 +395,10 @@ pub fn load_manifest_file(path: &Utf8Path) -> Result Result { - let manifest = - toml::from_str::(contents).map_err(|source| { - let detail = schema_detail(&source.to_string()); - BoundaryError::InvalidSchema { detail } - })?; - Ok(manifest.into()) + toml::from_str::(contents).map_err(|source| { + let detail = schema_detail(&source.to_string()); + BoundaryError::InvalidSchema { detail } + }) } /// Convert filesystem failures into stable manifest loading errors. @@ -440,6 +444,15 @@ fn schema_detail(source: &str) -> String { format!("{source}; remediation: {MANIFEST_REMEDIATION}") } +/// Treat empty TOML strings as absent optional manifest evidence. +fn empty_string_as_none<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let value = String::deserialize(deserializer)?; + Ok((!value.is_empty()).then_some(value)) +} + fn record_success(source: &'static str, path: Option<&Utf8Path>, manifest: &BoundaryManifest) { counter!(METRIC_LOAD_TOTAL, "source" => source, "outcome" => "success").increment(1); if let Some(manifest_path) = path { diff --git a/crates/weaver-docs-gate/src/manifest_adapter.rs b/crates/weaver-docs-gate/src/manifest_adapter.rs deleted file mode 100644 index 616a09bd..00000000 --- a/crates/weaver-docs-gate/src/manifest_adapter.rs +++ /dev/null @@ -1,147 +0,0 @@ -//! TOML adapter for the `OrthoConfig` consumer boundary manifest. -//! -//! This module is the crate's outbound deserialization adapter. It owns every -//! Serde derive and TOML spelling detail, then converts those DTOs into the -//! plain domain structs exported by `lib.rs`. `load_manifest` and -//! `load_manifest_file` parse text through `BoundaryManifestDto`; the renderer -//! never sees these adapter types and consumes only the resulting -//! `BoundaryManifest`. - -use serde::{Deserialize, Deserializer}; - -use crate::{BoundaryManifest, BoundaryState, BoundaryTask, UpstreamRef, UpstreamRole}; - -/// Deserialization shape for the complete TOML manifest. -#[derive(Debug, Deserialize)] -pub(super) struct BoundaryManifestDto { - pub(super) schema_version: u32, - pub(super) managed_tasks: Vec, - #[serde(rename = "task")] - pub(super) tasks: Vec, -} - -/// Deserialization shape for one `[[task]]` TOML row. -#[derive(Debug, Deserialize)] -pub(super) struct BoundaryTaskDto { - pub(super) id: String, - pub(super) gist: String, - pub(super) state: BoundaryStateDto, - pub(super) upstream: Vec, - #[serde(deserialize_with = "empty_string_as_none")] - pub(super) shipped_in: Option, - #[serde(deserialize_with = "empty_string_as_none")] - pub(super) removal_gate: Option, - #[serde(deserialize_with = "empty_string_as_none")] - pub(super) adr_anchor: Option, - #[serde(deserialize_with = "empty_string_as_none")] - pub(super) next_review_by: Option, - pub(super) last_reviewed: String, -} - -/// Deserialization shape for one `[[task.upstream]]` entry. -#[derive(Debug, Deserialize)] -pub(super) struct UpstreamRefDto { - pub(super) task: String, - pub(super) role: UpstreamRoleDto, -} - -/// Serde-backed spelling of the public boundary state vocabulary. -#[derive(Debug, Clone, Copy, Deserialize)] -#[serde(rename_all = "snake_case")] -pub(super) enum BoundaryStateDto { - Consumes, - Wraps, - Pending, - Divergent, -} - -/// Serde-backed spelling of upstream role values in the manifest. -#[derive(Debug, Clone, Copy, Deserialize)] -#[serde(rename_all = "snake_case")] -pub(super) enum UpstreamRoleDto { - Boundary, - Metadata, - CapabilityProvenance, - Vocabulary, - Renderer, - Profile, - Delivery, - Feedback, - ExecutionLedger, -} - -impl From for BoundaryManifest { - /// Convert adapter-owned TOML data into the public domain manifest. - fn from(dto: BoundaryManifestDto) -> Self { - Self { - schema_version: dto.schema_version, - managed_tasks: dto.managed_tasks, - tasks: dto.tasks.into_iter().map(BoundaryTask::from).collect(), - } - } -} - -impl From for BoundaryTask { - /// Convert one TOML task row into a public domain task. - fn from(dto: BoundaryTaskDto) -> Self { - Self { - id: dto.id, - gist: dto.gist, - state: BoundaryState::from(dto.state), - upstream: dto.upstream.into_iter().map(UpstreamRef::from).collect(), - shipped_in: dto.shipped_in, - removal_gate: dto.removal_gate, - adr_anchor: dto.adr_anchor, - next_review_by: dto.next_review_by, - last_reviewed: dto.last_reviewed, - } - } -} - -impl From for UpstreamRef { - /// Convert one TOML upstream reference into a public domain reference. - fn from(dto: UpstreamRefDto) -> Self { - Self { - task: dto.task, - role: UpstreamRole::from(dto.role), - } - } -} - -impl From for BoundaryState { - /// Convert the adapter state vocabulary into the public state enum. - fn from(dto: BoundaryStateDto) -> Self { - match dto { - BoundaryStateDto::Consumes => Self::Consumes, - BoundaryStateDto::Wraps => Self::Wraps, - BoundaryStateDto::Pending => Self::Pending, - BoundaryStateDto::Divergent => Self::Divergent, - } - } -} - -impl From for UpstreamRole { - /// Convert the adapter role vocabulary into the public role enum. - fn from(dto: UpstreamRoleDto) -> Self { - match dto { - UpstreamRoleDto::Boundary => Self::Boundary, - UpstreamRoleDto::Metadata => Self::Metadata, - UpstreamRoleDto::CapabilityProvenance => Self::CapabilityProvenance, - UpstreamRoleDto::Vocabulary => Self::Vocabulary, - UpstreamRoleDto::Renderer => Self::Renderer, - UpstreamRoleDto::Profile => Self::Profile, - UpstreamRoleDto::Delivery => Self::Delivery, - UpstreamRoleDto::Feedback => Self::Feedback, - UpstreamRoleDto::ExecutionLedger => Self::ExecutionLedger, - } - } -} - -/// Treat empty TOML strings as absent optional manifest evidence. -fn empty_string_as_none<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let value = String::deserialize(deserializer)?; - Ok((!value.is_empty()).then_some(value)) -} diff --git a/crates/weaver-docs-gate/tests/boundary_manifest.rs b/crates/weaver-docs-gate/tests/boundary_manifest.rs index a04e069c..f0f2997a 100644 --- a/crates/weaver-docs-gate/tests/boundary_manifest.rs +++ b/crates/weaver-docs-gate/tests/boundary_manifest.rs @@ -53,24 +53,13 @@ impl FieldName { #[test] fn manifest_registry_matches_rows_and_roadmap_tasks() -> TestResult { let manifest = manifest().map_err(|error| format!("load boundary manifest: {error}"))?; - let row_ids = manifest - .tasks - .iter() - .map(|task| task.id.as_str()) - .collect::>(); - let managed_ids = manifest - .managed_tasks - .iter() - .map(String::as_str) - .collect::>(); - ensure_equal(&managed_ids, &row_ids, "managed_tasks must match task rows")?; - ensure_unique(&managed_ids, FieldName::ManagedTasks)?; + validate_manifest_registry(&manifest)?; let roadmap = read_doc(Utf8Path::new(ROADMAP)).map_err(|error| format!("read {ROADMAP}: {error}"))?; let roadmap_ids = roadmap_task_ids(&roadmap); - for task_id in managed_ids { + for task_id in manifest.managed_tasks.iter().map(String::as_str) { ensure( roadmap_ids.contains(task_id), format!("manifest task {task_id} is missing from {ROADMAP}"), @@ -117,17 +106,7 @@ fn divergent_rows_reference_existing_adr_007_anchors() -> TestResult { .iter() .filter(|task| task.state == BoundaryState::Divergent) { - let anchor = task - .adr_anchor - .as_deref() - .ok_or_else(|| format!("divergent task {} has no ADR anchor", task.id))?; - ensure( - anchors.contains(anchor), - format!( - "task {} references missing ADR 007 anchor {anchor}", - task.id - ), - )?; + validate_divergent_adr_anchor(task, &anchors)?; } Ok(()) @@ -155,6 +134,38 @@ fn manifest() -> TestResult { load_manifest(contents.as_bytes()).map_err(|error| error.to_string()) } +/// Validate that the manifest registry and task rows reference each other. +fn validate_manifest_registry(manifest: &BoundaryManifest) -> TestResult { + let row_ids = manifest + .tasks + .iter() + .map(|task| task.id.as_str()) + .collect::>(); + let managed_ids = manifest + .managed_tasks + .iter() + .map(String::as_str) + .collect::>(); + + ensure_equal(&managed_ids, &row_ids, "managed_tasks must match task rows")?; + ensure_unique(&managed_ids, FieldName::ManagedTasks) +} + +/// Validate that a divergent row links to an existing ADR 007 anchor. +fn validate_divergent_adr_anchor(task: &BoundaryTask, anchors: &BTreeSet) -> TestResult { + let anchor = task + .adr_anchor + .as_deref() + .ok_or_else(|| format!("divergent task {} has no ADR anchor", task.id))?; + ensure( + anchors.contains(anchor), + format!( + "task {} references missing ADR 007 anchor {anchor}", + task.id + ), + ) +} + /// Read a repository documentation file as UTF-8 text. fn read_doc(doc_path: &Utf8Path) -> TestResult { let resolved_path = repo_path(doc_path)?; diff --git a/crates/weaver-docs-gate/tests/load_manifest.rs b/crates/weaver-docs-gate/tests/load_manifest.rs index 907f8b25..01c9cfa7 100644 --- a/crates/weaver-docs-gate/tests/load_manifest.rs +++ b/crates/weaver-docs-gate/tests/load_manifest.rs @@ -139,16 +139,16 @@ fn boundary_file_error_display_messages_are_stable() { ); } -/// Prove adapter DTOs map manifest values into plain domain types. +/// Prove TOML values deserialize directly into domain types. #[test] -fn load_manifest_maps_toml_dto_to_domain_types() -> TestResult { +fn load_manifest_deserializes_toml_into_domain_types() -> TestResult { let manifest = load_manifest(VALID_MANIFEST.as_bytes()).map_err(|error| error.to_string())?; ensure_equal(&manifest.schema_version, &1, "schema version should load")?; ensure_equal( &manifest.tasks.first().map(|task| task.state), &Some(BoundaryState::Consumes), - "state should map from TOML DTO", + "state should deserialize from TOML", )?; ensure_equal( &manifest diff --git a/crates/weaver-docs-gate/tests/support/boundary_properties.rs b/crates/weaver-docs-gate/tests/support/boundary_properties.rs index 072d8e14..5848dad9 100644 --- a/crates/weaver-docs-gate/tests/support/boundary_properties.rs +++ b/crates/weaver-docs-gate/tests/support/boundary_properties.rs @@ -4,10 +4,18 @@ //! gate. They keep generated ISO date shapes and state/evidence combinations //! separate from filesystem and Markdown rendering concerns. +use std::collections::BTreeSet; + use proptest::prelude::*; -use weaver_docs_gate::{BoundaryState, BoundaryTask, UpstreamRef, UpstreamRole}; +use weaver_docs_gate::{BoundaryManifest, BoundaryState, BoundaryTask, UpstreamRef, UpstreamRole}; -use super::{is_iso_date, validate_state_evidence}; +use super::{ + is_iso_date, + markdown_anchor, + validate_divergent_adr_anchor, + validate_manifest_registry, + validate_state_evidence, +}; /// Generated state-specific evidence for one manifest row. struct Evidence { @@ -17,6 +25,15 @@ struct Evidence { next_review_by: Option, } +/// Generated non-evidence fields for one manifest row. +#[derive(Debug)] +struct TaskFields { + id: String, + gist: String, + upstream_task: String, + last_reviewed: String, +} + /// Presence flags used to generate and check state evidence. #[derive(Clone, Copy)] struct EvidenceFlags(u8); @@ -42,6 +59,18 @@ impl Evidence { } } +impl TaskFields { + /// Build representative fields for properties that vary only evidence. + fn representative() -> Self { + Self { + id: "12.1.1".into(), + gist: "Track the downstream consumer boundary.".into(), + upstream_task: "renderer-contract".into(), + last_reviewed: "2026-06-20".into(), + } + } +} + impl EvidenceFlags { /// Return whether the named evidence bit is present. const fn has(self, mask: u8) -> bool { self.0 & mask != 0 } @@ -53,6 +82,18 @@ proptest! { ..ProptestConfig::default() })] + #[test] + /// Prove arbitrary registry IDs must exactly match row IDs and be unique. + fn manifest_referential_integrity_matches_generated_task_ids( + managed_tasks in proptest::collection::vec(task_id_text(), 0..24), + row_ids in proptest::collection::vec(task_id_text(), 0..24), + ) { + let expected = managed_tasks == row_ids && ids_are_unique(&managed_tasks); + let manifest = manifest_with_ids(managed_tasks, row_ids); + + prop_assert_eq!(validate_manifest_registry(&manifest).is_ok(), expected); + } + #[test] /// Prove formatted calendar dates satisfy the ISO date-shape check. fn iso_date_accepts_generated_yyyy_mm_dd( @@ -82,13 +123,121 @@ proptest! { fn state_evidence_constraints_match_boundary_state( state in boundary_state(), evidence_bits in 0u8..16, + fields in task_fields(), ) { let flags = EvidenceFlags(evidence_bits); - let task = task_with_evidence(state, Evidence::from_flags(flags)); + let task = task_with_evidence(state, Evidence::from_flags(flags), fields); let expected = expected_state_evidence(state, flags); prop_assert_eq!(validate_state_evidence(&task).is_ok(), expected); } + + #[test] + /// Prove divergent rows are accepted exactly when their ADR anchor exists. + fn adr_007_anchor_validation_matches_generated_anchor_sets( + anchor in anchor_text(), + is_present in any::(), + ) { + let mut anchors = BTreeSet::new(); + if is_present { + anchors.insert(anchor.clone()); + } else { + anchors.insert(format!("{anchor}-other")); + } + let task = task_with_evidence( + BoundaryState::Divergent, + Evidence { + shipped_in: None, + removal_gate: None, + adr_anchor: Some(anchor), + next_review_by: None, + }, + TaskFields::representative(), + ); + + prop_assert_eq!( + validate_divergent_adr_anchor(&task, &anchors).is_ok(), + is_present + ); + } + + #[test] + /// Prove generated Markdown heading anchors stay GitHub-safe. + fn markdown_heading_anchors_have_stable_slug_shape(heading in markdown_heading_text()) { + let anchor = markdown_anchor(&heading); + + prop_assert!(!anchor.is_empty()); + prop_assert!(!anchor.starts_with('-')); + prop_assert!(!anchor.ends_with('-')); + prop_assert!(!anchor.contains("--")); + prop_assert!(anchor + .chars() + .all(|char| char.is_ascii_lowercase() || char.is_ascii_digit() || char == '-')); + } +} + +/// Generate roadmap-like task IDs with enough variation to find registry drift. +fn task_id_text() -> impl Strategy { "[0-9]{1,3}(\\.[0-9]{1,3}){1,3}" } + +/// Generate valid ADR/GitHub anchor slugs. +fn anchor_text() -> impl Strategy { "[a-z0-9]{1,16}(-[a-z0-9]{1,16}){0,4}" } + +/// Generate headings that always yield a non-empty Markdown anchor. +fn markdown_heading_text() -> impl Strategy { + "[A-Za-z0-9][A-Za-z0-9 ._|!:-]{0,64}" +} + +/// Generate arbitrary non-evidence fields for state/evidence rows. +fn task_fields() -> impl Strategy { + ( + task_id_text(), + "[A-Za-z0-9 .|\\n]{0,64}", + "[A-Za-z0-9 .|\\n]{0,64}", + iso_date_text(), + ) + .prop_map(|(id, gist, upstream_task, last_reviewed)| TaskFields { + id, + gist, + upstream_task, + last_reviewed, + }) +} + +/// Generate formatted calendar dates for manifest row metadata. +fn iso_date_text() -> impl Strategy { + (0u32..=9999, 1u32..=12, 1u32..=28) + .prop_map(|(year, month, day)| format!("{year:04}-{month:02}-{day:02}")) +} + +/// Build a manifest from generated registry IDs and row IDs. +fn manifest_with_ids(managed_tasks: Vec, row_ids: Vec) -> BoundaryManifest { + BoundaryManifest { + schema_version: 1, + managed_tasks, + tasks: row_ids + .into_iter() + .map(|id| { + let mut fields = TaskFields::representative(); + fields.id = id; + task_with_evidence( + BoundaryState::Consumes, + Evidence { + shipped_in: Some("4339a6f3".into()), + removal_gate: None, + adr_anchor: None, + next_review_by: None, + }, + fields, + ) + }) + .collect(), + } +} + +/// Return whether generated IDs contain no duplicates. +fn ids_are_unique(ids: &[String]) -> bool { + let mut seen = BTreeSet::new(); + ids.iter().all(|id| seen.insert(id)) } /// Generate strings whose length alone rules out an ISO `YYYY-MM-DD` shape. @@ -118,21 +267,25 @@ fn boundary_state() -> impl Strategy { ] } -/// Build one task with generated state-specific evidence fields. -fn task_with_evidence(state: BoundaryState, evidence: Evidence) -> BoundaryTask { +/// Build one task with generated fields and state-specific evidence. +fn task_with_evidence( + state: BoundaryState, + evidence: Evidence, + fields: TaskFields, +) -> BoundaryTask { BoundaryTask { - id: "12.1.1".into(), - gist: "Track the downstream consumer boundary.".into(), + id: fields.id, + gist: fields.gist, state, upstream: vec![UpstreamRef { - task: "renderer-contract".into(), + task: fields.upstream_task, role: UpstreamRole::Renderer, }], shipped_in: evidence.shipped_in, removal_gate: evidence.removal_gate, adr_anchor: evidence.adr_anchor, next_review_by: evidence.next_review_by, - last_reviewed: "2026-06-20".into(), + last_reviewed: fields.last_reviewed, } } diff --git a/docs/developers-guide.md b/docs/developers-guide.md index d563b075..520b145e 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -56,9 +56,11 @@ consumer boundary matrix and the referential-integrity checks that keep command-contract roadmap tasks classified. The crate is intentionally a developer tool, not a runtime dependency for `weaver` or `weaverd`. -The public library API exposes plain domain types. TOML deserialization is kept -inside the crate's private adapter layer so manifest framework concerns do not -leak into the boundary vocabulary: +The public library API exposes the domain types used by the manifest, +validation gate, and renderer. Those types deserialize directly from TOML with +the manifest spellings used in `docs/orthoconfig-consumer-boundary.toml`; empty +optional evidence strings deserialize as absent values so the checked-in +manifest stays explicit without carrying separate DTO translation code: - `BoundaryState` is the four-state classification vocabulary: `consumes`, `wraps`, `pending`, and `divergent`. Use `as_str()` when diagnostics or diff --git a/docs/execplans/12-1-1-track-downstream-consumer-boundary.md b/docs/execplans/12-1-1-track-downstream-consumer-boundary.md index d5557721..849197ea 100644 --- a/docs/execplans/12-1-1-track-downstream-consumer-boundary.md +++ b/docs/execplans/12-1-1-track-downstream-consumer-boundary.md @@ -1,9 +1,8 @@ # Track the downstream consumer boundary (roadmap 12.1.1) -This ExecPlan (execution plan) is a living document. The sections -`Constraints`, `Tolerances`, `Risks`, `Progress`, `Surprises & Discoveries`, -`Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work -proceeds. +This ExecPlan (execution plan) is a living document. The sections `Constraints`, +`Tolerances`, `Risks`, `Progress`, `Surprises & Discoveries`, `Decision Log`, +and `Outcomes & Retrospective` must be kept up to date as work proceeds. Status: COMPLETE @@ -21,8 +20,8 @@ question "for every command-contract task in the live Weaver roadmap, what is its relationship to OrthoConfig?" — there is no single matrix, no machine check, and no fixed vocabulary. -Roadmap item 12.1.1 closes that gap. The success criterion in -`docs/roadmap.md` line 46 is: +Roadmap item 12.1.1 closes that gap. The success criterion in `docs/roadmap.md` +line 46 is: > Success: every command-contract task says whether it consumes OrthoConfig, > wraps it temporarily, or records a deliberate divergence in ADR 007. @@ -49,9 +48,9 @@ Observable behaviour after this change: - `docs/users-guide.md` notes the temporary divergences that users may see while OrthoConfig contracts ship. - `cargo test --workspace` exercises a new dedicated - `crates/weaver-docs-gate` integration test that parses the boundary - manifest (`docs/orthoconfig-consumer-boundary.toml`), the roadmap, and - the boundary matrix and asserts referential integrity. + `crates/weaver-docs-gate` integration test that parses the boundary manifest + (`docs/orthoconfig-consumer-boundary.toml`), the roadmap, and the boundary + matrix and asserts referential integrity. - `make check-fmt && make lint && make test && make markdownlint && make nixie` all pass. @@ -82,12 +81,12 @@ boundary becomes review-only again. 2. **No new product runtime crate dependencies.** Documentation and tests use workspace dependencies (`camino`, `serde`, `toml`, `rstest`, `pretty_assertions`, `googletest`, `tracing`, and `metrics`). The - observability follow-up keeps `metrics` and `tracing` as - `weaver-docs-gate` dev-dependencies so `weaver` and `weaverd` do not gain a - runtime dependency. The test must compile under - `--workspace --all-targets --all-features`. No new workspace member adds a - runtime binary; `crates/weaver-docs-gate` exists only to host the - integration test and the small manifest parser it shares with itself. + observability follow-up keeps `metrics` and `tracing` as `weaver-docs-gate` + dev-dependencies so `weaver` and `weaverd` do not gain a runtime dependency. + The test must compile under `--workspace --all-targets --all-features`. No + new workspace member adds a runtime binary; `crates/weaver-docs-gate` exists + only to host the integration test and the small manifest parser it shares + with itself. 3. **Do not duplicate the OrthoConfig roadmap.** Each matrix row references an OrthoConfig task ID, not its content. If OrthoConfig renames a task, the matrix needs a one-line update, not a re-derivation. @@ -99,28 +98,26 @@ boundary becomes review-only again. "Boundary classification" section sits alongside them and the matrix doc references both. 6. **400-line file limit.** Every Rust source file added or modified must - stay below 400 lines. The matrix Markdown is exempt by repository policy - for prose, but the manifest TOML should remain well below that limit by - design. + stay below 400 lines. The matrix Markdown is exempt by repository policy for + prose, but the manifest TOML should remain well below that limit by design. 7. **en-GB Oxford spelling.** All new prose follows `en-GB-oxendict` conventions and `docs/documentation-style-guide.md`. 8. **No prototype command grammar reintroduction.** The matrix references resource-first commands only. Prototype `observe`, `act`, `verify`, provider-first commands, and root `--output` remain archive provenance. 9. **Stable task IDs.** The manifest keys roadmap task IDs as strings - (`"12.1.2"`). Roadmap renumbering is out of scope; ID stability is - asserted by the test gate. + (`"12.1.2"`). Roadmap renumbering is out of scope; ID stability is asserted + by the test gate. 10. **Strict Clippy.** New code must compile under `cargo clippy --workspace --all-targets --all-features -- -D warnings`. ## Tolerances (exception triggers) - **Scope.** If implementation modifies more than twelve documentation - files or six Rust source files (excluding the new - `crates/weaver-docs-gate` member's `Cargo.toml`, `src/lib.rs`, - `examples/render_boundary_matrix.rs`, and the test file), stop and - escalate. The new workspace member is expected; growth beyond it is - not. + files or six Rust source files (excluding the new `crates/weaver-docs-gate` + member's `Cargo.toml`, `src/lib.rs`, `examples/render_boundary_matrix.rs`, + and the test file), stop and escalate. The new workspace member is expected; + growth beyond it is not. - **Roadmap renames.** If classifying the existing tasks requires renumbering a roadmap entry or moving a task between phases, stop and escalate. - **ADR 007 rewrites.** If the boundary classification cannot fit alongside @@ -129,8 +126,8 @@ boundary becomes review-only again. - **Dependencies.** If the test gate requires a new external crate dependency, stop and escalate. - **Divergent classifications.** If two or more command-contract tasks must - be classified `divergent` without an existing ADR 007 anchor, stop and - open a follow-up ADR-007 amendment before continuing. + be classified `divergent` without an existing ADR 007 anchor, stop and open a + follow-up ADR-007 amendment before continuing. - **Iterations.** If the test gate fails after three attempts at adjustment, stop and escalate. - **Time.** If any single stage (A through E) takes more than four working @@ -144,58 +141,56 @@ boundary becomes review-only again. `"5.2.3 — Record consumer dependency boundaries"`). The developers' guide documents a quarterly cross-repo reconciliation step that compares the manifest phrases against the OrthoConfig roadmap when that repository is - available locally. The CI gate intentionally does not check OrthoConfig - out; cross-repo drift is caught by the reconciliation rite, not by an - always-on probe. + available locally. The CI gate intentionally does not check OrthoConfig out; + cross-repo drift is caught by the reconciliation rite, not by an always-on + probe. - Risk: A command-contract task is missed during classification. Severity: - high. Likelihood: medium. Mitigation: the candidate set is an explicit - opt-in registry, not a topic-keyword heuristic. Every roadmap task ID - that should be classified appears in - `docs/orthoconfig-consumer-boundary.toml` under a top-level - `managed_tasks` array. The test gate fails when a roadmap task heading - matches an ID in `managed_tasks` but has no matching `[[task]]` row, and - also when a `[[task]]` row references an ID outside `managed_tasks`. - Adding a new command-contract task therefore requires editing the - registry first; default-fail prevents silent omission. + high. Likelihood: medium. Mitigation: the candidate set is an explicit opt-in + registry, not a topic-keyword heuristic. Every roadmap task ID that should be + classified appears in `docs/orthoconfig-consumer-boundary.toml` under a + top-level `managed_tasks` array. The test gate fails when a roadmap task + heading matches an ID in `managed_tasks` but has no matching `[[task]]` row, + and also when a `[[task]]` row references an ID outside `managed_tasks`. + Adding a new command-contract task therefore requires editing the registry + first; default-fail prevents silent omission. - Risk: Contributors classify a new task as `consumes` even though the upstream OrthoConfig contract has not shipped yet. Severity: medium. - Likelihood: medium. Mitigation: the manifest's `consumes` state requires - a non-empty `shipped_in` field naming the upstream release (or commit - SHA) that landed the contract, plus the upstream task ID. The test gate - rejects `consumes` rows without `shipped_in`. + Likelihood: medium. Mitigation: the manifest's `consumes` state requires a + non-empty `shipped_in` field naming the upstream release (or commit SHA) that + landed the contract, plus the upstream task ID. The test gate rejects + `consumes` rows without `shipped_in`. - Risk: A roadmap task depends on an OrthoConfig contract that is not yet decided. Severity: high. Likelihood: high (phases 17–20 in particular). - Mitigation: a fourth state `pending` captures exactly this case. A - `pending` row carries the upstream task ID with `shipped_in = None` and - a non-empty `next_review_by` field (ISO-8601 date). The test gate fails - when a `pending` row's `next_review_by` is more than 270 days in the past - to prevent the matrix from decaying into a parking lot. + Mitigation: a fourth state `pending` captures exactly this case. A `pending` + row carries the upstream task ID with `shipped_in = None` and a non-empty + `next_review_by` field (ISO-8601 date). The test gate fails when a `pending` + row's `next_review_by` is more than 270 days in the past to prevent the + matrix from decaying into a parking lot. - Risk: The matrix doc and the manifest TOML drift. Severity: high. - Likelihood: medium. Mitigation: the matrix is generated from the manifest - by a Rust helper that the test calls; the test asserts byte-for-byte - equality with the committed matrix Markdown. There is no second snapshot - layer to maintain. + Likelihood: medium. Mitigation: the matrix is generated from the manifest by + a Rust helper that the test calls; the test asserts byte-for-byte equality + with the committed matrix Markdown. There is no second snapshot layer to + maintain. - Risk: Boundary classification is treated as a one-off audit rather than a living contract. Severity: high. Likelihood: medium. Mitigation: - documentation in `docs/developers-guide.md` describes the workflow and - links to the matrix; the test gate runs on every CI build; every row - carries a `last_reviewed` ISO-8601 date and the gate warns (without - failing) when any row is more than 270 days stale, prompting a refresh. + documentation in `docs/developers-guide.md` describes the workflow and links + to the matrix; the test gate runs on every CI build; every row carries a + `last_reviewed` ISO-8601 date and the gate warns (without failing) when any + row is more than 270 days stale, prompting a refresh. - Risk: A `divergent` state is used as a parking lot for "we have not decided". Severity: medium. Likelihood: low (now that `pending` exists). - Mitigation: every `divergent` row must carry an `adr_anchor` field - pointing to a specific section heading in + Mitigation: every `divergent` row must carry an `adr_anchor` field pointing + to a specific section heading in `docs/adr-007-agent-native-command-surface.md` and the test asserts the - anchor exists. Anchors may be shared by multiple rows (many-to-one), but - the anchor itself must be present. + anchor exists. Anchors may be shared by multiple rows (many-to-one), but the + anchor itself must be present. - Risk: Wafflecat's lighter alternative (inline roadmap annotations with a - small lint, no separate manifest) would satisfy the same prose-level - success criterion at lower surface cost. Severity: low (this is a scope - trade-off, not a correctness risk). Likelihood: n/a. Mitigation: the - Decision Log captures the explicit choice to take the heavier matrix - approach for downstream queryability. If team capacity becomes the - binding constraint, the alternative remains available as a follow-up - redesign. + small lint, no separate manifest) would satisfy the same prose-level success + criterion at lower surface cost. Severity: low (this is a scope trade-off, + not a correctness risk). Likelihood: n/a. Mitigation: the Decision Log + captures the explicit choice to take the heavier matrix approach for + downstream queryability. If team capacity becomes the binding constraint, the + alternative remains available as a follow-up redesign. ## Progress @@ -278,6 +273,15 @@ state, not the intended sequence. failures, and runs as a named CI step before wider workspace coverage. `docs/developers-guide.md` documents the visible failure format and local rerun command. +- [x] 2026-06-22T00:00:00Z: Review feedback removed the mechanical + `manifest_adapter.rs` DTO layer. The public boundary domain types now + deserialize directly from the TOML manifest, including snake-case enum + spellings and empty-string-as-absent optional evidence fields. Property + coverage now also exercises generated manifest registry symmetry, + arbitrary state/evidence task rows, generated ADR 007 anchor sets, and + Markdown heading-anchor shape. Validation passed with + `cargo test -p weaver-docs-gate`, `make markdownlint`, `make nixie`, + `make check-fmt`, `make lint`, and `make test`. - [x] 2026-06-21T00:00:00Z: Review feedback moved boundary manifest observability into the docs-gate library entry points. `load_manifest` and `load_manifest_file` now emit structured `tracing` events, increment @@ -297,69 +301,62 @@ state, not the intended sequence. Recorded as they occur during implementation. Format: - Observation: … - Evidence: … - Impact: … + Evidence: … Impact: … - Observation: `toml` was already present in `Cargo.lock` but was not a - workspace dependency. - Evidence: repository search showed transitive lockfile entries and no - workspace dependency entry before Stage B. - Impact: Stage B added `toml = "0.9.12"` to `[workspace.dependencies]` so - the private docs-gate crate can use the parser explicitly. + workspace dependency. Evidence: repository search showed transitive lockfile + entries and no workspace dependency entry before Stage B. Impact: Stage B + added `toml = "0.9.12"` to `[workspace.dependencies]` so the private + docs-gate crate can use the parser explicitly. - Observation: `mdtablefix` rewrites generated Markdown tables and can break - rows that contain empty cells. - Evidence: the first `make fmt` attempt failed with `MD056` after wrapping - empty table cells in `docs/orthoconfig-consumer-boundary.md`. - Impact: the renderer now emits formatter-stable aligned tables and displays - absent values as `n/a`; the TOML manifest remains the evidence source for - genuinely empty fields. + rows that contain empty cells. Evidence: the first `make fmt` attempt failed + with `MD056` after wrapping empty table cells in + `docs/orthoconfig-consumer-boundary.md`. Impact: the renderer now emits + formatter-stable aligned tables and displays absent values as `n/a`; the TOML + manifest remains the evidence source for genuinely empty fields. - Observation: delivery and feedback OrthoConfig references need versioned - leaf task identifiers in shared upstream lists. - Evidence: `coderabbit review --agent` flagged the `12.1.5` manifest row - while later rows already used `OrthoConfig 9.2.1` and - `OrthoConfig 9.2.2`. - Impact: the manifest now uses the leaf task identifiers consistently before - generated matrix publication. + leaf task identifiers in shared upstream lists. Evidence: + `coderabbit review --agent` flagged the `12.1.5` manifest row while later + rows already used `OrthoConfig 9.2.1` and `OrthoConfig 9.2.2`. Impact: the + manifest now uses the leaf task identifiers consistently before generated + matrix publication. - Observation: the matrix drift test must compare against the Markdown shape after repository formatting, not only the raw renderer's first draft. Evidence: the first Stage D focused test run passed integrity checks but failed `committed_matrix_matches_manifest_rendering` because the renderer emitted pre-`mdtablefix` paragraph wrapping and a one-character wider final - table column. - Impact: the renderer now emits the formatter-stable matrix text that is - committed, making future manifest or renderer drift deterministic. + table column. Impact: the renderer now emits the formatter-stable matrix text + that is committed, making future manifest or renderer drift deterministic. - Observation: the final documentation gate tests were correct but initially - too string-heavy and locally repetitive. - Evidence: three follow-up commits changed helper signatures from path - strings to `&Utf8Path`, introduced the `FieldName` enum for manifest field - labels, extracted `validate_field_constraints`, and consolidated the - state-specific evidence wrappers into `validate_state_evidence`. - Impact: the test file now keeps the same assertions and diagnostics while - making the argument roles clearer and reducing duplication markers. + too string-heavy and locally repetitive. Evidence: three follow-up commits + changed helper signatures from path strings to `&Utf8Path`, introduced the + `FieldName` enum for manifest field labels, extracted + `validate_field_constraints`, and consolidated the state-specific evidence + wrappers into `validate_state_evidence`. Impact: the test file now keeps the + same assertions and diagnostics while making the argument roles clearer and + reducing duplication markers. - Observation: the documented pending review staleness rule was not enforced - by the original Stage D test. - Evidence: `validate_pending_evidence` checked that `next_review_by` looked - like `YYYY-MM-DD` but did not compare it with the build date. - Impact: `crates/weaver-docs-gate/tests/support/pending_review_date.rs` now - parses the date with the `time` crate and rejects pending reviews more than - 270 days behind the explicit build date provided by the manifest gate. + by the original Stage D test. Evidence: `validate_pending_evidence` checked + that `next_review_by` looked like `YYYY-MM-DD` but did not compare it with + the build date. Impact: + `crates/weaver-docs-gate/tests/support/pending_review_date.rs` now parses the + date with the `time` crate and rejects pending reviews more than 270 days + behind the explicit build date provided by the manifest gate. - Observation: review output needed the boundary gate to be easier to triage - in CI. - Evidence: the gate was covered by the workspace test suite, but CI did not - name the boundary-manifest test separately and failure messages did not - carry a stable machine-readable code plus remediation line. - Impact: `.github/workflows/ci.yml` now runs - `cargo test -p weaver-docs-gate --test boundary_manifest -- --nocapture` - as `Boundary manifest gate`; gate failures use the + in CI. Evidence: the gate was covered by the workspace test suite, but CI did + not name the boundary-manifest test separately and failure messages did not + carry a stable machine-readable code plus remediation line. Impact: + `.github/workflows/ci.yml` now runs + `cargo test -p weaver-docs-gate --test boundary_manifest -- --nocapture` as + `Boundary manifest gate`; gate failures use the `boundary_manifest_gate failure code=...` format documented in `docs/developers-guide.md`. - Observation: test-only observability did not help callers using the - docs-gate parser or filesystem adapter directly. - Evidence: review feedback noted that `load_manifest` and - `load_manifest_file` had no structured logging or metrics at their read and - schema-failure decision points. - Impact: those public entry points now record success and failure outcomes - through `tracing` and `metrics`, while leaving subscriber and recorder - installation to the application or CI harness boundary. + docs-gate parser or filesystem adapter directly. Evidence: review feedback + noted that `load_manifest` and `load_manifest_file` had no structured logging + or metrics at their read and schema-failure decision points. Impact: those + public entry points now record success and failure outcomes through `tracing` + and `metrics`, while leaving subscriber and recorder installation to the + application or CI harness boundary. ## Decision log @@ -367,114 +364,108 @@ Recorded for any decision that future work must respect. - Decision: execute this plan despite the document's previous draft status. Rationale: the user explicitly requested implementation of this plan on - 2026-06-14, satisfying the ExecPlan approval gate. Date/Author: - 2026-06-14, implementation. + 2026-06-14, satisfying the ExecPlan approval gate. Date/Author: 2026-06-14, + implementation. - Decision: pin `ortho_config` to commit `4339a6f3c61dc4fed86493d99ffb05230bee2a1b` until the project agrees an - OrthoConfig v0.9.0 release. - Rationale: the user requested a pinned SHA rather than a release version for - now; `git ls-remote https://github.com/leynos/ortho-config.git HEAD` - resolved that revision for the current upstream main branch. Date/Author: - 2026-06-14, implementation. + OrthoConfig v0.9.0 release. Rationale: the user requested a pinned SHA rather + than a release version for now; + `git ls-remote https://github.com/leynos/ortho-config.git HEAD` resolved that + revision for the current upstream main branch. Date/Author: 2026-06-14, + implementation. - Decision: classify boundary state with a closed four-value vocabulary (`consumes`, `wraps`, `pending`, `divergent`) rather than free-form prose. Rationale: the three-value vocabulary (consumes/wraps/divergent) borrowed - from OpenAPI Generator and IETF BCP 9 compliance reports does not - distinguish "upstream contract exists but unshipped" (`wraps`, with a - removal gate) from "upstream contract not yet decided" (`pending`, with - a `next_review_by` date). Without the fourth state the matrix would - pollute `wraps` and `divergent` with non-decisions, exactly the parking - lot the gate is meant to prevent. Date/Author: 2026-06-07, Logisphere - review by Telefono and Doggylump. + from OpenAPI Generator and IETF BCP 9 compliance reports does not distinguish + "upstream contract exists but unshipped" (`wraps`, with a removal gate) from + "upstream contract not yet decided" (`pending`, with a `next_review_by` + date). Without the fourth state the matrix would pollute `wraps` and + `divergent` with non-decisions, exactly the parking lot the gate is meant to + prevent. Date/Author: 2026-06-07, Logisphere review by Telefono and Doggylump. - Decision: hold the classification as a single TOML manifest at `docs/orthoconfig-consumer-boundary.toml`, render the Markdown matrix from - it, and gate referential integrity from a Rust integration test. - Rationale: keeps the source of truth machine-readable, avoids - hand-maintained tables drifting, and follows the `xtask`-style drift gate - pattern (matklad/cargo-xtask, `trycmd`) used in the wider Rust ecosystem. + it, and gate referential integrity from a Rust integration test. Rationale: + keeps the source of truth machine-readable, avoids hand-maintained tables + drifting, and follows the `xtask`-style drift gate pattern + (matklad/cargo-xtask, `trycmd`) used in the wider Rust ecosystem. Date/Author: 2026-06-07, planning. - Decision: host the test gate in a new tiny workspace member - `crates/weaver-docs-gate`, not in `crates/weaver-build-util`. - Rationale: `weaver-build-util` exists for build-script helpers (manual - page dates, capability-based `Dir` access). Adding documentation- - governance integration tests there mixes concerns and gives the - build-helper crate a misleading public surface. A dedicated workspace - member is honest about scope and keeps `weaver-build-util` boring. - Date/Author: 2026-06-07, Logisphere review by Pandalump. + `crates/weaver-docs-gate`, not in `crates/weaver-build-util`. Rationale: + `weaver-build-util` exists for build-script helpers (manual page dates, + capability-based `Dir` access). Adding documentation- governance integration + tests there mixes concerns and gives the build-helper crate a misleading + public surface. A dedicated workspace member is honest about scope and keeps + `weaver-build-util` boring. Date/Author: 2026-06-07, Logisphere review by + Pandalump. - Decision: identify upstream contract availability with a `shipped_in: Option` field (an OrthoConfig version tag such as - `"0.9.0"` or a fallback commit short SHA when no release exists yet), - not a Boolean `shipped` flag. Rationale: Boolean is too coarse for an - evolving dependency; a version string survives the `wraps → consumes` - transition audibly and lets the matrix encode minimum-known-good - versions. Date/Author: 2026-06-07, Logisphere review by Telefono. + `"0.9.0"` or a fallback commit short SHA when no release exists yet), not a + Boolean `shipped` flag. Rationale: Boolean is too coarse for an evolving + dependency; a version string survives the `wraps → consumes` transition + audibly and lets the matrix encode minimum-known-good versions. Date/Author: + 2026-06-07, Logisphere review by Telefono. - Decision: maintain the candidate set as an explicit `managed_tasks` - array in the manifest, not as a topic-keyword heuristic over the - roadmap prose. Rationale: heuristics produce silent false negatives - when a new task uses unfamiliar terminology; the explicit list defaults - to fail-closed and forces classification on every new command-contract - task. Date/Author: 2026-06-07, Logisphere review by Buzzy Bee. + array in the manifest, not as a topic-keyword heuristic over the roadmap + prose. Rationale: heuristics produce silent false negatives when a new task + uses unfamiliar terminology; the explicit list defaults to fail-closed and + forces classification on every new command-contract task. Date/Author: + 2026-06-07, Logisphere review by Buzzy Bee. - Decision: do not wire `rstest-bdd` scenarios for the matrix gate. - Rationale: the candidate behavioural scenarios are direct restatements - of the integration-test invariants. `rstest-bdd` would add ceremony - without adding behavioural coverage. The integration test stays as a - parameterized `rstest` suite. Date/Author: 2026-06-07, Logisphere - review by Dinolump. + Rationale: the candidate behavioural scenarios are direct restatements of the + integration-test invariants. `rstest-bdd` would add ceremony without adding + behavioural coverage. The integration test stays as a parameterized `rstest` + suite. Date/Author: 2026-06-07, Logisphere review by Dinolump. - Decision: assert matrix freshness by byte-for-byte equality with the - committed Markdown file, not via an `insta` snapshot. Rationale: the - manifest is the source of truth and the renderer is deterministic; a - second snapshot review layer would duplicate the equality check - without adding signal. Date/Author: 2026-06-07, Logisphere review by - Dinolump. + committed Markdown file, not via an `insta` snapshot. Rationale: the manifest + is the source of truth and the renderer is deterministic; a second snapshot + review layer would duplicate the equality check without adding signal. + Date/Author: 2026-06-07, Logisphere review by Dinolump. - Decision: do not generate the OrthoConfig dependency table in ADR 007 from the manifest. Rationale: ADR 007 is a stable, human-readable - architectural record. The manifest is the per-task index that points - back to the ADR; a cycle would couple them too tightly. Date/Author: - 2026-06-07, planning. + architectural record. The manifest is the per-task index that points back to + the ADR; a cycle would couple them too tightly. Date/Author: 2026-06-07, + planning. - Decision considered and deferred: switch entirely to inline roadmap - annotations (Wafflecat's lighter alternative) with no separate manifest - or matrix doc. Rationale for deferring: the heavier matrix approach - enables cross-phase queryability and a single discoverable artefact for - contributors and reviewers. If team capacity proves limiting, the - lighter alternative remains a viable follow-up. Date/Author: 2026-06-07, - Logisphere review by Wafflecat; deferred. + annotations (Wafflecat's lighter alternative) with no separate manifest or + matrix doc. Rationale for deferring: the heavier matrix approach enables + cross-phase queryability and a single discoverable artefact for contributors + and reviewers. If team capacity proves limiting, the lighter alternative + remains a viable follow-up. Date/Author: 2026-06-07, Logisphere review by + Wafflecat; deferred. - Decision: keep the boundary gate refactors as test-internal helper changes - rather than changing the manifest schema or generated matrix. - Rationale: the requested refinements addressed maintainability biomarkers - in `crates/weaver-docs-gate/tests/boundary_manifest.rs`; the observable - contract remains the TOML manifest, generated Markdown matrix, and - referential-integrity assertions. Date/Author: 2026-06-15, - implementation. + rather than changing the manifest schema or generated matrix. Rationale: the + requested refinements addressed maintainability biomarkers in + `crates/weaver-docs-gate/tests/boundary_manifest.rs`; the observable contract + remains the TOML manifest, generated Markdown matrix, and + referential-integrity assertions. Date/Author: 2026-06-15, implementation. - Decision: use the existing workspace `time` crate for pending review date - parsing and build-date comparison. - Rationale: manual calendar arithmetic conflicts with the repository's - strict integer-division and remainder lints, while `time::Date` expresses - the explicit-date policy directly and keeps the gate portable. - Date/Author: 2026-06-15, implementation. + parsing and build-date comparison. Rationale: manual calendar arithmetic + conflicts with the repository's strict integer-division and remainder lints, + while `time::Date` expresses the explicit-date policy directly and keeps the + gate portable. Date/Author: 2026-06-15, implementation. - Decision: keep boundary observability inside the docs-gate crate without - installing global telemetry infrastructure. - Rationale: the gate is documentation-governance behaviour, but callers also - use the parser and filesystem adapter directly. Emitting `tracing` events and - `metrics` counters from the library makes failures diagnosable, while - deferring subscriber and recorder setup keeps the crate reusable and avoids - global side effects. + installing global telemetry infrastructure. Rationale: the gate is + documentation-governance behaviour, but callers also use the parser and + filesystem adapter directly. Emitting `tracing` events and `metrics` counters + from the library makes failures diagnosable, while deferring subscriber and + recorder setup keeps the crate reusable and avoids global side effects. Date/Author: 2026-06-21, implementation. ## Outcomes & retrospective -Roadmap 12.1.1 is complete. The branch added the OrthoConfig consumer -boundary vocabulary to ADR 007, introduced the TOML boundary manifest and -generated Markdown matrix, linked the matrix from the roadmap and contributor +Roadmap 12.1.1 is complete. The branch added the OrthoConfig consumer boundary +vocabulary to ADR 007, introduced the TOML boundary manifest and generated +Markdown matrix, linked the matrix from the roadmap and contributor documentation, documented user-facing temporary wrapper and pending-contract states, and added the `weaver-docs-gate` parser, renderer, regenerator, and integration tests. The gate already caught useful drift during implementation. CodeRabbit found -generic `OrthoConfig 9.2` upstream references before publication of the -matrix, and the matrix rendering test caught formatter-sensitive Markdown -output before the generated file was committed as stable. Subsequent -code-quality refinements kept the same behaviour while improving the -test-helper API and eliminating duplicate state-evidence wrappers. +generic `OrthoConfig 9.2` upstream references before publication of the matrix, +and the matrix rendering test caught formatter-sensitive Markdown output before +the generated file was committed as stable. Subsequent code-quality refinements +kept the same behaviour while improving the test-helper API and eliminating +duplicate state-evidence wrappers. The final implementation uses a four-state vocabulary: `consumes`, `wraps`, `pending`, and `divergent`. The fourth state, `pending`, is an intentional @@ -482,14 +473,14 @@ schema adjustment from the roadmap's original three-state success wording; it prevents undecided upstream contracts from being recorded as either temporary wrappers or deliberate divergences. All final gates passed: `cargo test -p weaver-docs-gate`, `make check-fmt`, `make lint`, `make test`, -`make markdownlint`, and `make nixie`. The final CodeRabbit review for the -main implementation completed with `findings: 0`. +`make markdownlint`, and `make nixie`. The final CodeRabbit review for the main +implementation completed with `findings: 0`. A post-completion fix closed the documented pending-review expiry gap. Pending -rows now fail once `next_review_by` is more than 270 days behind the build -date supplied by the manifest gate. A later review follow-up made that build -date an explicit parameter so the gate no longer depends on -`SOURCE_DATE_EPOCH` or the wall clock. +rows now fail once `next_review_by` is more than 270 days behind the build date +supplied by the manifest gate. A later review follow-up made that build date an +explicit parameter so the gate no longer depends on `SOURCE_DATE_EPOCH` or the +wall clock. A later review follow-up made the Stage D gate operationally visible. CI now runs the boundary manifest integration test as its own named step, and @@ -501,18 +492,17 @@ validation decision points and increments Another review follow-up moved the operational signal into production library paths. `load_manifest` and `load_manifest_file` now emit structured events on load attempts, successes, read failures, invalid paths, missing files, and -schema failures. They increment -`weaver_docs_gate_boundary_manifest_load_total` with low-cardinality -`source` and `outcome` labels and attach the same remediation guidance to -generated read and schema errors that the CI gate prints for validation -failures. +schema failures. They increment `weaver_docs_gate_boundary_manifest_load_total` +with low-cardinality `source` and `outcome` labels and attach the same +remediation guidance to generated read and schema errors that the CI gate +prints for validation failures. The 2026-06-22 review follow-up made that operational contract visible in the public API documentation and widened the test boundary. The example binary now has error-path coverage for argument and output-path failures, generated -properties exercise date-shape and state/evidence invariants, and a -`trybuild` pass test proves the exported domain types, load functions, -renderer, and error trait implementations remain usable by downstream callers. +properties exercise date-shape and state/evidence invariants, and a `trybuild` +pass test proves the exported domain types, load functions, renderer, and error +trait implementations remain usable by downstream callers. ## Context and orientation @@ -524,61 +514,59 @@ A novice opening this plan needs the following landmarks before reading the - `docs/roadmap.md` — the live forward roadmap. Tasks 12 through 20 belong to the post-ADR-007 grammar. Task 12.1.1 lives at lines 38 to 47. - `docs/adr-007-agent-native-command-surface.md` — the ADR that introduced - the agent-native command surface, the OrthoConfig dependency table (lines - 104 to 122), and the temporary-adapter removal policy (lines 129 to 147). + the agent-native command surface, the OrthoConfig dependency table (lines 104 + to 122), and the temporary-adapter removal policy (lines 129 to 147). - `crates/weaver-cli/src/command_surface.rs` — the only current temporary adapter, holding `CommandSurfaceRecord`, `READ_ONLY_COMMANDS`, and `find_read_only_command` for the pilot `definitions get` and `references list` family. Its module documentation already names the OrthoConfig replacement tasks. This file is not modified by 12.1.1. - `crates/weaver-build-util/src/lib.rs` — a shared build-time helper - crate (manual-page dates, capability-based `Dir` access). This plan - does *not* touch it; the boundary gate has nothing to do with build - scripts. + crate (manual-page dates, capability-based `Dir` access). This plan does + *not* touch it; the boundary gate has nothing to do with build scripts. - `crates/weaver-docs-gate/` — new workspace member introduced by this plan. It hosts the path-free manifest parser (`load_manifest`), the filesystem adapter (`load_manifest_file`), the matrix renderer (`render_matrix`), the `cargo run --example` regenerator, and the - `boundary_gate` integration test. It has no runtime consumers; its - only role is to make documentation governance testable. + `boundary_gate` integration test. It has no runtime consumers; its only role + is to make documentation governance testable. - `docs/developers-guide.md` — the long-form contributor guide. A new subsection ("Tracking the OrthoConfig consumer boundary") sits next to the existing ADR guidance. - `docs/users-guide.md` — the long-form user guide. A new short paragraph - records that some `--json` and human-renderer details may temporarily - diverge from upstream until OrthoConfig contracts ship. + records that some `--json` and human-renderer details may temporarily diverge + from upstream until OrthoConfig contracts ship. ### Upstream landmarks (read once for context) - OrthoConfig roadmap, task 5.2.3: documentation-only commitment to record - consumer dependency boundaries for Weaver and Netsuke. The hard - dependencies are whole-CLI introspection, strict vocabulary policy, - agent-context IR, and localized help generation. The soft dependencies are - profiles, delivery, feedback, skill manifests, and execution ledgers. + consumer dependency boundaries for Weaver and Netsuke. The hard dependencies + are whole-CLI introspection, strict vocabulary policy, agent-context IR, and + localized help generation. The soft dependencies are profiles, delivery, + feedback, skill manifests, and execution ledgers. - OrthoConfig `docs/agent-native-cli-design.md` §2.1: states that OrthoConfig - owns schemas, command metadata, vocabulary, renderer metadata, generated - help and man pages, policy linting, and optional primitives for profiles, + owns schemas, command metadata, vocabulary, renderer metadata, generated help + and man pages, policy linting, and optional primitives for profiles, delivery, feedback, skill manifests, and execution ledgers. Weaver owns semantic execution, capability routing, providers, sandboxing, Double-Lock safety, edits, jobs, and provider-specific idempotency. -- OrthoConfig `docs/adr-003-define-schema-ownership-for-agent-native-contracts.md`: +- OrthoConfig + `docs/adr-003-define-schema-ownership-for-agent-native-contracts.md`: defines the three contract crates and the schema-version constants - (`ORTHO_AGENT_CONTEXT_SCHEMA_VERSION`, - `ORTHO_POLICY_REPORT_SCHEMA_VERSION`). + (`ORTHO_AGENT_CONTEXT_SCHEMA_VERSION`, `ORTHO_POLICY_REPORT_SCHEMA_VERSION`). ### Terms of art - **Command-contract task.** A roadmap task whose body or success criterion touches at least one of: command surface metadata, public CLI grammar, - resource path or canonical verb, renderer contract (`--json`, human - renderer, `--plain`, `--color`, `--width`, paging), exit-code taxonomy, - bounded-list output, mutation policy (`--dry-run`, `--force`, - idempotency), structured error schemas, enumerating diagnostics, selector - forms (`--uri`, `--position`, `--query`, `--from-stdin`), capability - discoverability (`context --json`, `capabilities list`, `help`, manpages, - completions, skills), profiles, delivery, feedback, jobs, execution - ledger, or drift gates. The canonical candidate list is built into the - manifest. + resource path or canonical verb, renderer contract (`--json`, human renderer, + `--plain`, `--color`, `--width`, paging), exit-code taxonomy, bounded-list + output, mutation policy (`--dry-run`, `--force`, idempotency), structured + error schemas, enumerating diagnostics, selector forms (`--uri`, `--position`, + `--query`, `--from-stdin`), capability discoverability (`context --json`, + `capabilities list`, `help`, manpages, completions, skills), profiles, + delivery, feedback, jobs, execution ledger, or drift gates. The canonical + candidate list is built into the manifest. - **Boundary state.** One of `consumes`, `wraps`, `pending`, or `divergent`. - `consumes` means the Weaver task imports or follows an OrthoConfig @@ -605,49 +593,49 @@ A novice opening this plan needs the following landmarks before reading the composite of several upstream tasks. The gate text matches the language already used in the ADR 007 removal policy. - **`managed_tasks` registry.** A top-level array in the manifest listing - every Weaver roadmap task ID whose body sits inside the boundary - surface. The registry is the candidate set; the gate fails when a - registry entry has no `[[task]]` row, when a `[[task]]` row references - an ID outside the registry, or when a candidate task heading appears - in the roadmap but is missing from both. + every Weaver roadmap task ID whose body sits inside the boundary surface. The + registry is the candidate set; the gate fails when a registry entry has no + `[[task]]` row, when a `[[task]]` row references an ID outside the registry, + or when a candidate task heading appears in the roadmap but is missing from + both. ### Hexagonal architecture relevance -Boundary classification is a dependency-rule artefact, not a runtime port. -The matrix records, for each Weaver task, which side of the -domain-versus-adapter line its contract sits on. `consumes` means Weaver -imports a port owned by OrthoConfig; `wraps` means Weaver maintains a local -adapter that will be replaced by the upstream port; `divergent` means -Weaver's domain deliberately defines its own port because the upstream port -does not fit. Treat the matrix as a guard against accidentally inverting the -dependency rule when a new task lands. +Boundary classification is a dependency-rule artefact, not a runtime port. The +matrix records, for each Weaver task, which side of the domain-versus-adapter +line its contract sits on. `consumes` means Weaver imports a port owned by +OrthoConfig; `wraps` means Weaver maintains a local adapter that will be +replaced by the upstream port; `divergent` means Weaver's domain deliberately +defines its own port because the upstream port does not fit. Treat the matrix +as a guard against accidentally inverting the dependency rule when a new task +lands. ## Plan of work -The work runs in five short stages. Each stage ends with a validation step -that must pass before the next stage starts. +The work runs in five short stages. Each stage ends with a validation step that +must pass before the next stage starts. ### Stage A: ratify the boundary vocabulary and matrix shape Stage A is documentation-led. No code lands. 1. Open `docs/adr-007-agent-native-command-surface.md` and add a new section - titled "Boundary classification" before "Consequences". The section - defines the four states (`consumes`, `wraps`, `pending`, `divergent`), - the evidence each state requires, and the location of the matrix + titled "Boundary classification" before "Consequences". The section defines + the four states (`consumes`, `wraps`, `pending`, `divergent`), the evidence + each state requires, and the location of the matrix (`docs/orthoconfig-consumer-boundary.md`). It does not duplicate the - existing "OrthoConfig dependencies" or "Temporary adapter removal - policy" sections; it points at them. It states explicitly that - `pending` exists to keep `wraps` honest about "shape committed - upstream" versus "shape not yet decided upstream". + existing "OrthoConfig dependencies" or "Temporary adapter removal policy" + sections; it points at them. It states explicitly that `pending` exists to + keep `wraps` honest about "shape committed upstream" versus "shape not yet + decided upstream". 2. Choose canonical column names for the matrix: `Roadmap task`, `Gist`, `State`, `Upstream OrthoConfig task`, `Shipped in`, - `Removal gate or divergence`, `Next review by`, `Last reviewed`. - Document them in the new ADR section. + `Removal gate or divergence`, `Next review by`, `Last reviewed`. Document + them in the new ADR section. 3. Open `docs/documentation-style-guide.md` only if a style entry is needed - for the badge or symbol convention used in the matrix (a leading - `✓`, `~`, or `×` character paired with the textual state name); if no - entry is needed, do not modify the style guide. + for the badge or symbol convention used in the matrix (a leading `✓`, `~`, or + `×` character paired with the textual state name); if no entry is needed, + do not modify the style guide. Validation: `make markdownlint` passes, `make nixie` passes, and the new section in ADR 007 reads as a self-contained explanation that a contributor @@ -655,13 +643,12 @@ without prior context can use to classify a new task. ### Stage B: produce the boundary manifest, generator, and matrix doc -Stage B introduces a single source of truth and the renderer that derives -the human-readable matrix from it. +Stage B introduces a single source of truth and the renderer that derives the +human-readable matrix from it. 1. Create `docs/orthoconfig-consumer-boundary.toml`. The file has a - top-level `managed_tasks` array listing every Weaver roadmap task ID - that falls inside the boundary surface, and one `[[task]]` row per - entry: + top-level `managed_tasks` array listing every Weaver roadmap task ID that + falls inside the boundary surface, and one `[[task]]` row per entry: ```toml schema_version = 1 @@ -708,13 +695,13 @@ the human-readable matrix from it. be shared by multiple rows); `shipped_in` and `removal_gate` are empty. - The `role` field on each upstream entry is a closed enum: - `boundary`, `metadata`, `capability_provenance`, `vocabulary`, - `renderer`, `profile`, `delivery`, `feedback`, `execution_ledger`. - The test gate rejects unknown roles. + The `role` field on each upstream entry is a closed enum: `boundary`, + `metadata`, `capability_provenance`, `vocabulary`, `renderer`, `profile`, + `delivery`, `feedback`, `execution_ledger`. The test gate rejects unknown + roles. 2. Populate the manifest with the candidate set derived from - `docs/roadmap.md` and ADR 007. Use the topic-keyword scan described in - the Risks section. At minimum the seed set must include: + `docs/roadmap.md` and ADR 007. Use the topic-keyword scan described in the + Risks section. At minimum the seed set must include: - 12.1.1 through 12.1.5; - 13.1.1, 13.1.2, 13.1.3, 13.2.1, 13.2.2, 13.2.3, 13.3.1, 13.3.2, 13.3.3, 13.3.4; @@ -727,13 +714,12 @@ the human-readable matrix from it. - 20.1.1, 20.1.2, 20.3.3. Each row carries its classification, upstream pairing, and (where - applicable) removal gate language derived from ADR 007's existing - table. Tasks classified `consumes` must reference an upstream that has - already shipped; tasks classified `wraps` must reference a removal - gate; tasks classified `divergent` must reference an ADR 007 anchor. + applicable) removal gate language derived from ADR 007's existing table. + Tasks classified `consumes` must reference an upstream that has already + shipped; tasks classified `wraps` must reference a removal gate; tasks + classified `divergent` must reference an ADR 007 anchor. 3. Create a new tiny workspace member at `crates/weaver-docs-gate` that - exposes the manifest parser and the matrix renderer to the test - harness: + exposes the manifest parser and the matrix renderer to the test harness: ```toml # crates/weaver-docs-gate/Cargo.toml @@ -744,10 +730,10 @@ the human-readable matrix from it. edition = "2024" ``` - The crate has no runtime dependencies it does not need: only - `serde`, `toml`, `camino`, and `thiserror` (all already in the - workspace). It is not a library that downstream code links; it is a - test fixture in crate form. The crate's `src/lib.rs` exposes: + The crate has no runtime dependencies it does not need: only `serde`, `toml`, + `camino`, and `thiserror` (all already in the workspace). It is not a + library that downstream code links; it is a test fixture in crate form. The + crate's `src/lib.rs` exposes: ```rust pub fn load_manifest(reader: impl Read) -> Result; @@ -755,40 +741,39 @@ the human-readable matrix from it. pub fn render_matrix(manifest: &BoundaryManifest) -> String; ``` - `BoundaryManifest` holds the `schema_version`, the ordered - `managed_tasks` array, and a `Vec` keyed by task ID. - `BoundaryTask` holds `id`, `gist`, `state` (an enum with four - variants), `upstream` (a `Vec`), `shipped_in`, - `removal_gate`, `adr_anchor`, `next_review_by`, and `last_reviewed`. + `BoundaryManifest` holds the `schema_version`, the ordered `managed_tasks` + array, and a `Vec` keyed by task ID. `BoundaryTask` holds `id`, + `gist`, `state` (an enum with four variants), `upstream` (a + `Vec`), `shipped_in`, `removal_gate`, `adr_anchor`, + `next_review_by`, and `last_reviewed`. 4. Generate `docs/orthoconfig-consumer-boundary.md` from the manifest. The - matrix uses pipe-delimited Markdown tables grouped by roadmap phase - (12, 13, 14, 15, 16, 17, 18, 19, 20). Each table row links the task ID - back to its roadmap anchor, displays the state with a leading symbol - (`✓` for `consumes`, `~` for `wraps`, `?` for `pending`, `×` for - `divergent`), and lists the upstream task IDs joined by commas. The - `Shipped in` column carries the OrthoConfig version tag for - `consumes` rows and is empty otherwise. Long removal-gate text wraps - in the cell; the gate is the single source of truth and is not - abbreviated. - -Validation: `cargo build --workspace` succeeds (the new -`weaver-docs-gate` crate compiles). `make markdownlint` and `make nixie` -pass. The generated matrix matches the committed -`docs/orthoconfig-consumer-boundary.md` byte for byte. + matrix uses pipe-delimited Markdown tables grouped by roadmap phase (12, 13, + 14, 15, 16, 17, 18, 19, 20). Each table row links the task ID back to its + roadmap anchor, displays the state with a leading symbol (`✓` for `consumes`, + `~` for `wraps`, `?` for `pending`, `×` for `divergent`), and lists the + upstream task IDs joined by commas. The `Shipped in` column carries the + OrthoConfig version tag for `consumes` rows and is empty otherwise. Long + removal-gate text wraps in the cell; the gate is the single source of truth + and is not abbreviated. + +Validation: `cargo build --workspace` succeeds (the new `weaver-docs-gate` +crate compiles). `make markdownlint` and `make nixie` pass. The generated +matrix matches the committed `docs/orthoconfig-consumer-boundary.md` byte for +byte. ### Stage C: cross-link the matrix from ADR 007, the roadmap, and the developers' guide -Stage C adds the back-links and a contributor workflow that prevents -silent drift. +Stage C adds the back-links and a contributor workflow that prevents silent +drift. 1. In `docs/adr-007-agent-native-command-surface.md`, finish the - "Boundary classification" section by linking to the matrix and the - manifest. The existing OrthoConfig dependency table stays as-is. + "Boundary classification" section by linking to the matrix and the manifest. + The existing OrthoConfig dependency table stays as-is. 2. In `docs/roadmap.md`, immediately under the "12.1.1" item, add a single - sentence linking the matrix: "See the OrthoConfig consumer boundary - matrix for the per-task classification." Add the same link near the - headings of phases 13 through 20 so contributors editing a later phase - discover the matrix without scrolling. + sentence linking the matrix: "See the OrthoConfig consumer boundary matrix + for the per-task classification." Add the same link near the headings of + phases 13 through 20 so contributors editing a later phase discover the + matrix without scrolling. 3. In `docs/developers-guide.md`, add a subsection titled "Tracking the OrthoConfig consumer boundary". It describes the workflow: - When adding a roadmap task that touches a command-contract topic, @@ -814,21 +799,21 @@ silent drift. developers' guide because the CI gate intentionally does not check OrthoConfig out. 4. In `docs/contents.md`, add a new entry for - `docs/orthoconfig-consumer-boundary.md` so the new doc is discoverable - from the index. + `docs/orthoconfig-consumer-boundary.md` so the new doc is discoverable from + the index. Validation: `make markdownlint` passes; the cross-links resolve when the -repository is browsed locally; the developers' guide entry is reachable -from `docs/contents.md`. +repository is browsed locally; the developers' guide entry is reachable from +`docs/contents.md`. ### Stage D: add the referential-integrity test gate and snapshot Stage D enforces the matrix at CI time. 1. In `crates/weaver-docs-gate/Cargo.toml`, add `rstest`, - `pretty_assertions`, and `googletest` to `[dev-dependencies]` (each - already lives in the workspace). The crate does not gain new runtime - dependencies and does not depend on `insta` or `rstest-bdd`. + `pretty_assertions`, and `googletest` to `[dev-dependencies]` (each already + lives in the workspace). The crate does not gain new runtime dependencies + and does not depend on `insta` or `rstest-bdd`. 2. Add `crates/weaver-docs-gate/tests/boundary_gate.rs`. It loads the manifest, generates the matrix Markdown, and asserts the following invariants. Each invariant is parameterized with `rstest` so the test @@ -875,8 +860,8 @@ Stage D enforces the matrix at CI time. byte for byte. 3. **Failure-message format.** Each invariant emits a diagnostic in a - fixed shape so contributors can resolve it without reading the test - source. The shape is: + fixed shape so contributors can resolve it without reading the test source. + The shape is: ```plaintext boundary-gate: @@ -907,32 +892,30 @@ Stage D enforces the matrix at CI time. see: docs/developers-guide.md#tracking-the-orthoconfig-consumer-boundary ``` - The integration test produces these strings through a small - helper. `pretty_assertions::assert_eq!` reports the message verbatim - on failure. + The integration test produces these strings through a small helper. + `pretty_assertions::assert_eq!` reports the message verbatim on failure. -Validation: the new test fails before the manifest exists (Red), passes -once Stage B is in place (Green), and remains green after Stage E. +Validation: the new test fails before the manifest exists (Red), passes once +Stage B is in place (Green), and remains green after Stage E. ### Stage E: refresh the users' guide, run the full quality gates, and run CodeRabbit 1. In `docs/users-guide.md`, add a short paragraph in the appropriate - chapter (the user-facing JSON or output-mode discussion) noting that - some `--json` fields, exit classes, and error codes may be provisional - until the OrthoConfig contracts ship. The paragraph links to the - matrix and notes that field names and exit classes remain stable - inside a Weaver minor release. + chapter (the user-facing JSON or output-mode discussion) noting that some + `--json` fields, exit classes, and error codes may be provisional until the + OrthoConfig contracts ship. The paragraph links to the matrix and notes that + field names and exit classes remain stable inside a Weaver minor release. 2. Run `make check-fmt` and capture the output under `/tmp/check-fmt-weaver-12-1-1-track-downstream-consumer-boundary.out`. 3. Run `make lint`, `make test`, `make markdownlint`, and `make nixie`, - each capturing to a log file with the same naming convention. Verify - each completes successfully. + each capturing to a log file with the same naming convention. Verify each + completes successfully. 4. Run `coderabbit review --agent` and resolve every comment before the plan is marked COMPLETE. -Validation: every command completes successfully and `coderabbit review ---agent` raises no outstanding concerns. The roadmap entry for 12.1.1 is -ticked off. +Validation: every command completes successfully and +`coderabbit review --agent` raises no outstanding concerns. The roadmap entry +for 12.1.1 is ticked off. ## Concrete steps @@ -968,9 +951,9 @@ git add crates/weaver-docs-gate docs/orthoconfig-consumer-boundary.toml \ git commit -m "Add OrthoConfig consumer boundary manifest, crate, and matrix" ``` -The renderer is exposed through `cargo run --example` rather than a -`[[bin]]` entry; the example shares the same parser the test calls so -the test cannot rot behind the generator. +The renderer is exposed through `cargo run --example` rather than a `[[bin]]` +entry; the example shares the same parser the test calls so the test cannot rot +behind the generator. ### Stage C commands @@ -1009,8 +992,7 @@ git add docs/users-guide.md git commit -m "Note OrthoConfig boundary in the user's guide" ``` -After Stage E, mark `12.1.1` complete in `docs/roadmap.md` in a final -commit. +After Stage E, mark `12.1.1` complete in `docs/roadmap.md` in a final commit. ## Validation and acceptance @@ -1018,38 +1000,38 @@ Quality criteria define what "done" means. - **Behaviour.** A contributor adding a new command-contract roadmap task without a manifest row sees `cargo test --workspace` fail with a - `boundary-gate: registry-symmetry` diagnostic naming the missing task. - A `wraps` row without a removal gate, a `consumes` row without - `shipped_in`, a `pending` row whose `next_review_by` is more than 270 - days in the past, or a `divergent` row without an ADR 007 anchor each - fail the same test with the matching diagnostic shape from Stage D. - The generated matrix Markdown matches the committed copy. + `boundary-gate: registry-symmetry` diagnostic naming the missing task. A + `wraps` row without a removal gate, a `consumes` row without `shipped_in`, a + `pending` row whose `next_review_by` is more than 270 days in the past, or a + `divergent` row without an ADR 007 anchor each fail the same test with the + matching diagnostic shape from Stage D. The generated matrix Markdown matches + the committed copy. - **Tests.** `cargo test --workspace` passes including the new - `boundary_gate` integration test in `crates/weaver-docs-gate`. The - test fails for the expected reason before Stage B is in place (Red), - passes after Stage B and Stage D land (Green), and the matrix - regeneration is idempotent (Refactor). + `boundary_gate` integration test in `crates/weaver-docs-gate`. The test fails + for the expected reason before Stage B is in place (Red), passes after Stage + B and Stage D land (Green), and the matrix regeneration is idempotent + (Refactor). - **Lint and typecheck.** `make check-fmt`, `make lint`, `make markdownlint`, and `make nixie` succeed. New code adds no `#[allow]` or `#[expect]` attributes beyond what existing files use. - **Documentation.** `docs/contents.md` lists the new boundary matrix; - `docs/users-guide.md` mentions provisional contracts where users may - notice them; `docs/developers-guide.md` carries the contributor - workflow; ADR 007 carries the boundary classification section. + `docs/users-guide.md` mentions provisional contracts where users may notice + them; `docs/developers-guide.md` carries the contributor workflow; ADR 007 + carries the boundary classification section. - **CodeRabbit.** `coderabbit review --agent` raises no outstanding concerns at completion. ### Red-Green-Refactor evidence - **Red.** Before Stage B and Stage D land, - `cargo test -p weaver-docs-gate --test boundary_gate` fails with the - exact `boundary-gate: registry-symmetry` diagnostic shape shown in - Stage D. Capture the failing transcript in `Progress` under Stage D. + `cargo test -p weaver-docs-gate --test boundary_gate` fails with the exact + `boundary-gate: registry-symmetry` diagnostic shape shown in Stage D. Capture + the failing transcript in `Progress` under Stage D. - **Green.** After Stage B (manifest, generator) and Stage D (test gate) land, the same command passes. Capture the passing transcript. - **Refactor.** Re-render the matrix with the `cargo run --example` - invocation in Stage B and rerun the test; both remain green. Capture - the output. + invocation in Stage B and rerun the test; both remain green. Capture the + output. ## Interfaces and dependencies @@ -1120,14 +1102,14 @@ pub fn load_manifest_file(path: &Utf8Path) -> Result String; ``` -The renderer takes no global state, accepts a parsed manifest, and -returns a `String`. The test gate reads the manifest file, calls -`load_manifest` and `render_matrix`, and compares the rendered output -against the committed matrix Markdown byte for byte. +The renderer takes no global state, accepts a parsed manifest, and returns a +`String`. The test gate reads the manifest file, calls `load_manifest` and +`render_matrix`, and compares the rendered output against the committed matrix +Markdown byte for byte. -The integration test depends only on existing workspace dependencies -(`rstest`, `pretty_assertions`, `googletest`, `toml`, `camino`, -`thiserror`). It does not use `insta` or `rstest-bdd`. +The integration test depends only on existing workspace dependencies (`rstest`, +`pretty_assertions`, `googletest`, `toml`, `camino`, `thiserror`). It does not +use `insta` or `rstest-bdd`. ## Boundary classification diagram @@ -1149,24 +1131,23 @@ flowchart TD Test --> CI[make test] ``` -The diagram captures the steady state once the matrix is in place. Each -new command-contract roadmap task adds a row to the manifest; the test -gate stops a commit that omits the row or carries the wrong evidence. +The diagram captures the steady state once the matrix is in place. Each new +command-contract roadmap task adds a row to the manifest; the test gate stops a +commit that omits the row or carries the wrong evidence. ## Idempotence and recovery - Re-running the matrix generator must produce byte-identical output. The - generator does not depend on wall-clock time. The `last_reviewed` field - is set explicitly per row by the contributor when they touch the row. + generator does not depend on wall-clock time. The `last_reviewed` field is + set explicitly per row by the contributor when they touch the row. - Re-running the test gate is safe: the test reads the manifest, the roadmap, the ADR, and the matrix, and asserts equality. No filesystem mutation occurs. - If the manifest and matrix drift, the test fails and the contributor - regenerates the matrix from the manifest. The manifest stays - authoritative. + regenerates the matrix from the manifest. The manifest stays authoritative. - A partially completed plan stays in `Progress` so the next contributor - resumes from the last completed stage. Each stage's commit is reversible - by `git revert`. + resumes from the last completed stage. Each stage's commit is reversible by + `git revert`. ## Artefacts and notes @@ -1186,26 +1167,24 @@ completes. Examples (filled in as work proceeds): 2026-06-07. Revised after a Logisphere pre-implementation design review. Changes applied: (1) host crate moved from `weaver-build-util` to a new -`weaver-docs-gate` workspace member; (2) classification vocabulary -expanded to four states (`consumes`, `wraps`, `pending`, `divergent`) -to absorb "upstream contract not yet decided" cleanly; (3) the -`shipped: bool` flag replaced with a `shipped_in: Option` field -carrying the OrthoConfig release tag; (4) the topic-keyword candidate -heuristic replaced with an explicit `managed_tasks` registry that fails -closed when a new ID is missing; (5) the test stack pruned (dropped the -`rstest-bdd` feature file and the `insta` snapshot in favour of one -byte-for-byte equality check); (6) the test gate's failure-message -shape pinned with concrete examples; (7) staleness rules added -(`last_reviewed` warn-at-270-days, `pending.next_review_by` -fail-at-270-days); (8) the `role` field tightened into a closed enum; -(9) the developers' guide adds a quarterly cross-repo reconciliation -ritual. The remaining unresolved alternative (inline roadmap -annotations) is recorded in the Decision Log as deferred. +`weaver-docs-gate` workspace member; (2) classification vocabulary expanded to +four states (`consumes`, `wraps`, `pending`, `divergent`) to absorb "upstream +contract not yet decided" cleanly; (3) the `shipped: bool` flag replaced with a +`shipped_in: Option` field carrying the OrthoConfig release tag; (4) +the topic-keyword candidate heuristic replaced with an explicit `managed_tasks` +registry that fails closed when a new ID is missing; (5) the test stack pruned +(dropped the `rstest-bdd` feature file and the `insta` snapshot in favour of +one byte-for-byte equality check); (6) the test gate's failure-message shape +pinned with concrete examples; (7) staleness rules added (`last_reviewed` +warn-at-270-days, `pending.next_review_by` fail-at-270-days); (8) the `role` +field tightened into a closed enum; (9) the developers' guide adds a quarterly +cross-repo reconciliation ritual. The remaining unresolved alternative (inline +roadmap annotations) is recorded in the Decision Log as deferred. ## Relevant skills and documentation -The contributor implementing this plan should load the following skills -and consult the following documents. +The contributor implementing this plan should load the following skills and +consult the following documents. Skills: @@ -1215,10 +1194,9 @@ Skills: - `arch-decision-records` — to amend ADR 007 cleanly when a `divergent` row needs an anchor. - `rust-unit-testing` — for the `rstest` parameterization and the - `pretty_assertions`/`googletest` shape of the gate. The plan - intentionally does *not* use `rstest-bdd` or `insta` because their - scenarios and snapshots would duplicate the integration test's - invariants. + `pretty_assertions`/`googletest` shape of the gate. The plan intentionally + does *not* use `rstest-bdd` or `insta` because their scenarios and snapshots + would duplicate the integration test's invariants. - `arch-crate-design` — to keep `crates/weaver-docs-gate` minimal and honest about its single test-fixture purpose. - `commit-message` and `pr-creation` — for the final commits and the draft @@ -1243,7 +1221,8 @@ Upstream references (read-only context): - OrthoConfig roadmap, task 5.2.3 (in the OrthoConfig repository). - OrthoConfig `docs/agent-native-cli-design.md` §2.1. -- OrthoConfig `docs/adr-003-define-schema-ownership-for-agent-native-contracts.md`. +- OrthoConfig + `docs/adr-003-define-schema-ownership-for-agent-native-contracts.md`. Prior art consulted for the gate design: @@ -1252,8 +1231,7 @@ Prior art consulted for the gate design: - The OpenAPI Generator per-generator support matrix: a three-state classification rendered into a single discoverable table. - The `clap_mangen` + `clap_complete` + `trycmd` drift pattern: a - CI-enforced gate that asserts each generated artefact matches the - manifest. + CI-enforced gate that asserts each generated artefact matches the manifest. - The Strangler-Fig retirement pattern (Martin Fowler): an ADR - superseded-by link is the right home for a `divergent` row that - later becomes `consumes`. + superseded-by link is the right home for a `divergent` row that later becomes + `consumes`.