#!/usr/bin/env node import assert from "node:assert/strict"; import { readdir, readFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { DEV_ENDPOINT, ENVIRONMENT_DEV, ERROR_CODES, SERVICE_IDS, validateRequest, validateResponse } from "../internal/protocol/index.mjs"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const examplesDir = "protocol/examples/m0-contract"; const auditDocPath = "docs/reference/spec-v02-documentation-governance.md"; const timestampPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; const idPattern = /^[a-z][a-z0-9]*_[A-Za-z0-9._:-]+$/; const actionPattern = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/; async function readJSON(relativePath) { const raw = await readFile(path.join(repoRoot, relativePath), "utf8"); return JSON.parse(raw); } async function loadExamples() { const entries = await readdir(path.join(repoRoot, examplesDir), { withFileTypes: true }); const examples = new Map(); for (const entry of entries) { if (!entry.isFile() || !entry.name.endsWith(".json")) { continue; } const relativePath = path.join(examplesDir, entry.name); examples.set(entry.name, await readJSON(relativePath)); } return examples; } function assertUnique(name, values) { assert.equal(new Set(values).size, values.length, `${name} must be unique`); } function assertServiceId(serviceId, context) { assert.ok(SERVICE_IDS.includes(serviceId), `${context} uses unknown serviceId ${serviceId}`); } function assertDevEnvironment(value, context) { assert.equal(value, ENVIRONMENT_DEV, `${context} must be dev`); } function assertId(value, context) { assert.equal(typeof value, "string", `${context} must be a string id`); assert.match(value, idPattern, `${context} must match HWLAB id pattern`); } function assertTimestamp(value, context) { assert.equal(typeof value, "string", `${context} must be a timestamp`); assert.match(value, timestampPattern, `${context} must be an RFC 3339 UTC timestamp`); } function assertServiceIdsExample(doc) { assert.equal(doc.contract, "m0-service-ids"); assertDevEnvironment(doc.environment, "service-ids environment"); assert.deepEqual(doc.serviceIds, SERVICE_IDS, "example service ids must match frozen runtime service ids"); assertUnique("example service ids", doc.serviceIds); } function assertJsonRpcExamples(request, response, errorEnvelope) { validateRequest(request); validateResponse(response); validateResponse(errorEnvelope); assert.equal(response.id, request.id, "JSON-RPC response must echo request id"); assert.equal(errorEnvelope.id, request.id, "JSON-RPC error must echo request id"); assert.equal(response.meta.traceId, request.meta.traceId, "response must keep request traceId"); assert.equal(errorEnvelope.meta.traceId, request.meta.traceId, "error must keep request traceId"); assertServiceId(request.meta.serviceId, "request meta"); assertServiceId(response.meta.serviceId, "response meta"); assertServiceId(errorEnvelope.meta.serviceId, "error meta"); assert.ok( Object.values(ERROR_CODES).includes(errorEnvelope.error.code), "error example must use a reserved JSON-RPC or HWLAB error code" ); for (const key of ["projectId", "gatewaySessionId", "resourceId", "capabilityId", "reason"]) { assert.ok(Object.hasOwn(errorEnvelope.error.data, key), `error.data must include ${key}`); } } function assertAuditEvent(doc) { for (const key of [ "auditId", "traceId", "actorType", "actorId", "action", "targetType", "targetId", "serviceId", "environment", "occurredAt" ]) { assert.ok(Object.hasOwn(doc, key), `audit event missing ${key}`); } for (const key of ["auditId", "traceId", "actorId", "targetId", "projectId", "gatewaySessionId", "workerSessionId", "operationId"]) { assertId(doc[key], `audit event ${key}`); } assert.match(doc.action, actionPattern, "audit action must be dotted lower-case"); assert.equal(doc.targetId, doc.operationId, "operation audit targetId must equal operationId"); assertServiceId(doc.serviceId, "audit event"); assertDevEnvironment(doc.environment, "audit event environment"); assertTimestamp(doc.occurredAt, "audit occurredAt"); } function assertCapabilityTopology(doc) { assert.equal(doc.contract, "m0-capability-topology"); assertDevEnvironment(doc.project.environment, "project environment"); assertId(doc.project.projectId, "projectId"); assert.equal(doc.gatewaySession.projectId, doc.project.projectId, "gateway session projectId must match project"); assert.ok(["hwlab-gateway", "hwlab-gateway-simu"].includes(doc.gatewaySession.serviceId), "gateway service must be gateway"); assertDevEnvironment(doc.gatewaySession.environment, "gateway session environment"); const resourceIds = new Set(); for (const resource of doc.resources) { assertId(resource.resourceId, "resourceId"); assert.equal(resource.projectId, doc.project.projectId, "resource projectId must match project"); assert.equal(resource.gatewaySessionId, doc.gatewaySession.gatewaySessionId, "resource gatewaySessionId must match gateway"); assertDevEnvironment(resource.environment, `resource ${resource.resourceId} environment`); resourceIds.add(resource.resourceId); } assert.ok(resourceIds.size >= 2, "topology example must include at least two resources"); const capabilityIds = new Set(); for (const capability of doc.capabilities) { assertId(capability.capabilityId, "capabilityId"); assert.ok(resourceIds.has(capability.resourceId), `capability ${capability.capabilityId} must reference a known resource`); assert.equal(capability.projectId, doc.project.projectId, "capability projectId must match project"); assert.match(capability.name, actionPattern, "capability name must be dotted lower-case"); assert.equal(typeof capability.mutatesState, "boolean", "capability mutatesState must be boolean"); capabilityIds.add(capability.capabilityId); } assert.ok(capabilityIds.size >= 2, "topology example must include multiple capabilities"); const wiring = doc.wiringConfig; assert.equal(wiring.projectId, doc.project.projectId, "wiring projectId must match project"); assert.equal(wiring.gatewaySessionId, doc.gatewaySession.gatewaySessionId, "wiring gatewaySessionId must match gateway"); assert.equal(wiring.status, "active", "wiring example should show an active config"); for (const connection of wiring.connections) { assert.ok(resourceIds.has(connection.from.resourceId), "wiring from.resourceId must exist"); assert.ok(resourceIds.has(connection.to.resourceId), "wiring to.resourceId must exist"); if (connection.from.capabilityId) { assert.ok(capabilityIds.has(connection.from.capabilityId), "wiring from.capabilityId must exist"); } if (connection.to.capabilityId) { assert.ok(capabilityIds.has(connection.to.capabilityId), "wiring to.capabilityId must exist"); } } const patch = doc.patchPanelStatus; assert.equal(patch.serviceId, "hwlab-patch-panel"); assert.equal(patch.projectId, doc.project.projectId, "patch panel projectId must match project"); assert.equal(patch.gatewaySessionId, doc.gatewaySession.gatewaySessionId, "patch panel gatewaySessionId must match gateway"); assert.equal(patch.wiringConfigId, wiring.wiringConfigId, "patch panel wiringConfigId must match wiring"); assertDevEnvironment(patch.environment, "patch panel environment"); for (const active of patch.activeConnections) { assert.ok(resourceIds.has(active.fromResourceId), "active fromResourceId must exist"); assert.ok(resourceIds.has(active.toResourceId), "active toResourceId must exist"); } assert.equal(doc.agentSession.projectId, doc.project.projectId, "agent session projectId must match project"); assert.equal(doc.agentSession.serviceId, "hwlab-agent-mgr"); assertDevEnvironment(doc.agentSession.environment, "agent session environment"); assert.equal(doc.workerSession.projectId, doc.project.projectId, "worker session projectId must match project"); assert.equal(doc.workerSession.agentSessionId, doc.agentSession.agentSessionId, "worker must reference agent session"); assert.equal(doc.workerSession.gatewaySessionId, doc.gatewaySession.gatewaySessionId, "worker must reference gateway session"); assert.equal(doc.workerSession.serviceId, "hwlab-agent-worker"); assertDevEnvironment(doc.workerSession.environment, "worker session environment"); const operation = doc.hardwareOperation; assert.equal(operation.projectId, doc.project.projectId, "operation projectId must match project"); assert.equal(operation.gatewaySessionId, doc.gatewaySession.gatewaySessionId, "operation gatewaySessionId must match gateway"); assert.equal(operation.agentSessionId, doc.agentSession.agentSessionId, "operation must reference agent session"); assert.equal(operation.workerSessionId, doc.workerSession.workerSessionId, "operation must reference worker session"); assert.ok(resourceIds.has(operation.resourceId), "operation resourceId must exist"); assert.ok(capabilityIds.has(operation.capabilityId), "operation capabilityId must exist"); assertDevEnvironment(operation.environment, "operation environment"); for (const traceEvent of doc.traceEvents) { assert.equal(traceEvent.projectId, doc.project.projectId, "trace projectId must match project"); assert.equal(traceEvent.operationId, operation.operationId, "trace operationId must match operation"); assertServiceId(traceEvent.serviceId, "trace event"); assertDevEnvironment(traceEvent.environment, "trace environment"); } const evidence = doc.evidenceRecord; assert.equal(evidence.projectId, doc.project.projectId, "evidence projectId must match project"); assert.equal(evidence.operationId, operation.operationId, "evidence operationId must match operation"); assert.equal(evidence.agentSessionId, doc.agentSession.agentSessionId, "evidence must reference agent session"); assert.equal(evidence.workerSessionId, doc.workerSession.workerSessionId, "evidence must reference worker session"); assert.match(evidence.sha256, /^[a-f0-9]{64}$/, "evidence sha256 must be lowercase hex"); assertServiceId(evidence.serviceId, "evidence"); assertDevEnvironment(evidence.environment, "evidence environment"); } function assertDeployManifest(doc) { assert.equal(doc.manifestVersion, "v1"); assertDevEnvironment(doc.environment, "deploy manifest environment"); assert.equal(doc.endpoint, DEV_ENDPOINT, "deploy manifest endpoint must be the frozen DEV endpoint"); assert.ok(doc.profiles.dev.enabled, "deploy manifest dev profile must be enabled"); assert.equal(doc.profiles.dev.endpoint, DEV_ENDPOINT, "deploy manifest dev profile endpoint must be frozen"); assert.ok(!doc.profiles.prod, "M0 example must not include a PROD profile"); const serviceIds = doc.services.map((service) => service.serviceId); assert.deepEqual(serviceIds, SERVICE_IDS, "deploy manifest example must cover every frozen service id in order"); assertUnique("deploy service ids", serviceIds); for (const service of doc.services) { assertServiceId(service.serviceId, "deploy service"); assert.equal(service.profile, "dev", `${service.serviceId} must use dev profile in M0 example`); assert.equal(service.namespace, doc.namespace, `${service.serviceId} namespace must match manifest namespace`); assert.ok(service.healthPath.startsWith("/"), `${service.serviceId} healthPath must be absolute`); } } async function assertAuditMarkdown() { const doc = await readFile(path.join(repoRoot, auditDocPath), "utf8"); for (const needle of [ "# v0.2 文档治理规格", "docs/m0-*", "旧里程碑", "#532" ]) { assert.ok(doc.includes(needle), `${auditDocPath} missing ${needle}`); } } const examples = await loadExamples(); for (const required of [ "service-ids.json", "json-rpc-request.json", "json-rpc-response.json", "json-rpc-error.json", "audit-event.json", "capability-topology.json", "deploy-manifest.dev.json" ]) { assert.ok(examples.has(required), `${examplesDir} missing ${required}`); } assertServiceIdsExample(examples.get("service-ids.json")); assertJsonRpcExamples( examples.get("json-rpc-request.json"), examples.get("json-rpc-response.json"), examples.get("json-rpc-error.json") ); assertAuditEvent(examples.get("audit-event.json")); assertCapabilityTopology(examples.get("capability-topology.json")); assertDeployManifest(examples.get("deploy-manifest.dev.json")); await assertAuditMarkdown(); console.log(`validated M0 contract examples: ${examples.size} JSON files, ${SERVICE_IDS.length} service ids`);