Skip to content

Commit dc20df8

Browse files
author
StackMemory Bot (CLI)
committed
feat(provenant): SOP compliance report command
Add to report SOP compliance from logged PROSE test results. - Add to retrieve nodes by source - Add with formatted pass/fail/unknown report - Register CLI command - Add database test for getNodesBySourceSystem - 39 Provenant tests passing
1 parent 9091390 commit dc20df8

4 files changed

Lines changed: 144 additions & 0 deletions

File tree

packages/provenant/src/__tests__/database.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,29 @@ describe('Database — sources and source edges', () => {
117117
expect(db.getSourceByExternalId('slack', 'msg-99')).toBeUndefined();
118118
expect(db.getSourceByExternalId('linear', 'msg-42')).toBeUndefined();
119119
});
120+
121+
it('finds nodes by source system', () => {
122+
const node = db.insertNode({
123+
type: 'decision',
124+
content: 'E.1 / SOP-101 Frame Lifecycle: compliance verified',
125+
embedding: null,
126+
actor: 'prose-harness',
127+
confidence: 0.95,
128+
});
129+
const source = db.insertSource({
130+
system: 'prose-test-run',
131+
external_id: 'run-1:E.1',
132+
raw_payload: '{}',
133+
hash: 'run-1:E.1',
134+
});
135+
db.linkNodeToSource(node.id, source.id, 'prose-test-run', 'run-1:E.1');
136+
137+
const found = db.getNodesBySourceSystem('prose-test-run');
138+
expect(found).toHaveLength(1);
139+
expect(found[0].id).toBe(node.id);
140+
141+
expect(db.getNodesBySourceSystem('other-system')).toHaveLength(0);
142+
});
120143
});
121144

122145
describe('Database — rejection log', () => {
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { existsSync } from 'node:fs';
2+
import { Database } from '../../schema/database.js';
3+
4+
interface ComplianceOpts {
5+
db: string;
6+
system?: string;
7+
}
8+
9+
interface ComplianceEntry {
10+
proseId: string;
11+
sop: string;
12+
status: 'pass' | 'fail' | 'unknown';
13+
confidence: number;
14+
runId: string;
15+
runAt: string;
16+
}
17+
18+
export function compliance(opts: ComplianceOpts): void {
19+
if (!existsSync(opts.db)) {
20+
console.error('No database found. Run `provenant log-decision` or ingest first.');
21+
process.exit(1);
22+
}
23+
24+
const db = new Database(opts.db);
25+
26+
try {
27+
const system = opts.system ?? 'prose-test-run';
28+
const nodes = db.getNodesBySourceSystem(system);
29+
30+
const entries: ComplianceEntry[] = [];
31+
const seen = new Set<string>();
32+
33+
for (const node of nodes) {
34+
const sources = db.getSourcesForNode(node.id);
35+
const source = sources.find((s) => s.system === system);
36+
if (!source) continue;
37+
38+
let metadata: Record<string, unknown> = {};
39+
try {
40+
metadata = JSON.parse(source.raw_payload) as Record<string, unknown>;
41+
} catch {
42+
// ignore malformed payload
43+
}
44+
45+
const proseId = (metadata['proseId'] as string) ?? extractProseId(node.content);
46+
const sop = (metadata['sop'] as string) ?? extractSop(node.content);
47+
if (!proseId || seen.has(proseId)) continue;
48+
seen.add(proseId);
49+
50+
const passed = node.content.includes('compliance verified');
51+
const failed = node.content.includes('compliance NOT verified');
52+
53+
entries.push({
54+
proseId,
55+
sop,
56+
status: passed ? 'pass' : failed ? 'fail' : 'unknown',
57+
confidence: node.confidence,
58+
runId: source.external_id.split(':')[0] ?? 'unknown',
59+
runAt: new Date(node.created_at).toISOString(),
60+
});
61+
}
62+
63+
// Sort by PROSE ID
64+
entries.sort((a, b) => a.proseId.localeCompare(b.proseId));
65+
66+
console.log(`SOP Compliance Report (${system})`);
67+
console.log('─'.repeat(60));
68+
69+
if (entries.length === 0) {
70+
console.log('No compliance decisions found.');
71+
console.log(`Run: npx tsx scripts/log-prose-results.ts ${opts.db}`);
72+
return;
73+
}
74+
75+
const passed = entries.filter((e) => e.status === 'pass').length;
76+
const failed = entries.filter((e) => e.status === 'fail').length;
77+
78+
for (const entry of entries) {
79+
const icon = entry.status === 'pass' ? '✓' : entry.status === 'fail' ? '✗' : '?';
80+
console.log(
81+
`${icon} ${entry.proseId} ${entry.sop.slice(0, 40).padEnd(40)} confidence ${entry.confidence.toFixed(2)}`
82+
);
83+
}
84+
85+
console.log('─'.repeat(60));
86+
console.log(`Total: ${entries.length} Passed: ${passed} Failed: ${failed}`);
87+
} finally {
88+
db.close();
89+
}
90+
}
91+
92+
function extractProseId(content: string): string | undefined {
93+
const match = content.match(/^(E\.\d+)/);
94+
return match?.[1];
95+
}
96+
97+
function extractSop(content: string): string {
98+
const match = content.match(/^E\.\d+ \/ ([^:]+):/);
99+
return match?.[1] ?? 'Unknown SOP';
100+
}

packages/provenant/src/cli/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
} from './commands/log-override.js';
1818
import { serve } from './commands/serve.js';
1919
import { calibrate } from './commands/calibrate.js';
20+
import { compliance } from './commands/compliance.js';
2021

2122
const DB_DEFAULT = process.env['PROVENANT_DB'] || '.provenant/graph.db';
2223

@@ -135,6 +136,14 @@ program
135136
.option('--db <path>', 'Database path (or set PROVENANT_DB)', DB_DEFAULT)
136137
.action(serve);
137138

139+
// SOP compliance report
140+
program
141+
.command('compliance')
142+
.description('Show SOP compliance report from logged PROSE test results')
143+
.option('--db <path>', 'Database path (or set PROVENANT_DB)', DB_DEFAULT)
144+
.option('--system <system>', 'Source system to report on', 'prose-test-run')
145+
.action(compliance);
146+
138147
// Shadow mode calibration
139148
program
140149
.command('calibrate')

packages/provenant/src/schema/database.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,18 @@ export class Database {
339339
.all(nodeId) as Source[];
340340
}
341341

342+
getNodesBySourceSystem(system: string): Node[] {
343+
return this.db
344+
.prepare(
345+
`SELECT n.* FROM nodes n
346+
JOIN source_edges se ON se.node_id = n.id
347+
JOIN sources s ON s.id = se.source_id
348+
WHERE s.system = ?
349+
ORDER BY n.created_at DESC`
350+
)
351+
.all(system) as Node[];
352+
}
353+
342354
// --- Rejection Log ---
343355

344356
insertRejection(params: {

0 commit comments

Comments
 (0)