diff --git a/.agents/skills/plainweave-workflow/SKILL.md b/.agents/skills/plainweave-workflow/SKILL.md new file mode 100644 index 0000000..1c71237 --- /dev/null +++ b/.agents/skills/plainweave-workflow/SKILL.md @@ -0,0 +1,213 @@ +--- +name: plainweave-workflow +description: > + This skill should be used when the user asks "why does this code exist", + "what's our intent coverage", "find orphan code / requirements / goals", + "trace this requirement up to a goal or down to code", "bind this SEI / + entity to a requirement", "draft / approve / supersede a requirement", "show + the requirement dossier", "what's unverified or stale", "baseline the + requirements", or when working in a project that uses Plainweave for code-up + requirements traceability and intent. Provides the read/author/verify + workflow, the doctrine invariants (advisory-only, no-silent-clean, + enrich-only, zero minted SEIs), and the cross-member peer-facts surfaces. +--- + +# Plainweave Workflow + +Plainweave is the Weft federation's **requirements and verification authority** — +"the permission-for-code-to-exist member." It maintains a **code-up intent graph** +(`Loomweave SEI → requirement → goal`) in a local `.plainweave/` store and answers +one question for every public surface: *"why does this exist?"* It is **advisory**: +it surfaces facts and lets agents decide; it never emits an allow/block verdict. + +Prefer the MCP tools (`mcp__plainweave__*`) when available; fall back to the +`plainweave` CLI. Every read CLI takes `--json` to emit a versioned envelope. + +## The model — three altitudes, one graph + +``` +goal strategic intent ("ship trustworthy federation seams") + ↑ links +requirement a reviewable statement ("the producer must never silent-clean") + ↑ binds +code (SEI) a Loomweave-identified public entity (function/class/module) +``` + +A node with **no upward edge** is an **orphan** — a reviewable question, not an +error. Code that skips its bind is exactly what surfaces. Requirements are +trivially mintable; consolidation ("these three are the same") is **agent-driven** +off the corpus, never an automated verdict. + +## Core read workflow — the four intent primitives + +```bash +plainweave intent coverage # north-star: fraction of public surfaces that answer "why?" +plainweave intent orphans code # unjustified nodes at an altitude: code | requirement | goal +plainweave intent trace code # justification neighborhood up to goals / down to code +plainweave intent corpus # readable dump of requirements + their code/goal links +``` + +- **`coverage`** is the self-computed north-star. It is **honestly qualified + in-band**: `denominator_complete`, `present_plugins`, namespace scoping, bounded + evidence. Scope the denominator with `--exclude-namespace PREFIX` (default excludes + `scripts.`, `tests.`) and `--surface-class {cli-command,entry-point,exported-api,http-route}`. + It **never reports a silent clean** when the denominator is partial. +- **`orphans `** lists nodes with no upward justification edge. Triage, + don't panic — an orphan is "should this be bound, or is it genuinely standalone?" +- **`trace `** walks both directions from a node. +- **`corpus`** is the artifact a curator reads to spot duplicates before consolidating. + +## Authoring workflow — draft → criterion → approve → bind → ladder + +Requirements are versioned with a draft/approve lifecycle: + +```bash +plainweave req add --title "…" --statement "…" [--actor human:] # create a DRAFT +plainweave criterion add … # acceptance criteria on the active draft +plainweave req approve # promote the active draft +plainweave req supersede … | req deprecate … | req reject # later lifecycle moves +``` + +Bind code and ladder to strategy: + +```bash +plainweave bind sei [--entity-kind …] [--content-hash …] +plainweave goal add "…" # strategic intent node +plainweave goal link +``` + +Trace links (graph edges) have their own propose/review lifecycle — a proposed +link is a *suggestion*, accepted is *fact*, rejected never reads as coverage: + +```bash +plainweave trace propose --from-kind … --from-id … --relation … --to-kind … --to-id … [--confidence N] +plainweave trace accept | trace reject +plainweave trace list +``` + +> **Rejected ≠ absent coverage.** A reviewed-and-rejected binding does **not** +> read as requirement coverage; the view drops `rejected` trace links before +> computing `present`/`absent`. + +## Verification — methods, evidence, status + +```bash +plainweave verify method … # define how a requirement is verified +plainweave verify evidence … # record evidence against a method +plainweave status requirement # verification status for one requirement +plainweave status unverified # requirements with no verification +plainweave status stale # requirements whose evidence has gone stale +plainweave dossier # the full per-requirement dossier (advisory boundary) +``` + +Lock and compare requirement sets over time: + +```bash +plainweave baseline create … # lock the approved-requirement set +plainweave baseline diff # drift of current approved vs a baseline +plainweave baseline list | baseline show +``` + +## Doctrine — the hard invariants (do not violate) + +Plainweave's behaviour is contract-validated against these; violating them is +rejected by the shared validators, not merely discouraged. + +- **Advisory, never gates.** Zero release allow/block/approved/verdict tokens. + Coverage facts ride out at the git/CI boundary *through Legis* — Plainweave adds + no enforcement of its own. Any teeth are dialled up by the consumer via Legis cells. +- **No silent-clean.** A degraded or language-partial denominator, an unavailable + adapter, or an unresolved identity is **flagged in-band** — never collapsed to a + clean-empty result. `unavailable` ("I can't tell") is distinct from `absent` + ("definitively none") and from a clean `present`. +- **Enrich-only.** Plainweave absent → Loomweave, Legis, and the code are + unaffected; solo mode degrades to manual file/symbol refs. +- **Zero minted SEIs.** Sibling identity (`loomweave:eid:…`) and sibling fact + bodies are consumed **opaquely** — surfaced, never parsed or minted. +- **Local-only.** Reads compute against the local store / re-scanned scope; no live + peer calls in the producers (`local_only: true`, `live_peer_calls: false`). + +## Cross-member peer facts (advisory producers) + +Two local-first producers expose Plainweave facts to siblings, each with a frozen +`.v1` contract, explicit degraded state, and no silent-clean: + +```bash +plainweave wardline-peer-facts [--limit N] [--offset N] [--json] # weft.plainweave.wardline_peer_facts.v1 +plainweave requirements-enrichment ... [--json] # weft.plainweave.requirements_enrichment.v1 +``` + +- **`wardline-peer-facts`** surfaces Wardline findings (active / waived / baselined + / judged) computed against the actually re-scanned scope. Reads `.wardline/*-findings.jsonl` + only; an absent `.wardline/` reports `freshness: unavailable`, **never clean**. +- **`requirements-enrichment`** is the Plainweave-owned producer for **Warpline's** + reserved `enrichment.requirements` slot. `entity_ref` is a SEI or dotted locator + (batch many in one call). Per-entity `status`: `present` (≥1 alive binding) / + `absent` (entity known, none bound) / `unavailable` (identity unresolved or store + error — "I can't tell," **never** "no requirements"). This is consumed live by the + Warpline `consult_federation` requirements member — see `references/cross-member-seams.md`. + +## Health and diagnostics + +```bash +plainweave init # create a .plainweave/ store +plainweave doctor # federation-parity health: store/schema, Loomweave catalog binding, MCP surface +plainweave doctor --fix # idempotent in-place store repairs, then re-check +plainweave doctor --root # inspect a root other than cwd (remediation is root-aware) +``` + +`doctor` exits non-zero on unresolved problems; `--fix` is safe and idempotent. + +## Response shapes & exit codes + +Read surfaces emit a versioned envelope under `--json` (and the MCP tools return the +same shape): + +- **Success** → `{schema: "weft.plainweave..v1", ok: true, …, authority_boundary: {local_only: true, live_peer_calls: false, governance_verdicts: false}}`. +- **Failure** → `{ok: false, error: {code, …}}`. `code` ∈ `VALIDATION`, `NOT_FOUND`, + `CONFLICT`, `POLICY_REQUIRED`, `PEER_ABSENT`, `PEER_STALE`, `PEER_CONTRACT`, + `LOCKED`, `UNSUPPORTED`, `INTERNAL`. Branch on `code`, not message text. +- **Exit codes** (surface commands): `ok:true → 0`; `INTERNAL → 4`; any other + failure (e.g. `NOT_FOUND`, uninitialised store) → `2`. An uninitialised store is a + *genuine* producer failure (exit 2 / `ok:false`), never a faked clean — that would + break no-silent-clean. NB: a piped exit capture (`… | head; echo $?`) reports the + pipe tail's status, not plainweave's. + +## MCP parity (read-only mirror) + +The MCP server (`plainweave-mcp`) mirrors the reads — `mutates: false`, +`local_only: true`, no peer side effects: `plainweave_intent_coverage` / +`_orphans` / `_trace` / `_corpus`, `plainweave_requirement_get` / `_search` / +`_dossier`, `plainweave_verification_status_get` / `_list`, `plainweave_baseline_*`, +`plainweave_trace_link_list`, `plainweave_entity_intent_context_get`, +`plainweave_loomweave_catalog_list`, `plainweave_preflight_facts_get`, +`plainweave_project_context_get`, `plainweave_wardline_peer_facts_list`, +`plainweave_requirements_enrichment_get`. + +## Reference sheets + +Load these when you hit a specific challenge, not upfront: + +- **`references/intent-graph-patterns.md`** — coverage-denominator scoping, + orphan triage at each altitude, corpus-driven consolidation, baselines & drift, + the verification lifecycle. +- **`references/cross-member-seams.md`** — the peer-facts producers and their + freshness/degraded vocab, the Loomweave catalog/SEI seam, the Legis boundary, + and the opacity rules for sibling identity. + +## Quick decision guide + +| Situation | Action | +|-----------|--------| +| "Why does this code exist?" | `plainweave intent trace code ` | +| "What's our intent coverage?" | `plainweave intent coverage` (read the in-band qualifiers) | +| "What code is unjustified?" | `plainweave intent orphans code` | +| "These requirements look duplicated" | `plainweave intent corpus`, consolidate by hand | +| "Record a new requirement" | `req add` → `criterion add` → `req approve` | +| "Link this entity to a requirement" | `plainweave bind sei ` | +| "Is this requirement verified?" | `plainweave status requirement ` / `dossier ` | +| "What's gone stale?" | `plainweave status stale` | +| "Did anything drift from the baseline?" | `plainweave baseline diff ` | +| "What requirements bind this entity (for Warpline)?" | `plainweave requirements-enrichment --json` | +| "Is the store healthy?" | `plainweave doctor` (`--fix` to repair) | +| "Plainweave says `unavailable`" | It can't tell (identity/store) — **not** "none." Resolve identity or `init`. | diff --git a/.agents/skills/plainweave-workflow/references/cross-member-seams.md b/.agents/skills/plainweave-workflow/references/cross-member-seams.md new file mode 100644 index 0000000..7b6bec2 --- /dev/null +++ b/.agents/skills/plainweave-workflow/references/cross-member-seams.md @@ -0,0 +1,77 @@ +# Cross-member seams + +How Plainweave consumes from and produces facts for its Weft siblings. The unifying +rule: **enrich-only, opaque identity, no silent-clean, no verdicts.** + +## Loomweave — identity & catalog (Plainweave consumes) + +Plainweave never mints code identity. It consumes Loomweave **SEIs** +(`loomweave:eid:<32hex>`) opaquely: + +- `plainweave catalog record` registers a public entity discovered by a sibling + catalog tool. The SEI is stored verbatim; Plainweave does not parse or synthesise it. +- `plainweave bind sei ` ties a requirement to that + identity. `--content-hash` captures the entity's hash at attach time so a consumer + can later detect drift. +- The Loomweave catalog adapter has a **frozen degraded-state contract** + (`weft.plainweave.loomweave_catalog.v1`): when the adapter is `unavailable` it + returns an explicit unavailable envelope routed through the same oracle as live + output — it never returns a clean-empty page that would read as "no entities." + +If Loomweave is absent, Plainweave degrades to manual file/symbol references; the +graph still works, identity is just less precise. + +## Legis — the git/CI boundary (Plainweave's facts ride out) + +Plainweave emits **no enforcement of its own**. Coverage facts cross the git/CI +boundary *through Legis* ("this change adds N public entities bound to no +requirement"). Advisory by default; a repo that wants teeth dials it up through +Legis's policy cells. The preflight advisory cell +(`plainweave_preflight_facts_get`) supplies the facts; Legis decides whether they gate. + +## Warpline — requirements enrichment (Plainweave produces, Warpline consumes live) + +`plainweave requirements-enrichment ... --json` is the Plainweave-owned +producer (`weft.plainweave.requirements_enrichment.v1`) for Warpline's reserved +`enrichment.requirements` slot. Warpline's `consult_federation` wires it as the +**4th federation member** (alongside filigree/wardline/legis), capability-gated on +the verb being advertised. + +Per-entity `status` semantics — **mapped Plainweave-side; consumers pass through, never re-interpret**: + +| status | meaning | +|---|---| +| `present` | ≥1 alive requirement bound (the `requirements` array is non-empty) | +| `absent` | entity known, none bound — a definitive "none here" | +| `unavailable` | could not determine (identity unresolved / store error) — **"I can't tell," never "no requirements"** | + +The `unavailable` ≠ `absent` distinction is load-bearing and fault-injection-tested: +collapsing `unavailable` to `absent` (or to a clean-empty) would violate no-silent-clean +and let a "can't tell" masquerade as "verified none." Requirement item bodies and SEIs +are **opaque** to the consumer — surfaced, never parsed. + +Operational note: a consumer that execs bare `plainweave` from `PATH` runs the +installed uv-tool snapshot, which can lag the dev tree. If the verb isn't advertised, +the consumer reads the member `disabled` even though the code is correct — keep the +installed tool current (`uv tool install --force` / an editable install). + +## Wardline — findings as peer facts (Plainweave produces) + +`plainweave wardline-peer-facts --json` (`weft.plainweave.wardline_peer_facts.v1`) +surfaces Wardline findings (active / waived / baselined / judged; defect / +non-defect) plus resolved-or-unseen, computed against the **actually re-scanned +scope** — scan-identity manifest primary, with a path-set heuristic fallback that +flags itself in-band. An absent `.wardline/` reports `freshness: unavailable`, never +a clean empty page. + +## The freshness / status vocabulary + +Across the producers, degraded state is **explicit and distinct** — these are not +interchangeable, and none of them is a silent clean: + +- `present` — facts found and returned. +- `absent` — definitively none (the authority looked and there are none). +- `unavailable` — could not determine (peer/store/identity gap). The honest "I can't tell." +- `stale` — facts exist but the thing they describe has moved on. + +Surface the exact one; never round `unavailable`/`stale` down to `absent` or up to a clean result. diff --git a/.agents/skills/plainweave-workflow/references/intent-graph-patterns.md b/.agents/skills/plainweave-workflow/references/intent-graph-patterns.md new file mode 100644 index 0000000..8c95b68 --- /dev/null +++ b/.agents/skills/plainweave-workflow/references/intent-graph-patterns.md @@ -0,0 +1,85 @@ +# Intent-graph patterns + +Deep-dive patterns for working the `SEI → requirement → goal` graph. Load when you +hit one of these specific situations. + +## Reading coverage honestly + +`plainweave intent coverage` reports the fraction of public surfaces that answer +"why does this exist?" — but the **number is meaningless without its qualifiers**. +Always read these from the envelope before quoting a percentage: + +- **`denominator_complete`** — `false` means the surface inventory is partial + (e.g. a language plugin is absent). A high percentage over an incomplete + denominator is not a clean bill of health; say so. +- **`present_plugins`** — which language/surface extractors actually ran. Coverage + is only as complete as this list. +- **Namespace scope** — the denominator excludes `scripts.` and `tests.` by + default. Add `--exclude-namespace PREFIX` (repeatable) to scope out generated or + vendored namespaces; never silently widen it to flatter the number. +- **Surface class** — `--surface-class {cli-command,entry-point,exported-api,http-route}` + restricts the denominator. Use it to answer "how covered is the HTTP surface + specifically?" rather than the blended figure. +- **`--max-surfaces N`** caps the evidence lists, **not** the counts — counts are + never truncated, so a capped list still reports the true total. + +The honest move when coverage looks high: state the figure *and* the denominator +qualifiers in the same breath ("92% of exported-api surfaces, but +`denominator_complete: false` — the Rust plugin didn't run"). + +## Triaging orphans at each altitude + +`plainweave intent orphans {code,requirement,goal}` returns nodes with no upward +edge. The triage question differs by altitude: + +- **code orphans** — a public entity bound to no requirement. Ask: should this be + bound (then `bind sei`), or is it genuinely infrastructural/standalone? Don't + reflexively mint a shell requirement just to clear the orphan — that manufactures + vanity coverage. +- **requirement orphans** — an approved requirement laddered to no goal. Either + link it (`goal link`) or accept it as a leaf with a note. A pile of goal-less + requirements is a signal the goal layer is under-modelled. +- **goal orphans** — a goal with no requirements beneath it. Usually means intent + was declared but never decomposed into reviewable requirements. + +## Corpus-driven consolidation + +`plainweave intent corpus` is the curator's artifact. Consolidation is **agent- +driven, never automated**: read the corpus, spot "these three say the same thing," +then supersede the duplicates into one canonical requirement (`req supersede`) and +re-bind. Plainweave serves the substrate; it does not auto-merge. The optional +Loomweave semantic-similarity hint *assists* this read — it is explicitly not a +dedup engine and never acts on its own. + +## Requirement lifecycle + +Requirements are versioned with an explicit draft/approve flow: + +1. `req add` creates a **draft** (the "active draft" for the project). +2. `criterion add` attaches acceptance criteria to the active draft. +3. `req approve` promotes it. Approved requirements are what baselines lock. +4. `req supersede` creates a new version that replaces an approved one; + `req deprecate` retires one; `req reject` discards an unwanted draft. + +Trace links carry their own review state: `propose → accept | reject`. A **proposed** +link is a suggestion; only **accepted** links count as justification; **rejected** +links are dropped before coverage is computed (a rejected binding never reads as +`present`). + +## Baselines & drift + +`plainweave baseline create` locks the current approved-requirement set under a +name. Later, `plainweave baseline diff ` shows how the live approved set has +drifted (added / removed / superseded). Use a baseline at a release boundary or a +review milestone, then diff against it to answer "what requirements moved since we +agreed this set?" Baselines are immutable once created — `show`/`list` read them. + +## Verification, not validation + +`verify method` declares *how* a requirement is checked; `verify evidence` records a +concrete result against a method; `status` reports the rollup +(`requirement` / `unverified` / `stale`). Evidence goes **stale** when the thing it +attested to has moved on — `status stale` surfaces exactly those, so re-verification +is targeted rather than wholesale. The `dossier` is the full advisory picture for one +requirement (statement, criteria, bindings, verification) — it reports, it does not +gate. diff --git a/.claude/skills/plainweave-workflow/SKILL.md b/.claude/skills/plainweave-workflow/SKILL.md new file mode 100644 index 0000000..1c71237 --- /dev/null +++ b/.claude/skills/plainweave-workflow/SKILL.md @@ -0,0 +1,213 @@ +--- +name: plainweave-workflow +description: > + This skill should be used when the user asks "why does this code exist", + "what's our intent coverage", "find orphan code / requirements / goals", + "trace this requirement up to a goal or down to code", "bind this SEI / + entity to a requirement", "draft / approve / supersede a requirement", "show + the requirement dossier", "what's unverified or stale", "baseline the + requirements", or when working in a project that uses Plainweave for code-up + requirements traceability and intent. Provides the read/author/verify + workflow, the doctrine invariants (advisory-only, no-silent-clean, + enrich-only, zero minted SEIs), and the cross-member peer-facts surfaces. +--- + +# Plainweave Workflow + +Plainweave is the Weft federation's **requirements and verification authority** — +"the permission-for-code-to-exist member." It maintains a **code-up intent graph** +(`Loomweave SEI → requirement → goal`) in a local `.plainweave/` store and answers +one question for every public surface: *"why does this exist?"* It is **advisory**: +it surfaces facts and lets agents decide; it never emits an allow/block verdict. + +Prefer the MCP tools (`mcp__plainweave__*`) when available; fall back to the +`plainweave` CLI. Every read CLI takes `--json` to emit a versioned envelope. + +## The model — three altitudes, one graph + +``` +goal strategic intent ("ship trustworthy federation seams") + ↑ links +requirement a reviewable statement ("the producer must never silent-clean") + ↑ binds +code (SEI) a Loomweave-identified public entity (function/class/module) +``` + +A node with **no upward edge** is an **orphan** — a reviewable question, not an +error. Code that skips its bind is exactly what surfaces. Requirements are +trivially mintable; consolidation ("these three are the same") is **agent-driven** +off the corpus, never an automated verdict. + +## Core read workflow — the four intent primitives + +```bash +plainweave intent coverage # north-star: fraction of public surfaces that answer "why?" +plainweave intent orphans code # unjustified nodes at an altitude: code | requirement | goal +plainweave intent trace code # justification neighborhood up to goals / down to code +plainweave intent corpus # readable dump of requirements + their code/goal links +``` + +- **`coverage`** is the self-computed north-star. It is **honestly qualified + in-band**: `denominator_complete`, `present_plugins`, namespace scoping, bounded + evidence. Scope the denominator with `--exclude-namespace PREFIX` (default excludes + `scripts.`, `tests.`) and `--surface-class {cli-command,entry-point,exported-api,http-route}`. + It **never reports a silent clean** when the denominator is partial. +- **`orphans `** lists nodes with no upward justification edge. Triage, + don't panic — an orphan is "should this be bound, or is it genuinely standalone?" +- **`trace `** walks both directions from a node. +- **`corpus`** is the artifact a curator reads to spot duplicates before consolidating. + +## Authoring workflow — draft → criterion → approve → bind → ladder + +Requirements are versioned with a draft/approve lifecycle: + +```bash +plainweave req add --title "…" --statement "…" [--actor human:] # create a DRAFT +plainweave criterion add … # acceptance criteria on the active draft +plainweave req approve # promote the active draft +plainweave req supersede … | req deprecate … | req reject # later lifecycle moves +``` + +Bind code and ladder to strategy: + +```bash +plainweave bind sei [--entity-kind …] [--content-hash …] +plainweave goal add "…" # strategic intent node +plainweave goal link +``` + +Trace links (graph edges) have their own propose/review lifecycle — a proposed +link is a *suggestion*, accepted is *fact*, rejected never reads as coverage: + +```bash +plainweave trace propose --from-kind … --from-id … --relation … --to-kind … --to-id … [--confidence N] +plainweave trace accept | trace reject +plainweave trace list +``` + +> **Rejected ≠ absent coverage.** A reviewed-and-rejected binding does **not** +> read as requirement coverage; the view drops `rejected` trace links before +> computing `present`/`absent`. + +## Verification — methods, evidence, status + +```bash +plainweave verify method … # define how a requirement is verified +plainweave verify evidence … # record evidence against a method +plainweave status requirement # verification status for one requirement +plainweave status unverified # requirements with no verification +plainweave status stale # requirements whose evidence has gone stale +plainweave dossier # the full per-requirement dossier (advisory boundary) +``` + +Lock and compare requirement sets over time: + +```bash +plainweave baseline create … # lock the approved-requirement set +plainweave baseline diff # drift of current approved vs a baseline +plainweave baseline list | baseline show +``` + +## Doctrine — the hard invariants (do not violate) + +Plainweave's behaviour is contract-validated against these; violating them is +rejected by the shared validators, not merely discouraged. + +- **Advisory, never gates.** Zero release allow/block/approved/verdict tokens. + Coverage facts ride out at the git/CI boundary *through Legis* — Plainweave adds + no enforcement of its own. Any teeth are dialled up by the consumer via Legis cells. +- **No silent-clean.** A degraded or language-partial denominator, an unavailable + adapter, or an unresolved identity is **flagged in-band** — never collapsed to a + clean-empty result. `unavailable` ("I can't tell") is distinct from `absent` + ("definitively none") and from a clean `present`. +- **Enrich-only.** Plainweave absent → Loomweave, Legis, and the code are + unaffected; solo mode degrades to manual file/symbol refs. +- **Zero minted SEIs.** Sibling identity (`loomweave:eid:…`) and sibling fact + bodies are consumed **opaquely** — surfaced, never parsed or minted. +- **Local-only.** Reads compute against the local store / re-scanned scope; no live + peer calls in the producers (`local_only: true`, `live_peer_calls: false`). + +## Cross-member peer facts (advisory producers) + +Two local-first producers expose Plainweave facts to siblings, each with a frozen +`.v1` contract, explicit degraded state, and no silent-clean: + +```bash +plainweave wardline-peer-facts [--limit N] [--offset N] [--json] # weft.plainweave.wardline_peer_facts.v1 +plainweave requirements-enrichment ... [--json] # weft.plainweave.requirements_enrichment.v1 +``` + +- **`wardline-peer-facts`** surfaces Wardline findings (active / waived / baselined + / judged) computed against the actually re-scanned scope. Reads `.wardline/*-findings.jsonl` + only; an absent `.wardline/` reports `freshness: unavailable`, **never clean**. +- **`requirements-enrichment`** is the Plainweave-owned producer for **Warpline's** + reserved `enrichment.requirements` slot. `entity_ref` is a SEI or dotted locator + (batch many in one call). Per-entity `status`: `present` (≥1 alive binding) / + `absent` (entity known, none bound) / `unavailable` (identity unresolved or store + error — "I can't tell," **never** "no requirements"). This is consumed live by the + Warpline `consult_federation` requirements member — see `references/cross-member-seams.md`. + +## Health and diagnostics + +```bash +plainweave init # create a .plainweave/ store +plainweave doctor # federation-parity health: store/schema, Loomweave catalog binding, MCP surface +plainweave doctor --fix # idempotent in-place store repairs, then re-check +plainweave doctor --root # inspect a root other than cwd (remediation is root-aware) +``` + +`doctor` exits non-zero on unresolved problems; `--fix` is safe and idempotent. + +## Response shapes & exit codes + +Read surfaces emit a versioned envelope under `--json` (and the MCP tools return the +same shape): + +- **Success** → `{schema: "weft.plainweave..v1", ok: true, …, authority_boundary: {local_only: true, live_peer_calls: false, governance_verdicts: false}}`. +- **Failure** → `{ok: false, error: {code, …}}`. `code` ∈ `VALIDATION`, `NOT_FOUND`, + `CONFLICT`, `POLICY_REQUIRED`, `PEER_ABSENT`, `PEER_STALE`, `PEER_CONTRACT`, + `LOCKED`, `UNSUPPORTED`, `INTERNAL`. Branch on `code`, not message text. +- **Exit codes** (surface commands): `ok:true → 0`; `INTERNAL → 4`; any other + failure (e.g. `NOT_FOUND`, uninitialised store) → `2`. An uninitialised store is a + *genuine* producer failure (exit 2 / `ok:false`), never a faked clean — that would + break no-silent-clean. NB: a piped exit capture (`… | head; echo $?`) reports the + pipe tail's status, not plainweave's. + +## MCP parity (read-only mirror) + +The MCP server (`plainweave-mcp`) mirrors the reads — `mutates: false`, +`local_only: true`, no peer side effects: `plainweave_intent_coverage` / +`_orphans` / `_trace` / `_corpus`, `plainweave_requirement_get` / `_search` / +`_dossier`, `plainweave_verification_status_get` / `_list`, `plainweave_baseline_*`, +`plainweave_trace_link_list`, `plainweave_entity_intent_context_get`, +`plainweave_loomweave_catalog_list`, `plainweave_preflight_facts_get`, +`plainweave_project_context_get`, `plainweave_wardline_peer_facts_list`, +`plainweave_requirements_enrichment_get`. + +## Reference sheets + +Load these when you hit a specific challenge, not upfront: + +- **`references/intent-graph-patterns.md`** — coverage-denominator scoping, + orphan triage at each altitude, corpus-driven consolidation, baselines & drift, + the verification lifecycle. +- **`references/cross-member-seams.md`** — the peer-facts producers and their + freshness/degraded vocab, the Loomweave catalog/SEI seam, the Legis boundary, + and the opacity rules for sibling identity. + +## Quick decision guide + +| Situation | Action | +|-----------|--------| +| "Why does this code exist?" | `plainweave intent trace code ` | +| "What's our intent coverage?" | `plainweave intent coverage` (read the in-band qualifiers) | +| "What code is unjustified?" | `plainweave intent orphans code` | +| "These requirements look duplicated" | `plainweave intent corpus`, consolidate by hand | +| "Record a new requirement" | `req add` → `criterion add` → `req approve` | +| "Link this entity to a requirement" | `plainweave bind sei ` | +| "Is this requirement verified?" | `plainweave status requirement ` / `dossier ` | +| "What's gone stale?" | `plainweave status stale` | +| "Did anything drift from the baseline?" | `plainweave baseline diff ` | +| "What requirements bind this entity (for Warpline)?" | `plainweave requirements-enrichment --json` | +| "Is the store healthy?" | `plainweave doctor` (`--fix` to repair) | +| "Plainweave says `unavailable`" | It can't tell (identity/store) — **not** "none." Resolve identity or `init`. | diff --git a/.claude/skills/plainweave-workflow/references/cross-member-seams.md b/.claude/skills/plainweave-workflow/references/cross-member-seams.md new file mode 100644 index 0000000..7b6bec2 --- /dev/null +++ b/.claude/skills/plainweave-workflow/references/cross-member-seams.md @@ -0,0 +1,77 @@ +# Cross-member seams + +How Plainweave consumes from and produces facts for its Weft siblings. The unifying +rule: **enrich-only, opaque identity, no silent-clean, no verdicts.** + +## Loomweave — identity & catalog (Plainweave consumes) + +Plainweave never mints code identity. It consumes Loomweave **SEIs** +(`loomweave:eid:<32hex>`) opaquely: + +- `plainweave catalog record` registers a public entity discovered by a sibling + catalog tool. The SEI is stored verbatim; Plainweave does not parse or synthesise it. +- `plainweave bind sei ` ties a requirement to that + identity. `--content-hash` captures the entity's hash at attach time so a consumer + can later detect drift. +- The Loomweave catalog adapter has a **frozen degraded-state contract** + (`weft.plainweave.loomweave_catalog.v1`): when the adapter is `unavailable` it + returns an explicit unavailable envelope routed through the same oracle as live + output — it never returns a clean-empty page that would read as "no entities." + +If Loomweave is absent, Plainweave degrades to manual file/symbol references; the +graph still works, identity is just less precise. + +## Legis — the git/CI boundary (Plainweave's facts ride out) + +Plainweave emits **no enforcement of its own**. Coverage facts cross the git/CI +boundary *through Legis* ("this change adds N public entities bound to no +requirement"). Advisory by default; a repo that wants teeth dials it up through +Legis's policy cells. The preflight advisory cell +(`plainweave_preflight_facts_get`) supplies the facts; Legis decides whether they gate. + +## Warpline — requirements enrichment (Plainweave produces, Warpline consumes live) + +`plainweave requirements-enrichment ... --json` is the Plainweave-owned +producer (`weft.plainweave.requirements_enrichment.v1`) for Warpline's reserved +`enrichment.requirements` slot. Warpline's `consult_federation` wires it as the +**4th federation member** (alongside filigree/wardline/legis), capability-gated on +the verb being advertised. + +Per-entity `status` semantics — **mapped Plainweave-side; consumers pass through, never re-interpret**: + +| status | meaning | +|---|---| +| `present` | ≥1 alive requirement bound (the `requirements` array is non-empty) | +| `absent` | entity known, none bound — a definitive "none here" | +| `unavailable` | could not determine (identity unresolved / store error) — **"I can't tell," never "no requirements"** | + +The `unavailable` ≠ `absent` distinction is load-bearing and fault-injection-tested: +collapsing `unavailable` to `absent` (or to a clean-empty) would violate no-silent-clean +and let a "can't tell" masquerade as "verified none." Requirement item bodies and SEIs +are **opaque** to the consumer — surfaced, never parsed. + +Operational note: a consumer that execs bare `plainweave` from `PATH` runs the +installed uv-tool snapshot, which can lag the dev tree. If the verb isn't advertised, +the consumer reads the member `disabled` even though the code is correct — keep the +installed tool current (`uv tool install --force` / an editable install). + +## Wardline — findings as peer facts (Plainweave produces) + +`plainweave wardline-peer-facts --json` (`weft.plainweave.wardline_peer_facts.v1`) +surfaces Wardline findings (active / waived / baselined / judged; defect / +non-defect) plus resolved-or-unseen, computed against the **actually re-scanned +scope** — scan-identity manifest primary, with a path-set heuristic fallback that +flags itself in-band. An absent `.wardline/` reports `freshness: unavailable`, never +a clean empty page. + +## The freshness / status vocabulary + +Across the producers, degraded state is **explicit and distinct** — these are not +interchangeable, and none of them is a silent clean: + +- `present` — facts found and returned. +- `absent` — definitively none (the authority looked and there are none). +- `unavailable` — could not determine (peer/store/identity gap). The honest "I can't tell." +- `stale` — facts exist but the thing they describe has moved on. + +Surface the exact one; never round `unavailable`/`stale` down to `absent` or up to a clean result. diff --git a/.claude/skills/plainweave-workflow/references/intent-graph-patterns.md b/.claude/skills/plainweave-workflow/references/intent-graph-patterns.md new file mode 100644 index 0000000..8c95b68 --- /dev/null +++ b/.claude/skills/plainweave-workflow/references/intent-graph-patterns.md @@ -0,0 +1,85 @@ +# Intent-graph patterns + +Deep-dive patterns for working the `SEI → requirement → goal` graph. Load when you +hit one of these specific situations. + +## Reading coverage honestly + +`plainweave intent coverage` reports the fraction of public surfaces that answer +"why does this exist?" — but the **number is meaningless without its qualifiers**. +Always read these from the envelope before quoting a percentage: + +- **`denominator_complete`** — `false` means the surface inventory is partial + (e.g. a language plugin is absent). A high percentage over an incomplete + denominator is not a clean bill of health; say so. +- **`present_plugins`** — which language/surface extractors actually ran. Coverage + is only as complete as this list. +- **Namespace scope** — the denominator excludes `scripts.` and `tests.` by + default. Add `--exclude-namespace PREFIX` (repeatable) to scope out generated or + vendored namespaces; never silently widen it to flatter the number. +- **Surface class** — `--surface-class {cli-command,entry-point,exported-api,http-route}` + restricts the denominator. Use it to answer "how covered is the HTTP surface + specifically?" rather than the blended figure. +- **`--max-surfaces N`** caps the evidence lists, **not** the counts — counts are + never truncated, so a capped list still reports the true total. + +The honest move when coverage looks high: state the figure *and* the denominator +qualifiers in the same breath ("92% of exported-api surfaces, but +`denominator_complete: false` — the Rust plugin didn't run"). + +## Triaging orphans at each altitude + +`plainweave intent orphans {code,requirement,goal}` returns nodes with no upward +edge. The triage question differs by altitude: + +- **code orphans** — a public entity bound to no requirement. Ask: should this be + bound (then `bind sei`), or is it genuinely infrastructural/standalone? Don't + reflexively mint a shell requirement just to clear the orphan — that manufactures + vanity coverage. +- **requirement orphans** — an approved requirement laddered to no goal. Either + link it (`goal link`) or accept it as a leaf with a note. A pile of goal-less + requirements is a signal the goal layer is under-modelled. +- **goal orphans** — a goal with no requirements beneath it. Usually means intent + was declared but never decomposed into reviewable requirements. + +## Corpus-driven consolidation + +`plainweave intent corpus` is the curator's artifact. Consolidation is **agent- +driven, never automated**: read the corpus, spot "these three say the same thing," +then supersede the duplicates into one canonical requirement (`req supersede`) and +re-bind. Plainweave serves the substrate; it does not auto-merge. The optional +Loomweave semantic-similarity hint *assists* this read — it is explicitly not a +dedup engine and never acts on its own. + +## Requirement lifecycle + +Requirements are versioned with an explicit draft/approve flow: + +1. `req add` creates a **draft** (the "active draft" for the project). +2. `criterion add` attaches acceptance criteria to the active draft. +3. `req approve` promotes it. Approved requirements are what baselines lock. +4. `req supersede` creates a new version that replaces an approved one; + `req deprecate` retires one; `req reject` discards an unwanted draft. + +Trace links carry their own review state: `propose → accept | reject`. A **proposed** +link is a suggestion; only **accepted** links count as justification; **rejected** +links are dropped before coverage is computed (a rejected binding never reads as +`present`). + +## Baselines & drift + +`plainweave baseline create` locks the current approved-requirement set under a +name. Later, `plainweave baseline diff ` shows how the live approved set has +drifted (added / removed / superseded). Use a baseline at a release boundary or a +review milestone, then diff against it to answer "what requirements moved since we +agreed this set?" Baselines are immutable once created — `show`/`list` read them. + +## Verification, not validation + +`verify method` declares *how* a requirement is checked; `verify evidence` records a +concrete result against a method; `status` reports the rollup +(`requirement` / `unverified` / `stale`). Evidence goes **stale** when the thing it +attested to has moved on — `status stale` surfaces exactly those, so re-verification +is targeted rather than wholesale. The `dossier` is the full advisory picture for one +requirement (statement, criteria, bindings, verification) — it reports, it does not +gate. diff --git a/CHANGELOG.md b/CHANGELOG.md index f81e9ec..cd3ad3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,13 @@ All notable changes to Plainweave are documented here. The format follows ## [Unreleased] -Closes the CLI/MCP parity gap for the 1.1 peer-facts producers (they shipped MCP-only). -Additive and backward-compatible; the new surfaces are advisory and verdict-free like the -rest of Plainweave. +## [1.2.0] — 2026-06-30 + +Closes the CLI/MCP parity gap for the 1.1 peer-facts producers (they shipped MCP-only), +delivers the operator web UX + accessibility overhaul, hardens three federation seams, +ships the `plainweave-workflow` agent skill, and fixes the wheel build. Additive and +backward-compatible; the new surfaces are advisory and verdict-free like the rest of +Plainweave. ### Added - **`plainweave wardline-peer-facts [--json] [--limit N] [--offset N]`** — surfaces the @@ -19,6 +23,29 @@ rest of Plainweave. `weft.plainweave.requirements_enrichment.v1` envelope over the CLI (previously MCP-only). Accepts a SEI or a dotted locator; preserves the no-silent-clean contract (`present`/`absent`/`unavailable` — an identity gap is `unavailable`, never `absent`). + Now consumed live by Warpline as the 4th `consult_federation` member (Warpline PDR-0008; + validated end-to-end, Plainweave PDR-017). +- **Frozen degraded-state contract for `weft.plainweave.loomweave_catalog.v1`** — the + Loomweave catalog producer's `unavailable`-adapter envelope is now pinned by a structural + validator + golden routed through the same oracle as live output (no-silent-clean: an + unavailable adapter never returns a clean-empty page). Seam-hardening, no behavior change + (PDR-018, production blocker #3). +- **Contract test for the Filigree seam** — pins `open_linked_work` as reserved-but-never- + emitted by the local-only producer (absence is the in-band `linked_work_facts_unavailable` + warning), the `filigree_issue` trace opacity + canonical relations, and the dossier's + advisory boundary (PDR-018, production blocker #5). +- **`plainweave-workflow` skill pack** — a federation-standard agent skill (`SKILL.md` + + reference sheets) authored in-package (`src/plainweave/skills/`), shipped as package data + and dogfooded into the repo's `.claude/` + `.agents/` skill trees. Documents the + read/author/verify workflow, the doctrine invariants (advisory-only, no-silent-clean, + enrich-only, zero minted SEIs), and the cross-member peer-facts seams. + +### Changed +- **Operator web UI — UX + accessibility overhaul (PDR-016)** — adopted the site-kit design + tokens and fixed contrast, focus-ring, and target-size; restored readable visited primary + anchors (a global `a:visited` rule had out-specified `.btn--primary`, dropping the link to + a ~1.7:1 WCAG AA failure once visited); and now dismiss toasts on every page (moved to + `base.html` so script-less pages such as the requirement dossier are covered too). ### Fixed - `requirements_enrichment` now drops `rejected` trace links before building the view — a @@ -28,6 +55,16 @@ rest of Plainweave. - `plainweave doctor` Wardline-findings remediation is now root-aware (`cd && wardline scan .` when the inspected root is not the cwd). _(Folded in from a sibling product's contract work.)_ +- ADR-006 now documents that the preflight producer emits 8 of its 11 fact kinds and why the + other three are superseded (dedicated wardline producer) or sibling-gated (Filigree); added + the previously-missing behavioral coverage for the `orphaned_entity_link` fact (PDR-018, + production blocker #4). +- **Wheel build (PDR-017)** — removed a redundant + `[tool.hatch.build.targets.wheel.force-include]` block that re-mapped `web/templates` + + `web/static` (already vendored by the `src/plainweave` src-layout), colliding on + `web/static/.gitkeep` and breaking `uv build`. The wheel now builds with each web asset + shipped exactly once, so a clean `uv tool install` again advertises every CLI verb — this + was darkening Warpline's requirements federation member in practice. ## [1.1.0] — 2026-06-27 @@ -101,5 +138,6 @@ coverage *completeness* remains a documented roadmap item, not a 1.0 gate.) - Zero Plainweave-minted SEIs; sibling SEIs consumed opaquely. - No silent-clean — a degraded or language-partial denominator is flagged in-band. +[1.2.0]: https://github.com/foundryside-dev/plainweave/releases/tag/v1.2.0 [1.1.0]: https://github.com/foundryside-dev/plainweave/releases/tag/v1.1.0 [1.0.0]: https://github.com/foundryside-dev/plainweave/releases/tag/v1.0.0 diff --git a/README.md b/README.md index ea73322..da1b4bd 100644 --- a/README.md +++ b/README.md @@ -139,10 +139,15 @@ Plainweave requires Python ≥ 3.12. Installing exposes two console commands: - `plainweave` — the CLI: `init`, `intent` (`coverage` / `orphans` / `trace` / `corpus`), `req`, `goal`, `bind`, `catalog`, `criterion`, `verify`, `status`, - `dossier`, `baseline`, `actor`, and `doctor`. + `dossier`, `baseline`, `actor`, `doctor`, the cross-member peer-facts surfaces + (`wardline-peer-facts`, `requirements-enrichment`), and the operator `web` UI. - `plainweave-mcp` — the read-only MCP server that mirrors the intent reads for agents (`mutates:false`, `local_only:true`). +Plainweave also ships an agent skill — `plainweave-workflow` (under +`src/plainweave/skills/`) — the federation-standard `SKILL.md` + reference sheets +that teach an agent the read/author/verify workflow and the doctrine invariants. + Quick start: ```bash diff --git a/docs/architecture/decisions/ADR-006-legis-preflight-fact-envelope.md b/docs/architecture/decisions/ADR-006-legis-preflight-fact-envelope.md index d6e2ddf..d3ad4eb 100644 --- a/docs/architecture/decisions/ADR-006-legis-preflight-fact-envelope.md +++ b/docs/architecture/decisions/ADR-006-legis-preflight-fact-envelope.md @@ -84,6 +84,26 @@ orphaned_entity_link untraced_change ``` +### Emission status (annotated 2026-06-29, PDR-018 — seam hardening) + +The local-only producer emits **8 of the 11** kinds above. Three are deliberately +**not emitted** by this producer, and their absence is reported in-band as +`info`/`freshness: unavailable` warnings (`linked_work_facts_unavailable`, +`finding_facts_unavailable`) — never as an empty-but-ok fact list (no-silent-clean): + +- `active_finding_linked`, `waived_finding_linked` — **superseded** by the dedicated + `weft.plainweave.wardline_peer_facts.v1` producer (PDR-014), which surfaces the same + local `.wardline/*-findings.jsonl` data. Wiring them into the preflight envelope as + well is an owner-gated ADR fork, intentionally **not** taken (PDR-018). +- `open_linked_work` — **sibling-gated**: no in-grant local Filigree source exists + (`.filigree/` is a Filigree-owned DB; the live `entity_association` read would break + `authority_boundary.live_peer_calls=false`). Handed off as + `docs/handoffs/2026-06-29-filigree-linked-work-facts.md`. + +Reversal trigger: if a boundary-clean local Filigree facts artifact (mirroring +`.wardline/*.jsonl`) lands, `open_linked_work` becomes emittable in-grant and this +fork reopens. + ## Related Decisions - ADR-001: Plainweave authority boundary. diff --git a/docs/handoffs/2026-06-28-warpline-requirements-enrichment-consumer-impl.md b/docs/handoffs/2026-06-28-warpline-requirements-enrichment-consumer-impl.md new file mode 100644 index 0000000..52c5437 --- /dev/null +++ b/docs/handoffs/2026-06-28-warpline-requirements-enrichment-consumer-impl.md @@ -0,0 +1,162 @@ +# Peer prompt — Warpline: wire the Plainweave requirements-enrichment consumer + +**To:** the Warpline maintainer / implementing agent. +**From:** Plainweave (owner-gated sibling handoff). +**Date:** 2026-06-28 (implementation-grounded refresh of `2026-06-27-warpline-requirements-enrichment-consumer.md`; that doc states the contract, this one states *how to build it against the warpline you have today*). +**Precondition (soft, see end):** the item schema is proposed but not yet ratified in the +interface lock (sibling prompt `2026-06-27-warpline-interface-lock-item-schema.md`). Because +warpline consumes peer facts **opaquely**, you can build this in parallel and land the +byte-pinned contract test when ratification completes — see "On the schema dependency". + +## Goal (one line) + +`requirements` is the **one federation member warpline never wired**. Wire Plainweave as a +4th member of the `consult_federation` hard seam — structurally identical to how `risk` +(wardline) and `governance` (legis) already work — so `warpline_reverify_worklist_get(..., +include_federation=True)` reports honest per-entity requirement facts and a real +`enrichment.requirements` scalar + reason instead of the reserved `disabled` default. + +## Where warpline is today (anchors, all confirmed) + +- `src/warpline/envelope.py:18` — `requirements` is already in the **frozen** `ENRICHMENT_VOCAB` + (`present|absent|unavailable`); `:29` defaults it to `unavailable`; `:78` injects + `requirements_reason()` as the default reason triple. +- `src/warpline/_enrichment.py:82-103` — `requirements_reason()` is a static **reserved / + `disabled`** triple ("no requirements-trace transport is wired in warpline yet"). This is the + honest placeholder you are replacing. `sei_reason()` at `:106-146` is the **template** to copy: + a status→reason-triple mapper (`clean` / `unresolved_input` / `unreachable`). +- `src/warpline/federation.py` — the `consult_federation` hard seam (PDR-0023): + `FEDERATION_MEMBERS = ("filigree","wardline","legis")` (`:44`); per-member `_consult_*` + functions return `(by_locator, weft_reason)`; `LegisGovernanceClient` (`:129-202`) + + `_consult_legis` (`:285-318`) are the **exact template** — a capability-gated CLI client + (`.available()` `--help` probe at `:148-168`), `clean` on facts, `unreachable` on raise, + `disabled` when no client. `federation_transport_blockers` (`:373-418`) declares the missing + member to the strike. +- `src/warpline/reverify.py:18` — `_empty_enrichment()` already includes `"requirements": []` + per item; today only `work` is filled inline. +- `src/warpline/commands.py:972-1078` — the reverify assembly: `consult_federation(...)` → + `_merge_federation_enrichment(items, federation)` (merges per-entity facts onto + `item.enrichment.{risk,governance}` and returns the scalars) → `_member_scalar(federation, ...)` + → `build_envelope(enrichment=enrichment_state(... risk=risk_state, governance=gov_state), + enrichment_reasons={...})`. **`requirements` is simply absent from every one of these calls.** +- `src/warpline/cop.py:366-376` composes the same consults — mirror the requirements member there too. + +## What Plainweave gives you (the producer — shipped, stable) + +- **CLI (use this — matches your `wardline dossier` / `legis governance-read` transport):** + `plainweave requirements-enrichment ... --json` + (`entity_ref` is `nargs="+"`; pass a SEI `loomweave:eid:<32hex>` or a dotted locator; batch + refs in one call). Defined at `plainweave/src/plainweave/cli_commands.py:137-143,1113-1121`. + There is an MCP equivalent (`plainweave_requirements_enrichment_get(entity_refs)`) but the CLI + is the right fit for warpline's subprocess pattern. +- **Envelope:** `weft.plainweave.requirements_enrichment.v1`. +- **Frozen golden (implement-to; do NOT edit it):** + `plainweave/tests/fixtures/contracts/warpline/requirements-enrichment.json`. Shape: + ```json + { + "schema": "weft.plainweave.requirements_enrichment.v1", + "items": [ + {"entity_ref": "...", "status": "present|absent|unavailable", + "requirements": [ {requirement_id, stable_id, version, type, criticality, + binding:{relation, actor_kind, freshness}} ], + "reason": "...|null", "freshness": "current|stale|orphaned|unknown|unavailable"} + ], + "summary": {"present": N, "absent": N, "unavailable": N}, + "authority_boundary": {"local_only": true, "live_peer_calls": false, + "governance_verdicts": false, "requirements_owner": "plainweave"} + } + ``` +- **Per-entity status (already mapped Plainweave-side — pass through, do NOT re-interpret):** + `present` = ≥1 alive requirement bound (`requirements` non-empty); `absent` = entity known, + none bound (definitive "none here"); `unavailable` = could not determine (identity unresolved / + store error) — **"I can't tell", never "no requirements"**. + +## Build steps (mirror the legis member end to end) + +1. **`federation.py` — add the member.** + - `RequirementsClient` Protocol: `requirements_for_refs(self, refs: list[str]) -> dict[str, dict]` + returning `{entity_ref: item}` from the producer's `items`. + - `PlainweaveRequirementsClient` over the CLI (copy `LegisGovernanceClient`): run + `plainweave requirements-enrichment --json` with `cwd=repo`, parse the envelope, + index `items` by `entity_ref`. Add a `.available()` `--help` probe for `requirements-enrichment`. + Batch all refs in one subprocess call (the CLI takes `nargs="+"`) — cheaper than per-SEI. + - `_consult_plainweave(items, requirements_client)` returning `(by_locator, weft_reason)`: + `disabled` (no client / verb absent) → `unreachable` (subprocess/JSON raised) → + else map each entity's producer `status`: `present` puts the item's `requirements` array in + `by_locator[locator]`; `absent`/`unavailable` contribute no facts but **must still drive the + scalar** (see step 3). Use `_seis()`/locator keying exactly as the other consults do. + - Add `"plainweave"` to `FEDERATION_MEMBERS`; thread `requirements_client` through + `consult_federation(...)` (params, `members["plainweave"]`, the per-entity `requirements` + slot in `entities`), and add a `federation_transport_blockers` entry. + +2. **`_enrichment.py` — replace the reserved reason with a real one.** + Add `requirements_reason_for(status: str)` mirroring `sei_reason()`: + `present`→`clean`; `absent`→`unresolved_input` (entity known, no binding — cause/fix); + `unavailable`→`unreachable` (could not determine — cause/fix). Keep the static + `requirements_reason()` as the `disabled` fallback for when the member isn't wired. + +3. **`commands.py` (and `cop.py`) — light it up.** + - Pass `requirements_client` into `consult_federation`. + - Extend `_merge_federation_enrichment` to also fill `item.enrichment["requirements"]` from the + federation `entities` (same merge it does for `risk`/`governance`) and return a `req_state` + scalar via `_member_scalar(federation, "plainweave")`. + - Add `requirements=req_state` to the `enrichment_state(...)` call (`commands.py:1073-1078`) and + `"requirements": requirements_reason_for(req_state)` to `enrichment_reasons` so the envelope's + reserved default is overridden. + - Capability-gate the client in the MCP wiring (`mcp.py`) exactly like the legis verb gate. + +## Status / scalar mapping (envelope-level, mirror `_member_scalar`) + +| Plainweave result for the worklist | `enrichment.requirements` | reason_class | +|---|---|---| +| ≥1 entity `present` (facts returned) | `present` | `clean` | +| all known entities `absent`, none present | `absent` | `unresolved_input` | +| producer unreachable / errored / verb absent | `unavailable` | `unreachable` (down) / `disabled` (no verb) | + +## Hard invariants (do not violate) + +- **Advisory, never gates.** `requirements` (incl. `criticality`) is an ordering/context signal + exactly like `risk`. Never produce or imply allow/block/clean/approved/verdict tokens — + warpline's reason/envelope validators reject them; so does Plainweave's. +- **No-silent-clean.** `absent` and `unavailable` stay explicit and distinct; `unavailable` is + never collapsed to `absent` or to an empty-clean — assert this with a fault-injection test. +- **Opaque identity & items.** Treat `loomweave:eid:...` / `plainweave:req:...` and the requirement + item bodies as opaque (pass through, like wardline findings / legis records); never mint or parse. +- **Local-only seam.** The producer is `local_only:true, live_peer_calls:false`; your one CLI hop + is the only call. Keep warpline's `meta.local_only` / `peer_side_effects: []` intact. +- **Member never omitted.** `plainweave` appears in the federation block with its own weft-reason on + every `include_federation=True` run — `disabled` when unwired, never silently dropped. + +## Acceptance criteria + +- A reverify-worklist item shows `enrichment.requirements: "present"` with a populated per-item + `requirements: [...]` array when Plainweave has bindings for the entity. +- Shows `"absent"` (empty array) when Plainweave knows the entity but has no requirement. +- Shows `"unavailable"` under producer fault — verified by a fault-injection test and asserted to + differ from `absent`. +- `disabled` (with a transport_blocker) when the installed plainweave lacks the + `requirements-enrichment` verb — never a faked-empty. +- A warpline-side **contract test pins the consumed shape to the frozen Plainweave golden** + `weft.plainweave.requirements_enrichment.v1`. +- `plainweave` is present in `FEDERATION_MEMBERS` and the federation block on every federated run. + +## On the schema dependency (sibling prompt #3) + +Prompt #3 asks the Warpline interface-lock owner to ratify the requirement **item** shape +(`requirement_id, stable_id, version, type, criticality, binding{relation, actor_kind, +freshness}`). It is **not yet ratified**, and Plainweave's golden already uses that shape. +Because warpline consumes items **opaquely** (it surfaces them, it doesn't parse their +internals), you can build and ship the consumer now; only the *byte-pinned* contract test +depends on ratification. Recommended sequencing: ratify #3 → byte-pin the contract test. If you +proceed before ratification, assert **status semantics** (present/absent/unavailable + the +no-silent-clean distinction), not the item internals — a later amend to the item shape then +won't break the consumer. + +## References + +- Plainweave producer: `cli_commands.py:137-143,1113-1121`; golden + `tests/fixtures/contracts/warpline/requirements-enrichment.json`; spec + `docs/superpowers/specs/2026-06-27-peer-facts-wardline-warpline-design.md` §6. +- Warpline template to copy: `federation.py` (legis member end to end), `_enrichment.py:106` (`sei_reason`). +- Warpline frozen contract / enrichment vocab: `/home/john/weft/pm/2026-06-13-warpline-interface-lock.md`; + warpline-workflow `references/degrade-and-federation.md`. diff --git a/docs/handoffs/2026-06-29-filigree-linked-work-facts.md b/docs/handoffs/2026-06-29-filigree-linked-work-facts.md new file mode 100644 index 0000000..7920587 --- /dev/null +++ b/docs/handoffs/2026-06-29-filigree-linked-work-facts.md @@ -0,0 +1,56 @@ +# Peer prompt — Filigree: emit a local linked-work facts artifact for Plainweave + +**To:** the Filigree maintainer / implementing agent. +**From:** Plainweave (owner-gated sibling handoff). +**Date:** 2026-06-29 (PDR-018, seam hardening). +**Status:** PROPOSED — owner-gated. Do **not** implement on the Plainweave side; this is +Filigree-side work plus a small Plainweave adapter once the artifact exists. + +## Why + +Plainweave's Legis preflight producer (`weft.plainweave.preflight_facts.v1`, ADR-006) +reserves an `open_linked_work` fact kind: "for a scoped requirement/gap, what Filigree +issues are open and linked?". Plainweave **cannot emit it today** and honestly reports the +gap in-band as the `linked_work_facts_unavailable` warning (no-silent-clean). It cannot be +emitted in-grant because: + +- Plainweave never calls a sibling live: `authority_boundary.live_peer_calls = false`. The + sanctioned `entity_association_list_by_entity` (ADR-029) is a live MCP/HTTP call, so it + would break that boundary. +- `.filigree/` is a Filigree-owned SQLite DB. Direct reads would couple Plainweave to + Filigree's internal schema — exactly the coupling the thin-member doctrine forbids. + +Unlike Wardline (whose append-only `.wardline/*-findings.jsonl` Plainweave already adapts +in-grant), Filigree exposes **no local, boundary-clean facts artifact** Plainweave can read. + +## The ask (Filigree-side) + +Emit a **local, append-only or snapshot facts artifact** — mirroring `.wardline/*.jsonl` — +that Plainweave can read read-only with no live call. Suggested: `.filigree/linked-work.jsonl` +(or a documented export command) with, per row, the stable identity Plainweave keys on plus +the issue's open/closed lifecycle facts. Concretely, enough to answer "for requirement R / +gap G, which Filigree issues are open and link to it": + +- the linked entity/requirement/gap identity (a SEI or the `filigree_issue` / `gap` ids + Plainweave already stores as opaque trace refs — see `service.py` canonical relations + `('filigree_issue','implements_work_for','requirement_version')` and + `('filigree_issue','resolves_gap','gap')`), +- issue id, status/lifecycle (open vs closed/resolved), and a freshness/observed-at stamp. + +Facts only — **no verdicts**. Filigree owns the work lifecycle; Plainweave only surfaces the +linkage as advisory context. Plainweave will **never** create or move Filigree work from +this seam (the `gap_create_work` write path stays unimplemented; it would mutate a sibling). + +## Then (Plainweave-side, once the artifact exists — a separate owner-gated bid) + +A `FiligreeAdapter` reading that artifact in-grant (the Wardline-adapter pattern), wiring real +`open_linked_work` facts into `preflight_facts.v1`, with the `linked_work_facts_unavailable` +warning degrading to a `filigree_linked_work_absent` code only when the artifact is genuinely +absent — and a frozen contract test (mirroring the wardline/warpline/loomweave contract tests +landed in PDR-018). + +## Reversal trigger (recorded in ADR-006) + +If this boundary-clean local artifact lands, `open_linked_work` becomes emittable in-grant and +the ADR-006 emission fork (PDR-018) reopens. Until then, the in-band +`linked_work_facts_unavailable` warning is the correct honest posture. diff --git a/docs/product/current-state.md b/docs/product/current-state.md index 48e0c31..7c71f10 100644 --- a/docs/product/current-state.md +++ b/docs/product/current-state.md @@ -1,4 +1,4 @@ -# Plainweave Current State Checkpoint: 2026-06-28 (PDR-016; prior PDR-015) · (commit recorded below) +# Plainweave Current State Checkpoint: 2026-06-29 (PDR-018; prior PDR-016) · (branch feat/seam-hardening-blockers-345) ## The bet right now @@ -65,6 +65,10 @@ north-star (coverage completeness) unchanged — owner-/sibling-gated. **Owner calls on the release escalation** — finalize the `1.2.0` CHANGELOG version/date, reconcile/retire the `release/1.2.0` branch against `main`, then the held PyPI publish. Then close the two **Lacuna-tour prerequisites** (fix the packaging bug → install plainweave → -clean-tree `make tour`). Then continue **harden + build** (remaining production blockers: -Loomweave-owned identity resolution, Legis fact emission, Filigree contract tests) or pivot to -**coverage-completeness** if the owner wants north-star movement. +clean-tree `make tour`). All 5 named production blockers are now retired plainweave-side — +**PDR-018 closed the last three** (#3 Loomweave identity / #4 Legis fact emission / #5 +Filigree contract tests) as test-and-docs-only hardening (390 tests, 91.18% cov; legis +cross-repo oracle stayed green; zero `src/` changes). The only open seam item is the +owner-gated Filigree `open_linked_work` handoff +(`docs/handoffs/2026-06-29-filigree-linked-work-facts.md`). Next: pivot to +**coverage-completeness** if the owner wants north-star movement, or finalize the release. diff --git a/docs/product/decisions/PDR-017-requirements-enrichment-producer-gated-live.md b/docs/product/decisions/PDR-017-requirements-enrichment-producer-gated-live.md new file mode 100644 index 0000000..1b604da --- /dev/null +++ b/docs/product/decisions/PDR-017-requirements-enrichment-producer-gated-live.md @@ -0,0 +1,39 @@ +# PDR-017: Requirements-enrichment producer gated live end-to-end against its first sibling consumer (Warpline); known wheel-build blocker fixed + +Date: 2026-06-29 Status: accepted Author: agent:claude (Wardline-session cross-repo gate, owner-directed) +Owner sign-off: owner directed the end-to-end re-gate of the sibling-consumer enrichment push ("ultracode") and the recording of this PDR ("lock down that PDR"). This records VALIDATION of the already-delivered producer (PDR-014/PDR-015) against its now-built consumer (Warpline PDR-0008), plus one in-grant, repo-local build-config fix. The fix is **uncommitted**; the commit, the `1.2.0` CHANGELOG/version cut, and the held PyPI publish remain **owner-gated** (authority grant: publication + sibling obligations escalate — see `current-state.md`, PDR-002/PDR-012). +Related: PDR-014 (producer delivered — `weft.plainweave.requirements_enrichment.v1`, `bc37a24`), PDR-015 (peer-facts CLI/MCP parity), PDR-009 (no-silent-clean / no-vanity-metric), Warpline **PDR-0008** (the consumer acceptance this pairs with), observation **`plainweave-obs-6a7255ffbe`** (the wheel-build blocker, now fixed), the producer handoff `docs/handoffs/2026-06-28-warpline-requirements-enrichment-consumer-impl.md`. + +## Context + +PDR-014 delivered the `requirements_enrichment.v1` producer Plainweave-side and PDR-015 added the `plainweave requirements-enrichment --json` CLI for transport parity — but **sibling follow-on #1** (the Warpline consumer) stayed handed-off/owner-gated, so the producer surface had never been exercised by a real consumer. Two things changed: (1) the Warpline session built and accepted the consumer (Warpline PDR-0008 — the 4th federation member, structure/status-pinned against the vendored golden); (2) an owner-directed re-gate proved the loop actually closes on PATH. + +The gate exposed that the producer's *correctness in source* (`uv run`) had masked a packaging regression: committed `main` (HEAD `c1e125e`, the PDR-016 web-UX commit) **cannot build a wheel** — the already-tracked **`plainweave-obs-6a7255ffbe`** (P2). `[tool.hatch.build.targets.wheel.force-include]` re-maps `web/templates` + `web/static`, which `packages = ["src/plainweave"]` (src-layout) already vendors, colliding on `web/static/.gitkeep`. Because `uv tool install` builds a wheel and the *installed* `plainweave` was a stale **1.0.0**, Warpline's `--help` capability probe never saw the verb → the requirements member read `disabled` in practice **even though both sides' code was correct**. The contract was dark for an install/packaging reason, not a contract reason. + +## The call + +**ACCEPT the requirements-enrichment producer as proven live-consumable, and bank the fix for the wheel-build blocker.** Removed the redundant `force-include` block — `packages = ["src/plainweave"]` already includes `web/templates` + `web/static`; the wheel now builds and ships every web asset exactly once. Reinstalled the `plainweave` uv tool **1.0.0 → 1.1.0** (both entry points, `plainweave` + `plainweave-mcp`), lighting the verb up on PATH. This is a repo-local, reversible build-config change (in-grant) — **not** a commit, a version cut, or a publish. + +## Validation evidence (end-to-end re-gate, firsthand) + +- **Wheel build fixed + asset-verified:** post-fix `uv build --wheel` succeeds; the wheel carries `app.css`, `htmx.min.js`, all 20+ templates, and `web/static/.gitkeep` **exactly once** (was the collision). Resolves `plainweave-obs-6a7255ffbe`. +- **Verb live on PATH:** `plainweave --help` advertises `requirements-enrichment`; the verb returns the `weft.plainweave.requirements_enrichment.v1` envelope (`ok:true`; `authority_boundary{local_only:true, live_peer_calls:false, governance_verdicts:false, requirements_owner:"plainweave"}`; producer 1.1.0). +- **Consumer flips disabled→enabled:** Warpline's real `PlainweaveRequirementsClient.available()` went `False → True` across the reinstall; a live CLI round-trip parsed cleanly into `{entity_ref: item}` with the 5-key item shape and honesty (`unavailable` ≠ `absent`) intact. +- **Graceful degradation traced:** against a non-Plainweave repo the producer returns its `ok:false` NOT_FOUND envelope; Warpline catches it (`federation.py:502`) → `reason("unreachable")`, never crashes. +- **Suite green:** Warpline's 41 requirements-scoped tests pass; no live/un-mocked test pins the disabled posture, so the reinstall flipped nothing. +- **Live scope, stated honestly:** `unavailable` is proven **live** end-to-end; `present`/`absent` remain pinned by Warpline's structure/status contract test (fixture-based), not live-demonstrated this session. + +## What this does NOT cover (owner-gated) + +- **Commit / version / publish.** The build-config fix is **uncommitted** on Plainweave `main` (also 1 ahead of `origin`). Committing it, the `1.2.0` CHANGELOG/version cut, and the held PyPI publish are owner escalations. **Until the fix commits, the *installed* tool diverges from committed source and the next clean `uv tool install` breaks again** — i.e. the fix is load-bearing for the contract staying live. +- **Patching Warpline.** None done — the consumer is Warpline's own work (PDR-0008), uncommitted on `release/1.2.0` and co-mingled with a concurrent stream; committing/untangling it is the Warpline owner's call. Warpline's uncommitted files were left untouched. +- **Item-schema ratification (interface-lock prompt #3).** Unchanged — the contract test stays structure/status-pinned until the requirement *item* shape is ratified, then byte-pin. + +## Flags routed to owners + +- **Plainweave (investigated — NOT a defect, no fix):** the originally-flagged exit-0-vs-2 "inconsistency" was a **measurement artifact**, not cwd-dependence. `_emit_surface_result` (`cli_commands.py:1204`; introduced `a6044a1`, used by `requirements-enrichment` from `9334838`) maps `ok:true → 0`, `INTERNAL → 4`, else `→ 2`, so `ok:false` NOT_FOUND is **deterministically exit 2** — reproduced across `/tmp`, `/home/john/warpline`, and `/home/john/wardline`, and pinned by `tests/test_cli_requirements_enrichment.py`. The phantom "exit 0" came from a piped exit capture (`… | head; echo $?` reports `head`'s status, not plainweave's). Exit 2 + `ok:false` is the *honest* answer (an uninitialised store is a genuine producer failure → Warpline maps it to `unavailable`); a faked `ok:true` with per-entity `unavailable` items would violate no-silent-clean. The real residual is **operational, not exit codes**: Warpline execs bare `plainweave` from PATH = the uv-tool snapshot, which silently lags the dev tree until `uv tool install --force` / `uv tool upgrade plainweave` (this gate's stale-1.0.0-binary was exactly that drift). Worth an editable install (`uv tool install -e .`) or a re-snapshot hook to kill the drift class — owner's call. +- **Warpline (in-flight session):** against a non-Plainweave repo the member now reads `enabled` (verb advertised) yet every consult is `unreachable` — whether that vs. `absent`/`disabled` is the right posture is a Warpline design call. + +## Reversal trigger + +Reopen if, over real federated use, the requirements dimension perpetually reads `disabled`/`unavailable` because the installed Plainweave never advertises `requirements-enrichment` in member repos — the producer is correct but never *present* where consumed, so the axis adds no signal (mirrors Warpline PDR-0008 trigger (a); watched via the `metrics.md` federation-member-coverage reading). The packaging fix going uncommitted is the near-term instance of exactly this failure mode: if `main` ships without it, every clean install re-darkens the member. diff --git a/docs/product/decisions/PDR-018-seam-hardening-blockers-345.md b/docs/product/decisions/PDR-018-seam-hardening-blockers-345.md new file mode 100644 index 0000000..f747168 --- /dev/null +++ b/docs/product/decisions/PDR-018-seam-hardening-blockers-345.md @@ -0,0 +1,78 @@ +# PDR-018: Seam hardening — production blockers #3/#4/#5 retired plainweave-side (contract tests + test-hardening) + +Date: 2026-06-29 Status: accepted Author: agent:claude-product-owner +Owner sign-off: EXPLICIT (owner set the bid — "do the seam hardening … then get it out the door" — and authorized subagent-driven multi-agent delivery via "ultracode"/"ultrathink") +Related: PDR-014 (peer facts; retired blockers 1/2 + Warpline/Wardline contract tests), PDR-009 (no-silent-clean / no-vanity-metric), PDR-011 (doctor federation parity), ADR-006 (Legis preflight fact envelope), ADR-005 (Loomweave SEI consumer contract) + +## Context + +PDR-014 retired 3 of the 5 named production blockers. This bid retires the remaining +three — (3) Loomweave-owned identity resolution, (4) Legis fact emission, (5) Filigree +contract tests — under the harden-and-ship posture. A scoping workflow (3 investigators + +adversarial challenge per blocker + contract-template auditor) established the **honest +size** of each: the seam BEHAVIOR was already built and tested for #3 and #4; the genuine +gaps were contract-test/parity artifacts and one zero-coverage fact kind. The adversarial +pass rejected two unsafe over-scopes (suppressing the Legis `*_unavailable` warnings — a +silent-clean hole; a dedicated Filigree validator module — Filigree emits no `.v1` payload). + +## The call + +**Plainweave-side, test-and-docs only — ZERO `src/` production changes** (the seam behavior +already exists; this freezes and pins it). Delivered via inline TDD with every new test +proven red-first against a transient producer mutation (anti-decorative), then reverted. + +- **#5 Filigree contract tests — RETIRED.** New `tests/contracts/test_filigree_contract.py` + pins: `open_linked_work` is never emitted by the local-only producer (absence is the + in-band `linked_work_facts_unavailable` warning, never empty-but-ok); the `filigree_issue` + `implements_work_for` relation (previously untested) is canonical and stored as an opaque + pointer; a non-canonical filigree relation is a VALIDATION error; the dossier introduces no + gate/decision/enforcement KEY. The scope-independent presence of `linked_work_facts_unavailable` + on an EMPTY scope is pinned by an extension to the existing empty-scope preflight test. + (Finding: the peer-facts value-token scanner does NOT apply to the dossier, which + legitimately carries lifecycle `"approved"`/`"rejected"` VALUES — the meaningful invariant + is the absence of a verdict KEY.) +- **#4 Legis fact emission — degraded-state already done; test-hardening landed; remaining + kinds handed-off / superseded (NOT "all kinds flowing").** The producer's explicit + degraded-state (`live_diff_resolution_unavailable`, `freshness: partial`, `requirement_nearby` + basis) was already present and tested. Added the missing behavioral coverage for + `orphaned_entity_link` (emitted but previously zero-coverage). Of ADR-006's 11 fact kinds the + producer emits 8; the three unemitted are now PROVENANCED in ADR-006 (annotation): + `active_finding_linked`/`waived_finding_linked` are **superseded** by the dedicated + `weft.plainweave.wardline_peer_facts.v1` producer (PDR-014) — wiring them into the preflight + envelope is an owner-gated fork intentionally NOT taken; `open_linked_work` is **sibling-gated** + (handed off, below). Fixed a stale doc claim (the Legis consumer now exists). +- **#3 Loomweave identity resolution — behavior already done+tested; PDR-014-parity contract + landed.** The live HTTP resolve + capability probe + closed-vocab degraded codes + SEI §8 + oracle were already implemented and tested. Added the missing PRODUCER-side contract: + `tests/loomweave_contract.py::validate_loomweave_catalog` + a degraded golden + (`tests/fixtures/contracts/loomweave/catalog-degraded.json`), routing the committed golden + AND live producer output through one validator so they cannot diverge. The cardinal + invariant pinned: an `unavailable` adapter never returns a clean-empty page (empty `items` + must carry a non-empty `degraded[]`). + +## Evidence + +`make ci` green: **390 tests** (up from 378), **91.18% coverage** (up from 91.14%), mypy +--strict + ruff clean. `wardline scan` clean (0 active). Cross-repo: legis's vendored preflight +oracle stays green (**28 passed**) — additive plainweave-side test/doc changes created no +legis obligation (legis pins the empty `commit_range` scenario; plainweave pins the populated +one). Every new test mutation-verified red-before-green. Zero `src/plainweave/*.py` changes. + +## Scope explicitly NOT taken (honesty) + +- The preflight wire-golden was NOT enriched with `orphaned_entity_link`/`requirement_nearby`; + the behavioral test closes the coverage gap, and the wire-golden remains one representative + scenario — avoiding a byte-pin re-vendor and any cross-repo churn for no added guarantee. +- No live Filigree join, no `gap_create_work` write path (would mutate a sibling; spine-prohibited). + +## Sibling follow-on (OWNER-GATED — handed off, not done) + +`docs/handoffs/2026-06-29-filigree-linked-work-facts.md`: a peer prompt asking Filigree to emit +a local, boundary-clean linked-work facts artifact (mirroring `.wardline/*.jsonl`) that +Plainweave could adapt in-grant to emit real `open_linked_work` facts. + +## Reversal trigger + +If a boundary-clean local Filigree facts artifact lands, `open_linked_work` becomes emittable +in-grant and the ADR-006 emission fork reopens (a `FiligreeAdapter` + its contract test, a +separate owner-gated bid). Recorded in ADR-006's emission-status annotation. diff --git a/pyproject.toml b/pyproject.toml index 4346af5..1e269e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,10 +58,6 @@ path = "src/plainweave/_version.py" [tool.hatch.build.targets.wheel] packages = ["src/plainweave"] -[tool.hatch.build.targets.wheel.force-include] -"src/plainweave/web/templates" = "plainweave/web/templates" -"src/plainweave/web/static" = "plainweave/web/static" - [tool.ruff] line-length = 120 target-version = "py312" diff --git a/src/plainweave/_version.py b/src/plainweave/_version.py index 6849410..c68196d 100644 --- a/src/plainweave/_version.py +++ b/src/plainweave/_version.py @@ -1 +1 @@ -__version__ = "1.1.0" +__version__ = "1.2.0" diff --git a/src/plainweave/skills/plainweave-workflow/SKILL.md b/src/plainweave/skills/plainweave-workflow/SKILL.md new file mode 100644 index 0000000..1c71237 --- /dev/null +++ b/src/plainweave/skills/plainweave-workflow/SKILL.md @@ -0,0 +1,213 @@ +--- +name: plainweave-workflow +description: > + This skill should be used when the user asks "why does this code exist", + "what's our intent coverage", "find orphan code / requirements / goals", + "trace this requirement up to a goal or down to code", "bind this SEI / + entity to a requirement", "draft / approve / supersede a requirement", "show + the requirement dossier", "what's unverified or stale", "baseline the + requirements", or when working in a project that uses Plainweave for code-up + requirements traceability and intent. Provides the read/author/verify + workflow, the doctrine invariants (advisory-only, no-silent-clean, + enrich-only, zero minted SEIs), and the cross-member peer-facts surfaces. +--- + +# Plainweave Workflow + +Plainweave is the Weft federation's **requirements and verification authority** — +"the permission-for-code-to-exist member." It maintains a **code-up intent graph** +(`Loomweave SEI → requirement → goal`) in a local `.plainweave/` store and answers +one question for every public surface: *"why does this exist?"* It is **advisory**: +it surfaces facts and lets agents decide; it never emits an allow/block verdict. + +Prefer the MCP tools (`mcp__plainweave__*`) when available; fall back to the +`plainweave` CLI. Every read CLI takes `--json` to emit a versioned envelope. + +## The model — three altitudes, one graph + +``` +goal strategic intent ("ship trustworthy federation seams") + ↑ links +requirement a reviewable statement ("the producer must never silent-clean") + ↑ binds +code (SEI) a Loomweave-identified public entity (function/class/module) +``` + +A node with **no upward edge** is an **orphan** — a reviewable question, not an +error. Code that skips its bind is exactly what surfaces. Requirements are +trivially mintable; consolidation ("these three are the same") is **agent-driven** +off the corpus, never an automated verdict. + +## Core read workflow — the four intent primitives + +```bash +plainweave intent coverage # north-star: fraction of public surfaces that answer "why?" +plainweave intent orphans code # unjustified nodes at an altitude: code | requirement | goal +plainweave intent trace code # justification neighborhood up to goals / down to code +plainweave intent corpus # readable dump of requirements + their code/goal links +``` + +- **`coverage`** is the self-computed north-star. It is **honestly qualified + in-band**: `denominator_complete`, `present_plugins`, namespace scoping, bounded + evidence. Scope the denominator with `--exclude-namespace PREFIX` (default excludes + `scripts.`, `tests.`) and `--surface-class {cli-command,entry-point,exported-api,http-route}`. + It **never reports a silent clean** when the denominator is partial. +- **`orphans `** lists nodes with no upward justification edge. Triage, + don't panic — an orphan is "should this be bound, or is it genuinely standalone?" +- **`trace `** walks both directions from a node. +- **`corpus`** is the artifact a curator reads to spot duplicates before consolidating. + +## Authoring workflow — draft → criterion → approve → bind → ladder + +Requirements are versioned with a draft/approve lifecycle: + +```bash +plainweave req add --title "…" --statement "…" [--actor human:] # create a DRAFT +plainweave criterion add … # acceptance criteria on the active draft +plainweave req approve # promote the active draft +plainweave req supersede … | req deprecate … | req reject # later lifecycle moves +``` + +Bind code and ladder to strategy: + +```bash +plainweave bind sei [--entity-kind …] [--content-hash …] +plainweave goal add "…" # strategic intent node +plainweave goal link +``` + +Trace links (graph edges) have their own propose/review lifecycle — a proposed +link is a *suggestion*, accepted is *fact*, rejected never reads as coverage: + +```bash +plainweave trace propose --from-kind … --from-id … --relation … --to-kind … --to-id … [--confidence N] +plainweave trace accept | trace reject +plainweave trace list +``` + +> **Rejected ≠ absent coverage.** A reviewed-and-rejected binding does **not** +> read as requirement coverage; the view drops `rejected` trace links before +> computing `present`/`absent`. + +## Verification — methods, evidence, status + +```bash +plainweave verify method … # define how a requirement is verified +plainweave verify evidence … # record evidence against a method +plainweave status requirement # verification status for one requirement +plainweave status unverified # requirements with no verification +plainweave status stale # requirements whose evidence has gone stale +plainweave dossier # the full per-requirement dossier (advisory boundary) +``` + +Lock and compare requirement sets over time: + +```bash +plainweave baseline create … # lock the approved-requirement set +plainweave baseline diff # drift of current approved vs a baseline +plainweave baseline list | baseline show +``` + +## Doctrine — the hard invariants (do not violate) + +Plainweave's behaviour is contract-validated against these; violating them is +rejected by the shared validators, not merely discouraged. + +- **Advisory, never gates.** Zero release allow/block/approved/verdict tokens. + Coverage facts ride out at the git/CI boundary *through Legis* — Plainweave adds + no enforcement of its own. Any teeth are dialled up by the consumer via Legis cells. +- **No silent-clean.** A degraded or language-partial denominator, an unavailable + adapter, or an unresolved identity is **flagged in-band** — never collapsed to a + clean-empty result. `unavailable` ("I can't tell") is distinct from `absent` + ("definitively none") and from a clean `present`. +- **Enrich-only.** Plainweave absent → Loomweave, Legis, and the code are + unaffected; solo mode degrades to manual file/symbol refs. +- **Zero minted SEIs.** Sibling identity (`loomweave:eid:…`) and sibling fact + bodies are consumed **opaquely** — surfaced, never parsed or minted. +- **Local-only.** Reads compute against the local store / re-scanned scope; no live + peer calls in the producers (`local_only: true`, `live_peer_calls: false`). + +## Cross-member peer facts (advisory producers) + +Two local-first producers expose Plainweave facts to siblings, each with a frozen +`.v1` contract, explicit degraded state, and no silent-clean: + +```bash +plainweave wardline-peer-facts [--limit N] [--offset N] [--json] # weft.plainweave.wardline_peer_facts.v1 +plainweave requirements-enrichment ... [--json] # weft.plainweave.requirements_enrichment.v1 +``` + +- **`wardline-peer-facts`** surfaces Wardline findings (active / waived / baselined + / judged) computed against the actually re-scanned scope. Reads `.wardline/*-findings.jsonl` + only; an absent `.wardline/` reports `freshness: unavailable`, **never clean**. +- **`requirements-enrichment`** is the Plainweave-owned producer for **Warpline's** + reserved `enrichment.requirements` slot. `entity_ref` is a SEI or dotted locator + (batch many in one call). Per-entity `status`: `present` (≥1 alive binding) / + `absent` (entity known, none bound) / `unavailable` (identity unresolved or store + error — "I can't tell," **never** "no requirements"). This is consumed live by the + Warpline `consult_federation` requirements member — see `references/cross-member-seams.md`. + +## Health and diagnostics + +```bash +plainweave init # create a .plainweave/ store +plainweave doctor # federation-parity health: store/schema, Loomweave catalog binding, MCP surface +plainweave doctor --fix # idempotent in-place store repairs, then re-check +plainweave doctor --root # inspect a root other than cwd (remediation is root-aware) +``` + +`doctor` exits non-zero on unresolved problems; `--fix` is safe and idempotent. + +## Response shapes & exit codes + +Read surfaces emit a versioned envelope under `--json` (and the MCP tools return the +same shape): + +- **Success** → `{schema: "weft.plainweave..v1", ok: true, …, authority_boundary: {local_only: true, live_peer_calls: false, governance_verdicts: false}}`. +- **Failure** → `{ok: false, error: {code, …}}`. `code` ∈ `VALIDATION`, `NOT_FOUND`, + `CONFLICT`, `POLICY_REQUIRED`, `PEER_ABSENT`, `PEER_STALE`, `PEER_CONTRACT`, + `LOCKED`, `UNSUPPORTED`, `INTERNAL`. Branch on `code`, not message text. +- **Exit codes** (surface commands): `ok:true → 0`; `INTERNAL → 4`; any other + failure (e.g. `NOT_FOUND`, uninitialised store) → `2`. An uninitialised store is a + *genuine* producer failure (exit 2 / `ok:false`), never a faked clean — that would + break no-silent-clean. NB: a piped exit capture (`… | head; echo $?`) reports the + pipe tail's status, not plainweave's. + +## MCP parity (read-only mirror) + +The MCP server (`plainweave-mcp`) mirrors the reads — `mutates: false`, +`local_only: true`, no peer side effects: `plainweave_intent_coverage` / +`_orphans` / `_trace` / `_corpus`, `plainweave_requirement_get` / `_search` / +`_dossier`, `plainweave_verification_status_get` / `_list`, `plainweave_baseline_*`, +`plainweave_trace_link_list`, `plainweave_entity_intent_context_get`, +`plainweave_loomweave_catalog_list`, `plainweave_preflight_facts_get`, +`plainweave_project_context_get`, `plainweave_wardline_peer_facts_list`, +`plainweave_requirements_enrichment_get`. + +## Reference sheets + +Load these when you hit a specific challenge, not upfront: + +- **`references/intent-graph-patterns.md`** — coverage-denominator scoping, + orphan triage at each altitude, corpus-driven consolidation, baselines & drift, + the verification lifecycle. +- **`references/cross-member-seams.md`** — the peer-facts producers and their + freshness/degraded vocab, the Loomweave catalog/SEI seam, the Legis boundary, + and the opacity rules for sibling identity. + +## Quick decision guide + +| Situation | Action | +|-----------|--------| +| "Why does this code exist?" | `plainweave intent trace code ` | +| "What's our intent coverage?" | `plainweave intent coverage` (read the in-band qualifiers) | +| "What code is unjustified?" | `plainweave intent orphans code` | +| "These requirements look duplicated" | `plainweave intent corpus`, consolidate by hand | +| "Record a new requirement" | `req add` → `criterion add` → `req approve` | +| "Link this entity to a requirement" | `plainweave bind sei ` | +| "Is this requirement verified?" | `plainweave status requirement ` / `dossier ` | +| "What's gone stale?" | `plainweave status stale` | +| "Did anything drift from the baseline?" | `plainweave baseline diff ` | +| "What requirements bind this entity (for Warpline)?" | `plainweave requirements-enrichment --json` | +| "Is the store healthy?" | `plainweave doctor` (`--fix` to repair) | +| "Plainweave says `unavailable`" | It can't tell (identity/store) — **not** "none." Resolve identity or `init`. | diff --git a/src/plainweave/skills/plainweave-workflow/references/cross-member-seams.md b/src/plainweave/skills/plainweave-workflow/references/cross-member-seams.md new file mode 100644 index 0000000..7b6bec2 --- /dev/null +++ b/src/plainweave/skills/plainweave-workflow/references/cross-member-seams.md @@ -0,0 +1,77 @@ +# Cross-member seams + +How Plainweave consumes from and produces facts for its Weft siblings. The unifying +rule: **enrich-only, opaque identity, no silent-clean, no verdicts.** + +## Loomweave — identity & catalog (Plainweave consumes) + +Plainweave never mints code identity. It consumes Loomweave **SEIs** +(`loomweave:eid:<32hex>`) opaquely: + +- `plainweave catalog record` registers a public entity discovered by a sibling + catalog tool. The SEI is stored verbatim; Plainweave does not parse or synthesise it. +- `plainweave bind sei ` ties a requirement to that + identity. `--content-hash` captures the entity's hash at attach time so a consumer + can later detect drift. +- The Loomweave catalog adapter has a **frozen degraded-state contract** + (`weft.plainweave.loomweave_catalog.v1`): when the adapter is `unavailable` it + returns an explicit unavailable envelope routed through the same oracle as live + output — it never returns a clean-empty page that would read as "no entities." + +If Loomweave is absent, Plainweave degrades to manual file/symbol references; the +graph still works, identity is just less precise. + +## Legis — the git/CI boundary (Plainweave's facts ride out) + +Plainweave emits **no enforcement of its own**. Coverage facts cross the git/CI +boundary *through Legis* ("this change adds N public entities bound to no +requirement"). Advisory by default; a repo that wants teeth dials it up through +Legis's policy cells. The preflight advisory cell +(`plainweave_preflight_facts_get`) supplies the facts; Legis decides whether they gate. + +## Warpline — requirements enrichment (Plainweave produces, Warpline consumes live) + +`plainweave requirements-enrichment ... --json` is the Plainweave-owned +producer (`weft.plainweave.requirements_enrichment.v1`) for Warpline's reserved +`enrichment.requirements` slot. Warpline's `consult_federation` wires it as the +**4th federation member** (alongside filigree/wardline/legis), capability-gated on +the verb being advertised. + +Per-entity `status` semantics — **mapped Plainweave-side; consumers pass through, never re-interpret**: + +| status | meaning | +|---|---| +| `present` | ≥1 alive requirement bound (the `requirements` array is non-empty) | +| `absent` | entity known, none bound — a definitive "none here" | +| `unavailable` | could not determine (identity unresolved / store error) — **"I can't tell," never "no requirements"** | + +The `unavailable` ≠ `absent` distinction is load-bearing and fault-injection-tested: +collapsing `unavailable` to `absent` (or to a clean-empty) would violate no-silent-clean +and let a "can't tell" masquerade as "verified none." Requirement item bodies and SEIs +are **opaque** to the consumer — surfaced, never parsed. + +Operational note: a consumer that execs bare `plainweave` from `PATH` runs the +installed uv-tool snapshot, which can lag the dev tree. If the verb isn't advertised, +the consumer reads the member `disabled` even though the code is correct — keep the +installed tool current (`uv tool install --force` / an editable install). + +## Wardline — findings as peer facts (Plainweave produces) + +`plainweave wardline-peer-facts --json` (`weft.plainweave.wardline_peer_facts.v1`) +surfaces Wardline findings (active / waived / baselined / judged; defect / +non-defect) plus resolved-or-unseen, computed against the **actually re-scanned +scope** — scan-identity manifest primary, with a path-set heuristic fallback that +flags itself in-band. An absent `.wardline/` reports `freshness: unavailable`, never +a clean empty page. + +## The freshness / status vocabulary + +Across the producers, degraded state is **explicit and distinct** — these are not +interchangeable, and none of them is a silent clean: + +- `present` — facts found and returned. +- `absent` — definitively none (the authority looked and there are none). +- `unavailable` — could not determine (peer/store/identity gap). The honest "I can't tell." +- `stale` — facts exist but the thing they describe has moved on. + +Surface the exact one; never round `unavailable`/`stale` down to `absent` or up to a clean result. diff --git a/src/plainweave/skills/plainweave-workflow/references/intent-graph-patterns.md b/src/plainweave/skills/plainweave-workflow/references/intent-graph-patterns.md new file mode 100644 index 0000000..8c95b68 --- /dev/null +++ b/src/plainweave/skills/plainweave-workflow/references/intent-graph-patterns.md @@ -0,0 +1,85 @@ +# Intent-graph patterns + +Deep-dive patterns for working the `SEI → requirement → goal` graph. Load when you +hit one of these specific situations. + +## Reading coverage honestly + +`plainweave intent coverage` reports the fraction of public surfaces that answer +"why does this exist?" — but the **number is meaningless without its qualifiers**. +Always read these from the envelope before quoting a percentage: + +- **`denominator_complete`** — `false` means the surface inventory is partial + (e.g. a language plugin is absent). A high percentage over an incomplete + denominator is not a clean bill of health; say so. +- **`present_plugins`** — which language/surface extractors actually ran. Coverage + is only as complete as this list. +- **Namespace scope** — the denominator excludes `scripts.` and `tests.` by + default. Add `--exclude-namespace PREFIX` (repeatable) to scope out generated or + vendored namespaces; never silently widen it to flatter the number. +- **Surface class** — `--surface-class {cli-command,entry-point,exported-api,http-route}` + restricts the denominator. Use it to answer "how covered is the HTTP surface + specifically?" rather than the blended figure. +- **`--max-surfaces N`** caps the evidence lists, **not** the counts — counts are + never truncated, so a capped list still reports the true total. + +The honest move when coverage looks high: state the figure *and* the denominator +qualifiers in the same breath ("92% of exported-api surfaces, but +`denominator_complete: false` — the Rust plugin didn't run"). + +## Triaging orphans at each altitude + +`plainweave intent orphans {code,requirement,goal}` returns nodes with no upward +edge. The triage question differs by altitude: + +- **code orphans** — a public entity bound to no requirement. Ask: should this be + bound (then `bind sei`), or is it genuinely infrastructural/standalone? Don't + reflexively mint a shell requirement just to clear the orphan — that manufactures + vanity coverage. +- **requirement orphans** — an approved requirement laddered to no goal. Either + link it (`goal link`) or accept it as a leaf with a note. A pile of goal-less + requirements is a signal the goal layer is under-modelled. +- **goal orphans** — a goal with no requirements beneath it. Usually means intent + was declared but never decomposed into reviewable requirements. + +## Corpus-driven consolidation + +`plainweave intent corpus` is the curator's artifact. Consolidation is **agent- +driven, never automated**: read the corpus, spot "these three say the same thing," +then supersede the duplicates into one canonical requirement (`req supersede`) and +re-bind. Plainweave serves the substrate; it does not auto-merge. The optional +Loomweave semantic-similarity hint *assists* this read — it is explicitly not a +dedup engine and never acts on its own. + +## Requirement lifecycle + +Requirements are versioned with an explicit draft/approve flow: + +1. `req add` creates a **draft** (the "active draft" for the project). +2. `criterion add` attaches acceptance criteria to the active draft. +3. `req approve` promotes it. Approved requirements are what baselines lock. +4. `req supersede` creates a new version that replaces an approved one; + `req deprecate` retires one; `req reject` discards an unwanted draft. + +Trace links carry their own review state: `propose → accept | reject`. A **proposed** +link is a suggestion; only **accepted** links count as justification; **rejected** +links are dropped before coverage is computed (a rejected binding never reads as +`present`). + +## Baselines & drift + +`plainweave baseline create` locks the current approved-requirement set under a +name. Later, `plainweave baseline diff ` shows how the live approved set has +drifted (added / removed / superseded). Use a baseline at a release boundary or a +review milestone, then diff against it to answer "what requirements moved since we +agreed this set?" Baselines are immutable once created — `show`/`list` read them. + +## Verification, not validation + +`verify method` declares *how* a requirement is checked; `verify evidence` records a +concrete result against a method; `status` reports the rollup +(`requirement` / `unverified` / `stale`). Evidence goes **stale** when the thing it +attested to has moved on — `status stale` surfaces exactly those, so re-verification +is targeted rather than wholesale. The `dossier` is the full advisory picture for one +requirement (statement, criteria, bindings, verification) — it reports, it does not +gate. diff --git a/tests/contracts/test_contract_fixtures.py b/tests/contracts/test_contract_fixtures.py index 482f6d0..a437ac3 100644 --- a/tests/contracts/test_contract_fixtures.py +++ b/tests/contracts/test_contract_fixtures.py @@ -5,6 +5,7 @@ from typing import Any, cast from tests.intent_coverage_contract import validate_intent_coverage +from tests.loomweave_contract import validate_loomweave_catalog from tests.preflight_contract import validate_preflight_facts from tests.wardline_contract import validate_wardline_peer_facts from tests.warpline_contract import validate_requirements_enrichment @@ -73,6 +74,7 @@ "intent/intent-coverage.json", "loomweave/identity-resolve.json", "loomweave/identity-sei.json", + "loomweave/catalog-degraded.json", "mcp/side-effect-metadata.json", "mcp/tool-inventory.json", "mcp/resource-inventory.json", @@ -396,6 +398,16 @@ def test_preflight_facts_fixture_contract() -> None: validate_preflight_facts({key: value for key, value in fixture.items() if key != "schema"}) +def test_loomweave_catalog_degraded_fixture_contract() -> None: + fixture = load_fixture("loomweave/catalog-degraded.json") + + assert fixture["schema"] == "weft.plainweave.loomweave_catalog.v1" + # The degraded envelope is validated through the SAME structural validator as live + # output (test_loomweave_contract.py), so the golden and the running tool cannot + # diverge without one test or the other failing. + validate_loomweave_catalog({key: value for key, value in fixture.items() if key != "schema"}) + + def test_intent_coverage_fixture_contract() -> None: fixture = load_fixture("intent/intent-coverage.json") diff --git a/tests/contracts/test_filigree_contract.py b/tests/contracts/test_filigree_contract.py new file mode 100644 index 0000000..675661a --- /dev/null +++ b/tests/contracts/test_filigree_contract.py @@ -0,0 +1,159 @@ +"""Contract test for Plainweave's FILIGREE seam (production blocker #5). + +Unlike the warpline/wardline/legis seams, Plainweave emits NO dedicated Filigree +``.v1`` payload: there is no filigree adapter, no ``filigree_*`` MCP tool, no +``weft.plainweave.filigree*.v1`` envelope. Plainweave's Filigree seam is two +plainweave-OWNED representations embedded in existing envelopes, plus a +deliberately-absent live join: + + (A) ``preflight_facts.v1``: ``open_linked_work`` is a RESERVED fact kind that the + local-only producer NEVER emits; Filigree linked-work absence is reported + in-band by the ``linked_work_facts_unavailable`` warning (no-silent-clean). + (B) ``requirement_dossier`` / ``trace_link``: ``filigree_issue`` is a canonical + TraceRef kind with two canonical relations; Plainweave stores an OPAQUE pointer + to a Filigree issue id and never reads/resolves/mutates live Filigree. + +This pins the honest representation, mirroring the warpline/wardline contract tests, +WITHOUT taking on Filigree's work lifecycle and WITHOUT a live Filigree call. The +live linked-work join (turning the warning into real ``open_linked_work`` facts) and +the ``gap_create_work`` write path are sibling-gated; see +``docs/handoffs/2026-06-29-filigree-linked-work-facts.md``. + +The scope-INDEPENDENT presence of ``linked_work_facts_unavailable`` (invariant 1) is +pinned by an extension to +``tests/test_mcp_read_surface.py::test_mcp_preflight_freshness_is_unavailable_for_empty_project_scope`` — +the two files together are the Filigree contract test. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any, cast + +import pytest +from tests.test_mcp_read_surface import approve_requirement, service_for + +from plainweave.errors import ErrorCode, PlainweaveError +from plainweave.mcp_surface import PlainweaveMcpSurface +from plainweave.models import TraceRef + +# Governance gate/decision tokens the advisory dossier must never introduce — checked both +# as KEYS and as VALUES. NOTE: unlike the peer-facts producers, the dossier legitimately +# carries requirement-lifecycle VALUES ("approved"/"rejected") and trace authority/state +# ("accepted") as DOMAIN state — those are not governance verdicts — so the off-the-shelf +# peer-facts value-token scanner (assert_no_preflight_verdicts) would false-positive here. +# Instead we whitelist exactly those lifecycle/authority values and still reject every OTHER +# governance token appearing as a value, so a verdict expressed as a value (e.g. "block") +# under a non-verdict key cannot slip through. +_VERDICT_KEYS = {"allow", "allowed", "block", "blocked", "verdict", "decision", "gate", "enforcement"} +_DOSSIER_LIFECYCLE_VALUES = {"approved", "rejected", "accepted"} +_VERDICT_VALUE_TOKENS = { + "allow", + "allowed", + "block", + "blocked", + "block_candidate", + "deny", + "denied", + "approved", + "rejected", + "pass_fail", + "verdict", +} - _DOSSIER_LIFECYCLE_VALUES + + +def data(envelope: dict[str, Any]) -> dict[str, Any]: + return cast(dict[str, Any], envelope["data"]) + + +def _assert_dossier_is_verdict_free(value: object) -> None: + """Reject governance verdicts by KEY anywhere and by VALUE (except the whitelisted + requirement-lifecycle / trace-authority values that are legitimate dossier state).""" + if isinstance(value, dict): + offenders = sorted(set(value) & _VERDICT_KEYS) + assert not offenders, f"verdict-like key in dossier: {offenders}" + for item in value.values(): + _assert_dossier_is_verdict_free(item) + elif isinstance(value, list): + for item in value: + _assert_dossier_is_verdict_free(item) + elif isinstance(value, str): + assert value.strip().lower() not in _VERDICT_VALUE_TOKENS, f"verdict-like value in dossier: {value}" + + +def test_filigree_linked_work_absence_is_warned_not_silently_clean(tmp_path: Path) -> None: + """No-silent-clean. The LOAD-BEARING pin is that ``linked_work_facts_unavailable`` is + present on every scope (it is the in-band signal of Filigree linked-work absence). The + second assertion (no ``open_linked_work`` fact) is a FORWARD-GUARD: ``open_linked_work`` + is reserved vocab the local-only producer never constructs today, so it must never appear + as an empty-but-ok fact list when a future live Filigree join lands (sibling-gated).""" + service = service_for(tmp_path) + requirement_id = approve_requirement(service) + surface = PlainweaveMcpSurface(tmp_path) + + for envelope in ( + surface.plainweave_preflight_facts_get(scope_kind="project"), + surface.plainweave_preflight_facts_get(scope_kind="explicit_requirements", requirement_ids=[requirement_id]), + surface.plainweave_preflight_facts_get(scope_kind="pending_diff"), + ): + preflight = data(envelope) + facts = cast(list[dict[str, Any]], preflight["facts"]) + assert all(fact["kind"] != "open_linked_work" for fact in facts) + warning_codes = {warning["code"] for warning in preflight["warnings"]} + assert "linked_work_facts_unavailable" in warning_codes + + +def test_filigree_issue_implements_work_for_trace_is_canonical_and_opaque(tmp_path: Path) -> None: + """The ``implements_work_for`` relation (previously untested; only ``resolves_gap`` + was exercised) is accepted and the Filigree issue id is stored as an OPAQUE + pointer surfaced verbatim in the dossier — no live Filigree resolution.""" + service = service_for(tmp_path) + requirement_id = approve_requirement(service) + + service.propose_trace_link( + TraceRef("filigree_issue", "PW-123"), + "implements_work_for", + TraceRef("requirement_version", f"{requirement_id}@1"), + actor="agent:codex", + ) + + dossier = service.requirement_dossier(requirement_id) + filigree_traces = [item for item in dossier.traces.items if item.from_ref.kind == "filigree_issue"] + assert len(filigree_traces) == 1 + assert filigree_traces[0].from_ref.id == "PW-123" # opaque, never resolved/rewritten + assert filigree_traces[0].relation == "implements_work_for" + + +def test_noncanonical_filigree_relation_is_rejected(tmp_path: Path) -> None: + """Only the two canonical ``filigree_issue`` relations are accepted; a + non-canonical relation is a VALIDATION error, never silently stored.""" + service = service_for(tmp_path) + requirement_id = approve_requirement(service) + + with pytest.raises(PlainweaveError) as exc_info: + service.propose_trace_link( + TraceRef("filigree_issue", "PW-123"), + "satisfies", # canonical for loomweave_entity, NOT for filigree_issue + TraceRef("requirement_version", f"{requirement_id}@1"), + actor="agent:codex", + ) + assert exc_info.value.code == ErrorCode.VALIDATION + + +def test_dossier_carrying_a_filigree_trace_is_verdict_free(tmp_path: Path) -> None: + """The advisory boundary: a dossier surfacing a Filigree trace introduces NO governance + verdict — by key OR by value — beyond the whitelisted requirement-lifecycle/authority + values that are legitimate domain state (see the _VERDICT_VALUE_TOKENS note above).""" + service = service_for(tmp_path) + requirement_id = approve_requirement(service) + service.propose_trace_link( + TraceRef("filigree_issue", "PW-123"), + "implements_work_for", + TraceRef("requirement_version", f"{requirement_id}@1"), + actor="agent:codex", + ) + + envelope = PlainweaveMcpSurface(tmp_path).plainweave_requirement_dossier_get(requirement_id) + + assert envelope["ok"] is True + _assert_dossier_is_verdict_free(envelope) diff --git a/tests/contracts/test_loomweave_contract.py b/tests/contracts/test_loomweave_contract.py new file mode 100644 index 0000000..3fc8865 --- /dev/null +++ b/tests/contracts/test_loomweave_contract.py @@ -0,0 +1,105 @@ +"""Contract test for ``weft.plainweave.loomweave_catalog.v1`` (production blocker #3: +explicit degraded peer-state envelope for the live Loomweave adapter). + +The Loomweave identity-resolution + catalog BEHAVIOR is already implemented and tested +(live HTTP resolve + capability probe + closed-vocab degraded codes + the SEI §8 oracle ++ adapter/producer degraded tests). The remaining PDR-014-parity gap was the absence of +a PRODUCER-side contract pinning the catalog envelope's DEGRADED state. This freezes it, +mirroring the wardline/preflight contract tests: the committed golden and the live +producer are validated through the SAME structural validator, so they cannot diverge. +No production code changes — the behavior already exists; this is the parity contract. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, cast + +import pytest +from tests.loomweave_contract import assert_no_loomweave_verdicts, validate_loomweave_catalog +from tests.loomweave_test_utils import seed_loomweave_catalog + +from plainweave.mcp_surface import PlainweaveMcpSurface +from plainweave.store import migrate + +FIXTURE = Path(__file__).resolve().parents[1] / "fixtures" / "contracts" / "loomweave" / "catalog-degraded.json" + + +def _surface(tmp_path: Path) -> PlainweaveMcpSurface: + migrate(tmp_path / ".plainweave" / "plainweave.db", project_key="AUTH") + return PlainweaveMcpSurface(tmp_path) + + +def _data(envelope: dict[str, Any]) -> dict[str, Any]: + return cast(dict[str, Any], envelope["data"]) + + +def _minimal_unavailable() -> dict[str, Any]: + return { + "items": [], + "limit": 50, + "offset": 0, + "has_more": False, + "next_offset": None, + "adapter_status": { + "status": "unavailable", + "db_path": ".weft/loomweave/loomweave.db", + "http_url": None, + "identity_http": "not_configured", + "sei_supported": False, + }, + "degraded": [{"code": "loomweave_db_missing", "message": "missing"}], + "coverage": { + "public_surface_tags": [], + "present_tags": [], + "absent_tags": [], + "complete": False, + "present_plugins": [], + }, + } + + +def test_validator_accepts_a_minimal_unavailable_payload() -> None: + validate_loomweave_catalog(_minimal_unavailable()) + + +def test_validator_rejects_a_silent_clean_unavailable_page() -> None: + """The cardinal violation: an unavailable adapter returning an empty page with NO + degraded reason — a clean-empty read that hides the degradation.""" + payload = _minimal_unavailable() + payload["degraded"] = [] + with pytest.raises(AssertionError): + validate_loomweave_catalog(payload) + + +def test_validator_rejects_a_verdict_token() -> None: + with pytest.raises(AssertionError): + assert_no_loomweave_verdicts({"status": "blocked"}) + + +def test_committed_degraded_golden_matches_the_validator() -> None: + fixture = json.loads(FIXTURE.read_text(encoding="utf-8")) + assert fixture["schema"] == "weft.plainweave.loomweave_catalog.v1" + # Validated through the SAME structural validator as live output, so the golden and + # the running tool cannot diverge without one test or the other failing. + validate_loomweave_catalog({key: value for key, value in fixture.items() if key != "schema"}) + + +def test_live_unavailable_adapter_envelope_is_valid_and_in_band_degraded(tmp_path: Path) -> None: + """The real producer over a root with no Loomweave db: an unavailable adapter, an + empty page, and a degraded reason carried in-band (no silent-clean).""" + data = _data(_surface(tmp_path).plainweave_loomweave_catalog_list(limit=50, offset=0)) + validate_loomweave_catalog(data) + assert data["adapter_status"]["status"] == "unavailable" + assert data["items"] == [] + assert data["degraded"], "unavailable adapter must report a degraded reason in-band" + + +def test_live_healthy_catalog_envelope_is_valid(tmp_path: Path) -> None: + surface = _surface(tmp_path) + seed_loomweave_catalog(tmp_path) + data = _data(surface.plainweave_loomweave_catalog_list(limit=50, offset=0)) + validate_loomweave_catalog(data) + assert data["adapter_status"]["status"] == "available" + assert data["items"] diff --git a/tests/contracts/test_preflight_facts_wire_golden.py b/tests/contracts/test_preflight_facts_wire_golden.py index 711208a..08adbb3 100644 --- a/tests/contracts/test_preflight_facts_wire_golden.py +++ b/tests/contracts/test_preflight_facts_wire_golden.py @@ -4,10 +4,13 @@ ``tests/fixtures/contracts/legis/preflight-facts.json`` is the preflight-facts ``schema + data`` payload plainweave emits from ``PlainweaveMcpSurface.plainweave_preflight_facts_get`` — the producer named in -ADR-006 (Status: Accepted). Legis is the intended CONSUMER of this envelope, but -the consumer side does NOT exist yet and legis is ringfenced, so this row is -PRODUCER-SIDE ONLY: it freezes plainweave's own produced bytes and ties them to -the live producer. There is no consumer oracle and no cross-repo drift check. +ADR-006 (Status: Accepted). Legis is the CONSUMER of this envelope. As of +2026-06-29 a legis-side consumer + constructed oracle of this contract exists, but +per the federation enrich-only discipline that read ships solo and creates NO +plainweave obligation — plainweave OWNS the contract and legis conforms to it. This +row therefore stays PRODUCER-SIDE ONLY by design: it freezes plainweave's own +produced bytes and ties them to the live producer; plainweave runs no cross-repo +drift gate against the legis copy (legis re-pins on its side). PLAINWEAVE IS THE AUTHORITY for this seam — it OWNS the preflight-facts shape via ``PlainweaveMcpSurface.plainweave_preflight_facts_get``. The protection is a diff --git a/tests/fixtures/contracts/loomweave/catalog-degraded.json b/tests/fixtures/contracts/loomweave/catalog-degraded.json new file mode 100644 index 0000000..bc0b22b --- /dev/null +++ b/tests/fixtures/contracts/loomweave/catalog-degraded.json @@ -0,0 +1,28 @@ +{ + "schema": "weft.plainweave.loomweave_catalog.v1", + "items": [], + "limit": 50, + "offset": 0, + "has_more": false, + "next_offset": null, + "adapter_status": { + "status": "unavailable", + "db_path": ".weft/loomweave/loomweave.db", + "http_url": null, + "identity_http": "not_configured", + "sei_supported": false + }, + "degraded": [ + { + "code": "loomweave_db_missing", + "message": "Loomweave database is missing: .weft/loomweave/loomweave.db" + } + ], + "coverage": { + "public_surface_tags": ["cli-command", "entry-point", "exported-api", "http-route"], + "present_tags": [], + "absent_tags": [], + "complete": false, + "present_plugins": [] + } +} diff --git a/tests/loomweave_contract.py b/tests/loomweave_contract.py new file mode 100644 index 0000000..532c24e --- /dev/null +++ b/tests/loomweave_contract.py @@ -0,0 +1,116 @@ +"""Single source of truth for the ``weft.plainweave.loomweave_catalog.v1`` shape. + +Mirrors ``tests/wardline_contract.py`` so the committed degraded golden and the live +``plainweave_loomweave_catalog_list`` producer cannot drift in shape or degraded-state +invariants (both are run through this one structural validator). The catalog envelope +carries NO ``severity`` field, so the no-verdict scan uses the warpline-style scanner +(no severity allowlist branch). + +The cardinal invariant is no-silent-clean: when the Loomweave adapter is +``unavailable`` (db/schema missing) the empty page MUST carry a non-empty +``degraded[]`` reason — an empty ``items`` list must never read as a clean +"nothing here". The Loomweave identity-resolution + catalog BEHAVIOR is already +implemented and tested (live HTTP resolve + capability probe + closed-vocab degraded +codes + the SEI §8 oracle + adapter/producer degraded tests); this validator freezes +the PRODUCER-side contract that was the remaining PDR-014-parity gap. +""" + +from __future__ import annotations + +from typing import Any + +LOOMWEAVE_ADAPTER_STATUSES = {"available", "unavailable"} +LOOMWEAVE_CATALOG_DATA_KEYS = { + "items", + "limit", + "offset", + "has_more", + "next_offset", + "adapter_status", + "degraded", + "coverage", +} +LOOMWEAVE_ADAPTER_STATUS_KEYS = {"status", "db_path", "http_url", "identity_http", "sei_supported"} +LOOMWEAVE_COVERAGE_KEYS = {"public_surface_tags", "present_tags", "absent_tags", "complete", "present_plugins"} +LOOMWEAVE_ITEM_KEYS = { + "sei", + "locator", + "kind", + "tags", + "source", + "content_hash", + "content_hash_at_attach", + "public_signal", + "briefing_blocked", + "lineage_status", + "freshness", + "observed_at", + "degraded", + "signals", +} + +_VERDICT_KEYS = {"allow", "allowed", "block", "blocked", "verdict", "decision", "gate", "enforcement"} +_VERDICT_VALUE_TOKENS = { + "allow", + "allowed", + "block", + "blocked", + "block_candidate", + "deny", + "denied", + "approved", + "rejected", + "pass_fail", + "verdict", +} + + +def assert_no_loomweave_verdicts(value: object) -> None: + """Reject gate semantics by key and by string value (the catalog has no severity field).""" + if isinstance(value, dict): + assert _VERDICT_KEYS.isdisjoint(value), f"verdict-like key in {sorted(value)}" + for item in value.values(): + assert_no_loomweave_verdicts(item) + elif isinstance(value, list): + for item in value: + assert_no_loomweave_verdicts(item) + elif isinstance(value, str): + assert value.strip().lower() not in _VERDICT_VALUE_TOKENS, f"verdict-like value: {value}" + + +def validate_loomweave_catalog(payload: dict[str, Any]) -> None: + """Structurally validate a loomweave-catalog *data* payload (no envelope wrapper).""" + assert set(payload) == LOOMWEAVE_CATALOG_DATA_KEYS, f"section drift: {sorted(payload)}" + + status_block = payload["adapter_status"] + assert set(status_block) == LOOMWEAVE_ADAPTER_STATUS_KEYS, f"adapter_status drift: {sorted(status_block)}" + assert status_block["status"] in LOOMWEAVE_ADAPTER_STATUSES + + degraded = payload["degraded"] + assert isinstance(degraded, list) + for entry in degraded: + assert {"code", "message"}.issubset(entry), f"degraded entry drift: {sorted(entry)}" + + items = payload["items"] + assert isinstance(items, list) + + coverage = payload["coverage"] + assert set(coverage) == LOOMWEAVE_COVERAGE_KEYS, f"coverage drift: {sorted(coverage)}" + assert isinstance(coverage["complete"], bool) + + # The cardinal no-silent-clean invariant: an unavailable adapter never returns a + # clean-empty page — it reports its degradation in-band AND never advertises any + # positive coverage/pagination signal that would read as "we have data" while down. + if status_block["status"] == "unavailable": + assert items == [], "unavailable adapter must not enumerate items" + assert degraded, "unavailable adapter must report a degraded reason in-band (no silent-clean)" + assert coverage["complete"] is False, "unavailable adapter must not claim complete coverage" + assert coverage["present_tags"] == [], "unavailable adapter must not claim present tags" + assert coverage["present_plugins"] == [], "unavailable adapter must not claim present plugins" + assert payload["has_more"] is False, "unavailable adapter must not claim more pages" + assert payload["next_offset"] is None, "unavailable adapter must not advertise a next page" + + for item in items: + assert set(item) == LOOMWEAVE_ITEM_KEYS, f"item key drift: {sorted(item)}" + + assert_no_loomweave_verdicts(payload) diff --git a/tests/test_mcp_read_surface.py b/tests/test_mcp_read_surface.py index af48e93..9479ba0 100644 --- a/tests/test_mcp_read_surface.py +++ b/tests/test_mcp_read_surface.py @@ -365,6 +365,16 @@ def test_mcp_preflight_freshness_is_unavailable_for_empty_project_scope(tmp_path assert preflight["facts"] == [] assert preflight["freshness"] == "unavailable" + # Filigree seam, scope-independent no-silent-clean (production blocker #5, paired with + # tests/contracts/test_filigree_contract.py): even on an EMPTY scope, linked-work absence + # is reported in-band as `linked_work_facts_unavailable`, never an empty-but-ok result. + warnings = cast(list[dict[str, Any]], preflight["warnings"]) + linked_work = next(w for w in warnings if w["code"] == "linked_work_facts_unavailable") + assert "Filigree" in linked_work["message"] + assert linked_work["severity"] == "info" + assert linked_work["freshness"] == "unavailable" + assert linked_work["provenance"]["inputs"] == [] + def test_mcp_preflight_soft_degrades_an_unresolvable_requirement_id(tmp_path: Path) -> None: service = service_for(tmp_path) @@ -398,6 +408,32 @@ def test_mcp_preflight_labels_corpus_fallback_requirements_nearby_not_touched(tm assert "requirement_touched" not in fact_kinds +def test_mcp_preflight_emits_orphaned_entity_link_for_a_stale_entity_trace(tmp_path: Path) -> None: + """Production blocker #4 test-hardening: orphaned_entity_link emits but had zero + behavioral coverage. A scoped requirement whose entity trace went stale/orphaned must + surface an orphaned_entity_link warn fact citing the offending trace.""" + service = service_for(tmp_path) + requirement_id = approve_requirement(service) + stale = service.create_trace_link( + TraceRef("file_ref", "src/legacy_auth.py"), + "fragile_satisfies", + TraceRef("requirement_version", f"{requirement_id}@1"), + actor="human:john", + authority="accepted", + ) + service.mark_trace_stale(stale.id, actor="agent:codex", reason="content changed") + surface = PlainweaveMcpSurface(tmp_path) + + preflight = data( + surface.plainweave_preflight_facts_get(scope_kind="explicit_requirements", requirement_ids=[requirement_id]) + ) + + orphaned = [fact for fact in preflight["facts"] if fact["kind"] == "orphaned_entity_link"] + assert len(orphaned) == 1 + assert orphaned[0]["severity"] == "warn" + assert stale.id in orphaned[0]["evidence_refs"] + + def test_mcp_entity_intent_context_returns_peer_ready_entity_facts(tmp_path: Path) -> None: service = service_for(tmp_path) seed = seed_loomweave_catalog(tmp_path)