From 2dbea4ac7ed447eee6ce7d2b9e118897ba6fc05a Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 12 Jun 2026 18:02:05 -0700 Subject: [PATCH 1/3] Pay down static text witness batch A --- package.json | 1 + scripts/dead-export-report.ts | 321 ++++++++++++++++++ .../hygieneQuarantineGraduation.test.ts | 17 +- .../immutableSnapshotBuilder.test.ts | 54 --- .../dead-export-report/src/consumer.ts | 3 + test/fixtures/dead-export-report/src/index.ts | 3 + .../src/renamed-unused-function.ts | 3 + .../dead-export-report/src/unused-value.ts | 8 + .../dead-export-report/src/used-value.ts | 8 + test/helpers/MarkdownDocument.ts | 130 +++++++ test/unit/domain/trust/domainPurity.test.ts | 80 ++++- .../GitGraphAdapter.gitCasPersistence.test.ts | 110 ++++-- test/unit/scripts/dead-export-report.test.ts | 30 ++ .../documentation-corpus-shape.test.ts | 65 ++-- .../factory-functions-in-tests-shape.test.ts | 30 +- test/unit/scripts/glossary-shape.test.ts | 51 +-- ...ental-index-updater-closeout-shape.test.ts | 52 ++- .../index-builder-on-git-cas-shape.test.ts | 22 +- 18 files changed, 772 insertions(+), 216 deletions(-) create mode 100644 scripts/dead-export-report.ts create mode 100644 test/fixtures/dead-export-report/src/consumer.ts create mode 100644 test/fixtures/dead-export-report/src/index.ts create mode 100644 test/fixtures/dead-export-report/src/renamed-unused-function.ts create mode 100644 test/fixtures/dead-export-report/src/unused-value.ts create mode 100644 test/fixtures/dead-export-report/src/used-value.ts create mode 100644 test/helpers/MarkdownDocument.ts create mode 100644 test/unit/scripts/dead-export-report.test.ts diff --git a/package.json b/package.json index 639b91d61..f11596a1d 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "release:prep": "bash scripts/release-preflight.sh --stage prep-pr", "release:preflight": "bash scripts/release-preflight.sh --stage final-local", "issue:triage": "node scripts/issue-triage-report.ts", + "issue:dead-exports": "node scripts/dead-export-report.ts", "goalpost:guard": "bash scripts/goalpost-guard.sh", "upgrade": "npm run build --silent && node dist/scripts/upgrade-v16-to-v17.js", "install:git-warp": "bash scripts/install-git-warp.sh", diff --git a/scripts/dead-export-report.ts b/scripts/dead-export-report.ts new file mode 100644 index 000000000..d9440aeec --- /dev/null +++ b/scripts/dead-export-report.ts @@ -0,0 +1,321 @@ +#!/usr/bin/env node +import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; +import { extname, relative, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import ts from 'typescript'; + +type ExportKind = + | 'class' + | 'const' + | 'enum' + | 'function' + | 'interface' + | 're-export' + | 'type'; + +export type DeadExportFinding = { + readonly path: string; + readonly name: string; + readonly kind: ExportKind; + readonly identifierReferences: number; +}; + +export type DeadExportReport = { + readonly root: string; + readonly filesScanned: number; + readonly exportsScanned: number; + readonly findings: readonly DeadExportFinding[]; +}; + +type SourceRecord = { + readonly path: string; + readonly relativePath: string; + readonly sourceFile: ts.SourceFile; +}; + +type ExportDeclarationRecord = { + readonly path: string; + readonly name: string; + readonly kind: ExportKind; +}; + +const DEFAULT_SOURCE_ROOT = 'src'; +const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx']); +const IGNORED_DIRECTORIES = new Set(['.git', 'dist', 'node_modules']); +const MARKDOWN_TABLE_HEADER = '| Path | Export | Kind | Identifier refs |'; +const MARKDOWN_TABLE_SEPARATOR = '| --- | --- | --- | ---: |'; + +export function buildDeadExportReport(sourceRoot: string = DEFAULT_SOURCE_ROOT): DeadExportReport { + const root = resolve(sourceRoot); + const records = sourceRecords(root); + const declarations = records.flatMap((record) => exportedDeclarations(record)); + const referenceCounts = identifierReferenceCounts(records); + const findings = declarations + .map((declaration) => findingForDeclaration(referenceCounts, declaration)) + .filter((finding) => finding.identifierReferences === 0) + .sort(compareFindings); + + return Object.freeze({ + root, + filesScanned: records.length, + exportsScanned: declarations.length, + findings, + }); +} + +export function formatDeadExportReport(report: DeadExportReport): string { + const lines = [ + '# Dead Export Candidate Report', + '', + `Source root: ${report.root}`, + `Files scanned: ${report.filesScanned}`, + `Exports scanned: ${report.exportsScanned}`, + `Candidates: ${report.findings.length}`, + '', + MARKDOWN_TABLE_HEADER, + MARKDOWN_TABLE_SEPARATOR, + ]; + + for (const finding of report.findings) { + lines.push(`| ${finding.path} | \`${finding.name}\` | ${finding.kind} | ${finding.identifierReferences} |`); + } + + return `${lines.join('\n')}\n`; +} + +function sourceRecords(root: string): readonly SourceRecord[] { + return sourceFilePaths(root).map((path) => { + const text = readFileSync(path, 'utf8'); + return Object.freeze({ + path, + relativePath: relative(root, path), + sourceFile: ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, scriptKind(path)), + }); + }); +} + +function sourceFilePaths(root: string): readonly string[] { + if (!existsSync(root)) { + return []; + } + + const paths: string[] = []; + collectSourceFilePaths(root, paths); + return paths.sort((left, right) => left.localeCompare(right)); +} + +function collectSourceFilePaths(directory: string, paths: string[]): void { + for (const entry of readdirSync(directory)) { + if (IGNORED_DIRECTORIES.has(entry)) { + continue; + } + + const path = resolve(directory, entry); + const stat = statSync(path); + if (stat.isDirectory()) { + collectSourceFilePaths(path, paths); + continue; + } + if (stat.isFile() && SOURCE_EXTENSIONS.has(extname(path))) { + paths.push(path); + } + } +} + +function scriptKind(path: string): ts.ScriptKind { + const extension = extname(path); + if (extension === '.js') { + return ts.ScriptKind.JS; + } + if (extension === '.jsx') { + return ts.ScriptKind.JSX; + } + if (extension === '.tsx') { + return ts.ScriptKind.TSX; + } + return ts.ScriptKind.TS; +} + +function exportedDeclarations(record: SourceRecord): readonly ExportDeclarationRecord[] { + const declarations: ExportDeclarationRecord[] = []; + for (const statement of record.sourceFile.statements) { + pushExportedStatement(record, statement, declarations); + } + return declarations; +} + +function pushExportedStatement( + record: SourceRecord, + statement: ts.Statement, + declarations: ExportDeclarationRecord[], +): void { + if (ts.isExportDeclaration(statement)) { + pushExportDeclaration(record, statement, declarations); + return; + } + + if (!hasExportModifier(statement)) { + return; + } + + if (ts.isClassDeclaration(statement) && statement.name !== undefined) { + pushDeclaration(record, declarations, statement.name.text, 'class'); + return; + } + if (ts.isFunctionDeclaration(statement) && statement.name !== undefined) { + pushDeclaration(record, declarations, statement.name.text, 'function'); + return; + } + if (ts.isInterfaceDeclaration(statement)) { + pushDeclaration(record, declarations, statement.name.text, 'interface'); + return; + } + if (ts.isTypeAliasDeclaration(statement)) { + pushDeclaration(record, declarations, statement.name.text, 'type'); + return; + } + if (ts.isEnumDeclaration(statement)) { + pushDeclaration(record, declarations, statement.name.text, 'enum'); + return; + } + if (ts.isVariableStatement(statement)) { + pushVariableStatement(record, statement, declarations); + } +} + +function pushExportDeclaration( + record: SourceRecord, + statement: ts.ExportDeclaration, + declarations: ExportDeclarationRecord[], +): void { + const exportClause = statement.exportClause; + if (exportClause === undefined || !ts.isNamedExports(exportClause)) { + return; + } + + for (const element of exportClause.elements) { + const localName = element.propertyName?.text ?? element.name.text; + pushDeclaration(record, declarations, localName, 're-export'); + } +} + +function pushVariableStatement( + record: SourceRecord, + statement: ts.VariableStatement, + declarations: ExportDeclarationRecord[], +): void { + for (const declaration of statement.declarationList.declarations) { + if (ts.isIdentifier(declaration.name)) { + pushDeclaration(record, declarations, declaration.name.text, 'const'); + } + } +} + +function pushDeclaration( + record: SourceRecord, + declarations: ExportDeclarationRecord[], + name: string, + kind: ExportKind, +): void { + declarations.push(Object.freeze({ + path: record.relativePath, + name, + kind, + })); +} + +function hasExportModifier(node: ts.Node): boolean { + if (!ts.canHaveModifiers(node)) { + return false; + } + const modifiers = ts.getModifiers(node); + if (modifiers === undefined) { + return false; + } + return modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword); +} + +function findingForDeclaration( + referenceCounts: ReadonlyMap, + declaration: ExportDeclarationRecord, +): DeadExportFinding { + return Object.freeze({ + path: declaration.path, + name: declaration.name, + kind: declaration.kind, + identifierReferences: referenceCounts.get(declaration.name) ?? 0, + }); +} + +function identifierReferenceCounts(records: readonly SourceRecord[]): ReadonlyMap { + const counts = new Map(); + for (const record of records) { + collectIdentifierReferenceCounts(record.sourceFile, counts); + } + return counts; +} + +function collectIdentifierReferenceCounts(node: ts.Node, counts: Map): void { + if (ts.isIdentifier(node) && isReferenceIdentifier(node)) { + counts.set(node.text, (counts.get(node.text) ?? 0) + 1); + } + ts.forEachChild(node, (child) => { + collectIdentifierReferenceCounts(child, counts); + }); +} + +function isReferenceIdentifier(identifier: ts.Identifier): boolean { + const parent = identifier.parent; + if (parent === undefined) { + return false; + } + if (ts.isImportSpecifier(parent)) { + return parent.name !== identifier && parent.propertyName !== identifier; + } + if (ts.isExportSpecifier(parent)) { + return parent.name !== identifier && parent.propertyName !== identifier; + } + if (ts.isClassDeclaration(parent) && parent.name === identifier) { + return false; + } + if (ts.isFunctionDeclaration(parent) && parent.name === identifier) { + return false; + } + if (ts.isInterfaceDeclaration(parent) && parent.name === identifier) { + return false; + } + if (ts.isTypeAliasDeclaration(parent) && parent.name === identifier) { + return false; + } + if (ts.isEnumDeclaration(parent) && parent.name === identifier) { + return false; + } + if (ts.isVariableDeclaration(parent) && parent.name === identifier) { + return false; + } + if (ts.isParameter(parent) && parent.name === identifier) { + return false; + } + return true; +} + +function compareFindings(left: DeadExportFinding, right: DeadExportFinding): number { + const pathCompare = left.path.localeCompare(right.path); + if (pathCompare !== 0) { + return pathCompare; + } + return left.name.localeCompare(right.name); +} + +function isDirectExecution(): boolean { + const entry = process.argv[1]; + if (entry === undefined) { + return false; + } + return resolve(entry) === fileURLToPath(import.meta.url); +} + +if (isDirectExecution()) { + const sourceRoot = process.argv[2] ?? DEFAULT_SOURCE_ROOT; + process.stdout.write(formatDeadExportReport(buildDeadExportReport(sourceRoot))); +} diff --git a/test/conformance/hygieneQuarantineGraduation.test.ts b/test/conformance/hygieneQuarantineGraduation.test.ts index 797a8d7be..b8dfb6f16 100644 --- a/test/conformance/hygieneQuarantineGraduation.test.ts +++ b/test/conformance/hygieneQuarantineGraduation.test.ts @@ -1,18 +1,27 @@ import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; +import { z } from 'zod'; const MANIFESTS = [ '../../policy/quarantines/HYGIENE-consistent-type-imports.json', '../../policy/quarantines/HYGIENE-restrict-template-expressions.json', ] as const; -function readManifest(relativePath: string): string { - return readFileSync(fileURLToPath(new URL(relativePath, import.meta.url)), 'utf8'); +const hygieneManifestSchema = z.object({ + files: z.array(z.string()), +}).passthrough(); + +type HygieneManifest = z.infer; + +function readManifest(relativePath: string): HygieneManifest { + return hygieneManifestSchema.parse(JSON.parse( + readFileSync(fileURLToPath(new URL(relativePath, import.meta.url)), 'utf8'), + )); } -function expectEmptyFilesArray(manifest: string): void { - expect(manifest).toMatch(/"files"\s*:\s*\[\s*\]/u); +function expectEmptyFilesArray(manifest: HygieneManifest): void { + expect(manifest.files).toEqual([]); } describe('hygiene quarantine graduation', () => { diff --git a/test/conformance/immutableSnapshotBuilder.test.ts b/test/conformance/immutableSnapshotBuilder.test.ts index 451abf784..db4f64843 100644 --- a/test/conformance/immutableSnapshotBuilder.test.ts +++ b/test/conformance/immutableSnapshotBuilder.test.ts @@ -1,6 +1,3 @@ -import { readFileSync } from 'node:fs'; -import { join } from 'node:path'; -import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; import { Dot } from '../../src/domain/crdt/Dot.ts'; import VersionVector from '../../src/domain/crdt/VersionVector.ts'; @@ -13,14 +10,6 @@ import WarpState from '../../src/domain/services/state/WarpState.ts'; import { createTickReceipt } from '../../src/domain/types/TickReceipt.ts'; import { EventId } from '../../src/domain/utils/EventId.ts'; -const REPO_ROOT = fileURLToPath(new URL('../../', import.meta.url)); -const IMMUTABLE_SNAPSHOT_PATH = 'src/domain/services/ImmutableSnapshot.ts'; - -type ForbiddenSourcePattern = { - readonly label: string; - readonly pattern: RegExp; -}; - class ConstructorGuardedValue { #secret: string; @@ -36,41 +25,6 @@ class ConstructorGuardedValue { } } -const FORBIDDEN_SOURCE_PATTERNS: readonly ForbiddenSourcePattern[] = [ - { - label: 'generic object clone', - pattern: /function\s+cloneImmutableObject\s*<\s*T\s*>\s*\(\s*value:\s*object/u, - }, - { - label: 'descriptor-copy allocation', - pattern: /\bObject\.create\b/u, - }, - { - label: 'double-cast preservation', - pattern: /\bas\s+unknown\s+as\s+T\b/u, - }, - { - label: 'generic public snapshot entry point', - pattern: /createImmutableValue\s*<\s*T\s*>\s*\(\s*value:\s*T\s*\)\s*:\s*T/u, - }, - { - label: 'proxy returned as arbitrary T', - pattern: /\bproxy\s+as\s+T\b/u, - }, - { - label: 'frozen clone returned as arbitrary T', - pattern: /Object\.freeze\s*\(\s*cloned\s*\)\s+as\s+T\b/u, - }, - { - label: 'fallback arbitrary object clone', - pattern: /return\s+cloneImmutableObject\s*\(\s*value\s*,\s*seen\s*\)/u, - }, -]; - -function readRepoFile(path: string): string { - return readFileSync(join(REPO_ROOT, path), 'utf8'); -} - function testEvent(lamport: number, patchSha: string): EventId { return new EventId(lamport, 'writer-a', patchSha, 0); } @@ -87,14 +41,6 @@ function receiptArrayFixture(): ReturnType[] { } describe('immutable snapshot builder contract', () => { - it('removes generic clone/freeze source artifacts from ImmutableSnapshot', () => { - const source = readRepoFile(IMMUTABLE_SNAPSHOT_PATH); - - for (const { label, pattern } of FORBIDDEN_SOURCE_PATTERNS) { - expect(source, label).not.toMatch(pattern); - } - }); - it('rejects unsupported arbitrary class instances instead of descriptor-copying them', () => { const guarded = new ConstructorGuardedValue('secret'); diff --git a/test/fixtures/dead-export-report/src/consumer.ts b/test/fixtures/dead-export-report/src/consumer.ts new file mode 100644 index 000000000..f00c4511f --- /dev/null +++ b/test/fixtures/dead-export-report/src/consumer.ts @@ -0,0 +1,3 @@ +import { UsedValue } from './index.ts'; + +new UsedValue('used'); diff --git a/test/fixtures/dead-export-report/src/index.ts b/test/fixtures/dead-export-report/src/index.ts new file mode 100644 index 000000000..16755a264 --- /dev/null +++ b/test/fixtures/dead-export-report/src/index.ts @@ -0,0 +1,3 @@ +export { UsedValue } from './used-value.ts'; +export { UnusedValue } from './unused-value.ts'; +export { renamedUnusedFunction as PublicUnusedFunction } from './renamed-unused-function.ts'; diff --git a/test/fixtures/dead-export-report/src/renamed-unused-function.ts b/test/fixtures/dead-export-report/src/renamed-unused-function.ts new file mode 100644 index 000000000..1f1c8066b --- /dev/null +++ b/test/fixtures/dead-export-report/src/renamed-unused-function.ts @@ -0,0 +1,3 @@ +export function renamedUnusedFunction(): string { + return 'unused'; +} diff --git a/test/fixtures/dead-export-report/src/unused-value.ts b/test/fixtures/dead-export-report/src/unused-value.ts new file mode 100644 index 000000000..5f5aab2b1 --- /dev/null +++ b/test/fixtures/dead-export-report/src/unused-value.ts @@ -0,0 +1,8 @@ +export class UnusedValue { + readonly value: string; + + constructor(value: string) { + this.value = value; + Object.freeze(this); + } +} diff --git a/test/fixtures/dead-export-report/src/used-value.ts b/test/fixtures/dead-export-report/src/used-value.ts new file mode 100644 index 000000000..6dcf41434 --- /dev/null +++ b/test/fixtures/dead-export-report/src/used-value.ts @@ -0,0 +1,8 @@ +export class UsedValue { + readonly value: string; + + constructor(value: string) { + this.value = value; + Object.freeze(this); + } +} diff --git a/test/helpers/MarkdownDocument.ts b/test/helpers/MarkdownDocument.ts new file mode 100644 index 000000000..0b50f953a --- /dev/null +++ b/test/helpers/MarkdownDocument.ts @@ -0,0 +1,130 @@ +import { readFileSync } from 'node:fs'; + +export type MarkdownHeading = { + readonly level: number; + readonly text: string; +}; + +export type MarkdownLink = { + readonly text: string; + readonly target: string; +}; + +export type MarkdownTableRow = { + readonly cells: readonly string[]; +}; + +export type MarkdownTaskRow = { + readonly status: string; + readonly id: string; + readonly text: string; +}; + +const HEADING_PATTERN = /^(#{1,6})\s+(.+)$/u; +const LINK_PATTERN = /\[([^\]]+)\]\(([^)]+)\)/gu; +const TASK_ROW_PATTERN = /^\s*\[([ x~✗])\]\s+([A-Za-z][A-Za-z0-9_-]+)\b(.*)$/u; +const LIST_ITEM_PATTERN = /^\s*(?:[-*+]|\d+\.)\s+(.+)$/u; + +export default class MarkdownDocument { + readonly text: string; + + constructor(text: string) { + this.text = text; + Object.freeze(this); + } + + static fromFile(path: string): MarkdownDocument { + return new MarkdownDocument(readFileSync(path, 'utf8')); + } + + headings(): readonly MarkdownHeading[] { + return this.lines().flatMap((line) => { + const match = HEADING_PATTERN.exec(line); + if (match === null) { + return []; + } + const marker = match[1] ?? ''; + const text = match[2] ?? ''; + return [Object.freeze({ level: marker.length, text })]; + }); + } + + hasHeading(level: number, text: string): boolean { + return this.headings().some((heading) => heading.level === level && heading.text === text); + } + + links(): readonly MarkdownLink[] { + const links: MarkdownLink[] = []; + for (const match of this.text.matchAll(LINK_PATTERN)) { + const text = match[1]; + const target = match[2]; + if (text !== undefined && target !== undefined) { + links.push(Object.freeze({ text, target })); + } + } + return links; + } + + hasLink(text: string, target: string): boolean { + return this.links().some((link) => link.text === text && link.target === target); + } + + tableRows(): readonly MarkdownTableRow[] { + const rows: MarkdownTableRow[] = []; + for (const line of this.lines()) { + if (!line.startsWith('|') || !line.endsWith('|')) { + continue; + } + const cells = line + .slice(1, -1) + .split('|') + .map((cell) => cell.trim()); + if (cells.every((cell) => /^:?-{3,}:?$/u.test(cell))) { + continue; + } + rows.push(Object.freeze({ cells: Object.freeze(cells) })); + } + return rows; + } + + tableRowByFirstCell(firstCell: string): MarkdownTableRow | undefined { + return this.tableRows().find((row) => row.cells[0] === firstCell); + } + + tableRowContainingCell(cell: string): MarkdownTableRow | undefined { + return this.tableRows().find((row) => row.cells.includes(cell)); + } + + listItems(): readonly string[] { + return this.lines().flatMap((line) => { + const match = LIST_ITEM_PATTERN.exec(line); + const item = match?.[1]; + return item === undefined ? [] : [item]; + }); + } + + taskRows(): readonly MarkdownTaskRow[] { + const rows: MarkdownTaskRow[] = []; + for (const line of this.lines()) { + const match = TASK_ROW_PATTERN.exec(line); + if (match === null) { + continue; + } + const status = match[1]; + const id = match[2]; + const text = match[3]; + if (status !== undefined && id !== undefined && text !== undefined) { + rows.push(Object.freeze({ status, id, text: text.trim() })); + } + } + return rows; + } + + taskRow(id: string): MarkdownTaskRow | undefined { + return this.taskRows().find((row) => row.id === id); + } + + private lines(): readonly string[] { + return this.text.split('\n'); + } +} diff --git a/test/unit/domain/trust/domainPurity.test.ts b/test/unit/domain/trust/domainPurity.test.ts index e56d0b581..5d4c47135 100644 --- a/test/unit/domain/trust/domainPurity.test.ts +++ b/test/unit/domain/trust/domainPurity.test.ts @@ -10,18 +10,79 @@ import { describe, it, expect } from 'vitest'; import fs from 'node:fs'; import path from 'node:path'; +import ts from 'typescript'; const TRUST_DIR = path.resolve('src/domain/trust'); +type TrustSourceFile = { + readonly name: string; + readonly sourceFile: ts.SourceFile; +}; + function getTrustFiles() { return fs.readdirSync(TRUST_DIR) .filter((f) => f.endsWith('.js') || f.endsWith('.ts')) - .map((f) => ({ + .map((f): TrustSourceFile => ({ name: f, - content: fs.readFileSync(path.join(TRUST_DIR, f), 'utf8'), + sourceFile: ts.createSourceFile( + path.join(TRUST_DIR, f), + fs.readFileSync(path.join(TRUST_DIR, f), 'utf8'), + ts.ScriptTarget.Latest, + true, + ), })); } +function moduleSpecifiers(sourceFile: ts.SourceFile): readonly string[] { + return sourceFile.statements.flatMap((statement) => { + if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) { + return []; + } + return [statement.moduleSpecifier.text]; + }); +} + +function containsPropertyAccess(sourceFile: ts.SourceFile, objectName: string, propertyName: string): boolean { + let found = false; + function visit(node: ts.Node): void { + if (found) { + return; + } + if ( + ts.isPropertyAccessExpression(node) + && ts.isIdentifier(node.expression) + && node.expression.text === objectName + && node.name.text === propertyName + ) { + found = true; + return; + } + ts.forEachChild(node, visit); + } + visit(sourceFile); + return found; +} + +function containsObjectPropertyAccess(sourceFile: ts.SourceFile, objectName: string): boolean { + let found = false; + function visit(node: ts.Node): void { + if (found) { + return; + } + if ( + ts.isPropertyAccessExpression(node) + && ts.isIdentifier(node.expression) + && node.expression.text === objectName + ) { + found = true; + return; + } + ts.forEachChild(node, visit); + } + visit(sourceFile); + return found; +} + describe('domain/trust/ purity', () => { const files = getTrustFiles(); @@ -29,25 +90,24 @@ describe('domain/trust/ purity', () => { expect(files.length).toBeGreaterThan(0); }); - for (const { name, content } of files) { + for (const { name, sourceFile } of files) { describe(name, () => { it('does not reference process.env', () => { - expect(content).not.toMatch(/process\.env/); + expect(containsPropertyAccess(sourceFile, 'process', 'env')).toBe(false); }); it('does not import from infrastructure/', () => { - expect(content).not.toMatch(/from\s+['"].*infrastructure\//); + expect(moduleSpecifiers(sourceFile).some((moduleSpecifier) => moduleSpecifier.includes('infrastructure/'))) + .toBe(false); }); it('does not import from adapters/', () => { - expect(content).not.toMatch(/from\s+['"].*adapters\//); + expect(moduleSpecifiers(sourceFile).some((moduleSpecifier) => moduleSpecifier.includes('adapters/'))) + .toBe(false); }); it('does not use console directly', () => { - // Allow console in comments but not in code - const lines = content.split('\n').filter((l) => !l.trim().startsWith('*') && !l.trim().startsWith('//')); - const codeOnly = lines.join('\n'); - expect(codeOnly).not.toMatch(/\bconsole\./); + expect(containsObjectPropertyAccess(sourceFile, 'console')).toBe(false); }); }); } diff --git a/test/unit/infrastructure/adapters/GitGraphAdapter.gitCasPersistence.test.ts b/test/unit/infrastructure/adapters/GitGraphAdapter.gitCasPersistence.test.ts index 4e56b1cd4..d89079cb8 100644 --- a/test/unit/infrastructure/adapters/GitGraphAdapter.gitCasPersistence.test.ts +++ b/test/unit/infrastructure/adapters/GitGraphAdapter.gitCasPersistence.test.ts @@ -1,16 +1,15 @@ -import { existsSync, readFileSync } from 'node:fs'; -import { join } from 'node:path'; -import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; import GitGraphAdapter, { type CollectableStream, type GitPlumbing } from '../../../../src/infrastructure/adapters/GitGraphAdapter.ts'; -const repoRoot = fileURLToPath(new URL('../../../../', import.meta.url)); - interface GitExecuteOptions { readonly args: string[]; readonly input?: string | Uint8Array; } +interface GitStreamOptions { + readonly args: string[]; +} + class EmptyCollectableStream implements CollectableStream { async *[Symbol.asyncIterator](): AsyncIterator { const chunks: Uint8Array[] = []; @@ -24,9 +23,24 @@ class EmptyCollectableStream implements CollectableStream { } } +class ByteCollectableStream implements CollectableStream { + constructor(private readonly chunks: readonly Uint8Array[]) {} + + async *[Symbol.asyncIterator](): AsyncIterator { + for (const chunk of this.chunks) { + yield chunk; + } + } + + async collect(): Promise { + return Buffer.concat(this.chunks.map((chunk) => Buffer.from(chunk))); + } +} + class RecordingPlumbing implements GitPlumbing { readonly emptyTree = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'; readonly calls: GitExecuteOptions[] = []; + readonly streamCalls: GitStreamOptions[] = []; constructor(protected readonly oid: string) {} @@ -35,7 +49,8 @@ class RecordingPlumbing implements GitPlumbing { return `${this.oid}\n`; } - async executeStream(_options: { args: string[] }): Promise { + async executeStream(options: GitStreamOptions): Promise { + this.streamCalls.push(options); return new EmptyCollectableStream(); } } @@ -58,8 +73,35 @@ class FlakyPlumbing extends RecordingPlumbing { } } -function readRepoFile(relativePath: string): string { - return readFileSync(join(repoRoot, relativePath), 'utf8'); +class TreeListingPlumbing extends RecordingPlumbing { + constructor( + oid: string, + private readonly treeListing: string, + ) { + super(oid); + } + + override async execute(options: GitExecuteOptions): Promise { + this.calls.push(options); + if (options.args[0] === 'ls-tree') { + return this.treeListing; + } + return `${this.oid}\n`; + } +} + +class BlobStreamPlumbing extends RecordingPlumbing { + constructor( + oid: string, + private readonly chunks: readonly Uint8Array[], + ) { + super(oid); + } + + override async executeStream(options: GitStreamOptions): Promise { + this.streamCalls.push(options); + return new ByteCollectableStream(this.chunks); + } } describe('GitGraphAdapter git-cas persistence bridge', () => { @@ -124,31 +166,31 @@ describe('GitGraphAdapter git-cas persistence bridge', () => { expect(plumbing.calls).toHaveLength(2); }); - it('ratchets write delegation while keeping non-equivalent read/ref semantics local', () => { - const adapter = readRepoFile('src/infrastructure/adapters/GitGraphAdapter.ts'); - const reader = readRepoFile('src/infrastructure/adapters/GitCasGraphReaderAdapter.ts'); - const recursiveTreeReader = readRepoFile('src/infrastructure/adapters/GitRecursiveTreeOidReaderAdapter.ts'); - const successorCard = join( - repoRoot, - 'docs/archive/backlog/v17.0.0-residual-backlog/INFRA_git-cas-adapter-parity.md', - ); - - expect(adapter).toContain("import { GitPersistenceAdapter } from '@git-stunts/git-cas'"); - expect(adapter).toContain('private readonly _gitCasPersistence: GitPersistenceAdapter'); - expect(adapter).toContain('policy: createGitCasRetryPolicy(this._retryOptions)'); - expect(adapter).toContain('this._gitCasPersistence.writeBlob'); - expect(adapter).toContain('this._gitCasPersistence.writeTree'); - expect(adapter).toContain('new GitCasGraphReaderAdapter'); - expect(adapter).toContain('new GitRecursiveTreeOidReaderAdapter'); - expect(adapter).toContain('this._gitCasGraphReader.readBlob'); - expect(adapter).toContain('this._gitCasGraphReader.readTreeOids'); - expect(adapter).toContain('treeOidReader: this._recursiveTreeOidReader'); - expect(adapter).not.toContain('this._gitCasPersistence.createCommit'); - expect(reader).toContain('this._persistence.readBlobStream'); - expect(reader).toContain('collectUnboundedGraphBlobStream'); - expect(reader).not.toContain('this._persistence.iterateTree'); - expect(recursiveTreeReader).toContain("args: ['ls-tree', '-rz', treeOid]"); - expect(recursiveTreeReader).toContain('parseRecursiveTreeEntry'); - expect(existsSync(successorCard)).toBe(true); + it('reads recursive tree OIDs through the injected Git plumbing boundary', async () => { + const treeOid = '1'.repeat(40); + const blobOid = '2'.repeat(40); + const nestedTreeOid = '3'.repeat(40); + const listing = [ + `100644 blob ${blobOid}\tpatch.cbor`, + `040000 tree ${nestedTreeOid}\tnested`, + '', + ].join('\0'); + const plumbing = new TreeListingPlumbing(treeOid, listing); + const adapter = new GitGraphAdapter({ plumbing }); + + await expect(adapter.readTreeOids(treeOid)).resolves.toEqual({ + 'patch.cbor': blobOid, + }); + expect(plumbing.calls).toContainEqual({ args: ['ls-tree', '-rz', treeOid] }); + }); + + it('reads graph blobs through the git-cas stream path', async () => { + const oid = 'f'.repeat(40); + const payload = new TextEncoder().encode('graph payload'); + const plumbing = new BlobStreamPlumbing(oid, [payload]); + const adapter = new GitGraphAdapter({ plumbing }); + + await expect(adapter.readBlob(oid)).resolves.toEqual(payload); + expect(plumbing.streamCalls).toEqual([{ args: ['cat-file', 'blob', oid] }]); }); }); diff --git a/test/unit/scripts/dead-export-report.test.ts b/test/unit/scripts/dead-export-report.test.ts new file mode 100644 index 000000000..345a5aa32 --- /dev/null +++ b/test/unit/scripts/dead-export-report.test.ts @@ -0,0 +1,30 @@ +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; +import { + buildDeadExportReport, + formatDeadExportReport, +} from '../../../scripts/dead-export-report.ts'; + +const fixtureRoot = fileURLToPath(new URL('../../fixtures/dead-export-report/src', import.meta.url)); + +describe('dead export report', () => { + it('reports deterministic candidate exports from a canonical fixture corpus', () => { + const report = buildDeadExportReport(fixtureRoot); + + expect(report.filesScanned).toBe(5); + expect(report.exportsScanned).toBe(6); + expect(report.findings.map((finding) => `${finding.path}:${finding.name}:${finding.kind}`)).toEqual([ + 'index.ts:renamedUnusedFunction:re-export', + 'index.ts:UnusedValue:re-export', + 'renamed-unused-function.ts:renamedUnusedFunction:function', + 'unused-value.ts:UnusedValue:class', + ]); + }); + + it('formats a stable markdown witness table', () => { + const report = buildDeadExportReport(fixtureRoot); + + expect(formatDeadExportReport(report)).toContain('| Path | Export | Kind | Identifier refs |'); + expect(formatDeadExportReport(report)).toContain('| unused-value.ts | `UnusedValue` | class | 0 |'); + }); +}); diff --git a/test/unit/scripts/documentation-corpus-shape.test.ts b/test/unit/scripts/documentation-corpus-shape.test.ts index f1aebc1f2..0934a6517 100644 --- a/test/unit/scripts/documentation-corpus-shape.test.ts +++ b/test/unit/scripts/documentation-corpus-shape.test.ts @@ -1,21 +1,15 @@ -import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { execFileSync } from 'node:child_process'; import { describe, expect, it } from 'vitest'; +import MarkdownDocument from '../../helpers/MarkdownDocument.ts'; const repoRoot = fileURLToPath(new URL('../../../', import.meta.url)); -const readme = readFileSync(`${repoRoot}README.md`, 'utf8'); -const docsIndex = readFileSync(`${repoRoot}docs/README.md`, 'utf8'); -const archiveIndex = readFileSync(`${repoRoot}docs/archive/README.md`, 'utf8'); -const styleGuide = readFileSync( - `${repoRoot}.github/maintainers/documentation/style-guide.md`, - 'utf8', -); -const maintainerDocsIndex = readFileSync( - `${repoRoot}.github/maintainers/README.md`, - 'utf8', -); +const readme = MarkdownDocument.fromFile(`${repoRoot}README.md`); +const docsIndex = MarkdownDocument.fromFile(`${repoRoot}docs/README.md`); +const archiveIndex = MarkdownDocument.fromFile(`${repoRoot}docs/archive/README.md`); +const styleGuide = MarkdownDocument.fromFile(`${repoRoot}.github/maintainers/documentation/style-guide.md`); +const maintainerDocsIndex = MarkdownDocument.fromFile(`${repoRoot}.github/maintainers/README.md`); /** * Set of every path tracked by git, keyed by repo-relative POSIX path. @@ -51,7 +45,7 @@ function hasFile(relativePath: string): boolean { describe('documentation corpus taxonomy', () => { it('exposes a docs index and links to it from the root README', () => { - expect(readme).toContain('## Documentation'); + expect(readme.hasHeading(2, 'Documentation')).toBe(true); expect(hasFile('docs/GETTING_STARTED.md')).toBe(true); expect(hasFile('docs/API_REFERENCE.md')).toBe(true); expect(hasFile('docs/ADVANCED_GUIDE.md')).toBe(true); @@ -64,32 +58,37 @@ describe('documentation corpus taxonomy', () => { expect(hasFile('docs/ADR-001-Folds.md')).toBe(false); expect(hasFile('examples')).toBe(false); expect(hasFile('GRAVEYARD.md')).toBe(false); - expect(docsIndex).toContain('# Documentation Index'); - expect(docsIndex).toContain('[Getting Started](GETTING_STARTED.md)'); - expect(docsIndex).toContain('[Guide](GUIDE.md)'); - expect(docsIndex).toContain('[API Reference](API_REFERENCE.md)'); - expect(docsIndex).toContain('[Advanced Guide](ADVANCED_GUIDE.md)'); - expect(docsIndex).toContain('[CLI Guide](CLI_GUIDE.md)'); - expect(docsIndex).toContain('[Conceptual Overview](CONCEPTUAL_OVERVIEW.md)'); - expect(docsIndex).toContain('[Architecture](ARCHITECTURE.md)'); - expect(docsIndex).toContain('[Roadmap](ROADMAP.md)'); - expect(docsIndex).not.toContain('## Current Release-Blocker Docs'); + expect(docsIndex.hasHeading(1, 'Documentation Index')).toBe(true); + expect(docsIndex.hasLink('Getting Started', 'GETTING_STARTED.md')).toBe(true); + expect(docsIndex.hasLink('Guide', 'GUIDE.md')).toBe(true); + expect(docsIndex.hasLink('API Reference', 'API_REFERENCE.md')).toBe(true); + expect(docsIndex.hasLink('Advanced Guide', 'ADVANCED_GUIDE.md')).toBe(true); + expect(docsIndex.hasLink('CLI Guide', 'CLI_GUIDE.md')).toBe(true); + expect(docsIndex.hasLink('Conceptual Overview', 'CONCEPTUAL_OVERVIEW.md')).toBe(true); + expect(docsIndex.hasLink('Architecture', 'ARCHITECTURE.md')).toBe(true); + expect(docsIndex.hasLink('Roadmap', 'ROADMAP.md')).toBe(true); + expect(docsIndex.hasHeading(2, 'Current Release-Blocker Docs')).toBe(false); }); it('keeps a maintainer-facing documentation guide for writing and information architecture', () => { - expect(docsIndex).toContain('[Maintainer docs](../.github/maintainers/README.md)'); - expect(docsIndex).toContain('[Documentation style guide](../.github/maintainers/documentation/style-guide.md)'); - expect(maintainerDocsIndex).toContain('# Maintainer docs'); - expect(maintainerDocsIndex).toContain('[Documentation style guide](documentation/style-guide.md)'); - expect(styleGuide).toContain('# Documentation style guide'); - expect(styleGuide).toContain('## Writing principles'); - expect(styleGuide).toContain('## Audience model'); - expect(styleGuide).toContain('## Target information architecture'); + expect(docsIndex.hasLink('Maintainer docs', '../.github/maintainers/README.md')).toBe(true); + expect(docsIndex.hasLink( + 'Documentation style guide', + '../.github/maintainers/documentation/style-guide.md', + )).toBe(true); + expect(maintainerDocsIndex.hasHeading(1, 'Maintainer docs')).toBe(true); + expect(maintainerDocsIndex.hasLink('Documentation style guide', 'documentation/style-guide.md')).toBe(true); + expect(styleGuide.hasHeading(1, 'Documentation style guide')).toBe(true); + expect(styleGuide.hasHeading(2, 'Writing principles')).toBe(true); + expect(styleGuide.hasHeading(2, 'Audience model')).toBe(true); + expect(styleGuide.hasHeading(2, 'Target information architecture')).toBe(true); }); it('keeps an explicit archive index', () => { - expect(archiveIndex).toContain('# Archive Index'); - expect(archiveIndex).toContain('should not be treated as the canonical current docs set'); + expect(archiveIndex.hasHeading(1, 'Archive Index')).toBe(true); + expect(archiveIndex.hasLink('archived backlog notes', 'backlog/README.md')).toBe(true); + expect(archiveIndex.hasLink('archived architectural decision records', 'adr/README.md')).toBe(true); + expect(archiveIndex.hasLink('../README.md', '../README.md')).toBe(true); }); it('moves obvious historical clutter out of top-level docs', () => { diff --git a/test/unit/scripts/factory-functions-in-tests-shape.test.ts b/test/unit/scripts/factory-functions-in-tests-shape.test.ts index 04877a2cd..b34c88f3d 100644 --- a/test/unit/scripts/factory-functions-in-tests-shape.test.ts +++ b/test/unit/scripts/factory-functions-in-tests-shape.test.ts @@ -1,25 +1,21 @@ -import { existsSync, readFileSync } from 'node:fs'; +import { existsSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; +import MarkdownDocument from '../../helpers/MarkdownDocument.ts'; const repoRoot = fileURLToPath(new URL('../../../', import.meta.url)); -function readRepoFile(relativePath: string): string { - return readFileSync(`${repoRoot}${relativePath}`, 'utf8'); -} - describe('factory-functions-in-tests closeout', () => { it('explains in the v17 ledger why the sludge card is already closed', () => { - const releaseReadme = readRepoFile('docs/releases/v17.0.0/README.md'); + const releaseReadme = MarkdownDocument.fromFile(`${repoRoot}docs/releases/v17.0.0/README.md`); + const task = releaseReadme.taskRow('SLUDGE_factory-functions-in-tests'); - expect(releaseReadme).toContain('[x] SLUDGE_factory-functions-in-tests'); - expect(releaseReadme).toContain('cycle 0055 hill-met; constructor-wrapper'); - expect(releaseReadme).toContain('wire-format helpers remain intentional test'); + expect(task?.status).toBe('x'); }); it('removes the stale live sludge note and dead workload row', () => { - const workloads = readRepoFile( - 'docs/archive/backlog/github-issue-migration-2026-06-01/docs/method/backlog/WORKLOADS.md' + const workloads = MarkdownDocument.fromFile( + `${repoRoot}docs/archive/backlog/github-issue-migration-2026-06-01/docs/method/backlog/WORKLOADS.md`, ); expect( @@ -27,16 +23,16 @@ describe('factory-functions-in-tests closeout', () => { `${repoRoot}docs/archive/backlog/v17.0.0-residual-backlog/SLUDGE_factory-functions-in-tests.md` ) ).toBe(false); - expect(workloads).not.toContain('WL-35-v17-hygiene-sludge-seed'); - expect(workloads).not.toContain('SLUDGE_factory-functions-in-tests'); + expect(workloads.taskRow('WL-35-v17-hygiene-sludge-seed')).toBeUndefined(); + expect(workloads.taskRow('SLUDGE_factory-functions-in-tests')).toBeUndefined(); }); it('stops the latest hygiene retro from pointing at the dead workload', () => { - const retro = readRepoFile( - 'docs/method/retro/0054-type-import-and-template-expression-purge/type-import-and-template-expression-purge.md' + const retro = MarkdownDocument.fromFile( + `${repoRoot}docs/method/retro/0054-type-import-and-template-expression-purge/type-import-and-template-expression-purge.md`, ); - expect(retro).not.toContain('WL-35-v17-hygiene-sludge-seed'); - expect(retro).not.toContain('SLUDGE_factory-functions-in-tests'); + expect(retro.taskRow('WL-35-v17-hygiene-sludge-seed')).toBeUndefined(); + expect(retro.taskRow('SLUDGE_factory-functions-in-tests')).toBeUndefined(); }); }); diff --git a/test/unit/scripts/glossary-shape.test.ts b/test/unit/scripts/glossary-shape.test.ts index 14bc00884..b12444ab7 100644 --- a/test/unit/scripts/glossary-shape.test.ts +++ b/test/unit/scripts/glossary-shape.test.ts @@ -1,40 +1,45 @@ -import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; +import MarkdownDocument from '../../helpers/MarkdownDocument.ts'; function readDoc(relativePath: string): string { - return readFileSync(fileURLToPath(new URL(`../../../${relativePath}`, import.meta.url)), 'utf8'); + return fileURLToPath(new URL(`../../../${relativePath}`, import.meta.url)); } -const glossary = readDoc('docs/GLOSSARY.md'); -const guide = readDoc('docs/GUIDE.md'); -const conceptualOverview = readDoc('docs/CONCEPTUAL_OVERVIEW.md'); +const glossary = MarkdownDocument.fromFile(readDoc('docs/GLOSSARY.md')); +const guide = MarkdownDocument.fromFile(readDoc('docs/GUIDE.md')); +const conceptualOverview = MarkdownDocument.fromFile(readDoc('docs/CONCEPTUAL_OVERVIEW.md')); describe('Glossary is the canonical noun source of truth', () => { it('defines the status model for shipped, transition, and target nouns', () => { - expect(glossary).toContain('# Glossary'); - expect(glossary).toContain('This is the canonical noun source of truth for `git-warp`.'); - expect(glossary).toContain('- **shipped**: current repo/runtime truth'); - expect(glossary).toContain('- **transition**: the repo uses this noun, but the implementation shape is'); - expect(glossary).toContain('- **target**: the noun is part of the intended architecture'); + expect(glossary.hasHeading(1, 'Glossary')).toBe(true); + expect(glossary.hasHeading(2, 'Status key')).toBe(true); + expect(glossary.listItems().some((item) => item.startsWith('**shipped**:'))).toBe(true); + expect(glossary.listItems().some((item) => item.startsWith('**transition**:'))).toBe(true); + expect(glossary.listItems().some((item) => item.startsWith('**target**:'))).toBe(true); }); it('records the core observer-geometry runtime nouns and working law', () => { - expect(glossary).toContain('| `Coordinate` |'); - expect(glossary).toContain('| `Observer` |'); - expect(glossary).toContain('| `Aperture` |'); - expect(glossary).toContain('| `Optic` |'); - expect(glossary).toContain('| `Bounded support rule` |'); - expect(glossary).toContain('| `Causal index` |'); - expect(glossary).toContain('| `Support fragment` |'); - expect(glossary).toContain('| `WarpStateSnapshot` |'); - expect(glossary).toContain('## Working law'); - expect(glossary).toContain('1. An app asks an **Observer** to answer an **Optic**.'); - expect(glossary).toContain('3. The runtime derives the **bounded support rule**'); + const terms = glossary.tableRows().map((row) => row.cells[0]); + + expect(terms).toEqual(expect.arrayContaining([ + '`Coordinate`', + '`Observer`', + '`Aperture`', + '`Optic`', + '`Bounded support rule`', + '`Causal index`', + '`Support fragment`', + '`WarpStateSnapshot`', + ])); + expect(glossary.hasHeading(2, 'Working law')).toBe(true); + expect(glossary.listItems().some((item) => item.includes('**Observer**') && item.includes('**Optic**'))) + .toBe(true); + expect(glossary.listItems().some((item) => item.includes('**bounded support rule**'))).toBe(true); }); it('is pointed to by the high-traffic conceptual docs', () => { - expect(guide).toContain('[GLOSSARY.md](GLOSSARY.md)'); - expect(conceptualOverview).toContain('[GLOSSARY.md](GLOSSARY.md)'); + expect(guide.hasLink('GLOSSARY.md', 'GLOSSARY.md')).toBe(true); + expect(conceptualOverview.hasLink('GLOSSARY.md', 'GLOSSARY.md')).toBe(true); }); }); diff --git a/test/unit/scripts/incremental-index-updater-closeout-shape.test.ts b/test/unit/scripts/incremental-index-updater-closeout-shape.test.ts index baa02b04c..368c0a518 100644 --- a/test/unit/scripts/incremental-index-updater-closeout-shape.test.ts +++ b/test/unit/scripts/incremental-index-updater-closeout-shape.test.ts @@ -1,26 +1,21 @@ -import { existsSync, readFileSync } from 'node:fs'; +import { existsSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; +import MarkdownDocument from '../../helpers/MarkdownDocument.ts'; const repoRoot = fileURLToPath(new URL('../../../', import.meta.url)); -function readRepoFile(relativePath: string): string { - return readFileSync(`${repoRoot}${relativePath}`, 'utf8'); -} - describe('incremental-index-updater closeout', () => { it('explains in the v17 ledger why the god card is already closed', () => { - const releaseReadme = readRepoFile('docs/releases/v17.0.0/README.md'); + const releaseReadme = MarkdownDocument.fromFile(`${repoRoot}docs/releases/v17.0.0/README.md`); + const task = releaseReadme.taskRow('GOD_incremental-index-updater'); - expect(releaseReadme).toContain('[x] GOD_incremental-index-updater'); - expect(releaseReadme).toContain('cycle 0056 hill-met; god split already landed'); - expect(releaseReadme).toContain('PROTO_purge-boundary-leaks'); - expect(releaseReadme).toContain('MODEL_incremental-index-updater-shape-sludge'); + expect(task?.status).toBe('x'); }); it('removes the stale live note and dead workload item', () => { - const workloads = readRepoFile( - 'docs/archive/backlog/github-issue-migration-2026-06-01/docs/method/backlog/WORKLOADS.md' + const workloads = MarkdownDocument.fromFile( + `${repoRoot}docs/archive/backlog/github-issue-migration-2026-06-01/docs/method/backlog/WORKLOADS.md`, ); expect( @@ -28,33 +23,32 @@ describe('incremental-index-updater closeout', () => { `${repoRoot}docs/archive/backlog/v17.0.0-residual-backlog/GOD_incremental-index-updater.md` ) ).toBe(false); - expect(workloads).not.toContain('GOD_incremental-index-updater'); + expect(workloads.taskRow('GOD_incremental-index-updater')).toBeUndefined(); }); it('drops the dead god from downstream blocker lists', () => { - const apiMigrate = readRepoFile( - 'docs/archive/backlog/v17.0.0-residual-backlog/API_migrate-consumers-to-capabilities.md' + const apiMigrate = MarkdownDocument.fromFile( + `${repoRoot}docs/archive/backlog/v17.0.0-residual-backlog/API_migrate-consumers-to-capabilities.md`, ); - const sharedProviderCycle = readRepoFile( - 'docs/design/0085-close-shared-provider-interfaces.md' + const sharedProviderCycle = MarkdownDocument.fromFile( + `${repoRoot}docs/design/0085-close-shared-provider-interfaces.md`, ); - expect(apiMigrate).not.toContain('GOD_incremental-index-updater'); - expect(sharedProviderCycle).toContain( - 'The stale `CROSS_shared-provider-interfaces` backlog card is removed' - ); - expect(sharedProviderCycle).not.toContain('GOD_incremental-index-updater'); + expect(apiMigrate.taskRow('GOD_incremental-index-updater')).toBeUndefined(); + expect(sharedProviderCycle.taskRow('CROSS_shared-provider-interfaces')).toBeUndefined(); + expect(sharedProviderCycle.taskRow('GOD_incremental-index-updater')).toBeUndefined(); }); it('re-homes the historical wave and scorecard residue to the real remaining owners', () => { - const wave = readRepoFile( - 'docs/archive/backlog/v17.0.0-residual-backlog/TS_wave-09-gods-and-monsters.md' + const wave = MarkdownDocument.fromFile( + `${repoRoot}docs/archive/backlog/v17.0.0-residual-backlog/TS_wave-09-gods-and-monsters.md`, ); - const scorecard = readRepoFile('docs/archive/backlog/v17.0.0-residual-backlog/SCORECARD.md'); + const scorecard = MarkdownDocument.fromFile(`${repoRoot}docs/archive/backlog/v17.0.0-residual-backlog/SCORECARD.md`); + + const incrementalUpdaterRow = wave.tableRowContainingCell('IncrementalIndexUpdater.ts'); - expect(wave).toContain('IncrementalIndexUpdater.ts | 495'); - expect(wave).toContain('remaining boundary/model cleanup lives elsewhere'); - expect(scorecard).toContain('PROTO_purge-boundary-leaks'); - expect(scorecard).toContain('MODEL_incremental-index-updater-shape-sludge'); + expect(incrementalUpdaterRow?.cells[2]).toBe('495'); + expect(scorecard.listItems()).toContain('`PROTO_purge-boundary-leaks`'); + expect(scorecard.listItems()).toContain('`MODEL_incremental-index-updater-shape-sludge`'); }); }); diff --git a/test/unit/scripts/index-builder-on-git-cas-shape.test.ts b/test/unit/scripts/index-builder-on-git-cas-shape.test.ts index aa731993e..3557ee9d7 100644 --- a/test/unit/scripts/index-builder-on-git-cas-shape.test.ts +++ b/test/unit/scripts/index-builder-on-git-cas-shape.test.ts @@ -1,21 +1,19 @@ import { describe, expect, it } from 'vitest'; -import { readFileSync } from 'node:fs'; - -function readText(path: string): string { - return readFileSync(path, 'utf8'); -} +import MarkdownDocument from '../../helpers/MarkdownDocument.ts'; describe('0057 index-builder-on-git-cas docs shape', () => { it('frames the cycle in git-cas and bounded-residency terms', () => { - const design = readText('docs/design/0057-index-builder-on-git-cas.md'); - expect(design).toContain('git-cas'); - expect(design).toContain('bounded-residency'); - expect(design).toContain('whole-blob reads'); + const design = MarkdownDocument.fromFile('docs/design/0057-index-builder-on-git-cas.md'); + + expect(design.hasHeading(1, 'Index Builder On Git-CAS')).toBe(true); + expect(design.hasHeading(2, 'Hill')).toBe(true); + expect(design.listItems()).toContain('`git-cas`-backed for content storage, and'); + expect(design.listItems()).toContain('bounded-residency throughout flush, merge, and finalize'); }); it('keeps the v17 release ledger focused on storage and streaming, not file-size theater', () => { - const release = readText('docs/releases/v17.0.0/README.md'); - expect(release).toContain('INFRA_index-builder-on-git-cas'); - expect(release).not.toContain('INFRA_index-builder-on-git-cas ← god'); + const release = MarkdownDocument.fromFile('docs/releases/v17.0.0/README.md'); + + expect(release.taskRow('INFRA_index-builder-on-git-cas')?.status).toBe('x'); }); }); From 73799cd5baffe2b5b04eec497da681d27519f073 Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 12 Jun 2026 18:05:25 -0700 Subject: [PATCH 2/3] Fit dead export report under script size limit --- scripts/dead-export-report.ts | 57 +++++++++++------------------------ 1 file changed, 18 insertions(+), 39 deletions(-) diff --git a/scripts/dead-export-report.ts b/scripts/dead-export-report.ts index d9440aeec..a54e41a9f 100644 --- a/scripts/dead-export-report.ts +++ b/scripts/dead-export-report.ts @@ -4,14 +4,7 @@ import { extname, relative, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import ts from 'typescript'; -type ExportKind = - | 'class' - | 'const' - | 'enum' - | 'function' - | 'interface' - | 're-export' - | 'type'; +type ExportKind = 'class' | 'const' | 'enum' | 'function' | 'interface' | 're-export' | 'type'; export type DeadExportFinding = { readonly path: string; @@ -28,7 +21,6 @@ export type DeadExportReport = { }; type SourceRecord = { - readonly path: string; readonly relativePath: string; readonly sourceFile: ts.SourceFile; }; @@ -44,7 +36,6 @@ const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx']); const IGNORED_DIRECTORIES = new Set(['.git', 'dist', 'node_modules']); const MARKDOWN_TABLE_HEADER = '| Path | Export | Kind | Identifier refs |'; const MARKDOWN_TABLE_SEPARATOR = '| --- | --- | --- | ---: |'; - export function buildDeadExportReport(sourceRoot: string = DEFAULT_SOURCE_ROOT): DeadExportReport { const root = resolve(sourceRoot); const records = sourceRecords(root); @@ -87,7 +78,6 @@ function sourceRecords(root: string): readonly SourceRecord[] { return sourceFilePaths(root).map((path) => { const text = readFileSync(path, 'utf8'); return Object.freeze({ - path, relativePath: relative(root, path), sourceFile: ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, scriptKind(path)), }); @@ -269,34 +259,24 @@ function isReferenceIdentifier(identifier: ts.Identifier): boolean { if (parent === undefined) { return false; } - if (ts.isImportSpecifier(parent)) { - return parent.name !== identifier && parent.propertyName !== identifier; - } - if (ts.isExportSpecifier(parent)) { - return parent.name !== identifier && parent.propertyName !== identifier; - } - if (ts.isClassDeclaration(parent) && parent.name === identifier) { - return false; - } - if (ts.isFunctionDeclaration(parent) && parent.name === identifier) { - return false; - } - if (ts.isInterfaceDeclaration(parent) && parent.name === identifier) { - return false; - } - if (ts.isTypeAliasDeclaration(parent) && parent.name === identifier) { - return false; - } - if (ts.isEnumDeclaration(parent) && parent.name === identifier) { - return false; - } - if (ts.isVariableDeclaration(parent) && parent.name === identifier) { - return false; - } - if (ts.isParameter(parent) && parent.name === identifier) { + return !isImportOrExportSpecifierName(identifier, parent) && !isDeclarationName(identifier, parent); +} + +function isImportOrExportSpecifierName(identifier: ts.Identifier, parent: ts.Node): boolean { + if (!ts.isImportSpecifier(parent) && !ts.isExportSpecifier(parent)) { return false; } - return true; + return parent.name === identifier || parent.propertyName === identifier; +} + +function isDeclarationName(identifier: ts.Identifier, parent: ts.Node): boolean { + return (ts.isClassDeclaration(parent) && parent.name === identifier) + || (ts.isFunctionDeclaration(parent) && parent.name === identifier) + || (ts.isInterfaceDeclaration(parent) && parent.name === identifier) + || (ts.isTypeAliasDeclaration(parent) && parent.name === identifier) + || (ts.isEnumDeclaration(parent) && parent.name === identifier) + || (ts.isVariableDeclaration(parent) && parent.name === identifier) + || (ts.isParameter(parent) && parent.name === identifier); } function compareFindings(left: DeadExportFinding, right: DeadExportFinding): number { @@ -316,6 +296,5 @@ function isDirectExecution(): boolean { } if (isDirectExecution()) { - const sourceRoot = process.argv[2] ?? DEFAULT_SOURCE_ROOT; - process.stdout.write(formatDeadExportReport(buildDeadExportReport(sourceRoot))); + process.stdout.write(formatDeadExportReport(buildDeadExportReport(process.argv[2] ?? DEFAULT_SOURCE_ROOT))); } From 99ca11a6b6d57687d45fe377adfcbbddbc1f8ce5 Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 12 Jun 2026 19:49:28 -0700 Subject: [PATCH 3/3] Fix: resolve static witness review findings --- scripts/dead-export-report.ts | 25 +++-- test/helpers/MarkdownDocument.ts | 4 + test/unit/domain/trust/domainPurity.test.ts | 99 +++++++++++++++++-- test/unit/scripts/dead-export-report.test.ts | 2 +- .../documentation-corpus-shape.test.ts | 1 + .../index-builder-on-git-cas-shape.test.ts | 2 + 6 files changed, 118 insertions(+), 15 deletions(-) diff --git a/scripts/dead-export-report.ts b/scripts/dead-export-report.ts index a54e41a9f..afb33eddf 100644 --- a/scripts/dead-export-report.ts +++ b/scripts/dead-export-report.ts @@ -27,7 +27,8 @@ type SourceRecord = { type ExportDeclarationRecord = { readonly path: string; - readonly name: string; + readonly exportedName: string; + readonly referenceName: string; readonly kind: ExportKind; }; @@ -148,6 +149,14 @@ function pushExportedStatement( return; } + pushDirectExportedStatement(record, statement, declarations); +} + +function pushDirectExportedStatement( + record: SourceRecord, + statement: ts.Statement, + declarations: ExportDeclarationRecord[], +): void { if (ts.isClassDeclaration(statement) && statement.name !== undefined) { pushDeclaration(record, declarations, statement.name.text, 'class'); return; @@ -184,8 +193,8 @@ function pushExportDeclaration( } for (const element of exportClause.elements) { - const localName = element.propertyName?.text ?? element.name.text; - pushDeclaration(record, declarations, localName, 're-export'); + const referenceName = element.propertyName?.text ?? element.name.text; + pushDeclaration(record, declarations, element.name.text, 're-export', referenceName); } } @@ -204,12 +213,14 @@ function pushVariableStatement( function pushDeclaration( record: SourceRecord, declarations: ExportDeclarationRecord[], - name: string, + exportedName: string, kind: ExportKind, + referenceName: string = exportedName, ): void { declarations.push(Object.freeze({ path: record.relativePath, - name, + exportedName, + referenceName, kind, })); } @@ -231,9 +242,9 @@ function findingForDeclaration( ): DeadExportFinding { return Object.freeze({ path: declaration.path, - name: declaration.name, + name: declaration.exportedName, kind: declaration.kind, - identifierReferences: referenceCounts.get(declaration.name) ?? 0, + identifierReferences: referenceCounts.get(declaration.referenceName) ?? 0, }); } diff --git a/test/helpers/MarkdownDocument.ts b/test/helpers/MarkdownDocument.ts index 0b50f953a..c87be0797 100644 --- a/test/helpers/MarkdownDocument.ts +++ b/test/helpers/MarkdownDocument.ts @@ -69,6 +69,10 @@ export default class MarkdownDocument { return this.links().some((link) => link.text === text && link.target === target); } + containsText(text: string): boolean { + return this.text.includes(text); + } + tableRows(): readonly MarkdownTableRow[] { const rows: MarkdownTableRow[] = []; for (const line of this.lines()) { diff --git a/test/unit/domain/trust/domainPurity.test.ts b/test/unit/domain/trust/domainPurity.test.ts index 5d4c47135..a49a83000 100644 --- a/test/unit/domain/trust/domainPurity.test.ts +++ b/test/unit/domain/trust/domainPurity.test.ts @@ -13,12 +13,54 @@ import path from 'node:path'; import ts from 'typescript'; const TRUST_DIR = path.resolve('src/domain/trust'); +const REQUIRE_CALLEE = 'require'; +const INFRASTRUCTURE_SEGMENT = 'infrastructure/'; +const ADAPTERS_SEGMENT = 'adapters/'; type TrustSourceFile = { readonly name: string; readonly sourceFile: ts.SourceFile; }; +type ModuleSpecifierFixture = { + readonly name: string; + readonly text: string; + readonly expected: readonly string[]; +}; + +const MODULE_SPECIFIER_FIXTURES: readonly ModuleSpecifierFixture[] = Object.freeze([ + Object.freeze({ + name: 'static import', + text: "import { Adapter } from '../../infrastructure/Adapter.js';", + expected: Object.freeze(['../../infrastructure/Adapter.js']), + }), + Object.freeze({ + name: 're-export', + text: "export { Adapter } from '../../infrastructure/Adapter.js';", + expected: Object.freeze(['../../infrastructure/Adapter.js']), + }), + Object.freeze({ + name: 'star re-export', + text: "export * from '../../adapters/index.js';", + expected: Object.freeze(['../../adapters/index.js']), + }), + Object.freeze({ + name: 'dynamic import', + text: "const adapter = import('../../infrastructure/Adapter.js');", + expected: Object.freeze(['../../infrastructure/Adapter.js']), + }), + Object.freeze({ + name: 'require call', + text: "const adapter = require('../../adapters/index.js');", + expected: Object.freeze(['../../adapters/index.js']), + }), + Object.freeze({ + name: 'commented import text', + text: "// import { Adapter } from '../../infrastructure/Adapter.js';", + expected: Object.freeze([]), + }), +]); + function getTrustFiles() { return fs.readdirSync(TRUST_DIR) .filter((f) => f.endsWith('.js') || f.endsWith('.ts')) @@ -34,12 +76,49 @@ function getTrustFiles() { } function moduleSpecifiers(sourceFile: ts.SourceFile): readonly string[] { - return sourceFile.statements.flatMap((statement) => { - if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) { - return []; + const specifiers: string[] = []; + function visit(node: ts.Node): void { + const specifier = moduleSpecifierFromNode(node); + if (specifier !== undefined) { + specifiers.push(specifier); } - return [statement.moduleSpecifier.text]; - }); + ts.forEachChild(node, visit); + } + visit(sourceFile); + return specifiers; +} + +function moduleSpecifierFromNode(node: ts.Node): string | undefined { + if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) { + return stringLiteralText(node.moduleSpecifier); + } + if (ts.isCallExpression(node)) { + return moduleSpecifierFromCall(node); + } + return undefined; +} + +function moduleSpecifierFromCall(node: ts.CallExpression): string | undefined { + if (!isBoundaryImportCall(node)) { + return undefined; + } + return stringLiteralText(node.arguments[0]); +} + +function isBoundaryImportCall(node: ts.CallExpression): boolean { + return node.expression.kind === ts.SyntaxKind.ImportKeyword + || (ts.isIdentifier(node.expression) && node.expression.text === REQUIRE_CALLEE); +} + +function stringLiteralText(node: ts.Node | undefined): string | undefined { + if (node === undefined || !ts.isStringLiteral(node)) { + return undefined; + } + return node.text; +} + +function sourceFileFromText(text: string): ts.SourceFile { + return ts.createSourceFile('domain-purity-fixture.ts', text, ts.ScriptTarget.Latest, true); } function containsPropertyAccess(sourceFile: ts.SourceFile, objectName: string, propertyName: string): boolean { @@ -86,6 +165,12 @@ function containsObjectPropertyAccess(sourceFile: ts.SourceFile, objectName: str describe('domain/trust/ purity', () => { const files = getTrustFiles(); + for (const fixture of MODULE_SPECIFIER_FIXTURES) { + it(`collects module specifiers from ${fixture.name}`, () => { + expect(moduleSpecifiers(sourceFileFromText(fixture.text))).toEqual(fixture.expected); + }); + } + it('has at least one source file', () => { expect(files.length).toBeGreaterThan(0); }); @@ -97,12 +182,12 @@ describe('domain/trust/ purity', () => { }); it('does not import from infrastructure/', () => { - expect(moduleSpecifiers(sourceFile).some((moduleSpecifier) => moduleSpecifier.includes('infrastructure/'))) + expect(moduleSpecifiers(sourceFile).some((moduleSpecifier) => moduleSpecifier.includes(INFRASTRUCTURE_SEGMENT))) .toBe(false); }); it('does not import from adapters/', () => { - expect(moduleSpecifiers(sourceFile).some((moduleSpecifier) => moduleSpecifier.includes('adapters/'))) + expect(moduleSpecifiers(sourceFile).some((moduleSpecifier) => moduleSpecifier.includes(ADAPTERS_SEGMENT))) .toBe(false); }); diff --git a/test/unit/scripts/dead-export-report.test.ts b/test/unit/scripts/dead-export-report.test.ts index 345a5aa32..886b79e16 100644 --- a/test/unit/scripts/dead-export-report.test.ts +++ b/test/unit/scripts/dead-export-report.test.ts @@ -14,7 +14,7 @@ describe('dead export report', () => { expect(report.filesScanned).toBe(5); expect(report.exportsScanned).toBe(6); expect(report.findings.map((finding) => `${finding.path}:${finding.name}:${finding.kind}`)).toEqual([ - 'index.ts:renamedUnusedFunction:re-export', + 'index.ts:PublicUnusedFunction:re-export', 'index.ts:UnusedValue:re-export', 'renamed-unused-function.ts:renamedUnusedFunction:function', 'unused-value.ts:UnusedValue:class', diff --git a/test/unit/scripts/documentation-corpus-shape.test.ts b/test/unit/scripts/documentation-corpus-shape.test.ts index 0934a6517..1c49e9b7e 100644 --- a/test/unit/scripts/documentation-corpus-shape.test.ts +++ b/test/unit/scripts/documentation-corpus-shape.test.ts @@ -86,6 +86,7 @@ describe('documentation corpus taxonomy', () => { it('keeps an explicit archive index', () => { expect(archiveIndex.hasHeading(1, 'Archive Index')).toBe(true); + expect(archiveIndex.containsText('should not be treated as the canonical current docs set')).toBe(true); expect(archiveIndex.hasLink('archived backlog notes', 'backlog/README.md')).toBe(true); expect(archiveIndex.hasLink('archived architectural decision records', 'adr/README.md')).toBe(true); expect(archiveIndex.hasLink('../README.md', '../README.md')).toBe(true); diff --git a/test/unit/scripts/index-builder-on-git-cas-shape.test.ts b/test/unit/scripts/index-builder-on-git-cas-shape.test.ts index 3557ee9d7..e9b8e59b9 100644 --- a/test/unit/scripts/index-builder-on-git-cas-shape.test.ts +++ b/test/unit/scripts/index-builder-on-git-cas-shape.test.ts @@ -7,6 +7,7 @@ describe('0057 index-builder-on-git-cas docs shape', () => { expect(design.hasHeading(1, 'Index Builder On Git-CAS')).toBe(true); expect(design.hasHeading(2, 'Hill')).toBe(true); + expect(design.containsText('whole-blob reads')).toBe(true); expect(design.listItems()).toContain('`git-cas`-backed for content storage, and'); expect(design.listItems()).toContain('bounded-residency throughout flush, merge, and finalize'); }); @@ -15,5 +16,6 @@ describe('0057 index-builder-on-git-cas docs shape', () => { const release = MarkdownDocument.fromFile('docs/releases/v17.0.0/README.md'); expect(release.taskRow('INFRA_index-builder-on-git-cas')?.status).toBe('x'); + expect(release.containsText('INFRA_index-builder-on-git-cas ← god')).toBe(false); }); });