Repository navigation
fix(devx): hold the cross-package gate's bare-specifier boundary to its own premise - #18667
Merged
Merged
Conversation
…ts own premise The import-side boundary excludes a BARE specifier because it "is an installed dependency resolved through node_modules". That ground is real, and it is a CONDITIONAL: it holds only while the importing package declares the target. The declaration is the edge `turbo ls --affected` walks and the edge that folds the target's build hash into `<pkg>#test`, so an undeclared workspace import resolved by a vitest alias or a hoisted node_modules has no graph, no glob and no flag — #7802 by a sixth spelling, through the one door the file had declared safe on reasoning alone. So the exclusion stays and the premise is checked on every run: `bareWorkspaceImportCensus()` fails naming any test whose bare workspace specifier its package does not declare, and prints the live census so the limb's silence is a measurement. Measured on this tree (3714 test sources under packages/, apps/, examples/): 2010 files import a workspace sibling by bare specifier, 3692 occurrences, 349 distinct package pairs over 66 importers and 55 targets — and 0 of the 349 lack a manifest edge. Collecting them into the radius roster is refused on that measurement: it would write packages/<x>/src/** onto 66 packages pointing at 55 siblings and buy nothing, because the graph already carries every pair. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
os-bill
marked this pull request as ready for review
September 17, 2026 13:47
os-bill
enabled auto-merge
September 17, 2026 13:47
This was referenced Sep 17, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…bjectstack-ai#18684) Fixes objectstack-ai#18500 Clause-②: no PR objectstack-ai#18486 landed the `dimensionless` schema marker with its **gate reader** and no **docs half**. Ruling B on objectstack-ai#14478 put both halves in the mechanism, verbatim: > declared ON THE SCHEMA, never in a gate ledger … marker the gate honours **and the docs generator publishes** The sibling marker `externalVocabulary` has had its renderer in `packages/spec/scripts/lib/schema-section.ts` since objectstack-ai#15626. `dimensionless` had none, so a key marked `dimensionless` would carry an in-schema declaration that the published reference page says nothing about. The gate's own header already asserts the half that did not exist. `check-duration-unit-keys.ts`, exemption class 4: *"Same mechanism as class 2, same literal-only validation, **same visibility**."* Class 2 is `externalVocabulary`, and its visibility **is** that renderer. This PR is what makes that sentence true. ## What landed - `dimensionlessNote(prop)` in `packages/spec/scripts/lib/schema-section.ts`, beside `externalVocabularyNote(prop)`, appended to the same description cell rather than given a column. - Six pins in `packages/spec/scripts/schema-section.test.ts`, mirroring the sibling's block one-for-one. A key described `Failures seen in the last 5 minutes` and carrying `.meta({ dimensionless: 'failed attempts' })` now publishes: ``` Failures seen in the last 5 minutes (dimensionless — counts failed attempts) ``` ### Why that wording The card suggested "counts `what`" and wrote "⛔ no ruling implied", so the shape was read off the declaration instead of taken from the suggestion. The marker's declared value is a **non-empty string literal naming what the number counts** — `check-duration-unit-keys.ts` refuses an empty string and a computed value, because an unverifiable claim exempts nothing — so the declared value is printed verbatim, and the same non-empty-string-literal test decides whether anything is printed at all. The word `dimensionless` is kept in front of it for the one thing the value alone does not state: the exemption's claim is that the number has **no unit**, which is exactly the ambiguity a reader has when the prose beside a naked number names a time unit. It also keeps one vocabulary across the marker name, the census line the gate prints, and the ruling. ## The three questions the dispatch asked **1. Which gate can actually go red on this change?** `packages/spec/scripts/schema-section.test.ts`, run by `pnpm --filter @objectstack/spec test` (vitest project `local`; the file is not listed in `vitest.repo-tests.json`) — in CI, `Test Core`. `check:docs` and `check:generated` structurally cannot move: the live population is **0** keys marked, so no reference page changes and `check:docs` is green both before and after. Established by ablation, not assumed — below. **2. Does the marker ride into the published JSON Schema via `z.toJSONSchema`?** — **Yes.** - Direct, on the zod this workspace installs (4.4.3): `z.number().meta({ dimensionless: 'failed attempts' })` emits `"dimensionless": "failed attempts"` on the node, unchanged. A fabricated marker name rides identically, so the channel is generic rather than per-key. - On the real generated tree, after `pnpm --filter @objectstack/spec gen:schema`: 18 files under `packages/spec/json-schema/` carry `"externalVocabulary"` (LIT control), **0** carry `"dimensionless"`, **0** carry the fabricated name (DARK controls). `packages/spec/json-schema/` has 0 tracked files, so this cannot be read from git — it was built and read. ⇒ **the docs generator is not the only publisher.** `json-schema` is in `packages/spec/package.json` `files[]`, so a marked key would also ship inside the npm tarball. Nothing is marked by this PR, so nothing moves today — but the *second* publisher is a fact the first card to mark a key inherits. **3. What should it print?** — answered from the declared value shape, above. ## Clause-② and the changeset Declared `no`, unchanged — the dispatch reserved that line, and question 2's answer does not move it for **this** diff: no key is marked, so no published byte changes. `skip-changeset`, measured rather than assumed: - `packages/spec/package.json` `files[]` = `dist`, `json-schema`, `liveness`, `prompts`, `llms.txt`, `README.md`, `src/**/*.zod.ts`, `CHANGELOG.md`, `api-surface`, `spec-changes.json`. `scripts/**` is not among them. - `dimensionlessNote` after the build: **0** hits in every one of those paths. Positive control on the same greps, `ObjectSchema`: `api-surface` 2, `llms.txt` 1, `README.md` 1, `src/**/*.zod.ts` 14. - `dist` cannot carry it either way: every `tsup` entry in `packages/spec/tsup.config.ts` is under `src/`, so `scripts/**` is not an entry. - `check:docs` green ⇒ **0** reference pages move.⚠️ For the seat that marks the first key: at that moment the marker reaches **both** publishers, and `json-schema/` is published — so that card's declaration and changeset are a different question from this one's. ## Testing Union run at `d6f2148892` (the final commit; gate logs carry no sha, so this is the tree every number below was taken on). | run | result | |---|---| | `pnpm --filter @objectstack/spec test` (project `local`) | 484 files passed / 1 skipped, **13872 passed** / 1 skipped | | `pnpm --filter @objectstack/spec typecheck` (incl. `check:scripts-typecheck`, `check:test-typecheck`) | exit 0 | | `pnpm --filter @objectstack/spec check:docs` · `check:variant-docs` · `check:liveness` · `check:empty-state` · `check:strictness-ledger` | exit 0 each | | `pnpm lint` (`eslint . --no-inline-config`, whole repo — not narrowed) | exit 0 | | `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ...` | **57 derived, 52 run, 5 NOT MEASURED, 0 UNRUN** | The 5 NOT MEASURED are `check:dts-closure`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:sourcemap-no-sources-content`, `check:type-check-debt` — each exited **3**, `PREREQUISITE NOT MET`, which those gates spell out as "not a pass and not a finding". All five read built output of the whole monorepo, which this worktree does not have; CI's `Build Core` is where they are measured. This diff adds no export, no entry point and no emitted file, so it has no channel to reach any of them. ### Red before green — ablation with an on-disk proof and a hash-verified restore Run from the committed state, under a `trap ... EXIT INT TERM`, against absolute paths: 1. `HEAD:packages/spec/scripts/lib/schema-section.ts` blob = `a92ba895b1f51e8eaf8945e45f8bf8c12ea77e90`. 2. Anchor count before mutation: **1**. Mutation deletes the `dimensionlessNote(prop)` term from the description cell. 3. **Landed on disk**, both directions: deleted text **0** occurrences after, injected marker **1** occurrence after; mutated blob `4f00017eab...` differs from the HEAD blob. 4. Red leg: `Tests 4 failed | 39 passed (43)` — the four cases that assert the note is printed. The two that assert its **absence** stayed green, which is the predicted direction and what keeps the note from decorating every row in the reference. 5. Restore leg: `git checkout HEAD -- path` (never a bare `git checkout --`), then hash re-read = `a92ba895b1...`, matching, **and** `git diff HEAD` empty. Green re-run at the final head: 43 passed. Predicted direction was stated before the run and matched: turn red, not "more diagnostics" and not "inverted". ## `skills/**` derivative **None.** Reading, not expectation: the diff is exactly 2 files (`git diff --name-only` against the merge base), **0** of them under `skills/`. `packages/spec/scripts/build-skill-docs.ts` does not import `schema-section`, and `check:skill-docs` / `check:skill-refs` were not derived for these paths. So no net-line-count reading is owed. ## Acceptance notes Out of scope, noted and deliberately not filed: - **The `.meta()` channel is generic, not a marker allowlist.** A fabricated key name rides `z.toJSONSchema` into `json-schema/` exactly as `externalVocabulary` and `dimensionless` do, and `json-schema/` is published. This is the documented mechanism (`xRef` / `xExpression` / `xEnumDeprecated` use the same channel by design), not a defect, and none of the three filing classes fits: no repro, no contract violated, and the hazard is metadata being *published* rather than silently dropped or refused. Who will meet this file next: objectstack-ai#18124 step 4, the card that will first mark keys. - **Nothing mechanically ties "a marker the gate reads" to "a renderer in `schema-section.ts`".** The next marker added to `check-duration-unit-keys.ts` can repeat exactly this half-build. Not filed: it is undemonstrated drift today, ruling B is honoured on the tree as of this PR, and both markers that exist now carry both halves. ## Dispatch fences observed `packages/spec/src/migrations/**` and `scripts/check-cross-package-test-inputs.mjs` untouched (the latter moved on `main` under objectstack-ai#18667 and arrived through a plain merge of `origin/main`, with no conflict and no edit by this branch). `packages/spec/scripts/check-duration-unit-keys.ts` read only. No live key was marked `dimensionless` — population stays 0, by measurement above. --- _Generated by [Claude Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_ 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.
Fixes #18236
Clause-②: no
File surface:— one path,scripts/check-cross-package-test-inputs.mjs. Noturbo.json, no.changeset/*.md, nopackages/spec/src/**, no governed surface. Why each of those stayed shut ismeasured below, not asserted.
The card's premise, re-taken on
origin/main— and one half of it is falsifiedThe card's mechanism is:
packages/cli/test/format-zod-union.test.tsconsumesListViewSchema.sortthrough the bare specifier@objectstack/spec, that specifier is excludedby the gate's import-side boundary, therefore the coupling is invisible to both the
affected-subset filter and the turbo cache. The first two links hold — the import is bare (
:37)and the gate does skip it. The third does not, and it is the one the fix would have rested on.
Measured on
62d830e54e, turbo 2.10.10, with a firing control beside each reading:turbo ls --affectedfor a diff touching ONLYpackages/spec/src/ui/view.zod.ts@objectstack/cliamong them--union-intoNo cross-package scan declares inputs touched by this diff.)@objectstack/cli#testturbo hash, view.zod.ts pristine09d6dde83abb1ddf2d8ebbbb4202c0e5— movespackages/qa/dogfood/test/action-params-contract.dogfood.test.ts, marker count 0 to 1)09d6dde83abb1ddf— unmoved, so the instrument is specificturbo ls --affected钉TURBO_SCM_BASE=HEAD后,干净树读 0 packages;只改view.zod.ts读 76,含@objectstack/cli⇒ the PROBE the card asks for already passes, before any change. Layer A reaches
@objectstack/clithrough the dependency graph, and Layer B moves because@objectstack/cli#testdependsOn its own
build, which dependsOn^build, which folds@objectstack/spec#build's hash in.packages/cli/package.jsonhas declared"@objectstack/spec": "workspace:*"throughout, includingat
1e20f816e^(checked on that tree, not recalled).Why that answers all three of the seat's questions at once, with numbers
Measured over all 3714 test sources under
packages/,apps/andexamples/:because the importing package declares the target — that is what pnpm's strict linking means —
and that declaration is an edge on the graph both CI layers already walk. All 349 pairs have one.
graph, so
turbo.jsonneeds no newinputsentry and this diff touches none.natural glob is
packages/TARGET/src/**, so 349 declarations across 66 packages pointing at 55siblings' whole source trees — the explosion the boundary was written to prevent — for zero
coverage, since the graph carries every pair. Refused on that measurement. The affected subset
after this PR is byte-for-byte the subset before it: 0 declarations added, 0 packages moved.
What IS wrong, and what this PR changes
The boundary asserts a conclusion ("a bare specifier is an installed dependency, never a repo source
input") while nothing checks its premise. That premise fails the moment a bare specifier names a
workspace package the importer does not declare, and such an import still resolves whenever
something else supplies it — a vitest
resolve.alias, a hoistednode_modules, a tsconfig path.Then there is no manifest edge, no graph edge, no glob and no flag: #7802 by a sixth spelling,
arriving through the one door this file had declared safe on reasoning rather than on a check.
So the exclusion stays and becomes conditional, with the condition enforced every run:
bareWorkspaceImportCensus()is its own walk, not a second output offindEscapingPackages(): thatwalk opens with a
FS_READ-or-ascending-literal pre-filter which a file importing only barespecifiers never passes — precisely the file this question is about. The verdict line now prints the
live census, so the limb's steady-state silence is a measurement rather than an absence:
Those two numbers are produced by the gate and agree with the independent scan above, which was
written first and separately.
Red before green — named gate, shown red, on the incident file itself
The gate that can go red here is
pnpm check:cross-package-test-inputs, and the ablation deletesthe one manifest edge the premise rests on. Committed first, then mutated, then restored.
check:cross-package-test-inputs"@objectstack/spec": "workspace:*"deleted frompackages/cli/package.jsongit diff HEAD --name-only0 to 1 line; the mutated manifest still parses as JSONgit hash-objectequals theHEADblob (5d84eb3ea…);git diff HEAD --name-onlyback to 0 linesThe decisive finding under mutation names the incident file and the incident import:
No rebuild leg is owed and none is claimed: this gate is a
scripts/*.mjssource scan invokeddirectly by node, with no
dist/anywhere in its resolution path, so the mutation reached the codeunder test the moment it reached disk.
Direction declared before the run and observed: turns red — the ordinary direction.
Controls
:38../src/utils/format,:39./helpers/serve-process.js) were already visible to the roster half and still are: this difftouches no code on that path, and
check:cross-package-test-inputsreports the same 29 escapingpackages, the same 13 walked roots and the same 3 accepted radii before and after.
vitest,zodandnode:fsmust read 0 — pinned by newself-test cases one per shape, beside the three pre-existing
does NOT flagcases that thecheck-cross-package-test-inputs' literal collector cannot see an escaping relative IMPORT specifier, so a test that imports a module outside its package goes undeclared silently #10452 boundary already carried. And the affected subset must not balloon: it does not move by a
single package, because this limb adds no declaration and no glob (0 of 349 pairs are findings).
at the top of this body is the evidence, taken with its own firing control.
Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackpnpm check:cross-package-test-inputsnode scripts/check-cross-package-test-inputs.mjs --self-testnode scripts/pm/check-widening-tells.mjs --declaration no --diff …scripts/**is covered by no declared surface, so no tell could have fired whatever the diff containsnode scripts/check-published-list-mirrors.mjsRECOGNISED_PATH_SPELLINGSis untouched; onlyRECOGNISED_IMPORT_SPELLINGS, which no mirror row declares, gains four comment linesgrep -naPover the changed file: 0 hits, besidepnpm check:nul-bytesskip-changeset: nothing here publishes.scripts/sits at the repo root, outside every packagedirectory, so no package's
files[]can ship it; the only packages that name this script do so inturbo
inputs, which is hashing, not shipping.Acceptance notes
sortclause on the list-view doors — the PRODUCER half of the sort seam #17914'sown run (34746808828,
Test Core): the timing table reportscache-replayed packages, excluded as non-measurements: 1 (@objectstack/cli)and marks every@objectstack/clirowreplayed log— so@objectstack/cli#testwas a turbo cache REPLAY on thePR that reddened
main, and the suite never ran. That contradicts the hash measurement above,which shows the task hash moving for exactly that diff. One of the two readings is about a
mechanism nobody has named yet. Reported to the dispatching seat as a finding to file; it is the
link that actually let feat(spec)!: retire the bare string
sortclause on the list-view doors — the PRODUCER half of the sort seam #17914 through, and it survives this PR untouched.queue-flake anchornaming convention (Queue-flake anchor: test/format-zod-union.test.ts #18032) is noted, not filed here: it is a triagenarrative question, and it already carries its own "This issue is a NAME, not a diagnosis" line.
Generated by Claude Code
Generated by Claude Code