Story navigation and details
Source-grounded
Thirteen private @topo/* runtime modules bundled by @jdylanmc/topo-code. Arrows are selected direct dependencies from package manifests, arranged as a readable composition spine, not a complete edge drawing or runtime call graph. Select a module for every declared incoming/outgoing internal dependency and its responsibility. Build-only eslint-config is excluded from this view; tooling and external context live in the companion map.
-
@topo/story
Open Source-anchor resolution and freshnessOpen Dependency context by role: Code analysisOpen Dependency context by role: Language parsingOpen Dependency context by role: Schema contractsOpen Dependency context by role: JSON validationOpen Dependency context by role: Build, test, docsOpen Dependency context by role: Development toolsOpen @topo/diagram-core: public surface: @topo/story
Parses authored stories and resolves file/symbol/excerpt anchors against a source snapshot. Defines the renderer-neutral story contract. All declared internal dependencies (not runtime calls): @topo/story -> @topo/languages [dependencies: workspace:*] Referenced by internal modules: @topo/cli -> @topo/story [dependencies: workspace:*] @topo/diagram-core -> @topo/story [dependencies: workspace:*] Stable drilldown identity: story. Open the source-backed sequence for this module's core algorithm.
packages/story/package.json:1-33 { "name": "@topo/story", "version": "0.0.0", "private": true, "type": "module", "files": [ "dist", "story.schema.json" ], "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }, "./story.schema.json": "./story.schema.json" }, "scripts": { "build": "tsc -b", "typecheck": "tsc -b --pretty false", "test": "vitest run" }, "dependencies": { "@topo/languages": "workspace:*", "typescript-compiler-api": "npm:typescript@6.0.3" }, "devDependencies": { "@types/node": "^22", "ajv": "8.17.1", "typescript": "^7.0.2", "vite": "8.2.2", "vitest": "^5.0.0" } }packages/story/src/index.ts:1-604 import { constants } from "node:fs"; import { open, realpath, stat } from "node:fs/promises"; import { isAbsolute, relative, resolve, sep } from "node:path"; import ts from "typescript-compiler-api"; import { createHash } from "node:crypto"; import { resolveRustSymbol } from "@topo/languages"; export interface SourceAnchor { readonly id: string; readonly path: string; readonly symbol?: string; readonly pattern?: string; readonly language?: "typescript" | "javascript" | "rust" | "text"; readonly sha256?: string; } export interface StoryTarget { readonly storyId: string; readonly nodeId?: string; } export interface StorySection { readonly id: string; readonly title: string; readonly body: string; readonly summary?: string; readonly semanticRole?: string; readonly drilldown?: StoryTarget; readonly anchorIds: readonly string[]; readonly kind?: "step" | "decision" | "data"; } export interface StoryConnection { readonly from: string; readonly to: string; readonly label?: string; readonly variant?: "return"; readonly classification?: "source-traced" | "inferred"; readonly anchorIds?: readonly string[]; readonly rationale?: string; } export type DiagramFamily = | "architecture" | "workflow" | "sequence" | "dataflow" | "lifecycle"; export type StoryClassification = "source-grounded" | "capability-demo"; export interface StoryDocument { readonly schemaVersion: "1.0"; readonly diagramFamily?: DiagramFamily; readonly classification?: StoryClassification; readonly renderer?: "archify" | "graphviz"; readonly id: string; readonly title: string; readonly summary: string; readonly category?: string; readonly parent?: StoryTarget & { readonly nodeId: string }; readonly sequenceLayout?: { readonly minimumParticipantGap: number }; readonly anchors: readonly SourceAnchor[]; readonly sections: readonly StorySection[]; readonly connections: readonly StoryConnection[]; } export interface ResolvedSourceAnchor extends SourceAnchor { readonly location: { readonly startLine: number; readonly endLine: number; }; readonly excerpt: string; } export interface ResolvedStoryDocument { readonly document: StoryDocument; readonly documentPath: string; readonly repositoryRoot: string; readonly source: { readonly revision: string; readonly dirty: boolean; }; readonly anchors: readonly ResolvedSourceAnchor[]; } export interface StoryArtifact { readonly kind: "html"; readonly mediaType: "text/html"; readonly contents: string; readonly assets?: Readonly<Record<string, string>>; readonly renderer: { readonly name: string; readonly pin: string; readonly sha256?: string; readonly sourceOutputSha256?: string; readonly outputSha256?: string; readonly adaptation?: string; }; } export interface StoryRenderer { render(story: ResolvedStoryDocument): StoryArtifact | Promise<StoryArtifact>; } export class StoryDocumentError extends Error { constructor( readonly documentPath: string, readonly anchorId: string | undefined, readonly code: string, message: string, ) { super( `${documentPath}${anchorId ? ` anchor "${anchorId}"` : ""} [${code}]: ${message}`, ); this.name = "StoryDocumentError"; } } export function parseStoryDocument( serialized: string, documentPath: string, ): StoryDocument { let value: unknown; try { value = JSON.parse(serialized) as unknown; } catch { throw invalidDocument(documentPath, "invalid JSON"); } assertStoryDocument(value, documentPath); return value; } export function assertStoryDocument( value: unknown, documentPath = "<story>", ): asserts value is StoryDocument { const issue = validateStoryDocument(value); if (issue !== undefined) { throw invalidDocument(documentPath, issue); } } export async function resolveStoryDocument( root: string, document: StoryDocument, documentPath: string, source: { revision: string; dirty: boolean }, loadSource?: (path: string) => Promise<string | undefined>, ): Promise<ResolvedStoryDocument> { const repositoryRoot = loadSource === undefined ? await realpath(root) : resolve(root); const anchors: ResolvedSourceAnchor[] = []; for (const anchor of document.anchors) { assertRepositoryPath(root, documentPath, anchor); const contents = loadSource === undefined ? await readRepositorySource(repositoryRoot, documentPath, anchor) : await loadSource(anchor.path); if (contents === undefined) { throw anchorError( documentPath, anchor.id, "missing-file", `source file "${anchor.path}" does not exist`, ); } anchors.push(await resolveAnchor(documentPath, anchor, contents)); } return { document, documentPath, repositoryRoot, source, anchors }; } function invalidDocument(documentPath: string, message: string): StoryDocumentError { return new StoryDocumentError( documentPath, undefined, "invalid-document", `invalid story document: ${message}`, ); } function anchorError( documentPath: string, anchorId: string, code: string, message: string, ): StoryDocumentError { return new StoryDocumentError(documentPath, anchorId, code, message); } function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value); } function exactKeys( value: Record<string, unknown>, required: readonly string[], optional: readonly string[] = [], ): string | undefined { for (const key of required) { if (!(key in value)) return `missing "${key}"`; } const allowed = new Set([...required, ...optional]); const extra = Object.keys(value).find((key) => !allowed.has(key)); return extra === undefined ? undefined : `unexpected "${extra}"`; } function nonemptyString(value: unknown): value is string { return typeof value === "string" && value.length > 0; } function uniqueStrings(value: unknown): value is string[] { return Array.isArray(value) && value.every(nonemptyString) && new Set(value).size === value.length; } function validateStoryTarget(value: unknown, requireNode: boolean): string | undefined { if (!isRecord(value)) return "must be an object"; const keys = exactKeys(value, requireNode ? ["storyId", "nodeId"] : ["storyId"], requireNode ? [] : ["nodeId"]); if (keys) return keys; if (typeof value.storyId !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(value.storyId)) { return "storyId must be a valid story id"; } if (value.nodeId !== undefined && !nonemptyString(value.nodeId)) return "nodeId must be nonempty"; return undefined; } function validateStoryDocument(value: unknown): string | undefined { if (!isRecord(value)) return "root must be an object"; const rootKeys = exactKeys( value, ["schemaVersion", "id", "title", "summary", "anchors", "sections", "connections"], ["category", "diagramFamily", "classification", "renderer", "parent", "sequenceLayout"], ); if (rootKeys) return rootKeys; if (value.schemaVersion !== "1.0") return 'schemaVersion must be "1.0"'; if (value.renderer !== undefined && value.renderer !== "archify" && value.renderer !== "graphviz") return "renderer must be archify or graphviz"; if (value.renderer === "graphviz" && value.diagramFamily !== "workflow") return "graphviz requires diagramFamily workflow"; if ( value.diagramFamily !== undefined && !["architecture", "workflow", "sequence", "dataflow", "lifecycle"] .includes(String(value.diagramFamily)) ) { return "diagramFamily must be a supported native family"; } if ( value.classification !== undefined && value.classification !== "source-grounded" && value.classification !== "capability-demo" ) { return "classification must be source-grounded or capability-demo"; } if (!nonemptyString(value.id) || !/^[a-z0-9][a-z0-9-]*$/.test(value.id)) { return "id must use lowercase letters, digits, and hyphens"; } if (!nonemptyString(value.title)) return "title must be nonempty"; if (!nonemptyString(value.summary)) return "summary must be nonempty"; if (value.sequenceLayout !== undefined) { if (value.diagramFamily !== "sequence" || !isRecord(value.sequenceLayout)) { return "sequenceLayout requires a sequence diagram and an object"; } const keys = exactKeys(value.sequenceLayout, ["minimumParticipantGap"], []); if (keys) return `sequenceLayout ${keys}`; const gap = value.sequenceLayout.minimumParticipantGap; if (typeof gap !== "number" || !Number.isSafeInteger(gap) || gap < 108) { return "sequenceLayout.minimumParticipantGap must be an integer of at least 108 SVG units"; } } if (value.parent !== undefined) { const error = validateStoryTarget(value.parent, true); if (error) return `parent ${error}`; } if ( value.category !== undefined && (!nonemptyString(value.category) || value.category.trim().length === 0) ) { return "category must be nonempty"; } if (!Array.isArray(value.anchors)) return "anchors must be an array"; if (!Array.isArray(value.sections) || value.sections.length === 0) { return "sections must be a nonempty array"; } if (!Array.isArray(value.connections)) return "connections must be an array"; const diagramFamily = value.diagramFamily ?? "architecture"; const sourceGrounded = value.classification !== "capability-demo"; if (sourceGrounded && value.anchors.length === 0) { return "source-grounded stories must define at least one anchor"; } const anchorIds = new Set<string>(); for (const [index, anchorValue] of value.anchors.entries()) { if (!isRecord(anchorValue)) return `anchors[${index}] must be an object`; const keys = exactKeys(anchorValue, ["id", "path"], ["symbol", "pattern", "language", "sha256"]); if (keys) return `anchors[${index}] ${keys}`; if (!nonemptyString(anchorValue.id)) return `anchors[${index}].id must be nonempty`; if (anchorIds.has(anchorValue.id)) return `duplicate anchor id "${anchorValue.id}"`; anchorIds.add(anchorValue.id); if (!nonemptyString(anchorValue.path)) return `anchors[${index}].path must be nonempty`; if (anchorValue.language !== undefined && !["typescript", "javascript", "rust", "text"].includes(String(anchorValue.language))) return `anchors[${index}].language is unsupported`; if (anchorValue.sha256 !== undefined && (typeof anchorValue.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(anchorValue.sha256))) return `anchors[${index}].sha256 must be a SHA-256 file digest`; if (value.renderer === "graphviz" && anchorValue.sha256 === undefined) return `technical anchors[${index}] require sha256 for stale-source rejection`; if (anchorValue.symbol !== undefined && !nonemptyString(anchorValue.symbol)) { return `anchors[${index}].symbol must be nonempty`; } if (anchorValue.pattern !== undefined) { if (!nonemptyString(anchorValue.pattern)) { return `anchors[${index}].pattern must be nonempty`; } } } const sectionIds = new Set<string>(); for (const [index, sectionValue] of value.sections.entries()) { if (!isRecord(sectionValue)) return `sections[${index}] must be an object`; const keys = exactKeys(sectionValue, ["id", "title", "body", "anchorIds"], ["kind", "summary", "semanticRole", "drilldown"]); if (keys) return `sections[${index}] ${keys}`; if (sectionValue.kind !== undefined && (!["step", "decision", "data"].includes(String(sectionValue.kind)) || value.renderer !== "graphviz")) return `sections[${index}].kind requires the explicit graphviz backend`; if (!nonemptyString(sectionValue.id)) return `sections[${index}].id must be nonempty`; if (sectionIds.has(sectionValue.id)) return `duplicate section id "${sectionValue.id}"`; sectionIds.add(sectionValue.id); if (!nonemptyString(sectionValue.title)) return `sections[${index}].title must be nonempty`; if (!nonemptyString(sectionValue.body)) return `sections[${index}].body must be nonempty`; if (sectionValue.drilldown !== undefined) { const error = validateStoryTarget(sectionValue.drilldown, false); if (error) return `sections[${index}].drilldown ${error}`; } if (sectionValue.summary !== undefined && (!nonemptyString(sectionValue.summary) || diagramFamily !== "architecture")) { return `sections[${index}].summary requires a nonempty architecture caption`; } if (sectionValue.semanticRole !== undefined && (typeof sectionValue.semanticRole !== "string" || !/^(?!constructor$)[a-z][a-z0-9-]{0,63}$/.test(sectionValue.semanticRole) || (diagramFamily !== "architecture" && diagramFamily !== "sequence"))) { return `sections[${index}].semanticRole requires an architecture or sequence role slug`; } if (!uniqueStrings(sectionValue.anchorIds)) { return `sections[${index}].anchorIds must contain unique nonempty strings`; } if (sourceGrounded && sectionValue.anchorIds.length === 0) { return `sections[${index}].anchorIds must reference source evidence`; } const missingAnchor = sectionValue.anchorIds.find((id) => !anchorIds.has(id)); if (missingAnchor) return `sections[${index}] references unknown anchor "${missingAnchor}"`; } for (const [index, connectionValue] of value.connections.entries()) { if (!isRecord(connectionValue)) return `connections[${index}] must be an object`; const keys = exactKeys(connectionValue, ["from", "to"], ["label", "variant", "classification", "anchorIds", "rationale"]); if (keys) return `connections[${index}] ${keys}`; if (!nonemptyString(connectionValue.from) || !sectionIds.has(connectionValue.from)) { return `connections[${index}].from must reference a section`; } if (!nonemptyString(connectionValue.to) || !sectionIds.has(connectionValue.to)) { return `connections[${index}].to must reference a section`; } if (connectionValue.label !== undefined && !nonemptyString(connectionValue.label)) { return `connections[${index}].label must be nonempty`; } if (value.renderer === "graphviz" && connectionValue.classification === undefined) return `technical connections[${index}] require an explicit classification`; if (connectionValue.classification !== undefined) { if (!["source-traced", "inferred"].includes(String(connectionValue.classification))) return `connections[${index}].classification must be source-traced or inferred; authored traces are not scanner-derived facts`; if (!nonemptyString(connectionValue.rationale)) return `connections[${index}] require a rationale`; if (!uniqueStrings(connectionValue.anchorIds) || connectionValue.anchorIds.some((id) => !anchorIds.has(id))) return `connections[${index}] require valid anchorIds`; if (connectionValue.classification === "source-traced" && connectionValue.anchorIds.length === 0) return `source-traced connections[${index}] require exact source evidence`; for (const anchorId of connectionValue.anchorIds) { if (!value.anchors.some((anchor) => isRecord(anchor) && anchor.id === anchorId && typeof anchor.sha256 === "string")) return `connections[${index}] require snapshot-bound anchors`; } } else if (connectionValue.anchorIds !== undefined || connectionValue.rationale !== undefined) { return `connections[${index}] evidence requires classification`; } if ( connectionValue.variant !== undefined && connectionValue.variant !== "return" ) { return `connections[${index}].variant must be "return"`; } if ( connectionValue.variant === "return" && diagramFamily !== "sequence" ) { return `connections[${index}] return variant is only supported for sequence stories`; } if ( (diagramFamily === "sequence" || diagramFamily === "dataflow") && connectionValue.label === undefined ) { return `${diagramFamily} connections[${index}].label is required`; } } return undefined; } function assertRepositoryPath( root: string, documentPath: string, anchor: SourceAnchor, ): void { const absolute = resolve(root, anchor.path); const rel = relative(root, absolute); if ( isAbsolute(anchor.path) || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel) ) { throw anchorError( documentPath, anchor.id, "invalid-path", `source path "${anchor.path}" escapes the repository`, ); } } async function readRepositorySource( repositoryRoot: string, documentPath: string, anchor: SourceAnchor, ): Promise<string | undefined> { const requested = resolve(repositoryRoot, anchor.path); let actual: string; try { actual = await realpath(requested); } catch (error) { if ( error instanceof Error && "code" in error && error.code === "ENOENT" ) { return undefined; } throw error; } if (actual !== requested) { throw anchorError( documentPath, anchor.id, "invalid-path", `source path "${anchor.path}" must not resolve through a symlink`, ); } const rel = relative(repositoryRoot, actual); if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) { throw anchorError( documentPath, anchor.id, "invalid-path", `source path "${anchor.path}" must be a regular file inside the repository`, ); } const initial = await stat(requested); if (!initial.isFile()) { throw anchorError( documentPath, anchor.id, "invalid-path", `source path "${anchor.path}" must be a regular file inside the repository`, ); } const handle = await open( requested, constants.O_RDONLY | constants.O_NOFOLLOW, ); try { const opened = await handle.stat(); if ( !opened.isFile() || opened.dev !== initial.dev || opened.ino !== initial.ino ) { throw anchorError( documentPath, anchor.id, "invalid-path", `source path "${anchor.path}" changed before it was read`, ); } const contents = await handle.readFile("utf8"); const openedAfter = await handle.stat(); const currentActual = await realpath(requested); const current = await stat(requested); if ( currentActual !== requested || !current.isFile() || current.dev !== opened.dev || current.ino !== opened.ino || openedAfter.size !== opened.size || openedAfter.mtimeMs !== opened.mtimeMs || openedAfter.ctimeMs !== opened.ctimeMs ) { throw anchorError( documentPath, anchor.id, "invalid-path", `source path "${anchor.path}" changed while it was being read`, ); } return contents; } finally { await handle.close(); } } function declarationName(node: ts.Node): string | undefined { if (!("name" in node)) return undefined; const name = (node as ts.NamedDeclaration).name; return name && ts.isIdentifier(name) ? name.text : undefined; } function findSymbolRange( sourceFile: ts.SourceFile, symbol: string, ): { start: number; end: number } | undefined { let result: ts.Node | undefined; const visit = (node: ts.Node): void => { if (result !== undefined) return; if (declarationName(node) === symbol) { result = node; return; } ts.forEachChild(node, visit); }; visit(sourceFile); return result === undefined ? undefined : { start: result.getStart(sourceFile), end: result.getEnd(), }; } async function resolveAnchor( documentPath: string, anchor: SourceAnchor, contents: string, ): Promise<ResolvedSourceAnchor> { if (anchor.sha256 !== undefined && createHash("sha256").update(contents).digest("hex") !== anchor.sha256) { throw anchorError(documentPath, anchor.id, "stale-source", `source "${anchor.path}" changed; inspect the change and deliberately update its digest, preserving anchor identity`); } const sourceFile = ts.createSourceFile( anchor.path, contents, ts.ScriptTarget.Latest, true, ); let start = 0; let end = contents.length; if (anchor.symbol !== undefined) { const language = anchor.language ?? (anchor.path.endsWith(".rs") ? "rust" : "typescript"); if (language === "text") throw anchorError(documentPath, anchor.id, "unsupported-symbol", "text anchors support exact patterns, not compiler symbols"); const rust = language === "rust" ? await resolveRustSymbol(anchor.path, contents, anchor.symbol) : undefined; const symbol = rust === undefined ? findSymbolRange(sourceFile, anchor.symbol) : { start: sourceFile.getPositionOfLineAndCharacter(rust.startLine - 1, rust.startColumn - 1), end: sourceFile.getPositionOfLineAndCharacter(rust.endLine - 1, rust.endColumn - 1), }; if (symbol === undefined) { throw anchorError( documentPath, anchor.id, "missing-symbol", `symbol "${anchor.symbol}" was not found in "${anchor.path}"`, ); } start = symbol.start; end = symbol.end; } if (anchor.pattern !== undefined) { const scope = contents.slice(start, end); const offset = scope.indexOf(anchor.pattern); if (offset < 0) { throw anchorError( documentPath, anchor.id, "missing-pattern", anchor.symbol === undefined ? `pattern "${anchor.pattern}" was not found in "${anchor.path}"` : `pattern "${anchor.pattern}" was not found within symbol "${anchor.symbol}" in "${anchor.path}"`, ); } if ( anchor.symbol === undefined && scope.indexOf(anchor.pattern, offset + 1) >= 0 ) { throw anchorError( documentPath, anchor.id, "ambiguous-pattern", `pattern "${anchor.pattern}" occurs more than once in "${anchor.path}"`, ); } start += offset; end = start + anchor.pattern.length; } const startLine = sourceFile.getLineAndCharacterOfPosition(start).line + 1; const endLine = sourceFile.getLineAndCharacterOfPosition( Math.max(start, end - 1), ).line + 1; return { ...anchor, location: { startLine, endLine }, excerpt: contents.slice(start, end), }; } -
@topo/languages
Open Dependency context by role: Code analysisOpen Dependency context by role: Language parsingOpen Dependency context by role: Build, test, docsOpen Dependency context by role: Development tools
Selects the first-party TypeScript/JavaScript scanner, partial Rust adapter, and Tauri bridge. Owns captured source inventory and language evidence; never runs a model or Cargo build. All declared internal dependencies (not runtime calls): @topo/languages -> @topo/scanner [dependencies: workspace:*] @topo/languages -> @topo/schema [dependencies: workspace:*] Referenced by internal modules: @topo/story -> @topo/languages [dependencies: workspace:*] @topo/cli -> @topo/languages [dependencies: workspace:*] Stable drilldown identity: languages. Module-specific detail maps will be curated later; there is no placeholder target.
packages/languages/package.json:1-31 { "name": "@topo/languages", "version": "0.0.0", "private": true, "type": "module", "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } }, "scripts": { "build": "tsc -b", "typecheck": "tsc -b --pretty false", "test": "node --test test/*.test.mjs" }, "dependencies": { "@topo/scanner": "workspace:*", "@topo/schema": "workspace:*", "typescript-compiler-api": "npm:typescript@6.0.3" }, "optionalDependencies": { "@ast-grep/lang-rust": "0.0.7", "@ast-grep/napi": "0.45.3", "@iarna/toml": "2.2.5" }, "devDependencies": { "@types/node": "^22", "typescript": "^7.0.2" } }packages/languages/src/index.ts:1-306 import { basename } from "node:path"; import type { ScanRepositoryOptions, ScanResult } from "@topo/scanner"; import { assertGraphDocument, createGraphDocument, createPathNodeId, type GraphDocument, type JsonValue, type LogicalArchitectureDocument, type SourceLocation as GraphLocation, } from "@topo/schema"; import type { Contribution, Entity, Relationship, ScanContext, SourceLocation } from "./contract-types.js"; import { assertInventoryCurrent, captureInventory } from "./inventory.mjs"; import { factId, hash, serialize } from "./contract.mjs"; export type { Contribution, Entity, Relationship, ScanContext, SourceLocation } from "./contract-types.js"; export { RUST_DECLARATION_KINDS } from "./contract.mjs"; export const LANGUAGE_EVIDENCE_KEY = "dev.topo.languages"; export interface PluginSelection { languages: readonly ("typescript" | "rust")[]; frameworks: readonly "tauri"[]; } export interface PluginManifest { readonly id: string; readonly kind: "language" | "framework"; readonly contractVersion: "1.0"; readonly requires: readonly string[]; } export const FIRST_PARTY_PLUGINS: readonly PluginManifest[] = [ { id: "typescript", kind: "language", contractVersion: "1.0", requires: [] }, { id: "rust", kind: "language", contractVersion: "1.0", requires: [] }, { id: "tauri", kind: "framework", contractVersion: "1.0", requires: ["typescript", "rust"] }, ]; export interface LanguageEvidence { schemaVersion: "1.0"; source: { sha256: string; files: { path: string; sha256: string; language: string }[] }; contributions: Contribution[]; } export function validateSelection(selection: PluginSelection): void { if (!selection.languages.length) throw new Error("Select at least one first-party language"); const selected = [...selection.languages, ...selection.frameworks]; if (new Set(selected).size !== selected.length) throw new Error("Duplicate selected adapter"); for (const [kind, ids] of [["language", selection.languages], ["framework", selection.frameworks]] as const) { for (const id of ids) { const plugin = FIRST_PARTY_PLUGINS.find((candidate) => candidate.id === id && candidate.kind === kind); if (!plugin) throw new Error(`Unknown ${kind} adapter: ${id}`); for (const required of plugin.requires) { if (!selected.some((id) => id === required)) { throw new Error(`${id} requires explicitly selected ${required}`); } } } } } export async function rustContribution(context: ScanContext): Promise<Contribution> { let adapter: typeof import("./plugins/rust.mjs"); try { adapter = await import("./plugins/rust.mjs"); } catch (error) { throw new Error( "Rust adapter unavailable. Install optional @ast-grep/napi@0.45.3, @ast-grep/lang-rust@0.0.7 and @iarna/toml@2.2.5 with compatible native prebuilds. No compiler/install hook is run by topo; select only typescript to operate without Rust.", { cause: error }, ); } return adapter.scanRust(context); } function graphLocation(value: SourceLocation): GraphLocation { return { path: value.path, start: { line: value.startLine, column: value.startColumn }, end: { line: value.endLine, column: value.endColumn }, }; } function factLocation(value: GraphLocation): SourceLocation { return { path: value.path, startLine: value.start.line, startColumn: value.start.column, endLine: value.end?.line ?? value.start.line, endColumn: value.end?.column ?? value.start.column, }; } function json(value: unknown): JsonValue { if (value === null || typeof value === "string" || typeof value === "boolean") return value; if (typeof value === "number" && Number.isFinite(value)) return value; if (Array.isArray(value)) return value.map(json); if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype) { return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, json(child)])); } throw new Error("Adapter evidence must contain only finite JSON values"); } function typescriptContribution(result: ScanResult): Contribution { return { plugin: { id: "typescript", kind: "language", version: "1.0", languages: ["typescript", "javascript"], capabilities: ["compiler-declarations", "static-references"] }, entities: result.logicalArchitecture.entities.map((entity) => { const location = entity.declarations[0]; if (!location) throw new Error(`TypeScript declaration has no source: ${entity.id}`); return { id: entity.id, language: /\.[cm]?jsx?$/.test(location.path) ? "javascript" : "typescript", kind: entity.kind, name: entity.name, qualifiedName: `${location.path}::${entity.name}`, location: factLocation(location), exported: entity.exported, signatures: entity.signatures, attributes: [] }; }), relationships: result.logicalArchitecture.relationships.map((edge) => ({ id: edge.id, from: edge.sourceId, to: edge.targetId, kind: edge.kind, method: "typescript-compiler-static-reference", evidence: edge.locations.map(factLocation), })), unresolved: [], diagnostics: result.diagnostics, coverage: { status: result.authoritative ? "complete" : "partial", analyzedFiles: result.graph.nodes.filter((node) => node.identity.kind === "path").map((node) => node.identity.value), limitations: ["Static TypeScript/JavaScript references are not runtime reachability or exhaustive impact. Unresolved imports are diagnostics, not guessed edges."], }, extensions: {}, }; } function assertFacts(evidence: LanguageEvidence, files: ScanContext["files"]): void { const sources = new Map(files.map((file) => [file.path, file.contents])); const ids = new Set(files.map((file) => factId("core", "file", file.path, file.path))); function location(loc: SourceLocation) { const source = sources.get(loc.path); if (source === undefined) throw new Error(`Adapter evidence references uncaptured source: ${loc.path}`); const lines = source.split(/\r?\n/); if (![loc.startLine, loc.endLine, loc.startColumn, loc.endColumn].every((n) => Number.isSafeInteger(n) && n > 0) || loc.endLine < loc.startLine || loc.endLine > lines.length || loc.startColumn > lines[loc.startLine - 1]!.length + 1 || loc.endColumn > lines[loc.endLine - 1]!.length + 1 || loc.endLine === loc.startLine && loc.endColumn < loc.startColumn) { throw new Error(`Invalid adapter evidence range: ${loc.path}`); } } for (const contribution of evidence.contributions) { if (contribution.plugin.version !== "1.0") throw new Error("Unsupported adapter contract"); for (const entity of contribution.entities) { if (!entity.id || ids.has(entity.id)) throw new Error(`Duplicate adapter entity: ${entity.id}`); ids.add(entity.id); location(entity.location); } } const edges = new Set<string>(); for (const contribution of evidence.contributions) { for (const entity of contribution.entities) { if (entity.ownerId && !ids.has(entity.ownerId)) throw new Error(`Unknown owner: ${entity.ownerId}`); } for (const edge of contribution.relationships) { if (!edge.id || edges.has(edge.id) || !ids.has(edge.from) || !ids.has(edge.to) || !edge.evidence.length || !edge.method) { throw new Error(`Invalid adapter relationship: ${edge.id}`); } edges.add(edge.id); edge.evidence.forEach(location); } for (const unresolved of contribution.unresolved) { location(unresolved.location); if (!unresolved.reason) throw new Error("Unresolved adapter evidence requires a reason"); } } json(evidence); } /** Core-owned selection; no arbitrary loader, Cargo, compiler build or model execution. */ export async function scanProject( options: ScanRepositoryOptions, selection: PluginSelection = { languages: ["typescript"], frameworks: [] }, ): Promise<ScanResult> { validateSelection(selection); const { createTypeScriptScannerAdapter, ScanError } = await import("@topo/scanner"); if (selection.languages.length === 1 && selection.languages[0] === "typescript" && !selection.frameworks.length) { return createTypeScriptScannerAdapter().scan(options); } const captured = await captureInventory(options.root); const repositoryId = options.repositoryId ?? basename(options.root); const hasTypeScript = captured.files.some((file: { path: string }) => /\.(?:[cm]?[jt]s|[jt]sx)$/.test(file.path)); let result: ScanResult | undefined; const contributions: Contribution[] = []; if (selection.languages.includes("typescript") && hasTypeScript) { result = await createTypeScriptScannerAdapter().scan({ ...options, quality: { ...options.quality, allowPartial: true } }); contributions.push(typescriptContribution(result)); } else if (selection.languages.includes("typescript")) { contributions.push({ plugin: { id: "typescript", kind: "language", version: "1.0", languages: ["typescript", "javascript"], capabilities: ["compiler-declarations", "static-references"] }, entities: [], relationships: [], unresolved: [], diagnostics: [], extensions: {}, coverage: { status: "unsupported", analyzedFiles: [], limitations: ["No TypeScript/JavaScript source was captured for the selected adapter."] }, }); } if (selection.languages.includes("rust")) contributions.push(await rustContribution(captured)); if (selection.frameworks.includes("tauri")) { const { scanTauri } = await import("./plugins/tauri.mjs"); contributions.push(await scanTauri(captured, contributions)); } const languageEvidence: LanguageEvidence = { schemaVersion: "1.0", source: captured.snapshot, contributions }; assertFacts(languageEvidence, captured.files); const diagnostics = contributions.flatMap((contribution) => [ ...contribution.diagnostics, ...contribution.coverage.limitations.map((message) => ({ code: `${contribution.plugin.id}.coverage`, severity: "warning" as const, message, })), ]); const authoritative = contributions.every((item) => item.coverage.status === "complete"); if (!authoritative && !options.quality?.allowPartial) { throw new ScanError("Selected adapters produced partial evidence; use --allow-partial to publish it (exit 2).", diagnostics); } const graph = result?.graph ?? createGraphDocument({ graphId: `repo:${repositoryId}`, repository: { id: repositoryId, label: repositoryId, ...(options.revision === undefined ? {} : { revision: options.revision }) }, }); const knownPaths = new Set(graph.nodes.filter((node) => node.identity.kind === "path").map((node) => node.identity.value)); for (const file of captured.files) { if (file.language !== "rust" && !/(?:^|\/)Cargo\.toml$/.test(file.path)) continue; if (!knownPaths.has(file.path)) graph.nodes.push({ id: createPathNodeId(file.path), label: file.path, kind: "file", identity: { kind: "path", value: file.path }, fingerprint: `sha256:${file.sha256}`, }); } graph.nodes.sort((a, b) => a.id.localeCompare(b.id, "en")); for (const contribution of contributions.filter((item) => item.plugin.id !== "typescript")) { graph.modules.push({ id: `@topo/${contribution.plugin.id}`, version: "1.0.0", schemaVersion: "1.0" }); } graph.extensions[LANGUAGE_EVIDENCE_KEY] = json(languageEvidence); graph.extensions["dev.topo.scanner"] = json({ authoritative, status: authoritative ? "complete" : "partial", diagnostics }); assertGraphDocument(graph); await assertInventoryCurrent(captured); if (options.revision !== undefined && options.revision !== captured.repository.revision) throw new Error("Source revision changed during scan"); const logicalArchitecture: LogicalArchitectureDocument = result?.logicalArchitecture ?? { schemaVersion: "1.0" as const, graphId: graph.graphId, ...(options.revision === undefined ? {} : { revision: options.revision }), snapshotId: `sha256:${captured.snapshot.sha256}`, positionNamespaceId: `repo:${repositoryId}`, coverage: { languages: ["javascript", "typescript"], relationshipKinds: [], completeSourceInventory: false, runtimeBehavior: false }, entities: [], relationships: [], responsibilities: [], unassignedEntityIds: [], diagnostics: [], }; const metrics = result?.metrics ?? { configCount: 0, workspacePackageCount: 0, coveredWorkspacePackageCount: 0, sourceFileCount: 0, assetFileCount: 0, linesOfCode: 0, localImportCount: 0, assetImportCount: 0, externalImportCount: 0, unresolvedImportCount: 0, }; const rustPaths = new Set(contributions.find((item) => item.plugin.id === "rust")?.coverage.analyzedFiles ?? []); const rustFiles = captured.files.filter((file) => file.language === "rust" && rustPaths.has(file.path)); return { graph, logicalArchitecture, diagnostics, authoritative, metrics: { ...metrics, sourceFileCount: metrics.sourceFileCount + rustFiles.length, linesOfCode: metrics.linesOfCode + rustFiles .reduce((total, file) => total + file.contents.split(/\r?\n/).filter((line) => line.trim()).length, 0), } }; } export async function resolveRustSymbol(path: string, contents: string, symbol: string): Promise<SourceLocation> { const contribution = await rustContribution({ root: "", files: [{ path, contents, sha256: hash(contents) }] }); const candidates = contribution.entities.filter((entity) => entity.name === symbol || entity.qualifiedName === symbol); if (candidates.length !== 1) throw new Error(`Rust symbol "${symbol}" in "${path}" is ${candidates.length ? "ambiguous; use a lexical qualifiedName" : "missing"}`); if (contribution.unresolved.some((item) => item.kind === "syntax-error")) throw new Error(`Rust source "${path}" has parser recovery; use an exact pattern after repairing it`); return candidates[0]!.location; } export function languageEvidence(graph: GraphDocument): JsonValue | undefined { return graph.extensions[LANGUAGE_EVIDENCE_KEY]; } export function sourceFileFactId(path: string): string { return factId("core", "file", path, path); } /** Only the displayable contract crosses from a stored snapshot into viewers. */ export function readLanguageFacts(graph: GraphDocument): { entities: Entity[]; relationships: Relationship[] } { const value = graph.extensions[LANGUAGE_EVIDENCE_KEY]; if (value === undefined) return { entities: [], relationships: [] }; function object(value: unknown): Record<string, unknown> { if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("Malformed language evidence object"); return value as Record<string, unknown>; } function text(value: unknown): string { if (typeof value !== "string" || !value) throw new Error("Malformed language evidence string"); return value; } function list(value: unknown): unknown[] { if (!Array.isArray(value)) throw new Error("Malformed language evidence collection"); return value; } function location(value: unknown): SourceLocation { const item = object(value); function position(key: string): number { const number = item[key]; if (typeof number !== "number" || !Number.isSafeInteger(number) || number < 1) throw new Error("Malformed language evidence location"); return number; } return { path: text(item.path), startLine: position("startLine"), startColumn: position("startColumn"), endLine: position("endLine"), endColumn: position("endColumn") }; } const document = object(value); if (document.schemaVersion !== "1.0") throw new Error("Unsupported language evidence version; scan again"); const entities: Entity[] = [], relationships: Relationship[] = []; for (const contribution of list(document.contributions).map(object)) { for (const item of list(contribution.entities).map(object)) { if (typeof item.exported !== "boolean") throw new Error("Malformed language entity visibility"); entities.push({ id: text(item.id), name: text(item.name), kind: text(item.kind), language: text(item.language), qualifiedName: text(item.qualifiedName), exported: item.exported, location: location(item.location), signatures: list(item.signatures).map(text), attributes: list(item.attributes).map(text), ...(item.ownerId === undefined ? {} : { ownerId: text(item.ownerId) }), }); } for (const item of list(contribution.relationships).map(object)) { relationships.push({ id: text(item.id), from: text(item.from), to: text(item.to), kind: text(item.kind), method: text(item.method), evidence: list(item.evidence).map(location) }); } } return { entities, relationships }; } export { graphLocation, serialize }; -
@topo/scanner
Open Compiler-backed evidence extractionOpen Dependency context by role: Code analysisOpen Dependency context by role: Language parsingOpen Dependency context by role: Path selectionOpen Dependency context by role: Ignore matchingOpen Dependency context by role: Build, test, docsOpen Dependency context by role: Development tools
Extracts TypeScript/JavaScript declarations, references, and graph evidence with the pinned compiler API and repository file-selection rules. All declared internal dependencies (not runtime calls): @topo/scanner -> @topo/schema [dependencies: workspace:*] Referenced by internal modules: @topo/languages -> @topo/scanner [dependencies: workspace:*] @topo/cli -> @topo/scanner [dependencies: workspace:*] Stable drilldown identity: scanner. Open the source-backed sequence for this module's core algorithm.
packages/scanner/package.json:1-32 { "name": "@topo/scanner", "version": "0.0.0", "private": true, "type": "module", "files": [ "dist" ], "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } }, "scripts": { "build": "tsc -b", "fixture": "node dist/fixture.js", "typecheck": "tsc -b --pretty false", "test": "vitest run src" }, "dependencies": { "@topo/schema": "workspace:*", "ignore": "^7.0.0", "typescript-compiler-api": "npm:typescript@6.0.3" }, "devDependencies": { "@types/node": "^22.0.0", "typescript": "^7.0.2", "vite": "8.2.2", "vitest": "^5.0.0" } }packages/scanner/src/typescript-scanner.ts:1-1452 import { createHash } from "node:crypto"; import { readFile, realpath } from "node:fs/promises"; import path from "node:path"; import { GRAPH_SCHEMA_VERSION, createAttributeId, createContainerId, createEdgeId, createExternalNodeId, createGraphDocument, createPathNodeId, serializeGraphDocument, type Evidence, type GraphContainer, type GraphEdge, type GraphNode, type JsonValue, type Provenance, } from "@topo/schema"; import ts from "typescript-compiler-api"; import { assertDirectory, discoverDeclaredPackageNames, discoverWorkspacePackages, isConfigFile, isSourceFile, toRepositoryPath, walkFiles, type WorkspacePackage, } from "./files.js"; import { parseScanRepositoryOptions } from "./input.js"; import { SCANNER_MODULE_ID, SCANNER_VERSION, ScanError, TYPESCRIPT_SCANNER_MANIFEST, type ScanDiagnostic, type ScanMetrics, type ScanRepositoryOptions, type ScanResult, type ScannerAdapter, } from "./types.js"; import { extractLogicalArchitecture } from "./semantic.js"; interface SourceRecord { absolutePath: string; repositoryPath: string; compilerOptions: ts.CompilerOptions; moduleResolutionCache: ts.ModuleResolutionCache; imports: string[]; fingerprint: string; lines: number; } interface AssetRecord { absolutePath: string; repositoryPath: string; fingerprint: string; } interface OutputMapping { configPath: string; rootDirectory: string; outputDirectory: string; } interface ImportRecord { source: SourceRecord; specifier: string; anchor: string; } function assetEvidence(asset: AssetRecord): Evidence { return { id: `evidence:asset:${asset.repositoryPath}`, kind: "source", label: asset.repositoryPath, fingerprint: asset.fingerprint, anchor: { path: asset.repositoryPath }, locator: `path:${asset.repositoryPath}`, }; } const DEFAULT_QUALITY = { allowPartial: false, minimumFileCount: 1, maxUnresolvedImportRatio: 0, requireWorkspaceCoverage: true, } as const; function compareText(left: string, right: string): number { return left < right ? -1 : left > right ? 1 : 0; } function sha256(content: string): string { return `sha256:${createHash("sha256").update(content).digest("hex")}`; } function formatTypeScriptDiagnostic( root: string, diagnostic: ts.Diagnostic, ): ScanDiagnostic { return { code: "typescript-config", severity: "error", message: ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"), ...(diagnostic.file === undefined ? {} : { path: toRepositoryPath(root, diagnostic.file.fileName) }), }; } function isWithin(root: string, target: string): boolean { const relative = path.relative(root, target); return ( relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative)) ); } function importSpecifiers(sourceFile: ts.SourceFile): ImportRecord["specifier"][] { const specifiers: string[] = []; const add = (value: ts.Expression | undefined): void => { if (value !== undefined && ts.isStringLiteralLike(value)) { specifiers.push(value.text); } }; for (const statement of sourceFile.statements) { if (ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement)) { add(statement.moduleSpecifier); } else if ( ts.isImportEqualsDeclaration(statement) && ts.isExternalModuleReference(statement.moduleReference) ) { add(statement.moduleReference.expression); } } const visit = (node: ts.Node): void => { if ( ts.isCallExpression(node) && node.arguments.length === 1 && (node.expression.kind === ts.SyntaxKind.ImportKeyword || (ts.isIdentifier(node.expression) && node.expression.text === "require")) ) { add(node.arguments[0]); } ts.forEachChild(node, visit); }; ts.forEachChild(sourceFile, visit); return specifiers; } function scriptKind(filePath: string): ts.ScriptKind { switch (path.extname(filePath).toLowerCase()) { case ".js": case ".cjs": case ".mjs": return ts.ScriptKind.JS; case ".jsx": return ts.ScriptKind.JSX; case ".tsx": return ts.ScriptKind.TSX; case ".json": return ts.ScriptKind.JSON; default: return ts.ScriptKind.TS; } } async function createSourceRecord( root: string, absolutePath: string, compilerOptions: ts.CompilerOptions, moduleResolutionCache: ts.ModuleResolutionCache, ): Promise<SourceRecord> { const repositoryPath = toRepositoryPath(root, absolutePath); const content = await readFile(absolutePath, "utf8"); const sourceFile = ts.createSourceFile( absolutePath, content, ts.ScriptTarget.Latest, false, scriptKind(absolutePath), ); return { absolutePath, repositoryPath, compilerOptions, moduleResolutionCache, imports: importSpecifiers(sourceFile), fingerprint: sha256(content), lines: content.length === 0 ? 0 : content.split(/\r\n?|\n/u).length, }; } function validateConfiguredTypeLibraries( root: string, configPath: string, compilerOptions: ts.CompilerOptions, diagnostics: ScanDiagnostic[], ): void { for (const typeLibrary of compilerOptions.types ?? []) { const resolution = ts.resolveTypeReferenceDirective( typeLibrary, configPath, compilerOptions, ts.sys, ); if (resolution.resolvedTypeReferenceDirective === undefined) { diagnostics.push({ code: "missing-type-library", severity: "error", message: `Cannot resolve configured type library "${typeLibrary}".`, path: toRepositoryPath(root, configPath), specifier: typeLibrary, }); } } } async function loadConfiguredSources( root: string, configPaths: readonly string[], diagnostics: ScanDiagnostic[], outputMappings: OutputMapping[], inventoryPaths: ReadonlySet<string>, ): Promise<Map<string, SourceRecord>> { const records = new Map<string, SourceRecord>(); const ignoredConfigSources = new Set<string>(); for (const configPath of configPaths) { const config = ts.readConfigFile(configPath, ts.sys.readFile); if (config.error !== undefined) { diagnostics.push(formatTypeScriptDiagnostic(root, config.error)); continue; } const parsed = ts.parseJsonConfigFileContent( config.config, ts.sys, path.dirname(configPath), undefined, configPath, ); diagnostics.push( ...parsed.errors.map((diagnostic) => formatTypeScriptDiagnostic(root, diagnostic), ), ); validateConfiguredTypeLibraries( root, configPath, parsed.options, diagnostics, ); if ( parsed.options.rootDir !== undefined && parsed.options.outDir !== undefined ) { outputMappings.push({ configPath: toRepositoryPath(root, configPath), rootDirectory: path.resolve(parsed.options.rootDir), outputDirectory: path.resolve(parsed.options.outDir), }); } const moduleResolutionCache = ts.createModuleResolutionCache( path.dirname(configPath), (fileName) => fileName, parsed.options, ); await collectConfiguredFiles( root, parsed.fileNames, parsed.options, moduleResolutionCache, records, inventoryPaths, ignoredConfigSources, ); } if (ignoredConfigSources.size > 0) { const paths = [...ignoredConfigSources].sort(compareText); diagnostics.push({ code: "ignored-config-source", severity: "warning", message: `${paths.length} TypeScript-configured files were excluded by Git or ignore rules: ${paths.join(", ")}.`, }); } return records; } async function collectConfiguredFiles( root: string, fileNames: readonly string[], compilerOptions: ts.CompilerOptions, moduleResolutionCache: ts.ModuleResolutionCache, records: Map<string, SourceRecord>, inventoryPaths?: ReadonlySet<string>, ignoredConfigSources?: Set<string>, ): Promise<void> { for (const fileName of fileNames) { const absolutePath = path.resolve(fileName); if (inventoryPaths !== undefined && !inventoryPaths.has(absolutePath)) { ignoredConfigSources?.add(toRepositoryPath(root, absolutePath)); continue; } if ( !isWithin(root, absolutePath) || !isSourceFile(absolutePath) ) { continue; } const repositoryPath = toRepositoryPath(root, absolutePath); if (records.has(repositoryPath)) { continue; } records.set( repositoryPath, await createSourceRecord( root, absolutePath, compilerOptions, moduleResolutionCache, ), ); } } async function loadInferredSources( root: string, sourcePaths: readonly string[], ): Promise<Map<string, SourceRecord>> { const options: ts.CompilerOptions = { allowJs: true, checkJs: false, module: ts.ModuleKind.NodeNext, moduleResolution: ts.ModuleResolutionKind.NodeNext, target: ts.ScriptTarget.ES2022, }; const records = new Map<string, SourceRecord>(); const moduleResolutionCache = ts.createModuleResolutionCache( root, (fileName) => fileName, options, ); await collectConfiguredFiles( root, sourcePaths, options, moduleResolutionCache, records, ); return records; } function packageName(specifier: string): string { if (specifier.startsWith("@")) { return specifier.split("/").slice(0, 2).join("/"); } return specifier.split("/")[0] ?? specifier; } function isPathAlias( specifier: string, compilerOptions: ts.CompilerOptions, ): boolean { return Object.keys(compilerOptions.paths ?? {}).some((pattern) => { const [prefix, suffix = ""] = pattern.split("*"); return ( specifier.startsWith(prefix ?? "") && specifier.endsWith(suffix) && specifier.length >= (prefix?.length ?? 0) + suffix.length ); }); } function workspaceForSpecifier( specifier: string, workspaces: readonly WorkspacePackage[], ): WorkspacePackage | undefined { return workspaces.find( (workspace) => specifier === workspace.name || specifier.startsWith(`${workspace.name}/`), ); } function manifestEntryStrings(value: unknown): string[] { if (typeof value === "string") { return [value]; } if (value === null || typeof value !== "object" || Array.isArray(value)) { return []; } return Object.values(value).flatMap(manifestEntryStrings); } function hasExactWorkspaceExport( exports: unknown, subpath: string, ): boolean { return ( exports === null || typeof exports !== "object" || Array.isArray(exports) ? false : Object.hasOwn(exports, subpath) ); } function sourceCandidatePathsForEntries( workspace: WorkspacePackage, entries: readonly string[], ): string[] { const candidates = new Set<string>(); for (const entry of entries) { const normalized = entry.replace(/^\.\//u, ""); candidates.add(path.resolve(workspace.directory, normalized)); if (normalized.startsWith("dist/")) { candidates.add( path.resolve( workspace.directory, `src/${normalized.slice("dist/".length)}`, ), ); } } return [...candidates]; } function sourceCandidatePaths( workspace: WorkspacePackage, specifier: string, ): string[] { const suffix = specifier === workspace.name ? "" : specifier.slice(workspace.name.length + 1); const manifest = workspace.manifest; const declared = suffix.length === 0 ? [ ...manifestEntryStrings(manifest.source), ...manifestEntryStrings(manifest.module), ...manifestEntryStrings(manifest.main), ...manifestEntryStrings(manifest.types), ] : manifest.exports === undefined ? [suffix] : []; const bases = [...declared, ...(suffix.length === 0 ? ["src/index"] : [])]; return sourceCandidatePathsForEntries(workspace, bases); } function resolveSourceCandidate( candidates: readonly string[], sourceByAbsolutePath: ReadonlyMap<string, SourceRecord>, ): SourceRecord | undefined { return resolveSourceCandidates(candidates, sourceByAbsolutePath)[0]; } function resolveSourceCandidates( candidates: readonly string[], sourceByAbsolutePath: ReadonlyMap<string, SourceRecord>, ): SourceRecord[] { const extensions = [ "", ".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", "/index.ts", "/index.tsx", "/index.mts", "/index.cts", "/index.js", "/index.jsx", ]; const matches = new Map<string, SourceRecord>(); for (const candidate of candidates) { const withoutOutputExtension = candidate.replace( /\.(?:d\.ts|[cm]?[jt]sx?)$/u, "", ); for (const extension of extensions) { const source = sourceByAbsolutePath.get( path.resolve(`${withoutOutputExtension}${extension}`), ); if (source !== undefined) { matches.set(source.absolutePath, source); } } } return [...matches.values()]; } const GENERATED_SOURCE_EXTENSIONS = [ [".d.mts", [".mts", ".mjs"]], [".d.cts", [".cts", ".cjs"]], [".d.ts", [".ts", ".tsx", ".js", ".jsx"]], [".mjs", [".mts", ".mjs"]], [".cjs", [".cts", ".cjs"]], [".jsx", [".tsx", ".jsx"]], [".js", [".ts", ".tsx", ".js", ".jsx"]], ] as const; function generatedSourcePaths(outputPath: string): string[] { for (const [outputExtension, sourceExtensions] of GENERATED_SOURCE_EXTENSIONS) { if (outputPath.endsWith(outputExtension)) { const basePath = outputPath.slice(0, -outputExtension.length); return sourceExtensions.map( (sourceExtension) => `${basePath}${sourceExtension}`, ); } } return []; } function resolveGeneratedOutput( resolvedFileName: string, outputMappings: readonly OutputMapping[], sourceByAbsolutePath: ReadonlyMap<string, SourceRecord>, ): | { kind: "none" } | { kind: "resolved"; source: SourceRecord } | { kind: "ambiguous"; configPaths: string[] } { const matches = new Map<string, { source: SourceRecord; configPath: string }>(); for (const mapping of outputMappings) { if (!isWithin(mapping.outputDirectory, resolvedFileName)) { continue; } const relativeOutputPath = path.relative( mapping.outputDirectory, resolvedFileName, ); const sourcePath = path.join(mapping.rootDirectory, relativeOutputPath); for (const generatedSourcePath of generatedSourcePaths(sourcePath)) { const source = sourceByAbsolutePath.get(generatedSourcePath); if (source !== undefined) { matches.set(source.absolutePath, { source, configPath: mapping.configPath, }); } } } if (matches.size === 0) { return { kind: "none" }; } if (matches.size === 1) { return { kind: "resolved", source: [...matches.values()][0]!.source }; } return { kind: "ambiguous", configPaths: [...new Set( [...matches.values()].map((match) => match.configPath), )].sort(compareText), }; } function compilerPathCandidates( root: string, canonicalRoot: string, resolvedFileName: string, ): string[] { const candidates = new Set<string>([path.resolve(resolvedFileName)]); const canonicalPath = ts.sys.realpath?.(resolvedFileName) ?? path.resolve(resolvedFileName); candidates.add(path.resolve(canonicalPath)); if (isWithin(canonicalRoot, canonicalPath)) { candidates.add( path.resolve(root, path.relative(canonicalRoot, canonicalPath)), ); } return [...candidates]; } function resolveCompilerSelectedSource( root: string, canonicalRoot: string, resolvedFileName: string, outputMappings: readonly OutputMapping[], sourceByAbsolutePath: ReadonlyMap<string, SourceRecord>, ): | { kind: "none" } | { kind: "resolved"; source: SourceRecord } | { kind: "ambiguous"; configPaths: string[] } { const candidates = compilerPathCandidates( root, canonicalRoot, resolvedFileName, ); for (const candidate of candidates) { const source = sourceByAbsolutePath.get(candidate); if (source !== undefined) { return { kind: "resolved", source }; } } for (const candidate of candidates) { if (!isWithin(root, candidate)) { continue; } const generated = resolveGeneratedOutput( candidate, outputMappings, sourceByAbsolutePath, ); if (generated.kind !== "none") { return generated; } } return { kind: "none" }; } function canonicalPath(fileName: string): string { const resolved = path.resolve(fileName); const canonical = ts.sys.realpath?.(resolved) ?? resolved; return ts.sys.useCaseSensitiveFileNames ? canonical : canonical.toLowerCase(); } function physicalWorkspacePackageState( workspace: WorkspacePackage, containingFile: string, ): "absent" | "workspace" | "other" { const packageSegments = workspace.name.split("/"); let directory = path.dirname(containingFile); while (true) { const packageDirectory = path.join( directory, "node_modules", ...packageSegments, ); if (ts.sys.fileExists(path.join(packageDirectory, "package.json"))) { return canonicalPath(packageDirectory) === canonicalPath(workspace.directory) ? "workspace" : "other"; } const parent = path.dirname(directory); if (parent === directory) { return "absent"; } directory = parent; } } function createWorkspaceModuleResolutionHost( root: string, canonicalRoot: string, workspace: WorkspacePackage, outputMappings: readonly OutputMapping[], sourceByAbsolutePath: ReadonlyMap<string, SourceRecord>, allowGeneratedOutput: boolean, ): ts.ModuleResolutionHost { const packageSegments = workspace.name.split("/"); const virtualPackageDirectories = [...new Set([root, canonicalRoot])] .map((workspaceRoot) => path.join(workspaceRoot, "node_modules", ...packageSegments), ); const workspaceMappings = outputMappings.filter( (mapping) => isWithin(workspace.directory, mapping.rootDirectory) && isWithin(workspace.directory, mapping.outputDirectory), ); const toWorkspacePath = (fileName: string): string | undefined => { for (const virtualDirectory of virtualPackageDirectories) { if (isWithin(virtualDirectory, fileName)) { return path.resolve( workspace.directory, path.relative(virtualDirectory, fileName), ); } } return undefined; }; const hasGeneratedPath = (fileName: string): boolean => resolveGeneratedOutput( fileName, workspaceMappings, sourceByAbsolutePath, ).kind === "resolved"; const hasGeneratedDirectory = (directory: string): boolean => workspaceMappings.some( (mapping) => isWithin(directory, mapping.outputDirectory) || isWithin(mapping.outputDirectory, directory), ); return { fileExists(fileName) { if (ts.sys.fileExists(fileName)) { return true; } const workspacePath = toWorkspacePath(fileName); return workspacePath !== undefined && (ts.sys.fileExists(workspacePath) || (allowGeneratedOutput && hasGeneratedPath(workspacePath))); }, readFile(fileName) { const workspacePath = toWorkspacePath(fileName); return ts.sys.readFile(workspacePath ?? fileName); }, directoryExists(directory) { if (ts.sys.directoryExists?.(directory) === true) { return true; } if ( virtualPackageDirectories.some((virtualDirectory) => isWithin(directory, virtualDirectory), ) ) { return true; } const workspacePath = toWorkspacePath(directory); return workspacePath !== undefined && (ts.sys.directoryExists?.(workspacePath) === true || (allowGeneratedOutput && hasGeneratedDirectory(workspacePath))); }, realpath(fileName) { const workspacePath = toWorkspacePath(fileName); if (workspacePath !== undefined) { return ts.sys.realpath?.(workspacePath) ?? workspacePath; } return ts.sys.realpath?.(fileName) ?? fileName; }, getCurrentDirectory: () => root, useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames, }; } function resolveWorkspaceModuleName( root: string, canonicalRoot: string, workspace: WorkspacePackage, source: SourceRecord, specifier: string, outputMappings: readonly OutputMapping[], sourceByAbsolutePath: ReadonlyMap<string, SourceRecord>, ): ts.ResolvedModuleFull | undefined { for (const allowGeneratedOutput of [false, true]) { const resolved = ts.resolveModuleName( specifier, source.absolutePath, source.compilerOptions, createWorkspaceModuleResolutionHost( root, canonicalRoot, workspace, outputMappings, sourceByAbsolutePath, allowGeneratedOutput, ), ts.createModuleResolutionCache( path.dirname(source.absolutePath), (fileName) => ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(), source.compilerOptions, ), ).resolvedModule; if (resolved !== undefined) { return resolved; } } return undefined; } function assetSpecifierPath(specifier: string): string | undefined { if (!specifier.startsWith(".")) { return undefined; } const literalPath = specifier.split(/[?#]/u, 1)[0]; return literalPath !== undefined && path.extname(literalPath).toLowerCase() === ".css" ? literalPath : undefined; } function externalLocator(specifier: string): string { return specifier.startsWith("node:") ? specifier : `npm:${packageName(specifier)}`; } function provenance(evidenceIds: string[]): Provenance { return { kind: "observed", moduleId: SCANNER_MODULE_ID, method: "typescript-compiler-api", evidenceIds: [...evidenceIds].sort(compareText), }; } function directoriesForPaths( rootLabel: string, repositoryPaths: readonly string[], ): GraphContainer[] { const directories = new Set<string>(["."]); for (const repositoryPath of repositoryPaths) { let directory = path.posix.dirname(repositoryPath); while (directory !== ".") { directories.add(directory); directory = path.posix.dirname(directory); } } return [...directories] .sort(compareText) .map((directory) => { const memberIds = repositoryPaths .filter( (repositoryPath) => path.posix.dirname(repositoryPath) === directory, ) .map((repositoryPath) => createPathNodeId(repositoryPath)) .sort(compareText); return { id: createContainerId("directory", directory), label: directory === "." ? rootLabel : path.posix.basename(directory), type: "directory", memberIds, ...(directory === "." ? {} : { parentId: createContainerId( "directory", path.posix.dirname(directory), ), }), }; }); } function sourceEvidence(source: SourceRecord): Evidence { return { id: `evidence:source:${source.repositoryPath}`, kind: "source", label: source.repositoryPath, fingerprint: source.fingerprint, anchor: { path: source.repositoryPath }, locator: `path:${source.repositoryPath}`, }; } function importEvidence( source: SourceRecord, specifier: string, ): Evidence { const anchor = sha256(`import:${specifier}`).slice("sha256:".length, 24); return { id: `evidence:import:${source.repositoryPath}:${anchor}`, kind: "source", label: `${source.repositoryPath} imports ${specifier}`, fingerprint: source.fingerprint, anchor: { path: source.repositoryPath, symbol: `import:${specifier}`, contentPattern: JSON.stringify(specifier), }, locator: `path:${source.repositoryPath}#import:${JSON.stringify(specifier)}`, }; } function qualityDiagnostics( metrics: ScanMetrics, quality: Required<NonNullable<ScanRepositoryOptions["quality"]>>, unsupportedCargo: boolean, ): ScanDiagnostic[] { const diagnostics: ScanDiagnostic[] = []; if (metrics.sourceFileCount < quality.minimumFileCount) { diagnostics.push({ code: "insufficient-source-files", severity: "error", message: `Found ${metrics.sourceFileCount} supported source files; at least ${quality.minimumFileCount} required.`, }); } const importTotal = metrics.localImportCount + metrics.unresolvedImportCount; const unresolvedRatio = importTotal === 0 ? 0 : metrics.unresolvedImportCount / importTotal; if (unresolvedRatio > quality.maxUnresolvedImportRatio) { diagnostics.push({ code: "unresolved-import-ratio", severity: "error", message: `Unresolved local/workspace import ratio ${unresolvedRatio.toFixed(6)} exceeds ${quality.maxUnresolvedImportRatio}.`, }); } if ( quality.requireWorkspaceCoverage && metrics.coveredWorkspacePackageCount < metrics.workspacePackageCount ) { diagnostics.push({ code: "incomplete-workspace-coverage", severity: "error", message: `Covered ${metrics.coveredWorkspacePackageCount} of ${metrics.workspacePackageCount} workspace packages.`, }); } if (unsupportedCargo) { diagnostics.push({ code: "unsupported-cargo-workspace", severity: "error", message: "Cargo workspaces are not supported by the TypeScript/JavaScript scanner.", path: "Cargo.toml", }); } return diagnostics; } export async function scanRepository( input: ScanRepositoryOptions, ): Promise<ScanResult> { const options = parseScanRepositoryOptions(input); const quality = { ...DEFAULT_QUALITY, ...options.quality }; await assertDirectory(options.root); const canonicalRoot = await realpath(options.root); const files = await walkFiles(options.root); const inventoryPaths = new Set(files.map((filePath) => path.resolve(filePath))); const configPaths = files.filter(isConfigFile).sort(compareText); const sourcePaths = files.filter(isSourceFile).sort(compareText); const workspaces = await discoverWorkspacePackages(options.root, files); const declaredPackageNames = await discoverDeclaredPackageNames( options.root, files, ); const diagnostics: ScanDiagnostic[] = []; const outputMappings: OutputMapping[] = []; const configuredSources = await loadConfiguredSources( options.root, configPaths, diagnostics, outputMappings, inventoryPaths, ); const unconfiguredPaths = sourcePaths.filter( (sourcePath) => !configuredSources.has(toRepositoryPath(options.root, sourcePath)), ); const inferredSources = await loadInferredSources( options.root, unconfiguredPaths, ); const sources = new Map([...configuredSources, ...inferredSources]); if (configuredSources.size > 0 && inferredSources.size > 0) { diagnostics.push({ code: "inferred-source-configuration", severity: "warning", message: `${inferredSources.size} source files were not covered by a discovered TypeScript config and used default compiler options.`, }); } const sortedSources = [...sources.values()].sort((left, right) => compareText(left.repositoryPath, right.repositoryPath), ); const sourceByAbsolutePath = new Map( sortedSources.map((source) => [path.resolve(source.absolutePath), source]), ); const filePaths = inventoryPaths; const nodes = new Map<string, GraphNode>(); const evidence = new Map<string, Evidence>(); const assets = new Map<string, AssetRecord>(); for (const source of sortedSources) { const nodeId = createPathNodeId(source.repositoryPath); nodes.set(nodeId, { id: nodeId, label: path.posix.basename(source.repositoryPath), kind: "file", identity: { kind: "path", value: source.repositoryPath }, fingerprint: source.fingerprint, }); const item = sourceEvidence(source); evidence.set(item.id, item); } const edgeEvidence = new Map< string, { sourceId: string; targetId: string; evidenceIds: Set<string> } >(); let localImportCount = 0; let assetImportCount = 0; let externalImportCount = 0; let unresolvedImportCount = 0; for (const source of sortedSources) { const imports = [...source.imports].sort(compareText); for (const specifier of imports) { const importItem = importEvidence(source, specifier); const assetPath = assetSpecifierPath(specifier); if (assetPath !== undefined) { const absoluteAssetPath = path.resolve( path.dirname(source.absolutePath), assetPath, ); if ( !isWithin(options.root, absoluteAssetPath) || !filePaths.has(absoluteAssetPath) ) { unresolvedImportCount += 1; diagnostics.push({ code: "unresolved-local-asset", severity: "error", message: `Could not resolve local CSS asset "${specifier}" from "${source.repositoryPath}".`, path: source.repositoryPath, specifier, }); continue; } let asset = assets.get(absoluteAssetPath); if (asset === undefined) { const content = await readFile(absoluteAssetPath, "utf8"); asset = { absolutePath: absoluteAssetPath, repositoryPath: toRepositoryPath( options.root, absoluteAssetPath, ), fingerprint: sha256(content), }; assets.set(absoluteAssetPath, asset); const assetId = createPathNodeId(asset.repositoryPath); nodes.set(assetId, { id: assetId, label: path.posix.basename(asset.repositoryPath), kind: "asset", identity: { kind: "path", value: asset.repositoryPath }, fingerprint: asset.fingerprint, }); const item = assetEvidence(asset); evidence.set(item.id, item); } const sourceId = createPathNodeId(source.repositoryPath); const targetId = createPathNodeId(asset.repositoryPath); const edgeId = createEdgeId("imports", sourceId, targetId); evidence.set(importItem.id, importItem); const accumulated = edgeEvidence.get(edgeId) ?? { sourceId, targetId, evidenceIds: new Set<string>(), }; accumulated.evidenceIds.add(importItem.id); edgeEvidence.set(edgeId, accumulated); assetImportCount += 1; continue; } const resolved = ts.resolveModuleName( specifier, source.absolutePath, source.compilerOptions, ts.sys, source.moduleResolutionCache, ).resolvedModule; let targetId: string | undefined; const workspace = workspaceForSpecifier(specifier, workspaces); if (workspace !== undefined) { let target: SourceRecord | undefined; if (resolved !== undefined) { const selected = resolveCompilerSelectedSource( options.root, canonicalRoot, resolved.resolvedFileName, outputMappings, sourceByAbsolutePath, ); if (selected.kind === "resolved") { target = selected.source; } else if (selected.kind === "ambiguous") { unresolvedImportCount += 1; diagnostics.push({ code: "ambiguous-generated-output", severity: "error", message: `Generated output import "${specifier}" from "${source.repositoryPath}" maps to multiple source projects: ${selected.configPaths.join(", ")}.`, path: source.repositoryPath, specifier, }); continue; } else if ( resolved.resolvedFileName .split(path.sep) .includes("node_modules") || !isWithin(options.root, resolved.resolvedFileName) ) { const locator = externalLocator(specifier); targetId = createExternalNodeId(locator); externalImportCount += 1; nodes.set(targetId, { id: targetId, label: packageName(specifier), kind: "external", identity: { kind: "external", value: locator }, }); } } else { const suffix = specifier === workspace.name ? "" : specifier.slice(workspace.name.length + 1); const exportedSubpath = suffix.length === 0 ? workspace.manifest.exports !== undefined : hasExactWorkspaceExport( workspace.manifest.exports, `./${suffix}`, ); const physicalPackage = physicalWorkspacePackageState( workspace, source.absolutePath, ); if (exportedSubpath && physicalPackage !== "other") { const virtualResolved = resolveWorkspaceModuleName( options.root, canonicalRoot, workspace, source, specifier, outputMappings, sourceByAbsolutePath, ); if (virtualResolved !== undefined) { const selected = resolveCompilerSelectedSource( options.root, canonicalRoot, virtualResolved.resolvedFileName, outputMappings, sourceByAbsolutePath, ); if (selected.kind === "resolved") { target = selected.source; } else if (selected.kind === "ambiguous") { unresolvedImportCount += 1; diagnostics.push({ code: "ambiguous-generated-output", severity: "error", message: `Generated output import "${specifier}" from "${source.repositoryPath}" maps to multiple source projects: ${selected.configPaths.join(", ")}.`, path: source.repositoryPath, specifier, }); continue; } } } else if (physicalPackage !== "other") { target = resolveSourceCandidate( sourceCandidatePaths(workspace, specifier), sourceByAbsolutePath, ); } } if (targetId === undefined) { if (target !== undefined) { targetId = createPathNodeId(target.repositoryPath); localImportCount += 1; } else { unresolvedImportCount += 1; diagnostics.push({ code: "unresolved-workspace-import", severity: "error", message: `Could not resolve workspace import "${specifier}" from "${source.repositoryPath}".`, path: source.repositoryPath, specifier, }); continue; } } } else if ( resolved !== undefined && resolved.resolvedFileName .split(path.sep) .includes("node_modules") ) { const locator = externalLocator(specifier); targetId = createExternalNodeId(locator); externalImportCount += 1; nodes.set(targetId, { id: targetId, label: packageName(specifier), kind: "external", identity: { kind: "external", value: locator }, }); } else if (resolved !== undefined && isWithin(options.root, resolved.resolvedFileName)) { const target = sourceByAbsolutePath.get( path.resolve(resolved.resolvedFileName), ); if (target !== undefined) { targetId = createPathNodeId(target.repositoryPath); localImportCount += 1; } else { const generated = resolveGeneratedOutput( path.resolve(resolved.resolvedFileName), outputMappings, sourceByAbsolutePath, ); if (generated.kind === "resolved") { targetId = createPathNodeId(generated.source.repositoryPath); localImportCount += 1; } else if (generated.kind === "ambiguous") { unresolvedImportCount += 1; diagnostics.push({ code: "ambiguous-generated-output", severity: "error", message: `Generated output import "${specifier}" from "${source.repositoryPath}" maps to multiple source projects: ${generated.configPaths.join(", ")}.`, path: source.repositoryPath, specifier, }); continue; } } } else if ( resolved !== undefined && !isWithin(options.root, resolved.resolvedFileName) ) { const locator = externalLocator(specifier); targetId = createExternalNodeId(locator); externalImportCount += 1; nodes.set(targetId, { id: targetId, label: packageName(specifier), kind: "external", identity: { kind: "external", value: locator }, }); } else { const local = specifier.startsWith(".") || specifier.startsWith("/") || (isPathAlias(specifier, source.compilerOptions) && !declaredPackageNames.has(packageName(specifier))); if (local) { unresolvedImportCount += 1; diagnostics.push({ code: "unresolved-local-import", severity: "error", message: `Could not resolve local import "${specifier}" from "${source.repositoryPath}".`, path: source.repositoryPath, specifier, }); continue; } const locator = externalLocator(specifier); targetId = createExternalNodeId(locator); externalImportCount += 1; nodes.set(targetId, { id: targetId, label: packageName(specifier), kind: "external", identity: { kind: "external", value: locator }, }); } if (targetId === undefined) { unresolvedImportCount += 1; diagnostics.push({ code: "resolved-import-outside-scan", severity: "error", message: `Resolved import "${specifier}" is not represented by a scanned source file.`, path: source.repositoryPath, specifier, }); continue; } const sourceId = createPathNodeId(source.repositoryPath); const edgeId = createEdgeId("imports", sourceId, targetId); evidence.set(importItem.id, importItem); const accumulated = edgeEvidence.get(edgeId) ?? { sourceId, targetId, evidenceIds: new Set<string>(), }; accumulated.evidenceIds.add(importItem.id); edgeEvidence.set(edgeId, accumulated); } } const edges: GraphEdge[] = [...edgeEvidence.entries()] .sort(([left], [right]) => compareText(left, right)) .map(([id, edge]) => ({ id, label: "imports", type: "imports", sourceId: edge.sourceId, targetId: edge.targetId, provenance: provenance([...edge.evidenceIds]), })); const coveredWorkspacePackageCount = workspaces.filter((workspace) => sortedSources.some( (source) => source.absolutePath.startsWith(`${workspace.directory}${path.sep}`), ), ).length; const metrics: ScanMetrics = { configCount: configPaths.length, workspacePackageCount: workspaces.length, coveredWorkspacePackageCount, sourceFileCount: sortedSources.length, assetFileCount: assets.size, linesOfCode: sortedSources.reduce((sum, source) => sum + source.lines, 0), localImportCount, assetImportCount, externalImportCount, unresolvedImportCount, }; const rootCargo = files.includes(path.join(options.root, "Cargo.toml")); diagnostics.push(...qualityDiagnostics(metrics, quality, rootCargo)); diagnostics.sort( (left, right) => compareText(left.path ?? "", right.path ?? "") || compareText(left.code, right.code) || compareText(left.specifier ?? "", right.specifier ?? ""), ); const errors = diagnostics.filter( (diagnostic) => diagnostic.severity === "error", ); const fatalEmpty = sortedSources.length === 0; if ( errors.length > 0 && (!quality.allowPartial || fatalEmpty) ) { throw new ScanError( `Repository scan failed with ${errors.length} error diagnostic${errors.length === 1 ? "" : "s"}.`, diagnostics, ); } const authoritative = errors.length === 0; const repositoryId = options.repositoryId ?? path.basename(options.root); const rootLabel = path.basename(options.root); const scannerExtension: JsonValue = { authoritative, status: authoritative ? "complete" : "partial", adapter: { id: TYPESCRIPT_SCANNER_MANIFEST.id, version: TYPESCRIPT_SCANNER_MANIFEST.version, contractVersion: TYPESCRIPT_SCANNER_MANIFEST.contractVersion, capabilities: { languages: [...TYPESCRIPT_SCANNER_MANIFEST.capabilities.languages], granularity: [...TYPESCRIPT_SCANNER_MANIFEST.capabilities.granularity], relationships: [ ...TYPESCRIPT_SCANNER_MANIFEST.capabilities.relationships, ], moduleResolution: TYPESCRIPT_SCANNER_MANIFEST.capabilities.moduleResolution, workspaceManifests: [ ...TYPESCRIPT_SCANNER_MANIFEST.capabilities.workspaceManifests, ], opaqueAssets: [ ...TYPESCRIPT_SCANNER_MANIFEST.capabilities.opaqueAssets, ], partialResults: TYPESCRIPT_SCANNER_MANIFEST.capabilities.partialResults, }, }, metrics: { ...metrics }, diagnostics: diagnostics.map((diagnostic) => ({ code: diagnostic.code, severity: diagnostic.severity, message: diagnostic.message, ...(diagnostic.path === undefined ? {} : { path: diagnostic.path }), ...(diagnostic.specifier === undefined ? {} : { specifier: diagnostic.specifier }), })), }; const graph = createGraphDocument({ graphId: `repo:${repositoryId}`, repository: { id: repositoryId, label: rootLabel, ...(options.revision === undefined ? {} : { revision: options.revision }), }, modules: [ { id: SCANNER_MODULE_ID, version: SCANNER_VERSION, schemaVersion: GRAPH_SCHEMA_VERSION, }, ], nodes: [...nodes.values()].sort((left, right) => compareText(left.id, right.id), ), edges, containers: directoriesForPaths( rootLabel, [ ...sortedSources.map((source) => source.repositoryPath), ...[...assets.values()].map((asset) => asset.repositoryPath), ].sort(compareText), ), attributes: sortedSources.flatMap((source) => { const nodeId = createPathNodeId(source.repositoryPath); const evidenceId = `evidence:source:${source.repositoryPath}`; return [ { id: createAttributeId(nodeId, "dev.topo.source.lines"), subject: { kind: "node" as const, id: nodeId }, key: "dev.topo.source.lines", value: source.lines, provenance: provenance([evidenceId]), evidenceIds: [evidenceId], confidence: 1, witnesses: [ { nodeId, fingerprint: source.fingerprint, relationship: "self" as const, }, ], }, ]; }), evidence: [...evidence.values()].sort((left, right) => compareText(left.id, right.id), ), extensions: { "dev.topo.scanner": scannerExtension, }, }); serializeGraphDocument(graph); const logicalArchitecture = await extractLogicalArchitecture({ root: options.root, graphId: graph.graphId, graph, authoritative, ...(options.revision ? { revision: options.revision } : {}), sources: sortedSources, ...(options.responsibilityFile ? { responsibilityFile: options.responsibilityFile } : {}), }); return { graph, logicalArchitecture, diagnostics, metrics, authoritative }; } export function createTypeScriptScannerAdapter(): ScannerAdapter { return { manifest: TYPESCRIPT_SCANNER_MANIFEST, scan(options: unknown) { return scanRepository(parseScanRepositoryOptions(options)); }, }; } -
@topo/schema
Open Dependency context by role: Schema contractsOpen Dependency context by role: JSON validationOpen Dependency context by role: Build, test, docsOpen Dependency context by role: Development tools
Defines graph/artifact types, JSON schemas, validation, stable IDs, canonical serialization and compatibility rules. All declared internal dependencies (not runtime calls): No matching declarations. Referenced by internal modules: @topo/languages -> @topo/schema [dependencies: workspace:*] @topo/scanner -> @topo/schema [dependencies: workspace:*] @topo/reports -> @topo/schema [dependencies: workspace:*] @topo/cli -> @topo/schema [dependencies: workspace:*] @topo/views -> @topo/schema [dependencies: workspace:*] @topo/site -> @topo/schema [dependencies: workspace:*] @topo/workspace -> @topo/schema [dependencies: workspace:*] @topo/modules -> @topo/schema [dependencies: workspace:*] @topo/enrichment -> @topo/schema [dependencies: workspace:*] @topo/graph -> @topo/schema [dependencies: workspace:*] Stable drilldown identity: schema. Module-specific detail maps will be curated later; there is no placeholder target.
packages/schema/package.json:1-34 { "name": "@topo/schema", "version": "0.0.0", "private": true, "type": "module", "files": [ "dist", "graph.schema.json" ], "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }, "./graph.schema.json": "./graph.schema.json" }, "scripts": { "build": "node scripts/generate-validator.mjs --check && tsc -b && node scripts/smoke-validator.mjs", "schema:generate": "node scripts/generate-validator.mjs", "schema:check": "node scripts/generate-validator.mjs --check", "typecheck": "node scripts/generate-validator.mjs --check && tsc -b --pretty false", "test": "node scripts/generate-validator.mjs --check && vitest run" }, "dependencies": { "ajv": "8.17.1", "ajv-formats": "3.0.1" }, "devDependencies": { "@types/node": "^22", "typescript": "^7.0.2", "vite": "8.2.2", "vitest": "^5.0.0" } }packages/schema/src/index.ts:1-10 export * from "./canonical.js"; export * from "./compatibility.js"; export * from "./factories.js"; export * from "./ids.js"; export * from "./json.js"; export * from "./layout.js"; export * from "./logical-architecture.js"; export * from "./model.js"; export * from "./schema.js"; export * from "./validation.js";
-
@topo/reports
Open Dependency context by role: Build, test, docsOpen Dependency context by role: Development tools
Parses normalized reports, checks them against graph evidence, and produces dashboard artifacts. All declared internal dependencies (not runtime calls): @topo/reports -> @topo/schema [dependencies: workspace:*] Referenced by internal modules: @topo/cli -> @topo/reports [dependencies: workspace:*] Stable drilldown identity: reports. Module-specific detail maps will be curated later; there is no placeholder target.
packages/reports/package.json:1-26 { "name": "@topo/reports", "version": "0.0.0", "private": true, "type": "module", "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } }, "scripts": { "build": "tsc -b", "typecheck": "tsc -b --pretty false", "test": "vitest run" }, "dependencies": { "@topo/schema": "workspace:*" }, "devDependencies": { "@types/node": "^22.20.1", "typescript": "^7.0.2", "vite": "8.2.2", "vitest": "^5.0.0" } }packages/reports/src/index.ts:1-197 import { createHash } from "node:crypto"; import { normalizeRepositoryPath, serializeJson, type GraphDocument, type JsonValue } from "@topo/schema"; export interface ReportSource { id: string; tool: string; adapterVersion: string; repositoryId: string; revision: string; collectedAt: string; } export interface ReportMetric { path: string; key: string; value: number; unit: "count" | "percent" | "ratio" | "milliseconds"; } export interface ReportFinding { id: string; path: string; severity: "info" | "warning" | "error"; message: string; } export interface ReportDocument { schemaVersion: "1.0"; id: string; source: ReportSource; configuration: Record<string, JsonValue>; metrics: ReportMetric[]; findings: ReportFinding[]; } export interface DashboardDocument { schemaVersion: "1.0"; repositoryId: string; revision: string; inputs: Array<{ id: string; source: ReportSource; configuration: Record<string, JsonValue>; fingerprint: string }>; metrics: Array<ReportMetric & { reportId: string; sourceId: string; nodeId: string; provenance: "observed" }>; findings: Array<ReportFinding & { reportId: string; sourceId: string; nodeId: string; provenance: "observed" }>; } function object(value: unknown, label: string, keys: string[]): Record<string, unknown> { if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${label} must be an object`); if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) { throw new Error(`${label} must be a plain JSON object`); } const record = value as Record<string, unknown>; const extra = Object.keys(record).filter((key) => !keys.includes(key)); if (extra.length) throw new Error(`${label} has unknown keys: ${extra.join(", ")}`); return record; } function text(value: unknown, label: string): string { if (typeof value !== "string" || !value.trim()) throw new Error(`${label} must be a nonempty string`); return value; } function path(value: unknown): string { const input = text(value, "Report path"); if (/^[a-z]:/i.test(input) || input.includes("\0")) throw new Error("Report paths must be repository-relative"); const normalized = normalizeRepositoryPath(input); if (normalized.split("/").some((part) => !part || part === ".")) throw new Error("Report paths must be canonical"); return normalized; } function json(value: unknown): JsonValue { if (value === null || typeof value === "string" || typeof value === "boolean") return value; if (typeof value === "number" && Number.isFinite(value)) return value; if (Array.isArray(value)) return value.map(json); if (typeof value === "object" && value !== null) { if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) { throw new Error("Report configuration must contain only plain JSON objects"); } return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, json(item)])); } throw new Error("Report configuration must contain only finite JSON values"); } function array(value: unknown, label: string): unknown[] { if (!Array.isArray(value)) throw new Error(`${label} must be an array`); return value; } function compare(left: string, right: string): number { return left < right ? -1 : left > right ? 1 : 0; } export function parseReport(input: unknown): ReportDocument { const value = object(input, "Report", ["schemaVersion", "id", "source", "configuration", "metrics", "findings"]); if (value.schemaVersion !== "1.0") throw new Error(`Unsupported report schema version: ${String(value.schemaVersion)}`); const sourceValue = object(value.source, "Report source", ["id", "tool", "adapterVersion", "repositoryId", "revision", "collectedAt"]); const source: ReportSource = { id: text(sourceValue.id, "Source id"), tool: text(sourceValue.tool, "Source tool"), adapterVersion: text(sourceValue.adapterVersion, "Adapter version"), repositoryId: text(sourceValue.repositoryId, "Repository id"), revision: text(sourceValue.revision, "Source revision"), collectedAt: text(sourceValue.collectedAt, "Collection time"), }; if (!/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{3})?Z$/.test(source.collectedAt) || !Number.isFinite(Date.parse(source.collectedAt)) || new Date(source.collectedAt).toISOString().replace(".000Z", "Z") !== source.collectedAt.replace(".000Z", "Z")) { throw new Error("Collection time must be an ISO UTC timestamp"); } const configuration = json(object(value.configuration, "Report configuration", Object.keys( typeof value.configuration === "object" && value.configuration !== null ? value.configuration : {}, ))); if (typeof configuration !== "object" || configuration === null || Array.isArray(configuration)) { throw new Error("Report configuration must be an object"); } const metrics = array(value.metrics, "Report metrics").map((inputMetric): ReportMetric => { const metric = object(inputMetric, "Metric", ["path", "key", "value", "unit"]); const metricPath = path(metric.path); const key = text(metric.key, "Metric key"); if (typeof metric.value !== "number" || !Number.isFinite(metric.value)) throw new Error(`Metric ${key} must be finite`); const unit = metric.unit; if (unit !== "count" && unit !== "percent" && unit !== "ratio" && unit !== "milliseconds") throw new Error(`Unsupported unit: ${String(unit)}`); if (metric.value < 0 || (unit === "percent" && metric.value > 100) || (unit === "ratio" && metric.value > 1) || (unit === "count" && !Number.isSafeInteger(metric.value))) throw new Error(`Metric ${key} is outside its unit range`); return { path: metricPath, key, value: metric.value, unit }; }).sort((a, b) => compare(`${a.path}\0${a.key}`, `${b.path}\0${b.key}`)); const findings = array(value.findings, "Report findings").map((inputFinding): ReportFinding => { const finding = object(inputFinding, "Finding", ["id", "path", "severity", "message"]); const severity = finding.severity; if (severity !== "info" && severity !== "warning" && severity !== "error") throw new Error("Invalid finding severity"); return { id: text(finding.id, "Finding id"), path: path(finding.path), severity, message: text(finding.message, "Finding message") }; }).sort((a, b) => compare(a.id, b.id)); if (new Set(metrics.map((metric) => `${metric.path}\0${metric.key}`)).size !== metrics.length) throw new Error("Duplicate report metric"); if (new Set(findings.map((finding) => finding.id)).size !== findings.length) throw new Error("Duplicate report finding"); return { schemaVersion: "1.0", id: text(value.id, "Report id"), source, configuration, metrics, findings }; } export function serializeReport(report: ReportDocument): string { return serializeJson(reportJson(parseReport(report))); } function reportJson(report: ReportDocument): JsonValue { return { ...report, source: { ...report.source }, metrics: report.metrics.map((metric) => ({ ...metric })), findings: report.findings.map((finding) => ({ ...finding })), }; } export function normalizeReports(inputs: unknown[], graph: GraphDocument): DashboardDocument { if (!inputs.length) throw new Error("At least one report is required"); const revision = graph.repository.revision; if (!revision) throw new Error("Report ingestion requires a versioned repository revision"); const reports = new Map<string, { report: ReportDocument; fingerprint: string }>(); const nodesByPath = new Map(graph.nodes.filter((node) => node.identity.kind === "path").map((node) => [node.identity.value, node.id])); for (const input of inputs) { const report = parseReport(input); if (report.source.repositoryId !== graph.repository.id) throw new Error(`Report ${report.id} belongs to another repository`); if (report.source.revision !== revision) throw new Error(`Stale report ${report.id}: expected revision ${revision}, received ${report.source.revision}`); const fingerprint = createHash("sha256").update(serializeReport(report)).digest("hex"); const prior = reports.get(report.id); if (prior && prior.fingerprint !== fingerprint) throw new Error(`Conflicting report id: ${report.id}`); reports.set(report.id, { report, fingerprint }); } const dashboard: DashboardDocument = { schemaVersion: "1.0", repositoryId: graph.repository.id, revision, inputs: [], metrics: [], findings: [], }; const metricEvidence = new Map<string, { value: number; unit: ReportMetric["unit"]; reportId: string }>(); for (const { report, fingerprint } of [...reports.values()].sort((a, b) => compare(a.report.id, b.report.id))) { dashboard.inputs.push({ id: report.id, source: { ...report.source }, configuration: report.configuration, fingerprint }); for (const metric of report.metrics) { const nodeId = nodesByPath.get(metric.path); if (!nodeId) throw new Error(`Report ${report.id} references unscanned file ${metric.path}`); const key = `${metric.path}\0${metric.key}`; const prior = metricEvidence.get(key); if (prior && (prior.value !== metric.value || prior.unit !== metric.unit)) { throw new Error(`Conflicting sources for ${metric.path} ${metric.key}: reports ${prior.reportId} and ${report.id}`); } metricEvidence.set(key, { value: metric.value, unit: metric.unit, reportId: report.id }); dashboard.metrics.push({ ...metric, reportId: report.id, sourceId: report.source.id, nodeId, provenance: "observed" }); } for (const finding of report.findings) { const nodeId = nodesByPath.get(finding.path); if (!nodeId) throw new Error(`Report ${report.id} references unscanned file ${finding.path}`); dashboard.findings.push({ ...finding, reportId: report.id, sourceId: report.source.id, nodeId, provenance: "observed" }); } } return dashboard; } export function serializeDashboard(dashboard: DashboardDocument): string { return serializeJson({ ...dashboard, inputs: dashboard.inputs.map((input) => ({ ...input, source: { ...input.source } })), metrics: dashboard.metrics.map((metric) => ({ ...metric })), findings: dashboard.findings.map((finding) => ({ ...finding })), }); } -
@topo/cli
Open @topo/cli: public surfaceOpen Dependency context by role: Build, test, docsOpen Dependency context by role: Development toolsOpen @topo/diagram-core: public surface: @topo/cliOpen From idea to architecture: Topocode CLI
Implements init, scan, story validation/preview, serving, bundling, ingestion, and explicit enrichment; generates Home and story navigation. All declared internal dependencies (not runtime calls): @topo/cli -> @topo/diagram-core [dependencies: workspace:*] @topo/cli -> @topo/enrichment [dependencies: workspace:*] @topo/cli -> @topo/graph [dependencies: workspace:*] @topo/cli -> @topo/languages [dependencies: workspace:*] @topo/cli -> @topo/modules [dependencies: workspace:*] @topo/cli -> @topo/reports [dependencies: workspace:*] @topo/cli -> @topo/scanner [dependencies: workspace:*] @topo/cli -> @topo/schema [dependencies: workspace:*] @topo/cli -> @topo/site [dependencies: workspace:*] @topo/cli -> @topo/story [dependencies: workspace:*] @topo/cli -> @topo/views [dependencies: workspace:*] @topo/cli -> @topo/workspace [dependencies: workspace:*] Referenced by internal modules: No matching declarations. Stable drilldown identity: cli. Open this module's public-surface diagram to inspect its exposed interfaces and references.
packages/cli/package.json:1-40 { "name": "@topo/cli", "version": "0.0.0", "private": true, "type": "module", "bin": { "topo": "./dist/main.js" }, "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } }, "scripts": { "build": "tsc -b && node ../../scripts/package-skills.mjs", "typecheck": "tsc -b --pretty false", "test": "vitest run --no-file-parallelism" }, "dependencies": { "@topo/diagram-core": "workspace:*", "@topo/enrichment": "workspace:*", "@topo/graph": "workspace:*", "@topo/languages": "workspace:*", "@topo/modules": "workspace:*", "@topo/reports": "workspace:*", "@topo/scanner": "workspace:*", "@topo/schema": "workspace:*", "@topo/site": "workspace:*", "@topo/story": "workspace:*", "@topo/views": "workspace:*", "@topo/workspace": "workspace:*" }, "devDependencies": { "@types/node": "^22.20.1", "typescript": "^7.0.2", "vite": "8.2.2", "vitest": "^5.0.0" } }packages/cli/src/main.ts:1-289 #!/usr/bin/env node import { dirname, resolve } from "node:path"; import { readFile, readdir, stat } from "node:fs/promises"; import { fileURLToPath, pathToFileURL } from "node:url"; import { parseArgs } from "node:util"; import { BUILTIN_MODULE_MANIFESTS, validateModuleCatalog } from "@topo/modules"; import { initializeWorkspace, isMissing, workspacePath } from "@topo/workspace"; import { runEnrichment } from "./enrichment.js"; import { bundleSite } from "./bundle.js"; import { generateArtifacts, ingestReports } from "./pipeline.js"; import { serveSite } from "./server.js"; import { assertCatalogueCurrent, buildCatalogue, initializeSite, writeBuiltCatalogue, } from "./catalogue.js"; import { validateStory } from "./story-validation.js"; import { prepareProjectSkills } from "./skills.js"; const HELP = `Topocode: local, deterministic repository maps topo init [repository] [--skills] topo scan [repository] [--allow-partial] [--responsibilities path] topo ingest <repository> <report.json> [more.json ...] topo enrich [repository] topo preview <repository> <story> topo story validate <repository> <story> topo story preview <repository> <story> topo serve [repository] [--port 4173] topo bundle [repository] [--output directory] [--base-path /path/] Requires a Git repository and Node.js 22 or newer. Scan is strict by default. --allow-partial publishes a visibly incomplete preview and exits 2; it never turns partial evidence into success. Serve binds only to 127.0.0.1. Config and authored metadata are never overwritten. Init creates a ready-to-serve diagram home without scanning or opening a browser. Scan adds repository exploration; story preview refreshes all committed stories. `; async function siteAssets(): Promise<string> { const assets = dirname(fileURLToPath(import.meta.resolve("@topo/site/notices"))); try { await Promise.all(["LICENSE.txt", "THIRD_PARTY_NOTICES.txt"].map((name) => readFile(resolve(assets, name)))); } catch (error) { if (!isMissing(error)) throw error; throw new Error("Built site license notices are missing; rebuild the Topocode checkout or reinstall @jdylanmc/topo-code."); } return assets; } export function validateConfiguredModules(modules: readonly string[]): void { validateModuleCatalog(BUILTIN_MODULE_MANIFESTS); const duplicates = modules.filter((id, index) => modules.indexOf(id) !== index); if (duplicates.length) { throw new Error(`Duplicate configured modules: ${[...new Set(duplicates)].join(", ")}`); } const builtins = new Set<string>(BUILTIN_MODULE_MANIFESTS.map((manifest) => manifest.id)); const unknown = modules.filter((id) => !builtins.has(id)); if (unknown.length) throw new Error(`Unknown configured modules: ${unknown.join(", ")}`); } export async function runCli(args: string[]): Promise<number> { const { positionals, values } = parseArgs({ args, allowPositionals: true, options: { help: { type: "boolean", short: "h" }, skills: { type: "boolean" }, port: { type: "string" }, "allow-partial": { type: "boolean" }, responsibilities: { type: "string" }, output: { type: "string" }, "base-path": { type: "string" }, }, }); const primaryCommand = positionals[0] ?? "help"; const command = primaryCommand === "story" ? `story-${positionals[1] ?? ""}` : primaryCommand; if (values.help || command === "help") { console.log(HELP); return 0; } if (![ "init", "scan", "ingest", "enrich", "preview", "serve", "bundle", "story-validate", "story-preview", ].includes(command)) { throw new Error(`Unknown command: ${positionals.slice(0, 2).join(" ")}`); } if (values.port !== undefined && command !== "serve") throw new Error("--port is only valid with serve"); if (values.skills !== undefined && command !== "init") throw new Error("--skills is only valid with init"); if (values.output !== undefined && command !== "bundle") throw new Error("--output is only valid with bundle"); if (values["base-path"] !== undefined && command !== "bundle") throw new Error("--base-path is only valid with bundle"); if (values["allow-partial"] !== undefined && command !== "scan") throw new Error("--allow-partial is only valid with scan"); if (values.responsibilities !== undefined && command !== "scan") throw new Error("--responsibilities is only valid with scan"); const storyCommand = command === "story-validate" || command === "story-preview"; if ( (!["ingest", "preview"].includes(command) && !storyCommand && positionals.length > 2) || (storyCommand && positionals.length > 4) ) { throw new Error(`Too many arguments for ${primaryCommand}`); } const root = resolve(positionals[storyCommand ? 2 : 1] ?? "."); if (command === "init") { const installSkills = values.skills ? await prepareProjectSkills(root) : undefined; const assets = await siteAssets(); const result = await initializeWorkspace(root); await initializeSite(root, assets); if (installSkills) console.log(`Installed ${await installSkills()} project skill/instruction files (identical files preserved).`); console.log(`${result.created ? "Initialized" : "Preserved"} ${root}/.topo`); console.log(`Site ready; run topo serve "${root}"`); return 0; } if (command === "serve") { if (values.port !== undefined && !/^\d+$/.test(values.port)) throw new Error("--port must be a nonnegative integer"); const { server, url } = await serveSite(root, values.port === undefined ? 4173 : Number(values.port)); console.log(`Topocode: ${url}`); let stopping = false; const stop = () => { if (stopping) return; stopping = true; server.close((error) => { if (error) { console.error(error.message); process.exitCode = 1; } }); server.closeAllConnections(); }; process.once("SIGINT", stop); process.once("SIGTERM", stop); return 0; } if (command === "bundle") { const output = values.output === undefined ? await workspacePath(root, "bundle") : resolve(values.output); const result = await bundleSite(root, output, { ...(values["base-path"] === undefined ? {} : { basePath: values["base-path"] }), }); console.log( `Bundled static site in ${result.siteDirectory} for ${result.basePath}`, ); return 0; } if (command === "story-validate") { if (positionals.length !== 4) { throw new Error( "story validate requires a repository and one story document", ); } const result = await validateStory(root, resolve(positionals[3]!)); console.log( `Validated ${result.documentPath} (${result.document.id}) against ${result.source.revision}${result.source.dirty ? " with working tree changes" : ""}`, ); for (const anchor of result.anchors) { console.log( ` ${anchor.id}: ${anchor.path}:${anchor.location.startLine}-${anchor.location.endLine}`, ); } console.warn( "Structural and anchor validation does not establish semantic accuracy or complete explanation coverage.", ); return 0; } if (command === "preview" || command === "story-preview") { const documentIndex = storyCommand ? 3 : 2; if (positionals.length !== (storyCommand ? 4 : 3)) { throw new Error( `${primaryCommand === "story" ? "story preview" : "preview"} requires a repository and one story document`, ); } try { if (!(await stat(await workspacePath(root, "cache/site/index.html"))).isFile()) { throw new Error("Site index is not a file; repair the generated site before preview"); } } catch (error) { if (!isMissing(error)) throw error; throw new Error("Site is not initialized; run topo init first"); } const catalogue = await buildCatalogue(root); const requested = resolve(positionals[documentIndex]!); const selected = catalogue.stories.find( (story) => resolve(root, story.documentPath) === requested, ); if (selected === undefined) { throw new Error("preview requires a committed story under stories/"); } const { config } = await initializeWorkspace(root); await writeBuiltCatalogue( root, catalogue, { title: config.repositoryId, ...config.catalogue }, ); if (catalogue.source.dirty) { console.warn( "Rendering against uncommitted source changes; anchors describe the working tree, not only HEAD.", ); } console.log( `Rendered ${selected.documentPath} with ${selected.renderer.name} (${selected.renderer.pin})`, ); console.log( `Story generated at ${await workspacePath(root, `cache/site/stories/${selected.document.id}/index.html`)}; run topo serve "${root}" and open /stories/${selected.document.id}/`, ); return 0; } if (command === "enrich") { const controller = new AbortController(); const abort = () => controller.abort(); process.once("SIGINT", abort); process.once("SIGTERM", abort); try { const result = await runEnrichment(root, { signal: controller.signal }); console.log(`Enriched analysis ${result.analysisHash}; ${result.comments} comments`); return 0; } finally { process.removeListener("SIGINT", abort); process.removeListener("SIGTERM", abort); } } const { config } = await initializeWorkspace(root); validateConfiguredModules(config.modules); const assets = await siteAssets(); const catalogue = await buildCatalogue(root); const state = catalogue.source; if (command === "ingest") { if (positionals.length < 3) throw new Error("ingest requires a repository and at least one report file"); if (state.dirty) throw new Error("Report ingestion requires clean source files at the scanned revision; .topo artifacts are excluded."); const result = await ingestReports( root, positionals.slice(2).map((path) => resolve(path)), assets, state.revision, catalogue, ); for (const warning of result.warnings) console.warn(warning); console.log(`Ingested ${result.dashboard.inputs.length} reports; ${result.dashboard.metrics.length} metrics, ${result.dashboard.findings.length} findings`); return 0; } if (state.dirty) { console.warn("Scanning uncommitted source changes; node fingerprints describe the working tree, not only HEAD."); if ((await readdir(await workspacePath(root, "reports/inputs"))).some((name) => name.endsWith(".json"))) { throw new Error("Persisted revision-bound reports cannot be applied to dirty source; commit changes or move stale inputs first."); } } const start = performance.now(); const { scanProject } = await import("@topo/languages"); const result = await scanProject({ root, repositoryId: config.repositoryId, revision: state.revision, quality: { allowPartial: values["allow-partial"] ?? false }, ...(values.responsibilities ? { responsibilityFile: resolve(values.responsibilities) } : {}), }, config.analysis); await assertCatalogueCurrent(root, catalogue); const artifacts = await generateArtifacts( root, result.graph, assets, result.logicalArchitecture, catalogue, ); for (const diagnostic of result.diagnostics) console.warn(`${diagnostic.severity}: ${diagnostic.code}: ${diagnostic.message}`); for (const warning of artifacts.layout.warnings) console.warn(`layout: ${warning.code}: ${warning.message}`); for (const warning of artifacts.warnings) console.warn(warning); console.log(`${result.authoritative ? "Scanned" : "PARTIAL PREVIEW:"} ${result.metrics.sourceFileCount} files, ${result.graph.edges.length} edges, ${result.metrics.linesOfCode} lines in ${((performance.now() - start) / 1000).toFixed(2)}s`); console.log(`Site generated in ${root}/.topo/cache/site; run topo serve "${root}"`); return result.authoritative ? 0 : 2; } export function describeError(error: unknown): string { const message = error instanceof Error ? error.message : String(error); if (typeof error !== "object" || error === null) return message; const details = "diagnostics" in error ? error.diagnostics : "issues" in error ? error.issues : undefined; return Array.isArray(details) ? `${message}\n${details.map((item: unknown) => JSON.stringify(item)).join("\n")}` : message; } if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) { runCli(process.argv.slice(2)).then((code) => { process.exitCode = code; }).catch((error: unknown) => { console.error(`Topocode: ${describeError(error)}`); process.exitCode = 1; }); } -
@topo/diagram-core
Open @topo/diagram-core: public surfaceOpen @topo/cli: public surface: @topo/diagram-coreOpen Dependency context by role: RenderingOpen Dependency context by role: Archify and GraphvizOpen Dependency context by role: Build, test, docsOpen Dependency context by role: Development tools
Adapts source-grounded stories to the pinned Archify renderer or explicit Graphviz backend; owns renderer integrity, output adaptation, and viewer theme/export behavior. All declared internal dependencies (not runtime calls): @topo/diagram-core -> @topo/story [dependencies: workspace:*] Referenced by internal modules: @topo/cli -> @topo/diagram-core [dependencies: workspace:*] @topo/site -> @topo/diagram-core [devDependencies: workspace:*] Stable drilldown identity: diagram-core. Open this module's public-surface diagram to inspect its exposed interfaces and references.
packages/diagram-core/package.json:1-34 { "name": "@topo/diagram-core", "version": "0.0.0", "private": true, "type": "module", "files": [ "dist", "archify-pin.json", "LICENSE", "THIRD_PARTY_NOTICES.md" ], "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } }, "scripts": { "build": "tsc -b", "typecheck": "tsc -b --pretty false", "test": "tsc -b && node --test test/*.test.mjs" }, "dependencies": { "@topo/story": "workspace:*", "@viz-js/viz": "3.30.0" }, "peerDependencies": { "@jdylanmc/topo-archify": "0.1.0" }, "devDependencies": { "@types/node": "^22", "typescript": "^7.0.2" } }packages/diagram-core/src/index.ts:1-1708 import { execFileSync } from "node:child_process"; import { createHash } from "node:crypto"; import { mkdtempSync, readFileSync, rmSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import type { ResolvedStoryDocument, StoryArtifact, StoryConnection, StoryDocument, } from "@topo/story"; import { cliPath as archifyCli } from "@jdylanmc/topo-archify"; import { verifyArchifyIntegrity } from "./integrity.js"; import { adaptViewerTheme, outputHash } from "./theme.js"; import { renderGraphviz } from "./graphviz.js"; import { componentId } from "./node-ids.js"; export { CORE_THEME_SCRIPT } from "./theme.js"; export { storyNodeIds } from "./node-ids.js"; export { verifyArchifyIntegrity, type ArchifyIntegrity, } from "./integrity.js"; interface ArchifySource { readonly path: string; readonly line?: number; readonly end_line?: number; } interface ArchifyComponent { readonly id: string; readonly type: "backend" | "frontend"; readonly label: string; readonly sublabel: string; readonly pos: readonly [number, number]; readonly size: readonly [number, number]; readonly sources?: readonly ArchifySource[]; } interface ArchifyArchitecture { readonly schema_version: 1; readonly diagram_type: "architecture"; readonly meta: { readonly title: string; readonly quality_profile: "standard"; readonly locale: "en"; readonly viewBox: readonly [number, number]; readonly legend: { readonly mode: "hidden" }; readonly repository?: { readonly url: string; readonly revision: string; readonly link_mode?: "local-only"; }; }; readonly components: readonly ArchifyComponent[]; readonly connections: readonly { readonly id: string; readonly from: string; readonly to: string; readonly label?: string; readonly labelAt?: readonly [number, number]; readonly labelDx?: number; readonly labelDy?: number; readonly labelSegment?: number; readonly fromSide?: "left" | "right" | "top" | "bottom"; readonly toSide?: "left" | "right" | "top" | "bottom"; readonly via?: readonly (readonly [number, number])[]; }[]; } interface ArchifyWorkflow { readonly schema_version: 2; readonly diagram_type: "workflow"; readonly meta: { readonly title: string; readonly quality_profile: "standard"; readonly locale: "en"; readonly legend: { readonly mode: "hidden" }; }; readonly lanes: readonly { readonly id: string; readonly label: string; }[]; readonly nodes: readonly { readonly id: string; readonly lane: string; readonly col: number; readonly type: "backend" | "frontend"; readonly label: string; readonly sublabel: string; readonly width: number; readonly height: number; readonly yOffset: number; }[]; readonly edges: readonly { readonly id: string; readonly from: string; readonly to: string; readonly label?: string; readonly labelDx?: number; readonly labelDy?: number; readonly role: "main"; }[]; } interface ArchifySequence { readonly schema_version: 1; readonly diagram_type: "sequence"; readonly meta: { readonly title: string; readonly quality_profile: "standard"; readonly locale: "en"; readonly legend: { readonly mode: "hidden" }; readonly column_fit: "spread"; readonly viewBox: readonly [number, number]; }; readonly participants: readonly { readonly id: string; readonly type: "backend" | "frontend"; readonly label: string; readonly sublabel: string; }[]; readonly messages: readonly { readonly id: string; readonly from: string; readonly to: string; readonly y: number; readonly label: string; readonly variant?: "return"; }[]; } interface ArchifyDataflow { readonly schema_version: 1; readonly diagram_type: "dataflow"; readonly meta: { readonly title: string; readonly quality_profile: "standard"; readonly locale: "en"; readonly viewBox: readonly [number, number]; readonly legend: { readonly mode: "hidden" }; }; readonly stages: readonly { readonly label: string; }[]; readonly nodes: readonly { readonly id: string; readonly type: "backend" | "frontend"; readonly label: string; readonly sublabel: string; readonly stage: number; readonly row: number; readonly width: number; }[]; readonly flows: readonly { readonly id: string; readonly from: string; readonly to: string; readonly label: string; readonly labelDy?: number; }[]; } interface ArchifyLifecycle { readonly schema_version: 1; readonly diagram_type: "lifecycle"; readonly meta: { readonly title: string; readonly quality_profile: "standard"; readonly locale: "en"; readonly legend: { readonly mode: "hidden" }; }; readonly lanes: readonly { readonly id: string; readonly label: string; }[]; readonly states: readonly { readonly id: string; readonly type: "start" | "active" | "success"; readonly label: string; readonly lane: "main" | "event" | "terminal"; readonly col: number; readonly width: number; }[]; readonly transitions: readonly { readonly id: string; readonly from: string; readonly to: string; readonly label?: string; readonly labelDy?: number; readonly route?: "right-channel"; }[]; } const architectureFontSize = 24; // The pinned renderer measures 8px monospace labels at 0.6em per text unit; // authored Architecture CSS scales the same glyphs to 24px. const architectureLabelAscent = 25; const architectureLabelDescent = 7; function architectureLabelUnits(label: string): number { return Array.from(label).reduce( (units, character) => units + ((character.codePointAt(0) ?? 0) > 0xff ? 2 : 1), 0, ); } function architectureLabelWidth(label: string): number { const units = architectureLabelUnits(label); return Math.max( 30, units * 4.8 + 10, units * architectureFontSize * 0.6, ); } const sequenceReadability = { minimumViewBoxWidth: 480, minimumViewBoxHeight: 480, messageStartY: 180, minimumMessageSpacing: 28, timelineBottomPadding: 83, sideMargin: 62, participantGapAllowance: 24, minimumParticipantGap: 108, rightMargin: 40, maximumParticipantWidth: 190, minimumParticipantWidth: 86, nativeLabelWidthFactor: 6.8, nativeLabelTolerance: 6, textHorizontalPadding: 8, textWidthFactor: 0.6, targetEffectiveFontSize: 12, maximumNativeFontSize: 13, viewerBodyHorizontalPadding: 64, compactViewerDiagramPadding: 32, regularViewerDiagramPadding: 48, compactViewerMaximumHeight: 920, maximumReaderWidth: 1440, supportedViewports: [ [1024, 768], [1280, 720], [1440, 900], [1600, 1000], [1920, 1080], ], } as const; interface SequenceLayout { readonly viewBox: readonly [number, number]; readonly fontSize: number; readonly participantWidth: number; readonly inlineBodyIds: ReadonlySet<string>; readonly messageYs: readonly number[]; } function stableId(prefix: string, value: string): string { const hash = createHash("sha256").update(value).digest("hex").slice(0, 16); return `${prefix}_${hash}`; } // Match the pinned renderer's text-unit model before deciding whether prose // can remain inline at the final readable font size. const fullwidthCharacter = /[\u1100-\u115F\u231A-\u231B\u2329-\u232A\u23E9-\u23EC\u23F0\u23F3\u25FD-\u25FE\u2614-\u2615\u2630-\u2637\u2648-\u2653\u267F\u268A-\u268F\u2693\u26A1\u26AA-\u26AB\u26BD-\u26BE\u26C4-\u26C5\u26CE\u26D4\u26EA\u26F2-\u26F3\u26F5\u26FA\u26FD\u2705\u270A-\u270B\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B-\u2B1C\u2B50\u2B55\u2E80-\uA4CF\uA960-\uA97C\uAC00-\uD7A3\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE6F\uFF01-\uFF60\uFFE0-\uFFE6\u{16FE0}-\u{18DFF}\u{1AFF0}-\u{1AFFF}\u{1B000}-\u{1B2FF}\u{1F000}-\u{1FAFF}\u{20000}-\u{3FFFD}]/u; function sequenceTextUnits(text: string): number { const characters = Array.from(text); let units = 0; for (let index = 0; index < characters.length; index += 1) { const codePoint = characters[index]!.codePointAt(0)!; if (codePoint >= 0xfe00 && codePoint <= 0xfe0f) continue; const next = characters[index + 1]?.codePointAt(0) ?? -1; if (next === 0xfe0f) units += 2; else if (next === 0xfe0e) units += 1; else units += fullwidthCharacter.test(characters[index]!) ? 2 : 1; } return units; } function requiredSequenceParticipantWidth(text: string): number { return Math.ceil( sequenceTextUnits(text) * sequenceReadability.nativeLabelWidthFactor - sequenceReadability.nativeLabelTolerance, ); } function fittedSequenceFontSize(text: string, width: number): number { const units = Math.max(1, sequenceTextUnits(text)); const available = Math.max( 1, width - sequenceReadability.textHorizontalPadding, ); return Math.floor( Math.min( sequenceReadability.maximumNativeFontSize, available / (units * sequenceReadability.textWidthFactor), ) * 10, ) / 10; } function sequenceLayout(story: ResolvedStoryDocument): SequenceLayout { const participantCount = Math.max(1, story.document.sections.length); const requiredParticipantWidth = Math.min( sequenceReadability.maximumParticipantWidth, Math.max( sequenceReadability.minimumParticipantWidth, ...story.document.sections.map(({ title }) => requiredSequenceParticipantWidth(title) ), ), ); const viewBoxWidth = Math.max( sequenceReadability.minimumViewBoxWidth, participantCount * (requiredParticipantWidth + sequenceReadability.participantGapAllowance) + sequenceReadability.sideMargin * 2, sequenceReadability.sideMargin + requiredParticipantWidth + (participantCount - 1) * (story.document.sequenceLayout?.minimumParticipantGap ?? sequenceReadability.minimumParticipantGap) + sequenceReadability.rightMargin, ); const messageYs = story.document.connections.map((_, index) => sequenceReadability.messageStartY + index * sequenceReadability.minimumMessageSpacing ); const lastMessageY = messageYs.at(-1) ?? sequenceReadability.messageStartY; const viewBox = [ viewBoxWidth, Math.max( sequenceReadability.minimumViewBoxHeight, lastMessageY + sequenceReadability.timelineBottomPadding, ), ] as const; const minimumScale = Math.min( ...sequenceReadability.supportedViewports.map(([width, height]) => { const readerWidth = Math.min( width - sequenceReadability.viewerBodyHorizontalPadding, sequenceReadability.maximumReaderWidth, ); const diagramPadding = height <= sequenceReadability.compactViewerMaximumHeight ? sequenceReadability.compactViewerDiagramPadding : sequenceReadability.regularViewerDiagramPadding; return Math.min( (readerWidth - diagramPadding) / viewBox[0], height / viewBox[1], ); }), ); const fontSize = Math.ceil( sequenceReadability.targetEffectiveFontSize / minimumScale * 10, ) / 10; if (fontSize > sequenceReadability.maximumNativeFontSize) { throw new Error( `Sequence story requires ${fontSize}px native text to remain ` + `${sequenceReadability.targetEffectiveFontSize}px effective across ` + "supported viewports; reduce the message count or split the story", ); } const participantWidth = Math.max( sequenceReadability.minimumParticipantWidth, Math.min( sequenceReadability.maximumParticipantWidth, Math.round( (viewBox[0] - sequenceReadability.sideMargin * 2) / participantCount, ) - sequenceReadability.participantGapAllowance, ), ); const inlineBodyIds = new Set<string>(); for (const section of story.document.sections) { if (fittedSequenceFontSize(section.title, participantWidth) < fontSize) { throw new Error( `Sequence participant "${section.title}" cannot fit inside its ` + `${participantWidth}px box at the ${fontSize}px native font ` + `required for ${sequenceReadability.targetEffectiveFontSize}px ` + "effective text", ); } if (fittedSequenceFontSize(section.body, participantWidth) >= fontSize) { inlineBodyIds.add(section.id); } } return { viewBox, fontSize, participantWidth, inlineBodyIds, messageYs, }; } function escapeHtml(value: string): string { return value .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); } function addSequenceNarrativeDetails( contents: string, story: ResolvedStoryDocument, layout: SequenceLayout, ): string { let result = contents; for (const section of story.document.sections) { if (layout.inlineBodyIds.has(section.id)) continue; const id = componentId(section.id); const marker = `data-node-id="${id}"`; const markerIndex = result.indexOf(marker); const tagStart = result.lastIndexOf("<g ", markerIndex); const tagEnd = result.indexOf(">", markerIndex); if (markerIndex < 0 || tagStart < 0 || tagEnd < 0) { throw new Error( `Archify sequence output is missing participant "${section.id}"`, ); } const body = escapeHtml(section.body); const openTag = result.slice(tagStart, tagEnd); const labelledTag = openTag.replace( / aria-label="([^"]*)"/, (_, label: string) => ` aria-label="${label}, ${body}"`, ); if (labelledTag === openTag) { throw new Error( `Archify sequence output is missing participant label "${section.id}"`, ); } result = result.slice(0, tagStart) + `${labelledTag} data-node-sublabel="${body}"` + result.slice(tagEnd); const titleStart = result.indexOf("<title>", tagEnd); const titleEnd = result.indexOf("</title>", titleStart); if (titleStart < 0 || titleEnd < 0) { throw new Error( `Archify sequence output is missing participant title "${section.id}"`, ); } const titleContentsStart = titleStart + "<title>".length; result = result.slice(0, titleContentsStart) + `${result.slice(titleContentsStart, titleEnd)} · ${body}` + result.slice(titleEnd); } return result; } function repositoryMetadata(repositoryRoot: string): { readonly url: string; readonly link_mode?: "local-only"; } { try { const url = execFileSync( "git", ["-C", repositoryRoot, "remote", "get-url", "origin"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }, ).trim(); if (url) return { url }; } catch { // Let Archify report its repository-evidence requirement consistently. } return { url: "https://localhost.invalid/repository", link_mode: "local-only", }; } export function architectureComponentWidth( title: string, sublabel = "", finalTypography = false, ): number { const units = Array.from(title).reduce( (total, character) => total + (character.codePointAt(0)! > 127 ? 2 : 1), 0, ); const titleWidth = finalTypography ? units * 24 * 0.6 : title.length * 11; return Math.max(280, titleWidth + 80, sublabel.length * 9 + 80); } function archifySpec( story: ResolvedStoryDocument, nativeSourceEvidence = true, ): ArchifyArchitecture { const anchors = new Map(story.anchors.map((anchor) => [anchor.id, anchor])); const componentIds = new Map( story.document.sections.map((section) => [ section.id, componentId(section.id), ]), ); const sections = story.document.sections; const resolved = sections.map((section) => { const sectionAnchors = section.anchorIds .map((anchorId) => anchors.get(anchorId)) .filter((anchor): anchor is NonNullable<typeof anchor> => anchor !== undefined); const primary = sectionAnchors[0]; // Short, legible sublabel (a code reference), never the full narrative body: // Archify enforces a per-component minimum-legibility width. const sublabel = section.summary ?? (primary === undefined || !nativeSourceEvidence ? "" : primary.symbol ?? primary.path.split("/").pop() ?? primary.path); const width = architectureComponentWidth(section.title, sublabel, !nativeSourceEvidence); return { section, sectionAnchors, sublabel, width }; }); // Balance components across rows of at most four, then snake each row so // consecutive sections stay adjacent without producing a tall column. const maxRowSize = sections.length >= 10 ? 4 : 3; const rowCount = Math.max(1, Math.ceil(sections.length / maxRowSize)); const shortRowSize = Math.floor(sections.length / rowCount); const longRowCount = sections.length % rowCount; const rowSizes = Array.from( { length: rowCount }, (_, row) => shortRowSize + (row < longRowCount ? 1 : 0), ); const baseBoxWidth = Math.max(280, ...resolved.map((entry) => entry.width)); const boxHeight = 130; const columnGap = 90; const cellOf = (index: number) => { let row = 0; let rowStart = 0; while (index >= rowStart + rowSizes[row]!) { rowStart += rowSizes[row]!; row += 1; } const rowSize = rowSizes[row]!; const positionInRow = index - rowStart; // Reverse odd rows so the sequence snakes and stays adjacent at the wrap. const column = row % 2 === 0 ? positionInRow : rowSize - 1 - positionInRow; return { row, column }; }; const sectionIndexById = new Map( sections.map((section, index) => [section.id, index]), ); // Plan label and route corridors before placing rows so shared gaps reserve // only the space their realized content and detours require. const sharedGapConnections = new Map<number, number[]>(); const crossRowRouteGaps = new Set<number>(); const detourLaneByConnection = new Map<number, number>(); let detourLaneCount = 0; let hasBottomOuterLabels = false; for ( const [connectionIndex, connection] of story.document.connections.entries() ) { const fromIndex = sectionIndexById.get(connection.from)!; const toIndex = sectionIndexById.get(connection.to)!; const from = cellOf(fromIndex); const to = cellOf(toIndex); const neighbouringDiagonal = Math.abs(to.row - from.row) === 1 && Math.abs(to.column - from.column) === 1; if ( Math.abs(to.row - from.row) + Math.abs(to.column - from.column) !== 1 && !neighbouringDiagonal ) { detourLaneByConnection.set(connectionIndex, detourLaneCount); detourLaneCount += 1; } for ( let gap = Math.min(from.row, to.row); gap < Math.max(from.row, to.row); gap += 1 ) { crossRowRouteGaps.add(gap); } if (connection.label === undefined) continue; const deltaColumn = to.column - from.column; if (from.row !== to.row || Math.abs(deltaColumn) !== 1) continue; const gap = deltaColumn > 0 ? from.row : from.row - 1; if (gap === rowCount - 1) { hasBottomOuterLabels = true; } if (gap < 0 || gap >= rowCount - 1) continue; const connections = sharedGapConnections.get(gap) ?? []; connections.push(connectionIndex); sharedGapConnections.set(gap, connections); } const sharedGapLaneByConnection = new Map( [...sharedGapConnections.entries()].flatMap(([gap, connections]) => connections.map((connectionIndex, lane) => [ connectionIndex, { gap, lane }, ] as const) ), ); const labelLaneStep = architectureLabelAscent + architectureLabelDescent + 1; const labelCorridorPadding = 2; const routeCorridorBand = 8; const requiredRowGap = Math.max( 0, ...[...sharedGapConnections.entries()].map(([gap, connections]) => labelCorridorPadding * 2 + architectureLabelAscent + architectureLabelDescent + (connections.length - 1) * labelLaneStep + (crossRowRouteGaps.has(gap) ? routeCorridorBand : 0) ), ); const rowGap = Math.max( 80, requiredRowGap, ); const maxSharedLabelWidth = Math.max( 0, ...[...sharedGapLaneByConnection.keys()].map((connectionIndex) => architectureLabelWidth( story.document.connections[connectionIndex]!.label!, ) ), ); const maxConnectionLabelWidth = Math.max( 0, ...story.document.connections.flatMap((connection) => connection.label === undefined ? [] : [architectureLabelWidth(connection.label)] ), ); const boxWidth = Math.max( baseBoxWidth, Math.ceil(maxSharedLabelWidth + labelCorridorPadding * 2), ); const margin = Math.max( 80, Math.ceil(maxConnectionLabelWidth / 2 + labelCorridorPadding), ); const components = resolved.map(({ section, sectionAnchors, sublabel }, index) => { const { row, column } = cellOf(index); return { id: componentIds.get(section.id)!, type: "backend" as const, label: section.title, sublabel, pos: [ margin + column * (boxWidth + columnGap), margin + row * (boxHeight + rowGap), ] as const, size: [boxWidth, boxHeight] as const, ...(sectionAnchors.length === 0 || !nativeSourceEvidence ? {} : { // Native cards allow three links; the story wrapper retains every // validated anchor and its excerpt in the full evidence panel. sources: sectionAnchors.slice(0, 3).map((anchor) => ({ path: anchor.path, line: anchor.location.startLine, end_line: anchor.location.endLine, })), }), }; }); const gridBottom = margin + rowCount * boxHeight + (rowCount - 1) * rowGap; const laneBase = gridBottom + (hasBottomOuterLabels ? rowGap / 2 + architectureLabelAscent + architectureLabelDescent + 4 : 60); const laneGap = 40; const widestRow = Math.max(...rowSizes); const gridRight = margin + widestRow * boxWidth + (widestRow - 1) * columnGap; const gapRouteCorridors = new Map<number, Set<number>>(); for (const connection of story.document.connections) { const fromIndex = sectionIndexById.get(connection.from)!; const toIndex = sectionIndexById.get(connection.to)!; const from = cellOf(fromIndex); const to = cellOf(toIndex); if (from.row === to.row) continue; const fromX = components[fromIndex]!.pos[0] + boxWidth / 2; const toX = components[toIndex]!.pos[0] + boxWidth / 2; for ( let gap = Math.min(from.row, to.row); gap < Math.max(from.row, to.row); gap += 1 ) { const corridors = gapRouteCorridors.get(gap) ?? new Set<number>(); corridors.add(fromX); corridors.add(toX); gapRouteCorridors.set(gap, corridors); } } const sharedGapLabelX = ( connectionIndex: number, gap: number, ): number => { const connection = story.document.connections[connectionIndex]!; const fromIndex = sectionIndexById.get(connection.from)!; const sourceX = components[fromIndex]!.pos[0] + boxWidth / 2; const width = architectureLabelWidth(connection.label!); const routeClearance = 12; const corridorXs = [...(gapRouteCorridors.get(gap) ?? [])] .sort((left, right) => left - right); const intervals: [number, number][] = []; let left = margin / 2; for (const corridorX of corridorXs) { intervals.push([left, corridorX - routeClearance]); left = corridorX + routeClearance; } intervals.push([left, gridRight + margin / 2]); const candidates = intervals.flatMap(([intervalLeft, intervalRight]) => { const minCenter = intervalLeft + width / 2; const maxCenter = intervalRight - width / 2; if (minCenter > maxCenter) return []; return [Math.min(maxCenter, Math.max(minCenter, sourceX))]; }); if (candidates.length === 0) { throw new Error( `Architecture label "${connection.label}" cannot fit a clear row-gap corridor.`, ); } return candidates.reduce((best, candidate) => Math.abs(candidate - sourceX) < Math.abs(best - sourceX) ? candidate : best ); }; const viewBoxWidth = Math.max( 800, gridRight + margin, ); const viewBoxHeight = Math.max( 500, gridBottom + margin, laneBase + Math.max(0, detourLaneCount - 1) * laneGap + margin, ); return { schema_version: 1, diagram_type: "architecture", meta: { title: story.document.title, quality_profile: "standard", locale: "en", viewBox: [viewBoxWidth, viewBoxHeight], legend: { mode: "hidden" }, ...(story.document.classification === "capability-demo" || !nativeSourceEvidence ? {} : { repository: { ...repositoryMetadata(story.repositoryRoot), revision: story.source.revision, }, }), }, components, // Neighbouring cells use native routing and port spreading. Distant // endpoints retain outside lanes to avoid unrelated nodes. connections: story.document.connections.map((connection, index) => { const fromIndex = sections.findIndex((s) => s.id === connection.from); const toIndex = sections.findIndex((s) => s.id === connection.to); const from = cellOf(fromIndex); const to = cellOf(toIndex); const deltaRow = to.row - from.row; const deltaColumn = to.column - from.column; const base = { id: stableId( "connection", `${index}\0${connection.from}\0${connection.to}`, ), from: componentIds.get(connection.from)!, to: componentIds.get(connection.to)!, ...(connection.label === undefined ? {} : { label: connection.label }), }; const adjacent = Math.abs(deltaRow) + Math.abs(deltaColumn) === 1; if (Math.abs(deltaRow) === 1 && Math.abs(deltaColumn) === 1) { return base; } if (adjacent) { const vertical = deltaRow !== 0; const horizontalLabelBelow = deltaColumn > 0; const sharedGapLane = sharedGapLaneByConnection.get(index); return { ...base, ...(vertical ? { labelDy: deltaRow > 0 ? 40 : -24 } : sharedGapLane === undefined ? { labelDy: horizontalLabelBelow ? boxHeight / 2 + rowGap / 2 : -(boxHeight / 2 + rowGap / 2), } : { labelAt: [ sharedGapLabelX(index, sharedGapLane.gap), margin + sharedGapLane.gap * (boxHeight + rowGap) + boxHeight + (crossRowRouteGaps.has(sharedGapLane.gap) ? routeCorridorBand : 0) + labelCorridorPadding + architectureLabelAscent + sharedGapLane.lane * labelLaneStep, ] as const, }), fromSide: vertical ? (deltaRow > 0 ? "bottom" as const : "top" as const) : (deltaColumn > 0 ? "right" as const : "left" as const), toSide: vertical ? (deltaRow > 0 ? "top" as const : "bottom" as const) : (deltaColumn > 0 ? "left" as const : "right" as const), }; } const fromX = components[fromIndex]!.pos[0] + boxWidth / 2; const toX = components[toIndex]!.pos[0] + boxWidth / 2; const lane = laneBase + detourLaneByConnection.get(index)! * laneGap; const blockedBelow = (endpointIndex: number) => { const endpoint = cellOf(endpointIndex); return sections.some((_, candidateIndex) => { if (candidateIndex === endpointIndex) return false; const candidate = cellOf(candidateIndex); return candidate.column === endpoint.column && candidate.row > endpoint.row; }); }; const outside = (column: number) => column < widestRow / 2 ? margin / 2 : gridRight + margin / 2; const upperCorridor = (endpointIndex: number) => { const endpoint = cellOf(endpointIndex); if (endpoint.row === 0) return margin / 2; return margin + endpoint.row * (boxHeight + rowGap) - rowGap + routeCorridorBand / 2; }; const fromBlocked = blockedBelow(fromIndex); const toBlocked = blockedBelow(toIndex); const fromOutside = outside(from.column); const toOutside = outside(to.column); const fromUpperCorridor = upperCorridor(fromIndex); const toUpperCorridor = upperCorridor(toIndex); return { ...base, fromSide: fromBlocked ? "top" as const : "bottom" as const, toSide: toBlocked ? "top" as const : "bottom" as const, via: [ ...(fromBlocked ? [ [fromX, fromUpperCorridor], [fromOutside, fromUpperCorridor], [fromOutside, lane], ] as const : [[fromX, lane]] as const), ...(toBlocked ? [ [toOutside, lane], [toOutside, toUpperCorridor], [toX, toUpperCorridor], ] as const : [[toX, lane]] as const), ], labelSegment: fromBlocked ? 3 : 1, }; }), }; } function workflowSpec(story: ResolvedStoryDocument): ArchifyWorkflow { const laneId = "story-flow"; const columns = 2; const rows = Math.ceil(story.document.sections.length / columns); const cellOf = (index: number) => { const row = Math.floor(index / columns); const position = index % columns; return { row, col: row % 2 === 0 ? position : columns - 1 - position, }; }; const nodeIds = new Map( story.document.sections.map((section) => [ section.id, componentId(section.id), ]), ); return { schema_version: 2, diagram_type: "workflow", meta: { title: story.document.title, quality_profile: "standard", locale: "en", legend: { mode: "hidden" }, }, lanes: [{ id: laneId, label: story.document.title }], nodes: story.document.sections.map((section, index) => { const { row, col } = cellOf(index); return { id: nodeIds.get(section.id)!, lane: laneId, col, type: index === story.document.sections.length - 1 ? "frontend" : "backend", label: section.title, sublabel: "", width: Math.max( 128, section.title.length * 7 + 24, ), height: 96, yOffset: (row - (rows - 1) / 2) * 160, }; }), edges: story.document.connections.map((connection, index) => { const fromIndex = story.document.sections.findIndex( (section) => section.id === connection.from, ); const toIndex = story.document.sections.findIndex( (section) => section.id === connection.to, ); const from = cellOf(fromIndex); const to = cellOf(toIndex); return { id: stableId( "edge", `${index}\0${connection.from}\0${connection.to}`, ), from: nodeIds.get(connection.from)!, to: nodeIds.get(connection.to)!, ...(connection.label === undefined ? {} : { label: connection.label }), ...(from.col === to.col ? { labelDx: from.col === 0 ? -180 : 180 } : { labelDy: 72 }), role: "main", }; }), }; } function requiredConnectionLabel( connection: StoryConnection, family: "sequence" | "dataflow", ): string { if (connection.label === undefined) { throw new Error(`${family} connections require a nonempty label`); } return connection.label; } function sequenceSpec( story: ResolvedStoryDocument, layout: SequenceLayout, ): ArchifySequence { const participantIds = new Map( story.document.sections.map((section) => [ section.id, componentId(section.id), ]), ); return { schema_version: 1, diagram_type: "sequence", meta: { title: story.document.title, quality_profile: "standard", locale: "en", legend: { mode: "hidden" }, column_fit: "spread", viewBox: layout.viewBox, }, participants: story.document.sections.map((section) => ({ id: participantIds.get(section.id)!, type: "backend", label: section.title, sublabel: layout.inlineBodyIds.has(section.id) ? section.body : "", })), messages: story.document.connections.map((connection, index) => ({ id: stableId( "message", `${index}\0${connection.from}\0${connection.to}`, ), from: participantIds.get(connection.from)!, to: participantIds.get(connection.to)!, y: layout.messageYs[index]!, label: requiredConnectionLabel(connection, "sequence"), ...(connection.variant === undefined ? {} : { variant: connection.variant }), })), }; } const dataflowReadability = { maximumFontSize: 15, minimumFontSize: 6, fontSizePrecision: 10, widthFactor: 0.6, horizontalPadding: 8, defaultNodeWidth: 112, firstNodeMaximumWidth: 152, stageCenterGap: 215, minimumNodeGap: 10, firstStageCenterX: 100, stageWidth: 168, viewBoxMargin: 24, minimumViewBoxWidth: 360, viewBoxHeight: 360, flowLabelHorizontalPadding: 12, minimumFlowLabelWidth: 34, nativeFlowLabelWidthFactor: 4.9, flowLabelMaskBaselineTop: 11, flowLabelMaskBaselineBottom: 5, flowLabelFontAscentPadding: 1, flowLabelClearanceDy: 53, } as const; function dataflowTextUnits(value: string): number { return Array.from(value).reduce( (total, character) => total + (character.codePointAt(0)! > 0xff ? 2 : 1), 0, ); } function dataflowNodeWidth( label: string, sublabel: string, fontSize: number, ): number { const units = Math.max( dataflowTextUnits(label), dataflowTextUnits(sublabel), ); return Math.max( dataflowReadability.defaultNodeWidth, Math.ceil( units * fontSize * dataflowReadability.widthFactor + dataflowReadability.horizontalPadding, ), ); } interface DataflowLayout { readonly fontSize: number; readonly nodeWidths: readonly number[]; readonly flowLabelWidths: readonly number[]; readonly viewBox: readonly [number, number]; } function dataflowFlowLabelWidth(label: string, fontSize: number): number { return Math.max( dataflowReadability.minimumFlowLabelWidth, Math.ceil( ( dataflowTextUnits(label) * fontSize * dataflowReadability.widthFactor + dataflowReadability.flowLabelHorizontalPadding ) * dataflowReadability.fontSizePrecision, ) / dataflowReadability.fontSizePrecision, ); } function dataflowNativeFlowLabelWidth(label: string): number { return Math.round( Math.max( dataflowReadability.minimumFlowLabelWidth, dataflowTextUnits(label) * dataflowReadability.nativeFlowLabelWidthFactor + dataflowReadability.flowLabelHorizontalPadding, ) * dataflowReadability.fontSizePrecision, ) / dataflowReadability.fontSizePrecision; } function dataflowFlowLabelMaskTop(fontSize: number): number { return Math.max( dataflowReadability.flowLabelMaskBaselineTop, Math.ceil( fontSize + dataflowReadability.flowLabelFontAscentPadding, ), ); } function dataflowLayout(story: ResolvedStoryDocument): DataflowLayout { const sections = story.document.sections; const minimumTenths = dataflowReadability.minimumFontSize * dataflowReadability.fontSizePrecision; const maximumTenths = dataflowReadability.maximumFontSize * dataflowReadability.fontSizePrecision; let selected: | { readonly fontSize: number; readonly nodeWidths: readonly number[] } | undefined; for (let tenths = maximumTenths; tenths >= minimumTenths; tenths -= 1) { const fontSize = tenths / dataflowReadability.fontSizePrecision; const nodeWidths = sections.map((section) => dataflowNodeWidth(section.title, section.body, fontSize) ); const firstNodeFits = (nodeWidths[0] ?? dataflowReadability.defaultNodeWidth) <= dataflowReadability.firstNodeMaximumWidth; const adjacentNodesFit = nodeWidths.every((width, index) => index === 0 || (nodeWidths[index - 1]! + width) / 2 <= dataflowReadability.stageCenterGap - dataflowReadability.minimumNodeGap ); if (firstNodeFits && adjacentNodesFit) { selected = { fontSize, nodeWidths }; break; } } const fallbackFontSize = dataflowReadability.minimumFontSize; const fontSize = selected?.fontSize ?? fallbackFontSize; const nodeWidths = selected?.nodeWidths ?? sections.map((section) => dataflowNodeWidth(section.title, section.body, fallbackFontSize) ); const lastStageX = dataflowReadability.firstStageCenterX + Math.max(0, sections.length - 1) * dataflowReadability.stageCenterGap; const lastNodeWidth = nodeWidths.at(-1) ?? dataflowReadability.defaultNodeWidth; const viewBoxWidth = Math.max( dataflowReadability.minimumViewBoxWidth, lastStageX + dataflowReadability.stageWidth / 2 + dataflowReadability.viewBoxMargin, lastStageX + lastNodeWidth / 2 + dataflowReadability.viewBoxMargin, ); return { fontSize, nodeWidths, flowLabelWidths: story.document.connections.map((connection) => dataflowFlowLabelWidth( requiredConnectionLabel(connection, "dataflow"), fontSize, ) ), viewBox: [Math.ceil(viewBoxWidth), dataflowReadability.viewBoxHeight], }; } function dataflowSpec( story: ResolvedStoryDocument, layout: DataflowLayout, ): ArchifyDataflow { const nodeIds = new Map( story.document.sections.map((section) => [ section.id, componentId(section.id), ]), ); return { schema_version: 1, diagram_type: "dataflow", meta: { title: story.document.title, quality_profile: "standard", locale: "en", viewBox: layout.viewBox, legend: { mode: "hidden" }, }, stages: story.document.sections.map((section) => ({ label: section.title, })), nodes: story.document.sections.map((section, index) => ({ id: nodeIds.get(section.id)!, type: index === story.document.sections.length - 1 ? "frontend" : "backend", label: section.title, sublabel: section.body, stage: index, row: 0, width: layout.nodeWidths[index]!, })), flows: story.document.connections.map((connection, index) => { const fromIndex = story.document.sections.findIndex( (section) => section.id === connection.from, ); const toIndex = story.document.sections.findIndex( (section) => section.id === connection.to, ); const label = requiredConnectionLabel(connection, "dataflow"); const endpointGap = Math.abs(toIndex - fromIndex) * dataflowReadability.stageCenterGap - (layout.nodeWidths[fromIndex]! + layout.nodeWidths[toIndex]!) / 2; const labelWidth = Math.max( layout.flowLabelWidths[index]!, dataflowNativeFlowLabelWidth(label), ); return { id: stableId( "flow", `${index}\0${connection.from}\0${connection.to}`, ), from: nodeIds.get(connection.from)!, to: nodeIds.get(connection.to)!, label, ...(labelWidth > endpointGap ? { labelDy: dataflowReadability.flowLabelClearanceDy + dataflowFlowLabelMaskTop(layout.fontSize) - dataflowReadability.flowLabelMaskBaselineTop, } : {}), }; }), }; } const lifecycleFontSize = 14.5; const lifecycleMinimumRouteLength = 32; function lifecycleSpec(story: ResolvedStoryDocument): ArchifyLifecycle { const stateDrafts = story.document.sections.map((section, index) => { const last = index === story.document.sections.length - 1; const event = story.document.sections.length >= 5 && index >= 2 && index < story.document.sections.length - 2; const lane = last ? "terminal" as const : event ? "event" as const : "main" as const; const defaultCol = last ? 2 : event ? index - 2 : index - Math.max(0, index - 2); return { section, index, lane, defaultCol, width: Math.max( 118, Math.ceil(Array.from(section.title).reduce( (total, character) => total + (character.codePointAt(0)! > 0xff ? 2 : 1), 0, ) * lifecycleFontSize * 0.5 + 28), ), }; }); const placements = new Map<string, { lane: "main" | "event" | "terminal"; col: number; }>(); for (const lane of ["main", "event", "terminal"] as const) { const drafts = stateDrafts.filter((draft) => draft.lane === lane); const centers = lane === "main" ? [94, 248, 402, 556, 710] : [402, 556, 710]; let best: { cols: number[]; score: number } | undefined; const search = (position: number, cols: number[], score: number) => { if (position === drafts.length) { if ( best === undefined || score < best.score || (score === best.score && cols.join(",") < best.cols.join(",")) ) { best = { cols: [...cols], score }; } return; } const draft = drafts[position]!; for (let col = 0; col < centers.length; col += 1) { if (position > 0 && col <= cols[position - 1]!) continue; const center = centers[col]!; const left = center - draft.width / 2; const right = center + draft.width / 2; if (left < 32 || right > 948) continue; const overlaps = drafts.slice(0, position).some((other, otherIndex) => { const otherCenter = centers[cols[otherIndex]!]!; const otherRight = otherCenter + other.width / 2; return left - otherRight < lifecycleMinimumRouteLength; }); if (overlaps) continue; search( position + 1, [...cols, col], score + Math.abs(col - draft.defaultCol), ); } }; search(0, [], 0); if (best === undefined) { throw new Error( `Lifecycle ${lane} states cannot fit the native columns without overlap`, ); } drafts.forEach((draft, index) => { placements.set(draft.section.id, { lane, col: best!.cols[index]!, }); }); } const stateIds = new Map( story.document.sections.map((section) => [ section.id, componentId(section.id), ]), ); return { schema_version: 1, diagram_type: "lifecycle", meta: { title: story.document.title, quality_profile: "standard", locale: "en", legend: { mode: "hidden" }, }, lanes: [ { id: "main", label: story.document.title }, { id: "event", label: "Interruptions + recovery" }, { id: "terminal", label: "Outcomes" }, ], states: stateDrafts.map(({ section, index, width }) => { const last = index === story.document.sections.length - 1; return { id: stateIds.get(section.id)!, type: index === 0 ? "start" : last ? "success" : "active", label: section.title, ...placements.get(section.id)!, width, }; }), transitions: story.document.connections.map((connection, index) => { const fromIndex = story.document.sections.findIndex( (section) => section.id === connection.from, ); const toIndex = story.document.sections.findIndex( (section) => section.id === connection.to, ); return { id: stableId( "transition", `${index}\0${connection.from}\0${connection.to}`, ), from: stateIds.get(connection.from)!, to: stateIds.get(connection.to)!, ...(connection.label === undefined ? {} : { label: connection.label }), ...(placements.get(story.document.sections[fromIndex]!.id)!.lane === placements.get(story.document.sections[toIndex]!.id)!.lane ? { labelDy: 55 } : {}), ...(index === story.document.connections.length - 1 ? { route: "right-channel" as const } : {}), }; }), }; } function commandError(error: unknown): Error { if ( typeof error === "object" && error !== null && "stdout" in error && "stderr" in error ) { const stdout = String(error.stdout ?? "").trim(); const stderr = String(error.stderr ?? "").trim(); return new Error( ["Archify rendering failed", stderr, stdout].filter(Boolean).join(": "), { cause: error }, ); } return error instanceof Error ? error : new Error(`Archify rendering failed: ${String(error)}`); } function serializeRendererInput(spec: { readonly meta: object }): string { return `${JSON.stringify({ ...spec, meta: { ...spec.meta, output: "story.html" }, }, null, 2)}\n`; } function adaptStorySemantics(contents: string, document: StoryDocument): string { const fallback = document.diagramFamily === "sequence" ? "participant" : "component"; const roles = new Map(document.sections.map(section => [componentId(section.id), section.semanticRole ?? fallback])); const summaries = new Set(document.sections .filter(section => section.summary !== undefined) .map(section => componentId(section.id))); const seen = new Set<string>(); const adapted = contents.replace(/<g\b[^>]*\bdata-node-id="([^"]+)"[^>]*>/g, (tag, id: string) => { const role = roles.get(id); if (role === undefined) return tag; if (!/^(?!constructor$)[a-z][a-z0-9-]{0,63}$/.test(role)) throw new Error(`Invalid semantic role for ${id}`); if (!/\bdata-node-kind="[^"]*"/.test(tag)) throw new Error(`Story node ${id} has no semantic metadata`); seen.add(id); const annotated = tag.replace(/\bdata-node-kind="[^"]*"/, `data-node-kind="${role}"`); return summaries.has(id) ? annotated.replace("<g", '<g data-topo-caption="summary"') : annotated; }); if (seen.size !== roles.size) throw new Error("Story semantic adaptation omitted an authored node"); const replacements: readonly [RegExp, string][] = [ [/(<strong\b[^>]*id="semantic-lens-title"[^>]*>)[^<]*(<\/strong>)/, "$1Compare responsibilities$2"], [/(<p\b[^>]*class="semantic-lens-instruction"[^>]*>)[^<]*(<\/p>)/, "$1Choose up to two authored roles. Highlight only the relationships drawn in this story: not runtime traffic, transitive impact, or the complete dependency graph. One role includes connected peers; two compare direct cross-role arrows.$2"], ]; return replacements.reduce((html, [pattern, replacement]) => { if (!pattern.test(html)) throw new Error("Pinned semantic lens presentation seam is missing"); return html.replace(pattern, replacement); }, adapted); } function improveStoryReadability( contents: string, family: | "architecture" | "workflow" | "sequence" | "dataflow" | "lifecycle", dataflowLayout?: DataflowLayout, sequence?: SequenceLayout, ): string { const headEnd = "</head>"; if (!contents.includes(headEnd)) { throw new Error(`Archify ${family} output is missing its closing head element`); } if (family === "sequence" && sequence === undefined) throw new Error("Missing sequence readability layout"); const sequenceRatio = sequence === undefined ? undefined : sequence.viewBox[0] / sequence.viewBox[1]; const semanticControls = family === "architecture" || family === "sequence" ? ` html[data-topo-app] .semantic-lens { width: min(34rem, calc(100% - 24px)); max-height: calc(100% - 16px); overflow: auto; } #semantic-lens-title { font-size: 1rem; } #semantic-lens .semantic-lens-instruction, #semantic-lens .semantic-lens-status, #semantic-lens .semantic-lens-actions button, #semantic-lens-kinds .semantic-lens-kind em { font-size: 0.8rem; line-height: 1.5; } #semantic-lens-kinds .semantic-lens-kind strong { font-size: 0.85rem; line-height: 1.4; white-space: normal; overflow: visible; overflow-wrap: anywhere; text-overflow: clip; }` : ""; const rules = family === "architecture" ? ` svg { max-height: 100vh; } /* The story shell supplies the node index and full source details. */ .reader-rail { display: none !important; } svg [data-source-evidence-beacon] { display: none; } svg text[data-node-label], svg text[data-detail="context"], svg g[data-edge-from] > text { font-size: ${architectureFontSize}px; } /* Native caption baselines are only 16 units below titles. */ svg text[data-detail="context"] { transform: translateY(20px); } svg [data-topo-caption="summary"] text[data-detail="context"] { font-size: 17px; transform: translateY(12px); }` : family === "workflow" ? ` svg text[data-node-label], svg text[font-size="10"][font-weight="600"], svg g[data-edge-from] > text { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 14px; font-weight: 600; }` : family === "sequence" ? ` svg { max-height: 100vh; } .reader-rail { display: none !important; } @media (min-width: 1400px) and (min-height: 900px) { html[data-topo-app] .container { min-width: min(calc(100vw - 64px), ${sequence === undefined ? 0 : sequence.viewBox[0] * sequenceReadability.targetEffectiveFontSize / sequence.fontSize + 50}px); max-width: min(calc(100vw - 64px), calc((100dvh - 260px) * ${sequenceRatio} + 30px)); } } @media (min-width: 1400px) and (min-height: 900px) and (max-height: 920px) { html[data-topo-app] .container { max-width: min(calc(100vw - 64px), calc((100dvh - 156px) * ${sequenceRatio} + 32px)); } html[data-topo-app] .diagram-container { padding-bottom: 56px; } } svg text { font-size: ${sequence?.fontSize}px; } .semantic-passport-detail { font-size: 0.875rem; }` : family === "dataflow" ? ` svg text[data-node-label], svg text[data-detail="context"], svg text[font-size="9"][font-weight="600"], svg g[data-edge-from] > text { font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, 'DejaVu Sans Mono', 'Liberation Mono', 'Noto Sans Mono CJK SC', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', monospace; font-size: ${dataflowLayout?.fontSize ?? dataflowReadability.maximumFontSize}px; font-weight: 600; }` : ` svg text[data-node-label], svg text[font-size="10"][font-weight="600"], svg g[data-edge-from] > text { font-family: Arial, Helvetica, sans-serif; font-size: ${lifecycleFontSize}px; }`; const style = `<style data-topo-story-readability>${rules}${semanticControls} </style>`; const adjustedContents = family === "dataflow" && dataflowLayout ? dataflowLayout.flowLabelWidths.reduce((html, width, index) => { const pattern = new RegExp( `(<g data-detail="context"[^>]*data-edge-key="${index}"[^>]*>\\s*<rect x=")(-?\\d+(?:\\.\\d+)?)(" y=")(-?\\d+(?:\\.\\d+)?)(" width=")(\\d+(?:\\.\\d+)?)(" height=")(\\d+(?:\\.\\d+)?)(" rx="4" class="c-mask"/>)`, ); let replaced = false; const next = html.replace( pattern, ( _match, prefix: string, xValue: string, yPrefix: string, yValue: string, widthPrefix: string, nativeWidthValue: string, heightPrefix: string, _nativeHeightValue: string, suffix: string, ) => { replaced = true; const nativeWidth = Number(nativeWidthValue); const center = Number(xValue) + nativeWidth / 2; const x = Math.round( (center - width / 2) * dataflowReadability.fontSizePrecision, ) / dataflowReadability.fontSizePrecision; const baseline = Number(yValue) + dataflowReadability.flowLabelMaskBaselineTop; const top = dataflowFlowLabelMaskTop(dataflowLayout.fontSize); const y = baseline - top; const height = top + dataflowReadability.flowLabelMaskBaselineBottom; const [viewBoxWidth, viewBoxHeight] = dataflowLayout.viewBox; if ( ![x, y, width, height].every(Number.isFinite) || x < 0 || y < 0 || x + width > viewBoxWidth || y + height > viewBoxHeight ) { throw new Error( `Archify dataflow final flow label mask ${index} exceeds the ${viewBoxWidth}x${viewBoxHeight} viewBox`, ); } return `${prefix}${x}${yPrefix}${y}${widthPrefix}${width}${heightPrefix}${height}${suffix}`; }, ); if (!replaced) { throw new Error( `Archify dataflow output is missing flow label mask ${index}`, ); } return next; }, contents) : contents; const accessibleContents = family === "architecture" || family === "sequence" ? adjustedContents.replace(/<svg\b[^>]*\brole="img"[^>]*>/, tag => tag.replace('role="img"', 'role="group"').replace("<svg", `<svg data-topo-family="${family}"`)) : adjustedContents; return accessibleContents.replace(headEnd, `${style}\n${headEnd}`); } export function renderArchitectureStories( stories: readonly ResolvedStoryDocument[], options: { readonly sourceEvidence?: "native" | "wrapper" } = {}, ): StoryArtifact[] { return [...renderArchitectureStoryBatch(stories, options)]; } export function* renderArchitectureStoryBatch( stories: readonly ResolvedStoryDocument[], options: { readonly sourceEvidence?: "native" | "wrapper" } = {}, ): Generator<StoryArtifact> { const integrity = verifyArchifyIntegrity(); if (stories.length === 0) return; const directory = mkdtempSync(path.join(tmpdir(), "topo-architecture-batch-")); try { const jobs = stories.map((story, index) => { if ((story.document.diagramFamily ?? "architecture") !== "architecture") { throw new Error("Architecture batch only accepts Architecture stories"); } const input = path.join(directory, `${index}.json`); const output = path.join(directory, `${index}.html`); writeFileSync(input, serializeRendererInput(archifySpec(story, options.sourceEvidence !== "wrapper"))); return { input, output, repository: story.repositoryRoot }; }); const manifest = path.join(directory, "batch.json"); // Native entrypoints are ES modules: cache-busted evaluations retain their // HTML and compiled code until process exit. Bound that lifetime per chunk. for (let offset = 0; offset < jobs.length; offset += 32) { writeFileSync(manifest, JSON.stringify(jobs.slice(offset, offset + 32))); execFileSync(process.execPath, [ fileURLToPath(new URL("./architecture-batch.js", import.meta.url)), manifest, ], { cwd: directory, encoding: "utf8", maxBuffer: 64 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] }); } for (const [index, { output }] of jobs.entries()) { const sourceContents = readFileSync(output, "utf8"); const contents = adaptViewerTheme(adaptStorySemantics( improveStoryReadability(sourceContents, "architecture"), stories[index]!.document, ), stories[index]!.document, "archify"); yield { kind: "html", mediaType: "text/html", contents, renderer: { name: "archify", pin: integrity.version, sha256: integrity.archiveSha256, sourceOutputSha256: outputHash(sourceContents), outputSha256: outputHash(contents), adaptation: "topocode-story-semantics-v2", }, }; } } catch (error) { throw commandError(error); } finally { rmSync(directory, { recursive: true, force: true }); } } export function renderStory(story: ResolvedStoryDocument): StoryArtifact | Promise<StoryArtifact> { if (story.document.renderer === "graphviz") return renderGraphviz(story); const integrity = verifyArchifyIntegrity(); const family = story.document.diagramFamily ?? "architecture"; const adaptiveSequenceLayout = family === "sequence" ? sequenceLayout(story) : undefined; const adaptiveDataflowLayout = family === "dataflow" ? dataflowLayout(story) : undefined; const spec = family === "workflow" ? workflowSpec(story) : family === "sequence" ? sequenceSpec(story, adaptiveSequenceLayout!) : family === "dataflow" ? dataflowSpec(story, adaptiveDataflowLayout!) : family === "lifecycle" ? lifecycleSpec(story) : archifySpec(story); const temporaryDirectory = mkdtempSync( path.join(tmpdir(), "topo-archify-render-"), ); const inputPath = path.join(temporaryDirectory, "story.json"); const outputPath = path.join(temporaryDirectory, "story.html"); try { writeFileSync(inputPath, serializeRendererInput(spec)); const args = [ archifyCli, "deliver", family, inputPath, outputPath, "--quality", "standard", "--json", ]; if ( family === "architecture" && story.document.classification !== "capability-demo" ) { args.splice(5, 0, "--repo-root", story.repositoryRoot); } execFileSync(process.execPath, args, { cwd: temporaryDirectory, encoding: "utf8", maxBuffer: 32 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"], }); const contents = family === "sequence" ? addSequenceNarrativeDetails( readFileSync(outputPath, "utf8"), story, adaptiveSequenceLayout!, ) : readFileSync(outputPath, "utf8"); const readable = improveStoryReadability( contents, family, adaptiveDataflowLayout, adaptiveSequenceLayout, ); const adapted = adaptViewerTheme(family === "architecture" || family === "sequence" ? adaptStorySemantics(readable, story.document) : readable, story.document); return { kind: "html", mediaType: "text/html", contents: adapted, renderer: { name: "archify", pin: integrity.version, sha256: integrity.archiveSha256, sourceOutputSha256: outputHash(readFileSync(outputPath, "utf8")), outputSha256: outputHash(adapted), adaptation: family === "architecture" || family === "sequence" ? "topocode-story-semantics-v2" : "topocode-readability-and-theme-v1", }, }; } catch (error) { throw commandError(error); } finally { rmSync(temporaryDirectory, { recursive: true, force: true }); } } -
@topo/views
Open Dependency context by role: Path selectionOpen Dependency context by role: Ignore matchingOpen Dependency context by role: Build, test, docsOpen Dependency context by role: Development tools
Resolves authored path-based views, review baselines, overrides and layout pins against graph evidence. All declared internal dependencies (not runtime calls): @topo/views -> @topo/graph [dependencies: workspace:*] @topo/views -> @topo/schema [dependencies: workspace:*] Referenced by internal modules: @topo/cli -> @topo/views [dependencies: workspace:*] @topo/site -> @topo/views [dependencies: workspace:*] Stable drilldown identity: views. Module-specific detail maps will be curated later; there is no placeholder target.
packages/views/package.json:1-30 { "name": "@topo/views", "version": "0.0.0", "private": true, "type": "module", "files": [ "dist" ], "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } }, "scripts": { "build": "tsc -b", "typecheck": "tsc -b --pretty false", "test": "vitest run" }, "dependencies": { "@topo/graph": "workspace:*", "@topo/schema": "workspace:*", "ignore": "^7.0.0" }, "devDependencies": { "typescript": "^7.0.2", "vite": "8.2.2", "vitest": "^5.0.0" } }packages/views/src/core.ts:1-867 import ignore from "ignore"; import { assertGraphDocument, serializeJson, type GraphDocument, type GraphNode, type JsonValue, } from "@topo/schema"; import type { ArchitectureDocument, LayoutPin } from "@topo/graph"; import type { CuratedViewDefinition, CuratedViewEvaluation, CuratedViewsSnapshot, ViewAnchor, ViewMember, ViewPin, } from "./model.js"; const MAX_STRING = 1024; const MAX_NAME = 256; const MAX_ARRAY = 10_000; const VIEW_KEYS = new Set([ "schemaVersion", "id", "name", "provenance", "pathRules", "includes", "excludes", "pins", "expandedPaths", "reviewed", ]); export interface CuratedViewValidationIssue { path: string; message: string; } export class CuratedViewValidationError extends Error { readonly issues: CuratedViewValidationIssue[]; constructor(issues: CuratedViewValidationIssue[]) { super( `Curated view validation failed with ${issues.length} issue${issues.length === 1 ? "" : "s"}: ${issues .map((issue) => `${issue.path} ${issue.message}`) .join("; ")}`, ); this.name = "CuratedViewValidationError"; this.issues = issues; } } function isRecord(value: unknown): value is Record<string, unknown> { return ( typeof value === "object" && value !== null && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null) ); } function compareText(left: string, right: string): number { return left < right ? -1 : left > right ? 1 : 0; } function anchorKey(anchor: ViewAnchor): string { return `${anchor.kind}\0${anchor.path}`; } function compareAnchors(left: ViewAnchor, right: ViewAnchor): number { return ( compareText(left.path, right.path) || compareText(left.kind, right.kind) ); } function assertKnownKeys( value: Record<string, unknown>, allowed: ReadonlySet<string>, path: string, issues: CuratedViewValidationIssue[], ): void { for (const key of Object.keys(value)) { if (!allowed.has(key)) { issues.push({ path: `${path}.${key}`, message: "Unknown property." }); } } } function readString( value: unknown, path: string, issues: CuratedViewValidationIssue[], maximum = MAX_STRING, ): string | undefined { if ( typeof value !== "string" || value.length === 0 || value.length > maximum ) { issues.push({ path, message: `Expected a non-empty string no longer than ${maximum} characters.`, }); return undefined; } return value; } function readPath( value: unknown, path: string, issues: CuratedViewValidationIssue[], allowRoot: boolean, ): string | undefined { const result = readString(value, path, issues); if (result === undefined) return undefined; if (result === "." && allowRoot) return result; if ( result.includes("\\") || /[\u0000-\u001f\u007f]/u.test(result) || result.startsWith("/") || result.endsWith("/") || result.includes("//") || result .split("/") .some((segment) => segment === "" || segment === "." || segment === "..") || result.startsWith("./") ) { issues.push({ path, message: "Expected a canonical repository-relative POSIX path.", }); return undefined; } if (result === "." && !allowRoot) { issues.push({ path, message: 'The repository root "." is directory-only.' }); return undefined; } return result; } function readArray( value: unknown, path: string, issues: CuratedViewValidationIssue[], ): unknown[] { if (!Array.isArray(value)) { issues.push({ path, message: "Expected an array." }); return []; } if (value.length > MAX_ARRAY) { issues.push({ path, message: `Expected no more than ${MAX_ARRAY} entries.`, }); return value.slice(0, MAX_ARRAY); } return value; } function parseAnchor( value: unknown, path: string, issues: CuratedViewValidationIssue[], ): ViewAnchor | undefined { if (!isRecord(value)) { issues.push({ path, message: "Expected an anchor object." }); return undefined; } assertKnownKeys(value, new Set(["kind", "path"]), path, issues); if (value.kind !== "node" && value.kind !== "directory") { issues.push({ path: `${path}.kind`, message: 'Expected "node" or "directory".', }); return undefined; } const anchorPath = readPath( value.path, `${path}.path`, issues, value.kind === "directory", ); return anchorPath === undefined ? undefined : { kind: value.kind, path: anchorPath }; } function parseAnchors( value: unknown, path: string, issues: CuratedViewValidationIssue[], ): ViewAnchor[] { const anchors: ViewAnchor[] = []; const seen = new Set<string>(); readArray(value, path, issues).forEach((entry, index) => { const anchor = parseAnchor(entry, `${path}[${index}]`, issues); if (!anchor) return; const key = anchorKey(anchor); if (seen.has(key)) { issues.push({ path: `${path}[${index}]`, message: `Duplicate anchor "${anchor.kind}:${anchor.path}".`, }); return; } seen.add(key); anchors.push(anchor); }); return anchors; } function parseRules( value: unknown, path: string, issues: CuratedViewValidationIssue[], ): string[] { const rules: string[] = []; const seen = new Set<string>(); readArray(value, path, issues).forEach((entry, index) => { const rulePath = `${path}[${index}]`; const rule = readString(entry, rulePath, issues); if (rule === undefined) return; if ( rule.trim() !== rule || rule.startsWith("!") || rule.startsWith("#") || rule.includes("\n") || rule.includes("\r") || rule.includes("\0") || rule.includes("\\") || rule.startsWith("/") || rule === "." || rule.startsWith("../") || rule.includes("/../") || rule.includes("//") ) { issues.push({ path: rulePath, message: "Expected one positive, root-relative gitignore-style pattern without comments, negation, escapes, or traversal.", }); return; } if (seen.has(rule)) { issues.push({ path: rulePath, message: `Duplicate path rule "${rule}".` }); return; } seen.add(rule); rules.push(rule); }); return rules; } function parsePins( value: unknown, path: string, issues: CuratedViewValidationIssue[], ): ViewPin[] { const pins: ViewPin[] = []; const seen = new Set<string>(); readArray(value, path, issues).forEach((entry, index) => { const pinPath = `${path}[${index}]`; if (!isRecord(entry)) { issues.push({ path: pinPath, message: "Expected a pin object." }); return; } assertKnownKeys(entry, new Set(["anchor", "position"]), pinPath, issues); const anchor = parseAnchor(entry.anchor, `${pinPath}.anchor`, issues); if (!isRecord(entry.position)) { issues.push({ path: `${pinPath}.position`, message: "Expected a position object.", }); return; } assertKnownKeys( entry.position, new Set(["x", "y"]), `${pinPath}.position`, issues, ); const { x, y } = entry.position; if ( typeof x !== "number" || typeof y !== "number" || !Number.isSafeInteger(x) || !Number.isSafeInteger(y) ) { issues.push({ path: `${pinPath}.position`, message: "Pin coordinates must be safe finite integers.", }); return; } if (!anchor) return; const key = anchorKey(anchor); if (seen.has(key)) { issues.push({ path: `${pinPath}.anchor`, message: `Multiple pins target "${anchor.kind}:${anchor.path}".`, }); return; } seen.add(key); pins.push({ anchor, position: { x, y } }); }); return pins; } function parseMembers( value: unknown, path: string, issues: CuratedViewValidationIssue[], ): ViewMember[] { const members: ViewMember[] = []; const seen = new Set<string>(); readArray(value, path, issues).forEach((entry, index) => { const memberPath = `${path}[${index}]`; if (!isRecord(entry)) { issues.push({ path: memberPath, message: "Expected a member object." }); return; } assertKnownKeys(entry, new Set(["path", "fingerprint"]), memberPath, issues); const repositoryPath = readPath( entry.path, `${memberPath}.path`, issues, false, ); let fingerprint: string | undefined; if (entry.fingerprint !== undefined) { fingerprint = readString( entry.fingerprint, `${memberPath}.fingerprint`, issues, ); } if (repositoryPath === undefined) return; if (seen.has(repositoryPath)) { issues.push({ path: `${memberPath}.path`, message: `Duplicate reviewed member "${repositoryPath}".`, }); return; } seen.add(repositoryPath); members.push({ path: repositoryPath, ...(fingerprint === undefined ? {} : { fingerprint }), }); }); return members; } function parseDefinition( value: unknown, path: string, issues: CuratedViewValidationIssue[], ): CuratedViewDefinition | undefined { if (!isRecord(value)) { issues.push({ path, message: "Expected a curated view object." }); return undefined; } assertKnownKeys(value, VIEW_KEYS, path, issues); if (value.schemaVersion !== "1.0") { issues.push({ path: `${path}.schemaVersion`, message: 'Expected schema version "1.0".', }); } if (value.provenance !== "human") { issues.push({ path: `${path}.provenance`, message: 'Expected provenance "human".', }); } const id = readString(value.id, `${path}.id`, issues, 128); if (id !== undefined && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(id)) { issues.push({ path: `${path}.id`, message: "Expected a lowercase slug using letters, digits, and single hyphens.", }); } const name = readString(value.name, `${path}.name`, issues, MAX_NAME); const pathRules = parseRules(value.pathRules, `${path}.pathRules`, issues); const includes = parseAnchors(value.includes, `${path}.includes`, issues); const excludes = parseAnchors(value.excludes, `${path}.excludes`, issues); const pins = parsePins(value.pins, `${path}.pins`, issues); const expandedPaths = readArray( value.expandedPaths, `${path}.expandedPaths`, issues, ).flatMap((entry, index) => { const expandedPath = readPath( entry, `${path}.expandedPaths[${index}]`, issues, true, ); return expandedPath === undefined ? [] : [expandedPath]; }); if (new Set(expandedPaths).size !== expandedPaths.length) { issues.push({ path: `${path}.expandedPaths`, message: "Expanded paths must be unique.", }); } let reviewed: CuratedViewDefinition["reviewed"]; if (value.reviewed !== undefined) { if (!isRecord(value.reviewed)) { issues.push({ path: `${path}.reviewed`, message: "Expected a reviewed snapshot object.", }); } else { assertKnownKeys( value.reviewed, new Set(["graphHash", "members"]), `${path}.reviewed`, issues, ); const graphHash = readString( value.reviewed.graphHash, `${path}.reviewed.graphHash`, issues, ); const members = parseMembers( value.reviewed.members, `${path}.reviewed.members`, issues, ); if (graphHash !== undefined) reviewed = { graphHash, members }; } } if ( id === undefined || name === undefined || value.schemaVersion !== "1.0" || value.provenance !== "human" ) { return undefined; } return { schemaVersion: "1.0", id, name, provenance: "human", pathRules, includes, excludes, pins, expandedPaths, ...(reviewed === undefined ? {} : { reviewed }), }; } export function parseCuratedView(value: unknown): CuratedViewDefinition { const issues: CuratedViewValidationIssue[] = []; const definition = parseDefinition(value, "$", issues); if (issues.length > 0 || definition === undefined) { throw new CuratedViewValidationError(issues); } return definition; } function canonicalDefinition( definitionValue: unknown, ): CuratedViewDefinition { const definition = parseCuratedView(definitionValue); return { ...definition, pathRules: [...definition.pathRules].sort(compareText), includes: [...definition.includes].sort(compareAnchors), excludes: [...definition.excludes].sort(compareAnchors), pins: [...definition.pins].sort((left, right) => compareAnchors(left.anchor, right.anchor), ), expandedPaths: [...definition.expandedPaths].sort(compareText), ...(definition.reviewed === undefined ? {} : { reviewed: { graphHash: definition.reviewed.graphHash, members: [...definition.reviewed.members].sort((left, right) => compareText(left.path, right.path), ), }, }), }; } export function serializeCuratedView( definition: CuratedViewDefinition, ): string { return serializeJson(canonicalDefinition(definition) as unknown as JsonValue); } export function parseCuratedViewsSnapshot( value: unknown, ): CuratedViewsSnapshot { const issues: CuratedViewValidationIssue[] = []; if (!isRecord(value)) { throw new CuratedViewValidationError([ { path: "$", message: "Expected a curated views snapshot object." }, ]); } assertKnownKeys( value, new Set(["schemaVersion", "graphHash", "views"]), "$", issues, ); if (value.schemaVersion !== "1.0") { issues.push({ path: "$.schemaVersion", message: 'Expected schema version "1.0".', }); } const graphHash = readString(value.graphHash, "$.graphHash", issues); const records = readArray(value.views, "$.views", issues); const views: CuratedViewsSnapshot["views"] = []; const ids = new Set<string>(); records.forEach((entry, index) => { const path = `$.views[${index}]`; if (!isRecord(entry)) { issues.push({ path, message: "Expected a view record object." }); return; } assertKnownKeys(entry, new Set(["definition", "revision"]), path, issues); const definition = parseDefinition( entry.definition, `${path}.definition`, issues, ); const revision = readString( entry.revision, `${path}.revision`, issues, ); if (!definition || revision === undefined) return; if (ids.has(definition.id)) { issues.push({ path: `${path}.definition.id`, message: `Duplicate view identifier "${definition.id}".`, }); return; } ids.add(definition.id); views.push({ definition, revision }); }); if ( issues.length > 0 || graphHash === undefined || value.schemaVersion !== "1.0" ) { throw new CuratedViewValidationError(issues); } return { schemaVersion: "1.0", graphHash, views, }; } interface PathIndex { nodesByPath: Map<string, GraphNode>; sortedPaths: string[]; } function graphPathIndex(graphValue: unknown): { graph: GraphDocument; index: PathIndex; } { if (isRecord(graphValue) && Array.isArray(graphValue.nodes)) { const nodeIdByPath = new Map<string, string>(); const issues: CuratedViewValidationIssue[] = []; graphValue.nodes.forEach((candidate, index) => { if ( !isRecord(candidate) || typeof candidate.id !== "string" || !isRecord(candidate.identity) || candidate.identity.kind !== "path" || typeof candidate.identity.value !== "string" ) { return; } readPath( candidate.identity.value, `$.graph.nodes[${index}].identity.value`, issues, false, ); const existingId = nodeIdByPath.get(candidate.identity.value); if (existingId !== undefined) { issues.push({ path: "$.graph.nodes", message: `Ambiguous path identity "${candidate.identity.value}" is used by "${existingId}" and "${candidate.id}".`, }); } nodeIdByPath.set(candidate.identity.value, candidate.id); }); if (issues.length > 0) throw new CuratedViewValidationError(issues); } assertGraphDocument(graphValue); const nodesByPath = new Map<string, GraphNode>(); for (const node of graphValue.nodes) { if (node.identity.kind !== "path") continue; const existing = nodesByPath.get(node.identity.value); if (existing) { throw new CuratedViewValidationError([ { path: "$.graph.nodes", message: `Ambiguous path identity "${node.identity.value}" is used by "${existing.id}" and "${node.id}".`, }, ]); } nodesByPath.set(node.identity.value, node); } return { graph: graphValue, index: { nodesByPath, sortedPaths: [...nodesByPath.keys()].sort(compareText), }, }; } function pathsForAnchor(index: PathIndex, anchor: ViewAnchor): string[] { if (anchor.kind === "node") { return index.nodesByPath.has(anchor.path) ? [anchor.path] : []; } if (anchor.path === ".") return index.sortedPaths; const prefix = `${anchor.path}/`; return index.sortedPaths.filter((path) => path.startsWith(prefix)); } function selectedPathState( index: PathIndex, definition: CuratedViewDefinition, ): { selectedPaths: Set<string>; unresolvedIncludes: ViewAnchor[]; unresolvedExcludes: ViewAnchor[]; missingPins: ViewAnchor[]; excludedPins: ViewAnchor[]; retainedPins: ViewAnchor[]; } { const matcher = ignore({ ignorecase: false }).add(definition.pathRules); const selectedPaths = new Set( index.sortedPaths.filter((path) => matcher.ignores(path)), ); const unresolvedIncludes: ViewAnchor[] = []; for (const anchor of definition.includes) { const paths = pathsForAnchor(index, anchor); if (paths.length === 0) unresolvedIncludes.push(anchor); for (const path of paths) selectedPaths.add(path); } const excludedPaths = new Set<string>(); const unresolvedExcludes: ViewAnchor[] = []; for (const anchor of definition.excludes) { const paths = pathsForAnchor(index, anchor); if (paths.length === 0) unresolvedExcludes.push(anchor); for (const path of paths) excludedPaths.add(path); } const missingPins: ViewAnchor[] = []; const excludedPins: ViewAnchor[] = []; const retainedPins: ViewAnchor[] = []; for (const pin of definition.pins) { const paths = pathsForAnchor(index, pin.anchor); if (paths.length === 0) { missingPins.push(pin.anchor); continue; } const remaining = paths.filter((path) => !excludedPaths.has(path)); if (remaining.length === 0) { excludedPins.push(pin.anchor); continue; } const retained = remaining.some((path) => !selectedPaths.has(path)); for (const path of remaining) selectedPaths.add(path); if (retained) retainedPins.push(pin.anchor); } for (const path of excludedPaths) selectedPaths.delete(path); return { selectedPaths, unresolvedIncludes, unresolvedExcludes, missingPins, excludedPins, retainedPins, }; } function memberForNode(node: GraphNode): ViewMember { return { path: node.identity.value, ...(node.fingerprint === undefined ? {} : { fingerprint: node.fingerprint }), }; } function memberChanged(left: ViewMember, right: ViewMember): boolean { return left.fingerprint !== right.fingerprint; } export function evaluateCuratedView( graphValue: unknown, definitionValue: CuratedViewDefinition, ): CuratedViewEvaluation { const definition = parseCuratedView(definitionValue); const { index } = graphPathIndex(graphValue); const state = selectedPathState(index, definition); const members = [...state.selectedPaths] .sort(compareText) .map((path) => memberForNode(index.nodesByPath.get(path)!)); const reviewedByPath = new Map( (definition.reviewed?.members ?? []).map((member) => [member.path, member]), ); const currentByPath = new Map(members.map((member) => [member.path, member])); const added = members.filter((member) => !reviewedByPath.has(member.path)); const removed = [...reviewedByPath.values()] .filter((member) => !currentByPath.has(member.path)) .sort((left, right) => compareText(left.path, right.path)); const changed = members.filter((member) => { const reviewed = reviewedByPath.get(member.path); return reviewed !== undefined && memberChanged(member, reviewed); }); const pinnedPaths = new Set( definition.pins.flatMap((pin) => pathsForAnchor(index, pin.anchor)), ); return { nodeIds: members.map((member) => index.nodesByPath.get(member.path)!.id), members, delta: { added, removed, changed, unplaced: added.filter((member) => !pinnedPaths.has(member.path)), missingPins: [...state.missingPins].sort(compareAnchors), excludedPins: [...state.excludedPins].sort(compareAnchors), retainedPins: [...state.retainedPins].sort(compareAnchors), unresolvedIncludes: [...state.unresolvedIncludes].sort(compareAnchors), unresolvedExcludes: [...state.unresolvedExcludes].sort(compareAnchors), }, }; } export function reviewCuratedView( graph: unknown, definitionValue: CuratedViewDefinition, graphHashValue: string, ): CuratedViewDefinition { const graphHashIssues: CuratedViewValidationIssue[] = []; const graphHash = readString(graphHashValue, "$.graphHash", graphHashIssues); if (graphHash === undefined) { throw new CuratedViewValidationError(graphHashIssues); } const definition = parseCuratedView(definitionValue); const evaluation = evaluateCuratedView(graph, definition); return { ...definition, pathRules: [...definition.pathRules], includes: definition.includes.map((anchor) => ({ ...anchor })), excludes: definition.excludes.map((anchor) => ({ ...anchor })), pins: definition.pins.map((pin) => ({ anchor: { ...pin.anchor }, position: { ...pin.position }, })), expandedPaths: [...definition.expandedPaths], reviewed: { graphHash, members: evaluation.members.map((member) => ({ ...member })), }, }; } function directoryPath(path: string): string { return path === "." ? "" : path; } export function resolveViewPins( graphValue: unknown, architecture: ArchitectureDocument, definitionValue: CuratedViewDefinition, ): LayoutPin[] { const definition = parseCuratedView(definitionValue); const { graph, index } = graphPathIndex(graphValue); if (architecture.graphId !== graph.graphId || architecture.version !== "1.0") { throw new CuratedViewValidationError([ { path: "$.architecture", message: "Architecture must be a version 1.0 derivation of this graph.", }, ]); } const selected = new Set(evaluateCuratedView(graph, definition).nodeIds); const containerByPath = new Map( architecture.directoryContainers.map((container) => [ container.path, container, ]), ); const pins: LayoutPin[] = []; for (const pin of definition.pins) { const paths = pathsForAnchor(index, pin.anchor); const selectedPaths = paths.filter((path) => selected.has(index.nodesByPath.get(path)!.id), ); if (selectedPaths.length === 0) continue; if (pin.anchor.kind === "node") { const node = index.nodesByPath.get(pin.anchor.path)!; pins.push({ id: `view:${definition.id}:pin:node:${pin.anchor.path}`, subject: { kind: "node" as const, id: node.id }, anchor: { path: pin.anchor.path }, position: { ...pin.position }, }); continue; } const container = containerByPath.get(directoryPath(pin.anchor.path)); if (!container) { throw new CuratedViewValidationError([ { path: "$.architecture.directoryContainers", message: `Missing directory container for "${pin.anchor.path}".`, }, ]); } pins.push({ id: `view:${definition.id}:pin:directory:${pin.anchor.path}`, subject: { kind: "container" as const, id: container.id }, anchor: { path: pin.anchor.path }, position: { ...pin.position }, }); } return pins; } export function anchorForEntity( graphValue: unknown, architecture: ArchitectureDocument, entityId: string, ): ViewAnchor | undefined { const { graph } = graphPathIndex(graphValue); if (architecture.graphId !== graph.graphId || architecture.version !== "1.0") { throw new CuratedViewValidationError([ { path: "$.architecture", message: "Architecture must be a version 1.0 derivation of this graph.", }, ]); } const node = graph.nodes.find((candidate) => candidate.id === entityId); if (node) { return node.identity.kind === "path" ? { kind: "node", path: node.identity.value } : undefined; } const directory = architecture.directoryContainers.find( (candidate) => candidate.id === entityId, ); return directory ? { kind: "directory", path: directory.path.length === 0 ? "." : directory.path } : undefined; } -
@topo/site
Open Dependency context by role: Build, test, docsOpen Dependency context by role: Development tools
Owns the site-data contract, artifact loading checks, notices, and browser regressions. It is not the marketing website or the generated shell implementation. All declared internal dependencies (not runtime calls): @topo/site -> @topo/enrichment [dependencies: workspace:*] @topo/site -> @topo/graph [dependencies: workspace:*] @topo/site -> @topo/modules [dependencies: workspace:*] @topo/site -> @topo/schema [dependencies: workspace:*] @topo/site -> @topo/views [dependencies: workspace:*] @topo/site -> @topo/diagram-core [devDependencies: workspace:*] Referenced by internal modules: @topo/cli -> @topo/site [dependencies: workspace:*] Stable drilldown identity: site. Module-specific detail maps will be curated later; there is no placeholder target.
packages/site/package.json:1-38 { "name": "@topo/site", "version": "0.0.0", "private": true, "type": "module", "files": [ "dist" ], "exports": { "./notices": "./dist/THIRD_PARTY_NOTICES.txt", "./data": { "types": "./src/data.ts", "default": "./dist/data-contract/data.js" } }, "scripts": { "build": "node scripts/clean-dist.mjs && tsc -p tsconfig.json --noEmit && tsc -p tsconfig.data.json", "typecheck": "tsc -p tsconfig.json --noEmit --pretty false && yarn typecheck:browser", "typecheck:browser": "tsc -p tsconfig.browser-tests.json --noEmit --pretty false", "test": "vitest run --config vitest.config.ts", "test:browser": "playwright test --config playwright.config.mjs" }, "dependencies": { "@topo/enrichment": "workspace:*", "@topo/graph": "workspace:*", "@topo/modules": "workspace:*", "@topo/schema": "workspace:*", "@topo/views": "workspace:*" }, "devDependencies": { "@playwright/test": "^1.55.0", "@topo/diagram-core": "workspace:*", "@types/node": "^22.18.0", "typescript": "^7.0.2", "vite": "8.2.2", "vitest": "^5.0.0" } }packages/site/src/data.ts:1-276 import { hashAnalysis, parseEnrichmentDocument, validateEnrichmentReferences } from "@topo/enrichment"; import { deriveArchitecture, type ArchitectureDocument, } from "@topo/graph"; import { validateModuleContributions, type StaticModuleManifest, } from "@topo/modules"; import { assertLayoutDocument, parseGraphDocument, parseLogicalArchitecture, validateLayoutAgainstGraph, type GraphDocument, type LayoutDocument, } from "@topo/schema"; import { parseCuratedViewsSnapshot } from "@topo/views"; import type { DashboardArtifact, LoadedArtifacts, } from "./contracts.js"; import { supportedSiteModules } from "./module-support.js"; export class ArtifactLoadError extends Error { readonly artifact: string; readonly causeMessage: string; constructor(artifact: string, cause: unknown) { const causeMessage = cause instanceof Error ? cause.message : String(cause); super(`Could not load ${artifact}: ${causeMessage}`); this.name = "ArtifactLoadError"; this.artifact = artifact; this.causeMessage = causeMessage; } } interface SiteDataEnvelope { schemaVersion: "1.0"; graph: unknown; layout: unknown; architecture?: unknown; dashboard: unknown | null; curatedViews?: unknown; enrichment?: unknown; logicalArchitecture?: unknown; } function isEmptyJson(value: unknown): boolean { if (value === null) return true; if (Array.isArray(value)) return value.length === 0; return ( typeof value === "object" && value !== null && Object.keys(value).length === 0 ); } function scannerQuality(graph: GraphDocument): { authoritative: boolean; status: string; warnings: string[]; } { const value = graph.extensions["dev.topo.scanner"]; if (value === undefined) { return { authoritative: true, status: "complete", warnings: [] }; } if ( typeof value !== "object" || value === null || Array.isArray(value) || typeof value.authoritative !== "boolean" || typeof value.status !== "string" || value.status.length === 0 || !Array.isArray(value.diagnostics) ) { throw new ArtifactLoadError( "data.json graph.extensions.dev.topo.scanner", "Expected authoritative:boolean, status:string, and diagnostics:array.", ); } const diagnostics = value.diagnostics.map((diagnostic, index) => { if (typeof diagnostic === "string" && diagnostic.length > 0) { return diagnostic; } if ( typeof diagnostic === "object" && diagnostic !== null && !Array.isArray(diagnostic) && "message" in diagnostic && typeof diagnostic.message === "string" && diagnostic.message.length > 0 ) { return diagnostic.message; } throw new ArtifactLoadError( "data.json graph.extensions.dev.topo.scanner", `Diagnostic ${index} must be a string or an object with a message.`, ); }); return { authoritative: value.authoritative, status: value.status, warnings: value.authoritative ? [] : [ `Scanner output is ${value.status} and is not authoritative.`, ...diagnostics, ], }; } function validateArchitecture( value: unknown, graph: GraphDocument, ): ArchitectureDocument { if ( typeof value !== "object" || value === null || !("version" in value) || value.version !== "1.0" || !("graphId" in value) || value.graphId !== graph.graphId || !("directoryContainers" in value) || !Array.isArray(value.directoryContainers) || !("stronglyConnectedComponents" in value) || !Array.isArray(value.stronglyConnectedComponents) ) { throw new ArtifactLoadError( "architecture.json", "Artifact is not a version 1.0 architecture for this graph.", ); } return value as ArchitectureDocument; } function validateEnvelope(value: unknown): SiteDataEnvelope { if ( typeof value !== "object" || value === null || !("schemaVersion" in value) || value.schemaVersion !== "1.0" || !("graph" in value) || !("layout" in value) || !("dashboard" in value) ) { throw new ArtifactLoadError( "data.json", "Expected schemaVersion 1.0 with graph, layout, and dashboard fields.", ); } return value as SiteDataEnvelope; } function validateLayout(value: unknown, graph: GraphDocument): LayoutDocument { try { assertLayoutDocument(value); } catch (error) { throw new ArtifactLoadError("data.json layout", error); } const layout = value; const issues = validateLayoutAgainstGraph(layout, graph); if (issues.length > 0) { throw new ArtifactLoadError( "data.json layout", issues.map((issue) => `${issue.path} ${issue.message}`).join("; "), ); } return layout; } export async function parseSiteData( value: unknown, compiledModules: readonly StaticModuleManifest[], editingToken?: string, ): Promise<LoadedArtifacts> { const envelope = validateEnvelope(value); let parsedGraph: ReturnType<typeof parseGraphDocument>; try { parsedGraph = parseGraphDocument( JSON.stringify(envelope.graph), supportedSiteModules(compiledModules), ); } catch (error) { throw new ArtifactLoadError("data.json graph", error); } try { validateModuleContributions(parsedGraph.document, compiledModules); } catch (error) { throw new ArtifactLoadError("data.json module contributions", error); } const layout = validateLayout(envelope.layout, parsedGraph.document); const hasArchitecture = envelope.architecture !== undefined && envelope.architecture !== null; const architecture = hasArchitecture ? validateArchitecture(envelope.architecture, parsedGraph.document) : deriveArchitecture(parsedGraph.document); const scanner = scannerQuality(parsedGraph.document); let curatedViews: LoadedArtifacts["curatedViews"]; if (envelope.curatedViews !== undefined) { try { curatedViews = parseCuratedViewsSnapshot(envelope.curatedViews); } catch (error) { throw new ArtifactLoadError("data.json curatedViews", error); } } let dashboard: DashboardArtifact; if (envelope.dashboard === null) { dashboard = { availability: "unavailable" }; } else if (isEmptyJson(envelope.dashboard)) { dashboard = { availability: "empty", value: envelope.dashboard }; } else { dashboard = { availability: "available", value: envelope.dashboard, }; } let enrichment: LoadedArtifacts["enrichment"]; let enrichmentError: string | undefined; if (envelope.enrichment !== undefined) { try { const document = parseEnrichmentDocument(envelope.enrichment); if ( document.analysisHash === await hashAnalysis(parsedGraph.document, envelope.dashboard) ) { validateEnrichmentReferences(document, parsedGraph.document); enrichment = document; } } catch (error) { enrichmentError = new ArtifactLoadError("AI commentary", error).message; } } let logicalArchitecture: LoadedArtifacts["logicalArchitecture"]; if (envelope.logicalArchitecture !== undefined) { try { logicalArchitecture = parseLogicalArchitecture( envelope.logicalArchitecture, ); if ( logicalArchitecture.graphId !== parsedGraph.document.graphId || logicalArchitecture.revision !== parsedGraph.document.repository.revision ) { throw new Error( "Artifact graph or revision does not match the loaded graph.", ); } } catch (error) { throw new ArtifactLoadError("data.json logicalArchitecture", error); } } return { graph: parsedGraph.document, compatibility: parsedGraph.compatibility, layout, architecture, architectureSource: hasArchitecture ? "artifact" : "derived", dashboard, ...(enrichment === undefined ? {} : { enrichment }), ...(enrichmentError === undefined ? {} : { enrichmentError }), ...(logicalArchitecture === undefined ? {} : { logicalArchitecture }), ...(curatedViews === undefined ? {} : { curatedViews }), ...(curatedViews && editingToken ? { viewEditingToken: editingToken } : {}), quality: { authoritative: parsedGraph.compatibility.authoritative && scanner.authoritative, scannerStatus: scanner.status, warnings: [...parsedGraph.compatibility.warnings, ...scanner.warnings], }, }; } -
@topo/workspace
Open Dependency context by role: Build, test, docsOpen Dependency context by role: Development tools
Owns .topo configuration, safe authored/generated file access, workspace locking, and publication transactions. All declared internal dependencies (not runtime calls): @topo/workspace -> @topo/schema [dependencies: workspace:*] Referenced by internal modules: @topo/cli -> @topo/workspace [dependencies: workspace:*] Stable drilldown identity: workspace. Module-specific detail maps will be curated later; there is no placeholder target.
packages/workspace/package.json:1-26 { "name": "@topo/workspace", "version": "0.0.0", "private": true, "type": "module", "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } }, "scripts": { "build": "tsc -b", "typecheck": "tsc -b --pretty false", "test": "vitest run" }, "dependencies": { "@topo/schema": "workspace:*" }, "devDependencies": { "@types/node": "^22.20.1", "typescript": "^7.0.2", "vite": "8.2.2", "vitest": "^5.0.0" } }packages/workspace/src/index.ts:1-460 import { createHash, randomUUID } from "node:crypto"; import { lstat, mkdir, readFile, readdir, realpath, rename, rmdir, unlink, writeFile } from "node:fs/promises"; import { AsyncLocalStorage } from "node:async_hooks"; import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path"; import { serializeJson, type JsonValue } from "@topo/schema"; export const WORKSPACE_VERSION = "1.0"; export const DEFAULT_ENRICHMENT_TIMEOUT_MS = 10 * 60 * 1000; export interface WorkspaceEnrichmentConfig { command: string[]; promptFile?: string; timeoutMs?: number; } export interface WorkspaceCatalogueConfig { title?: string; description?: string; accentColor?: string; categoryOrder?: string[]; storyCategories?: Record<string, string>; storyIds?: string[]; } export interface WorkspaceConfig { schemaVersion: "1.0"; repositoryId: string; modules: string[]; analysis?: { languages: ("typescript" | "rust")[]; frameworks: "tauri"[] }; enrichment?: WorkspaceEnrichmentConfig; catalogue?: WorkspaceCatalogueConfig; } export function isMissing(error: unknown): boolean { return error instanceof Error && "code" in error && error.code === "ENOENT"; } function isExists(error: unknown): boolean { return error instanceof Error && "code" in error && error.code === "EEXIST"; } function optionalNonemptyString( value: unknown, name: string, ): string | undefined { if (value === undefined) return undefined; if (typeof value !== "string" || value.trim().length === 0) { throw new Error(`Workspace catalogue ${name} must be a nonempty string`); } return value; } function parseCatalogueConfig(input: unknown): WorkspaceCatalogueConfig { if (typeof input !== "object" || input === null || Array.isArray(input)) { throw new Error("Workspace catalogue must be an object"); } const value = input as Record<string, unknown>; const title = optionalNonemptyString(value.title, "title"); const description = optionalNonemptyString(value.description, "description"); const accentColor = optionalNonemptyString(value.accentColor, "accentColor"); if (accentColor !== undefined && !/^#[0-9a-fA-F]{6}$/.test(accentColor)) { throw new Error("Workspace catalogue accentColor must be a six-digit hex color"); } let categoryOrder: string[] | undefined; if (value.categoryOrder !== undefined) { if ( !Array.isArray(value.categoryOrder) || !value.categoryOrder.every( (item): item is string => typeof item === "string" && item.trim().length > 0, ) || new Set(value.categoryOrder).size !== value.categoryOrder.length ) { throw new Error( "Workspace catalogue categoryOrder must contain distinct nonempty strings", ); } categoryOrder = [...value.categoryOrder]; } let storyCategories: Record<string, string> | undefined; let storyIds: string[] | undefined; if (value.storyIds !== undefined) { if (!Array.isArray(value.storyIds) || !value.storyIds.every((id): id is string => typeof id === "string" && /^[a-z0-9][a-z0-9-]*$/.test(id)) || new Set(value.storyIds).size !== value.storyIds.length) { throw new Error("Workspace catalogue storyIds must contain distinct valid story ids"); } storyIds = [...value.storyIds]; } if (value.storyCategories !== undefined) { if ( typeof value.storyCategories !== "object" || value.storyCategories === null || Array.isArray(value.storyCategories) ) { throw new Error("Workspace catalogue storyCategories must be an object"); } const entries = Object.entries(value.storyCategories); if ( entries.some( ([id, category]) => !/^[a-z0-9][a-z0-9-]*$/.test(id) || typeof category !== "string" || category.trim().length === 0, ) ) { throw new Error( "Workspace catalogue storyCategories must map story ids to nonempty strings", ); } storyCategories = Object.fromEntries(entries); } const unknown = Object.keys(value).filter( (key) => ![ "title", "description", "accentColor", "categoryOrder", "storyCategories", "storyIds", ].includes(key), ); if (unknown.length) { throw new Error(`Unknown workspace catalogue keys: ${unknown.join(", ")}`); } return { ...(title === undefined ? {} : { title }), ...(description === undefined ? {} : { description }), ...(accentColor === undefined ? {} : { accentColor }), ...(categoryOrder === undefined ? {} : { categoryOrder }), ...(storyCategories === undefined ? {} : { storyCategories }), ...(storyIds === undefined ? {} : { storyIds }), }; } export function parseConfig(input: unknown): WorkspaceConfig { if (typeof input !== "object" || input === null || Array.isArray(input)) { throw new Error("Workspace config must be an object"); } const value = input as Record<string, unknown>; if (value.schemaVersion !== WORKSPACE_VERSION) { throw new Error(`Unsupported workspace version: ${String(value.schemaVersion)}`); } if (typeof value.repositoryId !== "string" || !value.repositoryId.trim()) { throw new Error("Workspace repositoryId must be a nonempty string"); } if (!Array.isArray(value.modules) || !value.modules.every((item): item is string => typeof item === "string" && item.length > 0) || new Set(value.modules).size !== value.modules.length) { throw new Error("Workspace modules must be distinct nonempty strings"); } let enrichment: WorkspaceEnrichmentConfig | undefined; if (value.enrichment !== undefined) { if (typeof value.enrichment !== "object" || value.enrichment === null || Array.isArray(value.enrichment)) { throw new Error("Workspace enrichment must be an object"); } const configured = value.enrichment as Record<string, unknown>; if (!Array.isArray(configured.command) || configured.command.length === 0 || !configured.command.every((item): item is string => typeof item === "string" && item.length > 0)) { throw new Error("Workspace enrichment command must be a nonempty argv array"); } if (configured.promptFile !== undefined && (typeof configured.promptFile !== "string" || configured.promptFile.length === 0)) { throw new Error("Workspace enrichment promptFile must be a nonempty string"); } if (configured.timeoutMs !== undefined && (typeof configured.timeoutMs !== "number" || !Number.isSafeInteger(configured.timeoutMs) || configured.timeoutMs <= 0 || configured.timeoutMs > 2_147_483_647)) { throw new Error("Workspace enrichment timeoutMs must be a positive safe timer integer"); } const enrichmentUnknown = Object.keys(configured).filter( (key) => !["command", "promptFile", "timeoutMs"].includes(key), ); if (enrichmentUnknown.length) { throw new Error(`Unknown workspace enrichment keys: ${enrichmentUnknown.join(", ")}`); } enrichment = { command: [...configured.command], ...(configured.promptFile === undefined ? {} : { promptFile: configured.promptFile }), ...(configured.timeoutMs === undefined ? {} : { timeoutMs: configured.timeoutMs }), }; } const catalogue = value.catalogue === undefined ? undefined : parseCatalogueConfig(value.catalogue); let analysis: WorkspaceConfig["analysis"]; if (value.analysis !== undefined) { const configured = value.analysis; if (typeof configured !== "object" || configured === null || Array.isArray(configured) || !("languages" in configured) || !Array.isArray(configured.languages) || !configured.languages.length || !configured.languages.every((id): id is "typescript" | "rust" => id === "typescript" || id === "rust") || !("frameworks" in configured) || !Array.isArray(configured.frameworks) || !configured.frameworks.every((id): id is "tauri" => id === "tauri") || new Set(configured.languages).size !== configured.languages.length || new Set(configured.frameworks).size !== configured.frameworks.length || Object.keys(configured).some((key) => key !== "languages" && key !== "frameworks")) { throw new Error("Workspace analysis requires distinct first-party languages [typescript, rust] and frameworks [tauri]"); } if (configured.frameworks.includes("tauri") && (!configured.languages.includes("typescript") || !configured.languages.includes("rust"))) { throw new Error("Tauri requires explicitly selected typescript and rust languages"); } analysis = { languages: [...configured.languages], frameworks: [...configured.frameworks] }; } const unknown = Object.keys(value).filter( (key) => ![ "schemaVersion", "repositoryId", "modules", "enrichment", "catalogue", "analysis", ].includes(key), ); if (unknown.length) throw new Error(`Unknown workspace config keys: ${unknown.join(", ")}`); return { schemaVersion: WORKSPACE_VERSION, repositoryId: value.repositoryId, modules: [...value.modules], ...(enrichment === undefined ? {} : { enrichment }), ...(catalogue === undefined ? {} : { catalogue }), ...(analysis === undefined ? {} : { analysis }), }; } export async function workspacePath(root: string, name: string): Promise<string> { if (isAbsolute(name) || name.includes("\\") || name.split("/").some((part) => part === ".." || part === "." || !part)) { throw new Error(`Invalid workspace-relative path: ${name}`); } const repository = await realpath(root); const base = resolve(repository, ".topo"); const path = resolve(base, name); const rel = relative(base, path); if (rel.startsWith(`..${sep}`) || rel === ".." || isAbsolute(rel)) { throw new Error(`Path escapes .topo: ${name}`); } for (const part of [base, ...name.split("/").map((_, index, parts) => resolve(base, ...parts.slice(0, index + 1)))]) { try { if ((await lstat(part)).isSymbolicLink()) throw new Error(`Refusing workspace symlink: ${part}`); } catch (error) { if (!isMissing(error)) throw error; } } return path; } async function createAuthoredFile(path: string, content: string): Promise<boolean> { try { await writeFile(path, content, { flag: "wx" }); return true; } catch (error) { if (!isExists(error)) throw error; return false; } } export async function loadConfig(root: string): Promise<WorkspaceConfig> { const path = await workspacePath(root, "config.json"); return parseConfig(JSON.parse(await readFile(path, "utf8")) as unknown); } export async function initializeWorkspace(root: string): Promise<{ config: WorkspaceConfig; created: boolean }> { const path = await workspacePath(root, "config.json"); await mkdir(dirname(path), { recursive: true }); const created = await createAuthoredFile(path, `${JSON.stringify({ schemaVersion: WORKSPACE_VERSION, repositoryId: basename(await realpath(root)), modules: [], } satisfies WorkspaceConfig, null, 2)}\n`); const config = await loadConfig(root); for (const directory of ["graph", "reports/inputs", "reports/outputs", "cache", "metadata"]) { const sentinel = await workspacePath(root, `${directory}/.directory-check`); await mkdir(dirname(sentinel), { recursive: true }); } await createAuthoredFile(await workspacePath(root, ".gitignore"), "# Local caches only; other artifacts are independently reviewable.\n/cache/\n"); return { config, created }; } export async function readArtifact(root: string, name: string): Promise<unknown> { return JSON.parse(await readFile(await workspacePath(root, name), "utf8")) as unknown; } export async function readOptionalArtifact(root: string, name: string): Promise<unknown | undefined> { try { return await readArtifact(root, name); } catch (error) { if (!isMissing(error)) throw error; return undefined; } } export async function writeGenerated(root: string, name: string, content: string | Uint8Array): Promise<void> { if (!["graph/", "reports/outputs/", "cache/"].some((prefix) => name.startsWith(prefix))) { throw new Error(`Refusing to overwrite authored workspace artifact: ${name}`); } const target = await workspacePath(root, name); const transaction = generation.getStore(); if (transaction) { if (transaction.root !== await realpath(root)) throw new Error("A generated transaction cannot write another repository"); transaction.files.set(name, Buffer.from(content)); return; } await mkdir(dirname(target), { recursive: true }); const temporary = `${target}.${randomUUID()}.tmp`; try { await writeFile(temporary, content, { flag: "wx" }); await rename(temporary, target); } finally { try { await unlink(temporary); } catch (error) { if (!isMissing(error)) throw error; } } } interface Generation { root: string; files: Map<string, Buffer | null>; directories: Set<string>; } const generation = new AsyncLocalStorage<Generation>(); export async function removeGenerated(root: string, name: string): Promise<void> { if (!["graph/", "reports/outputs/", "cache/"].some((prefix) => name.startsWith(prefix))) { throw new Error(`Refusing to remove authored workspace artifact: ${name}`); } const target = await workspacePath(root, name); let info; try { info = await lstat(target); } catch (error) { if (isMissing(error)) return; throw error; } if (info.isSymbolicLink()) throw new Error(`Refusing generated symlink: ${name}`); if (info.isDirectory()) { for (const entry of await readdir(target)) await removeGenerated(root, `${name}/${entry}`); const transaction = generation.getStore(); if (transaction) transaction.directories.add(name); else await rmdir(target); } else if (info.isFile()) { const transaction = generation.getStore(); if (transaction) transaction.files.set(name, null); else await unlink(target); } else throw new Error(`Unsupported generated artifact: ${name}`); } /** Prepare all output before replacement; reported publication errors restore the old generation. * This is rollback-capable, not crash-atomic. Callers hold the workspace write lock. */ export async function withGeneratedTransaction<T>( root: string, action: () => Promise<T>, operations: { rename?: typeof rename } = {}, ): Promise<T> { const repository = await realpath(root); const parent = generation.getStore(); if (parent) { if (parent.root !== repository) throw new Error("Cannot nest transactions across repositories"); return action(); } const transaction: Generation = { root: repository, files: new Map(), directories: new Set() }; const result = await generation.run(transaction, action); const id = randomUUID(); const prepared: { target: string; staged?: string; backup?: string; previous?: Buffer }[] = []; const committed: typeof prepared = []; let preserveBackups = false; async function current(target: string): Promise<Buffer | undefined> { try { if (!(await lstat(target)).isFile()) throw new Error(`Generated destination is not a regular file: ${target}`); return await readFile(target); } catch (error) { if (isMissing(error)) return undefined; throw error; } } try { for (const [name, contents] of transaction.files) { const target = await workspacePath(root, name); const previous = await current(target); await mkdir(dirname(target), { recursive: true }); const staged = contents === null ? undefined : `${target}.${id}.stage`; const backup = previous === undefined ? undefined : `${target}.${id}.rollback`; const entry = { target, ...(staged ? { staged } : {}), ...(backup ? { backup } : {}), ...(previous ? { previous } : {}) }; prepared.push(entry); if (staged) await writeFile(staged, contents!, { flag: "wx" }); if (backup) await writeFile(backup, previous!, { flag: "wx" }); } for (const file of prepared) { const actual = await current(file.target); if (actual === undefined ? file.previous !== undefined : file.previous === undefined || !actual.equals(file.previous)) { throw new Error(`Generated artifact changed during staging: ${file.target}`); } } for (const file of prepared) { if (file.staged) await (operations.rename ?? rename)(file.staged, file.target); else if (file.previous !== undefined) await unlink(file.target); committed.push(file); } } catch (error) { const failures: unknown[] = [error]; for (const file of committed.reverse()) { try { if (file.backup) await rename(file.backup, file.target); else if (file.staged) await unlink(file.target); } catch (rollbackError) { failures.push(rollbackError); } } preserveBackups = failures.length > 1; if (preserveBackups) throw new AggregateError(failures, `Generated rollback incomplete; preserve .${id}.rollback files for repair`); throw error; } finally { for (const file of prepared) { for (const path of [file.staged, ...(preserveBackups ? [] : [file.backup])]) { if (!path) continue; try { await unlink(path); } catch (error) { if (!isMissing(error)) throw error; } } } } for (const name of [...transaction.directories].sort((a, b) => b.length - a.length)) { const path = await workspacePath(root, name); if (!(await readdir(path)).length) await rmdir(path); } return result; } export async function writeAuthoredAtomic(root: string, name: string, content: string | Uint8Array): Promise<void> { let target = await workspacePath(root, name); await mkdir(dirname(target), { recursive: true }); target = await workspacePath(root, name); const temporary = `${target}.${randomUUID()}.tmp`; try { await writeFile(temporary, content, { flag: "wx" }); await rename(temporary, target); } finally { try { await unlink(temporary); } catch (error) { if (!isMissing(error)) throw error; } } } export async function withWorkspaceLock<T>(root: string, action: () => Promise<T>): Promise<T> { await initializeWorkspace(root); const lock = await workspacePath(root, "cache/write.lock"); try { await writeFile(lock, JSON.stringify({ pid: process.pid }), { flag: "wx" }); } catch (error) { if (!isExists(error)) throw error; throw new Error(`Workspace is locked: ${lock}. Wait for the running command; remove a stale lock only after verifying its process has stopped.`); } try { return await action(); } finally { await unlink(lock); } } export function cacheKey(input: JsonValue): string { return createHash("sha256").update(serializeJson(input)).digest("hex"); } -
@topo/modules
Open Dependency context by role: Build, test, docsOpen Dependency context by role: Development tools
Composes the built-in degree and cycle contributions. These are graph overlays, not language plugins. All declared internal dependencies (not runtime calls): @topo/modules -> @topo/graph [dependencies: workspace:*] @topo/modules -> @topo/schema [dependencies: workspace:*] Referenced by internal modules: @topo/cli -> @topo/modules [dependencies: workspace:*] @topo/site -> @topo/modules [dependencies: workspace:*] Stable drilldown identity: modules. Module-specific detail maps will be curated later; there is no placeholder target.
packages/modules/package.json:1-29 { "name": "@topo/modules", "version": "0.0.0", "private": true, "type": "module", "files": [ "dist" ], "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } }, "scripts": { "build": "tsc -b", "typecheck": "tsc -b --pretty false", "test": "vitest run" }, "dependencies": { "@topo/graph": "workspace:*", "@topo/schema": "workspace:*" }, "devDependencies": { "typescript": "^7.0.2", "vite": "8.2.2", "vitest": "^5.0.0" } }packages/modules/src/modules.ts:1-613 import { deriveArchitecture } from "@topo/graph"; import { assessModuleCompatibility, assertGraphDocument, canonicalizeGraphDocument, createAttributeId, serializeJson, type Evidence, type GraphAttribute, type GraphDocument, type JsonValue, type ModuleManifestEntry, type SupportedModules, } from "@topo/schema"; import type { StaticModuleManifest } from "./model.js"; const MODULE_VERSION = "0.0.0"; const MODULE_SCHEMA_VERSION = "1.0"; const DEGREE_MODULE_ID = "@topo/module-degree"; const CYCLES_MODULE_ID = "@topo/module-cycles"; const DEGREE_INCOMING_KEY = `${DEGREE_MODULE_ID}/incoming-edge-count`; const DEGREE_OUTGOING_KEY = `${DEGREE_MODULE_ID}/outgoing-edge-count`; const CYCLE_SIZE_KEY = `${CYCLES_MODULE_ID}/membership-size`; const SEMANTIC_VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/; const SCHEMA_VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)$/; export const BUILTIN_MODULE_MANIFESTS: readonly StaticModuleManifest[] = [ { id: DEGREE_MODULE_ID, version: MODULE_VERSION, schemaVersion: MODULE_SCHEMA_VERSION, label: "Dependency edge degree", dependencies: [], attributes: [ { key: DEGREE_INCOMING_KEY, label: "Incoming dependency edge count", subjectKind: "node", schema: { type: "integer", minimum: 0 }, }, { key: DEGREE_OUTGOING_KEY, label: "Outgoing dependency edge count", subjectKind: "node", schema: { type: "integer", minimum: 0 }, }, ], views: [ { id: `${DEGREE_MODULE_ID}/dependency-degree`, label: "Dependency edge degree", attributeKeys: [DEGREE_INCOMING_KEY, DEGREE_OUTGOING_KEY], }, ], }, { id: CYCLES_MODULE_ID, version: MODULE_VERSION, schemaVersion: MODULE_SCHEMA_VERSION, label: "Dependency cycle membership", dependencies: [], attributes: [ { key: CYCLE_SIZE_KEY, label: "Dependency cycle membership size", subjectKind: "node", schema: { type: "integer", minimum: 0 }, }, ], views: [ { id: `${CYCLES_MODULE_ID}/cycle-membership`, label: "Dependency cycle membership", attributeKeys: [CYCLE_SIZE_KEY], }, ], }, ] as const; interface ModuleContribution { attributes: GraphAttribute[]; evidence: Evidence[]; } function fail(message: string): never { throw new Error(`Module composition validation failed: ${message}`); } function requireNonEmpty(value: string, label: string): void { if (value.trim().length === 0) { fail(`${label} must not be empty.`); } } function ownedPrefix(moduleId: string): string { return `${moduleId}/`; } function evidencePrefix(moduleId: string): string { return `evidence:${ownedPrefix(moduleId)}`; } function attributeEvidenceId(moduleId: string, nodeId: string): string { return `${evidencePrefix(moduleId)}${nodeId}`; } function sorted(values: Iterable<string>): string[] { return [...values].sort(); } function derivationLocator( moduleId: string, nodeId: string, inputs: Readonly<Record<string, readonly string[]>>, ): string { const parameters = Object.keys(inputs) .sort() .map( (key) => `${encodeURIComponent(key)}=${encodeURIComponent( serializeJson([...(inputs[key] ?? [])]).trim(), )}`, ) .join("&"); return `topo:derivation/${encodeURIComponent(moduleId)}/${encodeURIComponent(nodeId)}?${parameters}`; } function derivedAttribute( moduleId: string, nodeId: string, key: string, value: number, method: string, evidenceId: string, ): GraphAttribute { return { id: createAttributeId(nodeId, key), subject: { kind: "node", id: nodeId }, key, value, provenance: { kind: "derived", moduleId, method, evidenceIds: [evidenceId], }, evidenceIds: [evidenceId], confidence: 1, }; } function degreeContribution(graph: GraphDocument): ModuleContribution { const incoming = new Map(graph.nodes.map((node) => [node.id, [] as string[]])); const outgoing = new Map(graph.nodes.map((node) => [node.id, [] as string[]])); for (const edge of graph.edges) { incoming.get(edge.targetId)?.push(edge.id); outgoing.get(edge.sourceId)?.push(edge.id); } const attributes: GraphAttribute[] = []; const evidence: Evidence[] = []; for (const node of graph.nodes) { const incomingEdgeIds = sorted(incoming.get(node.id) ?? []); const outgoingEdgeIds = sorted(outgoing.get(node.id) ?? []); const evidenceId = attributeEvidenceId(DEGREE_MODULE_ID, node.id); evidence.push({ id: evidenceId, kind: "annotation", label: `Dependency edge inputs for ${node.id}`, locator: derivationLocator(DEGREE_MODULE_ID, node.id, { incomingEdgeIds, outgoingEdgeIds, }), }); attributes.push( derivedAttribute( DEGREE_MODULE_ID, node.id, DEGREE_INCOMING_KEY, incomingEdgeIds.length, "count-incoming-dependency-edges", evidenceId, ), derivedAttribute( DEGREE_MODULE_ID, node.id, DEGREE_OUTGOING_KEY, outgoingEdgeIds.length, "count-outgoing-dependency-edges", evidenceId, ), ); } return { attributes, evidence }; } function cyclesContribution(graph: GraphDocument): ModuleContribution { const membership = new Map< string, { memberNodeIds: string[]; internalEdgeIds: string[] } >(); for (const component of deriveArchitecture(graph).stronglyConnectedComponents) { const inputs = { memberNodeIds: sorted(component.memberNodeIds), internalEdgeIds: sorted(component.internalEdgeIds), }; for (const nodeId of component.memberNodeIds) { membership.set(nodeId, inputs); } } const evidenceId = `${evidencePrefix(CYCLES_MODULE_ID)}graph-inputs`; const evidence: Evidence[] = [ { id: evidenceId, kind: "annotation", label: "Directed graph inputs for strongly connected components", locator: derivationLocator(CYCLES_MODULE_ID, graph.graphId, { nodeIds: sorted(graph.nodes.map((node) => node.id)), edges: sorted( graph.edges.map( (edge) => `${edge.id}\u0000${edge.sourceId}\u0000${edge.targetId}`, ), ), }), }, ]; const attributes: GraphAttribute[] = []; for (const node of graph.nodes) { const inputs = membership.get(node.id) ?? { memberNodeIds: [], internalEdgeIds: [], }; attributes.push( derivedAttribute( CYCLES_MODULE_ID, node.id, CYCLE_SIZE_KEY, inputs.memberNodeIds.length, "strongly-connected-cycle-membership-size", evidenceId, ), ); } return { attributes, evidence }; } function generateContribution( moduleId: string, graph: GraphDocument, ): ModuleContribution { switch (moduleId) { case DEGREE_MODULE_ID: return degreeContribution(graph); case CYCLES_MODULE_ID: return cyclesContribution(graph); default: return fail(`No generator is compiled for module "${moduleId}".`); } } function moduleEntry(manifest: StaticModuleManifest): ModuleManifestEntry { return { id: manifest.id, version: manifest.version, schemaVersion: manifest.schemaVersion, }; } function isCompatibleEntry( entry: ModuleManifestEntry, manifest: StaticModuleManifest, ): boolean { return assessModuleCompatibility([entry], moduleSupport([manifest])) .authoritative; } function equalCanonical(left: unknown, right: unknown): boolean { return serializeJson(left as JsonValue) === serializeJson(right as JsonValue); } function equalAttributes( left: readonly GraphAttribute[], right: readonly GraphAttribute[], ): boolean { const byId = (values: readonly GraphAttribute[]) => [...values].sort((first, second) => first.id < second.id ? -1 : first.id > second.id ? 1 : 0 ); return equalCanonical(byId(left), byId(right)); } function referencedEvidenceIds(graph: GraphDocument): Set<string> { const references = new Set<string>(); for (const edge of graph.edges) { edge.provenance.evidenceIds.forEach((id) => references.add(id)); } for (const attribute of graph.attributes) { attribute.provenance.evidenceIds.forEach((id) => references.add(id)); attribute.evidenceIds.forEach((id) => references.add(id)); } return references; } export function validateModuleCatalog( manifests: readonly StaticModuleManifest[], ): void { const moduleIds = new Set<string>(); const attributeOwners = new Map<string, string>(); const viewOwners = new Map<string, string>(); for (const manifest of manifests) { requireNonEmpty(manifest.id, "Module identifier"); if (moduleIds.has(manifest.id)) { fail(`Duplicate module identifier "${manifest.id}".`); } moduleIds.add(manifest.id); requireNonEmpty(manifest.label, `Module "${manifest.id}" label`); if (!SEMANTIC_VERSION_PATTERN.test(manifest.version)) { fail( `Module "${manifest.id}" has invalid implementation version "${manifest.version}".`, ); } if (!SCHEMA_VERSION_PATTERN.test(manifest.schemaVersion)) { fail( `Module "${manifest.id}" has invalid schema version "${manifest.schemaVersion}".`, ); } if (manifest.dependencies.length > 0) { fail(`Module "${manifest.id}" must depend only on core.`); } const localAttributes = new Set<string>(); for (const attribute of manifest.attributes) { requireNonEmpty(attribute.key, `Module "${manifest.id}" attribute key`); requireNonEmpty( attribute.label, `Module "${manifest.id}" attribute "${attribute.key}" label`, ); if (!attribute.key.startsWith(ownedPrefix(manifest.id))) { fail( `Attribute "${attribute.key}" must be namespaced to module "${manifest.id}".`, ); } if (localAttributes.has(attribute.key)) { fail( `Module "${manifest.id}" declares attribute "${attribute.key}" more than once.`, ); } const owner = attributeOwners.get(attribute.key); if (owner !== undefined) { fail( `Attribute "${attribute.key}" is declared by both "${owner}" and "${manifest.id}".`, ); } localAttributes.add(attribute.key); attributeOwners.set(attribute.key, manifest.id); if ( attribute.subjectKind !== "node" || attribute.schema.type !== "integer" || attribute.schema.minimum !== 0 ) { fail( `Attribute "${attribute.key}" must be a node integer with minimum 0.`, ); } } for (const view of manifest.views) { requireNonEmpty(view.id, `Module "${manifest.id}" view identifier`); requireNonEmpty(view.label, `Module "${manifest.id}" view "${view.id}" label`); if (!view.id.startsWith(ownedPrefix(manifest.id))) { fail(`View "${view.id}" must be namespaced to module "${manifest.id}".`); } const owner = viewOwners.get(view.id); if (owner !== undefined) { fail( `View "${view.id}" is registered by both "${owner}" and "${manifest.id}".`, ); } viewOwners.set(view.id, manifest.id); const keys = new Set<string>(); for (const key of view.attributeKeys) { if (!localAttributes.has(key)) { fail( `View "${view.id}" references undeclared attribute "${key}".`, ); } if (keys.has(key)) { fail(`View "${view.id}" references attribute "${key}" more than once.`); } keys.add(key); } } } } export function moduleSupport( manifests: readonly StaticModuleManifest[] = BUILTIN_MODULE_MANIFESTS, ): SupportedModules { validateModuleCatalog(manifests); return Object.fromEntries( [...manifests] .sort((left, right) => (left.id < right.id ? -1 : left.id > right.id ? 1 : 0)) .map((manifest) => [ manifest.id, { version: manifest.version, schemaVersion: manifest.schemaVersion, }, ]), ); } export function validateModuleContributions( graph: GraphDocument, manifests: readonly StaticModuleManifest[] = BUILTIN_MODULE_MANIFESTS, ): void { validateModuleCatalog(manifests); assertGraphDocument(graph); const manifestById = new Map(manifests.map((manifest) => [manifest.id, manifest])); const entryById = new Map(graph.modules.map((entry) => [entry.id, entry])); const attributeOwner = new Map( manifests.flatMap((manifest) => manifest.attributes.map((attribute) => [attribute.key, manifest.id] as const), ), ); const compatibleIds = new Set( manifests .filter((manifest) => { const entry = entryById.get(manifest.id); return entry !== undefined && isCompatibleEntry(entry, manifest); }) .map((manifest) => manifest.id), ); const allEvidenceReferences = referencedEvidenceIds(graph); for (const attribute of graph.attributes) { const owner = attributeOwner.get(attribute.key); if (owner !== undefined) { if (!entryById.has(owner)) { fail( `Attribute "${attribute.id}" uses owned key "${attribute.key}" without module "${owner}".`, ); } if (compatibleIds.has(owner) && attribute.provenance.moduleId !== owner) { fail( `Attribute "${attribute.id}" shadows module-owned key "${attribute.key}".`, ); } } if ( compatibleIds.has(attribute.provenance.moduleId) && owner !== attribute.provenance.moduleId ) { fail( `Module "${attribute.provenance.moduleId}" contributed undeclared attribute "${attribute.key}".`, ); } } for (const moduleId of compatibleIds) { const expected = generateContribution(moduleId, graph); const actualAttributes = graph.attributes.filter( (attribute) => attribute.provenance.moduleId === moduleId, ); if (!equalAttributes(actualAttributes, expected.attributes)) { fail(`Module "${moduleId}" attributes do not match their derivation inputs.`); } const expectedEvidenceById = new Map( expected.evidence.map((evidence) => [evidence.id, evidence]), ); for (const [evidenceId, expectedEvidence] of expectedEvidenceById) { const actualEvidence = graph.evidence.find( (evidence) => evidence.id === evidenceId, ); if ( actualEvidence === undefined || !equalCanonical(actualEvidence, expectedEvidence) ) { fail( `Module "${moduleId}" evidence "${evidenceId}" does not match its derivation inputs.`, ); } } } for (const evidence of graph.evidence) { for (const manifest of manifests) { if ( evidence.id.startsWith(evidencePrefix(manifest.id)) && !entryById.has(manifest.id) && !allEvidenceReferences.has(evidence.id) ) { fail( `Module-owned evidence "${evidence.id}" is present without module "${manifest.id}".`, ); } } for (const moduleId of compatibleIds) { if ( evidence.id.startsWith(evidencePrefix(moduleId)) && !graph.attributes.some( (attribute) => attribute.provenance.moduleId === moduleId && (attribute.evidenceIds.includes(evidence.id) || attribute.provenance.evidenceIds.includes(evidence.id)), ) && !graph.edges.some((edge) => edge.provenance.evidenceIds.includes(evidence.id), ) && !graph.attributes.some( (attribute) => attribute.provenance.moduleId !== moduleId && (attribute.evidenceIds.includes(evidence.id) || attribute.provenance.evidenceIds.includes(evidence.id)), ) ) { fail(`Module "${moduleId}" has unreferenced evidence "${evidence.id}".`); } } } } export function composeModules( graph: GraphDocument, enabledModuleIds: readonly string[], ): GraphDocument { validateModuleCatalog(BUILTIN_MODULE_MANIFESTS); assertGraphDocument(graph); validateModuleContributions(graph); const manifestById = new Map<string, StaticModuleManifest>( BUILTIN_MODULE_MANIFESTS.map((manifest) => [manifest.id, manifest]), ); const enabled = new Set<string>(); for (const moduleId of enabledModuleIds) { if (enabled.has(moduleId)) { fail(`Configured module "${moduleId}" is duplicated.`); } if (!manifestById.has(moduleId)) { fail(`Configured module "${moduleId}" is not compiled into this generator.`); } enabled.add(moduleId); } for (const entry of graph.modules) { const manifest = manifestById.get(entry.id); if ( manifest !== undefined && (entry.version !== manifest.version || entry.schemaVersion !== manifest.schemaVersion) ) { fail( `Module "${entry.id}" version ${entry.version} / schema ${entry.schemaVersion} is unsupported by this generator.`, ); } } const knownIds = new Set<string>(manifestById.keys()); const baseAttributes = graph.attributes.filter( (attribute) => !knownIds.has(attribute.provenance.moduleId), ); const base: GraphDocument = { ...graph, modules: graph.modules.filter((entry) => !knownIds.has(entry.id)), attributes: baseAttributes, evidence: graph.evidence, }; const retainedEvidenceIds = referencedEvidenceIds(base); base.evidence = graph.evidence.filter( (evidence) => ![...knownIds].some((moduleId) => evidence.id.startsWith(evidencePrefix(moduleId)), ) || retainedEvidenceIds.has(evidence.id), ); const attributes = [...base.attributes]; const evidence = [...base.evidence]; const evidenceById = new Map(evidence.map((item) => [item.id, item])); const modules = [...base.modules]; for (const moduleId of sorted(enabled)) { const manifest = manifestById.get(moduleId)!; const contribution = generateContribution(moduleId, base); modules.push(moduleEntry(manifest)); attributes.push(...contribution.attributes); for (const item of contribution.evidence) { const existing = evidenceById.get(item.id); if (existing !== undefined) { if (!equalCanonical(existing, item)) { fail(`Evidence identifier "${item.id}" conflicts with module "${moduleId}".`); } continue; } evidence.push(item); evidenceById.set(item.id, item); } } const composed = canonicalizeGraphDocument({ ...base, modules, attributes, evidence, }); assertGraphDocument(composed); validateModuleContributions(composed); return composed; } -
@topo/enrichment
Open Dependency context by role: Build, test, docsOpen Dependency context by role: Development tools
Parses and validates enrichment documents, reference membership and analysis hashes. The CLI, not this data package, executes the configured enrichment command. All declared internal dependencies (not runtime calls): @topo/enrichment -> @topo/schema [dependencies: workspace:*] Referenced by internal modules: @topo/cli -> @topo/enrichment [dependencies: workspace:*] @topo/site -> @topo/enrichment [dependencies: workspace:*] Stable drilldown identity: enrichment. Module-specific detail maps will be curated later; there is no placeholder target.
packages/enrichment/package.json:1-28 { "name": "@topo/enrichment", "version": "0.0.0", "private": true, "type": "module", "files": [ "dist" ], "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } }, "scripts": { "build": "tsc -b", "typecheck": "tsc -b --pretty false", "test": "vitest run" }, "dependencies": { "@topo/schema": "workspace:*" }, "devDependencies": { "typescript": "^7.0.2", "vite": "8.2.2", "vitest": "^5.0.0" } }packages/enrichment/src/enrichment.ts:1-130 import { compareCodeUnits, serializeGraphDocument, serializeJson, type GraphDocument, type JsonValue, } from "@topo/schema"; import type { AnalysisInput, EnrichmentDocument } from "./model.js"; export const MAX_ENRICHMENT_BYTES = 4 * 1024 * 1024; export class EnrichmentValidationError extends Error { constructor(message: string) { super(message); this.name = "EnrichmentValidationError"; } } function fail(path: string, message: string): never { throw new EnrichmentValidationError(`${path}: ${message}`); } function record(value: unknown, path: string, keys: string[]): Record<string, unknown> { if (value === null || typeof value !== "object" || Array.isArray(value) || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) { fail(path, "Expected a JSON object."); } for (const key of Object.keys(value)) { if (!keys.includes(key)) fail(`${path}.${key}`, "Unknown field."); } return value as Record<string, unknown>; } function references(value: unknown, path: string): string[] { if (!Array.isArray(value) || value.length > 10_000) fail(path, "Expected at most 10000 reference IDs."); const ids: string[] = []; for (const id of value) { if (typeof id !== "string" || !id.length) fail(path, "Reference IDs must be nonempty strings."); ids.push(id); } if (new Set(ids).size !== ids.length) fail(path, "Duplicate reference IDs."); return ids.sort(compareCodeUnits); } function serializeUnchecked(document: EnrichmentDocument): string { return serializeJson({ schemaVersion: document.schemaVersion, analysisHash: document.analysisHash, provenance: document.provenance, comments: document.comments.map((comment) => ({ text: comment.text, nodeIds: comment.nodeIds, evidenceIds: comment.evidenceIds, })), }); } export function parseEnrichmentDocument(value: unknown): EnrichmentDocument { const input = record(value, "$", ["schemaVersion", "analysisHash", "provenance", "comments"]); if (input.schemaVersion !== "1.0") fail("$.schemaVersion", "Expected enrichment version 1.0."); if (typeof input.analysisHash !== "string" || !/^[a-f0-9]{64}$/.test(input.analysisHash)) { fail("$.analysisHash", "Expected a lowercase SHA-256 hash."); } if (input.provenance !== "inferred") fail("$.provenance", 'AI commentary must remain "inferred".'); if (!Array.isArray(input.comments) || input.comments.length > 10_000) { fail("$.comments", "Expected at most 10000 comments."); } const comments = input.comments.map((value: unknown, index: number) => { const path = `$.comments[${index}]`; const comment = record(value, path, ["text", "nodeIds", "evidenceIds"]); if (typeof comment.text !== "string" || !comment.text.trim() || comment.text.length > 32_000) { fail(`${path}.text`, "Expected nonblank commentary of at most 32000 characters."); } const nodeIds = references(comment.nodeIds, `${path}.nodeIds`); const evidenceIds = references(comment.evidenceIds, `${path}.evidenceIds`); if (!nodeIds.length && !evidenceIds.length) fail(path, "Commentary needs a node or evidence reference."); return { text: comment.text, nodeIds, evidenceIds }; }); const document: EnrichmentDocument = { schemaVersion: "1.0", analysisHash: input.analysisHash, provenance: "inferred", comments, }; if (new TextEncoder().encode(serializeUnchecked(document)).byteLength > MAX_ENRICHMENT_BYTES) { fail("$", "Canonical enrichment exceeds 4 MiB."); } return document; } export function validateEnrichmentReferences(document: EnrichmentDocument, graph: GraphDocument): void { const nodes = new Set(graph.nodes.map((node) => node.id)); const evidence = new Set(graph.evidence.map((item) => item.id)); for (const [index, comment] of document.comments.entries()) { for (const id of comment.nodeIds) { if (!nodes.has(id)) fail(`$.comments[${index}].nodeIds`, `Unknown node "${id}".`); } for (const id of comment.evidenceIds) { if (!evidence.has(id)) fail(`$.comments[${index}].evidenceIds`, `Unknown evidence "${id}".`); } } } export function serializeEnrichmentDocument(document: EnrichmentDocument): string { return serializeUnchecked(parseEnrichmentDocument(document)); } function assertJson(value: unknown, ancestors = new Set<object>()): asserts value is JsonValue { if (value === null || typeof value === "string" || typeof value === "boolean") return; if (typeof value === "number" && Number.isFinite(value)) return; if (typeof value !== "object" || ancestors.has(value) || (!Array.isArray(value) && ![Object.prototype, null].includes(Object.getPrototypeOf(value)))) { fail("$.dashboard", "Expected finite, acyclic JSON data."); } ancestors.add(value); try { for (const child of Array.isArray(value) ? value : Object.values(value)) assertJson(child, ancestors); } finally { ancestors.delete(value); } } export async function hashAnalysis(graph: GraphDocument, dashboard: unknown): Promise<string> { assertJson(dashboard); // Layout, authored views and AI output are not inputs to the static analysis. const content = `{"schemaVersion":"1.0","graph":${serializeGraphDocument(graph).trim()},"dashboard":${serializeJson(dashboard).trim()}}\n`; if (!globalThis.crypto?.subtle) throw new EnrichmentValidationError("Analysis hashing requires Web Crypto (localhost or HTTPS in a browser)."); const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(content)); return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join(""); } export async function createAnalysisInput(graph: GraphDocument, dashboard: unknown): Promise<AnalysisInput> { assertJson(dashboard); return { schemaVersion: "1.0", analysisHash: await hashAnalysis(graph, dashboard), graph, dashboard }; } -
@topo/graph
Open Graph projection and edge aggregationOpen Dependency context by role: Build, test, docsOpen Dependency context by role: Development tools
Derives architecture, graph projections, aggregation, deterministic layout and layout deltas from validated source evidence. All declared internal dependencies (not runtime calls): @topo/graph -> @topo/schema [dependencies: workspace:*] Referenced by internal modules: @topo/cli -> @topo/graph [dependencies: workspace:*] @topo/views -> @topo/graph [dependencies: workspace:*] @topo/site -> @topo/graph [dependencies: workspace:*] @topo/modules -> @topo/graph [dependencies: workspace:*] Stable drilldown identity: graph. Open the source-backed sequence for this module's core algorithm.
packages/graph/package.json:1-28 { "name": "@topo/graph", "version": "0.0.0", "private": true, "type": "module", "files": [ "dist" ], "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } }, "scripts": { "build": "tsc -b", "typecheck": "tsc -b --pretty false", "test": "vitest run" }, "dependencies": { "@topo/schema": "workspace:*" }, "devDependencies": { "typescript": "^7.0.2", "vite": "8.2.2", "vitest": "^5.0.0" } }packages/graph/src/index.ts:1-5 export * from "./canonical.js"; export * from "./derive.js"; export * from "./layout.js"; export * from "./project.js"; export * from "./types.js";
source-traced: cli to languages
Direct dependencies declaration; not a runtime call. See node details for the complete manifest relationship set.
{
"name": "@topo/cli",
"version": "0.0.0",
"private": true,
"type": "module",
"bin": {
"topo": "./dist/main.js"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"scripts": {
"build": "tsc -b && node ../../scripts/package-skills.mjs",
"typecheck": "tsc -b --pretty false",
"test": "vitest run --no-file-parallelism"
},
"dependencies": {
"@topo/diagram-core": "workspace:*",
"@topo/enrichment": "workspace:*",
"@topo/graph": "workspace:*",
"@topo/languages": "workspace:*",
"@topo/modules": "workspace:*",
"@topo/reports": "workspace:*",
"@topo/scanner": "workspace:*",
"@topo/schema": "workspace:*",
"@topo/site": "workspace:*",
"@topo/story": "workspace:*",
"@topo/views": "workspace:*",
"@topo/workspace": "workspace:*"
},
"devDependencies": {
"@types/node": "^22.20.1",
"typescript": "^7.0.2",
"vite": "8.2.2",
"vitest": "^5.0.0"
}
}
source-traced: languages to scanner
Direct dependencies declaration; not a runtime call. See node details for the complete manifest relationship set.
{
"name": "@topo/languages",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"scripts": {
"build": "tsc -b",
"typecheck": "tsc -b --pretty false",
"test": "node --test test/*.test.mjs"
},
"dependencies": {
"@topo/scanner": "workspace:*",
"@topo/schema": "workspace:*",
"typescript-compiler-api": "npm:typescript@6.0.3"
},
"optionalDependencies": {
"@ast-grep/lang-rust": "0.0.7",
"@ast-grep/napi": "0.45.3",
"@iarna/toml": "2.2.5"
},
"devDependencies": {
"@types/node": "^22",
"typescript": "^7.0.2"
}
}
source-traced: scanner to schema
Direct dependencies declaration; not a runtime call. See node details for the complete manifest relationship set.
{
"name": "@topo/scanner",
"version": "0.0.0",
"private": true,
"type": "module",
"files": [
"dist"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"scripts": {
"build": "tsc -b",
"fixture": "node dist/fixture.js",
"typecheck": "tsc -b --pretty false",
"test": "vitest run src"
},
"dependencies": {
"@topo/schema": "workspace:*",
"ignore": "^7.0.0",
"typescript-compiler-api": "npm:typescript@6.0.3"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^7.0.2",
"vite": "8.2.2",
"vitest": "^5.0.0"
}
}
source-traced: cli to diagram-core
Direct dependencies declaration; not a runtime call. See node details for the complete manifest relationship set.
{
"name": "@topo/cli",
"version": "0.0.0",
"private": true,
"type": "module",
"bin": {
"topo": "./dist/main.js"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"scripts": {
"build": "tsc -b && node ../../scripts/package-skills.mjs",
"typecheck": "tsc -b --pretty false",
"test": "vitest run --no-file-parallelism"
},
"dependencies": {
"@topo/diagram-core": "workspace:*",
"@topo/enrichment": "workspace:*",
"@topo/graph": "workspace:*",
"@topo/languages": "workspace:*",
"@topo/modules": "workspace:*",
"@topo/reports": "workspace:*",
"@topo/scanner": "workspace:*",
"@topo/schema": "workspace:*",
"@topo/site": "workspace:*",
"@topo/story": "workspace:*",
"@topo/views": "workspace:*",
"@topo/workspace": "workspace:*"
},
"devDependencies": {
"@types/node": "^22.20.1",
"typescript": "^7.0.2",
"vite": "8.2.2",
"vitest": "^5.0.0"
}
}
source-traced: diagram-core to story
Direct dependencies declaration; not a runtime call. See node details for the complete manifest relationship set.
{
"name": "@topo/diagram-core",
"version": "0.0.0",
"private": true,
"type": "module",
"files": [
"dist",
"archify-pin.json",
"LICENSE",
"THIRD_PARTY_NOTICES.md"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"scripts": {
"build": "tsc -b",
"typecheck": "tsc -b --pretty false",
"test": "tsc -b && node --test test/*.test.mjs"
},
"dependencies": {
"@topo/story": "workspace:*",
"@viz-js/viz": "3.30.0"
},
"peerDependencies": {
"@jdylanmc/topo-archify": "0.1.0"
},
"devDependencies": {
"@types/node": "^22",
"typescript": "^7.0.2"
}
}
source-traced: cli to workspace
Direct dependencies declaration; not a runtime call. See node details for the complete manifest relationship set.
{
"name": "@topo/cli",
"version": "0.0.0",
"private": true,
"type": "module",
"bin": {
"topo": "./dist/main.js"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"scripts": {
"build": "tsc -b && node ../../scripts/package-skills.mjs",
"typecheck": "tsc -b --pretty false",
"test": "vitest run --no-file-parallelism"
},
"dependencies": {
"@topo/diagram-core": "workspace:*",
"@topo/enrichment": "workspace:*",
"@topo/graph": "workspace:*",
"@topo/languages": "workspace:*",
"@topo/modules": "workspace:*",
"@topo/reports": "workspace:*",
"@topo/scanner": "workspace:*",
"@topo/schema": "workspace:*",
"@topo/site": "workspace:*",
"@topo/story": "workspace:*",
"@topo/views": "workspace:*",
"@topo/workspace": "workspace:*"
},
"devDependencies": {
"@types/node": "^22.20.1",
"typescript": "^7.0.2",
"vite": "8.2.2",
"vitest": "^5.0.0"
}
}
Direct dependencies declaration; not a runtime call. See node details for the complete manifest relationship set.
{
"name": "@topo/cli",
"version": "0.0.0",
"private": true,
"type": "module",
"bin": {
"topo": "./dist/main.js"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"scripts": {
"build": "tsc -b && node ../../scripts/package-skills.mjs",
"typecheck": "tsc -b --pretty false",
"test": "vitest run --no-file-parallelism"
},
"dependencies": {
"@topo/diagram-core": "workspace:*",
"@topo/enrichment": "workspace:*",
"@topo/graph": "workspace:*",
"@topo/languages": "workspace:*",
"@topo/modules": "workspace:*",
"@topo/reports": "workspace:*",
"@topo/scanner": "workspace:*",
"@topo/schema": "workspace:*",
"@topo/site": "workspace:*",
"@topo/story": "workspace:*",
"@topo/views": "workspace:*",
"@topo/workspace": "workspace:*"
},
"devDependencies": {
"@types/node": "^22.20.1",
"typescript": "^7.0.2",
"vite": "8.2.2",
"vitest": "^5.0.0"
}
}
Direct dependencies declaration; not a runtime call. See node details for the complete manifest relationship set.
{
"name": "@topo/cli",
"version": "0.0.0",
"private": true,
"type": "module",
"bin": {
"topo": "./dist/main.js"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"scripts": {
"build": "tsc -b && node ../../scripts/package-skills.mjs",
"typecheck": "tsc -b --pretty false",
"test": "vitest run --no-file-parallelism"
},
"dependencies": {
"@topo/diagram-core": "workspace:*",
"@topo/enrichment": "workspace:*",
"@topo/graph": "workspace:*",
"@topo/languages": "workspace:*",
"@topo/modules": "workspace:*",
"@topo/reports": "workspace:*",
"@topo/scanner": "workspace:*",
"@topo/schema": "workspace:*",
"@topo/site": "workspace:*",
"@topo/story": "workspace:*",
"@topo/views": "workspace:*",
"@topo/workspace": "workspace:*"
},
"devDependencies": {
"@types/node": "^22.20.1",
"typescript": "^7.0.2",
"vite": "8.2.2",
"vitest": "^5.0.0"
}
}
Direct dependencies declaration; not a runtime call. See node details for the complete manifest relationship set.
{
"name": "@topo/site",
"version": "0.0.0",
"private": true,
"type": "module",
"files": [
"dist"
],
"exports": {
"./notices": "./dist/THIRD_PARTY_NOTICES.txt",
"./data": {
"types": "./src/data.ts",
"default": "./dist/data-contract/data.js"
}
},
"scripts": {
"build": "node scripts/clean-dist.mjs && tsc -p tsconfig.json --noEmit && tsc -p tsconfig.data.json",
"typecheck": "tsc -p tsconfig.json --noEmit --pretty false && yarn typecheck:browser",
"typecheck:browser": "tsc -p tsconfig.browser-tests.json --noEmit --pretty false",
"test": "vitest run --config vitest.config.ts",
"test:browser": "playwright test --config playwright.config.mjs"
},
"dependencies": {
"@topo/enrichment": "workspace:*",
"@topo/graph": "workspace:*",
"@topo/modules": "workspace:*",
"@topo/schema": "workspace:*",
"@topo/views": "workspace:*"
},
"devDependencies": {
"@playwright/test": "^1.55.0",
"@topo/diagram-core": "workspace:*",
"@types/node": "^22.18.0",
"typescript": "^7.0.2",
"vite": "8.2.2",
"vitest": "^5.0.0"
}
}
Direct dependencies declaration; not a runtime call. See node details for the complete manifest relationship set.
{
"name": "@topo/views",
"version": "0.0.0",
"private": true,
"type": "module",
"files": [
"dist"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"scripts": {
"build": "tsc -b",
"typecheck": "tsc -b --pretty false",
"test": "vitest run"
},
"dependencies": {
"@topo/graph": "workspace:*",
"@topo/schema": "workspace:*",
"ignore": "^7.0.0"
},
"devDependencies": {
"typescript": "^7.0.2",
"vite": "8.2.2",
"vitest": "^5.0.0"
}
}
source-traced: site to enrichment
Direct dependencies declaration; not a runtime call. See node details for the complete manifest relationship set.
{
"name": "@topo/site",
"version": "0.0.0",
"private": true,
"type": "module",
"files": [
"dist"
],
"exports": {
"./notices": "./dist/THIRD_PARTY_NOTICES.txt",
"./data": {
"types": "./src/data.ts",
"default": "./dist/data-contract/data.js"
}
},
"scripts": {
"build": "node scripts/clean-dist.mjs && tsc -p tsconfig.json --noEmit && tsc -p tsconfig.data.json",
"typecheck": "tsc -p tsconfig.json --noEmit --pretty false && yarn typecheck:browser",
"typecheck:browser": "tsc -p tsconfig.browser-tests.json --noEmit --pretty false",
"test": "vitest run --config vitest.config.ts",
"test:browser": "playwright test --config playwright.config.mjs"
},
"dependencies": {
"@topo/enrichment": "workspace:*",
"@topo/graph": "workspace:*",
"@topo/modules": "workspace:*",
"@topo/schema": "workspace:*",
"@topo/views": "workspace:*"
},
"devDependencies": {
"@playwright/test": "^1.55.0",
"@topo/diagram-core": "workspace:*",
"@types/node": "^22.18.0",
"typescript": "^7.0.2",
"vite": "8.2.2",
"vitest": "^5.0.0"
}
}
source-traced: site to modules
Direct dependencies declaration; not a runtime call. See node details for the complete manifest relationship set.
{
"name": "@topo/site",
"version": "0.0.0",
"private": true,
"type": "module",
"files": [
"dist"
],
"exports": {
"./notices": "./dist/THIRD_PARTY_NOTICES.txt",
"./data": {
"types": "./src/data.ts",
"default": "./dist/data-contract/data.js"
}
},
"scripts": {
"build": "node scripts/clean-dist.mjs && tsc -p tsconfig.json --noEmit && tsc -p tsconfig.data.json",
"typecheck": "tsc -p tsconfig.json --noEmit --pretty false && yarn typecheck:browser",
"typecheck:browser": "tsc -p tsconfig.browser-tests.json --noEmit --pretty false",
"test": "vitest run --config vitest.config.ts",
"test:browser": "playwright test --config playwright.config.mjs"
},
"dependencies": {
"@topo/enrichment": "workspace:*",
"@topo/graph": "workspace:*",
"@topo/modules": "workspace:*",
"@topo/schema": "workspace:*",
"@topo/views": "workspace:*"
},
"devDependencies": {
"@playwright/test": "^1.55.0",
"@topo/diagram-core": "workspace:*",
"@types/node": "^22.18.0",
"typescript": "^7.0.2",
"vite": "8.2.2",
"vitest": "^5.0.0"
}
}