/* Maintains packages/os/site-kernel/src/workspace-discovery.ts as an authored site-kernel authored module so agents can evolve it without rediscovering local boundaries. Do not execute package scripts or load application modules. Do not implement the full pnpm glob language beyond supported repository patterns. Workspace discovery walks the filesystem — no registry file is required. RFC-0253: add shared workspace discovery for ACP, workspace surface, and test signal checks. RFC-1097: step 6 — compass.migrate codemod run Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order. RFC-1097: sweep — werkstatt-engine clean Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics. RFC-1251: step 1 — normalize 7 diagnostic emitters to canonical ruleIds */ import { join, relative } from "node:path"; import { parse } from "yaml"; import type { Diagnostic } from "@warpgogol/werkstatt-shared/kernel"; import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io"; export interface WorkspacePackageInfo { name: string; directory: string; absoluteDirectory: string; packageJsonPath: string; workspacePattern: string; kind: "app" | "package" | "os-package" | "service" | "mission" | "other"; packageJson: { name?: string; version?: string; scripts?: Record; dependencies?: Record; devDependencies?: Record; peerDependencies?: Record; gogol?: unknown; }; } export interface WorkspaceDiscoveryResult { workspaceRoot: string; packageGlobs: string[]; packages: WorkspacePackageInfo[]; diagnostics: Diagnostic[]; } type WorkspacePackageJson = WorkspacePackageInfo["packageJson"]; const PNPM_WORKSPACE_PATH = "pnpm-workspace.yaml"; function toPosixPath(value: string): string { return value.replace(/\\/g, "/"); } async function pathExists( filePath: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { try { await io.readFile(filePath); return true; } catch { return false; } } const WORKSPACE_DISCOVERY_RULES = { "01": "WORKSPACE-DISCOVERY-01", "02": "WORKSPACE-DISCOVERY-02", "03": "WORKSPACE-DISCOVERY-03", "04": "WORKSPACE-DISCOVERY-04", } as const; function workspaceDiscoveryDiagnostic( suffix: keyof typeof WORKSPACE_DISCOVERY_RULES, message: string, fixHint: string, data?: Record, ): Diagnostic { return { ruleId: WORKSPACE_DISCOVERY_RULES[suffix], severity: "error", file: PNPM_WORKSPACE_PATH, message, fixHint, ...(data ? { data } : {}), }; } function workspacePackageGlobsFromYaml(source: string, _diagnostics: Diagnostic[]): string[] { const parsed = parse(source) as { packages?: unknown }; if (!Array.isArray(parsed.packages)) return []; return parsed.packages .filter((entry): entry is string => typeof entry === "string") .filter((entry) => entry.trim().length > 0); } function isSupportedWorkspacePattern(pattern: string): boolean { const rawPattern = pattern.startsWith("!") ? pattern.slice(1) : pattern; return rawPattern .split("/") .filter(Boolean) .every((segment) => segment === "*" || /^[a-zA-Z0-9._-]+$/.test(segment)); } async function expandWorkspacePattern( workspaceRoot: string, pattern: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { const segments = pattern.split("/").filter(Boolean); let candidates = [workspaceRoot]; for (const segment of segments) { const nextCandidates: string[] = []; for (const candidate of candidates) { if (segment === "*") { let entries; try { entries = await io.readdir(candidate); } catch { continue; } for (const entry of entries) { if (!entry.isDirectory || entry.name.startsWith(".") || entry.name.startsWith("-")) continue; nextCandidates.push(join(candidate, entry.name)); } continue; } nextCandidates.push(join(candidate, segment)); } candidates = nextCandidates; } const packageDirectories: string[] = []; for (const candidate of candidates) { if (await pathExists(join(candidate, "package.json"))) packageDirectories.push(candidate); } return packageDirectories; } export function workspacePackageKind(directory: string): WorkspacePackageInfo["kind"] { const normalized = toPosixPath(directory); if (normalized.startsWith("apps/")) return "app"; if (normalized.startsWith("packages/os/")) return "os-package"; if (normalized.startsWith("packages/")) return "package"; if (normalized.startsWith("services/")) return "service"; if (normalized.startsWith("missions/")) return "mission"; return "other"; } export async function discoverWorkspacePackages( workspaceRoot: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { const diagnostics: Diagnostic[] = []; const workspaceSource = await io.readFile(join(workspaceRoot, PNPM_WORKSPACE_PATH)); const rawPackageGlobs = workspacePackageGlobsFromYaml(workspaceSource, diagnostics); const packageGlobs = rawPackageGlobs .filter((pattern) => !pattern.startsWith("!")) .sort((a, b) => a.localeCompare(b)); const negativeGlobs = rawPackageGlobs .filter((pattern) => pattern.startsWith("!")) .map((pattern) => pattern.slice(1)) .sort((a, b) => a.localeCompare(b)); const unsupportedPatterns = rawPackageGlobs.filter( (pattern) => !isSupportedWorkspacePattern(pattern), ); for (const pattern of unsupportedPatterns) { diagnostics.push( workspaceDiscoveryDiagnostic( "03", `Workspace pattern ${pattern} is not supported by the shared discovery helper.`, "Use literal path segments and * wildcards, or extend discoverWorkspacePackages with a tested parser before using this pattern.", { workspacePattern: pattern }, ), ); } const byDirectory = new Map< string, { absoluteDirectory: string; workspacePattern: string; matches: string[] } >(); for (const pattern of packageGlobs.filter((pattern) => isSupportedWorkspacePattern(pattern))) { for (const absoluteDirectory of await expandWorkspacePattern(workspaceRoot, pattern)) { const directory = toPosixPath(relative(workspaceRoot, absoluteDirectory)); const existing = byDirectory.get(directory); if (!existing) { byDirectory.set(directory, { absoluteDirectory, workspacePattern: pattern, matches: [pattern], }); continue; } existing.matches.push(pattern); if (pattern.localeCompare(existing.workspacePattern) < 0) existing.workspacePattern = pattern; } } for (const pattern of negativeGlobs.filter((pattern) => isSupportedWorkspacePattern(pattern))) { for (const absoluteDirectory of await expandWorkspacePattern(workspaceRoot, pattern)) { byDirectory.delete(toPosixPath(relative(workspaceRoot, absoluteDirectory))); } } const packages: WorkspacePackageInfo[] = []; for (const [directory, entry] of [...byDirectory.entries()].sort((a, b) => a[0].localeCompare(b[0]), )) { if (entry.matches.length > 1) { diagnostics.push( workspaceDiscoveryDiagnostic( "04", `Workspace package ${directory} is matched by multiple patterns.`, "Keep workspace package globs non-overlapping, or rely on the deterministic lexicographic tie-break recorded in workspacePattern.", { directory, workspacePatterns: entry.matches.sort((a, b) => a.localeCompare(b)), selectedPattern: entry.workspacePattern, }, ), ); } const kind = workspacePackageKind(directory); if (kind === "other") { diagnostics.push( workspaceDiscoveryDiagnostic( "01", `Workspace package ${directory} is not classified.`, "Add an explicit package kind rule for this workspace directory family.", { directory, workspacePattern: entry.workspacePattern }, ), ); } const packageJsonPath = join(entry.absoluteDirectory, "package.json"); const packageJson = JSON.parse(await io.readFile(packageJsonPath)) as WorkspacePackageJson; if (!packageJson.name) { diagnostics.push( workspaceDiscoveryDiagnostic( "02", `Workspace package ${directory} is missing package.json name.`, "Set package.json name so workspace package discovery can identify it.", { directory, workspacePattern: entry.workspacePattern }, ), ); } packages.push({ name: packageJson.name ?? directory, directory, absoluteDirectory: entry.absoluteDirectory, packageJsonPath, workspacePattern: entry.workspacePattern, kind, packageJson, }); } return { workspaceRoot, packageGlobs, packages, diagnostics, }; }