feat(hwpod): add runtime spec CRUD
This commit is contained in:
+19
-4
@@ -1,12 +1,13 @@
|
||||
/*
|
||||
* SPEC: PJ2026-010103 HWPOD 服务。
|
||||
* SPEC: PJ2026-010103 HWPOD 服务;
|
||||
* PJ2026-01010305 71FREQ 预装 draft-2026-06-26-71freq-v03-hwpod-preinstall。
|
||||
* 实现引用: draft-2026-07-20-hwpod-temporal-split。
|
||||
* 责任: 独立 HWPOD API HTTP 合同。
|
||||
*/
|
||||
|
||||
import { validateHwpodOperationInput } from "./contracts.ts";
|
||||
|
||||
export function createHwpodHttpApp(options: { runtimeApiUrl: string; runtimeApiAuthorization: string; temporal: any }) {
|
||||
export function createHwpodHttpApp(options: { runtimeApiUrl: string; runtimeApiAuthorization: string; temporal: any; specRegistry: any }) {
|
||||
return {
|
||||
async fetch(request: Request) {
|
||||
const url = new URL(request.url);
|
||||
@@ -14,7 +15,19 @@ export function createHwpodHttpApp(options: { runtimeApiUrl: string; runtimeApiA
|
||||
if (url.pathname === "/health/live") return json(200, { ok: true, serviceId: "hwlab-hwpod-api", status: "live", valuesPrinted: false });
|
||||
if (url.pathname === "/health/ready") return readiness(options);
|
||||
if (url.pathname === "/v1/hwpod" && request.method === "GET") return json(200, { ok: true, serviceId: "hwlab-hwpod-api", executionAuthority: "temporal", runtimeApiUrl: options.runtimeApiUrl, valuesPrinted: false });
|
||||
if (url.pathname === "/v1/hwpod/specs" && request.method === "GET") return proxyGet(options.runtimeApiUrl, `/v1/hwpod/specs${url.search}`, options.runtimeApiAuthorization);
|
||||
if (url.pathname === "/v1/hwpod/specs" && request.method === "GET") return json(200, await options.specRegistry.list());
|
||||
if (url.pathname === "/v1/hwpod/specs" && request.method === "POST") {
|
||||
const body = await request.json();
|
||||
return json(201, await options.specRegistry.create(specDocument(body)));
|
||||
}
|
||||
const specMatch = /^\/v1\/hwpod\/specs\/([^/]+)$/u.exec(url.pathname);
|
||||
if (specMatch) {
|
||||
const hwpodId = decodeURIComponent(specMatch[1]);
|
||||
if (request.method === "GET") return json(200, await options.specRegistry.get(hwpodId));
|
||||
if (request.method === "PUT") return json(200, await options.specRegistry.update(hwpodId, specDocument(await request.json())));
|
||||
if (request.method === "DELETE") return json(200, await options.specRegistry.delete(hwpodId));
|
||||
return json(405, { ok: false, error: { code: "method_not_allowed", message: "GET, PUT, or DELETE required" }, valuesPrinted: false });
|
||||
}
|
||||
if (url.pathname === "/v1/hwpod/topology" && request.method === "GET") return proxyGet(options.runtimeApiUrl, `/v1/hwpod/topology${url.search}`, options.runtimeApiAuthorization);
|
||||
if (url.pathname === "/v1/hwpod-node-ops" && request.method === "GET") return proxyGet(options.runtimeApiUrl, `/v1/hwpod-node-ops${url.search}`, options.runtimeApiAuthorization);
|
||||
if (url.pathname === "/v1/hwpod/operations" && request.method === "POST") {
|
||||
@@ -35,7 +48,8 @@ export function createHwpodHttpApp(options: { runtimeApiUrl: string; runtimeApiA
|
||||
return json(404, { ok: false, error: { code: "hwpod_route_not_found", message: "HWPOD API route was not found" } });
|
||||
} catch (error: any) {
|
||||
const code = error?.code ?? "hwpod_http_error";
|
||||
return json(code.includes("not_found") ? 404 : 400, { ok: false, error: { code, message: error?.message ?? String(error) }, valuesPrinted: false });
|
||||
const status = code.includes("not_found") ? 404 : code === "hwpod_spec_frozen" || code === "hwpod_spec_exists" || code === "hwpod_spec_authority_conflict" ? 409 : 400;
|
||||
return json(status, { ok: false, status: "failed", error: { code, message: error?.message ?? String(error), ...(error?.details && typeof error.details === "object" ? error.details : {}) }, valuesPrinted: false });
|
||||
}
|
||||
},
|
||||
close: async () => options.temporal.close()
|
||||
@@ -47,4 +61,5 @@ async function readiness(options: { runtimeApiUrl: string; runtimeApiAuthorizati
|
||||
catch { return json(503, { ok: false, serviceId: "hwlab-hwpod-api", temporal: "configured", runtimeApi: "unreachable", valuesPrinted: false }); }
|
||||
}
|
||||
async function proxyGet(base: string, route: string, authorization: string) { const response = await fetch(`${base}${route}`, { headers: { authorization }, signal: AbortSignal.timeout(10_000) }); return new Response(await response.text(), { status: response.status, headers: { "content-type": response.headers.get("content-type") ?? "application/json", "cache-control": "no-store" } }); }
|
||||
function specDocument(body: any) { return body && typeof body === "object" && !Array.isArray(body) && body.document !== undefined ? body.document : body; }
|
||||
function json(status: number, body: unknown) { return Response.json(body, { status, headers: { "cache-control": "no-store" } }); }
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
/*
|
||||
* SPEC: PJ2026-010103 HWPOD 服务。
|
||||
* SPEC: PJ2026-010103 HWPOD 服务;
|
||||
* PJ2026-01010305 71FREQ 预装 draft-2026-06-26-71freq-v03-hwpod-preinstall。
|
||||
* 实现引用: draft-2026-07-20-hwpod-temporal-split。
|
||||
* 责任: HWPOD API 的 Temporal 与迁移期 Cloud API 配置解析。
|
||||
*/
|
||||
|
||||
import { createHwpodTemporalGateway } from "./temporal.ts";
|
||||
import { hwpodSpecRegistryFromEnv } from "./spec-registry.ts";
|
||||
|
||||
export function hwpodRuntime(env: Record<string, string | undefined> = process.env) {
|
||||
const address = String(env.HWPOD_TEMPORAL_ADDRESS ?? env.TEMPORAL_ADDRESS ?? "").trim();
|
||||
@@ -14,7 +16,15 @@ export function hwpodRuntime(env: Record<string, string | undefined> = process.e
|
||||
const runtimeApiAuthorization = String(env.HWPOD_RUNTIME_API_AUTHORIZATION ?? "").trim();
|
||||
if (!runtimeApiUrl) throw codedError("hwpod_runtime_api_url_required", "HWPOD_RUNTIME_API_URL is required");
|
||||
if (!runtimeApiAuthorization) throw codedError("hwpod_runtime_api_authorization_required", "HWPOD_RUNTIME_API_AUTHORIZATION is required");
|
||||
return { address, namespace, taskQueue, runtimeApiUrl, runtimeApiAuthorization, temporal: createHwpodTemporalGateway({ address, namespace, taskQueue }) };
|
||||
return {
|
||||
address,
|
||||
namespace,
|
||||
taskQueue,
|
||||
runtimeApiUrl,
|
||||
runtimeApiAuthorization,
|
||||
temporal: createHwpodTemporalGateway({ address, namespace, taskQueue }),
|
||||
specRegistry: hwpodSpecRegistryFromEnv(env),
|
||||
};
|
||||
}
|
||||
|
||||
function codedError(code: string, message: string) { return Object.assign(new Error(message), { code }); }
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* SPEC: PJ2026-010101 HWPOD 标准;PJ2026-010102 HWPOD 工具。
|
||||
* 责任: 提供 HWPOD spec document 的共享标准化与校验。
|
||||
*/
|
||||
|
||||
export function normalizeHwpodSpec(document: any, specPath: string) {
|
||||
const root = objectValue(document);
|
||||
if (root.kind !== "Hwpod") throw codedError("invalid_hwpod_spec_kind", "hwpod-spec kind must be Hwpod", { specPath, kind: root.kind });
|
||||
const spec = objectValue(root.spec);
|
||||
const metadata = objectValue(root.metadata);
|
||||
for (const key of ["targetDevice", "workspace", "debugProbe", "ioProbe"]) {
|
||||
if (!isPlainObject(spec[key])) throw codedError("invalid_hwpod_spec_element", `hwpod-spec missing ${key}`, { specPath, element: key });
|
||||
}
|
||||
const workspacePath = requiredText(spec.workspace.path, "spec.workspace.path");
|
||||
const nodeBinding = objectValue(spec.nodeBinding);
|
||||
const nodeId = requiredText(nodeBinding.nodeId, "spec.nodeBinding.nodeId");
|
||||
const name = requiredText(metadata.name ?? metadata.uid, "metadata.name");
|
||||
return {
|
||||
apiVersion: text(root.apiVersion) || "hwlab.pikastech.com/v1alpha1",
|
||||
kind: "Hwpod",
|
||||
metadata: { ...metadata, name },
|
||||
spec: {
|
||||
...spec,
|
||||
nodeBinding: { ...nodeBinding, nodeId },
|
||||
workspace: { ...spec.workspace, path: workspacePath },
|
||||
targetDevice: spec.targetDevice,
|
||||
debugProbe: spec.debugProbe,
|
||||
ioProbe: spec.ioProbe,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function objectValue(value: unknown) { return isPlainObject(value) ? value as Record<string, any> : {}; }
|
||||
function isPlainObject(value: unknown) { return Boolean(value && typeof value === "object" && !Array.isArray(value)); }
|
||||
function text(value: unknown) { return typeof value === "string" && value.trim() ? value.trim() : ""; }
|
||||
function requiredText(value: unknown, name: string) { const normalized = text(value); if (!normalized) throw codedError("required_option_missing", `${name} is required`, { name }); return normalized; }
|
||||
function codedError(code: string, message: string, details: Record<string, unknown> = {}) { return Object.assign(new Error(message), { code, details }); }
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* SPEC: PJ2026-010102 HWPOD 工具;PJ2026-010103 HWPOD 服务;
|
||||
* PJ2026-01010305 71FREQ 预装 draft-2026-06-26-71freq-v03-hwpod-preinstall。
|
||||
* 责任: 验证 runtime spec CRUD、重启持久化与 YAML-first 内置冻结合同。
|
||||
*/
|
||||
|
||||
import { afterEach, expect, test } from "bun:test";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { createHwpodHttpApp } from "./http.ts";
|
||||
import { createHwpodSpecRegistry } from "./spec-registry.ts";
|
||||
|
||||
const temporaryDirs: string[] = [];
|
||||
afterEach(async () => { await Promise.all(temporaryDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); });
|
||||
|
||||
test("runtime spec repository persists CRUD and freezes YAML-first built-ins", async () => {
|
||||
const runtimeDir = await tempDir();
|
||||
const builtIn = spec("builtin-pod", "/builtin");
|
||||
const registry = createHwpodSpecRegistry({ runtimeDir, builtIns: [{ configRef: "config/hwpod.yaml#spec", sha256: "source-sha", document: builtIn }] });
|
||||
|
||||
const initial = await registry.list();
|
||||
expect(initial.specs).toHaveLength(1);
|
||||
expect(initial.specs[0]).toMatchObject({ hwpodId: "builtin-pod", authority: "yaml-first-builtin", mutable: false, frozen: true });
|
||||
|
||||
const created = await registry.create(spec("runtime-pod", "/runtime-v1"));
|
||||
expect(created).toMatchObject({ status: "created", hwpodId: "runtime-pod", authority: "runtime", mutable: true, frozen: false, mutation: true });
|
||||
|
||||
const restarted = createHwpodSpecRegistry({ runtimeDir, builtIns: [{ configRef: "config/hwpod.yaml#spec", document: builtIn }] });
|
||||
expect((await restarted.get("runtime-pod")).document.spec.workspace.path).toBe("/runtime-v1");
|
||||
expect((await restarted.update("runtime-pod", spec("runtime-pod", "/runtime-v2"))).status).toBe("updated");
|
||||
expect((await restarted.get("runtime-pod")).document.spec.workspace.path).toBe("/runtime-v2");
|
||||
expect((await restarted.delete("runtime-pod")).status).toBe("deleted");
|
||||
await expect(restarted.get("runtime-pod")).rejects.toMatchObject({ code: "hwpod_spec_not_found" });
|
||||
|
||||
await expect(restarted.create(spec("builtin-pod", "/shadow"))).rejects.toMatchObject({ code: "hwpod_spec_frozen", details: { mutation: false } });
|
||||
await expect(restarted.update("builtin-pod", builtIn)).rejects.toMatchObject({ code: "hwpod_spec_frozen" });
|
||||
await expect(restarted.delete("builtin-pod")).rejects.toMatchObject({ code: "hwpod_spec_frozen" });
|
||||
expect((await restarted.get("builtin-pod")).document.spec.workspace.path).toBe("/builtin");
|
||||
});
|
||||
|
||||
test("L1 HWPOD HTTP exposes runtime CRUD and typed frozen errors", async () => {
|
||||
const runtimeDir = await tempDir();
|
||||
const registry = createHwpodSpecRegistry({ runtimeDir, builtIns: [{ configRef: "config/hwpod.yaml#spec", document: spec("builtin-pod", "/builtin") }] });
|
||||
const temporal = { close: async () => {}, start: async () => ({}), describe: async () => ({}), result: async () => ({}) };
|
||||
const app = createHwpodHttpApp({ runtimeApiUrl: "http://runtime.invalid", runtimeApiAuthorization: "Bearer test", temporal, specRegistry: registry });
|
||||
|
||||
const created = await request(app, "POST", "/v1/hwpod/specs", { document: spec("runtime-http", "/http-v1") });
|
||||
expect(created).toMatchObject({ status: 201, body: { ok: true, status: "created", hwpodId: "runtime-http" } });
|
||||
expect(await request(app, "GET", "/v1/hwpod/specs/runtime-http")).toMatchObject({ status: 200, body: { document: { spec: { workspace: { path: "/http-v1" } } } } });
|
||||
expect(await request(app, "PUT", "/v1/hwpod/specs/runtime-http", { document: spec("runtime-http", "/http-v2") })).toMatchObject({ status: 200, body: { status: "updated" } });
|
||||
expect(await request(app, "DELETE", "/v1/hwpod/specs/runtime-http")).toMatchObject({ status: 200, body: { status: "deleted", mutation: true } });
|
||||
|
||||
const frozen = await request(app, "DELETE", "/v1/hwpod/specs/builtin-pod");
|
||||
expect(frozen).toMatchObject({ status: 409, body: { ok: false, error: { code: "hwpod_spec_frozen", authority: "yaml-first-builtin", mutation: false } } });
|
||||
expect((await request(app, "GET", "/v1/hwpod/specs/builtin-pod")).body.document.spec.workspace.path).toBe("/builtin");
|
||||
await app.close();
|
||||
});
|
||||
|
||||
async function request(app: any, method: string, pathname: string, body?: unknown) {
|
||||
const response = await app.fetch(new Request(`http://hwpod.test${pathname}`, { method, headers: { "content-type": "application/json" }, ...(body === undefined ? {} : { body: JSON.stringify(body) }) }));
|
||||
return { status: response.status, body: await response.json() };
|
||||
}
|
||||
|
||||
async function tempDir() { const dir = await mkdtemp(path.join(tmpdir(), "hwpod-spec-registry-")); temporaryDirs.push(dir); return dir; }
|
||||
function spec(name: string, workspacePath: string) { return { apiVersion: "hwlab.dev/v0alpha1", kind: "Hwpod", metadata: { name }, spec: { targetDevice: { board: "test" }, workspace: { path: workspacePath }, debugProbe: { type: "test" }, ioProbe: { type: "test" }, nodeBinding: { nodeId: "node-test" } } }; }
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* SPEC: PJ2026-010102 HWPOD 工具;PJ2026-010103 HWPOD 服务;
|
||||
* PJ2026-01010305 71FREQ 预装 draft-2026-06-26-71freq-v03-hwpod-preinstall。
|
||||
* 责任: 为 L0 native function 与 L1 API 提供同一 HWPOD runtime spec registry。
|
||||
*/
|
||||
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import { normalizeHwpodSpec } from "./spec-document.ts";
|
||||
|
||||
const CONTRACT_VERSION = "hwpod-spec-registry-v2";
|
||||
|
||||
type BuiltInInput = { configRef: string; sha256?: string; document: unknown };
|
||||
type RuntimeRecord = { contractVersion: string; authority: "runtime"; hwpodId: string; createdAt: string; updatedAt: string; document: unknown };
|
||||
|
||||
export function hwpodSpecRegistryFromEnv(env: Record<string, string | undefined> = process.env, options: { cwd?: string; runtimeDir?: string } = {}) {
|
||||
const cwd = options.cwd ?? process.cwd();
|
||||
const runtimeDirValue = String(options.runtimeDir ?? env.HWPOD_RUNTIME_SPEC_REGISTRY_DIR ?? "").trim();
|
||||
if (!runtimeDirValue) throw codedError("hwpod_runtime_spec_registry_dir_required", "HWPOD_RUNTIME_SPEC_REGISTRY_DIR or --runtime-spec-dir is required");
|
||||
const runtimeDir = path.resolve(cwd, runtimeDirValue);
|
||||
const builtIns = parseBuiltIns(env.HWPOD_BUILTIN_SPEC_DOCUMENTS_JSON);
|
||||
return createHwpodSpecRegistry({ runtimeDir, builtIns });
|
||||
}
|
||||
|
||||
export function createHwpodSpecRegistry(options: { runtimeDir: string; builtIns?: BuiltInInput[]; now?: () => string }) {
|
||||
const runtimeDir = path.resolve(options.runtimeDir);
|
||||
const now = options.now ?? (() => new Date().toISOString());
|
||||
const builtIns = normalizeBuiltIns(options.builtIns ?? []);
|
||||
|
||||
async function list() {
|
||||
const runtime = await readRuntimeRecords(runtimeDir);
|
||||
assertNoAuthorityConflicts(builtIns, runtime);
|
||||
const specs = [...builtIns.values(), ...runtime.values()]
|
||||
.map(summary)
|
||||
.sort((left, right) => left.hwpodId.localeCompare(right.hwpodId));
|
||||
return { ok: true, status: specs.length === 0 ? "empty" : "completed", contractVersion: CONTRACT_VERSION, count: specs.length, specs, valuesPrinted: false };
|
||||
}
|
||||
|
||||
async function get(hwpodId: string) {
|
||||
const id = validId(hwpodId);
|
||||
const builtIn = builtIns.get(id);
|
||||
if (builtIn) return entity(builtIn);
|
||||
const runtime = (await readRuntimeRecords(runtimeDir)).get(id);
|
||||
if (!runtime) throw codedError("hwpod_spec_not_found", `HWPOD spec was not found: ${id}`, { hwpodId: id });
|
||||
return entity(runtime);
|
||||
}
|
||||
|
||||
async function create(documentInput: unknown) {
|
||||
const normalized = normalizedDocument(documentInput, "runtime:create");
|
||||
const id = specId(normalized);
|
||||
assertNotFrozen(id, builtIns);
|
||||
const runtime = await readRuntimeRecords(runtimeDir);
|
||||
if (runtime.has(id)) throw codedError("hwpod_spec_exists", `HWPOD runtime spec already exists: ${id}`, { hwpodId: id });
|
||||
const observedAt = now();
|
||||
const record: RuntimeRecord = { contractVersion: CONTRACT_VERSION, authority: "runtime", hwpodId: id, createdAt: observedAt, updatedAt: observedAt, document: normalized };
|
||||
await writeRuntimeRecord(runtimeDir, record);
|
||||
return mutation("created", record);
|
||||
}
|
||||
|
||||
async function update(hwpodId: string, documentInput: unknown) {
|
||||
const id = validId(hwpodId);
|
||||
assertNotFrozen(id, builtIns);
|
||||
const runtime = await readRuntimeRecords(runtimeDir);
|
||||
const existing = runtime.get(id);
|
||||
if (!existing) throw codedError("hwpod_spec_not_found", `HWPOD runtime spec was not found: ${id}`, { hwpodId: id });
|
||||
const normalized = normalizedDocument(documentInput, `runtime:${id}`);
|
||||
const documentId = specId(normalized);
|
||||
if (documentId !== id) throw codedError("hwpod_spec_id_mismatch", `HWPOD spec document id ${documentId} does not match route id ${id}`, { hwpodId: id, documentHwpodId: documentId });
|
||||
const record: RuntimeRecord = { ...existing, updatedAt: now(), document: normalized };
|
||||
await writeRuntimeRecord(runtimeDir, record);
|
||||
return mutation("updated", record);
|
||||
}
|
||||
|
||||
async function remove(hwpodId: string) {
|
||||
const id = validId(hwpodId);
|
||||
assertNotFrozen(id, builtIns);
|
||||
const runtime = await readRuntimeRecords(runtimeDir);
|
||||
const existing = runtime.get(id);
|
||||
if (!existing) throw codedError("hwpod_spec_not_found", `HWPOD runtime spec was not found: ${id}`, { hwpodId: id });
|
||||
await rm(runtimeRecordPath(runtimeDir, id), { force: true });
|
||||
return { ok: true, status: "deleted", contractVersion: CONTRACT_VERSION, hwpodId: id, authority: "runtime", mutable: true, frozen: false, mutation: true, previousFingerprint: documentFingerprint(existing.document), valuesPrinted: false };
|
||||
}
|
||||
|
||||
return { list, get, create, update, delete: remove, runtimeDir };
|
||||
}
|
||||
|
||||
function parseBuiltIns(raw: string | undefined): BuiltInInput[] {
|
||||
if (!String(raw ?? "").trim()) return [];
|
||||
let value: unknown;
|
||||
try { value = JSON.parse(String(raw)); }
|
||||
catch (error: any) { throw codedError("invalid_hwpod_builtin_specs", `HWPOD_BUILTIN_SPEC_DOCUMENTS_JSON is invalid JSON: ${error?.message ?? String(error)}`); }
|
||||
if (!Array.isArray(value)) throw codedError("invalid_hwpod_builtin_specs", "HWPOD_BUILTIN_SPEC_DOCUMENTS_JSON must be an array");
|
||||
return value.map((item: any, index) => {
|
||||
if (!item || typeof item !== "object" || Array.isArray(item)) throw codedError("invalid_hwpod_builtin_specs", `built-in spec item ${index} must be an object`);
|
||||
const configRef = String(item.configRef ?? "").trim();
|
||||
if (!configRef) throw codedError("invalid_hwpod_builtin_specs", `built-in spec item ${index} is missing configRef`);
|
||||
return { configRef, sha256: String(item.sha256 ?? "").trim() || undefined, document: item.document };
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeBuiltIns(inputs: BuiltInInput[]) {
|
||||
const records = new Map<string, any>();
|
||||
for (const input of inputs) {
|
||||
const document = normalizedDocument(input.document, input.configRef);
|
||||
const hwpodId = specId(document);
|
||||
if (records.has(hwpodId)) throw codedError("hwpod_spec_authority_conflict", `duplicate YAML-first built-in HWPOD spec: ${hwpodId}`, { hwpodId });
|
||||
records.set(hwpodId, { authority: "yaml-first-builtin", hwpodId, configRef: input.configRef, sourceFingerprint: input.sha256 ?? null, document });
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
async function readRuntimeRecords(runtimeDir: string) {
|
||||
const records = new Map<string, RuntimeRecord>();
|
||||
let entries: string[] = [];
|
||||
try { entries = await readdir(runtimeDir); }
|
||||
catch (error: any) { if (error?.code !== "ENOENT") throw error; }
|
||||
for (const name of entries.filter((entry) => entry.endsWith(".json")).sort()) {
|
||||
const file = path.join(runtimeDir, name);
|
||||
let parsed: any;
|
||||
try { parsed = JSON.parse(await readFile(file, "utf8")); }
|
||||
catch (error: any) { throw codedError("invalid_hwpod_runtime_spec", `invalid runtime HWPOD spec record ${file}: ${error?.message ?? String(error)}`, { file }); }
|
||||
const document = normalizedDocument(parsed?.document, file);
|
||||
const hwpodId = specId(document);
|
||||
if (parsed?.authority !== "runtime" || parsed?.hwpodId !== hwpodId) throw codedError("invalid_hwpod_runtime_spec", `runtime HWPOD spec record identity is invalid: ${file}`, { file, hwpodId });
|
||||
if (records.has(hwpodId)) throw codedError("hwpod_spec_authority_conflict", `duplicate runtime HWPOD spec: ${hwpodId}`, { hwpodId });
|
||||
records.set(hwpodId, { contractVersion: CONTRACT_VERSION, authority: "runtime", hwpodId, createdAt: String(parsed.createdAt ?? ""), updatedAt: String(parsed.updatedAt ?? ""), document });
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
async function writeRuntimeRecord(runtimeDir: string, record: RuntimeRecord) {
|
||||
await mkdir(runtimeDir, { recursive: true });
|
||||
const target = runtimeRecordPath(runtimeDir, record.hwpodId);
|
||||
const temporary = `${target}.${randomUUID()}.tmp`;
|
||||
await writeFile(temporary, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
||||
await rename(temporary, target);
|
||||
}
|
||||
|
||||
function runtimeRecordPath(runtimeDir: string, hwpodId: string) { return path.join(runtimeDir, `${encodeURIComponent(validId(hwpodId))}.json`); }
|
||||
function normalizedDocument(value: unknown, source: string) { return normalizeHwpodSpec(value, source); }
|
||||
function specId(document: any) { return validId(String(document?.metadata?.name ?? document?.metadata?.uid ?? "")); }
|
||||
function validId(value: string) { const id = String(value ?? "").trim(); if (!/^[A-Za-z0-9_.:-]{1,120}$/u.test(id)) throw codedError("invalid_hwpod_spec_id", `invalid HWPOD spec id: ${id || "<empty>"}`); return id; }
|
||||
function assertNotFrozen(id: string, builtIns: Map<string, any>) { const item = builtIns.get(id); if (item) throw codedError("hwpod_spec_frozen", `YAML-first built-in HWPOD spec is frozen: ${id}`, { hwpodId: id, authority: item.authority, configRef: item.configRef, mutable: false, frozen: true, documentFingerprint: documentFingerprint(item.document), mutation: false }); }
|
||||
function assertNoAuthorityConflicts(builtIns: Map<string, any>, runtime: Map<string, RuntimeRecord>) { for (const id of runtime.keys()) if (builtIns.has(id)) throw codedError("hwpod_spec_authority_conflict", `runtime HWPOD spec conflicts with YAML-first built-in spec: ${id}`, { hwpodId: id }); }
|
||||
function summary(record: any) { const document = record.document; return { hwpodId: record.hwpodId, authority: record.authority, mutable: record.authority === "runtime", frozen: record.authority !== "runtime", documentFingerprint: documentFingerprint(document), ...(record.configRef ? { configRef: record.configRef, sourceFingerprint: record.sourceFingerprint } : { createdAt: record.createdAt, updatedAt: record.updatedAt }), nodeId: document.spec.nodeBinding.nodeId, workspacePath: document.spec.workspace.path }; }
|
||||
function entity(record: any) { return { ok: true, status: "completed", contractVersion: CONTRACT_VERSION, ...summary(record), document: record.document, valuesPrinted: false }; }
|
||||
function mutation(status: "created" | "updated", record: RuntimeRecord) { return { ...entity(record), status, mutation: true }; }
|
||||
function documentFingerprint(document: unknown) { return `sha256:${createHash("sha256").update(stableJson(document)).digest("hex")}`; }
|
||||
function stableJson(value: unknown): string { if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`; if (value && typeof value === "object") return `{${Object.keys(value as any).sort().map((key) => `${JSON.stringify(key)}:${stableJson((value as any)[key])}`).join(",")}}`; return JSON.stringify(value) ?? "null"; }
|
||||
function codedError(code: string, message: string, details?: unknown) { return Object.assign(new Error(message), { code, details }); }
|
||||
Reference in New Issue
Block a user