diff --git a/plugins/reporting-governance/package-lock.json b/plugins/reporting-governance/package-lock.json index a4557bf..1b4abbd 100644 --- a/plugins/reporting-governance/package-lock.json +++ b/plugins/reporting-governance/package-lock.json @@ -8,8 +8,9 @@ "name": "@openclaw/plugin-reporting-governance", "version": "0.1.0-mainline", "dependencies": { - "ajv": "^8.20.0", - "yaml": "^2.8.4" + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "yaml": "^2.8.0" } }, "node_modules/ajv": { @@ -28,6 +29,23 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", diff --git a/plugins/reporting-governance/package.json b/plugins/reporting-governance/package.json index 99b9592..e9c8a94 100644 --- a/plugins/reporting-governance/package.json +++ b/plugins/reporting-governance/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", "yaml": "^2.8.0" } } diff --git a/plugins/reporting-governance/src/storage/decision-artifact.mjs b/plugins/reporting-governance/src/storage/decision-artifact.mjs index 6a85d55..05e7f8f 100644 --- a/plugins/reporting-governance/src/storage/decision-artifact.mjs +++ b/plugins/reporting-governance/src/storage/decision-artifact.mjs @@ -3,6 +3,7 @@ import fs from 'node:fs'; import path from 'node:path'; import Ajv2020 from 'ajv/dist/2020.js'; +import addFormats from 'ajv-formats'; const EXPECTED_KIND = 'DecisionRecordArtifact'; const EXPECTED_API_VERSION = 'reporting-governance/v1alpha1'; @@ -24,6 +25,7 @@ const decisionSchemaPath = path.resolve(repoRoot, 'schemas', 'reporting-governan const decisionRecordArtifactSchemaPath = path.resolve(repoRoot, 'schemas', 'reporting-governance', 'decision-record-artifact.schema.json'); const ajv = new Ajv2020({ allErrors: true, strict: false }); +addFormats(ajv); const decisionSchema = JSON.parse(fs.readFileSync(decisionSchemaPath, 'utf8')); const decisionRecordArtifactSchema = JSON.parse(fs.readFileSync(decisionRecordArtifactSchemaPath, 'utf8')); ajv.addSchema(decisionSchema); diff --git a/plugins/reporting-governance/test/decision-store-runtime.integration.test.mjs b/plugins/reporting-governance/test/decision-store-runtime.integration.test.mjs index be8d9a9..c7ba4b7 100644 --- a/plugins/reporting-governance/test/decision-store-runtime.integration.test.mjs +++ b/plugins/reporting-governance/test/decision-store-runtime.integration.test.mjs @@ -257,3 +257,57 @@ test('persisted decision artifact is consumable by downstream validator without fs.rmSync(root, { recursive: true, force: true }); } }); + + +test('downstream validator rejects persisted decision artifact with invalid canonical decision datetime format', () => { + const root = createFixtureRoot(); + try { + mkdirs(root, ['evidence', 'events', 'queue', 'spool', 'receipts', 'repo']); + const statePath = writeState(root); + const fakeRepoRoot = path.join(root, 'repo'); + const decisionsDir = path.join(fakeRepoRoot, 'state', 'decisions'); + fs.mkdirSync(decisionsDir, { recursive: true }); + + const result = executeRuntimeIntegratedGovernance({ + ...createBaseArgs(), + runtime: { + state: statePath, + evidenceDir: path.join(root, 'evidence'), + eventDir: path.join(root, 'events'), + queueDir: path.join(root, 'queue'), + spoolDir: path.join(root, 'spool'), + receiptDir: path.join(root, 'receipts'), + senderCommand: `node -e "process.stdout.write(JSON.stringify({state:'sent'}))"`, + writeState: true, + now: '2026-05-07T08:20:00.000Z', + }, + }); + + const queueItem = readSingleJson(path.join(root, 'queue')); + const runtimeEventRef = queueItem.evidence_refs.find((ref) => ref.label === 'watchdog_event'); + const store = createFileDecisionStore({ decisionsDir, repoRootOverride: fakeRepoRoot }); + const written = store.write({ + decision: result.planning.decision, + receipt: result.planning.receipt, + recordedAt: '2026-05-08T04:00:00.000Z', + source: { + task_id: queueItem.governance.task_id, + correlation_id: queueItem.governance.correlation_id, + event_id: runtimeEventRef.path, + }, + }); + + const persisted = JSON.parse(fs.readFileSync(written.artifactPath, 'utf8')); + persisted.spec.decision.operator_notice.deadline = 'tomorrow-ish'; + fs.writeFileSync(written.artifactPath, `${JSON.stringify(persisted, null, 2)} +`, 'utf8'); + + const malformedPersisted = JSON.parse(fs.readFileSync(written.artifactPath, 'utf8')); + assert.throws( + () => validateDecisionRecordArtifact(malformedPersisted), + /spec\/decision\/operator_notice\/deadline must match format "date-time"/ + ); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +});