Repository navigation
Gate: a test using a dist-resolved workspace dependency must pay its first load at module top — extend check:test-source-alias (2026-08-20 queue-incident layer ①) #10126
Description
Activity
- addedpriority:p0Critical: blocker, must ship before MVPCritical: blocker, must ship before MVPand removed
on Aug 20, 2026 Claim: spec seat, session
session_01URCaKuNTuK3BKJvwqM74QU· branchclaude/issue-10126-test-static-import-gate. Dispatched now on the maintainer's direct instruction (2026-08-20 live chat, verbatim: 「10127 关闭,10126 现在就派发执行」) — this supersedes waiting for the devx lane. Dev tier:opusper the card. Sibling note: #10127 was closed as not planned in the same ruling; #10112's re-scoped registry-shrink half remains the complementary card.
Generated by Claude Code
{ "issue": 10126, "status": "rework", "branch": "claude/issue-10126-test-static-import-gate", "pr": "https://github.com/objectstack-ai/objectstack/pull/10135", "premise_still_valid": true, "summary": "Layer 1 landed in full: check-test-source-alias.mjs gains a clocked-window limb — for every specifier a package really resolves through dist/ (the measurement KNOWN_UNALIASED_TEST_IMPORTS mirrors, read at specifier granularity), a test file that loads it via a dynamic import()/require() inside a function body with no module-scope load of the same specifier is a finding, with the PR #10120 remedy printed. Self-test 47 -> 57 assertions, each new leg observed failing and observed silent, including both-present-is-compliant and a decoy. Baseline sweep found 32 violations across 22 files in 9 packages, all repaired the #10120 way (module-top side-effect import); one cli file's redundant 60s beforeAll warm-up was removed with it. Status is rework, not done, because layer 4 (the convention sentence in AGENTS.md's testing conventions) is BLOCKED: AGENTS.md sits exactly on its shrink-only line ceiling (958, headroom 0), the mandated sentence costs +3 lines, and its own section has 2 lines of lossless rewrap headroom — see open_questions. The sentence is stated verbatim in the gate header and in the failure text, pinned by its own assertion, so no author meets the rule without meeting the sentence. One surprise worth flagging: ablating the function-body classifier surfaced a SILENT exemption it was already granting — serve-marketplace-offline-runtime-config.test.ts, whose helper signature ends `}): Promise< { … } > {` — which the sweep alone would never have shown; fixed, pinned as its own fixture, and the file repaired (32nd violation).", "tests": "Full workspace build first (turbo, 71 tasks successful, 6m06s), then per-package under flock. cli 135 files/1470 tests passed; runtime 177 files/2649 passed; objectql 221 files passed; rest 131 files/2134 passed; metadata-protocol 123 passed | 2 skipped (125); service-datasource 22 files passed; qa/dogfood 121 passed | 1 skipped (122), 880 passed | 3 skipped; examples/app-crm 4 files/42 passed; examples/embed-objectql 1 file/2 passed. typecheck EXIT=0 across all 9 packages. Gate union re-derived with `node scripts/pm/dispatch-gates.mjs <changed paths>` after the final commit and run: cross-package-test-inputs 0, dispatcher-error-vocabulary 0, docs-audit-scope 0, durability-log-level 0, pm-governed-merges 0, pm-governed-prose 0, pm-skill-id-lint 0, pm-skill-ratchet 0 (958/958 after the AGENTS.md revert), required-contexts 0, slot-lookup 0, test-source-alias 0, type-source-resolution 0, nul-bytes 0, query-options-erasure 0, type-check-coverage 0, type-check-debt --re-measure 0 (33 entries, 1924 raw errors, none above its recorded number), engine-double-contract 0, where-matcher 0, ratchet-remedy-authority 0, engine-split-ratio 0, affected-docs 0, spec empty-state/liveness/strictness-ledger/variant-docs 0. Additions beyond the named families that I derived and ran: ratchet-remedy-authority (it classifies THIS gate's refusal shape and the limb adds author-facing text) and the four spec-liveness gates (triggered by packages/qa/dogfood). Ablation, 4 legs, each predicted before running, each restored and verified byte-identical by git hash-object (1506cba4… twice, ff49d5a2…, 8f616ab5…), never by a matching --stat: (A) limb removed -> exactly the 4 finding-expecting assertions red, the 4 length===0/.every() legs vacuously green, repo scan green; (B) compliance leg disabled -> 3 red and the repo scan went 0 -> 38 findings, proving the 32 repairs plus 6 pre-existing compliant files are load-bearing; (C) every brace reads as a function body -> only the top-level-await leg red, plus the real miss above; (D) pre-repair angle walk restored -> only the typed-signature leg red, repo scan green (which is exactly why that fixture had to exist).", "open_questions": [ { "question": "Layer 4: where does the convention sentence live? AGENTS.md is capped at 958 lines by scripts/pm/check-skill-line-ratchet.mjs and origin/main is at exactly 958 (headroom 0). The maintainer-specified sentence — 'clocked windows measure behaviour, never loading — a test that boots a real plugin chain pays its first load at module top' — is 122 characters, so at the file's 90-column fill it costs 2 prose lines plus a blank: +3. Measured lossless rewrap headroom in the whole section it would join: 2 lines (type-check paragraph 7->6, the @ts-expect-error bullet 8->7); every other paragraph there is already at fill. The ratchet's own remedy is 'compress in place, or a maintainer ruling quoted in the raising PR'. Note the ruling I have (2026-08-20, 「立卡」, accepting layers 1+4) accepts the layer, not a ceiling raise, and was made without the collision in view — quoting it as authority to raise the roof would be exactly the stretch the ratchet exists to refuse.", "options": [ "A — maintainer rules the ceiling to 961 (+3); a one-commit follow-up adds the sentence to AGENTS.md § Build & Test and quotes the ruling in its PR body, per the ratchet's own escape hatch.", "B — a separate compression card lowers AGENTS.md by >= 3 lines first (the largest single lossless win in the file is 8 lines in the merge-discipline list around line 400); the sentence then lands inside the existing ceiling with headroom to spare.", "C — accept the sentence's current home: verbatim in the gate header AND in the failure text, pinned by a self-test assertion. Every author who trips the rule reads it; AGENTS.md carries nothing. Zero further work, but the convention is not in the binding instruction file.", "D — truncate the sentence to fit 1 line (e.g. 'Clocked windows measure behaviour, never loading — pay the first load at module top.', 84 chars, net-zero against the 2 lines of rewrap headroom). Lands today inside the ceiling, but edits a sentence the maintainer specified verbatim." ], "recommendation": "A, with B as the principled alternative. On real business need: the audience that caused this incident is agents, and agents read AGENTS.md — option C leaves the convention only where someone already failing meets it, which is remediation, not prevention. On long-term soundness: B is the architecturally cleanest (it pays down surface instead of raising a roof) but it makes a P0 remediation wait on an unrelated compression card, and the ratchet explicitly provides A as the sanctioned route for a deliberate protocol addition. On making AI-written code hard to get wrong: the gate already makes the mistake structurally impossible to merge, so the doc line is reinforcement rather than enforcement — which is why A's small, ruled, one-time cost beats D. I recommend against D specifically: silently trimming a maintainer-specified sentence to satisfy a line count is the kind of quiet edit that reads as the maintainer's wording to every later reader." } ], "out_of_scope_findings": [] }
Generated by Claude Code
Maintainer ruling on layer ④ (2026-08-20 live chat, session
session_01URCaKuNTuK3BKJvwqM74QU): option A selected from the four options in the os-dev report above — verbatim, untranslated: 「A — 抬上限到 961 (Recommended)」.Effect: the
scripts/pm/check-skill-line-ratchet.mjsceiling for AGENTS.md is ruled from 958 to 961 (+3), exactly the cost of the mandated convention sentence at 90-column fill. The follow-up commit adds the sentence — unedited — to AGENTS.md § Build & Test and its PR body quotes this ruling, per the ratchet's own escape hatch ("compress in place, or a maintainer ruling quoted in the raising PR"). This ruling authorizes THIS +3 for THIS sentence only; it is not a general license to raise the ceiling.Layer ④ is hereby unblocked; dispatching the follow-up to the same dev on the same branch/PR (#10135), which then becomes
Fixes #10126.
Generated by Claude Code
- added a commit that references this issue
on Aug 20, 2026 Supersedes the earlier report on this issue (comment 5353047299): layer ④ is now executed on the maintainer's ruling, so the status moves
rework→doneand PR #10135 now saysFixes #10126.{ "issue": 10126, "status": "done", "branch": "claude/issue-10126-test-static-import-gate", "pr": "https://github.com/objectstack-ai/objectstack/pull/10135", "premise_still_valid": true, "summary": "Both layers now land in PR #10135. Layer 1: check-test-source-alias.mjs gains the clocked-window limb — for every specifier a package really resolves through dist/, a test file that loads it via a dynamic import()/require() inside a function body with no module-scope load of the same specifier is a finding, carrying the PR #10120 remedy. Self-test 47 -> 57 assertions; baseline sweep found 32 violations across 22 files in 9 packages, all repaired the #10120 way. Layer 2 (the maintainer ruling round): AGENTS.md ceiling raised 958 -> 961 in scripts/pm/check-skill-line-ratchet.mjs — exactly +3, nothing more, with the ruling quoted verbatim and untranslated in the raising PR body as that gate's escape hatch requires — and the sentence added UNEDITED to AGENTS.md § Build & Test beside the sibling test-gate conventions. AGENTS.md lands at 961 (ceiling 961, headroom 0 again by construction); GitHub shows the file at +3 additions / 0 deletions, so nothing else in it moved. The gate header's now-stale note explaining why the sentence was NOT in AGENTS.md is corrected in the same commit. The ratchet's self-test pins coverage and shape (which files are covered, positive-integer ceilings, the root-file watch-hint declaration) and never a ceiling's numeric value, so it needed no extension — re-run green at 19/19.", "tests": "Layer 2 round: gate union re-derived with `node scripts/pm/dispatch-gates.mjs` over the new change set (AGENTS.md + scripts/pm/check-skill-line-ratchet.mjs now in it) — the derivation returned the same 20 families, since the ratchet script matches the `scripts/**` and `AGENTS.md/**` sources already present. All run green: pm-skill-ratchet 0 (AGENTS.md 961/961, self-test 19/19), pm-governed-prose 0, pm-governed-merges 0, pm-skill-id-lint 0, docs-audit-scope 0, required-contexts 0, cross-package-test-inputs 0, dispatcher-error-vocabulary 0, durability-log-level 0, slot-lookup 0, test-source-alias 0 plus --self-test at 57 assertions, type-source-resolution 0, nul-bytes 0, query-options-erasure 0, type-check-coverage 0, engine-double-contract 0, where-matcher 0, ratchet-remedy-authority 0, engine-split-ratio 0, affected-docs 0, spec empty-state/liveness/strictness-ledger/variant-docs 0. On-disk confirmation of the edits per contract: grep -c of the injected sentence = 1 for each half of it, grep -c of `['AGENTS.md', 961]` = 1 and of the old `['AGENTS.md', 958]` = 0; wc -l 961 and delta-vs-main exactly 3. One narrowing, declared: check:type-check-debt --re-measure was run green in the layer 1 round (33 entries, 1924 raw errors, none above its recorded number) and NOT re-run for the layer 2 commit, which touches only AGENTS.md and two scripts/*.mjs files — no .ts file it measures changed, so it cannot move a count; CI runs it regardless. Layer 1 round, unchanged and still the evidence for the test surface: full workspace build (71 tasks, 6m06s) then cli 135 files/1470 tests, runtime 177/2649, objectql 221 files, rest 131/2134, metadata-protocol 123 passed | 2 skipped, service-datasource 22 files, qa/dogfood 121 passed | 1 skipped and 880 passed | 3 skipped, examples/app-crm 4/42, examples/embed-objectql 1/2 — all passed, typecheck EXIT=0 across all 9. Ablation (layer 1) was 4 legs, each predicted before running and each restored byte-identical by git hash-object, never by a matching --stat; leg C surfaced a real SILENT exemption (serve-marketplace-offline-runtime-config.test.ts, signature ending `}): Promise< { … } > {`) that the sweep alone would never have shown, which became the 32nd violation and its own fixture.", "open_questions": [], "out_of_scope_findings": [] }
Generated by Claude Code
- added a commit that references this issue
on Aug 23, 2026
Filed by the spec seat on the maintainer's instruction (2026-08-20 live chat, verbatim: 「立卡」, accepting layers ①+④ of the queue-incident remediation menu). Provenance thread: #10115 (root cause) · PR #10120 (the instance fix) · #10112 (the mis-diagnosis record, re-scoped).
The incident this closes the class of
dev-plugin-security-enforcement-warning.test.ts(merged 01:53Z via PR #10092) deliberately boots the real, unmocked@objectstack/plugin-securitychain and paid its cold vite transform inside a clocked window (firstit()body → later abeforeAll). Measured: 3.1–3.6s idle on 4 vCPU (~70% of vitest's 5000ms default), ballooning to 20.26s on a starved core — past every clock, including a hypothetical 30s. The result wasTest timed out in 5000msonly under merge-queue full-suite load: 30 failed queue builds in 24h, main frozen 2h+, three innocent PRs ejected (#10105, #10003, #10008), and even the first fix PR (beforeAll warm-up — hooks are clocked too,hookTimeout10s) was ejected twice by the very test it fixed. The landed fix (PR #10120 head7ef8159b7) is a module-top static import — the cost moved OUT of every clocked window, verified immune at--hookTimeout=1 --testTimeout=1000.The mechanical rule
For every entry in
KNOWN_UNALIASED_TEST_IMPORTS(the registryscripts/check-test-source-alias.mjsalready keeps — workspace deps that test files resolve throughdist): any test file importing that dependency must reference it in a top-level static import, so the first paid load is at module scope, never insideit()/beforeAll/dynamic-import-in-body. A violation is exactly tonight's shape, caught at the offending PR's own CI instead of in the queue.Implementation shape (lead, not spec): one new check limb + self-test in the existing gate, matching its registry-plus-gate idiom. Detection: for each unaliased specifier, scan the package's test files — a dynamic
import()/requireof that specifier inside a function body with no top-level static import of the same specifier is a finding, reported against the test file with the incident-derived remedy ("add a module-topimport '<specifier>'so the transform is unclocked").Rider (layer ④, one prose line)
Add the convention sentence to the testing-conventions doc the gate's header points at: clocked windows measure behaviour, never loading — a test that boots a real plugin chain pays its first load at module top.
Dedup: #10112's re-scoped surviving half (shrink the unaliased registry by source-aliasing) is the adjacent, complementary card — aliasing removes entries from this gate's input; this gate covers whatever remains. Cross-linked, not duplicated. No other card names this rule (searched: test-source-alias / static import / clocked window).
Suggested size/model: S–M,
opus.