From 88630e8217e2cac018dc735b6ae785d4d3957e1d Mon Sep 17 00:00:00 2001 From: Juan Cruz Fortunatti Date: Mon, 5 Oct 2026 05:19:12 +0200 Subject: [PATCH 1/5] feat(architecture): read import declarations with their kind and location --- .github/scripts/architecture/graph.mts | 4 +- .../architecture/import-declarations.mts | 177 ++++++++++++++++++ .../architecture/import-declarations.test.mts | 73 ++++++++ 3 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 .github/scripts/architecture/import-declarations.mts create mode 100644 .github/scripts/architecture/import-declarations.test.mts diff --git a/.github/scripts/architecture/graph.mts b/.github/scripts/architecture/graph.mts index da81932196d..d13b2266484 100644 --- a/.github/scripts/architecture/graph.mts +++ b/.github/scripts/architecture/graph.mts @@ -28,7 +28,7 @@ export interface ImportEdge { readonly symbols: readonly string[]; readonly typeOnly: boolean; } -export interface ImportGraph { readonly files: ReadonlyMap; readonly edges: readonly ImportEdge[] } +export interface ImportGraph { readonly files: ReadonlyMap; readonly edges: readonly ImportEdge[]; readonly resolution?: { readonly tsconfigPath: string; readonly baseUrl: string | undefined; readonly modules: readonly CruisedModule[] } } /** One module of dependency-cruiser's JSON result, validated. */ interface CruisedDependency { readonly module: string; readonly resolved: string; readonly coreModule: boolean; readonly couldNotResolve: boolean } @@ -235,5 +235,5 @@ export async function buildImportGraph(checkout: string, options: BuildOptions): + unresolved.slice(0, 5).map(item => ` ${item.from}: ${item.specifier} (${item.reason})`).join('\n') + '\nFix the package exports or its tsup entries, or teach .github/scripts/architecture/workspaces.mts the new form.'); } - return { files, edges: edges.filter(edge => files.has(edge.to)) }; + return { files, edges: edges.filter(edge => files.has(edge.to)), resolution: { tsconfigPath, baseUrl, modules } }; } diff --git a/.github/scripts/architecture/import-declarations.mts b/.github/scripts/architecture/import-declarations.mts new file mode 100644 index 00000000000..1ad02c3bed1 --- /dev/null +++ b/.github/scripts/architecture/import-declarations.mts @@ -0,0 +1,177 @@ +/** Declaration inventory layered on the production cruise and resolver; no independent resolution policy. + * Computed imports remain unresolved. Astro CSS and new URL assets are recorded although the scanner + * cannot see them; markup component references use their frontmatter imports, as in the scanner. */ +import { execFileSync, spawnSync } from 'node:child_process'; +import { mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'; +import { dirname, extname, posix, resolve } from 'node:path'; +import { isBuiltin } from 'node:module'; +import { pathToFileURL } from 'node:url'; +import ts from 'typescript'; +import { astroScriptBlocks } from './astro-imports.mts'; +import { buildImportGraph, importTarget, readWorkspaces, trackedStandIn, type ImportGraph } from './graph.mts'; +import { createResolver } from './resolver.mts'; +import { byText, isTestPath, zoneOf } from './zones.mts'; + +export const KINDS = ['value', 'type', 'lazy', 'asset'] as const; +export type Kind = typeof KINDS[number]; +export interface Declaration { + readonly from: string; readonly line: number; readonly specifier: string; readonly to: string | null; + readonly kind: Kind; readonly symbols: readonly string[]; readonly form: string; + readonly sideEffectOnly: boolean; readonly test: boolean; readonly unresolved?: boolean; +} +interface Parsed extends Omit { readonly computed?: boolean } +export const codeTarget = (path: string): boolean => /\.(?:[cm]?[jt]s|tsx|jsx|astro)(?:[?#].*)?$/u.test(path); +const lineAt = (source: string, offset: number): number => source.slice(0, offset).split('\n').length; + +/** Parse declarations without resolving them; offsets in Astro always refer to the original source. */ +export function parseDeclarations(source: string, file: string): Parsed[] { + const found: Parsed[] = []; + const parse = (text: string, offset: number): void => { + const tree = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS); + const add = (node: ts.Node, specifier: string, kind: Kind, symbols: string[], form: string, sideEffectOnly = false, computed = false): void => { + found.push({ line: lineAt(source, offset + node.getStart(tree)), specifier, kind, symbols, form, sideEffectOnly, ...(computed ? { computed: true } : {}) }); + }; + const visit = (node: ts.Node): void => { + if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) { + const clause = ts.isImportDeclaration(node) ? node.importClause : undefined; + const bindings = clause?.namedBindings ?? (ts.isExportDeclaration(node) ? node.exportClause : undefined); + const symbols: string[] = clause?.name ? ['default'] : []; + if (bindings && (ts.isNamespaceImport(bindings) || ts.isNamespaceExport(bindings))) symbols.push('*'); + else if (bindings) for (const element of bindings.elements) symbols.push((element.propertyName ?? element.name).text); + if (ts.isExportDeclaration(node) && !node.exportClause) symbols.push('*'); + const inlineType = bindings && (ts.isNamedImports(bindings) || ts.isNamedExports(bindings)) && bindings.elements.length > 0 + && bindings.elements.every(element => element.isTypeOnly) && !clause?.name; + const type = clause?.isTypeOnly || (ts.isExportDeclaration(node) && node.isTypeOnly) || inlineType; + const side = ts.isImportDeclaration(node) && !clause; + add(node, node.moduleSpecifier.text, type ? 'type' : 'value', symbols, + side ? 'side-effect' : ts.isImportDeclaration(node) ? 'import' : node.exportClause ? 'export-from' : 'export-star', side); + } else if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument) && ts.isStringLiteralLike(node.argument.literal)) { + add(node, node.argument.literal.text, 'type', [node.qualifier?.getText(tree).split('.')[0] ?? '*'], 'import-type-node'); + } else if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference) + && node.moduleReference.expression && ts.isStringLiteralLike(node.moduleReference.expression)) { + add(node, node.moduleReference.expression.text, node.isTypeOnly ? 'type' : 'value', ['*'], 'import'); + } else if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword && node.arguments[0]) { + const arg = node.arguments[0]; + add(node, ts.isStringLiteralLike(arg) ? arg.text : arg.getText(tree), 'lazy', ['(dynamic)'], 'dynamic-import', false, !ts.isStringLiteralLike(arg)); + } else if (ts.isNewExpression(node) && node.expression.getText(tree) === 'URL' && node.arguments?.length === 2 + && node.arguments[1]?.getText(tree) === 'import.meta.url' && node.arguments[0] && ts.isStringLiteralLike(node.arguments[0])) { + add(node, node.arguments[0].text, 'value', [], 'new URL(.., import.meta.url)'); + } + ts.forEachChild(node, visit); + }; + visit(tree); + }; + if (file.endsWith('.css')) { + const uncommented = source.replace(/\/\*[\s\S]*?\*\//gu, comment => comment.replace(/[^\r\n]/gu, ' ')); + for (const match of uncommented.matchAll(/@import\s+(?:url\(\s*(?:["']([^"']+)["']|([^\s)]+))\s*\)|["']([^"']+)["'])/gu)) + found.push({ line: lineAt(source, match.index ?? 0), specifier: match[1] ?? match[2] ?? match[3] ?? '', kind: 'asset', symbols: [], form: 'side-effect', sideEffectOnly: true }); + } else if (!file.endsWith('.astro')) parse(source, 0); + else { + // Reuse the scanner's eligibility and extraction, then recover each block's real source offset. + const frontmatter = /^\s*---\r?\n([\s\S]*?)\r?\n---/u.exec(source); + if (frontmatter?.[1] !== undefined) parse(astroScriptBlocks(frontmatter[0])[0] ?? '', frontmatter[0].indexOf('\n', frontmatter[0].indexOf('---')) + 1); + for (const script of source.matchAll(/]*)>([\s\S]*?)<\/script>/gu)) { + const block = astroScriptBlocks(script[0])[0]; + if (block === undefined) continue; + const attributes = script[1] ?? ''; + const src = /\bsrc\s*=\s*["']([^"']+)["']/u.exec(attributes); + const offset = (script.index ?? 0) + (src ? script[0].indexOf(src[0]) : script[0].indexOf('>') + 1); + parse(block, offset); + } + for (const style of source.matchAll(/]*)>([\s\S]*?)<\/style>/gu)) { + const attributes = style[1] ?? '', body = style[2] ?? '', offset = (style.index ?? 0) + style[0].indexOf('>') + 1; + const src = /\bsrc\s*=\s*["']([^"']+)["']/u.exec(attributes); + if (src?.[1]) found.push({ line: lineAt(source, style.index ?? 0), specifier: src[1], kind: 'asset', symbols: [], form: 'side-effect', sideEffectOnly: true }); + const uncommented = body.replace(/\/\*[\s\S]*?\*\//gu, comment => comment.replace(/[^\r\n]/gu, ' ')); + for (const match of uncommented.matchAll(/@import\s+(?:url\(\s*(?:["']([^"']+)["']|([^\s)]+))\s*\)|["']([^"']+)["'])/gu)) { + found.push({ line: lineAt(source, offset + (match.index ?? 0)), specifier: match[1] ?? match[2] ?? match[3] ?? '', kind: 'asset', symbols: [], form: 'side-effect', sideEffectOnly: true }); + } + } + } + return found; +} + +export interface InventoryOptions { readonly prefix: string; readonly checkAgainstScanner?: boolean; readonly graph?: ImportGraph } +const pairKey = (from: string, to: string) => `${from}\n${to}`; +export async function readImportDeclarations(checkout: string, options: InventoryOptions) { + const root = realpathSync(checkout), files = execFileSync('git', ['ls-files', '-z'], { cwd: root, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 }).split('\0').filter(Boolean).sort(), tracked = new Set(files), workspaces = readWorkspaces(root, tracked); + const selected = files.filter(file => file.startsWith(options.prefix) && /\.(mts|ts|tsx|mjs|astro|css)$/u.test(file)); + const graph = options.graph ?? await buildImportGraph(root, { details: false }); + const resolveImport = createResolver(root, graph.resolution?.tsconfigPath ?? resolve(root, 'tsconfig.json'), graph.resolution?.baseUrl); + const dependencies = new Map(graph.resolution?.modules.map(module => [module.source, module.dependencies]) ?? []); + const parsedFiles = new Map(selected.map(from => [from, parseDeclarations(readFileSync(resolve(root, from), 'utf8'), from)])); + const relativePath = (from: string, specifier: string) => specifier.startsWith('.') ? posix.normalize(posix.join(posix.dirname(from), specifier.replace(/[?#].*$/u, ''))) : undefined; + const candidates = [...new Set([...parsedFiles].flatMap(([from, entries]) => entries.map(entry => relativePath(from, entry.specifier)).filter((path): path is string => path !== undefined)))]; + const ignoredResult = spawnSync('git', ['check-ignore', '-z', '--stdin'], { cwd: root, input: candidates.join('\0'), encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }); + if (ignoredResult.error) throw ignoredResult.error; + if (ignoredResult.status !== 0 && ignoredResult.status !== 1) throw new Error(`git check-ignore failed: ${ignoredResult.stderr}`); + const ignored = new Set(ignoredResult.stdout.split('\0').filter(Boolean)); + const declarations: Declaration[] = []; + for (const [from, entries] of parsedFiles) for (const parsed of entries) { + const { computed, ...entry } = parsed; + const problems: { from: string; specifier: string; reason: string }[] = []; + const dependency = dependencies.get(from)?.find(item => item.module === parsed.specifier); + const resolved = dependency && !dependency.couldNotResolve ? dependency.resolved : resolveImport(from, parsed.specifier); + const relative = relativePath(from, parsed.specifier); + // Resolve ignored generated inputs by their declared address, never by their presence on disk. + const declared = relative ? trackedStandIn(relative, tracked) ?? (ignored.has(relative) ? relative : undefined) : undefined; + let to = computed ? undefined : declared ?? importTarget(from, parsed.specifier, resolved, workspaces, tracked, problems); + const bare = !/^(\.|\/|#)/u.test(parsed.specifier); + if (!computed && to === undefined && !problems.length && (isBuiltin(parsed.specifier) || parsed.specifier.startsWith('astro:') || (bare && resolved !== undefined))) { + to = parsed.specifier.startsWith('@') ? parsed.specifier.split('/').slice(0, 2).join('/') : parsed.specifier.split('/')[0]; + if (isBuiltin(parsed.specifier) || parsed.specifier.startsWith('astro:')) to = parsed.specifier; + } + const target = to && tracked.has(to) ? to : resolved ?? parsed.specifier; + const kind = !computed && extname(target.replace(/[?#].*$/u, '')) && !codeTarget(target) ? 'asset' : entry.kind; + declarations.push({ ...entry, from, to: to ?? null, kind, test: isTestPath(from), ...(to === undefined ? { unresolved: true } : {}) }); + } + declarations.sort((a, b) => byText(a.from, b.from) || a.line - b.line || byText(a.specifier, b.specifier) || byText(a.form, b.form)); + const pairs = new Map; declarations: number }>(); + for (const entry of declarations) { + if (entry.to === null) continue; + const key = pairKey(entry.from, entry.to), pair = pairs.get(key) ?? { from: entry.from, to: entry.to, kinds: { value: 0, type: 0, lazy: 0, asset: 0 }, declarations: 0 }; + pair.kinds[entry.kind]++; pair.declarations++; pairs.set(key, pair); + } + const folderPairs = new Map; filePairs: number; declarations: number }>(); + for (const pair of pairs.values()) { + const from = zoneOf(pair.from), to = tracked.has(pair.to) ? zoneOf(pair.to) : pair.to, key = pairKey(from, to); + const entry = folderPairs.get(key) ?? { from, to, kinds: { value: 0, type: 0, lazy: 0, asset: 0 }, filePairs: 0, declarations: 0 }; + for (const kind of KINDS) if (pair.kinds[kind]) entry.kinds[kind]++; + entry.filePairs++; entry.declarations += pair.declarations; folderPairs.set(key, entry); + } + const scanner = { checked: false, missing: [] as string[], extra: [] as string[], exceptions: [] as { pair: string; reason: string }[], unexplained: 0 }; + if (options.checkAgainstScanner) { + const expected = new Set(graph.edges.filter(edge => selected.includes(edge.from) && tracked.has(edge.to)).map(edge => pairKey(edge.from, edge.to))); + const actual = new Set([...pairs.values()].filter(pair => tracked.has(pair.to)).map(pair => pairKey(pair.from, pair.to))); + scanner.checked = true; + scanner.missing = [...expected].filter(key => !actual.has(key)).sort(byText); + for (const key of [...actual].filter(key => !expected.has(key)).sort(byText)) { + const hits = declarations.filter(entry => entry.to !== null && pairKey(entry.from, entry.to) === key); + if (hits.every(entry => entry.form === 'new URL(.., import.meta.url)' || ((entry.from.endsWith('.astro') || entry.from.endsWith('.css')) && entry.kind === 'asset'))) + scanner.exceptions.push({ pair: key, reason: 'Production scanner does not read new URL or stylesheet imports' }); + else scanner.extra.push(key); + } + scanner.unexplained = scanner.missing.length + scanner.extra.length; + } + return { base: execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' }).trim(), declarations, + pairs: [...pairs.values()].sort((a, b) => byText(a.from, b.from) || byText(a.to, b.to)), + summary: { files: selected, declarations: declarations.length, filePairs: pairs.size, unresolved: declarations.filter(entry => entry.unresolved).length, + folderPairs: [...folderPairs.values()].sort((a, b) => byText(a.from, b.from) || byText(a.to, b.to)), scanner } }; +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { + try { + const args = process.argv.slice(2); let prefix = 'site/', out: string | undefined, checkAgainstScanner = false; + for (let i = 0; i < args.length; i++) { + if (args[i] === '--prefix' && args[i + 1]) prefix = args[++i]!; + else if (args[i] === '--out' && args[i + 1]) out = args[++i]!; + else if (args[i] === '--check-against-scanner') checkAgainstScanner = true; + else throw new Error(`Unknown or incomplete argument: ${args[i]}`); + } + const result = await readImportDeclarations(process.cwd(), { prefix, checkAgainstScanner }); + const json = `${JSON.stringify(result, null, 2)}\n`; + if (out) { mkdirSync(dirname(out), { recursive: true }); writeFileSync(out, json); } else console.log(json); + console.error(`${result.summary.declarations} declarations; ${result.summary.filePairs} file pairs; ${result.summary.scanner.unexplained} unexplained scanner mismatches`); + if (result.summary.scanner.unexplained) process.exitCode = 1; + } catch (error) { console.error(error); process.exitCode = 1; } +} diff --git a/.github/scripts/architecture/import-declarations.test.mts b/.github/scripts/architecture/import-declarations.test.mts new file mode 100644 index 00000000000..e06e7dd44f0 --- /dev/null +++ b/.github/scripts/architecture/import-declarations.test.mts @@ -0,0 +1,73 @@ +/** Declaration syntax, real Astro source lines and production-resolution parity in a tiny repository. */ +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; +import { test } from 'node:test'; +import { parseDeclarations, readImportDeclarations } from './import-declarations.mts'; + +test('each syntax keeps declaration kind, imported symbols and side-effect status', () => { + const parsed = parseDeclarations([ + "import Default, { type A, B as Local } from './a.mts';", "import { type A, type B } from './a.mts';", + "import type * as Types from './a.mts';", "export type { A } from './a.mts';", "export * from './a.mts';", + "export { B } from './a.mts';", "import './style.css';", "const lazy = import(`./a.mts`);", + "type X = typeof import('./a.mts');", "type Y = import('./a.mts').A;", "new URL('./pic.png', import.meta.url);", + 'import(name);', "import type Q = require('./a.mts');", + ].join('\n'), 'site/a.test.mts'); + assert.deepEqual(parsed.map(entry => entry.kind), ['value', 'type', 'type', 'type', 'value', 'value', 'value', 'lazy', 'type', 'type', 'value', 'lazy', 'type']); + assert.deepEqual(parsed[0]?.symbols, ['default', 'A', 'B']); + assert.equal(parsed[6]?.sideEffectOnly, true); + assert.equal(parsed[11]?.computed, true); + assert.deepEqual(parsed.map(entry => entry.line), Array.from({ length: 13 }, (_, i) => i + 1)); + assert.deepEqual(parsed.map(entry => entry.form), ['import', 'import', 'import', 'export-from', 'export-star', 'export-from', 'side-effect', 'dynamic-import', 'import-type-node', 'import-type-node', 'new URL(.., import.meta.url)', 'dynamic-import', 'import']); +}); + +test('Astro blocks, src and CSS have original lines; inline scripts match production exclusions', () => { + const parsed = parseDeclarations([ + '---', "import A from './A.astro';", '---', '', '', + '', '', '', + ].join('\n'), 'site/Page.astro'); + assert.deepEqual(parsed.map(entry => [entry.specifier, entry.line]), [['./A.astro', 2], ['./x.mts', 6], ['./entry.mts', 8], ['./style.css', 11]]); +}); + +test('Astro leading whitespace and CSS URL imports retain lines and ignore comments', () => { + const parsed = parseDeclarations('\n\n---\nimport "./a.mts";\n---\n', 'site/Page.astro'); + assert.deepEqual(parsed.map(entry => [entry.specifier, entry.line]), [['./a.mts', 4], ['./outer.css', 6], ['./inner.css', 8]]); +}); + +test('production resolution retains duplicates, assets, tests, generated stand-ins and zero scanner mismatches', async () => { + const root = mkdtempSync(join(tmpdir(), 'import-declarations-')); + const files = { + 'package.json': '{"name":"fixture","type":"module"}', + 'tsconfig.json': '{"compilerOptions":{"module":"ESNext","moduleResolution":"Bundler","noEmit":true}}', + '.gitignore': 'site/generated.mts\nnode_modules/\n', + 'node_modules/tiny/package.json': '{"name":"tiny"}', 'node_modules/tiny/style.css': 'body {}', + 'site/a.mts': "import { x } from './b.mts';\nimport type { X } from './b.mts';\nimport './data.json';\nimport './generated.mts';\nimport 'node:fs';\nimport './missing.mts';\nimport('node:os');\nimport 'tiny/style.css';\n", + 'site/b.mts': 'export const x = 1; export interface X {}', + 'site/generated.d.mts': 'export {};', 'site/generated.mts': 'export {};', 'site/data.json': '{}', + 'site/a.test.mts': "import './b.mts';", 'site/Empty.astro': '

', + }; + try { + // Reuse an existing HEAD without creating a commit, and populate only this fixture's index. + execFileSync('git', ['clone', '--quiet', '--shared', '--no-checkout', process.cwd(), root]); + for (const [file, source] of Object.entries(files)) { mkdirSync(dirname(join(root, file)), { recursive: true }); writeFileSync(join(root, file), source); } + execFileSync('git', ['add', '.'], { cwd: root }); + const result = await readImportDeclarations(root, { prefix: 'site/', checkAgainstScanner: true }); + assert.equal(result.summary.scanner.unexplained, 0); + assert.equal(result.declarations.find(entry => entry.specifier === './generated.mts')?.to, 'site/generated.d.mts'); + assert.equal(result.declarations.find(entry => entry.specifier === './data.json')?.kind, 'asset'); + assert.equal(result.declarations.find(entry => entry.specifier === './missing.mts')?.unresolved, true); + assert.equal(result.declarations.find(entry => entry.from.endsWith('.test.mts'))?.test, true); + assert.equal(result.pairs.find(pair => pair.from === 'site/a.mts' && pair.to === 'site/b.mts')?.declarations, 2); + assert.equal(result.declarations.find(entry => entry.specifier === 'tiny/style.css')?.to, 'tiny'); + assert.equal(result.declarations.find(entry => entry.specifier === 'tiny/style.css')?.kind, 'asset'); + assert.equal(result.summary.declarations, 9); + assert.ok(result.summary.files.includes('site/Empty.astro')); + } finally { rmSync(root, { recursive: true, force: true }); } +}); + +test('stylesheet imports retain asset kind and source line without reading commented imports', () => { + const imports = parseDeclarations('/* @import "ignored.css"; */\n@import "./marker.css";\n@import url(./panel.css);', 'site/site.css'); + assert.deepEqual(imports.map(entry => [entry.specifier, entry.kind, entry.line]), [['./marker.css', 'asset', 2], ['./panel.css', 'asset', 3]]); +}); From d7a1abf7abb0187b23ff94ee2b329e3468f24ecc Mon Sep 17 00:00:00 2001 From: Juan Cruz Fortunatti Date: Mon, 5 Oct 2026 05:19:13 +0200 Subject: [PATCH 2/5] feat(architecture): project moves and graph edits through tiers and refuse site production imports of tests --- .../architecture/production-tests.test.mts | 15 ++ .github/scripts/architecture/projection.mts | 253 ++++++++++++++++++ .../scripts/architecture/projection.test.mts | 150 +++++++++++ .github/scripts/architecture/rules.mts | 9 +- 4 files changed, 426 insertions(+), 1 deletion(-) create mode 100644 .github/scripts/architecture/production-tests.test.mts create mode 100644 .github/scripts/architecture/projection.mts create mode 100644 .github/scripts/architecture/projection.test.mts diff --git a/.github/scripts/architecture/production-tests.test.mts b/.github/scripts/architecture/production-tests.test.mts new file mode 100644 index 00000000000..6cd23b171a4 --- /dev/null +++ b/.github/scripts/architecture/production-tests.test.mts @@ -0,0 +1,15 @@ +/** Production must never depend on test consumers, including lazy and type-only edges. */ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { evaluateRules, LAYER_RULES } from './rules.mts'; +import type { ImportGraph } from './graph.mts'; + +test('production test boundary rejects fixtures/helpers; test consumers may import production', () => { + const edges = ['site/a.test.mts', 'site/world/fixtures/object.mts', 'site/world/object.test-support.mts'].map(to => + ({ from: 'site/world/object.mts', to, symbols: [], test: false, typeOnly: true })); + const graph: ImportGraph = { files: new Map(), edges: [...edges, + { from: 'site/a.test.mts', to: 'site/world/object.mts', symbols: [], test: true, typeOnly: false }] }; + assert.equal(evaluateRules(graph).get('production-imports-no-tests')?.length, 3); + // Deleting this rule must lose the three findings: its absence cannot silently pass this test. + assert.equal(evaluateRules(graph, LAYER_RULES.filter(rule => rule.id !== 'production-imports-no-tests')).get('production-imports-no-tests'), undefined); +}); diff --git a/.github/scripts/architecture/projection.mts b/.github/scripts/architecture/projection.mts new file mode 100644 index 00000000000..d8d7ae5498c --- /dev/null +++ b/.github/scripts/architecture/projection.mts @@ -0,0 +1,253 @@ +/** Project declaration connectivity through file moves and explicit post-move graph edits. + * Tier folders use longest-prefix ownership. Root allow entries share the tier-zero site(root) folder. + * Lateral allowances are directed [fromFolder, toFolder] pairs. Tests are included in every view. */ +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { stronglyConnected } from './folders.mts'; +import { KINDS, type Kind } from './import-declarations.mts'; +import { byText, isTestPath } from './zones.mts'; + +export interface Edge { readonly from: string; readonly to: string; readonly kind: Kind; readonly line?: number } +interface Tier { readonly tier: number; readonly name: string; readonly folders: string[] } +interface TierMap { readonly tiers: Tier[]; readonly root: { readonly allow: string[]; readonly entries: string[]; readonly tier: number }; readonly lateral: string[][]; readonly tests?: 'any-tier'; readonly testFiles: string[]; readonly assets: string[]; readonly status?: 'draft' | 'enforced'; readonly forbid: { from: string[]; to: string[]; reason: string }[] } +export interface ProjectionInput { readonly declarations: unknown; readonly moves: unknown; readonly tiers: unknown; readonly edits?: unknown } +function record(value: unknown, label: string): Record { + if (value === null || typeof value !== 'object' || Array.isArray(value)) throw new TypeError(`${label}: expected object`); + return Object.fromEntries(Object.entries(value)); +} +function keys(value: Record, allowed: readonly string[], label: string): void { + for (const key of Object.keys(value)) if (!allowed.includes(key)) throw new TypeError(`${label}: unknown key ${key}`); +} +function string(value: unknown, label: string): string { + if (typeof value !== 'string' || !value.length) throw new TypeError(`${label}: expected nonempty string`); + return value; +} +function path(value: unknown, label: string): string { + const text = string(value, label); + if (text.startsWith('/') || text.includes('\\') || text.split('/').some(part => !part || part === '.' || part === '..')) throw new TypeError(`${label}: expected repository-relative path`); + return text; +} +function array(value: unknown, label: string): unknown[] { + if (!Array.isArray(value)) throw new TypeError(`${label}: expected array`); + return value; +} +function kind(value: unknown): Kind { + for (const candidate of KINDS) if (candidate === value) return candidate; + throw new TypeError(`Unknown import kind: ${String(value)}`); +} +function tierMap(value: unknown): TierMap { + const map = record(value, 'tiers'); keys(map, ['tiers', 'root', 'lateral', 'tests', 'testFiles', 'assets', 'status', 'forbid', 'draftUntil', 'warningCeiling'], 'tiers'); + if (map.status !== undefined && map.status !== 'draft' && map.status !== 'enforced') throw new TypeError('status: expected draft or enforced'); + if (map.draftUntil !== undefined && (typeof map.draftUntil !== 'string' || !/^\d{4}-\d{2}-\d{2}$/u.test(map.draftUntil) + || !Number.isFinite(Date.parse(map.draftUntil)) || new Date(map.draftUntil).toISOString().slice(0, 10) !== map.draftUntil)) throw new TypeError('draftUntil: expected ISO date'); + if (map.warningCeiling !== undefined && (typeof map.warningCeiling !== 'number' || !Number.isSafeInteger(map.warningCeiling) || map.warningCeiling < 0)) throw new TypeError('warningCeiling: expected nonnegative integer'); + if (map.status === 'draft' && (map.draftUntil === undefined || map.warningCeiling === undefined)) throw new TypeError('draft requires draftUntil and warningCeiling'); + const forbid = array(map.forbid ?? [], 'forbid').map(value => { + const rule = record(value, 'forbid'); keys(rule, ['from', 'to', 'reason'], 'forbid'); + return { from: array(rule.from, 'forbid.from').map(value => path(value, 'folder')), to: array(rule.to, 'forbid.to').map(value => path(value, 'folder')), reason: string(rule.reason, 'forbid.reason') }; + }); + const tiers = array(map.tiers, 'tiers.tiers').map(value => { + const item = record(value, 'tier'); keys(item, ['tier', 'name', 'folders'], 'tier'); + if (typeof item.tier !== 'number' || !Number.isSafeInteger(item.tier) || item.tier < 0) throw new TypeError('tier: expected nonnegative integer'); + return { tier: item.tier, name: string(item.name, 'tier.name'), folders: array(item.folders, 'tier.folders').map(value => path(value, 'folder')) }; + }); + const folders = tiers.flatMap(tier => tier.folders); + if (new Set(folders).size !== folders.length) throw new TypeError('Duplicate tier folder'); + for (const rule of forbid) if (!rule.from.length || !rule.to.length || [...rule.from, ...rule.to].some(folder => !folders.includes(folder))) throw new TypeError('forbid: expected nonempty lists of declared folders'); + const root = record(map.root, 'root'); keys(root, ['allow', 'entries', 'tier'], 'root'); + const entries = root.entries === undefined ? [] : array(root.entries, 'root.entries').map(value => path(value, 'root.entry')); + const rootTier = root.tier ?? 0; + if (typeof rootTier !== 'number' || !Number.isSafeInteger(rootTier) || rootTier < 0) throw new TypeError('root.tier: expected nonnegative integer'); + if (map.tests !== undefined && map.tests !== 'any-tier') throw new TypeError('tests: expected any-tier'); + const assets = map.assets === undefined ? [] : array(map.assets, 'assets').map(value => path(value, 'asset')); + const testFiles = map.testFiles === undefined ? [] : array(map.testFiles, 'testFiles').map(value => path(value, 'test file')); + const allow = array(root.allow, 'root.allow').map(value => path(value, 'root.allow')); + const lateral = map.lateral === undefined ? [] : array(map.lateral, 'lateral').map(value => { + const pair = array(value, 'lateral pair').map(value => path(value, 'lateral folder')); + if (pair.length !== 2 || !pair.every(folder => folders.includes(folder))) throw new TypeError('lateral: expected two listed folders'); + if (tiers.find(tier => tier.folders.includes(pair[0]!))?.tier !== tiers.find(tier => tier.folders.includes(pair[1]!))?.tier) throw new TypeError('lateral folders must share a tier'); + return pair; + }); + if (!entries.every(file => allow.includes(file))) throw new TypeError('root.entries must be allowed'); + return { tiers, forbid, ...(map.status === undefined ? {} : { status: map.status }), root: { allow, entries, tier: rootTier }, lateral, assets, ...(map.tests === 'any-tier' ? { tests: map.tests } : {}), testFiles }; +} + +/** Validate the inventory envelope and each declaration, rather than trusting JSON type assertions. */ +function inventory(value: unknown): { edges: Edge[]; files: Set; tests: Set } { + const input = record(value, 'declarations'); keys(input, ['base', 'declarations', 'pairs', 'summary'], 'declarations'); + if (input.base !== undefined) string(input.base, 'base'); + if (input.pairs !== undefined) array(input.pairs, 'pairs'); // Derived cache is never a connectivity source. + const files = new Set(); + if (input.summary !== undefined) { + const summary = record(input.summary, 'summary'); + keys(summary, ['files', 'declarations', 'filePairs', 'unresolved', 'folderPairs', 'scanner'], 'summary'); + if (summary.files !== undefined) for (const value of array(summary.files, 'summary.files')) files.add(path(value, 'file')); + } + const tests = new Set(); + const edges: Edge[] = []; + for (const value of array(input.declarations, 'declarations')) { + const entry = record(value, 'declaration'); + keys(entry, ['from', 'to', 'line', 'specifier', 'kind', 'symbols', 'form', 'sideEffectOnly', 'test', 'unresolved'], 'declaration'); + const from = path(entry.from, 'from'); files.add(from); + if (entry.test === true) tests.add(from); + const importKind = kind(entry.kind); + if (entry.line !== undefined && (typeof entry.line !== 'number' || !Number.isSafeInteger(entry.line) || entry.line < 1)) throw new TypeError('line: expected positive integer'); + for (const key of ['test', 'sideEffectOnly', 'unresolved']) if (entry[key] !== undefined && typeof entry[key] !== 'boolean') throw new TypeError(`${key}: expected boolean`); + for (const key of ['specifier', 'form']) if (entry[key] !== undefined) string(entry[key], key); + if (entry.symbols !== undefined) for (const symbol of array(entry.symbols, 'symbols')) string(symbol, 'symbol'); + if (entry.to === null) continue; + const to = string(entry.to, 'to'); + // URLs and package identifiers are external; repository paths in site remain nodes. + if (to.startsWith('site/')) { path(to, 'to'); files.add(to); } + edges.push({ from, to, kind: importKind, ...(typeof entry.line === 'number' ? { line: entry.line } : {}) }); + } + return { edges, files, tests }; +} + +/** Accept legacy test inputs and validate the ordered plan envelope. */ +export function editList(value: unknown): unknown[] { + if (value === undefined) return []; + if (Array.isArray(value)) return value; + const data = record(value, 'edits'); keys(data, ['edits', 'changes'], 'edits'); + const edits = array(data.edits, 'edits.edits'); + const ids = edits.map(value => { + const edit = record(value, 'edit'); + if (edit.op === 'add-file') array(edit.imports, 'add-file.imports'); + return string(edit.id, 'id'); + }); + if (new Set(ids).size !== ids.length) throw new Error('Duplicate edit id'); + const used: string[] = []; + for (const value of array(data.changes, 'changes')) { + const change = record(value, 'change'); keys(change, ['id', 'edits'], 'change'); + if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(string(change.id, 'change.id'))) throw new Error('change.id: expected kebab string'); + for (const id of array(change.edits, 'change.edits').map(value => string(value, 'edit id'))) { + if (!ids.includes(id)) throw new Error(`Unknown change edit: ${id}`); used.push(id); + } + } + if (used.length !== ids.length || new Set(used).size !== ids.length) throw new Error('Changes must partition edits'); + return edits; +} + +const under = (file: string, folder: string) => file.startsWith(`${folder}/`); +const sortEdges = (a: Edge, b: Edge) => byText(a.from, b.from) || byText(a.to, b.to) || byText(a.kind, b.kind) || (a.line ?? 0) - (b.line ?? 0); +const VIEW_KINDS: Readonly> = { + value: ['value'], 'value+lazy': ['value', 'lazy'], 'value+lazy+type': ['value', 'lazy', 'type'], all: KINDS, +}; +export function project(input: ProjectionInput) { + const tiers = tierMap(input.tiers), original = inventory(input.declarations); + for (const file of [...tiers.testFiles, ...tiers.assets]) original.files.add(file); + const moves = record(input.moves, 'moves'), destinations = new Set(); + for (const [from, value] of Object.entries(moves)) { + path(from, 'move source'); + if (!original.files.has(from)) throw new Error(`Move source is not an inventory file: ${from} (folder moves are not allowed)`); + if (value !== null) { + const target = path(value, 'move target'); + if (destinations.has(target)) throw new Error(`Move collision: ${target}`); + destinations.add(target); + } + } + const moved = (file: string): string | null => { + const value = moves[file]; + return value === undefined ? file : value === null ? null : path(value, 'move target'); + }; + const testFiles = new Set([...original.files].filter(file => isTestPath(file) || original.tests.has(file) || tiers.testFiles.includes(file)).map(file => moved(file)).filter((file): file is string => file !== null)); + for (const file of tiers.testFiles) testFiles.add(file); + const files = new Set(); + for (const file of original.files) { + const target = moved(file); + if (target !== null) { if (files.has(target)) throw new Error(`Move collision: ${target}`); files.add(target); } + } + let edges: Edge[] = []; + for (const edge of original.edges) { + const from = moved(edge.from), to = moved(edge.to); + if (from !== null && to !== null) edges.push({ ...edge, from, to }); + } + for (const value of editList(input.edits)) { + const edit = record(value, 'edit'), op = string(edit.op, 'op'); + const fileOp = op === 'add-file' || op === 'remove-file'; + keys(edit, ['id', 'line', ...(fileOp ? ['op', 'path', 'note', 'change', 'imports'] : op === 'retarget' ? ['op', 'from', 'to', 'newTo', 'kind', 'note', 'change'] : ['op', 'from', 'to', 'kind', 'note', 'change'])], 'edit'); + if (edit.id !== undefined && !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(string(edit.id, 'id'))) throw new TypeError('id: expected kebab string'); + if (edit.line !== undefined && (typeof edit.line !== 'number' || !Number.isSafeInteger(edit.line) || edit.line < 1)) throw new TypeError('edit.line: expected positive integer'); + if (edit.note !== undefined) string(edit.note, 'note'); + if (edit.change !== undefined) string(edit.change, 'change'); + if (fileOp) { + const file = path(edit.path, 'edit.path'); + if (op === 'add-file') { if (files.has(file)) throw new Error(`add-file already exists: ${file}`); files.add(file); + for (const value of array(edit.imports ?? [], 'imports')) { + const imported = record(value, 'import'); keys(imported, ['to', 'kind'], 'import'); + edges.push({ from: file, to: path(imported.to, 'import.to'), kind: kind(imported.kind) }); + } + } + else { if (!files.delete(file)) throw new Error(`remove-file did not match: ${file}`); edges = edges.filter(edge => edge.from !== file && edge.to !== file); } + continue; + } + if (!['remove-import', 'add-import', 'retarget'].includes(op)) throw new TypeError(`Unknown edit op: ${op}`); + const from = path(edit.from, 'edit.from'), to = string(edit.to, 'edit.to'), importKind = edit.kind === undefined ? undefined : kind(edit.kind); + if (!files.has(from)) throw new Error(`Edit source file does not exist: ${from}`); + const checkTarget = (target: string): void => { if (target.startsWith('site/') && !files.has(target)) throw new Error(`Edit target file does not exist: ${target}`); }; + if (op === 'add-import') { + if (importKind === undefined) throw new TypeError('add-import requires kind'); + checkTarget(to); + if (edges.some(edge => edge.from === from && edge.to === to && edge.kind === importKind)) throw new Error(`add-import already exists: ${from} -> ${to}`); + edges.push({ from, to, kind: importKind }); + } else { + const matches = (edge: Edge) => edge.from === from && edge.to === to && (importKind === undefined || edge.kind === importKind) && (edit.line === undefined || edge.line === edit.line); + if (!edges.some(matches)) throw new Error(`${op} did not match: ${from} -> ${to}`); + if (op === 'remove-import') edges = edges.filter(edge => !matches(edge)); + else { const newTo = string(edit.newTo, 'newTo'); checkTarget(newTo); edges = edges.map(edge => matches(edge) ? { ...edge, to: newTo } : edge); } + } + } + for (const edge of edges) if (edge.to.startsWith('site/') && !files.has(edge.to)) throw new Error(`Edit target file does not exist: ${edge.to}`); + edges.sort(sortEdges); + const folders = tiers.tiers.flatMap(tier => tier.folders.map(folder => ({ folder, tier: tier.tier }))).sort((a, b) => b.folder.length - a.folder.length || byText(a.folder, b.folder)); + const owner = (file: string) => folders.find(item => under(file, item.folder)) ?? (tiers.root.allow.includes(file) ? { folder: tiers.root.entries.includes(file) ? 'site(entry)' : 'site(root)', tier: tiers.root.entries.includes(file) ? tiers.root.tier : 0 } : undefined); + const domain = (file: string) => file.startsWith('site/') || folders.some(item => under(file, item.folder)) || tiers.root.allow.includes(file); + const unassigned = [...files].filter(file => domain(file) && !owner(file)).sort(byText); + const assigned = [...files].filter(file => owner(file)).sort(byText); + const inside = edges.filter(edge => owner(edge.from) && owner(edge.to)); + const ignored = edges.length - inside.length; + const forbidden = inside.filter(edge => !(tiers.tests === 'any-tier' && testFiles.has(edge.from)) && tiers.forbid.some(rule => rule.from.some(folder => under(edge.from, folder)) && rule.to.some(folder => under(edge.to, folder)))); + const productionImportsTests = edges.filter(edge => !testFiles.has(edge.from) && (testFiles.has(edge.to) || isTestPath(edge.to))); + // Leaf consumers get separate folder nodes: colocated test imports must not imply production folder dependencies. + const folderOf = (file: string) => `${owner(file)!.folder}${tiers.tests === 'any-tier' && testFiles.has(file) ? ` (test: ${file})` : ''}`; + const cycles = (nodes: string[], links: { from: string; to: string }[], source: Edge[], folder = false) => + stronglyConnected(nodes, links).filter(component => component.length > 1 || (!folder && links.some(edge => edge.from === component[0] && edge.to === component[0]))).sort((a, b) => b.length - a.length || byText(a[0]!, b[0]!)) + .map(members => ({ members, edges: source.filter(edge => members.includes(folder ? folderOf(edge.from) : edge.from) && members.includes(folder ? folderOf(edge.to) : edge.to)) })); + const views = Object.fromEntries(Object.entries(VIEW_KINDS).map(([name, kinds]) => { + const selected = inside.filter(edge => kinds.includes(edge.kind)); + const upward = selected.filter(edge => !(tiers.tests === 'any-tier' && testFiles.has(edge.from)) && owner(edge.to)!.tier > owner(edge.from)!.tier); + const lateral = selected.filter(edge => { + if (tiers.tests === 'any-tier' && testFiles.has(edge.from)) return false; + const from = owner(edge.from)!, to = owner(edge.to)!; + return from.tier === to.tier && from.folder !== to.folder && !tiers.lateral.some(pair => pair[0] === from.folder && pair[1] === to.folder); + }); + return [name, { fileSccs: cycles(assigned, selected, selected), folderSccs: cycles([...new Set(assigned.map(file => folderOf(file)))].sort(byText), + selected.map(edge => ({ from: folderOf(edge.from), to: folderOf(edge.to) })), selected, true), upward, lateral }]; + })); + const counts = [...new Set(assigned.map(file => owner(file)!.folder))].sort(byText).map(folder => ({ folder, + in: inside.filter(edge => owner(edge.to)!.folder === folder && owner(edge.from)!.folder !== folder).length, + out: inside.filter(edge => owner(edge.from)!.folder === folder && owner(edge.to)!.folder !== folder).length })); + const failed = forbidden.length > 0 || productionImportsTests.length > 0 || unassigned.length > 0 || Object.values(views).some(view => view.fileSccs.length > 0 || view.folderSccs.length > 0 || view.upward.length > 0 || view.lateral.length > 0); + return { failed, edges, forbidden, unassigned, productionImportsTests, views, folderCounts: counts, worstOffenders: [...counts].sort((a, b) => b.out + b.in - a.out - a.in || byText(a.folder, b.folder)).slice(0, 10), + summary: { files: files.size, declarations: edges.length, internalDeclarations: inside.length, ignoredDeclarations: ignored } }; +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { + try { + const args = process.argv.slice(2), options = new Map(); + for (let i = 0; i < args.length; i += 2) { + const key = args[i], value = args[i + 1]; + if (!key || !['--declarations', '--moves', '--tiers', '--edits', '--json'].includes(key) || !value || options.has(key)) throw new Error(`Unknown or incomplete argument: ${key}`); + options.set(key, value); + } + const read = (key: string): unknown => { const file = options.get(key); if (!file) throw new Error(`Missing ${key}`); return JSON.parse(readFileSync(file, 'utf8')); }; + const result = project({ declarations: read('--declarations'), moves: read('--moves'), tiers: read('--tiers'), ...(options.has('--edits') ? { edits: read('--edits') } : {}) }); + for (const [name, view] of Object.entries(result.views)) console.error(`${name}: ${view.fileSccs.length} file SCCs, ${view.folderSccs.length} folder SCCs, ${view.upward.length} upward, ${view.lateral.length} lateral`); + console.error(`Unassigned: ${result.unassigned.length}; ignored declarations: ${result.summary.ignoredDeclarations}`); + console.error('Folder in/out:', result.folderCounts); console.error('Worst offenders:', result.worstOffenders); + const json = `${JSON.stringify(result, null, 2)}\n`, out = options.get('--json'); + if (out) { mkdirSync(dirname(out), { recursive: true }); writeFileSync(out, json); } else console.log(json); + if (result.failed) process.exitCode = 1; + } catch (error) { console.error(error); process.exitCode = 1; } +} diff --git a/.github/scripts/architecture/projection.test.mts b/.github/scripts/architecture/projection.test.mts new file mode 100644 index 00000000000..4ea74c73d2e --- /dev/null +++ b/.github/scripts/architecture/projection.test.mts @@ -0,0 +1,150 @@ +/** Hand-built graph projections prove connectivity, tier direction, view filters and stale-edit rejection. */ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { project, type Edge } from './projection.mts'; +const a = 'site/a/a.mts', b = 'site/b/b.mts', c = 'site/a/c.mts'; +const tiers = { tiers: [{ tier: 0, name: 'a', folders: ['site/a'] }, { tier: 1, name: 'b', folders: ['site/b'] }], root: { allow: [] } }; +const declarations = (edges: Edge[], files: string[] = [a, b, c]) => ({ declarations: edges, summary: { files } }); +const cycle: Edge[] = [{ from: a, to: b, kind: 'value', line: 3 }, { from: b, to: a, kind: 'value', line: 8 }]; + +test('moves preserve SCCs and internal lines; a retarget edit breaks the cycle', () => { + const next = 'site/a/moved.mts'; + const result = project({ declarations: declarations(cycle), moves: { [a]: next }, tiers }); + assert.equal(result.failed, true); + assert.deepEqual(result.views.value?.fileSccs[0]?.members, [next, b]); + assert.equal(result.views.value?.folderSccs.length, 1); + assert.deepEqual(result.views.value?.fileSccs[0]?.edges.map(edge => edge.line), [3, 8]); + const fixed = project({ declarations: declarations(cycle), moves: { [a]: next }, tiers, + edits: [{ op: 'retarget', from: b, to: next, newTo: c }] }); + assert.equal(fixed.views.value?.fileSccs.length, 0); +}); + +test('SCC alone fails even when all files belong to the same folder and tier', () => { + const result = project({ declarations: declarations(cycle), moves: {}, tiers: { tiers: [{ tier: 0, name: 'all', folders: ['site'] }], root: { allow: [] } } }); + assert.equal(result.views.all?.folderSccs.length, 0); + assert.equal(result.views.all?.fileSccs.length, 1); + assert.equal(result.failed, true); +}); + +test('type, lazy and asset back edges enter precisely their views', () => { + for (const kind of ['type', 'lazy', 'asset'] as const) { + const result = project({ declarations: declarations([cycle[0]!, { ...cycle[1]!, kind }]), moves: {}, tiers }); + assert.equal(result.views.value?.fileSccs.length, 0); + assert.equal(result.views['value+lazy']?.fileSccs.length, kind === 'lazy' ? 1 : 0); + assert.equal(result.views['value+lazy+type']?.fileSccs.length, kind === 'asset' ? 0 : 1); + assert.equal(result.views.all?.fileSccs.length, 1); + } +}); + +test('upward points to the higher tier; downward succeeds', () => { + const result = project({ declarations: declarations([cycle[0]!]), moves: {}, tiers }); + assert.equal(result.views.value?.upward.length, 1); assert.equal(result.failed, true); + const down = project({ declarations: declarations([cycle[1]!]), moves: {}, tiers }); + assert.equal(down.views.value?.upward.length, 0); assert.equal(down.failed, false); +}); + +test('lateral pairs are directed allowances; longest folder prefix wins', () => { + const lateralTiers = { ...tiers, tiers: [{ tier: 0, name: 'shared', folders: ['site', 'site/a', 'site/b'] }] }; + const input = { declarations: declarations([cycle[0]!]), moves: {}, tiers: lateralTiers }; + assert.equal(project(input).views.all?.lateral.length, 1); + assert.equal(project({ ...input, tiers: { ...lateralTiers, lateral: [['site/a', 'site/b']] } }).failed, false); + assert.equal(project({ ...input, tiers: { ...lateralTiers, lateral: [['site/b', 'site/a']] } }).failed, true); +}); + +test('unassigned isolated files fail; external targets are ignored and counted; root allow works', () => { + const result = project({ declarations: declarations([{ from: a, to: 'node:fs', kind: 'value' }], [a, 'site/orphan.mts']), moves: {}, tiers }); + assert.deepEqual(result.unassigned, ['site/orphan.mts']); assert.equal(result.failed, true); + assert.equal(result.summary.ignoredDeclarations, 1); + assert.equal(project({ declarations: declarations([], ['site/env.d.ts']), moves: {}, tiers: { ...tiers, root: { allow: ['site/env.d.ts'] } } }).failed, false); +}); + +test('edits must match, apply after moves, and add/remove files explicitly', () => { + const input = { declarations: declarations(cycle), moves: {}, tiers }; + assert.throws(() => project({ ...input, edits: [{ op: 'remove-import', from: a, to: c }] }), /did not match/u); + assert.throws(() => project({ ...input, edits: [{ op: 'retarget', from: a, to: b, newTo: c, kind: 'type' }] }), /did not match/u); + assert.throws(() => project({ ...input, moves: { [a]: 'site/a/moved.mts' }, edits: [{ op: 'remove-import', from: a, to: b }] }), /does not exist/u); + const result = project({ ...input, edits: [{ op: 'remove-import', from: a, to: b }, { op: 'remove-file', path: b }, + { op: 'add-file', path: 'site/a/new.mts' }, { op: 'add-import', from: a, to: 'site/a/new.mts', kind: 'type', note: 'new contract' }] }); + assert.equal(result.failed, false); assert.equal(result.summary.declarations, 1); +}); + +test('file deletes remove incident edges; collisions and folder moves fail', () => { + const input = { declarations: declarations(cycle), moves: {}, tiers }; + assert.equal(project({ ...input, moves: { [b]: null } }).failed, false); + assert.throws(() => project({ ...input, moves: { 'site/a': 'site/new' } }), /folder moves/u); + assert.throws(() => project({ ...input, moves: { [a]: b } }), /collision/u); +}); + +test('unknown keys and invalid external values are rejected', () => { + const input = { declarations: declarations(cycle), moves: {}, tiers }; + assert.throws(() => project({ ...input, tiers: { ...tiers, typo: 1 } }), /unknown key/u); + assert.throws(() => project({ ...input, tiers: { ...tiers, tiers: [{ tier: '0', name: 'a', folders: [] }] } }), /integer/u); + assert.throws(() => project({ ...input, edits: [{ op: 'remove-import', from: a, to: b, typo: 1 }] }), /unknown key/u); + assert.throws(() => project({ ...input, declarations: { declarations: [{ ...cycle[0], kind: 'bad' }] } }), /kind/u); + assert.throws(() => project({ ...input, declarations: { declarations: [{ ...cycle[0], line: 0 }] } }), /line/u); + assert.throws(() => project({ ...input, moves: { [a]: '../escape' } }), /relative/u); +}); + +test('any-tier tests remain SCC nodes but have individual leaf folder nodes', () => { + const lowTest = 'site/a/a.test.mts', helper = 'site/a/help.test-support.mts'; + const input = { declarations: declarations([{ from: lowTest, to: b, kind: 'lazy' }, { from: b, to: a, kind: 'value' }, + { from: lowTest, to: helper, kind: 'value' }], [a, b, lowTest, helper]), moves: {}, tiers: { ...tiers, tests: 'any-tier' } }; + const result = project(input); + assert.equal(result.failed, false); assert.equal(result.views.all?.upward.length, 0); + assert.equal(result.views.all?.fileSccs.length, 0); assert.equal(result.views.all?.folderSccs.length, 0); + assert.equal(project({ ...input, tiers }).failed, true); + const cyclic = project({ ...input, edits: [{ op: 'add-import', from: helper, to: lowTest, kind: 'type' }] }); + assert.equal(cyclic.views.all?.fileSccs.length, 1); assert.equal(cyclic.failed, true); +}); + +test('production cannot import a moved test, fixture or explicitly listed helper', () => { + for (const target of ['site/a/a.test.mts', 'site/a/fixtures/data.mts', 'site/a/helper.mts']) { + const result = project({ declarations: declarations([{ from: a, to: target, kind: 'type' }], [a, target]), + moves: { [target]: 'site/a/moved.mts' }, tiers: { ...tiers, tests: 'any-tier', testFiles: [target] } }); + assert.equal(result.productionImportsTests.length, 1); assert.equal(result.failed, true); + } + assert.throws(() => project({ declarations: declarations([]), moves: {}, tiers: { ...tiers, tests: 'ignore' } }), /any-tier/u); +}); + +test('a self-import is a file SCC even though intra-folder imports are allowed', () => { + const result = project({ declarations: declarations([{ from: a, to: a, kind: 'lazy' }]), moves: {}, tiers }); + assert.equal(result.views.all?.fileSccs.length, 1); assert.equal(result.failed, true); +}); + +test('explicit denies reject a downward client-to-server edge and validate their schema', () => { + const declarations = { declarations: [{ from: 'site/client/a.mts', to: 'site/server/b.mts', kind: 'value' }] }; + const tiers = { root: { allow: [] }, tiers: [{ tier: 2, name: 'Client', folders: ['site/client'] }, { tier: 0, name: 'Server', folders: ['site/server'] }], + forbid: [{ from: ['site/client'], to: ['site/server'], reason: 'Client cannot use server' }] }; + const result = project({ declarations, moves: {}, tiers }); + assert.equal(result.forbidden.length, 1); assert.equal(result.failed, true); + assert.equal(project({ declarations, moves: {}, tiers: { ...tiers, forbid: [] } }).failed, false); + assert.throws(() => project({ declarations, moves: {}, tiers: { ...tiers, forbid: [{ from: 'site/client', to: [], reason: 'bad' }] } }), /array/u); +}); +test('same-tier siblings are forbidden; declared new-file imports can expose back edges', () => { + const declarations = { declarations: [{ from: 'site/a/a.mts', to: 'site/b/b.mts', kind: 'type' }] }; + const tiers = { root: { allow: [] }, tiers: [{ tier: 0, name: 'Siblings', folders: ['site/a', 'site/b'] }] }; + assert.equal(project({ declarations, moves: {}, tiers }).views.all!.lateral.length, 1); + const result = project({ declarations, moves: {}, tiers, edits: [{ op: 'add-file', path: 'site/b/types.mts', imports: [{ to: 'site/a/a.mts', kind: 'type' }] }, + { op: 'retarget', from: 'site/a/a.mts', to: 'site/b/b.mts', newTo: 'site/b/types.mts' }] }); + assert.equal(result.views.all!.fileSccs.length, 1); +}); + +test('the committed deny map permits page entries and forbids other startup/client-server injectors', async () => { + const { readFileSync } = await import('node:fs'); + const data: unknown = JSON.parse(readFileSync('docs/site-architecture/tiers.json', 'utf8')); + assert.ok(data && typeof data === 'object' && 'forbid' in data && 'tiers' in data); + assert.ok(Array.isArray(data.tiers)); + const folders = data.tiers.flatMap((tier: unknown) => { + assert.ok(tier && typeof tier === 'object' && 'folders' in tier && Array.isArray(tier.folders)); + return tier.folders.map((folder: unknown) => { assert.equal(typeof folder, 'string'); return String(folder); }); + }); + const services = ['site/startup', 'site/server', 'site/build']; + const root = { allow: [] }, tiers = [{ tier: 0, name: 'Services', folders: services }, + { tier: 1, name: 'Consumers', folders: folders.filter(folder => !services.includes(folder)) }]; + const run = (from: string, to: string) => project({ declarations: { declarations: [{ from, to, kind: 'value' }] }, moves: {}, tiers: { root, tiers, forbid: data.forbid } }); + assert.equal(run('site/scene/a.mts', 'site/server/b.mts').forbidden.length, 1); + assert.equal(run('site/scene/a.mts', 'site/build/b.mts').forbidden.length, 1); + assert.equal(run('site/scene/a.mts', 'site/startup/b.mts').forbidden.length, 1); + assert.equal(run('site/layouts/a.mts', 'site/startup/b.mts').failed, false); + assert.equal(run('site/pages/a.mts', 'site/startup/b.mts').failed, false); +}); diff --git a/.github/scripts/architecture/rules.mts b/.github/scripts/architecture/rules.mts index cda04f7e190..94527ebdaa6 100644 --- a/.github/scripts/architecture/rules.mts +++ b/.github/scripts/architecture/rules.mts @@ -1,7 +1,7 @@ /** Layer rules over the file-level import graph. Each rule names the imports it forbids; the committed * baseline lists existing debt for ratcheted rules; no-baseline rules fail on every finding. */ import type { ImportEdge, ImportGraph } from './graph.mts'; -import { byText } from './zones.mts'; +import { byText, isTestPath } from './zones.mts'; export interface LayerRule { readonly id: string; @@ -45,6 +45,13 @@ const AUTHORING_ROOTS = ['packages/bake/authoring/', 'packages/telescope-cli/aut const AUTHORING_LEAF_EXCEPTIONS = new Set(['labs/nebula/packages/lab/src/adapters/preparation/circumstellar.ts']); export const LAYER_RULES: readonly LayerRule[] = [ + { + id: 'production-imports-no-tests', + description: 'Site production code may not import tests, fixtures or test helpers (type-only and lazy imports count)', + forbids: (from, to) => from.startsWith('site/') && !isTestPath(from) && isTestPath(to), + includeTests: true, + noBaseline: true, + }, { id: 'objects-imports-core-only', description: 'objects may import only itself, core, npm dependencies and Node built-ins (tests and type-only imports count)', From 7842b80272c62da17e7c676a5a20db33a482d3a1 Mon Sep 17 00:00:00 2001 From: Juan Cruz Fortunatti Date: Mon, 5 Oct 2026 05:19:14 +0200 Subject: [PATCH 3/5] docs(architecture): propose the site layers, move map and code changes, checked by the projection --- .github/scripts/architecture/README.md | 43 + .github/scripts/architecture/plan-proofs.mts | 68 + .../scripts/architecture/plan-proofs.test.mts | 47 + .../scripts/architecture/plan-references.mts | 83 + .../architecture/plan-references.test.mts | 43 + .github/scripts/architecture/report.mts | 7 +- .../architecture/site-architecture.mts | 181 ++ .../architecture/site-architecture.test.mts | 61 + .../audits/check-documentation-links.mts | 4 +- .../audits/check-documentation-links.test.mts | 7 + docs/README.md | 1 + docs/site-architecture.md | 247 ++ docs/site-architecture/edits.json | 432 ++++ docs/site-architecture/loader-options.json | 349 +++ docs/site-architecture/moves.json | 291 +++ docs/site-architecture/references.json | 2166 +++++++++++++++++ docs/site-architecture/tiers.json | 502 ++++ packages/bake/AGENTS.md | 2 +- 18 files changed, 4530 insertions(+), 4 deletions(-) create mode 100644 .github/scripts/architecture/plan-proofs.mts create mode 100644 .github/scripts/architecture/plan-proofs.test.mts create mode 100644 .github/scripts/architecture/plan-references.mts create mode 100644 .github/scripts/architecture/plan-references.test.mts create mode 100644 .github/scripts/architecture/site-architecture.mts create mode 100644 .github/scripts/architecture/site-architecture.test.mts create mode 100644 docs/site-architecture.md create mode 100644 docs/site-architecture/edits.json create mode 100644 docs/site-architecture/loader-options.json create mode 100644 docs/site-architecture/moves.json create mode 100644 docs/site-architecture/references.json create mode 100644 docs/site-architecture/tiers.json diff --git a/.github/scripts/architecture/README.md b/.github/scripts/architecture/README.md index 3eb2916309f..9fe5fc8cfe8 100644 --- a/.github/scripts/architecture/README.md +++ b/.github/scripts/architecture/README.md @@ -93,3 +93,46 @@ without git history; tests pin each ceiling to its current count. Removing debt the ceiling, while any increase requires an explicit reviewed ceiling change. Missing budget files fail in every checkout. Unit mutations prove math, sorting, root calls, and budget growth fail; tests and comments are excluded through the TypeScript syntax tree. + +## Pending site layout + +The draft is in docs/site-architecture.md. Moves, sibling tiers, explicit denies, +atomic edit groups, loader alternatives and a compact path/class inventory live beside it. +`site-architecture.mts` shares the ordinary check's live graph. Draft findings fail after draftUntil or above warningCeiling; allowed warnings emit Actions annotations. +`status: "enforced"` makes every finding fail. Existing rules and scanner failures still fail. +The architecture baseline is unchanged. + +Tests are leaf consumers (`tests: "any-tier"`) but remain in file SCCs. Production +cannot import tests, fixtures or helpers. New helpers use `.test-support.mts`. +Only env.d.ts remains at the source root; generated declarations sit beside their +ignored modules in prepared. Lazy and type imports count. Lateral edges are forbidden. + +The edits envelope contains `edits` with stable ids and `changes` that partition +those ids in S3 order. New modules declare imports. Minimality removes each atomic +change group; a removal must fail. Sequence replays S3 at current paths and S4 with +unmoved files in synthetic legacy. The doc explains the loader proof limitation. + +Acceptance reads declarations from tracked files only, so ignored generated files never change the result. From a clean +checkout with installed dependencies: + +```sh +export PATH=$HOME/.nvm/versions/node/v22.23.2/bin:$PATH +pnpm install --frozen-lockfile +node .github/scripts/architecture/import-declarations.mts --prefix site/ --out output/plan7/imports.json --check-against-scanner +node .github/scripts/architecture/site-architecture.mts --write +node .github/scripts/architecture/site-architecture.mts --accept +node .github/scripts/architecture/site-architecture.mts --identity +node .github/scripts/architecture/site-architecture.mts --sequence +node .github/scripts/architecture/site-architecture.mts --minimality +pnpm check:architecture +``` + +Each move PR deletes applied map entries and updates asset/test identities, +remaining edits, tables, references and wiring. It runs +`node .github/scripts/architecture/site-architecture.mts --references --old ` +after moves; zero live occurrences are required. Historical and plan pointers are +reported separately. S3 PRs remove applied groups. L6 removes null deletion entries. +`--references` prints full line-numbered evidence on demand; do not commit that output. `--write` regenerates all marked tables +and references. Ordinary additions during the draft do not turn plan warnings into +application refactors. Strict acceptance, sequence and minimality remain blocking +proof commands. See the draft for the contributor workflow and S3 behavior gates. diff --git a/.github/scripts/architecture/plan-proofs.mts b/.github/scripts/architecture/plan-proofs.mts new file mode 100644 index 00000000000..03feaa2e9d2 --- /dev/null +++ b/.github/scripts/architecture/plan-proofs.mts @@ -0,0 +1,68 @@ +/** Replay ordered semantic changes and folder moves, and reject unnecessary semantic groups. */ +import { project, editList, type ProjectionInput } from './projection.mts'; +import { isTestPath } from './zones.mts'; +function object(value: unknown): Record { + if (!value || typeof value !== 'object' || Array.isArray(value)) throw new TypeError('Expected object'); + return Object.fromEntries(Object.entries(value)); +} +function list(value: unknown): unknown[] { if (!Array.isArray(value)) throw new TypeError('Expected array'); return value; } +function text(value: unknown): string { if (typeof value !== 'string') throw new TypeError('Expected string'); return value; } +function changes(input: ProjectionInput) { + const data = object(input.edits); editList(data); + return list(data.changes).map(value => { const entry = object(value); return { id: text(entry.id), ids: list(entry.edits).map(text) }; }); +} +/** Removal is at the PR group boundary: imports, new modules and their users are one atomic change. */ +export function minimality(input: ProjectionInput) { + if (project(input).failed) throw new Error('Minimality requires a passing full projection'); + const edits = editList(input.edits).map(object); + const results = changes(input).map(change => { + try { return { change: change.id, necessary: project({ ...input, edits: edits.filter(edit => !change.ids.includes(text(edit.id))) }).failed }; } + catch { return { change: change.id, necessary: true }; } // A missing dependency is a projection failure too. + }); + return { failed: results.some(result => !result.necessary), results }; +} +export interface SequenceStep { phase: string; step: string; fileSccs: number; folderSccs: number; upward: number; lateral: number; failed: boolean } +/** S3 keeps old file locations; S4 holds every remaining file in one temporary top-tier owner. */ +export function sequence(input: ProjectionInput): { failed: boolean; steps: SequenceStep[] } { + const tiers = object(input.tiers), moves = object(input.moves), inventory = object(input.declarations); + const originals = [...new Set([...list(object(inventory.summary).files).map(text), ...list(tiers.assets ?? []).map(text), ...list(tiers.testFiles ?? []).map(text)])]; + const edits = editList(input.edits).map(object), added = new Set(edits.filter(edit => edit.op === 'add-file').map(edit => text(edit.path))); + const inverse = new Map(Object.entries(moves).filter((entry): entry is [string, string] => typeof entry[1] === 'string').map(([old, final]) => [final, old])); + const oldPath = (file: string) => added.has(file) ? `site/${file.slice(file.lastIndexOf('/') + 1)}` : inverse.get(file) ?? file; + const translate = (edit: Record) => Object.fromEntries(Object.entries(edit).map(([key, value]) => { + if (['from', 'to', 'newTo', 'path'].includes(key)) return [key, oldPath(text(value))]; + if (key === 'imports') return [key, list(value).map(value => { const imported = object(value); return { ...imported, to: oldPath(text(imported.to)) }; })]; + return [key, value]; + })); + const liveFolders = [...new Set(originals.filter(file => file.split('/').length > 2).map(file => file.slice(0, file.indexOf('/', 5))))]; + const finalTiers = list(tiers.tiers).map(object); + const s3Tiers = { ...tiers, forbid: [], lateral: [], tiers: [...new Set([...liveFolders, ...finalTiers.flatMap(tier => list(tier.folders).map(text))])].map(folder => ({ tier: 0, name: folder, folders: [folder] })), root: { allow: [...originals.filter(file => file.split('/').length === 2), ...[...added].map(oldPath)] } }; + const deleted = Object.fromEntries(Object.entries(moves).filter(([, value]) => value === null)); + const baseline = project({ declarations: input.declarations, moves: deleted, tiers: s3Tiers }); + const steps: SequenceStep[] = [], active: Record[] = []; + let previous = baseline; + const groups = changes(input); + for (const [index, change] of groups.entries()) { + active.push(...edits.filter(edit => change.ids.includes(text(edit.id))).map(translate)); + const result = project({ declarations: input.declarations, moves: deleted, tiers: s3Tiers, edits: active }), view = result.views.all!; + const failed = Object.entries(result.views).some(([name, current]) => current.fileSccs.length > previous.views[name]!.fileSccs.length + || current.folderSccs.some(cycle => !baseline.views[name]!.folderSccs.some(old => cycle.members.every(member => old.members.includes(member)))) + || (index === groups.length - 1 && current.fileSccs.length !== 0)); + steps.push({ phase: 'S3', step: change.id, fileSccs: view.fileSccs.length, folderSccs: view.folderSccs.length, upward: view.upward.length, lateral: view.lateral.length, failed }); + previous = result; + } + const final = project(input), finalFiles = [...new Set([...originals.map(file => moves[file] === undefined ? file : moves[file] === null ? '' : text(moves[file])), ...added])].filter(Boolean); + const tests = new Set(originals.filter(file => isTestPath(file) || list(tiers.testFiles ?? []).includes(file)).map(file => moves[file] === undefined ? file : moves[file] === null ? '' : text(moves[file])).filter(Boolean)); + const folderOrder = finalTiers.flatMap(tier => list(tier.folders).map(folder => ({ folder: text(folder), tier: Number(tier.tier) }))).sort((a, b) => a.tier - b.tier || a.folder.localeCompare(b.folder)); + const moved = new Set(), root = object(tiers.root), allow = list(root.allow).map(text); + for (const { folder } of folderOrder) { + moved.add(folder); + const locate = (file: string) => (allow.includes(file) && moved.size === folderOrder.length) || [...moved].some(folder => file.startsWith(`${folder}/`)) || !file.startsWith('site/') ? file : `site/legacy/${file.slice(5)}`; + const declarations = { declarations: final.edges.map(edge => ({ ...edge, from: locate(edge.from), to: locate(edge.to), test: tests.has(edge.from) })), summary: { files: finalFiles.map(locate) } }; + const result = project({ declarations, moves: {}, tiers: { ...tiers, assets: [], testFiles: [...tests].map(locate), + tiers: [...finalTiers, { tier: Math.max(...folderOrder.map(item => item.tier)) + 2, name: 'Temporary remaining files', folders: ['site/legacy'] }] } }); + const view = result.views.all!; + steps.push({ phase: 'S4', step: folder, fileSccs: view.fileSccs.length, folderSccs: view.folderSccs.length, upward: view.upward.length, lateral: view.lateral.length, failed: result.failed }); + } + return { failed: steps.some(step => step.failed), steps }; +} diff --git a/.github/scripts/architecture/plan-proofs.test.mts b/.github/scripts/architecture/plan-proofs.test.mts new file mode 100644 index 00000000000..c4842e1d13a --- /dev/null +++ b/.github/scripts/architecture/plan-proofs.test.mts @@ -0,0 +1,47 @@ +/** Mutations must invalidate ordered replay and group minimality, not only the final graph. */ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { sequence, minimality } from './plan-proofs.mts'; +import { readFileSync } from 'node:fs'; +import { trackedStandIn } from './graph.mts'; +const fixture = { + declarations: { declarations: [{ from: 'site/a.mts', to: 'site/b.mts', kind: 'value' }, { from: 'site/b.mts', to: 'site/a.mts', kind: 'type' }], summary: { files: ['site/a.mts', 'site/b.mts'] } }, + moves: { 'site/a.mts': 'site/high/a.mts', 'site/b.mts': 'site/low/b.mts' }, + tiers: { tests: 'any-tier', root: { allow: [] }, tiers: [{ tier: 0, name: 'Low', folders: ['site/low'] }, { tier: 1, name: 'High', folders: ['site/high'] }] }, + edits: { edits: [{ id: 'cut-cycle', op: 'remove-import', from: 'site/low/b.mts', to: 'site/high/a.mts', kind: 'type' }], changes: [{ id: 'cut', edits: ['cut-cycle'] }] }, +}; +test('old locations replay; S4 prefixes never import legacy from a moved lower folder', () => { + assert.equal(sequence(fixture).failed, false); + assert.equal(sequence(fixture).steps[0]!.fileSccs, 0); + const wrongOrder = { ...fixture, tiers: { ...fixture.tiers, tiers: [...fixture.tiers.tiers].reverse().map((tier, index) => ({ ...tier, tier: index })) } }; + assert.equal(sequence(wrongOrder).failed, true); +}); +test('new S3 cycles fail even if a later change cuts them', () => { + const edits = [ + { id: 'add-node', op: 'add-file', path: 'site/low/new.mts', imports: [] }, + { id: 'add-self-cycle', op: 'add-import', from: 'site/low/new.mts', to: 'site/low/new.mts', kind: 'value' }, + { id: 'remove-self-cycle', op: 'remove-import', from: 'site/low/new.mts', to: 'site/low/new.mts', kind: 'value' }, + ...fixture.edits.edits, + ]; + assert.equal(sequence({ ...fixture, edits: { edits, changes: [{ id: 'bad', edits: ['add-node', 'add-self-cycle'] }, { id: 'fix', edits: ['remove-self-cycle', 'cut-cycle'] }] } }).failed, true); +}); +test('a droppable group fails minimality; removing a required cut breaks acceptance', () => { + assert.equal(minimality(fixture).failed, false); + const redundant = { id: 'add-unused', op: 'add-file', path: 'site/low/unused.mts', imports: [] }; + assert.equal(minimality({ ...fixture, edits: { edits: [...fixture.edits.edits, redundant], changes: [...fixture.edits.changes, { id: 'unused', edits: ['add-unused'] }] } }).failed, true); +}); +test('generated module maps to its declaration in the same prepared folder', () => { + assert.equal(trackedStandIn('site/prepared/prepared-shell-icons.mjs', new Set(['site/prepared/prepared-shell-icons.d.mts'])), 'site/prepared/prepared-shell-icons.d.mts'); +}); +test('committed plan has seven ordered, necessary change groups', () => { + const data = JSON.parse(readFileSync('docs/site-architecture/edits.json', 'utf8')) as unknown; + assert.ok(data && typeof data === 'object' && 'changes' in data && Array.isArray(data.changes)); + assert.equal(data.changes.length, 7); +}); + +test('the last S3 prefix must eliminate pre-existing file cycles even without increasing them', () => { + const remaining = { ...fixture, edits: { edits: [{ id: 'noop', op: 'add-file', path: 'site/low/noop.mts', imports: [] }], changes: [{ id: 'noop', edits: ['noop'] }] } }; + const result = sequence(remaining); + assert.equal(result.steps.find(step => step.phase === 'S3')?.fileSccs, 1); + assert.equal(result.steps.find(step => step.phase === 'S3')?.failed, true); +}); diff --git a/.github/scripts/architecture/plan-references.mts b/.github/scripts/architecture/plan-references.mts new file mode 100644 index 00000000000..707959da126 --- /dev/null +++ b/.github/scripts/architecture/plan-references.mts @@ -0,0 +1,83 @@ +/** Inventory literal move references and depth-sensitive reads; dated records stay separate from live pointers. */ +import { spawnSync, execFileSync } from 'node:child_process'; +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, posix, resolve } from 'node:path'; +import { byText, isTestPath } from './zones.mts'; +export interface Reference { old: string; file: string; line: number; form: string; classification: string; scope: 'live' | 'history' | 'plan'; text: string } +function classify(file: string, line: string): string { + if (file.endsWith('AGENTS.md') || file.endsWith('CLAUDE.md')) return 'AGENTS.md contract text'; + if (file.endsWith('.py')) return 'Python script'; + if (file.includes('ci-areas')) return 'CI routing'; + if (file.startsWith('.github/workflows/')) return 'workflow'; + if (/tsconfig|eslint|package\.json/u.test(file)) return 'tsconfig/eslint/package.json'; + if (/import\s*\([^'"`]/u.test(line)) return 'computed import'; + if (/\b(?:import|export)\b.*['"]/u.test(line)) return 'source import'; + if (file.endsWith('.md')) return /\]\(/u.test(line) ? 'docs link' : 'docs code-span'; + return 'generator/producer literal'; +} +/** A literal scan includes untracked draft files, unlike plain git grep; ignores binary evidence. */ +export function references(root: string, moves: Readonly>, only?: readonly string[]) { + const paths = [...new Set([...Object.keys(moves), ...(Object.keys(moves).some(path => path.startsWith('site/test/')) ? ['site/test/'] : [])])], occurrences: Reference[] = [], relativeReads: { file: string; destination: string; line: number; text: string }[] = []; + const patterns = [...new Set(paths.flatMap(old => { + if (old.endsWith('/')) return [old, './test/']; + const base = old.slice(old.lastIndexOf('/') + 1), without = old.replace(/\.(?:[cm]?[jt]s|astro|json|css)$/u, ''); + const stem = base.replace(/\.(?:[cm]?[jt]s|astro|json|css)$/u, ''); + return [old, without, old.slice(5), base, ...["'", '"', '`'].map(quote => `/${stem}${quote}`)]; + }))]; + const grep = only ? { status: 1, stdout: '', stderr: '', error: undefined } : spawnSync('git', ['grep', '-I', '-l', '-F', '-f', '-', '--'], { cwd: root, encoding: 'utf8', input: `${patterns.join('\n')}\n`, maxBuffer: 64 * 1024 * 1024 }); + if (grep.error) throw grep.error; + if (grep.status !== 0 && grep.status !== 1) throw new Error(`git grep failed: ${grep.stderr}`); + const untracked = only ? [] : execFileSync('git', ['ls-files', '--others', '--exclude-standard', '-z'], { cwd: root, encoding: 'utf8' }).split('\0').filter(Boolean); + const selected = only ?? [...new Set([...grep.stdout.split('\n').filter(Boolean), ...untracked, ...paths.filter(path => !path.endsWith('/') && isTestPath(path))])].sort(); + for (const file of selected) { + if (!existsSync(resolve(root, file))) continue; + if (file.startsWith('docs/site-architecture/') || /^\.github\/scripts\/architecture\/(?:site-architecture|projection|plan-proofs|plan-references|import-declarations)(?:\.test)?\.mts$/u.test(file)) continue; // The inventory cannot inventory its own serialization. + const bytes = readFileSync(resolve(root, file)); if (bytes.includes(0)) continue; + const content = bytes.toString('utf8'); + const candidates = paths.filter(old => { + if (old.endsWith('/')) return content.includes(old) || content.includes(old.slice(5)); + const base = old.slice(old.lastIndexOf('/') + 1), stem = base.replace(/\.(?:[cm]?[jt]s|astro|json|css)$/u, ''); + return content.includes(old) || content.includes(old.replace(/\.(?:[cm]?[jt]s|astro|json|css)$/u, '')) || content.includes(old.slice(5)) || content.includes(base) + || ["'", '"', '`'].some(quote => content.includes(`/${stem}${quote}`)); + }); + if (!candidates.length && !(isTestPath(file) && typeof moves[file] === 'string')) continue; + const lines = content.split('\n'); + const scope = file.startsWith('docs/site-architecture') || /^\.github\/scripts\/architecture\/(?:site-architecture|projection|plan-proofs|plan-references|import-declarations)(?:\.test)?\.mts$/u.test(file) ? 'plan' + : /(?:^untangle\/|\/history\/|\/archive\/|\/done\/|(?:^|\/)(?:reports|records)\/)/u.test(file) ? 'history' : 'live'; + let generated = false; + for (const [index, line] of lines.entries()) { + if (file === 'docs/site-architecture.md' && line.startsWith('`, end = ``; + if (result.split(start).length !== 2 || result.split(end).length !== 2) throw new Error(`Missing or duplicate ${name} markers`); + const first = result.indexOf(start) + start.length, last = result.indexOf(end); + if (last < first) throw new Error(`Reversed ${name} markers`); + result = `${result.slice(0, first)}\n${content}\n${result.slice(last)}`; + } + return result; +} + +/** Fail on stale generated content; exported to mutation-test the actual throw. */ +export function assertCurrent(document: string, next: string): void { + if (document !== next) throw new Error('Site architecture tables are stale; run node .github/scripts/architecture/site-architecture.mts --write'); +} +/** Findings have a fixed draft deadline and a checked warning budget; strict modes always throw. */ +export function planFinding(status: unknown, check: () => void, policy: { draftUntil?: string; warningCeiling?: number; today?: string; actions?: boolean } = {}): void { + if (status !== 'draft' && status !== 'enforced' && status !== undefined) throw new TypeError('status: expected draft or enforced'); + try { check(); } catch (error) { + if (status !== 'draft') throw error; + const today = policy.today ?? new Date().toISOString().slice(0, 10); + if (!policy.draftUntil || today > policy.draftUntil) throw new Error(`Draft expired: ${policy.draftUntil ?? 'no deadline'}; ${String(error)}`); + if (1 > (policy.warningCeiling ?? 0)) throw new Error(`Draft warning ceiling exceeded (1 > ${policy.warningCeiling ?? 0}); ${String(error)}`); + const message = `SITE_PLAN_WARNING: ${String(error)}. Fix: node .github/scripts/architecture/site-architecture.mts --write`; + console.warn(message); + if (policy.actions ?? process.env.GITHUB_ACTIONS === 'true') console.warn(`::warning file=docs/site-architecture.md::${message.replaceAll('%', '%25').replaceAll('\r', '%0D').replaceAll('\n', '%0A')}`); + } +} + +export async function checkSiteArchitecture(root: string, options: { readonly graph?: ImportGraph; readonly write?: boolean; readonly sequence?: boolean; readonly minimality?: boolean; readonly identity?: boolean; readonly accept?: boolean; readonly references?: boolean; readonly old?: readonly string[] } = {}): Promise { + const started = performance.now(); + const read = (name: string): unknown => JSON.parse(readFileSync(resolve(root, DATA, `${name}.json`), 'utf8')); + const moves = read('moves'), tiers = read('tiers'), edits = read('edits'); + if (options.references) { + const inventory = references(root, { ...record(moves), ...Object.fromEntries((options.old ?? []).map(old => [old, null])) }); + if (options.old?.length) { + const findings = liveReferences(inventory, options.old); console.log(JSON.stringify({ liveOldPaths: findings, folderAndGlobReview: coveringReferences(inventory, options.old) }, null, 2)); + if (findings.length) throw new Error(`${findings.length} live old-path references`); + } else console.log(JSON.stringify(inventory, null, 2)); + return; + } + const declarations = await readImportDeclarations(root, { prefix: 'site/', graph: options.graph, checkAgainstScanner: true }); + if (declarations.summary.scanner.unexplained) throw new Error('Site declarations disagree with the production scanner'); + const known = new Set(repositoryFiles(root)); + const tierData = record(tiers); + const validateInputs = () => { + // Ignored generated inputs may be absent before preparation; their proposed relocations require the documented generator edits. + for (const file of [...Object.keys(record(moves)), ...list(tierData.testFiles ?? []), ...list(tierData.assets ?? [])].map(text)) { + if (!known.has(file) && !/^site\/(?:prepared-[^/]+|moon-labels\.prepared\.json)$/u.test(file)) throw new Error(`Unknown plan input: ${file}`); + } + }; + const input = { declarations, moves, tiers, edits }; + if (options.identity) { + const roots = declarations.summary.files.filter(file => file.split('/').length === 2); + const result = project({ declarations, moves: {}, tiers: { ...tierData, root: { allow: roots } } }); + console.log(JSON.stringify(result.views, null, 2)); return; + } + if (options.sequence || options.minimality) { + validateInputs(); + const result = options.sequence ? sequence(input) : minimality(input); console.log(JSON.stringify(result, null, 2)); + if (result.failed) throw new Error('Plan proof failed'); return; + } + planFinding(options.accept ? 'enforced' : tierData.status, () => { + validateInputs(); + const tables = renderTables(input); + if (!Array.isArray(edits)) { + const proof = sequence(input); + tables.sequence = [row(['Phase', 'Step', 'File SCCs', 'Folder SCCs', 'Upward', 'Lateral', 'Result']), row(['---', '---', '---:', '---:', '---:', '---:', '---']), + ...proof.steps.map(step => row([step.phase, step.step, step.fileSccs, step.folderSccs, step.phase === 'S3' ? 'n/a' : step.upward, step.phase === 'S3' ? 'n/a' : step.lateral, step.failed ? 'FAIL' : 'pass']))].join('\n'); + if (proof.failed) throw new Error('Sequence proof failed; run node .github/scripts/architecture/site-architecture.mts --sequence'); + if (minimality(input).failed) throw new Error('Minimality proof failed; run node .github/scripts/architecture/site-architecture.mts --minimality'); + const inventory = compactReferences(references(root, record(moves))); + const stored = readFileSync(resolve(root, DATA, 'references.json'), 'utf8'); + if (!options.write) assertCurrent(stored, `${JSON.stringify(inventory, null, 2)}\n`); + const referenceData = record(inventory); + const counts = new Map(); + for (const value of list(referenceData.occurrences)) { const entry = record(value); const key = `${text(entry.scope)}: ${text(entry.classification)}`; counts.set(key, (counts.get(key) ?? 0) + 1); } + tables.references = [row(['Scope and class', 'Path/class pairs']), row(['---', '---:']), ...[...counts].sort().map(([key, count]) => row([key, count]))].join('\n'); + tables['relative-reads'] = [row(['Test with depth-sensitive reads', 'Destination']), row(['---', '---']), + ...list(referenceData.relativeReads).map(value => { const entry = record(value); return row([text(entry.file), text(entry.destination)]); })].join('\n'); + const refPath = resolve(root, DATA, 'references.json'), expected = `${JSON.stringify(inventory, null, 2)}\n`; + if (options.write) writeFileSync(refPath, expected); + } + const docPath = resolve(root, DOC), document = readFileSync(docPath, 'utf8'), next = updateTables(document, tables); + if (options.write) { + writeFileSync(docPath, next); + } else assertCurrent(document, next); + console.log(`SITE_PLAN_OK: all views clear; tables match (${((performance.now() - started) / 1000).toFixed(1)} s${options.graph ? ', shared graph' : ', including scan'}).`); + }, { draftUntil: text(tierData.draftUntil), warningCeiling: number(tierData.warningCeiling) }); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { + try { + const args = process.argv.slice(2); + const modes = ['--write', '--sequence', '--minimality', '--identity', '--references', '--accept']; + const mode = args[0]; + if (mode && !modes.includes(mode)) throw new Error('Usage: site-architecture.mts [--accept|--write|--sequence|--minimality|--identity|--references [--old paths...]]'); + const old = mode === '--references' && args[1] === '--old' ? args.slice(2) : []; + if (args.length > 1 && !old.length) throw new Error('Unexpected arguments'); + await checkSiteArchitecture(resolve(import.meta.dirname, '../../..'), { write: mode === '--write', sequence: mode === '--sequence', + minimality: mode === '--minimality', identity: mode === '--identity', references: mode === '--references', old, accept: mode === '--accept' }); + } catch (error) { console.error(error); process.exitCode = 1; } +} diff --git a/.github/scripts/architecture/site-architecture.test.mts b/.github/scripts/architecture/site-architecture.test.mts new file mode 100644 index 00000000000..a0a455d601b --- /dev/null +++ b/.github/scripts/architecture/site-architecture.test.mts @@ -0,0 +1,61 @@ +/** The plan's generated tables must expose connectivity failures and become stale on data changes. */ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { renderTables, updateTables } from './site-architecture.mts'; +const input = { + declarations: { declarations: [{ from: 'site/a.mts', to: 'site/base/base.mts', kind: 'value' }], summary: { files: ['site/a.mts', 'site/base/base.mts'] } }, + moves: { 'site/a.mts': 'site/app/a.mts' }, + tiers: { tiers: [{ tier: 1, name: 'Foundation', folders: ['site/base'] }, { tier: 2, name: 'Application', folders: ['site/app'] }], root: { allow: [] } }, edits: [], +}; +test('renders counts, before/after and exact marked tables; changed data changes the document', () => { + const tables = renderTables(input); + assert.match(tables.folders!, /Application \| 1 \| 1/u); + assert.match(tables.numbers!, /\| all \| 0 \| 0 \| 1 \| 0 \| 0 \| 0 \| 0 \| 0 \|/u); + const document = Object.keys(tables).map(name => `\n`).join('\n'); + const written = updateTables(document, tables); + assert.notEqual(document, written); assert.equal(updateTables(written, tables), written); + assert.notEqual(updateTables(written, renderTables({ ...input, tiers: { ...input.tiers, + tiers: input.tiers.tiers.map(tier => ({ ...tier, name: `${tier.name} changed` })) } })), written); + assert.throws(() => updateTables('', tables), /markers/u); + assert.throws(() => updateTables(document + document, tables), /markers/u); +}); +test('refuses to render a plan with cycles or uncovered root files', () => { + assert.throws(() => renderTables({ ...input, moves: {} }), /fails projection/u); + assert.throws(() => renderTables({ ...input, edits: [{ op: 'add-import', from: 'site/base/base.mts', to: 'site/app/a.mts', kind: 'lazy', + change: 'Bad cycle', note: 'Mutation makes the base depend on application.' }] }), /fails projection/u); +}); + +test('the stale-table guard throws; equal content passes', async () => { + const { assertCurrent } = await import('./site-architecture.mts'); + assert.throws(() => assertCurrent('stale', 'current'), /stale/u); + assert.doesNotThrow(() => assertCurrent('current', 'current')); +}); +test('draft findings warn and enforced findings throw; malformed status fails', async () => { + const { planFinding } = await import('./site-architecture.mts'); + const failure = () => { throw new Error('plan changed'); }; + const warn = console.warn, messages: unknown[] = []; console.warn = value => { messages.push(value); }; + try { + assert.doesNotThrow(() => planFinding('draft', failure, { draftUntil: '2026-11-02', today: '2026-10-05', warningCeiling: 1 })); assert.match(String(messages[0]), /WARNING.*--write/u); + assert.throws(() => planFinding('enforced', failure), /plan changed/u); + assert.throws(() => planFinding('unsigned', failure), /status/u); + } finally { console.warn = warn; } +}); + +test('expired draft findings fail and a zero warning ceiling fails; Actions annotates allowed findings', async () => { + const { planFinding } = await import('./site-architecture.mts'); + const failure = () => { throw new Error('changed'); }; + assert.throws(() => planFinding('draft', failure, { draftUntil: '2000-01-01', today: '2026-10-05', warningCeiling: 1 }), /expired/u); + assert.throws(() => planFinding('draft', failure, { draftUntil: '2026-11-02', today: '2026-10-05', warningCeiling: 0 }), /ceiling/u); + const warn = console.warn, messages: string[] = []; console.warn = value => { messages.push(String(value)); }; + try { + planFinding('draft', failure, { draftUntil: '2026-11-02', today: '2026-10-05', warningCeiling: 1, actions: true }); + assert.ok(messages.some(message => message.startsWith('::warning file='))); + } finally { console.warn = warn; } +}); + +test('draft policy requires a valid calendar date and a nonnegative warning ceiling', () => { + const tiers = { ...input.tiers, status: 'draft', draftUntil: '2026-11-02', warningCeiling: 0 }; + assert.doesNotThrow(() => renderTables({ ...input, tiers })); + assert.throws(() => renderTables({ ...input, tiers: { ...tiers, draftUntil: '2026-02-30' } }), /draftUntil/u); + assert.throws(() => renderTables({ ...input, tiers: { ...tiers, warningCeiling: -1 } }), /warningCeiling/u); +}); diff --git a/.github/scripts/audits/check-documentation-links.mts b/.github/scripts/audits/check-documentation-links.mts index 16bb9abec63..94ff13c5eb4 100644 --- a/.github/scripts/audits/check-documentation-links.mts +++ b/.github/scripts/audits/check-documentation-links.mts @@ -196,7 +196,9 @@ function audit(snapshot: Snapshot): Audit { // Owner-designated documentation evidence beside the virtual telescope guide (Wave T). const projectionEvidence = new Set(['docs/fixtures/telescope-projection/europa-navigation.json', 'docs/fixtures/telescope-projection/europa-oracle.json', 'docs/fixtures/telescope-projection/europa-registration.json']); - for (const file of docs) if (!guides.has(file) && !illustrations.has(file) && !projectionEvidence.has(file)) errors.push({file, reason: 'docs/ accepts Markdown guides and illustrations under docs/images/; move code, fixtures and raw output to their owner'}); + // The site proposal is generated from these exact owner-requested inputs; arbitrary JSON stays forbidden. + const sitePlanInputs = new Set(['docs/site-architecture/moves.json', 'docs/site-architecture/tiers.json', 'docs/site-architecture/edits.json', 'docs/site-architecture/references.json', 'docs/site-architecture/loader-options.json']); + for (const file of docs) if (!guides.has(file) && !illustrations.has(file) && !projectionEvidence.has(file) && !sitePlanInputs.has(file)) errors.push({file, reason: 'docs/ accepts Markdown guides and illustrations under docs/images/; move code, fixtures and raw output to their owner'}); const index = 'docs/README.md'; if (!guides.has(index)) errors.push({file: index, reason: 'missing documentation index'}); const pending = guides.has(index) ? [index] : []; diff --git a/.github/scripts/audits/check-documentation-links.test.mts b/.github/scripts/audits/check-documentation-links.test.mts index 69f7c79de5c..42572858671 100644 --- a/.github/scripts/audits/check-documentation-links.test.mts +++ b/.github/scripts/audits/check-documentation-links.test.mts @@ -1,3 +1,4 @@ +/** Documentation links, placement and narrowly admitted executable plan inputs. */ import assert from 'node:assert/strict'; import {execFileSync, spawnSync} from 'node:child_process'; import {mkdirSync, mkdtempSync, rmSync, writeFileSync} from 'node:fs'; @@ -215,3 +216,9 @@ test('the three guide-owned telescope projection records are documentation evide f.write(`docs/fixtures/telescope-projection/europa-${name}.json`, '{}'); assert.deepEqual(f.check().errors, []); }); + +test('only named executable site-plan inputs are admitted beside the guide', t => { + const f = fixture(t); + for (const name of ['moves', 'tiers', 'edits', 'references', 'loader-options', 'unexpected']) f.write(`docs/site-architecture/${name}.json`, '{}'); + assert.deepEqual(f.check().errors.map(error => error.file), ['docs/site-architecture/unexpected.json']); +}); diff --git a/docs/README.md b/docs/README.md index 8d304a2764d..09cd6766490 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,6 +9,7 @@ For a body's sources, processing, evidence and known problems, read its | Topic | Guide | | --- | --- | +| Proposed site folders, dependency tiers and move sequence | [Site architecture draft](site-architecture.md) | | Computed schema ownership across packages and architecture locks | [Prepared format ownership](prepared-format-ownership.md) | | PR feedback budget, check selection, failure recovery and deployment | [CI/CD maintenance](ci-cd.md) | | Galaxies, LMC image datasets and extragalactic datasets | [Galaxies and the nearby universe](galaxies/README.md) | diff --git a/docs/site-architecture.md b/docs/site-architecture.md new file mode 100644 index 00000000000..96d6fd052f3 --- /dev/null +++ b/docs/site-architecture.md @@ -0,0 +1,247 @@ +# Proposed site architecture + +**Recommend option 3: relocate queued loader functions to their owners.** This draft requires owner and alowpoly sign-off before S3. S2 changes only the plan and checker; it moves no application files. The projection proves connectivity, not runtime behavior or generated chunk bytes. + +[moves.json](site-architecture/moves.json) records destinations; [tiers.json](site-architecture/tiers.json) records layers, denies and draft policy; [edits.json](site-architecture/edits.json) records the recommended semantic changes; [loader-options.json](site-architecture/loader-options.json) records alternatives; [references.json](site-architecture/references.json) is a compact path-and-class inventory. + +## Folder ownership + +A folder imports itself or strictly lower layers. Sibling folders, including L0 siblings, cannot import each other. Value, lazy, type, CSS and JSON edges all count. Tests are leaf consumers above production. There are no lateral allowances. For a legitimate new sibling edge, re-derive longest-path levels for the production DAG and regenerate all tables; a cycle requires an ownership change, not a tier exemption. + + +| Tier | Folder | Purpose | Final files | Incoming moves | +| ---: | --- | --- | ---: | --- | +| 0 | `browser/` | Browser input, DOM and serialized import queue | 17 | 15; `runtime-policy.mts`, `diagnostics-policy.mts`, `in-flight-loader.mts`, `next-frame.mts` | +| 0 | `model/` | Object identities, routes and held addresses | 8 | 7; `navigation/system-address.mts`, `root-object.mts`, `orbit-root.mts`, `planetary-system-members.mts` | +| 0 | `overview/` | Prepared spectral overview readers | 4 | 2 | +| 0 | `prepared/` | Prepared transports and generated inputs | 25 | 24; `prepared-catalogue.d.mts`, `prepared-navigation-markers.d.mts`, `prepared-object-path.mts`, `prepared-shell-icons.d.mts` | +| 0 | `source/` | Preserved input records and artwork | 60 | 0 | +| 0 | `vendor/` | Preserved third-party notices | 1 | 0 | +| 0 | `server-assets/` | Server and build prepared-asset origin | 1 | 1; `asset-origin.mts` | +| 0 | `contracts/` | Shared page and shell interfaces | 3 | 3; `object-shell-types.ts`, `object-page-contract.mts` | +| 1 | `directory/` | Startup reads and object catalogue directory | 10 | 9; `startup-world.mts`, `startup-requests.mts`, `object-entries.mts`, `object-directory.mts` | +| 1 | `minimap/` | Surface-map measurements and view formatting | 7 | 4 | +| 2 | `world/` | Shared framing, visibility and camera context | 54 | 53; `world-objects.mts`, `world-system-views.mts`, `context-availability.mts`, `context-datasets.mts` | +| 3 | `content/` | Card content, citations and metadata | 21 | 20; `dataset-content.mts`, `dataset-context.mts`, `object-text.mts`, `prepared-panel-content.mts` | +| 3 | `navigation/` | History, requests, flights and arrivals | 34 | 22; `prepared-arrival.mts`, `prepared-scene-ownership.mts`, `arrival-billboard.mts`, `prepared-world-navigation.mts` | +| 4 | `search/` | Catalogue search and result presentation | 17 | 6 | +| 4 | `selection/` | Committed selection and camera handovers | 6 | 6; `scene/scene-selection.mts`, `satellite-selection.mts`, `overview-selection.mts`, `showcase.mts` | +| 5 | `shell/` | Retained shell controls and panels | 27 | 20; `object-browser.mts`, `feature-browser.mts`, `destination-browser.mts`, `selection-presentation.mts` | +| 6 | `server/` | SSR readers, responses and host middleware | 30 | 17; `object-page-data.mts`, `first-view-transport.mts`, `object-entry.mts`, `world-places.mts` | +| 6 | `scene/` | Scene sessions, replacement and publication | 26 | 11; `object-adapter.mts`, `packaged-object-runtime.mts`, `startup-billboard.mts`, `initial-scene.mts` | +| 7 | `startup/` | Page boot helpers and router entry | 7 | 7; `shared-imports.mts`, `startup-boot.mts`, `startup-cover.mts`, `initial-shell-context.mts` | +| 7 | `build/` | Site preparation and packaging | 82 | 31; `prepare-body-moons.mts` | +| 8 | `layouts/` | Shared page frame and its styles | 7 | 5; `components/ObjectSwatchStyles.astro`, `site.css`, `object-shell.css`, `wordmark.css` | +| 9 | `components/` | Reusable Astro markup and SSR composition | 29 | 0 | +| 10 | `pages/` | Routes and object-page entries | 20 | 0 | +| 11 | `journeys/` | Cross-cutting journeys and support | 19 | 19 | +| 11 | `test/` | Retired test folder; no final occupants | 0 | 0 | + + +The table is the final layering. Directory owns startup requests, startup world reads, object entries, registry and world context plan: it is the lower world family. World owns framing, visibility, datasets and camera context. History and fragments belong to navigation. The model's held-address reader carries its WeakMap and registration/disposal state; history keeps its re-export. + +Startup is the page-boot root. Startup cover, initial shell context, error reporting and analytics are used only by ObjectLayout and belong there. Browser retains shared input/DOM primitives and the import queue. Server-assets owns the node-dependent asset-origin reader used by server and build; clients may not enter it. ObjectSwatchStyles moves beside its layout; ObjectPage stays in components. The layout module-script body stays in ObjectLayout: extraction is unnecessary. + +Server/search-response deliberately shares shell/selection-presentation and selection/scene-selection with the client. These operate on its supplied server document and selection values; they are shared presentation/selection logic, not browser entry points. This is an explicit permitted downward dependency, not a general server-to-client isolation claim. Client owners cannot import server, build or server-assets. Layouts/components/pages cannot import build. Only layouts and pages may enter startup. + +Registry types live in directory, navigation types in navigation and presenter types in scene. Their declared imports model implementation dependencies; adding an edge cannot establish necessity. + +## Draft workflow + +The draft deadline is **2026-11-02**, inclusive. The owner may extend draftUntil explicitly in tiers.json before expiry. Until then a plan finding is a warning: the check prints it with the fix command and, in GitHub Actions, as an annotation, and a contributor's PR is not blocked (the check stops at the first finding, so the ceiling `warningCeiling: 1` allows exactly that one warning). After the deadline every plan finding fails. Existing scanner/rule failures always fail. Strict --accept always fails on findings. + +When an unsigned plan drifts, update its destination/semantic edits and regenerate it. Do not reshape application behavior solely to silence a draft. Sign-off changes status from draft to enforced. Every mapped path is checked for existence, including deleted mapped files; generated inputs are explicit exceptions. Routine checks rescan the compact inventory so stale references fail rather than persist invisibly. + +## S3 atomic changes + +The changes array partitions stable edit ids into atomic groups. All seven groups are necessary by removal projection. **Minimality holds at group level only**: individual modelling imports can be removable. Groups currently commute; the listed order is a tested replay, not a claim that another order must fail. Added modules are placed at root during S3, where destination folders may not yet exist; S4 moves them with their owners. The L6 null moves are deletion preconditions. + + +| Tier | Change / PR | Edit ids | Required code change | +| ---: | --- | --- | --- | +| 0 | Extract held address reader | add-file-navigation-href, add-import-navigation-history-navigation-href, retarget-application-world-resources-navigation-href | Move navigationHref AND its owners WeakMap. Export a registration function for history; preserve deferred URL semantics. History registers and unregisters its held address reader in the same WeakMap; re-export the getter for existing navigation callers. Only lower world caller uses navigationHref; other callers retain history behavior. | +| 1 | Extract registry types | add-file-object-entry-types, retarget-object-directory-object-entry-types, add-import-objects-object-entry-types | Extract registry aliases from objects; directory must not import the registry that calls it. Use the extracted registry aliases without evaluating the full registry. Registry imports and re-exports its extracted aliases so clients can migrate without duplication. | +| 3 | Extract navigation contracts | add-file-navigation-types, retarget-prepared-arrival-navigation-types, retarget-navigation-history-navigation-types, add-import-prepared-world-navigation-navigation-types, add-import-navigation-request-navigation-types | Own WorldHandoff, NavigationHistory and NavigationIntent beside navigation. Import MountOptions, ObjectSceneLifecycle, ObjectWorldNavigation, SceneSubject and PageView; preserve erased aliases SceneView and SelectionTarget. Arrival reads WorldHandoff from the lower contract; navigation still owns flights. Extract NavigationHistory and NavigationIntent into navigation-types; the history transport must not import request orchestration. Flights import and re-export the extracted WorldHandoff contract. Request imports its extracted intent and history types and the plain selection shape; selection behavior is passed in. | +| 3 | Split the moon catalogue reader | add-file-moon-catalogue, retarget-object-children-moon-catalogue, remove-import-prepare-body-moons-moon-catalogues, add-import-prepare-body-moons-moon-catalogue | Split catalogue parsing, cached source catalogue and catalogueMoons into a runtime-safe reader; preparation keeps search-based ordering. Read catalogueMoons from the split reader; production must never import build preparation. Move the source catalogue import with its parser into the reader; preparation calls that reader. Use the single catalogue reader for preparation too; preserve the named-moon eligibility policy in preparation. | +| 3 | Pass selection into navigation requests | remove-import-navigation-request-scene-selection, add-import-navigation-request-scene-subject | Pass selectionTargetFromUrl explicitly through resolveNavigation and readNavigationSelection. Router uses the existing registry export, injects it into createSceneActivation, which forwards all three calls. Import erased SceneView/SelectionTarget aliases from navigation-types; never retain the selection import. Keep existing subject identity helpers in the lower world owner; pass selectionTargetFromUrl as a parameter. | +| 6 | Extract the frame presenter contract | add-file-scene-frame-presenter, retarget-scene-session-scene-frame-presenter, add-import-scene-world-scene-frame-presenter | Extract the structural presenter interface; a session must not import the world coordinator that consumes it. Session reads SceneFramePresenter from the lower contract, breaking the two-way type dependency. World coordinator imports and re-exports the structural presenter contract. | +| 0 | Relocate queued loader owners | add-file-scene-imports, add-file-world-imports, remove-queue-runtime, remove-shared-registry, remove-shared-world, retarget-adapter-scene-imports, retarget-router-world-imports, add-router-scene-imports, remove-directory-queue, retarget-boot-scene-imports, add-boot-world-imports | Own queued registry and packaged-runtime functions in scene; share browser queuedImport and its single last chain. Own the queued application-world import in world; no initializer inside the thunk. Move the runtime function out of the import-free browser queue implementation. Leave only importSceneRouter in startup/shared-imports. Move the world function to its owner. Adapter directly uses the same queued runtime function; preserve the frozen default adapter. Split router loader imports; preserve module-evaluation autostart. Import registry/runtime functions and register only the directory runtime loader using the existing object-directory import before first scene call. Directory accepts a typed runtime-loader registration; read the slot at scene invocation, not metadata construction. Missing registration fails explicitly. Body prestart and registry prestart reuse scene-owned promises. View prestart reuses the world-owned promise. | + + +## Recommended loader design: option 3 + +Read sources: site/import-queue.mts:13–26, site/shared-imports.mts:1–7, site/startup-boot.mts:1–24, site/layouts/ObjectLayout.astro:162–178, site/object-directory.mts:55–72, site/object-adapter.mts:1–20 and site/scene/scene-router.mts:839–845. + +```text +layout → startup/shared-imports → queued router import (module autostart) + → startup/startup-boot → scene/scene-imports → registry/runtime + → world/world-imports → application world + all loaders → browser/import-queue +router → directory runtime-loader registration → first scene call +``` + +- Move queuedImport and its **single last chain** to browser/import-queue. Keep its pending sharing and rejection-reset semantics unchanged. It has no imports of scene/runtime modules. +- Scene/scene-imports owns importSceneRegistry and importPackagedObjectRuntime. World/world-imports owns importApplicationWorld. Each function still wraps only import(), using the same queuedImport. Startup/shared-imports keeps only importSceneRouter; startup-boot imports scene/world loaders directly. +- Router imports scene/world loader modules. Its existing object-directory import registers only the directory runtime loader before any scene call, then runs the existing module-evaluation autostart. The adapter directly imports scene/scene-imports; its frozen default remains intact. No factory or .then(start) is introduced. +- Directory exports a typed registration function. It reads the configured loader at scene invocation, never during objectFromEntry or registry construction. Missing registration fails clearly. Metadata callers in build, Astro SSR and server endpoints remain valid without a loader. System-host recursion, abort signals, retry and entry identity must remain identical. +- Root-level scene-imports/world-imports exist during S3; imports point at current paths until S4. No thunk awaits a queued function behind itself. The router's rejection remains a module-evaluation failure with the existing retry behavior and layout failure cleanup. + +S3 must prove one shared queue chain, registration before the first scene call, metadata construction without registration, registration/retry/abort behavior, and queue settlement/error cleanup by tests with mutations. Compare entry-chunk module membership and bytes: the predicted additions are exactly the two tiny scene/world loader modules containing import() calls. This is an inference until an actual build comparison proves it. Reuse of functions does not prove unchanged bytes. + +## Loader alternatives + +1. **Import-free directory slot**, registered by the existing layout startup script before importSceneRouter (or a new startup/main if separately justified). Move loader implementations to startup and let router/adapter/directory read typed slot functions. It can preserve module autostart and downward edges; DI in general is not an impasse. The graph variant is recorded in loader-options.json. It is not recommended because it adds page-owned registration of registry/world/runtime plus use-time proxies across three consumers, where option 3 needs only directory registration and keeps function ownership beside their modules. It would require slot-before-import mutation tests and entry-chunk measurements; no S2 behavioral proof exists. +2. **Queued .then(start) DI**: pass loaders into a router initializer and adapter factory. Queue settlement may follow synchronous start, but module-evaluation phase differs, a retry can start twice, and any await of another queued loader inside start can deadlock. Prove these separately; a graph pass is not an equivalent-startup proof. This variant is not selected. +3. **Lazy-boundary exception**: preserve current service imports and explicitly exempt only the directory/adapter/router upward loader edges. Report service SCCs separately. This preserves current startup behavior but abandons the zero-cycle acceptance claim and requires changed rules; it is not selected. + +## Other S3 risks + +- Moon catalogue split changes build/SSR chunk membership, checkObjectTree timing and client bytes from the source parsers. The single reader owns parsing/cache/catalogueMoons; preparation retains search ordering and eligibility. Compare outputs, errors, timing and chunks before S3. +- Selection is passed into navigation requests. Router and activation must forward the registry function on all three readNavigationSelection calls. Test the **default production URL path** and mutation-remove its wiring; passing a non-default callback that routes around the defect is insufficient. +- Type-owner extractions must preserve aliases and re-exports. The held-address extraction must preserve deferred reads and disposal in the same WeakMap. + +## Generated delivery and S4 gates + +Declarations use **git ls-files only**. Ignored outputs resolve to their declared paths, with tracked sibling .d.mts stand-ins for generated modules. The snapshot must match in a clean checkout, a restored checkout and CI. The four tracked declarations move beside their ignored modules. Restore object prepared inputs with pnpm setup:prepared; generate all ignored site inputs locally with pnpm build:prepare (including prepare:inputs, prepare:world-presentation, prepare:shell-assets and prepare:object-json) when running application/build proofs; neither is needed for plan acceptance. S2 publishes nothing. + +Update producers and readers together: site prepare-catalog, shell-icons/titles, moon-labels, world-presentation, facilities and feature-index; packages/bake navigation preparation and dataset billboards. Preserve published addresses. Move the four layout CSS files with their style imports. Also update ignores and declaration/template identities. + +S3 replays current locations with root-level additions. File SCCs cannot increase, folder SCCs must remain contained in the baseline, and the final S3 prefix must have zero file SCCs. S3 tier counts are n/a because current folders are not final layers. S4's synthetic top-tier legacy replay is a **corollary of the final DAG**, not an independent proof: moving folders in tier order cannot fail when the final projection passes. S4's real risks are references and executed-test identities. + + +| Phase | Step | File SCCs | Folder SCCs | Upward | Lateral | Result | +| --- | --- | ---: | ---: | ---: | ---: | --- | +| S3 | extract-held-address-reader | 4 | 2 | n/a | n/a | pass | +| S3 | extract-registry-types | 4 | 2 | n/a | n/a | pass | +| S3 | extract-navigation-contracts | 3 | 2 | n/a | n/a | pass | +| S3 | split-the-moon-catalogue-reader | 3 | 2 | n/a | n/a | pass | +| S3 | pass-selection-into-navigation-requests | 3 | 2 | n/a | n/a | pass | +| S3 | extract-the-frame-presenter-contract | 2 | 2 | n/a | n/a | pass | +| S3 | relocate-queued-loader-owners | 0 | 2 | n/a | n/a | pass | +| S4 | site/browser | 0 | 0 | 0 | 0 | pass | +| S4 | site/contracts | 0 | 0 | 0 | 0 | pass | +| S4 | site/model | 0 | 0 | 0 | 0 | pass | +| S4 | site/overview | 0 | 0 | 0 | 0 | pass | +| S4 | site/prepared | 0 | 0 | 0 | 0 | pass | +| S4 | site/server-assets | 0 | 0 | 0 | 0 | pass | +| S4 | site/source | 0 | 0 | 0 | 0 | pass | +| S4 | site/vendor | 0 | 0 | 0 | 0 | pass | +| S4 | site/directory | 0 | 0 | 0 | 0 | pass | +| S4 | site/minimap | 0 | 0 | 0 | 0 | pass | +| S4 | site/world | 0 | 0 | 0 | 0 | pass | +| S4 | site/content | 0 | 0 | 0 | 0 | pass | +| S4 | site/navigation | 0 | 0 | 0 | 0 | pass | +| S4 | site/search | 0 | 0 | 0 | 0 | pass | +| S4 | site/selection | 0 | 0 | 0 | 0 | pass | +| S4 | site/shell | 0 | 0 | 0 | 0 | pass | +| S4 | site/scene | 0 | 0 | 0 | 0 | pass | +| S4 | site/server | 0 | 0 | 0 | 0 | pass | +| S4 | site/build | 0 | 0 | 0 | 0 | pass | +| S4 | site/startup | 0 | 0 | 0 | 0 | pass | +| S4 | site/layouts | 0 | 0 | 0 | 0 | pass | +| S4 | site/components | 0 | 0 | 0 | 0 | pass | +| S4 | site/pages | 0 | 0 | 0 | 0 | pass | +| S4 | site/journeys | 0 | 0 | 0 | 0 | pass | +| S4 | site/test | 0 | 0 | 0 | 0 | pass | + + +The reference scan covers full/extensionless/relative file strings plus the retired site/test/ folder prefix, including site/test/** and site/test/* globs. It does not prove arbitrary computed paths or every possible folder spelling. Plan-internal inventories and plan-checker implementation files are excluded. Generated tables exclude themselves. Dated history stays separate from live references. Full line-numbered output exists only on demand with --references; it is not committed. + + +| Scope and class | Path/class pairs | +| --- | ---: | +| live: AGENTS.md contract text | 4 | +| live: CI routing | 1 | +| live: Python script | 2 | +| live: computed import | 2 | +| live: docs code-span | 21 | +| live: docs link | 26 | +| live: generator/producer literal | 96 | +| live: source import | 236 | +| live: tsconfig/eslint/package.json | 20 | +| live: workflow | 3 | +| plan: docs code-span | 1 | + + +Before **every moved-test PR**, capture the actual executed test-file list. After the move, require every old test's mapped destination in the executed list, with no lost identity. Check both local wiring and each affected CI command. Node 22 may exit zero for a no-match glob alongside valid files; an exit code alone is insufficient. + +Required reference updates include: + +- .github/workflows/audit.yml:180,182 source/facility globs; universe.yml:66 sparse checkout prefix; eslint.config.mts:167,175 test/helper exclusions. +- docs/sources-catalogue.md:117; src/objects/README.md:37; packages/bake/AGENTS.md:117,126 folder guidance and its stale prepare-body-overview path (actual caller is site/build/charts/charts.ts). +- labs/investigations/capture-galaxies.mts:10 fixture output; typescript ownership and nebula inbound prefix checks; site tsconfigs, lint selectors, preload, rendered routes, affected-test wiring, directory growth baseline and CI areas. +- Root/object/bake contracts and outside-site producers/imports. Inspect computed import/read producers during each move. Compare actual typechecked files so tests/helpers do not silently enter production lint/typecheck policy. + + +| Test with depth-sensitive reads | Destination | +| --- | --- | +| site/test/arrival-discovery.test.mts | site/build/prepare/arrival-discovery.test.mts | +| site/test/body-additions.test.mts | site/build/prepare/body-additions.test.mts | +| site/test/category-frames.test.mts | site/build/prepare/category-frames.test.mts | +| site/test/compact-spectrum.test.mts | site/overview/compact-spectrum.test.mts | +| site/test/dataset-response.test.mts | site/server/dataset-response.test.mts | +| site/test/folded-transit.test.mts | site/build/charts/folded-transit.test.mts | +| site/test/hosted-banks.test.mts | site/build/prepare/hosted-banks.test.mts | +| site/test/load-object-content.mts | site/build/content/load-object-content.test-support.mts | +| site/test/lonlat-slice-table.test.mts | site/build/prepare/lonlat-slice-table.test.mts | +| site/test/moon-labels.test.mts | site/world/moon-labels.test.mts | +| site/test/neutral-catalogue-color.test.mts | site/journeys/neutral-catalogue-color.test.mts | +| site/test/object-page-data.test.mts | site/server/object-page-data.test.mts | +| site/test/object-systems.test.mts | site/world/object-systems.test.mts | +| site/test/prepare-spatial-context.test.mts | site/build/prepare/prepare-spatial-context.test.mts | +| site/test/prepared-panel-content.test.mts | site/content/prepared-panel-content.test.mts | +| site/test/prepared-world-context.test.ts | site/world/prepared-world-context.test.ts | +| site/test/satellite-arrival-framing.test.mts | site/navigation/satellite-arrival-framing.test.mts | +| site/test/scene-contract.test.mts | site/world/scene-contract.test.mts | +| site/test/system-framing.test.mts | site/world/system-framing.test.mts | +| site/test/system-text.test.mts | site/build/prepare/system-text.test.mts | +| site/test/system-view-file.mts | site/world/system-view-file.test-support.mts | +| site/test/world-billboards.test.mts | site/journeys/world-billboards.test.mts | +| site/test/world-stars.test.mts | site/world/world-stars.test.mts | +| site/test/world-tables.test.mts | site/world/world-tables.test.mts | +| site/test/zoom-scope.test.mts | site/world/zoom-scope.test.mts | + + +Move PRs delete applied mappings/groups, retarget pending edits and regenerate tables. The --references --old gate fails live old-file references and also prints covering folder/glob references for review. During partial moves, old globs may still serve remaining tests: extend wiring for the moved tests and prove executed identities. Once the folder is retired, run --references --old site/test/ and require zero live folder/glob references. Both gates work after removing applied map entries. + +## Executable projection + + +| View | Before file SCCs | Before folder SCCs | Before upward | Before lateral | After file SCCs | After folder SCCs | After upward | After lateral | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| value | 0 | 2 | 30 | 10 | 0 | 0 | 0 | 0 | +| value+lazy | 2 | 2 | 32 | 11 | 0 | 0 | 0 | 0 | +| value+lazy+type | 4 | 2 | 43 | 24 | 0 | 0 | 0 | 0 | +| all | 4 | 2 | 43 | 26 | 0 | 0 | 0 | 0 | + +Unassigned files: before 0; after 0. No lateral allowances. +Identity uses the proposed numbers on existing folders and tier zero for loose root files. It applies no moves or edits. + + +Counts represent declarations and cyclic components. External package boundaries remain under the ordinary architecture rules. Acceptance scans live tracked declarations and live compact references; it requires no ignored snapshot. No independent review is claimed unless a completed cited report exists. + +Commands from a clean checkout: + +```sh +export PATH=$HOME/.nvm/versions/node/v22.23.2/bin:$PATH +pnpm install --frozen-lockfile +node --test .github/scripts/architecture/*.test.mts .github/scripts/audits/check-documentation-links.test.mts +node .github/scripts/architecture/site-architecture.mts --accept +node .github/scripts/architecture/site-architecture.mts --identity +node .github/scripts/architecture/site-architecture.mts --sequence +node .github/scripts/architecture/site-architecture.mts --minimality +node .github/scripts/architecture/site-architecture.mts --references > /tmp/site-architecture-references.json +pnpm check:architecture +# When changing plan data, regenerate first, then rerun acceptance: +node .github/scripts/architecture/site-architecture.mts --write +node .github/scripts/architecture/site-architecture.mts --accept +# Per-move gate: substitute that PR's actual old paths; fails before migration. +node .github/scripts/architecture/site-architecture.mts --references --old site/test/source-link.test.mts +``` + +## Owner decisions + +- Approve recommended option 3, or select a documented loader alternative and its different proofs. +- Approve page-boot helpers in startup and the final layer ownership. +- Sign off by November 2, or explicitly extend draftUntil; name the real-iPad startup-proof owner. +- Coordinate alowpoly's S3/S4 timing with performance branches touching scene-router, startup-boot and ObjectLayout. diff --git a/docs/site-architecture/edits.json b/docs/site-architecture/edits.json new file mode 100644 index 00000000000..780823b7dcb --- /dev/null +++ b/docs/site-architecture/edits.json @@ -0,0 +1,432 @@ +{ + "edits": [ + { + "op": "add-file", + "path": "site/model/navigation-href.mts", + "imports": [], + "change": "Extract held address reader", + "note": "Move navigationHref AND its owners WeakMap. Export a registration function for history; preserve deferred URL semantics.", + "id": "add-file-navigation-href" + }, + { + "op": "add-import", + "from": "site/navigation/navigation-history.mts", + "to": "site/model/navigation-href.mts", + "kind": "value", + "change": "Extract held address reader", + "note": "History registers and unregisters its held address reader in the same WeakMap; re-export the getter for existing navigation callers.", + "id": "add-import-navigation-history-navigation-href" + }, + { + "op": "retarget", + "from": "site/world/application-world-resources.mts", + "to": "site/navigation/navigation-history.mts", + "newTo": "site/model/navigation-href.mts", + "kind": "value", + "change": "Extract held address reader", + "note": "Only lower world caller uses navigationHref; other callers retain history behavior.", + "id": "retarget-application-world-resources-navigation-href", + "line": 18 + }, + { + "op": "add-file", + "path": "site/directory/object-entry-types.mts", + "note": "Extract registry aliases from objects; directory must not import the registry that calls it.", + "change": "Extract registry types", + "imports": [ + { + "to": "site/browser/browser-types.mts", + "kind": "type" + }, + { + "to": "packages/objects/src/index.ts", + "kind": "type" + } + ], + "id": "add-file-object-entry-types" + }, + { + "op": "retarget", + "from": "site/directory/object-directory.mts", + "to": "site/directory/objects.mts", + "note": "Use the extracted registry aliases without evaluating the full registry.", + "kind": "type", + "newTo": "site/directory/object-entry-types.mts", + "change": "Extract registry types", + "id": "retarget-object-directory-object-entry-types", + "line": 3 + }, + { + "op": "add-import", + "to": "site/directory/object-entry-types.mts", + "kind": "type", + "note": "Registry imports and re-exports its extracted aliases so clients can migrate without duplication.", + "from": "site/directory/objects.mts", + "change": "Extract registry types", + "id": "add-import-objects-object-entry-types" + }, + { + "op": "add-file", + "path": "site/navigation/navigation-types.mts", + "imports": [ + { + "to": "site/browser/browser-types.mts", + "kind": "type" + }, + { + "to": "packages/renderer/src/runtime/world-navigation-types.ts", + "kind": "type" + }, + { + "to": "packages/renderer/src/runtime/object-scene.ts", + "kind": "type" + }, + { + "to": "site/world/scene-subject.mts", + "kind": "type" + }, + { + "to": "site/world/navigation-scope.mts", + "kind": "type" + } + ], + "change": "Extract navigation contracts", + "note": "Own WorldHandoff, NavigationHistory and NavigationIntent beside navigation. Import MountOptions, ObjectSceneLifecycle, ObjectWorldNavigation, SceneSubject and PageView; preserve erased aliases SceneView and SelectionTarget.", + "id": "add-file-navigation-types" + }, + { + "op": "retarget", + "from": "site/navigation/prepared-arrival.mts", + "to": "site/navigation/prepared-world-navigation.mts", + "note": "Arrival reads WorldHandoff from the lower contract; navigation still owns flights.", + "kind": "type", + "newTo": "site/navigation/navigation-types.mts", + "change": "Extract navigation contracts", + "id": "retarget-prepared-arrival-navigation-types", + "line": 4 + }, + { + "op": "retarget", + "from": "site/navigation/navigation-history.mts", + "to": "site/navigation/navigation-request.mts", + "newTo": "site/navigation/navigation-types.mts", + "kind": "type", + "note": "Extract NavigationHistory and NavigationIntent into navigation-types; the history transport must not import request orchestration.", + "change": "Extract navigation contracts", + "id": "retarget-navigation-history-navigation-types", + "line": 4 + }, + { + "op": "add-import", + "to": "site/navigation/navigation-types.mts", + "kind": "type", + "note": "Flights import and re-export the extracted WorldHandoff contract.", + "from": "site/navigation/prepared-world-navigation.mts", + "change": "Extract navigation contracts", + "id": "add-import-prepared-world-navigation-navigation-types" + }, + { + "op": "add-import", + "to": "site/navigation/navigation-types.mts", + "kind": "type", + "note": "Request imports its extracted intent and history types and the plain selection shape; selection behavior is passed in.", + "from": "site/navigation/navigation-request.mts", + "change": "Extract navigation contracts", + "id": "add-import-navigation-request-navigation-types" + }, + { + "op": "add-file", + "path": "site/content/moon-catalogue.mts", + "note": "Split catalogue parsing, cached source catalogue and catalogueMoons into a runtime-safe reader; preparation keeps search-based ordering.", + "change": "Split the moon catalogue reader", + "imports": [ + { + "to": "site/source/moon-catalogues.json", + "kind": "asset" + }, + { + "to": "packages/objects/src/sources/index.ts", + "kind": "value" + } + ], + "id": "add-file-moon-catalogue" + }, + { + "op": "retarget", + "from": "site/content/object-children.mts", + "to": "site/build/prepare/prepare-body-moons.mts", + "note": "Read catalogueMoons from the split reader; production must never import build preparation.", + "newTo": "site/content/moon-catalogue.mts", + "change": "Split the moon catalogue reader", + "id": "retarget-object-children-moon-catalogue", + "line": 3 + }, + { + "op": "remove-import", + "from": "site/build/prepare/prepare-body-moons.mts", + "to": "site/source/moon-catalogues.json", + "note": "Move the source catalogue import with its parser into the reader; preparation calls that reader.", + "kind": "asset", + "change": "Split the moon catalogue reader", + "id": "remove-import-prepare-body-moons-moon-catalogues", + "line": 4 + }, + { + "op": "add-import", + "from": "site/build/prepare/prepare-body-moons.mts", + "to": "site/content/moon-catalogue.mts", + "kind": "value", + "note": "Use the single catalogue reader for preparation too; preserve the named-moon eligibility policy in preparation.", + "change": "Split the moon catalogue reader", + "id": "add-import-prepare-body-moons-moon-catalogue" + }, + { + "op": "remove-import", + "from": "site/navigation/navigation-request.mts", + "to": "site/selection/scene-selection.mts", + "note": "Pass selectionTargetFromUrl explicitly through resolveNavigation and readNavigationSelection. Router uses the existing registry export, injects it into createSceneActivation, which forwards all three calls. Import erased SceneView/SelectionTarget aliases from navigation-types; never retain the selection import.", + "change": "Pass selection into navigation requests", + "id": "remove-import-navigation-request-scene-selection", + "line": 11 + }, + { + "op": "add-import", + "to": "site/world/scene-subject.mts", + "kind": "value", + "note": "Keep existing subject identity helpers in the lower world owner; pass selectionTargetFromUrl as a parameter.", + "from": "site/navigation/navigation-request.mts", + "change": "Pass selection into navigation requests", + "id": "add-import-navigation-request-scene-subject" + }, + { + "op": "add-file", + "path": "site/scene/scene-frame-presenter.mts", + "note": "Extract the structural presenter interface; a session must not import the world coordinator that consumes it.", + "change": "Extract the frame presenter contract", + "imports": [ + { + "to": "site/world/application-world-context.mts", + "kind": "type" + }, + { + "to": "site/world/world-viewport.mts", + "kind": "type" + } + ], + "id": "add-file-scene-frame-presenter" + }, + { + "op": "retarget", + "from": "site/scene/scene-session.mts", + "to": "site/scene/scene-world.mts", + "note": "Session reads SceneFramePresenter from the lower contract, breaking the two-way type dependency.", + "kind": "type", + "newTo": "site/scene/scene-frame-presenter.mts", + "change": "Extract the frame presenter contract", + "id": "retarget-scene-session-scene-frame-presenter", + "line": 2 + }, + { + "op": "add-import", + "to": "site/scene/scene-frame-presenter.mts", + "kind": "type", + "note": "World coordinator imports and re-exports the structural presenter contract.", + "from": "site/scene/scene-world.mts", + "change": "Extract the frame presenter contract", + "id": "add-import-scene-world-scene-frame-presenter" + }, + { + "id": "add-file-scene-imports", + "op": "add-file", + "change": "Relocate queued loader owners", + "path": "site/scene/scene-imports.mts", + "imports": [ + { + "to": "site/browser/import-queue.mts", + "kind": "value" + }, + { + "to": "site/scene/scene-registry.mts", + "kind": "lazy" + }, + { + "to": "site/scene/packaged-object-runtime.mts", + "kind": "lazy" + } + ], + "note": "Own queued registry and packaged-runtime functions in scene; share browser queuedImport and its single last chain." + }, + { + "id": "add-file-world-imports", + "op": "add-file", + "change": "Relocate queued loader owners", + "path": "site/world/world-imports.mts", + "imports": [ + { + "to": "site/browser/import-queue.mts", + "kind": "value" + }, + { + "to": "site/world/application-world-context.mts", + "kind": "lazy" + } + ], + "note": "Own the queued application-world import in world; no initializer inside the thunk." + }, + { + "id": "remove-queue-runtime", + "op": "remove-import", + "change": "Relocate queued loader owners", + "to": "site/scene/packaged-object-runtime.mts", + "kind": "lazy", + "line": 26, + "note": "Move the runtime function out of the import-free browser queue implementation.", + "from": "site/browser/import-queue.mts" + }, + { + "id": "remove-shared-registry", + "op": "remove-import", + "change": "Relocate queued loader owners", + "to": "site/scene/scene-registry.mts", + "kind": "lazy", + "line": 5, + "note": "Leave only importSceneRouter in startup/shared-imports.", + "from": "site/startup/shared-imports.mts" + }, + { + "id": "remove-shared-world", + "op": "remove-import", + "change": "Relocate queued loader owners", + "to": "site/world/application-world-context.mts", + "kind": "lazy", + "line": 6, + "note": "Move the world function to its owner.", + "from": "site/startup/shared-imports.mts" + }, + { + "id": "retarget-adapter-scene-imports", + "op": "retarget", + "change": "Relocate queued loader owners", + "to": "site/browser/import-queue.mts", + "newTo": "site/scene/scene-imports.mts", + "line": 1, + "note": "Adapter directly uses the same queued runtime function; preserve the frozen default adapter.", + "from": "site/scene/object-adapter.mts" + }, + { + "id": "retarget-router-world-imports", + "op": "retarget", + "change": "Relocate queued loader owners", + "to": "site/startup/shared-imports.mts", + "newTo": "site/world/world-imports.mts", + "line": 2, + "note": "Split router loader imports; preserve module-evaluation autostart.", + "from": "site/scene/scene-router.mts" + }, + { + "id": "add-router-scene-imports", + "op": "add-import", + "change": "Relocate queued loader owners", + "to": "site/scene/scene-imports.mts", + "kind": "value", + "note": "Import registry/runtime functions and register only the directory runtime loader using the existing object-directory import before first scene call.", + "from": "site/scene/scene-router.mts" + }, + { + "id": "remove-directory-queue", + "op": "remove-import", + "change": "Relocate queued loader owners", + "to": "site/browser/import-queue.mts", + "line": 1, + "note": "Directory accepts a typed runtime-loader registration; read the slot at scene invocation, not metadata construction. Missing registration fails explicitly.", + "from": "site/directory/object-directory.mts" + }, + { + "id": "retarget-boot-scene-imports", + "op": "retarget", + "change": "Relocate queued loader owners", + "to": "site/startup/shared-imports.mts", + "newTo": "site/scene/scene-imports.mts", + "line": 1, + "note": "Body prestart and registry prestart reuse scene-owned promises.", + "from": "site/startup/startup-boot.mts" + }, + { + "id": "add-boot-world-imports", + "op": "add-import", + "change": "Relocate queued loader owners", + "to": "site/world/world-imports.mts", + "kind": "value", + "note": "View prestart reuses the world-owned promise.", + "from": "site/startup/startup-boot.mts" + } + ], + "changes": [ + { + "id": "extract-held-address-reader", + "edits": [ + "add-file-navigation-href", + "add-import-navigation-history-navigation-href", + "retarget-application-world-resources-navigation-href" + ] + }, + { + "id": "extract-registry-types", + "edits": [ + "add-file-object-entry-types", + "retarget-object-directory-object-entry-types", + "add-import-objects-object-entry-types" + ] + }, + { + "id": "extract-navigation-contracts", + "edits": [ + "add-file-navigation-types", + "retarget-prepared-arrival-navigation-types", + "retarget-navigation-history-navigation-types", + "add-import-prepared-world-navigation-navigation-types", + "add-import-navigation-request-navigation-types" + ] + }, + { + "id": "split-the-moon-catalogue-reader", + "edits": [ + "add-file-moon-catalogue", + "retarget-object-children-moon-catalogue", + "remove-import-prepare-body-moons-moon-catalogues", + "add-import-prepare-body-moons-moon-catalogue" + ] + }, + { + "id": "pass-selection-into-navigation-requests", + "edits": [ + "remove-import-navigation-request-scene-selection", + "add-import-navigation-request-scene-subject" + ] + }, + { + "id": "extract-the-frame-presenter-contract", + "edits": [ + "add-file-scene-frame-presenter", + "retarget-scene-session-scene-frame-presenter", + "add-import-scene-world-scene-frame-presenter" + ] + }, + { + "id": "relocate-queued-loader-owners", + "edits": [ + "add-file-scene-imports", + "add-file-world-imports", + "remove-queue-runtime", + "remove-shared-registry", + "remove-shared-world", + "retarget-adapter-scene-imports", + "retarget-router-world-imports", + "add-router-scene-imports", + "remove-directory-queue", + "retarget-boot-scene-imports", + "add-boot-world-imports" + ] + } + ] +} diff --git a/docs/site-architecture/loader-options.json b/docs/site-architecture/loader-options.json new file mode 100644 index 00000000000..9d264d23c1e --- /dev/null +++ b/docs/site-architecture/loader-options.json @@ -0,0 +1,349 @@ +{ + "recommended": "owner-local-queued-functions", + "options": [ + { + "id": "owner-local-queued-functions", + "edits": "edits.json", + "autostart": "module evaluation", + "registration": "directory runtime only", + "proofs": [ + "one queue chain", + "registration before first scene call", + "entry modules and bytes", + "real-iPad delayed-summary cold loads" + ] + }, + { + "id": "directory-slot", + "autostart": "module evaluation", + "registration": "all loaders before router import", + "verdict": "viable graph alternative; wider startup registration and consumer proxies", + "proofs": [ + "slot-before-import mutation", + "use-time reads", + "entry modules and bytes" + ], + "graphEdits": [ + { + "op": "add-file", + "path": "site/model/navigation-href.mts", + "imports": [], + "change": "Extract held address reader", + "note": "Move navigationHref AND its owners WeakMap. Export a registration function for history; preserve deferred URL semantics.", + "id": "add-file-navigation-href" + }, + { + "op": "add-import", + "from": "site/navigation/navigation-history.mts", + "to": "site/model/navigation-href.mts", + "kind": "value", + "change": "Extract held address reader", + "note": "History registers and unregisters its held address reader in the same WeakMap; re-export the getter for existing navigation callers.", + "id": "add-import-navigation-history-navigation-href" + }, + { + "op": "retarget", + "from": "site/world/application-world-resources.mts", + "to": "site/navigation/navigation-history.mts", + "newTo": "site/model/navigation-href.mts", + "kind": "value", + "change": "Extract held address reader", + "note": "Only lower world caller uses navigationHref; other callers retain history behavior.", + "id": "retarget-application-world-resources-navigation-href", + "line": 18 + }, + { + "op": "add-file", + "path": "site/directory/object-entry-types.mts", + "note": "Extract registry aliases from objects; directory must not import the registry that calls it.", + "change": "Extract registry types", + "imports": [ + { + "to": "site/browser/browser-types.mts", + "kind": "type" + }, + { + "to": "packages/objects/src/index.ts", + "kind": "type" + } + ], + "id": "add-file-object-entry-types" + }, + { + "op": "retarget", + "from": "site/directory/object-directory.mts", + "to": "site/directory/objects.mts", + "note": "Use the extracted registry aliases without evaluating the full registry.", + "kind": "type", + "newTo": "site/directory/object-entry-types.mts", + "change": "Extract registry types", + "id": "retarget-object-directory-object-entry-types", + "line": 3 + }, + { + "op": "add-import", + "to": "site/directory/object-entry-types.mts", + "kind": "type", + "note": "Registry imports and re-exports its extracted aliases so clients can migrate without duplication.", + "from": "site/directory/objects.mts", + "change": "Extract registry types", + "id": "add-import-objects-object-entry-types" + }, + { + "op": "add-file", + "path": "site/navigation/navigation-types.mts", + "imports": [ + { + "to": "site/browser/browser-types.mts", + "kind": "type" + }, + { + "to": "packages/renderer/src/runtime/world-navigation-types.ts", + "kind": "type" + }, + { + "to": "packages/renderer/src/runtime/object-scene.ts", + "kind": "type" + }, + { + "to": "site/world/scene-subject.mts", + "kind": "type" + }, + { + "to": "site/world/navigation-scope.mts", + "kind": "type" + } + ], + "change": "Extract navigation contracts", + "note": "Own WorldHandoff, NavigationHistory and NavigationIntent beside navigation. Import MountOptions, ObjectSceneLifecycle, ObjectWorldNavigation, SceneSubject and PageView; preserve erased aliases SceneView and SelectionTarget.", + "id": "add-file-navigation-types" + }, + { + "op": "retarget", + "from": "site/navigation/prepared-arrival.mts", + "to": "site/navigation/prepared-world-navigation.mts", + "note": "Arrival reads WorldHandoff from the lower contract; navigation still owns flights.", + "kind": "type", + "newTo": "site/navigation/navigation-types.mts", + "change": "Extract navigation contracts", + "id": "retarget-prepared-arrival-navigation-types", + "line": 4 + }, + { + "op": "retarget", + "from": "site/navigation/navigation-history.mts", + "to": "site/navigation/navigation-request.mts", + "newTo": "site/navigation/navigation-types.mts", + "kind": "type", + "note": "Extract NavigationHistory and NavigationIntent into navigation-types; the history transport must not import request orchestration.", + "change": "Extract navigation contracts", + "id": "retarget-navigation-history-navigation-types", + "line": 4 + }, + { + "op": "add-import", + "to": "site/navigation/navigation-types.mts", + "kind": "type", + "note": "Flights import and re-export the extracted WorldHandoff contract.", + "from": "site/navigation/prepared-world-navigation.mts", + "change": "Extract navigation contracts", + "id": "add-import-prepared-world-navigation-navigation-types" + }, + { + "op": "add-import", + "to": "site/navigation/navigation-types.mts", + "kind": "type", + "note": "Request imports its extracted intent and history types and the plain selection shape; selection behavior is passed in.", + "from": "site/navigation/navigation-request.mts", + "change": "Extract navigation contracts", + "id": "add-import-navigation-request-navigation-types" + }, + { + "op": "add-file", + "path": "site/content/moon-catalogue.mts", + "note": "Split catalogue parsing, cached source catalogue and catalogueMoons into a runtime-safe reader; preparation keeps search-based ordering.", + "change": "Split the moon catalogue reader", + "imports": [ + { + "to": "site/source/moon-catalogues.json", + "kind": "asset" + }, + { + "to": "packages/objects/src/sources/index.ts", + "kind": "value" + } + ], + "id": "add-file-moon-catalogue" + }, + { + "op": "retarget", + "from": "site/content/object-children.mts", + "to": "site/build/prepare/prepare-body-moons.mts", + "note": "Read catalogueMoons from the split reader; production must never import build preparation.", + "newTo": "site/content/moon-catalogue.mts", + "change": "Split the moon catalogue reader", + "id": "retarget-object-children-moon-catalogue", + "line": 3 + }, + { + "op": "remove-import", + "from": "site/build/prepare/prepare-body-moons.mts", + "to": "site/source/moon-catalogues.json", + "note": "Move the source catalogue import with its parser into the reader; preparation calls that reader.", + "kind": "asset", + "change": "Split the moon catalogue reader", + "id": "remove-import-prepare-body-moons-moon-catalogues", + "line": 4 + }, + { + "op": "add-import", + "from": "site/build/prepare/prepare-body-moons.mts", + "to": "site/content/moon-catalogue.mts", + "kind": "value", + "note": "Use the single catalogue reader for preparation too; preserve the named-moon eligibility policy in preparation.", + "change": "Split the moon catalogue reader", + "id": "add-import-prepare-body-moons-moon-catalogue" + }, + { + "op": "remove-import", + "from": "site/navigation/navigation-request.mts", + "to": "site/selection/scene-selection.mts", + "note": "Pass selectionTargetFromUrl explicitly through resolveNavigation and readNavigationSelection. Router uses the existing registry export, injects it into createSceneActivation, which forwards all three calls. Import erased SceneView/SelectionTarget aliases from navigation-types; never retain the selection import.", + "change": "Pass selection into navigation requests", + "id": "remove-import-navigation-request-scene-selection", + "line": 11 + }, + { + "op": "add-import", + "to": "site/world/scene-subject.mts", + "kind": "value", + "note": "Keep existing subject identity helpers in the lower world owner; pass selectionTargetFromUrl as a parameter.", + "from": "site/navigation/navigation-request.mts", + "change": "Pass selection into navigation requests", + "id": "add-import-navigation-request-scene-subject" + }, + { + "op": "add-file", + "path": "site/scene/scene-frame-presenter.mts", + "note": "Extract the structural presenter interface; a session must not import the world coordinator that consumes it.", + "change": "Extract the frame presenter contract", + "imports": [ + { + "to": "site/world/application-world-context.mts", + "kind": "type" + }, + { + "to": "site/world/world-viewport.mts", + "kind": "type" + } + ], + "id": "add-file-scene-frame-presenter" + }, + { + "op": "retarget", + "from": "site/scene/scene-session.mts", + "to": "site/scene/scene-world.mts", + "note": "Session reads SceneFramePresenter from the lower contract, breaking the two-way type dependency.", + "kind": "type", + "newTo": "site/scene/scene-frame-presenter.mts", + "change": "Extract the frame presenter contract", + "id": "retarget-scene-session-scene-frame-presenter", + "line": 2 + }, + { + "op": "add-import", + "to": "site/scene/scene-frame-presenter.mts", + "kind": "type", + "note": "World coordinator imports and re-exports the structural presenter contract.", + "from": "site/scene/scene-world.mts", + "change": "Extract the frame presenter contract", + "id": "add-import-scene-world-scene-frame-presenter" + }, + { + "op": "add-file", + "path": "site/directory/runtime-loaders.mts", + "imports": [ + { + "to": "site/browser/browser-types.mts", + "kind": "type" + } + ] + }, + { + "op": "remove-import", + "from": "site/directory/object-directory.mts", + "to": "site/browser/import-queue.mts" + }, + { + "op": "add-import", + "from": "site/directory/object-directory.mts", + "to": "site/directory/runtime-loaders.mts", + "kind": "value" + }, + { + "op": "remove-import", + "from": "site/scene/object-adapter.mts", + "to": "site/browser/import-queue.mts" + }, + { + "op": "add-import", + "from": "site/scene/object-adapter.mts", + "to": "site/directory/runtime-loaders.mts", + "kind": "value" + }, + { + "op": "remove-import", + "from": "site/scene/scene-router.mts", + "to": "site/startup/shared-imports.mts" + }, + { + "op": "add-import", + "from": "site/scene/scene-router.mts", + "to": "site/directory/runtime-loaders.mts", + "kind": "value" + }, + { + "op": "remove-import", + "from": "site/browser/import-queue.mts", + "to": "site/scene/packaged-object-runtime.mts" + }, + { + "op": "add-import", + "from": "site/startup/shared-imports.mts", + "to": "site/scene/packaged-object-runtime.mts", + "kind": "lazy" + }, + { + "op": "add-import", + "from": "site/startup/shared-imports.mts", + "to": "site/directory/runtime-loaders.mts", + "kind": "value" + } + ] + }, + { + "id": "then-start-di", + "autostart": "after import evaluation inside queued thunk", + "risks": [ + "double start on retry", + "self-wait deadlock", + "adapter frozen default replaced" + ], + "proofs": [ + "synchronous start settlement", + "no queued await in start", + "retry single start", + "entry modules and bytes" + ] + }, + { + "id": "lazy-boundary-exception", + "autostart": "module evaluation", + "verdict": "abandons zero-cycle acceptance", + "proofs": [ + "explicit upward exceptions", + "report service SCCs separately" + ] + } + ] +} diff --git a/docs/site-architecture/moves.json b/docs/site-architecture/moves.json new file mode 100644 index 00000000000..cd576e840f6 --- /dev/null +++ b/docs/site-architecture/moves.json @@ -0,0 +1,291 @@ +{ + "site/runtime-policy.mts": "site/browser/runtime-policy.mts", + "site/diagnostics-policy.mts": "site/browser/diagnostics-policy.mts", + "site/in-flight-loader.mts": "site/browser/in-flight-loader.mts", + "site/next-frame.mts": "site/browser/next-frame.mts", + "site/narrow-layout.mts": "site/browser/narrow-layout.mts", + "site/object-shell-types.ts": "site/contracts/object-shell-types.ts", + "site/object-page-contract.mts": "site/contracts/object-page-contract.mts", + "site/navigation/system-address.mts": "site/model/system-address.mts", + "site/root-object.mts": "site/model/root-object.mts", + "site/orbit-root.mts": "site/model/orbit-root.mts", + "site/planetary-system-members.mts": "site/model/planetary-system-members.mts", + "site/object-classification-label.mts": "site/model/object-classification-label.mts", + "site/dataset-url.mts": "site/model/dataset-url.mts", + "site/startup-world.mts": "site/directory/startup-world.mts", + "site/startup-requests.mts": "site/directory/startup-requests.mts", + "site/object-entries.mts": "site/directory/object-entries.mts", + "site/object-directory.mts": "site/directory/object-directory.mts", + "site/objects.mts": "site/directory/objects.mts", + "site/world-context-plan.mts": "site/directory/world-context-plan.mts", + "site/world-objects.mts": "site/world/world-objects.mts", + "site/world-system-views.mts": "site/world/world-system-views.mts", + "site/context-availability.mts": "site/world/context-availability.mts", + "site/context-datasets.mts": "site/world/context-datasets.mts", + "site/stellar-extents.mts": "site/world/stellar-extents.mts", + "site/hosted-banks.mts": "site/world/hosted-banks.mts", + "site/dot-catalogues.mts": "site/world/dot-catalogues.mts", + "site/prepared-world-presentation.mts": "site/world/prepared-world-presentation.mts", + "site/application-world-context.mts": "site/world/application-world-context.mts", + "site/application-world-frames.mts": "site/world/application-world-frames.mts", + "site/application-world-resources.mts": "site/world/application-world-resources.mts", + "site/application-world-types.mts": "site/world/application-world-types.mts", + "site/application-world-visibility.mts": "site/world/application-world-visibility.mts", + "site/world-approach.mts": "site/world/world-approach.mts", + "site/world-camera.mts": "site/world/world-camera.mts", + "site/world-viewport.mts": "site/world/world-viewport.mts", + "site/world-preferences.mts": "site/world/world-preferences.mts", + "site/moon-orbit-policy.mts": "site/world/moon-orbit-policy.mts", + "site/catalogue-moon-labels.mts": "site/world/catalogue-moon-labels.mts", + "site/system-framing.mts": "site/world/system-framing.mts", + "site/system-framing-radii.mts": "site/world/system-framing-radii.mts", + "site/zoom-scope.mts": "site/world/zoom-scope.mts", + "site/zoom-carry.mts": "site/world/zoom-carry.mts", + "site/inside-view.mts": "site/world/inside-view.mts", + "site/object-systems.mts": "site/world/object-systems.mts", + "site/satellite-systems.mts": "site/world/satellite-systems.mts", + "site/scene/scene-subject.mts": "site/world/scene-subject.mts", + "site/navigation/navigation-scope.mts": "site/world/navigation-scope.mts", + "site/navigation/navigation-history.mts": "site/navigation/navigation-history.mts", + "site/navigation/navigation-fragments.mts": "site/navigation/navigation-fragments.mts", + "site/scene/scene-selection.mts": "site/selection/scene-selection.mts", + "site/satellite-selection.mts": "site/selection/satellite-selection.mts", + "site/overview-selection.mts": "site/selection/overview-selection.mts", + "site/showcase.mts": "site/selection/showcase.mts", + "site/object-adapter.mts": "site/scene/object-adapter.mts", + "site/packaged-object-runtime.mts": "site/scene/packaged-object-runtime.mts", + "site/startup-billboard.mts": "site/scene/startup-billboard.mts", + "site/initial-scene.mts": "site/scene/initial-scene.mts", + "site/prepared-arrival.mts": "site/navigation/prepared-arrival.mts", + "site/prepared-scene-ownership.mts": "site/navigation/prepared-scene-ownership.mts", + "site/arrival-billboard.mts": "site/navigation/arrival-billboard.mts", + "site/prepared-world-navigation.mts": "site/navigation/prepared-world-navigation.mts", + "site/view-url-runtime.mts": "site/navigation/view-url-runtime.mts", + "site/prepared-descriptor.mts": "site/navigation/prepared-descriptor.mts", + "site/import-queue.mts": "site/browser/import-queue.mts", + "site/shared-imports.mts": "site/startup/shared-imports.mts", + "site/startup-boot.mts": "site/startup/startup-boot.mts", + "site/object-browser.mts": "site/shell/object-browser.mts", + "site/feature-browser.mts": "site/shell/feature-browser.mts", + "site/destination-browser.mts": "site/shell/destination-browser.mts", + "site/selection-presentation.mts": "site/shell/selection-presentation.mts", + "site/information-card.mts": "site/shell/information-card.mts", + "site/chart-pixel-alignment.mts": "site/shell/chart-pixel-alignment.mts", + "site/dataset-picker.mts": "site/shell/dataset-picker.mts", + "site/tab-panels.mts": "site/shell/tab-panels.mts", + "site/layout-sections.mts": "site/shell/layout-sections.mts", + "site/view-readout.mts": "site/shell/view-readout.mts", + "site/native-view-forms.mts": "site/shell/native-view-forms.mts", + "site/source-link.mts": "site/browser/source-link.mts", + "site/dataset-content.mts": "site/content/dataset-content.mts", + "site/dataset-context.mts": "site/content/dataset-context.mts", + "site/object-text.mts": "site/content/object-text.mts", + "site/prepared-panel-content.mts": "site/content/prepared-panel-content.mts", + "site/volume-presentation.mts": "site/content/volume-presentation.mts", + "site/exploration-catalog.mts": "site/content/exploration-catalog.mts", + "site/source-documentation.mts": "site/content/source-documentation.mts", + "site/source-icons.mts": "site/content/source-icons.mts", + "site/sidebar-thumbnails.mts": "site/content/sidebar-thumbnails.mts", + "site/seo.mts": "site/content/seo.mts", + "site/seo-trail.mts": "site/content/seo-trail.mts", + "site/system-card.mts": "site/content/system-card.mts", + "site/object-children.mts": "site/content/object-children.mts", + "site/learn-more.mts": "site/content/learn-more.mts", + "site/native-input.mts": "site/browser/native-input.mts", + "site/startup-cover.mts": "site/startup/startup-cover.mts", + "site/initial-shell-context.mts": "site/startup/initial-shell-context.mts", + "site/error-report.mts": "site/startup/error-report.mts", + "site/web-analytics.mts": "site/startup/web-analytics.mts", + "site/asset-origin.mts": "site/server-assets/asset-origin.mts", + "site/object-page-data.mts": "site/server/object-page-data.mts", + "site/first-view-transport.mts": "site/server/first-view-transport.mts", + "site/object-entry.mts": "site/server/object-entry.mts", + "site/world-places.mts": "site/server/world-places.mts", + "site/dot-catalogue-data.mts": "site/server/dot-catalogue-data.mts", + "site/dataset-response.mts": "site/server/dataset-response.mts", + "site/built-pages.mts": "site/server/built-pages.mts", + "site/prepare-body-moons.mts": "site/build/prepare/prepare-body-moons.mts", + "site/components/ObjectSwatchStyles.astro": "site/layouts/ObjectSwatchStyles.astro", + "site/default-width-share.mts": "site/world/default-width-share.mts", + "site/diagnostic-recorder.mts": "site/shell/diagnostic-recorder.mts", + "site/env.d.ts": "site/env.d.ts", + "site/prepared-catalogue.d.mts": "site/prepared/prepared-catalogue.d.mts", + "site/prepared-navigation-markers.d.mts": "site/prepared/prepared-navigation-markers.d.mts", + "site/prepared-object-path.mts": "site/prepared/prepared-object-path.mts", + "site/prepared-shell-icons.d.mts": "site/prepared/prepared-shell-icons.d.mts", + "site/prepared-shell-titles.d.mts": "site/prepared/prepared-shell-titles.d.mts", + "site/social-images.mts": "site/server/social-images.mts", + "site/build/source-maps.test.mts": "site/build/source-maps.test.mts", + "site/navigation/evidence/star-click/one-click.jpg": "site/navigation/evidence/star-click/one-click.jpg", + "site/prepared-panel-content.shared-readers.test.mts": "site/content/prepared-panel-content.shared-readers.test.mts", + "site/test/application-world-frames.test.mts": "site/world/application-world-frames.test.mts", + "site/test/application-world-visibility.test.mts": "site/world/application-world-visibility.test.mts", + "site/test/arrival-billboard.test.mts": "site/navigation/arrival-billboard.test.mts", + "site/test/arrival-discovery.test.mts": "site/build/prepare/arrival-discovery.test.mts", + "site/test/asset-origin.test.mts": "site/server/asset-origin.test.mts", + "site/test/astro-component-imports.test.mts": "site/journeys/astro-component-imports.test.mts", + "site/test/authored-object-fixture.mts": "site/build/fixtures/authored-object-fixture.mts", + "site/test/body-additions.test.mts": "site/build/prepare/body-additions.test.mts", + "site/test/body-moons.test.mts": "site/build/prepare/body-moons.test.mts", + "site/test/browser-node-imports.test.mts": "site/journeys/browser-node-imports.test.mts", + "site/test/built-pages.test.mts": "site/server/built-pages.test.mts", + "site/test/camera-handover.test.mts": "site/scene/camera-handover.test.mts", + "site/test/catalogue-window.test.mts": "site/search/catalogue-window.test.mts", + "site/test/category-frames.test.mts": "site/build/prepare/category-frames.test.mts", + "site/test/chart-pixel-alignment.test.mts": "site/shell/chart-pixel-alignment.test.mts", + "site/test/charts.test.ts": "site/build/charts/charts.test.mts", + "site/test/check-preparation-inputs.test.mts": "site/build/prepare/check-preparation-inputs.test.mts", + "site/test/cloudflare-worker.test.mts": "site/journeys/cloudflare-worker.test.mts", + "site/test/compact-spectrum.test.mts": "site/overview/compact-spectrum.test.mts", + "site/test/context-lineage.test.mts": "site/journeys/context-lineage.test.mts", + "site/test/dataset-picker.test.mts": "site/shell/dataset-picker.test.mts", + "site/test/dataset-response.test.mts": "site/server/dataset-response.test.mts", + "site/test/dataset-url.test.mts": "site/model/dataset-url.test.mts", + "site/test/descriptor-roundtrip.test.mts": "site/build/prepare/descriptor-roundtrip.test.mts", + "site/test/destination-browser.test.mts": "site/shell/destination-browser.test.mts", + "site/test/diagnostic-accessor.test.mts": "site/journeys/diagnostic-accessor.test.mts", + "site/test/diagnostic-recorder.test.mts": "site/shell/diagnostic-recorder.test.mts", + "site/test/error-report.test.mts": "site/browser/error-report.test.mts", + "site/test/evidence/passive-minimaps/browser.json": "site/minimap/evidence/passive-minimaps/browser.json", + "site/test/evidence/passive-minimaps/earth.png": "site/minimap/evidence/passive-minimaps/earth.png", + "site/test/evidence/passive-minimaps/jupiter-sequence.png": "site/minimap/evidence/passive-minimaps/jupiter-sequence.png", + "site/test/first-view-transport.test.mts": "site/server/first-view-transport.test.mts", + "site/test/fixtures/anchor-table.mts": "site/world/fixtures/anchor-table.mts", + "site/test/fixtures/context-package.mts": "site/build/fixtures/context-package.mts", + "site/test/fixtures/galaxies/captures.json": "site/world/fixtures/galaxies/captures.json", + "site/test/fixtures/object-content-fixture.mts": "site/build/fixtures/object-content-fixture.mts", + "site/test/fixtures/objects.mts": "site/directory/fixtures/objects.mts", + "site/test/fixtures/prepared-schemas.mts": "site/world/fixtures/prepared-schemas.mts", + "site/test/fixtures/saturn-prepared.mts": "site/world/fixtures/saturn-prepared.mts", + "site/test/folded-transit.test.mts": "site/build/charts/folded-transit.test.mts", + "site/test/galactic-focus.test.mts": "site/search/galactic-focus.test.mts", + "site/test/hosted-banks.test.mts": "site/build/prepare/hosted-banks.test.mts", + "site/test/import-queue.test.mts": "site/startup/import-queue.test.mts", + "site/test/in-flight-loader.test.mts": "site/browser/in-flight-loader.test.mts", + "site/test/information-card-sections.test.mts": "site/shell/information-card-sections.test.mts", + "site/test/information-tab-styles.test.mts": "site/server/information-tab-styles.test.mts", + "site/test/initial-shell-context.test.mts": "site/browser/initial-shell-context.test.mts", + "site/test/inline-page-stylesheet.test.mts": "site/build/inline-page-stylesheet.test.mts", + "site/test/kept-load.test.mts": "site/server/kept-load.test.mts", + "site/test/list-distance.test.mts": "site/search/list-distance.test.mts", + "site/test/load-object-content.mts": "site/build/content/load-object-content.test-support.mts", + "site/test/lonlat-slice-table.test.mts": "site/build/prepare/lonlat-slice-table.test.mts", + "site/test/moon-labels.test.mts": "site/world/moon-labels.test.mts", + "site/test/navigation-content.test.mts": "site/navigation/navigation-content.test.mts", + "site/test/navigation-fragments.test.mts": "site/navigation/navigation-fragments.test.mts", + "site/test/navigation-history.test.mts": "site/navigation/navigation-history.test.mts", + "site/test/navigation-lifecycle.test.mts": "site/navigation/navigation-lifecycle.test.mts", + "site/test/navigation-ontology.test.mts": "site/world/navigation-ontology.test.mts", + "site/test/navigation-preparation.test.mts": "site/world/navigation-preparation.test.mts", + "site/test/navigation-readiness.test.mts": "site/navigation/navigation-readiness.test.mts", + "site/test/navigation-request.test.mts": "site/navigation/navigation-request.test.mts", + "site/test/navigation-router-context.mts": "site/scene/navigation-router-context.test-support.mts", + "site/test/navigation-scope.test.mts": "site/world/navigation-scope.test.mts", + "site/test/navigation-shell-content.test.mts": "site/navigation/navigation-shell-content.test.mts", + "site/test/navigation-styles.test.mts": "site/navigation/navigation-styles.test.mts", + "site/test/navigation-test-values.mts": "site/navigation/navigation-test-values.test-support.mts", + "site/test/navigation-timing.test.mts": "site/navigation/navigation-timing.test.mts", + "site/test/nebula-search.test.mts": "site/world/nebula-search.test.mts", + "site/test/neutral-catalogue-color.test.mts": "site/journeys/neutral-catalogue-color.test.mts", + "site/test/object-children.test.mts": "site/content/object-children.test.mts", + "site/test/object-control-binding.test.mts": "site/journeys/object-control-binding.test.mts", + "site/test/object-directory.test.mts": "site/directory/object-directory.test.mts", + "site/test/object-navigation.test.mts": "site/world/object-navigation.test.mts", + "site/test/object-package-contract.test.mts": "site/build/object-package-contract.test.mts", + "site/test/object-page-data.test.mts": "site/server/object-page-data.test.mts", + "site/test/object-result.test.mts": "site/search/object-result.test.mts", + "site/test/object-runtime-contract.test.mts": "site/journeys/object-runtime-contract.test.mts", + "site/test/object-search-rank.test.mts": "site/search/object-search-rank.test.mts", + "site/test/object-systems.test.mts": "site/world/object-systems.test.mts", + "site/test/object-text.test.mts": "site/content/object-text.test.mts", + "site/test/overview-selection.test.mts": "site/selection/overview-selection.test.mts", + "site/test/preparation-boundary.test.mts": "site/journeys/preparation-boundary.test.mts", + "site/test/prepare-authored.test.mts": "site/build/prepare/prepare-authored.test.mts", + "site/test/prepare-context-availability.test.mts": "site/build/prepare/prepare-context-availability.test.mts", + "site/test/prepare-object-json.test.mts": "site/build/prepare/prepare-object-json.test.mts", + "site/test/prepare-spatial-context.test.mts": "site/build/prepare/prepare-spatial-context.test.mts", + "site/test/prepare-text.test.mts": "site/build/prepare/prepare-text.test.mts", + "site/test/prepared-arrival.test.mts": "site/navigation/prepared-arrival.test.mts", + "site/test/prepared-body-billboards.test.mts": "site/world/prepared-body-billboards.test.mts", + "site/test/prepared-dataset-facts.test.mts": "site/build/content/prepared-dataset-facts.test.mts", + "site/test/prepared-dataset-labels.test.mts": "site/journeys/prepared-dataset-labels.test.mts", + "site/test/prepared-dataset-legends.test.mts": "site/build/content/prepared-dataset-legends.test.mts", + "site/test/prepared-descriptor.test.mts": "site/navigation/prepared-descriptor.test.mts", + "site/test/prepared-icons.test.mts": "site/build/prepare/prepared-icons.test.mts", + "site/test/prepared-motion-css.test.mts": "site/build/prepared-motion-css.test.mts", + "site/test/prepared-panel-content.test.mts": "site/content/prepared-panel-content.test.mts", + "site/test/prepared-presentation-bindings.test.mts": "site/contracts/prepared-presentation-bindings.test.mts", + "site/test/prepared-presentation-contract.test.mts": "site/journeys/prepared-presentation-contract.test.mts", + "site/test/prepared-world-context.test.ts": "site/world/prepared-world-context.test.ts", + "site/test/prepared-world-navigation.test.mts": "site/navigation/prepared-world-navigation.test.mts", + "site/test/preview.test.mts": "site/server/preview.test.mts", + "site/test/production-diagnostics.test.mts": "site/journeys/production-diagnostics.test.mts", + "site/test/recovery-commands.test.mts": "site/journeys/recovery-commands.test.mts", + "site/test/refresh-charts.test.mts": "site/build/prepare/refresh-charts.test.mts", + "site/test/rendered-page.test.mts": "site/journeys/rendered-page.test.mts", + "site/test/restore-inputs.test.mts": "site/build/restore-inputs.test.mts", + "site/test/run-script-chain.test.mts": "site/build/run-script-chain.test.mts", + "site/test/runtime-package.test.mts": "site/world/runtime-package.test.mts", + "site/test/runtime-policy.test.mts": "site/browser/runtime-policy.test.mts", + "site/test/satellite-arrival-framing.test.mts": "site/navigation/satellite-arrival-framing.test.mts", + "site/test/satellite-systems.test.mts": "site/world/satellite-systems.test.mts", + "site/test/scene-contract.test.mts": "site/world/scene-contract.test.mts", + "site/test/scene-datasets.test.mts": "site/scene/scene-datasets.test.mts", + "site/test/scene-frame.test.mts": "site/scene/scene-frame.test.mts", + "site/test/scene-publication.test.mts": "site/scene/scene-publication.test.mts", + "site/test/scene-session.test.mts": "site/scene/scene-session.test.mts", + "site/test/search-api.test.mts": "site/journeys/search-api.test.mts", + "site/test/search-response.test.mts": "site/server/search-response.test.mts", + "site/test/seo-discovery.mts": "site/journeys/seo-discovery.test-support.mts", + "site/test/seo-discovery.test.mts": "site/journeys/seo-discovery.test.mts", + "site/test/seo.test.mts": "site/content/seo.test.mts", + "site/test/setup-assets.test.mts": "site/build/prepare/setup-assets.test.mts", + "site/test/shell-selectors.test.mts": "site/search/shell-selectors.test.mts", + "site/test/shell-settings.test.mts": "site/shell/shell-settings.test.mts", + "site/test/shell-sheet.test.mts": "site/shell/shell-sheet.test.mts", + "site/test/showcase.test.mts": "site/selection/showcase.test.mts", + "site/test/source-evidence-values.mts": "site/overview/source-evidence-values.test-support.mts", + "site/test/source-link.test.mts": "site/browser/source-link.test.mts", + "site/test/startup-billboard.test.mts": "site/scene/startup-billboard.test.mts", + "site/test/startup-cover.test.mts": "site/browser/startup-cover.test.mts", + "site/test/startup-requests.test.mts": "site/directory/startup-requests.test.mts", + "site/test/stellar-extents.test.mts": "site/world/stellar-extents.test.mts", + "site/test/surface-label-loop.test.mts": "site/journeys/surface-label-loop.test.mts", + "site/test/surface-minimap.test.mts": "site/minimap/surface-minimap.test.mts", + "site/test/system-framing.test.mts": "site/world/system-framing.test.mts", + "site/test/system-text.test.mts": "site/build/prepare/system-text.test.mts", + "site/test/system-view-file.mts": "site/world/system-view-file.test-support.mts", + "site/test/view-readout.test.mts": "site/shell/view-readout.test.mts", + "site/test/view-url-runtime.test.mts": "site/navigation/view-url-runtime.test.mts", + "site/test/volume-presentation.test.mts": "site/content/volume-presentation.test.mts", + "site/test/web-analytics.test.mts": "site/browser/web-analytics.test.mts", + "site/test/world-billboards.test.mts": "site/journeys/world-billboards.test.mts", + "site/test/world-stars.test.mts": "site/world/world-stars.test.mts", + "site/test/world-tables.test.mts": "site/world/world-tables.test.mts", + "site/test/zoom-scope.test.mts": "site/world/zoom-scope.test.mts", + "site/moon-labels.prepared.json": "site/prepared/moon-labels.prepared.json", + "site/prepared-catalogue.mjs": "site/prepared/prepared-catalogue.mjs", + "site/prepared-context-bank-assets.json": "site/prepared/prepared-context-bank-assets.json", + "site/prepared-context-json.mts": "site/prepared/prepared-context-json.mts", + "site/prepared-context-objects.mts": "site/prepared/prepared-context-objects.mts", + "site/prepared-dataset-billboards.json": "site/prepared/prepared-dataset-billboards.json", + "site/prepared-dataset-volumes.json": "site/prepared/prepared-dataset-volumes.json", + "site/prepared-dot-catalogues.json": "site/prepared/prepared-dot-catalogues.json", + "site/prepared-facilities.json": "site/prepared/prepared-facilities.json", + "site/prepared-feature-index.json": "site/prepared/prepared-feature-index.json", + "site/prepared-fit-boxes.json": "site/prepared/prepared-fit-boxes.json", + "site/prepared-hosted-banks.json": "site/prepared/prepared-hosted-banks.json", + "site/prepared-navigation-markers.mjs": "site/prepared/prepared-navigation-markers.mjs", + "site/prepared-shell-icons.mjs": "site/prepared/prepared-shell-icons.mjs", + "site/prepared-shell-titles.mjs": "site/prepared/prepared-shell-titles.mjs", + "site/prepared-source-credits.json": "site/prepared/prepared-source-credits.json", + "site/prepared-sources.json": "site/prepared/prepared-sources.json", + "site/prepared-stellar-extents.json": "site/prepared/prepared-stellar-extents.json", + "site/prepared-world-presentation.json": "site/prepared/prepared-world-presentation.json", + "site/site.css": "site/layouts/site.css", + "site/object-shell.css": "site/layouts/object-shell.css", + "site/wordmark.css": "site/layouts/wordmark.css", + "site/object-navigation-marker.css": "site/layouts/object-navigation-marker.css", + "site/scientific-chart-order.mts": null, + "site/test/scientific-chart-order.test.mts": null +} diff --git a/docs/site-architecture/references.json b/docs/site-architecture/references.json new file mode 100644 index 00000000000..7a4b6beed0c --- /dev/null +++ b/docs/site-architecture/references.json @@ -0,0 +1,2166 @@ +{ + "occurrences": [ + { + "file": ".agents/skills/celestial-skill/references/implementation-map.md", + "classification": "docs code-span", + "scope": "live" + }, + { + "file": ".github/ci-areas.json", + "classification": "CI routing", + "scope": "live" + }, + { + "file": ".github/scripts/architecture/architecture.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": ".github/scripts/architecture/format-schema-exceptions.json", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": ".github/scripts/architecture/format-schema-ownership.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": ".github/scripts/architecture/graph.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": ".github/scripts/architecture/moved-format-ownership.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": ".github/scripts/architecture/nebula-inbound.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": ".github/scripts/architecture/nebula-inbound.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": ".github/scripts/architecture/nebula-inbound.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": ".github/scripts/audits/provenance-documentation-inventory.py", + "classification": "Python script", + "scope": "live" + }, + { + "file": ".github/scripts/audits/test_provenance_documentation_inventory.py", + "classification": "Python script", + "scope": "live" + }, + { + "file": ".github/scripts/checks/object-test-relocation.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": ".github/scripts/checks/package-boundaries.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": ".github/scripts/checks/typescript-ownership.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": ".github/scripts/ci/astroquery-tests.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": ".github/scripts/ci/check-ci.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": ".github/scripts/ci/classify-changes.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": ".github/scripts/ci/object-scope-gate.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": ".github/scripts/ci/prepare-typecheck.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": ".github/scripts/tsconfig.tests.json", + "classification": "tsconfig/eslint/package.json", + "scope": "live" + }, + { + "file": ".github/workflows/audit.yml", + "classification": "workflow", + "scope": "live" + }, + { + "file": ".github/workflows/nightly.yml", + "classification": "workflow", + "scope": "live" + }, + { + "file": ".github/workflows/universe.yml", + "classification": "workflow", + "scope": "live" + }, + { + "file": ".gitignore", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "AGENTS.md", + "classification": "AGENTS.md contract text", + "scope": "live" + }, + { + "file": "CONTRIBUTING.md", + "classification": "docs link", + "scope": "live" + }, + { + "file": "astro.config.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "cloudflare/worker.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "docs/architecture/exploration-catalog.md", + "classification": "docs link", + "scope": "live" + }, + { + "file": "docs/flight-lifecycle.md", + "classification": "docs code-span", + "scope": "live" + }, + { + "file": "docs/galaxies/README.md", + "classification": "docs link", + "scope": "live" + }, + { + "file": "docs/moon-catalogues.md", + "classification": "docs code-span", + "scope": "live" + }, + { + "file": "docs/native-scroll-zoom.md", + "classification": "docs code-span", + "scope": "live" + }, + { + "file": "docs/navigation-identity.md", + "classification": "docs link", + "scope": "live" + }, + { + "file": "docs/navigation-identity.md", + "classification": "docs code-span", + "scope": "live" + }, + { + "file": "docs/object-discovery.md", + "classification": "docs code-span", + "scope": "live" + }, + { + "file": "docs/object-provenance.md", + "classification": "docs code-span", + "scope": "live" + }, + { + "file": "docs/page-navigation-transport.md", + "classification": "docs code-span", + "scope": "live" + }, + { + "file": "docs/performance/marker-declutter.md", + "classification": "docs code-span", + "scope": "live" + }, + { + "file": "docs/performance/startup-billboard.md", + "classification": "docs code-span", + "scope": "live" + }, + { + "file": "docs/performance/startup-gate.md", + "classification": "docs link", + "scope": "live" + }, + { + "file": "docs/performance/world-context-by-system.md", + "classification": "docs link", + "scope": "live" + }, + { + "file": "docs/prepared-navigation-ownership.md", + "classification": "docs code-span", + "scope": "live" + }, + { + "file": "docs/provenance/CONTRACT.md", + "classification": "docs code-span", + "scope": "live" + }, + { + "file": "docs/reader-text.md", + "classification": "docs link", + "scope": "live" + }, + { + "file": "docs/reader-text.md", + "classification": "docs code-span", + "scope": "live" + }, + { + "file": "docs/satellite-system-navigation.md", + "classification": "docs link", + "scope": "live" + }, + { + "file": "docs/seo.md", + "classification": "docs code-span", + "scope": "live" + }, + { + "file": "docs/site-architecture.md", + "classification": "docs code-span", + "scope": "plan" + }, + { + "file": "docs/sources-catalogue.md", + "classification": "docs code-span", + "scope": "live" + }, + { + "file": "eslint.config.mts", + "classification": "tsconfig/eslint/package.json", + "scope": "live" + }, + { + "file": "labs/experiments/native-scroll/tsconfig.json", + "classification": "tsconfig/eslint/package.json", + "scope": "live" + }, + { + "file": "labs/investigations/capture-galaxies.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "labs/nebula/packages/lab/src/adapters/css-earth-shell.css", + "classification": "source import", + "scope": "live" + }, + { + "file": "labs/performance/README.md", + "classification": "docs code-span", + "scope": "live" + }, + { + "file": "labs/performance/tsconfig.tests.json", + "classification": "tsconfig/eslint/package.json", + "scope": "live" + }, + { + "file": "netlify.toml", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "netlify/functions/report.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "package.json", + "classification": "tsconfig/eslint/package.json", + "scope": "live" + }, + { + "file": "packages/astronomy/tsconfig.tests.json", + "classification": "tsconfig/eslint/package.json", + "scope": "live" + }, + { + "file": "packages/bake/AGENTS.md", + "classification": "AGENTS.md contract text", + "scope": "live" + }, + { + "file": "packages/bake/README.md", + "classification": "docs code-span", + "scope": "live" + }, + { + "file": "packages/bake/src/delivery/asset-origin-scenes.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "packages/bake/src/delivery/public-runtime-assets.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "packages/bake/src/navigation/prepare-navigation.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "packages/bake/src/prepared-presentation/prepared-presentation-bindings.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "packages/bake/src/presentation/clean-leaves.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "packages/bake/src/presentation/clean-leaves.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "packages/bake/src/run-implemented-objects/index.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "packages/bake/src/run-implemented-objects/test-discovery.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "packages/bake/src/run-implemented-objects/test-discovery.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "packages/bake/src/runtime-source/runtime-source-graph.test.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "packages/bake/src/site-assets/prepare-dataset-billboards.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "packages/bake/src/site-assets/prepare-search-thumbnails.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "packages/bake/src/site-assets/prepare-search-thumbnails.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "packages/bake/src/site-assets/prepare-search-thumbnails.ts", + "classification": "computed import", + "scope": "live" + }, + { + "file": "packages/bake/src/sources/facility-artwork-refresh.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "packages/bake/src/world-context/plain-star-dots.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "packages/bake/src/world-context/summary.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "packages/bake/tsconfig.tests.json", + "classification": "tsconfig/eslint/package.json", + "scope": "live" + }, + { + "file": "packages/catalog/tsconfig.tests.json", + "classification": "tsconfig/eslint/package.json", + "scope": "live" + }, + { + "file": "packages/core/tsconfig.tests.json", + "classification": "tsconfig/eslint/package.json", + "scope": "live" + }, + { + "file": "packages/engine/tsconfig.tests.json", + "classification": "tsconfig/eslint/package.json", + "scope": "live" + }, + { + "file": "packages/fits/tsconfig.tests.json", + "classification": "tsconfig/eslint/package.json", + "scope": "live" + }, + { + "file": "packages/objects/AGENTS.md", + "classification": "AGENTS.md contract text", + "scope": "live" + }, + { + "file": "packages/objects/README.md", + "classification": "docs code-span", + "scope": "live" + }, + { + "file": "packages/objects/src/node/prepared-registry.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "packages/objects/src/prepared-data/source/format-reader-ledger.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "packages/objects/src/provenance/source-credits.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "packages/objects/src/registry/index.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "packages/objects/tsconfig.tests.json", + "classification": "tsconfig/eslint/package.json", + "scope": "live" + }, + { + "file": "packages/renderer/AGENTS.md", + "classification": "AGENTS.md contract text", + "scope": "live" + }, + { + "file": "packages/renderer/src/labels/surface-feature-labels.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "packages/renderer/src/navigation/camera-layout.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "packages/renderer/src/navigation/eye-relative-readers.test.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "packages/renderer/src/node/test-exports.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "packages/renderer/src/rendering/prepared-asset-origin.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "packages/renderer/src/universe/world-context/world-context-orbit-demand.test.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "packages/renderer/src/universe/world-context/world-context-planner.test.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "packages/renderer/tsconfig.tests.json", + "classification": "tsconfig/eslint/package.json", + "scope": "live" + }, + { + "file": "packages/spice/tsconfig.tests.json", + "classification": "tsconfig/eslint/package.json", + "scope": "live" + }, + { + "file": "packages/telescope-cli/src/query.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "packages/telescope-cli/src/sphere/sphere-lane.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "packages/telescope-cli/tsconfig.tests.json", + "classification": "tsconfig/eslint/package.json", + "scope": "live" + }, + { + "file": "packages/telescope/tsconfig.tests.json", + "classification": "tsconfig/eslint/package.json", + "scope": "live" + }, + { + "file": "packages/volume-viewer/tsconfig.tests.json", + "classification": "tsconfig/eslint/package.json", + "scope": "live" + }, + { + "file": "site/application-world-context.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/application-world-frames.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/application-world-resources.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/application-world-resources.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/application-world-types.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/application-world-visibility.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/build/bundle-cloudflare-worker.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/build/bundle-netlify-functions.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/build/content/prepare.ts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/build/function-project-files.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/build/object-package-contract.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/build/prepare/check-preparation-inputs.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/build/prepare/prepare-catalog.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/build/prepare/prepare-catalog.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/build/prepare/prepare-context-availability.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/build/prepare/prepare-facilities.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/build/prepare/prepare-feature-index.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/build/prepare/prepare-feature-index.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/build/prepare/prepare-interior-fills.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/build/prepare/prepare-moon-labels.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/build/prepare/prepare-moon-labels.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/build/prepare/prepare-object-discovery.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/build/prepare/prepare-object-json.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/build/prepare/prepare-shell-icons.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/build/prepare/prepare-shell-titles.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/build/prepare/prepare-sidebar-thumbnails.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/build/prepare/prepare-spatial-context.ts", + "classification": "computed import", + "scope": "live" + }, + { + "file": "site/build/prepare/prepare-text.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/build/prepare/prepare-text.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/build/prepare/prepare-volume-presentation.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/build/prepare/prepare-world-presentation.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/build/prepare/prepare-world-presentation.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/build/prepare/refresh-source-icons.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/build/prepare/refresh-source-icons.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/build/prepare/system-packages.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/build/prepare/system-packages.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/build/prepare/system-text.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/build/share-images.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/catalogue-moon-labels.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/components/ChildList.astro", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/components/ContextNavigationMarker.astro", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/components/CssEarthWordmark.astro", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/components/DatasetContextPanels.astro", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/components/DatasetDescription.astro", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/components/DatasetLegend.astro", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/components/DatasetList.astro", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/components/ObjectBreadcrumbs.astro", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/components/ObjectCardHeader.astro", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/components/ObjectFactsheet.astro", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/components/ObjectInformationPanel.astro", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/components/ObjectNavigationMarker.astro", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/components/ObjectPage.astro", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/components/ObjectPage.astro", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/components/ObjectShell.astro", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/components/ObjectSwatchStyles.astro", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/components/PreparedObjectPanel.astro", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/components/SequencePlayer.astro", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/components/SourcesLink.astro", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/components/SystemCard.astro", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/dataset-picker.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/dataset-response.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/dot-catalogue-data.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/dot-catalogue-data.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/env.d.ts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/exploration-catalog.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/first-view-transport.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/hosted-banks.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/hosted-banks.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/import-queue.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/information-card.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/inside-view.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/layout-sections.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/layouts/ObjectLayout.astro", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/layouts/ObjectLayout.astro", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/navigation/navigation-content.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/navigation/navigation-fragments.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/navigation/navigation-history.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/navigation/navigation-request.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/navigation/navigation-scope.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/object-adapter.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/object-browser.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/object-children.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/object-directory.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/object-entries.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/object-entry.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/object-page-contract.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/object-page-data.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/object-systems.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/object-systems.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/object-text.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/objects.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/overview-selection.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/packaged-object-runtime.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/pages/[id].astro", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/pages/catalogues/dots.bin.ts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/pages/index.astro", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/pages/native-input.css.ts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/pages/native-input.css.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/pages/navigation/[id].astro", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/pages/objects/[id]/asset-hashes/[group].json.ts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/pages/objects/[id]/datasets/[dataset].json.ts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/pages/objects/[id]/entry.json.ts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/pages/objects/[id]/entry.json.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/pages/objects/[id]/first-view.json.ts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/pages/objects/[id]/object.json.ts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/pages/robots.txt.ts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/pages/sitemap.xml.ts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/pages/world/anywhere.json.ts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/pages/world/context-assets/[id].json.ts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/pages/world/dots/[id].bin.ts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/pages/world/orbits/[id].bin.ts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/pages/world/places/[id].json.ts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/pages/world/places/[id].json.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/pages/world/system-views/[id].json.ts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/pages/world/system-views/[id].json.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/pages/world/systems/[id].json.ts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/pages/world/systems/[id].json.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/planetary-system-members.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/planetary-system-members.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/prepare-body-moons.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/prepared-arrival.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/prepared-panel-content.shared-readers.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/prepared-world-navigation.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/prepared-world-navigation.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/prepared-world-presentation.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/prepared-world-presentation.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/prepared/prepared-world-context-node-source.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/root-object.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/satellite-selection.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/satellite-systems.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/scene/camera-handover.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/scene/scene-activation.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/scene/scene-datasets.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/scene/scene-feature.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/scene/scene-publication.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/scene/scene-registry.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/scene/scene-router.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/scene/scene-selection.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/scene/scene-session.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/scene/scene-subject.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/scene/scene-transition.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/scene/scene-transition.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/scene/scene-view.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/scene/scene-world.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/search/prepared-catalogue-index.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/search/prepared-catalogue-index.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/search/search-client.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/search/search-objects.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/selection-presentation.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/seo-trail.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/seo.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/server/feature-pin.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/server/load-prepared-scene.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/server/search-response.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/server/search-route.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/server/search-server.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/shared-imports.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/shell/object-shell-client.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/shell/object-shell-types.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/shell/shell-settings.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/shell/shell-sheet.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/site.css", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/social-images.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/source-documentation.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/startup-billboard.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/startup-boot.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/startup-world.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/stellar-extents.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/system-card.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/system-framing-radii.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/system-framing.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/system-framing.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/tab-panels.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/application-world-frames.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/application-world-visibility.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/arrival-billboard.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/asset-origin.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/body-additions.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/test/body-moons.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/browser-node-imports.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/test/built-pages.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/camera-handover.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/category-frames.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/chart-pixel-alignment.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/compact-spectrum.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/dataset-picker.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/dataset-response.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/dataset-url.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/destination-browser.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/diagnostic-recorder.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/error-report.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/first-view-transport.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/fixtures/objects.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/fixtures/saturn-prepared.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/hosted-banks.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/import-queue.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/in-flight-loader.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/information-card-sections.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/initial-shell-context.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/moon-labels.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/navigation-fragments.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/navigation-fragments.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/test/navigation-history.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/navigation-ontology.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/navigation-preparation.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/navigation-preparation.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/test/navigation-request.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/navigation-router-context.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/test/navigation-scope.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/navigation-styles.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/test/navigation-test-values.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/navigation-timing.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/nebula-search.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/object-children.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/object-directory.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/object-navigation.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/object-package-contract.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/object-package-contract.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/test/object-page-data.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/object-page-data.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/test/object-systems.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/object-text.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/overview-selection.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/prepare-context-availability.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/prepared-arrival.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/prepared-body-billboards.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/prepared-dataset-facts.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/prepared-dataset-legends.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/prepared-descriptor.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/prepared-icons.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/prepared-icons.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/test/prepared-panel-content.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/prepared-presentation-bindings.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/prepared-presentation-bindings.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/test/prepared-world-context.test.ts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/prepared-world-context.test.ts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/test/prepared-world-navigation.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/restore-inputs.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/restore-inputs.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/test/runtime-package.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/runtime-policy.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/test/satellite-arrival-framing.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/satellite-systems.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/scene-contract.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/scene-publication.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/scene-session.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/scientific-chart-order.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/test/search-api.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/search-response.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/seo-discovery.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/seo.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/setup-assets.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/shell-settings.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/showcase.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/source-link.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/startup-billboard.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/startup-cover.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/startup-requests.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/stellar-extents.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/system-framing.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/system-text.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/system-view-file.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/view-readout.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/view-url-runtime.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/volume-presentation.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/web-analytics.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/world-stars.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/world-tables.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/test/zoom-scope.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/view-readout.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/view-url-runtime.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/volume-presentation.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/world-approach.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/world-context-plan.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/world-objects.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/world-objects.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/world-places.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/world-system-views.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/world-system-views.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/zoom-scope.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "src/objects/README.md", + "classification": "docs code-span", + "scope": "live" + }, + { + "file": "src/objects/antares/README.md", + "classification": "docs code-span", + "scope": "live" + }, + { + "file": "src/objects/eps-indi-ba/README.md", + "classification": "docs link", + "scope": "live" + }, + { + "file": "src/objects/hd-189733-companion/README.md", + "classification": "docs link", + "scope": "live" + }, + { + "file": "src/objects/kelt-9b/README.md", + "classification": "docs link", + "scope": "live" + }, + { + "file": "src/objects/lmc-volume/investigations.json", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "src/objects/luhman-16/README.md", + "classification": "docs link", + "scope": "live" + }, + { + "file": "src/objects/nearby-universe-galaxies/README.md", + "classification": "docs link", + "scope": "live" + }, + { + "file": "src/objects/observable-universe-cmb/README.md", + "classification": "docs code-span", + "scope": "live" + }, + { + "file": "src/objects/observable-universe-cmb/README.md", + "classification": "docs link", + "scope": "live" + }, + { + "file": "src/objects/trappist-1/README.md", + "classification": "docs link", + "scope": "live" + }, + { + "file": "src/objects/trappist-1c/README.md", + "classification": "docs link", + "scope": "live" + }, + { + "file": "src/objects/trappist-1d/README.md", + "classification": "docs link", + "scope": "live" + }, + { + "file": "src/objects/trappist-1e/README.md", + "classification": "docs link", + "scope": "live" + }, + { + "file": "src/objects/trappist-1f/README.md", + "classification": "docs link", + "scope": "live" + }, + { + "file": "src/objects/trappist-1g/README.md", + "classification": "docs link", + "scope": "live" + }, + { + "file": "src/objects/trappist-1h/README.md", + "classification": "docs link", + "scope": "live" + }, + { + "file": "src/objects/wasp-103b/README.md", + "classification": "docs link", + "scope": "live" + }, + { + "file": "src/objects/wasp-121/README.md", + "classification": "docs link", + "scope": "live" + }, + { + "file": "src/objects/wasp-18/README.md", + "classification": "docs link", + "scope": "live" + }, + { + "file": "src/objects/wasp-76/README.md", + "classification": "docs link", + "scope": "live" + }, + { + "file": "src/objects/wasp-76b/README.md", + "classification": "docs link", + "scope": "live" + }, + { + "file": "src/tsconfig.tests.json", + "classification": "tsconfig/eslint/package.json", + "scope": "live" + }, + { + "file": "tsconfig.integration-tests.json", + "classification": "tsconfig/eslint/package.json", + "scope": "live" + }, + { + "file": "tsconfig.scripts.json", + "classification": "tsconfig/eslint/package.json", + "scope": "live" + } + ], + "relativeReads": [ + { + "file": "site/test/arrival-discovery.test.mts", + "destination": "site/build/prepare/arrival-discovery.test.mts" + }, + { + "file": "site/test/body-additions.test.mts", + "destination": "site/build/prepare/body-additions.test.mts" + }, + { + "file": "site/test/category-frames.test.mts", + "destination": "site/build/prepare/category-frames.test.mts" + }, + { + "file": "site/test/compact-spectrum.test.mts", + "destination": "site/overview/compact-spectrum.test.mts" + }, + { + "file": "site/test/dataset-response.test.mts", + "destination": "site/server/dataset-response.test.mts" + }, + { + "file": "site/test/folded-transit.test.mts", + "destination": "site/build/charts/folded-transit.test.mts" + }, + { + "file": "site/test/hosted-banks.test.mts", + "destination": "site/build/prepare/hosted-banks.test.mts" + }, + { + "file": "site/test/load-object-content.mts", + "destination": "site/build/content/load-object-content.test-support.mts" + }, + { + "file": "site/test/lonlat-slice-table.test.mts", + "destination": "site/build/prepare/lonlat-slice-table.test.mts" + }, + { + "file": "site/test/moon-labels.test.mts", + "destination": "site/world/moon-labels.test.mts" + }, + { + "file": "site/test/neutral-catalogue-color.test.mts", + "destination": "site/journeys/neutral-catalogue-color.test.mts" + }, + { + "file": "site/test/object-page-data.test.mts", + "destination": "site/server/object-page-data.test.mts" + }, + { + "file": "site/test/object-systems.test.mts", + "destination": "site/world/object-systems.test.mts" + }, + { + "file": "site/test/prepare-spatial-context.test.mts", + "destination": "site/build/prepare/prepare-spatial-context.test.mts" + }, + { + "file": "site/test/prepared-panel-content.test.mts", + "destination": "site/content/prepared-panel-content.test.mts" + }, + { + "file": "site/test/prepared-world-context.test.ts", + "destination": "site/world/prepared-world-context.test.ts" + }, + { + "file": "site/test/satellite-arrival-framing.test.mts", + "destination": "site/navigation/satellite-arrival-framing.test.mts" + }, + { + "file": "site/test/scene-contract.test.mts", + "destination": "site/world/scene-contract.test.mts" + }, + { + "file": "site/test/system-framing.test.mts", + "destination": "site/world/system-framing.test.mts" + }, + { + "file": "site/test/system-text.test.mts", + "destination": "site/build/prepare/system-text.test.mts" + }, + { + "file": "site/test/system-view-file.mts", + "destination": "site/world/system-view-file.test-support.mts" + }, + { + "file": "site/test/world-billboards.test.mts", + "destination": "site/journeys/world-billboards.test.mts" + }, + { + "file": "site/test/world-stars.test.mts", + "destination": "site/world/world-stars.test.mts" + }, + { + "file": "site/test/world-tables.test.mts", + "destination": "site/world/world-tables.test.mts" + }, + { + "file": "site/test/zoom-scope.test.mts", + "destination": "site/world/zoom-scope.test.mts" + } + ] +} diff --git a/docs/site-architecture/tiers.json b/docs/site-architecture/tiers.json new file mode 100644 index 00000000000..2014278212c --- /dev/null +++ b/docs/site-architecture/tiers.json @@ -0,0 +1,502 @@ +{ + "tiers": [ + { + "tier": 0, + "name": "Browser input, DOM and serialized import queue", + "folders": [ + "site/browser" + ] + }, + { + "tier": 0, + "name": "Preserved input records and artwork", + "folders": [ + "site/source" + ] + }, + { + "tier": 0, + "name": "Preserved third-party notices", + "folders": [ + "site/vendor" + ] + }, + { + "tier": 0, + "name": "Shared page and shell interfaces", + "folders": [ + "site/contracts" + ] + }, + { + "tier": 0, + "name": "Prepared transports and generated inputs", + "folders": [ + "site/prepared" + ] + }, + { + "tier": 0, + "name": "Object identities, routes and held addresses", + "folders": [ + "site/model" + ] + }, + { + "tier": 1, + "name": "Surface-map measurements and view formatting", + "folders": [ + "site/minimap" + ] + }, + { + "tier": 2, + "name": "Shared framing, visibility and camera context", + "folders": [ + "site/world" + ] + }, + { + "tier": 0, + "name": "Prepared spectral overview readers", + "folders": [ + "site/overview" + ] + }, + { + "tier": 3, + "name": "Card content, citations and metadata", + "folders": [ + "site/content" + ] + }, + { + "tier": 4, + "name": "Catalogue search and result presentation", + "folders": [ + "site/search" + ] + }, + { + "tier": 3, + "name": "History, requests, flights and arrivals", + "folders": [ + "site/navigation" + ] + }, + { + "tier": 4, + "name": "Committed selection and camera handovers", + "folders": [ + "site/selection" + ] + }, + { + "tier": 5, + "name": "Retained shell controls and panels", + "folders": [ + "site/shell" + ] + }, + { + "tier": 6, + "name": "Scene sessions, replacement and publication", + "folders": [ + "site/scene" + ] + }, + { + "tier": 7, + "name": "Page boot helpers and router entry", + "folders": [ + "site/startup" + ] + }, + { + "tier": 6, + "name": "SSR readers, responses and host middleware", + "folders": [ + "site/server" + ] + }, + { + "tier": 8, + "name": "Shared page frame and its styles", + "folders": [ + "site/layouts" + ] + }, + { + "tier": 9, + "name": "Reusable Astro markup and SSR composition", + "folders": [ + "site/components" + ] + }, + { + "tier": 10, + "name": "Routes and object-page entries", + "folders": [ + "site/pages" + ] + }, + { + "tier": 7, + "name": "Site preparation and packaging", + "folders": [ + "site/build" + ] + }, + { + "tier": 11, + "name": "Retired test folder; no final occupants", + "folders": [ + "site/test" + ] + }, + { + "tier": 11, + "name": "Cross-cutting journeys and support", + "folders": [ + "site/journeys" + ] + }, + { + "tier": 1, + "name": "Startup reads and object catalogue directory", + "folders": [ + "site/directory" + ] + }, + { + "tier": 0, + "name": "Server and build prepared-asset origin", + "folders": [ + "site/server-assets" + ] + } + ], + "root": { + "allow": [ + "site/env.d.ts" + ], + "entries": [ + "site/env.d.ts" + ], + "tier": 11 + }, + "lateral": [], + "tests": "any-tier", + "testFiles": [ + "site/build/source-maps.test.mts", + "site/navigation/evidence/star-click/one-click.jpg", + "site/prepared-panel-content.shared-readers.test.mts", + "site/test/application-world-frames.test.mts", + "site/test/application-world-visibility.test.mts", + "site/test/arrival-billboard.test.mts", + "site/test/arrival-discovery.test.mts", + "site/test/asset-origin.test.mts", + "site/test/astro-component-imports.test.mts", + "site/test/authored-object-fixture.mts", + "site/test/body-additions.test.mts", + "site/test/body-moons.test.mts", + "site/test/browser-node-imports.test.mts", + "site/test/built-pages.test.mts", + "site/test/camera-handover.test.mts", + "site/test/catalogue-window.test.mts", + "site/test/category-frames.test.mts", + "site/test/chart-pixel-alignment.test.mts", + "site/test/charts.test.ts", + "site/test/check-preparation-inputs.test.mts", + "site/test/cloudflare-worker.test.mts", + "site/test/compact-spectrum.test.mts", + "site/test/context-lineage.test.mts", + "site/test/dataset-picker.test.mts", + "site/test/dataset-response.test.mts", + "site/test/dataset-url.test.mts", + "site/test/descriptor-roundtrip.test.mts", + "site/test/destination-browser.test.mts", + "site/test/diagnostic-accessor.test.mts", + "site/test/diagnostic-recorder.test.mts", + "site/test/error-report.test.mts", + "site/test/evidence/passive-minimaps/browser.json", + "site/test/evidence/passive-minimaps/earth.png", + "site/test/evidence/passive-minimaps/jupiter-sequence.png", + "site/test/first-view-transport.test.mts", + "site/test/fixtures/anchor-table.mts", + "site/test/fixtures/context-package.mts", + "site/test/fixtures/galaxies/captures.json", + "site/test/fixtures/object-content-fixture.mts", + "site/test/fixtures/objects.mts", + "site/test/fixtures/prepared-schemas.mts", + "site/test/fixtures/saturn-prepared.mts", + "site/test/folded-transit.test.mts", + "site/test/galactic-focus.test.mts", + "site/test/hosted-banks.test.mts", + "site/test/import-queue.test.mts", + "site/test/in-flight-loader.test.mts", + "site/test/information-card-sections.test.mts", + "site/test/information-tab-styles.test.mts", + "site/test/initial-shell-context.test.mts", + "site/test/inline-page-stylesheet.test.mts", + "site/test/kept-load.test.mts", + "site/test/list-distance.test.mts", + "site/test/load-object-content.mts", + "site/test/lonlat-slice-table.test.mts", + "site/test/moon-labels.test.mts", + "site/test/navigation-content.test.mts", + "site/test/navigation-fragments.test.mts", + "site/test/navigation-history.test.mts", + "site/test/navigation-lifecycle.test.mts", + "site/test/navigation-ontology.test.mts", + "site/test/navigation-preparation.test.mts", + "site/test/navigation-readiness.test.mts", + "site/test/navigation-request.test.mts", + "site/test/navigation-router-context.mts", + "site/test/navigation-scope.test.mts", + "site/test/navigation-shell-content.test.mts", + "site/test/navigation-styles.test.mts", + "site/test/navigation-test-values.mts", + "site/test/navigation-timing.test.mts", + "site/test/nebula-search.test.mts", + "site/test/neutral-catalogue-color.test.mts", + "site/test/object-children.test.mts", + "site/test/object-control-binding.test.mts", + "site/test/object-directory.test.mts", + "site/test/object-navigation.test.mts", + "site/test/object-package-contract.test.mts", + "site/test/object-page-data.test.mts", + "site/test/object-result.test.mts", + "site/test/object-runtime-contract.test.mts", + "site/test/object-search-rank.test.mts", + "site/test/object-systems.test.mts", + "site/test/object-text.test.mts", + "site/test/overview-selection.test.mts", + "site/test/preparation-boundary.test.mts", + "site/test/prepare-authored.test.mts", + "site/test/prepare-context-availability.test.mts", + "site/test/prepare-object-json.test.mts", + "site/test/prepare-spatial-context.test.mts", + "site/test/prepare-text.test.mts", + "site/test/prepared-arrival.test.mts", + "site/test/prepared-body-billboards.test.mts", + "site/test/prepared-dataset-facts.test.mts", + "site/test/prepared-dataset-labels.test.mts", + "site/test/prepared-dataset-legends.test.mts", + "site/test/prepared-descriptor.test.mts", + "site/test/prepared-icons.test.mts", + "site/test/prepared-motion-css.test.mts", + "site/test/prepared-panel-content.test.mts", + "site/test/prepared-presentation-bindings.test.mts", + "site/test/prepared-presentation-contract.test.mts", + "site/test/prepared-world-context.test.ts", + "site/test/prepared-world-navigation.test.mts", + "site/test/preview.test.mts", + "site/test/production-diagnostics.test.mts", + "site/test/recovery-commands.test.mts", + "site/test/refresh-charts.test.mts", + "site/test/rendered-page.test.mts", + "site/test/restore-inputs.test.mts", + "site/test/run-script-chain.test.mts", + "site/test/runtime-package.test.mts", + "site/test/runtime-policy.test.mts", + "site/test/satellite-arrival-framing.test.mts", + "site/test/satellite-systems.test.mts", + "site/test/scene-contract.test.mts", + "site/test/scene-datasets.test.mts", + "site/test/scene-frame.test.mts", + "site/test/scene-publication.test.mts", + "site/test/scene-session.test.mts", + "site/test/scientific-chart-order.test.mts", + "site/test/search-api.test.mts", + "site/test/search-response.test.mts", + "site/test/seo-discovery.mts", + "site/test/seo-discovery.test.mts", + "site/test/seo.test.mts", + "site/test/setup-assets.test.mts", + "site/test/shell-selectors.test.mts", + "site/test/shell-settings.test.mts", + "site/test/shell-sheet.test.mts", + "site/test/showcase.test.mts", + "site/test/source-evidence-values.mts", + "site/test/source-link.test.mts", + "site/test/startup-billboard.test.mts", + "site/test/startup-cover.test.mts", + "site/test/startup-requests.test.mts", + "site/test/stellar-extents.test.mts", + "site/test/surface-label-loop.test.mts", + "site/test/surface-minimap.test.mts", + "site/test/system-framing.test.mts", + "site/test/system-text.test.mts", + "site/test/system-view-file.mts", + "site/test/view-readout.test.mts", + "site/test/view-url-runtime.test.mts", + "site/test/volume-presentation.test.mts", + "site/test/web-analytics.test.mts", + "site/test/world-billboards.test.mts", + "site/test/world-stars.test.mts", + "site/test/world-tables.test.mts", + "site/test/zoom-scope.test.mts" + ], + "assets": [ + "site/layouts/cubic-sky.css", + "site/moon-labels.prepared.json", + "site/navigation/evidence/star-click/one-click.jpg", + "site/object-navigation-marker.css", + "site/object-shell.css", + "site/prepared-catalogue.mjs", + "site/prepared-context-bank-assets.json", + "site/prepared-context-json.mts", + "site/prepared-context-objects.mts", + "site/prepared-dataset-billboards.json", + "site/prepared-dataset-volumes.json", + "site/prepared-dot-catalogues.json", + "site/prepared-facilities.json", + "site/prepared-feature-index.json", + "site/prepared-fit-boxes.json", + "site/prepared-hosted-banks.json", + "site/prepared-navigation-markers.mjs", + "site/prepared-shell-icons.mjs", + "site/prepared-shell-titles.mjs", + "site/prepared-source-credits.json", + "site/prepared-sources.json", + "site/prepared-stellar-extents.json", + "site/prepared-world-presentation.json", + "site/shell/maps-shell.css", + "site/shell/settings-panel.css", + "site/shell/shell-layout.css", + "site/site.css", + "site/source/agency-logos.json", + "site/source/branding/particle-globe.svg", + "site/source/facilities/catalog.json", + "site/source/facilities/emblem-library.json", + "site/source/facilities/emblems/README.md", + "site/source/facilities/emblems/originals/cassini.png", + "site/source/facilities/emblems/originals/dawn.png", + "site/source/facilities/emblems/originals/galileo.png", + "site/source/facilities/emblems/originals/grail-prepared.webp", + "site/source/facilities/emblems/originals/hayabusa.jpg", + "site/source/facilities/emblems/originals/hayabusa2.png", + "site/source/facilities/emblems/originals/hubble-commons.png", + "site/source/facilities/emblems/originals/juno.svg", + "site/source/facilities/emblems/originals/lro-commons.png", + "site/source/facilities/emblems/originals/magellan.png", + "site/source/facilities/emblems/originals/mars-global-surveyor-jpl.jpg", + "site/source/facilities/emblems/originals/messenger.png", + "site/source/facilities/emblems/originals/mro.png", + "site/source/facilities/emblems/originals/near-shoemaker.png", + "site/source/facilities/emblems/originals/new-horizons.png", + "site/source/facilities/emblems/originals/odyssey.png", + "site/source/facilities/emblems/originals/osiris-rex.png", + "site/source/facilities/emblems/originals/rosetta-patch.png", + "site/source/facilities/emblems/originals/sdo.png", + "site/source/facilities/emblems/originals/suomi-npp-commons.png", + "site/source/facilities/emblems/originals/terra-commons.png", + "site/source/facilities/emblems/originals/viking-commons.jpg", + "site/source/facilities/emblems/originals/voyager-1.png", + "site/source/facilities/emblems/originals/voyager-2.png", + "site/source/facilities/emblems/source-records.json", + "site/source/facilities/photograph-records.json", + "site/source/facilities/render-library.json", + "site/source/icons/SOURCE.md", + "site/source/icons/codepen-mark-LICENSE.txt", + "site/source/icons/github-mark-LICENSE.txt", + "site/source/icons/symbol-half-black-circle.svg", + "site/source/icons/symbol-proportional-to.svg", + "site/source/icons/symbol-quarter-phase.svg", + "site/source/icons/symbol-reference-mark.svg", + "site/source/icons/symbol-sine-wave.svg", + "site/source/icons/symbol-square-crosshatch-fill.svg", + "site/source/jpl-small-body-mission-targets.json", + "site/source/major-moons.json", + "site/source/moon-catalogues.json", + "site/source/moon-horizons.json.gz", + "site/source/overview-facts.json", + "site/source/scientific-charts/planetary-context.json", + "site/source/source-icons.json", + "site/source/titles/title-factsheet.svg", + "site/source/titles/title-reflectance-spectrum.svg", + "site/source/titles/title-settings.svg", + "site/source/titles/title-sources-resources.svg", + "site/source/titles/title-surface-dataset.svg", + "site/source/titles/title-surface-photos.svg", + "site/source/titles/title-telescope-images.svg", + "site/source/titles/title-thermal-profile.svg", + "site/test/evidence/passive-minimaps/browser.json", + "site/test/evidence/passive-minimaps/earth.png", + "site/test/evidence/passive-minimaps/jupiter-sequence.png", + "site/test/fixtures/galaxies/captures.json", + "site/vendor/cesium-LICENSE.md", + "site/wordmark.css" + ], + "status": "draft", + "forbid": [ + { + "from": [ + "site/browser", + "site/source", + "site/vendor", + "site/contracts", + "site/prepared", + "site/model", + "site/minimap", + "site/world", + "site/overview", + "site/content", + "site/search", + "site/navigation", + "site/selection", + "site/shell", + "site/scene", + "site/startup", + "site/directory" + ], + "to": [ + "site/server", + "site/build", + "site/server-assets" + ], + "reason": "Client owners cannot import server responses or preparation." + }, + { + "from": [ + "site/browser", + "site/source", + "site/vendor", + "site/contracts", + "site/prepared", + "site/model", + "site/minimap", + "site/world", + "site/overview", + "site/content", + "site/search", + "site/navigation", + "site/selection", + "site/shell", + "site/scene", + "site/server", + "site/components", + "site/build", + "site/directory" + ], + "to": [ + "site/startup" + ], + "reason": "Only layout and page entries enter the startup composition root." + }, + { + "to": [ + "site/build" + ], + "reason": "Page composition cannot import preparation.", + "from": [ + "site/layouts", + "site/components", + "site/pages" + ] + } + ], + "draftUntil": "2026-11-02", + "warningCeiling": 1 +} diff --git a/packages/bake/AGENTS.md b/packages/bake/AGENTS.md index 772e0d0559e..3a096c7f0fb 100644 --- a/packages/bake/AGENTS.md +++ b/packages/bake/AGENTS.md @@ -178,7 +178,7 @@ cycle. Share anything else through `@cssearth/core` or another package. Prepared reflectance, temperature-pressure, phase and light curves, FITS gallery pictures) and their shared SVG style. It imports no topic. The recipe dispatcher is site-owned preparation in `site/build/charts/`; the spectrum reader and compact spectrum are in `site/overview/`, because - `site/prepare-body-overview.mts` uses them and the runtime may not import the bake. + `site/build/charts/charts.ts` uses them and the runtime may not import the bake. - `objects/content`: the shared dataset vocabulary, dataset steps and prepared legends, each dataset control's billboard color, and the legend labels a palette dataset derives from the stretch its report states. It imports no topic. The content preparer that reads factsheets and writes the payload is site-owned preparation in `site/build/content/` From c2e3620d0b9a02d2921a8a42197aac1cd2e09812 Mon Sep 17 00:00:00 2001 From: Juan Cruz Fortunatti Date: Mon, 5 Oct 2026 05:35:31 +0200 Subject: [PATCH 4/5] test(architecture): build the declaration fixture in its own repository so partial clones can run it --- .github/scripts/architecture/import-declarations.test.mts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/scripts/architecture/import-declarations.test.mts b/.github/scripts/architecture/import-declarations.test.mts index e06e7dd44f0..aa090900bd3 100644 --- a/.github/scripts/architecture/import-declarations.test.mts +++ b/.github/scripts/architecture/import-declarations.test.mts @@ -49,8 +49,9 @@ test('production resolution retains duplicates, assets, tests, generated stand-i 'site/a.test.mts': "import './b.mts';", 'site/Empty.astro': '

', }; try { - // Reuse an existing HEAD without creating a commit, and populate only this fixture's index. - execFileSync('git', ['clone', '--quiet', '--shared', '--no-checkout', process.cwd(), root]); + // A repository of its own with one empty commit: a shared clone of the checkout fails on CI's partial clones. + execFileSync('git', ['init', '--quiet'], { cwd: root }); + execFileSync('git', ['-c', 'user.name=fixture', '-c', 'user.email=fixture@example.invalid', '-c', 'commit.gpgsign=false', 'commit', '--quiet', '--allow-empty', '-m', 'fixture'], { cwd: root }); for (const [file, source] of Object.entries(files)) { mkdirSync(dirname(join(root, file)), { recursive: true }); writeFileSync(join(root, file), source); } execFileSync('git', ['add', '.'], { cwd: root }); const result = await readImportDeclarations(root, { prefix: 'site/', checkAgainstScanner: true }); From 2b15cb43610d33a1d86df7a17d93da515ac69bae Mon Sep 17 00:00:00 2001 From: Juan Cruz Fortunatti Date: Mon, 5 Oct 2026 10:03:37 +0200 Subject: [PATCH 5/5] docs(architecture): record option 3 as the decided plan and make planned/enforced the plan status --- .github/scripts/architecture/README.md | 16 +- .github/scripts/architecture/projection.mts | 10 +- .../architecture/site-architecture.mts | 13 +- .../architecture/site-architecture.test.mts | 51 ++- docs/README.md | 2 +- docs/site-architecture.md | 58 ++- docs/site-architecture/edits.json | 2 +- docs/site-architecture/loader-options.json | 349 ------------------ docs/site-architecture/moves.json | 21 +- docs/site-architecture/references.json | 160 ++++++++ docs/site-architecture/tiers.json | 27 +- 11 files changed, 286 insertions(+), 423 deletions(-) delete mode 100644 docs/site-architecture/loader-options.json diff --git a/.github/scripts/architecture/README.md b/.github/scripts/architecture/README.md index 9fe5fc8cfe8..39c172f314b 100644 --- a/.github/scripts/architecture/README.md +++ b/.github/scripts/architecture/README.md @@ -96,10 +96,10 @@ and budget growth fail; tests and comments are excluded through the TypeScript s ## Pending site layout -The draft is in docs/site-architecture.md. Moves, sibling tiers, explicit denies, -atomic edit groups, loader alternatives and a compact path/class inventory live beside it. -`site-architecture.mts` shares the ordinary check's live graph. Draft findings fail after draftUntil or above warningCeiling; allowed warnings emit Actions annotations. -`status: "enforced"` makes every finding fail. Existing rules and scanner failures still fail. +The owner-decided plan is in docs/site-architecture.md. Moves, sibling tiers, explicit denies, +atomic edit groups, the decided loader design and a compact path/class inventory live beside it. +`site-architecture.mts` shares the ordinary check's live graph. `status: "planned"` findings always warn with a fix command and Actions annotation; there is no deadline or warning ceiling. +`status: "enforced"` makes every plan finding fail; the owner flips it when S4 ends. Strict `--accept` always fails on plan findings. Existing rules and scanner failures still fail. The architecture baseline is unchanged. Tests are leaf consumers (`tests: "any-tier"`) but remain in file SCCs. Production @@ -133,6 +133,10 @@ remaining edits, tables, references and wiring. It runs after moves; zero live occurrences are required. Historical and plan pointers are reported separately. S3 PRs remove applied groups. L6 removes null deletion entries. `--references` prints full line-numbered evidence on demand; do not commit that output. `--write` regenerates all marked tables -and references. Ordinary additions during the draft do not turn plan warnings into +and references. Ordinary additions while planned do not turn plan warnings into application refactors. Strict acceptance, sequence and minimality remain blocking -proof commands. See the draft for the contributor workflow and S3 behavior gates. +proof commands. See the plan for the contributor workflow and S3 behavior gates. + +When a warning names your file, update its mapping or semantic edits, run `--write`, +then `--accept`. Keep application behavior unchanged; a planned warning never blocks +`pnpm check:architecture`. diff --git a/.github/scripts/architecture/projection.mts b/.github/scripts/architecture/projection.mts index d8d7ae5498c..94f20b3b518 100644 --- a/.github/scripts/architecture/projection.mts +++ b/.github/scripts/architecture/projection.mts @@ -10,7 +10,7 @@ import { byText, isTestPath } from './zones.mts'; export interface Edge { readonly from: string; readonly to: string; readonly kind: Kind; readonly line?: number } interface Tier { readonly tier: number; readonly name: string; readonly folders: string[] } -interface TierMap { readonly tiers: Tier[]; readonly root: { readonly allow: string[]; readonly entries: string[]; readonly tier: number }; readonly lateral: string[][]; readonly tests?: 'any-tier'; readonly testFiles: string[]; readonly assets: string[]; readonly status?: 'draft' | 'enforced'; readonly forbid: { from: string[]; to: string[]; reason: string }[] } +interface TierMap { readonly tiers: Tier[]; readonly root: { readonly allow: string[]; readonly entries: string[]; readonly tier: number }; readonly lateral: string[][]; readonly tests?: 'any-tier'; readonly testFiles: string[]; readonly assets: string[]; readonly status?: 'planned' | 'enforced'; readonly forbid: { from: string[]; to: string[]; reason: string }[] } export interface ProjectionInput { readonly declarations: unknown; readonly moves: unknown; readonly tiers: unknown; readonly edits?: unknown } function record(value: unknown, label: string): Record { if (value === null || typeof value !== 'object' || Array.isArray(value)) throw new TypeError(`${label}: expected object`); @@ -37,12 +37,8 @@ function kind(value: unknown): Kind { throw new TypeError(`Unknown import kind: ${String(value)}`); } function tierMap(value: unknown): TierMap { - const map = record(value, 'tiers'); keys(map, ['tiers', 'root', 'lateral', 'tests', 'testFiles', 'assets', 'status', 'forbid', 'draftUntil', 'warningCeiling'], 'tiers'); - if (map.status !== undefined && map.status !== 'draft' && map.status !== 'enforced') throw new TypeError('status: expected draft or enforced'); - if (map.draftUntil !== undefined && (typeof map.draftUntil !== 'string' || !/^\d{4}-\d{2}-\d{2}$/u.test(map.draftUntil) - || !Number.isFinite(Date.parse(map.draftUntil)) || new Date(map.draftUntil).toISOString().slice(0, 10) !== map.draftUntil)) throw new TypeError('draftUntil: expected ISO date'); - if (map.warningCeiling !== undefined && (typeof map.warningCeiling !== 'number' || !Number.isSafeInteger(map.warningCeiling) || map.warningCeiling < 0)) throw new TypeError('warningCeiling: expected nonnegative integer'); - if (map.status === 'draft' && (map.draftUntil === undefined || map.warningCeiling === undefined)) throw new TypeError('draft requires draftUntil and warningCeiling'); + const map = record(value, 'tiers'); keys(map, ['tiers', 'root', 'lateral', 'tests', 'testFiles', 'assets', 'status', 'forbid'], 'tiers'); + if (map.status !== undefined && map.status !== 'planned' && map.status !== 'enforced') throw new TypeError('status: expected planned or enforced'); const forbid = array(map.forbid ?? [], 'forbid').map(value => { const rule = record(value, 'forbid'); keys(rule, ['from', 'to', 'reason'], 'forbid'); return { from: array(rule.from, 'forbid.from').map(value => path(value, 'folder')), to: array(rule.to, 'forbid.to').map(value => path(value, 'folder')), reason: string(rule.reason, 'forbid.reason') }; diff --git a/.github/scripts/architecture/site-architecture.mts b/.github/scripts/architecture/site-architecture.mts index 9492800f113..da9650a8e97 100644 --- a/.github/scripts/architecture/site-architecture.mts +++ b/.github/scripts/architecture/site-architecture.mts @@ -91,14 +91,11 @@ export function updateTables(document: string, tables: Readonly void, policy: { draftUntil?: string; warningCeiling?: number; today?: string; actions?: boolean } = {}): void { - if (status !== 'draft' && status !== 'enforced' && status !== undefined) throw new TypeError('status: expected draft or enforced'); +/** Planned findings warn without a deadline; enforced and strict acceptance findings throw. */ +export function planFinding(status: unknown, check: () => void, policy: { actions?: boolean } = {}): void { + if (status !== 'planned' && status !== 'enforced') throw new TypeError('status: expected planned or enforced'); try { check(); } catch (error) { - if (status !== 'draft') throw error; - const today = policy.today ?? new Date().toISOString().slice(0, 10); - if (!policy.draftUntil || today > policy.draftUntil) throw new Error(`Draft expired: ${policy.draftUntil ?? 'no deadline'}; ${String(error)}`); - if (1 > (policy.warningCeiling ?? 0)) throw new Error(`Draft warning ceiling exceeded (1 > ${policy.warningCeiling ?? 0}); ${String(error)}`); + if (status !== 'planned') throw error; const message = `SITE_PLAN_WARNING: ${String(error)}. Fix: node .github/scripts/architecture/site-architecture.mts --write`; console.warn(message); if (policy.actions ?? process.env.GITHUB_ACTIONS === 'true') console.warn(`::warning file=docs/site-architecture.md::${message.replaceAll('%', '%25').replaceAll('\r', '%0D').replaceAll('\n', '%0A')}`); @@ -164,7 +161,7 @@ export async function checkSiteArchitecture(root: string, options: { readonly gr writeFileSync(docPath, next); } else assertCurrent(document, next); console.log(`SITE_PLAN_OK: all views clear; tables match (${((performance.now() - started) / 1000).toFixed(1)} s${options.graph ? ', shared graph' : ', including scan'}).`); - }, { draftUntil: text(tierData.draftUntil), warningCeiling: number(tierData.warningCeiling) }); + }); } if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { diff --git a/.github/scripts/architecture/site-architecture.test.mts b/.github/scripts/architecture/site-architecture.test.mts index a0a455d601b..f3212c8b555 100644 --- a/.github/scripts/architecture/site-architecture.test.mts +++ b/.github/scripts/architecture/site-architecture.test.mts @@ -1,6 +1,9 @@ /** The plan's generated tables must expose connectivity failures and become stale on data changes. */ import assert from 'node:assert/strict'; import { test } from 'node:test'; +import { readFileSync } from 'node:fs'; +import { spawnSync } from 'node:child_process'; +import ts from 'typescript'; import { renderTables, updateTables } from './site-architecture.mts'; const input = { declarations: { declarations: [{ from: 'site/a.mts', to: 'site/base/base.mts', kind: 'value' }], summary: { files: ['site/a.mts', 'site/base/base.mts'] } }, @@ -30,32 +33,50 @@ test('the stale-table guard throws; equal content passes', async () => { assert.throws(() => assertCurrent('stale', 'current'), /stale/u); assert.doesNotThrow(() => assertCurrent('current', 'current')); }); -test('draft findings warn and enforced findings throw; malformed status fails', async () => { +test('planned findings only warn, enforced findings throw, and invalid statuses fail', async () => { const { planFinding } = await import('./site-architecture.mts'); const failure = () => { throw new Error('plan changed'); }; - const warn = console.warn, messages: unknown[] = []; console.warn = value => { messages.push(value); }; + const warn = console.warn, messages: string[] = []; console.warn = value => { messages.push(String(value)); }; try { - assert.doesNotThrow(() => planFinding('draft', failure, { draftUntil: '2026-11-02', today: '2026-10-05', warningCeiling: 1 })); assert.match(String(messages[0]), /WARNING.*--write/u); + // Removing the warn-only branch makes this assertion fail on the real finding. + assert.doesNotThrow(() => planFinding('planned', failure, { actions: false })); + assert.equal(messages.length, 1); + assert.match(messages[0]!, /WARNING.*plan changed.*Fix: node .*--write/u); assert.throws(() => planFinding('enforced', failure), /plan changed/u); - assert.throws(() => planFinding('unsigned', failure), /status/u); + for (const status of ['unsigned', 'draft', undefined, null]) assert.throws(() => planFinding(status, failure), /status/u); + assert.doesNotThrow(() => planFinding('enforced', () => {})); } finally { console.warn = warn; } }); -test('expired draft findings fail and a zero warning ceiling fails; Actions annotates allowed findings', async () => { +test('planned findings always emit Actions warnings, without a count ceiling', async () => { const { planFinding } = await import('./site-architecture.mts'); - const failure = () => { throw new Error('changed'); }; - assert.throws(() => planFinding('draft', failure, { draftUntil: '2000-01-01', today: '2026-10-05', warningCeiling: 1 }), /expired/u); - assert.throws(() => planFinding('draft', failure, { draftUntil: '2026-11-02', today: '2026-10-05', warningCeiling: 0 }), /ceiling/u); const warn = console.warn, messages: string[] = []; console.warn = value => { messages.push(String(value)); }; try { - planFinding('draft', failure, { draftUntil: '2026-11-02', today: '2026-10-05', warningCeiling: 1, actions: true }); - assert.ok(messages.some(message => message.startsWith('::warning file='))); + for (let i = 0; i < 3; i++) planFinding('planned', () => { throw new Error('changed%\nnext'); }, { actions: true }); + assert.equal(messages.length, 6); + assert.equal(messages.filter(message => message.startsWith('::warning file=docs/site-architecture.md::')).length, 3); + assert.match(messages[1]!, /changed%25%0Anext.*--write/u); } finally { console.warn = warn; } }); -test('draft policy requires a valid calendar date and a nonnegative warning ceiling', () => { - const tiers = { ...input.tiers, status: 'draft', draftUntil: '2026-11-02', warningCeiling: 0 }; - assert.doesNotThrow(() => renderTables({ ...input, tiers })); - assert.throws(() => renderTables({ ...input, tiers: { ...tiers, draftUntil: '2026-02-30' } }), /draftUntil/u); - assert.throws(() => renderTables({ ...input, tiers: { ...tiers, warningCeiling: -1 } }), /warningCeiling/u); +test('plan schema accepts only planned and enforced statuses', () => { + for (const status of ['planned', 'enforced']) assert.doesNotThrow(() => renderTables({ ...input, tiers: { ...input.tiers, status } })); + for (const status of ['draft', 'unsigned', null]) assert.throws(() => renderTables({ ...input, tiers: { ...input.tiers, status } }), /status/u); +}); + +test('deleting the planned warn-only rule turns the same finding assertion red', () => { + const source = readFileSync(new URL('./site-architecture.mts', import.meta.url), 'utf8'); + const start = source.indexOf('export function planFinding('), end = source.indexOf('\nexport async function checkSiteArchitecture', start); + assert.ok(start >= 0 && end > start); + const original = source.slice(start, end); + const mutant = original.replace("if (status !== 'planned') throw error;", 'throw error;'); + assert.notEqual(mutant, original); + const probe = (implementation: string) => { + const code = ts.transpileModule(implementation, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText; + return spawnSync(process.execPath, ['-e', `${code}\nrequire('node:assert/strict').doesNotThrow(() => planFinding('planned', () => { throw new Error('plan changed'); }, { actions: false }));`], { encoding: 'utf8' }); + }; + const passing = probe(original); assert.equal(passing.status, 0, passing.stderr); + assert.match(passing.stderr, /SITE_PLAN_WARNING.*plan changed.*--write/u); + const failing = probe(mutant); assert.equal(failing.status, 1, failing.stderr); + assert.match(failing.stderr, /AssertionError.*|Got unwanted exception/u); }); diff --git a/docs/README.md b/docs/README.md index 09cd6766490..98337cab529 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,7 +9,7 @@ For a body's sources, processing, evidence and known problems, read its | Topic | Guide | | --- | --- | -| Proposed site folders, dependency tiers and move sequence | [Site architecture draft](site-architecture.md) | +| Proposed site folders, dependency tiers and move sequence | [Site architecture plan](site-architecture.md) | | Computed schema ownership across packages and architecture locks | [Prepared format ownership](prepared-format-ownership.md) | | PR feedback budget, check selection, failure recovery and deployment | [CI/CD maintenance](ci-cd.md) | | Galaxies, LMC image datasets and extragalactic datasets | [Galaxies and the nearby universe](galaxies/README.md) | diff --git a/docs/site-architecture.md b/docs/site-architecture.md index 96d6fd052f3..7dce23855cd 100644 --- a/docs/site-architecture.md +++ b/docs/site-architecture.md @@ -1,8 +1,8 @@ -# Proposed site architecture +# Site architecture plan -**Recommend option 3: relocate queued loader functions to their owners.** This draft requires owner and alowpoly sign-off before S3. S2 changes only the plan and checker; it moves no application files. The projection proves connectivity, not runtime behavior or generated chunk bytes. +**Option 3 was decided by the owner on 2026-10-05: relocate queued loader functions to their owners.** The router starts at module evaluation exactly as today, and all loading uses the same single serialized queue chain. S2 changes only documentation and tools; it moves no application files. The projection proves connectivity, not runtime behavior or generated chunk bytes. This PR is ready for the owner to merge. -[moves.json](site-architecture/moves.json) records destinations; [tiers.json](site-architecture/tiers.json) records layers, denies and draft policy; [edits.json](site-architecture/edits.json) records the recommended semantic changes; [loader-options.json](site-architecture/loader-options.json) records alternatives; [references.json](site-architecture/references.json) is a compact path-and-class inventory. +[moves.json](site-architecture/moves.json) records destinations; [tiers.json](site-architecture/tiers.json) records layers, denies and planned/enforced status; [edits.json](site-architecture/edits.json) records the decided semantic changes; [references.json](site-architecture/references.json) is a compact path-and-class inventory. ## Folder ownership @@ -17,20 +17,20 @@ A folder imports itself or strictly lower layers. Sibling folders, including L0 | 0 | `prepared/` | Prepared transports and generated inputs | 25 | 24; `prepared-catalogue.d.mts`, `prepared-navigation-markers.d.mts`, `prepared-object-path.mts`, `prepared-shell-icons.d.mts` | | 0 | `source/` | Preserved input records and artwork | 60 | 0 | | 0 | `vendor/` | Preserved third-party notices | 1 | 0 | -| 0 | `server-assets/` | Server and build prepared-asset origin | 1 | 1; `asset-origin.mts` | +| 0 | `server-assets/` | Server and build prepared-asset origin | 2 | 2; `asset-origin.mts` | | 0 | `contracts/` | Shared page and shell interfaces | 3 | 3; `object-shell-types.ts`, `object-page-contract.mts` | -| 1 | `directory/` | Startup reads and object catalogue directory | 10 | 9; `startup-world.mts`, `startup-requests.mts`, `object-entries.mts`, `object-directory.mts` | +| 1 | `directory/` | Startup reads and object catalogue directory | 13 | 12; `startup-world.mts`, `startup-requests.mts`, `object-entries.mts`, `object-directory.mts` | | 1 | `minimap/` | Surface-map measurements and view formatting | 7 | 4 | | 2 | `world/` | Shared framing, visibility and camera context | 54 | 53; `world-objects.mts`, `world-system-views.mts`, `context-availability.mts`, `context-datasets.mts` | -| 3 | `content/` | Card content, citations and metadata | 21 | 20; `dataset-content.mts`, `dataset-context.mts`, `object-text.mts`, `prepared-panel-content.mts` | -| 3 | `navigation/` | History, requests, flights and arrivals | 34 | 22; `prepared-arrival.mts`, `prepared-scene-ownership.mts`, `arrival-billboard.mts`, `prepared-world-navigation.mts` | +| 3 | `content/` | Card content, citations and metadata | 22 | 21; `dataset-content.mts`, `dataset-context.mts`, `object-text.mts`, `prepared-panel-content.mts` | +| 3 | `navigation/` | History, requests, flights and arrivals | 35 | 23; `prepared-arrival.mts`, `prepared-scene-ownership.mts`, `arrival-billboard.mts`, `prepared-world-navigation.mts` | | 4 | `search/` | Catalogue search and result presentation | 17 | 6 | | 4 | `selection/` | Committed selection and camera handovers | 6 | 6; `scene/scene-selection.mts`, `satellite-selection.mts`, `overview-selection.mts`, `showcase.mts` | -| 5 | `shell/` | Retained shell controls and panels | 27 | 20; `object-browser.mts`, `feature-browser.mts`, `destination-browser.mts`, `selection-presentation.mts` | -| 6 | `server/` | SSR readers, responses and host middleware | 30 | 17; `object-page-data.mts`, `first-view-transport.mts`, `object-entry.mts`, `world-places.mts` | -| 6 | `scene/` | Scene sessions, replacement and publication | 26 | 11; `object-adapter.mts`, `packaged-object-runtime.mts`, `startup-billboard.mts`, `initial-scene.mts` | -| 7 | `startup/` | Page boot helpers and router entry | 7 | 7; `shared-imports.mts`, `startup-boot.mts`, `startup-cover.mts`, `initial-shell-context.mts` | -| 7 | `build/` | Site preparation and packaging | 82 | 31; `prepare-body-moons.mts` | +| 5 | `shell/` | Retained shell controls and panels | 32 | 25; `object-browser.mts`, `feature-browser.mts`, `destination-browser.mts`, `selection-presentation.mts` | +| 6 | `server/` | SSR readers, responses and host middleware | 32 | 19; `object-page-data.mts`, `first-view-transport.mts`, `object-entry.mts`, `world-places.mts` | +| 6 | `scene/` | Scene sessions, replacement and publication | 30 | 15; `object-adapter.mts`, `packaged-object-runtime.mts`, `startup-billboard.mts`, `initial-scene.mts` | +| 7 | `startup/` | Page boot helpers and router entry | 8 | 8; `shared-imports.mts`, `startup-boot.mts`, `startup-cover.mts`, `initial-shell-context.mts` | +| 7 | `build/` | Site preparation and packaging | 83 | 32; `prepare-body-moons.mts` | | 8 | `layouts/` | Shared page frame and its styles | 7 | 5; `components/ObjectSwatchStyles.astro`, `site.css`, `object-shell.css`, `wordmark.css` | | 9 | `components/` | Reusable Astro markup and SSR composition | 29 | 0 | | 10 | `pages/` | Routes and object-page entries | 20 | 0 | @@ -46,11 +46,11 @@ Server/search-response deliberately shares shell/selection-presentation and sele Registry types live in directory, navigation types in navigation and presenter types in scene. Their declared imports model implementation dependencies; adding an edge cannot establish necessity. -## Draft workflow +## Contributor workflow -The draft deadline is **2026-11-02**, inclusive. The owner may extend draftUntil explicitly in tiers.json before expiry. Until then a plan finding is a warning: the check prints it with the fix command and, in GitHub Actions, as an annotation, and a contributor's PR is not blocked (the check stops at the first finding, so the ceiling `warningCeiling: 1` allows exactly that one warning). After the deadline every plan finding fails. Existing scanner/rule failures always fail. Strict --accept always fails on findings. +There is no deadline. With `status: "planned"`, a plan finding is a WARNING with the fix command and a GitHub Actions annotation; it never fails `pnpm check:architecture`. The check reports the first plan finding. With `status: "enforced"`, every plan finding fails. The owner changes the status when S4 ends. Existing architecture rules and scanner failures always fail. Strict `--accept` always fails on plan findings. -When an unsigned plan drifts, update its destination/semantic edits and regenerate it. Do not reshape application behavior solely to silence a draft. Sign-off changes status from draft to enforced. Every mapped path is checked for existence, including deleted mapped files; generated inputs are explicit exceptions. Routine checks rescan the compact inventory so stale references fail rather than persist invisibly. +When a warning names your file, update its destination or semantic edits and run `node .github/scripts/architecture/site-architecture.mts --write`, then strict `--accept`. Do not change application behavior just to silence a plan warning. Every mapped path is checked for existence, including deleted mapped files; generated inputs are explicit exceptions. Routine checks rescan the compact inventory so stale references produce findings. ## S3 atomic changes @@ -65,10 +65,10 @@ The changes array partitions stable edit ids into atomic groups. All seven group | 3 | Split the moon catalogue reader | add-file-moon-catalogue, retarget-object-children-moon-catalogue, remove-import-prepare-body-moons-moon-catalogues, add-import-prepare-body-moons-moon-catalogue | Split catalogue parsing, cached source catalogue and catalogueMoons into a runtime-safe reader; preparation keeps search-based ordering. Read catalogueMoons from the split reader; production must never import build preparation. Move the source catalogue import with its parser into the reader; preparation calls that reader. Use the single catalogue reader for preparation too; preserve the named-moon eligibility policy in preparation. | | 3 | Pass selection into navigation requests | remove-import-navigation-request-scene-selection, add-import-navigation-request-scene-subject | Pass selectionTargetFromUrl explicitly through resolveNavigation and readNavigationSelection. Router uses the existing registry export, injects it into createSceneActivation, which forwards all three calls. Import erased SceneView/SelectionTarget aliases from navigation-types; never retain the selection import. Keep existing subject identity helpers in the lower world owner; pass selectionTargetFromUrl as a parameter. | | 6 | Extract the frame presenter contract | add-file-scene-frame-presenter, retarget-scene-session-scene-frame-presenter, add-import-scene-world-scene-frame-presenter | Extract the structural presenter interface; a session must not import the world coordinator that consumes it. Session reads SceneFramePresenter from the lower contract, breaking the two-way type dependency. World coordinator imports and re-exports the structural presenter contract. | -| 0 | Relocate queued loader owners | add-file-scene-imports, add-file-world-imports, remove-queue-runtime, remove-shared-registry, remove-shared-world, retarget-adapter-scene-imports, retarget-router-world-imports, add-router-scene-imports, remove-directory-queue, retarget-boot-scene-imports, add-boot-world-imports | Own queued registry and packaged-runtime functions in scene; share browser queuedImport and its single last chain. Own the queued application-world import in world; no initializer inside the thunk. Move the runtime function out of the import-free browser queue implementation. Leave only importSceneRouter in startup/shared-imports. Move the world function to its owner. Adapter directly uses the same queued runtime function; preserve the frozen default adapter. Split router loader imports; preserve module-evaluation autostart. Import registry/runtime functions and register only the directory runtime loader using the existing object-directory import before first scene call. Directory accepts a typed runtime-loader registration; read the slot at scene invocation, not metadata construction. Missing registration fails explicitly. Body prestart and registry prestart reuse scene-owned promises. View prestart reuses the world-owned promise. | +| 0 | Relocate queued loader owners | add-file-scene-imports, add-file-world-imports, remove-queue-runtime, remove-shared-registry, remove-shared-world, retarget-adapter-scene-imports, retarget-router-world-imports, add-router-scene-imports, remove-directory-queue, retarget-boot-scene-imports, add-boot-world-imports | Decided by the owner: loader modules hold only import() calls through the existing queue. Own registry and packaged-runtime functions in scene; move the single last chain to lower browser/import-queue. Prove one queue chain, registration before the first scene call, the same entry-chunk modules and bytes, identical L1/L2/L3, L7 no worse and the real-iPad startup journey. Own the queued application-world import in world; no initializer inside the thunk. Move the runtime function out of the import-free browser queue implementation. Leave only importSceneRouter in startup/shared-imports. Move the world function to its owner. Adapter directly uses the same queued runtime function; preserve the frozen default adapter. Split router loader imports; preserve module-evaluation autostart. Import registry/runtime functions and register only the directory runtime loader using the existing object-directory import before first scene call. Directory accepts a typed runtime-loader registration; read the slot at scene invocation, not metadata construction. Missing registration fails explicitly. Body prestart and registry prestart reuse scene-owned promises. View prestart reuses the world-owned promise. | -## Recommended loader design: option 3 +## Decided loader design: option 3 Read sources: site/import-queue.mts:13–26, site/shared-imports.mts:1–7, site/startup-boot.mts:1–24, site/layouts/ObjectLayout.astro:162–178, site/object-directory.mts:55–72, site/object-adapter.mts:1–20 and site/scene/scene-router.mts:839–845. @@ -86,13 +86,11 @@ router → directory runtime-loader registration → first scene call - Directory exports a typed registration function. It reads the configured loader at scene invocation, never during objectFromEntry or registry construction. Missing registration fails clearly. Metadata callers in build, Astro SSR and server endpoints remain valid without a loader. System-host recursion, abort signals, retry and entry identity must remain identical. - Root-level scene-imports/world-imports exist during S3; imports point at current paths until S4. No thunk awaits a queued function behind itself. The router's rejection remains a module-evaluation failure with the existing retry behavior and layout failure cleanup. -S3 must prove one shared queue chain, registration before the first scene call, metadata construction without registration, registration/retry/abort behavior, and queue settlement/error cleanup by tests with mutations. Compare entry-chunk module membership and bytes: the predicted additions are exactly the two tiny scene/world loader modules containing import() calls. This is an inference until an actual build comparison proves it. Reuse of functions does not prove unchanged bytes. +S3 must prove one shared queue chain, registration before the first scene call, metadata construction without registration, registration/retry/abort behavior, and queue settlement/error cleanup by tests with mutations. Require the same entry-chunk module membership and bytes, identical L1/L2/L3 results, L7 performance no worse, and the real-iPad startup journey (including delayed-summary cold loads). The extracted loader modules hold only import() calls through the shared queue; the single last chain lives in the lower browser module. These are S3 acceptance requirements, not results established by the S2 graph projection. Reuse of functions does not prove unchanged bytes. No functionality may break anywhere; performance must stay the same or improve. L7 is a performance guard beside the behavior checks. -## Loader alternatives +## Options not taken -1. **Import-free directory slot**, registered by the existing layout startup script before importSceneRouter (or a new startup/main if separately justified). Move loader implementations to startup and let router/adapter/directory read typed slot functions. It can preserve module autostart and downward edges; DI in general is not an impasse. The graph variant is recorded in loader-options.json. It is not recommended because it adds page-owned registration of registry/world/runtime plus use-time proxies across three consumers, where option 3 needs only directory registration and keeps function ownership beside their modules. It would require slot-before-import mutation tests and entry-chunk measurements; no S2 behavioral proof exists. -2. **Queued .then(start) DI**: pass loaders into a router initializer and adapter factory. Queue settlement may follow synchronous start, but module-evaluation phase differs, a retry can start twice, and any await of another queued loader inside start can deadlock. Prove these separately; a graph pass is not an equivalent-startup proof. This variant is not selected. -3. **Lazy-boundary exception**: preserve current service imports and explicitly exempt only the directory/adapter/router upward loader edges. Report service SCCs separately. This preserves current startup behavior but abandons the zero-cycle acceptance claim and requires changed rules; it is not selected. +Parameter injection with a changed router start changes the router's evaluation phase. An explicit lazy-boundary exception gives up the zero-cycle claim. Neither is part of the plan. ## Other S3 risks @@ -153,11 +151,11 @@ The reference scan covers full/extensionless/relative file strings plus the reti | live: AGENTS.md contract text | 4 | | live: CI routing | 1 | | live: Python script | 2 | -| live: computed import | 2 | +| live: computed import | 4 | | live: docs code-span | 21 | | live: docs link | 26 | -| live: generator/producer literal | 96 | -| live: source import | 236 | +| live: generator/producer literal | 106 | +| live: source import | 256 | | live: tsconfig/eslint/package.json | 20 | | live: workflow | 3 | | plan: docs code-span | 1 | @@ -239,9 +237,9 @@ node .github/scripts/architecture/site-architecture.mts --accept node .github/scripts/architecture/site-architecture.mts --references --old site/test/source-link.test.mts ``` -## Owner decisions +## Owner decisions and remaining ownership -- Approve recommended option 3, or select a documented loader alternative and its different proofs. -- Approve page-boot helpers in startup and the final layer ownership. -- Sign off by November 2, or explicitly extend draftUntil; name the real-iPad startup-proof owner. -- Coordinate alowpoly's S3/S4 timing with performance branches touching scene-router, startup-boot and ObjectLayout. +- Option 3 and the final folder ownership, including page-boot helpers in startup, were decided by the owner. +- There is no deadline. The owner sets S4/S5 timing and changes planned to enforced when S4 ends. +- Branches touching scene-router, startup-boot and ObjectLayout are timed together so they do not collide. +- The real-iPad startup-proof device owner remains to be assigned. diff --git a/docs/site-architecture/edits.json b/docs/site-architecture/edits.json index 780823b7dcb..ce6c53fb0e1 100644 --- a/docs/site-architecture/edits.json +++ b/docs/site-architecture/edits.json @@ -254,7 +254,7 @@ "kind": "lazy" } ], - "note": "Own queued registry and packaged-runtime functions in scene; share browser queuedImport and its single last chain." + "note": "Decided by the owner: loader modules hold only import() calls through the existing queue. Own registry and packaged-runtime functions in scene; move the single last chain to lower browser/import-queue. Prove one queue chain, registration before the first scene call, the same entry-chunk modules and bytes, identical L1/L2/L3, L7 no worse and the real-iPad startup journey." }, { "id": "add-file-world-imports", diff --git a/docs/site-architecture/loader-options.json b/docs/site-architecture/loader-options.json deleted file mode 100644 index 9d264d23c1e..00000000000 --- a/docs/site-architecture/loader-options.json +++ /dev/null @@ -1,349 +0,0 @@ -{ - "recommended": "owner-local-queued-functions", - "options": [ - { - "id": "owner-local-queued-functions", - "edits": "edits.json", - "autostart": "module evaluation", - "registration": "directory runtime only", - "proofs": [ - "one queue chain", - "registration before first scene call", - "entry modules and bytes", - "real-iPad delayed-summary cold loads" - ] - }, - { - "id": "directory-slot", - "autostart": "module evaluation", - "registration": "all loaders before router import", - "verdict": "viable graph alternative; wider startup registration and consumer proxies", - "proofs": [ - "slot-before-import mutation", - "use-time reads", - "entry modules and bytes" - ], - "graphEdits": [ - { - "op": "add-file", - "path": "site/model/navigation-href.mts", - "imports": [], - "change": "Extract held address reader", - "note": "Move navigationHref AND its owners WeakMap. Export a registration function for history; preserve deferred URL semantics.", - "id": "add-file-navigation-href" - }, - { - "op": "add-import", - "from": "site/navigation/navigation-history.mts", - "to": "site/model/navigation-href.mts", - "kind": "value", - "change": "Extract held address reader", - "note": "History registers and unregisters its held address reader in the same WeakMap; re-export the getter for existing navigation callers.", - "id": "add-import-navigation-history-navigation-href" - }, - { - "op": "retarget", - "from": "site/world/application-world-resources.mts", - "to": "site/navigation/navigation-history.mts", - "newTo": "site/model/navigation-href.mts", - "kind": "value", - "change": "Extract held address reader", - "note": "Only lower world caller uses navigationHref; other callers retain history behavior.", - "id": "retarget-application-world-resources-navigation-href", - "line": 18 - }, - { - "op": "add-file", - "path": "site/directory/object-entry-types.mts", - "note": "Extract registry aliases from objects; directory must not import the registry that calls it.", - "change": "Extract registry types", - "imports": [ - { - "to": "site/browser/browser-types.mts", - "kind": "type" - }, - { - "to": "packages/objects/src/index.ts", - "kind": "type" - } - ], - "id": "add-file-object-entry-types" - }, - { - "op": "retarget", - "from": "site/directory/object-directory.mts", - "to": "site/directory/objects.mts", - "note": "Use the extracted registry aliases without evaluating the full registry.", - "kind": "type", - "newTo": "site/directory/object-entry-types.mts", - "change": "Extract registry types", - "id": "retarget-object-directory-object-entry-types", - "line": 3 - }, - { - "op": "add-import", - "to": "site/directory/object-entry-types.mts", - "kind": "type", - "note": "Registry imports and re-exports its extracted aliases so clients can migrate without duplication.", - "from": "site/directory/objects.mts", - "change": "Extract registry types", - "id": "add-import-objects-object-entry-types" - }, - { - "op": "add-file", - "path": "site/navigation/navigation-types.mts", - "imports": [ - { - "to": "site/browser/browser-types.mts", - "kind": "type" - }, - { - "to": "packages/renderer/src/runtime/world-navigation-types.ts", - "kind": "type" - }, - { - "to": "packages/renderer/src/runtime/object-scene.ts", - "kind": "type" - }, - { - "to": "site/world/scene-subject.mts", - "kind": "type" - }, - { - "to": "site/world/navigation-scope.mts", - "kind": "type" - } - ], - "change": "Extract navigation contracts", - "note": "Own WorldHandoff, NavigationHistory and NavigationIntent beside navigation. Import MountOptions, ObjectSceneLifecycle, ObjectWorldNavigation, SceneSubject and PageView; preserve erased aliases SceneView and SelectionTarget.", - "id": "add-file-navigation-types" - }, - { - "op": "retarget", - "from": "site/navigation/prepared-arrival.mts", - "to": "site/navigation/prepared-world-navigation.mts", - "note": "Arrival reads WorldHandoff from the lower contract; navigation still owns flights.", - "kind": "type", - "newTo": "site/navigation/navigation-types.mts", - "change": "Extract navigation contracts", - "id": "retarget-prepared-arrival-navigation-types", - "line": 4 - }, - { - "op": "retarget", - "from": "site/navigation/navigation-history.mts", - "to": "site/navigation/navigation-request.mts", - "newTo": "site/navigation/navigation-types.mts", - "kind": "type", - "note": "Extract NavigationHistory and NavigationIntent into navigation-types; the history transport must not import request orchestration.", - "change": "Extract navigation contracts", - "id": "retarget-navigation-history-navigation-types", - "line": 4 - }, - { - "op": "add-import", - "to": "site/navigation/navigation-types.mts", - "kind": "type", - "note": "Flights import and re-export the extracted WorldHandoff contract.", - "from": "site/navigation/prepared-world-navigation.mts", - "change": "Extract navigation contracts", - "id": "add-import-prepared-world-navigation-navigation-types" - }, - { - "op": "add-import", - "to": "site/navigation/navigation-types.mts", - "kind": "type", - "note": "Request imports its extracted intent and history types and the plain selection shape; selection behavior is passed in.", - "from": "site/navigation/navigation-request.mts", - "change": "Extract navigation contracts", - "id": "add-import-navigation-request-navigation-types" - }, - { - "op": "add-file", - "path": "site/content/moon-catalogue.mts", - "note": "Split catalogue parsing, cached source catalogue and catalogueMoons into a runtime-safe reader; preparation keeps search-based ordering.", - "change": "Split the moon catalogue reader", - "imports": [ - { - "to": "site/source/moon-catalogues.json", - "kind": "asset" - }, - { - "to": "packages/objects/src/sources/index.ts", - "kind": "value" - } - ], - "id": "add-file-moon-catalogue" - }, - { - "op": "retarget", - "from": "site/content/object-children.mts", - "to": "site/build/prepare/prepare-body-moons.mts", - "note": "Read catalogueMoons from the split reader; production must never import build preparation.", - "newTo": "site/content/moon-catalogue.mts", - "change": "Split the moon catalogue reader", - "id": "retarget-object-children-moon-catalogue", - "line": 3 - }, - { - "op": "remove-import", - "from": "site/build/prepare/prepare-body-moons.mts", - "to": "site/source/moon-catalogues.json", - "note": "Move the source catalogue import with its parser into the reader; preparation calls that reader.", - "kind": "asset", - "change": "Split the moon catalogue reader", - "id": "remove-import-prepare-body-moons-moon-catalogues", - "line": 4 - }, - { - "op": "add-import", - "from": "site/build/prepare/prepare-body-moons.mts", - "to": "site/content/moon-catalogue.mts", - "kind": "value", - "note": "Use the single catalogue reader for preparation too; preserve the named-moon eligibility policy in preparation.", - "change": "Split the moon catalogue reader", - "id": "add-import-prepare-body-moons-moon-catalogue" - }, - { - "op": "remove-import", - "from": "site/navigation/navigation-request.mts", - "to": "site/selection/scene-selection.mts", - "note": "Pass selectionTargetFromUrl explicitly through resolveNavigation and readNavigationSelection. Router uses the existing registry export, injects it into createSceneActivation, which forwards all three calls. Import erased SceneView/SelectionTarget aliases from navigation-types; never retain the selection import.", - "change": "Pass selection into navigation requests", - "id": "remove-import-navigation-request-scene-selection", - "line": 11 - }, - { - "op": "add-import", - "to": "site/world/scene-subject.mts", - "kind": "value", - "note": "Keep existing subject identity helpers in the lower world owner; pass selectionTargetFromUrl as a parameter.", - "from": "site/navigation/navigation-request.mts", - "change": "Pass selection into navigation requests", - "id": "add-import-navigation-request-scene-subject" - }, - { - "op": "add-file", - "path": "site/scene/scene-frame-presenter.mts", - "note": "Extract the structural presenter interface; a session must not import the world coordinator that consumes it.", - "change": "Extract the frame presenter contract", - "imports": [ - { - "to": "site/world/application-world-context.mts", - "kind": "type" - }, - { - "to": "site/world/world-viewport.mts", - "kind": "type" - } - ], - "id": "add-file-scene-frame-presenter" - }, - { - "op": "retarget", - "from": "site/scene/scene-session.mts", - "to": "site/scene/scene-world.mts", - "note": "Session reads SceneFramePresenter from the lower contract, breaking the two-way type dependency.", - "kind": "type", - "newTo": "site/scene/scene-frame-presenter.mts", - "change": "Extract the frame presenter contract", - "id": "retarget-scene-session-scene-frame-presenter", - "line": 2 - }, - { - "op": "add-import", - "to": "site/scene/scene-frame-presenter.mts", - "kind": "type", - "note": "World coordinator imports and re-exports the structural presenter contract.", - "from": "site/scene/scene-world.mts", - "change": "Extract the frame presenter contract", - "id": "add-import-scene-world-scene-frame-presenter" - }, - { - "op": "add-file", - "path": "site/directory/runtime-loaders.mts", - "imports": [ - { - "to": "site/browser/browser-types.mts", - "kind": "type" - } - ] - }, - { - "op": "remove-import", - "from": "site/directory/object-directory.mts", - "to": "site/browser/import-queue.mts" - }, - { - "op": "add-import", - "from": "site/directory/object-directory.mts", - "to": "site/directory/runtime-loaders.mts", - "kind": "value" - }, - { - "op": "remove-import", - "from": "site/scene/object-adapter.mts", - "to": "site/browser/import-queue.mts" - }, - { - "op": "add-import", - "from": "site/scene/object-adapter.mts", - "to": "site/directory/runtime-loaders.mts", - "kind": "value" - }, - { - "op": "remove-import", - "from": "site/scene/scene-router.mts", - "to": "site/startup/shared-imports.mts" - }, - { - "op": "add-import", - "from": "site/scene/scene-router.mts", - "to": "site/directory/runtime-loaders.mts", - "kind": "value" - }, - { - "op": "remove-import", - "from": "site/browser/import-queue.mts", - "to": "site/scene/packaged-object-runtime.mts" - }, - { - "op": "add-import", - "from": "site/startup/shared-imports.mts", - "to": "site/scene/packaged-object-runtime.mts", - "kind": "lazy" - }, - { - "op": "add-import", - "from": "site/startup/shared-imports.mts", - "to": "site/directory/runtime-loaders.mts", - "kind": "value" - } - ] - }, - { - "id": "then-start-di", - "autostart": "after import evaluation inside queued thunk", - "risks": [ - "double start on retry", - "self-wait deadlock", - "adapter frozen default replaced" - ], - "proofs": [ - "synchronous start settlement", - "no queued await in start", - "retry single start", - "entry modules and bytes" - ] - }, - { - "id": "lazy-boundary-exception", - "autostart": "module evaluation", - "verdict": "abandons zero-cycle acceptance", - "proofs": [ - "explicit upward exceptions", - "report service SCCs separately" - ] - } - ] -} diff --git a/docs/site-architecture/moves.json b/docs/site-architecture/moves.json index cd576e840f6..7ac450013a6 100644 --- a/docs/site-architecture/moves.json +++ b/docs/site-architecture/moves.json @@ -287,5 +287,24 @@ "site/wordmark.css": "site/layouts/wordmark.css", "site/object-navigation-marker.css": "site/layouts/object-navigation-marker.css", "site/scientific-chart-order.mts": null, - "site/test/scientific-chart-order.test.mts": null + "site/test/scientific-chart-order.test.mts": null, + "site/asset-origin.characterization.test.mts": "site/server-assets/asset-origin.characterization.test.mts", + "site/dataset-picker.characterization.test.mts": "site/shell/dataset-picker.characterization.test.mts", + "site/dataset-response.characterization.test.mts": "site/server/dataset-response.characterization.test.mts", + "site/destination-browser.characterization.test.mts": "site/shell/destination-browser.characterization.test.mts", + "site/diagnostic-recorder.characterization.test.mts": "site/shell/diagnostic-recorder.characterization.test.mts", + "site/feature-browser.characterization.test.mts": "site/shell/feature-browser.characterization.test.mts", + "site/object-adapter.characterization.test.mts": "site/scene/object-adapter.characterization.test.mts", + "site/object-browser.characterization.test.mts": "site/shell/object-browser.characterization.test.mts", + "site/object-children.characterization.test.mts": "site/content/object-children.characterization.test.mts", + "site/object-directory.characterization.test.mts": "site/directory/object-directory.characterization.test.mts", + "site/object-page-data.characterization.test.mts": "site/server/object-page-data.characterization.test.mts", + "site/packaged-object-runtime.characterization.test.mts": "site/scene/packaged-object-runtime.characterization.test.mts", + "site/prepare-body-moons.characterization.test.mts": "site/build/prepare/prepare-body-moons.characterization.test.mts", + "site/startup-billboard.characterization.test.mts": "site/scene/startup-billboard.characterization.test.mts", + "site/startup-boot.characterization.test.mts": "site/startup/startup-boot.characterization.test.mts", + "site/startup-requests.characterization.test.mts": "site/directory/startup-requests.characterization.test.mts", + "site/startup-world.characterization.test.mts": "site/directory/startup-world.characterization.test.mts", + "site/view-url-runtime.characterization.test.mts": "site/navigation/view-url-runtime.characterization.test.mts", + "site/test/fixtures/characterization-runtime.mts": "site/scene/fixtures/characterization-runtime.mts" } diff --git a/docs/site-architecture/references.json b/docs/site-architecture/references.json index 7a4b6beed0c..7fd581e74c2 100644 --- a/docs/site-architecture/references.json +++ b/docs/site-architecture/references.json @@ -10,6 +10,16 @@ "classification": "CI routing", "scope": "live" }, + { + "file": ".github/coverage-ratchet.json", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": ".github/coverage-ratchet.json", + "classification": "source import", + "scope": "live" + }, { "file": ".github/scripts/architecture/architecture.test.mts", "classification": "generator/producer literal", @@ -60,6 +70,21 @@ "classification": "Python script", "scope": "live" }, + { + "file": ".github/scripts/build-compare/apply-moves.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": ".github/scripts/build-compare/apply-moves.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": ".github/scripts/build-compare/compare.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, { "file": ".github/scripts/checks/object-test-relocation.test.mts", "classification": "generator/producer literal", @@ -100,6 +125,16 @@ "classification": "generator/producer literal", "scope": "live" }, + { + "file": ".github/scripts/coverage/ratchet.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": ".github/scripts/coverage/report.mts", + "classification": "generator/producer literal", + "scope": "live" + }, { "file": ".github/scripts/tsconfig.tests.json", "classification": "tsconfig/eslint/package.json", @@ -565,6 +600,11 @@ "classification": "source import", "scope": "live" }, + { + "file": "site/asset-origin.characterization.test.mts", + "classification": "source import", + "scope": "live" + }, { "file": "site/build/bundle-cloudflare-worker.mts", "classification": "generator/producer literal", @@ -830,16 +870,36 @@ "classification": "source import", "scope": "live" }, + { + "file": "site/dataset-picker.characterization.test.mts", + "classification": "source import", + "scope": "live" + }, { "file": "site/dataset-picker.mts", "classification": "source import", "scope": "live" }, + { + "file": "site/dataset-response.characterization.test.mts", + "classification": "source import", + "scope": "live" + }, { "file": "site/dataset-response.mts", "classification": "source import", "scope": "live" }, + { + "file": "site/destination-browser.characterization.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/diagnostic-recorder.characterization.test.mts", + "classification": "source import", + "scope": "live" + }, { "file": "site/dot-catalogue-data.mts", "classification": "source import", @@ -860,6 +920,11 @@ "classification": "source import", "scope": "live" }, + { + "file": "site/feature-browser.characterization.test.mts", + "classification": "source import", + "scope": "live" + }, { "file": "site/first-view-transport.mts", "classification": "source import", @@ -930,21 +995,46 @@ "classification": "source import", "scope": "live" }, + { + "file": "site/object-adapter.characterization.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/object-adapter.characterization.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, { "file": "site/object-adapter.mts", "classification": "source import", "scope": "live" }, + { + "file": "site/object-browser.characterization.test.mts", + "classification": "source import", + "scope": "live" + }, { "file": "site/object-browser.mts", "classification": "source import", "scope": "live" }, + { + "file": "site/object-children.characterization.test.mts", + "classification": "source import", + "scope": "live" + }, { "file": "site/object-children.mts", "classification": "source import", "scope": "live" }, + { + "file": "site/object-directory.characterization.test.mts", + "classification": "source import", + "scope": "live" + }, { "file": "site/object-directory.mts", "classification": "source import", @@ -965,6 +1055,11 @@ "classification": "generator/producer literal", "scope": "live" }, + { + "file": "site/object-page-data.characterization.test.mts", + "classification": "source import", + "scope": "live" + }, { "file": "site/object-page-data.mts", "classification": "source import", @@ -995,6 +1090,21 @@ "classification": "source import", "scope": "live" }, + { + "file": "site/packaged-object-runtime.characterization.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/packaged-object-runtime.characterization.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/packaged-object-runtime.characterization.test.mts", + "classification": "computed import", + "scope": "live" + }, { "file": "site/packaged-object-runtime.mts", "classification": "source import", @@ -1130,6 +1240,11 @@ "classification": "generator/producer literal", "scope": "live" }, + { + "file": "site/prepare-body-moons.characterization.test.mts", + "classification": "source import", + "scope": "live" + }, { "file": "site/prepare-body-moons.mts", "classification": "source import", @@ -1355,16 +1470,56 @@ "classification": "source import", "scope": "live" }, + { + "file": "site/startup-billboard.characterization.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/startup-billboard.characterization.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, { "file": "site/startup-billboard.mts", "classification": "source import", "scope": "live" }, + { + "file": "site/startup-boot.characterization.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/startup-boot.characterization.test.mts", + "classification": "source import", + "scope": "live" + }, { "file": "site/startup-boot.mts", "classification": "source import", "scope": "live" }, + { + "file": "site/startup-requests.characterization.test.mts", + "classification": "source import", + "scope": "live" + }, + { + "file": "site/startup-requests.characterization.test.mts", + "classification": "computed import", + "scope": "live" + }, + { + "file": "site/startup-world.characterization.test.mts", + "classification": "generator/producer literal", + "scope": "live" + }, + { + "file": "site/startup-world.characterization.test.mts", + "classification": "source import", + "scope": "live" + }, { "file": "site/startup-world.mts", "classification": "source import", @@ -1885,6 +2040,11 @@ "classification": "source import", "scope": "live" }, + { + "file": "site/view-url-runtime.characterization.test.mts", + "classification": "source import", + "scope": "live" + }, { "file": "site/view-url-runtime.mts", "classification": "source import", diff --git a/docs/site-architecture/tiers.json b/docs/site-architecture/tiers.json index 2014278212c..d8636782844 100644 --- a/docs/site-architecture/tiers.json +++ b/docs/site-architecture/tiers.json @@ -188,9 +188,26 @@ "lateral": [], "tests": "any-tier", "testFiles": [ + "site/asset-origin.characterization.test.mts", "site/build/source-maps.test.mts", + "site/dataset-picker.characterization.test.mts", + "site/dataset-response.characterization.test.mts", + "site/destination-browser.characterization.test.mts", + "site/diagnostic-recorder.characterization.test.mts", + "site/feature-browser.characterization.test.mts", "site/navigation/evidence/star-click/one-click.jpg", + "site/object-adapter.characterization.test.mts", + "site/object-browser.characterization.test.mts", + "site/object-children.characterization.test.mts", + "site/object-directory.characterization.test.mts", + "site/object-page-data.characterization.test.mts", + "site/packaged-object-runtime.characterization.test.mts", + "site/prepare-body-moons.characterization.test.mts", "site/prepared-panel-content.shared-readers.test.mts", + "site/startup-billboard.characterization.test.mts", + "site/startup-boot.characterization.test.mts", + "site/startup-requests.characterization.test.mts", + "site/startup-world.characterization.test.mts", "site/test/application-world-frames.test.mts", "site/test/application-world-visibility.test.mts", "site/test/arrival-billboard.test.mts", @@ -224,6 +241,7 @@ "site/test/evidence/passive-minimaps/jupiter-sequence.png", "site/test/first-view-transport.test.mts", "site/test/fixtures/anchor-table.mts", + "site/test/fixtures/characterization-runtime.mts", "site/test/fixtures/context-package.mts", "site/test/fixtures/galaxies/captures.json", "site/test/fixtures/object-content-fixture.mts", @@ -336,7 +354,8 @@ "site/test/world-billboards.test.mts", "site/test/world-stars.test.mts", "site/test/world-tables.test.mts", - "site/test/zoom-scope.test.mts" + "site/test/zoom-scope.test.mts", + "site/view-url-runtime.characterization.test.mts" ], "assets": [ "site/layouts/cubic-sky.css", @@ -429,7 +448,7 @@ "site/vendor/cesium-LICENSE.md", "site/wordmark.css" ], - "status": "draft", + "status": "planned", "forbid": [ { "from": [ @@ -496,7 +515,5 @@ "site/pages" ] } - ], - "draftUntil": "2026-11-02", - "warningCeiling": 1 + ] }