Repository navigation
Closed
Labels
domain:devxDomain lane — seat card indexed by label:pm:seatDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3Medium: important, M3toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surfaceFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface
Description
Filed by the skills lane seat (session session_019RfFHiRCSs3JXLK4cwcfox, os-steve) at the landing of #16455 (flight X, PR #16481), 2026-09-07T04:3xZ, under the finding-admission threshold: a silent class (tier files leaking back into the queue) that nothing makes red on the pull request that introduces it. Surface owner stays domain:devx.
What
A pull request that adds a *.e2e.test.* or *.live.test.* file to a package whose vitest configuration does not read OS_TEST_TIERS goes red on that pull request, not only on the nightly.
Measured (from the #16455 delivery, PR #16481 at 770dd18)
- 72 standalone
vitest.config.tsfiles, none sharing a config, no root config or workspace; today exactly one package (packages/cli) owns tier-named files (60*.e2e.test.ts) and zero*.live.test.*files exist anywhere. scripts/nightly-tiers.mjs --checkrefuses to run past a tier-owning package that ignores the switch, but only on the nightly. Nothing makes that red on the pull request that introduces the file, so such a file runs in the queue (the cost ci: e2e and live tiers move to a nightly run on main; PR and queue runs keep unit, integration and conformance (maintainer-directed, part B tiering) #16455 moved out) until the next nightly notices, and the nightly's refusal then blocks the whole tier run for one package's omission.
Ruling
- One per-PR gate: a
check:*script underscripts/(ornightly-tiers.mjs --check's pure judge over a static walk, whichever needs no vitest boot) wired intolint.ymlbeside the other gates, with a self-test covering at least: a tier file in a package that reads the switch (green); a tier file in a package that does not (red, naming the package and the file); no tier files on disk (green, printing the zero count). - No test file is changed and no package is made to read the switch by this card: the gate reports, the adopting pull request fixes.
- Clause-②: no. The gate only narrows what a pull request may merge.
Acceptance
- Self-test green; the gate's own red case shown once in the PR with a scratch tier file in a non-reading package, then removed (one-time proof, no permanent test file).
pnpm check:pm-dispatch-gatesaccepts the new family.
Blocked-by: #16455 (the script it wires lands there).
Refs #16455.
Generated by Claude Code
Metadata
Metadata
Assignees
Labels
domain:devxDomain lane — seat card indexed by label:pm:seatDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3Medium: important, M3toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surfaceFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface
Activity
baozhoutao commentedon Sep 10, 2026
Claim: session
session_012GKcPZbMoGq7WPzKLfRBTU· branchclaude/issue-16494-tier-file-adoption-gateClause-②: no
Dispatched by the
domain:devx @ objectstackexecution PM seat (post #6023). The assignee field is the presence bit; this comment is the identity. The dev inherits both and ⛔ posts no second claim.Clause-②: nois the card's own declaration, not this seat's guess — the Ruling states it and gives the reason ("the gate only narrows what a pull request may merge"). Re-checked against the surface:scripts/**plus alint.ymlwiring, nopackages/**/src/**path, no published surface.packages/**/src/**, that declaration is wrong — ⛔ do not work around it, report it and let this seat re-declare.This card has no triage comment because it did not need one
It was filed by the skills lane seat (
session_019RfFHiRCSs3JXLK4cwcfox, os-steve) at the landing of #16455, and it carries its own Ruling and Acceptance sections. ⇒ The body IS the ruling. It is already routeddomain:devxand gradedpriority:p2. ⛔ Nothing here is left to a design debate — implement what the Ruling says.Blocked-by is CLEARED — verified before dispatch, not assumed
The card declares
Blocked-by: #16455. Measured onorigin/mainat 2026-09-10T01:3xZ:⇒ The script the gate is to wire is on
main. ⛔ The zeros above are not bare: the positive control fires on the same corpus in the same pass.And the card is not already satisfied:
package.jsoncarries nocheck:*tier*script. That is the gap this card fills.What to build — the Ruling, restated as work
check:*script underscripts/(or reusenightly-tiers.mjs --check's pure judge over a static walk — whichever needs no vitest boot; the card gives you that choice explicitly and it is a cost question, not a design one). Wire it intolint.ymlbeside the other gates.Acceptance — the card's, with its self-test cases spelled out
Its self-test must cover at least these three, and the middle one is the firing leg:
⭐ That third case is a REVERSE control and the card was right to demand it: a gate that reports "clean" by never looking is indistinguishable from one that looked and found nothing, unless the zero is printed. Make the zero visible.
Plus, from the card:
pnpm check:pm-dispatch-gatesmust accept the new family.Ground truth to re-derive yourself before editing
The card's census was taken at PR #16481 /
770dd18205and is now days old — ⛔ re-measure, do not transcribe:vitest.config.tsfiles, no shared/root/workspace config;packages/cli) owns tier-named files (60*.e2e.test.ts);*.live.test.*files exist anywhere.*.live.test.*handling are both being validated against an empty corpus. Say so plainly in the report rather than letting an untested arm read as tested. Every zero needs a same-corpus positive control in the same pass. A count is not a reading. An exit code is not a cost — exit 3 in this repo's gate family means NOT MEASURED, neither green nor red.⛔ Worktree-first
⛔ Never edit the shared primary checkout. One dedicated worktree, and keep EVERY command inside it. ⛔ Never
git stash(shared LIFO across worktrees). ⛔ Never editcontent/docs/releases/**. ⛔ Governed surfaces off limits:docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md.⛔ Draft PR only. Landing is this seat's act. ⛔ RULE 2 — no commit may carry a card-relation trailer (
Fixes/Closes/Resolves/Part of/Refs); the card reference goes in the PR body.Generated by Claude Code
claude commentedon Sep 10, 2026
os-dev-report
{ "issue": 16494, "status": "done", "branch": "claude/issue-16494-tier-file-adoption-gate", "pr": "https://github.com/objectstack-ai/objectstack/pull/17275", "premise_still_valid": true, "summary": "Added scripts/check-tier-file-adoption.mjs, wired as `pnpm check:tier-file-adoption` into lint.yml's `lint` job (the required `Lint & Repo Gates` context). ROUTE: the STATIC walk, not `nightly-tiers.mjs --check`'s vitest probe — the card left it open and called it a cost question, and --check spawns `vitest list --filesOnly` twice per tier-owning package, which is the right instrument for a nightly and the wrong one for every PR. The gate spawns nothing and imports --check's own `tierPackages` / `NIGHTLY_TIERS` / `TIER_ENV` from nightly-tiers.mjs, so the two cannot disagree about which files are tier files or which packages own them. Detector: it walks each tier-owning package's vitest-config local import closure and accepts a VALUE import of the switch-reading surface (readTierMode / selectTierFiles / OS_TEST_TIERS) from the reader module, or the variable named in a code position. That shape is forced by measurement, not taste — see `tests`. It is declared a SPELLING gate in its own header (green means the switch is wired in, never that the partition is right; the semantic verdict stays --check's on the nightly). Scope held: no test file changed, no package made to read the switch — the diff is 3 files, +720 lines, all under scripts/ + .github/workflows/lint.yml + a root package.json script entry.", "tests": "ALL 62 DERIVED GATE FAMILIES GREEN. Families derived with `node scripts/pm/dispatch-gates.mjs --commands` (no hand-written path list; the tool took its own changeset from the merge-base), at commit 0f4d7f0840, --repo-confirmed objectstack-ai/objectstack. Every exit code captured BEFORE any pipe (`cmd > /tmp/log 2>&1; EXIT=$?`).\n\n * 57 of 62 green on the first pass.\n * 5 returned exit 3 = PREREQUISITE NOT MET = NOT MEASURED (not green, not red): check:dts-closure, check:dual-build-cjs-loads, check:lean-entry-closure, check:sourcemap-no-sources-content, check:type-check-debt — all five read built `dist/`. DRIVEN TO A REAL VERDICT rather than reported as unmeasured: `pnpm build` under `scripts/pm/os-verify-lock.sh` (VERDICT command-exit 0, held 496s, waited 0s, 73/73 turbo tasks), then all five re-run => exit 0.\n * ACCEPTANCE ITEM: `pnpm check:pm-dispatch-gates` => exit 0, `dispatch-gates self-test: 1624 cases pass`, no ELIFECYCLE. The new family is accepted.\n * `pnpm lint` (repo-wide `eslint . --no-inline-config`) => exit 0 in 83s. NOT a narrowing — the full scan: 6464 files linted, 0 errors, 0 warnings, read from eslint's own `--format json` output, and scripts/check-tier-file-adoption.mjs is confirmed present in that population.\n * `pnpm check:nul-bytes` => exit 0, plus a manual control-byte sweep of the diff (`grep -naP` over the 3 changed files) => no hits.\n\nTHE GATE'S OWN SELF-TEST (`node scripts/check-tier-file-adoption.mjs --self-test`, EXIT=0): 12 fixture-workspace cases (each builds a real throwaway pnpm workspace in $TMPDIR and runs the real walk) + 8 pure-judge cases + population-declaration checks + a real-tree walk floor.\n\nTHE THREE ACCEPTANCE LEGS, MEASURED:\n LEG 1 (tier file in a package that READS the switch => GREEN): 2 rows green — a direct value import, and the transitive packages/cli shape (config -> local module -> reader). Real-tree confirmation on every run: `@objectstack/cli — 65 file(s); imports readTierMode, selectTierFiles from scripts/nightly-tiers.mjs (via packages/cli/vitest-tiers.ts)`.\n LEG 2 (tier file in a package that does NOT => RED, naming the package AND the file): 2 rows green — blind config, and no vitest config at all; both assert the finding string contains the package name and the file path.\n LEG 3 (no tier files on disk at all => GREEN, PRINTING THE ZERO): green, and asserted four ways, because a gate that reports clean by never looking would satisfy a weaker version of this. The case asserts tierFileCount === 0, owners === 0, packagesWalked GREATER THAN 0, and that the printed line carries a `0` for EACH tier BY NAME (`0 e2e`, `0 live`). The zero is printed on EVERY run, red or green: `N nightly-tier test file(s) on disk (X e2e, Y live)`. A walk that finds zero workspace packages is exit 2 (MEASUREMENT FAILED), never a green — that is the one anti-vacuity floor kept, and it is deliberately the INVERSE of the sibling check-registry-log-declared's selection floor, stated in the header: zero engine-booting packages means that gate went blind, while zero tier files is a legitimate state of this tree.\n\nCONTROLS FOR THE LEGS (each in the same pass):\n * REVERSE CONTROL for leg 3, so its zero is a judged zero and not an empty directory: a file named `live-dialect-matrix.test.ts` is NOT a tier file — counted 0. `live` is a tier a package adopts by naming a file into it, not a substring this gate hunts.\n * POSITIVE CONTROL — prose naming OS_TEST_TIERS does NOT satisfy the check (red).\n * POSITIVE CONTROL — a type-only import of the reader reads nothing at runtime (red).\n * POSITIVE CONTROL — importing only `isNightlyTierFile` (a pure filename predicate) is not a switch read (red).\n * A package that owns no tier file is never judged, however blind its config (green, 2 packages walked).\n * The .live. tier is judged exactly as .e2e. is (fixture corpus only — see the empty-corpus note below).\n\nWHY THE DETECTOR SHAPE WAS FORCED, MEASURED AT d03c3c96d6 OVER THE WHOLE POPULATION (which is one package): packages/cli/vitest.config.ts names OS_TEST_TIERS 5 times raw and 0 times with comments masked; packages/cli/vitest-tiers.ts 4 raw / 0 masked; scripts/nightly-tiers.mjs 5 raw / 1 masked. All 9 occurrences in the two config files are PROSE. So a comments-included grep is GREEN ON PROSE, and a comments-masked grep is RED ON packages/cli — the one package in the tree doing this correctly, a 100% fabrication rate over today's entire population. Both wrong detectors are pinned as controls. Comments are masked with the shared scripts/js-comment-mask.mjs (the module check:comment-mask-adoption exists to keep everyone on).\n\nONE-TIME RED PROOF ON THE REAL TREE (card requirement), trap-guarded, no permanent fixture:\n before: EXIT=0, `80 workspace package(s) walked, 65 nightly-tier test file(s) on disk (65 e2e, 0 live), owned by 1 package(s)`\n scratch file added at packages/types/src/scratch-red-proof.e2e.test.ts — mutation PROVEN ON DISK before the run (grep -c of the injected marker => 1, plus ls) rather than inferred from the editor's exit code\n RED: EXIT=1, `66 nightly-tier test file(s) on disk (66 e2e, 0 live); 1 owning package(s) do not read OS_TEST_TIERS`, naming `@objectstack/types (packages/types)` AND `packages/types/src/scratch-red-proof.e2e.test.ts`, with the remedy and a pointer to `node scripts/nightly-tiers.mjs --check`\n restored: file absent, `git status --porcelain` EMPTY (restoration proven by observing state, not by reading an exit code), after: EXIT=0 and byte-identical to `before`. Count moved 65 -> 66 -> 65.\n\nCENSUS RE-DERIVED ON THIS BRANCH (d03c3c96d6) — NOT transcribed from the card (taken at 770dd18205), and it HAS DRIFTED:\n standalone vitest.config.ts files: card 72 -> measured 73\n root/workspace vitest config: card none -> measured none (0 `vitest.workspace*`, no root `vitest.config.*`)\n packages owning tier files: card 1 (packages/cli) -> measured 1 (packages/cli)\n tier-named files: card 60 `*.e2e.test.ts` -> measured 65 `*.e2e.test.*`\n `*.live.test.*` files: card 0 -> measured 0\n `check:*tier*` script in package.json: card none -> measured none, so the card was NOT already satisfied\n CONTROLS in the same pass on the same tree: the `*.live.test.*` zero sits beside 3541 `*.test.*` files matched by the same enumeration, so the zero is a reading and not a broken glob; OS_TEST_TIERS occurs in 8 tracked files (positive control); a nonsense control term returns 0.\n\nEMPTY-CORPUS DECLARATION (explicit, as required): with zero `*.live.test.*` files anywhere in the tree, the gate's `live` arm and leg 3's zero for that tier are validated against an EMPTY REAL CORPUS. Their only coverage is the self-test's temporary fixture workspaces, which exercise the judging path and cannot tell you the arm has ever met a real file. This is NOT reported as tested. The gate prints the fact itself on every self-test run (`the OS_TEST_TIERS live-tier corpus is 0 file(s) — that arm is covered by FIXTURES ONLY`) rather than leaving it to a PR body nobody re-reads. The `e2e` arm is not in that position: 65 real files, one real reading package, measured on every run.\n\nCHANGESET GRADING — no `.changeset/*.md`; the `skip-changeset` label instead, on a MEASUREMENT: the diff touches repo-root scripts/, .github/workflows/lint.yml, and the root package.json which is `private: true` (@objectstack/spec-monorepo) and ships nothing. Across all 70 published (non-private) workspace packages there are exactly 10 distinct `files[]` entries (CHANGELOG.md, README.md, api-surface, dist, json-schema, liveness, llms.txt, prompts, spec-changes.json, src/**/*.zod.ts) — every one package-local, none reaching repo-root scripts/ or .github/. Post-build symbol scan over the 197 published `files[]` paths that exist on disk: 0 hits for `check-tier-file-adoption`; POSITIVE CONTROL on the same instrument in the same pass: `defineStack` DOES hit under packages/spec/dist. Nothing publishable moves, so an empty-frontmatter changeset is refused on principle (check:empty-changeset's own ledger: it buys nothing the label does not and uniquely risks #4898) and no bump is invented. Label applied via the additive POST endpoint (HTTP 200) and READ BACK after the size-labeler window: `skip-changeset` survives; `ci/cd`, `size/l` and `dependencies` were set by another actor and are left alone.\n\nCLAUSE-2: the card's `no` still holds. Surface is scripts/** + lint.yml + a root package.json script entry; no packages/**/src/**, no published surface. The implementation needed no change under packages/**, so the declaration is not contradicted and no PM re-declaration is owed.", "mcp_calls": "0 — every GitHub read and write on this run went through the container's proxy-credentialed REST channel (a repo-scoped probe was green for this session: the empty-branch push, the card and comment reads, the PR create, the label POST and both read-backs). No MCP GitHub tool was invoked; two schemas were loaded via ToolSearch and never called.", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: the card's census aged between filing (770dd18205) and this branch (d03c3c96d6) — 72 -> 73 standalone vitest.config.ts files and 60 -> 65 *.e2e.test.* files. Not a defect and not filable: the card's own numbers are correctly anchored to a named commit, which is this repo's convention, and this PR re-derives them. Successor: none needed — the new gate now prints the live count on every CI run, so the number has a standing carrier instead of a prose one.", "noted, not filed: the `*.live.test.*` tier has zero population repo-wide, so nothing in CI has ever exercised that arm against a real file. Not a defect (the tier is an opt-in a package adopts by naming a file into it, exactly as nightly-tiers.mjs's header states) and there is no contract text it violates. Successor: the first PR that adopts a `.live.` file — which is precisely the PR this gate now reds on, so the arm gets its first real corpus and its first real verdict in the same change.", "noted, not filed: the detector is a SPELLING gate — a config that imports readTierMode and ignores what it returns satisfies it and fails `nightly-tiers.mjs --check`. This is a declared boundary, not an oversight: it is stated in the script header and in the lint.yml step comment, and the semantic half already has an owner (the nightly). Successor: none — the two halves are deliberately split, and widening this one to a semantic check is the vitest boot the card ruled out." ] }Generated by Claude Code