diff --git a/scripts/bench/runtime-publish-gate.bench.mts b/scripts/bench/runtime-publish-gate.bench.mts deleted file mode 100644 index 85cbdc2e365..00000000000 --- a/scripts/bench/runtime-publish-gate.bench.mts +++ /dev/null @@ -1,612 +0,0 @@ -// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. -/** - * Runtime publish-gate benchmark (#9851) — what one `active` metadata publish - * costs at the shipped gate, as a function of the tenant's stored objects. - * - * ── The three modes ───────────────────────────────────────────────────────── - * - * --mode total (default) the whole-gate cost table. #9851's reading. - * --mode per-rule (#9905) the same total, ATTRIBUTED across the rules the - * door dispatches. - * --mode closure (#9905) the whole-gate cost when the `objects` collection - * is narrowed to the written item's reference closure. - * --mode package (#9612) the whole-gate cost when `objects` is narrowed to - * the written item's PACKAGE closure, by the SHIPPED - * deriver — plus the differential-verdict check and two - * positive controls. Unlike `--mode closure` this is not a - * hypothetical: it calls the same - * `narrowObjectsToPackageClosure` the gate calls, on a - * PACKAGED seed, so the number describes shipped behaviour. - * - * ⛔ `--mode closure` measures a HYPOTHETICAL. It narrows what the rules are - * handed HERE, in this script, and changes nothing about the shipped gate — - * which always receives the whole collection. Its closure deriver is a - * deliberately generous over-approximation written to bound the saving, NOT a - * proposed implementation: how (or whether) the shipped gate should scope its - * input is an open maintainer decision (#9612 / #9613), and one that this - * measurement exists to inform rather than to pre-empt. - * - * ── Why this file exists ──────────────────────────────────────────────────── - * - * Two published measurements of THIS operation disagreed by 16-25x with a - * non-constant ratio, and neither could be re-run: both came from throwaway - * harnesses that no longer exist. #9851 reconciled them — the disagreement is - * entirely the SHAPE OF THE SEEDED `objects` COLLECTION, not the span, not the - * warmup, and not a code change between the two refs — and left this file so - * the next reading is reproducible instead of re-litigated. - * - * npx tsx scripts/bench/runtime-publish-gate.bench.mts - * npx tsx scripts/bench/runtime-publish-gate.bench.mts --type object - * npx tsx scripts/bench/runtime-publish-gate.bench.mts --objects 21,105,420 --iterations 50 - * - * Requires `pnpm --filter '@objectstack/lint^...' build && pnpm --filter - * @objectstack/lint build` first: the gate is imported straight from - * `packages/lint/dist/runtime.js` — the built artifact the `./runtime` export - * points at, reached by path because this script is outside the workspace - * graph — so an unbuilt or stale `dist` reports the cost of the OLD registry - * (this is the `ablation-dist-preflight` hazard; the run prints the rules it - * actually dispatched, so a stale build shows up as a changed rule list rather - * than as a silently wrong number). - * - * ⚠️ Not a CI gate. Wall-clock numbers are machine-dependent and this asserts - * no threshold; it prints, and the operator compares. - * - * ── THE TIMED SPAN — inside vs outside ────────────────────────────────────── - * - * INSIDE the timed region: exactly one call to `runRuntimeAuthoringRules()` — - * the whole shipped gate for one write. That is `buildRuntimeWriteSnapshots` - * (baseline + candidate) plus the differential TWO passes of every rule that - * declares the written type in `runtimeTypes`, plus the fingerprint set-diff. - * - * OUTSIDE: seeding and cloning the stack, module load, and everything - * `saveMetaItem` wraps around the gate — the per-type Zod `safeParse`, - * persistence, hooks, audit, the HTTP envelope. - * - * ⭐ That exclusion is measured, not assumed to be small: the gate call alone - * reproduces the whole-`saveMetaItem` figure published as #9851's Reading A to - * within ~8%, so the gate IS substantially the whole per-publish bill and the - * span is NOT what separated the two published readings. - * - * ── WARMUP POLICY ─────────────────────────────────────────────────────────── - * - * `--warmup` iterations (default 5) run and are discarded, then `--iterations` - * (default 30) are timed and the MEDIAN is reported, with min/max so a noisy - * machine is visible. ⭐ Measured on this workload, warm and cold agree to - * within noise (a cold 3-iteration median lands inside the warm min/max band at - * both sizes), because a single iteration is already tens of milliseconds of - * the same loop — long enough for the JIT to settle mid-iteration. So warmup is - * kept for hygiene, NOT because it moves the number: it is not the explanation - * for any two harnesses disagreeing here. - * - * ── THE SEEDED STACK — the parameter that actually drives the cost ─────────── - * - * The gate's context is the bounded 4-collection `RuntimeStackContext` - * (`objects` / `permissions` / `books` / `datasets`); only `objects` is varied - * here, because that is the collection both published readings varied and the - * one a tenant grows. Two shapes ship, and running BOTH is the point: - * - * real — the shipped `examples/app-showcase` object declarations (22 of - * them), cloned under suffixed names up to N. Real authored surface: - * formula fields, validation rules, predicates, lookups — i.e. the - * expression-bearing keys `validateStackExpressions` walks. - * stub — synthetic 5-field objects carrying no expressions at all. - * - * ⭐ Same gate, same N, same warmup: `real` costs ~25x `stub`. **Per-publish - * cost is a function of the tenant's authored object SURFACE, not of its object - * COUNT** — count is only a proxy, and the proxy's constant is what the two - * published readings silently disagreed about. A number quoted as "X ms at N - * objects" without its stack shape is therefore not reproducible, and any - * acceptance threshold derived from one must name the shape it was set against. - * - * ⚠️ `real` imports the showcase app LIVE, so its absolute numbers drift as - * that app is edited. Deliberate — it is the only real, spec-parsed corpus in - * the repo, and a frozen copy would silently stop describing shipped metadata. - * Quote the ref you measured at. - * - * ── THE OBJECT DOOR, BOTH SIDES OF #4716 — MEASURED ON BOTH SHAPES (#9859) ── - * - * #9825's cost table for the object-door widening was published from the `stub` - * shape alone, so its `+0.31 / +3.25 ms` delta describes a stack in which the - * rules that judge validation rules and autonumber formats find nothing to - * judge. Both legs re-measured here at `17854cba0`, one machine, one session, - * median of 30 with warmup 5. The BEFORE leg is built from the PRE-WIDENING - * registry — `git restore --source=1408ae337^ -- packages/lint/src/authoring- - * rules.ts`, rebuild `@objectstack/lint`, then read `rules dispatched (2)` off - * the run's own header before believing any number (`ablation-dist-preflight` - * both ways: marker `P2 (#4463): judges an object/field declaration` PRESENT in - * `dist` on the BEFORE leg, ABSENT again after restoring): - * - * N=420, object write BEFORE (2 rules) AFTER (7 rules) delta - * stub 3.6-4.4 ms 8.5-8.6 ms ~ +4.6 ms - * real (showcase decls) 25.5-29.2 ms 282.8-304.4 ms ~ +265 ms - * - * N=21, object write BEFORE (2 rules) AFTER (7 rules) delta - * stub 0.19-0.21 ms 0.41-0.45 ms ~ +0.23 ms - * real (showcase decls) 1.20-1.32 ms 16.7-18.9 ms ~ +16.5 ms - * - * ⛔ The real-shape delta is NOT the stub delta scaled by the totals ratio, and - * this is the measurement that shows it: at N=420 the totals ratio is ~34x, so - * `+3.25 x 34` would predict ~+110 ms where the measured delta is ~+265 ms. A - * ratio measured on TOTALS does not transfer to a DELTA between two rule sets, - * because the sets walk different keys. `--mode per-rule` says exactly why — - * on the real shape at N=420 `validateRuleCompilability` alone is ~233 ms (82% - * of the whole gate) while on the stub shape it is ~0.04 ms (0.5%), the stub - * carrying no validation rules for it to judge. The two rules that were already - * at the door cost the same on both legs (~23-25 ms, real, N=420), as they must. - */ - -import { - runRuntimeAuthoringRules, - runtimeAuthoringRulesFor, - buildRuntimeWriteSnapshots, - narrowObjectsToPackageClosure, -} from '../../packages/lint/dist/runtime.js'; -import * as showcaseObjects from '../../examples/app-showcase/src/data/objects/index.js'; -import { allFlows } from '../../examples/app-showcase/src/automation/flows/index.js'; - -// Ambient `process` / `console`, declared to exactly the members used below. -// `scripts/**` is inside the ROOT tsconfig's program, and that config carries -// `lib: ['ES2020']` with no `types` — so Node's globals are absent and every -// use of them is a root-ledger tsc error (measured: 19 from this file alone, -// which is how it first landed). tsx provides the real objects at run time. -// Same shape and same reason as the ambient `process` in -// `examples/app-showcase/objectstack.config.ts`: narrow the declaration to -// what is actually called rather than widening the root's type surface, so -// this file pays its own way through `pnpm check:type-check-debt` instead of -// pushing a shrink-only ratchet back up. -declare const process: { - argv: string[]; - hrtime: { bigint(): bigint }; -}; -declare const console: { log(...args: unknown[]): void }; - -type AnyRec = Record; - -function arg(name: string, fallback: string): string { - const hit = process.argv.find((a) => a.startsWith(`--${name}=`)); - if (hit) return hit.slice(name.length + 3); - const i = process.argv.indexOf(`--${name}`); - return i >= 0 && process.argv[i + 1] ? process.argv[i + 1]! : fallback; -} - -const WRITE_TYPE = arg('type', 'flow'); -const SIZES = arg('objects', '21,420').split(',').map((s) => Number(s.trim())).filter(Boolean); -const ITERATIONS = Number(arg('iterations', '30')); -const WARMUP = Number(arg('warmup', '5')); -const MODE = arg('mode', 'total'); - -/** The shipped showcase object declarations — the `real` seed's source corpus. */ -const SHOWCASE_OBJECTS = (Object.values(showcaseObjects) as AnyRec[]) - .filter((o) => o && typeof o.name === 'string'); - -/** A real shipped flow, used as the written item for a `flow` publish. */ -const SHOWCASE_FLOW = (allFlows as AnyRec[]).find((f) => f.name === 'showcase_task_completed') - ?? (allFlows as AnyRec[])[0]; - -/** A minimal clean object body, used as the written item for an `object` publish. */ -const WRITTEN_OBJECT: AnyRec = { - name: 'bench_written_object', - label: 'Bench Written Object', - sharingModel: 'ReadWrite', - fields: { name: { type: 'text', label: 'Name' } }, -}; - -/** - * N objects cloned from the shipped showcase corpus, names suffixed to stay unique. - * - * ⚠️ The clone is `JSON.parse(JSON.stringify(...))`, so any FUNCTION-valued key - * in a showcase declaration is dropped rather than copied. Harmless for what is - * measured here — the runtime gate receives `sys_metadata` bodies, which are - * JSON at rest, so a JSON round-trip is the shape the real door sees — but it - * does mean this seed is a lower bound on a declaration whose authored surface - * lives partly in functions. Stated because the file's honesty about its own - * seed is the thing that makes a number taken from it re-runnable. - */ -function seedReal(n: number): AnyRec[] { - const out: AnyRec[] = []; - for (let i = 0; out.length < n; i++) { - const src = SHOWCASE_OBJECTS[i % SHOWCASE_OBJECTS.length]!; - const clone = JSON.parse(JSON.stringify(src)) as AnyRec; - const generation = Math.floor(i / SHOWCASE_OBJECTS.length); - if (generation > 0) clone.name = `${src.name}_c${generation}`; - out.push(clone); - } - return out; -} - -/** N synthetic 5-field objects carrying no expressions. */ -function seedStub(n: number): AnyRec[] { - return Array.from({ length: n }, (_, i) => ({ - name: `bench_object_${i}`, - label: `Bench Object ${i}`, - fields: { - name: { type: 'text', label: 'Name' }, - amount: { type: 'number', label: 'Amount' }, - active: { type: 'boolean', label: 'Active' }, - due: { type: 'date', label: 'Due' }, - notes: { type: 'textarea', label: 'Notes' }, - }, - })); -} - -const SHAPES: Record AnyRec[]> = { real: seedReal, stub: seedStub }; - -const ms = () => Number(process.hrtime.bigint()) / 1e6; -const median = (xs: number[]) => [...xs].sort((a, b) => a - b)[Math.floor(xs.length / 2)]!; - -function run(objects: AnyRec[], item: AnyRec) { - return runRuntimeAuthoringRules({ type: WRITE_TYPE, item, context: { objects } }); -} - -function measure(objects: AnyRec[], item: AnyRec) { - for (let i = 0; i < WARMUP; i++) run(objects, item); - const samples: number[] = []; - for (let i = 0; i < ITERATIONS; i++) { - const t0 = ms(); - run(objects, item); - samples.push(ms() - t0); - } - return { median: median(samples), min: Math.min(...samples), max: Math.max(...samples) }; -} - -const item = WRITE_TYPE === 'flow' ? SHOWCASE_FLOW : WRITTEN_OBJECT; - -// The rules actually dispatched, printed BEFORE any timing: a stale `dist/`, or -// a rule ablated off the runtime surface, changes this line — so a number that -// moved for a registry reason can never be read as a number that moved for a -// performance reason. -const dispatched = run(SHAPES.real!(1), item).rulesRun; -console.log(`runtime publish gate — write type '${WRITE_TYPE}'`); -console.log(` rules dispatched (${dispatched.length}): ${dispatched.join(', ') || ''}`); -console.log(` timed span: one runRuntimeAuthoringRules() call (differential two-pass)`); -console.log(` warmup ${WARMUP} discarded, ${ITERATIONS} timed, median reported`); -if (dispatched.length === 0) { - console.log(` ⚠️ no rule gates '${WRITE_TYPE}' at the runtime surface — every number below is gate overhead only`); -} - -/** Median wall-clock of `fn`, same warmup/iteration policy as {@link measure}. */ -function timeMedian(fn: () => void): number { - for (let i = 0; i < WARMUP; i++) fn(); - const samples: number[] = []; - for (let i = 0; i < ITERATIONS; i++) { - const t0 = ms(); - fn(); - samples.push(ms() - t0); - } - return median(samples); -} - -/** #9851's reading: the whole-gate cost table, both shapes, every N. */ -function totalMode() { - console.log(''); - console.log(' shape N objects median min max'); - const table: Record> = {}; - for (const shape of Object.keys(SHAPES)) { - table[shape] = {}; - for (const n of SIZES) { - const r = measure(SHAPES[shape]!(n), item); - table[shape]![n] = r.median; - console.log( - ` ${shape.padEnd(6)} ${String(n).padStart(9)} ${r.median.toFixed(2).padStart(7)} ms ` - + `${r.min.toFixed(2).padStart(7)} ${r.max.toFixed(2).padStart(7)}`, - ); - } - } - - console.log(''); - for (const n of SIZES) { - const real = table.real![n]!; - const stub = table.stub![n]!; - console.log(` N=${n}: real / stub = ${(real / stub).toFixed(1)}x — same gate, same N, different authored surface`); - } - console.log(''); - console.log(' ⇒ per-publish cost tracks authored object SURFACE, not object COUNT.'); - console.log(' Quote the stack shape with any number taken from this gate (#9851).'); -} - -/** - * #9905 deliverable 2 — the same total, attributed across the dispatched rules. - * - * Each rule is timed over BOTH gate passes (baseline + candidate), because that - * is what one publish actually costs it, and the snapshots come from the gate's - * own exported {@link buildRuntimeWriteSnapshots} rather than a mirror — so the - * shares are attributed against the real construction. - * - * ⭐ The UNATTRIBUTED line is the anti-vacuity check and is printed even when it - * is uninteresting: per-rule shares that do not add up to the measured total - * mean the attribution is wrong, and that is the finding. Confirm the top share - * independently by ablating that rule off the door (`runtimeTypes`), rebuilding, - * and checking the total moves by roughly its share — `rules dispatched` above - * changes with it, so a stale `dist/` cannot masquerade as a saving. - */ -function perRuleMode() { - const rules = runtimeAuthoringRulesFor(WRITE_TYPE); - const ctx = { sduiManifest: undefined }; - for (const shape of Object.keys(SHAPES)) { - for (const n of SIZES) { - const objects = SHAPES[shape]!(n); - const snapshots = buildRuntimeWriteSnapshots({ type: WRITE_TYPE, item, context: { objects } }); - if (!snapshots) continue; - const total = timeMedian(() => { run(objects, item); }); - const per = rules - .map((r) => ({ - name: r.name, - ms: timeMedian(() => { - try { r.run(snapshots.baseline, ctx); } catch { /* the gate reports throws as findings */ } - try { r.run(snapshots.candidate, ctx); } catch { /* idem */ } - }), - })) - .sort((a, b) => b.ms - a.ms); - const sum = per.reduce((acc, p) => acc + p.ms, 0); - const pct = (x: number) => `${((x / total) * 100).toFixed(1).padStart(5)}%`; - console.log(''); - console.log(` shape=${shape} N=${n} — whole-gate total ${total.toFixed(2)} ms`); - for (const p of per) console.log(` ${p.name.padEnd(32)} ${p.ms.toFixed(2).padStart(8)} ms ${pct(p.ms)}`); - console.log(` ${'Σ attributed'.padEnd(32)} ${sum.toFixed(2).padStart(8)} ms ${pct(sum)}`); - console.log(` ${'unattributed (snapshot + diff)'.padEnd(32)} ${(total - sum).toFixed(2).padStart(8)} ms ${pct(total - sum)}`); - } - } -} - -/** Every string anywhere in a value — the closure seed scan, deliberately broad. */ -function allStrings(value: unknown, out: string[] = []): string[] { - if (typeof value === 'string') out.push(value); - else if (Array.isArray(value)) for (const v of value) allStrings(v, out); - else if (value && typeof value === 'object') for (const v of Object.values(value)) allStrings(v, out); - return out; -} - -/** - * The transitive reference closure of the written item over `objects`: every - * object NAMED anywhere in the item, expanded through the included objects' - * declared relational edges (`fields[].reference`) to a fixed point. - * - * Over-approximating on purpose — it seeds from any string that happens to equal - * an object name, so it can only ever be too LARGE. A saving measured against it - * is therefore a lower bound on what an exact closure would buy, which is the - * safe direction for a number that argues FOR narrowing. - */ -function referenceClosure(written: AnyRec, objects: AnyRec[]): AnyRec[] { - const byName = new Map(); - for (const o of objects) if (typeof o.name === 'string') byName.set(o.name, o); - const reached = new Set(allStrings(written).filter((s) => byName.has(s))); - const frontier = [...reached]; - while (frontier.length > 0) { - const owner = byName.get(frontier.pop()!)!; - for (const field of Object.values((owner.fields ?? {}) as AnyRec)) { - const ref = (field as AnyRec)?.reference; - for (const target of Array.isArray(ref) ? ref : [ref]) { - if (typeof target === 'string' && byName.has(target) && !reached.has(target)) { - reached.add(target); - frontier.push(target); - } - } - } - } - return [...reached].map((name) => byName.get(name)!); -} - -/** - * #9905 deliverable 1 — what NARROWING the objects collection would buy, timed. - * - * Prints `|closure| / N` (the size ratio, which is NOT the saving) next to the - * measured saving (which is), and re-runs the gate on the narrowed collection to - * confirm the differential VERDICT is unchanged — a saving that changes the - * verdict is not a saving, it is PR #7886's phantom-findings failure. - */ -function closureMode() { - const stable = (r: { errors: AnyRec[]; advisories: AnyRec[] }) => - [...r.errors, ...r.advisories].map((f) => `${f.rule}|${f.where}|${f.path}|${f.message}`).sort().join('\n'); - for (const shape of Object.keys(SHAPES)) { - for (const n of SIZES) { - const objects = SHAPES[shape]!(n); - const closure = referenceClosure(item as AnyRec, objects); - const full = timeMedian(() => { run(objects, item); }); - const scoped = timeMedian(() => { run(closure, item); }); - const agree = stable(run(objects, item)) === stable(run(closure, item)); - console.log(''); - console.log(` shape=${shape} N=${n}: |closure| = ${closure.length} (closure/N = ${((closure.length / n) * 100).toFixed(1)}%)`); - console.log(` members: ${closure.map((o) => String(o.name)).join(', ') || ''}`); - console.log(` whole gate: full ${full.toFixed(2)} ms → closure ${scoped.toFixed(2)} ms — saving ${(((full - scoped) / full) * 100).toFixed(1)}%`); - console.log(` differential verdict unchanged: ${agree ? 'YES' : 'NO — narrowing changed the answer'}`); - } - } -} - - -// ───────────────────────────────────────────────────────────────────────────── -// #9612 — `--mode package`: what narrowing to the written item's PACKAGE -// closure buys, measured against the SHIPPED deriver rather than a hypothetical. -// ───────────────────────────────────────────────────────────────────────────── - -/** Objects per synthetic package in the packaged seed. */ -const PKG_SIZE = 22; -/** The package the written item belongs to. */ -const OWN_PKG = 'pkg_0'; -/** The one package `OWN_PKG` declares a dependency on. */ -const DEP_PKG = 'pkg_1'; -/** The platform's package — never declared as a dependency by anyone. */ -const PLATFORM_PKG = 'pkg_platform'; -/** A platform object the written item legitimately references. */ -const SYSTEM_OBJECT = 'sys_bench_ledger'; - -/** - * The seed, stamped with package provenance — the shape the ruling describes. - * - * A tenant that has grown to N objects is MANY packages, not one, so the seed - * distributes the corpus across `ceil(N / PKG_SIZE)` packages. It also carries - * ONE platform object (`isSystem`, `sys_` prefix, owned by a package nobody - * declares) so the "system objects are unconditionally in the closure" limb has - * something to be right or wrong about — control B below is what makes that a - * measurement rather than an assertion. - */ -function seedPackaged(shape: string, n: number): AnyRec[] { - const objects = SHAPES[shape]!(n).map((o, i) => ({ - ...o, - _packageId: `pkg_${Math.floor(i / PKG_SIZE)}`, - })); - const platform = JSON.parse(JSON.stringify(SHOWCASE_OBJECTS[0]!)) as AnyRec; - platform.name = SYSTEM_OBJECT; - platform.isSystem = true; - platform._packageId = PLATFORM_PKG; - return [...objects, platform]; -} - -/** `{ packageId, dependencies }` as the host resolves it from the registry. */ -const PACKAGE_SCOPE = { packageId: OWN_PKG, dependencies: [DEP_PKG] } as const; - -/** - * The written item, stamped into `OWN_PKG` and — for a flow — repointed at the - * platform object so control B has a reference that must survive the closure. - */ -function packagedWrittenItem(): AnyRec { - if (WRITE_TYPE !== 'flow') return { ...WRITTEN_OBJECT, _packageId: OWN_PKG }; - const flow = JSON.parse(JSON.stringify(SHOWCASE_FLOW)) as AnyRec; - const primary = referenceClosure(flow, SHAPES.real!(PKG_SIZE))[0]; - const primaryName = primary ? String(primary.name) : undefined; - const repointed = primaryName - ? (JSON.parse( - JSON.stringify(flow).split(JSON.stringify(primaryName).slice(1, -1)).join(SYSTEM_OBJECT), - ) as AnyRec) - : flow; - repointed._packageId = OWN_PKG; - return repointed; -} - -/** - * The item positive control A drives: one that really does reference objects in - * its OWN package, so dropping that package from the closure has something to - * break. For a flow that is the shipped showcase flow untouched; for an object - * write it is the same body the timing legs use, because the object door offers - * no cross-object reference to break — which is itself the measurement. - */ -function controlItemOwnPackage(): AnyRec { - if (WRITE_TYPE !== 'flow') return { ...WRITTEN_OBJECT, _packageId: OWN_PKG }; - const flow = JSON.parse(JSON.stringify(SHOWCASE_FLOW)) as AnyRec; - flow._packageId = OWN_PKG; - return flow; -} - -/** - * A finding's identity for verdict comparison — the semantic tuple, which - * since #10064 is the tuple AS EMITTED. - * - * The pre-#10064 reason for two readings is gone: the gate now keys a - * collection-resident finding's top-level collection entry by NAME - * (`objects.acme_invoice.sharingModel`), so narrowing the snapshot no longer - * moves anything in `path` and the raw tuple IS the semantic one. The - * differential comparison therefore reads the full - * `(rule, where, path, message)` tuple by default (the #10064 ruling). - * - * `verdictIndexInsensitive` survives as a CANARY, not as the default: the - * name-keying falls back to the positional spelling for an entry with no - * splice-safe name, and a positional top-level path CAN legitimately move - * under narrowing again. When the two readings disagree, the index-only - * difference is printed — hiding it would report a phantom as a verdict - * change (or vice versa) with nothing to diagnose it from. - */ -const stripIndices = (p: unknown) => String(p ?? '').replace(/\[\d+\]/g, '[i]'); -const verdict = (r: { errors: AnyRec[]; advisories: AnyRec[] }) => - [...r.errors, ...r.advisories].map((f) => `${f.rule}|${f.where}|${f.path}|${f.message}`).sort(); -const verdictIndexInsensitive = (r: { errors: AnyRec[]; advisories: AnyRec[] }) => - [...r.errors, ...r.advisories] - .map((f) => `${f.rule}|${f.where}|${stripIndices(f.path)}|${f.message}`) - .sort(); - -function diffCount(a: string[], b: string[]): { added: number; removed: number } { - const bs = new Set(b); - const as = new Set(a); - return { - added: b.filter((x) => !as.has(x)).length, - removed: a.filter((x) => !bs.has(x)).length, - }; -} - -function packageMode() { - const item = packagedWrittenItem(); - console.log(''); - console.log(` package closure — own '${OWN_PKG}' + declared dep '${DEP_PKG}' + platform/system + unpackaged`); - console.log(` seed: ${PKG_SIZE} objects per package, plus one '${SYSTEM_OBJECT}' owned by '${PLATFORM_PKG}' (declared by nobody)`); - - for (const shape of Object.keys(SHAPES)) { - for (const n of SIZES) { - const objects = seedPackaged(shape, n); - const closure = narrowObjectsToPackageClosure(objects, PACKAGE_SCOPE) as AnyRec[]; - - const runFull = () => runRuntimeAuthoringRules({ type: WRITE_TYPE, item, context: { objects } }); - const runScoped = () => - runRuntimeAuthoringRules({ type: WRITE_TYPE, item, context: { objects }, packageScope: PACKAGE_SCOPE }); - - const full = timeMedian(() => { runFull(); }); - const scoped = timeMedian(() => { runScoped(); }); - const fullV = runFull(); - const scopedV = runScoped(); - - console.log(''); - console.log( - ` shape=${shape} N=${objects.length}: |closure| = ${closure.length}` - + ` (closure/N = ${((closure.length / objects.length) * 100).toFixed(1)}%)`, - ); - console.log( - ` whole gate: full ${full.toFixed(2)} ms → package closure ${scoped.toFixed(2)} ms` - + ` — saving ${(((full - scoped) / full) * 100).toFixed(1)}%`, - ); - // [#10064] The semantic tuple is the default — and it is the tuple as - // emitted, path included, now that the wire path is name-keyed. The - // index-insensitive reading prints only when it disagrees (a positional - // fallback path moved), so an index-only artefact cannot silently pose - // as a verdict change. - const semD = diffCount(verdict(fullV), verdict(scopedV)); - const insD = diffCount(verdictIndexInsensitive(fullV), verdictIndexInsensitive(scopedV)); - console.log( - ` differential verdict: ${semD.added === 0 && semD.removed === 0 ? 'UNCHANGED' : `CHANGED (+${semD.added} / -${semD.removed})`}` - + (semD.added === insD.added && semD.removed === insD.removed - ? '' - : ` · index-insensitive reading disagrees: +${insD.added} / -${insD.removed} (a positional-fallback path moved)`), - ); - console.log(` findings: full ${fullV.errors.length}e/${fullV.advisories.length}a → scoped ${scopedV.errors.length}e/${scopedV.advisories.length}a`); - - // ── Positive control A — the closure loses the written package itself. - // - // Driven by {@link controlItemOwnPackage}, NOT by `item`: the timed item - // above was repointed at the platform object for control B's sake, so it - // no longer references its own package and would make this control - // vacuous BY CONSTRUCTION rather than by a property of the door. This - // control needs an item whose references live in `pkg_0`. - // - // If it prints a zero delta the verdict check above is not falsifiable on - // this door, so it is printed whatever it says. - const ctrlItem = controlItemOwnPackage(); - const ctrlAFull = runRuntimeAuthoringRules({ type: WRITE_TYPE, item: ctrlItem, context: { objects } }); - const ctrlA = objects.filter((o) => { - const owner = String(o._packageId ?? ''); - return owner === DEP_PKG || o.isSystem === true || String(o.name ?? '').startsWith('sys_'); - }); - const ctrlAV = runRuntimeAuthoringRules({ type: WRITE_TYPE, item: ctrlItem, context: { objects: ctrlA } }); - const ctrlAD = diffCount(verdict(ctrlAFull), verdict(ctrlAV)); - console.log( - ` positive control A (own package dropped, |set| = ${ctrlA.length}): ` - + `${ctrlAD.added === 0 && ctrlAD.removed === 0 ? '⚠️ NO DELTA — control is vacuous on this door' : `delta +${ctrlAD.added} / -${ctrlAD.removed}`}`, - ); - - // ── Positive control B — the card's named hazard: a closure that omits - // SYSTEM objects. The written item references `sys_bench_ledger`, which - // no package declares, so only the unconditional system limb keeps it. - const ctrlB = objects.filter((o) => { - const owner = String(o._packageId ?? ''); - return owner === OWN_PKG || owner === DEP_PKG; - }); - const ctrlBV = runRuntimeAuthoringRules({ type: WRITE_TYPE, item, context: { objects: ctrlB } }); - const ctrlBD = diffCount(verdict(fullV), verdict(ctrlBV)); - console.log( - ` positive control B (system objects dropped, |set| = ${ctrlB.length}): ` - + `${ctrlBD.added === 0 && ctrlBD.removed === 0 ? '⚠️ NO DELTA — this door has no objects×objects coupling' : `delta +${ctrlBD.added} / -${ctrlBD.removed}`}`, - ); - } - } -} - -if (MODE === 'per-rule') perRuleMode(); -else if (MODE === 'closure') closureMode(); -else if (MODE === 'package') packageMode(); -else totalMode(); diff --git a/scripts/measure-markdown-ts-blocks.mjs b/scripts/measure-markdown-ts-blocks.mjs deleted file mode 100644 index 12ec6b3955b..00000000000 --- a/scripts/measure-markdown-ts-blocks.mjs +++ /dev/null @@ -1,1276 +0,0 @@ -#!/usr/bin/env node -// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. - -/** - * measure-markdown-ts-blocks -- do the fenced TypeScript blocks in package-root - * Markdown compile? (#18715) - * - * node scripts/measure-markdown-ts-blocks.mjs # the census - * node scripts/measure-markdown-ts-blocks.mjs --json # machine-readable - * node scripts/measure-markdown-ts-blocks.mjs --only # one file (repeatable) - * node scripts/measure-markdown-ts-blocks.mjs --controls-only # just the two controls - * node scripts/measure-markdown-ts-blocks.mjs --self-test # the extractor's own battery - * - * ⛔ THIS IS A MEASUREMENT INSTRUMENT, NOT A GATE. It exits 0 on any census - * outcome, is wired into no CI job and no package `test`/`lint` chain, and - * nothing in this repository fails because of what it reports. Turning a - * census into a required gate is a NEW REQUIRED GATE — the maintainer's floor - * — and the wiring options belong in a letter to the maintainer, never in a - * `package.json` edit made by whoever wrote the instrument. - * - * It exits non-zero for exactly one reason: THE INSTRUMENT ITSELF COULD NOT - * RUN (its controls misbehaved, or the workspace is not built). "Could not - * run" is a failure, not a skip — a census that silently degrades to "0 - * failures" because nothing resolved is the worst possible output. - * - * ## POPULATION - * - * Markdown sitting at the ROOT of a package: every `*.md` directly inside a - * directory under `packages/` that has a `package.json`. Two strata are - * reported separately and ⛔ never summed into one headline: - * - * - HAND-WRITTEN — README.md, ARCHITECTURE.md, PHASE2_IMPLEMENTATION.md, … - * A failing block here is a defect an author can repair. - * - CHANGELOG.md — RELEASE-OWNED and compiled by `changeset version` from - * `.changeset/*.md` (AGENTS.md, Documentation Guardrails). Its blocks are - * HISTORY: before/after migration snippets that deliberately show removed - * APIs. A failing block here is frequently CORRECT documentation, and it - * cannot be repaired in a code PR at all. Counting the two together - * produces a number nobody can act on. - * - * `content/docs/**` is a DIFFERENT population with its own gates and is out of - * scope here (#18715 holds it out explicitly). - * - * ## THE ELISION CONVENTION — the one real design question, and its answer - * - * These documents are full of partial snippets. A census that counts every - * partial block as a failure measures nothing. So how does a block declare - * itself partial? - * - * ⛔ THE OBVIOUS ANSWER IS DISQUALIFIED, and this instrument proves it rather - * than asserting it. The obvious answer is an EXCLUSION rule: a block carrying - * an elision marker (`// ...`) is skipped. Run that rule against the one - * failure this repository has already PROVEN — `PHASE2_IMPLEMENTATION.md`'s - * pre-#18712 `kernel.logger` block, measured at `tsc --noEmit --strict` exit 2 - * with TS2341 — and it is excluded, because that block ends with the line - * `// ... plugin registration code ...`. An exclusion rule would have hidden - * the only defect in this family anyone has ever measured. That is the whole - * argument, and `FIRING_CONTROL` below is the standing proof of it. - * - * ✅ THE RULE THIS INSTRUMENT USES IS A TOLERANCE RULE, not an exclusion rule. - * Every block is compiled. What a legitimately partial block can PRODUCE is - * forgiven; nothing else is: - * - * forgiven TS2304 / TS2552 / TS2503 a name or namespace the block elided - * forgiven TS2307 on a RELATIVE specifier the reader's own file - * (`./my-kernel`, `./objectstack.config.js`) — never a - * `@objectstack/*` one, see below - * counted everything else, including TS2341 (private member), TS2339 - * (no such property), TS2345 (wrong argument type), TS2305 / - * TS2724 (no such export) and TS2307 on a `@objectstack/*` - * specifier - * - * TS2307 on a `@objectstack/*` specifier is deliberately NOT forgiven: the - * `paths` map this instrument compiles against is generated from each - * package's own `exports` map, so an unresolved one means the document tells a - * reader to import a subpath the package does not publish. That is the - * `check-published-readme-exports` family of defect, arriving here for free. - * - * ## BOTH NUMBERS, AND WHICH DIRECTION EACH ONE LIES IN - * - * The census reports RAW and TOLERANT side by side, and neither is "the" - * answer: - * - * RAW every block with at least one diagnostic. An UPPER BOUND: it - * counts fragments that were never meant to stand alone. - * TOLERANT every block with at least one diagnostic outside the forgiven - * set. A LOWER BOUND, and this is the direction that matters: a - * forgiven TS2304 leaves that name typed `any`, and every - * downstream use of it is then unchecked. Real defects hide behind - * an elision; they never appear because of one. - * - * ⇒ The honest sentence is "between TOLERANT and RAW", and the gate question - * is answered by TOLERANT, because that is the number a gate could be built to - * hold at zero. - * - * ## THE FORWARD CONVENTION, for the blocks tolerance cannot absolve - * - * A block that is a bare fragment — a type signature, half an object literal — - * produces SYNTAX errors, and ⛔ syntax is not forgiven: a tolerance rule that - * forgave TS1xxx would forgive a typo. Those blocks need an explicit - * declaration, and the proposed spelling is an info-string tag: - * - * ```ts partial - * - * ⛔ Zero blocks in the corpus carry it today, so it contributes nothing to - * this census — it is stated so the census can COUNT how many blocks would - * need one (`needs-explicit-partial-tag` below), which is the cost line the - * wiring letter needs. An `exclusion` census is also printed, labelled as the - * disqualified reading, so the number cannot hide inside the convention. - * - * ## THE TWO CONTROLS — run on EVERY census, not only under --self-test - * - * An instrument that finds zero failures and was never shown capable of - * finding one has measured nothing. - * - * GREEN_CONTROL a block that MUST compile. It fails when the workspace is - * not built (no `dist/*.d.ts` to resolve against), which is - * the state in which every other block would report TS2307 - * and the census would read as a catastrophe made of - * nothing. The run REFUSES instead of printing it. - * FIRING_CONTROL the reconstructed pre-#18712 `kernel.logger` block, which - * MUST be reported as failing, MUST carry TS2341, and MUST - * still fail under the tolerance rule. It is also the proof - * that the exclusion rule is disqualified: the same fixture - * is reported as EXCLUDED by that rule, every run. - * - * Both go through the identical path as corpus blocks — same extractor, same - * normalisation, same program — so a control green is a statement about the - * instrument that actually ran. - * - * ## NORMALISATION, stated because it changes the bytes compiled - * - * 1. The opening fence's indentation is stripped from every body line, so a - * block inside a list item compiles as the code the reader sees. - * 2. `export {};` is appended to every block. One tsc program holds every - * block at once; without it a block with no import/export is a SCRIPT in - * the global scope and collides with every other one (TS2451 on names - * like `kernel`), which would be a census of this instrument's own - * packaging. Appending at the end leaves every reported line number - * equal to the line number in the Markdown. - * - * ## WHY THE `tsc` BINARY, AND WHY TWO PASSES - * - * The TypeScript compiler API is deliberately not used. `scripts/ts-parse.mjs` - * is the one sanctioned door to it under `scripts/**`, and its contract is to - * EXIT on a source it cannot parse — correct for a gate walking real files, - * wrong here, where a block that does not parse is the measurement's subject. - * Driving the binary also makes this instrument ask #18715's own question - * verbatim: `tsc --noEmit --strict`. - * - * ⚠️ MEASURED PROPERTY OF THE CLI, which one pass gets wrong: when any file in - * a program has a SYNTACTIC diagnostic, tsc reports the syntax errors and - * never type-checks ANY file in that program. One pass over this corpus - * returned 772 TS1xxx and not one semantic diagnostic — the firing control - * included, which is exactly how this was caught. So pass 1 finds the blocks - * that do not parse and pass 2 re-runs over the ones that do. A block that - * does not parse carries its syntax diagnostics and no semantic ones, which is - * honest: tsc cannot read it either. - * - * This instrument was first written against `ts.createProgram` + - * `getPreEmitDiagnostics` and then rewritten onto the binary. Over all 1002 - * corpus blocks the two implementations agree on every block's verdict — raw, - * tolerant and well-formed-and-wrong — with ZERO disagreements. The binary run - * reports fewer DIAGNOSTICS on unparseable blocks, because the checker noise - * the API produced over wreckage is gone. - * - * ## COMPILER OPTIONS, and why each deviation from the repo's own tsconfig - * - * strict: true — #18715's proven measurement is `tsc --noEmit - * --strict`; kept verbatim so this instrument and - * that reading answer the same question. - * moduleResolution: — `exports` maps are honoured (so an unpublished - * Bundler subpath still fails) WITHOUT NodeNext's - * mandatory `.js` extension on relative - * specifiers, which would manufacture a failure - * out of a documentation style choice. - * noUnusedLocals: false — a snippet that declares a value to SHOW it is - * noUnusedParameters: not a defect. The repo's own tsconfig turns - * false these on for source; doing so here would count - * prose as breakage. - * lib: ES2022 + DOM — deliberately generous. Every ambient name the - * types: node instrument can supply is one fewer manufactured - * failure. - */ - -import { readFileSync, existsSync, readdirSync, mkdirSync, mkdtempSync, writeFileSync, rmSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { dirname, join, relative, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { createRequire } from 'node:module'; -import { spawnSync } from 'node:child_process'; - -import { isEntrypoint } from './invoked-as.mjs'; - -const HERE = dirname(fileURLToPath(import.meta.url)); -const REPO_ROOT = resolve(HERE, '..'); -const PACKAGES_DIR = join(REPO_ROOT, 'packages'); -const nodeRequire = createRequire(import.meta.url); -// ⛔ The TypeScript compiler API is deliberately NOT imported here. This -// instrument's SUBJECT is blocks that may not parse, so `createProgramChecked` -// — the one sanctioned API door under `scripts/**` — is the wrong tool: it -// exits 3 on the first syntactic diagnostic, which for this census is DATA. -// Driving the real `tsc` binary answers exactly the question #18715 measured -// (`tsc --noEmit --strict`, exit 2, TS2341) and keeps the parser home's rule -// that nothing outside it reaches the parser API. -const TSC_BIN = join(dirname(nodeRequire.resolve('typescript/package.json')), 'bin', 'tsc'); - -// ── Population ───────────────────────────────────────────────────────────── - -const SKIP_DIRS = new Set(['node_modules', 'dist', 'coverage', '.turbo', '.git', 'json-schema']); - -/** Every directory under `root` that carries a package.json, `root` included. */ -export function findPackageRoots(root, out = []) { - if (!existsSync(root)) return out; - for (const entry of readdirSync(root, { withFileTypes: true })) { - if (!entry.isDirectory()) continue; - if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) continue; - findPackageRoots(join(root, entry.name), out); - } - if (existsSync(join(root, 'package.json'))) out.push(root); - return out; -} - -export function populationFiles() { - const files = []; - for (const pkgRoot of findPackageRoots(PACKAGES_DIR)) { - for (const name of readdirSync(pkgRoot)) { - if (name.endsWith('.md')) files.push(join(pkgRoot, name)); - } - } - return files.sort(); -} - -/** CHANGELOG.md is release-owned history, reported as its own stratum. */ -export const stratumOf = (file) => (/(^|\/)CHANGELOG\.md$/.test(file) ? 'changelog' : 'handwritten'); - -/** The literal `packages/PKG/NAME.md` glob #18715 names, kept as a reportable sub-stratum. */ -export const isDepthOne = (relPath) => relPath.split('/').length === 3; - -// ── Extraction ───────────────────────────────────────────────────────────── - -const FENCE_RE = /^([ \t]*)(`{3,}|~{3,})[ \t]*(.*)$/; -const TS_LANGS = new Set(['ts', 'typescript', 'tsx']); - -/** - * Fenced blocks whose info-string's FIRST word is a TypeScript language. - * A fence opened inside an already-open block is body text, never a new block: - * a ```ts fence quoted inside a ```md example is not code this repo ships. - */ -export function extractBlocks(text) { - const lines = text.split('\n'); - const blocks = []; - let open = null; - for (let i = 0; i < lines.length; i += 1) { - const m = FENCE_RE.exec(lines[i]); - if (!m) { - if (open) open.body.push(lines[i]); - continue; - } - const [, indent, fence, info] = m; - if (!open) { - open = { indent, char: fence[0], len: fence.length, info: info.trim(), firstBodyLine: i + 2, body: [] }; - continue; - } - const closes = fence[0] === open.char && fence.length >= open.len && info.trim() === ''; - if (!closes) { - open.body.push(lines[i]); - continue; - } - const lang = open.info.split(/\s+/)[0].toLowerCase(); - if (TS_LANGS.has(lang)) { - const dedent = open.indent.length - ? open.body.map((l) => (l.startsWith(open.indent) ? l.slice(open.indent.length) : l.replace(/^[ \t]+/, ''))) - : open.body; - blocks.push({ - lang, - info: open.info, - firstBodyLine: open.firstBodyLine, - declaredPartial: /(^|\s)partial(\s|$)/.test(open.info), - code: dedent.join('\n'), - }); - } - open = null; - } - return blocks; -} - -/** - * The INFERRED elision marker: a comment carrying an ellipsis, or a bare `...` - * line. Used ONLY to compute the disqualified `exclusion` reading — the - * tolerance rule never consults it. - */ -export function hasElisionMarker(code) { - return code.split('\n').some((line) => { - const t = line.trim(); - if (t === '...') return true; - if (t.startsWith('//') && t.includes('...')) return true; - if (t.startsWith('/*') && t.includes('...')) return true; - if (t.startsWith('*') && t.includes('...')) return true; - return false; - }); -} - -/** One tsc program holds every block, so every block must be a module. */ -export const normalise = (code) => `${code}\nexport {};\n`; - -// ── Diagnostic classification ────────────────────────────────────────────── - -const FORGIVEN_NAME_CODES = new Set([2304, 2552, 2503]); -// "Cannot find name 'it' / 'describe' / 'process' / 'document'" — the READER's -// ambient environment (a test runner, node, the DOM), never something the -// document could have written. Same for a third-party module that ships no -// types, and for JSX with no `JSX.IntrinsicElements` in scope. -const FORGIVEN_AMBIENT_CODES = new Set([2580, 2581, 2582, 2583, 2584, 2591, 2592, 2593, 7016, 7026]); -const SYNTAX_MAX = 1999; - -/** - * Pure so the self-test can drive it without a compiler: takes the two fields - * of a diagnostic that decide its family. - */ -export function classifyDiagnostic({ code, message }) { - if (code >= 1000 && code <= SYNTAX_MAX) return { family: 'syntax', forgiven: false }; - if (FORGIVEN_NAME_CODES.has(code)) return { family: 'elided-name', forgiven: true }; - if (FORGIVEN_AMBIENT_CODES.has(code)) return { family: 'missing-ambient-environment', forgiven: true }; - if (code === 2307) { - const spec = /Cannot find module '([^']+)'/.exec(message)?.[1] ?? ''; - if (spec.startsWith('.')) return { family: 'doc-local-path', forgiven: true, specifier: spec }; - if (spec.startsWith('@objectstack/')) return { family: 'unpublished-subpath', forgiven: false, specifier: spec }; - return { family: 'external-not-installed', forgiven: true, specifier: spec }; - } - if (code >= 7000 && code <= 7099) return { family: 'implicit-any', forgiven: false }; - return { family: 'semantic', forgiven: false }; -} - -/** The families `classifyDiagnostic` marks forgiven — a legitimately partial block can produce these and nothing else. */ -export const FORGIVEN_FAMILIES = new Set(['elided-name', 'doc-local-path', 'external-not-installed', 'missing-ambient-environment']); - -/** - * `tsc --pretty false` prints one diagnostic per line, with wrapped detail - * indented under it. Parsed here rather than read from the API, and held - * honest by the two controls: if this parser broke, GREEN_CONTROL would read - * as failing or FIRING_CONTROL as clean, and the run would refuse. - */ -export function parseTscOutput(text) { - const HEAD = /^(\S[^(]*)\((\d+),(\d+)\): (?:error|warning) TS(\d+): (.*)$/; - const BARE = /^(?:error|warning) TS(\d+): (.*)$/; - const diagnostics = []; - const global = []; - for (const line of text.split('\n')) { - const head = HEAD.exec(line); - if (head) { - diagnostics.push({ file: head[1], line: Number(head[2]), code: Number(head[4]), message: head[5] }); - continue; - } - const bare = BARE.exec(line); - if (bare) { - global.push({ code: Number(bare[1]), message: bare[2], line: null }); - continue; - } - const tail = diagnostics.length > 0 ? diagnostics : global; - if (/^\s+\S/.test(line) && tail.length > 0) tail[tail.length - 1].message += ` ${line.trim()}`; - } - return { diagnostics, global }; -} - -// ── Controls ─────────────────────────────────────────────────────────────── - -/** MUST compile. Its failure means the workspace is not built — refuse, never report. */ -export const GREEN_CONTROL = [ - '# green control', - '', - '```ts', - "import { ObjectKernel } from '@objectstack/core';", - '', - 'const KernelCtor: typeof ObjectKernel = ObjectKernel;', - 'export { KernelCtor };', - '```', - '', -].join('\n'); - -/** - * MUST be reported failing, with TS2341, under BOTH readings. - * Reconstructed byte-for-byte from PR #18712's diff (the `-` side of - * packages/core/PHASE2_IMPLEMENTATION.md's "Integration with Kernel" block), - * the one failure in this family anyone has measured: `tsc --noEmit --strict` - * exit 2, TS2341 `Property 'logger' is private`. - * - * ⭐ Its last comment line is `// ... plugin registration code ...`, which is - * why the exclusion reading is reported as DISQUALIFIED rather than offered. - */ -export const FIRING_CONTROL = [ - '# firing control', - '', - '```typescript', - 'import { ', - ' ObjectKernel,', - ' PluginHealthMonitor,', - ' HotReloadManager,', - ' DependencyResolver,', - ' PluginPermissionManager,', - ' PluginSandboxRuntime', - "} from '@objectstack/core';", - '', - "const kernel = new ObjectKernel({ logger: { level: 'info' } });", - '', - '// Initialize Phase 2 components', - 'const healthMonitor = new PluginHealthMonitor(kernel.logger);', - 'const hotReload = new HotReloadManager(kernel.logger);', - 'const depResolver = new DependencyResolver(kernel.logger);', - 'const permManager = new PluginPermissionManager(kernel.logger);', - 'const sandbox = new PluginSandboxRuntime(kernel.logger);', - '', - '// Register plugins with enhanced features', - '// ... plugin registration code ...', - '', - '// Bootstrap kernel', - 'await kernel.bootstrap();', - '```', - '', -].join('\n'); - -/** - * The three specifiers UNPUBLISHED_SUBPATH_CONTROL exists to keep COUNTED. The - * block below and this list are held equal by a self-test battery, so editing - * one without the other is a failure rather than a silent narrowing. - */ -export const UNPUBLISHED_SUBPATH_SPECIFIERS = Object.freeze([ - '@objectstack/spec/hub', - '@objectstack/core/errors', - '@objectstack/core/plugin', -]); - -/** - * MUST be reported failing, with a TS2307 on EACH of the three specifiers - * above, every one of them classified `unpublished-subpath` and forgiven by - * nothing. - * - * Reconstructed line-for-line from `packages/spec/V3_MIGRATION_GUIDE.md` as it - * read BEFORE #18753 repaired it. That document announced a migration that - * never happened: `466f32ea22` deleted `createErrorResponse`, - * `getHttpStatusForCategory` and `definePlugin` from spec and created neither - * destination, so the guide told a reader to import from subpaths their - * packages publish in no `exports` entry. - * - * ⭐ SYNTHETIC ON PURPOSE, and it is the whole reason this fixture exists. A - * control that IS the live document disarms itself the moment the document is - * repaired — the census number falls, and the fourth instance in the next guide - * is just as green. FIRING_CONTROL was reconstructed from PR #18712's diff for - * exactly this reason; this is the same move for the `unpublished-subpath` - * family, which FIRING_CONTROL (a TS2341) does not cover at all. - * - * ⭐ WHAT IT GUARDS IS THE TOLERANCE RULE, not the corpus. Widen - * `classifyDiagnostic` until a `@objectstack/*` TS2307 is forgiven and every - * unresolvable documented import in this repository goes quiet at once — with - * no other control able to notice. That is why it sits on the refusal list - * beside the other two instead of firing only under `--self-test`. - * - * ⚠️ THE COST, stated because it is real: if one of the three subpaths is ever - * genuinely published, this control stops producing TS2307 for it and EVERY - * census refuses until someone edits the fixture. That is the same exposure - * FIRING_CONTROL already carries (make `ObjectKernel.logger` public and it - * stops producing TS2341), and it is answered the same way — the refusal text - * names the remedy instead of leaving a stale fixture to be diagnosed. - * - * ⛔ It carries NO elision marker and NO syntax error, both load-bearing: a - * marker would let the (disqualified) exclusion reading skip it, and a syntax - * error would land it in pass 1's unparseable set, where tsc never type-checks - * it and the three TS2307s it exists to produce would never be reported at all. - */ -export const UNPUBLISHED_SUBPATH_CONTROL = [ - '# unpublished-subpath control', - '', - '```typescript', - "import { TenantSchema } from '@objectstack/spec/hub';", - "import { createErrorResponse, getHttpStatusForCategory } from '@objectstack/core/errors';", - "import { definePlugin } from '@objectstack/core/plugin';", - '```', - '', -].join('\n'); - -// ── Resolution: a `paths` map generated from each package's own exports map ── - -/** Walk a conditional exports value for the `types` condition an importer sees. */ -export function pickTypesTarget(value) { - if (typeof value === 'string') return /\.d\.[cm]?ts$/.test(value) ? value : null; - if (!value || typeof value !== 'object') return null; - if (typeof value.types === 'string') return value.types; - for (const condition of ['import', 'node', 'default', 'require', 'browser']) { - if (condition in value) { - const found = pickTypesTarget(value[condition]); - if (found) return found; - } - } - return null; -} - -/** - * ⛔ A subpath with no entry here gets NO mapping, so it fails TS2307 and is - * counted — that is the point: the census sees exactly what a consumer sees. - */ -export function workspacePaths(packageRoots) { - const paths = {}; - for (const dir of packageRoots) { - let manifest; - try { - manifest = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')); - } catch { - continue; - } - if (!manifest.name || manifest.private === true) continue; - const add = (specifier, target) => { - if (target) paths[specifier] = [join(dir, target)]; - }; - if (manifest.exports && typeof manifest.exports === 'object') { - for (const [key, value] of Object.entries(manifest.exports)) { - if (!key.startsWith('.')) continue; - const specifier = key === '.' ? manifest.name : `${manifest.name}/${key.slice(2)}`; - add(specifier, pickTypesTarget(value)); - } - } else if (typeof manifest.exports === 'string') { - add(manifest.name, pickTypesTarget(manifest.exports)); - } - if (!paths[manifest.name] && typeof manifest.types === 'string') add(manifest.name, manifest.types); - } - return paths; -} - -/** Third-party packages live in each package's own store under pnpm, never at the root. */ -export function externalPaths(specifiers, packageRoots) { - const paths = {}; - const searchRoots = [REPO_ROOT, ...packageRoots]; - for (const specifier of specifiers) { - if (specifier.startsWith('.') || specifier.startsWith('@objectstack/') || specifier.startsWith('node:')) continue; - const parts = specifier.split('/'); - const pkgName = specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]; - // An @types package FIRST: a runtime package that ships no declarations would - // otherwise resolve and then report TS7016, turning "this checkout has no - // types for react" into a census of the documentation. - const typesName = pkgName.startsWith('@') ? `@types/${pkgName.slice(1).replace('/', '__')}` : `@types/${pkgName}`; - for (const root of searchRoots) { - for (const name of [typesName, pkgName]) { - const candidate = join(root, 'node_modules', name); - if (!existsSync(candidate)) continue; - paths[pkgName] = [candidate]; - paths[`${pkgName}/*`] = [join(candidate, '*')]; - break; - } - if (paths[pkgName]) break; - } - } - return paths; -} - -const IMPORT_SPECIFIER_RE = /(?:from\s*|import\s*\(\s*|require\s*\(\s*)['"]([^'"]+)['"]/g; -export const specifiersIn = (code) => [...code.matchAll(IMPORT_SPECIFIER_RE)].map((m) => m[1]); - -// ── Compilation ──────────────────────────────────────────────────────────── - -function compilerOptions(paths) { - return { - target: 'ES2022', - lib: ['ES2022', 'DOM', 'DOM.Iterable'], - module: 'ESNext', - moduleResolution: 'Bundler', - jsx: 'react-jsx', - strict: true, - noEmit: true, - skipLibCheck: true, - esModuleInterop: true, - allowSyntheticDefaultImports: true, - resolveJsonModule: true, - forceConsistentCasingInFileNames: true, - noUnusedLocals: false, - noUnusedParameters: false, - // ⛔ No `baseUrl`: TS 6.0 deprecates it, and every `paths` target below is - // already absolute, so there is nothing for it to be relative to. - typeRoots: [join(REPO_ROOT, 'node_modules', '@types')], - types: ['node'], - paths, - }; -} - -/** - * One program for every block at once. Per-block programs would multiply the - * `.d.ts` closure by a thousand; the `export {}` normalisation is what makes - * one program safe. - */ -function compile(units, packageRoots) { - const tmpDir = mkdtempSync(join(tmpdir(), 'os-md-ts-census-')); - const cleanup = () => { - try { - rmSync(tmpDir, { recursive: true, force: true }); - } catch { - /* a leftover temp dir is not worth failing a census over */ - } - }; - process.on('exit', cleanup); - process.on('SIGINT', () => { - cleanup(); - process.exit(130); - }); - process.on('SIGTERM', () => { - cleanup(); - process.exit(143); - }); - try { - units.forEach((unit, index) => { - const name = `block-${String(index).padStart(4, '0')}.${unit.lang === 'tsx' ? 'tsx' : 'ts'}`; - writeFileSync(join(tmpDir, name), normalise(unit.code)); - unit.compiledAs = name; - }); - const specifiers = new Set(units.flatMap((unit) => specifiersIn(unit.code))); - const paths = { ...workspacePaths(packageRoots), ...externalPaths(specifiers, packageRoots) }; - writeFileSync( - join(tmpDir, 'tsconfig.json'), - JSON.stringify({ compilerOptions: compilerOptions(paths), files: units.map((u) => `./${u.compiledAs}`) }, null, 2), - ); - - // The real binary, the same question #18715's reading asked. `cwd` is the - // temp dir so every diagnostic names a bare `block-NNNN.ts`, and stdout and - // stderr are joined because tsc splits config errors from file ones. - // - // ⚠️ TWO PASSES, and the reason is a measured property of `tsc` itself: - // the CLI reports SYNTACTIC diagnostics and then stops — it never type- - // checks ANY file in a program where one file has a syntax error. One pass - // over this corpus returned 913 TS1xxx and not a single semantic - // diagnostic, the firing control included. FIRING_CONTROL is what caught - // that, which is the entire argument for running it on every census. - // So: pass 1 finds the blocks that do not parse, pass 2 re-runs over the - // ones that do. A block that does not parse is never type-checked, which - // is honest — tsc cannot read it either. - const runTsc = (fileList, configName) => { - writeFileSync( - join(tmpDir, configName), - JSON.stringify({ compilerOptions: compilerOptions(paths), files: fileList.map((n) => `./${n}`) }, null, 2), - ); - const run = spawnSync(process.execPath, [TSC_BIN, '--project', configName, '--pretty', 'false'], { - cwd: tmpDir, - encoding: 'utf8', - maxBuffer: 256 * 1024 * 1024, - }); - if (run.error) throw run.error; - return parseTscOutput(`${run.stdout ?? ''}\n${run.stderr ?? ''}`); - }; - - const all = units.map((u) => u.compiledAs); - const pass1 = runTsc(all, 'tsconfig.json'); - const unparseable = new Set(pass1.diagnostics.filter((d) => d.code >= 1000 && d.code <= SYNTAX_MAX).map((d) => d.file)); - const parseable = all.filter((name) => !unparseable.has(name)); - const pass2 = parseable.length > 0 && parseable.length < all.length - ? runTsc(parseable, 'tsconfig.pass2.json') - : { diagnostics: parseable.length > 0 ? pass1.diagnostics : [], global: [] }; - - const global = [...pass1.global, ...pass2.global]; - const byFile = new Map(units.map((u) => [u.compiledAs, []])); - const seen = new Set(); - for (const d of [...pass1.diagnostics, ...pass2.diagnostics]) { - const key = `${d.file}:${d.line}:${d.code}:${d.message}`; - if (seen.has(key)) continue; - seen.add(key); - const bucket = byFile.get(d.file); - if (bucket) bucket.push(d); - else global.push(d); - } - for (const unit of units) { - unit.diagnostics = (byFile.get(unit.compiledAs) ?? []).map((record) => ({ - ...record, - ...classifyDiagnostic(record), - })); - unit.raw = unit.diagnostics.length > 0; - unit.tolerant = unit.diagnostics.some((d) => !d.forgiven); - unit.elided = hasElisionMarker(unit.code); - const unforgiven = unit.diagnostics.filter((d) => !d.forgiven); - unit.syntaxOnly = unforgiven.length > 0 && unforgiven.every((d) => d.family === 'syntax'); - // Parses as a TypeScript module and STILL fails: the class the #18712 - // `kernel.logger` block belongs to, and the only one a gate could hold - // at zero without anyone first tagging a thousand fragments. - unit.wellFormedAndWrong = unit.tolerant && !unit.diagnostics.some((d) => d.family === 'syntax'); - } - return { global, paths }; - } finally { - cleanup(); - } -} - -// ── Census ───────────────────────────────────────────────────────────────── - -function collectUnits(only) { - let files = populationFiles(); - if (only.length) files = files.filter((f) => only.some((o) => relative(REPO_ROOT, f).includes(o))); - const units = []; - for (const file of files) { - const rel = relative(REPO_ROOT, file); - extractBlocks(readFileSync(file, 'utf8')).forEach((block, index) => { - units.push({ ...block, file: rel, index, kind: 'corpus', stratum: stratumOf(rel), depthOne: isDepthOne(rel) }); - }); - } - return { files, units }; -} - -function controlUnits() { - return [ - { ...extractBlocks(GREEN_CONTROL)[0], file: 'control:green', index: 0, kind: 'control', control: 'green', stratum: 'control' }, - { ...extractBlocks(FIRING_CONTROL)[0], file: 'control:firing', index: 0, kind: 'control', control: 'firing', stratum: 'control' }, - { - ...extractBlocks(UNPUBLISHED_SUBPATH_CONTROL)[0], - file: 'control:unpublished-subpath', - index: 0, - kind: 'control', - control: 'unpublished-subpath', - stratum: 'control', - }, - ]; -} - -/** The refusal list. Empty means the instrument proved it can fire before reporting. */ -export function controlProblems(controls) { - const problems = []; - const green = controls.find((c) => c.control === 'green'); - const firing = controls.find((c) => c.control === 'firing'); - if (!green) problems.push('GREEN_CONTROL did not extract to a block — the extractor is broken.'); - else if (green.raw) { - problems.push( - `GREEN_CONTROL did NOT compile (${green.diagnostics.map((d) => `TS${d.code}`).join(', ')}). ` - + 'A block that must compile did not, so nothing else this run would report is a reading. ' - + 'The usual cause is an unbuilt workspace: run `pnpm build` so each package has the ' - + '`dist/*.d.ts` its own exports map points at, then re-run.', - ); - for (const d of green.diagnostics.slice(0, 4)) problems.push(` GREEN_CONTROL TS${d.code}: ${d.message}`); - } - if (!firing) problems.push('FIRING_CONTROL did not extract to a block — the extractor is broken.'); - else { - if (!firing.raw) { - problems.push( - 'FIRING_CONTROL compiled CLEAN. It reconstructs the pre-#18712 `kernel.logger` block, which measured ' - + 'exit 2 / TS2341. An instrument that cannot reproduce the one failure this family has proven has ' - + 'measured nothing, so this run reports nothing.', - ); - } - if (!firing.diagnostics.some((d) => d.code === 2341)) { - problems.push( - 'FIRING_CONTROL did not produce TS2341 (private member access). It may be failing for an unrelated ' - + `reason — it reported: ${firing.diagnostics.map((d) => `TS${d.code}`).join(', ') || 'nothing'}.`, - ); - } - if (firing.raw && !firing.tolerant) { - problems.push('The tolerance rule FORGAVE the firing control — the rule has been widened until it hides the proven defect.'); - } - if (!firing.elided) { - problems.push( - 'FIRING_CONTROL no longer carries an elision marker, so this run cannot demonstrate why the exclusion ' - + 'reading is disqualified. Restore the `// ... plugin registration code ...` line from PR #18712.', - ); - } - } - const subpath = controls.find((c) => c.control === 'unpublished-subpath'); - if (!subpath) problems.push('UNPUBLISHED_SUBPATH_CONTROL did not extract to a block — the extractor is broken.'); - else { - for (const specifier of UNPUBLISHED_SUBPATH_SPECIFIERS) { - const hit = subpath.diagnostics.find((d) => d.code === 2307 && d.specifier === specifier); - if (!hit) { - problems.push( - `UNPUBLISHED_SUBPATH_CONTROL produced no TS2307 for '${specifier}'. Either the compile path no longer ` - + 'reaches this fixture, or that subpath is now PUBLISHED. If it is published, this control is obsolete ' - + 'for that specifier: repoint it at one that is still in no `exports` entry, in the same edit that ' - + 'records why — ⛔ never by deleting the leg, which removes the guard for the other two with it.', - ); - continue; - } - if (hit.family !== 'unpublished-subpath' || hit.forgiven) { - problems.push( - `UNPUBLISHED_SUBPATH_CONTROL's TS2307 for '${specifier}' classified as ${hit.family} / ` - + `forgiven=${hit.forgiven}, not unpublished-subpath / forgiven=false. The tolerance rule has been ` - + 'widened until a documented import that resolves for NO consumer of the published package is forgiven, ' - + 'which silences this whole family of defect at once.', - ); - } - } - if (subpath.raw && !subpath.tolerant) { - problems.push( - 'The tolerance rule FORGAVE the unpublished-subpath control outright, so every unresolvable ' - + '`@objectstack/*` import in the corpus is now invisible to the TOLERANT reading — the one number a gate ' - + 'could be built on.', - ); - } - } - return problems; -} - -const EMPTY_STRATUM = () => ({ - blocks: 0, raw: 0, tolerant: 0, elided: 0, syntaxOnly: 0, wellFormedAndWrong: 0, - exclusionConsidered: 0, exclusionFail: 0, files: new Set(), filesFailing: new Set(), -}); - -export function summarise(units) { - const strata = {}; - const depthOne = EMPTY_STRATUM(); - const families = {}; - for (const unit of units) { - const bucket = (strata[unit.stratum] ??= EMPTY_STRATUM()); - for (const target of unit.depthOne ? [bucket, depthOne] : [bucket]) { - target.blocks += 1; - target.files.add(unit.file); - if (unit.raw) target.raw += 1; - if (unit.tolerant) { - target.tolerant += 1; - target.filesFailing.add(unit.file); - } - if (unit.elided) target.elided += 1; - if (unit.syntaxOnly) target.syntaxOnly += 1; - if (unit.wellFormedAndWrong) target.wellFormedAndWrong += 1; - if (!unit.elided) { - target.exclusionConsidered += 1; - if (unit.raw) target.exclusionFail += 1; - } - } - for (const d of unit.diagnostics) { - const f = (families[d.family] ??= { diagnostics: 0, blocks: new Set(), codes: {} }); - f.diagnostics += 1; - f.blocks.add(`${unit.file}#${unit.index}`); - f.codes[`TS${d.code}`] = (f.codes[`TS${d.code}`] ?? 0) + 1; - } - } - return { strata, depthOne, families }; -} - -const pct = (n, d) => (d === 0 ? ' n/a' : `${((n / d) * 100).toFixed(1).padStart(5)}%`); - -function reportStratum(label, s) { - console.log(`\n ${label}`); - console.log(` files with TS blocks ......... ${s.files.size}`); - console.log(` fenced ts/typescript/tsx .... ${s.blocks}`); - console.log(` RAW fail ............... ${String(s.raw).padStart(4)} ${pct(s.raw, s.blocks)} (upper bound)`); - console.log(` TOLERANT fail ............... ${String(s.tolerant).padStart(4)} ${pct(s.tolerant, s.blocks)} (lower bound — the gate-able number)`); - console.log(` WELL-FORMED AND WRONG ....... ${String(s.wellFormedAndWrong).padStart(4)} ${pct(s.wellFormedAndWrong, s.blocks)} (parses as a module and still fails — the #18712 class)`); - console.log(` files carrying >=1 tolerant fail: ${s.filesFailing.size}`); - console.log(` blocks whose only unforgiven diagnostics are SYNTAX (would need an explicit \`partial\` tag): ${s.syntaxOnly}`); - console.log(` [disqualified] EXCLUSION reading: ${s.exclusionFail} fail of ${s.exclusionConsidered} considered, ${s.elided} blocks skipped for carrying an elision marker`); -} - -function report(units, controls, summary, files) { - const firing = controls.find((c) => c.control === 'firing'); - console.log('markdown TS block census — package-root Markdown under packages/ (#18715)'); - console.log('\nCONTROLS (they run on every census, so a zero is a reading and not a dead search)'); - console.log(' GREEN_CONTROL compiles clean — `@objectstack/*` resolves through the built exports maps.'); - console.log( - ` FIRING_CONTROL reports ${firing.diagnostics.length} diagnostic(s) including ` - + `TS2341 at block line ${firing.diagnostics.find((d) => d.code === 2341)?.line ?? '?'} — ` - + 'the pre-#18712 `kernel.logger` block, reconstructed from that PR\'s diff, is caught.', - ); - console.log( - ' FIRING_CONTROL also carries an elision marker, so the EXCLUSION reading SKIPS it. ' - + 'That is why exclusion is reported below as disqualified rather than offered.', - ); - console.log( - ` UNPUBLISHED_SUBPATH_CONTROL reports TS2307 on all ${UNPUBLISHED_SUBPATH_SPECIFIERS.length} of its specifiers ` - + `(${UNPUBLISHED_SUBPATH_SPECIFIERS.join(', ')}), every one COUNTED as ` - + 'unpublished-subpath — the tolerance rule still refuses to forgive an import that resolves for no consumer.', - ); - - console.log('\nPOPULATION'); - console.log(` package roots scanned ....... ${findPackageRoots(PACKAGES_DIR).length}`); - console.log(` package-root .md files ...... ${files.length}`); - - for (const [label, key] of [['HAND-WRITTEN (repairable by an author)', 'handwritten'], ['CHANGELOG.md (release-owned history)', 'changelog']]) { - if (summary.strata[key]) reportStratum(label, summary.strata[key]); - } - reportStratum('SUB-STRATUM: the literal packages/*/*.md glob #18715 names', summary.depthOne); - - console.log('\nDIAGNOSTIC FAMILIES (every diagnostic on every corpus block)'); - const rows = Object.entries(summary.families).sort((a, b) => b[1].diagnostics - a[1].diagnostics); - for (const [family, f] of rows) { - const mark = FORGIVEN_FAMILIES.has(family) ? 'forgiven' : 'COUNTED '; - const top = Object.entries(f.codes).sort((a, b) => b[1] - a[1]).slice(0, 4).map(([c, n]) => `${c}x${n}`).join(' '); - console.log(` ${mark} ${family.padEnd(24)} ${String(f.diagnostics).padStart(5)} diagnostics in ${String(f.blocks.size).padStart(4)} blocks ${top}`); - } - - console.log('\nPER FILE — blocks / raw fail / tolerant fail (files with at least one tolerant fail)'); - const perFile = new Map(); - for (const unit of units) { - const row = perFile.get(unit.file) ?? { blocks: 0, raw: 0, tolerant: 0, stratum: unit.stratum }; - row.blocks += 1; - if (unit.raw) row.raw += 1; - if (unit.tolerant) row.tolerant += 1; - perFile.set(unit.file, row); - } - const failing = [...perFile.entries()].filter(([, r]) => r.tolerant > 0).sort((a, b) => b[1].tolerant - a[1].tolerant); - for (const [file, r] of failing) { - console.log(` ${String(r.blocks).padStart(4)} ${String(r.raw).padStart(4)} ${String(r.tolerant).padStart(4)} ${file}`); - } - const clean = [...perFile.entries()].filter(([, r]) => r.tolerant === 0); - console.log(`\n ${clean.length} file(s) with TS blocks and zero tolerant failures.`); - console.log('\n⛔ This is a census. It exits 0 on every outcome and gates nothing.'); -} - -function toJson(units, controls, summary, files) { - const plain = (s) => ({ ...s, files: s.files.size, filesFailing: s.filesFailing.size }); - return { - issue: 18715, - population: { packageRoots: findPackageRoots(PACKAGES_DIR).length, markdownFiles: files.length }, - controls: controls.map((c) => ({ - control: c.control, - diagnostics: c.diagnostics.map((d) => ({ code: d.code, family: d.family, forgiven: d.forgiven, line: d.line })), - elided: c.elided, - tolerantFail: c.tolerant, - })), - strata: Object.fromEntries(Object.entries(summary.strata).map(([k, v]) => [k, plain(v)])), - depthOneSubStratum: plain(summary.depthOne), - families: Object.fromEntries(Object.entries(summary.families).map(([k, v]) => [k, { diagnostics: v.diagnostics, blocks: v.blocks.size, codes: v.codes }])), - blocks: units.map((u) => ({ - file: u.file, index: u.index, lang: u.lang, firstBodyLine: u.firstBodyLine, - stratum: u.stratum, elided: u.elided, declaredPartial: u.declaredPartial, - rawFail: u.raw, tolerantFail: u.tolerant, syntaxOnly: u.syntaxOnly, wellFormedAndWrong: u.wellFormedAndWrong, - diagnostics: u.diagnostics.map((d) => ({ code: d.code, family: d.family, forgiven: d.forgiven, line: d.line, message: d.message })), - })), - }; -} - -// ── Self-test ────────────────────────────────────────────────────────────── -// -// Set as `selfTest()`'s LAST statement, after its verdict prints, and read at -// the dispatch: a `return` above the verdict prints nothing and still exits 0, -// which would report a self-test that never finished as one that passed. -let selfTestReachedVerdict = false; - -// The roster is a LITERAL the table is checked against — ⛔ never derived from -// the table, because `cases.length` moves with the table and a deleted row -// would delete its own floor. Each row LABEL is one battery with a floor of 1; -// `registerCase(c.label)` is the FIRST statement of the loop body, so the floor -// asserts REACH and a row whose guard always skipped would read DID NOT RUN. -// -// SCOPE: these batteries cover the EXTRACTOR, the elision proxy, the -// classifier and the fixtures — everything that needs no compiler. The two -// CONTROLS are deliberately not here: they need a built workspace, and they -// run on every census rather than only when someone types `--self-test`. -// `controlProblems()` is their refusal, and it is not optional either. -const SELF_TEST_BATTERIES = Object.freeze({ - 'extract — a plain ```ts fence is a block': 1, - 'extract — a ```typescript fence is a block': 1, - 'extract — a ```tsx fence is a block': 1, - 'extract — a tilde fence is a block': 1, - 'extract — a non-TypeScript language is not a block': 1, - 'extract — an indented fence is dedented to column 0': 1, - 'extract — a ts fence nested inside another fence is body, not a block': 1, - 'extract — an unterminated fence yields no block': 1, - 'extract — an info string carrying `partial` reads as declared-partial': 1, - 'elision — a trailing `// ... plugin registration code ...` is a marker': 1, - 'elision — ordinary comments are not an elision marker': 1, - 'classify — TS2304 is FORGIVEN (the name the block elided)': 1, - 'classify — TS2341 is COUNTED (private member — the one proven defect)': 1, - 'classify — TS2307 on a relative specifier is FORGIVEN': 1, - 'classify — TS2307 on an @objectstack specifier is COUNTED': 1, - 'classify — TS1005 syntax is COUNTED, never forgiven as elision': 1, - 'classify — TS2593 (a test-runner global) is FORGIVEN as the reader\'s ambient environment': 1, - 'classify — TS7016 (a module that ships no declarations) is FORGIVEN as ambient': 1, - 'externals — an @types package is preferred over the runtime package': 1, - 'tsc output — a located diagnostic line is parsed into file, line, code and message': 1, - 'tsc output — an indented continuation line joins the diagnostic above it': 1, - 'tsc output — a diagnostic with no file (a config error) lands in the global bucket': 1, - 'normalise — `export {}` is appended and body line numbers are unchanged': 1, - 'fixture — FIRING_CONTROL extracts to one block that reads kernel.logger': 1, - 'fixture — FIRING_CONTROL carries the elision marker that disqualifies exclusion': 1, - 'fixture — GREEN_CONTROL extracts to one block importing @objectstack/core': 1, - 'fixture — UNPUBLISHED_SUBPATH_CONTROL extracts to one block whose specifiers are exactly UNPUBLISHED_SUBPATH_SPECIFIERS': 1, - 'fixture — UNPUBLISHED_SUBPATH_CONTROL carries no elision marker, so no reading can skip it': 1, - 'classify — every UNPUBLISHED_SUBPATH_SPECIFIERS entry is COUNTED as unpublished-subpath': 1, - 'refusal — a missing TS2307 on a listed specifier REFUSES the census': 1, - 'refusal — a FORGIVEN unpublished-subpath diagnostic REFUSES the census': 1, - 'population — stratumOf separates CHANGELOG.md from hand-written Markdown': 1, - 'population — isDepthOne separates a depth-1 package root from a nested one': 1, - 'exports — pickTypesTarget reads the types entry under the import condition': 1, - 'exports — a subpath a package does not export gets no mapping': 1, -}); - -// Deleting an entry silences that battery's floor exactly as effectively as -// zeroing it, so the roster's own size is pinned too. -const SELF_TEST_BATTERY_FLOOR = 35; - -const md = (...lines) => lines.join('\n'); - -function selfTest() { - // Healthy stand-ins for the other two controls, so a refusal battery reads - // only the problems the unpublished-subpath leg contributes — and asserts the - // healthy trio reports NONE before asserting a broken one reports some, since - // a `controlProblems` that always complained would otherwise pass. - const healthyGreen = () => ({ control: 'green', raw: false, tolerant: false, elided: false, diagnostics: [] }); - const healthyFiring = () => ({ - control: 'firing', - raw: true, - tolerant: true, - elided: true, - diagnostics: [{ code: 2341, family: 'semantic', forgiven: false }], - }); - const healthySubpathDiagnostics = () => UNPUBLISHED_SUBPATH_SPECIFIERS.map((specifier) => ({ - code: 2307, specifier, family: 'unpublished-subpath', forgiven: false, - })); - const subpathControl = (diagnostics) => ({ control: 'unpublished-subpath', raw: true, tolerant: true, elided: false, diagnostics }); - - const cases = [ - { label: 'extract — a plain ```ts fence is a block', run: () => (extractBlocks(md('```ts', 'const a = 1;', '```')).length === 1 ? null : 'expected 1 block') }, - { label: 'extract — a ```typescript fence is a block', run: () => (extractBlocks(md('```typescript', 'const a = 1;', '```'))[0]?.lang === 'typescript' ? null : 'expected a typescript block') }, - { label: 'extract — a ```tsx fence is a block', run: () => (extractBlocks(md('```tsx', 'const a = ;', '```'))[0]?.lang === 'tsx' ? null : 'expected a tsx block') }, - { label: 'extract — a tilde fence is a block', run: () => (extractBlocks(md('~~~ts', 'const a = 1;', '~~~')).length === 1 ? null : 'expected 1 block from a tilde fence') }, - { label: 'extract — a non-TypeScript language is not a block', run: () => (extractBlocks(md('```json', '{}', '```')).length === 0 ? null : 'json counted as TypeScript') }, - { - label: 'extract — an indented fence is dedented to column 0', - run: () => { - const b = extractBlocks(md('- item', '', ' ```ts', ' const a = 1;', ' ```'))[0]; - return b && b.code === 'const a = 1;' ? null : `expected dedented code, got ${JSON.stringify(b?.code)}`; - }, - }, - { - label: 'extract — a ts fence nested inside another fence is body, not a block', - run: () => (extractBlocks(md('````md', '```ts', 'const a = 1;', '```', '````')).length === 0 ? null : 'a quoted fence was counted as shipped code'), - }, - { label: 'extract — an unterminated fence yields no block', run: () => (extractBlocks(md('```ts', 'const a = 1;')).length === 0 ? null : 'an unterminated fence produced a block') }, - { label: 'extract — an info string carrying `partial` reads as declared-partial', run: () => (extractBlocks(md('```ts partial', 'const a = 1;', '```'))[0]?.declaredPartial === true ? null : 'the partial tag was not read') }, - { label: 'elision — a trailing `// ... plugin registration code ...` is a marker', run: () => (hasElisionMarker('const a = 1;\n// ... plugin registration code ...') ? null : 'marker missed') }, - { label: 'elision — ordinary comments are not an elision marker', run: () => (hasElisionMarker('// build the kernel\nconst a = 1;') ? 'an ordinary comment read as an elision' : null) }, - { label: 'classify — TS2304 is FORGIVEN (the name the block elided)', run: () => (classifyDiagnostic({ code: 2304, message: "Cannot find name 'kernel'." }).forgiven ? null : 'TS2304 not forgiven') }, - { label: 'classify — TS2341 is COUNTED (private member — the one proven defect)', run: () => (classifyDiagnostic({ code: 2341, message: "Property 'logger' is private." }).forgiven ? 'TS2341 forgiven — the proven defect would be invisible' : null) }, - { label: 'classify — TS2307 on a relative specifier is FORGIVEN', run: () => (classifyDiagnostic({ code: 2307, message: "Cannot find module './my-kernel' or its corresponding type declarations." }).forgiven ? null : 'a doc-local path was counted') }, - { label: 'classify — TS2307 on an @objectstack specifier is COUNTED', run: () => (classifyDiagnostic({ code: 2307, message: "Cannot find module '@objectstack/core/plugin' or its corresponding type declarations." }).forgiven ? 'an unpublished subpath was forgiven' : null) }, - { label: 'classify — TS1005 syntax is COUNTED, never forgiven as elision', run: () => (classifyDiagnostic({ code: 1005, message: "';' expected." }).family === 'syntax' && !classifyDiagnostic({ code: 1005, message: '' }).forgiven ? null : 'syntax forgiven') }, - { label: "classify — TS2593 (a test-runner global) is FORGIVEN as the reader's ambient environment", run: () => (classifyDiagnostic({ code: 2593, message: "Cannot find name 'it'." }).family === 'missing-ambient-environment' ? null : 'a test-runner global was counted against the document') }, - { label: 'classify — TS7016 (a module that ships no declarations) is FORGIVEN as ambient', run: () => (classifyDiagnostic({ code: 7016, message: "Could not find a declaration file for module 'react/jsx-runtime'." }).forgiven ? null : 'a missing third-party declaration was counted against the document') }, - { - label: 'tsc output — a located diagnostic line is parsed into file, line, code and message', - run: () => { - const { diagnostics } = parseTscOutput("block-0007.ts(13,45): error TS2341: Property 'logger' is private and only accessible within class 'ObjectKernel'."); - const d = diagnostics[0]; - if (!d) return 'nothing parsed'; - return d.file === 'block-0007.ts' && d.line === 13 && d.code === 2341 && d.message.startsWith("Property 'logger' is private") - ? null - : `parsed ${JSON.stringify(d)}`; - }, - }, - { - label: 'tsc output — an indented continuation line joins the diagnostic above it', - run: () => { - const { diagnostics } = parseTscOutput('block-0001.ts(3,1): error TS2322: Type A is not assignable to type B.\n Property x is missing.'); - return diagnostics.length === 1 && diagnostics[0].message.includes('Property x is missing.') - ? null - : `got ${diagnostics.length} diagnostic(s): ${JSON.stringify(diagnostics.map((d) => d.message))}`; - }, - }, - { - label: 'tsc output — a diagnostic with no file (a config error) lands in the global bucket', - run: () => { - const { diagnostics, global } = parseTscOutput("error TS5101: Option 'baseUrl' is deprecated."); - return diagnostics.length === 0 && global.length === 1 && global[0].code === 5101 - ? null - : `diagnostics=${diagnostics.length} global=${JSON.stringify(global)}`; - }, - }, - { - label: 'externals — an @types package is preferred over the runtime package', - run: () => { - const dir = mkdtempSync(join(tmpdir(), 'os-md-ts-selftest-')); - try { - mkdirSync(join(dir, 'node_modules', '@types', 'reactish'), { recursive: true }); - mkdirSync(join(dir, 'node_modules', 'reactish'), { recursive: true }); - const paths = externalPaths(['reactish/jsx-runtime'], [dir]); - return paths.reactish?.[0]?.includes(`${'@types'}/reactish`) ? null : `mapped to ${paths.reactish?.[0]}`; - } finally { - rmSync(dir, { recursive: true, force: true }); - } - }, - }, - { - label: 'normalise — `export {}` is appended and body line numbers are unchanged', - run: () => { - const b = extractBlocks(md('# t', '', '```ts', 'const a = 1;', '```'))[0]; - if (b.firstBodyLine !== 4) return `firstBodyLine ${b.firstBodyLine}, expected 4`; - const n = normalise(b.code); - return n.startsWith('const a = 1;') && n.includes('export {};') ? null : 'normalisation changed the head of the block'; - }, - }, - { - label: 'fixture — FIRING_CONTROL extracts to one block that reads kernel.logger', - run: () => { - const b = extractBlocks(FIRING_CONTROL); - if (b.length !== 1) return `expected 1 block, got ${b.length}`; - return b[0].code.includes('kernel.logger') ? null : 'the fixture no longer reaches into kernel.logger'; - }, - }, - { label: 'fixture — FIRING_CONTROL carries the elision marker that disqualifies exclusion', run: () => (hasElisionMarker(extractBlocks(FIRING_CONTROL)[0].code) ? null : 'the fixture lost its elision marker, and with it the exclusion argument') }, - { - label: 'fixture — GREEN_CONTROL extracts to one block importing @objectstack/core', - run: () => { - const b = extractBlocks(GREEN_CONTROL); - if (b.length !== 1) return `expected 1 block, got ${b.length}`; - return specifiersIn(b[0].code).includes('@objectstack/core') ? null : 'the green control stopped importing anything'; - }, - }, - { - label: 'fixture — UNPUBLISHED_SUBPATH_CONTROL extracts to one block whose specifiers are exactly UNPUBLISHED_SUBPATH_SPECIFIERS', - run: () => { - const b = extractBlocks(UNPUBLISHED_SUBPATH_CONTROL); - if (b.length !== 1) return `expected 1 block, got ${b.length}`; - const found = specifiersIn(b[0].code); - const expected = [...UNPUBLISHED_SUBPATH_SPECIFIERS]; - return found.length === expected.length && expected.every((s) => found.includes(s)) - ? null - : `the block imports ${JSON.stringify(found)} but the roster is ${JSON.stringify(expected)} — editing one without the other narrows the guard silently`; - }, - }, - { - label: 'fixture — UNPUBLISHED_SUBPATH_CONTROL carries no elision marker, so no reading can skip it', - run: () => (hasElisionMarker(extractBlocks(UNPUBLISHED_SUBPATH_CONTROL)[0].code) - ? 'the fixture gained an elision marker, so the exclusion reading would skip the one leg guarding this family' - : null), - }, - { - label: 'classify — every UNPUBLISHED_SUBPATH_SPECIFIERS entry is COUNTED as unpublished-subpath', - run: () => { - for (const specifier of UNPUBLISHED_SUBPATH_SPECIFIERS) { - const verdict = classifyDiagnostic({ - code: 2307, - message: `Cannot find module '${specifier}' or its corresponding type declarations.`, - }); - if (verdict.family !== 'unpublished-subpath' || verdict.forgiven) { - return `${specifier} classified ${verdict.family} / forgiven=${verdict.forgiven}`; - } - } - return null; - }, - }, - { - label: 'refusal — a missing TS2307 on a listed specifier REFUSES the census', - run: () => { - const healthy = controlProblems([healthyGreen(), healthyFiring(), subpathControl(healthySubpathDiagnostics())]); - if (healthy.length !== 0) return `the healthy control trio already reported ${healthy.length} problem(s): ${healthy[0]}`; - const dropped = UNPUBLISHED_SUBPATH_SPECIFIERS[1]; - const problems = controlProblems([ - healthyGreen(), - healthyFiring(), - subpathControl(healthySubpathDiagnostics().filter((d) => d.specifier !== dropped)), - ]); - return problems.some((p) => p.includes(dropped)) ? null : `dropping ${dropped} produced ${JSON.stringify(problems)}`; - }, - }, - { - label: 'refusal — a FORGIVEN unpublished-subpath diagnostic REFUSES the census', - run: () => { - const target = UNPUBLISHED_SUBPATH_SPECIFIERS[2]; - const widened = healthySubpathDiagnostics().map((d) => (d.specifier === target - ? { ...d, family: 'external-not-installed', forgiven: true } - : d)); - const problems = controlProblems([healthyGreen(), healthyFiring(), subpathControl(widened)]); - return problems.some((p) => p.includes(target) && p.includes('forgiven=true')) - ? null - : `forgiving ${target} produced ${JSON.stringify(problems)}`; - }, - }, - { label: 'population — stratumOf separates CHANGELOG.md from hand-written Markdown', run: () => (stratumOf('packages/core/CHANGELOG.md') === 'changelog' && stratumOf('packages/core/README.md') === 'handwritten' ? null : 'stratum split broken') }, - { label: 'population — isDepthOne separates a depth-1 package root from a nested one', run: () => (isDepthOne('packages/core/README.md') && !isDepthOne('packages/plugins/plugin-auth/README.md') ? null : 'depth-1 sub-stratum broken') }, - { - label: 'exports — pickTypesTarget reads the types entry under the import condition', - run: () => { - const target = pickTypesTarget({ browser: { import: { types: './dist/browser.d.mts' } }, import: { types: './dist/index.d.mts', default: './dist/index.mjs' }, require: { types: './dist/index.d.ts' } }); - return target === './dist/index.d.mts' ? null : `picked ${target}`; - }, - }, - { - label: 'exports — a subpath a package does not export gets no mapping', - run: () => { - const dir = mkdtempSync(join(tmpdir(), 'os-md-ts-selftest-')); - try { - writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: '@x/y', exports: { '.': { types: './dist/index.d.ts' } } })); - const paths = workspacePaths([dir]); - return paths['@x/y'] && !paths['@x/y/secret'] ? null : 'an unexported subpath was mapped anyway'; - } finally { - rmSync(dir, { recursive: true, force: true }); - } - }, - }, - ]; - - let failed = 0; - const batterySeen = new Map(); - const registerCase = (name) => batterySeen.set(name, (batterySeen.get(name) ?? 0) + 1); - for (const c of cases) { - registerCase(c.label); - let verdict; - try { - verdict = c.run(); - } catch (error) { - verdict = `threw: ${error.message}`; - } - if (verdict) { - failed += 1; - console.error(` ✗ ${c.label}\n ${verdict}`); - } - } - - const declared = Object.keys(SELF_TEST_BATTERIES); - if (declared.length < SELF_TEST_BATTERY_FLOOR) { - failed += 1; - console.error(` ✗ SELF_TEST_BATTERIES declares ${declared.length} batteries, below the pinned ${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`); - } - for (const [name, count] of batterySeen) { - if (name in SELF_TEST_BATTERIES) continue; - failed += 1; - console.error(` ✗ battery "${name}" registered ${count} case(s) but is not declared in SELF_TEST_BATTERIES — a case attributed to no declared battery is one nothing floors.`); - } - for (const name of declared) { - const count = batterySeen.get(name) ?? 0; - if (count >= SELF_TEST_BATTERIES[name]) continue; - failed += 1; - console.error(count === 0 - ? ` ✗ battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned.` - : ` ✗ battery "${name}" registered ${count} case(s), below its pinned floor of ${SELF_TEST_BATTERIES[name]}.`); - } - - if (failed > 0) { - console.error(`\n✗ measure-markdown-ts-blocks self-test: ${failed} failure(s) (cases and floor).`); - process.exit(1); - } - console.log(`\n✓ measure-markdown-ts-blocks self-test: ${cases.length} cases pass, ${declared.length} batteries at or above floor.`); - selfTestReachedVerdict = true; -} - -// ── Dispatch ─────────────────────────────────────────────────────────────── - -function main() { - const argv = process.argv.slice(2); - - if (argv.includes('--self-test')) { - selfTest(); - if (!selfTestReachedVerdict) { - console.error( - '\n✗ measure-markdown-ts-blocks self-test: selfTest() returned without reaching its verdict,\n' - + 'so no success line was printed. Exiting 0 here would report a self-test that never\n' - + 'finished as a self-test that passed.\n', - ); - process.exit(1); - } - return; - } - - const wantJson = argv.includes('--json'); - const controlsOnly = argv.includes('--controls-only'); - const only = argv.flatMap((arg, i) => (arg === '--only' && argv[i + 1] ? [argv[i + 1]] : [])); - - const packageRoots = findPackageRoots(PACKAGES_DIR); - const { files, units } = controlsOnly ? { files: [], units: [] } : collectUnits(only); - const controls = controlUnits(); - const { global } = compile([...controls, ...units], packageRoots); - - const problems = controlProblems(controls); - for (const g of global) problems.push(`compiler option diagnostic TS${g.code}: ${g.message}`); - if (problems.length > 0) { - console.error('✗ measure-markdown-ts-blocks REFUSES to report a census:\n'); - for (const p of problems) console.error(` ${p}`); - console.error( - '\n"could not run" is a failure, not a skip. A census that degraded to "0 failures" because\n' - + 'nothing resolved would be the worst output this instrument could produce.\n', - ); - process.exit(1); - } - - const summary = summarise(units); - if (wantJson) console.log(JSON.stringify(toJson(units, controls, summary, files), null, 2)); - else if (controlsOnly) { - console.log( - '✓ all three controls behaved: GREEN compiles, FIRING reports TS2341 and survives the tolerance rule, ' - + 'UNPUBLISHED_SUBPATH reports a COUNTED TS2307 on each of its three specifiers.', - ); - } - else report(units, controls, summary, files); -} - -// Guarded so this file can be imported for its exports without running the -// census inside the importer (`scripts/invoked-as.mjs`, `check:entry-guard`). -if (isEntrypoint(import.meta.url)) main(); diff --git a/scripts/measure-reserved-identity-name-census.mjs b/scripts/measure-reserved-identity-name-census.mjs deleted file mode 100644 index 53143f425e1..00000000000 --- a/scripts/measure-reserved-identity-name-census.mjs +++ /dev/null @@ -1,400 +0,0 @@ -#!/usr/bin/env node -// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. -// -// measure-reserved-identity-name-census -- the #15972 census instrument. -// -// node scripts/measure-reserved-identity-name-census.mjs # declarations -// node scripts/measure-reserved-identity-name-census.mjs --json # machine -// node scripts/measure-reserved-identity-name-census.mjs --self-test # controls only -// node scripts/measure-reserved-identity-name-census.mjs --rows FILE # a deployment's rows -// node scripts/measure-reserved-identity-name-census.mjs --rows-schema # the export shape -// -// READ-ONLY, and that is the ruling, not a preference. Maintainer ruling -// (director seat, summon #20, decision batch #105 item 4, 2026-09-09) on what -// happens to rows that ALREADY collide, verbatim: -// -// 「refuse new writes only. No migration. A read-only census reports existing -// colliding rows to the maintainer; nothing rewrites them (option B -// refused: stored-data migration is the manual floor's own item).」 -// -// So this script opens no database connection, takes no credentials, and its -// `--rows` mode consumes a FILE the operator exported. There is no code path -// here that writes anything anywhere. It is NOT a gate: not wired into any -// workflow, exits 0 on any collision count, and deliberately not named -// `check:*` / `gen:*` so the #4203 script ledger has nothing to classify -- -// the shape `measure-position-name-fold-census.mjs` established. -// -// The only non-zero exits are a failing self-test and an unreadable/malformed -// `--rows` input. -// -// ## What collides, and why the two populations are reported separately -// -// ADR-0068 D2 reserves four names for the framework's built-in identities. -// Since #15972 the write path refuses them on both position doors, so the -// populations below can only be rows that PREDATE the guard: -// -// - `sys_position` rows whose `name` spells a reserved name while their -// `managed_by` is NOT the platform's own provenance. The platform seeds -// these four names per organization on purpose (`bootstrapBuiltinRoles`, -// `managed_by: 'platform'`); those rows are the catalog and are NOT -// collisions. A row with any other provenance is a tenant or package -// definition standing on a reserved name. -// - `sys_user_position` rows whose `position` spells a reserved name. ⚠️ ALL -// of them are collisions -- no writer in any package creates one, so there -// is no legitimate population to subtract. This is the row the card is -// about: it is what makes a plain member LOOK like a built-in identity to -// any reader that reads the name instead of the capability rung. -// -// ## The reserved set is READ from the spec constant, never retyped -// -// The ruling closes the enumeration: 「exactly the ADR-0068 built-in identity -// names, read from the spec constant that declares them (closed enumeration, -// ⛔ not retyped, ⛔ not widened to `org_*` shapes by pattern)」. A census that -// carried its own copy of the four strings would answer a different question -// from the guard it audits the day the constant moves, so the names are parsed -// out of `packages/spec/src/identity/eval-user.zod.ts`. -// -// ⚠️ A parse that silently found nothing would report a comfortable ZERO over -// every population. So the parse has a CONTROL that must fire ({@link -// readReservedNames} throws when it does not), and `--self-test` asserts it. -// -// ## What this instrument CANNOT see, stated up front -// -// The declaration census reads THIS REPOSITORY. Positions and assignments are -// RUNTIME rows: an operator who created a position in Setup, or an integration -// that wrote an assignment row, produces a collision no static census can ever -// see. That population is reachable only through `--rows`, which is why -// `--rows` refuses to call an empty input "zero" -- and why a zero from the -// default mode prints the sentence saying so, every time. - -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -import { isEntrypoint } from './invoked-as.mjs'; - -const HERE = path.dirname(fileURLToPath(import.meta.url)); -const REPO_ROOT = path.resolve(HERE, '..'); -const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', '.git', '.turbo', 'coverage', '.cache', '.next']); -const SPEC_DECL = 'packages/spec/src/identity/eval-user.zod.ts'; - -/** Provenance values that mean "the platform's own catalog row" (A4 #2920 plus its legacy spelling). */ -const PLATFORM_PROVENANCE = new Set(['platform', 'system']); - -/* ------------------------------------------------------------------------- * - * The reserved set — parsed out of the declaring constant - * ------------------------------------------------------------------------- */ - -/** - * The four ADR-0068 D2 names, read from the `BUILTIN_IDENTITY_NAMES` array in - * the spec and resolved through the `BUILTIN_IDENTITY_*` constants it lists. - * - * Throws rather than returning `[]`: a census that cannot find its own subject - * must say so, because the alternative is a zero that reads like an all-clear. - */ -export function readReservedNames(source = read(SPEC_DECL)) { - const consts = new Map(); - const constRe = /export const (BUILTIN_IDENTITY_[A-Z0-9_]+)\s*=\s*'([^']*)'/g; - let m; - while ((m = constRe.exec(source)) !== null) consts.set(m[1], m[2]); - - const arrayRe = /export const BUILTIN_IDENTITY_NAMES\s*=\s*\[([^\]]*)\]/; - const arr = arrayRe.exec(source); - if (!arr) throw new Error(`census control FAILED: no BUILTIN_IDENTITY_NAMES array in ${SPEC_DECL}`); - - const names = []; - for (const raw of arr[1].split(',')) { - const ident = raw.trim(); - if (!ident) continue; - const literal = /^'([^']*)'$/.exec(ident); - if (literal) { names.push(literal[1]); continue; } - if (!consts.has(ident)) { - throw new Error(`census control FAILED: ${ident} is listed in BUILTIN_IDENTITY_NAMES but never declared in ${SPEC_DECL}`); - } - names.push(consts.get(ident)); - } - if (names.length === 0) throw new Error(`census control FAILED: BUILTIN_IDENTITY_NAMES parsed empty in ${SPEC_DECL}`); - return names; -} - -/* ------------------------------------------------------------------------- * - * Corpus scan (declarations) - * ------------------------------------------------------------------------- */ - -function read(rel) { - try { return fs.readFileSync(path.join(REPO_ROOT, rel), 'utf8'); } catch { return ''; } -} - -function walk(root, exts, out = []) { - let entries; - try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { return out; } - for (const e of entries) { - if (e.name.startsWith('.')) continue; - const full = path.join(root, e.name); - if (e.isDirectory()) { - if (SKIP_DIRS.has(e.name)) continue; - walk(full, exts, out); - } else if (exts.has(path.extname(e.name))) { - out.push(path.relative(REPO_ROOT, full)); - } - } - return out; -} - -/** - * Every `name: ''` / `position: ''` STRING LITERAL in a - * declaration under `packages/`, `examples/` or `apps/`. - * - * Textual, and bounded on purpose (the sibling census records the same - * reasoning): the failure mode being hunted is an authored literal, and the - * price of a source scan is that it sees only the spellings it knows. It does - * NOT see a name assembled at runtime, and it does not know which key belongs - * to a position declaration versus something else that happens to have a - * `name:` -- so its hits are CANDIDATES a reader adjudicates, never verdicts. - */ -export function scanDeclarations(reserved, files) { - const hits = []; - const alternation = reserved.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'); - const re = new RegExp(`\\b(name|position)\\s*:\\s*'(${alternation})'`, 'g'); - for (const rel of files) { - const src = read(rel); - if (!src) continue; - let m; - re.lastIndex = 0; - while ((m = re.exec(src)) !== null) { - hits.push({ file: rel, line: src.slice(0, m.index).split('\n').length, key: m[1], name: m[2] }); - } - } - return hits; -} - -/* ------------------------------------------------------------------------- * - * Deployment rows (`--rows`) - * ------------------------------------------------------------------------- */ - -const ROWS_SCHEMA = ` ---rows FILE expects JSON exported READ-ONLY from the deployment: - - { - "deployment": "", - "sys_position": [ { "id": …, "name": …, "managed_by": …, "organization_id": … }, … ], - "sys_user_position": [ { "id": …, "user_id": …, "position": …, "organization_id": … }, … ] - } - -Both arrays are REQUIRED (an absent one is refused, not read as empty — see -below). Export them with reads only, e.g. against the data API: - - GET /api/v1/data/sys_position?fields=id,name,managed_by,organization_id - GET /api/v1/data/sys_user_position?fields=id,user_id,position,organization_id - -⚠️ An EMPTY array is accepted and reported as zero FOR THAT TABLE; a MISSING -key is refused. The distinction is the whole point: "we exported it and there -were none" and "we never exported it" must not produce the same all-clear. -`.trim(); - -export function censusRows(reserved, payload) { - const problems = []; - for (const table of ['sys_position', 'sys_user_position']) { - if (!Array.isArray(payload?.[table])) problems.push(`'${table}' is missing or not an array`); - } - if (problems.length) { - const err = new Error(`--rows input is not a census: ${problems.join('; ')}`); - err.schema = ROWS_SCHEMA; - throw err; - } - const reservedSet = new Set(reserved); - - const positions = payload.sys_position - .filter((r) => reservedSet.has(String(r?.name ?? ''))) - .map((r) => ({ - id: r?.id ?? null, - name: String(r?.name), - managed_by: r?.managed_by ?? null, - organization_id: r?.organization_id ?? null, - // The platform's own catalog rows are not collisions — they ARE the - // built-in identity catalog, seeded per organization by design. - platformCatalog: PLATFORM_PROVENANCE.has(String(r?.managed_by ?? '')), - })); - - const assignments = payload.sys_user_position - .filter((r) => reservedSet.has(String(r?.position ?? ''))) - .map((r) => ({ - id: r?.id ?? null, - user_id: r?.user_id ?? null, - position: String(r?.position), - organization_id: r?.organization_id ?? null, - })); - - return { - deployment: payload.deployment ?? null, - scanned: { sys_position: payload.sys_position.length, sys_user_position: payload.sys_user_position.length }, - positionCollisions: positions.filter((p) => !p.platformCatalog), - platformCatalogRows: positions.filter((p) => p.platformCatalog), - assignmentCollisions: assignments, - }; -} - -/* ------------------------------------------------------------------------- * - * Self-test — the controls, so a zero can be trusted - * ------------------------------------------------------------------------- */ - -function selfTest() { - const failures = []; - const check = (label, fn) => { - try { fn(); } catch (e) { failures.push(`${label}: ${e.message}`); } - }; - const eq = (a, b, what) => { - const [x, y] = [JSON.stringify(a), JSON.stringify(b)]; - if (x !== y) throw new Error(`${what}: expected ${y}, got ${x}`); - }; - - // 1. The reserved set is found in the real declaration, and it is the four. - check('reserved set parses from the spec', () => { - const names = readReservedNames(); - eq(names.length, 4, 'reserved name count'); - for (const n of names) if (!/^[a-z][a-z0-9_]*$/.test(n)) throw new Error(`not a machine name: ${n}`); - }); - - // 2. THE CONTROL THAT MATTERS — a broken parse must THROW, never report zero. - check('a declaration with no array is refused, not read as empty', () => { - let threw = false; - try { readReservedNames("export const BUILTIN_IDENTITY_PLATFORM_ADMIN = 'platform_admin';"); } - catch { threw = true; } - if (!threw) throw new Error('a source with no BUILTIN_IDENTITY_NAMES array was accepted'); - }); - check('a name listed but never declared is refused', () => { - let threw = false; - try { readReservedNames('export const BUILTIN_IDENTITY_NAMES = [\n BUILTIN_IDENTITY_GHOST,\n] as const;'); } - catch { threw = true; } - if (!threw) throw new Error('an undeclared member was accepted'); - }); - - // 3. The declaration scanner FIRES on a positive control and stays silent on - // the near-misses the ruling refuses to widen to. - check('the declaration scanner fires, and does not widen by pattern', () => { - const reserved = ['platform_admin', 'org_admin']; - const tmp = path.join(REPO_ROOT, 'packages/spec/src/identity/eval-user.zod.ts'); - if (!fs.existsSync(tmp)) throw new Error('control corpus file is missing'); - const hits = scanDeclarations(reserved, [SPEC_DECL]); - // The spec file declares the metadata map keyed by the constants, not by - // literals, so this control is about the SCANNER, run over a synthetic - // corpus below rather than over that file's spelling. - const synthetic = scanSynthetic(reserved, [ - "const a = { name: 'platform_admin' };", - "const b = { position: 'org_admin' };", - "const c = { name: 'org_manager' };", // ⛔ must NOT match - "const d = { name: 'platform_admin_x' };", // ⛔ must NOT match - "const e = { label: 'platform_admin' };", // ⛔ wrong key - ]); - eq(synthetic.map((h) => h.name), ['platform_admin', 'org_admin'], 'synthetic scanner hits'); - if (!Array.isArray(hits)) throw new Error('scanner did not return an array'); - }); - - // 4. `--rows` separates the platform catalog from a real collision, and - // refuses an input that never exported a table. - check('rows census: catalog rows are not collisions, assignments always are', () => { - const out = censusRows(['platform_admin', 'org_admin'], { - sys_position: [ - { id: 'p1', name: 'platform_admin', managed_by: 'platform' }, - { id: 'p2', name: 'platform_admin', managed_by: 'system' }, - { id: 'p3', name: 'org_admin', managed_by: 'admin' }, - { id: 'p4', name: 'sales_manager', managed_by: 'admin' }, - ], - sys_user_position: [ - { id: 'a1', user_id: 'u1', position: 'platform_admin' }, - { id: 'a2', user_id: 'u2', position: 'sales_manager' }, - ], - }); - eq(out.positionCollisions.map((r) => r.id), ['p3'], 'position collisions'); - eq(out.platformCatalogRows.map((r) => r.id), ['p1', 'p2'], 'platform catalog rows'); - eq(out.assignmentCollisions.map((r) => r.id), ['a1'], 'assignment collisions'); - }); - check('rows census: a MISSING table is refused, an EMPTY one is zero', () => { - let threw = false; - try { censusRows(['platform_admin'], { sys_position: [] }); } catch { threw = true; } - if (!threw) throw new Error('an input missing sys_user_position was accepted'); - const out = censusRows(['platform_admin'], { sys_position: [], sys_user_position: [] }); - eq(out.assignmentCollisions.length, 0, 'empty export'); - }); - - if (failures.length) { - console.error('SELF-TEST FAILED'); - for (const f of failures) console.error(` - ${f}`); - process.exit(1); - } - console.log(`self-test OK — ${5} controls, all firing`); -} - -/** Scanner over in-memory lines, for the self-test's synthetic corpus. */ -function scanSynthetic(reserved, lines) { - const alternation = reserved.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'); - const re = new RegExp(`\\b(name|position)\\s*:\\s*'(${alternation})'`, 'g'); - const hits = []; - for (const line of lines) { - let m; - re.lastIndex = 0; - while ((m = re.exec(line)) !== null) hits.push({ key: m[1], name: m[2] }); - } - return hits; -} - -/* ------------------------------------------------------------------------- * - * CLI - * ------------------------------------------------------------------------- */ - -const ZERO_CAVEAT = - '⚠️ A zero here is a zero over DECLARATIONS in this repository, never over a deployment. ' - + 'Positions and assignments are runtime rows; use --rows with an export to census a live store.'; - -function main(argv) { - if (argv.includes('--self-test')) return selfTest(); - if (argv.includes('--rows-schema')) { console.log(ROWS_SCHEMA); return; } - - const json = argv.includes('--json'); - const reserved = readReservedNames(); - - const rowsAt = argv.indexOf('--rows'); - if (rowsAt !== -1) { - const file = argv[rowsAt + 1]; - if (!file) { console.error('--rows needs a FILE'); process.exit(2); } - let payload; - try { payload = JSON.parse(fs.readFileSync(file, 'utf8')); } - catch (e) { console.error(`--rows: cannot read ${file}: ${e.message}`); process.exit(2); } - let out; - try { out = censusRows(reserved, payload); } - catch (e) { console.error(e.message); if (e.schema) console.error(`\n${e.schema}`); process.exit(2); } - - if (json) { console.log(JSON.stringify({ mode: 'rows', reserved, ...out }, null, 2)); return; } - console.log(`# reserved identity-name census — deployment rows${out.deployment ? ` (${out.deployment})` : ''}`); - console.log(`reserved set: ${reserved.join(', ')}`); - console.log(`scanned: ${out.scanned.sys_position} sys_position, ${out.scanned.sys_user_position} sys_user_position`); - console.log(`\n## sys_position rows standing on a reserved name (excluding the platform catalog): ${out.positionCollisions.length}`); - for (const r of out.positionCollisions) { - console.log(` - ${r.id} name=${r.name} managed_by=${r.managed_by} organization=${r.organization_id}`); - } - console.log(`\n## sys_user_position rows spelling a reserved name — ALL are collisions: ${out.assignmentCollisions.length}`); - for (const r of out.assignmentCollisions) { - console.log(` - ${r.id} user=${r.user_id} position=${r.position} organization=${r.organization_id}`); - } - console.log(`\n(platform catalog rows seen and NOT counted: ${out.platformCatalogRows.length})`); - console.log('\n⛔ Nothing here is rewritten. Renaming or removing a listed row is a maintainer decision.'); - return; - } - - const files = [ - ...walk(path.join(REPO_ROOT, 'packages'), new Set(['.ts', '.tsx'])), - ...walk(path.join(REPO_ROOT, 'examples'), new Set(['.ts', '.tsx'])), - ...walk(path.join(REPO_ROOT, 'apps'), new Set(['.ts', '.tsx'])), - ].filter((f) => !f.includes('.test.') && !f.includes('.spec.')); - const hits = scanDeclarations(reserved, files); - - if (json) { console.log(JSON.stringify({ mode: 'declarations', reserved, scannedFiles: files.length, hits, caveat: ZERO_CAVEAT }, null, 2)); return; } - console.log('# reserved identity-name census — declarations in this repository'); - console.log(`reserved set: ${reserved.join(', ')} (read from ${SPEC_DECL})`); - console.log(`scanned: ${files.length} non-test source files`); - console.log(`\ncandidate declarations spelling a reserved name: ${hits.length}`); - for (const h of hits) console.log(` - ${h.file}:${h.line} ${h.key}: '${h.name}'`); - console.log(`\n${ZERO_CAVEAT}`); -} - -if (isEntrypoint(import.meta.url)) main(process.argv.slice(2)); diff --git a/scripts/pm/dispatch-gates.mjs b/scripts/pm/dispatch-gates.mjs index 1275350484d..500401e48a1 100644 --- a/scripts/pm/dispatch-gates.mjs +++ b/scripts/pm/dispatch-gates.mjs @@ -6007,7 +6007,8 @@ export function packageRootAnchoredHint(hint, base, tree, files) { * false: `node_modules`, `dist`, `coverage` and `.turbo` off * check-refd-timer-probe's skip set; `.changeset` twice — off * check-keyed-text-bounds' `SKIP_DIRS` and off the noise floor the card was - * filed on; the two gate paths measurement-claim-triage declares it skips; and + * filed on; the two gate paths measurement-claim-triage declared it skips (that + * tool is removed from the tree, held at 7111e14ac9); and * `scripts/**`, `docs/adr/**` and `.changeset/**` off `DEFERRED_SURFACES`, * which is what the `DEFERRED` arm retired. The changeset pair is what a dev * actually saw when #15753 was filed: a card that has not written its diff --git a/scripts/pm/measurement-claim-triage.mjs b/scripts/pm/measurement-claim-triage.mjs deleted file mode 100644 index 1feb2d44fb3..00000000000 --- a/scripts/pm/measurement-claim-triage.mjs +++ /dev/null @@ -1,651 +0,0 @@ -#!/usr/bin/env node -// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. - -/** - * Tree-measurement claims under `scripts/**` — the CLASSIFIER (#17797). - * - * node scripts/pm/measurement-claim-triage.mjs the population + every verdict - * node scripts/pm/measurement-claim-triage.mjs --review the kind-3 residue alone - * node scripts/pm/measurement-claim-triage.mjs --self-test the controls, both directions - * - * ## Why a tool and not a sentence - * - * The card that asked for this states its own population as a count. That count - * was taken once and then decayed — which is the very defect the card is about, - * reproduced by the card. ⛔ So no number describing this population is written - * down here. The scope line this tool prints IS the number, re-derived on the - * tree it is run against, and a reader who wants the population runs the tool. - * ⭐ The figures are deliberately absent from this docblock; ⛔ do not helpfully - * restore one. - * - * ## The three kinds, and why one phrase carries all of them - * - * A sentence that reports a measurement of this repository is one of three - * things, and they take DIFFERENT repairs: - * - * KIND 1 a citation of a past measurement. It is scoped to a moment — by a - * revision, a date, or a named event ("before this gate was written", - * "at the commit that took it") or a counterfactual condition ("with - * the reservation removed"). It reads as history, so it stays true - * forever. ⇒ NEEDS NOTHING. - * - * KIND 2 a claim whose failure direction is NAMED, and which something in - * the tree announces when it moves — a zero whose falsifier is - * spelled out, a figure a gate reprints, a figure a pin reds on. - * It does not decay SILENTLY, which is the whole harm. ⇒ NEEDS - * NOTHING. - * - * KIND 3 a bare magnitude in the present tense: no moment, no named - * direction, nothing that speaks when it moves. It reads as current - * state and goes false in silence. ⇒ THIS IS THE DEFECT. - * - * ⛔ The phrase alone separates none of them, which is why the population below - * is an UPPER BOUND on a population and NOT a count of defects. Treating the - * sweep's size as a finding count is the error this tool exists to prevent. - * - * ## What is mechanical here, and what is not - * - * Tests T0–T3 run over source text and are recall filters: each one can only - * move a hit OUT of the review residue, and each prints the cue that moved it, - * so a verdict is always auditable against the sentence. What no regex decides - * is T4 — whether a reader acts on the magnitude's LEVEL or on a relation the - * level's drift survives. T4 is a judgement, so it is recorded per site in - * `TRIAGE` below, with its reason, exactly as the bare-root worklist records - * its own. A review hit with no `TRIAGE` row prints as UNTRIAGED and reds the - * self-test: a new claim of this shape has to be judged by someone. - * - * ## Scope, and the file deliberately outside it - * - * `scripts/pm/dispatch-gates.mjs` is EXCLUDED, with its reason carried as data - * in `EXCLUDED` and printed on every run rather than left in prose. The residue - * it holds is therefore un-swept, and the scope line says so instead of reading - * as complete. - * - * ## ⚠️ This tool is not wired into CI - * - * Wiring a `--self-test` needs a `package.json` script and a workflow step, and - * both sit outside the file surface this card was dispatched with. Until that - * lands, `--self-test` is run by hand and its controls are not watching - * anything on their own. ⛔ Do not read a green run here as CI coverage. - */ - -import { readdirSync, readFileSync, statSync } from 'node:fs'; -import { join, relative, sep } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { isEntrypoint } from '../invoked-as.mjs'; - -const HERE = fileURLToPath(new URL('.', import.meta.url)); -const REPO_ROOT = join(HERE, '..', '..'); -const SCAN_ROOT = join(REPO_ROOT, 'scripts'); - -/** This file's own path, relative to the repo root, in the spelling verdicts use. */ -const SELF = 'scripts/pm/measurement-claim-triage.mjs'; - -/** - * FORM A — the phrase the card names, in every spelling it actually occurs in - * on this tree. ⚠️ The card's own probe was case-sensitive and lower-case only; - * on this tree that spelling reaches strictly fewer sites than the sentence - * does, and the gap is not a rounding error. The count either way is printed, - * never written down. - */ -const FORM_A = /(?:\bre-)?measured\s+(?:on|against)\s+this\s+tree/gi; - -/** - * FORM B — sites FORM A cannot reach, declared per file with the reason. - * - * ⭐ That this list is non-empty is the finding: a phrase probe is not a - * population. The card's own named instance carries no phrase at all — it is a - * bare corpus size in the present tense — so a sweep defined by the phrase - * would have reported the tree clean at the one site everybody agreed was - * broken. - */ -const FORM_B = [ - { - file: 'scripts/docs-audit/README.md', - // The hand-written docs corpus size. `pnpm check:docs-audit-scope` prints - // today's; every literal of the older figure left in this file is a claim - // about the corpus that the gate can contradict. - pattern: /\b178\b/g, - // `#11178` is a card number, not a corpus size. - reject: /#\d*178\b/, - why: 'the hand-written docs corpus size, which `check:docs-audit-scope` prints live', - }, -]; - -/** Excluded files, with the reason carried as data so every run prints it. */ -const EXCLUDED = [ - { - file: 'scripts/pm/dispatch-gates.mjs', - why: 'edited by open PRs #19162 and #19024, and its `--self-test` exceeds the agent ' - + 'container foreground cap (#17765), so a change here could not be verified from a seat', - }, -]; - -/* ─────────────────────────── the tests ─────────────────────────────────── */ - -/** - * T0 — is there a magnitude at all? Applied LAST, as the weakest escape hatch: - * a sentence that is already anchored or already loud is settled before the - * question is asked, so T0 only ever speaks about the residue. - * - * Nothing decays if nothing is counted. A ZERO is a magnitude — kind 2 is - * defined on one — so the word counts even where no digit does. Revisions, dates, card numbers, tool - * versions and exit codes are struck first: each is an identifier that happens - * to be spelled in digits, and counting them as magnitudes would classify every - * anchored citation as the thing an anchor exists to prevent. - */ -const strikeNonMagnitudes = (text) => text - .replace(/`[0-9a-f]{7,40}`/g, ' ') - .replace(/\b20\d{2}-\d{2}-\d{2}\b/g, ' ') - .replace(/#\d+/g, ' ') - .replace(/\bv?\d+\.\d+(?:\.\d+)?\b/g, ' ') - .replace(/\bexit \d+\b/gi, ' ') - .replace(/\bADR-\d+\b/g, ' ') - .replace(/\bbash \d\.\d\b/gi, ' '); - -const MAGNITUDE = /(? { - const flat = text.replace(/\s+/g, ' '); - for (const [re, name] of cues) if (re.test(flat)) return name; - return null; -}; - -/* ───────────────────── population derivation ───────────────────────────── */ - -const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'coverage', '.turbo']); - -function* walk(dir) { - for (const entry of readdirSync(dir).sort()) { - if (SKIP_DIRS.has(entry)) continue; - const full = join(dir, entry); - const st = statSync(full); - if (st.isDirectory()) yield* walk(full); - else if (st.isFile()) yield full; - } -} - -/** - * ## The window a verdict is read over, and why it is a SENTENCE - * - * The first build of this tool read a fixed ±6-line block. It cleared the one - * site everybody had already agreed was broken (`scripts/docs-audit/README.md`, - * the present-tense corpus size): six lines above it an UNRELATED sentence - * cites a revision, and a block window handed that anchor to a claim that has - * none. ⛔ An anchor belongs to the sentence that carries the figure and to no - * other, so T2 and T3 read the SENTENCE. - * - * T1 and T3 are the exceptions, and deliberately. A narrative frame is - * routinely set one sentence earlier — the dispatcher-vocabulary gate opens - * with "An earlier revision of this paragraph counted DIRECTORIES …" and only - * then reports the reading — so T1 also sees the sentence before. A named failure direction is routinely set one - * sentence LATER — the card's own kind-2 exemplar spells the zero in one - * sentence and names what would falsify it in the next — so T3 sees both - * neighbours. ⛔ T2 sees neither: an anchor belongs to its own sentence, and - * widening T2 is precisely the bug this window replaced. - * - * ⚠️ Neither test sees further than one sentence, and a frame or a direction - * set further back than that lands in the review residue — the safe direction, - * where a person judges it and records the judgement in `TRIAGE`. - */ -const SENTENCE_BREAK = /(?<=[.!?])[ \n]+(?=[A-Z⛔⭐⚠️`*#·•])/; - -/** Comment furniture, so a sentence reads as prose rather than as a comment. */ -function deComment(line, isMarkdown) { - let out = line.replace(/^\s*(?:\/\/+|\*)\s?/, ''); - if (!isMarkdown) out = out.replace(/^\s*#+\s?/, ''); - return out; -} - -/** - * Split a file into sentences, each carrying the 1-based line it starts on. - * The line map is built from the ORIGINAL line count, so a reported line is a - * line in the file a reader will open and not an offset into a derived string. - */ -function sentencesOf(text, isMarkdown) { - const lines = text.split('\n'); - const prose = lines.map((l) => deComment(l, isMarkdown)); - const out = []; - let buffer = []; - let bufferStart = 1; - const flush = () => { - if (!buffer.length) return; - const joined = buffer.join('\n'); - let offset = 0; - for (const piece of joined.split(SENTENCE_BREAK)) { - const before = joined.slice(0, offset); - out.push({ text: piece, line: bufferStart + (before.match(/\n/g) ?? []).length }); - offset += piece.length + 1; - } - buffer = []; - }; - prose.forEach((line, i) => { - if (line.trim() === '') { - // ⛔ A blank comment line does NOT end a sentence that was introducing a - // data block. A docblock here routinely ends its lead-in on a colon and - // then sets the table off with an empty ` *` line; flushing at that blank - // strands every figure in a fragment carrying no cue, and the sweep reads - // a docblock full of magnitudes as carrying none. - if (buffer.length && /:\s*$/.test(buffer[buffer.length - 1])) return; - flush(); - return; - } - if (!buffer.length) bufferStart = i + 1; - buffer.push(line); - }); - flush(); - return out; -} - -function hitsIn(relPath, text) { - const isMarkdown = relPath.endsWith('.md'); - const sentences = sentencesOf(text, isMarkdown); - const found = []; - sentences.forEach((s, i) => { - const lead = i > 0 ? sentences[i - 1].text : ''; - const next = i + 1 < sentences.length ? sentences[i + 1].text : ''; - FORM_A.lastIndex = 0; - if (FORM_A.test(s.text)) { - found.push({ file: relPath, line: s.line, form: 'A', sentence: s.text, lead, next }); - } - for (const form of FORM_B) { - if (form.file !== relPath) continue; - form.pattern.lastIndex = 0; - if (!form.pattern.test(s.text)) continue; - if (form.reject && form.reject.test(s.text)) continue; - found.push({ file: relPath, line: s.line, form: 'B', sentence: s.text, lead, next, why: form.why }); - } - }); - return found; -} - -export function collectPopulation() { - const excluded = new Set(EXCLUDED.map((e) => e.file)); - const hits = []; - for (const full of walk(SCAN_ROOT)) { - const rel = relative(REPO_ROOT, full).split(sep).join('/'); - if (excluded.has(rel) || rel === SELF) continue; - let text; - try { text = readFileSync(full, 'utf8'); } catch { continue; } - hits.push(...hitsIn(rel, text)); - } - return hits; -} - -/** - * The classifier proper: T1–T3 then T0, over one sentence (plus the sentence - * before it, for T1 only). T4 — level or relation — is a judgement and lives in - * `TRIAGE`. - */ -export function classify(sentence, lead = '', next = '') { - const frame = firstCue(FRAME_CUES, `${lead}\n${sentence}`); - if (frame) return { kind: 'KIND-1', cue: frame }; - const anchor = firstCue(ANCHOR_CUES, sentence); - if (anchor) return { kind: 'KIND-1', cue: anchor }; - const loud = firstCue(LOUD_CUES, `${lead}\n${sentence}\n${next}`); - if (loud) return { kind: 'KIND-2', cue: loud }; - if (!MAGNITUDE.test(strikeNonMagnitudes(sentence.replace(/\s+/g, ' ')))) { - return { kind: 'NO-FIGURE', cue: 'no magnitude survives T0' }; - } - return { kind: 'REVIEW', cue: 'present-tense magnitude, no frame, no anchor, nothing loud' }; -} - -/* ─────────────────────────── T4, recorded ───────────────────────────────── */ - -/** - * T4 — LEVEL or RELATION, the half no regex decides. - * - * With T1–T3 all declining, one question is left: does a reader act on the - * magnitude's **level**, or on a **relation** (a ratio, an ordering, an - * existence claim, a set equality) that the level's drift survives? A level is - * kind 3. A relation is an ILLUSTRATION — the figure sizes an argument that - * stays correct when the figure moves, and rewriting it buys nothing while - * costing a reviewer a diff. - * - * ⛔ Every row is keyed by an EXCERPT and never by a line number. The card that - * asked for this cited its own known instance by line, and the line had moved - * before anyone read the card — a ledger keyed on line numbers decays exactly - * the way its subject does. - * - * `verdict`: - * ILLUSTRATION — judged a relation; stays as written. - * REPAIRED — judged kind 3 and repaired in #17797. The self-test holds - * the excerpt ABSENT: if it comes back, so has the defect. - */ -const TRIAGE = [ - { file: 'scripts/check-bash32-floor.mjs', match: '8 findings, every one of them a false positive', - verdict: 'ILLUSTRATION', why: 'a counterfactual reading of the rule this gate replaced — the SIGN (all false positives) carries it, and the rule it measured is gone from this tree' }, - { file: 'scripts/check-cross-package-test-inputs.mjs', match: '6 exist only inside comments', - verdict: 'ILLUSTRATION', why: 'the load-bearing claim is the zero that follows it — none of the six escapes its package — and the corpus sizes only scale it' }, - { file: 'scripts/check-cross-package-test-inputs.mjs', match: '15 extensionless', - verdict: 'ILLUSTRATION', why: 'three spellings EXIST, which is why three branches exist; no branch is chosen by how many files carry it' }, - { file: 'scripts/check-cross-package-test-inputs.mjs', match: 'test files importing a workspace sibling by BARE specifier', - verdict: 'ILLUSTRATION', why: 'the ORDERING decides the design (bare specifiers dwarf relative ones), and no plausible drift reverses it' }, - { file: 'scripts/check-cross-package-test-inputs.mjs', match: '6 of the 60 declared globs are held by exactly such reads', - verdict: 'ILLUSTRATION', why: 'a non-emptiness claim — the blind spot is real — with a named specimen; nobody acts on how many' }, - { file: 'scripts/check-dispatcher-error-vocabulary.mjs', match: 'the naive `[{,]` anchor alone reports 19', - verdict: 'ILLUSTRATION', why: 'two recognisers compared at one moment; 19-against-13 is a relation, and the six extras are named individually' }, - { file: 'scripts/check-driver-conformance.mjs', match: "105 files under `driver-sql/src` carry a literal", - verdict: 'ILLUSTRATION', why: 'the claim is that those files are NOT defects — a class judgement; the count only says the class is large' }, - { file: 'scripts/check-examples-live-imports.mjs', match: '76 coupled files across 3 packages', - verdict: 'ILLUSTRATION', why: 'the refusal rests on the SHARE (a tiny coupled set inside a huge root), which no ordinary churn moves' }, - { file: 'scripts/check-logger-receiver-detach.mjs', match: 'over the property reads whose NAME is a log channel', - verdict: 'ILLUSTRATION', why: 'the section head one paragraph up names `--census` as reproducing every number in it — instrument-backed, one sentence further than T3 reaches' }, - { file: 'scripts/check-parse-guard.mjs', match: '9 of the 28 sites live in', - verdict: 'ILLUSTRATION', why: 'the finding is that the printed remedy is false for a subset that EXISTS; the subset is then described by kind, not by size' }, - { file: 'scripts/check-pnpm-filter-targets.mjs', match: 'Measured on this tree, the declaration names 235 tracked files', - verdict: 'REPAIRED', why: 'kind 3 — a declaration-honesty ratio over a root that grows with every script added, read as current state, reprinted by nothing' }, - { file: 'scripts/check-published-files.mjs', match: 'Measured on this tree: the declaration names 5263 tracked files', - verdict: 'REPAIRED', why: 'kind 3 — same shape over the whole publishable workspace, which moves on every package add or removal' }, - { file: 'scripts/check-release-section-coverage.mjs', match: 'assertion 1 fires on 24 minors', - verdict: 'ILLUSTRATION', why: 'the figures justify shipping WITH a baseline, and the ruling that fixes them is dated in the sentence before' }, - { file: 'scripts/check-skill-compatibility-version.mjs', match: 'both were measured on this tree', - verdict: 'REPAIRED', why: 'kind 3 — four precision ratios deciding which roots are declared, all moving with the tree and none reprinted' }, - { file: 'scripts/check-slot-lookup-ratchet.mjs', match: 'Measured on this tree: 46s', - verdict: 'REPAIRED', why: 'kind 3, and the worst-ageing kind: a duration is a reading of one BOX as much as of one tree, so it decays without the tree changing at all' }, - { file: 'scripts/check-test-source-alias.mjs', match: '1832 of the 4844 tracked files', - verdict: 'ILLUSTRATION', why: 'it records the state the fix REMOVED — the derivation no longer decides this gate by where a package sits — in a past-tense narrative T1 does not reach' }, - { file: 'scripts/check-where-matcher-conformance.mjs', match: '2889 of the 2889 files', - verdict: 'ILLUSTRATION', why: 'a set EQUALITY held in both directions by this gate own liveness-and-precision pin: it reds on drift rather than decaying' }, - { file: 'scripts/check-widget-option-census.mjs', match: 'inheritable literals: 0', - verdict: 'ILLUSTRATION', why: 'a table of zeros making an inheritance claim; the zeros are the claim' }, - { file: 'scripts/docs-audit/README.md', match: 'matched 82, 113, 43, 13 and 10 of 178 pages', - verdict: 'ILLUSTRATION', why: 'past-tense narrative of the first build guards — the words arrived as real declarations and matched; it describes a tree that is gone' }, - { file: 'scripts/docs-audit/README.md', match: 'named by 59 of 178 pages', - verdict: 'ILLUSTRATION', why: 'the argument is the 15% corpus SHARE rule stated two lines up; this is the share that condemned one term when the rule was written' }, - { file: 'scripts/docs-audit/README.md', match: 'reads the same 178-page corpus', - verdict: 'REPAIRED', why: 'kind 3, and the instance the card names: a present-tense corpus size that `check:docs-audit-scope` contradicts today' }, - { file: 'scripts/eslint-stack-headroom.mjs', match: 'the narrow scope is the worst case', - verdict: 'ILLUSTRATION', why: 'worst-case against intermittent — a comparison between two arms, and the gate is built on the comparison, not on either rate' }, - { file: 'scripts/git-merge-regen.mjs', match: 'measured on this tree: 18 rows', - verdict: 'ILLUSTRATION', why: 'the claim is that reverting #13585 loosening would turn the reconciliation RED; the row split only shows both branches are populated' }, - { file: 'scripts/pm/bare-root-worklist.mjs', match: 'reaches 2755 of ', - verdict: 'ILLUSTRATION', why: 'a set equality this file own --self-test re-derives and reds on, which is the whole point of a worklist that runs' }, - { file: 'scripts/pm/bare-root-worklist.mjs', match: 'numerator from the instrument own printed scan ', - verdict: 'ILLUSTRATION', why: 'the sentence names both instruments the two terms came from and refuses to carry either from the row above' }, - { file: 'scripts/pm/bare-root-worklist.mjs', match: 'the ratio does not change the verdict either direction', - verdict: 'ILLUSTRATION', why: 'the sentence says in its own words that the level is not acted on — a relation by declaration' }, -]; - -/* ──────────────────────────── the controls ─────────────────────────────── */ - -/** - * ⭐ BOTH DIRECTIONS, and neither one alone. A classifier only ever seen to - * fire proves nothing; one only ever seen to stay quiet proves less. - * - * Each control carries a `liveness` substring that must still be present in the - * file it was taken from. ⛔ A control quoting a sentence the tree no longer - * holds is a control that has stopped controlling anything, and it would pass - * in silence — the exact failure this whole file is about. - */ -const CONTROLS = [ - { - name: 'kind 1 — a citation carrying its revision, left alone', - file: 'scripts/check-tier-file-adoption.mjs', - liveness: 'Measured on this tree at d03c3c96d6', - sentence: 'Measured on this tree at d03c3c96d6, on the ONE package that owns tier files: ' - + '`packages/cli/vitest.config.ts` names `OS_TEST_TIERS` 5 times and `packages/cli/vitest-tiers.ts` 4 times.', - lead: '', next: '', - expect: 'KIND-1', - }, - { - name: 'kind 2 — a zero whose failure direction is named, left alone', - file: 'scripts/docs-audit/affected-docs.mjs', - liveness: 'zero commands declare `static topic`', - sentence: 'Measured on this tree: zero commands declare `static topic`, and the one `static id` ' - + "(`init.ts` -> `'init'`) agrees with its path, so nothing is mis-derived today.", - lead: '', - next: 'The failure direction if that ever changes is a phrase matching NO page — a recall miss the ' - + 'anchor list makes visible, not a false positive that pollutes the work list.', - expect: 'KIND-2', - }, - { - name: 'kind 3 — the card own named instance, CAUGHT (pre-repair text)', - file: 'scripts/docs-audit/README.md', - // ⛔ No liveness key: this is the sentence as it read BEFORE the repair, so - // the tree is required NOT to hold it. `absent` is checked instead. - absent: 'reads the same 178-page corpus', - sentence: '**Cost** (the card open question): the anchor derivation reads the same 178-page corpus ' - + 'the old one did, plus the 18 route-source/ledger files (~875 KB) and one `git show` per changed file per side.', - lead: 'How often it renders, re-derived over the 40 first-parent commits ending at `e43b18fd9`: ' - + '3 of the 17 package-touching runs (18%).', - next: 'Measured end-to-end on the ten PRs above, `node affected-docs.mjs` went from 85-195 ms to 114-582 ms.', - expect: 'REVIEW', - }, - { - name: 'kind 3 repaired — the SAME site now points at the gate that prints the value', - file: 'scripts/docs-audit/README.md', - liveness: 'check:docs-audit-scope', - sentence: '**Cost** (the card open question): the anchor derivation reads the same hand-written corpus ' - + 'the old one did — run `check-audit-scope.mjs` for today\'s page count rather than trusting one written ' - + 'down here — plus the 18 route-source/ledger files and one `git show` per changed file per side.', - lead: '', next: '', - expect: 'KIND-2', - }, - { - name: 'kind 2 — a refusal a live assertion holds, left alone', - file: 'scripts/check-skill-compatibility-version.mjs', - liveness: 'the refusal is pinned in the self-test', - sentence: 'So the manifest side stays undeclared, deliberately, and the refusal is pinned in the ' - + 'self-test rather than left in this paragraph — a later author who adds `packages/**` meets an assertion.', - lead: '', next: '', - expect: 'KIND-2', - }, - { - name: '⛔ the one-sentence anchor rule — a neighbour revision does NOT clear a bare magnitude', - file: 'scripts/pm/measurement-claim-triage.mjs', - sentence: 'The declaration names 235 tracked files under scripts/ and this gate reads 228 of them.', - lead: 'Re-derived over the 40 first-parent commits ending at `e43b18fd9`.', - next: '', - expect: 'REVIEW', - }, -]; - -/* ──────────────────────────────── report ───────────────────────────────── */ - -const KINDS = ['KIND-1', 'KIND-2', 'NO-FIGURE', 'REVIEW']; - -export function triageOf(hit) { - const flat = hit.sentence.replace(/\s+/g, ' '); - return TRIAGE.find((row) => row.file === hit.file && row.verdict !== 'REPAIRED' && flat.includes(row.match)) - ?? null; -} - -function verdicts() { - return collectPopulation().map((hit) => ({ hit, ...classify(hit.sentence, hit.lead, hit.next) })); -} - -export function report({ reviewOnly = false } = {}) { - const rows = verdicts(); - const counts = Object.fromEntries(KINDS.map((k) => [k, rows.filter((r) => r.kind === k).length])); - - console.log( - `measurement-claim triage: ${rows.length} claim(s) over ` - + `${new Set(rows.map((r) => r.hit.file)).size} file(s) under scripts/ — ` - + KINDS.map((k) => `${k} ${counts[k]}`).join(' · '), - ); - console.log( - ' ⛔ REVIEW is a CANDIDATE bucket, not a defect count — T4 (level or relation) is a judgement,\n' - + ' recorded per site in TRIAGE. ⛔ Reading any number above as a finding count is the error\n' - + ' this tool exists to prevent.', - ); - for (const e of EXCLUDED) console.log(` ⚠️ NOT SWEPT ${e.file} — ${e.why}`); - console.log(''); - - for (const row of rows) { - if (reviewOnly && row.kind !== 'REVIEW') continue; - const t = row.kind === 'REVIEW' ? triageOf(row.hit) : null; - const tag = row.kind === 'REVIEW' ? (t ? `T4 ${t.verdict}` : 'T4 UNTRIAGED') : row.kind; - console.log(` ${tag.padEnd(18)} ${row.hit.file}:${row.hit.line} [${row.cue}]`); - if (t) console.log(` ${' '.repeat(18)} ↳ ${t.why}`); - } - return rows; -} - -/* ─────────────────────────────── self-test ─────────────────────────────── */ - -const SELF_TEST_VERDICT = 'measurement-claim-triage self-test: controls hold in BOTH directions'; - -export function selfTest() { - const problems = []; - const rows = verdicts(); - - // #4690: an empty population refuses rather than printing a confident zero. - if (rows.length === 0) problems.push('PREREQUISITE NOT MET: the sweep found no claims at all under scripts/.'); - - for (const control of CONTROLS) { - const got = classify(control.sentence, control.lead, control.next).kind; - if (got !== control.expect) problems.push(`CONTROL "${control.name}": expected ${control.expect}, got ${got}.`); - let text = ''; - try { text = readFileSync(join(REPO_ROOT, control.file), 'utf8'); } catch { - problems.push(`CONTROL "${control.name}": ${control.file} is gone, so the control quotes nothing.`); - continue; - } - if (control.liveness && !text.includes(control.liveness)) { - problems.push( - `CONTROL "${control.name}": ${control.file} no longer carries \`${control.liveness}\`. ` - + 'A control quoting a sentence the tree has lost passes in silence — re-point it at a live one.', - ); - } - if (control.absent && text.includes(control.absent)) { - problems.push( - `CONTROL "${control.name}": ${control.file} carries \`${control.absent}\` again. ` - + 'That is the repaired kind-3 sentence restored — the defect is back.', - ); - } - } - - // Every REVIEW hit is judged by somebody. - for (const row of rows) { - if (row.kind !== 'REVIEW') continue; - if (!triageOf(row.hit)) { - problems.push( - `UNTRIAGED ${row.hit.file}:${row.hit.line} — a present-tense magnitude with no frame, no anchor ` - + 'and nothing loud, and no TRIAGE row judging it. Judge it (level or relation) and record why.', - ); - } - } - - // A TRIAGE row that matches nothing is a judgement about a tree that is gone. - for (const row of TRIAGE) { - const live = rows.some((r) => r.hit.file === row.file && r.hit.sentence.replace(/\s+/g, ' ').includes(row.match)); - if (row.verdict === 'ILLUSTRATION' && !live) { - problems.push(`STALE TRIAGE row ${row.file} "${row.match}" matches nothing in the population any more.`); - } - if (row.verdict === 'REPAIRED' && live) { - problems.push(`REGRESSED ${row.file} "${row.match}" is back in the population — the repair was undone.`); - } - } - - // ⛔ Every cue must still match something, or it is decoration. - for (const [cues, label] of [[FRAME_CUES, 'FRAME'], [ANCHOR_CUES, 'ANCHOR'], [LOUD_CUES, 'LOUD']]) { - for (const [re, name] of cues) { - const corpus = rows.map((r) => `${r.hit.lead}\n${r.hit.sentence}\n${r.hit.next}`.replace(/\s+/g, ' ')); - const controls = CONTROLS.map((c) => `${c.lead}\n${c.sentence}\n${c.next}`.replace(/\s+/g, ' ')); - if (![...corpus, ...controls].some((t) => re.test(t))) { - problems.push(`DEAD CUE ${label}/${name} ${re} matches nothing in the population or the controls.`); - } - } - } - - // This file's own prose must make no claim the sweep would have to judge. - const own = readFileSync(join(REPO_ROOT, SELF), 'utf8'); - const prose = own.slice(0, own.indexOf('const TRIAGE')); - FORM_A.lastIndex = 0; - if (FORM_A.test(prose)) { - problems.push('This file own prose now carries the swept phrase outside CONTROLS, and it excludes itself from the sweep.'); - } - - if (problems.length) { - console.error(`\n✗ measurement-claim-triage self-test: ${problems.length} problem(s)\n`); - for (const p of problems) console.error(` - ${p}`); - console.error(''); - process.exit(1); - } - console.log(`✓ ${SELF_TEST_VERDICT} (${CONTROLS.length} controls, ${TRIAGE.length} recorded judgements).`); - return SELF_TEST_VERDICT; -} - -if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) { - if (selfTest() !== SELF_TEST_VERDICT) { - console.error('\n✗ measurement-claim-triage: selfTest() returned without reaching its verdict.\n'); - process.exit(1); - } - } else report({ reviewOnly: process.argv.includes('--review') }); -}