Skip to content

Commit 2dc5889

Browse files
fix(pm): the clause-② path limb and SUSPECT_TIER_GLOBS except spec test files (#19948)
Fixes #19936 Clause-②: no The clause-② enqueue gate's path limb read every file under `packages/spec/src`, while the contract-review rule owes an at-tier review for non-test files there only. For a test-only spec PR from an off-tier seat, the two rules gave opposite answers, so the PR could never enqueue. The ruling on the card (comment 5805897677, letter A) settles it toward the review rule: the path limb and `SUSPECT_TIER_GLOBS` except test files, using the repo's own test-file predicate. `contract-review.md` is unchanged. ## What changed ### `scripts/pm/dispatch-gates.mjs` - **`SUSPECT_TIER_GLOBS`**: the `packages/spec/src/**` entry gains `except: isTestPath` and an `exceptWhy` reason. The glob and its `why` are byte-identical, so `check-widening-tells.mjs`, which builds its contract surface from `g.glob` and `g.why`, sees no change. - **`deriveTier`**: a suspect glob's `except` is applied per path, before the suspicion is recorded. A mixed diff keeps the suspicion on its contract files. `MANDATORY_TIER_GLOBS` has no exception and its semantics are unchanged. - **The predicate is imported, not respelled**: `isTestPath` from `scripts/check-undeclared-dep-imports.mjs`. The docblock above the table says why this one was chosen (see the table below). - **Docs**: a new docblock section on `SUSPECT_TIER_GLOBS`, one sentence in the clause-② header paragraph, and one on `deriveTier`. - **Self-test**: 20 new cases, listed under Verification. One existing census row moved from `dispatch-gates.mjs:708` to `:713`, because the import shifted that file's own `inherited-population` declaration by five lines. That census names each declaration by file and line on purpose. ### `.claude/skills/pm-dispatch/references/landing-operations.md` line 10 The line is rewritten in place and no line is added. ```diff -- 路径肢 = diff 触及契约面 `packages/spec/src/**`,含 error-code-ledger 与 `*.zod.ts` 契约 schema。 +- 路径肢 = diff 触及契约面 `packages/spec/src/**` 非测试,含 error-code-ledger、`*.zod.ts` 契约 schema。 ``` The line goes from 111 to 119 bytes. `非测试` is the exact word `contract-review.md` line 9 uses. To stay under the 120-byte line cap, 与 became 、, with no change in meaning. The file is still 101 lines, at its ceiling of 101. ## Which test-file predicate, and why (measured) The ruling says to take "the repo's own test-file predicate, not a new spelling". The repo has several. Each was run over five shape probes and over the 1541 tracked files under `packages/spec/src` on this branch: | predicate | shape probes excepted | tracked files excepted | `*.zod.ts` excepted | |---|---|---|---| | `check-undeclared-dep-imports.mjs` `isTestPath` (exported) ⭐ | 5 of 5 | 499 | 0 | | `check-position-name-fold-loaders.mjs` `isTestFile` (exported) | 5 of 5 | 499 | 0 | | `check-examples-live-imports.mjs` (not exported) | 5 of 5 | 499 | 0 | | `check-dispatcher-error-vocabulary.mjs` `isTestFile` (not exported; also bare `fixtures/` anywhere) | 5 of 5 | 499 | 0 | | `check-runner-env-posture.mjs` `isTestFile` (also `.bench.`, `.pin.`, bare `fixtures/`) | 5 of 5 | 500 | 0 | | `check-adr-0087-registration.mjs` `TEST_FILE_RE` (filename only) | 2 of 5 | 500 | 0 | | this file's own `isTestFilePath` (filename only, deliberately) | 2 of 5 | 499 | 0 | | `tenant-audit-census.mjs` / `isystem-census.mjs` `isTestPath` (treats `qa/` as a test dir) | 4 of 5 (misses `test/`) | 501 | **1** (`qa/testing.zod.ts`) | The five shape probes cover the ruling's four shapes: `stack.test.ts`, `type-alias-convention.pin.test.ts`, a helper under `__tests__/`, a fixture under `__tests__/`, and a fixture under `test/`. `isTestPath` was chosen because its gate asks the ruling's exact question: which files under a package's `src/` are published source, and which are its tests. It is exported and it covers all four shapes. Its only test directories are `test/`, `tests/`, `__tests__/` and `__fixtures__/`, none of which is a contract domain under `packages/spec/src`. A predicate that also treats `qa/` as a test directory would drop a real contract: `packages/spec/src/qa/testing.zod.ts` is a real contract schema, and it stays a SUSPECT. That case is pinned. ## The lit case (ruling item 3) `node scripts/pm/dispatch-gates.mjs --tier --repo objectstack-ai/objectstack packages/spec/src/type-alias-convention.pin.test.ts` (the one file PR #19932 changes): - base `43460b95`: prints the SUSPECT block, and its path row reads `- packages/spec/src/type-alias-convention.pin.test.ts ⇢ 'packages/spec/src/**' — the contract surface (error-code ledger, *.zod.ts contract schemas) — the normal landing zone of a clause-② card`. - head `ef4d57c0`: no line contains `SUSPECT`. It prints `Model tier — no path-derived mandate: the surface hits none of the 3 declared glob(s) …` and the standing clause-② note (`Clause ② is NOT reachable from paths: … This line is a FLOOR, never a clearance.`). Same command on `packages/spec/src/ui/view.zod.ts` at head `ef4d57c0`: the SUSPECT block still prints, and its path row reads `- packages/spec/src/ui/view.zod.ts ⇢ 'packages/spec/src/**' — the contract surface …`. The block's header line names the contract-review tier's model id, so it is not quoted here. ## Verification (at HEAD `ef4d57c0`) The gates came from `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, run with no paths (36 commands). Five more came from the dispatch-time list. Each exit code was captured before any pipe. The `--ran` result: `✓ dispatch-gates --ran: 36 derived famil(ies) accounted for — 36 run, 0 NOT-MEASURED`. | gate | exit | verdict line | |---|---|---| | `pnpm check:pm-dispatch-gates` | 0 | `✓ dispatch-gates self-test: 1925 cases pass.` (831 s, run detached as its header prescribes) | | `pnpm check:pm-skill-ratchet` | 0 | `landing-operations.md is 101 lines (ceiling 101; headroom 0)` | | `pnpm check:pm-skill-id-lint` | 0 | `34 file(s) clean` | | `node scripts/check-scripts-symbol-anchors.mjs` | 0 | `3668 anchors across 281 scripts resolve` | | `node scripts/check-self-test-wired.mjs` | 0 | `every one of the 229 script(s) CI runs that ship a --self-test has that self-test run by CI` | | `node scripts/pm/check-governed-queue-guard.mjs --self-test` | 0 | `279 cases pass` | | `node scripts/check-skills-token-ratchet.mjs` | 0 | `54 authored bundle file(s) within their ceilings` | | `pnpm check:pm-widening-tells` (not derived; it imports this table) | 0 | `525 cases pass` | | `pnpm --filter @objectstack/lint run check:doc-formula-expressions` | 0 | The first run exited 3 (PREREQUISITE NOT MET: `formula` and `lint` were not built). It exited 0 after `turbo run build` of those two packages under the verify lock. | | `pnpm check:nul-bytes` | 0 | `scanned 9353 text file(s) … no raw ASCII control bytes` | | the other 30 derived or listed commands | 0 | green | The 20 new self-test cases: - The lit case raises no suspicion. Its rendering has no SUSPECT line and still carries the clause-② note. - A `*.zod.ts` path is still a suspect. - Five shape cases are each excepted: a plain test, a pin test, a `__tests__/` helper, a fixture under `__tests__/`, and a fixture under `test/`. - A mixed diff keeps its contract file. - `qa/testing.zod.ts` stays a suspect while `qa/testing.test.ts` is excepted. - A directory surface that contains the contract surface is still a suspect. - A test-named file under a mandated root keeps its mandate. - The exception is the imported function, and its home is a gate script, so a card editing it derives this gate by gate-script identity. - Every excepting entry carries a reason. - Live: none of the 203 tracked `*.zod.ts` files is excepted, and the exception really drops tracked files. - End to end on the CLI: `--tier` on the lit case prints no SUSPECT line, and `--tier` on `view.zod.ts` does. **Ablation**, from committed state: - Mutation: `deriveTier`'s guard `if (hintCovers(g.glob, p) && !(typeof g.except === 'function' && g.except(p))) {` became `if (hintCovers(g.glob, p)) {`. It was applied through `scripts/ablation-replace.mjs` in WRAP mode, and the file hashes show it landed: anchor 1 → 0, replacement 0 → 1, blob `caf66526bc7d` → `f2f8a50e2b5e`. - Predicted before the run: 11 red. Observed: `✗ dispatch-gates self-test: 11 of 1925 case(s) failed.`, the predicted 11 exactly. They are the two lit-case cases, the five shape cases, the mixed diff, the `qa/testing.test.ts` case, the live non-vacuity case, and the CLI lit case. - The zod, `qa/testing.zod.ts`, ancestor, mandate, provenance and live-zod cases stayed green, as they should with no exception applied. - Restore: blob after restore `caf66526bc7d` equals HEAD, `git diff HEAD` is empty, and `git status --porcelain` is empty. **eslint, narrowed** to the one changed `.mjs` file: - Population: `eslint --print-config` shows 2 rules (`no-restricted-imports`, `comment-swallow/no-code-inside-block-comment`), both per-file, and no `parserOptions.project`. - Count: `--format json` reports 1 file, 0 errors, 0 warnings. - Invariance: no type-aware linting is enabled, so this diff cannot move any untouched file's verdict. The `.md` file is not linted. NOT MEASURED: `pnpm check:published-readme-exports`, exit 3. It needs a full `pnpm build` of the workspace, which was not run. It is an artifact-roster family printed for every card. This diff touches no package, no package README and no type surface. ## Acceptance notes - `check-widening-tells.mjs`, the mechanical half of the declaration limb, imports `SUSPECT_TIER_GLOBS` and `hintCovers` from this file. It already excludes tests with its own filename-only predicate (`isContractSourceFile`). A `__tests__/` helper under `packages/spec/src` would still be read there for tells. There are none on the tree today. That is outside this card's surface and was not changed. - A derivation gap that predates this PR: `dispatch-gates.mjs --commands scripts/pm/dispatch-gates.mjs` does not name `pnpm check:pm-widening-tells`, although that gate imports from this file. The same holds for its other two imports (`scripts/regen-artifacts.mjs`, `scripts/pm/check-half-states.mjs`). The documented remedy is a module constant in the importing gate, the `SURFACE_MODULE` shape in `check-dispatch-gates.mjs`. The gate was run here anyway. Carrier: none. - `packages/spec/src/benchmark.bench.ts` stays a SUSPECT. Neither the chosen predicate nor the ruling's four shapes name bench files. - The ruling's `__tests__/**` shape would also except a `*.zod.ts` placed under a test directory. The package's `files[]` entry for `src` zod files would still ship such a file. None exists today, and the new live pin reds the day one does. ## 维护者速读(草稿) **改了什么**:入队闸门的「路径肢」(`landing-operations.md` 第 10 行)和 `dispatch-gates.mjs` 的嫌疑表 `SUSPECT_TIER_GLOBS`,现在都把 `packages/spec/src/**` 下的测试文件排除在外。排除用的是仓里现成的测试文件判据(`check-undeclared-dep-imports.mjs` 的 `isTestPath`),直接 import,没有另写一份。`--tier` 对只改 spec 测试的 diff 不再打印 SUSPECT 行;改 `*.zod.ts` 的照旧打印。`contract-review.md` 未动。 **为什么改**:按裁决 A(评论 5805897677,维护者「同意」)。两条规则原先对「只改 spec 测试文件的 PR」互相矛盾:一条要求先有达档复核记录,另一条又说不许为它另起复核。结果出自不达档席位的这类 PR 永远进不了队列,眼下卡住的是 PR #19932。测试文件不随包发布,改它不改变任何对外契约。 **风险与代价(含回滚)**:这道门放宽了,但只放宽到「测试文件」。另有两处自测直接对着真实文件树兜底:仓里 203 个 `*.zod.ts` 一个都不许被排除;`qa/testing.zod.ts` 这种名字像测试、实为契约的文件必须照旧打嫌疑行。消融实测:去掉排除,恰好预期的 11 条自测变红。强制档表(MANDATORY)没有任何排除。回滚:revert 本 PR 即可,无数据迁移。 **席位意见**: **你要做的**:无需操作。本 PR 属 Tier S(`.claude/**` + `scripts/pm/**`),由 skills 席位完成达档复核后入队;PR #19932 在本 PR 落地后入队。 --- _Generated by [Claude Code](https://claude.ai/code/session_01A22sUB3mUWs6M36VgfijBq)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9a0c0b5 commit 2dc5889

2 files changed

Lines changed: 108 additions & 6 deletions

File tree

‎.claude/skills/pm-dispatch/references/landing-operations.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
- fork PR = 提案,席位永不放行;采纳 diff 内部落地,见 `references/external-contributions.md`。
88
- 条款②入队闸门:翻 ready / 入队前先取 PR 实际 diff;diff 是事实,卡片语义是预测。
99
- `--tier` 嫌疑行是提示非裁定;双肢命中任一 ⇒ 无达档条款②复核 PASS 在案 ⛔ 禁止入队。
10-
- 路径肢 = diff 触及契约面 `packages/spec/src/**`,含 error-code-ledger 与 `*.zod.ts` 契约 schema。
10+
- 路径肢 = diff 触及契约面 `packages/spec/src/**` 非测试,含 error-code-ledger、`*.zod.ts` 契约 schema。
1111
- 声明肢 = 认领评论声明 `Clause-②: yes`,与路径无关;错误的 `no` 是可审计的假申报。
1212
- 交付后复核按面欠 ⛔ 不按车道(五面见 `references/contract-review.md`);双肢命中即 spec 车道。
1313
- 子代理起不来 ⇒ 复核缺席,PR 留 draft 队列外等档;唯一旁路是维护者亲审,逐次为准。

‎scripts/pm/dispatch-gates.mjs‎

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,11 @@ import { invokedAs, isEntrypoint } from '../invoked-as.mjs';
464464
// The human-merge line threshold is declared ONCE, in the landing gate; this
465465
// tool prints the same reading at dispatch time and never carries a second copy.
466466
import { HUMAN_MERGE_LINE_THRESHOLD, parseNumstat, sizeVerdict } from './check-governed-merges.mjs';
467+
// The test-file predicate the clause-② suspect table EXCEPTS by, read from the
468+
// gate whose whole question is which files under a package's `src/` are
469+
// published source and which are its tests — never respelled here (#19936). See
470+
// SUSPECT_TIER_GLOBS for why this predicate and not one of the repo's others.
471+
import { isTestPath } from '../check-undeclared-dep-imports.mjs';
467472

468473
// Re-exported so this tool's self-test drives the SAME predicates the gate
469474
// runs, not copies of them. They used to be written twice — see the shared
@@ -12347,9 +12352,10 @@ export const CONTRACT_REVIEW_TIER = 'claude-fable-5-1';
1234712352
* does to the contract — and a path cannot answer it. An ordinary-looking
1234812353
* surface (one package's source file) is the NORMAL shape of a clause-②
1234912354
* card. The closest a path can honestly get is SUSPICION:
12350-
* SUSPECT_TIER_GLOBS below marks the contract surface itself, and `--tier`
12351-
* prints a hint for it — never a verdict. The enforcement lives one step
12352-
* later, in the PM skill's enqueue gate over the PR's ACTUAL diff.
12355+
* SUSPECT_TIER_GLOBS below marks the contract surface itself — its test
12356+
* files excepted, because tests do not ship — and `--tier` prints a hint
12357+
* for it — never a verdict. The enforcement lives one step later, in the
12358+
* PM skill's enqueue gate over the PR's ACTUAL diff.
1235312359
*
1235412360
* A path derivation that pretended to cover clause ② would produce the failure
1235512361
* this whole file is written against, one level up: a "no mandate" line read as
@@ -12476,11 +12482,48 @@ export const MANDATORY_TIER_GLOBS = [
1247612482
* enqueue gate before the card may enqueue — the diff is a fact; the card's
1247712483
* semantics were a prediction. The gate itself lives in the PM skill
1247812484
* (入队与落地); this output only points at it.
12485+
*
12486+
* ## Test files are EXCEPTED, by a predicate this file imports (#19936)
12487+
*
12488+
* The enqueue gate's path limb reads this surface, and the review rule it
12489+
* guards (the skill's contract-review reference) owes an at-tier review for
12490+
* `packages/spec/src/**` NON-TEST files only. Without an exception the two
12491+
* disagreed on a test-only diff: the limb demanded an at-tier record that the
12492+
* review rule forbade spawning an agent to write, so an off-tier seat's
12493+
* test-only spec PR could never enqueue. The maintainer's ruling (director
12494+
* batch #219 item 1, letter A, comment 5805897677) settled it toward the review
12495+
* rule: a published-contract change owes the record; a test-only change does
12496+
* not, because tests do not ship.
12497+
*
12498+
* So an entry may carry `except`, a predicate over a path its glob covers, and
12499+
* `deriveTier` drops a path it answers true for BEFORE recording a suspicion.
12500+
* The predicate is `isTestPath`, imported from `check-undeclared-dep-imports.mjs`
12501+
* and never respelled, as the ruling orders ("the repo's own test-file
12502+
* predicate, not a new spelling"). Chosen over the repo's other test predicates
12503+
* on measurement, not taste: that gate's own question is which files under a
12504+
* package's `src/` are published source and which are its tests — the ruling's
12505+
* question exactly; it covers the four shapes the ruling names (`*.test.ts`,
12506+
* `*.pin.test.ts`, anything under `__tests__/`, fixtures under a test
12507+
* directory); and it excepts no directory word a contract domain carries. A
12508+
* census predicate that treats `qa/` as a test directory would drop
12509+
* `packages/spec/src/qa/testing.zod.ts`, a real contract schema — pinned.
12510+
*
12511+
* A subtraction fails SILENT, so this one is held live: the self-test reds if
12512+
* the exception drops any tracked `*.zod.ts` (the package's `files[]` ships
12513+
* every `*.zod.ts` under `src/` verbatim), and if the predicate stops being
12514+
* the imported one. The call hands it the repo-relative path although it was
12515+
* written for package-relative ones; for this glob that is exact, because no
12516+
* segment of `packages/spec/src/` is a test-directory name. ⛔ The exception
12517+
* narrows the SUSPICION only: MANDATORY_TIER_GLOBS carries none, and an input
12518+
* that CONTAINS the contract surface (a directory surface such as
12519+
* `packages/spec`) is still a suspect, since the predicate answers no for it.
1247912520
*/
1248012521
export const SUSPECT_TIER_GLOBS = [
1248112522
{
1248212523
glob: 'packages/spec/src/**',
1248312524
why: 'the contract surface (error-code ledger, *.zod.ts contract schemas) — the normal landing zone of a clause-② card',
12525+
except: isTestPath,
12526+
exceptWhy: 'a test file ships nothing, so a test-only diff changes no published contract and owes no at-tier record (the review rule already reads non-test files only)',
1248412527
},
1248512528
];
1248612529

@@ -12548,6 +12591,10 @@ export const RETIRED_TIER_WORDS = Object.freeze([]);
1254812591
* Throws when two globs covering the same surface mandate DIFFERENT tiers:
1254912592
* this file encodes no ordering over tiers, so choosing between them would be a
1255012593
* guess printed as a derivation.
12594+
*
12595+
* A suspect glob's `except` is applied per PATH, before the suspicion is
12596+
* recorded (SUSPECT_TIER_GLOBS says why test files are excepted): a mixed diff
12597+
* keeps every non-excepted path's suspicion, and a mandate is never excepted.
1255112598
*/
1255212599
export function deriveTier(paths, globs = MANDATORY_TIER_GLOBS, suspectGlobs = SUSPECT_TIER_GLOBS) {
1255312600
const hits = [];
@@ -12557,7 +12604,9 @@ export function deriveTier(paths, globs = MANDATORY_TIER_GLOBS, suspectGlobs = S
1255712604
if (hintCovers(g.glob, p)) hits.push({ path: p, glob: g.glob, tier: g.tier, why: g.why, oneLineExit: g.oneLineExit !== false });
1255812605
}
1255912606
for (const g of suspectGlobs) {
12560-
if (hintCovers(g.glob, p)) suspects.push({ path: p, glob: g.glob, why: g.why });
12607+
if (hintCovers(g.glob, p) && !(typeof g.except === 'function' && g.except(p))) {
12608+
suspects.push({ path: p, glob: g.glob, why: g.why });
12609+
}
1256112610
}
1256212611
}
1256312612
const tiers = [...new Set(hits.map((h) => h.tier))];
@@ -22895,7 +22944,7 @@ function selfTest() {
2289522944
'.github/workflows/scaffold-e2e.yml:23 no-check-families',
2289622945
'scripts/cli-build-prerequisite.mjs:111 inherited-population',
2289722946
'scripts/pm/check-expected-skips.mjs:131 self-test-reads',
22898-
'scripts/pm/dispatch-gates.mjs:708 inherited-population',
22947+
'scripts/pm/dispatch-gates.mjs:713 inherited-population',
2289922948
].join(' · '),
2290022949
censusRows.join(' · '),
2290122950
);
@@ -25570,6 +25619,51 @@ function selfTest() {
2557025619
t('a mandated surface still prints its suspect paths — the enqueue gate reads diffs, not dispatch tiers', mandatedAndSuspect.mandatory && mandatedAndSuspect.suspects.length === 1 && tierLines(mandatedAndSuspect).join('\n').includes('SUSPECT'));
2557125620
t('a verdict built without a suspects field still renders (suspicion defaults empty)', tierLines({ mandatory: false, tier: null, hits: [], declared: 1 }).length === 3);
2557225621

25622+
// ── Test files under the contract surface are EXCEPTED (#19936) ──────────
25623+
//
25624+
// The path limb must read what the review rule reads — non-test contract
25625+
// files — or a test-only spec PR from an off-tier seat can never enqueue.
25626+
// Ruling 5805897677 (letter A) names the pins: a test-only spec diff shows
25627+
// no SUSPECT line, a `*.zod.ts` diff still does, and PR #19932's one file is
25628+
// the lit case. The shapes are the four the ruling lists; the paths are
25629+
// judged by the pure function, so a hypothetical one decides as well as a
25630+
// tracked one does.
25631+
const LIT_TEST_ONLY = 'packages/spec/src/type-alias-convention.pin.test.ts';
25632+
const litTestOnly = fableOf([LIT_TEST_ONLY]);
25633+
const litRendered = tierLines(litTestOnly).join('\n');
25634+
t('⭐ the lit case — a test-only spec diff, PR #19932\'s one file — raises NO suspicion', litTestOnly.suspects.length === 0 && !litTestOnly.mandatory, litTestOnly.suspects);
25635+
t('…renders no SUSPECT line, and still prints the clause-② note, so the silence is not a clearance', !litRendered.includes('SUSPECT') && litRendered.includes('Clause ② is NOT reachable from paths'), litRendered);
25636+
const zodStill = fableOf(['packages/spec/src/ui/view.zod.ts']);
25637+
t('⭐ a *.zod.ts contract schema is STILL a suspect and still renders the SUSPECT line', zodStill.suspects.length === 1 && tierLines(zodStill).join('\n').includes('SUSPECT'));
25638+
for (const [shape, path] of [
25639+
['a plain *.test.ts', 'packages/spec/src/stack.test.ts'],
25640+
['a *.pin.test.ts', LIT_TEST_ONLY],
25641+
['a helper under __tests__/', 'packages/spec/src/data/__tests__/filter-helpers.ts'],
25642+
['a fixture under a __tests__/ directory', 'packages/spec/src/data/__tests__/fixtures/filter.fixture.json'],
25643+
['a fixture under a test/ directory', 'packages/spec/src/ui/test/fixtures/view-fixture.ts'],
25644+
]) {
25645+
t(`${shape} on the contract surface is excepted — no suspicion`, fableOf([path]).suspects.length === 0, path);
25646+
}
25647+
const mixedDiff = fableOf([LIT_TEST_ONLY, 'packages/spec/src/ui/view.zod.ts']);
25648+
t('a MIXED diff keeps its contract file\'s suspicion — the exception is per path, never per diff', mixedDiff.suspects.length === 1 && mixedDiff.suspects[0].path === 'packages/spec/src/ui/view.zod.ts', mixedDiff.suspects);
25649+
t('⛔ a contract domain whose NAME reads test-flavoured is no test: qa/testing.zod.ts stays a suspect', fableOf(['packages/spec/src/qa/testing.zod.ts']).suspects.length === 1);
25650+
t('…while the test file beside it is excepted', fableOf(['packages/spec/src/qa/testing.test.ts']).suspects.length === 0);
25651+
t('an input that CONTAINS the contract surface is still a suspect — the exception narrows files, never the reverse match', fableOf(['packages/spec']).suspects.length === 1);
25652+
t('a mandate is never excepted: a test-named file under a mandated root keeps its mandate', fableOf(['skills/objectstack-data/x.test.ts']).mandatory === true);
25653+
const contractEntry = SUSPECT_TIER_GLOBS.find((g) => g.glob === 'packages/spec/src/**');
25654+
t('the exception is the IMPORTED test-file predicate — the repo\'s own, never a respelling here', contractEntry?.except === isTestPath);
25655+
t('…whose home is a gate script, so a card editing it derives this gate by gate-script identity', isGateScriptPath('scripts/check-undeclared-dep-imports.mjs', gateFamilyFiles()));
25656+
t('every suspect entry that excepts carries a predicate and the reason for it', SUSPECT_TIER_GLOBS.every((g) => g.except === undefined || (typeof g.except === 'function' && typeof g.exceptWhy === 'string' && g.exceptWhy.length > 0)));
25657+
// The subtraction held LIVE, because a subtraction fails silent: the
25658+
// package's files[] ships every `*.zod.ts` under `src/` verbatim, so an
25659+
// exception that dropped one would take a shipped contract off the limb.
25660+
const specSrcTracked = trackedFiles().filter((f) => f.startsWith('packages/spec/src/'));
25661+
const specSrcZod = specSrcTracked.filter((f) => f.endsWith('.zod.ts'));
25662+
const exceptedZod = specSrcZod.filter((f) => fableOf([f]).suspects.length === 0);
25663+
t(`no tracked *.zod.ts on the contract surface is excepted (${specSrcZod.length} read; excepted: ${exceptedZod.join(', ') || 'none'})`, specSrcZod.length > 0 && exceptedZod.length === 0);
25664+
const exceptedTracked = specSrcTracked.filter((f) => fableOf([f]).suspects.length === 0);
25665+
t('the exception is live, not vacuous: it drops tracked test files on this tree, and only what the predicate names', exceptedTracked.length > 0 && exceptedTracked.every((f) => isTestPath(f)), `${exceptedTracked.length} of ${specSrcTracked.length}`);
25666+
2557325667
// ── The changed-line reading beside the tier verdict (2026-09-18 ruling) ──
2557425668
const overLine = changedLineLines({ additions: HUMAN_MERGE_LINE_THRESHOLD, deletions: 1 }).join('\n');
2557525669
t('over the threshold, the line says HUMAN MERGE, names the governed terminal and the landing pre-check',
@@ -26507,6 +26601,14 @@ function selfTest() {
2650726601
t('⭐ a second catalog SKILL.md prints the same mandate', catalogAiCli.status === 0 && (catalogAiCli.stdout ?? '').includes('MANDATORY') && (catalogAiCli.stdout ?? '').includes("'skills/**'"));
2650826602
const internalRefsCli = runCli(['--tier', '.claude/skills/pm-dispatch/references/state-machine.md']);
2650926603
t('⭐ --tier on an internal pm-dispatch references file still prints NO mandate', internalRefsCli.status === 0 && (internalRefsCli.stdout ?? '').includes('no path-derived mandate') && !(internalRefsCli.stdout ?? '').includes('MANDATORY'));
26604+
// The clause-② test exception on the real CLI (#19936): the ruling's pins are
26605+
// on `--tier` OUTPUT, so both acceptance paths are measured end to end. The
26606+
// lit case goes through the asserted helper, so a later rename of that file
26607+
// leaves the pin deciding rather than refusing as an absent path.
26608+
const litTierCli = runCliHypothetical(['--tier', 'packages/spec/src/type-alias-convention.pin.test.ts']);
26609+
t('⭐ --tier on the lit case (a test-only spec diff) derives, and prints NO SUSPECT line', litTierCli.status === 0 && (litTierCli.stdout ?? '').includes('no path-derived mandate') && !(litTierCli.stdout ?? '').includes('SUSPECT'), litTierCli.stdout);
26610+
const zodTierCli = runCliHypothetical(['--tier', 'packages/spec/src/ui/view.zod.ts']);
26611+
t('⭐ --tier on a *.zod.ts contract schema still prints the SUSPECT line naming it', zodTierCli.status === 0 && (zodTierCli.stdout ?? '').includes('SUSPECT') && (zodTierCli.stdout ?? '').includes('packages/spec/src/ui/view.zod.ts'), zodTierCli.stdout);
2651026612

2651126613
// ── The entry guard (#9757) ───────────────────────────────────────────────
2651226614
//

0 commit comments

Comments
 (0)