diff --git a/CHANGELOG.md b/CHANGELOG.md index d3be6a0..21bf533 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -106,9 +106,11 @@ repository still gets a decision, never by following the link; no release carrie - :memo: docs(openspec): fix 4 P1 design defects found by independent review of PR #47 - :memo: docs(openspec): close F7/F8/F10/F11/F17/F18 from the PR #47 re-review - :memo: docs(openspec): specify Scorecard fuzzing and Best Practices residuals +- :memo: specs(p5-ex): decompose complex in-tree examples epic ### Fixes - :bug: fix(ci): pin ci-audit-test in the AUD-S18 check-stage list +- :bug: specs(p5-ex): make S10 schema freeze and S07 fence non-vacuous ### Testing - :white_check_mark: test(release): anchor the D-120 note check on its header sentence, not the bare token diff --git a/docs/decisions/decisions.md b/docs/decisions/decisions.md index 2bb41c9..657a52d 100644 --- a/docs/decisions/decisions.md +++ b/docs/decisions/decisions.md @@ -147,3 +147,4 @@ project/process decisions. | D-140 | 2026-08-10 | **E10 (GitHub adapter + Actions entrypoint) is UNLOCKED by direct operator instruction; ADR-0021 governs the seam and `openspec/specs/p5-e10-github-forge/spec.md` decomposes it into 19 stories (S00, the addressing & representation model, was added after an adversarial review found two P0 representation defects).** E10 was **Locked** under D-012 ("unlocks with a named consumer"), and that lock was reaffirmed twice — D-017 named the consumer and still said "E10/E13 stay locked", D-019 restated "GitHub + remote packs locked". No prior decision authorized it, so the operator's instruction this session is a NEW unlock event, recorded here BEFORE any spec text or code exists (AGENTS.md rule 6). **What this authorizes**: the GitHub forge adapter, the multi-adapter seam work it depends on, and forge selection in `run`/`doctor`. **What it does NOT authorize**: E13 remote packs (still Locked under D-012 — the "named consumer" reasoning is untouched for that tier), E12 `serve`, E14 CRD, or any third forge / plugin-forge protocol (ADR-0021 Option D, rejected — no named consumer). **Scope of v1 GitHub**: behavioural parity for the GATE, not device-for-device parity (dossier §3, OQ-7/OQ-18 — required-conversation-resolution carries acknowledgement, `REQUEST_CHANGES` reserved for block); the three known deltas (review dismissal, auto-merge revoke, merge queue) are modelled as capabilities, and where GitHub cannot prove what the gate needs the adapter reports the gap and NEVER arms — the same shape as GitLab free tier. **Why a seam epic and not just an adapter**: the 2026-08-09 audit's ARCH-18/ARCH-19 found `docs/planning/design-notes/e10-forge-port-lift.md` under-scopes the epic, and that the conformance suite cannot be run by a second adapter because all 1,155 lines live in `_test.go` files Go cannot import (the package totals 1,166 including the non-test `doc.go`; the `~1,166` figure in the source design note was corrected on 2026-08-10) — so a GitHub adapter written today would be developed against no executable contract and D-084's `github-deferred` catalog rows would be unflippable by construction. The original ARCH-18/ARCH-19 finding text is NOT in the repo (only the one-line summary at `agent-context/PROJECT-AUDIT-2026-08-09.md:412` survives); the two design buckets were therefore RE-DERIVED as (A) no neutral capability model — the GitHub dossier §4 names eleven flags the port needs while `probeCapabilities` reads three project fields and `capabilityGap` is computed in GitLab terms — and (B) no port-level transport/auth policy — GitHub thread resolution is GraphQL-only and needs PAT-vs-App auth, while bounded reads/pagination caps (AUD-S10) and idempotent-GET retry/deadlines (AUD-S11) live inside the GitLab client. Both are recorded as a re-derivation, not as a citation. **OPEN SUB-QUESTION (operator)**: whether the Actions entrypoint (E10-S16) stays in scope — `later-phases.md` titles the epic "GitHub adapter + Actions entrypoint", but the action is packaging on top of an adapter and is the one story whose absence leaves everything else useful; default is to keep it last and independently droppable. **Consequence to watch (E10-S04)**: forcing every capability to be stated explicitly may turn a GitLab arming path that passes today into an honest capability gap — a user-visible behaviour change that gets its OWN decision row and changelog entry, never absorbed silently into "E10 refactor". Revert: re-lock E10 in `later-phases.md`, delete the spec directory; nothing here is published API (`forge.RunPort` is `internal/`), so no compatibility window applies. **✅ OPEN SUB-QUESTION CLOSED (operator, 2026-08-10): option (a) — the Actions entrypoint (E10-S16) STAYS in E10's scope**, kept last and independently droppable; `later-phases.md` titles the epic "GitHub adapter + Actions entrypoint", so cutting it would have needed the plan text corrected. E10-S16 is no longer operator-gated. | | D-141 | 2026-08-10 | **E11 (Rego complex-rule backend) implementation is UNLOCKED by direct operator instruction; `openspec/specs/p5-e11-rego-backend/spec.md` decomposes it into 13 stories under ADR-0002 v2.** E11's CONTRACT was already unlocked by D-017; what was gated was IMPLEMENTATION, twice: "after Phase 4" (satisfied — the Phase-4 adoption gate closed with D-042) and, per D-017, **evidence-based per rule** ("each ported rule tries CEL first, the backend is built when a concrete rule demonstrably exceeds the tier-1 ceiling"). This row records the operator lifting that per-rule evidence gate. **What it does NOT waive**: the DESIGN need the gate was protecting — E11-S01 still requires a written tier-1 ceiling document with concrete rules per shape (multi-pass, cross-manifest, set-difference, graph-relationship), and any shape found CEL-expressible is struck from scope. **What it does NOT authorize**: WASM or gRPC predicate backends (still Locked under D-012 — this unlocks Rego only); domain-aware joins and in-process Go rule plugins (D-017 DECLINED both permanently — not deferred); giving Rego any control over aggregation, effects, or points (ADR-0002 v2 boundary); any `EvaluationInput` change. **Two constraints found during design that shape the epic**: (1) **E11 is the first epic whose DoD is `git diff schemas/` != 0.** P3-E1-S02's backend-neutrality guarantee ("no field naming a predicate backend anywhere in the schema") applies to `EvaluationInput` and HOLDS — no decision contract changes — but `schemas/policy/v1alpha1/merge-policy.schema.json` defines the predicate leaf as `additionalProperties:false, required:["cel"]`, so a `rego:` leaf IS a policy-schema change. `API_STABILITY.md:19` permits exactly this within `v1alpha1` as an announced additive change with an openspec change and no `apiVersion` bump; the change is backward-compatible and deliberately forward-INcompatible (an older binary rejects a `rego:` leaf by strict-decode, which is the correct direction — it must not silently ignore a rule it cannot evaluate). A reviewer applying the previous epics' `git diff schemas/ == 0` habit will flag the correct change as a violation; E11-S02-04 scopes the drift guard rather than deleting it. (2) **The reflexive safety measure violates AGENTS.md rule 7.** Rego ships `time.now_ns()`, `rand.intn()`, and `http.send()`, all of which are denied structurally by the D-013 capability sandbox (E11-S04, compile-time failure, golden allowlist so an OPA upgrade cannot widen it unnoticed) — but bounding evaluation with a WALL-CLOCK TIMEOUT would itself be a rule-7 violation, making the same policy over the same ChangeSet decide differently on a slow runner. E11-S06 therefore requires a machine-independent evaluation budget, and exceeding it must be a PROCESS ERROR that can never be a policy outcome; "timeout → BLOCK" is explicitly rejected as machine-dependent while merely LOOKING fail-closed. **Also fixed by the spec**: zero violations NEVER proves a required obligation (`later-phases.md`'s explicit polarity rule; E11-S07-02 tests the failing polarity). **🔴 BLOCKING OPEN SUB-QUESTION (operator) — adopting OPA narrows rule 7's MECHANISM, and both existing purity gates would miss it.** Verified during the design session: `internal/core/purity_test.go` flags only each guarded file's OWN imports (`math/rand`, `crypto/rand`, `net`, `net/*`) and selectors (`os.Getenv`, `time.Now`), and `.golangci.yml`'s `pure-tree` depguard is `list-mode: lax`, deny-only, over DIRECT imports — **neither is transitive**. A file in `internal/core/**` importing `github.com/open-policy-agent/opa/rego` therefore passes both gates GREEN while transitively linking `net/http` (OPA ships the `http.send` builtin), defeating the `net` deny that encodes D-123 / rule 7 invisibly. S04's capability sandbox makes `http.send` uncallable FROM POLICY — the real threat — but the guarantee's nature changes from "the network stack is not linked into the decision path" (structural, greppable) to "linked but unreachable from policy" (behavioural, resting on a capability file). That is a hard-rule change and cannot be made by a story. Options: **(d1)** accept the narrowing with an ADR-0011/rule-7 amendment plus a transitive `go list -deps` purity check allowlisting exactly the OPA path (RECOMMENDED); **(d2)** keep the guarded tree OPA-free by injecting an evaluator from `cmd/assent` — honest, but it moves part of the decision path outside the tree rule 7 guards; **(d3)** drop OPA (a hand-rolled evaluator would be far worse; rejected unless d1 and d2 are). **E11-S04 is blocked on this answer** (it decides the evaluator's package and its gate); S01–S03 are unblocked. Separately on supply chain: OPA is a large dependency with a large transitive tree on a project shipping cosign/SLSA provenance, `govulncheck`, and Scorecard — recommended default is accept-and-pin, with S03 recording the binary-size delta. Revert: re-assert the D-017 per-rule evidence gate, delete the spec directory, leave the `# locked: D-012` quarantine marker in place. **✅ PARTIAL ANSWER (operator, 2026-08-10): `github.com/open-policy-agent/opa` is ACCEPTED as a dependency and pinned** — this closes the *supply-chain* half of judgment call (d) and rejects **(d3) drop OPA**. **🔴 The MECHANISM half is still OPEN and still blocks E11-S03 and E11-S04: (d1) accept the rule-7 narrowing explicitly (capability-enforced, plus a transitive purity guard) vs (d2) keep the guarded tree OPA-free behind an injected interface.** These are not the same question and "accept and pin" does not settle it — **(d2) also accepts and pins OPA**; it differs on *where the evaluator lives* and *which gate enforces rule 7*. Reading the dependency answer as settling the mechanism would silently choose (d1), i.e. narrow a hard rule by merging a story, which rule 6 forbids. Whichever way it resolves needs an **ADR-0011/rule-7 amendment plus its own D-row** (REQ-E11-S04-04). **Also corrected here (2026-08-10, independent review of PR #45):** the spec originally said E11-S01–S03 were unblocked while (d) was pending. **False — E11-S03 is the story that *effects* the narrowing** (REQ-E11-S03-03 adds OPA to `go.mod`; its Test paths sit in the D-123 guarded tree), and because *neither* purity gate is transitive it would have landed green. S03 is now explicitly blocked on (d). The epic also gains **E11-S00**, a deterministic-budget feasibility spike built in a **nested throwaway module** (no `go.work` exists, so Go excludes it from the root module's `./...`) so the question can be answered without adopting the dependency (d) has not authorised; E11 is therefore **14 stories, S00–S13**, and E11-S02 is blocked on S00 because an observe-only `rego:` leaf is a different published contract from a gating one. | | D-142 | 2026-08-10 | **Two public demo repositories are DESIGNED spec-first as `openspec/specs/p5-dem-demo-repos/spec.md` (15 stories, S00–S14); the answer to "do sample repos already exist" is NO.** Operator asked for a testable sample GitLab repo and sample GitHub repo, two designed self-service patterns, and an assessment of user-resolution extensibility. **Ground truth established before designing:** `examples/repos/**` are content layouts with no `.assent/` tree, `examples/packs/**` are policy trees with no repo identity, `gitlab.com/konrad.heimel/assent-lab` is the operator's personal D-042 adoption-proof project (not a showcase), and GitHub presence is zero because E10 is spec-only (D-140). **The constraint that shapes the epic: assent cannot run on a GitHub PR today**, so the design commits to a **two-tier demo contract** stated on each repo's front page — **tier 1** (`git clone && assent test .`, forge-independent, no token/network/IdP, deliverable now on both repos via the E6 harness) and **tier 2** (live MR/PR: GitLab now, GitHub blocked on E10-S18). Tier 1 is the "everybody can see it" claim. **Split by governance shape, not by tool** — otherwise two repos demonstrate the same three archetypes twice: **repo 1 `assent-demo-platform` (GitLab)** = *request a platform resource* (kafka-topic, **kafka-acl**, **argocd-application**), distinguished by being **referential** — an ACL names a topic and a principal, an App names a project/namespace/source, so the decision cannot be made from the changed file alone, which is exactly what `builtin/resource-owner` (E5-S08, REF-GAP-1) and `builtin/repo-file` (E5-S07) exist for and what no shipped example exercises end to end; **repo 2 `assent-demo-terraform` (GitHub)** = *change infrastructure* (**tf-module-instance** raw HCL, tf-vars, **tf-backend** deliberately ungoverned→REVIEW per D-063), distinguished by **magnitude/blast radius** plus the opaque-change fallback. Repo 2 also becomes **E10-S18's live adoption target**, so the epic needing a real GitHub repository gets one instead of minting a throwaway. **Judgment calls: (a)** demo trees live in-tree under `examples/demo//` and are mirrored out — authoring only in the public repos puts them outside `task check` and an ungated example rots; **(b)** platform→GitLab / terraform→GitHub is idiom plus the S18 target, and repo 2's README states the pending live tier in its first screenful; **(c)** no Keycloak/Entra/LDAP builtins (see OQ-32 rationale); **(d) 🔴 OPERATOR — creating public repos under the `PlatformRelay` org is outward-facing and NOT covered by AGENTS.md rule 2's push grant to `PlatformRelay/assent`**, so DEM-S13 is operator-gated and S01–S12 are unblocked by it; **(f)** DEM-S05's cross-manifest rules double as the **E11-S01 tier-1 ceiling probe** D-141 requires (cross-manifest is one of its four named shapes), feeding evidence rather than hypotheticals — and DEM-S05 does not wait for E11. **On user-resolution extensibility, the verified verdict is: the seam is genuinely good and extending it needs NO core change and NO fork** — the wire contract is frozen and published (`schemas/provider/v1alpha1/{request,response}.schema.json`, `additionalProperties:false`, fail-closed state machine with `resolved ⇒ value+expiresAt` encoded in-schema), `API_STABILITY.md:20` binds protocol majors, and extension is a **two-file repo-side change read from the PROTECTED TARGET REF** (`.assent/config.yaml` + `.assent/providers/.json`, per `cmd/assent/provider_host.go:61,81-88`), so an MR author cannot redefine their own fact semantics. **But four gaps stand between that and "easily extendable", and the spec states all four rather than demoing around them: G1 (P1)** — `find examples -type d -name providers` returns NOTHING; all three shipped packs declare `providers:` with no host declaration, so `provider_host.go:83-87` silently `continue`s and **every shipped example's provider resolves to nothing**, with the required path documented in no doc (DEM-S01 fixes examples + docs + a both-polarity gate); **G2 (P1, architectural → OQ-32)** — `CallHTTP` sets only `Content-Type`, the repo-side provider schema is `additionalProperties:false` over `{type,url,failure}`, and `ScrubEnv`/`ScrubArgv` refuse `(?i)TOKEN\|SECRET` names even when explicitly configured, so **no provider can call Entra ID or Keycloak directly** — both need a bearer token; the only working shape is a **broker** holding the IdP credential itself, which is defensible (ADR-0015 §7 intact, Spike C's hostile-provider isolation proof preserved) but undocumented and narrows what `docs/vision.md:67` promises; **G3 (P2)** — exec providers force adopters to recompute a `sha256:` pin per release, so the reference provider is HTTP; **G4** — ADR-0004 §1 listed OIDC/Keycloak/LDAP builtins that never shipped, and the epic **recommends never shipping them**: each is an unbounded auth surface entering the decision path's dependency tree for no capability HTTP lacks, and a published contract plus a copyable reference implementation is the stronger answer because it works for the adopter whose IdP nobody has heard of (`docs/architecture/c4-context.md:19` is already the only place telling this truth). The demo therefore ships a **four-layer ladder** (L0 fixtures → L1 `repo-file`/`resource-owner`, no network → L2 `forge-groups` → L3 your IdP broker) with a runnable `contrib/providers/idp-groups/` reference carrying Entra and Keycloak adapters, promoted from `hack/spikes/provider/toy.go`; the L2→L3 step is **two files changed, zero lines of assent rebuilt**. **No new ADR proposed** (examples/docs/contrib only) — unless OQ-32 resolves to adding a credential channel, which would need its own. **🔴 SECOND FINDING, more consequential than the GitHub gap and recorded here because rule 6 forbids deciding it silently: `(class, environment)` binding routing is NOT WIRED, so a multi-class demo repo fails closed in BOTH tiers on BOTH forges.** Found by reading the code against the design. `cmd/assent/run.go:493` `selectBinding` fails closed on any RulesetBinding with >1 binding — its own comment says the Config class-matcher *"is not wired in this lane"*; `cmd/assent/test.go:366` `selectBindingForTest` collapses a multi-binding document to its STRICTEST binding (D-060, fail-safe direction) but **fails closed when two bindings differ in `class`, `packs`, or `require[]`**. Both demo repos have three classes with different `require[]`, so as designed they fail closed on `assent run` AND `assent test`. **Why nobody noticed:** D-060 itself recorded the shipped packs' dev/prod split as *"empirically decision-neutral for the corpus"* — every shipped example is single-class with a decision-neutral environment split, so the collapse is invisible and the matcher is never missed. The demo is the first artifact needing real routing. **Consequence:** the epic gains **DEM-S00** ahead of everything (`[engine-grade · maintainer LGTM]` — it changes decision routing at the `cmd/assent` seam). The *matcher* is cheap — `policy.Config` already carries `Environments`/`Classes` as `NamedMatch` with `Match PathMatch` (`internal/core/policy/policy.go:196-228`), populated in every example pack, and `internal/glob.Match` already implements `*`/`**` — so `internal/core` stays byte-unchanged. **🔴 CORRECTION (2026-08-10, independent review of PR #47): this row originally said the data "verified to reach the call sites — no loader work". That was FALSE at both call sites and the correction changes S00's size.** On `assent run`, `selectBinding` is `run.go:219` but the Config load is `run.go:224-237` and is guarded by `if cfg.config != ""` — Config is loaded *after* selection and only when `--config` is supplied, so S00 must move the load ahead of selection **and decide the `--config`-absent behaviour explicitly** (it must fail closed, never fall back to a collapse or to binding zero). On `assent test` there is **no Config at all**: `test.go:67` uses `catalogue.LoadFromDir`, and `internal/catalogue/catalogue.go:124-127` states Config is *"deliberately absent; a later story that needs config-derived fields adds it then"* — `Input` is `{Packs, Bindings}`. S00 **is** that later story, so extending `catalogue.Input` is a deliberate **E6 contract change**, not wiring. `Config.Classes` has zero production readers today — it is precisely parsed-and-discarded. This is why tier 1 is the risk: an implementer who wires only `run.go` leaves `assent test` — the epic's primary deliverable — failing closed while the story reads done. S00 preserves fail-closed where routing is genuinely ambiguous (two matching classes, or a `(class, environment)` pair with no covering binding — both refuse, never guess), keeps `compare.go:407` fenced as D-060 left it, and **DELETES the `selectBindingForTest` collapse rather than leaving it as a fallback** (two live paths is how a fail-closed guarantee is lost quietly). `internal/core` byte-unchanged, `git diff schemas/` == 0. **Also newly visible: the epic's clearest demonstration — the identical diff that APPROVEs in `dev` and REVIEWs in `prod` — is impossible in either tier today**, which is why it becomes S00's sharpest acceptance test. **Third, smaller correction folded in:** D-063 covers the unmatched whole-file DELETE only (`aggregate.unmatchedDelete`); an unmatched EDIT is governed by ADR-0008 **§1**'s implicit `unclassified` class that no vouch rule may match (**correction, 2026-08-10, independent review of PR #47: the original text of this row cited a non-existent "ADR-0008 §27" — ADR-0008 has §1–§4 plus two amendments and no §27. The substance was right and the pointer was fabricated: the implicit class is §1 (`internal/core/classify/classify.go:18-20`) and the no-vouch-rule-may-match guarantee is enforced by `classify.ValidateRouting` / `ErrReservedClassRouting` at `classify.go:127-145`, whose own error message cites the ADR-0008 amendment + ADR-0015 §1**). **Two further corrections from the same review, recorded because they narrow what this row promises:** (i) **repo 1's headline differentiator is demonstrable only at tier 2.** The evaluation unit is ONE file — `assent run` takes a single `--subject file:` (`cmd/assent/run.go:266`) and diffs it alone (`:289`), and `adoptertest.Case` is singular — so the *referential* / cross-manifest claim above (an ACL naming a topic deleted in the same changeset) cannot be shown at tier 1 at all. It moves to **DEM-S14**, which is `[infra-gated · operator]`, via `builtin/repo-file`'s merged-result checkout. (ii) **the four-layer ladder's L1 rung does not run at tier 1 either**: `cmd/assent/test_provider_fence_test.go:69-80` fails the build if `assent test` touches the live provider host, so tier-1 facts come solely from `adoptertest.MapFacts` — a reader at tier 1 sees a fixture literal, not ownership resolved from repo files. Tier 1 therefore remains an honest and complete demonstration of **L0**, and the "everybody can see it" claim stands only at that rung., and `run.go:487-492` records that routing as unwired too — so DEM-S10 must DETERMINE the unmatched-edit behaviour by running the code rather than assert it, and anything other than a refusal or REVIEW is a finding to log, not a demo feature. Revert: delete `openspec/specs/p5-dem-demo-repos/`, this row, and OQ-32. Note the revert is not free of consequence — DEM-S00 describes a real routing gap that outlives this epic; reverting the design does not close it, and D-060 remains its standing record. | +| D-143 | 2026-08-15 | **Complex examples/tests/docs are a dedicated epic P5-EX, not an implementation of P5-DEM.** Operator asked for better and more complex in-tree examples, adopter tests, and documentation (multi-field nested resources; YAML + JSON + HCL + tfvars). The E1 adapters already exist; the thin layer is packs/fixtures/docs. **Options:** (A) implement P5-DEM after adding AUD2-S06-style `Test:`/`Verify:`/`Level:` annotations to DEM REQs — rejected: DEM is public org demo repos + engine-grade `(class, environment)` routing (DEM-S00) + provider brokers + operator-gated publish (DEM-S13), a different goal, and DEM today has **0** REQ annotations so "green" is undefined; (B) **dedicated P5-EX epic** (`openspec/specs/p5-ex-complex-examples/spec.md`) that **extends** `examples/packs/`, `examples/repos/` layouts, `examples/archetypes/`, `assent test` fixtures, and product `docs/` and closes REF-EX C1–C8 — **chosen**; (C) docs-only — rejected: untested README claims are the AUD-S06 class of defect. **Chosen B.** EX does not re-spec DEM, does not invent a parallel example system, does not duplicate E1 adapters, does not start E10/E11/SEC-SC/AUD2, does not wire DEM-S00, does not raise `COVERAGE_MIN`. Packs stay single-class. HCL story is honest: structured literal tfvars + `.tf` blocks opaque→REVIEW (`TestHCLStructuralGuardsFailSafe`; no new parser). `task dogfood-examples` is wired into `task check` and shares discovery with `verify.yaml` (steal DEM-S12 *wiring*, not demo-repo scope). Revert: delete `openspec/specs/p5-ex-complex-examples/`, remove the P5-EX table from `openspec/specs/backlog.md` and the P5-EX paragraph from `openspec/specs/later-phases.md`, restore the REF-EX row to OPEN; this row remains as a superseded decision if a later D-row replaces it. | diff --git a/openspec/specs/backlog.md b/openspec/specs/backlog.md index 91dc76d..cee2235 100644 --- a/openspec/specs/backlog.md +++ b/openspec/specs/backlog.md @@ -126,8 +126,8 @@ providers, not core-model redesign**. All rows below are generalized (invented n | ID | Gap | Status | Notes | | --- | --- | --- | --- | -| **REF-GAP-1** | Referenced-resource authorization fact source (a list value / ACL names *another* team's resource → who owns it?) | **CLOSED (E5-S08)** | `builtin/resource-owner` shipped; hermetic L0 + run-path wiring in E5-S10. Demonstrator fixture = C7 (deferred — D-071) | -| **REF-GAP-2** | In-repo-state-as-a-fact (quota/placement/limits registries + in-repo reviewers files that today no provider reads) | **CLOSED (E5-S07)** | `builtin/repo-file` most-specific-first shipped; hermetic run path in E5-S10 (`TestE5ExitGateResolvedFacts`). C5/C6 fixtures deferred — D-071 | +| **REF-GAP-1** | Referenced-resource authorization fact source (a list value / ACL names *another* team's resource → who owns it?) | **CLOSED (E5-S08)** | `builtin/resource-owner` shipped; hermetic L0 + run-path wiring in E5-S10. Demonstrator fixture = C7 (**P5-EX EX-S07**, D-143 — was deferred D-071) | +| **REF-GAP-2** | In-repo-state-as-a-fact (quota/placement/limits registries + in-repo reviewers files that today no provider reads) | **CLOSED (E5-S07)** | `builtin/repo-file` most-specific-first shipped; hermetic run path in E5-S10 (`TestE5ExitGateResolvedFacts`). C5/C6 fixtures = **P5-EX EX-S07** (D-143; was deferred D-071) | | **REF-GAP-3** | Cross-class / companion-file correlation ("two-step delete": remove from file A *and* append to manifest B) | **OPEN — likely out of v1** | `changes` is class-slice-scoped by contract (ADR-0017 §5); ship C8 as a known-limitation fixture (expected REVIEW), decide scope via OQ | | **REF-GAP-4** | Plan-level blast radius (weighting the expanded IaC plan, not the request diff) | **OUT of model** | assent gates the request diff; `points`/`threshold` bulk-guard on the diff is the in-scope approximation | @@ -135,7 +135,7 @@ providers, not core-model redesign**. All rows below are generalized (invented n | ID | Item | Status | Closest existing archetype | | --- | --- | --- | --- | -| **REF-EX** | Author 8 domain-neutral archetype fixtures C1–C8 (list-no-shrink, privilege-tier allow-list, wildcard-grant block, soft-delete-as-field-add, quota-ceiling-from-fact, placement allow-list, referenced-resource-ownership [gap demo], companion-file delete [known-limitation]) | **OPEN** (agent lane; do AFTER the E2 engine + E5 facts for C5/C6/C7) | extends no-destruction (C1/C4/C8), allowed-fields+ownership (C2/C3/C6/C7), bounded-change (C5); none duplicates an existing fixture | +| **REF-EX** | Author 8 domain-neutral archetype fixtures C1–C8 (list-no-shrink, privilege-tier allow-list, wildcard-grant block, soft-delete-as-field-add, quota-ceiling-from-fact, placement allow-list, referenced-resource-ownership [gap demo], companion-file delete [known-limitation]) | **SPECIFIED (P5-EX)** — C1–C4 = EX-S06; C5–C8 = EX-S07; not started | [p5-ex-complex-examples](p5-ex-complex-examples/spec.md). Extends no-destruction (C1/C4/C8), allowed-fields+ownership (C2/C3/C6/C7), bounded-change (C5). Engine+E5 facts already shipped; D-071 deferral is this epic. **Not** P5-DEM (D-143) | ## Phase 5 — E1 canonical change model stories @@ -605,6 +605,34 @@ LGTM** (published contract + the decision path itself). Independent of E10; may | E11-S12 | Docs & maturity truth; retire ADR-0002's "pluggable half unbuilt" line | **[autonomous]** | S11 | nothing still calls Rego locked | | E11-S13 | Exit gate | **[autonomous]** | S00–S12 | **the E11 exit gate** | +## Phase 5 — EX complex in-tree examples / adopter tests / docs truth + +Full INVEST stories in [p5-ex-complex-examples/spec.md](p5-ex-complex-examples/spec.md). +Operator ask (2026-08-15, **D-143**): thicker multi-field nested examples, tests, and docs +across **YAML, JSON, HCL, and tfvars** — not a one-field YAML happy path, and **not** +implementing P5-DEM. DEM remains the public-org demo + DEM-S00 routing + provider-broker +epic (0 `Test:`/`Verify:`/`Level:` annotations — that is why EX exists). EX extends +`examples/packs/`, `examples/archetypes/`, `assent test` fixtures, and `docs/`; closes +REF-EX C1–C8 in-tree. **No new HCL parser, no DEM-S00, no E10/E11/SEC-SC/AUD2, no schema +change, no `internal/core` edits.** `assent test` stays facts.yaml-stubbed. **Every story +`[autonomous]`.** REQ IDs `REQ-EX-S0n-nn`. + +| ID | Story | Execution | Depends on | Gate contribution | +| --- | --- | --- | --- | --- | +| EX-S01 | Docs + format-coverage inventory (paper-gate, both polarities) | **[autonomous]** | none | **do first** — claims cannot outrun dogfood | +| EX-S02 | Thicken topic-registry (YAML nested multi-field + nested-pointer rules) | **[autonomous]** | none | YAML complexity | +| EX-S03 | Thicken service-catalog (JSON nested objects/maps; D-061-safe) | **[autonomous]** | none | JSON complexity | +| EX-S04 | Thicken infra-vars (tfvars deeper nested maps) | **[autonomous]** | none | tfvars complexity | +| EX-S05 | HCL honesty: structured tfvars + `.tf` block → opaque → REVIEW | **[autonomous]** | S04 | fourth format; known-limitation fixture | +| EX-S06 | REF-EX C1–C4 in-tree fixtures | **[autonomous]** | S02, S03 | list-no-shrink, privilege-tier, wildcard-grant, soft-delete | +| EX-S07 | REF-EX C5–C8 (facts stubs + C8 known-limitation REVIEW) | **[autonomous]** | S04, S06 | closes D-071 demonstrators; C8 documented REVIEW | +| EX-S08 | Discover packs; wire `dogfood-examples` into `task check` + verify.yaml | **[autonomous]** | S01 | DEM-S12 wiring without demo-repo scope | +| EX-S09 | Product docs walkthrough byte-pinned to real complex-case CLI output | **[autonomous]** | S02–S07 | AUD-S06-style truth | +| EX-S10 | Exit gate: four formats, C1–C8, docs gates, schema freeze vs `v0.1.0` (D-132) | **[autonomous]** | S01–S09 | **the EX exit gate** | + +**Dependency order:** S01 → S08 ∥ {S02, S03, S04} → S05 (after S04) → S06 (after S02/S03) → +S07 → S09 → S10. **Do first: S01.** + ### P5-DEM — Public demo repositories + provider extensibility proof — spec: [p5-dem-demo-repos](p5-dem-demo-repos/spec.md) Designed spec-first by **D-142**; the provider-credential gap found while designing is **OQ-32**. @@ -662,7 +690,7 @@ Epic paragraphs (goal, ADR constraints, exit gate, story seeds) in | --- | --- | --- | | 3 — Contracts first | P3-E1 schemas + contract fixture (incl. ApprovalEvidence + named-consumer fixture) · P3-E2 versioning/compat spec · P3-E3 example migration · P3-E4 lifecycle: phase/profiles/comparison (ADR-0018) · P3-E5 publication reconciliation protocol (ADR-0019) | strict end-to-end contract fixture validates (ADR-0017 §8, D-016); new ADRs 0018/0019 accepted at the freeze review | | 4 — Walking skeleton | P4-E1 (+ rerun-idempotence gate, D-017) · **P2-E4-NS (OQ-24 timed run)** · holdout adjudication (OQ-25) | L3 skeleton green + **one real repo on live MRs** (D-012); north-star wording only after timed run | -| 5 — Implementation | E1–E7 **DONE**; **E7 AUTONOMOUS COMPLETE** (S01–S05+S08, D-087); **E8 AUTONOMOUS COMPLETE** ([p5-e8-renderer/spec.md](p5-e8-renderer/spec.md), S01–S14, D-098); **E9 AUTONOMOUS COMPLETE** ([p5-e9-distribution/spec.md](p5-e9-distribution/spec.md), S01–S13, D-099–D-111 CLOSED; Homebrew Formula live; PAT rotate optional); **PCS AUTONOMOUS COMPLETE** ([p5-pcs-policy-comparison/spec.md](p5-pcs-policy-comparison/spec.md), S01–S09, **D-057 closed**, D-118); **E10 UNLOCKED + DECOMPOSED** (D-140, [p5-e10-github-forge/spec.md](p5-e10-github-forge/spec.md), 19 stories, ADR-0021); **E11 IMPLEMENTATION UNLOCKED + DECOMPOSED** (D-141, [p5-e11-rego-backend/spec.md](p5-e11-rego-backend/spec.md), 14 stories); E12 **contract-unlocked** (D-017), not decomposed; E14 gated on Spike D; **E13 still locked** (D-012); **SEC-SC SPECIFIED, NOT STARTED** ([p5-sec-scorecard-residuals/spec.md](p5-sec-scorecard-residuals/spec.md), 2 stories — S01 autonomous fuzzing, S02 operator-gated Best Practices badge) | per-epic; E9 exit = tagged signed release + docs live + brew Formula (D-111); PAT rotate optional | +| 5 — Implementation | E1–E7 **DONE**; **E7 AUTONOMOUS COMPLETE** (S01–S05+S08, D-087); **E8 AUTONOMOUS COMPLETE** ([p5-e8-renderer/spec.md](p5-e8-renderer/spec.md), S01–S14, D-098); **E9 AUTONOMOUS COMPLETE** ([p5-e9-distribution/spec.md](p5-e9-distribution/spec.md), S01–S13, D-099–D-111 CLOSED; Homebrew Formula live; PAT rotate optional); **PCS AUTONOMOUS COMPLETE** ([p5-pcs-policy-comparison/spec.md](p5-pcs-policy-comparison/spec.md), S01–S09, **D-057 closed**, D-118); **E10 UNLOCKED + DECOMPOSED** (D-140, [p5-e10-github-forge/spec.md](p5-e10-github-forge/spec.md), 19 stories, ADR-0021); **E11 IMPLEMENTATION UNLOCKED + DECOMPOSED** (D-141, [p5-e11-rego-backend/spec.md](p5-e11-rego-backend/spec.md), 14 stories); E12 **contract-unlocked** (D-017), not decomposed; E14 gated on Spike D; **E13 still locked** (D-012); **SEC-SC SPECIFIED, NOT STARTED** ([p5-sec-scorecard-residuals/spec.md](p5-sec-scorecard-residuals/spec.md), 2 stories — S01 autonomous fuzzing, S02 operator-gated Best Practices badge); **P5-EX SPECIFIED, NOT STARTED** (D-143, [p5-ex-complex-examples/spec.md](p5-ex-complex-examples/spec.md), 10 stories — complex in-tree examples; **not** P5-DEM); **P5-DEM DESIGNED, NOT THIS ASK** (D-142, annotations still 0) | per-epic; E9 exit = tagged signed release + docs live + brew Formula (D-111); PAT rotate optional | Named-consumer disposition (what unlocked, what stayed locked, and why): [docs/planning/named-consumer-compat.md](../../docs/planning/named-consumer-compat.md). diff --git a/openspec/specs/later-phases.md b/openspec/specs/later-phases.md index 902d981..f550ef7 100644 --- a/openspec/specs/later-phases.md +++ b/openspec/specs/later-phases.md @@ -313,6 +313,22 @@ outcomes, emit a durable safety event, optionally open a revert MR via a separat authorized capability (never a direct revert push), and feed adjudicated outcomes into policy comparison without treating every human revert as proof the decision was wrong. +### P5-EX — Complex in-tree examples, adopter tests, and docs truth — **Specified, not started (D-143)** +Operator ask for thicker nested examples/tests/docs across YAML, JSON, HCL, and tfvars — +**not** public demo repos. Full INVEST stories: +[p5-ex-complex-examples](p5-ex-complex-examples/spec.md) (EX-S01–S10, all autonomous). +Extends `examples/packs/` + archetypes + `assent test` + `docs/`; closes REF-EX C1–C8; +wires `task dogfood-examples` into `task check`. Explicitly **not** P5-DEM (DEM-S00 routing, +org publish, provider brokers). No E10/E11/SEC-SC/AUD2. Schema freeze vs `v0.1.0` (AUD-S18 / +D-132 — not working-tree `git diff schemas/`). +**Do first: EX-S01.** + +### P5-DEM — Public demo repositories + provider extensibility proof — **Designed (D-142), not this operator ask (D-143)** +Still the vehicle for public `PlatformRelay` demo repos, `(class, environment)` routing +(DEM-S00), and operator-gated publish (DEM-S13). Spec: +[p5-dem-demo-repos](p5-dem-demo-repos/spec.md). Do not implement it as the complex-examples +epic; REQs still lack `Test:`/`Verify:`/`Level:`. + ### P5-SEC-SC — OpenSSF Scorecard residuals (fuzzing + Best Practices badge) — **Specified, not started** Two leftover Scorecard alerts after the 2026-08-13 security sweep (Dependabot/CodeQL/secrets/Sonar security clean; Maintained/Code-Review/Branch-Protection dismissed as tool-limit or solo-maintainer diff --git a/openspec/specs/p5-ex-complex-examples/spec.md b/openspec/specs/p5-ex-complex-examples/spec.md new file mode 100644 index 0000000..8a556ee --- /dev/null +++ b/openspec/specs/p5-ex-complex-examples/spec.md @@ -0,0 +1,813 @@ +# P5-EX — Complex in-tree examples, adopter tests, and docs truth + +**Epic ID / REQ prefix:** `EX` / `REQ-EX-S0n-nn`. + +**Origin:** operator instruction (2026-08-15) — better and more *complex* examples, tests, and +documentation (multi-field nested resources; YAML + JSON + HCL + tfvars), not a single-field +YAML happy path. Recorded as **D-143**. The engine already has E1 format adapters; the +*examples / tests / docs* are the thin layer. + +**Vehicle:** extend `examples/packs/`, `examples/repos/` (layouts only), `examples/archetypes/`, +existing `assent test` fixtures, and product docs under `docs/`. Close **REF-EX C1–C8** where +they fit existing archetypes. Do **not** invent a parallel example system. + +--- + +## Problem + +Shipped packs (`examples/packs/{topic-registry,service-catalog,infra-vars}`) are dogfood-green +under `assent test` / `--coverage`, but they are thin: + +- **topic-registry (YAML):** `schema{}` exists (`format`, `subject`) but no rule matches nested + pointers; `schema-valid` is a fact stub (`facts.schema.valid.value`), not a nested-field proof. +- **service-catalog (JSON):** pack fixtures are flat `{name, owner, tier, oncall}`. Nested + `endpoints` / `tags` were **stripped** (D-061): unkeyed nested lists make the differ opaque. + `examples/repos/service-catalog` still shows those lists; the *pack* does not govern them. +- **infra-vars (tfvars):** keyed object `workloads.*` with scalar `min_replicas` / `max_replicas` + / `memory_mb`. No deeper nested maps. **No `.tf` pack.** Class match is `envs/**/*.tfvars` only + (`examples/packs/infra-vars/.assent/config.yaml`). +- **HCL:** E1-S04 is **literal-only**. `parseHCL` (`internal/change/diff_hcl.go`) refuses HCL + *blocks* (`resource` / `module`) as not-tfvars, fail-safe opaque, **zero partial changes** + (`TestHCLStructuralGuardsFailSafe`). tfvars attribute objects **are** structured. There is no + adopter-visible fixture that demonstrates the `.tf` opaque path; infra-vars README comments + the fallback but does not run it. +- **Dogfood wiring (DEM-S12 finding, still true):** `task dogfood-examples` exists and is **not** + a `task check` stage. CI `verify.yaml` hardcodes the same three-pack loop. `greenExamplePacks` + in `cmd/assent/test_corpus_test.go` pins the names. Adding a pack requires three edits that + can skew. +- **Docs:** `docs/usage/walkthrough.md` still demos a one-file flat topic; `examples/README.md` + lists three packs and does not claim HCL. AUD-S06 truth-lag gates exist (`task docs-gates`) + but do not pin format coverage. + +`examples/repos/**` remain **layouts without `.assent/` trees** (D-142 ground truth). This epic +does not turn them into demo repos. + +--- + +## Why not P5-DEM (D-143) + +**P5-DEM** (`openspec/specs/p5-dem-demo-repos/spec.md`, D-142) is a *different goal*: public +org demo repos, **(class, environment) routing (DEM-S00, engine-grade)**, provider brokers, +operator-gated publish (DEM-S13). Its REQs have **zero** `Test:` / `Verify:` / `Level:` +annotations — that is why this operator ask is **not** implemented as DEM. + +EX **steals** DEM's *honest HCL truth* (DEM-S08/S10: determine unmatched / opaque behaviour by +running, do not invent a parser) and DEM-S12's *wiring* (edit both `Taskfile.yml` and +`verify.yaml`) without demo-repo scope, without DEM-S00, and without publishing. + +--- + +## Non-goals + +- **Do not re-spec or implement P5-DEM.** Leave the DEM table in the backlog. +- **Do not invent a parallel example system** (no `examples/demo/**`, no public org repos). +- **Do not duplicate E1 adapters.** No new HCL parser; no expression evaluation. +- **Do not start E10 / E11 / P5-SEC-SC / AUD2.** +- **Do not wire DEM-S00** `(class, environment)` routing. Packs stay **single-class**. + Multi-class in one pack fails closed today (`selectBinding` / `selectBindingForTest`). +- **Do not call live providers.** `assent test` stays `facts.yaml`-stubbed (ADR-0014). +- **Do not change `internal/core`.** Engine/decision-path changes only if a fixture cannot run + without a one-line harness *discovery* fix (prefer fixture/docs). +- **Schema freeze is ref-relative (AUD-S18 / D-132):** `git diff --name-status v0.1.0 -- schemas` over `*.json`, **not** a working-tree `git diff schemas/` (that is silent on committed edits — PCS vacuity). EX adds no schema JSON beyond the AUD-S18 permitted description-string. Base ref is the release tag; substituting `HEAD` reddens. +- **Do not raise `COVERAGE_MIN`** (91%, D-010/D-128). Example-only lanes that add no + `internal/` Go: coverage is N/A. +- **No live GitLab, public org repos, or PAT rotation.** Park as non-goals / operator-gated + (none expected). +- **D-002:** generic invented names only (orders-team, payments-gateway, …). No employer + names, internal systems, or verbatim private material. +- **Do not re-introduce unkeyed nested lists** into service-catalog pack fixtures (D-061). + Nested **objects / maps** and identity-keyed collections only. + +--- + +## ADRs and reuse + +**ADRs:** 0003 (opaque→REVIEW; HCL literal-only caveat), 0006 (dogfood examples in CI), 0008 +(unclassified / unmatched), 0010 (repo layout), 0014 (`assent test` / `--coverage` / facts +stubs), 0017 §5 (EntryRef, class-slice `changes`). **D-061** (nested-list opacity), **D-063** +(unmatched whole-file delete → REVIEW), **D-071** (C5–C7 demonstrators deferred to this +authoring lane), **D-086** (`verify.yaml` is CI superset; this epic *does* add dogfood to +`task check` — a deliberate local-gate extension, logged in judgment (a)). + +**Reuse:** `change.Diff` / `parseHCL`, `assent test` / `assent lint` / `--coverage`, +`greenExamplePacks` + `TestDogfoodScriptsIncludeGreenExamplePacks`, `task docs-gates` / +AUD-S06 pin-script pattern, `hack/check-sanitization.sh`. + +--- + +## Judgment calls (decide-and-log) + +**(a) Pack discovery vs hardcoded list — DECIDED: discover, pin the contract.** +Runtime dogfood walks `examples/packs/*/` directories that contain `.assent/tests/` (skip +dirs without adopter tests; never pick up `testdata/broken-pack`). A **shared script** +(`hack/dogfood-examples.sh`) is the single loop `task dogfood-examples` *and* `verify.yaml` +call — no forever-hardcoded three-name `for pack in …` in two files. `greenExamplePacks` in +`cmd/assent/test_corpus_test.go` becomes a **filesystem walk of the same glob**, with a pin +that Taskfile + verify.yaml invoke the script (not a copy of the names). Adding a pack with +`.assent/tests/` automatically enters dogfood; if it is not green, CI fails (desired). A +directory under `examples/packs/` *without* tests is a hard error of the inventory gate +(S01), not silently skipped — packs are complete adopter trees. + +**(b) C7 location — DECIDED: topic-registry, not a new pack.** +Referenced-resource ownership is an optional nested `acl` object on the existing +`kafka-topic` class. A new pack would be a fourth tree for a field the topic shape can host. +A second class in the same pack would fail closed (DEM-S00). The C7 rule matches +`topics/**/*.yaml` and proves when `!has(entry.acl) || facts.resource_owner.owner.value == entry.acl.owner` +(vacuous for existing topics without `acl`), so current goldens stay green. Dedicated +archetype seed: `examples/archetypes/referenced-resource-ownership/` (D-071). **Do not** put +C7 in a multi-class ACL pack. + +**(c) `.tf` opaque demo — DECIDED: case under infra-vars, not a fourth pack.** +`infra-vars` is tfvars-classed today; a `.tf` file in that pack with the current class match +is **ungoverned** (unclassified → fail-safe REVIEW) and would **not** exercise `parseHCL`'s +block guard. EX-S05 **extends the single class** `infra-vars` match paths to +`["envs/**/*.tfvars", "envs/**/*.tf"]` so a `resource`/`module` block is *parsed*, goes +opaque, and yields REVIEW with **zero partial changes**. Still one class; existing tfvars +fixtures do not include `.tf` files. **Determine** the adopter-test decision by running +`assent test`, do not pre-assert a fantasy (DEM-S08/S10 honesty). Document the known +limitation next to the fixture. + +**(d) Coverage floor — DECIDED: do not raise.** Example-only lanes may add no `internal/` +Go. If a lane adds no production Go, D-010 is N/A. `cmd/assent` tests (discovery walk) sit +outside the `internal/…` denominator (TEST-04 / D-132). + +**(e) `task check` vs D-086 — DECIDED: add `dogfood-examples` to `task check`.** +D-086 said verify is the CI superset and `task check` stays local fmt/vet/lint/test/coverage/build. +DEM-S12 and this operator ask contradict that for *example dogfood*: a pack that is only +green in CI (or only via a manual `task dogfood-examples`) rots locally. EX-S08 adds +`task: dogfood-examples` as a sequential `check:` stage (same pattern as `docs-gates`, +D-124 — not a parallel `deps:` entry, because dogfood `build`s a binary). D-086's "do not +fold verify-only steps into check" still holds for gitleaks/govulncheck/e2e-vet; dogfood is +an adopter-facing local gate, not a verify-only scan. + +--- + +## Executability + +**Every story `[autonomous]`.** No network, no forge, no PAT, no live provider. Facts from +authored `facts.yaml` → resolved envelope. D-002 sanitization on every new fixture. +TDD: a fixture/rule that is deleted must redden `assent test` / `assent test --coverage` / +`assent lint` (non-vacuity). Every REQ has `Test:` / `Verify:` / `Level:`. + +**Dependency order:** **S01** (inventory paper-gate) → **S08** (wire dogfood into `task check` ++ shared discovery; startable after S01, valuable immediately) ∥ **S02 / S03 / S04** (thicken +three packs, file-disjoint) → **S05** (HCL honesty on infra-vars, after S04 so nested tfvars +goldens exist) → **S06** (C1–C4; after S02/S03 so nested fields exist to hang rules on) → +**S07** (C5–C8; after S04/S06) → **S09** (walkthrough byte-pinned to real CLI output) → +**S10** (exit gate). **Do first: S01.** + +--- + +## Story index + +| ID | Story | Execution | Depends on | Gate contribution | +| --- | --- | --- | --- | --- | +| EX-S01 | Docs + format-coverage inventory (paper-gate, both polarities) | **[autonomous]** | none | **do first** — claims cannot outrun dogfood | +| EX-S02 | Thicken topic-registry (YAML nested multi-field + nested-pointer rules) | **[autonomous]** | none | YAML complexity; nested pointers both polarities | +| EX-S03 | Thicken service-catalog (JSON nested objects/maps; D-061-safe) | **[autonomous]** | none | JSON complexity; nested fields both polarities | +| EX-S04 | Thicken infra-vars (tfvars deeper nested maps) | **[autonomous]** | none | tfvars complexity; nested maps both polarities | +| EX-S05 | HCL honesty: structured tfvars + `.tf` block → opaque → REVIEW | **[autonomous]** | S04 | fourth format represented; known-limitation fixture | +| EX-S06 | REF-EX C1–C4 in-tree fixtures | **[autonomous]** | S02, S03 | list-no-shrink, privilege-tier, wildcard-grant, soft-delete | +| EX-S07 | REF-EX C5–C8 (facts stubs + C8 known-limitation REVIEW) | **[autonomous]** | S04, S06 | closes D-071 demonstrators; C8 documented REVIEW | +| EX-S08 | Discover packs; wire `dogfood-examples` into `task check` + verify.yaml | **[autonomous]** | S01 | DEM-S12 wiring without demo-repo scope | +| EX-S09 | Product docs walkthrough byte-pinned to real complex-case CLI output | **[autonomous]** | S02–S07 | AUD-S06-style truth for the new surface | +| EX-S10 | Exit gate: four formats, C1–C8, docs gates, schemas frozen | **[autonomous]** | S01–S09 | **the EX exit gate** | + +--- + +## EX-S01 — Docs + format-coverage inventory paper-gate [autonomous] + +**As a** maintainer **I want** a gate that fails when `examples/README.md` or product docs +claim a pack or input format that is not dogfooded — and when a dogfooded pack is missing +from those docs **so that** examples that don't run cannot be documented as if they do. + +**Goal:** a both-polarity inventory script (AUD-S06 pin pattern) that (1) enumerates +`examples/packs/*` directories containing `.assent/`; (2) records each pack's governed +format from class `match.paths` extensions (`.yaml` / `.json` / `.tfvars` / `.tf`); (3) +asserts `examples/README.md` names **exactly** those pack directory names; (4) asserts every +claimed format in the README format sentence is actually present on a dogfood pack; (5) +fails if a pack directory exists without `.assent/tests/` (incomplete tree). Wire into +`task docs-gates` so `task check` runs it once S08 lands; until then the script is invoked +from `docs-gates` in this story (docs-gates already runs on check — adding the script there +is the paper-gate, not the pack-loop wiring). + +**Operator input:** none (D-143). + +**Dependencies:** none. **Do first.** + +**Definition of done:** flipping README to claim `cue` or a pack `kafka-acl` reddens the +script; deleting `topic-registry` from the README pack list while the directory exists +reddens the other polarity; `hack/check-sanitization.sh` still green. + +**Not in scope:** thickening fixtures (S02–S05); C-series (S06–S07); changing the dogfood +loop (S08); HCL claims (README must **not** claim `.tf` / HCL until S05 adds the fixture and +updates the sentence). + +Requirements: + +- **REQ-EX-S01-01** — Given the three shipped packs, when the inventory script runs, then it + exits 0 and reports packs `infra-vars`, `service-catalog`, `topic-registry` with formats + yaml / json / tfvars (no `.tf` yet). + - Test: `hack/docs/example_format_inventory_test.sh` + - Verify: `bash hack/docs/example_format_inventory_test.sh` + - Level: L1 +- **REQ-EX-S01-02** — Given `examples/README.md` names a pack directory that does not exist + under `examples/packs/`, when the script runs, then it exits non-zero (claimed-but-missing + polarity). + - Test: `hack/docs/example_format_inventory_test.sh` (mutation: extra name in a temp copy) + - Verify: `bash hack/docs/example_format_inventory_test.sh` + - Level: L1 +- **REQ-EX-S01-03** — Given a real pack directory omitted from the README pack list, when + the script runs, then it exits non-zero (dogfooded-but-undocumented polarity). + - Test: `hack/docs/example_format_inventory_test.sh` + - Verify: `bash hack/docs/example_format_inventory_test.sh` + - Level: L1 +- **REQ-EX-S01-04** — Given the README claims an input format whose extension is not present + on any pack class match, when the script runs, then it exits non-zero. + - Test: `hack/docs/example_format_inventory_test.sh` + - Verify: `bash hack/docs/example_format_inventory_test.sh` + - Level: L1 +- **REQ-EX-S01-05** — Given `task docs-gates` is run, when the inventory script is deleted + from that task, then a pin in `hack/docs/truthlag_pins_test.sh` or the inventory script's + own wiring check fails (non-vacuity). + - Test: `hack/docs/truthlag_pins_test.sh` (or inventory script self-wiring stanza) + - Verify: `task docs-gates` + - Level: L1 + +--- + +## EX-S02 — Thicken topic-registry (YAML nested multi-field) [autonomous] + +**As a** policy author **I want** the YAML starter pack to govern a realistic nested topic +document (not one scalar `partitions`) **so that** nested JSON Pointers and both polarities +are proven under `assent test --coverage`. + +**Goal:** extend `topics/prod/orders.events.v1.yaml` (and siblings used by existing cases) +with nested fields the schema object already sketches: e.g. `schema.compatibility`, +`schema.references` as a **map** (not an unkeyed list), `retention.hours` / `retention.ms` +as a nested object *or* keep `retention_hours` and add `compaction.strategy` nested. Add a +rule whose `valueChanges.pointers` target a **nested** pointer (`/*/schema/compatibility` +or `/*/schema/format`) with proving + negative cases. Existing obligations +(ownership / bounded-change / non-destructive / schema-valid) must stay green — new fields +must not flip those decisions. Prefer editing existing fixtures over a new pack. + +**Operator input:** none. + +**Dependencies:** none (parallel with S03/S04). + +**Definition of done:** `assent test examples/packs/topic-registry` and `--coverage` green; +deleting the nested-pointer rule reddens `--coverage` or lint tests-per-rule; `assent lint` +clean; D-002 sanitization green. + +**Not in scope:** C1–C8 (S06/S07); `.tf`; JSON pack; engine changes. + +Requirements: + +- **REQ-EX-S02-01** — Given a topic document with at least three nested object levels under + the identity key (e.g. `schema.compatibility`, `schema.format`, plus a nested map), when + `assent test` runs the schema-valid proving case, then the decision stays the pinned + `expect.yaml` value (existing goldens do not silently change). + - Test: `examples/packs/topic-registry/.assent/tests/topics/schema-valid/expect.yaml` + - Verify: `./bin/assent test examples/packs/topic-registry` + - Level: L1 +- **REQ-EX-S02-02** — Given a `valueChanges` rule on a nested pointer (not `/*/partitions`), + when head modifies only that nested field within policy, then the case `decision: APPROVE` + (or the pack's existing aggregate) and `--coverage` counts a proving polarity for that rule. + - Test: `examples/packs/topic-registry/.assent/packs/topics/rules/` (new or extended rule) + + matching `.assent/tests/topics/` case + - Verify: `./bin/assent test --coverage examples/packs/topic-registry` + - Level: L1 +- **REQ-EX-S02-03** — Given the same nested-pointer rule, when head sets a disallowed nested + value, then the negative case is non-APPROVE (`REVIEW` or `BLOCK` per `onFailure`) and + `--coverage` counts the failing polarity. + - Test: `examples/packs/topic-registry/.assent/tests/topics//negative/expect.yaml` + - Verify: `./bin/assent test --coverage examples/packs/topic-registry` + - Level: L1 +- **REQ-EX-S02-04** — Given the nested-pointer rule file is deleted, when `assent lint` or + `--coverage` runs, then the gate is red (tests-per-rule and/or both-polarity non-vacuity). + - Test: `examples/packs/topic-registry/.assent/packs/topics/rules/` + lint corpus + - Verify: `./bin/assent lint examples/packs/topic-registry` + - Level: L1 + +--- + +## EX-S03 — Thicken service-catalog (JSON nested objects/maps) [autonomous] + +**As a** policy author **I want** the JSON catalog entries to carry nested objects beyond +`name` / `owner` / `tier` **so that** rules on nested fields are proven both polarities +without re-opening D-061 list opacity. + +**Goal:** add nested **objects/maps** to catalog entries, e.g. `sla: { slo_percent, window }` +and `runtime: { language, replicas }` — **not** unkeyed `endpoints`/`tags` arrays in the +*pack* fixtures. Optionally allow-list a nested pointer (`path.endsWith("/sla/slo_percent")`) +in addition to `/oncall`. Watch D-061: if a fixture goes opaque, **do not** weaken the engine; +strip or reshape the fixture (maps / keyed lists with `identity.pointer`). Update pack tests +so every proving case still satisfies all required obligations. + +**Operator input:** none. + +**Dependencies:** none. + +**Definition of done:** `assent test` + `--coverage` green on service-catalog; no opaque +changeset on the new nested-object modify cases; deleting the nested rule reddens coverage +or lint. + +**Not in scope:** restoring `endpoints`/`tags` unkeyed lists in the pack; C2 privilege-tier +(S06 may add `tier` transition rules on this thickened document); live oncall provider. + +Requirements: + +- **REQ-EX-S03-01** — Given a catalog JSON entry with nested objects (`sla` and/or `runtime`) + and **no** unkeyed nested lists in pack `base/`/`head/`, when `assent test` diffs a + nested-field modify, then the ChangeSet is **not** opaque and the proving case matches + `expect.yaml`. + - Test: `examples/packs/service-catalog/.assent/tests/catalog/` (extended or new case) + - Verify: `./bin/assent test examples/packs/service-catalog` + - Level: L1 +- **REQ-EX-S03-02** — Given a rule matching a nested pointer under `/services/*//…`, + when head changes only the allowed nested field, then proving polarity is counted by + `--coverage`. + - Test: `examples/packs/service-catalog/.assent/packs/catalog/rules/` + tests + - Verify: `./bin/assent test --coverage examples/packs/service-catalog` + - Level: L1 +- **REQ-EX-S03-03** — Given head changes a nested sensitive field (or a field outside the + allow-list), when the negative case runs, then the decision is non-APPROVE per `onFailure`. + - Test: `examples/packs/service-catalog/.assent/tests/catalog//negative/expect.yaml` + - Verify: `./bin/assent test examples/packs/service-catalog` + - Level: L1 +- **REQ-EX-S03-04** — Given a dedicated catalog case whose `head` re-introduces an **unkeyed** + nested list (`endpoints` and/or `tags` as JSON arrays — the D-061 shape), when `assent test` + runs that case, then the ChangeSet is **opaque** (or the pinned `expect.yaml` decision is + fail-safe non-APPROVE) and the case goes **red** if the differ starts emitting structured + field changes for that unkeyed list (silent accept). The S03-01 proving fixtures stay + keyed maps / nested objects only — this REQ is the both-polarity pin, not a comment. + - Test: `examples/packs/service-catalog/.assent/tests/catalog/unkeyed-list-opaque/` + - Verify: `./bin/assent test examples/packs/service-catalog` + - Level: L1 + +--- + +## EX-S04 — Thicken infra-vars (tfvars nested maps) [autonomous] + +**As a** policy author **I want** tfvars workloads to carry nested maps (not only scalar +replica/memory) **so that** HCL object-constructor nesting is adopter-visible and gated. + +**Goal:** nest e.g. `resources = { cpu = 500, memory_mb = 3072 }` and/or `labels = { team = +"orders-team", tier = "prod" }` under each workload. Point `valueChanges` at +`/workloads/*/resources/memory_mb` (or keep existing scalar rules **and** add nested ones). +Both polarities. Literal-only: no `${}` interpolations in these fixtures (those are S05 +opaque). Existing min/max replica cases stay green. + +**Operator input:** none. + +**Dependencies:** none. + +**Definition of done:** `assent test` + `--coverage` green; nested map modify is a structured +diff (not opaque); deleting the nested rule reddens coverage/lint. + +**Not in scope:** `.tf` resource blocks (S05); C6 placement (S07); changing `parseHCL`. + +Requirements: + +- **REQ-EX-S04-01** — Given a tfvars workload with a nested object (`resources` or `labels`) + of at least two keys, when head modifies one nested scalar, then `assent test` sees a + non-opaque ChangeSet and the proving `expect.yaml` holds. + - Test: `examples/packs/infra-vars/.assent/tests/vars/` (extended fixtures) + - Verify: `./bin/assent test examples/packs/infra-vars` + - Level: L1 +- **REQ-EX-S04-02** — Given a `valueChanges` pointer into the nested map, when the value + stays in band, then `--coverage` records proving polarity for that rule. + - Test: `examples/packs/infra-vars/.assent/packs/vars/rules/bounded-change.yaml` (extended) + - Verify: `./bin/assent test --coverage examples/packs/infra-vars` + - Level: L1 +- **REQ-EX-S04-03** — Given the nested value exceeds the stubbed fact band, when the negative + case runs, then decision is non-APPROVE (`challenge` → REVIEW). + - Test: `examples/packs/infra-vars/.assent/tests/vars//negative/expect.yaml` + - Verify: `./bin/assent test examples/packs/infra-vars` + - Level: L1 +- **REQ-EX-S04-04** — Given S04's pack fixtures, when `assent test` runs infra-vars, then at + least one case diffs a **nested-map** modify (S04-01) as a non-opaque structured change — + this story may not skip the nested fixture. S04 fixtures contain **no** `.tf` files and + **no** non-literal HCL (`var.team`, `"${…}"`); those belong to S05 only. Edge: adding a + `.tf` or expression to an S04 proving `head/` fails this REQ. + - Test: `examples/packs/infra-vars/.assent/tests/vars/` (nested-map case; glob must not + match `*.tf` under S04 cases) + - Verify: `./bin/assent test examples/packs/infra-vars` + - Level: L1 + +--- + +## EX-S05 — HCL honesty: structured tfvars + `.tf` block → opaque → REVIEW [autonomous] + +**As an** adopter **I want** an in-tree fixture that shows what assent **can** structure in +HCL (tfvars objects) and what it **cannot** (`.tf` `resource`/`module` blocks) **so that** I +do not assume a Terraform parser that does not exist. + +**Goal:** (1) keep S04 structured tfvars as the literal HCL success path. (2) Add +`envs/prod/backend.tf` (or `module.tf`) with a generic `resource "…" "…"` / `module "…"` +block — invented names only. (3) Extend **the same** class `infra-vars` `match.paths` to +include `envs/**/*.tf` (judgment (c)) so the file is governed and `parseHCL` runs. (4) Author +`assent test` base/head that change the `.tf` block. (5) **Run** the case and pin whatever +fail-safe decision the engine actually produces (expected: opaque ChangeSet → REVIEW, zero +partial changes). (6) Document the known limitation in `examples/packs/infra-vars/.assent/config.yaml` +(already comments the fallback) and `examples/README.md` (claim `.tf` / HCL only after this +story). Do **not** spec a new HCL parser. + +**Operator input:** none. + +**Dependencies:** S04 (nested tfvars goldens exist). + +**Definition of done:** one proving structured tfvars case (from S04) plus one `.tf` opaque +case with pinned `expect.yaml`; README inventory (S01) updated so the new format claim +matches dogfood; schema freeze vs `v0.1.0` (REQ-EX-S10-04); still single-class. + +**Not in scope:** Terraform plan blast radius (REF-GAP-4); DEM-S09 `tf-module-instance` class; +expression evaluation; a fourth pack. + +Requirements: + +- **REQ-EX-S05-01** — Given class `infra-vars` matches `envs/**/*.tf` **and** `envs/**/*.tfvars`, + when `assent lint` runs the pack, then it is clean (single class, bindings unchanged except + paths). + - Test: `examples/packs/infra-vars/.assent/config.yaml` + - Verify: `./bin/assent lint examples/packs/infra-vars` + - Level: L1 +- **REQ-EX-S05-02** — Given a directory case whose only governed change is a `.tf` file + containing an HCL **block**, when `assent test` runs, then the ChangeSet is opaque (or the + decision is the fail-safe outcome **measured** on the engine — pin the measured `decision` + in `expect.yaml`) and findings do not claim structured field diffs inside the block. + - Test: `examples/packs/infra-vars/.assent/tests/vars/tf-opaque/` (name flexible) + - Verify: `./bin/assent test examples/packs/infra-vars` + - Level: L1 +- **REQ-EX-S05-03** — Given that `.tf` case, when the `resource`/`module` block is replaced + with equivalent tfvars attributes in a *different* case, then that other case remains + structured (proves the adapter still diffs literals). Edge: do not let the opaque case + poison sibling tfvars cases in the same pack. + - Test: existing S04 tfvars cases still PASS in the same `assent test` invocation + - Verify: `./bin/assent test examples/packs/infra-vars` + - Level: L1 +- **REQ-EX-S05-04** — Given `examples/README.md` claims HCL / `.tf`, when S01 inventory runs, + then it exits 0 (format now dogfooded); claiming HCL before this fixture exists must have + been red (S01 polarity). + - Test: `hack/docs/example_format_inventory_test.sh` + - Verify: `bash hack/docs/example_format_inventory_test.sh` + - Level: L1 +- **REQ-EX-S05-05** — Given the `.tf` fixture or class-path extension is deleted, when + `--coverage` or inventory runs, then the gate is red (non-vacuity of the honesty case). + - Test: pack `--coverage` + inventory + - Verify: `./bin/assent test --coverage examples/packs/infra-vars` + - Level: L1 + +--- + +## EX-S06 — REF-EX C1–C4 in-tree fixtures [autonomous] + +**As a** maintainer **I want** generalized C1–C4 fixtures in existing packs **so that** +reference-derived patterns are dogfooded without private material (D-002). + +**Goal:** close the first four REF-EX rows by **extending** packs/archetypes, not new +products: + +| ID | Pattern | Host | Shape | +| --- | --- | --- | --- | +| C1 | list-no-shrink | topic-registry | identity-keyed `consumers` **map** (not an unkeyed list); entry delete → non-APPROVE (`require-review` / challenge). Reuses no-destruction. | +| C2 | privilege-tier allow-list | service-catalog | `tier` (already present) + rule: only allow-listed tier values or transitions; both polarities. Reuses allowed-fields. | +| C3 | wildcard-grant block | topic-registry | nested `acl.grants` map; a grant of `"*"` → **BLOCK**. | +| C4 | soft-delete-as-field-add | topic-registry | adding `tombstone: true` (or `status: retired`) treated as destruction → `require-review`, not silent APPROVE. | + +New rules that match all topic files must be **vacuous-true** when the new field is absent so +S02 goldens stay green; add the new obligation to `bindings.yaml` `require:` **only if** +every existing proving case still covers it (vacuous proof). Prefer a dedicated +`valueChanges` pointer so unrelated modifies do not fire C3/C4. + +**Operator input:** none. + +**Dependencies:** S02, S03 (nested documents exist). + +**Definition of done:** four named test directories (or inline cases) with both polarities +where the archetype has a failing polarity; `archetype-goldens.md` cross-check still holds +or is updated with new rows; sanitization green. + +**Not in scope:** C5–C8 (S07); live resource-owner; companion-file correlation engine. + +Requirements: + +- **REQ-EX-S06-01 (C1)** — Given a keyed `consumers` map on a topic, when head **removes** a + key, then decision is non-APPROVE; when head **adds** a key (or leaves the map unchanged in + a proving sibling), then the C1 rule's proving polarity is counted. + - Test: `examples/packs/topic-registry/.assent/tests/topics/list-no-shrink/` + - Verify: `./bin/assent test --coverage examples/packs/topic-registry` + - Level: L1 +- **REQ-EX-S06-02 (C2)** — Given catalog `tier`, when head sets a value outside the allow-list + (e.g. `tier: 0` or a disallowed promotion), then non-APPROVE; when head sets an allow-listed + tier (or only `oncall`), then proving polarity holds without breaking D-061. + - Test: `examples/packs/service-catalog/.assent/tests/catalog/privilege-tier/` + - Verify: `./bin/assent test --coverage examples/packs/service-catalog` + - Level: L1 +- **REQ-EX-S06-03 (C3)** — Given `acl.grants` contains `"*"`, when the negative case runs, + then **BLOCK**; a proving case with an explicit principal (invented name) does not block on + this rule. Edge: absent `acl` must not BLOCK existing topics (vacuous). + - Test: `examples/packs/topic-registry/.assent/tests/topics/wildcard-grant/` + - Verify: `./bin/assent test examples/packs/topic-registry` + - Level: L1 +- **REQ-EX-S06-04 (C4)** — Given head **adds** a tombstone/retired field that base lacked, + when the case runs, then non-APPROVE (soft-delete ≠ silent field add). Edge: modifying an + unrelated nested field without the tombstone does not fire C4. + - Test: `examples/packs/topic-registry/.assent/tests/topics/soft-delete/` + - Verify: `./bin/assent test examples/packs/topic-registry` + - Level: L1 +- **REQ-EX-S06-05** — Given any C1–C4 rule file is deleted, when `assent lint` or `--coverage` + runs, then the pack gate is red. + - Test: pack lint + `--coverage` + - Verify: `./bin/assent lint examples/packs/topic-registry && ./bin/assent lint examples/packs/service-catalog` + - Level: L1 + +--- + +## EX-S07 — REF-EX C5–C8 (facts stubs + C8 known-limitation) [autonomous] + +**As a** maintainer **I want** C5–C7 demonstrated with **stubbed** `facts.yaml` (repo-file / +resource-owner *shape*) and C8 pinned as expected REVIEW **so that** D-071 is closed without +live providers and without pretending companion-file correlation exists. + +**Goal:** + +| ID | Pattern | Host | Shape | +| --- | --- | --- | --- | +| C5 | quota-ceiling-from-fact | topic-registry | already has `facts.quota.max_partitions`; add an explicit C5 case + `examples/archetypes/quota-ceiling/` seed. Stub envelope matches `builtin/repo-file` output names. Do not call HTTP quota. | +| C6 | placement allow-list | infra-vars | `instance_set` (already on workloads) must be in `facts.placement.allowed.value`; both polarities. Archetype `examples/archetypes/placement-allow-list/`. | +| C7 | referenced-resource-ownership | topic-registry (judgment (b)) | optional `acl.owner` / `acl.resource`; stub `facts.resource_owner.owner`; mismatch → require-review. Gap **demo**, not a new builtin. Archetype `referenced-resource-ownership/`. | +| C8 | companion-file delete | infra-vars | a file **outside** class match (e.g. `envs/prod/NOTES.md` or `retired/manifest.yaml`) deleted in head; **expected REVIEW** (unmatched / unclassified — D-063 / ADR-0008 §1). Document **known limitation**: v1 does not correlate "delete A and append B". Out of v1 engine scope (REF-GAP-3). | + +C7 rule vacuous when `!has(entry.acl)` (judgment (b)). C8 must **not** add a second class. + +**Operator input:** none. + +**Dependencies:** S04, S06. + +**Definition of done:** C5–C8 named fixtures; C8 docs state REVIEW + out-of-v1; backlog +REF-EX row updated to specified/closed-by-EX; no live provider calls (`test_provider_fence` +stays green). + +**Not in scope:** implementing cross-file correlation; DEM-S00; HTTP quota URL. + +Requirements: + +- **REQ-EX-S07-01 (C5)** — Given `facts.quota.max_partitions` (or nested quota object) in + `facts.yaml`, when partitions exceed the stub, then challenge/REVIEW; when within ceiling, + proving polarity. Edge: omitted quota fact must not APPROVE a controlling predicate + (fail-safe unavailable — existing E2 behaviour; pin via a negative facts-omitted case or + document reuse of engine tests). + - Test: `examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/` + + `examples/archetypes/quota-ceiling/` + - Verify: `./bin/assent test examples/packs/topic-registry` + - Level: L1 +- **REQ-EX-S07-02 (C6)** — Given `facts.placement.allowed.value` is a list/map of instance + sets, when head sets `instance_set` to a value not in the stub, then non-APPROVE; when it + stays on an allowed set, proving polarity. + - Test: `examples/packs/infra-vars/.assent/tests/vars/placement/` + + `examples/archetypes/placement-allow-list/` + - Verify: `./bin/assent test --coverage examples/packs/infra-vars` + - Level: L1 +- **REQ-EX-S07-03 (C7)** — Given `entry.acl` names another team's resource and + `facts.resource_owner.owner.value` does not match the author groups, when the case runs, + then `require-review`. Edge: topics **without** `acl` still APPROVE the vacuous branch. + - Test: `examples/packs/topic-registry/.assent/tests/topics/referenced-ownership/` + + `examples/archetypes/referenced-resource-ownership/` + - Verify: `./bin/assent test examples/packs/topic-registry` + - Level: L1 +- **REQ-EX-S07-04 (C8)** — Given head deletes a companion file that **no** class path + matches, when `assent test` runs, then `expect.yaml` decision is **REVIEW** (measured, not + wished). Docs (`examples/README.md` or pack config comment + walkthrough S09) call this a + **known limitation**, not a feature. Edge: deleting a *governed* `.tfvars` file still hits + existing non-destructive / unmatched-delete rules — C8 is the ungoverned companion only. + - Test: `examples/packs/infra-vars/.assent/tests/vars/companion-delete/` + + optional `examples/archetypes/` known-limitation seed + - Verify: `./bin/assent test examples/packs/infra-vars` + - Level: L1 +- **REQ-EX-S07-05** — Given `cmd/assent/test_provider_fence_test.go`, when C5–C7 fixtures are + added, then `assent test` still must not construct the live provider host. + - Test: `cmd/assent/test_provider_fence_test.go` + - Verify: `go test ./cmd/assent/ -run TestAssentTestNeverCallsProviderHost` + - Level: L0 + +--- + +## EX-S08 — Discover packs; wire dogfood into `task check` + verify.yaml [autonomous] + +**As a** maintainer **I want** `task check` to run example dogfood and CI to use the same +discovery script **so that** a new pack cannot be green locally and absent in CI (or the +reverse). + +**Goal:** implement judgment (a). Add `hack/dogfood-examples.sh` that builds (or uses +`./bin/assent`) and runs `assent test` + `assent test --coverage` on every +`examples/packs/` that has `.assent/tests`. Point `Taskfile.yml` `dogfood-examples:` at +that script (drop the three-name `for` loop). Point `verify.yaml`'s dogfood step at the same +script. Add `task: dogfood-examples` to `check:` **after** `build` (script may depend on +build; avoid parallel `deps:` with `fmt` — D-124 lesson). Replace `greenExamplePacks` string +slice with a directory walk; keep `TestDogfoodScriptsIncludeGreenExamplePacks` as "script is +invoked" + "walk matches README/inventory" rather than grepping three literals. + +**Operator input:** none. Extends D-086 locally for dogfood only (judgment (e)). + +**Dependencies:** S01 (inventory contract). Does **not** wait for S05 — discovery of three +packs is enough; S05 stays one pack. + +**Definition of done:** `task check` runs dogfood; deleting the `check:` line reddens a pin +test (changelog_gate / new ex pin); adding `examples/packs/orphan/` with `.assent/` but no +tests reddens S01; a fourth green pack would be picked up without editing verify.yaml. + +**Not in scope:** `examples/demo/**`; raising coverage; gitleaks into check. + +Requirements: + +- **REQ-EX-S08-01** — Given `hack/dogfood-examples.sh`, when it runs, then every + `examples/packs/*/.assent/tests` pack is executed with `assent test` and `--coverage` and + the process exits 0 on the current corpus. + - Test: `hack/dogfood-examples.sh` + - Verify: `bash hack/dogfood-examples.sh` + - Level: L1 +- **REQ-EX-S08-02** — Given `Taskfile.yml` `dogfood-examples` and `verify.yaml` dogfood step, + when either hardcodes a three-name loop again, then `TestDogfoodScriptsIncludeGreenExamplePacks` + (or successor) fails — they must call the shared script. + - Test: `cmd/assent/test_corpus_test.go` + - Verify: `go test ./cmd/assent/ -run TestDogfoodScriptsIncludeGreenExamplePacks` + - Level: L0 +- **REQ-EX-S08-03** — Given `task check`, when `dogfood-examples` is omitted from `check:`, + then a wiring pin fails (follow `docs-gates` / `changelog_gate_test.sh` pattern). + - Test: `hack/release/changelog_gate_test.sh` WIRED_TASKS **or** `hack/docs/example_format_inventory_test.sh` / new `hack/examples/dogfood_wiring_test.sh` + - Verify: `bash hack/examples/dogfood_wiring_test.sh` (or extended existing pin) + - Level: L1 +- **REQ-EX-S08-04** — Given a new directory `examples/packs/extra/.assent/tests/...` that is + incomplete/red, when dogfood discovery runs, then the script fails (discovery does not + skip unknown names). Edge: `cmd/assent/testdata/broken-pack` is **not** discovered. + - Test: `hack/dogfood-examples.sh` + corpus test walk + - Verify: `go test ./cmd/assent/ -run TestAllExamplePacksGreenUnderAssentTest` + - Level: L0 +- **REQ-EX-S08-05** — Given `greenExamplePacks` is still a hardcoded three-tuple, when the + walk successor lands, then the slice is derived from the filesystem (or the pin test + proves the slice equals `filepath.Glob("examples/packs/*/.assent")`). + - Test: `cmd/assent/test_corpus_test.go` + - Verify: `go test ./cmd/assent/ -run TestAllExamplePacksGreenUnderAssentTest` + - Level: L0 + +--- + +## EX-S09 — Product docs walkthrough byte-pinned to complex cases [autonomous] + +**As an** adopter **I want** the published walkthrough and `examples/README.md` to show +real `assent test` / `assent lint` output on the complex fixtures **so that** docs cannot +drift to a one-field YAML sketch (AUD-S06 / D-124 pattern). + +**Goal:** update `docs/usage/walkthrough.md` (and CLI examples as needed) to copy **measured** +output from `assent test examples/packs/topic-registry` (nested YAML), a JSON catalog +nested case, a tfvars nested case, and the `.tf` opaque / C8 REVIEW case. Pin byte-for-byte +or stable substrings via `hack/docs/truthlag_pins_test.sh` (extend, do not fork). Keep +`examples/README.md` truthful (four formats after S05; C8 known limitation; repos are +layouts). Do not mention unbuilt `assent init` as shipped. D-002 names only. + +**Operator input:** none. + +**Dependencies:** S02–S07 (output must match the complex corpus). + +**Definition of done:** walkthrough Step 3 (and format section) matches a captured CLI run; +mutating the doc's PASS list without updating fixtures reddens the pin; `task docs-gates` +green. + +**Not in scope:** mkdocs theme; DEM public READMEs; changing CLI flags. + +Requirements: + +- **REQ-EX-S09-01** — Given a fresh `assent test examples/packs/topic-registry` run, when + compared to the walkthrough's console block, then every listed case name exists in actual + output (pin case names, not flaky timing). + - Test: `hack/docs/truthlag_pins_test.sh` (new pins) and/or `hack/docs/readme_smoke_test.sh` + - Verify: `task docs-gates` + - Level: L1 +- **REQ-EX-S09-02** — Given the walkthrough still shows only `partitions: 12` as the sole + governed field and omits nested YAML/JSON/tfvars/HCL, when S09 pins run, then they fail + until the page names the four formats honestly. + - Test: `hack/docs/truthlag_pins_test.sh` + - Verify: `bash hack/docs/truthlag_pins_test.sh` + - Level: L1 +- **REQ-EX-S09-03** — Given C8 / `.tf` opaque, when docs describe them, then they say + **REVIEW** / known limitation / literal-only HCL — not "assent understands Terraform". + Edge: a sentence claiming expression evaluation reddens a grep pin. + - Test: `hack/docs/truthlag_pins_test.sh` + - Verify: `bash hack/docs/truthlag_pins_test.sh` + - Level: L1 +- **REQ-EX-S09-04** — Given `examples/README.md`, when S01 inventory runs after the doc edit, + then it stays green (docs and dogfood still agree). + - Test: `hack/docs/example_format_inventory_test.sh` + - Verify: `bash hack/docs/example_format_inventory_test.sh` + - Level: L1 + +--- + +## EX-S10 — Exit gate [autonomous] + +**As a** maintainer **I want** one gate that proves the epic's invariants **so that** EX +cannot be marked done with a missing format, missing C-series case, stale docs, or schema +drift. + +**Goal:** `hack/examples/ex_exitgate_test.sh` (or Go test) asserts: (1) dogfood discovery +runs four **formats** (yaml, json, tfvars, tf/HCL block case); (2) C1–C8 test directories +exist and their `expect.yaml` decisions match the table (C3 BLOCK, C8 REVIEW, etc.); (3) +S01 inventory + `task docs-gates` green; (4) schema freeze is **ref-relative against +`v0.1.0`** (AUD-S18 / D-132 — not working-tree `git diff schemas/`); (5) `internal/core` +untouched on the lane (`git diff --exit-code origin/main...HEAD -- internal/core`); (6) +`task check` includes dogfood; (7) provider fence still holds; (8) D-002 sanitization +green. Do not raise `COVERAGE_MIN`. + +**Operator input:** none. + +**Dependencies:** S01–S09. + +**Definition of done:** exit-gate script green; deleting any C-series fixture reddens it; +backlog REF-EX marked closed by P5-EX. + +**Not in scope:** DEM-S13 publish; E10; coverage floor bump. + +Requirements: + +- **REQ-EX-S10-01** — Given the three packs after S01–S09, when the exit gate lists formats + from class match paths plus the `.tf` opaque case, then yaml, json, tfvars, and tf are all + present. + - Test: `hack/examples/ex_exitgate_test.sh` + - Verify: `bash hack/examples/ex_exitgate_test.sh` + - Level: L1 +- **REQ-EX-S10-02** — Given C1–C8 fixture paths, when any one directory is missing, then the + exit gate exits non-zero. + - Test: `hack/examples/ex_exitgate_test.sh` + - Verify: `bash hack/examples/ex_exitgate_test.sh` + - Level: L1 +- **REQ-EX-S10-03** — Given C8 `expect.yaml`, when the gate reads `decision:`, then it is + `REVIEW` (not APPROVE). + - Test: `hack/examples/ex_exitgate_test.sh` + - Verify: `bash hack/examples/ex_exitgate_test.sh` + - Level: L1 +- **REQ-EX-S10-04** — Given `schemas/**/*.json`, when the exit gate runs, then it diffs + against the immutable release tag **`v0.1.0`** — `git diff --name-status v0.1.0 -- schemas` + (JSON-only), the same freeze as AUD-S18 / D-132 / `hack/audit/exitgate_test.sh` + (`SCHEMA_BASE="${ASSENT_AUDIT_SCHEMA_BASE:-v0.1.0}"`). A working-tree `git diff schemas/` + MUST NOT be the Verify (PCS / `hack/compare/exitgate_test.sh` vacuity: committed schema + edits are invisible). The only JSON schema delta vs `v0.1.0` that may remain is the + AUD-S18 permitted description-string on + `schemas/decision/v1alpha1/decision-record.schema.json`; EX must add, delete, or modify + **no other** schema JSON. Edge: `SCHEMA_BASE` must match `^v[0-9]+\.[0-9]+\.[0-9]+$` and + resolve as `refs/tags/v0.1.0`; unsetting it or substituting `HEAD` reddens the gate + (D-132: a non-tag base compares the tree against itself). + - Test: `hack/examples/ex_exitgate_test.sh` (must contain the `v0.1.0` / `SCHEMA_BASE` + pin and `git diff --name-status`; deleting the base-ref reddens) + - Verify: `bash hack/examples/ex_exitgate_test.sh` + - Level: L1 +- **REQ-EX-S10-05** — Given `task check`, when dogfood or docs-gates or inventory is unwired, + then the exit gate or existing wiring pins fail. + - Test: `hack/examples/ex_exitgate_test.sh` + S08 wiring pin + - Verify: `task check` + - Level: L1 +- **REQ-EX-S10-06** — Given a format claimed in README that is not in the dogfood walk, when + the exit gate runs S01, then it fails (both polarities still owned by S01, re-invoked here). + - Test: `hack/docs/example_format_inventory_test.sh` + - Verify: `bash hack/docs/example_format_inventory_test.sh` + - Level: L1 +- **REQ-EX-S10-07** — Given the EX lane, when the exit gate diffs `internal/core` against + `origin/main...HEAD` (three-dot, merge-base of the lane), then the diff is empty. Edge: + a working-tree `git diff internal/core` MUST NOT be the Verify; substituting `HEAD` for + `origin/main` reddens the pin. + - Test: `hack/examples/ex_exitgate_test.sh` + - Verify: `git diff --exit-code origin/main...HEAD -- internal/core` + - Level: L1 +- **REQ-EX-S10-08** — Given new example fixtures and docs, when the exit gate runs D-002 + sanitization, then `hack/check-sanitization.sh` exits 0 (no employer names / internal + systems / verbatim private material). Edge: a planted employer token in a new fixture + reddens the script. + - Test: `hack/check-sanitization.sh` + - Verify: `bash hack/check-sanitization.sh` + - Level: L1 + +--- + +## Paths owned (file-disjoint guidance) + +| Story | Owns (prefer) | Avoid | +| --- | --- | --- | +| S01 | `hack/docs/example_format_inventory_test.sh`, `examples/README.md` (pack/format sentence only), `task docs-gates` invocation line | pack fixtures | +| S02 | `examples/packs/topic-registry/` schema nested fields + nested-pointer rule/tests | C-series test dir names | +| S03 | `examples/packs/service-catalog/` nested objects + `unkeyed-list-opaque` case | C2 dir (S06) | +| S04 | `examples/packs/infra-vars/` tfvars nested maps + bounded-change pointers | `.tf` files; `config.yaml` class paths | +| S05 | `config.yaml` class paths, `envs/**/*.tf` fixtures, README HCL sentence | rewriting S04 tfvars scalars | +| S06 | new rules/tests `list-no-shrink`, `privilege-tier`, `wildcard-grant`, `soft-delete` | quota/placement/C7/C8 | +| S07 | C5–C8 tests + `examples/archetypes/{quota-ceiling,placement-allow-list,referenced-resource-ownership}/` | E5 builtins | +| S08 | `hack/dogfood-examples.sh`, `Taskfile.yml`, `.github/workflows/verify.yaml`, `cmd/assent/test_corpus_test.go` walk | fixture content | +| S09 | `docs/usage/walkthrough.md`, `hack/docs/truthlag_pins_test.sh` pins | engine | +| S10 | `hack/examples/ex_exitgate_test.sh` | product behaviour | + +--- + +## Exit gate (epic) + +Four formats in dogfood; REF-EX C1–C8 present with expected decisions (C8 REVIEW); docs +inventory + walkthrough pins green; schema freeze vs `v0.1.0` (not working-tree); +`internal/core` unchanged vs `origin/main...HEAD`; D-002 sanitization green; +`task check` runs `dogfood-examples`; no live GitLab/PAT; D-002 clean.