@kontourai/flow-agents 0.2.0 → 0.4.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/.github/workflows/release-please.yml +13 -1
- package/.github/workflows/runtime-compat.yml +1 -1
- package/AGENTS.md +8 -1
- package/CHANGELOG.md +41 -0
- package/README.md +38 -19
- package/build/src/cli/flow-kit.js +9 -4
- package/build/src/cli/runtime-adapter.js +9 -5
- package/build/src/cli/telemetry-doctor.js +4 -1
- package/build/src/runtime-adapters.js +34 -0
- package/build/src/tools/build-universal-bundles.js +18 -1
- package/console.telemetry.json +115 -20
- package/docs/_layouts/default.html +2 -0
- package/docs/index.md +8 -0
- package/docs/integrations/index.md +4 -0
- package/docs/integrations/knowledge-kit-live.md +211 -0
- package/docs/kit-authoring-guide.md +169 -0
- package/docs/spec/runtime-hook-surface.md +56 -3
- package/evals/acceptance/run.sh +10 -1
- package/evals/acceptance/test_knowledge_kit_live.sh +221 -0
- package/evals/acceptance/test_pi_harness.sh +15 -0
- package/evals/integration/test_runtime_adapter_activation.sh +113 -1
- package/evals/static/test_universal_bundles.sh +10 -0
- package/integrations/strands/examples/knowledge_kit_live.py +461 -0
- package/integrations/strands/flow_agents_strands/steering.py +54 -1
- package/integrations/strands/tests/test_hooks.py +88 -0
- package/integrations/strands-ts/src/hooks.ts +104 -0
- package/integrations/strands-ts/test/test-steering.ts +159 -0
- package/kits/catalog.json +6 -0
- package/kits/knowledge/adapters/default-store/index.js +902 -0
- package/kits/knowledge/adapters/flow-runner/index.js +1469 -0
- package/kits/knowledge/adapters/flow-runner/telemetry.js +174 -0
- package/kits/knowledge/adapters/similarity-vector/index.js +284 -0
- package/kits/knowledge/docs/README.md +328 -0
- package/kits/knowledge/docs/store-contract.md +650 -0
- package/kits/knowledge/evals/consolidation/suite.test.js +1234 -0
- package/kits/knowledge/evals/contract-suite/suite.test.js +675 -0
- package/kits/knowledge/evals/ingest-compile/suite.test.js +574 -0
- package/kits/knowledge/evals/retirement/suite.test.js +1173 -0
- package/kits/knowledge/evals/similarity-vector/suite.test.js +685 -0
- package/kits/knowledge/evals/synthesis/suite.test.js +916 -0
- package/kits/knowledge/flows/compile.flow.json +60 -0
- package/kits/knowledge/flows/consolidate.flow.json +77 -0
- package/kits/knowledge/flows/ingest.flow.json +60 -0
- package/kits/knowledge/flows/retire.flow.json +77 -0
- package/kits/knowledge/flows/store-contract.flow.json +48 -0
- package/kits/knowledge/flows/synthesize.flow.json +77 -0
- package/kits/knowledge/kit.json +98 -0
- package/package.json +1 -1
- package/src/cli/flow-kit.ts +10 -4
- package/src/cli/runtime-adapter.ts +10 -5
- package/src/cli/telemetry-doctor.ts +4 -1
- package/src/runtime-adapters.ts +35 -0
- package/src/tools/build-universal-bundles.ts +18 -1
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge Kit — Store Contract Suite
|
|
3
|
+
*
|
|
4
|
+
* Parameterized by adapter module. Set KNOWLEDGE_ADAPTER env var to the
|
|
5
|
+
* absolute path of an adapter module, or pass --adapter=<path> as a CLI arg.
|
|
6
|
+
* Defaults to the bundled default-store adapter.
|
|
7
|
+
*
|
|
8
|
+
* Run:
|
|
9
|
+
* node --test kits/knowledge/evals/contract-suite/suite.test.js
|
|
10
|
+
* KNOWLEDGE_ADAPTER=/path/to/my-adapter.js node --test kits/knowledge/evals/contract-suite/suite.test.js
|
|
11
|
+
*
|
|
12
|
+
* Exit 0 = all tests passed. Exit nonzero = failures.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { test, describe, before, after } from "node:test";
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
import * as fs from "node:fs";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
import * as os from "node:os";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Adapter resolution
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const KIT_ROOT = path.resolve(__dirname, "../../../..");
|
|
28
|
+
|
|
29
|
+
function resolveAdapterPath() {
|
|
30
|
+
// 1. CLI flag: --adapter=<path>
|
|
31
|
+
const adapterFlag = process.argv.find((a) => a.startsWith("--adapter="));
|
|
32
|
+
if (adapterFlag) return path.resolve(adapterFlag.slice("--adapter=".length));
|
|
33
|
+
|
|
34
|
+
// 2. Environment variable
|
|
35
|
+
if (process.env.KNOWLEDGE_ADAPTER) return path.resolve(process.env.KNOWLEDGE_ADAPTER);
|
|
36
|
+
|
|
37
|
+
// 3. Default: bundled adapter
|
|
38
|
+
return path.join(KIT_ROOT, "kits/knowledge/adapters/default-store/index.js");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const adapterPath = resolveAdapterPath();
|
|
42
|
+
const adapterModule = await import(adapterPath);
|
|
43
|
+
const AdapterClass = adapterModule.default || adapterModule.DefaultKnowledgeStore;
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Helpers
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
function makeTempDir() {
|
|
50
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "knowledge-contract-suite-"));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function makeStore(dir) {
|
|
54
|
+
return new AdapterClass({ storeRoot: dir });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function assertMissingEvidence(fn, labelHint) {
|
|
58
|
+
return assert.rejects(fn, (err) => {
|
|
59
|
+
assert.equal(
|
|
60
|
+
err.code,
|
|
61
|
+
"MISSING_EVIDENCE",
|
|
62
|
+
`Expected MISSING_EVIDENCE for ${labelHint}; got code=${err.code}: ${err.message}`
|
|
63
|
+
);
|
|
64
|
+
return true;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Suite
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
describe("Knowledge Kit Store Contract Suite", () => {
|
|
73
|
+
// -----------------------------------------------------------------------
|
|
74
|
+
// §1 create — field enforcement
|
|
75
|
+
// -----------------------------------------------------------------------
|
|
76
|
+
describe("create: required field enforcement", () => {
|
|
77
|
+
let dir, store;
|
|
78
|
+
before(() => { dir = makeTempDir(); store = makeStore(dir); });
|
|
79
|
+
after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
80
|
+
|
|
81
|
+
test("rejects missing type", () =>
|
|
82
|
+
assertMissingEvidence(() =>
|
|
83
|
+
store.create({ title: "T", body: "B", category: "test", provenance: { agent: "tester" } }),
|
|
84
|
+
"missing type"
|
|
85
|
+
));
|
|
86
|
+
|
|
87
|
+
test("rejects invalid type", () =>
|
|
88
|
+
assertMissingEvidence(() =>
|
|
89
|
+
store.create({ type: "bogus", title: "T", body: "B", category: "test", provenance: { agent: "tester" } }),
|
|
90
|
+
"invalid type"
|
|
91
|
+
));
|
|
92
|
+
|
|
93
|
+
test("rejects missing title", () =>
|
|
94
|
+
assertMissingEvidence(() =>
|
|
95
|
+
store.create({ type: "raw", body: "B", category: "test", provenance: { agent: "tester" } }),
|
|
96
|
+
"missing title"
|
|
97
|
+
));
|
|
98
|
+
|
|
99
|
+
test("rejects missing body", () =>
|
|
100
|
+
assertMissingEvidence(() =>
|
|
101
|
+
store.create({ type: "raw", title: "T", category: "test", provenance: { agent: "tester" } }),
|
|
102
|
+
"missing body"
|
|
103
|
+
));
|
|
104
|
+
|
|
105
|
+
test("rejects missing category", () =>
|
|
106
|
+
assertMissingEvidence(() =>
|
|
107
|
+
store.create({ type: "raw", title: "T", body: "B", provenance: { agent: "tester" } }),
|
|
108
|
+
"missing category"
|
|
109
|
+
));
|
|
110
|
+
|
|
111
|
+
test("rejects empty category", () =>
|
|
112
|
+
assertMissingEvidence(() =>
|
|
113
|
+
store.create({ type: "raw", title: "T", body: "B", category: "", provenance: { agent: "tester" } }),
|
|
114
|
+
"empty category"
|
|
115
|
+
));
|
|
116
|
+
|
|
117
|
+
test("rejects invalid category segment", () =>
|
|
118
|
+
assertMissingEvidence(() =>
|
|
119
|
+
store.create({ type: "raw", title: "T", body: "B", category: "Bad Cat", provenance: { agent: "tester" } }),
|
|
120
|
+
"invalid category"
|
|
121
|
+
));
|
|
122
|
+
|
|
123
|
+
test("rejects missing provenance.agent", () =>
|
|
124
|
+
assertMissingEvidence(() =>
|
|
125
|
+
store.create({ type: "raw", title: "T", body: "B", category: "test", provenance: {} }),
|
|
126
|
+
"missing provenance.agent"
|
|
127
|
+
));
|
|
128
|
+
|
|
129
|
+
test("rejects missing provenance object entirely", () =>
|
|
130
|
+
assertMissingEvidence(() =>
|
|
131
|
+
store.create({ type: "raw", title: "T", body: "B", category: "test" }),
|
|
132
|
+
"missing provenance"
|
|
133
|
+
));
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// -----------------------------------------------------------------------
|
|
137
|
+
// §2 create — happy path & round-trip (AC3)
|
|
138
|
+
// -----------------------------------------------------------------------
|
|
139
|
+
describe("create: round-trip raw → stored → queried (AC3)", () => {
|
|
140
|
+
let dir, store;
|
|
141
|
+
before(() => { dir = makeTempDir(); store = makeStore(dir); });
|
|
142
|
+
after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
143
|
+
|
|
144
|
+
test("raw record round-trips with category and links intact", async () => {
|
|
145
|
+
const id = await store.create({
|
|
146
|
+
type: "raw",
|
|
147
|
+
title: "My Raw Note",
|
|
148
|
+
body: "Some raw content [[target-abc]]",
|
|
149
|
+
category: "research.notes",
|
|
150
|
+
tags: ["alpha", "beta"],
|
|
151
|
+
provenance: { agent: "test-agent", session_id: "sess-1" },
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
assert.ok(id, "create returns an id");
|
|
155
|
+
|
|
156
|
+
const record = await store.get(id);
|
|
157
|
+
assert.ok(record, "record is retrievable after create");
|
|
158
|
+
assert.equal(record.type, "raw");
|
|
159
|
+
assert.equal(record.title, "My Raw Note");
|
|
160
|
+
assert.equal(record.category, "research.notes");
|
|
161
|
+
assert.deepEqual(record.tags, ["alpha", "beta"]);
|
|
162
|
+
assert.ok(record.created_at, "created_at is set");
|
|
163
|
+
assert.ok(record.updated_at, "updated_at is set");
|
|
164
|
+
assert.equal(record.provenance.agent, "test-agent");
|
|
165
|
+
assert.equal(record.provenance.session_id, "sess-1");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("compiled record round-trips with source link", async () => {
|
|
169
|
+
const rawId = await store.create({
|
|
170
|
+
type: "raw",
|
|
171
|
+
title: "Source Raw",
|
|
172
|
+
body: "raw source",
|
|
173
|
+
category: "research",
|
|
174
|
+
provenance: { agent: "tester" },
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const compiledId = await store.create({
|
|
178
|
+
type: "compiled",
|
|
179
|
+
title: "Compiled from Raw",
|
|
180
|
+
body: "Normalized content",
|
|
181
|
+
category: "research",
|
|
182
|
+
links: [{ target_id: rawId, kind: "source" }],
|
|
183
|
+
provenance: { agent: "tester", source_ids: [rawId] },
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const compiled = await store.get(compiledId);
|
|
187
|
+
assert.ok(compiled, "compiled record retrievable");
|
|
188
|
+
assert.equal(compiled.type, "compiled");
|
|
189
|
+
assert.ok(Array.isArray(compiled.links), "links is array");
|
|
190
|
+
const srcLink = compiled.links.find((l) => l.target_id === rawId && l.kind === "source");
|
|
191
|
+
assert.ok(srcLink, "source link preserved after round-trip");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("concept record round-trips", async () => {
|
|
195
|
+
const cid = await store.create({
|
|
196
|
+
type: "concept",
|
|
197
|
+
title: "Idempotency",
|
|
198
|
+
body: "An operation is idempotent if applying it multiple times yields the same result.",
|
|
199
|
+
category: "engineering.principles",
|
|
200
|
+
provenance: { agent: "tester" },
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const concept = await store.get(cid);
|
|
204
|
+
assert.equal(concept.type, "concept");
|
|
205
|
+
assert.equal(concept.title, "Idempotency");
|
|
206
|
+
assert.equal(concept.category, "engineering.principles");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("all three record types are accepted", async () => {
|
|
210
|
+
for (const type of ["raw", "compiled", "concept"]) {
|
|
211
|
+
const id = await store.create({
|
|
212
|
+
type,
|
|
213
|
+
title: `${type} record`,
|
|
214
|
+
body: `body for ${type}`,
|
|
215
|
+
category: "test",
|
|
216
|
+
provenance: { agent: "tester" },
|
|
217
|
+
});
|
|
218
|
+
assert.ok(id, `${type} record created`);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// -----------------------------------------------------------------------
|
|
224
|
+
// §3 links + graph index
|
|
225
|
+
// -----------------------------------------------------------------------
|
|
226
|
+
describe("links: graph index consistency", () => {
|
|
227
|
+
let dir, store;
|
|
228
|
+
before(() => { dir = makeTempDir(); store = makeStore(dir); });
|
|
229
|
+
after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
230
|
+
|
|
231
|
+
test("getLinks returns forward links after create", async () => {
|
|
232
|
+
const targetId = await store.create({
|
|
233
|
+
type: "concept",
|
|
234
|
+
title: "Target Concept",
|
|
235
|
+
body: "target",
|
|
236
|
+
category: "test",
|
|
237
|
+
provenance: { agent: "tester" },
|
|
238
|
+
});
|
|
239
|
+
const sourceId = await store.create({
|
|
240
|
+
type: "raw",
|
|
241
|
+
title: "Source",
|
|
242
|
+
body: "body",
|
|
243
|
+
category: "test",
|
|
244
|
+
links: [{ target_id: targetId, kind: "related" }],
|
|
245
|
+
provenance: { agent: "tester" },
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const { forward, reverse } = await store.getLinks(sourceId);
|
|
249
|
+
assert.ok(forward.some((l) => l.target_id === targetId && l.kind === "related"),
|
|
250
|
+
"forward link present in graph index");
|
|
251
|
+
|
|
252
|
+
const { reverse: targetReverse } = await store.getLinks(targetId);
|
|
253
|
+
assert.ok(targetReverse.some((l) => l.source_id === sourceId),
|
|
254
|
+
"reverse link present in graph index");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("link op adds links and updates graph index", async () => {
|
|
258
|
+
const aId = await store.create({ type: "raw", title: "A", body: "a", category: "test", provenance: { agent: "tester" } });
|
|
259
|
+
const bId = await store.create({ type: "raw", title: "B", body: "b", category: "test", provenance: { agent: "tester" } });
|
|
260
|
+
|
|
261
|
+
await store.link(aId, [{ target_id: bId, kind: "related" }], { agent: "tester" });
|
|
262
|
+
|
|
263
|
+
const { forward } = await store.getLinks(aId);
|
|
264
|
+
assert.ok(forward.some((l) => l.target_id === bId && l.kind === "related"),
|
|
265
|
+
"link op reflected in graph index");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("link op is idempotent", async () => {
|
|
269
|
+
const aId = await store.create({ type: "raw", title: "Idem A", body: "a", category: "test", provenance: { agent: "tester" } });
|
|
270
|
+
const bId = await store.create({ type: "raw", title: "Idem B", body: "b", category: "test", provenance: { agent: "tester" } });
|
|
271
|
+
|
|
272
|
+
await store.link(aId, [{ target_id: bId, kind: "related" }], { agent: "tester" });
|
|
273
|
+
await store.link(aId, [{ target_id: bId, kind: "related" }], { agent: "tester" });
|
|
274
|
+
|
|
275
|
+
const { forward } = await store.getLinks(aId);
|
|
276
|
+
const dupes = forward.filter((l) => l.target_id === bId && l.kind === "related");
|
|
277
|
+
assert.equal(dupes.length, 1, "idempotent link: no duplicates");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("wikilinks in body are indexed", async () => {
|
|
281
|
+
const conceptId = await store.create({
|
|
282
|
+
type: "concept",
|
|
283
|
+
title: "WikiTarget",
|
|
284
|
+
body: "a concept",
|
|
285
|
+
category: "test",
|
|
286
|
+
provenance: { agent: "tester" },
|
|
287
|
+
});
|
|
288
|
+
const srcId = await store.create({
|
|
289
|
+
type: "raw",
|
|
290
|
+
title: "WikiSource",
|
|
291
|
+
body: `Refers to [[${conceptId}|Wiki Target]].`,
|
|
292
|
+
category: "test",
|
|
293
|
+
provenance: { agent: "tester" },
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const record = await store.get(srcId);
|
|
297
|
+
assert.ok(
|
|
298
|
+
record.links.some((l) => l.target_id === conceptId),
|
|
299
|
+
"wikilink extracted and stored in links array"
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
const { forward } = await store.getLinks(srcId);
|
|
303
|
+
assert.ok(
|
|
304
|
+
forward.some((l) => l.target_id === conceptId),
|
|
305
|
+
"wikilink reflected in graph index"
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// -----------------------------------------------------------------------
|
|
311
|
+
// §4 link op — required evidence enforcement
|
|
312
|
+
// -----------------------------------------------------------------------
|
|
313
|
+
describe("link: required evidence enforcement", () => {
|
|
314
|
+
let dir, store;
|
|
315
|
+
before(() => { dir = makeTempDir(); store = makeStore(dir); });
|
|
316
|
+
after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
317
|
+
|
|
318
|
+
test("rejects missing agent", async () => {
|
|
319
|
+
const aId = await store.create({ type: "raw", title: "A", body: "a", category: "test", provenance: { agent: "tester" } });
|
|
320
|
+
const bId = await store.create({ type: "raw", title: "B", body: "b", category: "test", provenance: { agent: "tester" } });
|
|
321
|
+
await assertMissingEvidence(
|
|
322
|
+
() => store.link(aId, [{ target_id: bId, kind: "related" }], {}),
|
|
323
|
+
"link missing agent"
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("rejects empty links array", async () => {
|
|
328
|
+
const aId = await store.create({ type: "raw", title: "A2", body: "a", category: "test", provenance: { agent: "tester" } });
|
|
329
|
+
await assertMissingEvidence(
|
|
330
|
+
() => store.link(aId, [], { agent: "tester" }),
|
|
331
|
+
"link empty array"
|
|
332
|
+
);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("rejects nonexistent target_id", async () => {
|
|
336
|
+
const aId = await store.create({ type: "raw", title: "A3", body: "a", category: "test", provenance: { agent: "tester" } });
|
|
337
|
+
await assert.rejects(
|
|
338
|
+
() => store.link(aId, [{ target_id: "nonexistent-id-xyz", kind: "related" }], { agent: "tester" }),
|
|
339
|
+
{ code: "NOT_FOUND" }
|
|
340
|
+
);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// -----------------------------------------------------------------------
|
|
345
|
+
// §5 update — required evidence enforcement
|
|
346
|
+
// -----------------------------------------------------------------------
|
|
347
|
+
describe("update: required evidence enforcement", () => {
|
|
348
|
+
let dir, store;
|
|
349
|
+
before(() => { dir = makeTempDir(); store = makeStore(dir); });
|
|
350
|
+
after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
351
|
+
|
|
352
|
+
test("rejects missing agent", async () => {
|
|
353
|
+
const id = await store.create({ type: "raw", title: "T", body: "B", category: "test", provenance: { agent: "tester" } });
|
|
354
|
+
await assertMissingEvidence(
|
|
355
|
+
() => store.update(id, { title: "New Title" }, {}),
|
|
356
|
+
"update missing agent"
|
|
357
|
+
);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("rejects no-op update (no mutable fields supplied)", async () => {
|
|
361
|
+
const id = await store.create({ type: "raw", title: "T", body: "B", category: "test", provenance: { agent: "tester" } });
|
|
362
|
+
await assertMissingEvidence(
|
|
363
|
+
() => store.update(id, {}, { agent: "tester" }),
|
|
364
|
+
"update no fields"
|
|
365
|
+
);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("updates fields and refreshes updated_at", async () => {
|
|
369
|
+
const id = await store.create({ type: "raw", title: "Original", body: "B", category: "test", provenance: { agent: "tester" } });
|
|
370
|
+
const before = await store.get(id);
|
|
371
|
+
await new Promise((r) => setTimeout(r, 5)); // ensure different timestamp
|
|
372
|
+
await store.update(id, { title: "Revised" }, { agent: "tester" });
|
|
373
|
+
const after = await store.get(id);
|
|
374
|
+
assert.equal(after.title, "Revised");
|
|
375
|
+
assert.ok(after.updated_at >= before.updated_at, "updated_at refreshed");
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("update with new links updates graph index", async () => {
|
|
379
|
+
const aId = await store.create({ type: "raw", title: "A", body: "a", category: "test", provenance: { agent: "tester" } });
|
|
380
|
+
const bId = await store.create({ type: "raw", title: "B", body: "b", category: "test", provenance: { agent: "tester" } });
|
|
381
|
+
await store.update(aId, { links: [{ target_id: bId, kind: "refines" }] }, { agent: "tester" });
|
|
382
|
+
const { forward } = await store.getLinks(aId);
|
|
383
|
+
assert.ok(forward.some((l) => l.target_id === bId && l.kind === "refines"),
|
|
384
|
+
"updated links reflected in graph index");
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// -----------------------------------------------------------------------
|
|
389
|
+
// §6 propose — required evidence enforcement
|
|
390
|
+
// -----------------------------------------------------------------------
|
|
391
|
+
describe("propose: required evidence enforcement", () => {
|
|
392
|
+
let dir, store;
|
|
393
|
+
before(() => { dir = makeTempDir(); store = makeStore(dir); });
|
|
394
|
+
after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
395
|
+
|
|
396
|
+
test("rejects missing agent", async () => {
|
|
397
|
+
const cid = await store.create({ type: "concept", title: "C", body: "c", category: "test", provenance: { agent: "tester" } });
|
|
398
|
+
const pid = await store.create({ type: "raw", title: "P", body: "p", category: "test", provenance: { agent: "tester" } });
|
|
399
|
+
await assertMissingEvidence(
|
|
400
|
+
() => store.propose(cid, pid, { proposal: "change body" }),
|
|
401
|
+
"propose missing agent"
|
|
402
|
+
);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("rejects missing proposal text", async () => {
|
|
406
|
+
const cid = await store.create({ type: "concept", title: "C2", body: "c", category: "test", provenance: { agent: "tester" } });
|
|
407
|
+
const pid = await store.create({ type: "raw", title: "P2", body: "p", category: "test", provenance: { agent: "tester" } });
|
|
408
|
+
await assertMissingEvidence(
|
|
409
|
+
() => store.propose(cid, pid, { agent: "tester" }),
|
|
410
|
+
"propose missing proposal"
|
|
411
|
+
);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("propose accepts any record type as target (Addendum B: retire flow needs non-concept targets)", async () => {
|
|
415
|
+
// Addendum B (S7) extends propose to accept any record type as the target,
|
|
416
|
+
// enabling the retire flow to attach proposals to compiled/raw/snapshot records.
|
|
417
|
+
const rawTarget = await store.create({ type: "raw", title: "NC", body: "nc", category: "test", provenance: { agent: "tester" } });
|
|
418
|
+
const pid = await store.create({ type: "raw", title: "P3", body: "p", category: "test", provenance: { agent: "tester" } });
|
|
419
|
+
// Should NOT throw — all record types are valid proposal targets
|
|
420
|
+
await store.propose(rawTarget, pid, { agent: "tester", proposal: "retirement proposal" });
|
|
421
|
+
const { forward } = await store.getLinks(pid);
|
|
422
|
+
assert.ok(
|
|
423
|
+
forward.some((l) => l.target_id === rawTarget && l.kind === "proposes"),
|
|
424
|
+
"propose on raw record creates proposes link (Addendum B extension)"
|
|
425
|
+
);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("happy path: propose creates proposes link", async () => {
|
|
429
|
+
const cid = await store.create({ type: "concept", title: "My Concept", body: "original", category: "test", provenance: { agent: "tester" } });
|
|
430
|
+
const pid = await store.create({ type: "raw", title: "Proposer", body: "I propose", category: "test", provenance: { agent: "tester" } });
|
|
431
|
+
await store.propose(cid, pid, { agent: "tester", proposal: "Extend definition to cover edge case X." });
|
|
432
|
+
|
|
433
|
+
const { forward } = await store.getLinks(pid);
|
|
434
|
+
assert.ok(
|
|
435
|
+
forward.some((l) => l.target_id === cid && l.kind === "proposes"),
|
|
436
|
+
"proposes link created in graph index"
|
|
437
|
+
);
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// -----------------------------------------------------------------------
|
|
442
|
+
// §7 apply — required evidence enforcement
|
|
443
|
+
// -----------------------------------------------------------------------
|
|
444
|
+
describe("apply: required evidence enforcement", () => {
|
|
445
|
+
let dir, store;
|
|
446
|
+
let conceptId, proposerId;
|
|
447
|
+
before(async () => {
|
|
448
|
+
dir = makeTempDir();
|
|
449
|
+
store = makeStore(dir);
|
|
450
|
+
conceptId = await store.create({ type: "concept", title: "Applyable", body: "v1", category: "test", provenance: { agent: "tester" } });
|
|
451
|
+
proposerId = await store.create({ type: "raw", title: "Proposer", body: "p", category: "test", provenance: { agent: "tester" } });
|
|
452
|
+
await store.propose(conceptId, proposerId, { agent: "tester", proposal: "Update to v2" });
|
|
453
|
+
});
|
|
454
|
+
after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
455
|
+
|
|
456
|
+
test("rejects missing agent", () =>
|
|
457
|
+
assertMissingEvidence(
|
|
458
|
+
() => store.apply(conceptId, proposerId, { new_body: "v2", rationale: "better" }),
|
|
459
|
+
"apply missing agent"
|
|
460
|
+
));
|
|
461
|
+
|
|
462
|
+
test("rejects missing new_body", () =>
|
|
463
|
+
assertMissingEvidence(
|
|
464
|
+
() => store.apply(conceptId, proposerId, { agent: "tester", rationale: "better" }),
|
|
465
|
+
"apply missing new_body"
|
|
466
|
+
));
|
|
467
|
+
|
|
468
|
+
test("rejects missing rationale", () =>
|
|
469
|
+
assertMissingEvidence(
|
|
470
|
+
() => store.apply(conceptId, proposerId, { agent: "tester", new_body: "v2" }),
|
|
471
|
+
"apply missing rationale"
|
|
472
|
+
));
|
|
473
|
+
|
|
474
|
+
test("rejects apply when no proposes link exists", async () => {
|
|
475
|
+
const otherId = await store.create({ type: "raw", title: "Other", body: "o", category: "test", provenance: { agent: "tester" } });
|
|
476
|
+
await assertMissingEvidence(
|
|
477
|
+
() => store.apply(conceptId, otherId, { agent: "tester", new_body: "v2", rationale: "r" }),
|
|
478
|
+
"apply no proposes link"
|
|
479
|
+
);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test("happy path: apply updates concept body", async () => {
|
|
483
|
+
await store.apply(conceptId, proposerId, { agent: "tester", new_body: "v2 body", rationale: "more precise" });
|
|
484
|
+
const concept = await store.get(conceptId);
|
|
485
|
+
assert.equal(concept.body, "v2 body", "concept body updated after apply");
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// -----------------------------------------------------------------------
|
|
490
|
+
// §8 reject — required evidence enforcement
|
|
491
|
+
// -----------------------------------------------------------------------
|
|
492
|
+
describe("reject: required evidence enforcement", () => {
|
|
493
|
+
let dir, store;
|
|
494
|
+
let conceptId, proposerId;
|
|
495
|
+
before(async () => {
|
|
496
|
+
dir = makeTempDir();
|
|
497
|
+
store = makeStore(dir);
|
|
498
|
+
conceptId = await store.create({ type: "concept", title: "Rejectable", body: "stable", category: "test", provenance: { agent: "tester" } });
|
|
499
|
+
proposerId = await store.create({ type: "raw", title: "Proposer", body: "p", category: "test", provenance: { agent: "tester" } });
|
|
500
|
+
await store.propose(conceptId, proposerId, { agent: "tester", proposal: "Controversial change" });
|
|
501
|
+
});
|
|
502
|
+
after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
503
|
+
|
|
504
|
+
test("rejects missing agent", () =>
|
|
505
|
+
assertMissingEvidence(
|
|
506
|
+
() => store.reject(conceptId, proposerId, { reason: "not suitable" }),
|
|
507
|
+
"reject missing agent"
|
|
508
|
+
));
|
|
509
|
+
|
|
510
|
+
test("rejects missing reason", () =>
|
|
511
|
+
assertMissingEvidence(
|
|
512
|
+
() => store.reject(conceptId, proposerId, { agent: "tester" }),
|
|
513
|
+
"reject missing reason"
|
|
514
|
+
));
|
|
515
|
+
|
|
516
|
+
test("rejects reject when no proposes link exists", async () => {
|
|
517
|
+
const otherId = await store.create({ type: "raw", title: "Other", body: "o", category: "test", provenance: { agent: "tester" } });
|
|
518
|
+
await assertMissingEvidence(
|
|
519
|
+
() => store.reject(conceptId, otherId, { agent: "tester", reason: "r" }),
|
|
520
|
+
"reject no proposes link"
|
|
521
|
+
);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test("happy path: reject does not mutate concept body", async () => {
|
|
525
|
+
const bodyBefore = (await store.get(conceptId)).body;
|
|
526
|
+
await store.reject(conceptId, proposerId, { agent: "tester", reason: "Not aligned with goals." });
|
|
527
|
+
const concept = await store.get(conceptId);
|
|
528
|
+
assert.equal(concept.body, bodyBefore, "concept body unchanged after reject");
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// -----------------------------------------------------------------------
|
|
533
|
+
// §9 listByCategory
|
|
534
|
+
// -----------------------------------------------------------------------
|
|
535
|
+
describe("listByCategory", () => {
|
|
536
|
+
let dir, store;
|
|
537
|
+
before(async () => {
|
|
538
|
+
dir = makeTempDir();
|
|
539
|
+
store = makeStore(dir);
|
|
540
|
+
await store.create({ type: "raw", title: "A", body: "a", category: "eng.api", provenance: { agent: "tester" } });
|
|
541
|
+
await store.create({ type: "raw", title: "B", body: "b", category: "eng.api.rest", provenance: { agent: "tester" } });
|
|
542
|
+
await store.create({ type: "raw", title: "C", body: "c", category: "eng.db", provenance: { agent: "tester" } });
|
|
543
|
+
await store.create({ type: "raw", title: "D", body: "d", category: "design", provenance: { agent: "tester" } });
|
|
544
|
+
});
|
|
545
|
+
after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
546
|
+
|
|
547
|
+
test("exact match returns only matching records", async () => {
|
|
548
|
+
const results = await store.listByCategory("eng.api");
|
|
549
|
+
assert.equal(results.length, 1);
|
|
550
|
+
assert.equal(results[0].title, "A");
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
test("prefix match returns record and all descendants", async () => {
|
|
554
|
+
const results = await store.listByCategory("eng", { prefix: true });
|
|
555
|
+
assert.equal(results.length, 3, "all eng.* records returned");
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
test("no match returns empty array", async () => {
|
|
559
|
+
const results = await store.listByCategory("nonexistent.cat");
|
|
560
|
+
assert.equal(results.length, 0);
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
// -----------------------------------------------------------------------
|
|
565
|
+
// §10 listByType
|
|
566
|
+
// -----------------------------------------------------------------------
|
|
567
|
+
describe("listByType", () => {
|
|
568
|
+
let dir, store;
|
|
569
|
+
before(async () => {
|
|
570
|
+
dir = makeTempDir();
|
|
571
|
+
store = makeStore(dir);
|
|
572
|
+
await store.create({ type: "raw", title: "R1", body: "r", category: "test", provenance: { agent: "tester" } });
|
|
573
|
+
await store.create({ type: "raw", title: "R2", body: "r", category: "test", provenance: { agent: "tester" } });
|
|
574
|
+
await store.create({ type: "compiled", title: "C1", body: "c", category: "test", provenance: { agent: "tester" } });
|
|
575
|
+
await store.create({ type: "concept", title: "CN1", body: "cn", category: "test", provenance: { agent: "tester" } });
|
|
576
|
+
});
|
|
577
|
+
after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
578
|
+
|
|
579
|
+
test("listByType raw returns only raw records", async () => {
|
|
580
|
+
const results = await store.listByType("raw");
|
|
581
|
+
assert.ok(results.length >= 2, "at least 2 raw records");
|
|
582
|
+
assert.ok(results.every((r) => r.type === "raw"), "all returned are raw");
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
test("listByType compiled returns only compiled records", async () => {
|
|
586
|
+
const results = await store.listByType("compiled");
|
|
587
|
+
assert.ok(results.every((r) => r.type === "compiled"));
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
test("listByType concept returns only concept records", async () => {
|
|
591
|
+
const results = await store.listByType("concept");
|
|
592
|
+
assert.ok(results.every((r) => r.type === "concept"));
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// -----------------------------------------------------------------------
|
|
597
|
+
// §11 mutation log
|
|
598
|
+
// -----------------------------------------------------------------------
|
|
599
|
+
describe("mutation log", () => {
|
|
600
|
+
let dir, store;
|
|
601
|
+
before(() => { dir = makeTempDir(); store = makeStore(dir); });
|
|
602
|
+
after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
603
|
+
|
|
604
|
+
test("update appends mutation log entry", async () => {
|
|
605
|
+
const id = await store.create({ type: "raw", title: "T", body: "B", category: "test", provenance: { agent: "tester" } });
|
|
606
|
+
await store.update(id, { title: "T2" }, { agent: "editor", note: "Fixed title" });
|
|
607
|
+
const record = await store.get(id);
|
|
608
|
+
assert.ok(Array.isArray(record.mutation_log), "mutation_log is array");
|
|
609
|
+
const entry = record.mutation_log.find((e) => e.op === "update");
|
|
610
|
+
assert.ok(entry, "update entry in mutation log");
|
|
611
|
+
assert.equal(entry.agent, "editor");
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
test("propose appends mutation log entry on concept", async () => {
|
|
615
|
+
const cid = await store.create({ type: "concept", title: "ML Concept", body: "body", category: "test", provenance: { agent: "tester" } });
|
|
616
|
+
const pid = await store.create({ type: "raw", title: "ML Proposer", body: "p", category: "test", provenance: { agent: "tester" } });
|
|
617
|
+
await store.propose(cid, pid, { agent: "tester", proposal: "Extend" });
|
|
618
|
+
const concept = await store.get(cid);
|
|
619
|
+
const entry = concept.mutation_log.find((e) => e.op === "propose");
|
|
620
|
+
assert.ok(entry, "propose entry in concept mutation log");
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
// -----------------------------------------------------------------------
|
|
625
|
+
// §12 get returns null for missing record
|
|
626
|
+
// -----------------------------------------------------------------------
|
|
627
|
+
describe("get: missing record", () => {
|
|
628
|
+
let dir, store;
|
|
629
|
+
before(() => { dir = makeTempDir(); store = makeStore(dir); });
|
|
630
|
+
after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
631
|
+
|
|
632
|
+
test("get returns null for nonexistent id", async () => {
|
|
633
|
+
const result = await store.get("definitely-not-a-real-id-00000");
|
|
634
|
+
assert.equal(result, null);
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
// -----------------------------------------------------------------------
|
|
639
|
+
// §13 graph index file consistency
|
|
640
|
+
// -----------------------------------------------------------------------
|
|
641
|
+
describe("graph index file consistency", () => {
|
|
642
|
+
let dir, store;
|
|
643
|
+
before(() => { dir = makeTempDir(); store = makeStore(dir); });
|
|
644
|
+
after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
645
|
+
|
|
646
|
+
test("graph-index.json is valid JSON after operations", async () => {
|
|
647
|
+
const aId = await store.create({ type: "raw", title: "GA", body: "a", category: "test", provenance: { agent: "tester" } });
|
|
648
|
+
const bId = await store.create({ type: "concept", title: "GB", body: "b", category: "test", provenance: { agent: "tester" } });
|
|
649
|
+
await store.link(aId, [{ target_id: bId, kind: "related" }], { agent: "tester" });
|
|
650
|
+
|
|
651
|
+
const graphPath = path.join(dir, "graph-index.json");
|
|
652
|
+
assert.ok(fs.existsSync(graphPath), "graph-index.json exists");
|
|
653
|
+
const raw = fs.readFileSync(graphPath, "utf8");
|
|
654
|
+
let graph;
|
|
655
|
+
assert.doesNotThrow(() => { graph = JSON.parse(raw); }, "graph-index.json is valid JSON");
|
|
656
|
+
assert.equal(graph.schema_version, "1.0", "graph has correct schema_version");
|
|
657
|
+
assert.ok(graph.forward, "graph has forward index");
|
|
658
|
+
assert.ok(graph.reverse, "graph has reverse index");
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
test("graph index forward and reverse are consistent", async () => {
|
|
662
|
+
const xId = await store.create({ type: "raw", title: "GX", body: "x", category: "test", provenance: { agent: "tester" } });
|
|
663
|
+
const yId = await store.create({ type: "raw", title: "GY", body: "y", category: "test", provenance: { agent: "tester" } });
|
|
664
|
+
await store.link(xId, [{ target_id: yId, kind: "refines" }], { agent: "tester" });
|
|
665
|
+
|
|
666
|
+
const graphPath = path.join(dir, "graph-index.json");
|
|
667
|
+
const graph = JSON.parse(fs.readFileSync(graphPath, "utf8"));
|
|
668
|
+
|
|
669
|
+
const fwd = (graph.forward[xId] || []).some((l) => l.target_id === yId && l.kind === "refines");
|
|
670
|
+
const rev = (graph.reverse[yId] || []).some((l) => l.source_id === xId && l.kind === "refines");
|
|
671
|
+
assert.ok(fwd, "forward index has the link");
|
|
672
|
+
assert.ok(rev, "reverse index has the backlink");
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
});
|