import { readdir, readFile, stat } from "node:fs/promises"; import path from "node:path"; import { HWPOD_NODE_OPS_CONTRACT_VERSION } from "../../tools/src/hwpod-node-ops-contract.ts"; import { readHwpodSpec } from "../../tools/src/hwpod-harness-lib.ts"; const HWPOD_SPEC_DISCOVERY_CONTRACT_VERSION = "hwpod-spec-discovery-v1"; const DEFAULT_HWPOD_SPEC_PATH = ".hwlab/hwpod-spec.yaml"; const DEFAULT_HWPOD_SPEC_REGISTRY_DIR = ".hwlab/hwpod-specs"; export { HWPOD_SPEC_DISCOVERY_CONTRACT_VERSION }; export async function discoverHwpodSpecs(options: any = {}) { const env = options.env ?? process.env; const workspace = text(env.HWLAB_CODE_AGENT_CODEX_WORKSPACE) || text(env.HWLAB_CODE_AGENT_WORKSPACE) || process.cwd(); const roots = uniquePaths([workspace, process.cwd(), options.repoRoot].filter(Boolean)); const explicitPaths = splitEnvList(env.HWLAB_HWPOD_SPEC_PATHS).map((value) => resolveMaybeRelative(value, process.cwd())); const explicitDirs = splitEnvList(env.HWLAB_HWPOD_SPEC_REGISTRY_DIRS).map((value) => resolveMaybeRelative(value, process.cwd())); const candidatePaths = uniquePaths([ ...explicitPaths, ...roots.map((root) => path.resolve(root, DEFAULT_HWPOD_SPEC_PATH)), ...await candidateRegistrySpecPaths(uniquePaths([ ...explicitDirs, ...roots.map((root) => path.resolve(root, DEFAULT_HWPOD_SPEC_REGISTRY_DIR)) ])) ]); const discovered = []; for (const specPath of candidatePaths) { const item = await readSpecCandidate(specPath); if (item) discovered.push(item); } return dedupeSpecs(discovered); } export function hwpodSpecDiscoveryPayload(specs: any[], options: any = {}) { const availableCount = specs.filter((item) => item.availability?.ok === true).length; const probed = specs.some((item) => item.availability?.status !== "unprobed"); return { ok: true, status: specs.length === 0 ? "empty" : "completed", contractVersion: HWPOD_SPEC_DISCOVERY_CONTRACT_VERSION, nodeOpsContractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, route: "/v1/hwpod/specs", specAuthority: "workspace-or-registry", count: specs.length, availableCount: probed ? availableCount : null, specs, observedAt: options.observedAt ?? new Date().toISOString() }; } export function hwpodSpecWorkspaceProbePlan(spec: any, options: any = {}) { const document = spec.document ?? spec; const metadata = objectValue(document.metadata); const body = objectValue(document.spec); const nodeBinding = objectValue(body.nodeBinding); const workspace = objectValue(body.workspace); const hwpodId = text(metadata.name) || text(metadata.uid); const nodeId = text(nodeBinding.nodeId); return { contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, planId: options.planId ?? `hwpod_spec_probe_${safeId(hwpodId || nodeId || "spec")}_${Date.now()}`, hwpodId, nodeId, intent: "workspace.ls", source: { compiler: "hwpod-spec-discovery", specPath: spec.specPath ?? options.specPath, specAuthority: spec.authority ?? "workspace-or-registry" }, resourceHints: { workspacePath: workspace.path, targetDevice: body.targetDevice, debugProbe: body.debugProbe, ioProbe: body.ioProbe }, ops: [ { opId: "op_01_workspace_ls", op: "workspace.ls", args: { hwpodId, workspacePath: workspace.path, path: ".", targetDevice: body.targetDevice, debugProbe: body.debugProbe, ioProbe: body.ioProbe } } ] }; } async function candidateRegistrySpecPaths(dirs: string[]) { const paths: string[] = []; for (const dir of dirs) { try { const entries = await readdir(dir, { withFileTypes: true }); for (const entry of entries) { if (!entry.isFile()) continue; if (!/\.ya?ml$/iu.test(entry.name)) continue; paths.push(path.join(dir, entry.name)); } } catch (error) { if (error?.code !== "ENOENT" && error?.code !== "ENOTDIR") throw error; } } return paths; } async function readSpecCandidate(specPath: string) { try { const info = await stat(specPath); if (!info.isFile()) return null; const document = await readHwpodSpec(specPath); const metadata = await readSpecMetadata(specPath); return summarizeSpec({ document, specPath, metadata }); } catch (error) { if (error?.code === "ENOENT" || error?.code === "ENOTDIR") return null; return { ok: false, status: "invalid", specPath, error: { code: error?.code ?? "invalid_hwpod_spec", message: error?.message ?? String(error) }, availability: { ok: false, status: "invalid", checkedAt: new Date().toISOString() } }; } } async function readSpecMetadata(specPath: string) { for (const candidate of metadataCandidates(specPath)) { try { const parsed = JSON.parse(await readFile(candidate, "utf8")); return isPlainObject(parsed) ? { ...parsed, metadataPath: candidate } : { metadataPath: candidate }; } catch (error) { if (error?.code !== "ENOENT") return { metadataPath: candidate, metadataError: error?.message ?? String(error) }; } } return {}; } function metadataCandidates(specPath: string) { const parsed = path.parse(specPath); const basename = parsed.name === "hwpod-spec" ? "hwpod-spec.meta.json" : `${parsed.name}.meta.json`; return [path.join(parsed.dir, basename), `${specPath}.meta.json`]; } function summarizeSpec({ document, specPath, metadata }: { document: any; specPath: string; metadata: any }) { const docMetadata = objectValue(document.metadata); const body = objectValue(document.spec); const nodeBinding = objectValue(body.nodeBinding); const workspace = objectValue(body.workspace); const name = text(docMetadata.name) || text(docMetadata.uid); const uid = text(docMetadata.uid) || null; const source = clean({ kind: text(metadata?.source?.kind) || "workspace-or-registry", caseRepo: text(metadata?.source?.caseRepo), caseId: text(metadata?.source?.caseId), caseSpecPath: text(metadata?.source?.caseSpecPath), verificationIssue: text(metadata?.source?.verificationIssue), verificationPr: text(metadata?.source?.verificationPr), verifiedAt: text(metadata?.source?.verifiedAt), metadataPath: text(metadata?.metadataPath), metadataError: text(metadata?.metadataError) }); return { ok: true, status: "discovered", name, hwpodId: name, uid, metadata: docMetadata, specPath, authority: source.kind, source, nodeBinding, nodeId: text(nodeBinding.nodeId), nodeType: text(nodeBinding.nodeType) || null, workspace, workspacePath: text(workspace.path), targetDevice: body.targetDevice, debugProbe: body.debugProbe, ioProbe: body.ioProbe, document, availability: { ok: null, status: "unprobed", checkedAt: null } }; } function dedupeSpecs(specs: any[]) { const byKey = new Map(); for (const spec of specs) { const key = spec.ok === false ? `invalid:${spec.specPath}` : [spec.name || spec.uid, spec.nodeId, spec.workspacePath].filter(Boolean).join("|") || spec.specPath; const previous = byKey.get(key); if (!previous || specPriority(spec) > specPriority(previous)) byKey.set(key, spec); } return Array.from(byKey.values()).sort((left, right) => String(left.name ?? left.specPath).localeCompare(String(right.name ?? right.specPath))); } function specPriority(spec: any) { const sourceKind = text(spec.source?.kind); const specPath = text(spec.specPath); if (specPath.endsWith(DEFAULT_HWPOD_SPEC_PATH)) return 30; if (sourceKind.includes("preinstalled")) return 20; return 10; } function splitEnvList(value: unknown) { return String(value ?? "").split(/[,\n]/u).map((item) => item.trim()).filter(Boolean); } function resolveMaybeRelative(value: string, base: string) { return path.isAbsolute(value) ? value : path.resolve(base, value); } function uniquePaths(values: string[]) { return Array.from(new Set(values.map((value) => path.resolve(value)))); } function safeId(value: string) { return String(value || "spec").replace(/[^A-Za-z0-9_.:-]+/gu, "_").slice(0, 80) || "spec"; } function text(value: unknown) { return typeof value === "string" && value.trim() ? value.trim() : ""; } function objectValue(value: any) { return isPlainObject(value) ? value : {}; } function isPlainObject(value: any) { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function clean(value: Record) { return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== "" && item !== null)); }