ctx-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +169 -0
- package/migrations/001_init.sql +52 -0
- package/migrations/002_fts.sql +27 -0
- package/package.json +33 -0
- package/scripts/risk-check.ts +46 -0
- package/scripts/seed-trace.ts +144 -0
- package/scripts/similarity.ts +35 -0
- package/src/explain/explain.ts +125 -0
- package/src/graph/graph.ts +153 -0
- package/src/mcp/resources.ts +234 -0
- package/src/mcp/tools.ts +522 -0
- package/src/schemas.ts +238 -0
- package/src/server.ts +38 -0
- package/src/similarity/similarity.ts +299 -0
- package/src/storage/db.ts +17 -0
- package/src/storage/engine.ts +14 -0
- package/src/storage/migrations.ts +19 -0
- package/src/storage/queries.ts +294 -0
- package/src/storage/sqljsEngine.ts +207 -0
- package/src/util/fs.ts +15 -0
- package/src/util/hashing.ts +5 -0
- package/src/util/ids.ts +8 -0
- package/src/util/redact.ts +26 -0
- package/tests/explain.test.ts +81 -0
- package/tests/graph.test.ts +85 -0
- package/tests/query.test.ts +275 -0
- package/tests/similarity.test.ts +103 -0
- package/tests/storage.test.ts +91 -0
- package/tsconfig.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# ctx-mcp
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/ctx-mcp)
|
|
4
|
+
|
|
5
|
+
Local MCP server for decision trace capture and deterministic explanations.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Node.js 18+
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Run (stdio MCP server)
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm run dev
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Architecture (high level)
|
|
24
|
+
|
|
25
|
+
```mermaid
|
|
26
|
+
flowchart LR
|
|
27
|
+
Client[MCP client] -->|stdio| Server[ctx MCP server]
|
|
28
|
+
Server --> Tools[Trace tools]
|
|
29
|
+
Tools --> Store[(Trace store)]
|
|
30
|
+
Server --> Resources[trace:// resources]
|
|
31
|
+
Store --> Resources
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage contract (agents)
|
|
35
|
+
|
|
36
|
+
- You MUST call `trace.start` when a user-requested task begins (once intent is clear).
|
|
37
|
+
- You MUST call `trace.finish` when that task is completed, even if the conversation continues, to mark outcome/status for downstream analysis.
|
|
38
|
+
- Treat a "task" as a single user goal; if the user pivots to a new goal, start a new trace.
|
|
39
|
+
- If a task is completed and the user continues with a new goal, start a new trace (optionally link the prior trace in metadata).
|
|
40
|
+
|
|
41
|
+
## Core tools
|
|
42
|
+
|
|
43
|
+
- `trace.start`
|
|
44
|
+
- `trace.add_node`
|
|
45
|
+
- `trace.add_edge`
|
|
46
|
+
- `trace.attach_artifact`
|
|
47
|
+
- `trace.finish`
|
|
48
|
+
- `trace.query`
|
|
49
|
+
- `trace.get_subgraph`
|
|
50
|
+
- `trace.find_paths`
|
|
51
|
+
- `trace.explain_decision`
|
|
52
|
+
- `trace.similarity`
|
|
53
|
+
- `trace.risk_check`
|
|
54
|
+
|
|
55
|
+
## Example tool calls
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"tool": "trace.start",
|
|
60
|
+
"input": {
|
|
61
|
+
"intent": "Investigate alert",
|
|
62
|
+
"tags": ["incident", "p1"],
|
|
63
|
+
"metadata": { "ticket": "INC-123" }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"tool": "trace.add_node",
|
|
71
|
+
"input": {
|
|
72
|
+
"trace_id": "trace-uuid",
|
|
73
|
+
"type": "Decision",
|
|
74
|
+
"summary": "Roll back release",
|
|
75
|
+
"data": { "reason": "error rate spike" },
|
|
76
|
+
"confidence": 0.8
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"tool": "trace.add_edge",
|
|
84
|
+
"input": {
|
|
85
|
+
"trace_id": "trace-uuid",
|
|
86
|
+
"from_node_id": "decision-node-id",
|
|
87
|
+
"to_node_id": "action-node-id",
|
|
88
|
+
"relation_type": "causes"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"tool": "trace.explain_decision",
|
|
96
|
+
"input": {
|
|
97
|
+
"trace_id": "trace-uuid",
|
|
98
|
+
"decision_node_id": "decision-node-id",
|
|
99
|
+
"depth": 4
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"tool": "trace.similarity",
|
|
107
|
+
"input": {
|
|
108
|
+
"decision_node_id": "decision-node-id",
|
|
109
|
+
"scope": "all",
|
|
110
|
+
"limit": 5
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"tool": "trace.risk_check",
|
|
118
|
+
"input": {
|
|
119
|
+
"decision_node_id": "decision-node-id",
|
|
120
|
+
"threshold": 0.6
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Resources
|
|
126
|
+
|
|
127
|
+
- `trace://{trace_id}`
|
|
128
|
+
- `trace://{trace_id}/timeline`
|
|
129
|
+
- `trace://{trace_id}/graph`
|
|
130
|
+
- `trace://{trace_id}/subgraph?center=...&depth=...&dir=...`
|
|
131
|
+
- `trace://{trace_id}/explain?decision=...&depth=...`
|
|
132
|
+
- `trace://search?text=...&trace_id=...&type=...`
|
|
133
|
+
- `trace://{trace_id}/similarity?decision=...&scope=...&limit=...&depth=...&max_traces=...`
|
|
134
|
+
- `trace://{trace_id}/risk?decision=...&scope=...&limit=...&depth=...&threshold=...&max_traces=...`
|
|
135
|
+
|
|
136
|
+
## MCP config example
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"mcpServers": {
|
|
141
|
+
"ctx": {
|
|
142
|
+
"command": "npx",
|
|
143
|
+
"args": ["ctx-mcp"]
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Testing
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
npm test
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Seed a sample trace
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
npm run seed
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## CLI examples
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
npm run risk-check -- decision-node-id
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
npm run similarity -- decision-node-id
|
|
169
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS traces (
|
|
2
|
+
trace_id TEXT PRIMARY KEY,
|
|
3
|
+
workflow_id TEXT,
|
|
4
|
+
actor TEXT,
|
|
5
|
+
intent TEXT NOT NULL,
|
|
6
|
+
tags_json TEXT NOT NULL,
|
|
7
|
+
metadata_json TEXT NOT NULL,
|
|
8
|
+
started_at INTEGER NOT NULL,
|
|
9
|
+
finished_at INTEGER,
|
|
10
|
+
status TEXT,
|
|
11
|
+
outcome_json TEXT
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
15
|
+
node_id TEXT PRIMARY KEY,
|
|
16
|
+
trace_id TEXT NOT NULL,
|
|
17
|
+
type TEXT NOT NULL,
|
|
18
|
+
summary TEXT NOT NULL,
|
|
19
|
+
data_json TEXT NOT NULL,
|
|
20
|
+
confidence REAL,
|
|
21
|
+
metadata_json TEXT NOT NULL,
|
|
22
|
+
created_at INTEGER NOT NULL
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
26
|
+
edge_id TEXT PRIMARY KEY,
|
|
27
|
+
trace_id TEXT NOT NULL,
|
|
28
|
+
from_node_id TEXT NOT NULL,
|
|
29
|
+
to_node_id TEXT NOT NULL,
|
|
30
|
+
relation_type TEXT NOT NULL,
|
|
31
|
+
data_json TEXT NOT NULL,
|
|
32
|
+
created_at INTEGER NOT NULL
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
CREATE TABLE IF NOT EXISTS artifacts (
|
|
36
|
+
artifact_id TEXT PRIMARY KEY,
|
|
37
|
+
trace_id TEXT NOT NULL,
|
|
38
|
+
node_id TEXT,
|
|
39
|
+
artifact_type TEXT NOT NULL,
|
|
40
|
+
content TEXT NOT NULL,
|
|
41
|
+
redaction_level TEXT NOT NULL,
|
|
42
|
+
metadata_json TEXT NOT NULL,
|
|
43
|
+
sha256 TEXT NOT NULL,
|
|
44
|
+
created_at INTEGER NOT NULL
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_trace ON nodes(trace_id);
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_trace_type ON nodes(trace_id, type);
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(trace_id, from_node_id);
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_edges_to ON edges(trace_id, to_node_id);
|
|
51
|
+
CREATE INDEX IF NOT EXISTS idx_artifacts_trace ON artifacts(trace_id);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_artifacts_node ON artifacts(node_id);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
|
|
2
|
+
node_id,
|
|
3
|
+
trace_id,
|
|
4
|
+
type,
|
|
5
|
+
summary
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
INSERT INTO nodes_fts(rowid, node_id, trace_id, type, summary)
|
|
9
|
+
SELECT rowid, node_id, trace_id, type, summary FROM nodes;
|
|
10
|
+
|
|
11
|
+
CREATE TRIGGER IF NOT EXISTS nodes_ai AFTER INSERT ON nodes BEGIN
|
|
12
|
+
INSERT INTO nodes_fts(rowid, node_id, trace_id, type, summary)
|
|
13
|
+
VALUES (new.rowid, new.node_id, new.trace_id, new.type, new.summary);
|
|
14
|
+
END;
|
|
15
|
+
|
|
16
|
+
CREATE TRIGGER IF NOT EXISTS nodes_ad AFTER DELETE ON nodes BEGIN
|
|
17
|
+
DELETE FROM nodes_fts WHERE rowid = old.rowid;
|
|
18
|
+
END;
|
|
19
|
+
|
|
20
|
+
CREATE TRIGGER IF NOT EXISTS nodes_au AFTER UPDATE ON nodes BEGIN
|
|
21
|
+
UPDATE nodes_fts SET
|
|
22
|
+
node_id = new.node_id,
|
|
23
|
+
trace_id = new.trace_id,
|
|
24
|
+
type = new.type,
|
|
25
|
+
summary = new.summary
|
|
26
|
+
WHERE rowid = old.rowid;
|
|
27
|
+
END;
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ctx-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Decision Trace MCP Server",
|
|
6
|
+
"repository": "https://github.com/mhingston/ctx",
|
|
7
|
+
"author": "Mark Hingston",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"main": "dist/server.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc -p tsconfig.json",
|
|
12
|
+
"dev": "tsx src/server.ts",
|
|
13
|
+
"start": "node dist/server.js",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"seed": "tsx scripts/seed-trace.ts",
|
|
16
|
+
"risk-check": "tsx scripts/risk-check.ts",
|
|
17
|
+
"similarity": "tsx scripts/similarity.ts"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
21
|
+
"sql.js": "^1.10.2",
|
|
22
|
+
"zod": "^3.25.76"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^22.13.5",
|
|
26
|
+
"tsx": "^4.19.2",
|
|
27
|
+
"typescript": "^5.7.3",
|
|
28
|
+
"vitest": "^2.1.5"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { initStorage } from "../src/storage/db.js";
|
|
2
|
+
import { assessDecisionRisk, findSimilarDecisions } from "../src/similarity/similarity.js";
|
|
3
|
+
|
|
4
|
+
function usage(): void {
|
|
5
|
+
console.log("Usage: npm run risk-check -- <decision_node_id>");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
const decisionNodeId = process.argv[2];
|
|
10
|
+
if (!decisionNodeId) {
|
|
11
|
+
usage();
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const engine = await initStorage({
|
|
16
|
+
onLog: (msg) => console.error(`[ctx] ${msg}`),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const matches = findSimilarDecisions(engine, decisionNodeId, {
|
|
20
|
+
scope: "all",
|
|
21
|
+
limit: 5,
|
|
22
|
+
depth: 4,
|
|
23
|
+
maxTraces: 200,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const assessment = assessDecisionRisk(engine, decisionNodeId, {
|
|
27
|
+
scope: "all",
|
|
28
|
+
limit: 10,
|
|
29
|
+
depth: 4,
|
|
30
|
+
threshold: 0.6,
|
|
31
|
+
maxTraces: 200,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
console.log("Top matches:");
|
|
35
|
+
for (const match of matches) {
|
|
36
|
+
console.log(`- ${match.node.node_id} (${match.score.toFixed(2)}) ${match.node.summary}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log("Risk assessment:");
|
|
40
|
+
console.log(JSON.stringify(assessment, null, 2));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
main().catch((err) => {
|
|
44
|
+
console.error("Risk check failed", err);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { initStorage } from "../src/storage/db.js";
|
|
2
|
+
import {
|
|
3
|
+
insertArtifact,
|
|
4
|
+
insertEdge,
|
|
5
|
+
insertNode,
|
|
6
|
+
insertTrace,
|
|
7
|
+
type ArtifactRow,
|
|
8
|
+
type EdgeRow,
|
|
9
|
+
type NodeRow,
|
|
10
|
+
type TraceRow,
|
|
11
|
+
} from "../src/storage/queries.js";
|
|
12
|
+
import { newId } from "../src/util/ids.js";
|
|
13
|
+
import { sha256 } from "../src/util/hashing.js";
|
|
14
|
+
import { redactSecrets } from "../src/util/redact.js";
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
const engine = await initStorage({
|
|
18
|
+
onLog: (msg) => console.error(`[ctx] ${msg}`),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
const traceId = newId();
|
|
23
|
+
|
|
24
|
+
const trace: TraceRow = {
|
|
25
|
+
trace_id: traceId,
|
|
26
|
+
workflow_id: "demo",
|
|
27
|
+
actor: "local-user",
|
|
28
|
+
intent: "Example trace",
|
|
29
|
+
tags_json: JSON.stringify(["example", "seed"]),
|
|
30
|
+
metadata_json: JSON.stringify({ source: "seed-script" }),
|
|
31
|
+
started_at: now,
|
|
32
|
+
finished_at: null,
|
|
33
|
+
status: null,
|
|
34
|
+
outcome_json: null,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const decisionId = newId();
|
|
38
|
+
const actionId = newId();
|
|
39
|
+
const observationId = newId();
|
|
40
|
+
const outcomeId = newId();
|
|
41
|
+
|
|
42
|
+
const nodes: NodeRow[] = [
|
|
43
|
+
{
|
|
44
|
+
node_id: decisionId,
|
|
45
|
+
trace_id: traceId,
|
|
46
|
+
type: "Decision",
|
|
47
|
+
summary: "Ship with feature flag",
|
|
48
|
+
data_json: JSON.stringify({ reason: "Reduce rollout risk" }),
|
|
49
|
+
confidence: 0.7,
|
|
50
|
+
metadata_json: "{}",
|
|
51
|
+
created_at: now + 1,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
node_id: actionId,
|
|
55
|
+
trace_id: traceId,
|
|
56
|
+
type: "Action",
|
|
57
|
+
summary: "Deploy with gradual rollout",
|
|
58
|
+
data_json: JSON.stringify({ percent: 10 }),
|
|
59
|
+
confidence: null,
|
|
60
|
+
metadata_json: "{}",
|
|
61
|
+
created_at: now + 2,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
node_id: observationId,
|
|
65
|
+
trace_id: traceId,
|
|
66
|
+
type: "Observation",
|
|
67
|
+
summary: "No errors in canary",
|
|
68
|
+
data_json: JSON.stringify({ errorRate: 0 }),
|
|
69
|
+
confidence: null,
|
|
70
|
+
metadata_json: "{}",
|
|
71
|
+
created_at: now + 3,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
node_id: outcomeId,
|
|
75
|
+
trace_id: traceId,
|
|
76
|
+
type: "Outcome",
|
|
77
|
+
summary: "Rollout completed",
|
|
78
|
+
data_json: JSON.stringify({ status: "success" }),
|
|
79
|
+
confidence: null,
|
|
80
|
+
metadata_json: "{}",
|
|
81
|
+
created_at: now + 4,
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const edges: EdgeRow[] = [
|
|
86
|
+
{
|
|
87
|
+
edge_id: newId(),
|
|
88
|
+
trace_id: traceId,
|
|
89
|
+
from_node_id: decisionId,
|
|
90
|
+
to_node_id: actionId,
|
|
91
|
+
relation_type: "causes",
|
|
92
|
+
data_json: "{}",
|
|
93
|
+
created_at: now + 2,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
edge_id: newId(),
|
|
97
|
+
trace_id: traceId,
|
|
98
|
+
from_node_id: actionId,
|
|
99
|
+
to_node_id: observationId,
|
|
100
|
+
relation_type: "derived_from",
|
|
101
|
+
data_json: "{}",
|
|
102
|
+
created_at: now + 3,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
edge_id: newId(),
|
|
106
|
+
trace_id: traceId,
|
|
107
|
+
from_node_id: observationId,
|
|
108
|
+
to_node_id: outcomeId,
|
|
109
|
+
relation_type: "causes",
|
|
110
|
+
data_json: "{}",
|
|
111
|
+
created_at: now + 4,
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
const artifactContent = "deploy log: no errors";
|
|
116
|
+
const redacted = redactSecrets(artifactContent);
|
|
117
|
+
const artifact: ArtifactRow = {
|
|
118
|
+
artifact_id: newId(),
|
|
119
|
+
trace_id: traceId,
|
|
120
|
+
node_id: observationId,
|
|
121
|
+
artifact_type: "log",
|
|
122
|
+
content: redacted.redacted,
|
|
123
|
+
redaction_level: redacted.wasRedacted ? "scrubbed" : "internal",
|
|
124
|
+
metadata_json: JSON.stringify({ source: "seed-script" }),
|
|
125
|
+
sha256: sha256(redacted.redacted),
|
|
126
|
+
created_at: now + 3,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
engine.transaction(() => {
|
|
130
|
+
insertTrace(engine, trace);
|
|
131
|
+
for (const node of nodes) insertNode(engine, node);
|
|
132
|
+
for (const edge of edges) insertEdge(engine, edge);
|
|
133
|
+
insertArtifact(engine, artifact);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
await engine.flushNow();
|
|
137
|
+
|
|
138
|
+
console.log(`Seeded trace ${traceId}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
main().catch((err) => {
|
|
142
|
+
console.error("Seed failed", err);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { initStorage } from "../src/storage/db.js";
|
|
2
|
+
import { findSimilarDecisions } from "../src/similarity/similarity.js";
|
|
3
|
+
|
|
4
|
+
function usage(): void {
|
|
5
|
+
console.log("Usage: npm run similarity -- <decision_node_id>");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
const decisionNodeId = process.argv[2];
|
|
10
|
+
if (!decisionNodeId) {
|
|
11
|
+
usage();
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const engine = await initStorage({
|
|
16
|
+
onLog: (msg) => console.error(`[ctx] ${msg}`),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const matches = findSimilarDecisions(engine, decisionNodeId, {
|
|
20
|
+
scope: "all",
|
|
21
|
+
limit: 10,
|
|
22
|
+
depth: 4,
|
|
23
|
+
maxTraces: 200,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
console.log("Top matches:");
|
|
27
|
+
for (const match of matches) {
|
|
28
|
+
console.log(`- ${match.node.node_id} (${match.score.toFixed(2)}) ${match.node.summary}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
main().catch((err) => {
|
|
33
|
+
console.error("Similarity check failed", err);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { ArtifactRow, EdgeRow, NodeRow } from "../storage/queries.js";
|
|
2
|
+
import { buildGraph } from "../graph/graph.js";
|
|
3
|
+
|
|
4
|
+
export type Explanation = {
|
|
5
|
+
decision_node: NodeRow;
|
|
6
|
+
assumptions: NodeRow[];
|
|
7
|
+
evidence: NodeRow[];
|
|
8
|
+
alternatives: NodeRow[];
|
|
9
|
+
checks: NodeRow[];
|
|
10
|
+
outcome: NodeRow | null;
|
|
11
|
+
artifacts: ArtifactRow[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function explainDecision(
|
|
15
|
+
decisionNodeId: string,
|
|
16
|
+
nodes: NodeRow[],
|
|
17
|
+
edges: EdgeRow[],
|
|
18
|
+
artifacts: ArtifactRow[],
|
|
19
|
+
depth: number
|
|
20
|
+
): Explanation {
|
|
21
|
+
const graph = buildGraph(nodes, edges);
|
|
22
|
+
const nodeById = new Map(nodes.map((node) => [node.node_id, node]));
|
|
23
|
+
const decisionNode = nodeById.get(decisionNodeId);
|
|
24
|
+
if (!decisionNode) {
|
|
25
|
+
throw new Error(`Decision node ${decisionNodeId} not found`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const upstream = new Set<string>();
|
|
29
|
+
const queue: Array<{ id: string; depth: number }> = [{ id: decisionNodeId, depth: 0 }];
|
|
30
|
+
|
|
31
|
+
while (queue.length > 0) {
|
|
32
|
+
const current = queue.shift();
|
|
33
|
+
if (!current) continue;
|
|
34
|
+
if (current.depth >= depth) continue;
|
|
35
|
+
|
|
36
|
+
const inbound = graph.inbound.get(current.id) ?? [];
|
|
37
|
+
for (const edge of inbound) {
|
|
38
|
+
if (upstream.has(edge.from)) continue;
|
|
39
|
+
upstream.add(edge.from);
|
|
40
|
+
queue.push({ id: edge.from, depth: current.depth + 1 });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const assumptions: NodeRow[] = [];
|
|
45
|
+
const evidence: NodeRow[] = [];
|
|
46
|
+
const alternatives: NodeRow[] = [];
|
|
47
|
+
const checks: NodeRow[] = [];
|
|
48
|
+
|
|
49
|
+
for (const id of upstream) {
|
|
50
|
+
const node = nodeById.get(id);
|
|
51
|
+
if (!node) continue;
|
|
52
|
+
switch (node.type) {
|
|
53
|
+
case "Assumption":
|
|
54
|
+
assumptions.push(node);
|
|
55
|
+
break;
|
|
56
|
+
case "Observation":
|
|
57
|
+
evidence.push(node);
|
|
58
|
+
break;
|
|
59
|
+
case "Option":
|
|
60
|
+
alternatives.push(node);
|
|
61
|
+
break;
|
|
62
|
+
case "Verification":
|
|
63
|
+
checks.push(node);
|
|
64
|
+
break;
|
|
65
|
+
default:
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const outcome = findOutcome(decisionNodeId, graph, nodeById, depth);
|
|
71
|
+
|
|
72
|
+
const artifactNodeIds = new Set<string>([
|
|
73
|
+
decisionNodeId,
|
|
74
|
+
...assumptions.map((node) => node.node_id),
|
|
75
|
+
...evidence.map((node) => node.node_id),
|
|
76
|
+
...alternatives.map((node) => node.node_id),
|
|
77
|
+
...checks.map((node) => node.node_id),
|
|
78
|
+
...(outcome ? [outcome.node_id] : []),
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
const linkedArtifacts = artifacts.filter(
|
|
82
|
+
(artifact) => artifact.node_id && artifactNodeIds.has(artifact.node_id)
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
decision_node: decisionNode,
|
|
87
|
+
assumptions,
|
|
88
|
+
evidence,
|
|
89
|
+
alternatives,
|
|
90
|
+
checks,
|
|
91
|
+
outcome,
|
|
92
|
+
artifacts: linkedArtifacts,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function findOutcome(
|
|
97
|
+
decisionNodeId: string,
|
|
98
|
+
graph: ReturnType<typeof buildGraph>,
|
|
99
|
+
nodeById: Map<string, NodeRow>,
|
|
100
|
+
depth: number
|
|
101
|
+
): NodeRow | null {
|
|
102
|
+
const queue: Array<{ id: string; depth: number }> = [{ id: decisionNodeId, depth: 0 }];
|
|
103
|
+
const visited = new Set<string>([decisionNodeId]);
|
|
104
|
+
|
|
105
|
+
while (queue.length > 0) {
|
|
106
|
+
const current = queue.shift();
|
|
107
|
+
if (!current) continue;
|
|
108
|
+
|
|
109
|
+
const node = nodeById.get(current.id);
|
|
110
|
+
if (node && node.type === "Outcome" && current.depth > 0) {
|
|
111
|
+
return node;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (current.depth >= depth) continue;
|
|
115
|
+
|
|
116
|
+
const outbound = graph.out.get(current.id) ?? [];
|
|
117
|
+
for (const edge of outbound) {
|
|
118
|
+
if (visited.has(edge.to)) continue;
|
|
119
|
+
visited.add(edge.to);
|
|
120
|
+
queue.push({ id: edge.to, depth: current.depth + 1 });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return null;
|
|
125
|
+
}
|