docs: add M0 contract audit

This commit is contained in:
HWLAB Code Queue
2026-05-21 15:03:49 +00:00
parent 24eb3bf6b3
commit e039deea10
9 changed files with 825 additions and 0 deletions
+109
View File
@@ -0,0 +1,109 @@
# M0 Contract Audit
This audit independently reviews whether the L0 skeleton is enough for M0
contract work under the issue constraints supplied for pikasTech/HWLAB#19 and
the referenced #7/#11/#12/#16 guardrails.
Applied constraints:
- MVP acceptance is DEV only at `http://74.48.78.17:6667`.
- PROD is reserved and must not be deployed for this audit.
- HWLAB runtime services must keep their frozen service IDs and must not be
replaced by UniDesk services.
- This audit does not modify L1-L6 service implementation directories and does
not create or claim the L5 deploy manifest.
## Audit Result
Status: conditionally passes M0 after this audit patch.
The repository already contains the L0 contract skeleton needed by parallel
MVP work:
- Root README freezes MVP scope, DEV endpoint, service IDs, and repository
layout.
- `internal/protocol/index.mjs` centralizes DEV endpoint, frozen service IDs,
table names, JSON-RPC version, and HWLAB error codes.
- `protocol/*.md` documents JSON-RPC envelopes, error codes, audit fields,
capability model, topology constraints, and the sequence-level MVP flow.
- `protocol/schemas/*.json` names the key protocol records and uses
draft-2020-12 JSON Schema shape files.
- `deploy/deploy.schema.json` names the deploy manifest fields and reuses the
frozen service ID list.
- `scripts/validate-contract.mjs` already checks protocol constants against
the common and deploy schemas.
This patch adds M0 examples under `protocol/examples/m0-contract/` and a
lightweight executable check in `scripts/validate-m0-contract.mjs` so the audit
is not document-only.
## Coverage Matrix
| Contract area | Current L0 source | Added M0 sample/check | Result |
| --- | --- | --- | --- |
| Frozen service IDs | `README.md`, `internal/protocol/index.mjs`, `protocol/schemas/common.json`, `deploy/deploy.schema.json` | `service-ids.json`, deploy sample, exact list comparison | Covered |
| JSON-RPC request/response | `protocol/json-rpc.md`, `json-rpc-envelope.schema.json`, runtime validators | `json-rpc-request.json`, `json-rpc-response.json` | Covered |
| Error envelope | `protocol/errors.md`, runtime `ERROR_CODES` | `json-rpc-error.json`, reserved code and stable `error.data` fields | Covered |
| Audit fields | `protocol/audit.md`, `audit-event.schema.json` | `audit-event.json`, required field and ID checks | Covered |
| Capability and topology | `protocol/capabilities.md`, `protocol/topology.md`, related schemas | `capability-topology.json`, cross-reference checks | Covered for M0 |
| Deploy schema fields | `deploy/deploy.schema.json`, `deploy/README.md` | `deploy-manifest.dev.json`, DEV-only example checks | Covered without L5 manifest changes |
## Rework Risk Register
Blocker: persisted schema naming drift before L1/L4/L6 integration.
`internal/db/migrations/0001_cloud_core_skeleton.sql` uses placeholder columns
such as `request_id`, `actor`, `source`, `operation`, `result`, `operation_type`,
and `evidence_type`, while the protocol schemas use `auditId`, `traceId`,
`actorType`, `serviceId`, `action`, `outcome`, `capabilityId`, and `kind`.
Before services persist real records, L1 must either align the migration with
the protocol names or provide an explicit mapping layer that is tested.
Blocker: schema files are parseable but not fully enforced at runtime.
The existing validators and the new M0 check intentionally avoid heavyweight
dependencies and do not evaluate all JSON Schema references. L1-L6 service
entry points need a shared validator such as Ajv or a generated equivalent
before accepting untrusted API, gateway, or agent payloads.
Blocker: topology constraints are partly prose-only.
M0 can verify that examples cross-reference known resources, capabilities, and
sessions. M1-M3 need a real topology validator for cycle detection, active
resource-group exclusivity, patch-panel override behavior, and gateway/box
ownership.
Blocker: deploy schema allows future `prod` profile shape.
That is acceptable for L0 compatibility, but L5 must keep MVP execution DEV-only
and reject any real PROD deploy path until a later milestone explicitly changes
the acceptance target.
Enhancement: RPC method catalog is not yet complete.
M0 freezes envelope shape, method naming, and representative
`hardware.operation.request`. Later milestones should add a method registry with
params/result schemas for project, gateway, patch-panel, worker, evidence, and
cleanup flows.
Enhancement: capability constraints are generic JSON objects.
This keeps M0 flexible, but hardware-facing work should add typed constraint
profiles for common operations such as voltage ranges, serial settings,
firmware flashing, and measurement tolerances.
Enhancement: evidence integrity is structural only.
The sample checks SHA-256 format but does not hash referenced artifacts. M4/M5
should verify artifact existence, size, content hash, and retention policy.
Enhancement: service ID drift checks are duplicated.
Current validation compares runtime constants with schema enums. A later cleanup
could generate schema enums and docs from one registry.
## Validation
Run:
```sh
npm run check
node --check scripts/validate-m0-contract.mjs
node scripts/validate-m0-contract.mjs
```
The M0 script parses all JSON files in `protocol/examples/m0-contract/`, checks
the audit Markdown headings, and verifies the representative cross-document
invariants needed for M0.
@@ -0,0 +1,22 @@
{
"auditId": "aud_01J00000000000000000000010",
"traceId": "trc_01J00000000000000000000010",
"actorType": "worker",
"actorId": "wrk_01J00000000000000000000010",
"action": "hardware.operation.succeeded",
"targetType": "hardware_operation",
"targetId": "op_01J00000000000000000000010",
"projectId": "prj_01J00000000000000000000010",
"gatewaySessionId": "gws_01J00000000000000000000010",
"workerSessionId": "wks_01J00000000000000000000010",
"operationId": "op_01J00000000000000000000010",
"serviceId": "hwlab-agent-worker",
"environment": "dev",
"outcome": "succeeded",
"reason": "operation_completed",
"metadata": {
"capabilityId": "cap_01J00000000000000000000010",
"evidenceId": "evd_01J00000000000000000000010"
},
"occurredAt": "2026-05-21T00:00:05.000Z"
}
@@ -0,0 +1,224 @@
{
"contract": "m0-capability-topology",
"project": {
"projectId": "prj_01J00000000000000000000010",
"name": "M0 Contract Project",
"status": "active",
"environment": "dev",
"createdBy": "usr_01J00000000000000000000010",
"createdAt": "2026-05-21T00:00:00.000Z",
"updatedAt": "2026-05-21T00:00:00.000Z"
},
"gatewaySession": {
"gatewaySessionId": "gws_01J00000000000000000000010",
"projectId": "prj_01J00000000000000000000010",
"serviceId": "hwlab-gateway-simu",
"gatewayId": "gtw_01J00000000000000000000010",
"endpoint": "http://127.0.0.1:7101",
"status": "connected",
"environment": "dev",
"startedAt": "2026-05-21T00:00:00.000Z",
"lastSeenAt": "2026-05-21T00:00:02.000Z",
"labels": {
"mode": "simulated"
}
},
"resources": [
{
"resourceId": "res_01J00000000000000000000010",
"projectId": "prj_01J00000000000000000000010",
"gatewaySessionId": "gws_01J00000000000000000000010",
"boxId": "box_01J00000000000000000000010",
"resourceType": "power_supply",
"name": "simulated power supply",
"state": "available",
"environment": "dev",
"metadata": {
"ports": ["out0"]
},
"createdAt": "2026-05-21T00:00:00.000Z",
"updatedAt": "2026-05-21T00:00:00.000Z"
},
{
"resourceId": "res_01J00000000000000000000011",
"projectId": "prj_01J00000000000000000000010",
"gatewaySessionId": "gws_01J00000000000000000000010",
"boxId": "box_01J00000000000000000000010",
"resourceType": "board",
"name": "simulated dut board",
"state": "reserved",
"environment": "dev",
"metadata": {
"ports": ["vin", "uart0"]
},
"createdAt": "2026-05-21T00:00:00.000Z",
"updatedAt": "2026-05-21T00:00:00.000Z"
}
],
"capabilities": [
{
"capabilityId": "cap_01J00000000000000000000010",
"resourceId": "res_01J00000000000000000000010",
"projectId": "prj_01J00000000000000000000010",
"name": "power.cycle",
"description": "Cycle simulated DUT power through the gateway.",
"direction": "bidirectional",
"valueType": "object",
"constraints": {
"holdMs": {
"minimum": 100,
"maximum": 5000
}
},
"mutatesState": true,
"createdAt": "2026-05-21T00:00:00.000Z",
"updatedAt": "2026-05-21T00:00:00.000Z"
},
{
"capabilityId": "cap_01J00000000000000000000011",
"resourceId": "res_01J00000000000000000000010",
"projectId": "prj_01J00000000000000000000010",
"name": "power.set_voltage",
"description": "Set simulated output voltage.",
"direction": "input",
"valueType": "number",
"unit": "V",
"constraints": {
"minimum": 0,
"maximum": 12
},
"mutatesState": true,
"createdAt": "2026-05-21T00:00:00.000Z",
"updatedAt": "2026-05-21T00:00:00.000Z"
}
],
"wiringConfig": {
"wiringConfigId": "wir_01J00000000000000000000010",
"projectId": "prj_01J00000000000000000000010",
"gatewaySessionId": "gws_01J00000000000000000000010",
"name": "M0 simulated DUT power wiring",
"status": "active",
"connections": [
{
"from": {
"resourceId": "res_01J00000000000000000000010",
"port": "out0",
"capabilityId": "cap_01J00000000000000000000011"
},
"to": {
"resourceId": "res_01J00000000000000000000011",
"port": "vin"
},
"mode": "exclusive"
}
],
"constraints": {
"oneActiveConfigPerResourceGroup": true,
"cyclesAllowed": false
},
"createdAt": "2026-05-21T00:00:00.000Z",
"updatedAt": "2026-05-21T00:00:00.000Z"
},
"patchPanelStatus": {
"patchPanelStatusId": "pps_01J00000000000000000000010",
"projectId": "prj_01J00000000000000000000010",
"gatewaySessionId": "gws_01J00000000000000000000010",
"wiringConfigId": "wir_01J00000000000000000000010",
"serviceId": "hwlab-patch-panel",
"state": "active",
"environment": "dev",
"activeConnections": [
{
"fromResourceId": "res_01J00000000000000000000010",
"fromPort": "out0",
"toResourceId": "res_01J00000000000000000000011",
"toPort": "vin"
}
],
"observedAt": "2026-05-21T00:00:01.000Z",
"metadata": {
"connectionCount": 1
}
},
"agentSession": {
"agentSessionId": "ags_01J00000000000000000000010",
"projectId": "prj_01J00000000000000000000010",
"serviceId": "hwlab-agent-mgr",
"status": "active",
"environment": "dev",
"requestedBy": "usr_01J00000000000000000000010",
"goal": "Cycle DUT power and persist evidence.",
"startedAt": "2026-05-21T00:00:02.000Z",
"updatedAt": "2026-05-21T00:00:02.000Z"
},
"workerSession": {
"workerSessionId": "wks_01J00000000000000000000010",
"agentSessionId": "ags_01J00000000000000000000010",
"projectId": "prj_01J00000000000000000000010",
"serviceId": "hwlab-agent-worker",
"gatewaySessionId": "gws_01J00000000000000000000010",
"status": "active",
"environment": "dev",
"startedAt": "2026-05-21T00:00:03.000Z",
"updatedAt": "2026-05-21T00:00:03.000Z"
},
"hardwareOperation": {
"operationId": "op_01J00000000000000000000010",
"projectId": "prj_01J00000000000000000000010",
"gatewaySessionId": "gws_01J00000000000000000000010",
"agentSessionId": "ags_01J00000000000000000000010",
"workerSessionId": "wks_01J00000000000000000000010",
"resourceId": "res_01J00000000000000000000010",
"capabilityId": "cap_01J00000000000000000000010",
"requestedBy": "usr_01J00000000000000000000010",
"input": {
"cycle": true,
"holdMs": 250
},
"output": {
"powerState": "on"
},
"status": "succeeded",
"environment": "dev",
"requestedAt": "2026-05-21T00:00:03.000Z",
"startedAt": "2026-05-21T00:00:04.000Z",
"completedAt": "2026-05-21T00:00:05.000Z",
"updatedAt": "2026-05-21T00:00:05.000Z"
},
"traceEvents": [
{
"traceEventId": "tev_01J00000000000000000000010",
"traceId": "trc_01J00000000000000000000010",
"projectId": "prj_01J00000000000000000000010",
"agentSessionId": "ags_01J00000000000000000000010",
"workerSessionId": "wks_01J00000000000000000000010",
"operationId": "op_01J00000000000000000000010",
"serviceId": "hwlab-agent-worker",
"level": "info",
"message": "hardware operation completed",
"environment": "dev",
"metadata": {
"status": "succeeded"
},
"occurredAt": "2026-05-21T00:00:05.000Z"
}
],
"evidenceRecord": {
"evidenceId": "evd_01J00000000000000000000010",
"projectId": "prj_01J00000000000000000000010",
"operationId": "op_01J00000000000000000000010",
"agentSessionId": "ags_01J00000000000000000000010",
"workerSessionId": "wks_01J00000000000000000000010",
"kind": "report",
"uri": "protocol/examples/m0-contract/evidence/power-cycle-report.md",
"mimeType": "text/markdown",
"sha256": "0000000000000000000000000000000000000000000000000000000000000000",
"sizeBytes": 0,
"serviceId": "hwlab-agent-worker",
"environment": "dev",
"metadata": {
"source": "m0_contract_example"
},
"createdAt": "2026-05-21T00:00:05.000Z"
}
}
@@ -0,0 +1,126 @@
{
"manifestVersion": "v1",
"environment": "dev",
"commitId": "6509a35804ee97dd8d711bdde334c46ca37c55b9",
"namespace": "hwlab-dev",
"endpoint": "http://74.48.78.17:6667",
"profiles": {
"dev": {
"name": "dev",
"enabled": true,
"namespace": "hwlab-dev",
"endpoint": "http://74.48.78.17:6667",
"notes": "M0 contract example only; not a deploy manifest."
}
},
"services": [
{
"serviceId": "hwlab-cloud-api",
"image": "ghcr.io/pikastech/hwlab-cloud-api:6509a35",
"namespace": "hwlab-dev",
"healthPath": "/healthz",
"profile": "dev",
"replicas": 1,
"env": {
"HWLAB_ENV": "dev"
}
},
{
"serviceId": "hwlab-cloud-web",
"image": "ghcr.io/pikastech/hwlab-cloud-web:6509a35",
"namespace": "hwlab-dev",
"healthPath": "/healthz",
"profile": "dev",
"replicas": 1
},
{
"serviceId": "hwlab-agent-mgr",
"image": "ghcr.io/pikastech/hwlab-agent-mgr:6509a35",
"namespace": "hwlab-dev",
"healthPath": "/healthz",
"profile": "dev",
"replicas": 1
},
{
"serviceId": "hwlab-agent-worker",
"image": "ghcr.io/pikastech/hwlab-agent-worker:6509a35",
"namespace": "hwlab-dev",
"healthPath": "/healthz",
"profile": "dev",
"replicas": 1
},
{
"serviceId": "hwlab-gateway",
"image": "ghcr.io/pikastech/hwlab-gateway:6509a35",
"namespace": "hwlab-dev",
"healthPath": "/healthz",
"profile": "dev",
"replicas": 0
},
{
"serviceId": "hwlab-gateway-simu",
"image": "ghcr.io/pikastech/hwlab-gateway-simu:6509a35",
"namespace": "hwlab-dev",
"healthPath": "/healthz",
"profile": "dev",
"replicas": 1
},
{
"serviceId": "hwlab-box-simu",
"image": "ghcr.io/pikastech/hwlab-box-simu:6509a35",
"namespace": "hwlab-dev",
"healthPath": "/healthz",
"profile": "dev",
"replicas": 1
},
{
"serviceId": "hwlab-patch-panel",
"image": "ghcr.io/pikastech/hwlab-patch-panel:6509a35",
"namespace": "hwlab-dev",
"healthPath": "/healthz",
"profile": "dev",
"replicas": 1
},
{
"serviceId": "hwlab-router",
"image": "ghcr.io/pikastech/hwlab-router:6509a35",
"namespace": "hwlab-dev",
"healthPath": "/healthz",
"profile": "dev",
"replicas": 1
},
{
"serviceId": "hwlab-tunnel-client",
"image": "ghcr.io/pikastech/hwlab-tunnel-client:6509a35",
"namespace": "hwlab-dev",
"healthPath": "/healthz",
"profile": "dev",
"replicas": 1
},
{
"serviceId": "hwlab-edge-proxy",
"image": "ghcr.io/pikastech/hwlab-edge-proxy:6509a35",
"namespace": "hwlab-dev",
"healthPath": "/healthz",
"profile": "dev",
"replicas": 1
},
{
"serviceId": "hwlab-cli",
"image": "ghcr.io/pikastech/hwlab-cli:6509a35",
"namespace": "hwlab-dev",
"healthPath": "/healthz",
"profile": "dev",
"replicas": 0
},
{
"serviceId": "hwlab-agent-skills",
"image": "ghcr.io/pikastech/hwlab-agent-skills:6509a35",
"namespace": "hwlab-dev",
"healthPath": "/healthz",
"profile": "dev",
"replicas": 0
}
],
"createdAt": "2026-05-21T00:00:00.000Z"
}
@@ -0,0 +1,20 @@
{
"jsonrpc": "2.0",
"id": "req_01J00000000000000000000010",
"error": {
"code": -32020,
"message": "Requested hardware capability is unavailable",
"data": {
"projectId": "prj_01J00000000000000000000010",
"gatewaySessionId": "gws_01J00000000000000000000010",
"resourceId": "res_01J00000000000000000000010",
"capabilityId": "cap_01J00000000000000000000010",
"reason": "capability_unavailable"
}
},
"meta": {
"traceId": "trc_01J00000000000000000000010",
"serviceId": "hwlab-gateway",
"environment": "dev"
}
}
@@ -0,0 +1,26 @@
{
"jsonrpc": "2.0",
"id": "req_01J00000000000000000000010",
"method": "hardware.operation.request",
"params": {
"projectId": "prj_01J00000000000000000000010",
"gatewaySessionId": "gws_01J00000000000000000000010",
"resourceId": "res_01J00000000000000000000010",
"capabilityId": "cap_01J00000000000000000000010",
"input": {
"cycle": true,
"holdMs": 250
},
"audit": {
"actorType": "user",
"actorId": "usr_01J00000000000000000000010",
"reason": "m0_contract_example"
}
},
"meta": {
"traceId": "trc_01J00000000000000000000010",
"actorId": "usr_01J00000000000000000000010",
"serviceId": "hwlab-cloud-api",
"environment": "dev"
}
}
@@ -0,0 +1,14 @@
{
"jsonrpc": "2.0",
"id": "req_01J00000000000000000000010",
"result": {
"accepted": true,
"operationId": "op_01J00000000000000000000010",
"status": "accepted"
},
"meta": {
"traceId": "trc_01J00000000000000000000010",
"serviceId": "hwlab-agent-mgr",
"environment": "dev"
}
}
@@ -0,0 +1,19 @@
{
"contract": "m0-service-ids",
"environment": "dev",
"serviceIds": [
"hwlab-cloud-api",
"hwlab-cloud-web",
"hwlab-agent-mgr",
"hwlab-agent-worker",
"hwlab-gateway",
"hwlab-gateway-simu",
"hwlab-box-simu",
"hwlab-patch-panel",
"hwlab-router",
"hwlab-tunnel-client",
"hwlab-edge-proxy",
"hwlab-cli",
"hwlab-agent-skills"
]
}
+265
View File
@@ -0,0 +1,265 @@
#!/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/m0-contract-audit.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 [
"# M0 Contract Audit",
"## Audit Result",
"## Coverage Matrix",
"## Rework Risk Register",
"## Validation"
]) {
assert.ok(doc.includes(needle), `${auditDocPath} missing ${needle}`);
}
assert.ok(doc.includes("Blocker"), `${auditDocPath} must classify blocker risks`);
assert.ok(doc.includes("Enhancement"), `${auditDocPath} must classify enhancement risks`);
}
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`);