From b3f22782e12704ebdea17b2a96988f47255dbdde Mon Sep 17 00:00:00 2001 From: "developerz-ai[bot]" <286432749+developerz-ai[bot]@users.noreply.github.com> Date: Sat, 24 Oct 2026 16:24:40 +0000 Subject: [PATCH 1/2] feat(verify): enforce source headers and a dependency allowlist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two contracts the repo stated but nothing enforced now fail the gate. - `scripts/source-headers.ts` — every shipped source file carries a 1–4 line header comment stating its single responsibility. Existing violations are held by a per-file pin table that may only fall. - `scripts/dependency-allowlist.ts` — every `dependencies` / `devDependencies` entry is either an `@ultimat3/*` workspace edge or a name in the checked-in allowlist. A new dependency is a new row, and a new row is a review. Both reach `x verify` through their own `scripts/.test.ts`, the way `node-imports` and `fix-prose` do. Dz-Task-Id: tsk_35060e816384db46d4d2d836b97ee6c2 --- docs/architecture/guards.md | 2 + framework.manifest.json | 37 +- package.json | 2 + .../src/templates/emitted-contract.test.ts | 16 +- scripts/corpus-unscanned.test.ts | 2 + scripts/dependency-allowlist.test.ts | 137 +++++++ scripts/dependency-allowlist.ts | 198 ++++++++++ scripts/lib/dependency-allowlist-data.ts | 59 +++ scripts/lib/source-header-pins.ts | 358 ++++++++++++++++++ scripts/source-headers.test.ts | 58 +++ scripts/source-headers.ts | 164 ++++++++ scripts/tmp-probe-headers.ts | 18 + wiki/Error-Codes.md | 9 +- 13 files changed, 1057 insertions(+), 3 deletions(-) create mode 100644 scripts/dependency-allowlist.test.ts create mode 100644 scripts/dependency-allowlist.ts create mode 100644 scripts/lib/dependency-allowlist-data.ts create mode 100644 scripts/lib/source-header-pins.ts create mode 100644 scripts/source-headers.test.ts create mode 100644 scripts/source-headers.ts create mode 100644 scripts/tmp-probe-headers.ts diff --git a/docs/architecture/guards.md b/docs/architecture/guards.md index 836d72ccd..15785f647 100644 --- a/docs/architecture/guards.md +++ b/docs/architecture/guards.md @@ -26,6 +26,7 @@ hand-edited; `--check` refuses drift (`X_GUARDS_DOC_DRIFT`). A guard is a runnab | `bun run dead-docs-host` | Enforce, as a ratchet, that no shipped source builds a URL on `ultimate.dev`. | `X_DEAD_DOCS_HOST` `X_DEAD_DOCS_HOST_PIN_STALE` `X_DEAD_DOCS_HOST_UNSCANNED` | | `bun run declaration-readers-callers` | Enforce that every `@ultimat3/core` export that THROWS has a non-test caller — in shipped framework source or in a tracked app. | `X_CORE_THROWERS_UNSCANNED` `X_CORE_THROWER_PIN_STALE` `X_CORE_THROWER_UNCALLED` | | `bun run declaration-readers` | Enforce that every leaf key of every DECLARATION an app writes — the type each primitive and each `define*` factory takes — is read by something in `packages/*/src`. | `X_DECLARATION_READERS_UNSCANNED` `X_DECLARATION_READER_PIN_STALE` `X_DECLARED_KEY_UNREAD` | +| `bun run dependency-allowlist` | Enforce that every external workspace dependency is an explicit row in the checked-in allowlist. | `X_DEPENDENCY_ALLOWLIST_STALE` `X_DEPENDENCY_NOT_ALLOWLISTED` `X_DEPENDENCY_UNSCANNED` | | `bun run doc-commands` | Enforce, as a gate step, that a command a doc page hands a reader is a command this build ships. | `X_DOC_COMMAND_ALLOWANCE_STALE` `X_DOC_COMMAND_PIN_EXCEEDED` `X_DOC_COMMAND_PIN_STALE` `X_DOC_COMMAND_UNKNOWN` `X_DOC_COMMAND_UNSCANNED` `X_GUARD_LOAD_FAILED` | | `bun run scripts/doc-config-keys.ts` | Enforce, as a gate step, that documentation citing an `app.config.ts` key names a key that EXISTS. | `X_DOC_CONFIG_ALLOWANCE_STALE` `X_DOC_CONFIG_KEY_UNKNOWN` | | `bun run scripts/doc-fixes.ts` | Enforce, as a gate step, that the `Fix` column of `wiki/Error-Codes.md` is runnable — the same rule `x verify`'s `errors` step already applies to every `fix:` in shipped source. | `X_DOC_CAUSE_ECHOES_TITLE` `X_DOC_FIX_UNRUNNABLE` `X_DOC_FIX_UNSCANNED` `X_NEW_ERROR_CODE_EXISTS` `X_NEW_ERROR_CODE_INVALID` `X_NEW_ERROR_CODE_PATTERN_UNKNOWN` | @@ -84,6 +85,7 @@ hand-edited; `--check` refuses drift (`X_GUARDS_DOC_DRIFT`). A guard is a runnab | `bun run scripts/setup-commands.ts` | Enforce, as a gate step, that every page enumerating what a scaffolded app's `bun run setup` runs enumerates the script `x new` actually writes. | `X_DOC_SETUP_COMMANDS_UNSCANNED` `X_DOC_SETUP_COMMAND_STALE` | | `bun run side-effects` | Two rules, and both are about what a bundler that HONOURS the array does with it. | `X_CLI_BAD_FLAG` `X_SIDE_EFFECTS_ANCHOR_STALE` `X_SIDE_EFFECTS_BY_USE_STALE` `X_SIDE_EFFECTS_ENTRY_STALE` `X_SIDE_EFFECTS_MISSING` `X_SIDE_EFFECTS_PIN_STALE` `X_SIDE_EFFECTS_UNANCHORED` `X_SIDE_EFFECTS_UNDECLARED` `X_SIDE_EFFECTS_UNSCANNED` | | `bun run skip-if-cleanup` | Refuse a test file that resets a PROCESS-GLOBAL registry from inside a skippable block — a `.skipIf(` or a `describe.skip` alias — where the reset never runs in the configuration the block is skipped in. | `X_SKIP_CLEANUP_UNREACHED` `X_SKIP_CLEANUP_UNSCANNED` | +| `bun run source-headers` | Enforce that every shipped source file opens with a 1–4 line header stating its one responsibility. | `X_SOURCE_HEADER_MISSING` `X_SOURCE_HEADER_OUT_OF_CONTRACT` `X_SOURCE_HEADER_PIN_STALE` `X_SOURCE_HEADER_UNSCANNED` | | `bun run spawn-timeout` | Enforce, as a ratchet, that a child process this tree waits on carries a deadline: a `timeout` (or an abort `signal`) in its options. | `X_CORPUS_UNSCANNED` `X_SPAWN_TIMEOUT_PIN_STALE` `X_SPAWN_TIMEOUT_PIN_UNEXPLAINED` `X_SPAWN_UNTIMED` | | `bun run sql-export-readers` | Enforce that every `SQL_*` statement a package's public entry exports has a reader outside it. | `X_SQL_EXPORT_UNREAD` `X_SQL_EXPORT_UNSCANNED` | | `bun run sql-literal-copies` | Enforce that ONE module in this tree turns a value into a SQL string literal. | `X_SQL_LITERAL_COPY` `X_SQL_LITERAL_UNSCANNED` | diff --git a/framework.manifest.json b/framework.manifest.json index ba081a51d..791f98cec 100644 --- a/framework.manifest.json +++ b/framework.manifest.json @@ -1,6 +1,6 @@ { "version": 1, - "buildId": "dba97173dbb32de7a1f5817cb16c4bd60f4147adc8caddb4e9b406b0c8248122", + "buildId": "a0a1ac6e9ef56b7c6886a501e270e5174f67c49551bc39d1911a26e14fd40036", "tiers": { "0": [ "core", @@ -1204,6 +1204,21 @@ "owner": "scripts", "at": "scripts/declaration-readers.ts" }, + { + "code": "X_DEPENDENCY_ALLOWLIST_STALE", + "owner": "scripts", + "at": "scripts/dependency-allowlist.ts" + }, + { + "code": "X_DEPENDENCY_NOT_ALLOWLISTED", + "owner": "scripts", + "at": "scripts/dependency-allowlist.ts" + }, + { + "code": "X_DEPENDENCY_UNSCANNED", + "owner": "scripts", + "at": "scripts/dependency-allowlist.ts" + }, { "code": "X_DEPLOY_FAILED", "owner": "cli", @@ -4014,6 +4029,26 @@ "owner": "realtime", "at": "packages/realtime/src/error-titles.ts" }, + { + "code": "X_SOURCE_HEADER_MISSING", + "owner": "scripts", + "at": "scripts/source-headers.ts" + }, + { + "code": "X_SOURCE_HEADER_OUT_OF_CONTRACT", + "owner": "scripts", + "at": "scripts/source-headers.ts" + }, + { + "code": "X_SOURCE_HEADER_PIN_STALE", + "owner": "scripts", + "at": "scripts/source-headers.ts" + }, + { + "code": "X_SOURCE_HEADER_UNSCANNED", + "owner": "scripts", + "at": "scripts/source-headers.ts" + }, { "code": "X_SPAWN_TIMED_OUT", "owner": "scripts", diff --git a/package.json b/package.json index 1e48ddbbc..84f1ef916 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "declaration-readers": "bun --preload ./scripts/lib/guard-preload.ts scripts/declaration-readers.ts", "doc-commands": "bun --preload ./scripts/lib/guard-preload.ts scripts/doc-commands.ts", "doc-paths": "bun run scripts/doc-paths.ts", + "dependency-allowlist": "bun run scripts/dependency-allowlist.ts", "flight-copies": "bun run scripts/flight-copies.ts", "finite-bounds": "bun run scripts/finite-bounds.ts", "dev": "bun run --filter @ultimat3/cli dev", @@ -70,6 +71,7 @@ "side-effects": "bun run scripts/side-effects.ts", "skip-if-cleanup": "bun run scripts/skip-if-cleanup.ts", "test-discovery": "bun run scripts/test-discovery.ts", + "source-headers": "bun run scripts/source-headers.ts", "spawn-timeout": "bun run scripts/spawn-timeout.ts", "sql-literal-copies": "bun run scripts/sql-literal-copies.ts", "test": "bun test --isolate --path-ignore-patterns='**/dist/**' --path-ignore-patterns='**/examples/**' --path-ignore-patterns='**/dummy/**'", diff --git a/packages/cli/src/templates/emitted-contract.test.ts b/packages/cli/src/templates/emitted-contract.test.ts index abe36c206..d4e7c0fc6 100644 --- a/packages/cli/src/templates/emitted-contract.test.ts +++ b/packages/cli/src/templates/emitted-contract.test.ts @@ -86,6 +86,16 @@ describe('unit · every emitted file survives the linter the scaffold configures sandboxes.push(root); try { for (const file of files) await Bun.write(join(dir, file.path), file.contents); + // A config of the sandbox's OWN, whenever the files under lint carry none. Biome resolves a + // config by walking UP from the directory it runs in, so a sandbox with no `biome.json` of + // its own reads the one above it — and the temp root is shared with everything else on the + // machine. A stray `/tmp/biome.json` left by another run is not a config Biome can parse, + // so the lint failed on the AMBIENT file rather than on the emitted one, and the answer + // depended on what else had been in the temp root that day. The scaffold carries its own + // config, so this only ever fires for the probe cases below. + if (!files.some((file) => file.path === 'biome.json')) { + await Bun.write(join(dir, 'biome.json'), '{}\n'); + } const biome = join(REPO_ROOT, 'node_modules', '.bin', 'biome'); // AWAITED, through the CLI's one subprocess boundary — never `Bun.spawnSync`. A synchronous // wait holds the test worker's only thread, so a Biome that does not come back is a worker @@ -93,7 +103,11 @@ describe('unit · every emitted file survives the linter the scaffold configures // eight minutes (run 36992691094, `worker crashed: SIGKILL` under this file's header, after // `killed 1 dangling process` and no result line). Awaited, a slow or stuck Biome is one // test failing at its own timeout while the worker moves on to the next. - const run = await exec([biome, 'check', '.'], { cwd: dir }); + // `--config-path` names the config to use and DISABLES the upward walk, so the run reads + // the sandbox's own config and nothing above it: the sandbox is the whole world the lint + // answers about. Without it a sandbox that carries no config still inherits one from the + // temp root, and the verdict stops being a fact about the emitted files. + const run = await exec([biome, 'check', '--config-path=.', '.'], { cwd: dir }); return run.ok ? '' : `${run.stdout}${run.stderr}`; } finally { // The findings are already a string. Left behind, each run kept a whole scaffold under the diff --git a/scripts/corpus-unscanned.test.ts b/scripts/corpus-unscanned.test.ts index 91a5cbee1..f6663ff2f 100644 --- a/scripts/corpus-unscanned.test.ts +++ b/scripts/corpus-unscanned.test.ts @@ -31,6 +31,7 @@ import { probeDatabaseScan } from './probe-databases'; import { protoIndexFindings } from './proto-index'; import { vocabularyFindings } from './render-modes'; import { secretCompareFindings } from './secret-compare'; +import { sourceHeaderFindings } from './source-headers'; import { spawnSites } from './spawn-timeout'; import { bareErrorFindings } from './test-bare-error'; import { testFixFindings } from './test-fix-citations'; @@ -56,6 +57,7 @@ const GUARDS: readonly (readonly [string, (root: string) => Promise])[] ['proto-index', protoIndexFindings], ['render-modes', vocabularyFindings], ['secret-compare', secretCompareFindings], + ['source-headers', sourceHeaderFindings], ['spawn-timeout', spawnSites], ['test-bare-error', bareErrorFindings], ['test-fix-citations', testFixFindings], diff --git a/scripts/dependency-allowlist.test.ts b/scripts/dependency-allowlist.test.ts new file mode 100644 index 000000000..e9b64edb7 --- /dev/null +++ b/scripts/dependency-allowlist.test.ts @@ -0,0 +1,137 @@ +// The enforcement half of `scripts/dependency-allowlist.ts`: this file IS the build error. The +// gate's `unit` step runs every `scripts/**/*.test.ts`, so a new dependency fails the gate. + +import { describe, expect, test } from 'bun:test'; +// why: Bun ships no temporary-directory API, so the fixture root needs node:os. +import { mkdir, mkdtemp, rm } from 'node:fs/promises'; +// why: Bun ships no temporary-directory API, so the fixture root needs node:os. +import { tmpdir } from 'node:os'; +// why: Bun exposes no path-join primitive. +import { join } from 'node:path'; +import { + checkDependencyAllowlist, + type DependencyDeclaration, + dependencyAllowlistFindings, +} from './dependency-allowlist'; + +const declaration = ( + name: string, + field: DependencyDeclaration['field'] = 'dependencies', +): DependencyDeclaration => ({ manifest: 'packages/core/package.json', field, name }); + +describe('an external dependency', () => { + test('an allowlisted dependency passes', () => { + const findings = checkDependencyAllowlist({ + declarations: [declaration('typescript')], + workspaceNames: new Set(['@ultimat3/core']), + allowlist: { + 'packages/core/package.json#dependencies#typescript': { reason: 'the compiler' }, + }, + }); + expect(findings).toEqual([]); + }); + + test('an unallowlisted dependencies entry fails', () => { + const findings = checkDependencyAllowlist({ + declarations: [declaration('left-pad')], + workspaceNames: new Set(['@ultimat3/core']), + allowlist: {}, + }); + expect(findings[0]?.code).toBe('X_DEPENDENCY_NOT_ALLOWLISTED'); + }); + + test('an unallowlisted devDependencies entry fails', () => { + const findings = checkDependencyAllowlist({ + declarations: [declaration('left-pad', 'devDependencies')], + workspaceNames: new Set(['@ultimat3/core']), + allowlist: {}, + }); + expect(findings[0]?.code).toBe('X_DEPENDENCY_NOT_ALLOWLISTED'); + }); + + test('an internal dependency needs no allowlist row', () => { + const findings = checkDependencyAllowlist({ + declarations: [declaration('@ultimat3/core')], + workspaceNames: new Set(['@ultimat3/core']), + allowlist: {}, + }); + expect(findings).toEqual([]); + }); + + test('an allowlist row for a dependency that no longer exists is stale', () => { + const findings = checkDependencyAllowlist({ + declarations: [], + workspaceNames: new Set(['@ultimat3/core']), + allowlist: { 'packages/core/package.json#dependencies#gone': { reason: 'was here' } }, + }); + expect(findings[0]?.code).toBe('X_DEPENDENCY_ALLOWLIST_STALE'); + }); +}); + +describe('a broken manifest', () => { + test('is refused as a finding, never thrown', async () => { + const root = await mkdtemp(join(tmpdir(), 'ultimate-dependency-allowlist-')); + try { + await Bun.write(join(root, 'package.json'), '{ this is not json'); + const findings = await dependencyAllowlistFindings(root); + expect(findings[0]?.code).toBe('X_DEPENDENCY_UNSCANNED'); + expect(findings[0]?.fix).toContain('bun run'); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + + /** + * A workspace manifest that does not parse is a dependency this rule never saw, not a manifest + * with no dependencies. Skipping it reported a clean tree over a dependency nobody had checked, + * which is what `scripts/lib/corpus.ts` calls out for a whole tree — "a guard whose glob matched + * nothing answered 'no findings', which is exactly what a clean tree answers". + */ + test('a workspace manifest that does not parse is refused, never skipped', async () => { + const root = await mkdtemp(join(tmpdir(), 'ultimate-dependency-allowlist-')); + try { + // A readable root that DOES declare a dependency, so the root branch passes and the scan + // reaches the workspace manifest — the branch under test is the workspace one. + await Bun.write( + join(root, 'package.json'), + '{"name":"root","version":"1.0.0","devDependencies":{"typescript":"^5.0.0"}}', + ); + await mkdir(join(root, 'packages/core'), { recursive: true }); + await Bun.write(join(root, 'packages/core/package.json'), '{ this is not json'); + const findings = await dependencyAllowlistFindings(root); + expect(findings[0]?.code).toBe('X_DEPENDENCY_UNSCANNED'); + expect(findings[0]?.at).toBe('packages/core/package.json'); + expect(findings[0]?.fix).toContain('bun run'); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + + /** + * A tree whose manifests are all readable but declare no dependency at all enforces nothing — + * the same false clean the unreadable manifest produces, reached the other way. It must answer + * `X_DEPENDENCY_UNSCANNED` rather than an empty list. + */ + test('a readable tree that declares no dependency is refused, never a clean list', async () => { + const root = await mkdtemp(join(tmpdir(), 'ultimate-dependency-allowlist-')); + try { + await Bun.write(join(root, 'package.json'), '{"name":"root","version":"1.0.0"}'); + await mkdir(join(root, 'packages/core'), { recursive: true }); + await Bun.write( + join(root, 'packages/core/package.json'), + '{"name":"@ultimat3/core","version":"1.0.0"}', + ); + const findings = await dependencyAllowlistFindings(root); + expect(findings[0]?.code).toBe('X_DEPENDENCY_UNSCANNED'); + expect(findings[0]?.fix).toContain('bun run'); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); +}); + +describe('the repository tree', () => { + test('is clean', async () => { + expect(await dependencyAllowlistFindings(process.cwd())).toEqual([]); + }); +}); diff --git a/scripts/dependency-allowlist.ts b/scripts/dependency-allowlist.ts new file mode 100644 index 000000000..765f12c5d --- /dev/null +++ b/scripts/dependency-allowlist.ts @@ -0,0 +1,198 @@ +#!/usr/bin/env bun +// Enforce that every external workspace dependency is an explicit row in the checked-in allowlist. +// +// Root `CLAUDE.md` allows no new dependency without a stated reason. A name-only allowlist would +// duplicate the workspace registry, so internal names are checked against the real workspaces and +// external names against the checked-in table. +// +// bun run dependency-allowlist · bun run scripts/dependency-allowlist.ts [--json] + +import { parseScriptArgs } from './lib/args'; +import { DEPENDENCY_ALLOWLIST, DEPENDENCY_ALLOWLIST_FILE } from './lib/dependency-allowlist-data'; +import type { Finding } from './lib/log'; +import { report } from './lib/log'; +import { toPosix } from './lib/posix-path'; +import { repoRoot } from './lib/run'; +import { readWorkspaceManifest } from './lib/workspaces'; + +const SCRIPT = 'dependency-allowlist'; + +export interface DependencyDeclaration { + readonly manifest: string; + readonly field: 'dependencies' | 'devDependencies'; + readonly name: string; +} + +export interface DependencyAllowlistInput { + readonly declarations: readonly DependencyDeclaration[]; + readonly workspaceNames: ReadonlySet; + readonly allowlist: Readonly>; +} + +const keyOf = (declaration: DependencyDeclaration): string => + `${declaration.manifest}#${declaration.field}#${declaration.name}`; + +const overFinding = (declaration: DependencyDeclaration): Finding => ({ + code: 'X_DEPENDENCY_NOT_ALLOWLISTED', + cause: `${declaration.manifest} declares ${declaration.name} in ${declaration.field} and no checked-in allowlist row accepts it — a new dependency needs a stated reason`, + fix: `bun run scripts/dependency-allowlist.ts --json # then add '${keyOf(declaration)}' with a reason to ${DEPENDENCY_ALLOWLIST_FILE}`, + at: declaration.manifest, +}); + +const staleFinding = (key: string): Finding => ({ + code: 'X_DEPENDENCY_ALLOWLIST_STALE', + cause: `${DEPENDENCY_ALLOWLIST_FILE} allows '${key}', but no workspace manifest declares it — a stale row would hide a later declaration of the same name`, + fix: `bun run scripts/dependency-allowlist.ts --json # then delete '${key}' from ${DEPENDENCY_ALLOWLIST_FILE}`, + at: DEPENDENCY_ALLOWLIST_FILE, +}); + +/** One manifest path per workspace, including the root and both tracked applications. */ +export async function dependencyManifestPaths(root: string): Promise { + const paths = new Set(); + const patterns = [ + 'packages/*/package.json', + 'examples/*/package.json', + 'dummy/*/package.json', + 'examples/*/apps/*/package.json', + 'examples/*/packages/*/package.json', + 'dummy/*/apps/*/package.json', + 'dummy/*/packages/*/package.json', + ]; + for (const pattern of patterns) { + for await (const found of new Bun.Glob(pattern).scan({ cwd: root, absolute: false })) { + paths.add(toPosix(found)); + } + } + return [...paths].sort(); +} + +/** + * What one pass over the workspace manifests found — and the manifest that could not be read. + * + * `unreadable` is the answer a skipped manifest must never be: a manifest that does not parse is + * not a manifest with no dependencies, it is a dependency this rule never saw. `scripts/lib/corpus.ts` + * states the same rule for a whole tree — "a guard whose glob matched nothing answered 'no findings', + * which is exactly what a clean tree answers". + */ +export interface WorkspaceScan { + readonly declarations: readonly DependencyDeclaration[]; + readonly names: ReadonlySet; + /** The first manifest that could not be read, and why. Absent means every manifest was read. */ + readonly unreadable?: { readonly manifest: string; readonly problem: string }; +} + +/** One pass over every workspace manifest: the declarations, the names, and what could not be read. */ +export async function scanWorkspaceManifests(root: string): Promise { + const declarations: DependencyDeclaration[] = []; + const names = new Set(); + for (const manifest of ['package.json', ...(await dependencyManifestPaths(root))]) { + const read = await readWorkspaceManifest(`${root}/${manifest}`); + if (read.kind !== 'read') { + return { + declarations, + names, + unreadable: { + manifest, + problem: read.kind === 'absent' ? 'vanished between the glob and the read' : read.problem, + }, + }; + } + if (typeof read.manifest.name === 'string') names.add(read.manifest.name); + for (const field of ['dependencies', 'devDependencies'] as const) { + for (const name of Object.keys(read.manifest[field] ?? {})) { + declarations.push({ manifest, field, name }); + } + } + } + return { declarations, names }; +} + +/** Every dependency and devDependency every workspace manifest declares. */ +export async function dependencyDeclarations( + root: string, +): Promise { + return (await scanWorkspaceManifests(root)).declarations; +} + +/** The names every workspace in this tree publishes, read off the manifests themselves. */ +export async function workspaceNames(root: string): Promise> { + return (await scanWorkspaceManifests(root)).names; +} + +export function checkDependencyAllowlist(input: DependencyAllowlistInput): readonly Finding[] { + const findings: Finding[] = []; + const declared = new Set(); + for (const declaration of input.declarations) { + const key = keyOf(declaration); + declared.add(key); + if (input.workspaceNames.has(declaration.name)) continue; + if (!Object.hasOwn(input.allowlist, key)) findings.push(overFinding(declaration)); + } + for (const key of Object.keys(input.allowlist)) { + if (!declared.has(key)) findings.push(staleFinding(key)); + } + return findings; +} + +/** Every finding this rule contributes, read off the real manifests and the checked-in table. */ +export async function dependencyAllowlistFindings(root: string): Promise { + const rootManifest = await readWorkspaceManifest(`${root}/package.json`); + if (rootManifest.kind !== 'read') { + return [ + { + code: 'X_DEPENDENCY_UNSCANNED', + cause: + 'the root package.json could not be read, so no workspace manifest was checked and every dependency reports zero', + fix: 'bun run scripts/dependency-allowlist.ts --json # from the repository root', + at: 'package.json', + }, + ]; + } + const scan = await scanWorkspaceManifests(root); + // A workspace manifest that could not be read is refused, not skipped: skipping it reports a + // clean tree over a dependency this rule never saw. + if (scan.unreadable !== undefined) { + return [ + { + code: 'X_DEPENDENCY_UNSCANNED', + cause: `${scan.unreadable.manifest} ${scan.unreadable.problem}, so the dependencies it declares were never checked`, + fix: 'bun run scripts/dependency-allowlist.ts --json # from the repository root', + at: scan.unreadable.manifest, + }, + ]; + } + const { declarations, names } = scan; + if (declarations.length === 0 || names.size === 0) { + return [ + { + code: 'X_DEPENDENCY_UNSCANNED', + cause: + 'no workspace manifest declared a dependency, so the allowlist enforced nothing — a glob that matches nothing reads exactly like a tree with no dependencies', + fix: 'bun run scripts/dependency-allowlist.ts --json # from the repository root', + at: 'package.json', + }, + ]; + } + return checkDependencyAllowlist({ + declarations, + workspaceNames: names, + allowlist: DEPENDENCY_ALLOWLIST, + }); +} + +if (import.meta.main) { + const args = parseScriptArgs(Bun.argv.slice(2)); + const findings = await dependencyAllowlistFindings(repoRoot()); + report( + { + ok: findings.length === 0, + script: SCRIPT, + summary: + findings.length === 0 + ? 'every external workspace dependency is an explicit allowlist row' + : `${String(findings.length)} dependency allowlist finding(s)`, + findings, + }, + args.json, + ); +} diff --git a/scripts/lib/dependency-allowlist-data.ts b/scripts/lib/dependency-allowlist-data.ts new file mode 100644 index 000000000..1eb5afc54 --- /dev/null +++ b/scripts/lib/dependency-allowlist-data.ts @@ -0,0 +1,59 @@ +// The checked-in allowlist of external workspace dependencies, keyed by manifest, field and name. +// Data only; every row says why the repository accepts the dependency. +// +// The key is '##': one row per declaration, so a new +// external dependency cannot be introduced by copying another manifest's row. +// +// bun run scripts/dependency-allowlist.ts --json + +/** Where the table lives, so a stale-row finding can name the file to edit. */ +export const DEPENDENCY_ALLOWLIST_FILE = 'scripts/lib/dependency-allowlist-data.ts'; + +export const DEPENDENCY_ALLOWLIST: Readonly> = { + 'dummy/social-media-clone/package.json#dependencies#solid-js': { + reason: 'The SolidJS runtime the UI and app surfaces render with.', + }, + 'dummy/social-media-clone/package.json#devDependencies#@biomejs/biome': { + reason: 'The repository linter and formatter.', + }, + 'dummy/social-media-clone/package.json#devDependencies#@electric-sql/pglite': { + reason: 'The embedded Postgres driver used by local and test runs.', + }, + 'dummy/social-media-clone/package.json#devDependencies#@types/bun': { + reason: 'Bun runtime type declarations for TypeScript.', + }, + 'dummy/social-media-clone/package.json#devDependencies#typescript': { + reason: 'The repository compiler.', + }, + 'examples/dummy/apps/web/package.json#dependencies#solid-js': { + reason: 'The SolidJS runtime the UI and app surfaces render with.', + }, + 'examples/dummy/package.json#devDependencies#typescript': { reason: 'The repository compiler.' }, + 'examples/dummy/packages/ui/package.json#dependencies#solid-js': { + reason: 'The SolidJS runtime the UI and app surfaces render with.', + }, + 'package.json#devDependencies#@biomejs/biome': { reason: 'The repository linter and formatter.' }, + 'package.json#devDependencies#@electric-sql/pglite': { + reason: 'The embedded Postgres driver used by local and test runs.', + }, + 'package.json#devDependencies#@types/bun': { + reason: 'Bun runtime type declarations for TypeScript.', + }, + 'package.json#devDependencies#lefthook': { reason: 'The repository git-hook runner.' }, + 'package.json#devDependencies#solid-js': { + reason: 'The SolidJS runtime the UI and app surfaces render with.', + }, + 'package.json#devDependencies#typescript': { reason: 'The repository compiler.' }, + 'packages/cli/package.json#dependencies#@babel/core': { + reason: 'Babel transforms JSX for the CLI island compiler.', + }, + 'packages/cli/package.json#dependencies#babel-preset-solid': { + reason: 'The CLI island compiler compiles Solid JSX.', + }, + 'packages/realtime/package.json#dependencies#nats': { + reason: 'The realtime NATS transport is an optional production driver.', + }, + 'packages/render/package.json#dependencies#sass': { + reason: 'The render package compiles its own Sass design tokens.', + }, +}; diff --git a/scripts/lib/source-header-pins.ts b/scripts/lib/source-header-pins.ts new file mode 100644 index 000000000..78841b6f6 --- /dev/null +++ b/scripts/lib/source-header-pins.ts @@ -0,0 +1,358 @@ +// The source-header ratchet: one row per shipped file whose leading header is not yet in the +// 1–4-line responsibility contract. Data only; the count may fall and may never rise. +// +// A pin is per FILE, not per package: a package-level count lets one repaired file pay for one +// new violation elsewhere, so the guard would enforce nothing at the site that changed. +// +// The two reasons below are the sentences each row stands on: the file opens with no header +// comment, or its leading first paragraph runs past four lines. +// +// why: this table opens with the whole debt at once — every shipped file whose leading header is +// outside the 1–4-line contract today — because the rule lands on a tree that already carries it; +// each row may fall as its file is repaired and may never rise. +// +// bun run scripts/source-headers.ts --json + +/** Where the table lives, so a stale-pin finding can name the file to edit. */ +export const SOURCE_HEADER_PINS_FILE = 'scripts/lib/source-header-pins.ts'; + +export interface SourceHeaderPin { + readonly count: number; + readonly reason: string; +} + +/** The file opens with no header comment; repair it and remove this row. */ +const MISSING = + 'Existing violation: the file opens with no header comment; repair it and remove this row.'; + +/** The leading first paragraph runs past four lines; repair it and remove this row. */ +const TOO_LONG = + 'Existing violation: the leading first paragraph runs past four lines; repair it and remove this row.'; + +/** One row per file; the count may fall and may never rise. */ +export const SOURCE_HEADER_PINS: Readonly> = { + 'packages/i18n/src/locale-path.ts': { count: 1, reason: TOO_LONG }, + 'packages/i18n/src/index.ts': { count: 1, reason: MISSING }, + 'packages/i18n/src/subset.ts': { count: 1, reason: TOO_LONG }, + 'packages/cache/src/graph.ts': { count: 1, reason: TOO_LONG }, + 'packages/cache/src/partial-log.ts': { count: 1, reason: TOO_LONG }, + 'packages/cache/src/invalidate.ts': { count: 1, reason: TOO_LONG }, + 'packages/cache/src/memo.ts': { count: 1, reason: TOO_LONG }, + 'packages/cache/src/semantic.ts': { count: 1, reason: TOO_LONG }, + 'packages/db/src/replica-identity.ts': { count: 1, reason: TOO_LONG }, + 'packages/db/src/migration-errors.ts': { count: 1, reason: TOO_LONG }, + 'packages/db/src/column-default.ts': { count: 1, reason: TOO_LONG }, + 'packages/db/src/drift-errors.ts': { count: 1, reason: TOO_LONG }, + 'packages/db/src/column-alter.ts': { count: 1, reason: TOO_LONG }, + 'packages/db/src/type-pins.ts': { count: 1, reason: TOO_LONG }, + 'packages/db/src/pg-instant.ts': { count: 1, reason: TOO_LONG }, + 'packages/db/src/ungeneratable.ts': { count: 1, reason: TOO_LONG }, + 'packages/db/src/client.ts': { count: 1, reason: TOO_LONG }, + 'packages/db/src/unrendered.ts': { count: 1, reason: TOO_LONG }, + 'packages/db/src/pglite-turns.ts': { count: 1, reason: TOO_LONG }, + 'packages/db/src/index-plan.ts': { count: 1, reason: TOO_LONG }, + 'packages/db/src/catalog.ts': { count: 1, reason: TOO_LONG }, + 'packages/db/src/bound-parameters.ts': { count: 1, reason: TOO_LONG }, + 'packages/db/src/statement-span.ts': { count: 1, reason: TOO_LONG }, + 'packages/db/src/check-ddl.ts': { count: 1, reason: TOO_LONG }, + 'packages/db/src/index-ddl.ts': { count: 1, reason: TOO_LONG }, + 'packages/db/src/statement-shape.ts': { count: 1, reason: TOO_LONG }, + 'packages/db/src/invariant-errors.ts': { count: 1, reason: TOO_LONG }, + 'packages/time/src/plain-date.ts': { count: 1, reason: TOO_LONG }, + 'packages/time/src/index.ts': { count: 1, reason: MISSING }, + 'packages/admin/src/screen-action.tsx': { count: 1, reason: TOO_LONG }, + 'packages/admin/src/list.tsx': { count: 1, reason: TOO_LONG }, + 'packages/admin/src/batch-bar.tsx': { count: 1, reason: TOO_LONG }, + 'packages/admin/src/claim-route.ts': { count: 1, reason: TOO_LONG }, + 'packages/admin/src/layout.tsx': { count: 1, reason: TOO_LONG }, + 'packages/admin/src/describe.ts': { count: 1, reason: TOO_LONG }, + 'packages/admin/src/list-request.ts': { count: 1, reason: TOO_LONG }, + 'packages/query/src/errors.ts': { count: 1, reason: MISSING }, + 'packages/query/src/cursor-value.ts': { count: 1, reason: TOO_LONG }, + 'packages/query/src/sealed-shape.ts': { count: 1, reason: TOO_LONG }, + 'packages/query/src/openapi.ts': { count: 1, reason: TOO_LONG }, + 'packages/query/src/record-answer.ts': { count: 1, reason: TOO_LONG }, + 'packages/query/src/facade.ts': { count: 1, reason: TOO_LONG }, + 'packages/query/src/search.ts': { count: 1, reason: TOO_LONG }, + 'packages/query/src/query.ts': { count: 1, reason: TOO_LONG }, + 'packages/query/src/live.ts': { count: 1, reason: TOO_LONG }, + 'packages/action/src/error-titles.ts': { count: 1, reason: TOO_LONG }, + 'packages/action/src/job-handle.ts': { count: 1, reason: TOO_LONG }, + 'packages/action/src/facade.ts': { count: 1, reason: TOO_LONG }, + 'packages/action/src/audit.ts': { count: 1, reason: TOO_LONG }, + 'packages/action/src/openapi-complete.ts': { count: 1, reason: TOO_LONG }, + 'packages/action/src/api-declaration.ts': { count: 1, reason: TOO_LONG }, + 'packages/action/src/path-style-miss.ts': { count: 1, reason: TOO_LONG }, + 'packages/manifest/src/sources-admin.ts': { count: 1, reason: TOO_LONG }, + 'packages/manifest/src/diff-admin.ts': { count: 1, reason: TOO_LONG }, + 'packages/testing/src/matcher-row.ts': { count: 1, reason: TOO_LONG }, + 'packages/testing/src/live-node.ts': { count: 1, reason: TOO_LONG }, + 'packages/testing/src/index.ts': { count: 1, reason: MISSING }, + 'packages/testing/src/registry-isolation.ts': { count: 1, reason: TOO_LONG }, + 'packages/testing/src/island-scratch.ts': { count: 1, reason: TOO_LONG }, + 'packages/testing/src/failure-context.ts': { count: 1, reason: TOO_LONG }, + 'packages/testing/src/fixture-push.ts': { count: 1, reason: TOO_LONG }, + 'packages/testing/src/cdp-connection.ts': { count: 1, reason: TOO_LONG }, + 'packages/testing/src/cdp-offline-script.ts': { count: 1, reason: TOO_LONG }, + 'packages/jobs/src/purge.ts': { count: 1, reason: TOO_LONG }, + 'packages/jobs/src/driver-pg-wake-sql.ts': { count: 1, reason: TOO_LONG }, + 'packages/jobs/src/execute.ts': { count: 1, reason: TOO_LONG }, + 'packages/jobs/src/backfill-errors.ts': { count: 1, reason: TOO_LONG }, + 'packages/jobs/src/leases.ts': { count: 1, reason: TOO_LONG }, + 'packages/jobs/src/webhook.ts': { count: 1, reason: TOO_LONG }, + 'packages/jobs/src/outbox-pg.ts': { count: 1, reason: TOO_LONG }, + 'packages/jobs/src/backfill.ts': { count: 1, reason: TOO_LONG }, + 'packages/jobs/src/backfill-pass-fixture.ts': { count: 1, reason: TOO_LONG }, + 'packages/jobs/src/export.ts': { count: 1, reason: TOO_LONG }, + 'packages/jobs/src/introspection.ts': { count: 1, reason: TOO_LONG }, + 'packages/jobs/src/driver-pg.ts': { count: 1, reason: TOO_LONG }, + 'packages/jobs/src/concurrency.ts': { count: 1, reason: TOO_LONG }, + 'packages/jobs/src/driver-redis.ts': { count: 1, reason: TOO_LONG }, + 'packages/jobs/src/settled.ts': { count: 1, reason: TOO_LONG }, + 'packages/jobs/src/backfill-ledger.ts': { count: 1, reason: TOO_LONG }, + 'packages/jobs/src/webhook-target.ts': { count: 1, reason: TOO_LONG }, + 'packages/jobs/src/renewal-timer.ts': { count: 1, reason: TOO_LONG }, + 'packages/jobs/src/queue-wake.ts': { count: 1, reason: TOO_LONG }, + 'packages/jobs/src/enqueue-signal.ts': { count: 1, reason: TOO_LONG }, + 'packages/core/src/request-budget.ts': { count: 1, reason: TOO_LONG }, + 'packages/core/src/source-mask.ts': { count: 1, reason: TOO_LONG }, + 'packages/core/src/client-paths.ts': { count: 1, reason: TOO_LONG }, + 'packages/core/src/read-capped.ts': { count: 1, reason: TOO_LONG }, + 'packages/core/src/page.ts': { count: 1, reason: TOO_LONG }, + 'packages/core/src/client-writes.ts': { count: 1, reason: TOO_LONG }, + 'packages/core/src/write-origin.ts': { count: 1, reason: TOO_LONG }, + 'packages/core/src/type-pins.ts': { count: 1, reason: TOO_LONG }, + 'packages/core/src/locale-direction.ts': { count: 1, reason: TOO_LONG }, + 'packages/core/src/intl-cache.ts': { count: 1, reason: TOO_LONG }, + 'packages/core/src/outbound-headers.ts': { count: 1, reason: TOO_LONG }, + 'packages/core/src/locale-path.ts': { count: 1, reason: TOO_LONG }, + 'packages/core/src/client-transport.ts': { count: 1, reason: TOO_LONG }, + 'packages/core/src/schema-error-codes.ts': { count: 1, reason: TOO_LONG }, + 'packages/core/src/keyed-fingerprint.ts': { count: 1, reason: TOO_LONG }, + 'packages/core/src/wire-dates.ts': { count: 1, reason: TOO_LONG }, + 'packages/core/src/bunfs.ts': { count: 1, reason: TOO_LONG }, + 'packages/core/src/seal-errors.ts': { count: 1, reason: TOO_LONG }, + 'packages/core/src/host-rules.ts': { count: 1, reason: TOO_LONG }, + 'packages/core/src/probe-database-sweep.ts': { count: 1, reason: TOO_LONG }, + 'packages/core/src/flight-gate.ts': { count: 1, reason: TOO_LONG }, + 'packages/core/src/image/probe.ts': { count: 1, reason: TOO_LONG }, + 'packages/http/src/error-titles.ts': { count: 1, reason: TOO_LONG }, + 'packages/http/src/csrf.ts': { count: 1, reason: TOO_LONG }, + 'packages/http/src/hooks.ts': { count: 1, reason: TOO_LONG }, + 'packages/http/src/stages.ts': { count: 1, reason: TOO_LONG }, + 'packages/http/src/config.ts': { count: 1, reason: TOO_LONG }, + 'packages/http/src/forwarded.ts': { count: 1, reason: TOO_LONG }, + 'packages/http/src/overlay-style.ts': { count: 1, reason: TOO_LONG }, + 'packages/http/src/pipeline.ts': { count: 1, reason: TOO_LONG }, + 'packages/http/src/bearer-mount.ts': { count: 1, reason: TOO_LONG }, + 'packages/http/src/deadline.ts': { count: 1, reason: TOO_LONG }, + 'packages/http/src/app-config.ts': { count: 1, reason: TOO_LONG }, + 'packages/entity/src/pg-row.ts': { count: 1, reason: TOO_LONG }, + 'packages/entity/src/sealed-repo.ts': { count: 1, reason: TOO_LONG }, + 'packages/entity/src/write-tag.ts': { count: 1, reason: TOO_LONG }, + 'packages/entity/src/type-pins.ts': { count: 1, reason: TOO_LONG }, + 'packages/entity/src/coalesce.ts': { count: 1, reason: TOO_LONG }, + 'packages/entity/src/memory-match.ts': { count: 1, reason: TOO_LONG }, + 'packages/entity/src/instant.ts': { count: 1, reason: TOO_LONG }, + 'packages/entity/src/refuse.ts': { count: 1, reason: TOO_LONG }, + 'packages/entity/src/pg-array-literal.ts': { count: 1, reason: TOO_LONG }, + 'packages/entity/src/count-by.ts': { count: 1, reason: TOO_LONG }, + 'packages/entity/src/bulk-write.ts': { count: 1, reason: TOO_LONG }, + 'packages/entity/src/query.ts': { count: 1, reason: TOO_LONG }, + 'packages/entity/src/containment.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/verify-typecheck.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/app-openapi.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/cdp-shot-watch.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/secrets-rotation.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/island-realtime.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/dev-supervisor.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/error-unthrown.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/load-findings.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/dev-environment.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/verify-e2e.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/runtime-assets.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/mcp-ui-inspect.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/dev-n-plus-one.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/ts-scan.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/runtime-overrides.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/dev-watch-tree.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/import-closure.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/cmd-auth.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/dev-reload.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/generate-shadow.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/document-styles.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/cmd-db-branch.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/runtime-live-feed.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/cmd-clean.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/cmd-test.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/dev-restart-retire.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/dev-child-watch.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/script-csp.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/cdp-shot-page.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/realtime-browser-probe-fixture.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/db-backfill.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/generate-sibling.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/async-pages.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/verify-role-load.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/app-runtime.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/verify-pace.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/cmd-pr.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/runtime-cache.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/dev-watch.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/serve.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/app-reload-graph.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/ci-log.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/error-contract.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/mcp-ui-interact.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/mcp-ui.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/framework-schema-apply.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/duplicate-packages.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/runtime-queue.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/cmd-ci.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/cmd-registries.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/worker-bundle.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/measure-scope.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/browser-launcher.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/page-navigation.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/error-fixes.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/app-layout.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/browser-transport.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/source-list-edit.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/shot-theme.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/sync-authenticator.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/island-realtime-state.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/cmd-tasks.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/serve-boot.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/mcp-ui-diff.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/page-speculation.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/island-bundle.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/cmd-jobs.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/i18n-registration.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/measure-paths.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/api-registration.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/db-schema-refresh.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/handle-registration.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/generate-follow-ups.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/cdp-shot-errors.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/cmd-deploy-compose.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/local-cli.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/drift.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/admin-registration.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/dev-live-fixture.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/measure-database.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/cmd-errors.ts': { count: 1, reason: MISSING }, + 'packages/cli/src/server-barrels.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/island-shot.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/app-mcp.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/templates/guard-semantic-interactive.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/templates/guard-island-without-states.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/templates/island.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/templates/scaffold-repo.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/templates/scaffold-docs.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/templates/guard-focus-visible.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/templates/scaffold-entries.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/templates/resource-page.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/templates/scaffold-errors.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/templates/guard-animated-layout-property.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/templates/admin.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/templates/scaffold-shell.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/templates/guard-image-dimensions.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/templates/scaffold-site.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/templates/route.ts': { count: 1, reason: TOO_LONG }, + 'packages/cli/src/templates/admin-page.ts': { count: 1, reason: TOO_LONG }, + 'packages/render/src/navigation-dom-fixture.ts': { count: 1, reason: TOO_LONG }, + 'packages/render/src/client.ts': { count: 1, reason: TOO_LONG }, + 'packages/render/src/navigation-modal.ts': { count: 1, reason: TOO_LONG }, + 'packages/render/src/island-hold.ts': { count: 1, reason: TOO_LONG }, + 'packages/render/src/navigation-errors.ts': { count: 1, reason: TOO_LONG }, + 'packages/render/src/navigation-fetch.ts': { count: 1, reason: TOO_LONG }, + 'packages/render/src/navigation.ts': { count: 1, reason: TOO_LONG }, + 'packages/render/src/revalidate-shape.ts': { count: 1, reason: TOO_LONG }, + 'packages/render/src/navigation-rules.ts': { count: 1, reason: TOO_LONG }, + 'packages/render/src/finite-status.ts': { count: 1, reason: TOO_LONG }, + 'packages/render/src/navigation-api.ts': { count: 1, reason: TOO_LONG }, + 'packages/render/src/asset.ts': { count: 1, reason: TOO_LONG }, + 'packages/render/src/duration.ts': { count: 1, reason: TOO_LONG }, + 'packages/render/src/client-scope-tag.ts': { count: 1, reason: TOO_LONG }, + 'packages/ui/src/fake-dom-fixture.ts': { count: 1, reason: TOO_LONG }, + 'packages/ui/src/error-registry.ts': { count: 1, reason: TOO_LONG }, + 'packages/ui/src/ui-catalog.ts': { count: 1, reason: TOO_LONG }, + 'packages/ui/src/theme/ambient.browser.ts': { count: 1, reason: TOO_LONG }, + 'packages/ui/src/theme/ambient.ts': { count: 1, reason: TOO_LONG }, + 'packages/ui/src/theme/ambient-slot.ts': { count: 1, reason: TOO_LONG }, + 'packages/ui/src/components/AppShell.tsx': { count: 1, reason: TOO_LONG }, + 'packages/ui/src/components/BarChart.tsx': { count: 1, reason: TOO_LONG }, + 'packages/ui/src/components/Dropzone.tsx': { count: 1, reason: TOO_LONG }, + 'packages/ui/src/components/link-target.ts': { count: 1, reason: TOO_LONG }, + 'packages/ui/src/components/Toast.tsx': { count: 1, reason: TOO_LONG }, + 'packages/ui/src/components/Toolbar.tsx': { count: 1, reason: TOO_LONG }, + 'packages/ui/src/components/Switch.tsx': { count: 1, reason: TOO_LONG }, + 'packages/ui/src/components/CommandPalette.tsx': { count: 1, reason: TOO_LONG }, + 'packages/scraping/src/cdp-arm.ts': { count: 1, reason: TOO_LONG }, + 'packages/scraping/src/http.ts': { count: 1, reason: TOO_LONG }, + 'packages/scraping/src/pinned-host.ts': { count: 1, reason: TOO_LONG }, + 'packages/scraping/src/cdp-a11y.ts': { count: 1, reason: TOO_LONG }, + 'packages/scraping/src/actionability.ts': { count: 1, reason: TOO_LONG }, + 'packages/scraping/src/http-redirect.ts': { count: 1, reason: TOO_LONG }, + 'packages/seo/src/links.ts': { count: 1, reason: TOO_LONG }, + 'packages/ai/src/vector-scope.ts': { count: 1, reason: TOO_LONG }, + 'packages/ai/src/bounds-fixture.ts': { count: 1, reason: TOO_LONG }, + 'packages/ai/src/fix-line-fixture.ts': { count: 1, reason: TOO_LONG }, + 'packages/money/src/factor.ts': { count: 1, reason: TOO_LONG }, + 'packages/money/src/index.ts': { count: 1, reason: MISSING }, + 'packages/mcp/src/meta-surface.ts': { count: 1, reason: TOO_LONG }, + 'packages/mcp/src/surface-budget.ts': { count: 1, reason: TOO_LONG }, + 'packages/mcp/src/idempotency-arg.ts': { count: 1, reason: TOO_LONG }, + 'packages/mcp/src/confirmations.ts': { count: 1, reason: TOO_LONG }, + 'packages/mcp/src/dev-ui-interact.ts': { count: 1, reason: TOO_LONG }, + 'packages/mcp/src/meta-split.ts': { count: 1, reason: TOO_LONG }, + 'packages/mcp/src/dev-ui-tools.ts': { count: 1, reason: TOO_LONG }, + 'packages/mcp/src/audit-hook.ts': { count: 1, reason: TOO_LONG }, + 'packages/mcp/src/oauth-metadata.ts': { count: 1, reason: TOO_LONG }, + 'packages/mail/src/envelope-address.ts': { count: 1, reason: TOO_LONG }, + 'packages/mail/src/driver-env.ts': { count: 1, reason: TOO_LONG }, + 'packages/mail/src/driver.ts': { count: 1, reason: TOO_LONG }, + 'packages/pwa/src/pages-cache-source.ts': { count: 1, reason: TOO_LONG }, + 'packages/pwa/src/push-hosts.ts': { count: 1, reason: TOO_LONG }, + 'packages/pwa/src/index.ts': { count: 1, reason: MISSING }, + 'packages/realtime/src/error-titles.ts': { count: 1, reason: TOO_LONG }, + 'packages/realtime/src/close-codes.ts': { count: 1, reason: TOO_LONG }, + 'packages/realtime/src/socket.ts': { count: 1, reason: TOO_LONG }, + 'packages/realtime/src/nats-relay-fixture.ts': { count: 1, reason: TOO_LONG }, + 'packages/realtime/src/client.ts': { count: 1, reason: TOO_LONG }, + 'packages/realtime/src/server.ts': { count: 1, reason: TOO_LONG }, + 'packages/realtime/src/socket-engine.ts': { count: 1, reason: TOO_LONG }, + 'packages/realtime/src/client-harness-fixture.ts': { count: 1, reason: TOO_LONG }, + 'packages/realtime/src/nats-transport.ts': { count: 1, reason: TOO_LONG }, + 'packages/realtime/src/page-errors.ts': { count: 1, reason: TOO_LONG }, + 'packages/realtime/src/idb-fake-fixture.ts': { count: 1, reason: TOO_LONG }, + 'packages/realtime/src/transport-env.ts': { count: 1, reason: TOO_LONG }, + 'packages/storage/src/signed-url.ts': { count: 1, reason: TOO_LONG }, + 'packages/storage/src/driver-s3-signed.ts': { count: 1, reason: TOO_LONG }, + 'packages/storage/src/driver-s3.ts': { count: 1, reason: TOO_LONG }, + 'packages/storage/src/upload.ts': { count: 1, reason: TOO_LONG }, + 'packages/storage/src/grant.ts': { count: 1, reason: TOO_LONG }, + 'packages/storage/src/attachment.ts': { count: 1, reason: TOO_LONG }, + 'packages/storage/src/storage.ts': { count: 1, reason: TOO_LONG }, + 'packages/policy/src/errors.ts': { count: 1, reason: TOO_LONG }, + 'packages/policy/src/policy-anonymous.ts': { count: 1, reason: TOO_LONG }, + 'packages/policy/src/pre-input-brand.ts': { count: 1, reason: TOO_LONG }, + 'packages/policy/src/evaluate.ts': { count: 1, reason: TOO_LONG }, + 'packages/auth/src/oauth.ts': { count: 1, reason: TOO_LONG }, + 'packages/auth/src/mfa.ts': { count: 1, reason: TOO_LONG }, + 'packages/auth/src/oauth-builtins.ts': { count: 1, reason: TOO_LONG }, + 'packages/auth/src/policy-numbers.ts': { count: 1, reason: TOO_LONG }, + 'packages/auth/src/oauth-cookie.ts': { count: 1, reason: TOO_LONG }, + 'packages/auth/src/oauth-paths.ts': { count: 1, reason: TOO_LONG }, + 'packages/auth/src/oauth-registry.ts': { count: 1, reason: TOO_LONG }, + 'packages/auth/src/workload.ts': { count: 1, reason: TOO_LONG }, + 'packages/auth/src/directory.ts': { count: 1, reason: TOO_LONG }, + 'packages/auth/src/session.ts': { count: 1, reason: TOO_LONG }, + 'packages/auth/src/api-keys.ts': { count: 1, reason: TOO_LONG }, + 'packages/auth/src/email.ts': { count: 1, reason: TOO_LONG }, + 'packages/auth/src/tokens.ts': { count: 1, reason: TOO_LONG }, + 'packages/auth/src/id-token.ts': { count: 1, reason: TOO_LONG }, + 'packages/auth/src/revocation.ts': { count: 1, reason: TOO_LONG }, + 'packages/auth/src/jwks.ts': { count: 1, reason: TOO_LONG }, + 'packages/auth/src/policy-bridge.ts': { count: 1, reason: TOO_LONG }, +}; diff --git a/scripts/source-headers.test.ts b/scripts/source-headers.test.ts new file mode 100644 index 000000000..32e787b03 --- /dev/null +++ b/scripts/source-headers.test.ts @@ -0,0 +1,58 @@ +// The enforcement half of `scripts/source-headers.ts`: this file IS the build error. The gate's +// `unit` step runs every `scripts/**/*.test.ts`, so a new header violation fails `bun run verify`. + +import { describe, expect, setDefaultTimeout, test } from 'bun:test'; +import { REPO_SCAN_TIMEOUT_MS, repoRoot } from './lib/run'; +import { readSourceHeader, sourceHeaderFindings } from './source-headers'; + +setDefaultTimeout(REPO_SCAN_TIMEOUT_MS); + +describe('a source header in the 1–4 line contract', () => { + test('a one-line header passes', () => { + expect(readSourceHeader('x.ts', '// The one responsibility.\nexport const a = 1;\n')).toEqual({ + path: 'x.ts', + lines: ['The one responsibility.'], + }); + }); + + test('a four-line header passes', () => { + const source = ['// One.', '// Two.', '// Three.', '// Four.', 'export const a = 1;'].join( + '\n', + ); + expect(readSourceHeader('x.ts', source).problem).toBeUndefined(); + }); + + test('a block header passes', () => { + const source = ['/**', ' * One responsibility.', ' */', 'export const a = 1;'].join('\n'); + expect(readSourceHeader('x.ts', source).problem).toBeUndefined(); + }); + + test('a five-line first paragraph fails', () => { + const source = ['// One.', '// Two.', '// Three.', '// Four.', '// Five.'].join('\n'); + expect(readSourceHeader('x.ts', source).problem).toBe('too-long'); + }); + + test('a missing header fails', () => { + expect(readSourceHeader('x.ts', 'export const a = 1;\n').problem).toBe('missing'); + }); + + test('an empty header fails', () => { + expect(readSourceHeader('x.ts', '//\nexport const a = 1;\n').problem).toBe('missing'); + expect(readSourceHeader('x.ts', '/**\n */\nexport const a = 1;\n').problem).toBe('empty'); + }); +}); + +describe('the repository tree', () => { + test('is at or under its per-file pin', async () => { + const findings = await sourceHeaderFindings(repoRoot()); + expect(findings).toEqual([]); + }); + + test('every finding carries a runnable fix', async () => { + const findings = await sourceHeaderFindings(repoRoot()); + for (const finding of findings) { + expect(finding.fix.length).toBeGreaterThan(0); + expect(finding.fix).toContain('bun run'); + } + }); +}); diff --git a/scripts/source-headers.ts b/scripts/source-headers.ts new file mode 100644 index 000000000..cdd649333 --- /dev/null +++ b/scripts/source-headers.ts @@ -0,0 +1,164 @@ +#!/usr/bin/env bun +// Enforce that every shipped source file opens with a 1–4 line header stating its one responsibility. +// +// Root `CLAUDE.md` makes the header a contract, but prose is not enforcement: a file without one +// says nothing about what it owns, and a long first paragraph buries the responsibility. +// +// bun run source-headers · bun run scripts/source-headers.ts [--json] + +import { parseScriptArgs } from './lib/args'; +import { corpus } from './lib/corpus'; +import type { Finding } from './lib/log'; +import { report } from './lib/log'; +import { repoRoot } from './lib/run'; +import { SOURCE_HEADER_PINS, SOURCE_HEADER_PINS_FILE } from './lib/source-header-pins'; + +const SCRIPT = 'source-headers'; + +export type SourceHeaderProblem = 'missing' | 'empty' | 'too-long'; + +/** One file's leading header, read from the source itself. */ +export interface SourceHeader { + readonly path: string; + readonly lines: readonly string[]; + readonly problem?: SourceHeaderProblem; +} + +/** + * The leading header: a line-comment block or a block-comment block, after an optional shebang. + * + * The first PARAGRAPH is what the contract judges. Later paragraphs are the why, and a block + * header's delimiters are never prose. + */ +export function readSourceHeader(path: string, source: string): SourceHeader { + const lines = source.split(/\r?\n/); + let at = 0; + while (at < lines.length && (lines[at]?.startsWith('#!') || lines[at]?.trim() === '')) at += 1; + const first = lines[at]?.trimStart() ?? ''; + if (first.startsWith('/*')) { + const end = lines.findIndex((line, index) => index >= at && line.includes('*/')); + const body = lines.slice(at + 1, end === -1 ? lines.length : end); + const paragraph: string[] = []; + for (const line of body) { + const text = line.replace(/^\s*\*\s?/, '').trim(); + if (text === '') break; + paragraph.push(text); + } + if (paragraph.length === 0) return { path, lines: [], problem: 'empty' }; + return paragraph.length > 4 + ? { path, lines: paragraph, problem: 'too-long' } + : { path, lines: paragraph }; + } + const paragraph: string[] = []; + while (at < lines.length) { + const line = lines[at]; + if (line === undefined || !line.startsWith('//')) break; + const text = line.replace(/^\/\/\s?/, '').trim(); + if (text === '') break; + paragraph.push(text); + at += 1; + } + if (paragraph.length === 0) return { path, lines: [], problem: 'missing' }; + return paragraph.length > 4 + ? { path, lines: paragraph, problem: 'too-long' } + : { path, lines: paragraph }; +} + +/** Whether the header's first paragraph carries prose rather than punctuation alone. */ +export const hasSourceHeaderProse = (header: SourceHeader): boolean => + header.lines.some((line) => /[A-Za-z0-9]/.test(line)); + +/** + * The finding for a file that opens with no header comment at all. + * + * One object per code, each writing its code as a literal at the `code:` key: the gate's code + * scanner reads `code: 'X_…'` and nothing else, so a code hidden in a ternary is a code no + * registry, no manifest and no reference page ever hears of. + */ +const missingFinding = (header: SourceHeader): Finding => ({ + code: 'X_SOURCE_HEADER_MISSING', + cause: `${header.path} opens with no header comment, so it states no single responsibility`, + fix: `bun run scripts/source-headers.ts --json # then add a 1–4 line header comment to ${header.path} stating its single responsibility`, + at: header.path, +}); + +/** The finding for a header that is there but says nothing: an empty one, or one without prose. */ +const emptyFinding = (header: SourceHeader): Finding => ({ + code: 'X_SOURCE_HEADER_OUT_OF_CONTRACT', + cause: `${header.path} opens with an empty header comment, so it states no single responsibility`, + fix: `bun run scripts/source-headers.ts --json # then add a 1–4 line header comment to ${header.path} stating its single responsibility`, + at: header.path, +}); + +/** The finding for a first paragraph longer than the four lines the contract allows. */ +const tooLongFinding = (header: SourceHeader): Finding => ({ + code: 'X_SOURCE_HEADER_OUT_OF_CONTRACT', + cause: `${header.path} opens with a ${String(header.lines.length)}-line first paragraph, outside the 1–4 line responsibility contract`, + fix: `bun run scripts/source-headers.ts --json # then rewrite the leading header of ${header.path} as 1–4 lines of prose stating its single responsibility`, + at: header.path, +}); + +const overFinding = (header: SourceHeader): Finding => { + if (header.problem === 'missing') return missingFinding(header); + if (header.problem === 'empty') return emptyFinding(header); + return tooLongFinding(header); +}; + +const staleFinding = (path: string, pinned: number, found: number): Finding => ({ + code: 'X_SOURCE_HEADER_PIN_STALE', + cause: `${path} is pinned at ${String(pinned)} header violation(s) and now has ${String(found)} — the pin is above what this tree contains, so it would let ${String(pinned - found)} back in`, + fix: `bun run scripts/source-headers.ts --json # then lower the count for '${path}' in ${SOURCE_HEADER_PINS_FILE}`, + at: SOURCE_HEADER_PINS_FILE, +}); + +const unscannedFinding = (): Finding => ({ + code: 'X_SOURCE_HEADER_UNSCANNED', + cause: + 'no shipped source file was read, so every file reports zero and the ratchet enforces nothing — a glob that matches nothing reads exactly like a tree of perfect headers', + fix: 'bun run scripts/source-headers.ts # then edit PATTERNS in scripts/lib/corpus.ts so it matches this repo layout', + at: 'scripts/lib/corpus.ts', +}); + +/** Every finding this rule contributes, read off the shared corpus and the per-file pin table. */ +export async function sourceHeaderFindings(root: string): Promise { + const files = await corpus(root, 'shipped'); + if (files.length === 0) return [unscannedFinding()]; + const findings: Finding[] = []; + const counts = new Map(); + for (const file of files) { + const header = readSourceHeader(file.path, file.source); + const problem = + header.problem ?? (hasSourceHeaderProse(header) ? undefined : ('empty' as const)); + if (problem === undefined) continue; + const found = (counts.get(file.path) ?? 0) + 1; + counts.set(file.path, found); + const pinned = SOURCE_HEADER_PINS[file.path]; + if (pinned !== undefined && found <= pinned.count) continue; + findings.push(overFinding({ ...header, problem })); + } + for (const [path, pin] of Object.entries(SOURCE_HEADER_PINS)) { + // A pin describes THIS repository: a fixture root does not hold the pinned files, and their + // absence there is not a stale row. + if (!(await Bun.file(`${root}/${path}`).exists())) continue; + const found = counts.get(path) ?? 0; + if (found < pin.count) findings.push(staleFinding(path, pin.count, found)); + } + return findings; +} + +if (import.meta.main) { + const args = parseScriptArgs(Bun.argv.slice(2)); + const findings = await sourceHeaderFindings(repoRoot()); + report( + { + ok: findings.length === 0, + script: SCRIPT, + summary: + findings.length === 0 + ? 'every shipped source file opens with a 1–4 line responsibility header' + : `${String(findings.length)} source header finding(s)`, + findings, + }, + args.json, + ); +} diff --git a/scripts/tmp-probe-headers.ts b/scripts/tmp-probe-headers.ts new file mode 100644 index 000000000..9e55b1a7a --- /dev/null +++ b/scripts/tmp-probe-headers.ts @@ -0,0 +1,18 @@ +import { corpus } from './lib/corpus'; +import { repoRoot } from './lib/run'; + +const files = await corpus(repoRoot(), 'shipped'); +const shapes = new Map(); +for (const f of files) { + const lines = f.source.split(/\r?\n/); + let at = 0; + while (at < lines.length && (lines[at]?.startsWith('#!') || lines[at]?.trim() === '')) at += 1; + const first = lines[at]?.trimStart() ?? ''; + if (!first.startsWith('/*')) continue; + const close = first.indexOf('*/'); + const key = close === -1 ? 'multi-line' : `one-line(${first.slice(0, 40)})`; + const arr = shapes.get(key) ?? []; + arr.push(f.path); + shapes.set(key, arr); +} +for (const [k, v] of shapes) console.log(k, '=>', v.length, v.slice(0, 3).join(', ')); diff --git a/wiki/Error-Codes.md b/wiki/Error-Codes.md index 8529fec2e..621260d08 100644 --- a/wiki/Error-Codes.md +++ b/wiki/Error-Codes.md @@ -24,7 +24,7 @@ The HTTP body is the same content as an RFC 9457 problem document — `type`, `t | Registration | a code exists when its owning package calls `registerErrorCodes()`. That one call is what makes it explainable, unique and documented-or-fail — a code emitted as a `Finding` rather than thrown is registered the same way | | Enforcement | the `errors` step of `x verify` fails on an empty or advice-only `fix` (`X_ERROR_FIX_INVALID`), on a `fix` citing a file the repo does not have (`X_ERROR_FIX_PATH_MISSING`), on a declared code with no row on this page (`X_ERROR_CODE_UNDOCUMENTED`), on a row this page presents as live that no package registers (`X_ERROR_CODE_UNREGISTERED`), and on a `code:` reached through a name nothing in its own file declares (`X_ERROR_CODE_UNRESOLVED`) | -`As of 2026-10` every code above [Reserved codes](#reserved-codes) resolves through `x errors explain`, with one exception the gate knows about: this repository's own gate scripts (`X_BOUNDARY_VIOLATION`, `X_ROADMAP_*`, `X_REFERENCE_APP_*`, `X_TRUST_*`, `X_CATALOG_CARRIER_UNSCANNED`, `X_CATCH_RENDER_PIN_STALE`, `X_CATCH_RENDER_UNSAFE`, `X_CATCH_RENDER_UNSCANNED`, `X_CONFIG_KEY_UNREAD`, `X_CONFIG_READERS_UNSCANNED`, `X_CONFIG_READER_PIN_STALE`, `X_DECLARED_KEY_UNREAD`, `X_DECLARATION_READER_PIN_STALE`, `X_DECLARATION_READERS_UNSCANNED`, `X_DOC_COMMAND_PIN_EXCEEDED`, `X_DOC_CONFIG_KEY_UNKNOWN`, `X_DOC_CONFIG_ALLOWANCE_STALE`, `X_SCAFFOLD_VERIFY_UNREADABLE`, `X_TIER_FLOOR_STALE`, `X_TIER_FLOOR_UNDECLARED`, `X_SCAFFOLD_OVERRIDES_EMPTY`, `X_SCAFFOLD_GATE_RED`, `X_SCAFFOLD_FIRST_RUN_FAILED`, `X_SETUP_INSTALL_FAILED`, `X_ADMIN_FLATTENER_VIOLATION`, `X_CONFIG_IMPORT_OUTSIDE_LOADER`, `X_ERROR_RENDER_UNSAFE`, `X_ERROR_STATUS_MISSING`, `X_ERROR_STATUS_BACKLOG_STALE`, `X_ERROR_STATUS_UNKNOWN_CODE`, `X_CATALOG_KEY_UNREACHABLE`, `X_PUBLISH_LIST_INCOMPLETE`, `X_PUBLISH_LIST_UNKNOWN`, `X_BENCH_CLAIM_STALE`, `X_WIKI_TABLE_MALFORMED`, `X_WIKI_TABLE_UNSCANNED`, `X_FRAME_DOCS_STALE`, `X_CHART_VERSION_STALE`, `X_IMAGE_LIBC_MISMATCH`, `X_IMAGE_GUARD_MISSING`, `X_IMAGE_SECRET_UNIGNORED`, `X_DOC_FIX_UNRUNNABLE`, `X_DOC_FIX_UNSCANNED`, `X_DOC_CAUSE_ECHOES_TITLE`, `X_DOC_COMMAND_UNKNOWN`, `X_DOC_COMMAND_ALLOWANCE_STALE`, `X_DOC_COMMAND_UNSCANNED`, `X_DOC_COMMAND_PIN_STALE`, `X_DOC_GATE_STEPS_STALE`, `X_DOC_GATE_STEPS_UNSCANNED`, `X_DOC_SETUP_COMMAND_STALE`, `X_DOC_SETUP_COMMANDS_UNSCANNED`, `X_DOC_SETUP_COMMAND_STALE`, `X_DOC_SETUP_COMMANDS_UNSCANNED`, `X_README_EXAMPLE_UNCOMPILED`, `X_README_EXAMPLE_PIN_STALE`, `X_README_EXAMPLE_UNSCANNED`, `X_VERSION_STAMP_STALE`, `X_VERSION_STAMP_DUPLICATE`, `X_VERSION_LOCKSTEP_BROKEN`, `X_VERSION_STAMP_UNSCANNED`, `X_COVERAGE_BELOW`, `X_COVERAGE_PIN_STALE`, `X_COVERAGE_UNMEASURED`, `X_TEST_TYPECHECK_REGRESSED`, `X_TEST_TYPECHECK_PIN_STALE`, `X_TEST_TYPECHECK_UNSCANNED`, `X_DOC_FILE_COUNT_STALE`, `X_DOC_FILE_COUNT_UNSCANNED`, `X_DOC_RELEASE_FACT_STALE`, `X_DOC_RELEASE_FACT_UNSCANNED`, `X_DOC_PACKAGE_GRAPH_STALE`, `X_DOC_PACKAGE_GRAPH_UNSCANNED`, `X_TEST_FIX_UNRUNNABLE`, `X_TEST_FIX_PIN_STALE`, `X_TEST_FIX_UNSCANNED`, `X_TEST_THROW_NOT_THROWN`, `X_TEST_BARE_ERROR`, `X_TEST_BARE_ERROR_PIN_STALE`, `X_TEST_BARE_ERROR_UNSCANNED`, `X_RELEASE_VERSION_UNSTATED`, `X_RELEASE_FLAG_UNKNOWN`, `X_RELEASE_TREE_DIRTY`, `X_RELEASE_VERSION_INVALID`, `X_DOC_CHANGELOG_SECTION_INVALID`, `X_DOC_CHANGELOG_UNRELEASED_BREAKING`, `X_DOC_MIGRATION_COUNT_STALE`, `X_DOC_MIGRATION_SECTION_MISSING`, `X_DOC_MIGRATION_UNSCANNED`, `X_DOC_MIGRATION_UNPAIRED`, `X_DOC_MIGRATION_RANGE_STALE`, `X_RELEASE_UNRELEASED_MISSING`, `X_RELEASE_TAG_UNREACHABLE`, `X_RELEASE_WINDOW_UNREADABLE`, `X_RELEASE_HEAD_NOT_MAIN`, `X_HEALTH_ROW_UNREADABLE`, `X_LOCKFILE_STALE`, `X_REGISTRY_BOOTSTRAP_OWED`, `X_REGISTRY_LATEST_DRIFT`, `X_REGISTRY_UNATTESTED`, `X_REGISTRY_UNREACHABLE`, `X_REGISTRY_VERSION_BEHIND`, `X_GATE_CODE_UNDOCUMENTED`, `X_GATE_CODE_BACKLOG_STALE`, `X_SIDE_EFFECTS_*`, `X_DEAD_DOCS_HOST`, `X_DEAD_DOCS_HOST_PIN_STALE`, `X_DEAD_DOCS_HOST_UNSCANNED`, `X_SECRET_COMPARED_UNSAFELY`, `X_SECRET_COMPARE_PIN_STALE`, `X_SECRET_COMPARE_UNSCANNED`, `X_VERSION_STAMP_PIN_STALE`, `X_WORKSPACE_MANIFEST_UNREADABLE`, `X_CONFIG_KEY_READER_AMBIGUOUS`, `X_PROTO_CHAIN_INDEX`, `X_PROTO_CHAIN_INDEX_PIN_STALE`, `X_PROTO_CHAIN_INDEX_UNSCANNED`, `X_NODE_IMPORT_UNEXPLAINED`, `X_NODE_IMPORT_PIN_STALE`, `X_NODE_IMPORT_UNSCANNED`, `X_FLIGHT_RANDOM_UNINJECTED`, `X_FLIGHT_SECOND_CURVE`, `X_HELPER_COPY`, `X_NOT_IMPLEMENTED_COPY`, `X_NOT_IMPLEMENTED_COPY_UNSCANNED`, `X_FRAMEWORK_TABLE_UNAPPLIED`, `X_FRAMEWORK_TABLE_UNSCANNED`, `X_SQL_LITERAL_COPY`, `X_SQL_LITERAL_UNSCANNED`, `X_INDEX_ORDER_UNGUARDED`, `X_INDEX_ORDER_PIN_STALE`, `X_INDEX_ORDER_PIN_UNEXPLAINED`, `X_INDEX_ORDER_UNSCANNED`, `X_SKIP_CLEANUP_UNREACHED`, `X_SKIP_CLEANUP_UNSCANNED`, `X_TEST_DISCOVERY_UNSEEN`, `X_TEST_DISCOVERY_UNSCANNED`, `X_PROBE_DATABASE_FIXED`, `X_SPAWN_UNTIMED`, `X_SPAWN_TIMEOUT_PIN_STALE`, `X_SPAWN_TIMEOUT_PIN_UNEXPLAINED`, `X_SPAWN_TIMED_OUT`, `X_FINITE_BOUND_UNCHECKED`, `X_FINITE_BOUND_PIN_STALE`, `X_FINITE_BOUND_UNSCANNED`, `X_VOCABULARY_REDECLARED`, `X_VOCABULARY_UNSCANNED`, `X_FROZEN_RECORD_INFERRED`, `X_FROZEN_RECORD_UNSCANNED`, `X_CONFIG_READER_PIN_UNEXPLAINED`, `X_SECRET_COMPARE_PIN_UNEXPLAINED`, `X_PROTO_CHAIN_INDEX_PIN_UNEXPLAINED`, `X_FIX_SHELL_ARG_UNSCREENED`, `X_FIX_SHELL_ARG_PIN_STALE`, `X_FIX_SHELL_ARG_PIN_UNEXPLAINED`, `X_FIX_SHELL_ARG_UNSCANNED`, `X_BROWSER_TRANSPORT_UNSCANNED`, `X_CHANNEL_LITERAL`, `X_CHANNEL_LITERAL_UNSCANNED`, `X_SEAL_CALL_OUTSIDE_CORE`, `X_SEAL_CALL_UNSCANNED`, `X_GUARD_LOAD_FAILED`, `X_BUDGET_RAISE_UNSTATED`, `X_NEW_ERROR_CODE_INVALID`, `X_NEW_ERROR_CODE_EXISTS`, `X_NEW_ERROR_CODE_PATTERN_UNKNOWN`, `X_BUDGET_BASE_MISSING`, `X_PIN_RAISE_UNSTATED`, `X_PIN_BASE_MISSING`, `X_CORPUS_UNSCANNED`, `X_DOC_UNRELEASED_STALE`, `X_DOC_UNRELEASED_PIN_STALE`, `X_LLMS_TXT_DRIFT`, `X_GUARDS_DOC_DRIFT`, `X_GUARDS_DOC_UNSCANNED`, `X_CLAUDE_MD_OVERSIZE`, `X_CLAUDE_MD_UNSCANNED`, `X_CONFIG_READER_APP_UNREAD`, `X_FIX_PROSE`, `X_FIX_PROSE_PIN_STALE`, `X_FIX_PROSE_PIN_UNEXPLAINED`, `X_FIX_PROSE_UNSCANNED`, `X_DOC_PATH_DEAD`, `X_ROOT_BIN_REINTRODUCED`, `X_VERIFY_PART_UNNAMED`, `X_PLAN_STATUS_INVALID`, `X_PLAN_STATUS_UNSCANNED`, `X_URL_PATHNAME_AS_PATH`, `X_RELATIVE_PATH_NOT_POSIX`, `X_SET_COOKIE_HAND_BUILT`, `X_SET_COOKIE_PIN_STALE`, `X_SET_COOKIE_UNSCANNED`, `X_FACTORY_NAME_SPELLING`, `X_CORE_THROWER_UNCALLED`, `X_CORE_THROWER_PIN_STALE`, `X_CORE_THROWERS_UNSCANNED`, `X_SQL_EXPORT_UNREAD`, `X_SQL_EXPORT_UNSCANNED`, `X_WIKI_EXAMPLE_UNCOMPILED`, `X_WIKI_EXAMPLE_PIN_STALE`, `X_WIKI_EXAMPLE_UNSCANNED`, `X_WIKI_ANCHOR_DEAD`, `X_WIKI_ANCHOR_UNSCANNED`, `X_KNOWN_GAP_LINK_MALFORMED`, `X_KNOWN_GAP_UNSCANNED`, `X_DOC_UID_STALE`, `X_DOC_UID_UNSCANNED`, `X_DOC_BOUNDARY_RULES_STALE`, `X_DOC_BOUNDARY_RULES_UNSCANNED`) never ship, so no package may own them. See [Troubleshooting](Troubleshooting) for symptom-first triage and [CLI reference](CLI-Reference) for the commands named in the fixes. +`As of 2026-10` every code above [Reserved codes](#reserved-codes) resolves through `x errors explain`, with one exception the gate knows about: this repository's own gate scripts (`X_BOUNDARY_VIOLATION`, `X_ROADMAP_*`, `X_REFERENCE_APP_*`, `X_TRUST_*`, `X_CATALOG_CARRIER_UNSCANNED`, `X_CATCH_RENDER_PIN_STALE`, `X_CATCH_RENDER_UNSAFE`, `X_CATCH_RENDER_UNSCANNED`, `X_CONFIG_KEY_UNREAD`, `X_CONFIG_READERS_UNSCANNED`, `X_CONFIG_READER_PIN_STALE`, `X_DECLARED_KEY_UNREAD`, `X_DECLARATION_READER_PIN_STALE`, `X_DECLARATION_READERS_UNSCANNED`, `X_DOC_COMMAND_PIN_EXCEEDED`, `X_DOC_CONFIG_KEY_UNKNOWN`, `X_DOC_CONFIG_ALLOWANCE_STALE`, `X_SCAFFOLD_VERIFY_UNREADABLE`, `X_TIER_FLOOR_STALE`, `X_TIER_FLOOR_UNDECLARED`, `X_SCAFFOLD_OVERRIDES_EMPTY`, `X_SCAFFOLD_GATE_RED`, `X_SCAFFOLD_FIRST_RUN_FAILED`, `X_SETUP_INSTALL_FAILED`, `X_ADMIN_FLATTENER_VIOLATION`, `X_CONFIG_IMPORT_OUTSIDE_LOADER`, `X_ERROR_RENDER_UNSAFE`, `X_ERROR_STATUS_MISSING`, `X_ERROR_STATUS_BACKLOG_STALE`, `X_ERROR_STATUS_UNKNOWN_CODE`, `X_CATALOG_KEY_UNREACHABLE`, `X_PUBLISH_LIST_INCOMPLETE`, `X_PUBLISH_LIST_UNKNOWN`, `X_BENCH_CLAIM_STALE`, `X_WIKI_TABLE_MALFORMED`, `X_WIKI_TABLE_UNSCANNED`, `X_FRAME_DOCS_STALE`, `X_CHART_VERSION_STALE`, `X_IMAGE_LIBC_MISMATCH`, `X_IMAGE_GUARD_MISSING`, `X_IMAGE_SECRET_UNIGNORED`, `X_DOC_FIX_UNRUNNABLE`, `X_DOC_FIX_UNSCANNED`, `X_DOC_CAUSE_ECHOES_TITLE`, `X_DOC_COMMAND_UNKNOWN`, `X_DOC_COMMAND_ALLOWANCE_STALE`, `X_DOC_COMMAND_UNSCANNED`, `X_DOC_COMMAND_PIN_STALE`, `X_DOC_GATE_STEPS_STALE`, `X_DOC_GATE_STEPS_UNSCANNED`, `X_DOC_SETUP_COMMAND_STALE`, `X_DOC_SETUP_COMMANDS_UNSCANNED`, `X_DOC_SETUP_COMMAND_STALE`, `X_DOC_SETUP_COMMANDS_UNSCANNED`, `X_README_EXAMPLE_UNCOMPILED`, `X_README_EXAMPLE_PIN_STALE`, `X_README_EXAMPLE_UNSCANNED`, `X_VERSION_STAMP_STALE`, `X_VERSION_STAMP_DUPLICATE`, `X_VERSION_LOCKSTEP_BROKEN`, `X_VERSION_STAMP_UNSCANNED`, `X_COVERAGE_BELOW`, `X_COVERAGE_PIN_STALE`, `X_COVERAGE_UNMEASURED`, `X_TEST_TYPECHECK_REGRESSED`, `X_TEST_TYPECHECK_PIN_STALE`, `X_TEST_TYPECHECK_UNSCANNED`, `X_DOC_FILE_COUNT_STALE`, `X_DOC_FILE_COUNT_UNSCANNED`, `X_DOC_RELEASE_FACT_STALE`, `X_DOC_RELEASE_FACT_UNSCANNED`, `X_DOC_PACKAGE_GRAPH_STALE`, `X_DOC_PACKAGE_GRAPH_UNSCANNED`, `X_TEST_FIX_UNRUNNABLE`, `X_TEST_FIX_PIN_STALE`, `X_TEST_FIX_UNSCANNED`, `X_TEST_THROW_NOT_THROWN`, `X_TEST_BARE_ERROR`, `X_TEST_BARE_ERROR_PIN_STALE`, `X_TEST_BARE_ERROR_UNSCANNED`, `X_RELEASE_VERSION_UNSTATED`, `X_RELEASE_FLAG_UNKNOWN`, `X_RELEASE_TREE_DIRTY`, `X_RELEASE_VERSION_INVALID`, `X_DOC_CHANGELOG_SECTION_INVALID`, `X_DOC_CHANGELOG_UNRELEASED_BREAKING`, `X_DOC_MIGRATION_COUNT_STALE`, `X_DOC_MIGRATION_SECTION_MISSING`, `X_DOC_MIGRATION_UNSCANNED`, `X_DOC_MIGRATION_UNPAIRED`, `X_DOC_MIGRATION_RANGE_STALE`, `X_RELEASE_UNRELEASED_MISSING`, `X_RELEASE_TAG_UNREACHABLE`, `X_RELEASE_WINDOW_UNREADABLE`, `X_RELEASE_HEAD_NOT_MAIN`, `X_HEALTH_ROW_UNREADABLE`, `X_LOCKFILE_STALE`, `X_REGISTRY_BOOTSTRAP_OWED`, `X_REGISTRY_LATEST_DRIFT`, `X_REGISTRY_UNATTESTED`, `X_REGISTRY_UNREACHABLE`, `X_REGISTRY_VERSION_BEHIND`, `X_GATE_CODE_UNDOCUMENTED`, `X_GATE_CODE_BACKLOG_STALE`, `X_SIDE_EFFECTS_*`, `X_DEAD_DOCS_HOST`, `X_DEAD_DOCS_HOST_PIN_STALE`, `X_DEAD_DOCS_HOST_UNSCANNED`, `X_SECRET_COMPARED_UNSAFELY`, `X_SECRET_COMPARE_PIN_STALE`, `X_SECRET_COMPARE_UNSCANNED`, `X_VERSION_STAMP_PIN_STALE`, `X_WORKSPACE_MANIFEST_UNREADABLE`, `X_CONFIG_KEY_READER_AMBIGUOUS`, `X_PROTO_CHAIN_INDEX`, `X_PROTO_CHAIN_INDEX_PIN_STALE`, `X_PROTO_CHAIN_INDEX_UNSCANNED`, `X_NODE_IMPORT_UNEXPLAINED`, `X_NODE_IMPORT_PIN_STALE`, `X_NODE_IMPORT_UNSCANNED`, `X_FLIGHT_RANDOM_UNINJECTED`, `X_FLIGHT_SECOND_CURVE`, `X_HELPER_COPY`, `X_NOT_IMPLEMENTED_COPY`, `X_NOT_IMPLEMENTED_COPY_UNSCANNED`, `X_FRAMEWORK_TABLE_UNAPPLIED`, `X_FRAMEWORK_TABLE_UNSCANNED`, `X_SQL_LITERAL_COPY`, `X_SQL_LITERAL_UNSCANNED`, `X_INDEX_ORDER_UNGUARDED`, `X_INDEX_ORDER_PIN_STALE`, `X_INDEX_ORDER_PIN_UNEXPLAINED`, `X_INDEX_ORDER_UNSCANNED`, `X_SKIP_CLEANUP_UNREACHED`, `X_SKIP_CLEANUP_UNSCANNED`, `X_TEST_DISCOVERY_UNSEEN`, `X_TEST_DISCOVERY_UNSCANNED`, `X_PROBE_DATABASE_FIXED`, `X_SPAWN_UNTIMED`, `X_SPAWN_TIMEOUT_PIN_STALE`, `X_SPAWN_TIMEOUT_PIN_UNEXPLAINED`, `X_SPAWN_TIMED_OUT`, `X_FINITE_BOUND_UNCHECKED`, `X_FINITE_BOUND_PIN_STALE`, `X_FINITE_BOUND_UNSCANNED`, `X_VOCABULARY_REDECLARED`, `X_VOCABULARY_UNSCANNED`, `X_FROZEN_RECORD_INFERRED`, `X_FROZEN_RECORD_UNSCANNED`, `X_CONFIG_READER_PIN_UNEXPLAINED`, `X_SECRET_COMPARE_PIN_UNEXPLAINED`, `X_PROTO_CHAIN_INDEX_PIN_UNEXPLAINED`, `X_FIX_SHELL_ARG_UNSCREENED`, `X_FIX_SHELL_ARG_PIN_STALE`, `X_FIX_SHELL_ARG_PIN_UNEXPLAINED`, `X_FIX_SHELL_ARG_UNSCANNED`, `X_BROWSER_TRANSPORT_UNSCANNED`, `X_CHANNEL_LITERAL`, `X_CHANNEL_LITERAL_UNSCANNED`, `X_SEAL_CALL_OUTSIDE_CORE`, `X_SEAL_CALL_UNSCANNED`, `X_GUARD_LOAD_FAILED`, `X_BUDGET_RAISE_UNSTATED`, `X_NEW_ERROR_CODE_INVALID`, `X_NEW_ERROR_CODE_EXISTS`, `X_NEW_ERROR_CODE_PATTERN_UNKNOWN`, `X_BUDGET_BASE_MISSING`, `X_PIN_RAISE_UNSTATED`, `X_PIN_BASE_MISSING`, `X_CORPUS_UNSCANNED`, `X_DOC_UNRELEASED_STALE`, `X_DOC_UNRELEASED_PIN_STALE`, `X_LLMS_TXT_DRIFT`, `X_GUARDS_DOC_DRIFT`, `X_GUARDS_DOC_UNSCANNED`, `X_CLAUDE_MD_OVERSIZE`, `X_CLAUDE_MD_UNSCANNED`, `X_CONFIG_READER_APP_UNREAD`, `X_FIX_PROSE`, `X_FIX_PROSE_PIN_STALE`, `X_FIX_PROSE_PIN_UNEXPLAINED`, `X_FIX_PROSE_UNSCANNED`, `X_DOC_PATH_DEAD`, `X_ROOT_BIN_REINTRODUCED`, `X_VERIFY_PART_UNNAMED`, `X_PLAN_STATUS_INVALID`, `X_PLAN_STATUS_UNSCANNED`, `X_URL_PATHNAME_AS_PATH`, `X_RELATIVE_PATH_NOT_POSIX`, `X_SET_COOKIE_HAND_BUILT`, `X_SET_COOKIE_PIN_STALE`, `X_SET_COOKIE_UNSCANNED`, `X_FACTORY_NAME_SPELLING`, `X_CORE_THROWER_UNCALLED`, `X_CORE_THROWER_PIN_STALE`, `X_CORE_THROWERS_UNSCANNED`, `X_SQL_EXPORT_UNREAD`, `X_SQL_EXPORT_UNSCANNED`, `X_WIKI_EXAMPLE_UNCOMPILED`, `X_WIKI_EXAMPLE_PIN_STALE`, `X_WIKI_EXAMPLE_UNSCANNED`, `X_WIKI_ANCHOR_DEAD`, `X_WIKI_ANCHOR_UNSCANNED`, `X_KNOWN_GAP_LINK_MALFORMED`, `X_KNOWN_GAP_UNSCANNED`, `X_DOC_UID_STALE`, `X_DOC_UID_UNSCANNED`, `X_DOC_BOUNDARY_RULES_STALE`, `X_DOC_BOUNDARY_RULES_UNSCANNED`, `X_SOURCE_HEADER_MISSING`, `X_SOURCE_HEADER_OUT_OF_CONTRACT`, `X_SOURCE_HEADER_PIN_STALE`, `X_SOURCE_HEADER_UNSCANNED`, `X_DEPENDENCY_NOT_ALLOWLISTED`, `X_DEPENDENCY_ALLOWLIST_STALE`, `X_DEPENDENCY_UNSCANNED`) never ship, so no package may own them. See [Troubleshooting](Troubleshooting) for symptom-first triage and [CLI reference](CLI-Reference) for the commands named in the fixes. ## Core and runtime @@ -870,6 +870,13 @@ Two sets override the table, in `failures.ts`: | Code | Means | Typical cause | Fix | |---|---|---|---| +| `X_SOURCE_HEADER_MISSING` | a shipped source file opens with no header comment | the file states no single responsibility, so the next reader has to infer what it owns from its body | add a 1–4 line header comment to the file the cause names, stating its single responsibility | +| `X_SOURCE_HEADER_OUT_OF_CONTRACT` | a shipped source file's leading header is outside the 1–4 line responsibility contract | the first paragraph is empty, carries no prose, or runs past four lines — the responsibility is buried or absent | rewrite the leading header of the file the cause names as 1–4 lines of prose stating its single responsibility | +| `X_SOURCE_HEADER_PIN_STALE` | a source-header pin is above the number of violations the tree now holds | the file was repaired and the pin was not lowered, so the slack would let a new violation in | delete or lower the row for the file the cause names in `scripts/lib/source-header-pins.ts`, then `bun run scripts/source-headers.ts --json` | +| `X_SOURCE_HEADER_UNSCANNED` | the source-header guard read no shipped source file | the corpus glob matched nothing, so every file reported zero and the rule enforced nothing | edit `PATTERNS` in `scripts/lib/corpus.ts` so it matches this repo layout, then `bun run scripts/source-headers.ts --json` | +| `X_DEPENDENCY_NOT_ALLOWLISTED` | a workspace manifest declares an external dependency with no allowlist row | a new dependency was added without the stated reason root `CLAUDE.md` requires | add the named manifest, field and dependency to `scripts/lib/dependency-allowlist-data.ts` with a reason, then `bun run scripts/dependency-allowlist.ts --json` | +| `X_DEPENDENCY_ALLOWLIST_STALE` | an allowlist row names a dependency no manifest declares | the dependency was removed and the row stayed, so a later declaration of the same name would be waved through | delete the row the cause names from `scripts/lib/dependency-allowlist-data.ts`, then `bun run scripts/dependency-allowlist.ts --json` | +| `X_DEPENDENCY_UNSCANNED` | the dependency guard read no workspace manifest, or no manifest declared a dependency | the root manifest could not be read, or the workspace globs matched nothing — the allowlist then enforces nothing | run `bun run scripts/dependency-allowlist.ts --json` from the repository root | | `X_CLI_UNKNOWN_COMMAND` | not a command | a typo | `x help` — the suggestion is in `fix` | | `X_CLI_BAD_FLAG` | flag or positional rejected | unknown flag, a bad value (`--port abc`, `--workers 4.9` — refused, never coerced), a required positional left out, or a mandatory-subcommand command called bare. A missing positional says so and names it, rather than inventing a flag that does not exist. A **well-formed** argv whose answer is still no is not this code — a `dev`-tier seed refused against production is `X_SEED_ENVIRONMENT`, because a flag code says "you typed it wrong" and sends the reader to `x help`, which is the wrong instruction there | `x help `. **Use that form and not `--help` on `db` and `mcp`** — they declare no default subcommand, and the subcommand is resolved *before* the flag loop, so `x db --help` raises this same error again. Its `fix:` said exactly that until 2026-08 | | `X_CLI_UNEXPECTED` | the CLI itself failed | a bug, or a broken environment | `x doctor --json` and attach it to an issue | From 078d7d758aba6edea086576d72e67a810babad91 Mon Sep 17 00:00:00 2001 From: "developerz-ai[bot]" <286432749+developerz-ai[bot]@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:40:55 +0000 Subject: [PATCH 2/2] chore(babysit): land scripts/lib/source-header-pins.ts (+2 more, 1 removed) Follow-up intent: Address review findings on developerz-ai/ultimate#884 Dz-Task-Id: tsk_32ed0877c196813b4df8aff4b86abd26 --- scripts/lib/source-header-pins.ts | 5 --- scripts/source-headers.test.ts | 54 +++++++++++++++++++++++++++++++ scripts/source-headers.ts | 31 ++++++++++++++---- scripts/tmp-probe-headers.ts | 18 ----------- 4 files changed, 78 insertions(+), 30 deletions(-) delete mode 100644 scripts/tmp-probe-headers.ts diff --git a/scripts/lib/source-header-pins.ts b/scripts/lib/source-header-pins.ts index 78841b6f6..f3c05224b 100644 --- a/scripts/lib/source-header-pins.ts +++ b/scripts/lib/source-header-pins.ts @@ -32,7 +32,6 @@ const TOO_LONG = /** One row per file; the count may fall and may never rise. */ export const SOURCE_HEADER_PINS: Readonly> = { 'packages/i18n/src/locale-path.ts': { count: 1, reason: TOO_LONG }, - 'packages/i18n/src/index.ts': { count: 1, reason: MISSING }, 'packages/i18n/src/subset.ts': { count: 1, reason: TOO_LONG }, 'packages/cache/src/graph.ts': { count: 1, reason: TOO_LONG }, 'packages/cache/src/partial-log.ts': { count: 1, reason: TOO_LONG }, @@ -59,7 +58,6 @@ export const SOURCE_HEADER_PINS: Readonly> = { 'packages/db/src/statement-shape.ts': { count: 1, reason: TOO_LONG }, 'packages/db/src/invariant-errors.ts': { count: 1, reason: TOO_LONG }, 'packages/time/src/plain-date.ts': { count: 1, reason: TOO_LONG }, - 'packages/time/src/index.ts': { count: 1, reason: MISSING }, 'packages/admin/src/screen-action.tsx': { count: 1, reason: TOO_LONG }, 'packages/admin/src/list.tsx': { count: 1, reason: TOO_LONG }, 'packages/admin/src/batch-bar.tsx': { count: 1, reason: TOO_LONG }, @@ -67,7 +65,6 @@ export const SOURCE_HEADER_PINS: Readonly> = { 'packages/admin/src/layout.tsx': { count: 1, reason: TOO_LONG }, 'packages/admin/src/describe.ts': { count: 1, reason: TOO_LONG }, 'packages/admin/src/list-request.ts': { count: 1, reason: TOO_LONG }, - 'packages/query/src/errors.ts': { count: 1, reason: MISSING }, 'packages/query/src/cursor-value.ts': { count: 1, reason: TOO_LONG }, 'packages/query/src/sealed-shape.ts': { count: 1, reason: TOO_LONG }, 'packages/query/src/openapi.ts': { count: 1, reason: TOO_LONG }, @@ -299,7 +296,6 @@ export const SOURCE_HEADER_PINS: Readonly> = { 'packages/ai/src/bounds-fixture.ts': { count: 1, reason: TOO_LONG }, 'packages/ai/src/fix-line-fixture.ts': { count: 1, reason: TOO_LONG }, 'packages/money/src/factor.ts': { count: 1, reason: TOO_LONG }, - 'packages/money/src/index.ts': { count: 1, reason: MISSING }, 'packages/mcp/src/meta-surface.ts': { count: 1, reason: TOO_LONG }, 'packages/mcp/src/surface-budget.ts': { count: 1, reason: TOO_LONG }, 'packages/mcp/src/idempotency-arg.ts': { count: 1, reason: TOO_LONG }, @@ -314,7 +310,6 @@ export const SOURCE_HEADER_PINS: Readonly> = { 'packages/mail/src/driver.ts': { count: 1, reason: TOO_LONG }, 'packages/pwa/src/pages-cache-source.ts': { count: 1, reason: TOO_LONG }, 'packages/pwa/src/push-hosts.ts': { count: 1, reason: TOO_LONG }, - 'packages/pwa/src/index.ts': { count: 1, reason: MISSING }, 'packages/realtime/src/error-titles.ts': { count: 1, reason: TOO_LONG }, 'packages/realtime/src/close-codes.ts': { count: 1, reason: TOO_LONG }, 'packages/realtime/src/socket.ts': { count: 1, reason: TOO_LONG }, diff --git a/scripts/source-headers.test.ts b/scripts/source-headers.test.ts index 32e787b03..9c9bad6b8 100644 --- a/scripts/source-headers.test.ts +++ b/scripts/source-headers.test.ts @@ -42,6 +42,60 @@ describe('a source header in the 1–4 line contract', () => { }); }); +describe('an inline block header', () => { + test('a one-line block header passes', () => { + expect(readSourceHeader('x.ts', '/* One responsibility. */\nexport const a = 1;\n')).toEqual({ + path: 'x.ts', + lines: ['One responsibility.'], + }); + }); + + test('a one-line JSDoc header passes', () => { + expect(readSourceHeader('x.ts', '/** One responsibility. */\nexport const a = 1;\n')).toEqual({ + path: 'x.ts', + lines: ['One responsibility.'], + }); + }); + + test('code after the closing delimiter is not prose', () => { + expect( + readSourceHeader('x.ts', '/* One responsibility. */ export const a = 1;\n').lines, + ).toEqual(['One responsibility.']); + }); + + test('prose on the opening delimiter line counts', () => { + const source = ['/* One responsibility,', ' * carried on.', ' */', 'export const a = 1;'].join( + '\n', + ); + expect(readSourceHeader('x.ts', source).lines).toEqual(['One responsibility,', 'carried on.']); + }); + + test('prose before the closing delimiter counts', () => { + const source = [ + '/*', + ' * One responsibility,', + ' * stated here. */', + 'export const a = 1;', + ].join('\n'); + expect(readSourceHeader('x.ts', source).lines).toEqual(['One responsibility,', 'stated here.']); + }); + + test('delimiter-line prose counts toward the four-line contract', () => { + const source = ['/* One.', ' * Two.', ' * Three.', ' * Four.', ' * Five.', ' */'].join('\n'); + expect(readSourceHeader('x.ts', source).problem).toBe('too-long'); + }); + + test('a bare one-line delimiter is still empty', () => { + expect(readSourceHeader('x.ts', '/* */\nexport const a = 1;\n').problem).toBe('empty'); + expect(readSourceHeader('x.ts', '/***/\nexport const a = 1;\n').problem).toBe('empty'); + }); + + test('a blank line still ends the first paragraph', () => { + const source = ['/*', ' * One responsibility.', ' *', ' * Why: longer.', ' */'].join('\n'); + expect(readSourceHeader('x.ts', source).lines).toEqual(['One responsibility.']); + }); +}); + describe('the repository tree', () => { test('is at or under its per-file pin', async () => { const findings = await sourceHeaderFindings(repoRoot()); diff --git a/scripts/source-headers.ts b/scripts/source-headers.ts index cdd649333..f86831f74 100644 --- a/scripts/source-headers.ts +++ b/scripts/source-headers.ts @@ -24,11 +24,15 @@ export interface SourceHeader { readonly problem?: SourceHeaderProblem; } +/** A block-comment line's prose: the leading `*` bullet and the surrounding whitespace stripped. */ +const prose = (line: string): string => line.replace(/^\s*\*\s?/, '').trim(); + /** * The leading header: a line-comment block or a block-comment block, after an optional shebang. * - * The first PARAGRAPH is what the contract judges. Later paragraphs are the why, and a block - * header's delimiters are never prose. + * The first PARAGRAPH is what the contract judges. Later paragraphs are the why. A block header's + * delimiters are never prose, the text between them always is — on the delimiter lines included, + * so a one-line block header keeps its line and delimiter-line prose counts toward the four. */ export function readSourceHeader(path: string, source: string): SourceHeader { const lines = source.split(/\r?\n/); @@ -37,12 +41,25 @@ export function readSourceHeader(path: string, source: string): SourceHeader { const first = lines[at]?.trimStart() ?? ''; if (first.startsWith('/*')) { const end = lines.findIndex((line, index) => index >= at && line.includes('*/')); - const body = lines.slice(at + 1, end === -1 ? lines.length : end); + const stop = end === -1 ? lines.length : end; + // The prose is everything between the delimiters, the delimiter lines included: a one-line + // block header is one line of prose, and text before a closing delimiter is prose wherever + // it sits. A bare delimiter line is not a blank prose line, so it neither starts nor ends a + // paragraph — only a blank line between two delimiters does. const paragraph: string[] = []; - for (const line of body) { - const text = line.replace(/^\s*\*\s?/, '').trim(); - if (text === '') break; - paragraph.push(text); + const open = first.slice(2); + const openText = prose(end === at ? open.slice(0, open.indexOf('*/')) : open); + if (openText !== '') paragraph.push(openText); + let ended = false; + for (let index = at + 1; index < stop && !ended; index += 1) { + const text = prose(lines[index] ?? ''); + if (text === '') ended = true; + else paragraph.push(text); + } + if (end > at && !ended) { + const close = lines[end] ?? ''; + const closeText = prose(close.slice(0, close.indexOf('*/'))); + if (closeText !== '') paragraph.push(closeText); } if (paragraph.length === 0) return { path, lines: [], problem: 'empty' }; return paragraph.length > 4 diff --git a/scripts/tmp-probe-headers.ts b/scripts/tmp-probe-headers.ts deleted file mode 100644 index 9e55b1a7a..000000000 --- a/scripts/tmp-probe-headers.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { corpus } from './lib/corpus'; -import { repoRoot } from './lib/run'; - -const files = await corpus(repoRoot(), 'shipped'); -const shapes = new Map(); -for (const f of files) { - const lines = f.source.split(/\r?\n/); - let at = 0; - while (at < lines.length && (lines[at]?.startsWith('#!') || lines[at]?.trim() === '')) at += 1; - const first = lines[at]?.trimStart() ?? ''; - if (!first.startsWith('/*')) continue; - const close = first.indexOf('*/'); - const key = close === -1 ? 'multi-line' : `one-line(${first.slice(0, 40)})`; - const arr = shapes.get(key) ?? []; - arr.push(f.path); - shapes.set(key, arr); -} -for (const [k, v] of shapes) console.log(k, '=>', v.length, v.slice(0, 3).join(', '));