AUD-S15: lift MRInfo/ErrNotFound into the forge port (ARCH-02) - #34
Merged
Conversation
…-02) AUD-S15 / REQ-AUD-S15-01+02. The orchestration read port `cmd/assent` depends on spoke GitLab-named types — `GetMR(...) (gitlab.MRInfo, error)` and an absent-file signal matched against `gitlab.ErrNotFound` — so a second adapter could not satisfy it without cmd importing GitLab. Both now live on the port: - `internal/forge/port.go` owns `forge.MRInfo` (moved verbatim, all seven fields) and `forge.ErrNotFound`; - `internal/forge/gitlab` keeps `type MRInfo = forge.MRInfo` and `var ErrNotFound = forge.ErrNotFound` as transitional aliases (design note step 1/2), so the adapter and its tests are untouched; - `cmd/assent` names only `New`, `WithSleeper` and `SyntheticDigest` from the adapter — construction, plus the documented E10 residue. Zero behaviour change is the contract, so the 404 message is preserved to the byte: the sentinel deliberately carries no `forge: ` prefix (unlike every other sentinel in the package, which is returned directly by forge code) because the adapter supplies its own, keeping the render at `gitlab: resource not found (404): file "x" at ref "y"`. Gates: `internal/forge/port_test.go` drives a REAL *gitlab.Client against an httptest 404 and asserts errors.Is reaches forge.ErrNotFound through the actual wrap chain (errors.Unwrap must arrive at the sentinel VALUE, so a bespoke Is() shim cannot pass), pins the rendered message, pins the MRInfo field list, and reflects over the port asserting no gitlab-package type appears in any signature — with a deliberately-leaking probe port as the positive control, so "found nothing" cannot mean "the walker is broken". `hack/lint/depguard_test.sh` gains the ARCH-02 symbol allowlist. depguard cannot express this rule (cmd legitimately imports the adapter to construct it), so it is grep-based — and, since a grep matching nothing passes open, it carries two controls: the scanner must see the real allowed call-sites, and a violating COPY of cmd/assent in $TMPDIR must be reported by file and symbol. Out of scope, per docs/planning/design-notes/e10-forge-port-lift.md: the `forge.RunPort` composite port and the `SyntheticDigest` collapse onto `Snapshot.Heads.MergeResultDigest` (steps 3-5, E10).
The note's problem statement described a state AUD-S15 has now fixed. Keep the paragraph verbatim as the record of what was wrong, but label it pre-AUD-S15 and state what is still true today: only the gitlab.SyntheticDigest call remains in cmd/assent, and it is grep-pinned to an explicit allowlist. Steps 3-5 stay open for E10.
Review catch on AUD-S15. scan_gitlab_symbols matches `gitlab.<Exported>`, so an aliased import (`import gl "…/forge/gitlab"`) makes every reference read `gl.MRInfo` — the pattern matches nothing, the allowlist reports zero violations and passes OPEN. That is precisely the empty-sweep failure the section's other controls exist to prevent. aliased_gitlab_imports() now requires cmd/assent to import the adapter under its bare path. One rule covers both import forms: a leading `import` keyword is stripped first (sed preserves the line count, so reported line numbers stay true), reducing `import gl "…"` to the grouped `gl "…"`, after which any token before the quoted path is an alias and a plain `import "…"` has none. Positive-controlled like everything else in this section: the violating copy in $TMPDIR gains a second probe file with an aliased import that MUST be reported, while the existing unaliased probe must NOT be — otherwise a check that flagged every import would be silent on the real tree for the wrong reason.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
AUD-S15 — ARCH-02: lift
MRInfo/ErrNotFoundinto the forge port (pre-E10).Spec:
openspec/specs/p5-aud-audit-remediation/spec.md(AUD-S15, REQ-AUD-S15-01/02).Design note:
docs/planning/design-notes/e10-forge-port-lift.mdsteps 1–2.A pure mechanical, behaviour-preserving lift.
forge.MRInfoandforge.ErrNotFoundnowlive on the port;
internal/forge/gitlabkeepstype MRInfo = forge.MRInfoandvar ErrNotFound = forge.ErrNotFoundas transitional aliases;cmd/assentno longer nameseither.
Out of scope (E10, per the design note): the
forge.RunPortcomposite port and theSyntheticDigestcollapse ontoSnapshot.Heads.MergeResultDigest(steps 3–5). No GitHubadapter code (Locked per D-012).
1.
gitlab.grep ofcmd/assent, before and afterCode-level references only (
gitlab.<Exported>, whole-line comments excluded — the treecarries prose mentions of
*gitlab.Client).BEFORE (base
9e50e17) — 21 references, 5 distinct symbolsAFTER — 10 references, 3 distinct symbols:
cmd/assent/policy_test.go,cmd/assent/run_render.goandcmd/assent/run_render_test.gono longer import the adapter at all.
Deviation from the literal acceptance wording — please note
The criterion reads "only the constructor site(s) remain". Two
gitlab.SyntheticDigestcall-sites remain. This is deliberate and consistent with the rest of the story text:
run.go:61,420,473,496,533,624,run_render.go:20) maps — at a uniform +32-line offset on the base — onto exactly theMRInfo/ErrNotFoundsites and excludes bothSyntheticDigestsites;SyntheticDigestcollapse in E10;"stop computing it in cmd" — E10 step 4 collapses both call-sites onto
snapshot.Heads.MergeResultDigest, whichSnapshotalready computes. Doing it herewould reorder what
buildDesireddepends on, i.e. not a mechanical lift.Both sites carry an in-code note saying so, and the new gate pins the residue as an
allowlist (
New,WithSleeper,SyntheticDigest) rather than waving it through:anything else — notably
MRInfoandErrNotFoundcoming back — fails. E10 tightens thelist to
New/WithSleeper; it is never widened without an ADR.Second wording deviation: no
.golangci.ymldiffREQ-AUD-S15-01 says "the S07 depguard rule extended in
hack/lint/depguard_test.sh". Thescript is extended;
.golangci.ymlis deliberately untouched. depguard is import-leveland this rule is symbol-level —
cmd/assentlegitimately imports the adapter toconstruct it, so an import deny-rule would have to exempt
run.go, the very file the storyis about. An unused import does not compile in Go anyway, so the symbol allowlist strictly
subsumes any file-level import rule. Leaving the config alone also avoids rescoping the
existing
sedextraction (its- "**/…/**"and- pkg: "…"patterns are file-wide), whichwould have put the working S07 gate at risk for no added strength.
2. Byte-identical goldens (REQ-AUD-S15-02, the refactor gate)
A 32-artifact deterministic capture was taken on the clean base
9e50e17and again onthe same tree with this change applied, then diffed:
assent testandassent test --coverageon all three example packs (6 artifacts)assent compare --suiteon all three comparison suites (3)assent lint+assent catalogueon all three packs (6)go test -vovercmd/assent,internal/forge,internal/render,internal/core/aggregate,internal/core/decision,internal/adoptertest,examples/comparison, timings/ports/paths normalised (7)task determinismmatrix (1)git status --porcelainover everytestdata/,examples/,schemas/(1)go tool cover -funcoverinternal/forge/...(1)cmd/assent's tests are the ones that matter most here: they driverunRunend-to-endagainst a real
*gitlab.Clientpointed at an httptest GitLab, so they are the onlyartifact that exercises the forge path at all (
assent test/compare/lintnever build aforge).
Result: 30 of 32 artifacts byte-identical. The two that differ are fully accounted for:
gotest-forge.txtRUN/PASSlines of the newport_test.go.comm -23 pre postis empty: nothing was removed or changed.coverage-forge.txtgitlab.go). Normalising\.go:[0-9]+:→\.go:L:makes it byte-identical:diffexits 0, every per-function percentage unchanged.testdata-drift.txtis empty in both runs — no golden file was rewritten by any suite.3.
errors.Isthrough the real wrap chainPreserving the chain and the rendered bytes constrained the sentinel's own message.
forge.ErrNotFounddeliberately carries noforge:prefix — unlike every other sentinelin the package, which is returned directly by forge code and names itself — because this one
is only ever returned wrapped by an adapter, which supplies the prefix:
which renders
gitlab: resource not found (404): file "x" at ref "y"— byte-identical tothe pre-lift adapter. A
forge:-prefixed sentinel would have producedgitlab: forge: resource not found …, a silent user-visible change no golden would havecaught. This is now pinned by a test.
internal/forge/port_test.go(new,package forge_test— an in-package test would be animport cycle) drives a real
*gitlab.Clientagainst an httptest 404 and asserts on theerror the adapter actually returned — never a hand-constructed one:
errors.Is(err, forge.ErrNotFound)holds;errors.Unwrap(err) == forge.ErrNotFound— the value, so a bespokeIs()shim thatmerely answers "yes" cannot pass;
gitlab.ErrNotFound == forge.ErrNotFound(the alias is still an alias);broken forge would read as "file absent" and the presence signal would fail open.
Plus
TestMRInfoIsOneTypeAcrossPortAndAdapter(samereflect.Type, owned byinternal/forge, field list verbatim) andTestRunReadPortCarriesNoAdapterType, whichreflects transitively over the read port's method signatures asserting no
internal/forge/gitlabtype appears anywhere in them — the ARCH-02 binding constraint madeexecutable.
4. Positive controls — every negative assertion can fire
Nine mutations were applied to the real tree (each verified landed via
git diff), the gateconfirmed RED, then restored:
gitlab:prefix from the 404 wrapport_test.gomessage pin:got: resource not found (404): file …errors.New("resource not found (404)")— message stays byte-identicalerrors.Is(err, forge.ErrNotFound) = false … the adapter did not wrap onto the port sentinelforge.MRInfoforge.MRInfo fields = […ForkMR Extra], want […ForkMR]*gitlab.Client does not implement runReadPort (wrong type for method GetMR)adapterTypesIn found no gitlab type in a port that deliberately exposes gitlab.Option — the detector cannot fire, so its silence proves nothingthe orchestration read port exposes adapter types [gitlab.Option gitlab.Client gitlab.RetryPolicy](note the transitive walk)runReadPort exposes only 1 methods — the port shrank and the sweep below would be near-vacuousgitlab.MRInfoback intocmd/assent/run.gorun.go:68: gitlab.MRInfo→FAIL: cmd/assent names gitlab adapter symbols outside the construction allowlistFAIL: scan_gitlab_symbols found only 0 gitlab.<Exported> references in cmd/assent — the scanner stopped matching real code, so its silence would prove nothingdoctor_forge_test.go(import gl "…/gitlab", refs becomegl.New)doctor_forge_test.go:13: gl "…/internal/forge/gitlab"→FAIL: cmd/assent imports the gitlab adapter under an ALIAS — the symbol allowlist above cannot see through itOn the depguard extension specifically. REQ-AUD-S15-01 says "grep-pinned", and it has to
be: the rule is symbol-level, and depguard is import-level —
cmd/assentlegitimatelyimports the adapter to construct it, so an import rule would have to exempt
run.go, thevery file the story is about. A grep that matches nothing passes open, so the new section in
hack/lint/depguard_test.shcarries three controls of its own:cmd/assentand the knowngitlab.Newand
gitlab.SyntheticDigestcall-sites (M6 proves this fires);cmd/assenttree is materialised in$TMPDIRwith anadded file referencing
gitlab.MRInfo/gitlab.ErrNotFound, and the scanner must reportboth symbols and name the violating file;
cmd/assentmust import the adapter under its bare path — an aliased import(
import gl "…/gitlab") would make every reference readgl.MRInfo, the pattern wouldmatch nothing, and the allowlist would pass open. This closes the one evasion that
reproduces the empty-sweep failure the other controls exist to prevent, and it gets its
own control: a second probe file in the violating copy carries an aliased import that
must be reported, while the unaliased probe must not be — a check that flagged every
import would be silent on the real tree for the wrong reason;
Nothing is written into the repository working tree. Whole-line comments are blanked (not
deleted) before scanning, so reported line numbers stay true; trailing
// …comments aredeliberately not stripped, because stripping from the first
//would also truncate aline containing a
"http://…"literal and could hide a real reference after it — scanningmore text is the fail-closed direction. Block comments in
cmd/assentare refused outrightwith an instruction to extend the scanner first.
5. Gate matrix
Run on the rebased tree (base
669d805, after #25 merged):task checkFAILlinestask determinismtask dogfood-examplestask checkbash hack/lint/depguard_test.shgo build ./.../go vet ./...CI on this PR is green:
verifypass (5m48s),Analyze (go)pass,Analyze (actions)pass, CodeQL pass, MkDocs build pass, goreleaser snapshot pass. The table above is the local run.The known PR #27 flake (
internal/provider TestExecDigestPin/match_allows_exec,TestIsolationNoWriteToken,signal: killedunder-raceload) aborted the first tworuns — once in
test, once incoverage. It passes in isolation(
go test ./internal/provider/ -run 'TestExecDigestPin|TestIsolationNoWriteToken'→ ok) andthis branch touches nothing in
internal/provider. The runs reported above are complete,unaborted runs.
Merge sequencing
Heads-up for AUD-S13 (
internal/forge/clearslot_test.go): this changes types ininternal/forge/internal/forge/gitlab, but both are kept source-compatible by thealiases —
gitlab.MRInfoandgitlab.ErrNotFoundstill resolve, anderrors.Isagainsteither still works.
internal/forge/gitlab's own test files were not modified. A rebaseeither way should be clean; this PR adds one new file to
internal/forge/(
port_test.go,package forge_test, which the directory already mixes).Files touched