Skip to content

finding(check-governed-merges): the merge queue's build-to-land skew can put a governed landing BELOW a round's --since boundary, where it is silently absent from the audit list #12633

Closed

Description

@os-litant

Turned up while measuring #11996 (whether the audit sees a governed-surface PR that lands through a merge-queue batch). That question measured YES — every batch topology lists the governed file. This is a different, adjacent property: not the batch topology, but the sweep's window. Filed separately rather than fixed on #11996, whose disposition rule fires only on a blind-to-the-topology verdict.

The mechanism, measured

GitHub's merge queue writes a queue entry's commit when the batch is built, and main receives it when the batch lands. Those are not the same moment. Measured on this repo, on two governed PRs from one batch:

PR mainline commit committer date PR merged_at skew
#12440 (.claude/agents/os-dev.md + 2 references) 04600d939 @ 2026-08-26T05:00:27Z 2026-08-26T05:25:22Z ~25 min
#12443 (.claude/skills/pm-dispatch/SKILL.md + 1 reference) ade757fb3 @ 2026-08-26T05:00:49Z 2026-08-26T05:33:08Z ~32 min

Consequence: a commit can sit above another on main's first-parent chain (so it landed later) while carrying an earlier committer date. That is not hypothetical — over the last 3612 first-parent pairs on main:

inversions: 10 of 3612 pairs; max 874 s; median 539 s
max skew as a fraction of a 24h window: 1.012%

Two of those ten inverted commits are governed:

git merge-base --is-ancestor 98eec7e248 01a7337fc0 exits 0, so the ancestry direction is not an inference.

The measured miss, on real history

mainlineCommits() windows with git log --first-parent --since=DATE, which cuts on committer date. Set the boundary to the parent's date — exactly what --since-ref ROUND_TIP yields, and what "since the last round" yields whenever the last round ended between the two:

node scripts/pm/check-governed-merges.mjs --repos objectstack --since 2026-08-14T05:55:02Z --json
   scanned=1719  governed_entries=190   01a7337fc0 listed? False

node scripts/pm/check-governed-merges.mjs --repos objectstack --since 2026-08-14T05:44:52Z --json
   scanned=1721  governed_entries=191   01a7337fc0 listed? True
       -> 01a7337fc0 PR #8620  surfaces: docs/adr/0029-kernel-object-ownership-and-platform-objects-decomposition.md

Same tree, same landed set. The only difference is a boundary ten minutes earlier. A governed ADR landing that reached main after the boundary is absent from the first list, and nothing in the output marks the gap — no warning, no incomplete classification on that account. In the degenerate case where it is the only governed landing in the window, the sweep prints a clean window and exits 0 = "swept COMPLETELY", which is precisely the reading #4690 and #9902 say must never be producible.

Reproduced as a constructed signature too (queue chain whose governed entry is dated 874 s — the measured maximum — before its own first parent):

QS-7  build-land skew, --since at the previous round tip
      BLIND — governed file absent from the list   [exit 0, 0 entries, 0 mainline commits scanned]
QS-7c same tree, --since backed off by the measured 874 s max skew
      SEES — listed, attributed to the governed PR [exit 2, 1 entry, 2 mainline commits scanned]

What is NOT broken

The hole opens only when the window start falls between the governed commit's committer date and its landing time. The documented default invocation is safe by a wide margin: the largest measured skew is 874 s = 1.012% of a --since 24h window, and consecutive default rounds overlap by nearly a full day. The exposure is the knife-edge boundary — --since-ref ROUND_TIP, or an exact previous-round timestamp, which is what "实跑 --since 上轮" invites.

Also worth stating plainly: this is queue-specific. A direct merge writes its commit at merge time, so it produces no skew and no inversion.

Options (not taken here; this needs a ruling on windowing semantics)

  • A. Widen by a stated margin. Subtract a skew budget from any operator-supplied --since (and from --since-ref), and say so in the report line. Cheap, mechanical; costs some re-listing of already-recognised entries at each boundary, which the audit tolerates by design (a re-listed entry is recognised again; a dropped one is never seen).
  • B. Window on landing order, not on dates. Take --since-ref topologically: enumerate REF..origin/main instead of --since on the ref's date. That is exactly right for the round-to-round question and immune to any date skew, but it needs the previous round's ref to be recorded, which today it is not.
  • C. Detect and refuse. Notice a committer-date inversion crossing the boundary and classify the sweep INCOMPLETE rather than clean. Smallest behaviour change, keeps the check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690 posture, but reports a problem instead of answering the question.
  • D. Accept and document. State in the header that --since is a date window, not a landing window, and pin the default-invocation margin.

Recommendation: B for --since-ref, A for --since — B is the only one that makes the round-to-round reading exact, and A covers the operator who passes a bare timestamp. Both keep the audit's report-only posture and neither weakens any judgment.

Measured on tree 0043c9224 (plus 4be2e880e, which main fast-forwarded to mid-measurement). Gate baseline is untouched: node scripts/pm/check-governed-merges.mjs --self-test prints "157 assertions" and the lock's VERDICT line reads command-exit 0.

Back-link: #11996 (the measurement this came out of), #11831 (the ruling that chartered it).


Generated by Claude Code

Activity

  1. os-litant commented on Aug 27, 2026

    @os-litant
    ContributorAuthor

    Skills-lane grading (session session_01MnijPVVDakqK2J335JoJtq; the fix lands in scripts/pm/** = this lane's surface — labels pre-filled with this grading, triage may correct): filed bare by the measuring dev → pm:queue, type Bug. The #4690 invariant ("a sweep that missed something must never read clean") is declared and not enforced at a knife-edge --since boundary: mechanism named (build-to-land skew, 25–32 min measured), the miss reproduced on real history (190 vs 191 governed entries across a ten-minute boundary shift, the dropped entry a governed ADR landing) and as a constructed signature (QS-7 blind / QS-7c sees).

    Ruled route (seat discretion — restores a declared invariant, strengthens-only, no product semantics touched): the card's own recommendation, B for --since-ref + A for --since — topological windowing (REF..origin/main) for round-to-round refs, and a stated skew budget subtracted from bare timestamps, said in the report line so re-listed boundary entries read as re-recognition, not noise. C declined (reports a problem instead of answering the question); D declined (documents the hole instead of closing it). Implementation mechanics (how the previous round's ref is recorded/passed) are the implementing dev's design space.

    Rider from #11996's close: carry a one-line header note recording the queue-topology verdict — measured non-blind 2026-08-27 (six topologies, live replay), re-measure if the repo's merge method or the mainline-commits/commit-paths enumeration changes.

    Dispatching this round into the slot freed by #11996's close.


    Generated by Claude Code

  2. added
    pm:queueReady for the PM dispatch loop
    domain:skillsDomain lane — seat card indexed by label:pm:seat
    on Aug 27, 2026
  3. added theissue type on Aug 27, 2026
  4. os-litant commented on Aug 27, 2026

    @os-litant
    ContributorAuthor

    Claim: PM loop round R1 W1 (continuation — slot freed by the #11996 close)
    Session: session_01MnijPVVDakqK2J335JoJtq
    Branch: claude/issue-12633-governed-merges-since-skew
    Worktree: objectstack-issue-12633
    Domain: domain:skills
    File surface: scripts/pm/check-governed-merges.mjs (+ its --self-test register; includes the one-line header rider ruled at #11996's close) (stop on breach; explain in the report)
    Container & model: M (windowing semantics + self-test coverage), mode:subagent, model: opus — this dispatch's node scripts/pm/dispatch-gates.mjs --tier scripts/pm/check-governed-merges.mjs (tree 0043c92): "no path-derived mandate … floor sonnet · default opus · ceiling fable".
    Clause-②: no — report-only audit tooling; strengthens the sweep's completeness classification; no contract accept/reject behaviour change, no public surface widened.
    Serial constraints cleared: this file was held this wave by the #11996 measurement, which closed with zero code changes; the in-flight #12220 chain holds check-half-states.mjs (a different script) and #12518 holds os-verify-lock.sh; no open PR touches this file (walked at dispatch time). The grading comment above is the ruling this claim implements.


    Generated by Claude Code

  5. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 27, 2026
  6. self-assigned this
    on Aug 27, 2026
  7. os-litant commented on Aug 27, 2026

    @os-litant
    ContributorAuthor

    Dev claim (os-dev subagent under PM session session_01MnijPVVDakqK2J335JoJtq; under mode:subagent all seats share the PM's session id, so the branch is the identity marker for this claim).

    Branch: claude/issue-12633-governed-merges-since-skew
    Worktree: objectstack-issue-12633 (base aa5994e17)
    File surface: scripts/pm/check-governed-merges.mjs only (including its --self-test register and the one-line queue-topology header rider ruled at the #11996 close). Anything beyond that surface stops the run and is explained in the report instead.

    Implementing the ruled route from the grading comment above, not re-deciding it: B for --since-ref (topological window REF..origin/main, immune to committer-date skew) + A for --since (a stated skew budget subtracted from a bare timestamp, said out loud in the report line so a re-listed boundary entry reads as re-recognition rather than noise). C and D stay declined. The audit's report-only posture is unchanged and no judgment is weakened; the existing 157 --self-test assertions must stay green, with new assertions for the new window semantics including a QS-7-shaped regression pin (on a skew boundary a governed entry is either listed or the sweep is INCOMPLETE — never clean-and-absent).

    Assignee left as set by the PM; not touched.


    Generated by Claude Code

  8. os-litant commented on Aug 27, 2026

    @os-litant
    ContributorAuthor

    os-dev-report

    {
      "issue": 12633,
      "status": "done",
      "branch": "claude/issue-12633-governed-merges-since-skew",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12643",
      "premise_still_valid": true,
      "summary": "Implemented the ruled route in scripts/pm/check-governed-merges.mjs only (545+/49-, one file): --since-ref is now a TOPOLOGICAL window (first-parent REF..origin/main, resolved per repo, repeatable as --since-ref repoId=REF, with a named per-repo fallback when a ref does not resolve there) and a bare --since is a date boundary backed off by a declared SKEW_BUDGET_SECONDS = 3600, stated in the report line so a boundary re-listing reads as re-recognition. Added beyond route A, and reported as a fork: the date window is CLOSED TOPOLOGICALLY down to the deepest commit dated inside it, so an entry is never dropped for being dated below something that landed before it whatever the budget is, and a walk that never reached below the boundary marks that repo WINDOW EDGE — green tick suppressed, exit 2 — so the card's invariant holds mechanically rather than up to a constant. Report-only posture unchanged, no judgment weakened, no pre-existing assertion removed or altered. The #11996 rider is carried as the one-line header note it was ruled to be. Every sweep now prints the --since-ref line to record for the next round, which discharges the PM's flagged 'no recording mechanism' gap without adding state to the script. Two deviations from the dispatch text, both stated in the PR body: the pre-existing self-test baseline on this tree is 159 assertions, not the 157 the card recorded (main moved between measurement and dispatch), and WINDOW EDGE is a backstop that cannot fire on a complete clone (the #9902 horizon guard fires first on a shallow one) — said plainly in the header rather than implied to be the primary defence.",
      "tests": "Gate union re-run AFTER the final commit, on fdff4bad1, as one && chain under scripts/pm/os-verify-lock.sh — its own verdict line: 'os-verify-lock: VERDICT command-exit 0 · held the lock 146s (2m26s) · waited 0s'. Chain: check:pm-governed-merges, check:pm-governed-prose, check:pm-dispatch-gates, bare-root-worklist --self-test, check-governed-queue-guard --self-test, ci-failure --self-test, check:nul-bytes, check:agent-test-spelling, check:bash32-floor, check:cli-command-ids, check:cross-package-test-inputs, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check-ci-filter-parity (self-test + scan), check-skills-token-ratchet (self-test + scan), and whole-repo pnpm lint = 'eslint . --no-inline-config' with NO narrowing (65 s). Family derived by 'node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack' from the real change set, plus its two convention-triggered obligations for a gate-script edit, plus the three sibling scripts that IMPORT this module (an import is not a path literal, so the derivation cannot name them). Self-test: '✓ check-governed-merges --self-test: 193 assertions' (baseline 159 on this tree; +34, none removed or weakened). New assertions include the QS-7 regression pin in both directions — a control asserting the OLD bare date cut drops the governed entry, and the new window listing it — the topological close carrying an entry skewed PAST the budget, the unproven-boundary EDGE, the listed-or-INCOMPLETE invariant as a property loop over every fixture, the escalating floors (one walk in the ordinary case), per-repo --since-ref resolution with its named fallback, and the window words. REAL-HISTORY BOUNDARY REPLAY (enumeration only, i.e. the sweep minus its per-entry attribution GETs, which are unrelated to the window): BEFORE, on origin/main — '--since 2026-08-14T05:55:02Z scanned=1720 governed_entries=190 01a7337fc0 listed? false' vs '--since 2026-08-14T05:44:52Z scanned=1722 governed_entries=191 01a7337fc0 listed? true' (the card's 190/191 reproduced exactly). AFTER, this branch — both boundaries read 'scanned=1726 governed_entries=193 01a7337fc0 listed? true', i.e. the two readings that used to disagree by one governed ADR now agree. END-TO-END WITH REAL EXIT CODES on a constructed QS-7 repo (governed entry landed above the round tip, dated 874 s before it; no PR numbers in the subjects so the run costs zero API lookups): the pristine origin/main copy of the script prints '✅ clean window — no governed surface was merged in any governed repo.' at exit 0 over that landing, while this branch lists it at both --since (date + budget) and --since-ref (topological), and a sibling repo the ref does not resolve in falls back, names why, and still lists it. Inversion provenance re-measured locally on this mainline, not quoted from the card: 10 inversions in 3,613 first-parent pairs, max 874 s, median 539 s. Live default invocation on the real checkouts also exercised end to end ('--repos objectstack,objectui --since 24h'): 6 governed entries, window line and next-round --since-ref line printed; exit 2 from pre-existing attribution-channel failure in this container, unrelated to this change. No ablation was run: nothing here is a new gate whose ability to fail needed proving — the equivalent evidence is the pristine-upstream BEFORE run above, which is the unmutated tree rather than a mutation of mine.",
      "mcp_calls": "11 — issue_read get + get_comments, add_issue_comment (dev claim), create_pull_request, issue_read get_labels (failed: GraphQL cannot resolve a PR number as an Issue), pull_request_read ×2 (label read + read-back), issue_write (labels), search_issues (dedupe), issue_write (filed #12645), and this report comment. Channel switch declared: repo-scoped REST is 403 on this seat (probed 'GET /repos/objectstack-ai/objectstack/labels/skip-changeset' → 403), so labels went through the MCP whole-set write with a mandatory read-back — read back as ['size/l','skip-changeset'], the concurrent size label intact.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #12645: a PR-less mainline entry — the audit's loudest line — renders 'merged_by UNAVAILABLE — every channel failed' when the attribution loop skipped it entirely (zero channels tried), and points at an attribution note that is not printed for it; unassigned, unlabelled, left for PM triage"
      ]
    }

    PR #12643 is a draft and stays one. It touches scripts/pm/** only — --test on the final file list answers NOT governed, exit 0, derived from the register rather than recalled — and carries skip-changeset (CI-internal script, publishes nothing). CI convergence is the PM's to read; gate status at report time is in_progress.


    Generated by Claude Code

  9. os-litant commented on Aug 27, 2026

    @os-litant
    ContributorAuthor

    ACCEPT (reviewer of record: skills seat, session session_01MnijPVVDakqK2J335JoJtq, contract-review tier) — PR #12643. Verified against GitHub and the tree, not the report's narrative — review worktree at head fdff4bad1: ① self-test re-run by this seat: the +34 added / none removed claim holds exactly in this environment (157 → 191 here vs the dev's 159 → 193; the constant +2 on both legs is the generator-toolchain conditional rows, present in the dev's env and absent in this one — same delta both sides, so the counts reconcile mechanically); ② the card's own knife-edge probe re-run on the branch: the governed ADR entry the old cut dropped is PRESENT at the exact boundary that used to lose it; ③ the #11996 rider confirmed in the header verbatim (non-blind 2026-08-27, six topologies + live replay, named re-measure triggers); ④ PR shape: draft, base main, Fixes #12633 first line, one file, size/l + skip-changeset correct.

    Fork adjudication (the dispatch brief explicitly invited a better window semantics with the fork reported): part 3 — the topological close of the date window plus the WINDOW EDGE incomplete classification — is accepted as within the ruling's intent: it strengthens the ruled #4690 invariant from "holds up to a declared constant" to "holds mechanically" (listed or INCOMPLETE, never clean-and-absent), keeps the report-only posture, weakens nothing, and does not resurrect declined option C as the primary route (B and A remain the answers; EDGE is a backstop the header itself calls a backstop). The 3600 s budget's derivation (1.86× the largest directly measured skew, deliberately not pinned at today's worst case) is reasoned and stated in the report line where the operator reads it. The recording-gap discharge (printing next round's exact --since-ref line on every sweep, zero state added) is the right shape.

    Deviations reviewed and accepted: the 157/159 baseline drift (main moved between measurement and dispatch — and this review adds the toolchain-conditional explanation for the residual ±2); WINDOW EDGE's cannot-fire-on-complete-clone honesty note. Out-of-scope finding #12645 graded separately by this seat (same file ⇒ serial behind this PR's landing).

    Landing path: pure code, no governed path ⇒ seat-reviewed, ready + enqueue when every check on the head is green.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:skillsDomain lane — seat card indexed by label:pm:seat

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions