diff --git a/packages/lint/src/authoring-rule-wiring.test.ts b/packages/lint/src/authoring-rule-wiring.test.ts index bb267ac439..d8ff2ec2bb 100644 --- a/packages/lint/src/authoring-rule-wiring.test.ts +++ b/packages/lint/src/authoring-rule-wiring.test.ts @@ -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['"]/); }); diff --git a/packages/lint/src/index.ts b/packages/lint/src/index.ts index 0b40167a2b..e79e0c6c13 100644 --- a/packages/lint/src/index.ts +++ b/packages/lint/src/index.ts @@ -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, diff --git a/packages/lint/src/runtime.ts b/packages/lint/src/runtime.ts index 42c47a8a03..c5af8b3ec0 100644 --- a/packages/lint/src/runtime.ts +++ b/packages/lint/src/runtime.ts @@ -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 diff --git a/packages/lint/tsup.config.ts b/packages/lint/tsup.config.ts index cfbcbdb9eb..54940a82a5 100644 --- a/packages/lint/tsup.config.ts +++ b/packages/lint/tsup.config.ts @@ -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'],