HOMELAB-1679: fix(forgejo): lock hardening — gated knobs off the server's repo name, the server-name rule, fail-closed evidence reads (LR-20261111-017, Amendments 15/16/18) #131

Merged
pi-landing merged 5 commits from lr-20261111-017-lockharden into main 2026-11-11 18:32:17 +00:00
Collaborator

Spec 017, consolidated per AMENDMENT 18: three stacked commits, each its own fix with its own old-code-failing tests. Supersedes #130 (N6) — closed with a reference; #125's r5 merges this branch. Gate-changing: two pane seats (via forgejo_comment_as as ld-reviewer) + homelab-93's security review + SEATSID before El Director's go. Base 51daf88 → head c4e93bbf20.

Commit 1 — 7e52108: gated knobs pinned, keyed off the SERVER's repo name (Amendment 15, homelab-93 (1)–(2))

On the two repos named in code (GO_GATED_REPOS), the trusted reviewer set AND FORGEJO_DANGEROUS_PATHS / FORGEJO_LEGACY_PANEL_OWNERS / FORGEJO_PREMIUM_MODELS stay at defaults — a value a session can set is not a gate (B3 class). The gated decision keys off pr.base.repo.full_name (threaded from getPR / the fetched PR; unreadable ⇒ pinned), never the requested string. Each ignored knob is logged once per call, by name only.
Old-code-failing tests: …DANGEROUS_PATHS cannot make a deploy/** change routine, …LEGACY_PANEL_OWNERS cannot switch a gated repo to the legacy panel, …PREMIUM_MODELS is ignored, says each ignored knob once … never echoes a value, pins an unreadable server name …, a gated repo the SERVER names cannot be widened by the session env.

Commit 2 — 1ddb150: the lock judges the repo the FORGE reports (review-88-pr130 B1/n1/n3/n5/n6, homelab-93 (1))

Every gate choice keys off pr.base.repo.full_name, case-normalized; a requested name that differs (one decode + case-fold) is REFUSED, naming both; an absent name refuses merges and the census. % is rejected in parseRepoRef and --repo. homelab-88's reproductions (Homelab/pr%6Fd, %48omelab/prod, Homelab/infra%2Dcore) each refuse or stay pinned.
Old-code-failing tests: refuses "Homelab/pr%6Fd", "%48omelab/prod", "Homelab/infra%2Dcore", …different repos, naming both, …reports no repo name at all, refuses mid-census too, refuses --repo with a percent-escape, says the line ONCE across a posting run.

Commit 3 — c4e93bb: incomplete evidence reads refuse on gated repos (Amendment 16, review B8, homelab-93 (3))

A live hole in today's tool: listAllPRComments silently stops at 300 comments, reviews are read 50-and-never-paged, and inline read errors are swallowed — a hold beyond the cut never reaches the ledger and the merge goes through. On gated repos: the comment cap REFUSES ("refuse, never truncate"), reviews page to the end under a hard refusing bound, inline 5xx refuses, the changed-file read bound refuses. Non-gated repos unchanged. Every pagination path fails closed — a cap only ever comes with a refusal.
Old-code-failing tests: hold as comment 301 REFUSES, 51st review SEES the hold, inline 502 REFUSES, inline bound, changed-file bound.

Notes for the r5 merge (homelab-93 (4))

One owner for GO_GATED_REPOS / isGoGatedRepo; repoKey decodes (superset of #125's); resolveLandingPolicy's second param widens to string | ServerRepoView (string still compiles); listAllPRComments/listPRFiles take an options object ({ maxPages }) — #125's call sites adapt.

Proof

56 files / 1268 pass (1 known pre-existing merge-identity ambient-token failure, identical on base); tsc --noEmit clean; every fix's red→green shown by stash. no independent walk: implementer-run checks only.

Lab-Run: LR-20261111-017

r2 round (stacked on c4e93bb): homelab-93's B1/B2/F3/F6, verified against Forgejo v14.0.3 source

  • B1 (was a live fail-open): the gated review read ended at the first short page — v14.0.3 cuts the DB page of 50 FIRST and only then drops other users' PENDING reviews, so one pending review shortens page 1 and a hold at row 51+ was never read (93's probe merged with the hold standing). Fix: the read ends by the SERVER's count — page 1, take X-Total-Count (DB rows, hidden included), read every page to ceil(total/50); missing/unparseable header refuses on gated repos; an ignored-paging server is detected (page N repeats page 1's first id) and falls back to one validated read. Red→green: hidden-PENDING row 51 refuses (9 red → 19 green across B1/B2).
  • B2 (false refusals + a 50-comment landing DoS): v14.0.3's comment/inline endpoints IGNORE page/limit — 6 'full' pages tripped the 300-cap with a false reason. Fix: length === X-Total-Count ⇒ complete in ONE request; short of the count ⇒ count-driven paging with the repeat-id detector; the 300 cap now fires only when paging genuinely works. Inline: one GET per review validated against comments_count.
  • F3: the fake now models v14.0.3 (issue/inline comments unpaged with total count; reviews unpaginated without page, DB page cut before the PENDING post-filter, count includes hidden rows); the B8 tests are re-expressed against that real behaviour.
  • F6: tests/landing-census-gated-policy-017.test.ts kills the census mutation m2 (policy = undefined / gated: false).

Pre-existing items homelab-93 listed, NOT fixed here (named for the record)

F4: combined-status 30-context cap and single-page PR-commit/verdict-cli reads; F5: FORGEJO_BASE_URL (the largest remaining lever — every read and the merge POST go there) and FORGEJO_LANDING_IDENTITY, still env-settable on gated repos. These predate this PR; per the lead, homelab-88 runs a read-only live check (does X-Total-Count arrive? do comment pages ignore page?) BEFORE this lands.

HEAD now 7813f704e2; base 51daf8842e; patch: piext-131-diff.patch (3030 lines) in the epic folder.

Spec 017, consolidated per AMENDMENT 18: three stacked commits, each its own fix with its own old-code-failing tests. Supersedes #130 (N6) — closed with a reference; #125's r5 merges this branch. Gate-changing: two pane seats (via `forgejo_comment_as` as ld-reviewer) + homelab-93's security review + SEATSID before El Director's go. Base 51daf88 → head c4e93bbf2008d29aa77e24715fab5b916bb29a5a. ## Commit 1 — `7e52108`: gated knobs pinned, keyed off the SERVER's repo name (Amendment 15, homelab-93 (1)–(2)) On the two repos named in code (`GO_GATED_REPOS`), the trusted reviewer set AND `FORGEJO_DANGEROUS_PATHS` / `FORGEJO_LEGACY_PANEL_OWNERS` / `FORGEJO_PREMIUM_MODELS` stay at defaults — a value a session can set is not a gate (B3 class). The gated decision keys off `pr.base.repo.full_name` (threaded from getPR / the fetched PR; unreadable ⇒ pinned), never the requested string. Each ignored knob is logged once per call, by name only. Old-code-failing tests: `…DANGEROUS_PATHS cannot make a deploy/** change routine`, `…LEGACY_PANEL_OWNERS cannot switch a gated repo to the legacy panel`, `…PREMIUM_MODELS is ignored`, `says each ignored knob once … never echoes a value`, `pins an unreadable server name …`, `a gated repo the SERVER names cannot be widened by the session env`. ## Commit 2 — `1ddb150`: the lock judges the repo the FORGE reports (review-88-pr130 B1/n1/n3/n5/n6, homelab-93 (1)) Every gate choice keys off `pr.base.repo.full_name`, case-normalized; a requested name that differs (one decode + case-fold) is REFUSED, naming both; an absent name refuses merges and the census. `%` is rejected in `parseRepoRef` and `--repo`. homelab-88's reproductions (`Homelab/pr%6Fd`, `%48omelab/prod`, `Homelab/infra%2Dcore`) each refuse or stay pinned. Old-code-failing tests: `refuses "Homelab/pr%6Fd"`, `"%48omelab/prod"`, `"Homelab/infra%2Dcore"`, `…different repos, naming both`, `…reports no repo name at all`, `refuses mid-census too`, `refuses --repo with a percent-escape`, `says the line ONCE across a posting run`. ## Commit 3 — `c4e93bb`: incomplete evidence reads refuse on gated repos (Amendment 16, review B8, homelab-93 (3)) A live hole in today's tool: `listAllPRComments` silently stops at 300 comments, reviews are read 50-and-never-paged, and inline read errors are swallowed — a hold beyond the cut never reaches the ledger and the merge goes through. On gated repos: the comment cap REFUSES ("refuse, never truncate"), reviews page to the end under a hard refusing bound, inline 5xx refuses, the changed-file read bound refuses. Non-gated repos unchanged. Every pagination path fails closed — a cap only ever comes with a refusal. Old-code-failing tests: `hold as comment 301 REFUSES`, `51st review SEES the hold`, `inline 502 REFUSES`, `inline bound`, `changed-file bound`. ## Notes for the r5 merge (homelab-93 (4)) One owner for `GO_GATED_REPOS` / `isGoGatedRepo`; `repoKey` decodes (superset of #125's); `resolveLandingPolicy`'s second param widens to `string | ServerRepoView` (string still compiles); `listAllPRComments`/`listPRFiles` take an options object (`{ maxPages }`) — #125's call sites adapt. ## Proof 56 files / 1268 pass (1 known pre-existing merge-identity ambient-token failure, identical on base); `tsc --noEmit` clean; every fix's red→green shown by stash. **no independent walk: implementer-run checks only.** Lab-Run: LR-20261111-017 ## r2 round (stacked on c4e93bb): homelab-93's B1/B2/F3/F6, verified against Forgejo v14.0.3 source - **B1 (was a live fail-open)**: the gated review read ended at the first short page — v14.0.3 cuts the DB page of 50 FIRST and only then drops other users' PENDING reviews, so one pending review shortens page 1 and a hold at row 51+ was never read (93's probe merged with the hold standing). Fix: the read ends by the SERVER's count — page 1, take `X-Total-Count` (DB rows, hidden included), read every page to `ceil(total/50)`; missing/unparseable header refuses on gated repos; an ignored-paging server is detected (page N repeats page 1's first id) and falls back to one validated read. Red→green: hidden-PENDING row 51 refuses (9 red → 19 green across B1/B2). - **B2 (false refusals + a 50-comment landing DoS)**: v14.0.3's comment/inline endpoints IGNORE page/limit — 6 'full' pages tripped the 300-cap with a false reason. Fix: `length === X-Total-Count` ⇒ complete in ONE request; short of the count ⇒ count-driven paging with the repeat-id detector; the 300 cap now fires only when paging genuinely works. Inline: one GET per review validated against `comments_count`. - **F3**: the fake now models v14.0.3 (issue/inline comments unpaged with total count; reviews unpaginated without `page`, DB page cut before the PENDING post-filter, count includes hidden rows); the B8 tests are re-expressed against that real behaviour. - **F6**: `tests/landing-census-gated-policy-017.test.ts` kills the census mutation m2 (`policy = undefined` / `gated: false`). ## Pre-existing items homelab-93 listed, NOT fixed here (named for the record) F4: combined-status 30-context cap and single-page PR-commit/verdict-cli reads; F5: `FORGEJO_BASE_URL` (the largest remaining lever — every read and the merge POST go there) and `FORGEJO_LANDING_IDENTITY`, still env-settable on gated repos. These predate this PR; per the lead, homelab-88 runs a read-only live check (does `X-Total-Count` arrive? do comment pages ignore `page`?) BEFORE this lands. HEAD now 7813f704e2756ae11f947b1d61f773b97256dd91; base 51daf8842e7c6cf81b5b9a47602030028a03fd4e; patch: piext-131-diff.patch (3030 lines) in the epic folder.
Amendment 15 / review N6 says a value a session can set is not a gate. #130
delivered the narrow half (FORGEJO_LANDING_REVIEWERS ignored on the two
GO-gated repos) but decided "gated" from the string the CALLER typed, and left
three quieter knobs live there.

(1) Every gate choice now keys off `pr.base.repo.full_name` — the name the
FORGE reports — threaded out of `getPR` in the merge gate and the census and out
of the fetched PR in the verdict helper. Review-88-pr130 B1 measured the bypass:
`parseRepoRef("Homelab/pr%6Fd")` reached `/repos/Homelab/pr%6Fd`, Forgejo
percent-decodes path params, so the write landed on Homelab/prod while
`repoKey` saw an ungated repo. There is no `%` decoding in the gate's input path
any more: the caller's spelling is irrelevant to the decision. A name that could
not be READ is pinned (`goGatedFromServerName`), never treated as ungated (n1).

(2) On a GO-gated repo ALL FOUR knobs fall back to their defaults —
FORGEJO_LANDING_REVIEWERS, FORGEJO_DANGEROUS_PATHS,
FORGEJO_LEGACY_PANEL_OWNERS, FORGEJO_PREMIUM_MODELS. Pinning reviewers alone
left `FORGEJO_DANGEROUS_PATHS=nothing/**` able to make a `deploy/**` change
routine, a widened legacy-owner list able to swap the panel rule, and a model
added to the premium list able to upgrade a seat. Each ignored override is said
once per call, by NAME only (n3: the line never echoes the value).

The census resolves per row (each row is its own read) behind a de-duplicating
sink, so "once per call" still holds for the operator.

#125's review r4 carries the same N15 pin inside the lock; same names, same
values, same semantics — whichever lands first, the gated policy is the default.

Tests (each fails on #130's code): the pins file's four gated cases and the two
server-name cases in the N6 file; the non-gated controls assert today's
behaviour is untouched. The #694 replay is updated to the truth the pin implies:
a gated repo can no longer be declared legacy through the env, so its panel is
judged by the DEFAULT owners (session rule), and the cured stream is asserted
under an explicitly built era policy.

Lab-Run: LR-20261111-017
Amendment 15's gate was still decided from the string the CALLER typed.
Review-88-pr130 B1 measured the consequence: `parseRepoRef` and `request()`
passed the owner/repo through unchanged, Forgejo runs `url.PathUnescape` over
those path parameters, so `Homelab/pr%6Fd` ADDRESSED `Homelab/prod` while
`repoKey` saw an ungated repo — the trusted set widened, no line printed, and
the merge went out.

Now, at the one place a PR is read (`collectLandingEvidence`):
- the gated decision and every gate choice key off `pr.base.repo.full_name`,
  one URL-decode and case-fold behind it (`serverRepoView`);
- the requested name is used ONLY to refuse: a disagreement is one line naming
  both;
- an unreadable name REFUSES a merge and the census row, and PINS the verdict
  helper — a missing name must never mean "ungated" (n1);
- a `%` anywhere in a repo ref is refused outright, in `parseRepoRef` and in
  the verdict helper's `--repo` parser, naming the character class.

`repoKey` now also decodes one level, so a percent-encoded spelling of a gated
repo matches the gated list even if it reaches a comparison (defence in depth,
not the gate).

n1 delivery: the merge tool, the census and `verdict-cli` all carry the SERVER's
gate decision; undoing any one of them is caught by a test that drives the tool
over a fake forge, not by a unit test handed a ref.
n5: a posting run reads the PR twice, so `runCli` hands `collectState` a
de-duplicating sink — one line per ignored knob, not two.
n6: docs/forgejo.md now says the four overrides are not levers on a GO-gated
repo and that gated-ness comes from the forge's own name.

Tests (12 of 13 are red on the previous commit, and red on #130):
`forgejo_merge_pr refuses "Homelab/pr%6Fd" | "%48omelab/prod" |
"Homelab/infra%2Dcore" and sends no merge` (the homelab-88 reproductions),
`refuses when the request and the forge's own name are different repos`,
`refuses when the forge reports no repo name at all`, `refuses mid-census too`,
`refuses --repo with a percent-escape`, `says the line ONCE across a posting
run`, plus `serverRepoView` unit cases (still pinned + logged for a
percent-encoded request against a gated server name).

Lab-Run: LR-20261111-017
fix(forgejo): an incomplete evidence read refuses on GO-gated repos (LR-20261111-017, Amendment 16, review B8, homelab-93 (3))
All checks were successful
CI / pr-title (pull_request) Successful in 2s
CI / pi-otel-telemetry (pull_request) Successful in 50s
CI / ask-user-question (pull_request) Successful in 50s
CI / refocus (pull_request) Successful in 56s
CI / skill-args (pull_request) Successful in 52s
tests / python (pull_request) Successful in 8s
CI / forgejo-ci (pull_request) Successful in 1m10s
CI / mcp-bridge (pull_request) Successful in 26s
CI / nats-intercom (pull_request) Successful in 28s
CI / plane (pull_request) Successful in 1m12s
CI / todo (pull_request) Successful in 1m23s
CI / redact (pull_request) Successful in 54s
CI / pi-friction (pull_request) Successful in 57s
CI / forgejo (pull_request) Successful in 2m13s
CI / pi-swarm-suite (pull_request) Successful in 4m2s
c4e93bbf20
Review-88-pr125-r4 B8 measured the fail-open shape: a hold can be posted as any
comment, any review body or any inline note, and every evidence read had a cap
that said nothing when it stopped — so a hold behind filler (comment 301, the
51st review, an inline comment whose read 5xx'd) was absent from the ledger
while an earlier exact-head approval was, and the merge went out with the hold
standing.

On a GO-gated repo (this PR's server-name gate) that is now a REFUSAL:
- general comments: the 6x50 page cap throws, naming the cap, instead of
  returning a truncated record;
- reviews: paged to the end of the list under a hard refusing bound (a full
  last page at the bound refuses) — the 51st review body is READ, not missed;
- inline comments: enumerated through the COMPLETE review list the lock already
  read (a second capped review read would re-introduce the bound), each
  per-review list paged to its end under a refusing bound, and a read error
  refuses instead of being swallowed (B8's 502);
- changed files: the dangerous-class input had the same silent cap (ten full
  pages), so a `.tf` or `deploy/**` change past it under-reported the class and
  dropped the panel rule with it — it now refuses.

Non-gated repos are unchanged, and each case is asserted both ways so the split
cannot widen by accident. The gated list is NOT duplicated here: `api.ts` uses
`isGoGatedRepo` from `landing.ts`, so there is one owner for the set (#125
redefines `GO_GATED_REPOS`/`repoKey` in that same file with the same values and
semantics — a three-way merge keeps one copy).

Tests (5 red on the previous commit, 6 non-gated controls green by design):
`a hold as comment 301 … REFUSES on a gated repo, naming the cap, and sends no
merge`, `a hold in the 51st review body … SEES the hold and refuses`, `an inline
read that answers 502 … REFUSES`, `every pagination path refuses at its bound …
one review's inline comments` and `… the changed-file list`.

Lab-Run: LR-20261111-017
Collaborator

LD-VERDICT APPROVED head=c4e93bbf2008d29aa77e24715fab5b916bb29a5a session=LR-20261111-017-seatB-20261111T1722Z

Reviewed a read-only clone of the branch at the pinned head (git rev-parse on origin/lr-20261111-017-lockharden = c4e93bbf20, base 51daf88): the full base..head diff, all three commits, 14 files, read end to end. Full suite 1268/1269; the one failure is tests/merge-identity.test.ts (ambient landing token) and is identical on the untouched base — pre-existing, not a hold. tsc --noEmit clean.

(1) homelab-93 (1) — the gate keys off the SERVER's name. All four landing call sites pass a ServerRepoView decided from pr.base.repo.full_name: index.ts:361 (merge/census evidence, requireReported: true → refuses an absent or disagreeing name), index.ts:533 (policy), index.ts:1136-1142 (census, per row, refusing rows reported not-landable with the reason), verdict-cli.ts:483 (helper, absent name PINNED gated rather than read as ungated, +990-996 fallback gated:true). I grepped every gated decision: none is fed by the requested string — the requested ref appears only in the refusal comparison (landing.ts:3096-3106) and in display (details.repoRef). % refused in parseRepoRef (index.ts:274-280) and --repo (verdict-cli.ts:157-162), which also refuses anything but a single slash. repoKey decodes once, case-folds, last two segments — so Homelab/pr%6Fd, %48omelab/prod, Homelab/infra%2Dcore cannot address a gated repo under another name.

(2) homelab-93 (2) — the four knobs cannot loosen a gated repo: knob() (landing.ts:3195-3196) returns the default for dangerousPaths / premiumModels / legacyPanelOwners / reviewerIdentities whenever the view is gated, so a widened reviewer set, an emptied dangerous-path list, an added legacy owner or a promoted model is inert there; each ignored variable is logged once per call, by name and repo key only, never by value (landing.ts:3178-3190; dedupe sinks index.ts:1074-1078, verdict-cli.ts:897-905). Only those four env reads exist in landing.ts, so there is no fifth lever.

(3) homelab-93 (3) — every pagination cap in the diff now comes WITH a refusal on a gated repo: comments (api.ts:2161-2168), reviews paged to the end under a refusing bound (api.ts:2187-2202), per-review inline comments (api.ts:2230-2247, inline read enumerated through the reviews already read to their end, and a 5xx is no longer swallowed — index.ts:465-476), changed files (api.ts:2536-2551). No silent truncation is introduced anywhere in the diff.

Red→green, measured not asserted: with the four source files (api.ts, index.ts, landing.ts, verdict-cli.ts) stashed back to base and the new fixtures kept, 27 of the 38 tests in the four new files go red — the comment-301 hold, the 51st-review hold, the inline 502, the inline bound, the changed-file bound, all three percent-encodings, both requested-vs-reported disagreements, the absent-name refusal (merge, census and helper), and both knob pins. The other 11 stay green on base and green at head: the non-gated halves (silent comment cap of 6 pages, single 50-row review page, swallowed inline error, unenforced inline bound, silent file cap, honoured env). So non-gated behaviour is pinned unchanged, not merely left untested. The named tests exist and pin their fixes.

No holds.

Two non-blocking findings, recorded so they are not lost:

  1. verdict-cli.ts:330 (paged() maxPages=10) and verdict-cli.ts:596-600 (inline read errors swallowed) keep the B8 shape on the helper's own preview path — it caps silently at 500 and swallows the inline failure. It cannot produce a wrongful merge (the merge gate refuses at 300 comments on a gated repo, so any PR where the helper's read was short is unmergeable), but the --resolve-review finding check reads a possibly truncated record. Worth a follow-up, not this PR's scope.
  2. landing.ts:3192-3193 says an explicitly empty override still means "none"; split() returns null for an empty value, so it falls back to the default (fail-safe, and pre-existing behaviour). Comment wording only.

model: omniroute/deepseek-flash
session: LR-20261111-017-seatB-20261111T1722Z

LD-VERDICT APPROVED head=c4e93bbf2008d29aa77e24715fab5b916bb29a5a session=LR-20261111-017-seatB-20261111T1722Z Reviewed a read-only clone of the branch at the pinned head (git rev-parse on origin/lr-20261111-017-lockharden = c4e93bbf2008d29aa77e24715fab5b916bb29a5a, base 51daf88): the full base..head diff, all three commits, 14 files, read end to end. Full suite 1268/1269; the one failure is tests/merge-identity.test.ts (ambient landing token) and is identical on the untouched base — pre-existing, not a hold. `tsc --noEmit` clean. (1) homelab-93 (1) — the gate keys off the SERVER's name. All four landing call sites pass a `ServerRepoView` decided from `pr.base.repo.full_name`: index.ts:361 (merge/census evidence, `requireReported: true` → refuses an absent or disagreeing name), index.ts:533 (policy), index.ts:1136-1142 (census, per row, refusing rows reported not-landable with the reason), verdict-cli.ts:483 (helper, absent name PINNED gated rather than read as ungated, +990-996 fallback gated:true). I grepped every gated decision: none is fed by the requested string — the requested ref appears only in the refusal comparison (landing.ts:3096-3106) and in display (`details.repoRef`). `%` refused in parseRepoRef (index.ts:274-280) and `--repo` (verdict-cli.ts:157-162), which also refuses anything but a single slash. `repoKey` decodes once, case-folds, last two segments — so `Homelab/pr%6Fd`, `%48omelab/prod`, `Homelab/infra%2Dcore` cannot address a gated repo under another name. (2) homelab-93 (2) — the four knobs cannot loosen a gated repo: `knob()` (landing.ts:3195-3196) returns the default for dangerousPaths / premiumModels / legacyPanelOwners / reviewerIdentities whenever the view is gated, so a widened reviewer set, an emptied dangerous-path list, an added legacy owner or a promoted model is inert there; each ignored variable is logged once per call, by name and repo key only, never by value (landing.ts:3178-3190; dedupe sinks index.ts:1074-1078, verdict-cli.ts:897-905). Only those four env reads exist in landing.ts, so there is no fifth lever. (3) homelab-93 (3) — every pagination cap in the diff now comes WITH a refusal on a gated repo: comments (api.ts:2161-2168), reviews paged to the end under a refusing bound (api.ts:2187-2202), per-review inline comments (api.ts:2230-2247, inline read enumerated through the reviews already read to their end, and a 5xx is no longer swallowed — index.ts:465-476), changed files (api.ts:2536-2551). No silent truncation is introduced anywhere in the diff. Red→green, measured not asserted: with the four source files (`api.ts`, `index.ts`, `landing.ts`, `verdict-cli.ts`) stashed back to base and the new fixtures kept, 27 of the 38 tests in the four new files go red — the comment-301 hold, the 51st-review hold, the inline 502, the inline bound, the changed-file bound, all three percent-encodings, both requested-vs-reported disagreements, the absent-name refusal (merge, census and helper), and both knob pins. The other 11 stay green on base and green at head: the non-gated halves (silent comment cap of 6 pages, single 50-row review page, swallowed inline error, unenforced inline bound, silent file cap, honoured env). So non-gated behaviour is pinned unchanged, not merely left untested. The named tests exist and pin their fixes. No holds. Two non-blocking findings, recorded so they are not lost: 1. verdict-cli.ts:330 (`paged()` maxPages=10) and verdict-cli.ts:596-600 (inline read errors swallowed) keep the B8 shape on the helper's own preview path — it caps silently at 500 and swallows the inline failure. It cannot produce a wrongful merge (the merge gate refuses at 300 comments on a gated repo, so any PR where the helper's read was short is unmergeable), but the `--resolve-review` finding check reads a possibly truncated record. Worth a follow-up, not this PR's scope. 2. landing.ts:3192-3193 says an explicitly empty override still means "none"; `split()` returns null for an empty value, so it falls back to the default (fail-safe, and pre-existing behaviour). Comment wording only. model: omniroute/deepseek-flash session: LR-20261111-017-seatB-20261111T1722Z
Collaborator

LD-VERDICT APPROVED head=c4e93bbf2008d29aa77e24715fab5b916bb29a5a session=LR-20261111-017-seatA-20261111T1722Z

Independent clone, branch lr-20261111-017-lockharden, current head equals the pin; judged the full base 51daf88..c4e93bb diff (three commits) and the checks only.

What I checked

  1. homelab-93 (1) — every gated decision keys off the server's name. goGatedFromServerName (landing.ts:3055) and serverRepoView (landing.ts:3081) are the only producers of a gated answer, and the only production consumers are the merge gate (index.ts:361 with requireReported: true, applied at index.ts:533), the census (index.ts:1136, resolved per row) and the verdict helper (verdict-cli.ts:483 → 646). Grepped every resolveLandingPolicy( call site: no gate is fed a requested string. A requested-vs-reported disagreement refuses naming both, an absent name refuses on merge and census, and % is refused in parseRepoRef (index.ts:275) and in --repo (verdict-cli.ts:157).
  2. homelab-93 (2) — the four knobs cannot loosen a gated repo: landing.ts:3179/3195 return the defaults for all four on a gated view, including an explicitly empty override (?? not ||), and each set-and-ignored knob is logged once, by name, never by value.
  3. homelab-93 (3) — every cap in the diff comes with a refusal: comments past 300 refuse (api.ts:2142), reviews page to the end under a refusing bound (api.ts:2179), inline reads page and a 5xx propagates (api.ts:2204/2236), the changed-file bound refuses (api.ts:2527). No new silent truncation anywhere in the diff; the only silent caps left are the pre-existing non-gated ones, pinned as such.
  4. Each named test exists and pins its fix. Stash-check against the new fixtures: commit-1 tests on the base source → 8 red, the gated cases red behaviourally (env override honoured on a gated repo) while all 6 non-gated pins stay green; commit-2 tests on parent 7e52108 → 12 red (the three percent-encodings, absent name, different-repo naming both, mid-census, --repo %, the once-only line); commit-3 tests on parent 1ddb150 → exactly the 5 gated B8 cases red (comment-301, 51st review, inline 502, inline bound, changed-file bound) with the 6 non-gated pins green.
  5. Non-gated behaviour unchanged: the negative controls assert today's silent 6-page comment cap, the single 50-row review page, the swallowed inline 502 and the silent file cap — all green. Fixture edits only add base.repo.full_name and page-aware routes; no assertion is weakened except two verdict-694-replay expectations, which now assert the pin working and additionally assert the era policy through an explicitly built policy — noted, not a hold.
  6. Head run: 56 files, 1268 pass, 1 fail = tests/merge-identity.test.ts:144 (pre-existing ambient landing token, same on the untouched base — not a hold). tsc --noEmit clean.

Holds: none.

Observations (non-blocking): (a) a legacy-era replay on Homelab/infra-core now refuses under the pinned panel — intended by the amendment and asserted; (b) requireReported: true makes a PR response without base.repo.full_name a merge refusal — fail-closed by design, a live-shape dependency worth knowing; (c) the head-date fallback still reads listPRCommits as one 50-row page (api.ts:2364) — pre-existing, untouched here, still fails closed to null.

model: omniroute/deepseek-flash
session: LR-20261111-017-seatA-20261111T1722Z

LD-VERDICT APPROVED head=c4e93bbf2008d29aa77e24715fab5b916bb29a5a session=LR-20261111-017-seatA-20261111T1722Z Independent clone, branch lr-20261111-017-lockharden, current head equals the pin; judged the full base 51daf88..c4e93bb diff (three commits) and the checks only. **What I checked** 1. homelab-93 (1) — every gated decision keys off the server's name. `goGatedFromServerName` (landing.ts:3055) and `serverRepoView` (landing.ts:3081) are the only producers of a gated answer, and the only production consumers are the merge gate (index.ts:361 with `requireReported: true`, applied at index.ts:533), the census (index.ts:1136, resolved per row) and the verdict helper (verdict-cli.ts:483 → 646). Grepped every `resolveLandingPolicy(` call site: no gate is fed a requested string. A requested-vs-reported disagreement refuses naming both, an absent name refuses on merge and census, and `%` is refused in `parseRepoRef` (index.ts:275) and in `--repo` (verdict-cli.ts:157). 2. homelab-93 (2) — the four knobs cannot loosen a gated repo: landing.ts:3179/3195 return the defaults for all four on a gated view, including an explicitly empty override (`??` not `||`), and each set-and-ignored knob is logged once, by name, never by value. 3. homelab-93 (3) — every cap in the diff comes with a refusal: comments past 300 refuse (api.ts:2142), reviews page to the end under a refusing bound (api.ts:2179), inline reads page and a 5xx propagates (api.ts:2204/2236), the changed-file bound refuses (api.ts:2527). No new silent truncation anywhere in the diff; the only silent caps left are the pre-existing non-gated ones, pinned as such. 4. Each named test exists and pins its fix. Stash-check against the new fixtures: commit-1 tests on the base source → 8 red, the gated cases red behaviourally (env override honoured on a gated repo) while all 6 non-gated pins stay green; commit-2 tests on parent 7e52108 → 12 red (the three percent-encodings, absent name, different-repo naming both, mid-census, `--repo` %, the once-only line); commit-3 tests on parent 1ddb150 → exactly the 5 gated B8 cases red (comment-301, 51st review, inline 502, inline bound, changed-file bound) with the 6 non-gated pins green. 5. Non-gated behaviour unchanged: the negative controls assert today's silent 6-page comment cap, the single 50-row review page, the swallowed inline 502 and the silent file cap — all green. Fixture edits only add `base.repo.full_name` and page-aware routes; no assertion is weakened except two verdict-694-replay expectations, which now assert the pin working and additionally assert the era policy through an explicitly built policy — noted, not a hold. 6. Head run: 56 files, 1268 pass, 1 fail = tests/merge-identity.test.ts:144 (pre-existing ambient landing token, same on the untouched base — not a hold). `tsc --noEmit` clean. **Holds:** none. **Observations (non-blocking):** (a) a legacy-era replay on Homelab/infra-core now refuses under the pinned panel — intended by the amendment and asserted; (b) `requireReported: true` makes a PR response without `base.repo.full_name` a merge refusal — fail-closed by design, a live-shape dependency worth knowing; (c) the head-date fallback still reads `listPRCommits` as one 50-row page (api.ts:2364) — pre-existing, untouched here, still fails closed to null. model: omniroute/deepseek-flash session: LR-20261111-017-seatA-20261111T1722Z
Review-93 measured the second round against the live server's source
(/home/aaron/repos/homelab/forgejo-src, v14.0.3), and both paging models the
previous commit assumed are wrong.

B1 (BLOCKING fail-open). ListPullReviews cuts the page of 50 in the DATABASE
first and only then drops other users' PENDING reviews (ToPullReviewList), so
page 1 can hold 49 visible rows while page 2 exists — and `batch.length < 50`
read that as the end of the list. A hold in review row 51 was never read, an
earlier exact-head approval counted, and the merge went out with the hold
standing. X-Total-Count is CountReviews: it counts the DB rows, hidden ones
included. On a gated repo the read now takes the total from page 1 and reads
every page up to ceil(total/50); a missing or unparseable header REFUSES (one
line), because then a short page cannot be told from the end of the list. A
server that ignores `page` (page 1 already held the whole counted list, or page
2 repeats page 1's first row) is read ONCE and validated against the count.

B2 (availability + false refusal). ListIssueComments builds its options with no
ListOptions, so v14.0.3 IGNORES page/limit and answers every comment on every
call: six "full" pages of the same N rows made a false "more than 300" refusal,
and ~50 filler comments blocked any gated landing. The count now decides: a
first read whose length equals X-Total-Count is the complete list (ONE request,
no duplicates, no false cap), a read short of it is paged to ceil(total/50), and
a repeated first row means the endpoint ignores paging and refuses. The 300 cap
now fires only when paging genuinely works and the cap is truly reached. The
per-review inline endpoint is unpaged too and sends no total, so it is ONE GET
validated against that review's comments_count; a short read pages once, stops
at a repeated page, and refuses — no 100-request loop, and no false "more than
5000" for a review whose comments were all read.

F3 (the tests proved behaviour the server does not have). The fake now models
v14.0.3: issue and inline comments are UNPAGED (issue comments carry
X-Total-Count), reviews are unpaginated when `page` is absent and paged when it
is present, PENDING rows are hidden AFTER the DB page is cut, and the count
still includes them. Opt-in switches (commentsPaging, reviewsPaging,
reviewsTotalCount, commentsTotalCount) exist only to pin the defensive paths and
the missing-header refusals. The B8 tests are re-expressed against that server:
comment 301 is READ via the count (the hold refuses, not a false cap), the 51st
review via count-driven paging, the inline bound via comments_count.

Tests, red on c4e93bb with the corrected fake and green here (9 red → green):
B1 — `pages to the SERVER'S COUNT and refuses on the hold in row 51`, `REFUSES
when the review read carries no X-Total-Count`, `REFUSES when the comment read
carries no X-Total-Count`, `reads ONCE when the review endpoint ignores paging,
and refuses a short read`; B2 — `reads the whole list once, SEES the hold, and
sends no merge on a gated repo`, `reads 60 comments in ONE request and lets the
merge through`, `reads exactly 300 comments completely and lets the merge
through`, `reads exactly 5000 inline comments ONCE and lets the merge through`,
`REFUSES when a review reports more inline comments than can be read`. The
non-gated controls and the genuine-cap paging case stay green on both.

F4 (statuses/commits/verdict-cli paging) and F5 (FORGEJO_BASE_URL and
FORGEJO_LANDING_IDENTITY) are PRE-EXISTING and deliberately NOT touched here;
they are recorded in the PR body.

Lab-Run: LR-20261111-017
test(forgejo): pin the CENSUS policy path on a GO-gated repo (LR-20261111-017, review-93 F6, mutation m2)
All checks were successful
CI / pr-title (pull_request) Successful in 1s
CI / forgejo-ci (pull_request) Successful in 1m12s
CI / skill-args (pull_request) Successful in 49s
CI / pi-otel-telemetry (pull_request) Successful in 1m17s
CI / ask-user-question (pull_request) Successful in 51s
CI / mcp-bridge (pull_request) Successful in 21s
CI / plane (pull_request) Successful in 1m25s
CI / todo (pull_request) Successful in 1m2s
tests / python (pull_request) Successful in 7s
CI / nats-intercom (pull_request) Successful in 32s
CI / refocus (pull_request) Successful in 48s
CI / pi-friction (pull_request) Successful in 37s
CI / redact (pull_request) Successful in 41s
CI / forgejo (pull_request) Successful in 1m54s
CI / pi-swarm-suite (pull_request) Successful in 4m16s
7813f704e2
homelab-93's mutation m2 set the census policy (index.ts ~:1138) to undefined
and the whole suite still passed (1269 tests): nothing exercised the census on a
GO-gated repo with a WIDENED environment. The census would then report a gated
PR as landable under a session-set reviewer set — the merge still refuses, but
the queue the operator reads would say "landable", which is the same class of
defect as the gate being movable.

The new test censuses a gated PR whose only exact-head approval comes from a
reviewer identity added through FORGEJO_LANDING_REVIEWERS, and asserts BOTH that
the row is not landable ("no exact-head verdict") and that the resolution really
happened for that row ("FORGEJO_LANDING_REVIEWERS is set and ignored on GO-gated
repo homelab/infra-core" on stderr) — so an undefined policy is caught, not only
an honoured env. The control runs the same fixture on a non-gated repo, which is
landable with that verdict, so the pin cannot pass by refusing everywhere.

Verified against both mutations of that line: `policy = undefined` fails both
tests, and `gated: false` fails the gated one.

Lab-Run: LR-20261111-017
Collaborator

LD-VERDICT APPROVED head=7813f704e2756ae11f947b1d61f773b97256dd91 session=LR-20261111-017-seatB-20261111T1810Z

Seat B, run LR-20261111-017. The PR head is exactly 7813f704e2, so the pin holds; I judged the full diff 51daf88..7813f70, with the judgment concentrated on the r2 delta c4e93bb..7813f70 (api.ts, tests/fake.ts, the B8 evidence tests, the F6 census test).

What I checked (r2):

  • B1 (reviews): the gated read now takes X-Total-Count from page 1 and reads every page to ceil(total/limit), so a page shortened by a PENDING row filtered AFTER the DB cut no longer ends the read; api.ts refuses when the header is absent (and Number.isFinite refuses a non-numeric one read through parseInt). The fake cuts the DB page first and then hides PENDING rows, and its count includes the hidden rows — i.e. the fixture reproduces the measured fail-open rather than a friendlier server. The hold at review row 51 is reached and the refusal is the hold leg, not a cap.
  • B2 (comments/inline): completeness is length === X-Total-Count → ONE request (no six duplicated pages, no false 300-cap on 60 or exactly 300 comments); short of the count → count-driven paging bounded by maxPages BEFORE the loop; a page repeating page 1's first id is detected and refuses instead of looping or silently accepting a short read; the final all.length !== total check closes the comment path. Inline reads once per review and validates against comments_count (5000 inline read in one call, over-count refuses naming the review), and a paging-ignoring endpoint is detected rather than causing a 100-request loop.
  • F3: the fake now models v14.0.3 (unpaged issue/inline comments with total count; reviews unpaginated without page, DB cut before the PENDING post-filter); commentsPaging/reviewsPaging are opt-in exceptions used only to pin the defensive paths.
  • F6: landing-census-gated-policy-017.test.ts asserts BOTH halves (pinned reviewer set on the gated repo with the "set and ignored" line, and the non-gated control still landable).
  • Earlier commits reviewed too: all four knobs pinned on GO-gated repos with the decision keyed off pr.base.repo.full_name (serverRepoView, requireReported on merge + census, pinned not ungated for the verdict helper), one URL-decode + case-fold with a % refusal, and the B8 fail-closed evidence reads.

Red→green spot-checks (scratch copies, no tracked file touched):

  • m1 — restored the old short-page break in listAllPRReviews → a page shortened by a hidden PENDING review (B1) goes red (merge goes through with the hold standing).
  • m2 — census policy = undefined → both F6 tests red.
  • m3 — removed the count-at-once return in listAllPRComments → 3 B2 tests red (false 300 + hold-shaped one-request assertions).

Suite on the exact head (deps symlinked as instructed): 57 files, 1278 passed, 1 failed — only tests/merge-identity.test.ts (ambient landing token), and I confirmed the same test fails identically on the untouched base 51daf88 in a worktree: pre-existing, not a hold.

Holds: none.
Observation, not a hold (n1): forgejo/tests/fake.ts can omit X-Total-Count (reviewsTotalCount/commentsTotalCount: false) but cannot send a non-numeric one, so the unparseable-header refusal (NaN → Number.isFinite false → refuse) is correct code with no test behind it; a totalCountGarbage option would pin it cheaply.

Exercised only the diff and the checks: no other seat's verdict, no PR comments or reviews read.

model: omniroute/deepseek-flash
session: LR-20261111-017-seatB-20261111T1810Z

LD-VERDICT APPROVED head=7813f704e2756ae11f947b1d61f773b97256dd91 session=LR-20261111-017-seatB-20261111T1810Z Seat B, run LR-20261111-017. The PR head is exactly 7813f704e2756ae11f947b1d61f773b97256dd91, so the pin holds; I judged the full diff 51daf88..7813f70, with the judgment concentrated on the r2 delta c4e93bb..7813f70 (api.ts, tests/fake.ts, the B8 evidence tests, the F6 census test). What I checked (r2): - B1 (reviews): the gated read now takes `X-Total-Count` from page 1 and reads every page to `ceil(total/limit)`, so a page shortened by a PENDING row filtered AFTER the DB cut no longer ends the read; `api.ts` refuses when the header is absent (and `Number.isFinite` refuses a non-numeric one read through `parseInt`). The fake cuts the DB page first and then hides PENDING rows, and its count includes the hidden rows — i.e. the fixture reproduces the measured fail-open rather than a friendlier server. The hold at review row 51 is reached and the refusal is the hold leg, not a cap. - B2 (comments/inline): completeness is `length === X-Total-Count` → ONE request (no six duplicated pages, no false 300-cap on 60 or exactly 300 comments); short of the count → count-driven paging bounded by maxPages BEFORE the loop; a page repeating page 1's first id is detected and refuses instead of looping or silently accepting a short read; the final `all.length !== total` check closes the comment path. Inline reads once per review and validates against `comments_count` (5000 inline read in one call, over-count refuses naming the review), and a paging-ignoring endpoint is detected rather than causing a 100-request loop. - F3: the fake now models v14.0.3 (unpaged issue/inline comments with total count; reviews unpaginated without `page`, DB cut before the PENDING post-filter); `commentsPaging`/`reviewsPaging` are opt-in exceptions used only to pin the defensive paths. - F6: `landing-census-gated-policy-017.test.ts` asserts BOTH halves (pinned reviewer set on the gated repo with the "set and ignored" line, and the non-gated control still landable). - Earlier commits reviewed too: all four knobs pinned on GO-gated repos with the decision keyed off `pr.base.repo.full_name` (`serverRepoView`, `requireReported` on merge + census, pinned not ungated for the verdict helper), one URL-decode + case-fold with a `%` refusal, and the B8 fail-closed evidence reads. Red→green spot-checks (scratch copies, no tracked file touched): - m1 — restored the old short-page break in `listAllPRReviews` → `a page shortened by a hidden PENDING review (B1)` goes red (merge goes through with the hold standing). - m2 — census `policy = undefined` → both F6 tests red. - m3 — removed the count-at-once return in `listAllPRComments` → 3 B2 tests red (false 300 + hold-shaped one-request assertions). Suite on the exact head (deps symlinked as instructed): 57 files, 1278 passed, 1 failed — only `tests/merge-identity.test.ts` (ambient landing token), and I confirmed the same test fails identically on the untouched base 51daf88 in a worktree: pre-existing, not a hold. Holds: none. Observation, not a hold (n1): `forgejo/tests/fake.ts` can omit `X-Total-Count` (`reviewsTotalCount/commentsTotalCount: false`) but cannot send a non-numeric one, so the unparseable-header refusal (NaN → `Number.isFinite` false → refuse) is correct code with no test behind it; a `totalCountGarbage` option would pin it cheaply. Exercised only the diff and the checks: no other seat's verdict, no PR comments or reviews read. model: omniroute/deepseek-flash session: LR-20261111-017-seatB-20261111T1810Z
Collaborator

LD-VERDICT APPROVED head=7813f704e2756ae11f947b1d61f773b97256dd91 session=LR-20261111-017-seatA-20261111T1810Z

Head verified: the PR's current head IS 7813f704e2 (base 51daf88), so the pin holds and there is nothing to refuse. Judged the full 51daf88..7813f70 diff, with the r2 delta c4e93bb..7813f70 (api.ts, tests/fake.ts, the two 017 test files) read closely.

WHAT I CHECKED (r2)

  1. B1 — the gated review read ends by the SERVER's count, not by page length. listAllPRReviews reads page 1, refuses when X-Total-Count is absent (api.ts:2297) or unparseable (the same guard: parseInt of a non-numeric header yields NaN, which Number.isFinite rejects), then reads every DB page up to ceil(total/50) (api.ts:2306-2317). The fixture is the live shape (one other reader's PENDING row shortening page 1 to 49 visible of 51, hold in DB row 51): page 2 IS requested and the hold refuses.
    SPOT-CHECK m1: in a scratch copy I restored the old short-page break in that path — the B1 test goes RED in the fail-open direction (refusal null: the merge goes through with the hold unread). Red→green confirmed.
  2. B2 — comments: length === X-Total-Count is accepted as complete after ONE GET, a short read is paged by the count, and the 300 cap now fires only when paging genuinely works (the commentsPaging opt-in server). Unpaged endpoint ⇒ one request, no duplidated pages, no false cap: 60 comments and exactly 300 comments both land in 1 read; 301 against a paging server still refuses naming the cap. Inline: one GET per review validated against comments_count, with the repeat-id detector ending the attempt instead of walking 100 pages; a review reporting 5000 but serving 60 refuses after 2 reads, 5000 served 5000 accepts after 1.
  3. F3 — the fake now models v14.0.3: issue comments unpaged with an exact count; inline unpaged, no total; reviews unpaginated unless page is sent, DB page cut BEFORE the PENDING post-filter, X-Total-Count counting hidden rows. Both switches default to the honest server, the exceptions are opt-in.
  4. F6 — the census test does kill the m2 class. SPOT-CHECK m2: in a scratch copy I dropped the gated view from the census policy resolution (resolveLandingPolicy(process.env, undefined, policyLog) at index.ts:1136) — the gated row comes back canMerge: true and landing-census-gated-policy-017 goes RED; its non-gated control stays green.
  5. Also read: the goGatedFromServerName / serverRepoView seam and % refusal (r1), the fail-closed wrappers on every gated evidence read, docs/forgejo.md consistency.

CHECKS
Suite at the pinned head: 1278 passed, 1 failed — tests/merge-identity.test.ts (landing token assertion). That failure reproduces identically at base 51daf88 in this environment (ambient landing token), so it is pre-existing, not a hold. tsc --noEmit clean.

HOLDS: none.

Non-blocking notes, not holds:

  • api.ts:2326 — unlike the comment path, the gated review path has no final all.length === total check. That asymmetry is deliberate and sound: the only unreadable rows are other readers' PENDING reviews, and reading every DB page up to ceil(total/50) already covers all readable rows.
  • The old non-gated "silent 300 cap" test was removed without a replacement (a paging NON-gated server is no longer pinned); production behaviour off the gated list is unchanged, so this is a coverage nit only.
  • No test covers a non-numeric (rather than absent) X-Total-Count; the code refuses it via Number.isFinite, so it is a test gap, not a defect.

model: omniroute/deepseek-flash
session: LR-20261111-017-seatA-20261111T1810Z

LD-VERDICT APPROVED head=7813f704e2756ae11f947b1d61f773b97256dd91 session=LR-20261111-017-seatA-20261111T1810Z Head verified: the PR's current head IS 7813f704e2756ae11f947b1d61f773b97256dd91 (base 51daf88), so the pin holds and there is nothing to refuse. Judged the full 51daf88..7813f70 diff, with the r2 delta c4e93bb..7813f70 (api.ts, tests/fake.ts, the two 017 test files) read closely. WHAT I CHECKED (r2) 1. B1 — the gated review read ends by the SERVER's count, not by page length. `listAllPRReviews` reads page 1, refuses when `X-Total-Count` is absent (api.ts:2297) or unparseable (the same guard: `parseInt` of a non-numeric header yields NaN, which `Number.isFinite` rejects), then reads every DB page up to `ceil(total/50)` (api.ts:2306-2317). The fixture is the live shape (one other reader's PENDING row shortening page 1 to 49 visible of 51, hold in DB row 51): page 2 IS requested and the hold refuses. SPOT-CHECK m1: in a scratch copy I restored the old short-page break in that path — the B1 test goes RED in the fail-open direction (`refusal` null: the merge goes through with the hold unread). Red→green confirmed. 2. B2 — comments: `length === X-Total-Count` is accepted as complete after ONE GET, a short read is paged by the count, and the 300 cap now fires only when paging genuinely works (the `commentsPaging` opt-in server). Unpaged endpoint ⇒ one request, no duplidated pages, no false cap: 60 comments and exactly 300 comments both land in 1 read; 301 against a paging server still refuses naming the cap. Inline: one GET per review validated against `comments_count`, with the repeat-id detector ending the attempt instead of walking 100 pages; a review reporting 5000 but serving 60 refuses after 2 reads, 5000 served 5000 accepts after 1. 3. F3 — the fake now models v14.0.3: issue comments unpaged with an exact count; inline unpaged, no total; reviews unpaginated unless `page` is sent, DB page cut BEFORE the PENDING post-filter, `X-Total-Count` counting hidden rows. Both switches default to the honest server, the exceptions are opt-in. 4. F6 — the census test does kill the m2 class. SPOT-CHECK m2: in a scratch copy I dropped the gated view from the census policy resolution (`resolveLandingPolicy(process.env, undefined, policyLog)` at index.ts:1136) — the gated row comes back `canMerge: true` and `landing-census-gated-policy-017` goes RED; its non-gated control stays green. 5. Also read: the `goGatedFromServerName` / `serverRepoView` seam and `%` refusal (r1), the fail-closed wrappers on every gated evidence read, docs/forgejo.md consistency. CHECKS Suite at the pinned head: 1278 passed, 1 failed — `tests/merge-identity.test.ts` (landing token assertion). That failure reproduces identically at base 51daf88 in this environment (ambient landing token), so it is pre-existing, not a hold. `tsc --noEmit` clean. HOLDS: none. Non-blocking notes, not holds: - api.ts:2326 — unlike the comment path, the gated review path has no final `all.length === total` check. That asymmetry is deliberate and sound: the only unreadable rows are other readers' PENDING reviews, and reading every DB page up to `ceil(total/50)` already covers all readable rows. - The old non-gated "silent 300 cap" test was removed without a replacement (a paging NON-gated server is no longer pinned); production behaviour off the gated list is unchanged, so this is a coverage nit only. - No test covers a non-numeric (rather than absent) `X-Total-Count`; the code refuses it via `Number.isFinite`, so it is a test gap, not a defect. model: omniroute/deepseek-flash session: LR-20261111-017-seatA-20261111T1810Z
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Homelab/pi-extensions!131
No description provided.