@kontourai/flow-agents 0.3.0 → 1.0.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.
Files changed (62) hide show
  1. package/.github/workflows/kit-gates-demo.yml +171 -0
  2. package/.github/workflows/release-please.yml +13 -1
  3. package/AGENTS.md +8 -1
  4. package/CHANGELOG.md +53 -0
  5. package/CONTEXT.md +1 -1
  6. package/README.md +13 -2
  7. package/build/src/cli/flow-kit.js +41 -2
  8. package/build/src/flow-kit/validate.js +98 -0
  9. package/build/src/tools/validate-source-tree.js +2 -1
  10. package/context/scripts/hooks/config-protection.js +217 -15
  11. package/docs/fixture-ownership.md +1 -0
  12. package/docs/index.md +9 -1
  13. package/docs/kit-authoring-guide.md +126 -0
  14. package/docs/knowledge-kit.md +69 -0
  15. package/docs/vision.md +22 -0
  16. package/evals/fixtures/kit-conformance-levels/k0-flows-only/flows/review.flow.json +26 -0
  17. package/evals/fixtures/kit-conformance-levels/k0-flows-only/kit.json +13 -0
  18. package/evals/fixtures/kit-conformance-levels/k1-agent-extension/docs/README.md +3 -0
  19. package/evals/fixtures/kit-conformance-levels/k1-agent-extension/flows/build.flow.json +26 -0
  20. package/evals/fixtures/kit-conformance-levels/k1-agent-extension/kit.json +20 -0
  21. package/evals/fixtures/kit-conformance-levels/k2-with-evals/docs/README.md +3 -0
  22. package/evals/fixtures/kit-conformance-levels/k2-with-evals/eval-suites/contract-suite/suite.test.js +1 -0
  23. package/evals/fixtures/kit-conformance-levels/k2-with-evals/flows/synthesize.flow.json +26 -0
  24. package/evals/fixtures/kit-conformance-levels/k2-with-evals/kit.json +27 -0
  25. package/evals/fixtures/kit-conformance-levels/third-party-extension/flows/review.flow.json +26 -0
  26. package/evals/fixtures/kit-conformance-levels/third-party-extension/kit.json +19 -0
  27. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  28. package/evals/integration/test_hook_category_behaviors.sh +51 -0
  29. package/evals/integration/test_kit_conformance_levels.sh +209 -0
  30. package/evals/run.sh +2 -0
  31. package/evals/static/test_universal_bundles.sh +10 -0
  32. package/kits/catalog.json +6 -0
  33. package/kits/knowledge/adapters/default-store/index.js +95 -14
  34. package/kits/knowledge/adapters/flow-runner/entity-extractor.js +194 -0
  35. package/kits/knowledge/adapters/flow-runner/index.js +639 -0
  36. package/kits/knowledge/adapters/obsidian-store/README.md +141 -0
  37. package/kits/knowledge/adapters/obsidian-store/demo.js +181 -0
  38. package/kits/knowledge/adapters/obsidian-store/index.js +868 -0
  39. package/kits/knowledge/adapters/shared/codec.js +325 -0
  40. package/kits/knowledge/adapters/similarity-vector/index.js +284 -0
  41. package/kits/knowledge/docs/README.md +193 -0
  42. package/kits/knowledge/docs/store-contract.md +196 -0
  43. package/kits/knowledge/evals/contract-suite/suite.test.js +10 -5
  44. package/kits/knowledge/evals/entities/demo-acme.js +125 -0
  45. package/kits/knowledge/evals/entities/suite.test.js +722 -0
  46. package/kits/knowledge/evals/retirement/suite.test.js +1173 -0
  47. package/kits/knowledge/evals/similarity-vector/suite.test.js +685 -0
  48. package/kits/knowledge/evals/synthesis/suite.test.js +10 -3
  49. package/kits/knowledge/flows/retire.flow.json +77 -0
  50. package/kits/knowledge/kit.json +31 -1
  51. package/kits/release-evidence/fixtures/claims/README.md +14 -0
  52. package/kits/release-evidence/fixtures/claims/fail-rejected-release.trust.json +22 -0
  53. package/kits/release-evidence/fixtures/claims/pass-trusted-release.trust.json +22 -0
  54. package/kits/release-evidence/flows/release-evidence.flow.json +38 -0
  55. package/kits/release-evidence/kit.json +13 -0
  56. package/package.json +1 -1
  57. package/packaging/conformance/fixtures/config-protection--allow-no-verify-in-string.json +20 -0
  58. package/packaging/conformance/fixtures/config-protection--block-git-no-verify.json +23 -0
  59. package/scripts/hooks/config-protection.js +217 -15
  60. package/src/cli/flow-kit.ts +40 -2
  61. package/src/flow-kit/validate.ts +127 -0
  62. package/src/tools/validate-source-tree.ts +2 -1
@@ -24,6 +24,15 @@ See [`store-contract.md`](store-contract.md) for the full specification. Quick r
24
24
  | `raw` | Unprocessed source material — excerpts, transcripts, URLs with notes. |
25
25
  | `compiled` | Normalized, editor-reviewed distillations of raw records. |
26
26
  | `concept` | Named ideas or principles that other records reference. |
27
+ | `snapshot` | Bounded decision summary for a topic (Addendum A). |
28
+
29
+ **Record status lifecycle**
30
+
31
+ | Status | Meaning | Default |
32
+ |---|---|---|
33
+ | `active` | Live, part of the working set. | Yes (records without status field are treated as active). |
34
+ | `implemented` | Decision was shipped; transitional state before archival. | No |
35
+ | `retired` | Excluded from default working-set queries; history preserved. | No |
27
36
 
28
37
  **Mutation operations**
29
38
 
@@ -35,6 +44,8 @@ See [`store-contract.md`](store-contract.md) for the full specification. Quick r
35
44
  | `propose` | `agent`, `proposal` (non-empty) |
36
45
  | `apply` | `agent`, `new_body` (non-empty), `rationale` (non-empty) |
37
46
  | `reject` | `agent`, `reason` (non-empty) |
47
+ | `supersede` | `agent`, `rationale` (non-empty), non-empty `supersededIds` array |
48
+ | `retire` | `agent`, `rationale` (non-empty), `implementedByRef` (when `targetStatus="implemented"`) |
38
49
 
39
50
  Every mutation throws with `error.code === "MISSING_EVIDENCE"` when required evidence is absent.
40
51
 
@@ -127,9 +138,191 @@ and adapter infrastructure remain the foundation.
127
138
 
128
139
  ---
129
140
 
141
+ ## Decision Lifecycle — Retiring Records (S7)
142
+
143
+ Implemented or obsolete records can be retired from the working set via the `knowledge.retire`
144
+ flow. Retirement is **non-destructive**: the record body, links, and creation provenance remain
145
+ intact; the record is simply excluded from the default working set.
146
+
147
+ ### Status transitions
148
+
149
+ | From | To | Evidence required |
150
+ |---|---|---|
151
+ | `active` | `implemented` | `rationale` (non-empty) + `implementedByRef` (non-empty ref to implementing artifact) |
152
+ | `active` | `retired` | `rationale` (non-empty) |
153
+ | `implemented` | `retired` | `rationale` (non-empty) |
154
+ | `retired` | *(any)* | Invalid — `retired` is terminal |
155
+
156
+ ### Working-set exclusion
157
+
158
+ Retired records are excluded from:
159
+
160
+ - `listByType(type)` — default query
161
+ - `listByCategory(category, options)` — default query
162
+ - `defaultSimilarityDetector` — default cluster candidates
163
+ - `createVectorSimilarityDetector` — vector cluster candidates
164
+
165
+ Add `{ includeRetired: true }` to any query to restore retired records.
166
+
167
+ `get(id)` **always** returns the full record regardless of status.
168
+
169
+ ### Using the retire flow
170
+
171
+ ```js
172
+ import { KnowledgeFlowRunner } from './adapters/flow-runner/index.js';
173
+
174
+ const runner = new KnowledgeFlowRunner({ store, workspace });
175
+
176
+ // Retire a compiled decision record that was implemented
177
+ const result = await runner.retire(compiledId, {
178
+ targetStatus: 'implemented',
179
+ rationale: 'REST API shipped in v1.0 (PR #42).',
180
+ implementedByRef: 'https://github.com/org/repo/pull/42',
181
+ decision: 'apply',
182
+ });
183
+
184
+ // Retire an obsolete concept record
185
+ await runner.retire(conceptId, {
186
+ targetStatus: 'retired',
187
+ rationale: 'Superseded by new architecture decision in ADR-007.',
188
+ decision: 'apply',
189
+ });
190
+
191
+ // Reject a retirement proposal (status unchanged)
192
+ await runner.retire(recordId, {
193
+ targetStatus: 'retired',
194
+ rationale: 'Proposing retirement.',
195
+ decision: 'reject',
196
+ rejectReason: 'Still needed for reference.',
197
+ });
198
+ ```
199
+
200
+ ### Accessing retired records with provenance
201
+
202
+ ```js
203
+ // Always works — returns full record including retirement evidence
204
+ const record = await store.get(retiredId);
205
+ console.log(record.status); // "retired"
206
+ console.log(record.mutation_log); // includes retire entry with rationale
207
+
208
+ // Query all retired records of a type
209
+ const allCompiled = await store.listByType('compiled', { includeRetired: true });
210
+
211
+ // Get history from snapshot provenance
212
+ const snapshot = await store.get(snapshotId);
213
+ for (const srcId of snapshot.provenance.source_ids) {
214
+ const src = await store.get(srcId); // works even if src is retired
215
+ console.log(src.id, src.status);
216
+ }
217
+ ```
218
+
219
+ ---
220
+
130
221
  ## Non-Goals (this iteration)
131
222
 
132
223
  - Vector/semantic retrieval (parked as I10)
133
224
  - Multi-user concurrency
134
225
  - Store migrations
135
226
  - Personal-KB import (parked as I11)
227
+
228
+ ---
229
+
230
+ ## Similarity Detectors
231
+
232
+ The `synthesize` and `consolidate` flows accept a pluggable `similarityDetector` option. A
233
+ detector has the signature:
234
+
235
+ ```js
236
+ async (concept: Record, candidates: Record[], store: KnowledgeStoreAdapter) => string[]
237
+ ```
238
+
239
+ It receives the target concept, all compiled candidates, and the store; it returns the IDs of
240
+ candidates that are similar enough to form a cluster. The `KnowledgeFlowRunner` uses the cluster
241
+ as its evidence base — an empty cluster throws `MISSING_EVIDENCE` at the detect-cluster gate.
242
+
243
+ ### Choosing a detector
244
+
245
+ | Detector | Best for | Tradeoff |
246
+ |---|---|---|
247
+ | `defaultSimilarityDetector` (built-in) | Fast, zero-config. Works well when records share a structured category taxonomy and inter-record wikilinks. | Relies on category prefixes and link-overlap (Jaccard ≥ 0.10). Misses semantic similarity across category boundaries. |
248
+ | `createVectorSimilarityDetector` | Semantic clustering. Finds similar records regardless of how they were categorised. | Requires an embedding backend (ollama by default). Adds latency proportional to cluster size. |
249
+
250
+ ### Vector detector — ollama embedding
251
+
252
+ The vector adapter lives at `adapters/similarity-vector/index.js`. It is zero-dependency and
253
+ calls ollama's `/api/embed` endpoint via the built-in `fetch`.
254
+
255
+ ```js
256
+ import { createVectorSimilarityDetector } from './adapters/similarity-vector/index.js';
257
+
258
+ // Default: uses ollama at localhost:11434 with nomic-embed-text
259
+ const detector = createVectorSimilarityDetector();
260
+
261
+ // Or customise host, model, and threshold:
262
+ const detector = createVectorSimilarityDetector({
263
+ host: 'http://localhost:11434',
264
+ model: 'nomic-embed-text',
265
+ threshold: 0.60, // cosine similarity cutoff
266
+ });
267
+
268
+ // Pass to synthesize:
269
+ await runner.synthesize(conceptId, {
270
+ proposedBody: '...',
271
+ rationale: '...',
272
+ similarityDetector: detector,
273
+ });
274
+ ```
275
+
276
+ **Starting ollama:**
277
+
278
+ ```bash
279
+ ollama serve &
280
+ ollama pull nomic-embed-text # 274 MB, one-time pull
281
+ ```
282
+
283
+ **Threshold guidance:**
284
+
285
+ The default threshold of `0.60` is validated against `nomic-embed-text` (768-dim). Empirical
286
+ scores observed in the eval suite:
287
+
288
+ | Pair | Score |
289
+ |---|---|
290
+ | Semantically similar API design texts | ~0.77 |
291
+ | Semantically unrelated (API vs. bread baking) | ~0.41 |
292
+
293
+ A threshold of `0.60` cleanly separates these two classes. If your domain records are more
294
+ homogeneous (narrow vocabulary, very similar boilerplate) you may need to raise the threshold
295
+ to `0.70–0.80` to avoid over-clustering.
296
+
297
+ ### Fail-closed rationale
298
+
299
+ The vector detector throws an `Error` with `code="EMBED_FAILURE"` rather than returning `[]`
300
+ when the embedding call fails (network error, HTTP error, malformed response, wrong vector
301
+ count). This is intentional.
302
+
303
+ A detector that silently returns `[]` on infrastructure failure is indistinguishable from one
304
+ that found no similar records. The result is a misleading `MISSING_EVIDENCE` at the detect-cluster
305
+ gate, which looks like "this concept has no sources" rather than "the embedding service is down".
306
+
307
+ Failing closed makes the infrastructure problem visible immediately, at the right level, with a
308
+ clear error code. Operators can catch `EMBED_FAILURE` separately from `MISSING_EVIDENCE` and
309
+ route them to different alerting channels.
310
+
311
+ ### Injecting a custom embed function
312
+
313
+ For tests or alternative providers (OpenAI, Cohere, etc.), pass `embed` directly:
314
+
315
+ ```js
316
+ const detector = createVectorSimilarityDetector({
317
+ embed: async (texts) => {
318
+ // texts: string[] — one per record (title + "\n" + body by default)
319
+ // must return: number[][] — one vector per input text
320
+ const response = await myEmbeddingAPI.embed(texts);
321
+ return response.vectors;
322
+ },
323
+ threshold: 0.70,
324
+ });
325
+ ```
326
+
327
+ The `embed` function is called once per `synthesize`/`consolidate` call with all texts in a
328
+ single batch (concept first, then candidates).
@@ -524,3 +524,199 @@ Superseded snapshots MUST remain queryable:
524
524
 
525
525
  There is no `"archived"` or `"deleted"` status field; the supersession chain is expressed entirely
526
526
  through links and mutation log entries.
527
+
528
+ ---
529
+
530
+ ## Addendum B — Record Status Lifecycle (S7)
531
+
532
+ ### B.1 Status Field
533
+
534
+ Every record envelope (§1.1) gains an optional `status` field:
535
+
536
+ | Field | Type | Required | Default | Description |
537
+ |---|---|---|---|---|
538
+ | `status` | `"active"` \| `"implemented"` \| `"retired"` | no | `"active"` | Lifecycle status of the record. Records without a status field are treated as `"active"`. |
539
+
540
+ `status` is a mutable field but MUST only change via the `retire` mutation op (§B.4).
541
+ Direct field updates via the `update` op MUST NOT change `status`; `update` MUST ignore `status` if
542
+ supplied in the fields argument.
543
+
544
+ ### B.2 Allowed Status Transitions
545
+
546
+ | From | To | Op | Required Evidence |
547
+ |---|---|---|---|
548
+ | `"active"` | `"implemented"` | `retire` | `implementedByRef` (non-empty, references the implementing artifact/commit/PR) |
549
+ | `"active"` | `"retired"` | `retire` | `rationale` (non-empty, explains obsolescence) |
550
+ | `"implemented"` | `"retired"` | `retire` | `rationale` (non-empty) |
551
+
552
+ No other transitions are permitted. Attempting an invalid transition MUST throw with
553
+ `error.code === "MISSING_EVIDENCE"` and a human-readable `message`.
554
+
555
+ Records in `"retired"` status have no further transitions — they are terminal.
556
+
557
+ ### B.3 Working-Set Exclusion
558
+
559
+ Records with `status === "retired"` are EXCLUDED from the default working set:
560
+
561
+ - `listByType(type)` returns only non-retired records by default.
562
+ - `listByCategory(category, options?)` returns only non-retired records by default.
563
+ - `defaultSimilarityDetector` considers only non-retired compiled records as candidates.
564
+ - The vector similarity detector (`createVectorSimilarityDetector`) considers only non-retired
565
+ compiled records as candidates.
566
+
567
+ All four filtering surfaces accept an `includeRetired: true` option (or equivalent flag on the
568
+ similarity detector) to restore retired records to the result set.
569
+
570
+ Retired records remain **fully queryable with provenance**:
571
+ - `get(id)` always returns the full record regardless of status.
572
+ - `listByType(type, { includeRetired: true })` returns all records of that type.
573
+ - `listByCategory(category, { includeRetired: true })` returns all matching records.
574
+ - The record's `mutation_log` carries the full retirement evidence.
575
+
576
+ ### B.4 `retire` Mutation Operation
577
+
578
+ The `retire` op transitions a record from `"active"` or `"implemented"` to the target status.
579
+ It NEVER deletes the record. The record body, links, and provenance remain intact.
580
+
581
+ **Required fields:**
582
+
583
+ | Field | Location | Description |
584
+ |---|---|---|
585
+ | `id` | argument | ID of the record to retire. |
586
+ | `targetStatus` | argument | Target status: `"implemented"` or `"retired"`. |
587
+ | `agent` | evidence | Agent performing the retirement. |
588
+ | `rationale` | evidence | Non-empty string explaining why the record is being retired. Required for all target statuses. |
589
+
590
+ **Conditional evidence fields:**
591
+
592
+ | Field | Location | Condition | Description |
593
+ |---|---|---|---|
594
+ | `implementedByRef` | evidence | `targetStatus === "implemented"` | Non-empty reference to the implementing artifact (commit SHA, PR URL, issue number, etc.). |
595
+ | `supersededByRef` | evidence | optional for `targetStatus === "retired"` | Reference to a superseding record or artifact. |
596
+
597
+ **Rejection conditions:**
598
+ - Record with `id` does not exist.
599
+ - `targetStatus` is not `"implemented"` or `"retired"`.
600
+ - Current status transition is invalid (see §B.2).
601
+ - `rationale` is missing or empty.
602
+ - `targetStatus === "implemented"` and `implementedByRef` is missing or empty.
603
+ - Missing `agent` in evidence.
604
+
605
+ **Post-conditions:**
606
+ - Record `status` is updated to `targetStatus`.
607
+ - Record `updated_at` is refreshed.
608
+ - A mutation log entry (op=`"retire"`) is appended, carrying `targetStatus`, `rationale`,
609
+ and any supplied `implementedByRef` / `supersededByRef`.
610
+ - The record body, `links`, and creation `provenance` are NOT changed.
611
+ - `get(id)` returns the full record with the updated status.
612
+ - `listByType(type)` (without `includeRetired`) no longer returns this record if
613
+ `targetStatus === "retired"`.
614
+
615
+ ### B.5 Adapter Contract Extension
616
+
617
+ The adapter interface (§8) is extended:
618
+
619
+ ```ts
620
+ interface KnowledgeStoreAdapter {
621
+ // ... existing methods ...
622
+ retire(id: string, targetStatus: "implemented" | "retired", evidence: RetireEvidence): Promise<void>;
623
+ listByType(type: RecordType, options?: { includeRetired?: boolean }): Promise<Record[]>;
624
+ listByCategory(category: string, options?: { prefix?: boolean; includeRetired?: boolean }): Promise<Record[]>;
625
+ }
626
+ ```
627
+
628
+ `RetireEvidence`:
629
+ ```ts
630
+ interface RetireEvidence {
631
+ agent: string;
632
+ rationale: string;
633
+ implementedByRef?: string; // required when targetStatus === "implemented"
634
+ supersededByRef?: string; // optional
635
+ note?: string;
636
+ }
637
+ ```
638
+
639
+ ### B.6 Provenance and History Guarantee
640
+
641
+ Retired records MUST remain reachable from:
642
+ - `get(id)` — always returns the full record.
643
+ - `listByType(type, { includeRetired: true })` and `listByCategory(category, { includeRetired: true })`.
644
+ - Snapshot `provenance.source_ids` — any snapshot that included the record in its cluster before
645
+ retirement retains the reference intact. The retired record can be retrieved via `get(sourceId)`.
646
+ - The retirement `mutation_log` entry carries the full evidence of why and when the record was
647
+ retired and by whom.
648
+
649
+ There is no deletion of records. Physical purge (if ever needed) is a separate, future policy
650
+ hook not defined in this version.
651
+
652
+ ---
653
+
654
+ ## Addendum C — Person Record Type (Entity Cards)
655
+
656
+ ### C.1 `person` Record Type
657
+
658
+ A `person` record is a first-class entity card for a named individual mentioned in the knowledge base.
659
+ It participates fully in links, the graph index, and status lifecycle like any other record type.
660
+
661
+ | Field | Notes |
662
+ |---|---|
663
+ | `type` | `"person"` |
664
+ | `title` | The person's full name. Used for exact-match resolution. |
665
+ | `body` | Structured prose with role and/or org: `**Role/Org:** <text>`. Merged on apply during card union. |
666
+ | `tags` | May include `alias:<name>` entries for alternative names (nicknames, initials). |
667
+ | `category` | Dot-separated category. The Obsidian adapter ignores this for routing — person records always land in `people/`. |
668
+
669
+ ### C.2 Link Kinds Extended
670
+
671
+ | Kind | Direction | Meaning |
672
+ |---|---|---|
673
+ | `"appears-in"` | person → raw\|compiled | Person was mentioned in that record. |
674
+ | `"person"` | compiled → person | Compiled record references a person card. |
675
+
676
+ ### C.3 Obsidian Adapter Layout
677
+
678
+ The Obsidian store adapter (`adapters/obsidian-store`) places person records under a
679
+ top-level `people/` folder regardless of category, so person cards are vault-global entities.
680
+
681
+ ```
682
+ <storeRoot>/
683
+ people/
684
+ dana-smith.md
685
+ lee-wong.md
686
+ <category-as-path>/
687
+ <title-slug>.md (concept, snapshot)
688
+ <sourcesDir>/
689
+ <title-slug>.md (raw, compiled)
690
+ ```
691
+
692
+ Person cards render an **Appears In** section listing all `appears-in` links as Obsidian wikilinks.
693
+ Notes that reference people render a **People** section listing `person` links.
694
+
695
+ ### C.4 Alias Storage
696
+
697
+ Aliases are stored in the `tags` array using the prefix `alias:`:
698
+
699
+ ```yaml
700
+ tags: [alias:Dana S., alias:D. Smith]
701
+ ```
702
+
703
+ Resolution checks both `title` and all `alias:*` tags for exact normalised-name match.
704
+
705
+ ### C.5 Entity Extraction (Flow Runner)
706
+
707
+ The `KnowledgeFlowRunner.extractEntities(compiledId, options)` method:
708
+
709
+ 1. Runs the extractor (default: `defaultEntityExtractor`) against the compiled record and its source raws.
710
+ 2. For each mention, resolves via exact-name match (incl. aliases) or creates a new person card.
711
+ 3. Near-matches (same surname + initial) create a **separate** card with a `related` link labelled
712
+ `possible-duplicate` — no auto-merge.
713
+ 4. Writes bidirectional links: `person → raw+compiled` (kind `appears-in`) and `compiled → person` (kind `person`).
714
+
715
+ ### C.6 Card Merge
716
+
717
+ Card merge uses the existing `propose → apply/reject` gate:
718
+
719
+ - `KnowledgeFlowRunner.mergePerson(primaryId, duplicateId, options)`
720
+ - **apply**: unions body, adds `alias:<duplicate title>` tag to primary, unions `appears-in` links, calls
721
+ `store.supersede(primaryId, [duplicateId])` to archive the duplicate (supersede-not-delete invariant).
722
+ - **reject**: both cards remain byte-identical.
@@ -411,12 +411,17 @@ describe("Knowledge Kit Store Contract Suite", () => {
411
411
  );
412
412
  });
413
413
 
414
- test("rejects non-concept concept_id", async () => {
415
- const notConcept = await store.create({ type: "raw", title: "NC", body: "nc", category: "test", provenance: { agent: "tester" } });
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" } });
416
418
  const pid = await store.create({ type: "raw", title: "P3", body: "p", category: "test", provenance: { agent: "tester" } });
417
- await assertMissingEvidence(
418
- () => store.propose(notConcept, pid, { agent: "tester", proposal: "change" }),
419
- "propose non-concept target"
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)"
420
425
  );
421
426
  });
422
427
 
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Entity Cards Demo — Acme Example
3
+ *
4
+ * Demonstrates the full entity extraction flow:
5
+ * 1. Capture a raw note with Attendees line (Dana Smith + Lee Wong)
6
+ * 2. Compile the raw note
7
+ * 3. Extract person entities → create person cards with bidirectional links
8
+ * 4. Print Dana's person card verbatim (as Obsidian markdown)
9
+ *
10
+ * Run:
11
+ * node kits/knowledge/evals/entities/demo-acme.js
12
+ */
13
+
14
+ import * as fs from "node:fs";
15
+ import * as os from "node:os";
16
+ import * as path from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+ const KIT_ROOT = path.resolve(__dirname, "../../../..");
21
+
22
+ // Use Obsidian adapter for richest output
23
+ const obsidianPath = path.join(KIT_ROOT, "kits/knowledge/adapters/obsidian-store/index.js");
24
+ const runnerPath = path.join(KIT_ROOT, "kits/knowledge/adapters/flow-runner/index.js");
25
+
26
+ const { ObsidianKnowledgeStore } = await import(obsidianPath);
27
+ const { KnowledgeFlowRunner } = await import(runnerPath);
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Setup temp store
31
+ // ---------------------------------------------------------------------------
32
+
33
+ const storeDir = fs.mkdtempSync(path.join(os.tmpdir(), "acme-entity-demo-"));
34
+
35
+ const store = new ObsidianKnowledgeStore({ storeRoot: storeDir, sourcesDir: "meetings" });
36
+ const runner = new KnowledgeFlowRunner({ store, agent: "acme-demo" });
37
+
38
+ console.log("Store root:", storeDir);
39
+ console.log("");
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // 1. Capture a raw Acme meeting note
43
+ // ---------------------------------------------------------------------------
44
+
45
+ const meetingNote = `Acme Q3 Kickoff — 2026-06-12
46
+
47
+ Attendees: Dana Smith (Acme VP Eng), Lee Wong
48
+
49
+ Agenda:
50
+ - Q3 roadmap review
51
+ - Engineering capacity planning
52
+ - Open items from Q2 retro
53
+
54
+ Action Items:
55
+ - Dana Smith to share updated roadmap by Friday
56
+ - Lee Wong to provide capacity estimates
57
+
58
+ Next meeting: 2026-06-19`;
59
+
60
+ const { id: rawId } = await runner.capture(meetingNote, {
61
+ title: "Acme Q3 Kickoff",
62
+ category: "sales.acme.meetings",
63
+ });
64
+
65
+ console.log("Captured raw note:", rawId);
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // 2. Compile the raw note
69
+ // ---------------------------------------------------------------------------
70
+
71
+ const { id: compiledId } = await runner.compile([rawId], {
72
+ title: "Compiled: Acme Q3 Kickoff",
73
+ category: "sales.acme.meetings",
74
+ });
75
+
76
+ console.log("Compiled note: ", compiledId);
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // 3. Extract entities → create person cards
80
+ // ---------------------------------------------------------------------------
81
+
82
+ const result = await runner.extractEntities(compiledId);
83
+
84
+ console.log("");
85
+ console.log("Person cards created:");
86
+ for (const pc of result.personCards) {
87
+ const card = await store.get(pc.cardId);
88
+ console.log(` - ${card.title} (id: ${pc.cardId}, created: ${pc.created})`);
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // 4. Print Dana's person card verbatim (as Obsidian markdown)
93
+ // ---------------------------------------------------------------------------
94
+
95
+ const danaResult = result.personCards.find((pc) => pc.name === "Dana Smith");
96
+ if (!danaResult) throw new Error("Dana Smith card not found");
97
+
98
+ // Read the file directly from the vault
99
+ const { default: DefaultKnowledgeStore } = await import(
100
+ path.join(KIT_ROOT, "kits/knowledge/adapters/default-store/index.js")
101
+ );
102
+
103
+ // Find the obsidian file for Dana's card
104
+ const pathIndexFile = path.join(storeDir, ".graph-index.json");
105
+ const pathIndex = JSON.parse(fs.readFileSync(pathIndexFile, "utf8"));
106
+ const danaEntry = pathIndex.by_id[danaResult.cardId];
107
+ if (!danaEntry) throw new Error("Dana card not found in path index");
108
+
109
+ const danaFilePath = path.join(storeDir, danaEntry.path);
110
+ const danaMarkdown = fs.readFileSync(danaFilePath, "utf8");
111
+
112
+ console.log("");
113
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
114
+ console.log("Dana Smith's person card (verbatim Obsidian markdown):");
115
+ console.log("File:", danaFilePath.replace(storeDir, "<vault>"));
116
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
117
+ console.log(danaMarkdown);
118
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // Cleanup
122
+ // ---------------------------------------------------------------------------
123
+
124
+ fs.rmSync(storeDir, { recursive: true, force: true });
125
+ console.log("\nDemo complete.");