From 032fadbbc377b41308740bb503f1cedd8bdc1c37 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Fri, 7 Aug 2026 13:44:18 +0200 Subject: [PATCH 01/16] :bug: docs(readme): pass the repo root to lint/test, and execute the quick-start (DOC-07) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `assent lint .assent/` exits 2: discoverAssentTree joins `.assent` onto the path it is given (`cmd/assent/lint.go`), so the documented command looked for `.assent/.assent`. Every reader who followed the README quick-start verbatim since it was written hit that. Same for `assent test .assent/`. The prose now says why the argument is the repository, not the policy directory, and names a shipped sample tree for readers without a repo of their own. `hack/docs/readme_smoke_test.sh` stops this recurring by construction: it extracts the fenced bash blocks under "## Quick start" and RUNS each `assent` line against a freshly built binary in a throwaway copy of the sample repo the README itself names. `go install` and `task` lines are skipped with a printed reason and a counter, and a block with zero executed commands fails — so the gate cannot go vacuous. Discrimination proven, not assumed: with `assent lint .assent/` restored in a scratch copy of the tree, the script exits 1 naming the failing command. REQ-AUD-S06-01. --- README.md | 11 +++- hack/docs/readme_smoke_test.sh | 111 +++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 3 deletions(-) create mode 100755 hack/docs/readme_smoke_test.sh diff --git a/README.md b/README.md index 9dd3fb9..c33eec3 100644 --- a/README.md +++ b/README.md @@ -75,13 +75,18 @@ assent version Or verify a release archive with the checksum script — details in [docs/usage/install.md](https://platformrelay.github.io/assent/usage/install/). -Lint and test policies locally: +Lint and test policies locally. Both commands take the **repository root** — `assent` +appends `.assent` itself, so passing `.assent/` makes it look for `.assent/.assent`: ```bash -assent lint .assent/ -assent test .assent/ +assent lint . +assent test . ``` +No repo of your own yet? A clone of this one ships runnable sample policy trees; run the +two commands above from `examples/packs/service-catalog` (that is the fixture +`hack/docs/readme_smoke_test.sh` executes this block against). + Developers: gates live in the [`Taskfile`](Taskfile.yml): ```bash diff --git a/hack/docs/readme_smoke_test.sh b/hack/docs/readme_smoke_test.sh new file mode 100755 index 0000000..e62df25 --- /dev/null +++ b/hack/docs/readme_smoke_test.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +# REQ-AUD-S06-01 (DOC-07) — the README quick-start is EXECUTED, not eyeballed. +# +# Extracts every fenced ```bash block from README.md's "## Quick start" section and +# runs each `assent …` line verbatim against a freshly built binary, in a throwaway +# copy of the sample repo the README itself names. A quick-start command that exits +# non-zero fails this gate — that is the whole point: `assent lint .assent/` shipped +# in the README for the entire pre-release period and exits 2 for every reader. +# +# Two command families are DELIBERATELY skipped, loudly (never silently dropped): +# go install … — needs the network and the module proxy; and what it produces is +# pinned separately by the DOC-11 caveat pin in truthlag_pins_test.sh. +# task … — `task check` IS the gate this script runs under; invoking it here +# would recurse. +# Every skip is printed with its reason and counted, so deleting the executed lines +# cannot leave the script trivially green (see the "no assent command" fail below). +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$ROOT" + +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +BIN="$WORK/bin/assent" +mkdir -p "$WORK/bin" +echo "building $BIN" +go build -o "$BIN" ./cmd/assent + +# The fixture is whatever the README names — single source of truth, so retitling the +# sample repo in the README cannot leave this script testing a stale path. +FIXTURE_REL="$(grep -o 'examples/packs/[a-z0-9-]*' README.md | head -1 || true)" +if [[ -z "$FIXTURE_REL" ]]; then + echo "FAIL: README.md quick-start names no examples/packs/ fixture to run against" >&2 + exit 1 +fi +if [[ ! -d "$ROOT/$FIXTURE_REL/.assent" ]]; then + echo "FAIL: README names fixture $FIXTURE_REL, which has no .assent/ tree" >&2 + exit 1 +fi +FIXTURE="$WORK/repo" +mkdir -p "$FIXTURE" +cp -R "$ROOT/$FIXTURE_REL/." "$FIXTURE/" +echo "fixture: $FIXTURE_REL -> $FIXTURE" + +# Quick-start bash blocks: from "## Quick start" to the next "## " heading, keeping +# only the contents of ```bash fences. +BLOCKS="$WORK/quickstart.sh" +awk ' + /^## Quick start/ { inqs = 1; next } + inqs && /^## / { inqs = 0 } + inqs && /^```bash/ { infence = 1; next } + inqs && infence && /^```/ { infence = 0; next } + inqs && infence { print } +' README.md > "$BLOCKS" + +if [[ ! -s "$BLOCKS" ]]; then + echo "FAIL: no \`\`\`bash blocks found under README.md '## Quick start'" >&2 + exit 1 +fi + +ran=0 +skipped=0 +failed=0 +while IFS= read -r line; do + # Strip trailing inline comments (`task check # fmt + vet …`) and whitespace. + cmd="${line%%#*}" + cmd="$(printf '%s' "$cmd" | sed -e 's/[[:space:]]*$//' -e 's/^[[:space:]]*//')" + [[ -z "$cmd" ]] && continue + + case "$cmd" in + "go install"*) + echo "SKIP $cmd (network + module proxy; its output is pinned by the DOC-11 caveat pin)" + skipped=$((skipped + 1)) + continue + ;; + task*) + echo "SKIP $cmd (this script runs under \`task check\`; invoking it would recurse)" + skipped=$((skipped + 1)) + continue + ;; + assent*) + run="$BIN${cmd#assent}" + ;; + *) + echo "FAIL: quick-start line is neither an assent command nor a known skip: $cmd" >&2 + failed=$((failed + 1)) + continue + ;; + esac + + echo "RUN ($FIXTURE_REL) $cmd" + if (cd "$FIXTURE" && eval "$run"); then + ran=$((ran + 1)) + else + rc=$? + echo "FAIL: README quick-start command exited $rc: $cmd" >&2 + failed=$((failed + 1)) + fi +done < "$BLOCKS" + +if [[ "$ran" -eq 0 ]]; then + echo "FAIL: no assent command in the README quick-start was executed ($skipped skipped) — the gate would be vacuous" >&2 + exit 1 +fi +if [[ "$failed" -ne 0 ]]; then + echo "FAIL: $failed README quick-start command(s) failed" >&2 + exit 1 +fi + +echo "OK: $ran README quick-start command(s) green, $skipped skipped with a stated reason" From 8e95b6e3eb495b07c1841f1b63b0a9d9f526fcca Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Fri, 7 Aug 2026 13:45:06 +0200 Subject: [PATCH 02/16] :memo: docs(install): `go install` binaries report 0.0.0-dev, not a stamped version (DOC-11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The README opened the quick-start with "Install a stamped binary" directly above a `go install` block, and install.md offered `@v0.1.0` as the reproducible-toolchain route. Neither is true: only goreleaser passes `-X main.version`, so `go install` leaves the compile-time default in place regardless of the ref. Verified by execution, not by reading the ldflags: $ GOBIN=... go install github.com/PlatformRelay/assent/cmd/assent@v0.1.0 $ assent version assent 0.0.0-dev Both surfaces now say so and route readers who need a true version string at the Homebrew tap or a release archive. The install page's "Once tagged releases publish (E9-S05/S06)" preamble also goes — they publish; v0.1.0 is out. REQ-AUD-S06-02 (DOC-11). --- README.md | 9 ++++++--- docs/usage/install.md | 23 ++++++++++++++++++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c33eec3..90a539f 100644 --- a/README.md +++ b/README.md @@ -65,15 +65,18 @@ See [system context](docs/architecture/c4-context.md) for the full C4 diagram. ## Quick start -Install a stamped binary ([install guide](docs/usage/install.md)): +Install from source ([install guide](docs/usage/install.md)): ```bash go install github.com/PlatformRelay/assent/cmd/assent@latest assent version ``` -Or verify a release archive with the checksum script — details in -[docs/usage/install.md](https://platformrelay.github.io/assent/usage/install/). +`go install` compiles without link-time stamping, so the binary it produces reports +`assent 0.0.0-dev` — even when you pin a tag (`@v0.1.0`). For a **version-stamped** +binary take the Homebrew tap or a release archive: goreleaser injects the version +(`-X main.version`) and the archives are checksum- and signature-verifiable. Both +routes are in [docs/usage/install.md](https://platformrelay.github.io/assent/usage/install/). Lint and test policies locally. Both commands take the **repository root** — `assent` appends `.assent` itself, so passing `.assent/` makes it look for `.assent/.assent`: diff --git a/docs/usage/install.md b/docs/usage/install.md index 8b2f551..972f52e 100644 --- a/docs/usage/install.md +++ b/docs/usage/install.md @@ -3,6 +3,11 @@ Assent ships as a single static Go binary. Prefer a checksum-verified install for release artifacts; use `go install` when developing from source. +Only the **release archives and the Homebrew bottle carry a stamped version** — +goreleaser injects it at link time (`-s -w -X main.version={{.Version}}` in +`.goreleaser.yaml`). A `go install` build has no such injection; see the caveat below +before you rely on `assent version` for provenance. + ## go install Requires Go 1.25+ (see `go.mod`). @@ -23,6 +28,22 @@ Confirm: assent version ``` +!!! warning "`go install` binaries report `0.0.0-dev`" + + `go install` does not pass the release ldflags, so the version stays at its + compile-time default whatever ref you build: + + ```console + $ go install github.com/PlatformRelay/assent/cmd/assent@v0.1.0 + $ assent version + assent 0.0.0-dev + ``` + + That is cosmetic for local policy authoring (`assent lint` / `assent test`), but it + means a `go install` binary cannot identify itself in a `DecisionRecord` or a support + thread. Use the [release archive](#github-release-url-pattern) or + [Homebrew](#homebrew) route when the version string has to be true. + ## curl / local install script (checksum-verified) [`hack/install.sh`](https://github.com/PlatformRelay/assent/blob/main/hack/install.sh) verifies the archive SHA256 against a @@ -46,7 +67,7 @@ Pick the archive that matches your OS/arch if the glob expands to more than one ### GitHub release URL pattern -Once tagged releases publish (E9-S05/S06): +Tagged releases publish under this pattern (`v0.1.0` onwards): ```bash VERSION=0.1.0 From a89defc07bf2ea15d2db50dc5e9cbf17875c662a Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Fri, 7 Aug 2026 13:45:38 +0200 Subject: [PATCH 03/16] :bug: docs(readme): point the ADR-0014 link at the file that exists (DOC-05) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `docs/adr/0014-policy-test-harness.md` has never existed; the adopter test format ADR is `docs/adr/0014-adopter-test-format.md`. `mkdocs --strict` cannot see this — README.md is outside `docs_dir` — so the link 404'd on GitHub unnoticed. REQ-AUD-S06-02 (DOC-05). --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 90a539f..b9bd127 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ it destructive, which policy applies. assent encodes that reasoning as policy so - **Semantic diffs** — JSON, YAML, and HCL/tfvars parse into field-level adds/modifies/deletes, not line noise ([ADR-0003](docs/adr/0003-canonical-change-model.md)). - **Testable policies** — fixture changes in, expected decision out; policies without tests - are a lint error ([ADR-0014](docs/adr/0014-policy-test-harness.md)). + are a lint error ([ADR-0014](docs/adr/0014-adopter-test-format.md)). ## How it works From af0d0cf6269321b7547612d713b4b135de2b9990 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Fri, 7 Aug 2026 13:47:15 +0200 Subject: [PATCH 04/16] =?UTF-8?q?:memo:=20docs(contract):=20fileEvents=20s?= =?UTF-8?q?hips=20add/delete=20=E2=80=94=20retire=20the=20"not=20yet=20imp?= =?UTF-8?q?lemented"=20note=20(DOC-06)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit E-FILEEVENTS landed whole-file lifecycle matching, but the stability contract still told adopters the domain did not exist, and two doc comments in the policy model said the same. The note now states what actually holds and where the boundary is: - `internal/change.FileEvent` mints whole-file add/delete changes; - `LoadMergePolicy` accepts `match.fileEvents.kinds` ⊆ {add, delete} and REJECTS modify/rename at load with a located error; - the frozen schema still accepts the full four-kind enum, so widening later is additive and needs no apiVersion bump. Verified by running the binary, not by reading the loader. `kinds: [add, delete]` in `examples/packs/service-catalog` lints clean and its delete case decides BLOCK; adding `rename` to that same rule turns `assent lint` red with match.fileEvents kind "rename" is not supported — only add and delete whole-file events are emitted; modify and rename are deferred `docs/api-stability.md` is the published mirror of the root file and is regenerated from it (only the link prefixes differ). The policy.go edit is comment-only — `git diff` contains no non-comment line. REQ-AUD-S06-02 (DOC-06). --- API_STABILITY.md | 23 ++++++++++++++--------- docs/api-stability.md | 23 ++++++++++++++--------- internal/core/policy/policy.go | 10 ++++++---- 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/API_STABILITY.md b/API_STABILITY.md index 6c9ca62..d1759c3 100644 --- a/API_STABILITY.md +++ b/API_STABILITY.md @@ -48,15 +48,20 @@ replace — the broader hard-error table in [`docs/planning/lint-hard-errors.md` Executable guards: `go test ./schemas/... -run TestDoNotGeneralize`. Removing a schema guard so one of these fixtures validates is a failing regression, not a silent policy expansion. -> **Match-domain implementation status (E1-S06).** The four match-domain *primitives* shipped in -> `internal/core/classify/matcher.go` are `files` / `values.pointers` / `valueChanges` / -> **`entryEvents`**. `entryEvents` matches collection-*entry* identity churn (a keyed map/list -> entry added/removed/renamed within one file, via the E1-S05 `EntryRef`); it is deliberately a -> distinct domain from ADR-0017 §5's **`fileEvents`**, which denotes ADR-0003's whole-file -> git-detected add/delete/rename. Whole-file `fileEvents` is **not yet implemented** (deferred to a -> fast-follow after E1-S08, which first enumerates the MR's full changed-file set). The frozen §5 -> vocabulary above is unchanged; this note records that the shipped primitive set substitutes -> `entryEvents` for `fileEvents` until the latter lands. +> **Match-domain implementation status (E1-S06, E-FILEEVENTS).** Alongside the ADR-0017 §5 +> vocabulary, `internal/core/classify/matcher.go` also ships **`entryEvents`** — collection-*entry* +> identity churn (a keyed map/list entry added/removed/renamed within one file, via the E1-S05 +> `EntryRef`). It is deliberately a distinct domain from §5's **`fileEvents`**, which denotes +> ADR-0003's whole-file git-detected lifecycle. +> +> Whole-file `fileEvents` **is implemented, for a narrowed kind set**: `internal/change.FileEvent` +> mints whole-file lifecycle changes and the loader accepts `match.fileEvents.kinds` ⊆ +> **`{add, delete}`**. `modify` and `rename` have no minting path, so a rule naming either is +> **rejected at load** with a located error (`internal/core/policy/loader.go`) rather than left to +> match nothing — closing a vacuous-cover fail-open. This loader-level narrowing sits on top of the +> frozen schema, which still accepts the full `add`/`modify`/`delete`/`rename` enum: widening the +> accepted kinds later is additive and needs no `apiVersion` bump; the frozen §5 vocabulary above is +> unchanged. ## Portability notes (validators) diff --git a/docs/api-stability.md b/docs/api-stability.md index 5efaf64..541f5d2 100644 --- a/docs/api-stability.md +++ b/docs/api-stability.md @@ -48,15 +48,20 @@ replace — the broader hard-error table in [`docs/planning/lint-hard-errors.md` Executable guards: `go test ./schemas/... -run TestDoNotGeneralize`. Removing a schema guard so one of these fixtures validates is a failing regression, not a silent policy expansion. -> **Match-domain implementation status (E1-S06).** The four match-domain *primitives* shipped in -> `internal/core/classify/matcher.go` are `files` / `values.pointers` / `valueChanges` / -> **`entryEvents`**. `entryEvents` matches collection-*entry* identity churn (a keyed map/list -> entry added/removed/renamed within one file, via the E1-S05 `EntryRef`); it is deliberately a -> distinct domain from ADR-0017 §5's **`fileEvents`**, which denotes ADR-0003's whole-file -> git-detected add/delete/rename. Whole-file `fileEvents` is **not yet implemented** (deferred to a -> fast-follow after E1-S08, which first enumerates the MR's full changed-file set). The frozen §5 -> vocabulary above is unchanged; this note records that the shipped primitive set substitutes -> `entryEvents` for `fileEvents` until the latter lands. +> **Match-domain implementation status (E1-S06, E-FILEEVENTS).** Alongside the ADR-0017 §5 +> vocabulary, `internal/core/classify/matcher.go` also ships **`entryEvents`** — collection-*entry* +> identity churn (a keyed map/list entry added/removed/renamed within one file, via the E1-S05 +> `EntryRef`). It is deliberately a distinct domain from §5's **`fileEvents`**, which denotes +> ADR-0003's whole-file git-detected lifecycle. +> +> Whole-file `fileEvents` **is implemented, for a narrowed kind set**: `internal/change.FileEvent` +> mints whole-file lifecycle changes and the loader accepts `match.fileEvents.kinds` ⊆ +> **`{add, delete}`**. `modify` and `rename` have no minting path, so a rule naming either is +> **rejected at load** with a located error (`internal/core/policy/loader.go`) rather than left to +> match nothing — closing a vacuous-cover fail-open. This loader-level narrowing sits on top of the +> frozen schema, which still accepts the full `add`/`modify`/`delete`/`rename` enum: widening the +> accepted kinds later is additive and needs no `apiVersion` bump; the frozen §5 vocabulary above is +> unchanged. ## Portability notes (validators) diff --git a/internal/core/policy/policy.go b/internal/core/policy/policy.go index 123ec76..9e9c177 100644 --- a/internal/core/policy/policy.go +++ b/internal/core/policy/policy.go @@ -97,9 +97,10 @@ type RuleDocs struct { Summary string `yaml:"summary"` } -// Match is restricted to exactly one of four ADR-0017 §5 domains. FileEvents is -// modelled so an authored use is DETECTED and rejected at load — E1 deferred -// whole-file fileEvents, so E2 supports only files/values/valueChanges. +// Match is restricted to exactly one of four ADR-0017 §5 domains. All four are +// supported; FileEvents is narrowed at load to the kinds the engine can actually +// mint (add/delete — see LoadMergePolicy and fileEventKindEmittable), so an +// authored modify/rename is DETECTED and rejected rather than matching nothing. type Match struct { Files *FilesMatch `yaml:"files"` Values *ValuesMatch `yaml:"values"` @@ -118,7 +119,8 @@ type ValuesMatch struct { Pointers []string `yaml:"pointers"` } -// FileEventsMatch is the E1-deferred whole-file lifecycle domain (rejected). +// FileEventsMatch is the whole-file lifecycle domain (EFE). Kinds are narrowed at +// load to {add, delete}; modify and rename are rejected (no minting path exists). type FileEventsMatch struct { Paths []string `yaml:"paths"` Kinds []string `yaml:"kinds"` From 5e6b6230e63b0fe7d51ee4895849b1a40ddf474d Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Fri, 7 Aug 2026 13:49:30 +0200 Subject: [PATCH 05/16] :memo: docs(walkthrough): per-step Shipped/Planned banners replace the design-fiction header (DOC-09) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The published walkthrough opened with "This is design fiction. Nothing below is implemented." That was false in both directions after v0.1.0: run/lint/test/doctor/ compare ship, while init/scan/stats/explain do not exist at all. A reader who believed the banner skipped a working product; a reader who ignored it typed commands that are not in the dispatch table. Every step now carries a Shipped or Planned banner matching the dispatch table the CLI reference is pinned to, each Planned step names what to do today instead, and a status summary table closes the page. Three specific fictions replaced with verified truth: - Step 1's `assent init --sample topic-registry` -> copy `examples/packs/ topic-registry/.assent`, which I ran: `assent lint` on the copy is clean. - Step 3's invented `assent test` output -> the real output of `assent test examples/packs/topic-registry`, pasted from the run; flags and exit codes taken from the pinned CLI reference. - Step 5's `image: ghcr.io//assent:v0` -> an in-job install. No container image is published: `.goreleaser.yaml` declares no `dockers:` block and no workflow pushes to ghcr.io. Also corrected: the observe-phase name is `observe`, not `advise` (ADR-0018 §1 enum `off | observe | enforce`). REQ-AUD-S06-02 (DOC-09). --- docs/usage/walkthrough.md | 147 ++++++++++++++++++++++++++------------ 1 file changed, 101 insertions(+), 46 deletions(-) diff --git a/docs/usage/walkthrough.md b/docs/usage/walkthrough.md index dc3480a..e9b49fa 100644 --- a/docs/usage/walkthrough.md +++ b/docs/usage/walkthrough.md @@ -1,8 +1,11 @@ -# Imagined walkthrough — adopting assent on a topic registry - -> **This is design fiction.** Nothing below is implemented; it exists so we can *feel* the -> UX before freezing contracts (meta-plan Phase 3). Where a command or field survives review, -> it becomes a spec REQ. Config semantics: [ADR-0010](../adr/0010-config-files-repo-layout.md); +# Walkthrough — adopting assent on a topic registry + +> **Mixed status — read the per-step banner.** This page began life as design fiction +> before any code existed. Most of it now describes the shipped v0.1.0 binary; the steps +> that still describe unbuilt commands are labelled **Planned**, and each one names what +> you can do today instead. The authority for what exists is +> [the CLI reference](cli.md), which is pinned byte-for-byte to `assent --help`. +> Config semantics: [ADR-0010](../adr/0010-config-files-repo-layout.md); > effects: [ADR-0007](../adr/0007-rule-effects-decision-aggregation.md). ## The repo @@ -19,78 +22,119 @@ retentionMs: 604800000 Today every MR waits for a platform engineer. Goal: routine changes merge themselves. -## Step 1 — init (5 min) +## Step 1 — scaffold a policy tree -```console -$ assent init --sample topic-registry -created .assent/config.yaml (environments: prod/dev by path; class: kafka-topic) -created .assent/bindings.yaml (kafka-topic -> pack "topics", thresholds dev=10 prod=4) -created .assent/packs/topics/ (starter rules: ownership, bounded-change, no-deletion) -created .assent/tests/topics/ (passing fixtures for every starter rule) -next: assent test && assent scan --since 90d +> **Planned — `assent init` does not exist.** There is no scaffolding subcommand in the +> shipped binary. Copy a starter pack instead; the shipped ones are complete, linted, and +> covered by their own tests. + +```bash +git clone https://github.com/PlatformRelay/assent +cp -R assent/examples/packs/topic-registry/.assent .assent +assent lint . ``` -Committed starter pack (runnable policy content): [`examples/packs/topic-registry/`](https://github.com/PlatformRelay/assent/tree/main/examples/packs/topic-registry). +That gives you `.assent/config.yaml` (environments prod/dev by path, class +`kafka-topic`), `.assent/bindings.yaml`, `.assent/packs/topics/` (ownership, +bounded-change, non-destructive, schema-valid) and `.assent/tests/topics/` with a passing +fixture for every rule — `assent lint` rejects a rule with no test case, so the two ship +together by construction. + +Committed starter packs: [`examples/packs/`](https://github.com/PlatformRelay/assent/tree/main/examples/packs) +(`topic-registry`, `service-catalog`, `infra-vars`). ## Step 2 — make a rule yours -Edit the starter pack (`.assent/packs/topics/rules/safety.yaml`), e.g. cap partitions via -your quota provider and challenge retention shrinks — see the full rule file example in -ADR-0010. Wire your company's permission source in `config.yaml`: +> **Shipped.** Policy authoring is the frozen `assent.dev/v1alpha1` surface; `assent lint` +> is the gate over it. + +Edit the starter pack (`.assent/packs/topics/rules/bounded-change.yaml`), e.g. cap +partitions via your quota provider and challenge retention shrinks — see the full rule +file example in ADR-0010. Wire your company's permission source in `config.yaml`: ```yaml providers: - author: { type: builtin/gitlab-groups } # swap for http/exec/grpc — ADR-0004 + author: { type: builtin/gitlab-groups, failure: closed } # http/exec too — ADR-0004 ``` +Re-run `assent lint .` after every edit: unknown fields, unknown enums, duplicate +collection IDs, a rule without a test, and an unsupported `match.fileEvents` kind are all +hard errors, not warnings. + ## Step 3 — test the policy like code +> **Shipped** — `assent test ` runs `.assent/tests/**` against the real engine. + ```console -$ assent test -PACK topics - ✓ partition-increase-ok APPROVE (2 obligations proved, score 1/10) - ✓ partition-decrease-challenged REVIEW: challenge retention-shrink-challenge - ✓ topic-delete-blocked BLOCK no-topic-deletion - ✗ foreign-topic-edit expected REVIEW, got APPROVE - ownership: facts.author.groups fixture lists team-orders; entry owner is team-billing - -> did you mean expect: REVIEW (uncovered)? see .assent/tests/topics/foreign-topic-edit/ -4 fixtures, 3 passed, 1 failed +$ assent test . +PASS topics/bounded-change (APPROVE) +PASS topics/bounded-change/negative (REVIEW) +PASS topics/ownership (APPROVE) +PASS topics/ownership/negative (REVIEW) +PASS topics/schema-valid (APPROVE) +PASS topics/schema-valid/negative (BLOCK) +PASS topics/non-destructive (APPROVE) +PASS topics/non-destructive-delete (REVIEW) ``` +That is real output from the shipped starter pack. Each case is a directory with +`base/`, `head/`, `facts.yaml` and `expect.yaml` (or an inline `cases.yaml` entry); a +failing case prints the expected and actual decision plus the findings that differ. +`--update` rewrites expectations from the produced actuals (refused when `CI` is set), +`--coverage` is the read-only both-polarity completeness gate. Exit `0` every case +matched; `1` a mismatch, write or load error; `2` usage, discovery, or the CI guard +refusing `--update`. + ## Step 4 — backtest before trusting it -```console -$ assent scan --since 90d --out reports/ -scanned 214 MRs (2026-04-22..2026-07-21) -$ assent stats reports/ -outcome count % top rules firing -APPROVE 131 61% bounded-change/partition-increase (88) -REVIEW 71 33% retention-shrink-challenge (24), uncovered-change (31) -BLOCK 12 6% no-topic-deletion (12) -would-have-automerged: 61% · median score 2 · 0 nondeterministic re-runs -⚠ facts resolved live (today), not as of each MR — treat the % as an estimate +> **Planned — `assent scan` and `assent stats` do not exist.** There is no historical +> backtest over closed MRs in v0.1.0, and no "would-have-automerged %" report. + +What ships instead is **`assent compare`**: replay a comparison suite of a *baseline* +versus a *candidate* policy over a fixed corpus and apply the promotion gates, so you can +see what a policy change would do before you enforce it. + +```bash +assent compare --suite compare-suite/ ``` -61% automerge on day one, and the 12 blocks are all real topic deletions. Ship it. +Use it the way you would use a backtest for *policy edits*. For first-adoption evidence +on live MRs, set the pack's rollout `phase: observe` (ADR-0018) — rules evaluate and land +in `findings.observed`, structurally excluded from the decision — and read the emitted +`DecisionRecord`s rather than expecting a scan report. ## Step 5 — wire CI (GitLab first) +> **Shipped** — `assent run` is the CI entry point. One caveat below: **no container +> image is published**, so the job installs the binary. + ```yaml # included from a PROTECTED source (compliance pipeline / protected include) — the assent # job definition must not be editable from the MR branch (ADR-0015 §4); `assent doctor` # verifies this and the required forge settings (all-threads-resolved merge gate) at setup. # One-publisher-per-MR (ADR-0019 §3): resource_group keyed per MR IID serializes concurrent # assent jobs for the same MR. Without it, duplicates converge only on the next reconcile. -# (serve mode: use a keyed per-MR lock instead — multi-replica HA is unsupported.) assent: - image: ghcr.io//assent:v0 + image: golang:1.25 # no ghcr.io/…/assent image is published yet — install in-job rules: [{ if: $CI_MERGE_REQUEST_IID }] resource_group: assent-mr-$CI_MERGE_REQUEST_IID - script: [assent run] # MR context from CI env; least-privilege token from CI variable + before_script: + # Or fetch + checksum-verify a release archive — see the install guide. + - go install github.com/PlatformRelay/assent/cmd/assent@v0.1.0 + script: + - assent run --project "$CI_PROJECT_ID" --mr "$CI_MERGE_REQUEST_IID" + --subject "file:$SUBJECT" --bot-author "$ASSENT_BOT" ``` +`GITLAB_TOKEN` comes from a masked CI variable and is never a flag. Run `assent doctor` +once during setup: it reports whether this environment can arm auto-merge and why not +when it cannot. See [Install](install.md) for the checksum-verified archive route, and +[CLI reference](cli.md) for every `assent run` flag. + ## Step 6 — the contributor experience +> **Shipped**, except the `assent explain` block at the end of this section. + A dev bumps `partitions: 12 -> 24` on their own topic in dev: pipeline runs, the MR gets a summary comment ("APPROVE — 1 obligation proved, score 1/10"), approval, and merges. Nobody was interrupted. @@ -104,7 +148,10 @@ resolved, **GitLab itself** merges (ADR-0009 amendment). Any new push cancels th re-evaluates from scratch; the policies that judged this MR came from the *target* branch, so nobody can weaken the rules in the MR they gate (ADR-0015). -Something weird? Anyone can ask locally: +> **Planned — `assent explain` does not exist.** Today the same information is in the +> `DecisionRecord` that every run emits (`assent run --emit record.json`): matched and +> unmatched rules, obligation results, and the aggregation that produced the decision. +> The sketch below is the intended local ergonomics, not a shipped command. ```console $ assent explain --mr 481 @@ -118,8 +165,16 @@ change topics/prod/orders.yaml /retentionMs modify 604800000 -> 86400000 aggregation: no block · 1 unresolved challenge -> REVIEW ``` -## What this walkthrough commits us to +## Status summary + +| Step | Command | Status | +| --- | --- | --- | +| 1 — scaffold | `assent init` | **Planned** — copy `examples/packs//.assent` | +| 2 — author | `assent lint ` | **Shipped** | +| 3 — test | `assent test ` | **Shipped** | +| 4 — backtest | `assent scan` / `assent stats` | **Planned** — `assent compare` covers policy-change replay | +| 5 — CI | `assent run` · `assent doctor` | **Shipped** (no published container image) | +| 6 — explain | `assent explain` | **Planned** — read the emitted `DecisionRecord` | -`init` with runnable samples · fixture tests with decision-level asserts and helpful failure -hints · `scan`/`stats` for evidence-based rollout · one-line CI install · rendered comments -with expandable docs/debug · `explain` that answers "why" without reading Go code. +The full dispatched command set — including `catalogue`, `render`, `eval-input` and +`version` — is in the [CLI reference](cli.md). From 7d4e6a580db468486bf3fb26410a46cd347df5c6 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Fri, 7 Aug 2026 13:50:21 +0200 Subject: [PATCH 06/16] :memo: docs(meta-plan): renumber the Phase-5 epic table to the epics that executed (DOC-10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The public meta-plan still carried the Phase-2 *proposed* cut — E2 "Decision engine + Rego frontend", E3 "Declarative YAML frontend", E8 "Forge: GitHub adapter" — which contradicts the README maturity table (Rego = E11, GitHub = E10) and every spec directory under `openspec/specs/`. A reader comparing the two pages could not tell which numbering was live. Rows are now derived from the spec titles that actually exist, each row names its spec directory, and the deferred tiers (E10-E13) are listed separately with the pointer to the README table. The three off-sequence Phase-5 epics (EFE, PCS, AUD) are named too. REQ-AUD-S06-02 (DOC-10). --- docs/planning/meta-plan.md | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/docs/planning/meta-plan.md b/docs/planning/meta-plan.md index cecf44f..1036d2c 100644 --- a/docs/planning/meta-plan.md +++ b/docs/planning/meta-plan.md @@ -75,19 +75,31 @@ minimal. ## Phase 5 — Epic execution -Spec-first, vertical slices per epic (proposed cut, refined in Phase 3): - -| Epic | Slice | -| --- | --- | -| E1 | Canonical change model: JSON + YAML (+ HCL/tfvars) | -| E2 | Decision engine + Rego frontend | -| E3 | Declarative YAML frontend | -| E4 | Forge: GitLab adapter (threads, approve, merge) | -| E5 | Provider host: built-ins + HTTP/exec | -| E6 | Adopter test harness (`assent test`) + examples | -| E7 | E2E infra: kind GitLab, sample-repo generator, conformance suite | -| E8 | Forge: GitHub adapter + Actions entrypoint | -| E9 | Distribution: releases, container, CI templates, docs site | +Spec-first, vertical slices per epic. The E-numbering below is the one that actually +executed — each row names its spec under `openspec/specs/` — and it matches the README +feature-maturity table. (The Phase-2 proposed cut had E2 as a Rego frontend and E8 as the +GitHub adapter; both moved to the deferred tier, so the numbering shifted.) + +| Epic | Slice | Spec | Status | +| --- | --- | --- | --- | +| E1 | Canonical change model: JSON + YAML (+ HCL/tfvars) | `p5-e1-canonical-change-model` | shipped | +| E2 | Decision engine + CEL predicate backend | `p5-e2-decision-engine` | shipped | +| E3 | Policy surface: `assent lint` hard errors + rule catalogue | `p5-e3-policy-surface` | shipped | +| E4 | GitLab forge adapter: Snapshot / Resolve / Reconcile | `p5-e4-gitlab-forge` | shipped | +| E5 | Provider host + builtins (HTTP/exec, gitlab-groups, ownership) | `p5-e5-provider-host` | shipped | +| E6 | Adopter test harness (`assent test`) + `assent compare` seed | `p5-e6-adopter-test` | shipped | +| E7 | E2E & conformance infra | `p5-e7-e2e-conformance` | shipped | +| E8 | Renderer & presentation (ADR-0016 tier 0) | `p5-e8-renderer` | shipped | +| E9 | Distribution & release (oss-playbook) | `p5-e9-distribution` | shipped (v0.1.0) | + +Follow-on epics cut during Phase 5, outside the E1–E9 sequence: **EFE** +(`p5-e-fileevents`, whole-file `match.fileEvents`), **PCS** +(`p5-pcs-policy-comparison`, full comparison-suite runner), **AUD** +(`p5-aud-audit-remediation`, post-release audit remediation). + +Deferred tiers keep their own numbers and unlock only with a named consumer (D-012): +**E10** GitHub adapter, **E11** Rego backend, **E12** `serve` (HTTP API), **E13** remote +packs — see the feature-maturity table in `README.md`. Ordering constraint: E7 starts early (alongside E1) because every later epic's exit gate depends on it. From 0c8afcd0fdd2f8410076f0960be0ad2e248b352c Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Fri, 7 Aug 2026 13:50:56 +0200 Subject: [PATCH 07/16] =?UTF-8?q?:memo:=20docs(adr):=20ADR-0020=20is=20Acc?= =?UTF-8?q?epted=20=E2=80=94=20its=20contract=20shipped=20in=20AUD-S01?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ADR that specified the checked-file completeness contract still read `Status: Proposed` while the mechanism it specifies is in `main`: `forge.Snapshot` carries `ChangedFilesComplete`/`ChangedFilesGap`, the GitLab adapter paginates `/diffs` with the `changes_count` cross-check and the page ceiling, an unprovable enumeration folds to an opaque changeset and lands on the frozen `changeset.undecidable` axis, and the point-6 conformance obligations have catalog rows tagged `adr: ADR-0020` with tests behind them. Every other implemented ADR in the index reads `Accepted (…)`. Status and the index row now say so, using the sibling convention of naming the decision that accepted it (D-119). No qualifier is needed — points 1-6 all landed; this is not a partial acceptance. REQ-AUD-S06-02 (routed truth-lag item). --- docs/adr/0020-forge-snapshot-changed-file-completeness.md | 2 +- docs/adr/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/adr/0020-forge-snapshot-changed-file-completeness.md b/docs/adr/0020-forge-snapshot-changed-file-completeness.md index cc1ecfa..059bdf1 100644 --- a/docs/adr/0020-forge-snapshot-changed-file-completeness.md +++ b/docs/adr/0020-forge-snapshot-changed-file-completeness.md @@ -2,7 +2,7 @@ | | | | --- | --- | -| **Status** | Proposed | +| **Status** | Accepted (D-119 — implemented in P5-AUD-S01) | | **Date** | 2026-08-06 | | **Deciders** | Konrad Heimel | | **Context links** | [ADR-0008](0008-change-classification-routing-scope.md) §4 · [ADR-0015](0015-trust-boundaries-merge-integrity.md) §1 · [ADR-0017](0017-contract-model-obligations.md) §1 (honest capability gaps) · D-042 · D-076 · D-077 · REL-07 (PROJECT-AUDIT-2026-08-06) | diff --git a/docs/adr/README.md b/docs/adr/README.md index 9c64619..8c80239 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -31,4 +31,4 @@ supersessions by ADR-0016/0017 are noted on each ADR's status line (not full | [0017](0017-contract-model-obligations.md) | Contract model: governed subjects, required obligations, typed facts, preconditioned reconciliation | Accepted | | [0018](0018-policy-lifecycle-phase-profile-comparison.md) | Policy lifecycle — phase, profiles, comparison | Accepted (D-030) | | [0019](0019-publication-marker-reconciliation-protocol.md) | Publication marker + reconciliation protocol (database-free) | Accepted (D-030) | -| [0020](0020-forge-snapshot-changed-file-completeness.md) | Forge snapshot changed-file completeness contract | Proposed | +| [0020](0020-forge-snapshot-changed-file-completeness.md) | Forge snapshot changed-file completeness contract | Accepted (D-119) | From 9ccb92f2fb6c46a8645cf575ce76d8d2e0fae6d2 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Fri, 7 Aug 2026 13:52:06 +0200 Subject: [PATCH 08/16] :memo: docs(examples): drop the pre-alpha banner and the "once it exists" harness caveat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two stale claims on a shipped surface. The banner said "Pre-alpha: schemas are illustrative drafts ... the authoritative contracts are frozen in meta-plan Phase 3" — both halves are false: Phase 3 froze (ADR-0018/0019 read `Accepted (D-030 — Phase-3 freeze review)`) and the authored surfaces are the frozen `assent.dev/v1alpha1` schemas under `schemas/`. Removed rather than reworded; no part of it survives. The intro's "must pass the adopter test harness (`assent test`) once it exists" goes the same way — it exists, and the packs pass under it. The index also listed 3 of the 9 example directories. All nine are listed now, each with what actually holds for it, verified by running the gate that covers it: - `examples/packs/**` lint clean and test green — pinned by TestExamplesPacksLoadAndLintClean and TestAllExamplePacksGreenUnderAssentTest, both inside `task check`; confirmed by hand for all three packs. - `examples/lint-fixtures/**` good/bad polarity — pinned by TestEveryHardErrorFixtureCaught. - `examples/policies/rego/` is marked illustrative: the Rego backend is deferred tier E11, so that file is a sketch, not a runnable path. REQ-AUD-S06-02 (routed truth-lag item). --- examples/README.md | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/examples/README.md b/examples/README.md index 2107df1..0f61604 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,13 +2,26 @@ Living documentation. Rules are written in the **YAML envelope** (ADR-0002 v2/ADR-0013): `assert` predicates (CEL leaves, string shorthand) for the archetypes, `rego` modules where -logic outgrows tier 1 — the rego example here shows that escape hatch. Every example must -pass the adopter test harness (`assent test`) once it exists — examples that don't run are -lies. +logic outgrows tier 1 — the rego example here sketches that escape hatch. Examples that +don't run are lies, so the runnable ones are executed by the gates, not just read. -> ⚠️ Pre-alpha: schemas are **illustrative drafts** anchored by ADR-0010/0013; the -> authoritative contracts are frozen in meta-plan Phase 3. +- [`packs/`](packs/) — complete adopter policy trees (`topic-registry`, `service-catalog`, + `infra-vars`). Each is a repo root: `assent lint ` is clean and + `assent test ` passes, both under `task check`. Start here. +- [`policies/declarative/`](policies/declarative/) — standalone envelope rules with + `assert` predicates +- [`policies/rego/`](policies/rego/) — the tier-2 escape hatch for the same archetype. + **Illustrative only**: the Rego backend is a deferred tier (E11); the CEL/assert path is + what ships. +- [`archetypes/`](archetypes/) — one directory per rule archetype from `docs/vision.md` +- [`lint-fixtures/`](lint-fixtures/) — `good`/`bad` pairs pinning each `assent lint` + hard error in both polarities +- [`repos/`](repos/) — generic sample self-service repo layouts (generated; e2e seeds) and + the open-source corpus snapshots +- [`comparison/`](comparison/) — `assent compare` suites and promotion-gate fixtures +- [`render/`](render/) — committed finding fixtures for `assent render` +- [`contracts/`](contracts/) — frozen contract fixtures (D-016 strict, named-consumer compat) -- [`policies/declarative/`](policies/declarative/) — envelope rules with `assert` predicates -- [`policies/rego/`](policies/rego/) — the tier-2 escape hatch for the same archetype -- [`repos/`](repos/) — generic sample self-service repo layouts (generated; e2e seeds) +The authored surfaces here are the **frozen** `assent.dev/v1alpha1` schemas under +`schemas/`, not drafts; the compatibility promises attached to them are in +[`API_STABILITY.md`](../API_STABILITY.md). From 47ad5c8ba522a2b96762b073d978d53848c567ae Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Fri, 7 Aug 2026 13:52:43 +0200 Subject: [PATCH 09/16] :memo: docs(usage): document the checkout-less enumeration contract on the -checkout flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AUD-S01 changed what a checkout-less run does when the forge cannot prove it enumerated every changed file: the change set becomes opaque and the decision degrades to REVIEW with `changeset.undecidable`. The CLI reference still described `-checkout` purely as "when unset, only the governed subject is diffed", which reads as a scoping choice rather than a fail-safe with an observable outcome — an operator debugging a surprise REVIEW had nothing here to explain it. Adds a short section under `assent run` covering the completeness proof, the REVIEW degradation and its finding code, the still-BLOCK case for a visible `.assent/**` path, and the D-077 checkout-mode carve-out. The fenced help block is untouched — it stays pinned byte-for-byte to the binary (AUD-S05); this is prose and one flag-table cell only. REQ-AUD-S06-02 (routed truth-lag item). --- docs/usage/cli.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/usage/cli.md b/docs/usage/cli.md index 4790a01..6299452 100644 --- a/docs/usage/cli.md +++ b/docs/usage/cli.md @@ -84,7 +84,7 @@ and is never a flag; without it the command exits `2` before contacting the forg | `-binding` | `.assent/ruleset-binding.yaml` | RulesetBinding path, loaded from the target ref | | `-config` | — | optional Config path; when set, provider posture is validated | | `-pack` | — | optional Pack path; its `spec.phase` caps every rule's phase | -| `-checkout` | — | local checkout dir (`base/` + `head/` subtrees) used to enumerate the MR's full changed-file set; when unset, only the governed subject is diffed | +| `-checkout` | — | local checkout dir (`base/` + `head/` subtrees) used to enumerate the MR's full changed-file set; when unset, the forge snapshot is the sole enumerator (see below) | | `-emit` | stdout | path to write the `DecisionRecord` JSON | | `-arm` | off | sandbox arming override — approve and merge only when set **and** the decision is APPROVE | @@ -92,6 +92,22 @@ Exit codes: `0` the run completed and produced a valid receipt (an advisory REVIEW/BLOCK, or an APPROVE without `--arm`, is still a clean `0`); `1` a hard error during orchestration; `2` a missing flag, a missing `GITLAB_TOKEN`, or `-h`. +### Checkout-less runs and enumeration completeness + +Without `-checkout`, the forge snapshot's changed-file list is the only thing that can +see a `.assent/**` policy edit outside the governed subject — so an incomplete list +would silently starve the self-edit guard. Per [ADR-0020](../adr/0020-forge-snapshot-changed-file-completeness.md) +the adapter must therefore *prove* completeness (paginated `/diffs`, cross-checked +against the MR's `changes_count`, below a page ceiling). When it cannot, the run does +not guess and does not fail silently: the change set is marked opaque and the decision +degrades to **REVIEW** with finding code `changeset.undecidable`, carrying the gap +reason. A `DecisionRecord` is still emitted and a thread still posted; approve and +merge are impossible on that path. A `.assent/**` path that *is* visible in a partial +list still dominates to BLOCK. + +With `-checkout` the local tree is the sole authority (D-077) and snapshot completeness +is not consulted. + ## assent doctor Report whether this environment can arm auto-merge, and why not when it cannot. From ad448fc87ea3a9f4c6e1b614d71d8317dd6c83d6 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Fri, 7 Aug 2026 13:53:10 +0200 Subject: [PATCH 10/16] :memo: docs(release): mandate patch tags over in-place asset replacement (SEC-07) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v0.1.0 assets were replaced in place twice after publication. Each replacement was re-signed and re-attested consistently, so the audit accepted the outcome — but a consumer who had already verified the previous checksum has no signal that the bytes changed. One runbook line so the next occurrence is a patch tag instead, and so `workflow_dispatch` on an existing tag stays scoped to recovering a failed upload. No process machinery beyond this: SEC-07 is an accepted finding, and the epic's non-goals rule out anything heavier. REQ-AUD-S06-02 (SEC-07 runbook line). --- hack/release/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/hack/release/README.md b/hack/release/README.md index 027f8c9..3a828e0 100644 --- a/hack/release/README.md +++ b/hack/release/README.md @@ -48,6 +48,12 @@ notes — same pattern as mkurator: `hack/release/snapshot_test.sh` — both run goreleaser without publish credentials (sign/sbom skipped — no fake signatures locally). +**Never replace published assets in place (SEC-07).** Once a tag's artifacts are uploaded, +fix anything wrong with them by cutting a **patch tag** (`v0.1.1`), not by re-running the +workflow over the same tag: a consumer who verified the old checksum or attestation has no +way to learn the bytes changed underneath them. `workflow_dispatch` on an existing tag is for +recovering a *failed* upload, not for revising a successful one. + ### Supply chain on tagged release (E9-S06, D-109) The publish job in `.github/workflows/release.yaml` (tag push / `workflow_dispatch` only): From 8b190109dee1b0e1de40b317bc80baafcc9466f7 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Fri, 7 Aug 2026 13:57:15 +0200 Subject: [PATCH 11/16] :white_check_mark: test(docs): pin the retired truth-lag claims so they cannot come back (DOC-05/06/09/10/11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `hack/docs/truthlag_pins_test.sh` guards the surfaces AUD-S05's TestNoStaleProductClaims does not walk — repo-root markdown and examples/ — plus four drift pairs no build step checks: DOC-05 every in-repo relative link in README.md resolves on the filesystem. `mkdocs --strict` cannot see these: README.md is outside docs_dir, which is exactly why the dead ADR-0014 link survived. DOC-06 neither API-stability copy claims something is "not yet implemented", both still carry the fileEvents note (so the first pin cannot go vacuous), and docs/api-stability.md stays byte-identical to the root file modulo the two docs-relative link prefixes. DOC-09 no design-fiction banner, and EACH `## Step` heading is followed by its own Shipped/Planned banner before the next heading. DOC-10 the Phase-5 table has all nine E1..E9 rows and binds no deferred tier (Rego/GitHub/serve) to one of those numbers. DOC-11 README and install.md name the version `go install` actually prints. plus docs/adr/README.md's status column agrees with each ADR's own Status row — the drift that left ADR-0020 "Proposed" in two places at once. Every pin was proven to discriminate, not assumed to: nine mutations applied one at a time to a scratch copy of the tree (banner restored, link broken, mirror drifted, epic renumbered, caveat deleted, ADR status reverted, ...) and each turned the script red naming its finding. The first draft of the DOC-09 check compared banner COUNTS and survived deleting a step's banner; that is why it is now a per-step walk. The walkthrough header drops the words "design fiction" so the phrase pin stays exact. REQ-AUD-S06-02. --- docs/usage/walkthrough.md | 4 +- hack/docs/truthlag_pins_test.sh | 175 ++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+), 2 deletions(-) create mode 100755 hack/docs/truthlag_pins_test.sh diff --git a/docs/usage/walkthrough.md b/docs/usage/walkthrough.md index e9b49fa..a9333bc 100644 --- a/docs/usage/walkthrough.md +++ b/docs/usage/walkthrough.md @@ -1,7 +1,7 @@ # Walkthrough — adopting assent on a topic registry -> **Mixed status — read the per-step banner.** This page began life as design fiction -> before any code existed. Most of it now describes the shipped v0.1.0 binary; the steps +> **Mixed status — read the per-step banner.** This page began life as a UX sketch +> written before any code existed. Most of it now describes the shipped v0.1.0 binary; the steps > that still describe unbuilt commands are labelled **Planned**, and each one names what > you can do today instead. The authority for what exists is > [the CLI reference](cli.md), which is pinned byte-for-byte to `assent --help`. diff --git a/hack/docs/truthlag_pins_test.sh b/hack/docs/truthlag_pins_test.sh new file mode 100755 index 0000000..a59db0c --- /dev/null +++ b/hack/docs/truthlag_pins_test.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env bash +# REQ-AUD-S06-02 — grep pins for the truth-lag DOC-05/06/09/10/11 closed. +# +# These are cheap mechanisms that keep a corrected claim corrected. They cover the +# surfaces `cmd/assent`'s TestNoStaleProductClaims does NOT walk (repo-root markdown +# and examples/), plus four drift pairs that no build step checks: +# +# DOC-05 README.md's relative links resolve on the filesystem. `mkdocs --strict` +# cannot see them: README.md is outside docs_dir. +# DOC-06 neither API-stability copy claims fileEvents is unimplemented, and the +# published mirror stays byte-identical to the root file modulo the +# docs-relative link prefixes. +# DOC-09 the walkthrough carries no design-fiction banner, and every step carries a +# Shipped/Planned banner. +# DOC-10 the meta-plan Phase-5 epic table covers E1..E9 and does not bind a +# deferred-tier concept (Rego, GitHub adapter) to one of those numbers. +# DOC-11 the `go install` caveat names the version it actually prints. +# plus docs/adr/README.md's status column agrees with each ADR's own Status row. +# +# Every check prints PASS or FAIL and the script exits 1 if any failed, so a +# regression names the finding it reopens. +set -uo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$ROOT" + +fails=0 +pass() { echo "PASS $1"; } +fail() { echo "FAIL $1" >&2; fails=$((fails + 1)); } + +# --- retired pre-release phrases ------------------------------------------------- +# Scope: repo-root markdown + examples/**, i.e. exactly what TestNoStaleProductClaims +# leaves uncovered. README's maturity legend legitimately defines "Planned = designed +# seam, not yet implemented", so that phrase is pinned only on the contract files. +for f in README.md API_STABILITY.md examples/README.md; do + if grep -qi 'pre-alpha' "$f"; then + fail "DOC-08/examples: $f still carries a 'pre-alpha' claim" + else + pass "no pre-alpha claim in $f" + fi +done + +for f in API_STABILITY.md docs/api-stability.md; do + if grep -qi 'not yet implemented' "$f"; then + fail "DOC-06: $f still says something is 'not yet implemented' — fileEvents ships add/delete" + else + pass "DOC-06: no unimplemented claim in $f" + fi + if ! grep -q 'fileEvents' "$f"; then + fail "DOC-06: $f no longer discusses fileEvents at all — the pin above went vacuous" + else + pass "DOC-06: $f still carries the fileEvents match-domain note" + fi +done + +# --- DOC-06: the published mirror tracks the root file --------------------------- +MIRROR_EXPECTED="$(mktemp)" +trap 'rm -f "$MIRROR_EXPECTED"' EXIT +sed -e 's|](docs/adr/|](adr/|g' -e 's|](docs/planning/|](planning/|g' API_STABILITY.md > "$MIRROR_EXPECTED" +if diff -q "$MIRROR_EXPECTED" docs/api-stability.md >/dev/null; then + pass "DOC-06: docs/api-stability.md matches API_STABILITY.md (modulo docs-relative links)" +else + fail "DOC-06: docs/api-stability.md has drifted from API_STABILITY.md:" + diff "$MIRROR_EXPECTED" docs/api-stability.md >&2 || true +fi + +# --- DOC-05: README relative links resolve --------------------------------------- +broken=0 +checked=0 +while read -r target; do + [[ -z "$target" ]] && continue + checked=$((checked + 1)) + # Strip any #anchor; only the file must exist. + path="${target%%#*}" + [[ -z "$path" ]] && continue + if [[ ! -e "$path" ]]; then + echo " broken README link: $target" >&2 + broken=$((broken + 1)) + fi +done < <(grep -o '](\(docs\|examples\|hack\|internal\|cmd\|schemas\|openspec\|test\)/[^)]*)' README.md | sed -e 's/^](//' -e 's/)$//') + +if [[ "$checked" -eq 0 ]]; then + fail "DOC-05: README.md has no in-repo relative links to check — the pin would be vacuous" +elif [[ "$broken" -ne 0 ]]; then + fail "DOC-05: $broken README.md link(s) do not resolve on the filesystem" +else + pass "DOC-05: all $checked in-repo README.md links resolve" +fi + +# --- DOC-09: walkthrough banners -------------------------------------------------- +WT=docs/usage/walkthrough.md +if grep -qi 'design fiction\|Nothing below is implemented' "$WT"; then + fail "DOC-09: $WT still opens with a design-fiction banner" +else + pass "DOC-09: no design-fiction banner in $WT" +fi + +# Per-step, not a count: each `## Step` heading must be FOLLOWED by its own +# Shipped/Planned banner before the next heading. A count comparison is not +# discriminating — extra banners elsewhere on the page would mask a step that lost one. +unbannered="$(awk ' + /^## / { + if (step != "" && !seen) print step + step = "" + if ($0 ~ /^## Step /) { step = $0; seen = 0 } + next + } + step != "" && /^> \*\*(Shipped|Planned)/ { seen = 1 } + END { if (step != "" && !seen) print step } +' "$WT")" +steps="$(grep -c '^## Step ' "$WT")" +if [[ "$steps" -eq 0 ]]; then + fail "DOC-09: $WT has no '## Step' sections — the banner pin would be vacuous" +elif [[ -n "$unbannered" ]]; then + fail "DOC-09: walkthrough step(s) with no Shipped/Planned banner:" + printf ' %s\n' "$unbannered" >&2 +else + pass "DOC-09: all $steps walkthrough steps carry a Shipped/Planned banner" +fi + +# --- DOC-10: meta-plan epic numbering --------------------------------------------- +MP=docs/planning/meta-plan.md +# The Phase-5 epic table rows, E1..E9. +epic_rows="$(grep -c '^| E[1-9] |' "$MP")" +if [[ "$epic_rows" -ne 9 ]]; then + fail "DOC-10: $MP Phase-5 table has $epic_rows E1..E9 rows, expected 9" +else + pass "DOC-10: $MP lists all nine executed epics" +fi +# Deferred tiers must not be bound to an E1..E9 row (the old table had E2=Rego, E8=GitHub). +for concept in Rego GitHub serve; do + if grep '^| E[1-9] |' "$MP" | grep -qi "$concept"; then + fail "DOC-10: $MP binds deferred tier '$concept' to an E1..E9 row — README says E10-E13" + else + pass "DOC-10: deferred tier '$concept' is not an E1..E9 row in $MP" + fi +done + +# --- DOC-11: the go install version caveat ---------------------------------------- +for f in README.md docs/usage/install.md; do + if grep -q '0\.0\.0-dev' "$f" && grep -q 'go install' "$f"; then + pass "DOC-11: $f states what \`go install\` actually reports" + else + fail "DOC-11: $f mentions \`go install\` without the 0.0.0-dev caveat" + fi +done + +# --- ADR index status agrees with each ADR's own Status row ------------------------ +adr_checked=0 +for adr in docs/adr/0*.md; do + base="$(basename "$adr")" + own="$(grep -m1 '^| \*\*Status\*\* |' "$adr" | sed -e 's/^| \*\*Status\*\* | *//' -e 's/ *|$//' | awk '{print $1}')" + # Appendices (e.g. 0013-appendix-syntax-gallery.md) carry no Status row of their own + # and get no index row of their own; they are linked from their parent ADR's row. + [[ -z "$own" ]] && continue + # Only the row whose FIRST cell links this file — not a parent ADR row that mentions it. + row="$(grep "^| \[[0-9]*\]($base)" docs/adr/README.md | head -1)" + [[ -z "$row" ]] && { fail "ADR index: no row for $base"; continue; } + idx="$(printf '%s' "$row" | awk -F'|' '{print $4}' | sed -e 's/^ *//' | awk '{print $1}')" + adr_checked=$((adr_checked + 1)) + if [[ "$own" != "$idx" ]]; then + fail "ADR index: $base says '$own' but docs/adr/README.md says '$idx'" + fi +done +if [[ "$adr_checked" -eq 0 ]]; then + fail "ADR index: no ADRs compared — the pin would be vacuous" +else + pass "ADR index: $adr_checked ADR status rows agree with their files" +fi + +if [[ "$fails" -ne 0 ]]; then + echo "FAILED: $fails truth-lag pin(s) reopened" >&2 + exit 1 +fi +echo "OK: all truth-lag pins green" From 43e822c7fadc7e9341577541353e3f81162134b3 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Fri, 7 Aug 2026 14:02:41 +0200 Subject: [PATCH 12/16] :memo: docs(install): narrow the 0.0.0-dev consequence to the version string (D-120) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit D-120 landed on main while this lane was open: `pins.toolDigest` now derives from the binary's Go build info, so a `go install` build IS distinguishable in a DecisionRecord. The DOC-11 caveat written earlier in this lane overshot — it said such a binary "cannot identify itself in a DecisionRecord", which D-120 made false. What actually holds is narrower: `pins.toolVersion` reads `0.0.0-dev` and cannot be mapped back to a released tag. The walkthrough's CI job used `go install` with no note, which contradicted the install page's own advice on the one path that emits records. It now states the tradeoff inline and points at the archive route. REQ-AUD-S06-02 (DOC-11). --- docs/usage/install.md | 10 ++++++---- docs/usage/walkthrough.md | 6 +++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/usage/install.md b/docs/usage/install.md index 972f52e..ec5e3b9 100644 --- a/docs/usage/install.md +++ b/docs/usage/install.md @@ -39,10 +39,12 @@ assent version assent 0.0.0-dev ``` - That is cosmetic for local policy authoring (`assent lint` / `assent test`), but it - means a `go install` binary cannot identify itself in a `DecisionRecord` or a support - thread. Use the [release archive](#github-release-url-pattern) or - [Homebrew](#homebrew) route when the version string has to be true. + That is cosmetic for local policy authoring (`assent lint` / `assent test`). A + `DecisionRecord` from such a binary is still identifiable — `pins.toolDigest` is a + sha256 over the binary's Go build info (D-120), so different builds differ regardless + of the version string — but `pins.toolVersion` reads `0.0.0-dev` and cannot be mapped + back to a released tag. Use the [release archive](#github-release-url-pattern) or + [Homebrew](#homebrew) route when the version string itself has to be true. ## curl / local install script (checksum-verified) diff --git a/docs/usage/walkthrough.md b/docs/usage/walkthrough.md index a9333bc..9dd5cf7 100644 --- a/docs/usage/walkthrough.md +++ b/docs/usage/walkthrough.md @@ -119,7 +119,11 @@ assent: rules: [{ if: $CI_MERGE_REQUEST_IID }] resource_group: assent-mr-$CI_MERGE_REQUEST_IID before_script: - # Or fetch + checksum-verify a release archive — see the install guide. + # Simplest route, shown here to keep the example self-contained. It stamps the + # binary `0.0.0-dev`, so every DecisionRecord this job emits carries that in + # `pins.toolVersion` (`pins.toolDigest` still identifies the build, D-120). + # For a record that names the real tag, install the checksum-verified release + # archive instead — the full URL pattern is in the install guide. - go install github.com/PlatformRelay/assent/cmd/assent@v0.1.0 script: - assent run --project "$CI_PROJECT_ID" --mr "$CI_MERGE_REQUEST_IID" From d393efcc493ccc68177e90b7b6078696610d1d0c Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Fri, 7 Aug 2026 14:33:05 +0200 Subject: [PATCH 13/16] :bug: fix(docs-gates): the scripts claimed a wiring that does not exist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `readme_smoke_test.sh` told the reader "`task check` IS the gate this script runs under", and printed the same claim at runtime when it skipped a `task` line. Neither script is invoked by `Taskfile.yml` or any workflow — they run only by hand. A tool built to kill unverified claims was carrying one about itself. Both now state the truth: the wiring is intended, is Lane B's to add, and is recorded as D-124; the skip reason says the recursion would happen *once wired*. Both headers carry an explicit NOT YET WIRED note with the consequence. REQ-AUD-S06-01, REQ-AUD-S06-02. --- hack/docs/readme_smoke_test.sh | 11 ++++++++--- hack/docs/truthlag_pins_test.sh | 4 ++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/hack/docs/readme_smoke_test.sh b/hack/docs/readme_smoke_test.sh index e62df25..f3dd01f 100755 --- a/hack/docs/readme_smoke_test.sh +++ b/hack/docs/readme_smoke_test.sh @@ -10,10 +10,15 @@ # Two command families are DELIBERATELY skipped, loudly (never silently dropped): # go install … — needs the network and the module proxy; and what it produces is # pinned separately by the DOC-11 caveat pin in truthlag_pins_test.sh. -# task … — `task check` IS the gate this script runs under; invoking it here -# would recurse. +# task … — this script is INTENDED to run inside `task check` (D-124: wiring is +# Lane B's, not yet landed), so invoking `task check` from here would +# recurse the moment that wiring exists. # Every skip is printed with its reason and counted, so deleting the executed lines # cannot leave the script trivially green (see the "no assent command" fail below). +# +# NOT YET WIRED INTO ANY GATE (D-124): `Taskfile.yml` is Lane B's file, so today this +# runs only when invoked by hand. Until Lane B adds it (and truthlag_pins_test.sh) to +# `task check`, a README edit can reopen DOC-07 with nothing going red. set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" @@ -75,7 +80,7 @@ while IFS= read -r line; do continue ;; task*) - echo "SKIP $cmd (this script runs under \`task check\`; invoking it would recurse)" + echo "SKIP $cmd (this script belongs inside \`task check\`; invoking it here would recurse once wired — D-124)" skipped=$((skipped + 1)) continue ;; diff --git a/hack/docs/truthlag_pins_test.sh b/hack/docs/truthlag_pins_test.sh index a59db0c..b6eed7e 100755 --- a/hack/docs/truthlag_pins_test.sh +++ b/hack/docs/truthlag_pins_test.sh @@ -19,6 +19,10 @@ # # Every check prints PASS or FAIL and the script exits 1 if any failed, so a # regression names the finding it reopens. +# +# NOT YET WIRED INTO ANY GATE (D-124): `Taskfile.yml` is Lane B's file, so today this +# runs only when invoked by hand. Until Lane B adds it (and readme_smoke_test.sh) to +# `task check`, a docs edit can reopen these findings with nothing going red. set -uo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" From 98831db51cbfbc6ac821989cee00eac4de07e566 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Fri, 7 Aug 2026 14:33:15 +0200 Subject: [PATCH 14/16] =?UTF-8?q?:memo:=20docs(decisions):=20record=20D-12?= =?UTF-8?q?4=20=E2=80=94=20the=20AUD-S06=20docs=20gates=20are=20unwired,?= =?UTF-8?q?=20Lane=20B=20owns=20the=20wiring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two truth-lag gates from this lane are green and mutation-proven but nothing invokes them, and that fact existed nowhere in the repository — only in a session transcript, which is to say nowhere. D-124 records it durably: the scripts, why AUD-S06 could not wire them (`Taskfile.yml` and `.github/workflows/**` are Lane B's), the assignment (a `docs-gates` task as a `check:` dependency, following the `hack/compare/exitgate_test.sh` precedent from D-118), and the consequence in plain words — until then a README edit reopens DOC-07 with nothing going red. The row also carries the two known pin gaps to extend when wiring: the DOC-09 check asserts banner presence, not polarity; the DOC-05 link loop covers README.md only, so the links added to examples/README.md are unpinned. REQ-AUD-S06-02. --- docs/decisions/decisions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/decisions/decisions.md b/docs/decisions/decisions.md index 8714c0e..e4e93f7 100644 --- a/docs/decisions/decisions.md +++ b/docs/decisions/decisions.md @@ -128,3 +128,4 @@ project/process decisions. | D-121 | 2026-08-06 | **ARCH-04 — canonical-hash split codified; `assent-jcs-v1` wired into the replay-bundle digest (corrects D-114 drift).** Rule: digests over BYTE artifacts (policySha over policy bytes, marker occurrence over judged head bytes, marker decision over emitted DecisionRecord bytes, toolDigest) are raw `sha256:` — byte identity is the point; digests over SCHEMA-OWNED JSON DOCUMENTS that consumers re-parse and re-verify use `internal/core/hash.Digest` (ADR-0017 §9 domain separation) with the schema `$id` as domain. Exactly one digest switches now: `compare.ReplayBundleDigest` → `hash.Digest("https://assent.dev/schemas/decision/v1alpha1/replay-bundle.schema.json", raw)` (D-114 claimed this; implementation used undomained `sha256(json.Marshal(decoded))`). Migration (pre-v1, same commit): regenerate every `replayBundleDigest` in `examples/comparison/*/suite.yaml`; caseIds/bundles unchanged (D-113 immutability preserved — algorithm versioned by this row). ADR-0019 marker grammar untouched. Revert: restore undomained digest + old corpus pins. | | D-122 | 2026-08-06 | **REL-08 — DecisionRecord emit precedes Reconcile; emit is atomic and fail-closed.** New invariant: NO forge write without a schema-valid, durably-emitted DecisionRecord. Order in `orchestrate`: build → marshal → schema-validate → EMIT (`--emit`: write `.tmp` same-dir + `os.Rename`; stdout: unchanged) → Reconcile → summary. Emit failure → hard error, zero forge writes. Pure reordering: `recordJSON` is fully determined pre-Reconcile (receipt lives in the summary line, not the record) — byte-identical records, marker digests and determinism gate unaffected. Rejected: post-reconcile record stamping (breaks record byte-stability vs marker `decision` digest). Revert: move emit back below Reconcile. | | D-123 | 2026-08-06 | **ARCH-01 — boundary enforcement automated (depguard + extended purity walk); ADR-0011 Amendment 3 truths the "arch-lint enforced" claim.** Two layers: (1) golangci `depguard` deny-rules — `internal/core/**`, `internal/change/**`, `internal/glob`, `internal/lint`, `internal/catalogue`, `internal/evaldecode`, `internal/compare`, `schemas/**` may import none of `internal/forge/**`, `internal/render/**`, `cmd/**`, `net/**`; (2) `TestCorePurity` walk extends to `../evaldecode`, `../compare`, and `../../schemas` (call-level: `time.Now`/`os.Getenv`/`os.Environ`/rand/net, adversarial self-test retained). Scope note: this EXTENDS the AGENTS.md rule-7 pure tree — `internal/evaldecode` (engine input decode) and `internal/compare` (D-116/D-117 gate determinism) join the determinism guard; `schemas` is embedded compile-time authority. Acceptance: a synthetic violating import/call fails CI both ways. Revert: drop depguard rules + walk dirs and re-amend ADR-0011. | +| D-124 | 2026-08-07 | **AUD-S06 residual — the two docs truth-lag gates exist but are UNWIRED; wiring is assigned to Lane B.** `hack/docs/readme_smoke_test.sh` (REQ-AUD-S06-01, executes every README quick-start command) and `hack/docs/truthlag_pins_test.sh` (REQ-AUD-S06-02, 18 grep/diff pins over DOC-05/06/09/10/11 + the ADR status index) are green and mutation-proven, but nothing invokes them: `Taskfile.yml` and `.github/workflows/**` belong to **Lane B** (AUD-S02/S03/S09/S14), so AUD-S06 could not add them. **Consequence, stated plainly: until they are wired, a future README or docs edit reopens DOC-07 (a quick-start that exits 2) or any pinned claim with NOTHING going red** — the mechanism is a manual gate, which is the same class of defect AUD-S06 exists to close. Assignment: Lane B adds a `docs-gates` task (`bash hack/docs/readme_smoke_test.sh && bash hack/docs/truthlag_pins_test.sh`) as a `check:` dependency, following the `hack/compare/exitgate_test.sh` precedent (D-118), alongside its AUD-S09/S14 workflow work. **Extend when wiring** (known pin gaps, both accepted for now): (a) the DOC-09 walkthrough check asserts banner PRESENCE, not polarity — flipping a step's `Planned` to `Shipped` stays green; (b) the DOC-05 link-resolution loop is scoped to `README.md`, so the relative links in `examples/README.md` are unpinned. Revert: delete the two scripts and reopen DOC-05/06/07/09/10/11 as live findings. | From 9e0946e1dea9faa19bc0a5f8cff918e64df73694 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Fri, 7 Aug 2026 14:33:24 +0200 Subject: [PATCH 15/16] :bug: docs(examples): starter packs advertised a subcommand that does not exist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All three `examples/packs/*/.assent/config.yaml` opened with "(assent init --sample )". These trees are meant to be COPIED into an adopter's repository — the walkthrough now tells readers to do exactly that, because there is no `assent init` in the dispatch table — so the false claim was shipping verbatim into every adopter repo that followed the documented path, in the one file they are most likely to read first. Each header now says what to do instead and points at the walkthrough. All three packs still lint clean. REQ-AUD-S06-02 (routed truth-lag item). --- examples/packs/infra-vars/.assent/config.yaml | 3 ++- examples/packs/service-catalog/.assent/config.yaml | 3 ++- examples/packs/topic-registry/.assent/config.yaml | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/examples/packs/infra-vars/.assent/config.yaml b/examples/packs/infra-vars/.assent/config.yaml index cb77088..72a4c10 100644 --- a/examples/packs/infra-vars/.assent/config.yaml +++ b/examples/packs/infra-vars/.assent/config.yaml @@ -1,4 +1,5 @@ -# Starter pack for examples/repos/infra-vars (assent init --sample infra-vars). +# Starter pack for examples/repos/infra-vars. Copy this .assent/ tree into your +# repo root (there is no `assent init` subcommand — see docs/usage/walkthrough.md). # # Opaque-change fallback (ADR-0003): when the HCL parser cannot classify a construct # (parse failure or unmodeled shape), the change is opaque → REVIEW — never silently diff --git a/examples/packs/service-catalog/.assent/config.yaml b/examples/packs/service-catalog/.assent/config.yaml index 7dc948b..89a9e77 100644 --- a/examples/packs/service-catalog/.assent/config.yaml +++ b/examples/packs/service-catalog/.assent/config.yaml @@ -1,4 +1,5 @@ -# Starter pack for examples/repos/service-catalog (assent init --sample service-catalog). +# Starter pack for examples/repos/service-catalog. Copy this .assent/ tree into your +# repo root (there is no `assent init` subcommand — see docs/usage/walkthrough.md). # Keyed list: packs/catalog declares entries.mode=list with identity.pointer=/name # (never unkeyed — ADR-0017 §5). Reordering entries must not change EntryRef subjects. apiVersion: assent.dev/v1alpha1 diff --git a/examples/packs/topic-registry/.assent/config.yaml b/examples/packs/topic-registry/.assent/config.yaml index 2644a7d..76c2eed 100644 --- a/examples/packs/topic-registry/.assent/config.yaml +++ b/examples/packs/topic-registry/.assent/config.yaml @@ -1,4 +1,5 @@ -# Starter pack for examples/repos/topic-registry (assent init --sample topic-registry). +# Starter pack for examples/repos/topic-registry. Copy this .assent/ tree into your +# repo root (there is no `assent init` subcommand — see docs/usage/walkthrough.md). # Class kafka-topic: one YAML file per topic; map-at-root identity = the topic-name key # (EFE-S04: document mode leaves Entry nil under whole-pack replay — D-056 — so # entry.owner cannot bind; map mode reconstructs the topic object for ownership). From 547d30ae97927ede896a60c459d5083f9f04d59c Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Fri, 7 Aug 2026 14:38:03 +0200 Subject: [PATCH 16/16] :memo: docs(decisions): fold three unfixed residuals into D-124 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All three were live only in a session transcript or in gitignored agent-context, which is the same "a follow-up that lives nowhere does not exist" gap D-124 opened for. Recorded, not fixed — each is editorial and widening the reviewed diff costs more than it buys. Verified before writing, not restated: - meta-plan.md's "Ordering constraint: E7 starts early (alongside E1)" now sits directly under a heading asserting the table is the numbering that executed. It isn't: E6's tip ec91226 is an ancestor of E7-S08 f27457d, so E7 landed after E6. - install.md credits a stamped version to "the Homebrew bottle". `.goreleaser.yaml`'s `brews:` block publishes a Formula whose `url_template` points at the release archive; no bottle is built or hosted. The version claim is true, the term is not. - AUD-S05's TestNoStaleProductClaims walks `..`, `../../internal`, `../../docs` — so hack/, .github/ and test/ markdown are covered by no pin, and that file is not AUD-S06's to widen. D-124. --- docs/decisions/decisions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/decisions/decisions.md b/docs/decisions/decisions.md index e4e93f7..d89b4ae 100644 --- a/docs/decisions/decisions.md +++ b/docs/decisions/decisions.md @@ -128,4 +128,4 @@ project/process decisions. | D-121 | 2026-08-06 | **ARCH-04 — canonical-hash split codified; `assent-jcs-v1` wired into the replay-bundle digest (corrects D-114 drift).** Rule: digests over BYTE artifacts (policySha over policy bytes, marker occurrence over judged head bytes, marker decision over emitted DecisionRecord bytes, toolDigest) are raw `sha256:` — byte identity is the point; digests over SCHEMA-OWNED JSON DOCUMENTS that consumers re-parse and re-verify use `internal/core/hash.Digest` (ADR-0017 §9 domain separation) with the schema `$id` as domain. Exactly one digest switches now: `compare.ReplayBundleDigest` → `hash.Digest("https://assent.dev/schemas/decision/v1alpha1/replay-bundle.schema.json", raw)` (D-114 claimed this; implementation used undomained `sha256(json.Marshal(decoded))`). Migration (pre-v1, same commit): regenerate every `replayBundleDigest` in `examples/comparison/*/suite.yaml`; caseIds/bundles unchanged (D-113 immutability preserved — algorithm versioned by this row). ADR-0019 marker grammar untouched. Revert: restore undomained digest + old corpus pins. | | D-122 | 2026-08-06 | **REL-08 — DecisionRecord emit precedes Reconcile; emit is atomic and fail-closed.** New invariant: NO forge write without a schema-valid, durably-emitted DecisionRecord. Order in `orchestrate`: build → marshal → schema-validate → EMIT (`--emit`: write `.tmp` same-dir + `os.Rename`; stdout: unchanged) → Reconcile → summary. Emit failure → hard error, zero forge writes. Pure reordering: `recordJSON` is fully determined pre-Reconcile (receipt lives in the summary line, not the record) — byte-identical records, marker digests and determinism gate unaffected. Rejected: post-reconcile record stamping (breaks record byte-stability vs marker `decision` digest). Revert: move emit back below Reconcile. | | D-123 | 2026-08-06 | **ARCH-01 — boundary enforcement automated (depguard + extended purity walk); ADR-0011 Amendment 3 truths the "arch-lint enforced" claim.** Two layers: (1) golangci `depguard` deny-rules — `internal/core/**`, `internal/change/**`, `internal/glob`, `internal/lint`, `internal/catalogue`, `internal/evaldecode`, `internal/compare`, `schemas/**` may import none of `internal/forge/**`, `internal/render/**`, `cmd/**`, `net/**`; (2) `TestCorePurity` walk extends to `../evaldecode`, `../compare`, and `../../schemas` (call-level: `time.Now`/`os.Getenv`/`os.Environ`/rand/net, adversarial self-test retained). Scope note: this EXTENDS the AGENTS.md rule-7 pure tree — `internal/evaldecode` (engine input decode) and `internal/compare` (D-116/D-117 gate determinism) join the determinism guard; `schemas` is embedded compile-time authority. Acceptance: a synthetic violating import/call fails CI both ways. Revert: drop depguard rules + walk dirs and re-amend ADR-0011. | -| D-124 | 2026-08-07 | **AUD-S06 residual — the two docs truth-lag gates exist but are UNWIRED; wiring is assigned to Lane B.** `hack/docs/readme_smoke_test.sh` (REQ-AUD-S06-01, executes every README quick-start command) and `hack/docs/truthlag_pins_test.sh` (REQ-AUD-S06-02, 18 grep/diff pins over DOC-05/06/09/10/11 + the ADR status index) are green and mutation-proven, but nothing invokes them: `Taskfile.yml` and `.github/workflows/**` belong to **Lane B** (AUD-S02/S03/S09/S14), so AUD-S06 could not add them. **Consequence, stated plainly: until they are wired, a future README or docs edit reopens DOC-07 (a quick-start that exits 2) or any pinned claim with NOTHING going red** — the mechanism is a manual gate, which is the same class of defect AUD-S06 exists to close. Assignment: Lane B adds a `docs-gates` task (`bash hack/docs/readme_smoke_test.sh && bash hack/docs/truthlag_pins_test.sh`) as a `check:` dependency, following the `hack/compare/exitgate_test.sh` precedent (D-118), alongside its AUD-S09/S14 workflow work. **Extend when wiring** (known pin gaps, both accepted for now): (a) the DOC-09 walkthrough check asserts banner PRESENCE, not polarity — flipping a step's `Planned` to `Shipped` stays green; (b) the DOC-05 link-resolution loop is scoped to `README.md`, so the relative links in `examples/README.md` are unpinned. Revert: delete the two scripts and reopen DOC-05/06/07/09/10/11 as live findings. | +| D-124 | 2026-08-07 | **AUD-S06 residual — the two docs truth-lag gates exist but are UNWIRED; wiring is assigned to Lane B.** `hack/docs/readme_smoke_test.sh` (REQ-AUD-S06-01, executes every README quick-start command) and `hack/docs/truthlag_pins_test.sh` (REQ-AUD-S06-02, 18 grep/diff pins over DOC-05/06/09/10/11 + the ADR status index) are green and mutation-proven, but nothing invokes them: `Taskfile.yml` and `.github/workflows/**` belong to **Lane B** (AUD-S02/S03/S09/S14), so AUD-S06 could not add them. **Consequence, stated plainly: until they are wired, a future README or docs edit reopens DOC-07 (a quick-start that exits 2) or any pinned claim with NOTHING going red** — the mechanism is a manual gate, which is the same class of defect AUD-S06 exists to close. Assignment: Lane B adds a `docs-gates` task (`bash hack/docs/readme_smoke_test.sh && bash hack/docs/truthlag_pins_test.sh`) as a `check:` dependency, following the `hack/compare/exitgate_test.sh` precedent (D-118), alongside its AUD-S09/S14 workflow work. **Extend when wiring** (known pin gaps, both accepted for now): (a) the DOC-09 walkthrough check asserts banner PRESENCE, not polarity — flipping a step's `Planned` to `Shipped` stays green; (b) the DOC-05 link-resolution loop is scoped to `README.md`, so the relative links in `examples/README.md` are unpinned; (c) AUD-S05's `TestNoStaleProductClaims` (`cmd/assent/main_help_test.go`, Lane A5's file) walks only `cmd/`, `internal/` and `docs/` — markdown under `hack/`, `.github/` and `test/` is grepped by no pin at all. **Known unfixed truth-lag, deliberately not corrected in AUD-S06 to keep the reviewed diff narrow — fix when next editing these files:** (i) `docs/planning/meta-plan.md` closes the Phase-5 epic table with "Ordering constraint: E7 starts early (alongside E1)", directly under the new heading asserting the table is the numbering that actually executed — E7 in fact landed after E6 (E6 tip `ec91226` is an ancestor of E7-S08 `f27457d`, both 2026-08-04); (ii) `docs/usage/install.md` credits a stamped version to "the Homebrew **bottle**", but `.goreleaser.yaml`'s `brews:` block publishes a **Formula** whose `url_template` points at the release archive — no bottle is built or hosted; the version claim is true, only the term is wrong. Revert: delete the two scripts and reopen DOC-05/06/07/09/10/11 as live findings. |