Skip to content

fix(assent): read judged content at the pinned commit SHA - #149

Merged
konih merged 7 commits into
mainfrom
fm/assent-b1-pinned-sha
Sep 26, 2026
Merged

konih merged 7 commits into
mainfrom
fm/assent-b1-pinned-sha

Conversation

@konih

@konih konih commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Intent

The six-leg Assent review found that the live run path reads the content it judges from a mutable branch name while the decision record and the merge CAS pin a commit SHA, so a contributor who controls push timing can force-push back to the pinned SHA after assent judged different bytes. Fix the read so the judged content is read at the pinned SHA — the round's first fix slice. Evidence and exact call sites: finding U-04 in the unification register and QFN-01 in the differential report; the B1 lane names the six call sites in cmd/assent/run.go at lines 203, 211, 230, 249, 270 and 274 (the MergePolicy, RulesetBinding, Config, Pack and governed base reads, plus the governed head read), the two polarity tests to flip in internal/forge/gitlab/gitlab_test.go at line 82 and cmd/assent/run_test.go at line 388, and a move-and-restore conformance case where the head moves away from the pinned SHA and back to it between evaluation and the merge CAS.

What Changed

  • assent run now resolves the MergePolicy, RulesetBinding, Config, Pack, and governed base/head reads at the pinned target/source commit SHAs instead of the mutable branch names, so the bytes judged match the SHA-guarded merge (REV1-S01 / U-04 item 1).
  • Flips the run-path fake router and TestFileAtRef to serve/reject only the pinned SHA refs, and adds run-path tests proving the Pack load follows the pin and that a moved source branch tip is ignored in favor of the pin's violating bytes.
  • Adds the sha-guard-source-moved-and-restored conformance case with a Fixture.MoveSourceHead seam and catalog row, documenting that the merge CAS alone cannot distinguish a restore from a never-moved head, plus the REV1 spec and D-183 decision record.

Risk Assessment

✅ Low: The change is a minimal, uniform one-argument-per-site pinning of six content reads to the commit SHAs already used by the approval/merge CAS, every site is covered by a behavior-level polarity lock, and the added conformance case honestly documents the CAS's move-and-restore limit rather than overclaiming.

Testing

Built the real assent binary and live-drove it end-to-end against a GitLab REST stub for the three run-path scenarios: with the pinned source SHA holding a policy-violating shrink while the moved source branch held a benign grow, the binary judged the pin (REVIEW, no merge) and never requested the branch; with the bytes reversed it APPROVEd and issued the SHA-pinned merge (merge?sha=srcSHA); with --config/--pack both were fetched at the pinned target SHA (the stub 400s branch refs, so a regression would fail). The stub transcript shows all six reads resolving pins: merge-policy, ruleset-binding, config, pack and governed base at ref=tgtTIP, governed head at ref=srcSHA. The move-and-restore conformance case passes on both fake and gitlab adapters, and the flipped polarity tests pass. No product surface issue found.

  • Live validation: ✅ go - 4 of 4 scenarios driven live against the product
Scenario Result Live Evidence
A contributor force-pushes the MR source branch to benign bytes after the pinned SHA; running the real assent run judges the PINNED source SHA's violating bytes -> decision REVIEW and no merge ✅ pass live rev1_live_s1.txt / rev1_live_runs.log: exit 0, decision REVIEW, HTTP transcript shows governed head fetched at ref=srcSHA and the branch ref=feature never requested
The pinned source SHA holds passing bytes while the moved branch tip holds violating bytes; the real assent run APPROVEs and issues the SHA-pinned merge at the pin, ignoring the branch tip ✅ pass live rev1_live_B_record.json / rev1_live_runs.log: decision APPROVE, PUT /api/v4/projects/42/merge_requests/7/merge?sha=srcSHA, no ref=feature fetch
Running the real assent run with --config and --pack resolves both documents at the pinned target SHA (a branch-name read would 400 and fail the run) ✅ pass live rev1_live_s2.txt: exit 0; config and pack raw reads carry ref=tgtTIP
Move-and-restore: the MR head moves away from the pin and is restored to it between evaluation and the merge CAS; the pre-check fails closed while moved and the CAS merges at the pin when restored ✅ pass live rev1_conformance_moverestore.log: TestConformanceSourceMovedAndRestored passes for fake and gitlab adapters, both moved-away and restored phases
Evidence: Live real-binary runs (S1 pinned-shrink, S2 config+pack pinned, B pinned-grow APPROVE+merge) with HTTP ref transcripts
=== LIVE SCENARIO S1: pinned source SHA holds a violating shrink, moved source branch holds benign grow ===
stub: HEAD_FILE(ref=srcSHA)='partitions: 3' SOURCE_HEAD_FILE(ref=feature)='partitions: 24'
EXIT=0
--- stdout ---
{"apiVersion":"assent.dev/v1alpha1","kind":"DecisionRecord","decision":"REVIEW","findings":{"observed":[],"enforcing":[{"rule":"partitions-must-not-shrink","obligation":"non-destructive","effect":"challenge","subject":"file:topics/orders.yaml","points":0,"code":"partition-count-shrunk"}]},"pins":{"toolVersion":"0.0.0-dev","toolDigest":"sha256:33495ff6935ffb66715f8367e41c447d3d088ffef6e201aca555ee75c9e1de4a","policySha":"sha256:cd8477e49ffcae03362ab0a0d3eccf7d50c71ef13d9f038d412a62cd19a87a5d","sourceSha":"srcSHA","targetSha":"tgtTIP","mergeResultDigest":null,"capabilityGap":"gitlab plain-merge exposes no merge-result digest (no merge train); ADR-0017 §1 capabilityGap","factsResolvedAt":{}}}
decision=REVIEW arm=false → 2 forge operation(s) written

--- stderr ---
--- refs+merge+approve ---
   2 GET /api/v4/projects/42/merge_requests/7/approval_rules?per_page=100&page=1
   1 GET /api/v4/projects/42/merge_requests/7/approval_state?
   1 GET /api/v4/projects/42/merge_requests/7/diffs?per_page=100&page=1
   3 GET /api/v4/projects/42/merge_requests/7/discussions?per_page=100&page=1
   2 GET /api/v4/projects/42/merge_requests/7/notes?per_page=100&page=1
   3 GET /api/v4/projects/42/merge_requests/7?
   1 GET /api/v4/projects/42/repository/files/.assent%2Fmerge-policy.yaml/raw?ref=tgtTIP
   1 GET /api/v4/projects/42/repository/files/.assent%2Fruleset-binding.yaml/raw?ref=tgtTIP
   1 GET /api/v4/projects/42/repository/files/topics%2Forders.yaml/raw?ref=srcSHA
   1 GET /api/v4/projects/42/repository/files/topics%2Forders.yaml/raw?ref=tgtTIP
   1 POST /api/v4/projects/42/merge_requests/7/discussions?
   1 POST /api/v4/projects/42/merge_requests/7/notes?

=== LIVE SCENARIO S2: --config + --pack served ONLY at the pinned target SHA ===
EXIT=0
--- stdout ---
{"apiVersion":"assent.dev/v1alpha1","kind":"DecisionRecord","decision":"REVIEW","findings":{"observed":[],"enforcing":[{"rule":"aggregate.uncovered","obligation":"non-destructive","effect":"require-review","subject":"obligation:non-destructive","points":0,"code":"obligation.uncovered"}]},"pins":{"toolVersion":"0.0.0-dev","toolDigest":"sha256:33495ff6935ffb66715f8367e41c447d3d088ffef6e201aca555ee75c9e1de4a","policySha":"sha256:cd8477e49ffcae03362ab0a0d3eccf7d50c71ef13d9f038d412a62cd19a87a5d","sourceSha":"srcSHA","targetSha":"tgtTIP","mergeResultDigest":null,"capabilityGap":"gitlab plain-merge exposes no merge-result digest (no merge train); ADR-0017 §1 capabilityGap","factsResolvedAt":{}}}
decision=REVIEW arm=false → 2 forge operation(s) written

--- stderr ---
--- refs+merge+approve ---
   2 GET /api/v4/projects/42/merge_requests/7/approval_rules?per_page=100&page=1
   1 GET /api/v4/projects/42/merge_requests/7/approval_state?
   1 GET /api/v4/projects/42/merge_requests/7/diffs?per_page=100&page=1
   3 GET /api/v4/projects/42/merge_requests/7/discussions?per_page=100&page=1
   2 GET /api/v4/projects/42/merge_requests/7/notes?per_page=100&page=1
   3 GET /api/v4/projects/42/merge_requests/7?
   1 GET /api/v4/projects/42/repository/files/.assent%2Fconfig.yaml/raw?ref=tgtTIP
   1 GET /api/v4/projects/42/repository/files/.assent%2Fmerge-policy.yaml/raw?ref=tgtTIP
   1 GET /api/v4/projects/42/repository/files/.assent%2Fpacks%2Fp%2Fpack.yaml/raw?ref=tgtTIP
   1 GET /api/v4/projects/42/repository/files/.assent%2Fproviders%2Fquota.json/raw?ref=main
   1 GET /api/v4/projects/42/repository/files/.assent%2Fruleset-binding.yaml/raw?ref=tgtTIP
   1 GET /api/v4/projects/42/repository/files/topics%2Forders.yaml/raw?ref=srcSHA
   1 GET /api/v4/projects/42/repository/files/topics%2Forders.yaml/raw?ref=tgtTIP
   1 POST /api/v4/projects/42/merge_requests/7/discussions?
   1 POST /api/v4/projects/42/merge_requests/7/notes?

=== LIVE SCENARIO B: pinned source SHA holds benign grow, moved source branch holds violating shrink ===
stub: HEAD_FILE(ref=srcSHA)='partitions: 24' SOURCE_HEAD_FILE(ref=feature)='partitions: 3'
EXIT=0
{"apiVersion":"assent.dev/v1alpha1","kind":"DecisionRecord","decision":"APPROVE","findings":{"observed":[],"enforcing":[]},"pins":{"toolVersion":"0.0.0-dev","toolDigest":"sha256:33495ff6935ffb66715f8367e41c447d3d088ffef6e201aca555ee75c9e1de4a","policySha":"sha256:cd8477e49ffcae03362ab0a0d3eccf7d50c71ef13d9f038d412a62cd19a87a5d","sourceSha":"srcSHA","targetSha":"tgtTIP","mergeResultDigest":null,"capabilityGap":"gitlab plain-merge exposes no merge-result digest (no merge train); ADR-0017 §1 capabilityGap","factsResolvedAt":{}}}
decision=APPROVE arm=false → 3 forge operation(s) written

--- refs+merge+approve ---
   2 GET /api/v4/projects/42/merge_requests/7/approval_rules?per_page=100&page=1
   1 GET /api/v4/projects/42/merge_requests/7/approval_state?
   1 GET /api/v4/projects/42/merge_requests/7/diffs?per_page=100&page=1
   1 GET /api/v4/projects/42/merge_requests/7/discussions?per_page=100&page=1
   2 GET /api/v4/projects/42/merge_requests/7/notes?per_page=100&page=1
   5 GET /api/v4/projects/42/merge_requests/7?
   1 GET /api/v4/projects/42/repository/files/.assent%2Fmerge-policy.yaml/raw?ref=tgtTIP
   1 GET /api/v4/projects/42/repository/files/.assent%2Fruleset-binding.yaml/raw?ref=tgtTIP
   1 GET /api/v4/projects/42/repository/files/topics%2Forders.yaml/raw?ref=srcSHA
   1 GET /api/v4/projects/42/repository/files/topics%2Forders.yaml/raw?ref=tgtTIP
   1 POST /api/v4/projects/42/merge_requests/7/approve?
   1 POST /api/v4/projects/42/merge_requests/7/notes?
   1 PUT /api/v4/projects/42/merge_requests/7/merge?sha=srcSHA
Evidence: Scenario S1: pinned srcSHA shrink, branch tip grow -> REVIEW, no branch fetch

EXIT=0 decision=REVIEW -> 2 forge operation(s) written refs: merge-policy?ref=tgtTIP, ruleset-binding?ref=tgtTIP, topics%2Forders.yaml?ref=tgtTIP (base), topics%2Forders.yaml?ref=srcSHA (head); no ref=feature

EXIT=0
--- stdout ---
{"apiVersion":"assent.dev/v1alpha1","kind":"DecisionRecord","decision":"REVIEW","findings":{"observed":[],"enforcing":[{"rule":"partitions-must-not-shrink","obligation":"non-destructive","effect":"challenge","subject":"file:topics/orders.yaml","points":0,"code":"partition-count-shrunk"}]},"pins":{"toolVersion":"0.0.0-dev","toolDigest":"sha256:33495ff6935ffb66715f8367e41c447d3d088ffef6e201aca555ee75c9e1de4a","policySha":"sha256:cd8477e49ffcae03362ab0a0d3eccf7d50c71ef13d9f038d412a62cd19a87a5d","sourceSha":"srcSHA","targetSha":"tgtTIP","mergeResultDigest":null,"capabilityGap":"gitlab plain-merge exposes no merge-result digest (no merge train); ADR-0017 §1 capabilityGap","factsResolvedAt":{}}}
decision=REVIEW arm=false → 2 forge operation(s) written

--- stderr ---
--- refs+merge+approve ---
   2 GET /api/v4/projects/42/merge_requests/7/approval_rules?per_page=100&page=1
   1 GET /api/v4/projects/42/merge_requests/7/approval_state?
   1 GET /api/v4/projects/42/merge_requests/7/diffs?per_page=100&page=1
   3 GET /api/v4/projects/42/merge_requests/7/discussions?per_page=100&page=1
   2 GET /api/v4/projects/42/merge_requests/7/notes?per_page=100&page=1
   3 GET /api/v4/projects/42/merge_requests/7?
   1 GET /api/v4/projects/42/repository/files/.assent%2Fmerge-policy.yaml/raw?ref=tgtTIP
   1 GET /api/v4/projects/42/repository/files/.assent%2Fruleset-binding.yaml/raw?ref=tgtTIP
   1 GET /api/v4/projects/42/repository/files/topics%2Forders.yaml/raw?ref=srcSHA
   1 GET /api/v4/projects/42/repository/files/topics%2Forders.yaml/raw?ref=tgtTIP
   1 POST /api/v4/projects/42/merge_requests/7/discussions?
   1 POST /api/v4/projects/42/merge_requests/7/notes?
Evidence: Scenario B: pinned srcSHA grow, branch tip shrink -> APPROVE + PUT merge?sha=srcSHA

{"decision":"APPROVE",..."sourceSha":"srcSHA","targetSha":"tgtTIP"...} ; requests: topics%2Forders.yaml?ref=srcSHA and PUT /merge_requests/7/merge?sha=srcSHA

{"apiVersion":"assent.dev/v1alpha1","kind":"DecisionRecord","decision":"APPROVE","findings":{"observed":[],"enforcing":[]},"pins":{"toolVersion":"0.0.0-dev","toolDigest":"sha256:33495ff6935ffb66715f8367e41c447d3d088ffef6e201aca555ee75c9e1de4a","policySha":"sha256:cd8477e49ffcae03362ab0a0d3eccf7d50c71ef13d9f038d412a62cd19a87a5d","sourceSha":"srcSHA","targetSha":"tgtTIP","mergeResultDigest":null,"capabilityGap":"gitlab plain-merge exposes no merge-result digest (no merge train); ADR-0017 §1 capabilityGap","factsResolvedAt":{}}}
decision=APPROVE arm=false → 3 forge operation(s) written
Evidence: Scenario S2: --config and --pack fetched at ref=tgtTIP, exit 0

EXIT=0 GET .assent%2Fconfig.yaml/raw?ref=tgtTIP GET .assent%2Fpacks%2Fp%2Fpack.yaml/raw?ref=tgtTIP GET .assent%2Fproviders%2Fquota.json/raw?ref=main (provider declarations, deliberately out of scope U-11/D-130)

EXIT=0
--- stdout ---
{"apiVersion":"assent.dev/v1alpha1","kind":"DecisionRecord","decision":"REVIEW","findings":{"observed":[],"enforcing":[{"rule":"aggregate.uncovered","obligation":"non-destructive","effect":"require-review","subject":"obligation:non-destructive","points":0,"code":"obligation.uncovered"}]},"pins":{"toolVersion":"0.0.0-dev","toolDigest":"sha256:33495ff6935ffb66715f8367e41c447d3d088ffef6e201aca555ee75c9e1de4a","policySha":"sha256:cd8477e49ffcae03362ab0a0d3eccf7d50c71ef13d9f038d412a62cd19a87a5d","sourceSha":"srcSHA","targetSha":"tgtTIP","mergeResultDigest":null,"capabilityGap":"gitlab plain-merge exposes no merge-result digest (no merge train); ADR-0017 §1 capabilityGap","factsResolvedAt":{}}}
decision=REVIEW arm=false → 2 forge operation(s) written

--- stderr ---
--- refs+merge+approve ---
   2 GET /api/v4/projects/42/merge_requests/7/approval_rules?per_page=100&page=1
   1 GET /api/v4/projects/42/merge_requests/7/approval_state?
   1 GET /api/v4/projects/42/merge_requests/7/diffs?per_page=100&page=1
   3 GET /api/v4/projects/42/merge_requests/7/discussions?per_page=100&page=1
   2 GET /api/v4/projects/42/merge_requests/7/notes?per_page=100&page=1
   3 GET /api/v4/projects/42/merge_requests/7?
   1 GET /api/v4/projects/42/repository/files/.assent%2Fconfig.yaml/raw?ref=tgtTIP
   1 GET /api/v4/projects/42/repository/files/.assent%2Fmerge-policy.yaml/raw?ref=tgtTIP
   1 GET /api/v4/projects/42/repository/files/.assent%2Fpacks%2Fp%2Fpack.yaml/raw?ref=tgtTIP
   1 GET /api/v4/projects/42/repository/files/.assent%2Fproviders%2Fquota.json/raw?ref=main
   1 GET /api/v4/projects/42/repository/files/.assent%2Fruleset-binding.yaml/raw?ref=tgtTIP
   1 GET /api/v4/projects/42/repository/files/topics%2Forders.yaml/raw?ref=srcSHA
   1 GET /api/v4/projects/42/repository/files/topics%2Forders.yaml/raw?ref=tgtTIP
   1 POST /api/v4/projects/42/merge_requests/7/discussions?
   1 POST /api/v4/projects/42/merge_requests/7/notes?
Evidence: Move-and-restore conformance case passes on fake and gitlab adapters
=== RUN   TestConformanceSourceMovedAndRestored
=== RUN   TestConformanceSourceMovedAndRestored/fake
=== RUN   TestConformanceSourceMovedAndRestored/fake/moved-away
=== RUN   TestConformanceSourceMovedAndRestored/fake/restored
=== RUN   TestConformanceSourceMovedAndRestored/gitlab
=== RUN   TestConformanceSourceMovedAndRestored/gitlab/moved-away
=== RUN   TestConformanceSourceMovedAndRestored/gitlab/restored
--- PASS: TestConformanceSourceMovedAndRestored (0.17s)
    --- PASS: TestConformanceSourceMovedAndRestored/fake (0.11s)
        --- PASS: TestConformanceSourceMovedAndRestored/fake/moved-away (0.00s)
        --- PASS: TestConformanceSourceMovedAndRestored/fake/restored (0.00s)
    --- PASS: TestConformanceSourceMovedAndRestored/gitlab (0.06s)
        --- PASS: TestConformanceSourceMovedAndRestored/gitlab/moved-away (0.05s)
        --- PASS: TestConformanceSourceMovedAndRestored/gitlab/restored (0.00s)
PASS
ok  	github.com/PlatformRelay/assent/internal/forge/conformance	1.441s
Evidence: Flipped polarity tests pass (TestFileAtRef SHA ref; TestRunJudgesPinnedSHAWhenBranchMoves; TestRunPackFromPinnedSHA; TestRunPolicyFromTargetRefOnly)
=== RUN   TestRunPolicyFromTargetRefOnly
--- PASS: TestRunPolicyFromTargetRefOnly (0.01s)
=== RUN   TestRunPackFromPinnedSHA
--- PASS: TestRunPackFromPinnedSHA (0.16s)
=== RUN   TestRunJudgesPinnedSHAWhenBranchMoves
--- PASS: TestRunJudgesPinnedSHAWhenBranchMoves (0.13s)
PASS
ok  	github.com/PlatformRelay/assent/cmd/assent	1.681s
Evidence: gitlab adapter TestFileAtRef requires the pinned SHA ref
=== RUN   TestFileAtRef
--- PASS: TestFileAtRef (0.00s)
=== RUN   TestFileAtRefNotFound
--- PASS: TestFileAtRefNotFound (0.00s)
=== RUN   TestFileAtRefUnexpectedStatus
--- PASS: TestFileAtRefUnexpectedStatus (0.00s)
PASS
ok  	github.com/PlatformRelay/assent/internal/forge/gitlab	8.111s

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

⏭️ **Rebase** - skipped

Step was skipped.

✅ **Review** - passed

✅ No issues found.

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 4 of 5 scenarios driven live against the product
Scenario Result Live Evidence
Operator runs assent run when the MR source branch tip has moved to benign grow bytes while the pinned source SHA holds a policy-violating shrink; the emitted DecisionRecord follows the pinned SHA (… ✅ pass live ~/.no-mistakes/evidence/01M3CGVE9DY5FMPBFQPWATANBQ/live-binary-pinned-sha.log (TestLiveBinaryJudgesPinnedSHA: decision REVIEW, approvals=0 merges=0, governed head ?ref=srcSHA)
Operator runs assent run --pack and every judged-content read (MergePolicy, RulesetBinding, Pack, governed base, governed head) resolves a pinned commit SHA with no mutable branch-name ref ✅ pass live ~/.no-mistakes/evidence/01M3CGVE9DY5FMPBFQPWATANBQ/live-binary-pinned-sha.log (TestLiveBinaryAllReadsPinned: exit 0; ref log shows merge-policy/ruleset-binding/pack/base at tgtTIP and hea…
Operator runs assent run --config and the Config read resolves the pinned target SHA (the fake rejects any non-SHA config ref) ✅ pass live ~/.no-mistakes/evidence/01M3CGVE9DY5FMPBFQPWATANBQ/live-binary-pinned-sha.log (TestLiveBinaryConfigReadsPinned: config.yaml ?ref=tgtTIP, exit 0)
Conformance move-and-restore: forge.Reconcile refuses a head moved away from the pin and then merges once when the head is restored to the pin, proving the CAS alone cannot catch a restore; the case… ⏸️ untested no The prior payload recorded live=false for this scenario: it was driven through the conformance suite's in-process GitLab adapter over httptest and automated tests, not against the running assent pro…
Adversarial non-vacuity: reverting the governed-head read to info.SourceBranch makes the run-path test and the real binary APPROVE the moved branch tip with 3 forge writes, and reverting the MergePo… ✅ pass live ~/.no-mistakes/evidence/01M3CGVE9DY5FMPBFQPWATANBQ/mutation-head-branch-live-exploit.log, mutation-head-branch-red.log, mutation-policy-branch-red.log
  • go test -count=1 -tags livee2e ./cmd/assent/ -run TestLiveBinary (real compiled binary against local fake GitLab; three scenarios)
  • go test -count=1 ./cmd/assent/ -run 'TestRunJudgesPinnedSHAWhenBranchMoves|TestRunPackFromPinnedSHA|TestRunPolicyFromTargetRefOnly'
  • go test -count=1 ./internal/forge/gitlab/ -run TestFileAtRef
  • go test -count=1 ./internal/forge/conformance/ -run 'TestConformanceSourceMovedAndRestored|TestConformanceSourceMovedRejected|TestConformanceTargetAdvancedRejected'
  • go test -count=1 ./internal/forge/conformance/ -run 'TestEveryCaseCanFail|TestCatalogRowsMatchRunSuite|TestRunSuite'
  • Mutation: run.go governed-head read reverted to info.SourceBranch -> TestRunJudgesPinnedSHAWhenBranchMoves red and live binary APPROVEs the moved tip with 3 forge writes; restored
  • Mutation: run.go MergePolicy read reverted to info.TargetBranch -> TestRunPolicyFromTargetRefOnly red (400 at ?ref=main); restored
  • go test -count=1 ./cmd/assent/... ./internal/forge/gitlab/... ./internal/forge/conformance/... (all green after restore)

✅ No issues found.

  • Live validation: ✅ go - 4 of 4 scenarios driven live against the product
Scenario Result Live Evidence
A contributor force-pushes the MR source branch to benign bytes after the pinned SHA; running the real assent run judges the PINNED source SHA's violating bytes -> decision REVIEW and no merge ✅ pass live rev1_live_s1.txt / rev1_live_runs.log: exit 0, decision REVIEW, HTTP transcript shows governed head fetched at ref=srcSHA and the branch ref=feature never requested
The pinned source SHA holds passing bytes while the moved branch tip holds violating bytes; the real assent run APPROVEs and issues the SHA-pinned merge at the pin, ignoring the branch tip ✅ pass live rev1_live_B_record.json / rev1_live_runs.log: decision APPROVE, PUT /api/v4/projects/42/merge_requests/7/merge?sha=srcSHA, no ref=feature fetch
Running the real assent run with --config and --pack resolves both documents at the pinned target SHA (a branch-name read would 400 and fail the run) ✅ pass live rev1_live_s2.txt: exit 0; config and pack raw reads carry ref=tgtTIP
Move-and-restore: the MR head moves away from the pin and is restored to it between evaluation and the merge CAS; the pre-check fails closed while moved and the CAS merges at the pin when restored ✅ pass live rev1_conformance_moverestore.log: TestConformanceSourceMovedAndRestored passes for fake and gitlab adapters, both moved-away and restored phases
  • go build -o bin/assent ./cmd/assent then GITLAB_TOKEN=... ./bin/assent run --project 42 --mr 7 --bot-author assent-bot --subject file:topics/orders.yaml --gitlab-endpoint http://127.0.0.1:8899 against rev1_stub.py (scenario S1: pinned srcSHA shrink, branch feature grow) -> exit 0, decision REVIEW, no merge
  • same real-binary run with stub bytes flipped (scenario B: pinned srcSHA grow, branch feature shrink) -> exit 0, decision APPROVE, PUT .../merge?sha=srcSHA
  • same real-binary run with --pack .assent/packs/p/pack.yaml --config .assent/config.yaml (scenario S2) -> exit 0; config and pack fetched at ref=tgtTIP
  • go test ./internal/forge/conformance/ -run TestConformanceSourceMovedAndRestored -v (fake + gitlab adapters, moved-away and restored phases)
  • go test ./cmd/assent/ -run 'TestRunJudgesPinnedSHAWhenBranchMoves|TestRunPackFromPinnedSHA|TestRunPolicyFromTargetRefOnly' -v
  • go test ./internal/forge/gitlab/ -run TestFileAtRef -v
✅ **Document** - passed

✅ No issues found.

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

✅ No issues found.

@konih
konih force-pushed the fm/assent-b1-pinned-sha branch from cb1d11f to 8071c39 Compare September 25, 2026 14:10
@konih konih changed the title fix(run): read judged content at the pinned commit SHA fix(run): read the judged content at the pinned commit SHA Sep 25, 2026
The live run path pinned info.SourceSHA/info.TargetSHA for the approval and
merge CAS, but read every judged byte by mutable branch name, so a contributor
controlling push timing could force-push back to the pinned SHA after assent
judged different bytes (U-04 item 1 / QFN-01).

Read the six content sites at the pin: merge-policy, ruleset-binding, config,
pack and governed base at info.TargetSHA; governed head at info.SourceSHA.

Test lock: the run-path fake now serves the pinned SHAs and refuses any other
ref, so a regression to a branch name reddens the suite; TestFileAtRef asserts
the SHA; a new run-path case judges the pin while the branch points at benign
bytes; a new forge conformance case (sha-guard-source-moved-and-restored) shows
the CAS merges once a moved head is restored, which is why the read-pin is
load-bearing.

Spec: openspec/specs/p5-rev1-pinned-sha/spec.md. U-04 items 2-4 stay out of
scope as a named follow-on.
…ode in `internal/forge/conformance/suite.go` (the `caseSHAGuardSourceMovedAndRestored` conformance case) left 6 executable lines uncovered, and SonarCloud's new-code coverage condition (new_coverage < 80%) was the sole failing gate condition on PR 149 (64.3% vs 80% required; run.go's 4 changed error-return lines are also uninstrumented because `task coverage` profiles only `./internal/...`). The 6 uncovered lines were the phase-2 ("restored") failure branches. They were unreachable under the existing sabotage/non-vacuity harness (`TestEveryCaseCanFail`): a phase-1 assertion failure aborted the whole case via `failRecorder.Fatalf`, so phase 2 never ran against a sabotaged backend — meaning those phase-2 assertions were themselves unproven assertions, the exact defect class the sabotage gate exists to catch. Fix: split the two phases into `t.Run("moved-away", ...)` / `t.Run("restored", ...)` subtests so `failRecorder.Run` absorbs a phase-1 abort and still executes phase 2 against the sabotaged backend, and collapse phase 2's three separate failure branches into one compound assertion (`err != nil || merges != 1 || attempts != 1`) whose body now executes under sabotage. This makes the case 100% statement-covered (verified locally: `caseSHAGuardSourceMovedAndRestored 100.0%`), strengthens the non-vacuity proof (both phases are now shown capable of failing), and raises the projected new-code coverage to ~85.7% (>=80%), while `task coverage` stays at 91.1% (>= the 91 floor). No production behavior changed; the pinned-SHA read fix is untouched. Verification run locally: `go test ./cmd/... ./internal/forge/...` all pass; `go test -race ./internal/forge/conformance/` passes; `gofmt -l` and `go vet` clean; internal coverage 91.1%
The rebase onto main (24e9acf) landed main's mr.labels row as D-182, so the
pinned-SHA row this lane added was renumbered D-183. Update the four citations
that still named D-182 for the pinned-SHA decision.
@konih
konih force-pushed the fm/assent-b1-pinned-sha branch from bc480c1 to 32c1be3 Compare September 25, 2026 16:39
@konih konih changed the title fix(run): read the judged content at the pinned commit SHA fix(assent): read judged content at the pinned commit SHA Sep 25, 2026
@sonarqubecloud

Copy link
Copy Markdown

@konih
konih merged commit 0377ec2 into main Sep 26, 2026
10 checks passed
@konih
konih deleted the fm/assent-b1-pinned-sha branch September 26, 2026 15:20
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