Skip to content

Merge-queue triage guidance: the AssertionError vs timeout heuristic has a counterexample class — the discriminator is what the assertion MEASURES, not what kind of failure it is #13830

Description

@os-warren

Split out of #13691 by the triage seat. That card is one timing guard in driver-sql; this is the reusable correction its author flagged as "worth more than this single test" — and which, filed nowhere, would have closed along with it. Prose in a comment is invisible to candidate queries, sweeps and ageing alarms; this card is the record.

The standing guidance

The merge-queue triage comment instructs readers, verbatim:

「超时多半是负载/时序……断言(AssertionError: …)才指向真实的行为改变」

That heuristic is sound in general and is doing real work — the domain:devx seat used it the same day to refuse a flake reading on #13683 (an AssertionError on run-dev-unbuilt-workspace.e2e.test.ts), correctly, and that refusal is why that card exists.

The counterexample

#13691 is an AssertionError:

Temporal Conformance (live PG + MySQL)
AssertionError: this run took over a second, so second-precision stamps could have
differed too: expected 1006 to be less than 1000

By the heuristic this reads as "real behaviour change". It is not. The assertion is a validity precondition — the test requires its own run to fit inside one second so that a truncated second-precision stamp could not have distinguished the updates, which is what gives the following distinctness assertion its meaning. The real invariant (monotonicity, asserted on the line above) held.

⇒ The experiment was invalidated. The product was not falsified. And it was confirmed load-dependent by evidence: same head, same test, re-queued once with no change → success.

The correction

The discriminator is not AssertionError vs timeout. It is what the assertion measures.

assertion measures belongs with example
product behaviour (a value, a shape, a refusal) real behaviour change — investigate, ⛔ do not re-run away #13683
elapsed time, ordering under load, or any other precondition of the experiment's own validity timeouts — load-sensitive, and a re-run is a legitimate discriminator #13691

A validity-precondition assertion is a timing measurement wearing an AssertionError's clothes.

Why it is worth encoding rather than remembering

⚠️ Both readings are expensive in opposite directions, and the heuristic as written only protects one of them:

  • Treating a behaviour assertion as a flake → a real defect gets re-run until it passes. The heuristic exists to prevent this, and it does.
  • Treating a validity-precondition assertion as a behaviour change → an author is sent to debug a product that is not broken, on a red they cannot reproduce. Under the current wording this is the instructed outcome.

⭐ The tell is cheap and mechanical: the assertion's message or its compared values name a duration, a timestamp, a count of elapsed units, or an ordering that only holds under a time budget. expected 1006 to be less than 1000, with a message beginning "this run took over a second", is unambiguous on its face.

Scope

One paragraph in the merge-queue triage guidance (and, if the same wording is carried in the dispatch playbook, there too — grep for the 超时/断言 pair before editing, so the two copies do not diverge).

Refs

Activity

  1. claude commented on Aug 31, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 13830,
      "status": "done",
      "branch": "claude/issue-13830-queue-triage-discriminator",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/13966",
      "premise_still_valid": true,
      "summary": "Carrier grep on origin/main resolves the 超时/断言 pair to EXACTLY ONE carrier: .github/workflows/merge-queue-triage.yml (the two body-array lines that compose the posted triage comment). landing-operations.md, dispatch-runbook.md (the HELD file — so there is no would-be edit to record against PR #13948), and pm-dispatch/SKILL.md carry no copy, so neither stop condition fires; check-merge-queue-triage-outcome.mjs and the merge-queue fixtures name AssertionError as parser input and fixture bytes, not as guidance prose. Added the named exception in two places in that one file: one paragraph in the posted comment body, and a provenance section in the YAML header. The discriminator is what the assertion MEASURES — product behaviour (a value, a shape, a refusal) reads as a real behaviour change and is never re-queued away; a precondition of the experiment's own validity (elapsed time, ordering under load, anything holding only inside a time budget, the tell being that the message or the compared values name a duration/timestamp/elapsed count) reads WITH the timeouts, where one re-queue is a legitimate discriminator. The diff is 46 insertions, 0 deletions: the existing heuristic lines are byte-identical to origin/main, so the rule is named-around, not softened, and the re-queue allowance is untouched (stated as a boundary in both new passages). The card's measured pair is cited with issue numbers in the YAML header, this file's existing idiom for provenance; the posted body carries the two cases descriptively, as measured bytes, because that body is auto-posted on every red queue build and bare refs in it would mint a fresh cross-reference on both cards each time. No id-lint gate is in force on .github/workflows (check:pm-skill-id-lint scopes to .claude/skills/pm-dispatch), so that is a judgment on comment noise, declared in the PR body and reversible in one line. PR is draft; skip-changeset applied at open time (the diff publishes nothing) and confirmed surviving the size-labeler's whole-set write by comparison read-back.",
      "tests": "All on the final head 0183098e (= the PR head; nothing landed after the gate run). Gate union derived on that head with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` — the script's own change-set derivation, no hand-built path list; stderr confirmed the answer came from objectstack-ai/objectstack at 0183098e and that --repo held against the checkout's origin. It named 19 families. Ran all 19, plus the pin suite's --self-test leg (lint.yml invokes both) and the always-run check:nul-bytes = 21 invocations, 21 PASS, 0 red. Exit codes captured before any pipe (redirect to a file, then read it); verdicts quoted are the lines the gates themselves printed. Load-bearing ones: 'check-merge-queue-triage-outcome --self-test: 89 assertions, 17 mutations of the shipped script each driven to red' and 'check-merge-queue-triage-outcome: OK (73 assertions over 20 scenarios, driving the 28340-char script extracted from .github/workflows/merge-queue-triage.yml against real captured logs)' — the char count is the proof the edited YAML still parses and the comment body still composes; also 'check-workflow-status-functions: OK (scanned 29 workflow file(s), 52 job(s))' and 'check-nul-bytes: OK (scanned 7660 text file(s) ... no raw ASCII control bytes)'. Edits were confirmed on disk by grep -c on both injected anchors (1 and 1) before any measurement, and by git diff --stat reading 46 insertions / 0 deletions. No ablation was run: the change is static prose in a comment body, with no code path to mutate. DECLARED NARROWING — pnpm lint (repo-wide eslint) was narrowed to the diff's paths, and the narrowing is a measurement, not a skip, on three readings: (1) population read from eslint.config.mjs itself — every files: glob is ts/tsx/mts/cts/js/jsx/mjs/cjs, none names .yml; (2) count read from --format json — the one path returns a single result entry whose only message is 'File ignored because no matching configuration was supplied', errorCount 0, so the diff contributes zero linted files; (3) invariance for untouched files — no parserOptions.project and no projectService anywhere in the config (it states this at line 328), so with no cross-file type program a .yml edit cannot move any untouched file's verdict, and the diff edits no eslint config. CI runs the full farm anyway.",
      "mcp_calls": "1 — the claim comment on 13830. Everything else went zero-quota or core-bucket: issue body and comments off the public-repo page payload, all listing/dedup/PR-create/label/issue-create/read-back over repo-scoped REST (probed 200 in this container first), and the carrier grep over git.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #13967: the merge-queue triage comment's operative guidance prose is unpinned — check-merge-queue-triage-outcome.mjs pins extraction and cross-PR aggregation but reads the fenced excerpt only (by design, per excerptLines' own docblock), so both guidance lines could be deleted with the whole 19-family union staying green. Observation-class, labelled `finding`, unassigned; deliberately NOT fixed in this PR — different defect class from this card, and choosing which claims are load-bearing enough to pin is the judgment the card is for."
      ]
    }

    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

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions