HOMELAB-2682: fix(forgejo): three lock defects (resolution author check, no-defect headings, id-less hint) #99
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "plane/HOMELAB-2682-lock-defects"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Three merge-lock defects El Director reported (OVERSEER order on HOMELAB-2682), each confirmed by a failing unit test on synthetic PR text — no real PR was used for any case, and no network was touched.
Where they were measured. The report was read off the deployed source drops, and the drops are not the checkout:
forgejo/landing.tsat the two sandboxes (sbx-lab,sbx-gits) is sha256cce9615…= commitef7a662(HOMELAB-2686), while the mgmt-VM drop andorigin/mainarec48a734…=1d24858. Commit1e7c397(HOMELAB-2682, "reviewer-only resolutions, heading-only blockers") already closed two of the three arms in the checkout. Running this PR's new test file against the deployed copy fails 10 of 17:and against this branch's head, 17/17 pass.
D1 — a resolution needed no particular author (top priority)
Confirmed: at the deployed copy, an untrusted
blockers resolved, an untrustedFIX WORKER — blocker fixed, an untrustedresolved B1, and a single untrusted comment that both raises and closes all cleared the finding (findOpenBlockersreturned[]). In the checkout, the id-ed and id-less arms are already gated byauthorOwnsResolutions(1e7c397), so those four cases are pinned as non-regressions. One closure path was still open at this head: the in-line RESOLUTION RESTATEMENT.Root cause.
findOpenBlockerscloses a finding on four paths: (a) a laterresolved <id>citation by a trusted reviewer, (b) a later id-less resolution phrase (BLOCKER_RESOLUTION_PHRASE_RE) by a trusted reviewer, (c) the section resolution lead, and (d) the RESTATEMENT — a reviewer's closing report that restates a finding as a bolded numbered label and reports the fix in the same line (**Blocker 1 (census crash).** readKey now returns null on ENOENT). (a) and (b) askauthorOwnsResolutions; (d) was evaluated asid !== null && isResolutionRestatement(line)against the line itself, with no identity check at all. Any identity could therefore write the restatement shape and the line was swallowed before it ever became a finding — a finding nobody raised and nobody closed.Failing test (before the fix):
Fix. The restatement is gated by the same
authorOwnsResolutions(items[itemIndex], policy)the later-line scan uses. Untrusted, the line is prose and is reported as a finding (fail-closed). The identity is judged against the item that carries the line, so a comment that raises and closes in one body cannot slip through either. No behaviour change for a trusted reviewer: all three of the reviewer's own closure forms (phrase, citation, restatement) still close, and a policy member other than the default still counts.Pinned by: the six
D1cases — untrusted phrase, untrusted fix-worker note, untrusted citation, untrusted self-closing body, missing author, untrusted restatement — plus three no-change cases for the trusted reviewer and one for a non-default policy member.D2 — a heading that reports no defect counted as a finding
Confirmed at this head (
## Blocker test applied→[null], an id-less finding, which no citation can close):Root cause.
isSectionOnlyHeading— #96's guard that keeps a bare## Blockersheading from being an id-less finding — matched section nouns plus a closed list of qualifiers (open,remaining,no, …) and nothing else. A heading that names the section and then the META word for what the reviewer did (test applied,check applied,scan applied,tested,verification,analysis) was outside that list, so it fell through to the finding patterns and refused the merge.Fix.
isSectionOnlyHeadingnow also matchesSECTION_META_ONLY_RE: section noun + at least one META word, whole remainder, closed vocabulary, with the reviewer's own determination on the heading allowed (— no defect found). It is still a section TITLE, so it keeps supplying the blocker context to the lines beneath it — #97's rule is untouched and its tests are green. Fail-closed direction, deliberately: anything carrying a body stays a finding (## Blocker test applied on a repo the tenant filter still leaks,## Blocker scan_failed on the list query,## Blockers (severity: blocker) — …,### Blocker 1 — …), and a real defect bullet under a meta heading is still a finding whileNo defects found./None.report nothing.Pinned by: the four
D2cases (nine heading shapes; defect bullets beneath them; five headings that must stay findings; the #96/#97 shapes).D3 — the refusal hint was wrong for a finding with no id
Confirmed at this head: every open finding id-less, and the hint still said
Cite the finding id in the resolution ("resolved B1").Root cause.
evaluateLandingLock's hint chose onids.lengthonly: any id present → theresolved <id>form; no ids at all → the same string it used for the mixed case, telling the author to cite an id that does not exist. An id-less finding is closed only by a trusted reviewer's resolution PHRASE, so the advice was unsatisfiable.Fix. The hint now matches the finding's kind: id-ed →
No "resolved <id>" / "fixed-in <id>" marker follows.; id-less → the finding carries no id, no citation can close it, and what does is a later comment BY THE REVIEWER (policy.reviewerIdentities) declaring the blocker resolved or fixed; mixed → keeps the existingGive every blocker an id so a resolution can name it.nudge and adds the id-less note.Pinned by: the three
D3cases (id-less, id-ed, mixed).Suite
npm run test:deployed(deployed source drop,npm ci,tsc --noEmit,vitest run)npx vitest run)index.tscannot resolve baretypeboxwithout an install — the documentedTESTING.mdshape, not a regressionNo pre-existing reds. New file:
forgejo/tests/lock-defects-2682.test.ts(17 cases).Not merged — seats come next.
Verdict of record — ld-reviewer — head
86cf49991d: APPROVEmodel: deepseek-flash
session: 01a1ad36-eb65-757a-b84d-2769acc9ca3c
Independent seat 1 of 2 (gate tooling). Reviewed from primary sources: the diff
1d24858..86cf499,forgejo/landing.tsat the head, the newforgejo/tests/lock-defects-2682.test.ts, plus my own probes executed against the head file with1d24858run alongside for comparison. Two files changed —landing.ts(+66/−6) and one new 239-line test file — and nothing outside the three reported defects, so item 4 is clear. No seat-2 input was used.Findings
None.
D1 — the restatement closure is author-gated, and in the right place
The gate sits exactly where the restatement closure is decided (
findOpenBlockers), readsauthorOwnsResolutions(items[itemIndex], …)— the LINE'S OWN item author, not a neighbouring item's — and takes the samepolicythe id-ed and id-less arms use, which the lock supplies (findOpenBlockers(ordered, { policy, headSha })). Live in the real path, not only in tests.Probes at the head:
resolved <id>citation, restatement), and a member of a non-defaultreviewerIdentitiesset still counts.D2 — the reported shapes are fixed and the fail-closed set is intact
I re-ran every shape in the report and the run's five "must stay findings" headings. All nine no-defect headings are now section titles with zero findings;
## Blocker test applied on a repo the tenant filter still leaks,## Blocker scan_failed on the list query,## Blockers (severity: blocker) — …,### Blocker 1 — …and## Blockers: the guard only prints and never refusesall still refuse. I also fuzzed 33 headings against BOTH1d24858and the head: the newly-swallowed set is exactly section noun + closed meta vocabulary, and no heading that states a defect (check failed,review missed …,fix regressed,test evidence missing,verification incomplete,audit revealed a leak,status open) is in it. #97's context rule holds: a real defect bullet under a meta heading is still a finding. The residual brittleness is recorded under Concerns; every miss I found is in the safe direction.D3 — the hint now matches what the code accepts
Ran
evaluateLandingLockand read the three refusal texts. id-less: "…it carries no id, so no citation can close it. A later comment BY THE REVIEWER (ld-reviewer) declaring the blocker resolved or fixed … is what does" — accurate: I confirmed a trusted laterblockers resolvedcloses exactly that finding and that nothing else does. id-ed keeps theresolved <id>/fixed-in <id>wording; mixed keeps the give-it-an-id nudge and adds the id-less note. No unsatisfiable advice is left.Verification I ran
npm run test:deployedin a clean worktree at the head: typecheck ok, 26/26 files loaded, 702/702 tests passed, pinned treetypebox 1.3.34. Forgejo CI on86cf499: run #292 every job pass, #293 python pass — 14/14 green. No pre-existing red.Concerns
## Blockers found,## Blocker found,## Blocker search,## Blocker verification complete,## Blocker check: clean,## Blocker re-scan, and the shape named in the brief —## Blocker analysis still running("still" and "running" are both outside the list). Verified: each reads asisSectionOnlyHeading = falseon base and on the head, and each refuses with a finding carrying no label. None of them is a regression and none of them can swallow a defect, so this does not gate the change; a follow-up can widen the list (found,identified,search,complete,done,clean,round) or accept that the reviewer labels what it reports.SECTION_META_ONLY_REcomment says an id-less finding "can never be closed", while this same commit's D3 hint andisResolved's id-less arm say a trusted reviewer's phrase does close it — I verified the phrase closes it. The narrower true statement is "can never be closed by a citation". The new test titles likewise describe an intent ("a heading that reports no defect is never a finding") that a closed vocabulary does not fully implement. Wording only.resolutionHeadOk, while the id-ed citation arm applies the head discipline. Pre-existing at base and narrow — a restatement can only suppress its own line — noted because this diff is about making the restatement arm behave like its siblings.Verdict of record — ld-reviewer — head
86cf49991d: APPROVEmodel: deepseek-flash
session: 01a1ad36-eb9a-757a-b84d-276b1348e755
Exact-head seat 2 of 2, adversarial: I tried to close a finding as an untrusted identity around the new author gate, and to hide a real defect behind the new no-defect heading vocabulary. Every probe was a unit-level synthetic PR stream — no real PR was used and no network was touched.
Blockers
None.
Concerns
isResolutionRestatementis evaluated against the line being tested, so a restatement never cleared a DIFFERENT, earlier finding — the citation and phrase arms, which are the ones that can, were already identity-gated in1e7c397. I re-ran the whole attack matrix against the parent commit and every "the raised finding stays open" assertion already held there; what actually flipped at this head is that the untrusted restatement LINE is now reported. So the arm was a fail-open on reporting, not a live way to clear someone else's finding. The fix is still correct and strictly fail-closed; it is the framing ("any identity could clear a finding by posting that shape") that reads wider than the behaviour.toContain("1")should not be read as proof the line was reported): a label with no severity token (**B1 (census crash).** …) was never a finding on any path, and a bareresolved 1 — fixed in …line is a resolution line by design.verdict.ts:724callsfindOpenBlockers(items)withoutpolicy. Under a non-defaultFORGEJO_LANDING_REVIEWERS, the composer therefore reads the configured reviewer's own restatement as an open finding and would refuse to publish a--resolve-reviewverdict that the lock itself accepts. I confirmed the same divergence already exists for the phrase and citation arms, so this is a pre-existing pattern that this PR extends by one arm rather than a new class — one-line fix. The new "a policy member other than the default still counts" case does not cover the composer path because it passes the policy explicitly.## Blocker test applied — no defect,— no defects,— nothing found,— none, and: clean. Only— no defect foundis accepted, so the vocabulary boundary is one word wide; a reviewer who writes the shorter form meets exactly the defect D2 set out to remove.Adversarial probes and what survived them
fixed in <sha>, a bareresolved 1, and a quoted-example resolution. Each ran as a plain comment, as a formal review body, and as an inline comment; each also as a raise-and-close in ONE body, and as two untrusted comments (one raising, one closing). Every variant left the raised finding open.resolved <id>still closes a labelled finding; a trusted raise-and-close body still closes.## Blocker analysis: token leak,## Blocker test applied to the list endpoint,## Verification blocker: session fix,## Blocker — verified,## Blocker review findings,## Blocker test failures,## Blocker fix missing,## Blocker: no verification,## Blocker review of the tenant filter: leaksamong them. Prose, bullets and ordered items under a no-defect heading still fail closed, and a defect sentence on the heading line itself stays a finding.npm run test:deployedat this head: 26 of 26 files, 702 of 702 tests, typecheck clean.