Skip to content

fix(devx): hold the cross-package gate's bare-specifier boundary to its own premise - #18667

Merged
os-bill merged 1 commit into
mainfrom
claude/issue-18236-cross-package-bare-specifier
Sep 17, 2026
Merged

os-bill merged 1 commit into
mainfrom
claude/issue-18236-cross-package-bare-specifier

Conversation

@os-bill

@os-bill os-bill commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #18236

Clause-②: no

File surface: — one path, scripts/check-cross-package-test-inputs.mjs. No turbo.json, no
.changeset/*.md, no packages/spec/src/**, no governed surface. Why each of those stayed shut is
measured below, not asserted.

The card's premise, re-taken on origin/main — and one half of it is falsified

The card's mechanism is: packages/cli/test/format-zod-union.test.ts consumes
ListViewSchema.sort through the bare specifier @objectstack/spec, that specifier is excluded
by 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:

reading result
turbo ls --affected for a diff touching ONLY packages/spec/src/ui/view.zod.ts 76 of 81 packages, @objectstack/cli among them
the same document after the real --union-into 76, unchanged (No cross-package scan declares inputs touched by this diff.)
@objectstack/cli#test turbo hash, view.zod.ts pristine 09d6dde83abb1ddf
the same hash, view.zod.ts mutated (marker count 0 to 1 on disk) 2d8ebbbb4202c0e5 — moves
CONTROL: the same hash, an unrelated package mutated instead (packages/qa/dogfood/test/action-params-contract.dogfood.test.ts, marker count 0 to 1) 09d6dde83abb1ddf — unmoved, so the instrument is specific
PM 补的 Layer A 亮控(复核时第一手加测):同一条 turbo ls --affected 钉 TURBO_SCM_BASE=HEAD 后,干净树读 0 packages;只改 view.zod.ts 读 76,含 @objectstack/cli ⚠️ 不钉 base 时干净树也读 76 —— 上面第一行那个 76 单独看不区分「改了」和「没改」;钉了 base 之后结论不变

⇒ the PROBE the card asks for already passes, before any change. Layer A reaches
@objectstack/cli through the dependency graph, and Layer B moves because @objectstack/cli#test
dependsOn its own build, which dependsOn ^build, which folds @objectstack/spec#build's hash in.
packages/cli/package.json has declared "@objectstack/spec": "workspace:*" throughout, including
at 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/ and examples/:

test files importing a workspace sibling by BARE specifier 2010
bare workspace specifier occurrences 3692
distinct (importer, imported) package pairs 349
distinct importing packages / imported packages 66 / 55
pairs NOT backed by an edge on the importer's manifest 0
  1. How should bare specifiers be admitted? They should not. A bare specifier resolves only
    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.
  2. Can turbo express it? It does not have to. The coupling is already in turbo's own dependency
    graph, so turbo.json needs no new inputs entry and this diff touches none.
  3. Does the fix put every package's suite on every sibling? The card's route 2 would: the
    natural glob is packages/TARGET/src/**, so 349 declarations across 66 packages pointing at 55
    siblings' 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 hoisted node_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:

A bare specifier is excluded from the radius roster BECAUSE it is an installed dependency. Every
bare specifier naming a workspace package must therefore be declared as one by the package
importing it.

bareWorkspaceImportCensus() is its own walk, not a second output of findEscapingPackages(): that
walk opens with a FS_READ-or-ascending-literal pre-filter which a file importing only bare
specifiers 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:

OK: 29 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob
(6 of them on a split "test:repo" task); 13 walked root(s) judged, 3 on ACCEPTED_WALK_RADII;
2010 test file(s) import a workspace sibling by bare specifier over 349 package pair(s), every one declared.

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 deletes
the one manifest edge the premise rests on. Committed first, then mutated, then restored.

leg on-disk proof check:cross-package-test-inputs
baseline — exit 0, the OK line above
mutated: "@objectstack/spec": "workspace:*" deleted from packages/cli/package.json deleted-text count 1 to 0; git diff HEAD --name-only 0 to 1 line; the mutated manifest still parses as JSON exit 1, 92 findings
restored git hash-object equals the HEAD blob (5d84eb3ea…); git diff HEAD --name-only back to 0 lines —

The decisive finding under mutation names the incident file and the incident import:

- @objectstack/cli imports the workspace package `@objectstack/spec` from a test and does not declare it.
      packages/cli/test/format-zod-union.test.ts
        @objectstack/spec

No rebuild leg is owed and none is claimed: this gate is a scripts/*.mjs source scan invoked
directly by node, with no dist/ anywhere in its resolution path, so the mutation reached the code
under test the moment it reached disk.

Direction declared before the run and observed: turns red — the ordinary direction.

Controls

  • ⭐ LIT CONTROL. The incident file's two relative imports (:38 ../src/utils/format, :39
    ./helpers/serve-process.js) were already visible to the roster half and still are: this diff
    touches no code on that path, and check:cross-package-test-inputs reports the same 29 escaping
    packages, the same 13 walked roots and the same 3 accepted radii before and after.
  • ⭐ DARK CONTROL, two halves. vitest, zod and node:fs must read 0 — pinned by new
    self-test cases one per shape, beside the three pre-existing does NOT flag cases that the
    check-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).
  • ⭐ PROBE. Satisfied before and after, by the graph rather than by this gate — the measurement
    at the top of this body is the evidence, taken with its own firing control.

Gates

gate verdict
30 commands derived by node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack see the run table in the report; all exit 0
pnpm check:cross-package-test-inputs exit 0
node scripts/check-cross-package-test-inputs.mjs --self-test 214 cases (was 191), exit 0, verdict line reached
node scripts/pm/check-widening-tells.mjs --declaration no --diff … NOT MEASURED — exit 0 while judging nothing: scripts/** is covered by no declared surface, so no tell could have fired whatever the diff contains
node scripts/check-published-list-mirrors.mjs exit 0 — RECOGNISED_PATH_SPELLINGS is untouched; only RECOGNISED_IMPORT_SPELLINGS, which no mirror row declares, gains four comment lines
control characters grep -naP over the changed file: 0 hits, beside pnpm check:nul-bytes

skip-changeset: nothing here publishes. scripts/ sits at the repo root, outside every package
directory, so no package's files[] can ship it; the only packages that name this script do so in
turbo inputs, which is hashing, not shipping.

Acceptance notes


Generated by Claude Code


Generated by Claude Code

…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 os-bill added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 17, 2026 — with Claude
@os-bill
os-bill marked this pull request as ready for review September 17, 2026 13:47
@os-bill
os-bill enabled auto-merge September 17, 2026 13:47
@os-bill
os-bill added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit 137781f Sep 17, 2026
44 checks passed
@os-bill
os-bill deleted the claude/issue-18236-cross-package-bare-specifier branch September 17, 2026 14:14
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants