moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,109 @@
# Art.17 Frame + Index + KG Erasure Companion (2026-07-01)
Continues the #7 verbatim-provenance arc. The S2 handoff (`06c8574b`) shipped
**redaction-only erasure of the `raw_archive` provenance rows**. That is
provenance-only: the DERIVED `memory_frames` (whose summaries quote source PII)
plus their FTS / vector / chunk-vector / KnowledgeGraph projections stay
searchable and recall-able. This slice closes that gap.
## Erasure surface (verified 2026-07-01)
`PRAGMA foreign_keys = ON` (db.ts:50) → `ON DELETE CASCADE` fires.
| Store | Holds PII? | Cascaded by frame DELETE? | Handled by `FrameStore.delete()` today |
|---|---|---|---|
| `memory_frames.content` | yes (summary quotes source) | n/a (the row itself) | DELETE ✔ |
| `memory_frames_fts` (fts5) | yes (indexed content) | no (virtual) | manual DELETE ✔ |
| `memory_frames_vec` (vec0) | yes (embedding) | no (virtual) | manual DELETE ✔ |
| `memory_frame_chunks` | yes (chunk text) | **yes** (FK CASCADE) | via cascade ✔ |
| `memory_frame_chunks_vec` (vec0) | yes (chunk embedding) | no (virtual) | **✗ LEAK** |
| `kg_entity_frames` | link only | yes (FK CASCADE) | manual DELETE ✔ |
| `knowledge_entities` | yes (`name`/`properties`) | no (shared) | not touched — needs orphan sweep |
| `knowledge_relations` | yes (`properties`) | no | not touched — needs orphan sweep |
| `raw_archive` | yes (verbatim) | no (append-only) | redacted by `RawArchive.erase` ✔ |
**Leak #1**`memory_frame_chunks_vec` is a vec0 virtual table (no FK); its rowid
is the *chunk* id (`memory_frame_chunks.id`). Frame DELETE cascades the chunk
rows away but leaves the chunk EMBEDDINGS keyed by orphaned ids, and `search()`
queries `memory_frame_chunks_vec` first. → still recall-able.
**Leak #2** — an entity derived solely from an erased frame becomes an orphan
(zero remaining `kg_entity_frames` links) but its `name`/`properties` PII persists
and is still returned by `searchEntities` / contextual recall.
**FK gotcha**`knowledge_relations` references `knowledge_entities` WITHOUT
`ON DELETE CASCADE`; hard-deleting an entity with live relations raises
`SQLITE_CONSTRAINT`. Relations must go first.
## Decisions
1. **Derived frames → DELETE** (not redact). The `raw_archive` skeleton already
is the founder-ratified audit record ("item existed, erased at T for reason
R"). A redacted frame in the corpus would need its own PII-stripping and would
pollute recall. DELETE is the simpler, more compliant choice.
2. **Orphaned entities → hard-delete** (relations first, then entity). Retire
(bitemporal `valid_to`) hides from active recall but leaves `name`/`properties`
PII physically present → not Art.17-compliant. Only delete entities with ZERO
surviving frame links (never shared entities).
3. **Fix the `chunks_vec` leak in `FrameStore.delete()`** — it is a latent
correctness bug. `delete()` now purges `memory_frame_chunks_vec`, and
`compact()`'s three prune/merge sites are routed THROUGH `delete()` so every
deletion path is covered (the adversarial review confirmed `compact()` bypassed
`delete()` and still leaked `_vec`/`_fts`/`_chunks_vec`). Strictly additive
(removes stale vectors that should be gone); cannot degrade recall of surviving
frames. SOTA-neutral.
4. **All-or-nothing** — every multi-table erasure runs in one `db.transaction()`.
A partial erasure is a compliance failure.
5. **`archive_uid` re-identification residual → DEFERRED.** `archive_uid =
sha256(source∥sourceRef∥content)` stays frozen post-erasure → re-id vector for
low-entropy content. Rotating it breaks the frame→archive link; distinct arc.
## API (new module `mind/erasure.ts`)
```ts
interface EraseResult {
framesDeleted: number;
archiveRedacted: number; // raw_archive rows redacted
chunkVectorsPurged: number; // memory_frame_chunks_vec rows removed
entitiesErased: number; // orphaned KG entities hard-deleted
relationsErased: number; // relations of those entities removed
}
class MindErasure {
constructor(db, frameStore, rawArchive, knowledgeGraph)
eraseFrame(frameId, reason): EraseResult // one frame + provenance + orphans
eraseBySourceRef(source, sourceRef, reason): EraseResult // subject sweep
}
```
`eraseFrame` (single-frame primitive): collect linked entity ids → redact linked
archive rows (`RawArchive.eraseByFrame`) → purge chunk vectors → `FrameStore.delete`
(fts/vec/chunks/kg-bridge cascade) → for each previously-linked entity now at zero
links, hard-delete its relations then the entity. One transaction.
`eraseBySourceRef` (subject sweep): reaches the subject's derived corpus through
THREE keys, because one harvested item fans out into differently-keyed frames:
- (a) the distilled **summary** frame — `metadata.archiveUids` reverse lookup;
- (b) the verbatim **`[mind-rawturn …]`** frames — content-prefix keyed by
`sanitize(source∥sourceRef)` (they carry NO archive link — the review's CRITICAL);
- (c) synthesized **B-frames** — swept by `content.references` ∩ erased-ids (fixpoint).
Then redact any subject provenance row no frame reached. One transaction.
## Adversarial review (2026-07-01, 4-lens, each finding verified)
Raised 14, confirmed 4 — all fixed in this arc:
- **CRITICAL** verbatim raw-turn frames survived (link-only sweep) → (b) above.
- **HIGH** harvest KG entities were born **unlinked** (`createEntity` with no
`linkEntityToFrame`) so the orphan sweep couldn't reach them → centralized into
`KnowledgeGraph.importEntitiesForFrame` (creates + links); both MCP harvest
handlers routed through it. Existing unlinked entities are covered by the
boot-time `backfillKgEntityFrames` string-match.
- **MEDIUM** `compact()` bypassed `delete()` → routed through it (decision #3).
- **LOW** B-frames quoting entity-name PII → (c) above.
## Out of scope (documented residuals)
- **`archive_uid` opaque-id rotation** (decision 5) — re-id vector for low-entropy
content survives; interacts with frame→archive link stability. Distinct arc.
- **`eraseFrame` is single-frame** — it does NOT sweep raw-turns/B-frames; full
data-subject erasure must go through `eraseBySourceRef`. Documented on the method.
- Server route / MCP tool / Memory-Center "erase" button (substrate first).
- OSS forward-port of this delta → `marolinik/hive-mind` (after it lands + PR #21 merges).