@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.
- package/.github/workflows/kit-gates-demo.yml +171 -0
- package/.github/workflows/release-please.yml +13 -1
- package/AGENTS.md +8 -1
- package/CHANGELOG.md +53 -0
- package/CONTEXT.md +1 -1
- package/README.md +13 -2
- package/build/src/cli/flow-kit.js +41 -2
- package/build/src/flow-kit/validate.js +98 -0
- package/build/src/tools/validate-source-tree.js +2 -1
- package/context/scripts/hooks/config-protection.js +217 -15
- package/docs/fixture-ownership.md +1 -0
- package/docs/index.md +9 -1
- package/docs/kit-authoring-guide.md +126 -0
- package/docs/knowledge-kit.md +69 -0
- package/docs/vision.md +22 -0
- package/evals/fixtures/kit-conformance-levels/k0-flows-only/flows/review.flow.json +26 -0
- package/evals/fixtures/kit-conformance-levels/k0-flows-only/kit.json +13 -0
- package/evals/fixtures/kit-conformance-levels/k1-agent-extension/docs/README.md +3 -0
- package/evals/fixtures/kit-conformance-levels/k1-agent-extension/flows/build.flow.json +26 -0
- package/evals/fixtures/kit-conformance-levels/k1-agent-extension/kit.json +20 -0
- package/evals/fixtures/kit-conformance-levels/k2-with-evals/docs/README.md +3 -0
- package/evals/fixtures/kit-conformance-levels/k2-with-evals/eval-suites/contract-suite/suite.test.js +1 -0
- package/evals/fixtures/kit-conformance-levels/k2-with-evals/flows/synthesize.flow.json +26 -0
- package/evals/fixtures/kit-conformance-levels/k2-with-evals/kit.json +27 -0
- package/evals/fixtures/kit-conformance-levels/third-party-extension/flows/review.flow.json +26 -0
- package/evals/fixtures/kit-conformance-levels/third-party-extension/kit.json +19 -0
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_hook_category_behaviors.sh +51 -0
- package/evals/integration/test_kit_conformance_levels.sh +209 -0
- package/evals/run.sh +2 -0
- package/evals/static/test_universal_bundles.sh +10 -0
- package/kits/catalog.json +6 -0
- package/kits/knowledge/adapters/default-store/index.js +95 -14
- package/kits/knowledge/adapters/flow-runner/entity-extractor.js +194 -0
- package/kits/knowledge/adapters/flow-runner/index.js +639 -0
- package/kits/knowledge/adapters/obsidian-store/README.md +141 -0
- package/kits/knowledge/adapters/obsidian-store/demo.js +181 -0
- package/kits/knowledge/adapters/obsidian-store/index.js +868 -0
- package/kits/knowledge/adapters/shared/codec.js +325 -0
- package/kits/knowledge/adapters/similarity-vector/index.js +284 -0
- package/kits/knowledge/docs/README.md +193 -0
- package/kits/knowledge/docs/store-contract.md +196 -0
- package/kits/knowledge/evals/contract-suite/suite.test.js +10 -5
- package/kits/knowledge/evals/entities/demo-acme.js +125 -0
- package/kits/knowledge/evals/entities/suite.test.js +722 -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 +10 -3
- package/kits/knowledge/flows/retire.flow.json +77 -0
- package/kits/knowledge/kit.json +31 -1
- package/kits/release-evidence/fixtures/claims/README.md +14 -0
- package/kits/release-evidence/fixtures/claims/fail-rejected-release.trust.json +22 -0
- package/kits/release-evidence/fixtures/claims/pass-trusted-release.trust.json +22 -0
- package/kits/release-evidence/flows/release-evidence.flow.json +38 -0
- package/kits/release-evidence/kit.json +13 -0
- package/package.json +1 -1
- package/packaging/conformance/fixtures/config-protection--allow-no-verify-in-string.json +20 -0
- package/packaging/conformance/fixtures/config-protection--block-git-no-verify.json +23 -0
- package/scripts/hooks/config-protection.js +217 -15
- package/src/cli/flow-kit.ts +40 -2
- package/src/flow-kit/validate.ts +127 -0
- 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("
|
|
415
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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.");
|