Skip to content

Phase 4: the DAST tier (D.1-D.28) - #15

Open
tom-snyder wants to merge 21 commits into
feat/phase3-lane-a-emitfrom
feat/phase4-dast-kernel
Open

Phase 4: the DAST tier (D.1-D.28)#15
tom-snyder wants to merge 21 commits into
feat/phase3-lane-a-emitfrom
feat/phase4-dast-kernel

Conversation

@tom-snyder

Copy link
Copy Markdown
Member

Stacked on #14. Retargets to main automatically when that merges.

26 of 31 Phase 4 packets. D.29 inherits its base model from the SAST adjudicator selection, so it, its critic D.30, and the D.31 integration test are held on M0.18.

What is here

  • D.1–D.9 the fail-closed authorization kernel: 21 gates across build/packaging, run initiation, per-target admission, per-request enforcement, and disclosure
  • D.10–D.13 containment: ephemeral provisioning, netns/nftables, the canary assertion, reset lifecycle
  • D.14–D.17 nuclei and ZAP drivers, and template pinning
  • D.18–D.22 attack-surface inventory across four tiers, plus confirmation
  • D.23–D.28 crawl, authenticated crawl, coverage reporting, the confirmation gate

Verification

36 adversarial critics ran against this work and all 36 returned FAIL correctly. The last six found zero behavioural defects in the confirmation gate, proven by AST comparison rather than asserted.

Findings worth naming, all now closed:

  • A live vulnerability reported as clean. A target that rate-limits during confirmation produced output byte-identical to a phantom, and AssertNotSilentlyClean derived completed_clean from it.
  • The raw response body escaping through the error return, unbounded — while the type-closure guard that should have caught it walked three types that could not carry it.
  • A repo-authored .anvil/target.yaml pointing the health check at 169.254.169.254. Gate 10 covers probe time; the health check is a different path.
  • The 45-day embargo driveable to zero by back-dating vendor contact against a caller-supplied clock.

Rulings

Nineteen orchestrator rulings are recorded in the commit messages. Most are one lesson in new clothes — a denylist loses — which recurred nine times: a status list, an entity bound, a benign corpus, a generator's alphabet, a depth-0 union, a class union excluding literals, an operator tag, a case-boundary heuristic, a wording registry.

What ended it was ruling 14: enforce the invariant where it is exactly decidable. Nine rounds tried to decide statically what a regex could match. The invariant was always about a match — two integers, one comparison — and extractSpan had been computing it all along, wired to whether to inline a span rather than whether to confirm a finding.

Gate 1 is no longer vacuous

go list -deps previously found zero internal/dast packages in either binary, so the S9-AMENDED artifact split passed by emptiness and would have kept passing if the kernel were deleted. cmd/anvil-dast now genuinely links internal/dast/{authz,engines} and cmd/anvil stays empty of them.

Known limits

Docker, Linux network namespaces, nftables, nuclei, ZAP and Java are all absent from the dev host, so containment and the engine drivers are proven against recorded shapes with the tool-absent path refusing loudly. Every gap is recorded in internal/SKIPPED-CONTROLS.md with what would settle it. No t.Skip was added anywhere.

Note on history

One commit range was rewritten before pushing: a synthetic GitLab-PAT-shaped test fixture (glpat- plus 20 keyboard-pattern characters, inside the base64url oracle-family test) tripped GitHub push protection. It was never a real credential, but it matched the format exactly. Replaced with a zero-entropy glpat-NOTAREALTOKEN0000000 that still matches the pattern under test, and purged from history rather than allow-listed — so the repository never contains a credential-shaped string.

Twenty-one gates across four phases, plus the target manifest and the build-time
invariants that make gates 2 and 3 enforceable rather than aspirational. This is
the code every network request in Anvil passes through, and it is written on the
assumption that it will be attacked.

Three rulings were issued before dispatch, because plan/50-dast.md would
otherwise have produced a red tree and two silently wrong fields.

Its dast_status enum (:1149) is the PRE-RULING five-value set. Section 6 froze
ten values, and internal/record/contract_test.go:108-111 rejects "clean",
"findings", "failed_to_boot" and "partial" BY NAME, attributing each to area D.
Its target_provenance (:1150) is populated with anvil/target.provisioning values
-- two different fields, which is the original defect section 6 caught. And its
"Conflicts With Spine" section says it implements the config-gated DAST model
"because that is what spine S9 specifies"; S9 was AMENDED, and CI has enforced
the two-artifact split since Phase 3.

Three more rulings settled questions workers escalated rather than acted on.
Gate 7 was removed from the admission chain: it is a run-initiation gate, and
widening gateFunc to carry trigger provenance would have broken S7's "pure
function of (target, scope, attestation, clock)" -- the property that makes the
decision auditable. Gates 13-17 stay out of that chain but now have their own
chain TYPE, because an unregistered gate and a forgotten one are
indistinguishable by inspection. And gate 13 re-checks scope membership:
revalidationChain was {8,9,10}, which canonicalize, pin and screen reserved
ranges but never ask "is this host in the allow list", so a redirect to an
in-scope-LOOKING host would have sailed through.

What the critics caught

Sixteen critics have now run on this project and all sixteen correctly returned
FAIL. This round was no exception, and the two criticals were compiled and run
rather than reasoned about.

Gate 5's thirty-day ceiling was not a ceiling. NewCap was exported and
unvalidated, so NewCap(10*365*24*time.Hour) minted a ten-year attestation still
live nine years later -- while types.go asserted "raising is not expressible".
Cap is the same primitive gate 14's six caps are built from. It is unexported
now, and NewAttestation compares every lifetime against a package const BEFORE
consulting any supplied ceiling: an independent verifier forged a working Cap
through unsafe struct punning and the attestation was still refused, because a
const comparison takes no argument.

Scope was not sealed. NewScope shallow-copied ScopeEntry, sharing the Ports
backing arrays, and writing one integer turned an explicitly DENIED host into a
permitted one. The test that claimed to cover this mutated only the Host string
-- the one field where the copy happened to be real. A test that passes for the
wrong reason. Both directions are deep-copied now, in AND out, and NewScope no
longer accepts caller-built entries at all: it takes the raw bytes and derives
the hash from them, so the assertion has nowhere left to be made.

Four unfalsifiable comments were DELETED rather than softened, per the standard
this project holds itself to.

What is verified, and what is not

D.7 lost its report to a session cap but not its code. Rather than trust a green
suite, it was audited from scratch: 47 single-guard mutations, 41 red with
named on-point tests, file restored byte-identical (sha256 unchanged). That is
how gates 18-21 are known to work rather than assumed to.

D.9's guards resolve by IMPORT PATH, because a sibling guard in
internal/collector/host was defeated by an import ALIAS earlier in this build.
Its test builds a throwaway module and proves plain, alias, dot, blank and
transitive-through-an-innocently-named-intermediary imports are each caught.
The egress check is an ALLOWLIST of ~50 provably-inert import paths, so the next
networking API someone reaches for fails by default rather than by memory.

Gate 1 is recorded as VACUOUS IN BOTH DIRECTIONS and not papered over: `go list
-deps` finds zero internal/dast packages for anvil AND for anvil-dast, so the
split is currently satisfied by emptiness and would keep passing if this kernel
were deleted. D.9 prints a PROVEN/UNPROVEN ledger saying so. No consumer was
fabricated to make the number look better.

Open findings from this round -- gate 21 writing zero audit rows on the denials
it issues, attestation expiry not re-checked per request, a parse-then-hash
TOCTOU in the scope loader, an embargo reachable by back-dating vendor contact,
and a manifest that accepts 169.254.169.254 as an authorized service -- are
being fixed next and are named here so they are not lost if this commit is read
alone.

Evidence: gofmt, vet, build clean; go test -count=1 ./... green across 28
packages; no t.Skip added.
Nineteen critics have now run on this project and all nineteen correctly
returned FAIL. Four of them ran against this kernel. What follows is what they
found, because the findings are more instructive than the code.

Two criticals, compiled and run rather than reasoned about

Gate 5's thirty-day ceiling was not a ceiling: NewCap was exported and
unvalidated, so one call minted a ten-year attestation still live nine years
later -- while the source asserted "raising is not expressible". Cap is the same
primitive gate 14's six caps are built from. It is unexported now, and
NewAttestation compares every lifetime against a package const BEFORE consulting
any supplied ceiling. A verifier forged a working Cap through unsafe struct
punning and the attestation was still refused, because a const comparison takes
no argument.

Scope was not sealed: NewScope shallow-copied ScopeEntry, sharing the Ports
backing arrays, and one integer write turned an explicitly DENIED host into a
permitted one. The test claiming to cover this mutated only the Host string --
the one field where the copy happened to be real.

Fixing that closed the named attack and RELOCATED it. The same function read the
caller's buffer twice, so a concurrent write between parse and hash produced a
sealed Scope whose entries came from one document and whose hash came from
another: 2012 of 4000 scopes mismatched under measurement. Gate 5's scope
binding is what makes an attestation specific to a scope rather than to a
session, and that defeated it.

Three rulings, each settling a root cause rather than a symptom

A RUN HAS EXACTLY ONE CLOCK. An embargo could be driven to zero by dating the
vendor contact in January, recording it against a January clock, and publishing
at the real August present -- 45 days of embargo, zero days of vendor notice,
every gate green. The previous round had bounded one caller-supplied instant
against a second caller-supplied instant, which catches the lie told with one
clock and misses the lie told with two. A function that TAKES a clock can be
lied to; one that READS THE RUN'S clock cannot. RunClock is sealed by the same
unexported idiom already carrying Scope and Attestation.

AN UNVERIFIED CALLER ASSERTION MAY DELETE ONLY THE PART OF A CONTROL IT IS
ABOUT. Gate 20's self-assertion bypass was fixed by hoisting the scope and
attestation checks above the ownership branch -- and reappeared at gate 18,
which had the identical shape. "This repo is mine" bears on whether a patch is
unsolicited. It does not bear on whether an embargo elapsed or what time it is.

PIN THE RELATION, NOT ONLY THE VALUES. MaxVendorContactBackdate and
MinAcceleratedEmbargo were both 24h. Each was individually defensible; because
they were EQUAL, a contact back-dated exactly to the permitted bound cancelled
the acceleration floor and published in the same run that recorded first
contact. The backdate bound moved to 1h -- that constant, not the floor, because
the floor is a claim about the world (the shortest window in which a vendor told
about active exploitation can act) and the bound is only operational slack for
clock skew. The test now asserts the INEQUALITY, and the 23h difference, so the
prose cannot drift from the code.

A gate that can only see what the caller hands it cannot enforce a state machine

A recorded decision to WITHHOLD was overturned by writing a second row, because
gate 18 read only the PersistedDisclosure passed to it and DisclosureStore had
no read method at all. It has one now, and "no row" and "I cannot tell you" are
different facts: gate 18 is fail-closed on a read error, an absent row, a nil
store and a wrong medium. A read that fails does not mean nothing was withheld.

Three lessons about guards, all learned by watching one fail

A DENYLIST LOSES: the first pin for the TOCTOU fix enumerated two spellings and
lost to `rawAlias := raw[:]`, leaving the entire suite green. Its replacement
was a shape allowlist that caught 18 of 21 attack shapes -- and matched the
parameter BY POSITION, so a leading ctx parameter disarmed it silently. It
resolves by identity now and fails loudly when it cannot find exactly one
candidate. An allowlist matched by position is a denylist wearing better
clothes.

A CHECK THAT CANNOT SEE THE DAMAGE IS NOT A CHECK: a round reported the tree
UTF-8 clean while four U+FFFD characters sat in production doc comments. The
claim was true and irrelevant -- EF BF BD is itself valid UTF-8. The replacement
guard scans for the codepoint.

A CONTROL THAT RUNS NOWHERE IS NOT A CONTROL: the concurrency harness carries
//go:build !race for a real reason (the detector fires on the fix itself), and
the only CI step compiling that package was the -race one, so it was compiled in
zero lanes. CI now runs both.

Evidence: gofmt, vet clean; go test -race -count=1 ./... green; no t.Skip added.
Two findings remain open from the final pass and are named in the next commit
rather than left in a report: an audit-ordering inversion in
AuditedPersistDisclosure, and a tree guard whose walk is narrower than the set
of tracked files.
…ment

Ephemeral provisioning, the netns/nftables ruleset, the canary assertion and the
reset lifecycle. 8,355 lines. Docker is not installed here, Linux network
namespaces do not exist here, and that shapes this commit more than the code
does.

The instruction that mattered was: the Windows path must REFUSE, not pass. A
test going green by doing nothing would be the silent-clean failure this
codebase keeps catching, and it would be worse here than anywhere else, because
"the sandbox is contained" is the claim that licenses Anvil to run probes at
all. A control that reports contained-when-unverified is the one that gets
someone breached. The critic confirmed: no t.Skip, no Windows path that reports
contained, no unconditional success in AssertContainment.

The design spike the plan deferred (D.11) came out on the canary side. A
host-side probe proves something about the netns AS CONFIGURED; a canary inside
proves what a process in the sandbox can actually REACH. Only the second is the
claim the gate makes.

Three things the critic found, all real, none fixed yet -- named here so they
are not lost in a report:

Target.Containers() is a shallow copy whose doc comment asserts the opposite,
and this is the Scope critical again in a new package: the test passes only
because it mutates the single string field. Rewriting Mounts, SecurityOpt and
CapDrop THROUGH the copy made the sealed Target's audit trail read
docker.sock / seccomp=unconfined / CapDrop=NONE.

The build/boot split -- which D.10's own packet calls the point -- is destroyed
by ordering. health.timeout_seconds is the total build+start+health budget and
the deadline branch is checked before the runner's own status, so a runner
explicitly reporting BuildFailed after a slow image build is recorded as
boot_failed. Two different failures with two different fixes, collapsed in the
ordinary case.

DeniedByRuleset fails OPEN for any zoned IPv6 address, including
fd00:ec2::254%eth0, because netip.Prefix.Contains drops zones. The relation test
exists; its generator cannot produce the input that breaks the relation.

And the two halves do not compose: Provision seals booted_clean -- the literal
that authorizes probing -- without any netns, and nothing in the tree calls
SetupNetns, AssertContainment or Provision. The "runs on every scan" property is
instantiated zero times today because there is no scan.

Also in this commit, round 5 against the kernel: AuditedPersistDisclosure was
applying its durable write BEFORE the audit write, so an audit-sink failure --
a transient disk-full is enough, no attacker required -- left the state
transition applied with no audit row, which consumed the withheld control round
4 had just built. It is DECIDE -> AUDIT -> APPLY now, audit-first rather than
compensating, because DisclosureStore has no delete and a compensating row is
itself a write that can fail.

The U+FFFD guard now walks git ls-files (236 of 236 tracked) instead of four
hand-listed directories (151, none outside them) -- it had the same shape as the
finding it was written to close.

The race-detector fact is now written into the repo: go test -race works from
PowerShell and fails from Git Bash on this host with ThreadSanitizer error code
87, uniformly across every package. That is an address-space issue, not a race.
Two rounds were spent on this disagreement -- a worker claimed it cannot build,
a critic called that false, and both were right about their own shell.

Evidence: gofmt, vet clean; go test -count=1 ./... green; -race clean from
PowerShell, 26 packages, 0 data races; no t.Skip added.
…uous

The nuclei and ZAP drivers, the containment fix round, and the template pinning
job. The pinning job closes a gap this phase has carried since D.1.

Gate 1's positive half was satisfied by emptiness

`go list -deps` found zero internal/dast packages for anvil AND for anvil-dast,
so the artifact split passed and would have kept passing if the entire kernel
were deleted. cmd/anvil-dast was a 32-line placeholder. D.17 is the first packet
whose write scope reaches it, and wiring pin-templates in makes the dependency
REAL rather than decorative:

  go list -deps ./cmd/anvil-dast | grep internal/dast
    internal/dast/authz
    internal/dast/engines
  go list -deps ./cmd/anvil | grep internal/dast
    (empty)

The dependency is honest rather than manufactured to satisfy a check:
LoadTemplates is what detects a `code:` template, so the pinning job genuinely
needs the engines package. TestSplit_DASTBinaryActuallyLinksTheDASTTier now
fails if the kernel is deleted, instead of both halves staying green.

Every pin read from a primary source, none from memory

nuclei-templates at 83234ce456da3e90dda86dfbc5e605e64a846df3 (v10.4.7), MIT,
licence BODY archived with its sha256 -- read via git cat-file, not API
metadata, per S8. The worker also checked something worth recording: ls-remote
returns ZERO peeled ^{} refs, so every tag is lightweight and a tag SHA is a
commit SHA. That is the kind of assumption that silently breaks a pin, and it
was verified rather than assumed. A version invented from memory 404'd once on
this project already.

The shallow-copy defect, found for the second time

Container.clone() now deep-copies every reference field. The INBOUND snapshot
was shallow too, which was worse than the reported defect: a Docker seam
retaining the slices it returned could rewrite the sealed audit trail AFTER
containment was asserted. There is now a reflection guard that fails when a
reference-typed field is added to Container or Mount and left uncopied, because
"remember to update the clone" is not a control.

Three containment findings worth their own note

The build/boot split was destroyed by ordering in the ORDINARY case: one
timeout covered build+pull+create+start+health and the deadline was consulted
before the runner's own reported status, so a slow image build on a cold cache
recorded a working target as boot_failed. health.timeout_seconds is a POLLING
budget by D.1's own validation -- charging docker build to it leaves the health
wait nothing. Build now has its own budget; the runner's status is read first;
the clock decides only when the runner named no phase, and then says the phase
is UNKNOWN rather than guessing.

DeniedByRuleset failed OPEN for every zoned IPv6 address, because
netip.Prefix.Contains drops zones. The relation test existed and could not
construct the input that broke the relation -- proven by leaving the bug in and
deleting the new generator arm: 393,226 addresses swept, all green. The
generator was the defect.

The nftables chain hooked `output` only, which never sees forwarded traffic --
and provision.go's own NetworkMode assertion requires exactly the bridge
arrangement that forwards. So the ruleset constrained the canary and nothing
else. It hooks forward as well now, with the identical rule list, parsed back by
a reader separate from the writer.

Also: the 4-in-6 spelling ::ffff:169.254.169.254/128 walked past a metadata
refusal whose doc comment and six-row table both claimed to stop it --
canonicalize before matching, the lesson gate 8 already encodes, reusing the
kernel's own Unmap rather than a second implementation that can disagree.

CI now has a windows-latest lane, because the sole guard on "containment can
never report success where nothing was checked" ran in zero lanes: all five jobs
were ubuntu-latest, where that branch cannot fire.

Evidence: gofmt, vet clean; go test -count=1 ./... green; TestSplit passes with
a real dependency on both sides; no t.Skip added.
… allow

Twenty-four thousand lines across five inventory packets, plus the end-to-end
proof that became possible when gate 11 stopped being an unimplemented gate.

The kernel had never admitted anything

Every proof in this repository until now was a proof that the kernel REFUSES.
Twenty-one gates, five fix rounds, twenty adversarial critics -- all of it
demonstrating rejection. That was not thoroughness, it was a gap:
Gate11RobotsDeny sat in admissionChain with nothing registered, and a missing
gate is a refusal, so Adjudicate could not return an allow and no Authorization
could be minted outside package authz at all.

D.14 saw that and planted a tripwire rather than working around it. Ruling 6
made gate 11 a scope narrowing -- which is what its own row in the gate table
describes, "removes paths from scope; permissive adds nothing" is a
transformation, not a predicate -- and the tripwire fired on exactly the
condition it was written for. It then named its own replacement: five tests, in
order, then delete yourself.

All five are written. The one that matters is the positive control: a gate stack
only ever shown to refuse could be refusing for the wrong reason. It asserts ONE
AUDIT ROW PER GATE against GovernorGateOrder() by position and identity -- and
that assertion earned itself immediately, because a mutation writing only the
last gate's row produced [gate14] against a want of six, which a len(rows)>0
check would have passed.

Cross-target token reuse had never been exercised either, because no token
existed to reuse. The sweep found that the pre-existing
TestTargetSpecRefusesEveryUnauthorizedRoute stayed GREEN while a real
Authorization was accepted for the wrong target: it only ever tried a ZERO
Authorization, so it could not see the damage.

Gate 3 was red for three packets, and that is the guard working

encoding/xml was not on the egress allowlist, so it failed by default. D.19
named it, D.20 confirmed it by measurement, D.22 confirmed it a third time --
each by moving its own files out, re-running the gate, and comparing the finding
count. None of them reached outside its write scope to fix it.

That is the allowlist doing its job: a new import cannot enter the dynamic tier
without someone deciding it is inert. The decision is recorded where it was
made. Go's XML decoder resolves no external entities and fetches no DTDs -- it
has no XXE, which is the only reason D.19 may point it at a spec file harvested
from an untrusted repository. The one route to a socket is Decoder.CharsetReader,
which is caller-supplied; verified that nothing under internal/ sets it.

Where the plan was overridden, and why

D.19 makes Tier 1 routes CANDIDATES. plan/50-dast.md:632-635 says every Tier 1
route is `status: confirmed`. That is the direction which moves an endpoint into
endpoint_coverage's NUMERATOR, on the strength of a file the repository wrote.
Confirmation is D.22's job and requires an observation Anvil made.

D.20 did NOT vendor go-apispec. The maintenance spike the plan asked for was run
against the network and measured: main last moved 2026-06-24, sixty days stale,
while the repo's pushed_at reads 2026-08-21 because Dependabot branches move and
main does not. Only the LICENCE body, NOTICE and a re-runnable PIN.md are in
third_party/. The type-checked path is a seam that returns a loud
ErrNoTypeCheckedExtractor rather than an empty route list.

D.21 names what it is blind to in SupportMatrix().BlindTo, and the tests require
those lists to be non-empty. A static extractor that silently finds nothing for
a language it cannot really read does not merely miss endpoints -- it shrinks
the DENOMINATOR, so coverage goes UP. The metric improves because the tool got
worse, and nobody reading the number can see it.

D.22 keys the union on (method, canonical path), so forty GraphQL root fields on
one POST /graphql collapse to one row. That DEFLATES coverage, which is the
direction nobody investigates, so it is reported rather than assumed:
OperationCount() carries the second number and every multi-operation address
leaves a MergeNote.

Open, and named here so it is not lost in a report: internal/record carries no
spec-harvest field. Ruling 7 told D.19 to reconcile its input shape against the
record schema; it did, and found nothing to consume. D.19 is therefore PARTIAL.

Evidence: gofmt, vet clean; go test -count=1 ./... green across 30 packages;
-race clean from PowerShell; no t.Skip added.
… slots

Twenty-six of thirty-one packets. Two record-contract gaps closed, four DAST
packets landed, and two critics that found what the packets could not see about
themselves.

The record can now tell a specless repo from an unwired handoff

D.19 was PARTIAL because ruling 7 told it to reconcile against internal/record
and there was nothing there to consume -- no SpecFile, no artifacts list, no
run-level slot. That was the correct outcome of a reconciliation, not a failure
of it.

anvil/specHarvest closes it, and the test that proves it is the one worth
reading: TestASpeclessRepositoryAndAnUnwiredHandoffAreDifferentRecords. With the
field removed, both cases serialise to byte-identical records --
{"anvil/half":"sast","anvil/status":"running","anvil/sealedAt":null} -- and both
land in the denominator of anvil/dastCoverage while only one describes the
target.

OmittedFileCount is a POINTER for a reason that generalises: a plain int
defaults to 0, and 0 ASSERTS that the file list is complete. Forgetting to set
it would have been a silent claim. As a pointer, forgetting fails closed. The
same reasoning makes a nil SpecHarvest mean "the record makes no statement"
rather than harvest_skipped -- unknown and "this repo ships no specs" are
different facts.

The wire schema was checked by RUNNING it, not by reading it: fixtures
marshalled and validated against schemas/anvil-record-v1.schema.json with
jsonschema as draft 2020-12, twelve negative controls all refused. That found a
real Go/schema disagreement -- a skipped harvest marshals files as null, which
the Go validator accepted and the first schema draft refused.

What the critics found, and both are the same lesson in new places

CANONICALIZE BEFORE MATCHING, third occurrence. Gate 8 encodes it; containment
learned it when ::ffff:169.254.169.254/128 walked past a metadata refusal; and
now the crawler fetches /%2e%2e/admin while correctly refusing /admin. Every
browser resolves those to the same path, so gate 11's robots narrowing is being
matched against a spelling the spider will never request. Measured end to end
with a real kernel, on both the link route and the redirect route.

A GUARD POINTED AT THE WRONG TYPE. D.27's raw-body type closure is real work --
Finding genuinely has nowhere to put a body, and there is a test proving it
against a violating fixture. But the body leaves through the ERROR return:
ConfirmFinding wraps the Reprober's error with %w, unbounded, and an ordinary
Reprober that quotes an unparseable response puts 4301 verbatim body bytes onto
Ledger.Refusals()[0].Err. The walker that would have caught this is in the same
file and never points at Refusal.

And the one that matters most, because it is the failure this whole project is
built to avoid: a vulnerable target that rate-limits during the confirmation
pass produces rejected / did_not_reproduce_on_any_attempt / confidence 0.000 --
INDISTINGUISHABLE FROM A PHANTOM. AssertNotSilentlyClean ignores RejectedCount,
so that ledger derives completed_clean. A real vulnerability, reported clean,
because the target defended itself.

Credential redaction is byte-exact over three spellings, so an HTML-entity or
JSON-escaped credential is stored verbatim and every assertion reports clean --
demonstrated with D.24's OWN fixture password, and AuthArtifactStorageState is
JSON by definition, so the escaped case is the ordinary one.

All are open and being fixed next; named here so they are not lost in a report.

D.27's report was lost to a structured-output cap, the third packet to lose one.
Its code landed and D.28 reviewed it regardless, which is why the findings above
exist at all.

Evidence: gofmt, vet clean; go test -count=1 ./... green across 31 packages; no
t.Skip added.
…arrowing

Gate 11 narrows the scope by removing paths a robots.txt disallows. The crawler
was matching that narrowing against the spelling it was HANDED, not the spelling
it would REQUEST -- so a target-supplied link or 302 Location of "/%2e%2e/admin"
was fetched while the plain "/admin" was correctly refused. Every browser
resolves those to the same path. Measured end to end by D.25 with a real
Governor, a real GateAudit and a real robots-narrowed scope, on both the link
route and the redirect route.

kernelAcceptsPath blocked only the literal ".." -- the one spelling a browser
would have rejected anyway.

THIS IS THE THIRD TIME THIS CODEBASE HAS LOST TO A NON-CANONICAL SPELLING. Gate
8 encodes the rule. Containment lost to ::ffff:169.254.169.254/128 walking past
a metadata refusal whose doc comment and six-row table both claimed to stop it.
Now the crawler. Three packages, three encodings, one lesson: canonicalize
BEFORE matching, and reuse the kernel's canonicalization rather than writing
another one that can disagree.

dotSegmentsSpelledPlainly rewrites every encoded spelling of "." and ".." before
the scope check, and the fix goes past what the critic demonstrated: encoded
separators ("..%2f", "%2e%2e%2fadmin", "x%5C..%5Cadmin") and doubly-encoded
segments ("%252e%252e"), the latter with the reasoning written down -- a browser
decodes once, so "%252e%252e" IS the ordinary segment "%2e%2e", and an origin
that decodes twice reads it as "..".

Four tests, and note what the last two are for: it is not enough that the
non-canonical spellings are dropped. TestEachNonCanonicalSpellingIsClassified
RatherThanMerelyDropped asserts each one is accounted for rather than silently
discarded, and TestCanonicalizationPreservesCoverageAndDoesNotDoubleCount
asserts the rewrite does not turn one endpoint into two in the union -- which
would inflate the denominator of endpoint_coverage and make coverage look worse
for a fix that made the crawler safer.

internal/dast/record is unchanged in this commit. A concurrent worker was
mid-way through replacing EvidenceRef.spanTruncatedFrom with spanOverBroadBytes
-- correctly, because truncating an over-broad match still inlines a raw body
prefix, and refusing to produce a span at all is the right answer -- when the
session cap stopped it. The change was coherent but half-applied and its
red-verification never ran, so it was reverted rather than hand-completed. An
unverified security fix that compiles is worse than an absent one, because it
looks done.

Evidence: gofmt, vet clean; go test -count=1 ./... green across 31 packages.
a live vulnerability as clean

Two criticals, four highs, and a lesson that outlasts all of them.

The failure this project exists to avoid

A genuinely vulnerable target whose rate limiter tripped during the confirmation
pass produced outcome=rejected, reason=did_not_reproduce_on_any_attempt,
confidence 0.000 -- byte-identical to what a phantom produces. Observation.Status
was captured, stamped onto the Finding, and read by nothing; decide() never saw
it. AssertNotSilentlyClean ignored RejectedCount, so the ledger derived
completed_clean. A real vulnerability, reported clean, because the target
defended itself.

A defended attempt is now indecisive rather than disproven, and
AssertNotSilentlyClean sees rejections.

The raw body was leaving through the error return

Finding's type closure is real -- it genuinely has nowhere to put a body, proven
against a violating fixture. But ConfirmFinding wrapped the Reprober's error
with %w, unbounded, and Refusal.Err is an exported error interface: a Reprober
quoting an unparseable response put 4301 verbatim body bytes onto
Ledger.Refusals()[0].Err. The Reprober lives on the far side of the gate-3
boundary, so this could never have been fixed by asking implementers to behave.

The walker that would have caught it was in the same test file, pointed at three
types that cannot carry the damage. Every exit is now verified closed: both
returns, all fmt verbs, Unwrap, Refusal.Detail, Ledger.String(), and a
reflective walk of every reachable string.

The credential control stopped reading bytes

A byte-exact sweep over three spellings stored an HTML-entity or JSON-escaped
credential verbatim while every assertion reported clean -- demonstrated with
the file's OWN fixture password, and AuthArtifactStorageState is JSON by
definition, so the escaped case is the ordinary one.

The primary control is now PROVENANCE, not content: an artifact belonging to a
step that types a secret is refused for every kind, without looking at the
bytes, so no spelling defeats it. The sweep is demoted to a backstop and says
so -- AssertNoCredentialWasFound's nil now means "nothing was FOUND", not "there
is nothing there". The fixture password was rewritten to BREAK the sweep instead
of suiting it, which is why the encodings were found at all.

An obligation no implementation can keep

Headless-browser sub-requests could not reach the kernel: CrawlRequest expresses
one method+path and CrawlPage has no field to report a fetch or XHR, so they hit
no gate, spent no gate-14 token, wrote no gate-21 row. The worker declined to
add a reporting field, and the reasoning is right: admitting a request already
issued is not admission, it is a post-hoc audit row, and whether that is
acceptable is a kernel ruling this file may not make. A rendering spider is
refused instead, before FetchPage, across six discipline spellings.

WHAT THE VERIFIER FOUND, AND IT IS ONE PATTERN

Each fix replaced a losing enumeration with a NEW enumeration one layer down --
four defence statuses, an eight-byte entity-name bound, a seventeen-entry
720-byte benign corpus -- and each loses to the neighbouring input. 403 alone
reproduces the completed_clean outcome. Zero-padded numeric character
references walk past the entity decoder. A pattern requiring 721 bytes beats the
benign corpus and confirms an ordinary product page at confidence 1.000.

"A denylist loses" was already a standing order. It was obeyed at the top layer
and reintroduced underneath. Those three are open and being closed next.

Evidence: gofmt, vet clean; go test -count=1 ./... green across 31 packages;
all 111 Test identifiers referenced from non-test files resolve; no t.Skip.
… means

Ruling 8 said: when a guard must decide "is this X", enumerate what X IS, and
when the input domain is unbounded, generate rather than validate against a
fixed corpus. Three fixes applied it. A verifier then found the enumeration had
recurred a FOURTH time -- inside the fix written to end it.

What the inversion bought, and it is real

Defence detection was a four-entry status allowlist {429,502,503,504}, and 403
alone reproduced the defect that reported a live vulnerability as clean. The
question was wrong: "which statuses mean the target defended itself" is
unbounded and vendor-specific.

The question is now "did the target answer AS THE APPLICATION", with ten
statuses that establish it and exactly one code path returning true. Everything
else -- every status nobody has shipped, transport failure, Issued==false,
status 0 -- falls to indecisive by CONTROL FLOW rather than by list membership.
A -1..600 sweep through ConfirmAll produces zero rejections outside the
allowlist, and a mutation adding 403 to it fails with the reason written out.

The cost is stated rather than hidden: a 403-answering misconfiguration
candidate is now unconfirmed rather than rejected, and a 200 block page remains
indistinguishable from an application response with no DefenceSignature wired.

And one piece of work that ends a family instead of outrunning it: over-broad
LENGTH patterns are closed by ARITHMETIC. Go's regexp caps a repeat's expansion
at 1000 and checks the product, a repeat costs at least one pattern byte, and
MaxPatternBytes is 1024 -- so no compilable signature can demand more than
1,024,000 bytes, which is below authz.CodedMaxBodyBytes. Both bounds are
asserted. That is what a closed class looks like: no ceiling to raise, no
neighbouring input.

The lesson, which sharpens the ruling rather than repeating it

A GENERATOR WITH A FIXED SEED AND A HAND-WRITTEN ALPHABET IS A CORPUS.

The benign corpus was not generated out of the enumeration; the enumeration
moved into the generator. Five hand-written vocabulary slices and a fixed seed
produce 109 fixed strings, so the attacker's budget is unchanged in kind and
merely larger in size: one over-broad pattern anchored outside 13 tag names, 33
words, 14 JSON keys and 45 punctuation tokens. `(?s)<h1[\s\S]{0,400}` -- the
fix's own motivating example, one tag over -- passes, and an ordinary 404 page
carrying the word "expired" confirms a SQL-injection finding at confidence
1.000.

"Generate, do not enumerate" is satisfied by the sampling MECHANISM only when
the space being sampled is not itself a written-down list. The two places the
ruling held are the two where nothing needs keeping current: an allowlist with
no deny side, and a bound proved by arithmetic.

Also closed this round: IndecisiveRejectionCount was DELETED rather than kept as
an unreachable claim (a 52-case sweep reached it zero times), and replaced by an
assertion on a path that can fire. CarriedSession is exported but cannot widen
anything -- the decision requires an unexported seal bound to the labelling
session, and a struct literal from outside the package counts as not-carried and
is named unattested.

Open and being closed next: the generator's alphabet, a greedy one-way read of
semicolon-less character references, a meta-guard covering 21 of 26 exported
types, and 404/500 sitting in the application allowlist where a CDN answers them.

Evidence: gofmt, vet clean; go test -count=1 ./... green across 31 packages.
The over-broadness decision no longer runs the pattern against bodies. It parses
it and walks the tree, and the class is closed by construction rather than by a
corpus with a size.

Three rules, and the first two contain no number at all:

R1 FOOTING -- every match path must require at least one byte of literal
content. The threshold is zero, so there is no ceiling to raise.

R2 NO OPEN POSITION -- every consumable rune must be spelled by the pattern or
drawn from a class confined to printable ASCII. THE CHARSET IS NOT A CHOICE: it
is extractSpan's own, asserted equal over all 256 byte values by running the
extractor. The rule is "a signature may not match through bytes its own evidence
extractor drops". That kills [\s\S], ., [^\n], \s and therefore the whole
bounded-prefix family whatever tag it names.

R3 QUOTATION -- positions that can cross a token boundary may not outnumber the
pattern's spelled bytes. One relation, 1:1 against the pattern's own footing.

shapeOf's default arm returns open, so an unknown regexp/syntax operator is a
refused signature -- an allowlist with no deny side, the same shape as the
status inversion.

Measured: 0 of 20 bounded-prefix HTML anchors accepted, where 17 passed before.
TestTheControlDecidesOnTheStructureAndNotOnASampleOfBodies runs NewSignature
with the corpus EMPTIED and requires the verdicts not to move -- which is the
claim, stated as a test.

The attack rerun found its own defect one class to the left

(?s)<h1[0-9A-Za-z]{0,400} was ACCEPTED and inlined 403 verbatim body bytes,
because a class of alphanumerics is not content-bearing and R3 deliberately lets
it repeat -- that carve-out is what allows AKIA[0-9A-Z]{16} to compile at all.
So the inlining was closed a second time, independently: extractSpan now inlines
a match only when L - spelled <= spelled, which re-applies the inequality to the
ACTUAL match and knows nothing about classes. It therefore holds for patterns R3
never examines. 403 bytes became 0.

The corpus is retained and DEMOTED IN WRITING to a backstop, with its vocabulary
named as a budget -- a backstop's budget, not a control's. Describing a backstop
as the control is how the previous four rounds happened.

Gate 3 caught the new import, which is the allowlist working

regexp/syntax is not on inertImports, so the build went red the moment ruling 9
landed. It is added here with the review it exists to force done in place: it is
the parser behind regexp -- Parse, the Regexp tree, the Op constants -- and it
compiles nothing, executes nothing and opens nothing.

Ruling 10 did not close, and the reason generalises

The union over ambiguous readings was taken at the point of MATCHING, against a
form that the decoder had already produced by picking the greedy reading. So the
union exists at encoding depth 0 and nowhere else: 0 of 450 semicolon-less
re-spellings missed flat, 135 of 450 missed under one url.QueryEscape -- exactly
the genuinely ambiguous ones -- with the first miss being the same fixture string
the previous round quoted as closed.

A union taken over the output of a decoder that already chose is not a union.
Ambiguity has to branch where it ARISES and the branches have to survive to the
comparison. Same root cause, second face: three decoders compose within one pass
and only the composition is retained.

Worth recording: asked directly whether its fix still had a budget, the
inventory worker answered "YES, AND I AM NAMING IT RATHER THAN CLAIMING
OTHERWISE" about a work-bytes ceiling, and wrote where it bites. That is the
answer the question was added to elicit.

Evidence: gofmt, vet clean; go test -count=1 ./... green across 31 packages,
verified AFTER adding regexp/syntax to the egress allowlist.
The credential sweep no longer runs four fixed pipelines composing
decodeEntities(decodeBackslash(decodePercent(x))) and keeping only the
composition. One step is ONE decoder, every reading a step produces stays in the
candidate set, and the next step maps over all of them. Composition of passes is
composition of sets.

That closes the fifth recurrence at its root. Previously the union existed at
encoding depth 0 and nowhere else -- 0 of 450 semicolon-less re-spellings missed
flat, 135 of 450 missed under one url.QueryEscape. It also closes the second
face nobody had connected to it: three decoders composing within one pass
destroyed a secret whose own bytes contain an escape before the later decoder
saw it, losing six of ten realistic secret shapes.

Measured by an independent verifier, not by the author: 363 exhaustive orderings
at depths 1-5, 192 interleavings at depths 1-8, and all ten escape-shaped
secrets under three layers of each of the three encoders. ZERO MISSES. No
decoder grows a form -- 0 bytes of growth over 60,000 adversarial strings.

The half the matcher structurally cannot do

A non-greedy reading whose bytes must be DECODED AGAIN is invisible to any
union taken at match time: "&#3741" read at index 1 is '%' then "41", and one
further percent step makes it 'A'. The matcher matches, it does not decode. Only
a decoder emitting that reading as its own candidate reaches it. That is the
concrete reason ruling 11 is about WHERE the branch happens.

The list that matters more than the findings

An independent verifier was asked to state which decisions now close by
construction or arithmetic with no budget, and the list came back longer than
its findings:

  the decoder composition (a real BFS over the DAG);
  referenceReadingsAt's reading count, bounded at eight BY ARITHMETIC not a cap;
  unionRunes normalising to at most 48 pairs whatever the pattern does;
  shapeOf's default arm sending an unknown operator to open, which R2 refuses;
  IsApplicationResponseStatus false for every unnamed status including 0;
  extractSpan property 1b, which contains the inlining even of the pattern R3
    still lets through -- a 383-byte match inlines 0 bytes;
  go list -deps ./cmd/anvil reaching no internal/dast package.

Five disclosed residuals were re-measured independently and are accurate,
including a depth curve published as 60+/50/24/11 and measured at 70/50/24/11.

Two disclosures were NOT accurate and are open: the '+' branch has the same
same-depth cross-product residual as the reading index and nobody named it, and
codedSweepWorkBytes is disclosed as a bound on bytes SCANNED while the candidate
set is also RETAINED -- 183 MB live for one 4 MiB artifact, an axis the doc does
not mention.

Also open: R3's capture-group evasion is rebuilt one layer down. The union is
taken over CLASSES, and patternShape.consumes excludes spelled runes by design,
so spelling the punctuation as captured single-rune LITERALS restores it whole.
And closureViolations dedups by reflect.Type, so the SECOND []byte anywhere in a
closure is never reported.

Both are the same two shapes that produced the last two rounds: a capture group
blocking a merge, and a dedup key hiding a second instance.

Worth recording against the standing order it answers: asked whether its fix
still had a budget, the worker answered "YES, TWO, AND I AM NAMING BOTH RATHER
THAN CLAIMING OTHERWISE", published a measured curve instead of a sentence, and
wrote a test that PINS A WEAKNESS OPEN -- asserting a miss, with a failure
message instructing the next author to delete the test and the disclosure
together if the residual ever closes.

Evidence: gofmt, vet clean; go test -count=1 ./... green across 31 packages;
-race clean from PowerShell.
Two guards were asking their question of a convenient representation instead of
the thing itself. Both are re-keyed, and one of them is now closed for good.

SHAPE B, CLOSED. closureViolations marked seen[reflect.Type] on entry, so the
SECOND []byte anywhere in a closure was never reported -- adding Raw []byte
AFTER Body left the package green. The subject is a ROUTE, not a type, and the
dedup key merged two of them.

The dedup is gone entirely, and dropping it is safe by GO'S OWN TYPE RULES
rather than by luck: a struct may not contain itself directly or through arrays,
so the part of the closure the walk descends into is finite and acyclic by
construction, and every kind that could close a cycle -- pointer, slice, map,
interface, func, chan -- is REPORTED and returned from rather than followed.
That argument is asserted rather than claimed: a self-referential fixture proves
termination by the assertion being reached at all.

Verified by mutating the PRODUCTION types rather than fixtures -- zzRaw []byte
inserted as the first, second and third field of Finding, EvidenceRef and
Observation in turn: red 9 of 9, including the third-slot case that is the exact
reported defect. Also caught: two []byte on different types in one closure, one
behind a pointer, one behind a slice of structs, two routes through
structurally identical anonymous structs, a named byte-slice type, and a
fixed-size byte array.

SHAPE A, CLOSED FOR EVERY SPELLING THAT GOES THROUGH AN ALTERNATION. A literal
at an alternation position is an alphabet of one, and it is in the union now.
The measured evasion -- 29 captured single-rune literals matching 252 of 321
bytes at confidence 1.000 -- is refused, as is the all-literal form with no
class anywhere, which the worker constructed itself as an extension of the
attack rather than waiting to be told.

All 17 positive controls re-verified with their shape numbers read out. Two
rationales in the test file were REWRITTEN rather than left standing, because
the change made them false: they said a spelled rune is not in the union, and
now it is. The dotted-quad pattern still compiles because digits and a dot carry
no letter, which is a different reason than the one previously written down.

The price is disclosed, not hidden: a repeat of an alternation whose union
crosses letters into punctuation is now refused even when every branch is
spelled. And the benchmark was RE-MEASURED rather than left stale -- 972ns to
2292ns/op, with the regression named.

Still open, and it is the same move one level out: the promotion asks
re.Op == OpAlternate, but an alternation is not the only spelling of an
undecided position. x?y? denotes (?:|x|y|xy). Three spellings of one language:
(?:[a,]?)* refused, (?:(?:a|,)?)* refused, (?:a?,?)* ACCEPTED -- 225 bytes at
full alphabet, matching 369 of 369 bytes of an ordinary page at confidence
1.000.

The verifier's no-budget list now runs to seventeen entries against two
findings, fourteen of them verified in that round rather than carried forward.
That ratio, not the FAIL, is the state of this component.

Evidence: gofmt, vet clean; go test -count=1 ./... green across 31 packages.
shapeOf no longer asks which operator a node is. patternShape carries `decided`
(every consumable position in the denoted language is pinned to one spelled
rune) and `positions`, both computed bottom-up from an ALLOWLIST of shapes that
produce a decided position. The zero value is false, so classes, alternations,
any-char and the default arm are undecided by inheritance rather than by
someone remembering to list them.

Verified by an independent verifier, not by the author: 40 mechanically
generated spellings of one language, every one refused; 40 spellings each of
four safe languages, every one accepted; 200 patterns, zero splits. The
invariant `decided` rests on was brute-forced over 1,439 generated patterns,
429 of them decided, zero counterexamples. All 20 positive controls compile
with the exact spelled counts claimed.

A second defect surfaced while fixing the first and was closed with it: the
promotion has to be folded into the repeat UNIT before the unit's quotation is
read, because a repeat is the only place quotation is taken over a union.
Without that, Z(?:a,?)* was accepted -- the unit is decided on its own, and the
promotion landing on the star node was read by nobody, because the star's parent
is a concatenation.

classShape's one-rune arm was DELETED rather than qualified. A parser probe
showed regexp/syntax rewrites every one-rune class into a literal, so the arm
was unreachable and its stated justification was never demonstrable. The test
prose claiming otherwise was corrected.

The eighth recurrence, and why it changes the approach

quotationOverUnion is called at OpAlternate and in all three repeat arms, and
never on an OpConcat. So a concatenation of individually-narrow but UNBOUNDED
repeats reports quoted=0 while `declared` is already unbounded and `consumes` is
already content-bearing: THE VALUE R3 REFUSES IS COMPUTED, SITS IN THE STRUCT,
AND THE ARM RETURNS WITHOUT READING IT.

One language, two verdicts: `X [a-z]*` accepted at quoted=0, `X(?: [a-z]*){1}`
refused. At 125 concatenated runs it matched 12,500,141 bytes of an ordinary
page. The residual disclosed as bounded at 255 bytes has no byte ceiling at all,
because MaxPatternBytes bounds the NUMBER of concatenated elements and not the
bytes each one consumes.

And the test the previous ruling commissioned cannot fail on it: every spelling
its generator produces is wrapped in an outer repeat, so the property is closed
over one shape rather than over the grammar.

The verifier also established that the naive repair is unavailable -- applying
quotationOverUnion to a concatenation's own shape refuses legitimate patterns.

Eight rounds have tried to decide statically what a pattern can match. That is
the wrong question, and the next commit changes it rather than answering it
again. The no-budget list stands at nineteen entries, seventeen verified in that
round.

Evidence: gofmt, vet clean; go test -count=1 ./... green across 31 packages;
-race clean.
An independent verifier was asked whether the confirmation guarantee still
depends on any claim about the regex grammar, and answered: it does not.

At confirmation time the gate reads two integers -- the length of the match that
actually happened and the pattern's literal footing -- and compares them once.
No parse tree, no alphabet, no corpus, no sample. The verifier could not
construct a confirmation from a match that swallows an ordinary page by any
route: not the 125-run concatenation (12,500,016 bytes, unconfirmed), not the
minimal witness, not at the exact boundary (2s confirms, 2s+1 does not), not by
scaling spelled to its measured ceiling of 500,000, not through a mixed
multi-attempt run, and not through a second code path -- there is exactly one
assignment of outcome in the package and exactly one Reason that maps to
confirmed.

Nine rounds tried to decide statically what a pattern can match. The invariant
was always about a MATCH -- R3's own message says the span is the response
rather than evidence about it -- and extractSpan had been computing exactly that
comparison all along, wired to whether to inline a span rather than to whether
the finding was confirmed. The right check existed at the wrong decision.

The static layer was NOT tuned to make this work: the diff through shapeWalk,
refuseOverBroadPattern, classShape, unionRunes and unboundedIfDeclaring is
comment-only, and both disclosed acceptance cases still accept. R1/R2/R3 remain
as a cheap early refusal whose doc now says plainly that a signature which
passed NewSignature HAS NOT BEEN PROVEN NARROW.

The outcome choice was argued rather than picked: unconfirmed, not rejected,
because AssertNotSilentlyClean deliberately does not count rejections -- a
ledger of nothing but rejections is an earned clean -- so filing "the signature
cannot see" as "the target is fine" would be the silent clean arriving through a
new door. Demonstrated verbatim: the 12.5 MB attack yields unconfirmed and
AssertNotSilentlyClean fires.

AND IT OVERSHOT, IN THE DIRECTION THE BRIEF NAMED

spelled counts LITERAL footing, but a credential oracle's evidence is by
construction a character class -- so the arithmetic treats the thing the oracle
exists to find as the response quoting itself.

AKIA[0-9A-Z]{16} has four spelled bytes against a twenty-byte match. A
REPRODUCED AWS KEY EXPOSURE NOW LANDS UNCONFIRMED and never reaches dast_status
findings. The dotted-quad leak and the GitHub, Stripe and PHPSESSID oracles fail
identically: three of the fifteen patterns this package itself calls every
signature the suite and the plan actually use.

That is a real vulnerability reported as a signature defect. It is high rather
than critical only because AssertNotSilentlyClean still fires, so it is not a
silent clean. It is open and fixed next.

Also open: the demotion is partial. NewSignature's own doc comment -- the one
surface go doc renders to an external reader -- still calls the incomplete
static analysis THE CONTROL and argues it from "samples nothing", while the
withdrawal sits four lines below in the function body where go doc does not
show it.

Evidence: gofmt, vet clean; go test -count=1 ./... green across 31 packages.
The over-broadness sequence is behaviourally complete. An independent verifier
tested both halves and both hold.

NOTHING OVER-BROAD CONFIRMS. The 125-run concatenation at 12,500,141 bytes
against 141 spelled, the 302-byte minimal witness, mixed multi-attempt runs in
all four orderings, the second code path, the 5001-byte fixed-width unit, the
exact span boundary, and the scaled-footing attack -- every one still fails to
confirm. The new floor was attacked directly and held: the boundary is exactly
at MaxUnspelledBytes end to end on a fixture the package does not own (255 and
256 confirm, 257 does not), and a 4,200-case sweep produced no inlined span
carrying more than 256 unspelled bytes.

EVERY REAL ORACLE CONFIRMS AGAIN. All 18 the package names, including the one
the previous round broke: AKIA[0-9A-Z]{16}, spelled=4, matchLen=20, CONFIRMED,
span "AKIA1234567890ABCDEF". Plus 16 of 22 further oracles the verifier
constructed -- private key header, JSESSIONID, ASP.NET and Django fingerprints,
SQLSTATE, /etc/passwd, XXE, CORS wildcard.

The floor is derived, not chosen, and the derivation is checkable in one
sentence: an inlined span exists only when its match fits in MaxSpanBytes, and
the quotation relation caps unspelled bytes at the spelled bytes, so
2q <= L <= MaxSpanBytes gives q <= MaxSpanBytes/2 = 256 -- which is exactly the
ceiling the R-rules header has published since it was written.

Worth recording how the alternative was rejected: a floor at MaxSpanBytes itself
was IMPLEMENTED AND RUN, and turned the minimal witness green -- 302 bytes
against 2 spelled, confirmed. MaxSpanBytes bounds the whole span, Anvil's
literals plus the response; the invariant is about the response half alone. And
MaxSpanBytes/8 would also have passed every acceptance case: the worker rejected
it in writing because /8 has no provenance, and kept the larger floor precisely
because it is derived rather than fitted.

WHAT STILL FAILS, AND IT IS DOCUMENTATION AND ONE PIN

The floor is unpinned. Replacing MaxSpanBytes/2 with the bare literal 299 leaves
the whole package green, window [255, 300], because the boundary test computes
its edge FROM MaxUnspelledBytes. That is "a pin computing its expectation from
the constant it pins" -- a standing sweep item -- reproduced inside the test
written to close it.

Two sentences state withdrawn arithmetic as current, one of them fifteen lines
below the paragraph that withdrew its twin, and both are measurably false at the
new boundary. That is the measurably-false-justification class, reopened in the
same comment block a previous round was ordered to correct.

And the refused-oracle residual is a FAMILY rather than the single case
disclosed: every credential oracle whose class is base64url -- containing an
underscore or a hyphen -- is refused upstream at R3.

Evidence: gofmt, vet clean; go test -count=1 ./... green across 31 packages.
The confirmation gate's behaviour is settled. This round was claims and pins,
and the production diff proves it: stripped of comments it is exactly one line
pair -- a declaration byte-identical with a changed trailing comment. The
test-file diff deletes one line, and it is the self-referential edge assignment
that caused the finding.

THE PIN THAT WAS NOT A PIN. MaxUnspelledBytes = MaxSpanBytes/2 is a correct
derivation that enforced nothing: substituting the bare literal 299 left the
whole package green, anywhere in [255, 300]. The cause sat inside the test
written to close the previous round -- it took its edge FROM the constant it was
pinning, which is the standing-sweep item reproduced inside the pin.

Two pins now, neither computed from the other, and their independence is proven
in both directions: the literal pin catches a bare substitution and is blind to
a changed span bound; the relation pin catches a changed span bound and is blind
to an equal literal. Six mutations -- 299, 255, 257, 64, MaxSpanBytes with the
derivation kept, MaxSpanBytes with a literal floor left behind -- each turned
the right test red with a message naming the integers.

TWO FALSE SENTENCES DELETED RATHER THAN QUALIFIED. Both asserted the pre-floor
arithmetic, and both were measured false at the witnesses that disprove them:
q=256 against L=257 where the span is still inlined, and s=1 against L=201 where
the finding confirms at confidence 1.000 with a 201-byte span. What survives is
the half that holds unconditionally -- q <= 256 for every inlined span, carried
by matchQuotesMoreThanItSpells and not by R3, which refuses early and does not
make the inequality true.

THE REFUSED-ORACLE FAMILY IS NAMED AND MEASURED. Every credential oracle whose
class admits a letter and a non-alphanumeric is refused by R3 upstream of the
floor -- base64url's underscore and hyphen, standard base64's plus and slash, a
JWT's dot. Six measured members with their quoted/spelled pairs: JWT pairs,
Google OAuth2, OpenAI-style keys, GitLab PATs, Slack bot tokens, raw bearer
headers. The remedy is measured too, and so is its cost: dropping the punctuation
from the class makes the oracle compile and MISSES 48% of real tokens, because
the repeat's minimum must be met by alphanumerics alone. Two members that fail a
DIFFERENT way are named and excluded rather than folded in. The disclosure is a
test, and widening R3 tenfold turns it red, so it is not vacuous.

Still open, and it is one sentence in the worst possible place: ExtractedSpan's
GODOC -- the description a consumer reads without opening the file -- still
carries the pre-floor arithmetic. The mandated sweep corrected the field comment
twelve lines above it and the accessor six lines below it, and stepped over the
exported method between them.

Evidence: gofmt, vet clean; go test -count=1 ./... green across 31 packages.
Every attack re-measured by an independent verifier and not one verdict moved.
… spot

Three rounds were told to sweep this file for false claims and each corrected
the sentence it was pointed at and left another behind. A human sweep of prose
is a denylist of sentences somebody remembered to read, so this round built a
mechanical check instead: an arithmetic claim must CITE the identifier that
enforces it and the test that measures it, and an uncited claim fails the build.

BEHAVIOUR PROVED UNMOVED TWICE. The comment-strip on the production diff is
empty, and an AST comparison against 1cf871e -- parse without comments, reprint,
diff -- returns identical, which also rules out a moved string literal or
constant that a strip cannot see. Every attack re-measured to the same verdict
and all 18 oracles confirmed at 1.000 with their hard-coded pairs.

THE EXPORTED GODOC IS NOW TRUE, AND SAYS MORE THAN IT DID. ExtractedSpan's
description told a consumer the span is empty in TWO situations and that
SpanOverBroadBytes tells them apart. There are FOUR, measured -- and the fourth
was found by the worker rather than named in the brief: an all-indecisive run
yields span "", dropped 0, over 0, byte-identical on all three fields to
"matched nothing". So the field does not discriminate, and the godoc now opens
by saying so rather than by claiming it does.

The refuted witness is worth keeping in view: a CONFIRMED finding, oracle fired
3/3, with an empty span and SpanOverBroadBytes 0 -- exactly the pair the old
sentence told a consumer meant the signature matched nothing.

THE FAMILY DISCLOSURE GOT STRICTLY BETTER. What was six base64url examples is
now a swept predicate: R3 refuses a repeat drawing from a class that admits a
letter AND a printable non-alphanumeric, when the ceiling exceeds the literal
footing. Adding each of the 33 printable non-alphanumeric runes in turn to an
ACCEPTED class flips it to refusal, 33 of 33, none by another rule. Six members
outside base64url were added. A verifier tried to falsify the widened predicate
with a 960-member sweep and got zero falsifications.

THE GUARD HAS A BLIND SPOT AND ITS DOC DENIES IT

enforcerShape claims that requiring a case boundary "is what separates a NAME
from an English word without keeping a denylist of English words". It does not.
436 declared identifiers satisfy it and about three dozen are ordinary English
nouns this file's prose uses constantly -- Finding appears in 62 comment
paragraphs, Signature in 27.

A verifier appended a comment to the real file asserting q <= 4096 and that an
inlined span may run to twice the published ceiling -- both false, the real
numbers being 256 and 512 -- carrying no citation the author intended, only the
nouns Evidence and Finding. THE GUARD PASSED IT.

That is the exact shape the brief named as the thing to catch: a guard that
silently covers less than it appears to. It is the defect this file has been
fighting, arriving inside the control built to end it.

Everything else about the guard is sound: it goes red on all three shapes it
claims to catch, its other disclosed holes are real and honestly stated, and all
twenty existing claims pass with a genuine technical enforcer named in each.

Evidence: gofmt, vet clean; go test -count=1 ./... green across 31 packages.
The arithmetic-claim guard now requires a template rather than a name.
enforcerShape and citeToken are deleted. A claim paragraph carries
ENFORCED BY <identifier>, MEASURED BY <TestName>, and three arms run per
citation: the FORM (prose naming the same two identifiers in English no longer
satisfies anything), the NAMES (both resolve against declared identifiers), and
the APTNESS (the cited test must mention the cited enforcer).

The predecessor's attack is dead. The false 4096/1024 paragraph citing only the
nouns Evidence and Finding is now caught, naming the missing form. Typos, wrong
case, invented test names, undeclared enforcers and camel-case unrelated
citations are all caught with the right defect named -- and the `defect` column
that makes those arms separable was added by the worker on its own initiative,
after it found that disabling BOTH resolution arms left the whole table green
because the aptness arm was silently answering for all three.

The ceiling is disclosed and demonstrated rather than asserted: the same false
paragraph with a WELL-FORMED citation passes, and that is written down as the
measured limit.

BEHAVIOUR PROVABLY UNMOVED, third round running. The comment-stripped AST is
identical to 6889aaf with zero diff lines, and a declaration-by-declaration hash
comparison shows 153 production declarations unchanged and exactly 8 test
declarations touched, all of them the guard.

Two findings, and both are the same lesson recurring inside its own fix

THE STREAK REACHED FIVE. The sentence this round was sent to correct still
stands, verbatim, in the SIBLING FILE -- 1,331 lines from the copy that was
fixed at length, in a paragraph about the same deleted counter, citing the same
52-run sweep. Four consecutive rounds have each left exactly one behind; this is
the fifth and what it left behind is the same one. The guard's own header
describes this failure mode and the round reproduced it at file scale.

RULING 16 RECURRED INSIDE RULING 16. The aptness arm claims a citation cannot
name a real rule and an unrelated real test. `mentions` is a whole-word TEXT
SEARCH over the cited test's declaration, so an enforcer spelled like an English
word is mentioned by any prose using the word. 281 of 770 declared identifiers
are reachable this way, demonstrated as a matched pair: cited
ENFORCED BY matchQuotesMoreThanItSpells it is caught; cited ENFORCED BY decide,
MEASURED BY a test in internal/match about advisory ordering, it passes. Only
the spelling of the name differs.

Names are words. The form fixed the citation layer and the aptness layer still
resolves a name against prose.

Worth recording: the round also caught itself shipping unreproducible figures.
The 436/62/27 numbers this ruling quoted do not reproduce against the tree, and
rather than pin new prose figures the worker wrote a subtest that RECOMPUTES the
population every run and asserts a floor. A number in a report that does not
reproduce is the same defect as a false comment.

Of 69 claim sites read against the code -- 20 cited arithmetic claims, 24 rule
references, 25 "the only" claims -- 66 are true, and all 20 arithmetic claims hold.

Evidence: gofmt, vet clean; 31/31 packages green; record green under -race.
The aptness arm asked whether a cited test "mentions" the cited enforcer by TEXT
SEARCH, so an enforcer spelled like an English word was mentioned by any prose
using the word -- 281 of 770 declared identifiers reachable that way. It now
reads the cited test's REFERENCE SET: every ast.Ident under the FuncDecl, less
its own name. Comments contribute no ast.Ident, so prose cannot satisfy a
citation, and the self-certifying citation is dead by construction.

A verifier attacked it with eleven purpose-built tests and could not break it.
It refuses all four prose-and-string forms the withdrawn search accepts -- the
enforcer named only in a string literal, only in an in-body comment, only in the
test's own doc comment, only in a subtest name. Of the seven it accepts, two are
correct and five fall inside holes the doc already discloses in terms. The
verifier then wrote its OWN stricter reference set and re-ran all 31 shipped
citations under it: zero verdicts changed.

THE ONE-HOP ALTERNATIVE WAS BUILT AND MEASURED RATHER THAN ARGUED, and the
measurement went the other way: over 1,435,112 pairs, text admits 35,450, direct
15,866, one hop 41,108. ONE HOP IS THE LOOSEST OF THE THREE -- a callee carries
its whole reference set into every test naming it -- and it turns the aptness
check's own reason-to-exist case green. That result was not the expected one,
which is why it is asserted from a live count rather than reasoned about.

The price was paid in the open: one citation now names the helper the test
actually drives, with the outer function named in the prose beside it.

THE STREAK IS NOT BROKEN, AND THE REASON IS THE LESSON

The round built a withdrawn-phrasing registry and it works for its class: a
synthetic sentence planted in both files at two different wrappings was caught
in both, and half-correcting it left the surviving twin red.

But the class that has recurred six times is a false ATTRIBUTION, not a wording,
and a registry keyed on wording is a denylist. The verifier swept the general
case instead -- every camelCase token in the two files' comments, resolved
against every ast.Ident in the module. Eighteen resolve nowhere; seventeen are
explicitly narrated as dead, historical or synthetic; the eighteenth is live
prose at confirm_gate.go:2828 saying an EvidenceRef "came from newEvidenceRef",
a constructor that has never existed -- 115 lines below the paragraph that
withdraws exactly that attribution.

False twice, in fact: the constructor does not exist, and the method it
describes does not check constructor provenance at all.

The registry behaved exactly as designed and the defect walked past it, because
a paraphrase is invisible to a denylist. That is the same sentence this project
has now written about a status list, an entity bound, a benign corpus, a
generator alphabet, a class union, a type-keyed dedup, an operator tag, a
concatenation arm, and a case-boundary heuristic.

BEHAVIOUR PROVEN UNMOVED, fourth round running: confirm_gate.go at 197
declarations on both sides of an AST comparison against 27139d8, 197 identical,
zero changed, zero added, zero removed. No new imports, no go/types, no t.Skip.

Evidence: gofmt, vet clean; 31/31 packages green; race clean from PowerShell.
confirm_gate.go's package doc told every reader -- and go doc, which renders it
first -- that regexp/syntax is NOT on gate 3's inert allowlist, that
TestGate3NoSocketIsConstructedOutsideTheKernel therefore FAILS on this file "by
design", that the one-line widening "IS NOT MADE HERE", and that it stood
reported to the orchestrator for review.

All four were false. regexp/syntax is at
internal/dast/authz/egress_chokepoint_test.go:186; the gate is green (110 files
scanned, 10 egress sites, all inside the kernel or on the allowlist); and the
indented gate03-refused block it quoted no longer reproduces without deleting
the allowlist line first.

HOW IT BECAME FALSE IS WORTH MORE THAN THE CORRECTION

Every sentence was TRUE WHEN WRITTEN. The packet reported the widening instead
of making it, which is exactly right -- a packet that widens the allowlist in
the same diff that needs it has reviewed itself. The orchestrator then made the
edit, in the SAME COMMIT (e20f64c). Nobody was wrong and the paragraph was false
on arrival, because a sentence about a PENDING DECISION rots the instant the
decision lands.

It then stood through nine commits and six fix rounds, in the doc go doc renders
first, about the control that decides which packages may hold egress capability
-- the one place in this repository where a reader believing a stale doc has a
security consequence. This correction is the orchestrator's, not a worker's: the
error was mine.

It also named encoding/base64 in internal/dast/inventory/auth_helper.go as the
same state. That request IS still open -- verified, the allowlist has no entry
-- so the file was presenting one closed review and one open one identically.
The corrected text names that as the specific error to avoid next time.

The justification is kept verbatim, because it is the review record: regexp/syntax
is the parser and compiler behind regexp, which is already on the allowlist; it
has no dialer, listener, transport or I/O; gate 3 attributes by import line
rather than transitively, which is why the line was needed at all.

THE WINDOW GUARD CAUGHT THIS EDIT, WHICH IS THE POINT OF IT

Rewriting the paragraph moved the comment-node and physical-line counts the
window disclosure pins, and the suite went red naming both old figures and both
new ones. Re-measured in the same diff: 6,198 to 6,211 nodes, 6,197 to 6,210
lines. A guard that fails when the thing it describes moves is the only kind
that keeps a disclosure true.

The production change is comments only -- verified by filtering the diff to
non-comment lines, which is empty.

Evidence: gofmt, vet clean; 31/31 packages green; gate 3 green.
The last verifier's headline: "NOT YET -- but it is documentary only, and the
single defect is one wrong word."

BEHAVIOUR: ZERO DEFECTS, SIX ROUNDS RUNNING, AND NOW MECHANICALLY PROVEN RATHER
THAN ARGUED. confirm_gate.go is AST-identical to 06c69a4 with comments stripped
-- verified by parsing both revisions with comments discarded and diffing the
printed declarations, not by filtering comment lines out of a text diff. The
test file gained one hoisted literal, one hoisted local, five equality
assertions and four re-measured constants. No import, no exported declaration,
no altered expectation, no t.Skip.

RULING 19 LANDED ACROSS THE FILE. Eleven sites carrying 39 figures each got one
of the two treatments or were deleted. Two are worth naming:

The "17 of 20 bounded-prefix HTML anchors" figure, stated three times and once
in the present tense, described a family of twenty THAT WAS NEVER WRITTEN DOWN
ANYWHERE. Unreproducible by a reader, unpinnable by the package, and incapable
of ever going red -- no vocabulary edit and no rename could move it. It is
replaced by a hoisted anchorTags var and three pinned equalities over the corpus
arm run alone, measured at 3 caught / 11 missed / 14 members.

The benchmark block took treatment (a) with its reasoning stated: a timing is
the one measurement here that CANNOT be pinned as an equality, because pinning
it would pin the hardware. It now names the tree it was taken on, the Go
version, CPU and OS, and the command to reproduce it. A duplicate of one figure
sixty lines away was deleted on the ground that a timing written twice is a
timing that goes stale once.

THE LAST THREE, FIXED HERE RATHER THAN DISPATCHED

zzCarrier was called "a two-field struct". It has one -- zzCarrier{ Payload
[]byte }; the two-field struct in that worked example is the OUTER one reaching
it twice, which the entry's next clause already says correctly. The previous
round was pointed at this exact string, corrected the clause after the comma,
and left the clause before it.

The plan citation read "D.27 (lines 870-911)". D.27's fenced block runs 869-907;
911 opens D.28's. Verified against the fence lines.

And the corpus header count said its figure was written "four times over". It is
written three times -- "nine vocabularies" twice and "NINE OF THEM" once. A
count somebody took by eye, in the sentence complaining that the number it
describes was a count somebody took by eye.

Eight rounds each left exactly one behind. These three were verified directly
against the tree rather than delegated, because a workflow for three words is
not proportionate and the verification is a grep.

The production diff is comments only.

Evidence: gofmt, vet clean; 31/31 packages green; gate 3 green; race clean.
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