Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions packages/lint/src/authoring-rule-wiring.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -411,13 +411,17 @@ describe('authoring-rule registry wiring (#4409)', () => {
`here rebuilds, on a fourth surface, the exact drift #3583 → #4409 took five repairs to end.`,
).toEqual([]);

// And it must not reach past the kernel-safe entry: `@objectstack/lint`'s
// root barrel pulls the react/jsx rules' module graph, which is the one
// thing the boot path may not name (`lazy-deps.test.ts`).
// And it must not reach past the narrow entry. What that pins is the
// EXPORT surface, not the module graph: measured, `./runtime` reaches 70
// of the root's 72 modules, so BOTH entries name the react/jsx rules'
// modules (`runtime-lazy-deps.test.ts` says so from the other side). What
// the root barrel adds is the 258 further NAMES, every CLI-only rule
// among them — and a kernel-path consumer that can name one can
// hand-call one, which is the drift #4463 closed.
expect(
source,
`${RUNTIME_GATE_FILE} must import from '@objectstack/lint/runtime', not the root barrel — ` +
`the root entry reaches the typescript/sucrase rules the kernel boot path must not name.`,
`the root entry exports the CLI-only rules the kernel gate must never call directly.`,
).not.toMatch(/from\s*['"]@objectstack\/lint['"]/);
});

Expand Down
7 changes: 5 additions & 2 deletions packages/lint/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -658,8 +658,11 @@ export type {
} from './authoring-rules.js';

// The runtime publish gate over that registry. Also published as the
// `@objectstack/lint/runtime` subpath — the entry the kernel boot path imports,
// so a consumer there never names the graph that reaches the source parsers.
// `@objectstack/lint/runtime` subpath — the entry the kernel boot path imports.
// That subpath narrows the EXPORT surface to these five names, not the module
// graph: measured, it reaches 70 of this entry's 72 modules and 93.8% of its
// bundled bytes, and it does name the modules that reach the source parsers.
// `runtime.ts`'s header carries the measurement and what the narrowing buys.
export {
buildRuntimeWriteSnapshots,
runRuntimeAuthoringRules,
Expand Down
66 changes: 53 additions & 13 deletions packages/lint/src/runtime.ts
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,59 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* `@objectstack/lint/runtime` — the KERNEL-SAFE entry (#4463).
*
* The metadata write path (`@objectstack/metadata-protocol`) sits on the kernel
* boot path and must reach the shared rule core without dragging the lint
* package's gate-only dependencies (`typescript` ~9 MB, `sucrase`) into it.
* That constraint is real and it is guarded from both ends:
*
* - `lazy-deps.test.ts` pins that no `src/` file eagerly imports either dep, so
* importing this entry loads neither;
* - `runtime-lazy-deps.test.ts` pins the stronger claim this entry needs — that
* RUNNING the gate on a real, gated body loads neither either, because the
* rules #4463 wired to `runtime-publish` (flow / approval / expression /
* reference) never parse authored source.
* `@objectstack/lint/runtime` — the NARROW entry. Not the light one (#4463).
*
* ## What it is, measured
*
* This entry narrows the EXPORT SURFACE. It does not narrow the module graph,
* and reading it as a weight boundary is a trap that costs a console build to
* disprove. Measured by walking the static import graph of `src/` from each
* entry, and by `stat` on this package's own `tsup` output:
*
* | | modules reached | ESM bundle | exported names |
* |------------|------------------------|------------------|----------------|
* | `.` | 72 | 552,936 B | 263 |
* | `./runtime`| 71, 70 shared with `.` | 518,583 B (93.8%)| 5 |
*
* So: **93.8% of the bytes, 1.9% of the surface.** The one non-barrel module
* `.` reaches and this entry does not is `lint-startup-registry-verdict.ts`;
* that single module IS the whole graph delta, and it is not the reason the
* entry exists. (Re-derive rather than trust these: the numbers move with the
* rule set, the ratio has not.)
*
* The published `.d.ts` is the one place the narrowing shows as bytes —
* `dist/runtime.d.ts` is a 278 B re-export line against `dist/index.d.ts`'s
* ~158 KB — because types are erased, and code is not.
*
* ## What it does NOT carry
*
* `validateCapabilityReferences` is not exported here, and switching to this
* entry to get a cheaper capability check gets neither half: the rule's code is
* compiled INTO `dist/runtime.js` (it is a member of the one shared registry),
* it is only not named on the way out. You pay its bytes and cannot call it.
* The root entry is where that rule, and the other 257 names, are reachable.
*
* ## Why it exists anyway
*
* So the kernel boot path can name only the five gate functions below, and so a
* test can prove it named nothing else. `@objectstack/metadata-protocol`'s
* runtime gate must reach this package through this entry rather than the root
* barrel, and `authoring-rule-wiring.test.ts` fails if it ever does otherwise.
* That is an import-discipline boundary, machine-checked: it stops a
* kernel-path consumer from hand-calling a CLI-only rule, which is the drift
* #4463 closed. The value is the pin, not the payload.
*
* ## What is the PACKAGE's doing, not this entry's
*
* Lazy dependency loading. `lazy-deps.test.ts` pins that no `src/` file eagerly
* imports `typescript` (~9 MB), `sucrase` or `ajv` — so importing `.` loads
* none of them either, and this entry is not what buys that.
* `runtime-lazy-deps.test.ts` adds the claim this consumer actually needs: that
* RUNNING the gate on a real, gated body loads none of them, because the rules
* #4463 wired to `runtime-publish` (flow / approval / expression / reference)
* never parse authored source. Both are properties of which rules RUN. Neither
* is a property of which entry you import — the react/jsx rules' modules are
* present in this entry's graph, exactly as `runtime-lazy-deps.test.ts` states.
*
* The deliberate NON-goal: this is not a second, lighter rule set. It re-exports
* a filtered view of the ONE registry in `authoring-rules.ts`. If the two ever
Expand Down
9 changes: 6 additions & 3 deletions packages/lint/tsup.config.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,9 +5,12 @@ import { defineConfig } from 'tsup';
* cannot use the repo-root `tsup.config.ts` (single `src/index.ts`).
*
* - `index` — the full authoring surface, used by the CLI.
* - `runtime` — the kernel-safe subset the metadata write path imports. Kept a
* separate entry so a consumer on the boot path never even names the module
* graph that reaches the react/jsx source parsers.
* - `runtime` — the narrowed subset the metadata write path imports. It is a
* separate entry so that surface can be PINNED (`authoring-rule-wiring.test.ts`
* fails if the kernel gate imports the root barrel instead), not because it is
* lighter. `splitting: false` emits each entry self-contained, and measured
* `dist/runtime.js` is 93.8% of `dist/index.js` and does name the react/jsx
* rules' modules. `src/runtime.ts`'s header carries the measurement.
*/
export default defineConfig({
entry: ['src/index.ts', 'src/runtime.ts'],
Expand Down
Loading