You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(mirror,relations): persist edges into entity markdown frontmatter
Two related bugs that meant relations created via UI/REST/MCP never
made it into the markdown mirror, plus a silent data-loss path on
subsequent entity updates.
1. Mirror was never told about relations
The phase4 SQLite migration (c8997ee) wired StoreManager to call
mirrorNoteCreate/Update, mirrorTaskCreate/Update, mirrorSkillCreate/
Update, mirrorEpicCreate/Update, but hardcoded the `relations`
parameter as `[]` everywhere, and dropped all calls to the dedicated
mirrorNoteRelation / mirrorTaskRelation / mirrorSkillRelation /
mirrorEpicRelation event functions. Those functions stayed in
file-mirror.ts but became dead code, and their tests were deleted in
the same commit.
Effects:
- Edges created through createEdge() never appeared in the mirror
file frontmatter — they lived in SQLite only.
- Every updateNote/updateTask/updateSkill regenerated the snapshot
with `relations: []`, wiping any frontmatter relations that had
been put there by an external file edit.
- If a user then edited the markdown file in their IDE and saved it,
the reverse-import path (syncEdgesFromFile) would diff the empty
desired list against current SQLite edges and *delete every edge
for that entity*. Silent data loss.
Fix:
- Add private StoreManager.buildOutgoingForMirror(graph, id, slug)
that walks findOutgoingEdges + resolveIdToSlug to construct the
RelationLike[] the file-mirror layer expects. Indexed-graph
targets (docs/code/files) are skipped because they have no slug.
- Replace all 9 hardcoded `[]` arguments at the entity create/update/
move/reorder/bulk call sites with this helper.
- Add private StoreManager.mirrorRelationEvent(action, edge) that
dispatches to mirrorNoteRelation / mirrorTaskRelation /
mirrorSkillRelation / mirrorEpicRelation depending on edge.fromGraph,
re-uses buildMirrorTaskAttrs and the inline note/skill/epic attr
builders, and records the mirror write through the existing tracker
so the watcher does not feedback-loop the change back into SQLite.
- Wire createEdge() and deleteEdge() to call mirrorRelationEvent
after the SQLite mutation succeeds.
2. REST DELETE /relations and /links never deleted anything
The REST handlers for knowledge/tasks/skills delete-edge endpoints
passed `kind: ''` to deleteEdge() unconditionally. The underlying
SQL DELETE matches edges by (fromGraph, fromId, toGraph, toId, kind),
so the empty kind never matched any real row — the SQL affected
0 rows but the handler returned 204 OK. Pre-existing bug, masked
until now because the mirror file was empty too, so the UI saw
"delete worked".
Fix:
- .pick() the validation schemas to include `kind` (already required
by createRelationSchema/createTaskLinkSchema/createSkillLinkSchema).
- Read kind from req.body and pass it through to deleteEdge.
- Update the frontend deleteRelation / deleteTaskLink /
deleteSkillLink type signatures to require kind, and update
RelationManager.handleDeleteConfirmed to pass rel.kind from the
original edge.
Tests
- Restored 3 unit tests in src/tests/file-mirror.test.ts covering
mirrorNoteRelation / mirrorTaskRelation / mirrorSkillRelation, so
these dedicated event functions never silently rot again.
- 8 round-trip tests in src/tests/store/store-manager.test.ts that
drive StoreManager.createEdge/deleteEdge and assert the markdown
file frontmatter, including the silent-data-loss case (createEdge
followed by updateNote must preserve the relation), cross-graph
edges (graph: tag), and tasks/skills mirror coverage.
- New "2.3.1 Forward mirror" group in functional test
02-knowledge.ts that POSTs a relation, PUTs the note body, and
DELETEs the relation, asserting the mirror file frontmatter at
each step through the actual REST API + dist build.
- Updated existing tests in 02-knowledge.ts, 03-tasks.ts,
13-websocket.ts, 19-coverage-gaps.ts and rest-api-gaps.test.ts to
pass the now-required `kind` field. Several of those tests had
been silently exercising the broken code path.
Verified: 1597 jest tests pass, 502 functional sandbox tests pass.
0 commit comments