From d07beb61b1bcda2b96c618d7e13d0beeb28241de Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Fri, 7 Aug 2026 15:05:02 +0200 Subject: [PATCH 01/12] :wrench: chore(changelog): render version headings in Keep-a-Changelog bracket form The body template emitted `## v0.1.0 - 2026-08-05` while the hand-written historical stub in the footer uses `## [0.0.0] - 2026-08-04`. Two heading shapes in one generated file, and neither matches the Keep a Changelog format the header claims the file follows. Release surface: CHANGELOG.md is a published contract artifact; the heading text of every released section changes on the next regeneration. REQ-AUD-S02-01 --- cliff.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cliff.toml b/cliff.toml index e56cd36..49ceee1 100644 --- a/cliff.toml +++ b/cliff.toml @@ -14,7 +14,7 @@ Release notes are generated from gitmoji-conventional commits on the default bra """ body = """ -{% if version %}## {{ version }} - {{ timestamp | date(format="%Y-%m-%d") }}{% else %}## Unreleased{% endif %} +{% if version %}## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}{% else %}## Unreleased{% endif %} {% for group, commits in commits | group_by(attribute="group") %} ### {{ group }} {% for commit in commits %}- {{ commit.message | split(pat="\n") | first }} From ed26f2c5d583ff1da71cbdf2cb594406d6363016 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Fri, 7 Aug 2026 15:05:50 +0200 Subject: [PATCH 02/12] :memo: docs(changelog): warn record consumers that pins.toolDigest changed value (D-120) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AUD-S04 changed what `pins.toolDigest` *is*: pre-D-120 records pin sha256 over the tool version string, post-D-120 records pin a Go build-info content digest. Same field, same type, same frozen v1alpha1 schema — different value, so the two never compare equal. Nothing in the published artifacts said so, and a record consumer diffing digests across the boundary would read "different build". The note lives in the cliff.toml header rather than in CHANGELOG.md because the changelog is regenerated in full from commit history: a hand-edit would be wiped by the next `task changelog-write` and the drift gate would go green with the warning gone. Release surface: CHANGELOG.md is a published contract artifact. --- cliff.toml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/cliff.toml b/cliff.toml index 49ceee1..e4c7c3b 100644 --- a/cliff.toml +++ b/cliff.toml @@ -12,6 +12,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Release notes are generated from gitmoji-conventional commits on the default branch using [git-cliff](https://git-cliff.org/). +## Compatibility notes + +Long-lived notes for consumers of released artifacts. They live in `cliff.toml`'s changelog +header because `CHANGELOG.md` is regenerated in full from commit history and a hand-edit here +would be silently overwritten by the next `task changelog-write`. + +- **`pins.toolDigest` changes value after `v0.1.0` (D-120).** DecisionRecords emitted by + `v0.1.0` and earlier pin `toolDigest` as sha256 over the *tool version string*, so every + build stamped with the same version shared one digest; records carrying that derivation are + identifiable by exactly it. Builds after D-120 derive `toolDigest` from the binary's + canonical Go build info instead (main module path/version/sum, dependency checksums, VCS + revision and dirty flag), falling back to `sha256("buildinfo-unavailable\\n" + toolVersion)` + when build info is absent. The field, its type and the frozen `v1alpha1` schema are + unchanged — records published with `v0.1.0` remain schema-valid — but the *value* is not + comparable across the boundary: a mismatch between a pre-D-120 and a post-D-120 record means + "derived differently", not "different build". + """ body = """ {% if version %}## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}{% else %}## Unreleased{% endif %} From 1ebfc5720d8aaf590948a3b9c69358cf0a13a091 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Fri, 7 Aug 2026 15:08:25 +0200 Subject: [PATCH 03/12] :white_check_mark: test(release): pin the CHANGELOG drift gate content, wiring and polarity Failing first, for the right reason: CHANGELOG.md has no [0.1.0] section and no task or workflow invokes hack/release/verify-changelog.sh. Every wiring assertion is a function over a file path and is re-run against a temp copy with the wired line deleted, so an assertion that cannot go red fails the script instead of shipping green. Each block extraction is positive-controlled (non-empty, anchored on a known-present line, bounded before the next block). REQ-AUD-S02-01, REQ-AUD-S02-02 --- hack/release/changelog_gate_test.sh | 236 ++++++++++++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100755 hack/release/changelog_gate_test.sh diff --git a/hack/release/changelog_gate_test.sh b/hack/release/changelog_gate_test.sh new file mode 100755 index 0000000..a71328d --- /dev/null +++ b/hack/release/changelog_gate_test.sh @@ -0,0 +1,236 @@ +#!/usr/bin/env bash +# REQ-AUD-S02-01/02 — the CHANGELOG drift gate is REGENERATED, WIRED, and FIRES. +# +# `hack/release/verify-changelog.sh` has existed since E9-S03 and was invoked by +# nothing: no `task` target listed it, no workflow ran it. A gate nobody calls is +# a comment, which is how `CHANGELOG.md` came to have no `[0.1.0]` section at all +# after the v0.1.0 tag. This script pins the three things that make it a gate: +# +# 1. content — CHANGELOG.md carries the released `## [0.1.0] - 2026-08-05` +# section (REQ-AUD-S02-01). +# 2. wiring — `task check` runs `changelog-verify`, and the `verify:` job in +# verify.yaml runs it too (REQ-AUD-S02-02). The same wiring +# assertions cover the three OTHER gates that shipped unwired and +# are wired by this lane: `docs-gates` (D-124) and +# `lint-depguard-test` (AUD-S07 / D-123). +# 3. polarity — a stale CHANGELOG.md makes `verify-changelog.sh` exit non-zero. +# +# Anti-vacuity discipline (this epic shipped several gates that could not fail): +# * every "is it wired" assertion is a FUNCTION over a file path, and each one is +# re-run against a temp copy with the wired line DELETED — if it does not go red +# there, the assertion is not testing what it claims and this script fails. +# * every extraction (task block, workflow job block, workflow step block) is +# positive-controlled: non-empty AND containing a known-present line AND not +# containing a line from the NEXT block, so a broken awk range fails loudly +# instead of silently asserting nothing over an empty string. +# * no `grep -q` on the read end of a pipe (SIGPIPE 141 under `pipefail`); +# extractions go to files first. +# * the drift probe is verified to have LANDED in the file before its red is +# believed — `verify-changelog.sh` resolves its own repo root from +# ${BASH_SOURCE}, so a "temp copy of CHANGELOG.md" probe would never reach it. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$ROOT" + +TASKFILE="$ROOT/Taskfile.yml" +WORKFLOW="$ROOT/.github/workflows/verify.yaml" + +WORK="$(mktemp -d)" +CHANGELOG_BACKUP="$WORK/CHANGELOG.md.orig" +cp "$ROOT/CHANGELOG.md" "$CHANGELOG_BACKUP" +# Restore unconditionally: the drift probe mutates the REAL CHANGELOG.md (see above). +trap 'cp -f "$CHANGELOG_BACKUP" "$ROOT/CHANGELOG.md"; rm -rf "$WORK"' EXIT + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +# ---------------------------------------------------------------- extraction -- + +# extract_block — the body of a top-level 2-space-indented +# mapping key (a Taskfile task, or a workflow job), up to the next such key. +# Task names may contain a colon ("tools:git-cliff"), hence the exact-string match. +extract_block() { + awk -v name="$2" ' + $0 == " " name ":" { inblk = 1; next } + inblk && /^ [A-Za-z0-9_.:-]+:[[:space:]]*$/ { inblk = 0 } + inblk { print } + ' "$1" +} + +# extract_step — one workflow step (6-space "- " item) whose +# first line matches , up to the start of the next step. +extract_step() { + extract_block "$1" "$2" >"$WORK/job.for-step" + awk -v pat="$3" ' + /^ - / { instep = ($0 ~ pat) } + instep { print } + ' "$WORK/job.for-step" +} + +# ------------------------------------------------- assertions (file -> status) -- + +# check_lists_task — is a sequential command of +# the `check` task? Not "does the task exist" and not "does the name appear in the +# file": deleting the line from `check:` while leaving the task defined must fail. +check_lists_task() { + extract_block "$1" check >"$WORK/check.block" + grep -qE "^[[:space:]]+- task: $2\$" "$WORK/check.block" +} + +# verify_job_runs_changelog_gate — does verify.yaml's `verify:` job run +# `task changelog-verify`? +verify_job_runs_changelog_gate() { + extract_block "$1" verify >"$WORK/verify.block" + grep -qE 'task changelog-verify' "$WORK/verify.block" +} + +# ------------------------------------------------------- 0. positive controls -- + +echo "== 0. extraction positive controls ==" + +extract_block "$TASKFILE" check >"$WORK/check.control" +[[ -s "$WORK/check.control" ]] || fail "Taskfile check: block extracted EMPTY — the awk range is broken, every wiring assertion below would be vacuous" +grep -qE '^[[:space:]]+- task: fmt$' "$WORK/check.control" \ + || fail "Taskfile check: block does not contain the known-present '- task: fmt' — extraction is wrong" +grep -qE '^[[:space:]]+- task: compare-exitgate-test$' "$WORK/check.control" \ + || fail "Taskfile check: block does not contain the known-present '- task: compare-exitgate-test' (D-118 precedent) — extraction is wrong" +if grep -q 'compare-exitgate-test:' "$WORK/check.control"; then + fail "Taskfile check: block ran past the end of check into the next task definition" +fi +echo "OK: check: block extracted ($(wc -l <"$WORK/check.control" | tr -d ' ') lines), anchored on fmt + compare-exitgate-test, bounded" + +extract_block "$WORKFLOW" verify >"$WORK/verify.control" +[[ -s "$WORK/verify.control" ]] || fail "verify.yaml verify: job extracted EMPTY — the awk range is broken" +grep -q 'hack/compare/exitgate_test.sh' "$WORK/verify.control" \ + || fail "verify.yaml verify: job does not contain the known-present compare exit-gate step — extraction is wrong" +if grep -q 'hack/release/exitgate_test.sh' "$WORK/verify.control"; then + fail "verify.yaml verify: job ran past its end into release-exitgate:" +fi +echo "OK: verify: job extracted ($(wc -l <"$WORK/verify.control" | tr -d ' ') lines), anchored on the compare exit gate, bounded before release-exitgate" + +# --------------------------------------------- 1. REQ-AUD-S02-01 (content) -- + +echo "== 1. REQ-AUD-S02-01: CHANGELOG.md carries the released v0.1.0 section ==" +grep -qE '^## \[0\.1\.0\] - 2026-08-05$' "$ROOT/CHANGELOG.md" \ + || fail "CHANGELOG.md has no '## [0.1.0] - 2026-08-05' section — run 'task changelog-write' and commit (REQ-AUD-S02-01)" +grep -qE '^## Unreleased$' "$ROOT/CHANGELOG.md" \ + || fail "CHANGELOG.md has no '## Unreleased' section (REQ-AUD-S02-01)" +# Post-tag commits must sit under Unreleased, i.e. ABOVE the released section. +unrel_line="$(grep -nE '^## Unreleased$' "$ROOT/CHANGELOG.md" | head -1 | cut -d: -f1)" +rel_line="$(grep -nE '^## \[0\.1\.0\] - 2026-08-05$' "$ROOT/CHANGELOG.md" | head -1 | cut -d: -f1)" +[[ "$unrel_line" -lt "$rel_line" ]] \ + || fail "CHANGELOG.md orders [0.1.0] before Unreleased (line $rel_line vs $unrel_line)" +[[ $((rel_line - unrel_line)) -gt 2 ]] \ + || fail "CHANGELOG.md Unreleased section is empty — the post-tag commits are missing (REQ-AUD-S02-01)" +echo "OK: [0.1.0] at line $rel_line, non-empty Unreleased above it at line $unrel_line" + +# The D-120 record-consumer warning is generated from cliff.toml's header, so a +# hand-edit of CHANGELOG.md cannot carry it and `changelog-write` cannot wipe it. +grep -q 'pins.toolDigest' "$ROOT/CHANGELOG.md" \ + || fail "CHANGELOG.md carries no pins.toolDigest compatibility note — AUD-S04 changed the value of a published record field with no warning to record consumers (D-120)" +grep -q 'pins.toolDigest' "$ROOT/cliff.toml" \ + || fail "the pins.toolDigest note is in CHANGELOG.md but not in cliff.toml — the next 'task changelog-write' will wipe it" +echo "OK: D-120 toolDigest note present in CHANGELOG.md and sourced from cliff.toml" + +# --------------------------------------------- 2. REQ-AUD-S02-02 (wiring) -- + +echo "== 2. REQ-AUD-S02-02: gates wired into 'task check' ==" +# changelog-verify is this story's gate; the other two are the gates that AUD-S06 +# (D-124) and AUD-S07 shipped green but unwired — this lane owns Taskfile.yml. +WIRED_TASKS=(changelog-verify docs-gates lint-depguard-test) +for t in "${WIRED_TASKS[@]}"; do + check_lists_task "$TASKFILE" "$t" \ + || fail "'task check' does not run '$t' — the gate is defined but invoked by nothing" + extract_block "$TASKFILE" "$t" >"$WORK/def.$t" + [[ -s "$WORK/def.$t" ]] || fail "'$t' is listed in check: but not defined in Taskfile.yml" + echo "OK: check runs $t (and $t is defined)" +done + +# The task bodies must still invoke the scripts they exist for — a wired task with +# a gutted body is the same defect one level down. +grep -q 'hack/release/verify-changelog.sh' "$WORK/def.changelog-verify" \ + || fail "the changelog-verify task no longer runs hack/release/verify-changelog.sh" +grep -q 'hack/docs/readme_smoke_test.sh' "$WORK/def.docs-gates" \ + || fail "the docs-gates task no longer runs hack/docs/readme_smoke_test.sh (D-124)" +grep -q 'hack/docs/truthlag_pins_test.sh' "$WORK/def.docs-gates" \ + || fail "the docs-gates task no longer runs hack/docs/truthlag_pins_test.sh (D-124)" +grep -q 'hack/lint/depguard_test.sh' "$WORK/def.lint-depguard-test" \ + || fail "the lint-depguard-test task no longer runs hack/lint/depguard_test.sh (D-123/AUD-S07)" +echo "OK: each wired task still invokes its script" + +echo "== 2b. the wiring assertion itself can fail (mutation) ==" +for t in "${WIRED_TASKS[@]}"; do + mutant="$WORK/Taskfile.no-$t.yml" + grep -vE "^[[:space:]]+- task: $t\$" "$TASKFILE" >"$mutant" + # Prove the mutation landed before believing anything about its result. + if grep -qE "^[[:space:]]+- task: $t\$" "$mutant"; then + fail "mutation did not land: '- task: $t' is still in $mutant" + fi + if [[ "$(wc -l <"$mutant")" -eq "$(wc -l <"$TASKFILE")" ]]; then + fail "mutation did not land: $mutant has the same line count as Taskfile.yml" + fi + if check_lists_task "$mutant" "$t"; then + fail "check_lists_task reports '$t' wired in a Taskfile with that line deleted — the assertion is vacuous" + fi + echo "OK: deleting '- task: $t' from check: turns the assertion red" +done + +echo "== 3. REQ-AUD-S02-02: verify.yaml's verify: job runs the changelog gate ==" +verify_job_runs_changelog_gate "$WORKFLOW" \ + || fail "the verify: job in verify.yaml does not run 'task changelog-verify' (REQ-AUD-S02-02)" +grep -q 'go-task/task/v3/cmd/task@' "$WORK/verify.control" \ + || fail "the verify: job runs 'task changelog-verify' but never installs Task" + +extract_step "$WORKFLOW" verify 'changelog' >"$WORK/step.changelog" +[[ -s "$WORK/step.changelog" ]] || fail "could not extract the changelog gate step from the verify: job" +grep -q 'task changelog-verify' "$WORK/step.changelog" \ + || fail "the extracted step does not run task changelog-verify — extraction matched the wrong step" +# D-125: the step is deliberately NOT run on pull_request. On that event +# actions/checkout builds refs/pull/N/merge, a merge commit minted at CI time whose +# subject git-cliff renders through the catch-all parser — a line that cannot exist +# in any committed CHANGELOG.md. Without this guard every PR is red by construction. +grep -qF "github.event_name != 'pull_request'" "$WORK/step.changelog" \ + || fail "the changelog gate step has no 'github.event_name != '\''pull_request'\''' guard — on the PR merge ref the generated changelog always contains a synthetic merge subject, so the step would be red on every PR by construction (D-125)" +echo "OK: verify: job runs task changelog-verify, guarded off pull_request (D-125)" + +echo "== 3b. the workflow assertion itself can fail (mutation) ==" +mutant="$WORK/verify.no-changelog.yaml" +grep -v 'task changelog-verify' "$WORKFLOW" >"$mutant" +if grep -q 'task changelog-verify' "$mutant"; then + fail "mutation did not land: $mutant still runs task changelog-verify" +fi +[[ "$(wc -l <"$mutant")" -lt "$(wc -l <"$WORKFLOW")" ]] || fail "mutation did not land: $mutant has the same line count as verify.yaml" +if verify_job_runs_changelog_gate "$mutant"; then + fail "verify_job_runs_changelog_gate reports the gate present in a workflow with that line deleted — the assertion is vacuous" +fi +echo "OK: deleting the run line from verify.yaml turns the assertion red" + +# ------------------------------------------------ 4. drift polarity (in-tree) -- + +echo "== 4. verify-changelog.sh passes on the committed tree ==" +bash "$ROOT/hack/release/verify-changelog.sh" >"$WORK/clean.out" 2>&1 || { + cat "$WORK/clean.out" >&2 + fail "verify-changelog.sh is red on the committed tree — run 'task changelog-write' and commit" +} +echo "OK: clean polarity green" + +echo "== 5. verify-changelog.sh fails closed on a stale CHANGELOG.md ==" +printf '\n\n' >>"$ROOT/CHANGELOG.md" +grep -q 'AUD-S02 drift probe' "$ROOT/CHANGELOG.md" \ + || fail "drift probe did not land in CHANGELOG.md — the red below would prove nothing" +if bash "$ROOT/hack/release/verify-changelog.sh" >"$WORK/drift.out" 2>&1; then + cat "$WORK/drift.out" >&2 + fail "verify-changelog.sh exited 0 on a stale CHANGELOG.md — the drift gate does not fire (REQ-AUD-S02-02)" +fi +grep -q 'drift' "$WORK/drift.out" \ + || fail "verify-changelog.sh failed on the stale CHANGELOG.md but did not report drift — it failed for some other reason" +cp -f "$CHANGELOG_BACKUP" "$ROOT/CHANGELOG.md" +if grep -q 'AUD-S02 drift probe' "$ROOT/CHANGELOG.md"; then + fail "restore failed: the drift probe is still in CHANGELOG.md" +fi +echo "OK: stale CHANGELOG.md is caught, probe restored" + +echo "PASS: changelog drift gate regenerated, wired into task check + verify.yaml, and proven at both polarities (REQ-AUD-S02-01/02)" From cf7eeb2ba5ae0c2d0b84ff6cd69ef825a1ec964b Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Fri, 7 Aug 2026 15:09:04 +0200 Subject: [PATCH 04/12] :construction_worker: ci(release): run the CHANGELOG drift gate in task check and on main CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hack/release/verify-changelog.sh shipped in E9-S03 and was invoked by no task and no workflow. CHANGELOG.md consequently lost its released section entirely after the v0.1.0 tag with nothing going red. Local: a check: entry immediately after compare-exitgate-test, so the first commit that leaves CHANGELOG.md stale reds the next task check. CI: a step in the verify job, guarded off pull_request. On that event actions/checkout builds refs/pull/N/merge — a merge commit minted at CI time whose subject git-cliff renders through the catch-all parser, a line no committed CHANGELOG.md can contain. Verified locally by merging a probe branch and observing the merge subject in the generated output. Push-to-main and the weekly schedule check out the real commit and are the backstop. Recorded as D-125. Release surface: CHANGELOG.md is a published contract artifact. REQ-AUD-S02-02 --- .github/workflows/verify.yaml | 9 +++++++++ Taskfile.yml | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/.github/workflows/verify.yaml b/.github/workflows/verify.yaml index 713b756..d1ad232 100644 --- a/.github/workflows/verify.yaml +++ b/.github/workflows/verify.yaml @@ -67,6 +67,15 @@ jobs: run: go install github.com/go-task/task/v3/cmd/task@latest - name: PCS compare exit gate (RELSE-03 — REQ-PCS-S09) run: bash hack/compare/exitgate_test.sh + # AUD-S02 (REQ-AUD-S02-02) — CHANGELOG.md drift gate, main + schedule only. + # NOT on pull_request (D-125): there actions/checkout builds refs/pull/N/merge, a + # merge commit minted at CI time whose subject ("Merge into ") git-cliff + # renders through the catch-all commit parser. That line cannot exist in any + # committed CHANGELOG.md, so a PR-scoped step would be red by construction and + # unfixable by the author. `task check` runs this gate on every local commit. + - name: changelog drift gate (AUD-S02 — REQ-AUD-S02-02) + if: github.event_name != 'pull_request' + run: task changelog-verify # RELSE-03: release exit gate includes goreleaser snapshot + docs-build — main push and # weekly schedule only; PRs run compare-exitgate above without this job. diff --git a/Taskfile.yml b/Taskfile.yml index bdd0e3f..166e83d 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -76,6 +76,12 @@ tasks: - task: build - task: dogfood-comparison - task: compare-exitgate-test + # AUD-S02 (REQ-AUD-S02-02): CHANGELOG.md drift gate. It is the LOCAL half of the + # wiring — CI runs it only off pull_request (D-125), because on the PR merge ref + # the generated changelog always carries a synthetic merge subject. Consequence, + # stated plainly: a commit that changes history without regenerating CHANGELOG.md + # makes the NEXT `task check` red until `task changelog-write` is committed. + - task: changelog-verify # AUD-S03: the release gate's own guard. Its only other CI caller is release-exitgate, # which is skipped on pull_request — without this a regression merges green and only # reds main afterwards. Fast and offline (stubbed `gh`); needs jq. From 51faaae4276e517b339fba2c0502e167e1d0dc72 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Fri, 7 Aug 2026 15:10:26 +0200 Subject: [PATCH 05/12] :construction_worker: ci(docs): wire the AUD-S06 docs truth-lag gates into task check (D-124) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit readme_smoke_test.sh and truthlag_pins_test.sh shipped green, mutation- proven, and invoked by nothing — a manual gate, which is the defect class AUD-S06 exists to close. A README or docs edit could reopen DOC-07 or any of the 18 pins with nothing going red. New `docs-gates` task running both, listed sequentially in check: rather than as a deps: entry — go-task runs deps in parallel and the smoke test builds a binary while fmt is rewriting the tree. Both scripts advertised "NOT YET WIRED INTO ANY GATE"; that statement is now false, so it is replaced. The smoke test still skips `task …` lines from the README quick-start — that skip is what stops task check from calling itself, and its comment now says so. --- Taskfile.yml | 11 +++++++++++ hack/docs/readme_smoke_test.sh | 15 ++++++++------- hack/docs/truthlag_pins_test.sh | 6 +++--- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index 166e83d..25762cc 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -86,6 +86,17 @@ tasks: # which is skipped on pull_request — without this a regression merges green and only # reds main afterwards. Fast and offline (stubbed `gh`); needs jq. - task: release-verify-tag-gate-test + # D-124: the AUD-S06 docs truth-lag gates shipped green and invoked by nothing, + # which is the same manual-gate defect they exist to close. Sequential (not a + # `deps:` entry) — go-task runs deps in parallel and readme_smoke_test.sh builds + # a binary while `fmt` rewrites the tree. + - task: docs-gates + + docs-gates: + desc: "D-124 docs truth-lag gates: README quick-start executes (REQ-AUD-S06-01) + 18 doc pins (REQ-AUD-S06-02)" + cmds: + - bash hack/docs/readme_smoke_test.sh + - bash hack/docs/truthlag_pins_test.sh compare-exitgate-test: desc: "PCS-S09 exit gate: full suite + E6 seed dir + schema drift guard (REQ-PCS-S09-01..03)" diff --git a/hack/docs/readme_smoke_test.sh b/hack/docs/readme_smoke_test.sh index f3dd01f..c13a174 100755 --- a/hack/docs/readme_smoke_test.sh +++ b/hack/docs/readme_smoke_test.sh @@ -10,15 +10,16 @@ # 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 … — 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. +# task … — this script RUNS inside `task check` (via `task docs-gates`, D-124), +# so invoking `task check` from here would recurse without terminating. +# The skip is load-bearing, not a convenience: if this case ever stops +# matching, `task check` calls itself forever. # 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. +# WIRED (D-124/D-125): `task docs-gates` runs this and truthlag_pins_test.sh, and +# `task check` runs `docs-gates`. A README edit that reopens DOC-07 reds the gate every +# developer runs before every commit. set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" @@ -80,7 +81,7 @@ while IFS= read -r line; do continue ;; task*) - echo "SKIP $cmd (this script belongs inside \`task check\`; invoking it here would recurse once wired — D-124)" + echo "SKIP $cmd (this script runs inside \`task check\` via \`task docs-gates\`; invoking it here would recurse — D-124)" skipped=$((skipped + 1)) continue ;; diff --git a/hack/docs/truthlag_pins_test.sh b/hack/docs/truthlag_pins_test.sh index b6eed7e..32cc68b 100755 --- a/hack/docs/truthlag_pins_test.sh +++ b/hack/docs/truthlag_pins_test.sh @@ -20,9 +20,9 @@ # 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. +# WIRED (D-124/D-125): `task docs-gates` runs this and readme_smoke_test.sh, and +# `task check` runs `docs-gates`. A docs edit that reopens one of these findings reds +# the gate every developer runs before every commit. set -uo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" From abc3d9ecc9c97e4a1e6eb85954737d39802e83f7 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Fri, 7 Aug 2026 15:10:48 +0200 Subject: [PATCH 06/12] :construction_worker: ci(lint): wire the depguard polarity proof into task check (AUD-S07) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hack/lint/depguard_test.sh is the adversarial half of the D-123 boundary control — it builds a violating module outside the repo from the repo`s real .golangci.yml and requires depguard to report it. Nothing invoked it, so deleting `- depguard` from .golangci.yml passed every wired gate: `task lint` only ever proved the clean polarity. task check only, not verify.yaml: the CI lint step is golangci-lint-action, which does not leave the binary on PATH for later steps, and this gate refuses to skip when golangci-lint is missing (by design — it is the boundary control). --- Taskfile.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Taskfile.yml b/Taskfile.yml index 25762cc..5d3b0b3 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -91,6 +91,14 @@ tasks: # `deps:` entry) — go-task runs deps in parallel and readme_smoke_test.sh builds # a binary while `fmt` rewrites the tree. - task: docs-gates + # AUD-S07 / D-123: `task lint` proves only the CLEAN depguard polarity. Without + # this, deleting `- depguard` from .golangci.yml passes every wired gate. + - task: lint-depguard-test + + lint-depguard-test: + desc: "AUD-S07 gate: adversarial proof the D-123 depguard boundary rules fire (REQ-AUD-S07-01)" + cmds: + - bash hack/lint/depguard_test.sh docs-gates: desc: "D-124 docs truth-lag gates: README quick-start executes (REQ-AUD-S06-01) + 18 doc pins (REQ-AUD-S06-02)" From f8719d012ad9a346ed9af275b6f4121ed4ce2e05 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Fri, 7 Aug 2026 15:11:38 +0200 Subject: [PATCH 07/12] =?UTF-8?q?:memo:=20docs(decisions):=20record=20D-12?= =?UTF-8?q?5=20=E2=80=94=20CHANGELOG=20drift=20gate=20placement=20and=20it?= =?UTF-8?q?s=20cost?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The spec asked for the drift gate on pull_request too. Reproduced locally why that cannot work: git-cliff renders merge subjects through the catch-all parser, and on pull_request actions/checkout builds refs/pull/N/merge, so the generated changelog always contains a line no committed CHANGELOG.md can hold. Logged with the walk-back, the consequence, and the working rule for regeneration commits. --- docs/decisions/decisions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/decisions/decisions.md b/docs/decisions/decisions.md index d89b4ae..94a44fa 100644 --- a/docs/decisions/decisions.md +++ b/docs/decisions/decisions.md @@ -129,3 +129,4 @@ project/process decisions. | 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; (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. | +| D-125 | 2026-08-07 | **AUD-S02 judgment call (b) — the CHANGELOG drift gate runs in `task check` on every local commit and in CI on push-to-main + schedule, NOT on `pull_request`.** The spec asked for both placements; the PR placement is not merely noisy, it is red by construction. Evidence (reproduced locally, not reasoned): `hack/release/verify-changelog.sh` diffs the WHOLE generated changelog against the committed file, and `cliff.toml`'s parser list ends in a catch-all `{ message = ".*", group = "Other" }`, so merge commits are rendered — merging a probe branch put `- Merge 1234567 into 89abcde` in the generated output. On `pull_request`, `actions/checkout` checks out `refs/pull/N/merge`: a merge commit minted at CI time whose subject is exactly that shape. No committed `CHANGELOG.md` can contain a line naming a SHA that did not exist when it was written, so a PR-scoped step fails on every PR with no author fix (and would push commit SHAs into the changelog, contra **D-101**). Walk-back taken under the spec's decide-and-log sanction, using the `release-exitgate` precedent already in the file: `if: github.event_name != 'pull_request'`. **Consequence, stated plainly:** PR CI does not catch changelog drift. The `check:` entry does — one commit later, by construction: `task check` is green at HEAD, the next commit makes `CHANGELOG.md` stale, and the following `task check` is red until `task changelog-write` is committed. Push-to-main is the backstop, so a lane that lands without regenerating reds main until a regeneration commit follows. **Working rule:** a regeneration commit must be subject-prefixed `:memo: chore(release):` or `:wrench: chore(release):` — the two forms `cliff.toml` skips — or it creates fresh drift itself; and a lane must regenerate AFTER its last content commit and after any `git merge origin/main`. Two companion changes ship with it: version headings render Keep-a-Changelog style (`## [0.1.0] - 2026-08-05`, matching the hand-written `[0.0.0]` stub), and the **D-120** `pins.toolDigest` record-consumer warning lives in `cliff.toml`'s `[changelog] header` — CHANGELOG.md is regenerated in full, so a hand-edit there would be wiped by the next `changelog-write` with the drift gate still green. **Closes the D-124 assignment** in the same `check:` list: `docs-gates` (`hack/docs/readme_smoke_test.sh` + `truthlag_pins_test.sh`) and `lint-depguard-test` (`hack/lint/depguard_test.sh`) are now sequential `check:` commands — sequential, not `deps:`, because go-task runs deps in parallel and the smoke test builds a binary while `fmt` rewrites the tree. The depguard proof is local-only: CI lints via `golangci-lint-action`, which leaves no binary on PATH for a later step, and that gate refuses to skip when `golangci-lint` is absent. D-124's known pin gaps (a)/(b)/(c) and truth-lag items (i)/(ii) are NOT addressed here and remain open. Revert: drop the `check:` entries and the verify.yaml step — reopens RELSE-01 and D-124. | From 7b211c48cafa8ffc5fff2b4b6784f093eafbb7b8 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Fri, 7 Aug 2026 15:17:44 +0200 Subject: [PATCH 08/12] :memo: docs(release): the changelog drift gate is in task check now, not outside it hack/release/README.md carried a "Not in `task check`" section arguing the gate must stay out of the local loop. AUD-S02 put it in, so that paragraph became a false claim in the same file as the tooling it describes. Replaced with where the gate actually runs, the one-commit- behind behaviour it implies, the chore(release): prefix that keeps a regeneration commit from creating fresh drift, and why CI skips it on pull_request (D-125). Adds changelog_gate_test.sh to the task table and records that cliff.toml`s header, not CHANGELOG.md, is where long-lived consumer notes live. --- hack/release/README.md | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/hack/release/README.md b/hack/release/README.md index 3a828e0..a00c04b 100644 --- a/hack/release/README.md +++ b/hack/release/README.md @@ -15,16 +15,30 @@ template emits **categorized subject lines only — no commit SHAs** (D-101, oss | `task changelog-write` | Regenerate `CHANGELOG.md` from tags + unreleased commits | | `task changelog-verify` | Fail closed if `CHANGELOG.md` drifts from `cliff.toml` output (release gate) | | `bash hack/release/verify-changelog.sh` | Same check as `task changelog-verify` (script entry point) | - -**Not in `task check`:** regenerating `CHANGELOG.md` is a separate commit (`task changelog-write`). -Running verify inside every local `task check` would chicken-egg — any commit after -`changelog-write` fails until the next regeneration. Instead: - -- **Local / PR:** run `task changelog-verify` (or `bash hack/release/changelog_test.sh`) when - touching release docs or before opening a release-prep PR. -- **CI (E9-S05):** PRs that touch release paths run the `snapshot` job in - `.github/workflows/release.yaml` (goreleaser `--snapshot --skip=publish`). Changelog drift is - still a separate gate (`task changelog-verify`) — not bundled into `task check`. +| `bash hack/release/changelog_gate_test.sh` | AUD-S02: proves the drift gate is wired and fires (REQ-AUD-S02-01/02) | + +Long-lived notes for consumers of released artifacts (currently the **D-120 `pins.toolDigest`** +warning) live in `cliff.toml`'s `[changelog] header`, not in `CHANGELOG.md`: the file is +regenerated in full, so a hand-edit is wiped by the next `changelog-write` while the drift gate +stays green. + +**In `task check` since AUD-S02 (D-125).** `verify-changelog.sh` shipped in E9-S03 wired to +nothing, and `CHANGELOG.md` lost its released section entirely after the v0.1.0 tag with nothing +going red. Where it runs now: + +- **Local:** `task check` runs `changelog-verify` after `compare-exitgate-test`. It is + deliberately one commit behind — `check` is green at HEAD, the next commit makes the changelog + stale, and the following `check` is red until `task changelog-write` is committed. Prefix the + regeneration commit `:memo: chore(release):` or `:wrench: chore(release):` (the two subjects + `cliff.toml` skips), or the regeneration itself creates fresh drift. Regenerate after your last + content commit and after any `git merge origin/main`. +- **CI:** the `verify` job in `.github/workflows/verify.yaml`, guarded + `if: github.event_name != 'pull_request'` — push-to-main and the weekly schedule only. On + `pull_request`, `actions/checkout` builds `refs/pull/N/merge`, a merge commit minted at CI time + whose subject git-cliff renders through the catch-all parser; that line cannot exist in any + committed `CHANGELOG.md`, so a PR-scoped step is red by construction (D-125). +- **Release paths:** PRs touching them still run the `snapshot` job in + `.github/workflows/release.yaml` (goreleaser `--snapshot --skip=publish`). ### Release workflow (E9-S05) From 367a0020d12fcef9dd3e31bb1dabe71bce4def05 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Fri, 7 Aug 2026 15:11:55 +0200 Subject: [PATCH 09/12] :wrench: chore(release): regenerate CHANGELOG.md for v0.1.0 and the post-tag commits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The committed file was generated before the v0.1.0 tag existed: it had no released section at all, and 62 post-tag commits were still filed under Unreleased. Regenerated from cliff.toml, so [0.1.0] - 2026-08-05 is now a real section and Unreleased holds only what came after it. Also picks up the bracket heading form and the D-120 pins.toolDigest compatibility note from the changelog header. This commit is prefixed chore(release): — one of the two subjects cliff.toml skips — so regenerating does not itself create fresh drift. Release surface: CHANGELOG.md is a published contract artifact. REQ-AUD-S02-01 --- CHANGELOG.md | 102 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f99687..1fb44ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,110 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Release notes are generated from gitmoji-conventional commits on the default branch using [git-cliff](https://git-cliff.org/). +## Compatibility notes + +Long-lived notes for consumers of released artifacts. They live in `cliff.toml`'s changelog +header because `CHANGELOG.md` is regenerated in full from commit history and a hand-edit here +would be silently overwritten by the next `task changelog-write`. + +- **`pins.toolDigest` changes value after `v0.1.0` (D-120).** DecisionRecords emitted by +`v0.1.0` and earlier pin `toolDigest` as sha256 over the *tool version string*, so every +build stamped with the same version shared one digest; records carrying that derivation are +identifiable by exactly it. Builds after D-120 derive `toolDigest` from the binary's +canonical Go build info instead (main module path/version/sum, dependency checksums, VCS +revision and dirty flag), falling back to `sha256("buildinfo-unavailable\n" + toolVersion)` +when build info is absent. The field, its type and the frozen `v1alpha1` schema are +unchanged — records published with `v0.1.0` remain schema-valid — but the *value* is not +comparable across the boundary: a mismatch between a pre-D-120 and a post-D-120 record means +"derived differently", not "different build". + ## Unreleased +### Chores +- :wrench: chore(changelog): render version headings in Keep-a-Changelog bracket form + +### Documentation +- :memo: docs(decisions): close D-111 E9 exit gate after v0.1.0 +- :memo: docs(release): Homebrew tap operator runbook and honest install +- :memo: docs(install): document live Homebrew tap install +- :memo: docs(decisions): record D-111 Homebrew Formula published +- :memo: docs(openspec): Formula live; residual is PAT rotate +- :memo: docs(adr): ADR-0020 forge snapshot changed-file completeness (REL-07 P1) +- :memo: docs(decisions): D-119..D-123 audit-remediation designs +- :memo: docs(openspec): P5-AUD audit-remediation epic — 18 stories, 3 release conditions +- :memo: docs(adr): drop AI-provenance marker + review polish (D-019) +- :memo: docs(openspec): review polish — S01 lane coordination, SEC-08 alias, RELSE-08 residual +- :memo: docs(conformance): point each AUD-S01 catalog row at the test that proves it +- :memo: docs(adr): ADR-0011 Amendment 3 -- boundary enforcement mechanism made true (D-123) +- :memo: docs(release): note the jq dependency and fix the release-tooling list numbering +- :memo: docs(usage): CLI reference pinned to the binary's help output (REQ-AUD-S05-02) +- :memo: docs(usage): cover the two compare exit codes outside the gate contract (REQ-AUD-S05-02) +- :memo: docs(schemas): publish the D-120 toolDigest description (annotation only) +- :memo: docs(cli): correct the `assent run` step order to emit-before-reconcile (D-122) +- :memo: docs(install): `go install` binaries report 0.0.0-dev, not a stamped version (DOC-11) +- :memo: docs(contract): fileEvents ships add/delete — retire the "not yet implemented" note (DOC-06) +- :memo: docs(walkthrough): per-step Shipped/Planned banners replace the design-fiction header (DOC-09) +- :memo: docs(meta-plan): renumber the Phase-5 epic table to the epics that executed (DOC-10) +- :memo: docs(adr): ADR-0020 is Accepted — its contract shipped in AUD-S01 +- :memo: docs(examples): drop the pre-alpha banner and the "once it exists" harness caveat +- :memo: docs(usage): document the checkout-less enumeration contract on the -checkout flag +- :memo: docs(release): mandate patch tags over in-place asset replacement (SEC-07) +- :memo: docs(install): narrow the 0.0.0-dev consequence to the version string (D-120) +- :memo: docs(decisions): record D-124 — the AUD-S06 docs gates are unwired, Lane B owns the wiring +- :memo: docs(decisions): fold three unfixed residuals into D-124 +- :memo: docs(changelog): warn record consumers that pins.toolDigest changed value (D-120) +- :memo: docs(decisions): record D-125 — CHANGELOG drift gate placement and its cost +- :memo: docs(release): the changelog drift gate is in task check now, not outside it + +### Features +- :sparkles: feat(cli): dispatch-table help listing the real subcommands (REQ-AUD-S05-01) + +### Fixes +- :bug: fix(ci): install uv on release-exitgate for docs-build +- :bug: fix(release): do not skip=publish so Homebrew tap can push +- :bug: fix(release): use a POSIX class, not \t, in the gate-step if: guard +- :bug: fix(cli): show GITLAB_TOKEN in the run usage form (REQ-AUD-S05-01) +- :bug: docs(readme): pass the repo root to lint/test, and execute the quick-start (DOC-07) +- :bug: docs(readme): point the ADR-0014 link at the file that exists (DOC-05) +- :bug: fix(docs-gates): the scripts claimed a wiring that does not exist +- :bug: docs(examples): starter packs advertised a subcommand that does not exist + +### Other +- :construction_worker: ci(lint): depguard deny-rules for the D-123 pure tree (REQ-AUD-S07-01) +- :closed_lock_with_key: ci(release): gate the release job on verify green at the tag SHA +- :construction_worker: ci(release): enforce the verify-tag gate's own test in CI and document it +- :construction_worker: ci(release): run the verify-tag gate's test in task check +- :construction_worker: ci(schemadrift): fence the D-120 toolDigest annotation edit +- :construction_worker: ci(release): run the CHANGELOG drift gate in task check and on main CI +- :construction_worker: ci(docs): wire the AUD-S06 docs truth-lag gates into task check (D-124) +- :construction_worker: ci(lint): wire the depguard polarity proof into task check (AUD-S07) + +### Security +- :lock: fix(forge): prove changed-file enumeration completeness or declare a gap +- :lock: fix(run): degrade a checkout-less run to REVIEW when enumeration is incomplete +- :lock: fix(release): reject a tag whose only verify run is a pull_request run +- :lock: fix(run): derive pins.toolDigest from Go build info (D-120) +- :lock: fix(run): emit the DecisionRecord before forge reconcile (D-122) + +### Testing +- :white_check_mark: test(forge): model truncation and diff-endpoint failure in the fake +- :white_check_mark: test(cmd): serve the paginated diffs cassette in the run-path fakes +- :white_check_mark: test(conformance): require the changed-file-completeness cases +- :white_check_mark: test(core): extend the purity walk to evaldecode, compare and schemas (REQ-AUD-S07-02) +- :white_check_mark: test(lint): fail the depguard gate on an unmapped deny target (REQ-AUD-S07-01) +- :white_check_mark: test(release): scope the gate-step wiring assertions to the gate step +- :white_check_mark: test(release): make the all-runs-green rule discriminate, and pin the gate step armed +- :white_check_mark: test(release): bind the query string, drop the pipelines, and control the negatives +- :white_check_mark: test(cli): prove each dispatch name reaches its own handler (REQ-AUD-S05-01) +- :white_check_mark: test(cli): make the binding probe unsatisfiable by the usage listing (REQ-AUD-S05-01) +- :white_check_mark: test(cli): walk the whole cmd/ tree in the stale-claim pin (REQ-AUD-S05-01) +- :white_check_mark: test(schemadrift): derive the D-120 baseline anchor instead of pinning it +- :white_check_mark: test(run): pin the atomic --emit replace by target file mode (D-122) +- :white_check_mark: test(run): pin the emit-before-reconcile invariant on stdout too (D-122) +- :white_check_mark: test(docs): pin the retired truth-lag claims so they cannot come back (DOC-05/06/09/10/11) +- :white_check_mark: test(release): pin the CHANGELOG drift gate content, wiring and polarity +## [0.1.0] - 2026-08-05 + ### Chores - :tada: chore: scaffold repository — vision, ADRs, C4, meta-plan, specs skeleton, Go module, examples - build(deps): bump actions/setup-go from 5.6.0 to 7.0.0 From 8d8823e099a53fdb91155fc39ba756ea8a365107 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Fri, 7 Aug 2026 15:23:06 +0200 Subject: [PATCH 10/12] :construction_worker: ci(release): wire the changelog gate test itself into task check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit changelog_gate_test.sh is the only thing that reds when the `- task: changelog-verify` line is deleted from check:, and it was invoked by no task and no workflow — the exact defect this lane exists to close, shipped by the lane closing it. Proven: with that line deleted, `task release-changelog-gate-test` fails with "task check does not run changelog-verify"; nothing else in the gate set notices. Listed after changelog-verify because it probes the real CHANGELOG.md (append, assert red, trap restore), so a restore failure cannot red the simpler gate for the wrong reason. Deliberately NOT added to the script`s own WIRED_TASKS list: a script cannot assert its own invocation — deleting the check: line stops it running at all. Follows the release-verify-tag-gate-test precedent (AUD-S03). --- Taskfile.yml | 10 ++++++++++ hack/release/README.md | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Taskfile.yml b/Taskfile.yml index 5d3b0b3..5fa4ae8 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -82,6 +82,11 @@ tasks: # stated plainly: a commit that changes history without regenerating CHANGELOG.md # makes the NEXT `task check` red until `task changelog-write` is committed. - task: changelog-verify + # AUD-S02: the drift gate's own guard, and the ONLY thing that reds when the + # `- task: changelog-verify` line above is deleted. Runs after changelog-verify + # because it probes the real CHANGELOG.md (append + trap restore) — a restore + # failure must not red the simpler gate for the wrong reason. + - task: release-changelog-gate-test # AUD-S03: the release gate's own guard. Its only other CI caller is release-exitgate, # which is skipped on pull_request — without this a regression merges green and only # reds main afterwards. Fast and offline (stubbed `gh`); needs jq. @@ -203,6 +208,11 @@ tasks: cmds: - bash hack/release/verify_test.sh + release-changelog-gate-test: + desc: "AUD-S02 gate: the CHANGELOG drift gate is wired and fires (REQ-AUD-S02-01/02)" + cmds: + - bash hack/release/changelog_gate_test.sh + release-verify-tag-gate-test: desc: "AUD-S03 gate: verify-green-on-tag-SHA polarity table + release.yaml step order (REQ-AUD-S03-01/02)" cmds: diff --git a/hack/release/README.md b/hack/release/README.md index a00c04b..e40987d 100644 --- a/hack/release/README.md +++ b/hack/release/README.md @@ -15,7 +15,7 @@ template emits **categorized subject lines only — no commit SHAs** (D-101, oss | `task changelog-write` | Regenerate `CHANGELOG.md` from tags + unreleased commits | | `task changelog-verify` | Fail closed if `CHANGELOG.md` drifts from `cliff.toml` output (release gate) | | `bash hack/release/verify-changelog.sh` | Same check as `task changelog-verify` (script entry point) | -| `bash hack/release/changelog_gate_test.sh` | AUD-S02: proves the drift gate is wired and fires (REQ-AUD-S02-01/02) | +| `task release-changelog-gate-test` | AUD-S02: proves the drift gate is wired and fires — in `task check` (REQ-AUD-S02-01/02) | Long-lived notes for consumers of released artifacts (currently the **D-120 `pins.toolDigest`** warning) live in `cliff.toml`'s `[changelog] header`, not in `CHANGELOG.md`: the file is From 0573b6dcd8229e2e6a573959d9b386b3daa07a47 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Fri, 7 Aug 2026 15:23:23 +0200 Subject: [PATCH 11/12] :memo: docs(changelog): name the second toolDigest fallback branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The note said the fallback applies when build info is absent, matching the schema description verbatim. canonicalBuildInfo also falls back when build info is PRESENT but not content-bearing — no module sum and no vcs.revision, i.e. `go build -buildvcs=false` and test binaries — which is the branch a record consumer is more likely to actually meet. --- cliff.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cliff.toml b/cliff.toml index e4c7c3b..f0d9093 100644 --- a/cliff.toml +++ b/cliff.toml @@ -24,7 +24,9 @@ would be silently overwritten by the next `task changelog-write`. identifiable by exactly it. Builds after D-120 derive `toolDigest` from the binary's canonical Go build info instead (main module path/version/sum, dependency checksums, VCS revision and dirty flag), falling back to `sha256("buildinfo-unavailable\\n" + toolVersion)` - when build info is absent. The field, its type and the frozen `v1alpha1` schema are + when that build info is absent or does not identify the main module's content (no module sum + and no VCS revision — `go build -buildvcs=false`, test binaries). The field, its type and the + frozen `v1alpha1` schema are unchanged — records published with `v0.1.0` remain schema-valid — but the *value* is not comparable across the boundary: a mismatch between a pre-D-120 and a post-D-120 record means "derived differently", not "different build". From 76a4e3c324521f687fe452f2683c07ba5eae12b4 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Fri, 7 Aug 2026 15:23:33 +0200 Subject: [PATCH 12/12] :wrench: chore(release): regenerate CHANGELOG.md after the AUD-S02 wiring commits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Picks up this lane`s commits under Unreleased and the tightened D-120 toolDigest fallback wording from the changelog header. Prefixed chore(release): — one of the two subjects cliff.toml skips — so the regeneration does not itself leave the file stale. Release surface: CHANGELOG.md is a published contract artifact. --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fb44ff..72b3287 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,9 @@ build stamped with the same version shared one digest; records carrying that der identifiable by exactly it. Builds after D-120 derive `toolDigest` from the binary's canonical Go build info instead (main module path/version/sum, dependency checksums, VCS revision and dirty flag), falling back to `sha256("buildinfo-unavailable\n" + toolVersion)` -when build info is absent. The field, its type and the frozen `v1alpha1` schema are +when that build info is absent or does not identify the main module's content (no module sum +and no VCS revision — `go build -buildvcs=false`, test binaries). The field, its type and the +frozen `v1alpha1` schema are unchanged — records published with `v0.1.0` remain schema-valid — but the *value* is not comparable across the boundary: a mismatch between a pre-D-120 and a post-D-120 record means "derived differently", not "different build". @@ -61,6 +63,7 @@ comparable across the boundary: a mismatch between a pre-D-120 and a post-D-120 - :memo: docs(changelog): warn record consumers that pins.toolDigest changed value (D-120) - :memo: docs(decisions): record D-125 — CHANGELOG drift gate placement and its cost - :memo: docs(release): the changelog drift gate is in task check now, not outside it +- :memo: docs(changelog): name the second toolDigest fallback branch ### Features - :sparkles: feat(cli): dispatch-table help listing the real subcommands (REQ-AUD-S05-01) @@ -84,6 +87,7 @@ comparable across the boundary: a mismatch between a pre-D-120 and a post-D-120 - :construction_worker: ci(release): run the CHANGELOG drift gate in task check and on main CI - :construction_worker: ci(docs): wire the AUD-S06 docs truth-lag gates into task check (D-124) - :construction_worker: ci(lint): wire the depguard polarity proof into task check (AUD-S07) +- :construction_worker: ci(release): wire the changelog gate test itself into task check ### Security - :lock: fix(forge): prove changed-file enumeration completeness or declare a gap