Skip to content

✅ test(aud-s18): audit exit gate — 2026-08-06 conditions closed, gates green at the new bar - #38

Merged
konih merged 47 commits into
mainfrom
lane/aud-s18-exit-gate
Aug 9, 2026
Merged

✅ test(aud-s18): audit exit gate — 2026-08-06 conditions closed, gates green at the new bar#38
konih merged 47 commits into
mainfrom
lane/aud-s18-exit-gate

Conversation

@konih

@konih konih commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

AUD-S18 — exit gate for the 2026-08-06 project audit

Last story of the P5-AUD epic. hack/audit/exitgate_test.sh (~1980 lines) + hack/audit/README.md,
wired into the release-exitgate job of .github/workflows/verify.yaml and available locally as
task audit-exitgate-test. Deliberately not in task check — it runs task check itself.

⚠️ Stacked on #35 — expected, do not unstack

This branch carries the commits of PR #35 (lane/aud-s13-test-depth, 1c4b571..1cede44)
beneath its own, because #35 has not merged yet and AUD-S18 grades the 91% coverage bar #35 raises.
Merge #35 first; this PR's own diff is the seven commits from e8528fa onward.

What the gate asserts

One invocation, seven checks, every failure naming the audit finding ID it reopens:

# Asserts Names on failure
1 The AUD-S01 fail-closed cassettes PASS by name — not "go test exited 0", which a -run regex matching nothing also does REL-07
2 task check green with all 14 pinned stages actually executed (incl. changelog-verify) and measured ./internal/... coverage at or above 91.0% RELSE-01, TEST-03
3 The release job still runs the verify-green-on-tag-SHA gate, before anything is built/signed/published, and undisarmed RELSE-05
4 The AUD-S05/S06 truth pins pass; no retired pre-release phrase has resurfaced on a front-of-house surface DOC-05DOC-11
5 The determinism double-run gate is green and every test its -run patterns name really ran, twice AUD-S18 determinism
6 The frozen JSON schemas are unchanged against an immutable base ref, except the one permitted D-120 toolDigest description line ARCH-03, D-120
7 The spec's Appendix-B disposition table covers all 37 2026-08-06 finding IDs (27 Done / 4 Operator / 6 Accepted) and every row resolves to a real story, backlog row or decision row REQ-AUD-S18-02

Plus a CI-wiring check that the release-exitgate job runs this script, undisarmed, with no arguments.

A green run is not release clearance: check 7b pins the spec's Post-audit release blockers
section (OQ-27, OQ-28) so the gate cannot be read as "all known fail-opens are closed".

Verified empirically

  • task check — all 14 stage banners printed, exit 0. Graded on the banners, not the exit
    code, because task check aborts at the first failure:
    fmt · vet · lint · test · coverage · build · dogfood-comparison · compare-exitgate-test · changelog-verify · release-changelog-gate-test · release-verify-tag-gate-test · docs-gates · lint-depguard-test · lint-workflow-pins-test

  • Full gate, no arguments: exit 0, 81 OK: lines — cassettes, task check, determinism
    double-run and the ref-relative schema freeze all really ran.

  • Requirement (6) probed against COMMITTED schema changes, which is the class the pre-existing
    git diff schemas/ guard (hack/compare/exitgate_test.sh:15-19) is silent on. Three probes
    clone the repo onto a throwaway branch, commit a schema edit, and assert_committed_only proves
    both git status --porcelain -- schemas and git diff -- schemas are empty before the check
    runs. All three red for their own pinned message:

    • probe A — a validation keyword changed (minLength: 12) → "changed line(s) since"
    • probe B — the one permitted line rewritten to non-D-120 text, still exactly 2 changed lines,
      so only the content check can discriminate → "not the sanctioned toolDigest description replacement"
    • probe C — a frozen schema deleted → "added, deleted or renamed since"

    Plus: base overridden to HEAD / to a branch / to a non-existent tag, and a deliberately broken
    baseline pathspec — each red for its own reason. Requirement (6) is genuinely closed.

  • Five disarm vectors were confirmed open on the previous revision, by replaying each new
    mutant through the old check functions (HOLE CONFIRMED (old gate GREEN) × 5). None of the new
    controls is decorative:

    1. Deleting the gate step's - name: line merges its run: into the preceding step. The
      merged region still contains the invocation, still starts with a step marker, still carries no
      env:/if: — so every disarm assertion was reading two steps as one. Closed by
      step_run_count, pinning the isolated region at exactly one run: key — in both the
      CI-wiring check and the RELSE-05 release-gate check, both of which were open.
    2. A job-level env: ASSENT_AUDIT_SCHEMA_BASE: steers check (6)'s baseline without touching
      the gate step, walking past the step-level env: ban.
    3. So does an earlier step writing ASSENT_AUDIT_SCHEMA_BASE=… >> $GITHUB_ENV — no env: key
      anywhere. (2) and (3) are closed by one directory-wide absence sweep over
      .github/workflows/**, positive-controlled on the workflow inventory.
    4. A job-level continue-on-error: on release-exitgate makes every step advisory: the gate
      runs, reds, and the workflow still concludes success.
  • Three banned pipe shapes removed (c2fdd94). The gate's own header forbids grep -q on a
    pipe read end — under pipefail the early close SIGPIPEs the writer and the pipeline reports 141
    — and it carried three violations of its own rule. The dangerous one was probe C's
    git ls-files … | grep | grep | head -1 as a bare assignment under set -e: a 141 there
    kills the gate outright. The other two red for the wrong reason rather than passing open. That
    intermediate commit was itself run end-to-end (probe C included) to confirm it is not a broken
    bisect point.

Verified by reading, not execution

The seven pre-existing checks and their ~50 pre-existing mutation controls were read line by line
for the repo's dominant failure mode (negative greps without positive controls, \t in an ERE,
extractions that sweep an empty set, markers satisfiable by help text). Everything found is listed
above; the previously-fixed set is acdb88f ("four vacuity defects"). No further vacuity was found
in the layers I did not change.

Container / GNU-grep status

docker run --rm -v "$PWD:/w" -w /w debian:stable-slim bash hack/audit/exitgate_test.sh --text-only
exit 0, 67 OK: lines, under GNU grep + mawk. Scope honestly: this covers the parsers, the
workflow/Taskfile/spec structural pins and the full mutation harness for those layers —
including all the new find/awk/grep code in check_ci_wiring. It does not cover
check_schema_freeze or any real go test transcript; those need git history and the Go
toolchain and ran only on macOS/BSD. No \t, \s, \b or grep -P appears in any pattern.

Residuals left open, deliberately

  • The release-exitgate job-level if: github.event_name != 'pull_request' is not pinned. It
    is load-bearing — D-125 makes task check's changelog stage red by construction on PRs — and no
    rule that admits it can also refuse if: false without being brittle or trivially reworded
    around. A weak check here would be worse than a named residual. Documented in
    hack/audit/README.md. The structural fix is RELSE-08 (make release-exitgate a required PR
    check), already fenced to the operator.
  • check_schema_extensions uses find -type f -name '*.*', so a file under schemas/ with no
    extension at all is invisible to the extension pin. Low realism (a schema artifact without an
    extension), but it is the same "narrowing became a loophole" shape D-131 exists to close.
  • The pre-existing PCS working-tree guard (hack/compare/exitgate_test.sh, D-118) is left as-is —
    lane owner PCS. AUD-S18's ref-relative guard is what carries this epic's freeze claim (D-131).

Decisions

No new D-nnn. D-131 is introduced by this branch and unmerged, so its env: clause was
corrected in place to the widened scope (one-line edit, keeping the footprint minimal in a
merge-conflict hotspot) rather than superseded by a D-132.

konih added 30 commits August 8, 2026 15:30
…ranches, repo_file containment + expiry

TEST-02 (REQ-AUD-S13-01) — internal/core/aggregate/evaluate_tocel_test.go:
a json.Number fitting neither int64 nor float64 falls back to its STRING form
(evaluate.go:191). Pins that the fallback is observable (type(new) == string)
and that every predicate shape over it ERRORS -> fail-safe, plus a Cover-level
test proving the error reaches the decision as predicate.error/require-review
and never APPROVEs through a string/number confusion.

TEST-05 (REQ-AUD-S13-02) — internal/forge/clearslot_test.go:
drives forge.Reconcile down the clear-slot path over all branches of
reconcileClearSlot, each error branch at both polarities: list failure,
resolve refusal, PARTIAL CLEAR (forge reports success but leaves the thread
open), idempotent already-clear, rescan-listing failure, no-thread-for-slot,
and duplicate open threads. Every refusal asserts zero operations, the thread
still open, and an unrelated slot untouched. Error injection uses a local
stub embedding *fake.Forge -- no production or fake change.

TEST-06 (REQ-AUD-S13-03) — internal/provider/builtin/repo_file_test.go:
a path-containment table (traversal, absolute, backslash, normalization-only
escape, sibling-prefix directory, walk-up clipping, File-as-basename) and an
expiry table (missing/unparseable/zero/negative maxAge; the expiry boundary
asserted one nanosecond either side), plus the undecodable/empty-document
axis. Rejections assert fact.Value == nil -- never a fact from outside the
roots or past expiry. The expiry table asserts the builtin's OWN response as
well as the host outcome, because the host schema gate masks the provider
reason for a malformed maxAge.

Every table carries a case-count positive control so a lost case cannot pass
vacuously. Zero production-code changes.
The aggregate ./internal/... total sat at 90.28% against a 90% floor, so the
gate had stopped MEASURING and started STEERING: any lane adding a branch
without a test reddened task check for a reason unrelated to that lane, and
the cheapest green was a filler test. AUD-S13's three behavior families lift
it to 91.010% (4479 -> 4515 of 4961 statements), so the floor moves with them.

Taskfile.yml coverage: desc, echo, awk threshold and FAIL message all move
together -- a half-updated gate prints a message that lies. GUIDELINES.md's
'>=90% coverage gate' claim moves with it.

Logged as D-128, which also records the two things a future reader needs:
the margin is 0.01pp (about half a statement), and .github/workflows/
verify.yaml still enforces 90% because that path belongs to Lane B / PR #25 --
so CI enforces the OLD floor until Lane B raises it.

D-128 supersedes the AUD-S13 spec's 'Not in scope: raising the D-010 floor'
line; the operator ruled otherwise for this change.
…nership line

Two corrections to the row landed one commit ago, both material to how a
future reader acts on it.

1. The margin was stated as '0.01pp, about half a statement'. That reads the
   raw ratio, but the gate compares the ONE-DECIMAL total that
   `go tool cover -func` prints -- pct is the string '91.0', not '91.010'.
   Anything rounding to 91.0 passes, so the effective floor is ~90.95% and the
   true minimum is 4513 of 4961 statements. Actual slack is 2 statements, not
   half of one. Understating it invites someone to conclude the 91% floor is
   unsustainable and walk it back.

2. The row assigned the verify.yaml raise to 'Lane B'. PR #25 (AUD-S09/S14),
   the workflow-owning lane, merged 2026-08-08 -- before this lane landed. No
   open lane owns .github/workflows/**, so the split-gate residual is UNOWNED,
   and a reader who trusts the old wording would assume it is covered while CI
   quietly keeps enforcing 90%.

Row text only; no gate behavior change.
…t skew (D-128)

The fence that kept AUD-S13 out of .github/workflows/** was PR #25, which
merged. So the split gate this lane flagged is now closable, and the right
fix is not to raise a second literal -- that only resets the drift clock.

verify.yaml no longer re-implements the coverage gate. The 'install Task'
step moves above it and the step body is just `task coverage`, so CI runs
the same gate developers run. The threshold now exists in exactly one place:
Taskfile.yml's coverage: task var COVERAGE_MIN, interpolated into the echo
and the awk comparison, with desc naming the var instead of the number.
verify.yaml carries no coverage threshold at all any more.

Proven at both polarities rather than assumed: COVERAGE_MIN=99 reds the gate
with the right message. That control matters specifically because an
unrendered {{.COVERAGE_MIN}} would yield min="", min+0 == 0, and a gate
that passes everything -- a templating typo would have been a silent
fail-open, not a visible break.

hack/lint/workflow_pins_test.sh stays green (39 controls), including 'both
Task installs interpolate a single workflow-level TASK_VERSION' after the
step move; verified the YAML still parses and the step order is right.

Also logs the two fail-opens AUD-S13 found while writing its tests, as
OQ-27 (toCEL lexical compare returns a silently wrong boolean when BOTH
operands are over-range -- and evaldecode.go:61 understates the known
residual, so it is a docs-truth defect too) and OQ-28 (repo_file enforces
path containment but not filesystem containment; os.DirFS is not a security
boundary and the escaping symlink can arrive in the MR under evaluation).
Neither is fixed here -- both are routed for a severity ruling. No new
D-row: they are undecided, so they are questions, not decisions.

D-128 updated: its split-gate residual is closed, and the closure mechanism
recorded so a future editor does not re-inline the shell.
… deepen OQ-28 (review F4/F5)

Both findings are worse than AUD-S13 first reported. Independent review
escalated them; I reproduced each claim before writing it down.

OQ-27 was scoped to numeric overflow. The class is ANY string-bound operand
under a relational CEL leaf: cel-go defines < <= > >= over two strings, so it
returns a clean boolean exactly where the engine assumes an error. The P1
instance needs no overflow at all -- a quoted YAML scalar. Reproduced through
the production aggregate.Cover entry point with the D-016-shaped
partitions-must-not-shrink rule: 12 -> 6 numeric gives BLOCK with one
partition-count-shrunk finding; the identical policy with "12" -> "6"
gives APPROVE with ZERO findings. evaldecode keeps !!str a Go string
deliberately, but that design assumed the compare would fail safe.

Verified no lint guard exists: checkLeafScope and checkPredicateScope
validate identifier scope, checkFactsShape validates facts-path shape; none
type-checks relational operands.

Recorded the severity split explicitly -- mechanism P1, over-range instance
alone P2 on reachability, P1 resting on the quoted-string case -- and that
hard rule 7 is NOT violated: a lexical compare is deterministic. What is
violated is the fail-safe direction and evaldecode's own written claim.
Conflating those is how a real finding gets waved away.

OQ-28 gains the discriminating question its severity ruling depends on: does
internal/change see a symlink swap of a governed path as a change to it? Git
records 100644->120000 so enumeration should surface it, but that is asserted
not verified, and it separates "reads arbitrary files" from the worse
"silently influences a decision". Also recorded: the escape reaches
arbitrary ABSOLUTE host paths and has a second, stronger form (a file symlink
at a legitimate in-root path, where every inspected path string is clean);
it is live in production via provider_host.go's checkoutFS building
os.DirFS over the MR HEAD tree, so the symlink arrives in the MR under
evaluation; resolved non-sensitive values reach the MR comment through
render.formatMessageScalar/displayFactValue, with sensitive:true the only
limiter; the target must still be a mapping with a matching top-level key,
which keeps it narrow. Named the sibling: LoadResourceOwnerMap reads the same
FS with no roots at all, and that registry decides who may approve.

No OQ-29 and no D-row: one class, one row, and neither is decided.
…le (review F6)

repo_file_test.go's `if fact.Value == float64(99)` could never fire: to
reach it a case must already have passed the state assertion and the
`fact.Value != nil` check, so Value is nil by construction. It was dead
weight that made the table look like it had a guard it did not have.

The sibling-prefix case still bites without it -- re-ran the
HasPrefix(p, root+"/") -> HasPrefix(p, root) mutation and it fails on
`state = "resolved" ... want "unavailable"`. Adjusted the fixture comment
to say which assertion actually does the work, and why the per-directory
values are still worth keeping (they name the leak's source in the failure
output rather than leaving "resolved, but from where?").
…s green at the new bar

REQ-AUD-S18-01/02. One invocation, seven checks, every failure naming the
audit finding ID it reopens; wired into the release-exitgate CI job.

Requirement (6) is deliberately NOT the working-tree `git diff schemas/` the
PCS gate uses: that is silent on COMMITTED schema changes, which are the only
kind that ship. This diffs schemas/**/*.json against the immutable v0.1.0 tag
and pins the extension set under schemas/ so the *.json narrowing cannot
become a loophole. Recorded in D-131.

Every check is a function with a mutation control that must red for its own
stated reason, following hack/lint/workflow_pins_test.sh.

Cites: D-131, D-120, D-124, D-125, D-128.
…d the OQ-27/28 blocker rows are its dependency

# Conflicts:
#	docs/decisions/decisions.md
Found by running the harness, not by reading it:

* the COVERAGE_MIN extraction never matched (a stray leading colon in the
  regex over a single-file code_view), so the D-010 floor pin was asserting
  nothing — its own positive control reported "found 0" and reddened;
* check_coverage_bar grepped 'coverage: NN.N%' unanchored, so it graded the
  WORST per-package line `go test -coverprofile` prints against the aggregate
  bar; now anchored at column 1 on the gate's own echo, with a control that
  injects a 62.5% package line and must stay green;
* the RELSE-05 step-order check was file-scoped, so release.yaml's
  pull_request-only `snapshot` job made it red on the clean tree; now scoped
  to the `release` job, with the gate step asserted to be inside it;
* the committed-schema probe had only one polarity. Probe B now rewrites the
  ONE permitted line while keeping the diff at exactly two lines, so the
  line-count guard cannot be what fires — only the content check can.

Also: --text-only now prints a banner saying it certifies nothing about the
tree, instead of the full-run banner it was reusing.
…operator residuals

Backlog: AUD status paragraph (S01-S18; 37 findings dispositioned 27 Done /
4 Operator / 6 Accepted) plus an explicit, non-dropped handover of SEC-05 PAT
rotation, SEC-06 tag ruleset, RELSE-07 enforce_admins and RELSE-08
required-check — all live GitHub settings no in-tree gate can reach.

States plainly that AUD complete is NOT release clearance: OQ-27 and OQ-28 are
post-audit decision-path fail-opens that block the tag independently.

Cites: D-131.
…t open

The base ref of check (6) is overridable (ASSENT_AUDIT_SCHEMA_BASE) so it can
be moved forward to a later release. Nothing stopped it being moved to HEAD —
a step-level `env: ASSENT_AUDIT_SCHEMA_BASE: HEAD` empties the diff and leaves
the whole freeze guard vacuous while check_ci_wiring, which only looked for
'continue-on-error' and 'if:', printed OK over it. Having just closed
'--text-only' as a disarm, the gate shipped a wider one beside it.

Two layers, each with its own control:
  * check_schema_freeze refuses any base that is not a vX.Y.Z TAG that resolves
    as a tag. Note 'base == HEAD' is deliberately NOT the discriminator — right
    after a tag that is legitimate; the constraint is on the ref's shape.
  * check_ci_wiring refuses an 'env:' block on the gate step at all. This gate
    takes no configuration, so the cheaper invariant also covers whichever knob
    is added next.

Cites: D-131.
…xit gate

The gate's own header bans `grep -q` on the read end of a pipe (under
`set -o pipefail` the early close SIGPIPEs the writer and the pipeline reports
141) and it carried three violations of its own rule:

  * `git ls-files … | grep | grep | head -1` picking probe C's victim schema
    was a BARE assignment under `set -e` — a 141 there kills the gate outright
    rather than reporting anything. This is the undefended one.
  * `grep -n '^  release:$' … | head -1 | cut` and the same shape resolving the
    `release-exitgate` job line. Both are followed by an emptiness test, so a
    141 reds them for the WRONG reason rather than passing open — still the
    banned shape, still intermittently wrong.
  * `printf '%s\n' "$line" | grep -Eq '(OPEN|CLOSED)'` in the post-audit
    blocker row check, which would read a 141 as "no status token".

Matches go to a file first and the file is inspected; first lines come from a
new `first_line` helper (`awk NR==1`), never from a pipe.
…disarms

Adversarial self-verification against the bar `hack/lint/workflow_pins_test.sh`
sets (31 self-executing mutation controls). Five disarms were confirmed GREEN
against the previous revision of this gate, each by replaying the new mutant
through the old check functions:

  1. Delete the gate step's `- name:` line and its `run:` merges into the
     PRECEDING step. The merged region still contains the invocation, still
     starts with a step marker and still carries no `env:`/`if:`, so every
     downstream assertion was reading two steps as one. `step_run_count` now
     pins the isolated region at exactly one `run:` key — in the CI-wiring
     check and in the RELSE-05 release-gate check, both of which were open.
  2. A JOB-level `env: ASSENT_AUDIT_SCHEMA_BASE:` steers check (6)'s frozen
     baseline without touching the gate step, walking straight past the
     step-level `env:` ban.
  3. So does an earlier step writing `ASSENT_AUDIT_SCHEMA_BASE=… >> $GITHUB_ENV`
     — no `env:` key anywhere in the file.
  4. A job-level `continue-on-error:` on `release-exitgate` makes every step in
     the job advisory: the gate runs, reds, and the workflow concludes success.
  5. (2) and (3) are closed by one directory-wide absence sweep over
     `.github/workflows/**`, positive-controlled on the workflow inventory so a
     collapsed listing cannot read as "no override found anywhere".

Deliberately NOT pinned, and recorded as such in hack/audit/README.md: the
job-level `if: github.event_name != 'pull_request'`. It is load-bearing (D-125
makes the changelog stage red by construction on PRs) and no rule that admits it
can also refuse `if: false` without being brittle or trivially reworded around.

D-131's env clause and the README are corrected to the widened scope: the
CI-wiring check now refuses ASSENT_AUDIT_SCHEMA_BASE anywhere under
`.github/workflows/**`, not just an `env:` block on the gate step.

Cites: D-131, D-125.
toCEL no longer demotes an unrepresentable numeric literal to its lexical
string form; since D-131 it binds a CEL error value. TEST-02 was written to
detect exactly that change ("if a future toCEL change made either one typed,
every case below would stop testing the fallback branch") and it did, so the
assertions move to the new contract rather than being removed or weakened:

- the branch probe asserts a CEL error value, explicitly NOT a string, and that
  the refusal names the number it refused (the equivalent of the old "the
  fallback kept the literal");
- the activation probe flips from "type(new) == string is true" to "type(new)
  == string is unanswerable" — a predicate with no relational operator in it,
  so D-131's textOrderGuard provably is not what makes it fail;
- the predicate table gains string(new) == "1e400" (asking for the very value
  the old fallback handed over must not resurrect it) and each row now asserts
  the error names the refused literal, so the failure is attributable to toCEL
  rather than to an unrelated overload miss.

The Int64/Float64 preconditions and the 400-digit non-exponent literal stay:
they are what makes this test catch a half-closed fix that refuses only the
exponent forms, which leaves relational_string_test.go's whole suite green.
…nd close OQ-27

Both were written against the pre-D-131 engine and are now false as written:

- openspec AUD-S13 goal (1), its G-W-T row and REQ-AUD-S13-01 all described
  toCEL's string FALLBACK as the thing under test. The fallback is gone; the
  requirement now names the refusal, the non-exponent literal and the
  non-relational operators the realigned test pins.
- OQ-27 still read as an OPEN P1 with "release tag BLOCKED on this". D-131 /
  ADR-0013 Amendment 1 closed it on main, in the dedicated decision-path lane
  OQ-27 itself demanded. The row is marked resolved and its blocker withdrawn;
  the analysis body is kept as the record of how the defect was found and is
  labelled as describing the pre-fix engine.
…ts mutation control

D-131 / ADR-0013 Amendment 1 landed on main and closed OQ-27 in exactly the
dedicated decision-path lane the row demanded. After merging the AUD-S13 lane
this tree contradicted itself: docs/planning/open-questions.md said RESOLVED
while the spec's Post-audit release blockers table still said OPEN — and that
table is what the exit gate reads as the release-blocker record, so a stale
OPEN keeps the tag blocked for a defect that is fixed.

The gate itself needed no logic change (check_post_audit_blockers accepts
OPEN or CLOSED, and OQ-28 keeps the section non-empty), but its mutation
control was pinned to the literal '| OQ-27 | OPEN' and would have stopped
mutating, failing the harness with 'the mutant is identical to the clean
input'. It is now status-agnostic, so the next status flip cannot break it.
… blocker

The blockers TABLE was flipped to CLOSED in the previous commit but the prose
around it still asserted, in the present tense, that OQ-27 blocks the tag —
exitgate_test.sh's header (WHAT THIS GATE DOES NOT CERTIFY),
check_post_audit_blockers' doc comment and failure message, the closing banner,
and hack/audit/README.md. Same contradiction, other half of the tree.

The prose now says what the gate actually does: it grades that the record
exists, resolves, and carries an OPEN/CLOSED token — never the statuses
themselves — and a closed row stays in the table because the row IS the record.
No check logic changed; bash -n and the gate's own controls are unaffected.
konih added 15 commits August 9, 2026 02:27
… de-contradict its headline

The D-128 row shipped arithmetic that the D-131 lane's landing invalidated:
it claimed 90.28% -> 91.010% (4479->4515 of 4961), true minimum 4513, slack
TWO STATEMENTS. Re-measured at origin/main 71b573e and at this lane head the
real figures are 90.3885% -> 91.0893% (4514 -> 4549 of 4994), true minimum
4543, slack SIX. The stale figures understated the margin, so a reader acting
on them was over-conservative rather than under -- but it is still a
docs-truth defect of the exact class this epic exists to close.

The margin is now stated as a RULE (ceil(0.9095 * N) for the current N) with
a dated instance beside it, so the next denominator move cannot rot it again.
The identical denominator on both sides (4994) is recorded as the proof that
AUD-S13 adds tests only.

Also removes the row's self-contradictory headline: it read "local gate only;
the CI half is NOT raised here" and then reversed itself in its own
"Split-gate CLOSED" paragraph.
OQ-28's end-to-end escape is closed on main by D-133 (71b573e), which refuses
any symlink under base/ or head/ at changed-file enumeration -- before
providers resolve. The provider-layer guard (builtin.OpenRepoRoot syscall root
plus classifyCandidate's per-component refusal, D-129) is defence in depth and
lives in the dedicated provider lane, PR #36 at 1e71cec.

Verified against origin/lane/fix-provider-symlink-containment rather than
transcribed: OpenRepoRoot, the fs.Lstat/ModeSymlink arm in classifyCandidate,
and the three symlink test files are all present there.

The spec row states plainly that docs/planning/open-questions.md still shows
OQ-28 unstruck on THIS branch -- that close-out belongs to PR #36 and arrives
when it merges -- so the two files do not silently disagree.

The status token is edited, never the row: check_post_audit_blockers requires
the OQ-28 row to keep existing, and a closed row is the record.
…tself

One review round against one gate file and its two doc carriers, so it lands as
one change; D-132's amendment records all four in one block.

F2 -- a CONSTANT job-level `if:` on release-exitgate is now refused. `if: false`
skips the job, and a skipped job reports SUCCESS: the gate stopped running while
check_ci_wiring returned 0. Reproduced end-to-end here, not just in a mutant
directory. The lane's justification for leaving `if:` unpinned -- "no rule that
admits it can also refuse `if: false` without being either brittle or trivially
reworded around" -- was overstated and is retracted in the comment. The value is
normalised (${{ }} unwrapped, quotes stripped, comment tail dropped, trimmed,
lowercased) before comparing, so `if: false`, `if: true`, `if: ${{ false }}` and
`if: 'false'` all red with their own controls, and a fifth control in the other
polarity proves a genuinely REWORDED condition still passes -- the refusal is on
constants, not on one spelling. NOT claimed closed: a semantically-never-true
EXPRESSION (`if: github.event_name == 'deployment_status'`) disarms the job
identically, is verified here to still pass, and stays fenced to RELSE-08.

F3 -- stage BODIES are pinned by content. Replacing the coverage: task's cmds:
with `echo "coverage: 99.9% (required: {{.COVERAGE_MIN}}%)"`, leaving the stage
wired, its vars intact and its banner printing, left check_check_wiring,
check_coverage_floor, check_check_stages AND check_coverage_bar all at exit 0 --
the last of them grading the fabricated 99.9. coverage: is the one stage whose
output this gate consumes as evidence. check_stage_bodies pins a real
`go test -coverprofile=` over ./internal/..., a `go tool cover -func=` the total
derives from, an evidence line interpolating ${pct} rather than a literal, and a
comparison still reading {{.COVERAGE_MIN}} (an unrendered one makes min+0 == 0
and admits everything, D-128); lint-workflow-pins-test: is pinned to its script
the same way. Same mechanism as hack/release/changelog_gate_test.sh, which
already pinned four other stage bodies but not these. Five controls, including
one that keeps the ${pct} pin from being redundant with the measurement pins
(measurement kept, evidence line literalised) and one vacuity control on the
extraction.

F4 -- extension-less files under schemas/ had zero coverage. A committed
schemas/FROZEN passed check_schema_extensions (find -name '*.*' never sees a
dotless name) AND check_schema_freeze (the $NF ~ /\.json$/ filter drops it from
--name-status), which made D-132's and hack/audit/README.md's "cannot become a
loophole" claim false for that one shape. The extension pin now reads the
BASENAME (a dot in a directory component is not an extension) and buckets a
dotless name as `(none)`, which is not in the pinned set. The freeze check's
*.json filter is deliberately left narrow -- widening it would red on AUD-S16's
legitimate schemas/*.go edits. Both claims corrected; control added.

F5 -- the OPEN/CLOSED status-token branch of check_post_audit_blockers was the
only assertion in the file without a mutation control, a side effect of
loosening the neighbouring control to be status-agnostic. It now has one, itself
status-agnostic (it substitutes whatever token is present for a sentinel, so it
cannot silently stop mutating when a row flips), so the file header's promise
that every check is run against an input carrying its own violation is true as
written.

Gate green in both modes and under debian:stable-slim (GNU grep + mawk):
80 OK: lines, identical on both.
…t lane #36) into the AUD-S13 lane

# Conflicts:
#	CHANGELOG.md
#	docs/decisions/decisions.md
The provider containment lane (#36) merged while this lane was in review, so
the denominator moved a second time: 4994 -> 5023. Re-measured at the new
merge base and at the lane head -- 90.4041% -> 91.0810%, i.e. 4541 -> 4575 of
5023 (+34 covered), denominator identical on both sides, which is still the
tests-only proof. New dated instance: true minimum ceil(0.9095 * 5023) = 4569
(4569 -> 90.9616% -> prints 91.0 -> PASS; 4568 -> 90.9417% -> 90.9 -> FAIL),
so the slack is again 6 statements.

Second re-measurement in one lane, both caused by main moving within a day --
recorded in the row as the reason the FORMULA is the durable part and the pair
of integers is not.
) into the AUD-S18 lane

# Conflicts:
#	CHANGELOG.md
…s on main

The provider containment lane merged as 7513d79 while this lane was in review,
so the caveat this branch carried -- that open-questions.md still showed OQ-28
unstruck here and the close-out would arrive with PR #36 -- is now false. It
is removed from the spec's blocker row, from D-132 and from the gate's three
prose copies; the OQ-28 row itself stays CLOSED and stays in the table, which
is what check_post_audit_blockers requires.

Swept by identifier (OQ-28, PR #36, 1e71cec) rather than by the line numbers
fixed earlier, since the merge moved them.
@konih
konih merged commit fb648c1 into main Aug 9, 2026
8 checks passed
@konih
konih deleted the lane/aud-s18-exit-gate branch August 9, 2026 09:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant