docs: add schema drift map
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
# Schema Drift Map
|
||||
|
||||
This document records the naming drift between the protocol schemas, the L1
|
||||
cloud-core migration skeleton, the audit chain, the evidence chain, and the
|
||||
deploy manifest example. It is a closure artifact for M0 audit work only.
|
||||
|
||||
## Scope
|
||||
|
||||
- protocol: `protocol/schemas/*.json`
|
||||
- DB migration: `internal/db/migrations/0001_cloud_core_skeleton.sql`
|
||||
- audit event: `protocol/schemas/audit-event.schema.json`
|
||||
- evidence chain: `protocol/evidence-chain.md`
|
||||
- evidence record: `protocol/schemas/evidence-record.schema.json`
|
||||
- deploy manifest: `deploy/deploy.json`
|
||||
- deploy schema: `deploy/deploy.schema.json`
|
||||
|
||||
The source of truth for machine checks is `protocol/schema-drift-map.json`.
|
||||
This page is the human summary.
|
||||
|
||||
## Aligned Fields
|
||||
|
||||
The following IDs are already aligned at the protocol layer and are carried
|
||||
through the current examples:
|
||||
|
||||
- `projectId`
|
||||
- `operationId`
|
||||
- `auditId`
|
||||
- `traceId`
|
||||
- `evidenceId`
|
||||
- `serviceId`
|
||||
- `environment`
|
||||
- `resourceId`
|
||||
- `sessionId` family: `gatewaySessionId`, `agentSessionId`, `workerSessionId`
|
||||
- evidence chain roots: `chainId`, `schemaVersion`, `scenario`, `title`
|
||||
- evidence chain collections: `hardwareOperation`, `auditEvents`, `traceEvents`, `evidenceRecords`
|
||||
|
||||
## Known Gaps
|
||||
|
||||
These are the places where the L1 skeleton still drifts from protocol naming
|
||||
and needs a real persistence fix before production data is written:
|
||||
|
||||
- `audit_events.request_id` should become `traceId`
|
||||
- `audit_events.actor` should split into `actorType` and `actorId`
|
||||
- `audit_events.source` should become `actorId`
|
||||
- `audit_events.operation` should become `action`
|
||||
- `audit_events.target` should split into `targetType` and `targetId`
|
||||
- `audit_events.timestamp` should become `occurredAt`
|
||||
- `evidence_records.evidence_type` should become `kind`
|
||||
- `evidence_records.metadata_json.sha256` needs a first-class persisted target
|
||||
- `hardware_operations.operation_json.resourceId` needs a tested mapping path
|
||||
- `hardware_operations.operation_json.capabilityId` needs a tested mapping path
|
||||
|
||||
## Evidence Chain
|
||||
|
||||
The evidence chain fixture set is already internally coherent. It binds the
|
||||
root IDs to the protocol schemas without changing the storage model:
|
||||
|
||||
- `chainId` anchors the fixture
|
||||
- `operationId` ties hardware, audit, trace, and evidence together
|
||||
- `traceId` threads through audit and trace events and is copied into evidence
|
||||
metadata
|
||||
- `gatewaySessionId`, `agentSessionId`, and `workerSessionId` stay consistent
|
||||
where they appear
|
||||
- `hardwareOperation`, `auditEvents`, `traceEvents`, and `evidenceRecords`
|
||||
each point back to their schema counterparts
|
||||
|
||||
## Impact
|
||||
|
||||
The current examples and validators are sufficient for audit evidence and MVP
|
||||
closure, but they are not a persistence contract. The drift map is intentionally
|
||||
read-only so later implementation work can pick one of two paths:
|
||||
|
||||
1. rename the real storage columns to match the protocol, or
|
||||
2. keep the current skeleton and add a tested mapping layer before writes.
|
||||
|
||||
## Validation
|
||||
|
||||
Run:
|
||||
|
||||
```sh
|
||||
node --check scripts/validate-schema-drift-map.mjs
|
||||
node scripts/validate-schema-drift-map.mjs
|
||||
```
|
||||
@@ -0,0 +1,700 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://hwlab.pikastech.local/protocol/schema-drift-map.json",
|
||||
"title": "HWLAB Schema Drift Map",
|
||||
"version": 1,
|
||||
"sources": {
|
||||
"protocol": "protocol/schemas",
|
||||
"dbMigration": "internal/db/migrations/0001_cloud_core_skeleton.sql",
|
||||
"audit": "protocol/schemas/audit-event.schema.json",
|
||||
"evidenceChain": "protocol/evidence-chain.md",
|
||||
"evidence": "protocol/schemas/evidence-record.schema.json",
|
||||
"deployManifest": "deploy/deploy.json",
|
||||
"deploy": "deploy/deploy.schema.json"
|
||||
},
|
||||
"summary": {
|
||||
"aligned": [
|
||||
"projectId",
|
||||
"operationId",
|
||||
"auditId",
|
||||
"traceId",
|
||||
"evidenceId",
|
||||
"serviceId",
|
||||
"environment",
|
||||
"resourceId",
|
||||
"sessionId"
|
||||
],
|
||||
"gapCount": 12,
|
||||
"notes": [
|
||||
"This map describes naming drift only and does not change any storage implementation.",
|
||||
"A field can be aligned at the protocol level while still requiring a DB migration rename or mapping layer before real persistence."
|
||||
]
|
||||
},
|
||||
"entities": [
|
||||
{
|
||||
"name": "hardware_operation",
|
||||
"protocol": "protocol/schemas/hardware-operation.schema.json",
|
||||
"dbTable": "hardware_operations",
|
||||
"drift": [
|
||||
{
|
||||
"field": "operationId",
|
||||
"protocolField": "operationId",
|
||||
"dbColumn": "id",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "projectId",
|
||||
"protocolField": "projectId",
|
||||
"dbColumn": "project_id",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "gatewaySessionId",
|
||||
"protocolField": "gatewaySessionId",
|
||||
"dbColumn": "gateway_session_id",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "agentSessionId",
|
||||
"protocolField": "agentSessionId",
|
||||
"dbColumn": "agent_session_id",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "workerSessionId",
|
||||
"protocolField": "workerSessionId",
|
||||
"dbColumn": "worker_session_id",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "resourceId",
|
||||
"protocolField": "resourceId",
|
||||
"dbColumn": "operation_json.resourceId",
|
||||
"status": "gap",
|
||||
"gap": "The skeleton table has no dedicated resource column and currently stores the operation payload as opaque JSON."
|
||||
},
|
||||
{
|
||||
"field": "capabilityId",
|
||||
"protocolField": "capabilityId",
|
||||
"dbColumn": "operation_json.capabilityId",
|
||||
"status": "gap",
|
||||
"gap": "The skeleton table has no dedicated capability column and currently stores the operation payload as opaque JSON."
|
||||
},
|
||||
{
|
||||
"field": "requestedBy",
|
||||
"protocolField": "requestedBy",
|
||||
"dbColumn": "requested_by",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "status",
|
||||
"protocolField": "status",
|
||||
"dbColumn": "status",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "environment",
|
||||
"protocolField": "environment",
|
||||
"dbColumn": "operation_json.environment",
|
||||
"status": "gap",
|
||||
"gap": "The skeleton table does not carry a dedicated environment column for the operation row."
|
||||
},
|
||||
{
|
||||
"field": "requestedAt",
|
||||
"protocolField": "requestedAt",
|
||||
"dbColumn": "requested_at",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "updatedAt",
|
||||
"protocolField": "updatedAt",
|
||||
"dbColumn": "updated_at",
|
||||
"status": "aligned"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "audit_event",
|
||||
"protocol": "protocol/schemas/audit-event.schema.json",
|
||||
"dbTable": "audit_events",
|
||||
"drift": [
|
||||
{
|
||||
"field": "auditId",
|
||||
"protocolField": "auditId",
|
||||
"dbColumn": "id",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "traceId",
|
||||
"protocolField": "traceId",
|
||||
"dbColumn": "request_id",
|
||||
"status": "gap",
|
||||
"gap": "The migration still uses request_id instead of traceId."
|
||||
},
|
||||
{
|
||||
"field": "actorType",
|
||||
"protocolField": "actorType",
|
||||
"dbColumn": "actor",
|
||||
"status": "gap",
|
||||
"gap": "The migration stores a single actor string instead of a typed actorType field."
|
||||
},
|
||||
{
|
||||
"field": "actorId",
|
||||
"protocolField": "actorId",
|
||||
"dbColumn": "source",
|
||||
"status": "gap",
|
||||
"gap": "The migration stores source instead of a stable actorId."
|
||||
},
|
||||
{
|
||||
"field": "action",
|
||||
"protocolField": "action",
|
||||
"dbColumn": "operation",
|
||||
"status": "gap",
|
||||
"gap": "The migration uses operation instead of the protocol action name."
|
||||
},
|
||||
{
|
||||
"field": "targetType",
|
||||
"protocolField": "targetType",
|
||||
"dbColumn": "target",
|
||||
"status": "gap",
|
||||
"gap": "The migration stores a single target string instead of targetType and targetId."
|
||||
},
|
||||
{
|
||||
"field": "targetId",
|
||||
"protocolField": "targetId",
|
||||
"dbColumn": "target",
|
||||
"status": "gap",
|
||||
"gap": "The migration stores a single target string instead of targetType and targetId."
|
||||
},
|
||||
{
|
||||
"field": "projectId",
|
||||
"protocolField": "projectId",
|
||||
"dbColumn": "event_json.projectId",
|
||||
"status": "aligned",
|
||||
"notes": "Present in protocol and fixtures, but not a dedicated migration column."
|
||||
},
|
||||
{
|
||||
"field": "gatewaySessionId",
|
||||
"protocolField": "gatewaySessionId",
|
||||
"dbColumn": "event_json.gatewaySessionId",
|
||||
"status": "aligned",
|
||||
"notes": "Optional correlation field in protocol; still payload-only in the skeleton."
|
||||
},
|
||||
{
|
||||
"field": "workerSessionId",
|
||||
"protocolField": "workerSessionId",
|
||||
"dbColumn": "event_json.workerSessionId",
|
||||
"status": "aligned",
|
||||
"notes": "Optional correlation field in protocol; still payload-only in the skeleton."
|
||||
},
|
||||
{
|
||||
"field": "operationId",
|
||||
"protocolField": "operationId",
|
||||
"dbColumn": "event_json.operationId",
|
||||
"status": "aligned",
|
||||
"notes": "Present in protocol and fixtures, but not a dedicated migration column."
|
||||
},
|
||||
{
|
||||
"field": "serviceId",
|
||||
"protocolField": "serviceId",
|
||||
"dbColumn": "event_json.serviceId",
|
||||
"status": "aligned",
|
||||
"notes": "Frozen service ID appears in payload only."
|
||||
},
|
||||
{
|
||||
"field": "environment",
|
||||
"protocolField": "environment",
|
||||
"dbColumn": "event_json.environment",
|
||||
"status": "aligned",
|
||||
"notes": "MVP environment is carried in payload only."
|
||||
},
|
||||
{
|
||||
"field": "occurredAt",
|
||||
"protocolField": "occurredAt",
|
||||
"dbColumn": "timestamp",
|
||||
"status": "gap",
|
||||
"gap": "The migration uses timestamp instead of occurredAt."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "evidence_chain",
|
||||
"protocol": "protocol/evidence-chain.md",
|
||||
"dbTable": null,
|
||||
"drift": [
|
||||
{
|
||||
"field": "chainId",
|
||||
"protocolField": "chainId",
|
||||
"dbColumn": "n/a",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "schemaVersion",
|
||||
"protocolField": "schemaVersion",
|
||||
"dbColumn": "n/a",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "scenario",
|
||||
"protocolField": "scenario",
|
||||
"dbColumn": "n/a",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "title",
|
||||
"protocolField": "title",
|
||||
"dbColumn": "n/a",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "projectId",
|
||||
"protocolField": "projectId",
|
||||
"dbColumn": "n/a",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "operationId",
|
||||
"protocolField": "operationId",
|
||||
"dbColumn": "n/a",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "traceId",
|
||||
"protocolField": "traceId",
|
||||
"dbColumn": "n/a",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "environment",
|
||||
"protocolField": "environment",
|
||||
"dbColumn": "n/a",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "gatewaySessionId",
|
||||
"protocolField": "gatewaySessionId",
|
||||
"dbColumn": "n/a",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "agentSessionId",
|
||||
"protocolField": "agentSessionId",
|
||||
"dbColumn": "n/a",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "workerSessionId",
|
||||
"protocolField": "workerSessionId",
|
||||
"dbColumn": "n/a",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "resourceId",
|
||||
"protocolField": "resourceId",
|
||||
"dbColumn": "n/a",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "capabilityId",
|
||||
"protocolField": "capabilityId",
|
||||
"dbColumn": "n/a",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "hardwareOperation",
|
||||
"protocolField": "hardwareOperation",
|
||||
"dbColumn": "protocol/schemas/hardware-operation.schema.json",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "auditEvents",
|
||||
"protocolField": "auditEvents",
|
||||
"dbColumn": "protocol/schemas/audit-event.schema.json",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "traceEvents",
|
||||
"protocolField": "traceEvents",
|
||||
"dbColumn": "protocol/schemas/trace-event.schema.json",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "evidenceRecords",
|
||||
"protocolField": "evidenceRecords",
|
||||
"dbColumn": "protocol/schemas/evidence-record.schema.json",
|
||||
"status": "aligned"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "trace_event",
|
||||
"protocol": "protocol/schemas/trace-event.schema.json",
|
||||
"dbTable": "agent_trace_events",
|
||||
"drift": [
|
||||
{
|
||||
"field": "traceEventId",
|
||||
"protocolField": "traceEventId",
|
||||
"dbColumn": "id",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "traceId",
|
||||
"protocolField": "traceId",
|
||||
"dbColumn": "trace_id",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "projectId",
|
||||
"protocolField": "projectId",
|
||||
"dbColumn": "event_json.projectId",
|
||||
"status": "aligned",
|
||||
"notes": "Project scope is payload-only in the skeleton."
|
||||
},
|
||||
{
|
||||
"field": "agentSessionId",
|
||||
"protocolField": "agentSessionId",
|
||||
"dbColumn": "agent_session_id",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "workerSessionId",
|
||||
"protocolField": "workerSessionId",
|
||||
"dbColumn": "worker_session_id",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "operationId",
|
||||
"protocolField": "operationId",
|
||||
"dbColumn": "event_json.operationId",
|
||||
"status": "aligned",
|
||||
"notes": "Operation scope is payload-only in the skeleton."
|
||||
},
|
||||
{
|
||||
"field": "serviceId",
|
||||
"protocolField": "serviceId",
|
||||
"dbColumn": "event_json.serviceId",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "level",
|
||||
"protocolField": "level",
|
||||
"dbColumn": "level",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "message",
|
||||
"protocolField": "message",
|
||||
"dbColumn": "message",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "environment",
|
||||
"protocolField": "environment",
|
||||
"dbColumn": "event_json.environment",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "occurredAt",
|
||||
"protocolField": "occurredAt",
|
||||
"dbColumn": "occurred_at",
|
||||
"status": "aligned"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "evidence_record",
|
||||
"protocol": "protocol/schemas/evidence-record.schema.json",
|
||||
"dbTable": "evidence_records",
|
||||
"drift": [
|
||||
{
|
||||
"field": "evidenceId",
|
||||
"protocolField": "evidenceId",
|
||||
"dbColumn": "id",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "projectId",
|
||||
"protocolField": "projectId",
|
||||
"dbColumn": "project_id",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "operationId",
|
||||
"protocolField": "operationId",
|
||||
"dbColumn": "operation_id",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "agentSessionId",
|
||||
"protocolField": "agentSessionId",
|
||||
"dbColumn": "metadata_json.agentSessionId",
|
||||
"status": "aligned",
|
||||
"notes": "Optional in protocol and currently payload-only in the skeleton."
|
||||
},
|
||||
{
|
||||
"field": "workerSessionId",
|
||||
"protocolField": "workerSessionId",
|
||||
"dbColumn": "metadata_json.workerSessionId",
|
||||
"status": "aligned",
|
||||
"notes": "Optional in protocol and currently payload-only in the skeleton."
|
||||
},
|
||||
{
|
||||
"field": "kind",
|
||||
"protocolField": "kind",
|
||||
"dbColumn": "evidence_type",
|
||||
"status": "gap",
|
||||
"gap": "The migration uses evidence_type, but the protocol requires kind."
|
||||
},
|
||||
{
|
||||
"field": "uri",
|
||||
"protocolField": "uri",
|
||||
"dbColumn": "uri",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "sha256",
|
||||
"protocolField": "sha256",
|
||||
"dbColumn": "metadata_json.sha256",
|
||||
"status": "gap",
|
||||
"gap": "The migration has no dedicated sha256 column."
|
||||
},
|
||||
{
|
||||
"field": "serviceId",
|
||||
"protocolField": "serviceId",
|
||||
"dbColumn": "metadata_json.serviceId",
|
||||
"status": "aligned",
|
||||
"notes": "Frozen service ID is present in protocol and fixtures."
|
||||
},
|
||||
{
|
||||
"field": "environment",
|
||||
"protocolField": "environment",
|
||||
"dbColumn": "metadata_json.environment",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "createdAt",
|
||||
"protocolField": "createdAt",
|
||||
"dbColumn": "created_at",
|
||||
"status": "aligned"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "deploy_manifest",
|
||||
"protocol": "deploy/deploy.schema.json",
|
||||
"dbTable": null,
|
||||
"drift": [
|
||||
{
|
||||
"field": "manifestVersion",
|
||||
"protocolField": "manifestVersion",
|
||||
"dbColumn": "n/a",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "environment",
|
||||
"protocolField": "environment",
|
||||
"dbColumn": "n/a",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "commitId",
|
||||
"protocolField": "commitId",
|
||||
"dbColumn": "n/a",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "profiles.dev.name",
|
||||
"protocolField": "profiles.dev.name",
|
||||
"dbColumn": "n/a",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "services[].serviceId",
|
||||
"protocolField": "services[].serviceId",
|
||||
"dbColumn": "n/a",
|
||||
"status": "aligned"
|
||||
},
|
||||
{
|
||||
"field": "services[].replicas",
|
||||
"protocolField": "services[].replicas",
|
||||
"dbColumn": "n/a",
|
||||
"status": "aligned"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"requiredKeyCoverage": {
|
||||
"projectId": {
|
||||
"protocol": [
|
||||
"hardware-operation",
|
||||
"audit-event",
|
||||
"trace-event",
|
||||
"evidence-record",
|
||||
"deploy-manifest"
|
||||
],
|
||||
"dbMigration": [
|
||||
"projects.project_id",
|
||||
"gateway_sessions.project_id",
|
||||
"box_resources.project_id",
|
||||
"wiring_configs.project_id",
|
||||
"hardware_operations.project_id",
|
||||
"agent_sessions.project_id",
|
||||
"audit_events.event_json.projectId",
|
||||
"agent_trace_events.event_json.projectId",
|
||||
"evidence_records.project_id"
|
||||
]
|
||||
},
|
||||
"operationId": {
|
||||
"protocol": [
|
||||
"hardware-operation",
|
||||
"audit-event",
|
||||
"trace-event",
|
||||
"evidence-record"
|
||||
],
|
||||
"dbMigration": [
|
||||
"hardware_operations.id",
|
||||
"audit_events.event_json.operationId",
|
||||
"agent_trace_events.event_json.operationId",
|
||||
"evidence_records.operation_id"
|
||||
]
|
||||
},
|
||||
"auditId": {
|
||||
"protocol": [
|
||||
"audit-event",
|
||||
"evidence-record.metadata.producedByAuditId",
|
||||
"trace-event.metadata.auditId"
|
||||
],
|
||||
"dbMigration": [
|
||||
"audit_events.id",
|
||||
"audit_events.event_json.auditId",
|
||||
"agent_trace_events.event_json.auditId",
|
||||
"evidence_records.metadata_json.producedByAuditId"
|
||||
]
|
||||
},
|
||||
"traceId": {
|
||||
"protocol": [
|
||||
"audit-event",
|
||||
"trace-event",
|
||||
"evidence-record.metadata.traceId"
|
||||
],
|
||||
"dbMigration": [
|
||||
"audit_events.request_id",
|
||||
"agent_trace_events.trace_id",
|
||||
"evidence_records.metadata_json.traceId"
|
||||
]
|
||||
},
|
||||
"evidenceId": {
|
||||
"protocol": [
|
||||
"evidence-record",
|
||||
"audit-event.targetId",
|
||||
"trace-event.metadata.evidenceId"
|
||||
],
|
||||
"dbMigration": [
|
||||
"evidence_records.id",
|
||||
"audit_events.event_json.evidenceId",
|
||||
"agent_trace_events.event_json.evidenceId"
|
||||
]
|
||||
},
|
||||
"serviceId": {
|
||||
"protocol": [
|
||||
"audit-event",
|
||||
"trace-event",
|
||||
"evidence-record",
|
||||
"deploy-manifest.services[].serviceId"
|
||||
],
|
||||
"dbMigration": [
|
||||
"audit_events.event_json.serviceId",
|
||||
"agent_trace_events.event_json.serviceId",
|
||||
"evidence_records.metadata_json.serviceId",
|
||||
"gateway_sessions.gateway_service_id"
|
||||
]
|
||||
},
|
||||
"environment": {
|
||||
"protocol": [
|
||||
"hardware-operation",
|
||||
"audit-event",
|
||||
"trace-event",
|
||||
"evidence-record",
|
||||
"deploy-manifest"
|
||||
],
|
||||
"dbMigration": [
|
||||
"hardware_operations.operation_json.environment",
|
||||
"audit_events.event_json.environment",
|
||||
"agent_trace_events.event_json.environment",
|
||||
"evidence_records.metadata_json.environment"
|
||||
]
|
||||
},
|
||||
"resourceId": {
|
||||
"protocol": [
|
||||
"hardware-operation",
|
||||
"audit-event.metadata.resourceId",
|
||||
"evidence-record.metadata.resourceId"
|
||||
],
|
||||
"dbMigration": [
|
||||
"box_resources.id",
|
||||
"hardware_operations.operation_json.resourceId",
|
||||
"audit_events.event_json.resourceId",
|
||||
"evidence_records.metadata_json.resourceId"
|
||||
]
|
||||
},
|
||||
"sessionId": {
|
||||
"protocol": [
|
||||
"gatewaySessionId",
|
||||
"agentSessionId",
|
||||
"workerSessionId"
|
||||
],
|
||||
"dbMigration": [
|
||||
"gateway_sessions.id",
|
||||
"agent_sessions.id",
|
||||
"worker_sessions.id",
|
||||
"hardware_operations.gateway_session_id",
|
||||
"agent_trace_events.agent_session_id",
|
||||
"agent_trace_events.worker_session_id",
|
||||
"evidence_records.metadata_json.agentSessionId",
|
||||
"evidence_records.metadata_json.workerSessionId"
|
||||
]
|
||||
}
|
||||
},
|
||||
"missingToFixBeforeRealPersistence": [
|
||||
{
|
||||
"field": "audit_events.request_id",
|
||||
"reason": "Must be renamed or mapped to traceId before audit rows are treated as protocol-shaped records."
|
||||
},
|
||||
{
|
||||
"field": "audit_events.actor",
|
||||
"reason": "Must be split into actorType and actorId or mapped through a persisted envelope."
|
||||
},
|
||||
{
|
||||
"field": "audit_events.source",
|
||||
"reason": "Must be renamed or mapped to actorId."
|
||||
},
|
||||
{
|
||||
"field": "audit_events.operation",
|
||||
"reason": "Must be renamed or mapped to action."
|
||||
},
|
||||
{
|
||||
"field": "audit_events.target",
|
||||
"reason": "Must be split into targetType and targetId."
|
||||
},
|
||||
{
|
||||
"field": "audit_events.timestamp",
|
||||
"reason": "Must be renamed or mapped to occurredAt."
|
||||
},
|
||||
{
|
||||
"field": "evidence_records.evidence_type",
|
||||
"reason": "Must be renamed or mapped to kind."
|
||||
},
|
||||
{
|
||||
"field": "evidence_records.metadata_json.sha256",
|
||||
"reason": "Must become a first-class persisted field or a tested mapping target."
|
||||
},
|
||||
{
|
||||
"field": "hardware_operations.operation_json.resourceId",
|
||||
"reason": "Must be exposed through a real column or tested extraction path."
|
||||
},
|
||||
{
|
||||
"field": "hardware_operations.operation_json.capabilityId",
|
||||
"reason": "Must be exposed through a real column or tested extraction path."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env node
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const driftMapPath = path.join(repoRoot, "protocol/schema-drift-map.json");
|
||||
|
||||
const requiredKeys = [
|
||||
"projectId",
|
||||
"operationId",
|
||||
"auditId",
|
||||
"traceId",
|
||||
"evidenceId",
|
||||
"serviceId",
|
||||
"environment",
|
||||
"resourceId",
|
||||
"sessionId"
|
||||
];
|
||||
|
||||
const requiredStatuses = new Set(["aligned", "gap"]);
|
||||
|
||||
function assertObject(value, label) {
|
||||
assert.ok(value && typeof value === "object" && !Array.isArray(value), `${label} must be an object`);
|
||||
}
|
||||
|
||||
function loadDriftMap() {
|
||||
const raw = readFile(driftMapPath, "utf8");
|
||||
return raw.then((content) => JSON.parse(content));
|
||||
}
|
||||
|
||||
function assertRequiredCoverage(map) {
|
||||
assertObject(map, "drift map");
|
||||
assert.equal(map.version, 1, "drift map version must be 1");
|
||||
assertObject(map.summary, "drift map summary");
|
||||
assert.ok(Array.isArray(map.summary.aligned), "drift map summary.aligned must be an array");
|
||||
assert.ok(Array.isArray(map.entities), "drift map entities must be an array");
|
||||
assert.ok(
|
||||
map.entities.length >= 5,
|
||||
"expected drift map entities for protocol, DB, audit, evidence chain, evidence, and deploy"
|
||||
);
|
||||
assertObject(map.requiredKeyCoverage, "drift map requiredKeyCoverage");
|
||||
|
||||
for (const key of requiredKeys) {
|
||||
assert.ok(Object.hasOwn(map.requiredKeyCoverage, key), `missing required coverage for ${key}`);
|
||||
const coverage = map.requiredKeyCoverage[key];
|
||||
assertObject(coverage, `coverage for ${key}`);
|
||||
assert.ok(Array.isArray(coverage.protocol) && coverage.protocol.length >= 1, `${key} protocol coverage must exist`);
|
||||
assert.ok(
|
||||
Array.isArray(coverage.dbMigration) && coverage.dbMigration.length >= 1,
|
||||
`${key} dbMigration coverage must exist`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function assertEntityDrift(entity) {
|
||||
assertObject(entity, `entity ${entity?.name ?? "<unknown>"}`);
|
||||
assert.equal(typeof entity.name, "string", "entity.name must be a string");
|
||||
assert.equal(typeof entity.protocol, "string", `entity ${entity.name} protocol must be a string`);
|
||||
assert.ok(Array.isArray(entity.drift) && entity.drift.length >= 1, `entity ${entity.name} drift must exist`);
|
||||
|
||||
for (const [index, entry] of entity.drift.entries()) {
|
||||
assertObject(entry, `${entity.name}.drift[${index}]`);
|
||||
assert.equal(typeof entry.field, "string", `${entity.name}.drift[${index}].field must be a string`);
|
||||
assert.equal(typeof entry.protocolField, "string", `${entity.name}.drift[${index}].protocolField must be a string`);
|
||||
assert.equal(typeof entry.dbColumn, "string", `${entity.name}.drift[${index}].dbColumn must be a string`);
|
||||
assert.ok(requiredStatuses.has(entry.status), `${entity.name}.drift[${index}].status must be aligned or gap`);
|
||||
if (entry.status === "gap") {
|
||||
assert.equal(typeof entry.gap, "string", `${entity.name}.drift[${index}].gap must describe the gap`);
|
||||
assert.ok(entry.gap.length > 0, `${entity.name}.drift[${index}].gap must not be empty`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertSummary(map) {
|
||||
const aligned = new Set(map.summary?.aligned ?? []);
|
||||
for (const key of requiredKeys) {
|
||||
assert.ok(aligned.has(key), `summary.aligned must include ${key}`);
|
||||
}
|
||||
assert.equal(typeof map.summary.gapCount, "number", "summary.gapCount must be numeric");
|
||||
assert.ok(map.summary.gapCount >= 1, "summary.gapCount must report at least one gap");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const map = await loadDriftMap();
|
||||
|
||||
assert.equal(map.$schema, "https://json-schema.org/draft/2020-12/schema", "drift map $schema");
|
||||
assert.equal(
|
||||
map.$id,
|
||||
"https://hwlab.pikastech.local/protocol/schema-drift-map.json",
|
||||
"drift map $id"
|
||||
);
|
||||
|
||||
assertSummary(map);
|
||||
assertRequiredCoverage(map);
|
||||
|
||||
let alignCount = 0;
|
||||
let gapCount = 0;
|
||||
for (const entity of map.entities) {
|
||||
assertEntityDrift(entity);
|
||||
alignCount += entity.drift.filter((entry) => entry.status === "aligned").length;
|
||||
gapCount += entity.drift.filter((entry) => entry.status === "gap").length;
|
||||
}
|
||||
|
||||
assert.equal(gapCount, map.summary.gapCount, "summary.gapCount must match computed gap count");
|
||||
|
||||
assert.ok(
|
||||
Array.isArray(map.missingToFixBeforeRealPersistence) && map.missingToFixBeforeRealPersistence.length >= 1,
|
||||
"expected a non-empty list of persistence blockers"
|
||||
);
|
||||
|
||||
assert.ok(alignCount >= 20, "expected broad alignment coverage");
|
||||
assert.ok(gapCount >= 5, "expected explicit drift gaps");
|
||||
|
||||
console.log(`validated schema drift map with ${alignCount} aligned fields and ${gapCount} gaps`);
|
||||
}
|
||||
|
||||
await main();
|
||||
Reference in New Issue
Block a user