Repository navigation
fix(scripts): read nested exports conditions in check-lean-entry-closure, and refuse unreadable ones by name - #17100
Merged
Conversation
…nes by name `resolveConditions()` handed `exports[subpath][flavour]` straight to `node:path`'s `resolve()` with no string check. The one guarded row (`@objectstack/objectql/core`) spells its conditions flat, so the gate's live verdict was correct and nothing was mis-measured. The cost fell entirely on the next author: `packages/metadata`'s `./errors` spells them NESTED (`import`/`require` -> objects of `types`/`default`), and that row died with a raw `TypeError: The "paths[2]" argument must be of type string` — no gate name, no entry id, no repair. - `conditionTarget()` reads both spellings this workspace uses: a path string, or a nested condition object's `default`. - Any other shape is REFUSED with this gate's own diagnostic naming the entry id, subpath, flavour and the shape found — never a crash from a node builtin. The gate does not guess a target, because a closure measured from the wrong artifact would be reported as if it were the right one. The shape guard one level up already had the right form; it was checking the wrong level. - Absent now means `undefined`/`null` only. The previous `if (!rel)` silently skipped `""` and `false`, which are not a manifest declining to publish a condition. - The header no longer sizes a second `GUARDED_ENTRIES` row as "a one-row change": a second entry needs its own `ADMITTED_PACKAGES` set, and admitting a package there moves the ADR-0076 D2 boundary and carries `RATCHET_AUTHORITY_MARKER`. That sentence was false before this change and stays false after it, so it says the real price instead. - Self-test gains a `reading a condition` battery: one case per branch (flat, nested, absent) plus two refusals and one end-to-end nested manifest. Battery floor ratcheted 5 -> 6; cases 16 -> 22. No `GUARDED_ENTRIES` row is added and `ADMITTED_PACKAGES` is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
baozhoutao
marked this pull request as ready for review
September 9, 2026 09:04
baozhoutao
enabled auto-merge
September 9, 2026 09:04
This was referenced Oct 2, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Oct 7, 2026
…ld to turbo.json by check-ci-filter-parity (objectstack-ai#21338) Fixes objectstack-ai#21202 Clause-②: no ## What changes 1. **`.github/workflows/ci.yml`, the `core:` filter** gains 18 literal entries: `turbo.json`, the root `tsup.config.ts`, and the 16 `scripts/*.mjs` files that a build task Build Core runs declares as a `$TURBO_ROOT$/...` input. A PR that changes only one of them now starts Build Core at PR time. Before this change the merge queue was the first place that build ran (PR objectstack-ai#21199, a `turbo.json`-only diff, skipped Build Core). 2. **`scripts/check-ci-filter-parity.mjs`** gets a third subject, `judgeBuildInputs`. It derives the required set and does not keep a copy: - from `turbo.json`: the file itself, its `globalDependencies`, and every `$TURBO_ROOT$/...` input of a `build` or `PKG#build` task; - minus the builds that Build Core's `run: pnpm build` skips. Those are read from the root `build` script's `--filter=!PKG` tokens (today `@objectstack/docs#build` and its `content/**` input). If the script is misread, the gate can only drop an exclusion, which makes it require more of `core`. So a misread is a loud red, never a silent gap. - It reds when a declared build input has no covering `core` entry. - It also checks the reverse direction where the declarations alone can decide it. A literal `core` entry sitting in a directory that holds declared build inputs, but covering none of them, reds as a leftover. - It refuses eight unread states: no `build-core` job; its `if:` no longer names `core`; no `run: pnpm build` step; `turbo.json` or the root manifest unreadable or not JSON; no `tasks` map; no build task; a `build` script that is not `turbo run build ...`. - It uses the same pure-string coverage rule as the existing `crosspkg` subject, so no third matcher is added. 3. **Two forced follow-ons, both outside the claim's declared file surface.** Each is the remedy text of a gate that this change turned red. Neither is a design choice. - **Build Core gains `node scripts/check-dts-emitted.mjs --self-test`.** Once `core:` named that file, `check:self-test-wired` reported that a workflow names a script whose `--self-test` no workflow runs. That is true: 67 package build commands end with that checker, and its self-test ran nowhere in CI. The gate's remedy is "wire it into the step that runs it". The step takes milliseconds, needs no prerequisites, and runs before `pnpm build`. - **`scripts/pm/dispatch-gates.mjs`, one self-test specimen.** The case "a name-shaped script no family runs is NOT a gate script" used `scripts/check-dts-emitted.mjs`, which item 3a turned into a family file. The specimen is now `scripts/check-test-typecheck.mts`, which only package manifests run. That file is already named in the same docblock's list of such files. 4. **Three comments this change made false, corrected in `e6aa08f322` (patch round 2, comments only):** the GAP 1 block in `scripts/sync-template-versions.mjs`, the same block in `.github/workflows/lint.yml`, and the extglob docblock sentence in `scripts/pm/dispatch-gates.mjs` ("one entry of six"). Each now reads true with `core` matching the root build inputs. The re-measure for a diff confined to `scripts/sync-template-versions.mjs` reads `core=true`, `crosspkg=true`. No change to `turbo.json` or `fleet-write.yml`. `lint.yml` changes only its GAP 1 comment block (about lines 2660 to 2675), which is disjoint from objectstack-ai#21295's pm roster block. ## Granularity: literal files, not `scripts/**` I measured this over the 3288 first-parent commits of `main` in the 30 days to `1371dc980c`, with `scripts/pm/git-history.mjs` proving the window was present (floor 2026-06-27). I used picomatch `{ dot: true }`, the options `dorny/paths-filter` uses, under both 2.3.1 and 4.0.5. The two versions agree on every commit. | `core` filter | commits it matches | added by this change | |:--|--:|--:| | before | 2247 | — | | + the 18 literal build inputs (this PR) | 2275 | **+28** | | + `scripts/**`, `turbo.json`, `tsup.config.ts` | 2840 | +593 | 516 of the 593 that `scripts/**` would add touch a `scripts/pm/` tool or a `scripts/check-*` gate. The gate holds the literal list to the declarations, so the narrow choice costs nothing in drift. Widening `core` starts every job that `core` gates, not only Build Core: Test Core, Temporal Conformance and Dogfood also run on those 28. That matches turbo's own view, because every one of those jobs builds, and a moved build hash re-runs everything downstream of it. ## Acceptance: the filter evaluated per path The `core` list is parsed out of `ci.yml` at the base and at this head, in the action's order (YAML, then the `filters` string). Each path is matched with picomatch `{ dot: true }`. | path | before (2.3.1 / 4.0.5) | after (2.3.1 / 4.0.5) | |:--|:--|:--| | `turbo.json` | false / false | true / true | | `tsup.config.ts` | false / false | true / true | | each of the 16 `scripts/*.mjs` build inputs | false / false | true / true | | `tsconfig.json` (globalDependencies) | true / true | true / true | | `packages/cli/src/commands/init.ts` (create-objectstack#build) | true / true | true / true | | control: `scripts/check-ci-filter-parity.mjs` | false / false | false / false | | control: `scripts/pm/dispatch-gates.mjs` | false / false | false / false | | control: `content/docs/index.mdx` | false / false | false / false | | control: `packages/spec/src/ui/view.zod.ts` | true / true | true / true | The 16 scripts are each checked individually: `build-input-hash`, `check-dev-prereqs`, `check-dts-emitted`, `check-dts-references`, `check-regen-pending`, `cli-build-prerequisite`, `git-env`, `import-prerequisite`, `invoked-as`, `js-comment-mask`, `regen-artifacts`, `sync-scaffold-emission-policy`, `sync-template-versions`, `ts-parse`, `tsup-drop-sources-content`, `workspace-enumerator`. **Live run: none yet.** This PR changes `ci.yml`, which `core` already matched, so its own run proves nothing. The first demonstration will be the first PR after this one lands whose diff is confined to listed paths. At the measured rate that is about one a day (28 in 30 days). The `turbo.json` card objectstack-ai#20790 is the known candidate if its PR touches only `turbo.json`. ## The new leg can go red - **Self-test battery (9), 33 cases; the gate goes from 68 to 101 assertions.** - A build input dropped from a fixture `core:` reds, naming the path and the task that declared it. - `turbo.json` itself, a `globalDependencies` entry, and an input under a new root are each required. - Test-task inputs, negated inputs, and the excluded docs build are left out. The docs build's input is required again once the build script stops excluding it. - A leftover entry reds. - All eight refusals are covered (ten cases). - The checked-in tree is green. With `scripts/invoked-as.mjs` dropped from it, the gate reds through `main()`, the report path. - **Battery (8)'s report-path scratch root** now carries the real `turbo.json` and root manifest, so a red there cannot come from the new subject refusing a missing file. - **Before/after on the real tree.** The extended gate run over the base `ci.yml` exits 1 and lists the 18 inputs. Over this head it exits 0. - **Ablations**, done with `scripts/ablation-replace.mjs`. Each mutation was confirmed on disk, and each restore was proven by blob equal to `HEAD` and an empty `git diff HEAD`: - Coverage forced true in `judgeBuildInputs`: production exit 0 (green), `--self-test` exit 1 with 9 named failures. - Leftover rule disabled: production exit 0, `--self-test` exit 1 with 1 named failure. - My first attempt at the leftover ablation was a no-op. Its replacement text contained the anchor, so the tool refused and wrote nothing. I re-ran it with a non-overlapping replacement. ## Gates (local, head ec413ce) - I re-derived the list with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (no paths; merge base `1371dc980`): 58 commands. - `--ran` reconciliation: "58 derived famil(ies) accounted for — 54 run, 4 NOT-MEASURED (0 DERIVED from a recorded exit 3, 4 claimed)". Every one of the 54 recorded exit 0. - That includes `check-ci-filter-parity` and its `--self-test`, `check:self-test-wired`, `bare-root-worklist --self-test`, `check:required-contexts`, `check:workflow-step-name-quoting`, `check:step-collectors`, `check:stall-guard-budget` and `check:nul-bytes`. - It also includes `check:pm-dispatch-gates`, whose self-test passed 1976 cases (778 s on this box). Its "fabrication direction" case passes with the new specimen. - The four `dist/`-reading Build Core gates (`check:dts-closure`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:sourcemap-no-sources-content`): their `--self-test` legs exit 0. Their real legs are **NOT MEASURED** locally, because they read a full workspace `dist/` and this diff moves no build hash. The changed files are a workflow and two root scripts, none of them a declared build input. Build Core runs them on this PR. ## Changeset None. The changed files are a workflow, a root script and a `scripts/pm/` tool. None of them is in any published package's `files[]`, and the root package is private. `skip-changeset` applies. ## Acceptance notes (not changed here) - **Two comments this change made false, plus a third the grep found:** all three are corrected in this PR (`e6aa08f322`, item 4 above), per the seat's patch round. The carrier this note first named, objectstack-ai#21294's PR, had already landed. (Seat edit.) - **Build Core's own guard scripts do not schedule Build Core.** `check-dts-closure`, `check-dual-build-cjs-loads`, `check-sourcemap-no-sources-content` and `check-lean-entry-closure` run only inside Build Core. A diff confined to one of them leaves `core` false (picomatch 2.3.1 and 4.0.5, both before and after this PR). Measured on PR objectstack-ai#17100 (`91f65c4ea4`), which changed only `scripts/check-lean-entry-closure.mjs`: Build Core was `skipped`, so neither the changed gate's self-test nor its real leg ran at PR time. This is a different population from build inputs, because turbo does not declare these files, so it is reported here and left unchanged. --- _Generated by [Claude Code](https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Oct 7, 2026
…pts, held to the job steps by check-ci-filter-parity (objectstack-ai#21368) Fixes objectstack-ai#21341 Clause-②: no ## What changes 1. **`.github/workflows/ci.yml`, the `core:` filter**, gains five literal rows after the build-input rows objectstack-ai#21202 added: - `scripts/check-dts-closure.mjs` - `scripts/check-dual-build-cjs-loads.mjs` - `scripts/check-lean-entry-closure.mjs` - `scripts/check-sourcemap-no-sources-content.mjs` - `scripts/population-floor.mjs` (the dual-build guard imports it) A PR that changes only one of them now starts Build Core, so the changed guard runs its `--self-test` and its real leg at PR time. Before this change Build Core skipped on such a PR. PR objectstack-ai#17100 changed only `scripts/check-lean-entry-closure.mjs`, and its Build Core concluded `skipped`. 2. **`scripts/check-ci-filter-parity.mjs`** holds those rows. The Build Core subject objectstack-ai#21202 added (`judgeBuildInputs`) now judges a second population beside the build inputs from turbo.json: every repository script that a `build-core` `run:` step runs, plus that script's first-party imports. - **Derived from the job, not kept by hand.** Each step goes through `collectInvocations`. That is the recognizer `check:self-test-wired` already uses to answer "which repository scripts does this CI command run": a `scripts/...` path named directly, or one reached through a root `package.json` alias chain. Each script it names brings its first-party closure from `first-party-closure.mjs`. - **Uncovered goes red.** If no `core` row covers a script that a step runs, the gate fails and names the path and the step. So a guard step added to Build Core without a filter row goes red. - **Leftovers go red.** objectstack-ai#21202's leftover rule now covers this population too. A literal `core` row next to the build inputs and the guards that covers neither is reported. So a row left behind by a deleted guard step goes red too. - **No new gate, no new workflow step, no new list.** The existing parity gate gains a population, and the rows in ci.yml are the one list. ## Why derived, and not a `BUILD_GUARDS` table like `CONSOLE_GUARDS` `CONSOLE_GUARDS` exists because the console subject has to classify each row. A `console:` row is either hashed into the dist key or a guard, and nothing in the workflow says which. Build Core rows need no classification: the scripts its steps run can be read from the steps, just as objectstack-ai#21202 reads build inputs from turbo.json. A `BUILD_GUARDS` table would be a second list of the same fact, kept in step by hand. It would also need two holding rules of its own: table against steps, and table against rows. The derivation needs only one rule in each direction, and the self-test pins it against the real job. ## Measurements Branch head `aa0b21ae2c`, base `96b12b589f`. **The card's instance, through the matcher.** `core` is read at base and at head. Each path is evaluated with picomatch using `{ dot: true }`, which is how `dorny/paths-filter@v4` calls it. Version 2.3.1 is the one the action bundles; 4.0.5 is the one this tree resolves. | path | before, 2.3.1 | before, 4.0.5 | after, 2.3.1 | after, 4.0.5 | |---|---|---|---|---| | `scripts/check-dts-closure.mjs` | false | false | true | true | | `scripts/check-dual-build-cjs-loads.mjs` | false | false | true | true | | `scripts/check-sourcemap-no-sources-content.mjs` | false | false | true | true | | `scripts/check-lean-entry-closure.mjs` | false | false | true | true | | `scripts/population-floor.mjs` | false | false | true | true | | `scripts/check-dts-emitted.mjs` (already a build input) | true | true | true | true | | control: `scripts/check-ci-filter-parity.mjs` | false | false | false | false | | control: `scripts/check-self-test-wired.mjs` | false | false | false | false | | control: `scripts/pm/dispatch-gates.mjs` | false | false | false | false | | control: `scripts/dual-build-cjs-loads.baseline.json` | false | false | false | false | The two versions agree on every row. **Width.** The window is the 3287 first-parent commits of `main` in the 30 days to `96b12b589f`. `scripts/pm/git-history.mjs` proved it complete; the history floor is 2026-06-27. - `core` matched 2276 of those commits. With the five rows it matches 2278: +2, identical under 2.3.1 and 4.0.5. - Seven commits touched a guard path. Only one touched nothing but guard paths: `91f65c4ea4`, which is PR objectstack-ai#17100, the card's own instance. - The other newly scheduled commit is `83649b870a`. It changed `check-dual-build-cjs-loads.mjs` and `population-floor.mjs` along with two lint-side gates. `population-floor.mjs` on its own adds 0 commits in the window. **The gate before the rows.** This is the new script run over the base ci.yml: exit 1, naming exactly five scripts, each with the step that runs it. ```text - 5 script(s) the `build-core` job's own steps run are covered by no `core:` entry in .github/workflows/ci.yml. ... scripts/check-dts-closure.mjs (Sweep the built closure for vanished declarations (pnpm check:dts-closure)) scripts/check-dual-build-cjs-loads.mjs (Every published require entry point actually loads (pnpm check:dual-build-cjs-loads)) scripts/population-floor.mjs (imported by scripts/check-dual-build-cjs-loads.mjs) scripts/check-sourcemap-no-sources-content.mjs (No published source map embeds source text (pnpm check:sourcemap-no-sources-content)) scripts/check-lean-entry-closure.mjs (The lean engine entry loads no forbidden or unlisted package (pnpm check:lean-entry-closure)) ``` **After:** exit 0. The gate prints: `all 20 build input(s) ... and all 11 script(s) the build-core job's own steps run (5 named by a step, 6 only imported by one) are covered by core ... no literal core entry beside them is a leftover`. **Self-test.** 131 assertions, exit 0. The new battery (10) has a floor of 30, which is the count it registers. The roster floor goes from 9 to 10. On fixtures, battery (10) checks each of these: - A guard row dropped from `core:` goes red, naming the path and the step. - A guard step added with no row goes red, whether it reaches its script through an alias or names it directly. - An imported module dropped from `core:` goes red. - An alias chain is followed to its script. - A commented-out line requires nothing. - A guard that is also a build input needs only its one row. - A row whose step is gone is reported as a leftover, including in a directory that holds only a guard. - A script whose closure cannot be computed is refused. Over the real tree, it checks: - The five step-run scripts and `population-floor.mjs` are each covered by their own literal row. - With the lean-entry row dropped, `main()` returns 1. - The two recognizer bounds below are pinned to the real job. The scratch tree that the report-path cases run in now stages the guards' closures. Without that, a "returns 1" case could pass only because the build subject refused. **Ablation, run once on the real script and then restored.** The step loop in `buildGuardsOf` was mutated to read nothing, through `scripts/ablation-replace.mjs`: anchor 1 → 0, blob `bfb0ec815a` → `2f03c5416d`. - `--self-test`: exit 1, with 27 of 131 assertions failing. - 24 of battery (10)'s 30 failed. - Three real-tree cases in (5) and (9) that read the same verdict also failed. - Six cases in (10) held. Five of them do not depend on the derivation: the anchor, the control, the two bound pins, and the shared-row case. - The sixth is the report-path `main() === 1`, which the leftover direction below satisfied. Its sibling, which asserts the exact finding, failed. - The production gate: exit 1 as well, but in the reverse direction. With no population derived, the five rows became leftovers. So the rows are held to the steps from both sides, and an emptied derivation cannot pass silently. - Restore: the blob equals HEAD `bfb0ec815a`, `git diff HEAD` is empty, and the marker count is 0. ## Known bounds (stated in the header, two of them pinned in the self-test) - **Package-scoped scripts are not followed.** This means a script that a package's own manifest runs, via `pnpm --filter PKG SCRIPT`. Build Core's only such step today is the spec's `analyze`, which runs a file inside `packages/spec`, and `packages/**` covers that. Pinned. - **Local composite actions are not followed.** Build Core's only one is setup-pnpm. Its `run:` steps name no repository script, and every job in every workflow runs it. Pinned. - **`pnpm install`'s lifecycle is not a guard.** `prepare` runs `scripts/setup-git-hooks.mjs`, but every job installs, including the unfiltered lint.yml jobs, so a change to it already runs at PR time. - **Some extensions are not read.** A script in `.cjs` or `.ts` is not seen. This is the same as `check:self-test-wired`, because the recognizer is shared. - **Data files are not scripts.** A data file a guard reads is not derived. ## Acceptance notes - **The dual-build baseline is not covered.** `scripts/dual-build-cjs-loads.baseline.json` is a shrink-only ledger that `check:dual-build-cjs-loads` reads. A diff confined to it still skips Build Core (control row above). Closing that needs either a declared data-input list or reading the gate's own declaration, which goes beyond this card's "guard scripts". There is one such baseline today. Noted, not filed. - **A pre-existing comment overstates when Build Core runs.** `scripts/check-dual-build-cjs-loads.mjs` (its dispatch-gates declaration note) says Build Core is "a required context that runs on every PR". Build Core is path-scoped by `core:`, so that was not true before this PR either. Its operative claim, that a card touching `packages/**` still gets Build Core, holds. This PR does not make the sentence false, and the file is outside this card's surface. Noted, not edited. - **Comments this PR corrects.** These sentences were made false by the new population and are corrected here: - ci.yml's build-input comment ("a literal entry beside them covers none (a leftover)") - the parity header's leftover sentence - the parity header's known-bounds sentence ## Local verification All readings at `aa0b21ae2c`. - **Derived gate families.** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (no paths) derives 58 commands, the same 58 the seat derived at dispatch. - 53 exit 0. - The four dist-reading guards (`check:dts-closure`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:sourcemap-no-sources-content`) pass their `--self-test` legs, and their real legs exit 3 PREREQUISITE NOT MET because there is no `dist/` here. NOT MEASURED locally, reason: no build. This PR's ci.yml change schedules Build Core, which runs them. - `check:pm-dispatch-gates` exits 0: 1976 cases, and the battery took 791.9s. - `--ran` reconciliation: 58 derived, 54 run, 4 NOT-MEASURED (the exit-3 four), 0 unrun. - **Roster gates.** Those whose roster sits under a directory this diff touches: `check-platform-checklist-watchdog`, `check-published-list-mirrors`, `check:engine-double-contract` and `check:i18n-stale-fill` exit 0. `check:published-readme-exports` exits 3: NOT MEASURED, it needs built `.d.ts`. - **ESLint.** Narrowed to the changed lintable files and run with `--no-inline-config`. - That is one file, `scripts/check-ci-filter-parity.mjs`; ci.yml matches no `files` glob in `eslint.config.mjs`. - JSON output: 1 file, 0 errors, 0 warnings. - The config has no `parserOptions.project`, so linting is not type-aware and this diff cannot move any untouched file's result. - **The edited script's own tests.** No `*.test.*` in `scripts/` or in the root package names `check-ci-filter-parity`. The three hits elsewhere (`packages/types`, `packages/objectql`, `packages/runtime`) mention it in comments only. No changeset: workflow and repo-gate code only, nothing any package publishes (`skip-changeset`). --- _Generated by [Claude Code](https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #16980 — ⛔ deliberately NOT
Fixes. This lands options 1, 2 and 3. The half left open is the one the ruling reserved: adding the@objectstack/metadata/errorsrow toGUARDED_ENTRIES, which needs a secondADMITTED_PACKAGESset and therefore a MAINTAINER decision. The PM decides whether the card closes on this PR.scripts/check-lean-entry-closure.mjs'sresolveConditions()handedexports[subpath][flavour]straight tonode:path'sresolve()with no string check. Options 1 + 2 + 3 from the card, per the dispatch ruling. ⛔ NoGUARDED_ENTRIESrow is added andADMITTED_PACKAGESis untouched.Clause-②: no
scripts/**only — one file. Re-derived rather than copied: 70 published (non-private) manifests, zero shipscripts/check-lean-entry-closure.mjsin theirfiles[]; positive control, 70 of 70 ship adist/path. So the change reaches 0 published surfaces.Before leg — the card's own recipe, reproduced verbatim
Measured on a freshly created worktree at
origin/mainc43bac7048, UNBUILT (packages/objectql/distandpackages/metadata/distboth absent). The gate blob is byte-identical to the854639b311the brief pinned (e9370b2db2), so the brief's line numbers still hold.missingis the deliberate exit-3 PREREQUISITE path on an unbuilt tree, not the defect — as the card and the brief both warn.After leg — same recipe, same rows, same tree
The metadata row now reaches the gate's own diagnostics with its targets named, instead of never reaching a diagnostic at all.
The flat control is byte-identical, and that is measured mechanically rather than eyeballed:
git show origin/main:scripts/check-lean-entry-closure.mjswas written beside the patched file (same directory, soROOTresolves identically), both were imported in one process, and their control-row results were string-compared.The temporary baseline copy was removed;
git status --porcelainshowed only the one intended modification afterwards.What a future author now gets for an unreadable shape (option 2)
End-to-end through
resolveConditions()against a real fixture manifest whoseimportcondition is an object with nodefault:Error, notTypeError— so the refusal is distinguishable from thenode:pathcrash it replaces. On acceptance criterion 5 (the message must be honest): it names both live causes and asserts neither, because from insideconditionTarget()the gate genuinely cannot tell a malformed manifest from anexportsspelling it has not been taught. It does not tell the reader to build, and it does not claim the manifest is wrong.if (!rel) continueskipped every falsy value. Now onlyundefined/nullmean "this condition is not published";""andfalseare refusals. No manifest in this workspace has either (measured below), so nothing in the tree changes — but it is a change, not a refactor.Negative control — the gate on a BUILT tree, figures RE-DERIVED
Built with
pnpm turbo run build --filter=@objectstack/objectql --concurrency=2underscripts/pm/os-verify-lock.sh(15 of 15 tasks successful;VERDICT command-exit 0).⭐ Re-derived on
c43bac7048+ this branch, not copied from the card. They land on the card's figures — 2 conditions, 15 packages, 185 / 183 modules, admitted set held exactly — which is the result, not the input.And the strongest form of "unchanged": the
origin/maingate and the patched gate were both run against this same built tree and their stdout diffed byte for byte — identical.Self-test: one case per branch, and the floor ratcheted
New battery
reading a condition, 6 cases: flat condition, nested condition, absent condition, an object with nodefaultrefused, a third shape (array) refused, and one end-to-endresolveConditions()over a nested fixture manifest. Roster 5 → 6 batteries;SELF_TEST_BATTERY_FLOOR5 → 6; cases 16 → 22.Every mutation below was proved on disk (
grep -con the exact anchor text before and after, plus the file'sgit hash-objectagainst itsHEADblob) and restored withgit checkout HEAD -- <path>, verified by an emptygit diff HEADand by the restored blob hashing back to8672f581160c61f80134ea605072ade106d2b850. The fix was committed first, so the restore leg pointed at aHEADthat carried it.Firing control —
SELF_TEST_BATTERY_FLOOR6 → 7, i.e. above the 6 declared batteries:Nonsense control — the roster name changed to one no case registers under (
'reading a condition'→'reading a conditionZZZ'), the exact silencing the roster exists to catch:Ablation of the fix itself —
conditionTarget()reverted to the pre-fixreturn value;. The new battery does not merely go red, it reproduces the reported defect exactly, at the same argument index:Header (option 3) — the sentence that stays false after 1 and 2
The table shape is what makes that a one-row change for whoever owns it.is replaced with what a second entry actually costs: its ownADMITTED_PACKAGESset, and a package entering an admitted set moves the ADR-0076 D2 boundary and carriesRATCHET_AUTHORITY_MARKER = '⛔ MAINTAINER-ONLY'. ⇒ Whoever owns a second entry owns a maintainer decision, not just a row.Does option 1 actually cover this repo? (the triage seat left this unmeasured)
The triage comment asked, explicitly, not to assume
.defaultexists on all nested conditions. Measured across all 71 workspace manifests with anexportsmap:default⇒ Option 1 covers 211 of 211 live conditions. And because there are zero third shapes in the tree today, option 2 is prophylactic by construction — which is why its self-test cases have to supply the third shape synthetically. That is the honest reading of what each half buys.
Gates run — TEXT, not just exit codes
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 31 families from the merge basec43bac704(its own change-set derivation, not a hand-written list). All 31 were run and reconciled:Zero non-zero exits across the batch. Named among them:
check:lean-entry-closure,check:ratchet-remedy-authority(it reads this very file),check:nul-bytes,check:self-test-wired,check:scripts-symbol-anchors,check:pm-dispatch-gates.Beyond the derived set:
eslint . --no-inline-configwas run in full, not narrowed — 6410 files, 0 errors, 0 warnings, on the final commit9998c82a5e. No narrowing argument is needed. (For the record, the config states ateslint.config.mjs:328that there is noparserOptions.projectand no typed rules.)packages/objectql/src/core-boundary.ratchet.test.ts— the neighbouring source-scan guard: 1 file / 2 tests passed. It names this gate only in a comment; there is no executable coupling.check:nul-bytes:grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'on the changed file — no hits.Swept: the 31 derived families, the full repo eslint, the neighbouring ratchet test, the published-manifest
files[]reach, the workspace-wide condition-shape census, control characters.Not swept, and I am naming them rather than implying clearance:
dispatch-gatesitself prints as outside the derived total. Their silence is a fact about a list, not about these paths.check-shard-attestation,check-test-completeness) — NOT MEASURED locally by construction.throws; it does not become afindings[]entry. So throughmain()it still surfaces as an uncaughtErrorwith a stack — the message is on top and carries everything, but it is not routed through the gate's own✗ N finding(s)report. I kept the existing mechanism deliberately: the shape guard one level up (exports[subpath]is not an object) already throws the same way, and the triage seat's advice was to move that existing pattern down a level rather than design a new diagnostic style. Deciding that "this gate cannot measure this row" deserves its own report channel — a fourth judgement alongside pass, finding and PREREQUISITE — is a wider call that belongs with whoever adds a second entry. ⛔ It is not the PREREQUISITE path: routing it there would print "run the build first", which is a cause that did not occur.:116; onorigin/mainc43bac7048it spans:120-121.RATCHET_AUTHORITY_MARKERat:156is exact.⛔ I did not add the
@objectstack/metadata/errorsrow and did not widenADMITTED_PACKAGES. Nothing I measured changed my view that adding it is a separate card and a maintainer decision — and the census above sharpens the price rather than lowering it: the row would resolve cleanly now, which is precisely what makes the second admitted set the whole remaining cost.Changeset:
scripts/reaches 0 published manifests (measured above). IfCheck Changesetreds, I have not labelled — that is the PM'sskip-changesetto apply.🤖 Generated with Claude Code
https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
Generated by Claude Code