@ijfw/memory-server 1.4.4 → 1.5.1

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 (245) hide show
  1. package/bin/ijfw-memorize +14 -7
  2. package/fixtures/team/book.json +6 -6
  3. package/fixtures/team/business.json +146 -20
  4. package/fixtures/team/content.json +6 -6
  5. package/fixtures/team/design.json +148 -20
  6. package/fixtures/team/mixed.json +206 -27
  7. package/fixtures/team/research.json +146 -20
  8. package/fixtures/team/software.json +148 -20
  9. package/fixtures/truncation-corpus/_generate-corpus.js +367 -0
  10. package/fixtures/truncation-corpus/fx-01-clean-exit-01/events.jsonl +2 -0
  11. package/fixtures/truncation-corpus/fx-01-clean-exit-01/intent-journal.jsonl +2 -0
  12. package/fixtures/truncation-corpus/fx-01-clean-exit-01/meta.json +18 -0
  13. package/fixtures/truncation-corpus/fx-01-clean-exit-01/target/.ijfw/state/workflow.json +1 -0
  14. package/fixtures/truncation-corpus/fx-01-clean-exit-02/events.jsonl +2 -0
  15. package/fixtures/truncation-corpus/fx-01-clean-exit-02/intent-journal.jsonl +2 -0
  16. package/fixtures/truncation-corpus/fx-01-clean-exit-02/meta.json +18 -0
  17. package/fixtures/truncation-corpus/fx-01-clean-exit-02/target/.ijfw/state/workflow.json +1 -0
  18. package/fixtures/truncation-corpus/fx-01-clean-exit-03/events.jsonl +2 -0
  19. package/fixtures/truncation-corpus/fx-01-clean-exit-03/intent-journal.jsonl +2 -0
  20. package/fixtures/truncation-corpus/fx-01-clean-exit-03/meta.json +18 -0
  21. package/fixtures/truncation-corpus/fx-01-clean-exit-03/target/.ijfw/state/workflow.json +1 -0
  22. package/fixtures/truncation-corpus/fx-01-clean-exit-04/events.jsonl +2 -0
  23. package/fixtures/truncation-corpus/fx-01-clean-exit-04/intent-journal.jsonl +2 -0
  24. package/fixtures/truncation-corpus/fx-01-clean-exit-04/meta.json +18 -0
  25. package/fixtures/truncation-corpus/fx-01-clean-exit-04/target/.ijfw/state/workflow.json +1 -0
  26. package/fixtures/truncation-corpus/fx-01-clean-exit-05/events.jsonl +2 -0
  27. package/fixtures/truncation-corpus/fx-01-clean-exit-05/intent-journal.jsonl +2 -0
  28. package/fixtures/truncation-corpus/fx-01-clean-exit-05/meta.json +18 -0
  29. package/fixtures/truncation-corpus/fx-01-clean-exit-05/target/.ijfw/state/workflow.json +1 -0
  30. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/events.jsonl +1 -0
  31. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/intent-journal.jsonl +3 -0
  32. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/meta.json +18 -0
  33. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/snapshots/v-midO-1-advance.json +11 -0
  34. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/target/.ijfw/state/workflow.json +1 -0
  35. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/events.jsonl +1 -0
  36. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/intent-journal.jsonl +3 -0
  37. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/meta.json +18 -0
  38. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/snapshots/v-midO-2-advance.json +11 -0
  39. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/target/.ijfw/state/workflow.json +1 -0
  40. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/events.jsonl +1 -0
  41. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/intent-journal.jsonl +3 -0
  42. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/meta.json +18 -0
  43. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/snapshots/v-midO-3-advance.json +11 -0
  44. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/target/.ijfw/state/workflow.json +1 -0
  45. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/events.jsonl +1 -0
  46. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/intent-journal.jsonl +3 -0
  47. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/meta.json +18 -0
  48. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/snapshots/v-midO-4-advance.json +11 -0
  49. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/target/.ijfw/state/workflow.json +1 -0
  50. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/events.jsonl +1 -0
  51. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/intent-journal.jsonl +3 -0
  52. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/meta.json +18 -0
  53. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/snapshots/v-midO-5-advance.json +11 -0
  54. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/target/.ijfw/state/workflow.json +1 -0
  55. package/fixtures/truncation-corpus/fx-03-mid-append-01/events.jsonl +1 -0
  56. package/fixtures/truncation-corpus/fx-03-mid-append-01/intent-journal.jsonl +3 -0
  57. package/fixtures/truncation-corpus/fx-03-mid-append-01/meta.json +18 -0
  58. package/fixtures/truncation-corpus/fx-03-mid-append-01/target/.ijfw/blackboard/decisions.jsonl +1 -0
  59. package/fixtures/truncation-corpus/fx-03-mid-append-02/events.jsonl +1 -0
  60. package/fixtures/truncation-corpus/fx-03-mid-append-02/intent-journal.jsonl +3 -0
  61. package/fixtures/truncation-corpus/fx-03-mid-append-02/meta.json +18 -0
  62. package/fixtures/truncation-corpus/fx-03-mid-append-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
  63. package/fixtures/truncation-corpus/fx-03-mid-append-03/events.jsonl +1 -0
  64. package/fixtures/truncation-corpus/fx-03-mid-append-03/intent-journal.jsonl +3 -0
  65. package/fixtures/truncation-corpus/fx-03-mid-append-03/meta.json +18 -0
  66. package/fixtures/truncation-corpus/fx-03-mid-append-03/target/.ijfw/blackboard/decisions.jsonl +1 -0
  67. package/fixtures/truncation-corpus/fx-03-mid-append-04/events.jsonl +1 -0
  68. package/fixtures/truncation-corpus/fx-03-mid-append-04/intent-journal.jsonl +3 -0
  69. package/fixtures/truncation-corpus/fx-03-mid-append-04/meta.json +18 -0
  70. package/fixtures/truncation-corpus/fx-03-mid-append-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
  71. package/fixtures/truncation-corpus/fx-03-mid-append-05/events.jsonl +1 -0
  72. package/fixtures/truncation-corpus/fx-03-mid-append-05/intent-journal.jsonl +3 -0
  73. package/fixtures/truncation-corpus/fx-03-mid-append-05/meta.json +18 -0
  74. package/fixtures/truncation-corpus/fx-03-mid-append-05/target/.ijfw/blackboard/decisions.jsonl +1 -0
  75. package/fixtures/truncation-corpus/fx-04-no-events-01/events.jsonl +0 -0
  76. package/fixtures/truncation-corpus/fx-04-no-events-01/intent-journal.jsonl +1 -0
  77. package/fixtures/truncation-corpus/fx-04-no-events-01/meta.json +18 -0
  78. package/fixtures/truncation-corpus/fx-04-no-events-01/snapshots/v-noEv-1-set-phase.json +11 -0
  79. package/fixtures/truncation-corpus/fx-04-no-events-01/target/.ijfw/state/workflow.json +1 -0
  80. package/fixtures/truncation-corpus/fx-04-no-events-02/events.jsonl +0 -0
  81. package/fixtures/truncation-corpus/fx-04-no-events-02/intent-journal.jsonl +1 -0
  82. package/fixtures/truncation-corpus/fx-04-no-events-02/meta.json +18 -0
  83. package/fixtures/truncation-corpus/fx-04-no-events-02/snapshots/v-noEv-2-set-phase.json +11 -0
  84. package/fixtures/truncation-corpus/fx-04-no-events-02/target/.ijfw/state/workflow.json +1 -0
  85. package/fixtures/truncation-corpus/fx-04-no-events-03/events.jsonl +0 -0
  86. package/fixtures/truncation-corpus/fx-04-no-events-03/intent-journal.jsonl +1 -0
  87. package/fixtures/truncation-corpus/fx-04-no-events-03/meta.json +18 -0
  88. package/fixtures/truncation-corpus/fx-04-no-events-03/snapshots/v-noEv-3-set-phase.json +11 -0
  89. package/fixtures/truncation-corpus/fx-04-no-events-03/target/.ijfw/state/workflow.json +1 -0
  90. package/fixtures/truncation-corpus/fx-04-no-events-04/events.jsonl +0 -0
  91. package/fixtures/truncation-corpus/fx-04-no-events-04/intent-journal.jsonl +1 -0
  92. package/fixtures/truncation-corpus/fx-04-no-events-04/meta.json +18 -0
  93. package/fixtures/truncation-corpus/fx-04-no-events-04/snapshots/v-noEv-4-set-phase.json +11 -0
  94. package/fixtures/truncation-corpus/fx-04-no-events-04/target/.ijfw/state/workflow.json +1 -0
  95. package/fixtures/truncation-corpus/fx-04-no-events-05/events.jsonl +0 -0
  96. package/fixtures/truncation-corpus/fx-04-no-events-05/intent-journal.jsonl +1 -0
  97. package/fixtures/truncation-corpus/fx-04-no-events-05/meta.json +18 -0
  98. package/fixtures/truncation-corpus/fx-04-no-events-05/snapshots/v-noEv-5-set-phase.json +11 -0
  99. package/fixtures/truncation-corpus/fx-04-no-events-05/target/.ijfw/state/workflow.json +1 -0
  100. package/fixtures/truncation-corpus/fx-05-error-terminated-01/events.jsonl +2 -0
  101. package/fixtures/truncation-corpus/fx-05-error-terminated-01/intent-journal.jsonl +3 -0
  102. package/fixtures/truncation-corpus/fx-05-error-terminated-01/meta.json +18 -0
  103. package/fixtures/truncation-corpus/fx-05-error-terminated-01/snapshots/v-errT-1-partial.json +11 -0
  104. package/fixtures/truncation-corpus/fx-05-error-terminated-01/target/.ijfw/state/workflow.json +1 -0
  105. package/fixtures/truncation-corpus/fx-05-error-terminated-02/events.jsonl +2 -0
  106. package/fixtures/truncation-corpus/fx-05-error-terminated-02/intent-journal.jsonl +3 -0
  107. package/fixtures/truncation-corpus/fx-05-error-terminated-02/meta.json +18 -0
  108. package/fixtures/truncation-corpus/fx-05-error-terminated-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
  109. package/fixtures/truncation-corpus/fx-05-error-terminated-03/events.jsonl +2 -0
  110. package/fixtures/truncation-corpus/fx-05-error-terminated-03/intent-journal.jsonl +3 -0
  111. package/fixtures/truncation-corpus/fx-05-error-terminated-03/meta.json +18 -0
  112. package/fixtures/truncation-corpus/fx-05-error-terminated-03/snapshots/v-errT-3-partial.json +11 -0
  113. package/fixtures/truncation-corpus/fx-05-error-terminated-03/target/.ijfw/state/workflow.json +1 -0
  114. package/fixtures/truncation-corpus/fx-05-error-terminated-04/events.jsonl +2 -0
  115. package/fixtures/truncation-corpus/fx-05-error-terminated-04/intent-journal.jsonl +3 -0
  116. package/fixtures/truncation-corpus/fx-05-error-terminated-04/meta.json +18 -0
  117. package/fixtures/truncation-corpus/fx-05-error-terminated-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
  118. package/fixtures/truncation-corpus/fx-05-error-terminated-05/events.jsonl +2 -0
  119. package/fixtures/truncation-corpus/fx-05-error-terminated-05/intent-journal.jsonl +3 -0
  120. package/fixtures/truncation-corpus/fx-05-error-terminated-05/meta.json +18 -0
  121. package/fixtures/truncation-corpus/fx-05-error-terminated-05/snapshots/v-errT-5-partial.json +11 -0
  122. package/fixtures/truncation-corpus/fx-05-error-terminated-05/target/.ijfw/state/workflow.json +1 -0
  123. package/package.json +6 -3
  124. package/src/active-extension-writer.js +144 -64
  125. package/src/api-client.js +43 -5
  126. package/src/audit-roster.js +80 -5
  127. package/src/blackboard.js +298 -6
  128. package/src/cli-run.js +33 -5
  129. package/src/codex-agents.js +96 -5
  130. package/src/cost/aggregator.js +39 -9
  131. package/src/cost/pricing.js +57 -0
  132. package/src/cost/readers/gemini.js +1 -1
  133. package/src/cross-audit-chunker.js +189 -0
  134. package/src/cross-dispatcher.js +124 -21
  135. package/src/cross-orchestrator-cli.js +754 -159
  136. package/src/cross-orchestrator.js +1065 -17
  137. package/src/cross-project-search.js +195 -9
  138. package/src/dashboard-client-waves.html +304 -0
  139. package/src/dashboard-client.html +5 -1
  140. package/src/dashboard-server.js +73 -0
  141. package/src/deploy-alerts.js +150 -0
  142. package/src/design/iframe-bridge.js +242 -0
  143. package/src/design-companion.js +144 -0
  144. package/src/dispatch/checkpoint-cli.js +97 -0
  145. package/src/dispatch/colon-syntax.js +81 -1
  146. package/src/dispatch/extension.js +26 -2
  147. package/src/dispatch/registry-cli.js +4 -1
  148. package/src/dispatch/wave-cli.js +201 -6
  149. package/src/dispatch/worktree-cli.js +40 -0
  150. package/src/dispatch-planner.js +97 -2
  151. package/src/dream/runner.mjs +47 -11
  152. package/src/dream/stage-runner.js +40 -0
  153. package/src/dream/state-file.js +102 -0
  154. package/src/extension-installer.js +70 -24
  155. package/src/extension-quota-tracker.js +4 -2
  156. package/src/extension-registry.js +289 -35
  157. package/src/feedback-detector.js +26 -0
  158. package/src/fs-lock.js +259 -7
  159. package/src/gate-result.js +95 -1
  160. package/src/hardware-signer.js +4 -2
  161. package/src/hero-line.js +86 -5
  162. package/src/intent-router.js +35 -0
  163. package/src/lib/a11y-contract.js +117 -0
  164. package/src/lib/atomic-io.js +29 -8
  165. package/src/lib/cache-keepalive.js +150 -0
  166. package/src/lib/jsonl-rotation.js +104 -0
  167. package/src/lib/lighthouse-pillar.js +121 -0
  168. package/src/lib/llm-call.js +121 -0
  169. package/src/lib/playwright-baseline.js +205 -0
  170. package/src/lib/rekor-bridge.js +221 -0
  171. package/src/lib/repo-map.js +392 -0
  172. package/src/lib/shasum-verify.js +164 -0
  173. package/src/lib/sketches-gc.js +132 -0
  174. package/src/lib/tmp-suffix.js +62 -0
  175. package/src/lib/ui-review-runner.js +595 -0
  176. package/src/lib/uispec-drift.js +301 -0
  177. package/src/lib/uispec-intake.js +381 -0
  178. package/src/lib/worktree-guards.js +118 -0
  179. package/src/lib/worktree-recovery.js +100 -0
  180. package/src/memory/auto-linker.js +267 -0
  181. package/src/memory/benchmark.js +498 -0
  182. package/src/memory/dedup.js +126 -0
  183. package/src/memory/embedding-cache.js +136 -0
  184. package/src/memory/fact-extractor.js +168 -0
  185. package/src/memory/fts5.js +65 -1
  186. package/src/memory/migration-runner.js +6 -1
  187. package/src/memory/migrations/004-bitemporal.js +91 -0
  188. package/src/memory/migrations/005-vector-cache.js +61 -0
  189. package/src/memory/migrations/006-obsidian-graph.js +46 -0
  190. package/src/memory/migrations/007-skill-telemetry.js +24 -0
  191. package/src/memory/migrations/008-write-provenance.js +41 -0
  192. package/src/memory/migrations/009-obsidian-backfill.js +50 -0
  193. package/src/memory/obsidian-parser.js +152 -0
  194. package/src/memory/query-dataview.js +86 -0
  195. package/src/memory/search.js +46 -15
  196. package/src/memory/temporal.js +529 -0
  197. package/src/memory/tokenize.js +10 -0
  198. package/src/memory-facts-handler.js +37 -0
  199. package/src/memory-feedback.js +260 -2
  200. package/src/model-refresh.js +292 -0
  201. package/src/observability/cost-anomaly.js +166 -0
  202. package/src/observability/evaluator-checkpoint-contract.js +117 -0
  203. package/src/observability/trace-id.js +163 -0
  204. package/src/orchestrator/agents-md-blackboard.js +152 -0
  205. package/src/orchestrator/checkpoint-contract.md +140 -0
  206. package/src/orchestrator/debug-trident-trigger.js +374 -0
  207. package/src/orchestrator/debug-trident.js +570 -0
  208. package/src/orchestrator/merge-block-aware.js +350 -0
  209. package/src/orchestrator/plan-checker.js +475 -0
  210. package/src/orchestrator/post-done-runner.js +277 -0
  211. package/src/orchestrator/review.js +38 -3
  212. package/src/orchestrator/skill-telemetry-sink.js +29 -0
  213. package/src/orchestrator/skill-telemetry.js +37 -0
  214. package/src/orchestrator/state-events.js +459 -0
  215. package/src/orchestrator/state-sdk.js +1932 -0
  216. package/src/orchestrator/status-protocol.js +84 -17
  217. package/src/orchestrator/subagent-telemetry.js +471 -0
  218. package/src/orchestrator/termination.js +160 -0
  219. package/src/orchestrator/verification-gate.js +200 -16
  220. package/src/orchestrator/wave-state.js +332 -23
  221. package/src/orchestrator/worktree-provision.js +77 -0
  222. package/src/override-resolver.js +5 -3
  223. package/src/override-use-registry.js +111 -5
  224. package/src/receipts.js +36 -4
  225. package/src/recovery/checkpoint.js +56 -3
  226. package/src/recovery/code-fixer.js +961 -0
  227. package/src/recovery/truncation.js +317 -0
  228. package/src/redactor.js +75 -6
  229. package/src/runtime-mediator.js +15 -1
  230. package/src/sanitizer.js +10 -0
  231. package/src/search-hybrid.js +139 -0
  232. package/src/server.js +795 -112
  233. package/src/swarm/worktree.js +27 -4
  234. package/src/swarm-config.js +102 -17
  235. package/src/team/domain-templates/book.json +51 -0
  236. package/src/team/domain-templates/business.json +44 -0
  237. package/src/team/domain-templates/content.json +50 -0
  238. package/src/team/domain-templates/design.json +44 -0
  239. package/src/team/domain-templates/research.json +44 -0
  240. package/src/team/domain-templates/software.json +40 -0
  241. package/src/team/generator.js +440 -3
  242. package/src/team/modify.js +203 -0
  243. package/src/team/schemas.js +48 -0
  244. package/src/update-apply.js +19 -3
  245. package/src/dashboard-charts.js +0 -239
@@ -0,0 +1,50 @@
1
+ // IJFW v1.5.1 -- memory migration 009: M1 obsidian-index backfill.
2
+ //
3
+ // Source authority: Trident r5 finding 1.2 (HIGH).
4
+ //
5
+ // Round-4 Fix-1 (commit 3218812) wired M1 (Obsidian wikilink/tag/meta
6
+ // indexing -- indexObsidianRelations) and M2 (A-Mem auto-linking -- autoLink)
7
+ // into the production memory-write path (handleStore, search.js#autoIndex).
8
+ // But the fix is forward-only: every memory_entries row written during
9
+ // v1.5.0 -- when M1/M2 were bypassed -- has empty memory_links / memory_tags
10
+ // / memory_meta. An existing user upgrading to v1.5.1 got auto-linking +
11
+ // wikilink indexing only on NEW entries; their accumulated memory stayed
12
+ // un-indexed.
13
+ //
14
+ // This migration runs a ONE-TIME M1 backfill: it walks every existing
15
+ // memory_entries row and runs indexObsidianRelations over its body. M1 is:
16
+ // - free -- pure markdown parse, zero LLM / network / cost
17
+ // - idempotent -- indexObsidianRelations does DELETE-then-INSERT per id,
18
+ // so re-applying produces identical aux rows
19
+ // which is exactly what makes it safe to run inside a schema migration: it
20
+ // runs once (user_version gates re-application), deterministically, and a
21
+ // crash rolls the whole txn back to user_version 8.
22
+ //
23
+ // M2 (autoLink) is NOT backfilled here -- it makes one LLM call per row and
24
+ // can cost real money over a large memory. M2 backfill is opt-in via the
25
+ // `ijfw memory reindex --m2` CLI verb, which is budget-gated (respects
26
+ // IJFW_AUTOLINK_BUDGET_USD / IJFW_AUTOLINK_OFF / IJFW_AUTOLINK_BACKFILL).
27
+ //
28
+ // Ordering: migration 001 creates memory_entries; migration 006 creates
29
+ // memory_links / memory_tags / memory_meta. Both run before 009, so by the
30
+ // time up() executes the source table and the three aux tables all exist.
31
+ //
32
+ // Crash safety: the migration runner wraps up() in BEGIN IMMEDIATE.
33
+ // backfillObsidianIndex's per-row indexObsidianRelations opens a nested
34
+ // SQLite transaction (savepoint) -- valid inside the outer txn -- and the
35
+ // whole thing rolls back to user_version 8 on any failure.
36
+
37
+ import { backfillObsidianIndex } from '../obsidian-parser.js';
38
+
39
+ export const VERSION = 9;
40
+ export const DESCRIPTION =
41
+ 'memory v1.5.1 -- one-time M1 obsidian-index backfill for pre-fix rows (Trident r5 1.2)';
42
+
43
+ export function up(db) {
44
+ // backfillObsidianIndex tolerates a missing memory_entries table (returns
45
+ // zero counts) so a brand-new db that jumps straight to v9 is a clean
46
+ // no-op -- there are no pre-fix rows to backfill on a fresh install.
47
+ backfillObsidianIndex(db);
48
+ }
49
+
50
+ export default { version: VERSION, description: DESCRIPTION, up };
@@ -0,0 +1,152 @@
1
+ // IJFW v1.5.0 -- Obsidian-grade markdown extractor.
2
+ //
3
+ // Pulls three structured signals out of memory body text:
4
+ // 1. [[wikilinks]] -> links: [{ target, line }]
5
+ // 2. #nested/tags -> tags: [{ path, depth }]
6
+ // 3. [key:: value] (Dataview) -> meta: [{ key, value }]
7
+ //
8
+ // Code fences and inline-code spans are masked before extraction so that
9
+ // example syntax never produces fake edges. DB writes happen in
10
+ // indexObsidianRelations (idempotent re-index: clears prior rows for the
11
+ // memory id before re-inserting).
12
+
13
+ const FENCE_RE = /```[\s\S]*?```/g;
14
+ const INLINE_CODE_RE = /`[^`\n]+`/g;
15
+ // eslint-disable-next-line security/detect-unsafe-regex -- parses developer-authored markdown notes on local disk; negated [^\]\n] classes bound match to one line per token
16
+ const WIKILINK_RE = /\[\[([^\]\n|]+)(?:\|[^\]\n]+)?\]\]/g;
17
+ const TAG_RE = /(?:^|[^\w&])#([\w/-]+)/g;
18
+ const META_RE = /\[([A-Za-z_][\w-]*)::\s*([^\]\n]+?)\]/g;
19
+
20
+ function maskCode(text) {
21
+ return text
22
+ .replace(FENCE_RE, (m) => ' '.repeat(m.length))
23
+ .replace(INLINE_CODE_RE, (m) => ' '.repeat(m.length));
24
+ }
25
+
26
+ function lineOf(masked, idx) {
27
+ let line = 1;
28
+ for (let i = 0; i < idx; i++) if (masked.charCodeAt(i) === 10) line++;
29
+ return line;
30
+ }
31
+
32
+ function normaliseLinkTarget(raw) {
33
+ return raw
34
+ .trim()
35
+ .toLowerCase()
36
+ .replace(/\s+/g, '-')
37
+ .replace(/[^a-z0-9/_-]/g, '');
38
+ }
39
+
40
+ export function parseObsidian(text) {
41
+ if (typeof text !== 'string' || text.length === 0) {
42
+ return { links: [], tags: [], meta: [] };
43
+ }
44
+ const masked = maskCode(text);
45
+
46
+ const links = [];
47
+ for (const m of masked.matchAll(WIKILINK_RE)) {
48
+ const target = normaliseLinkTarget(m[1]);
49
+ if (target) links.push({ target, line: lineOf(masked, m.index) });
50
+ }
51
+
52
+ const tagSet = new Map();
53
+ for (const m of masked.matchAll(TAG_RE)) {
54
+ const path = m[1].toLowerCase().replace(/^\/+|\/+$/g, '');
55
+ if (!path) continue;
56
+ const depth = path.split('/').length;
57
+ if (!tagSet.has(path)) tagSet.set(path, { path, depth });
58
+ }
59
+
60
+ const meta = [];
61
+ for (const m of masked.matchAll(META_RE)) {
62
+ meta.push({ key: m[1].toLowerCase(), value: m[2].trim() });
63
+ }
64
+
65
+ return { links, tags: [...tagSet.values()], meta };
66
+ }
67
+
68
+ export function indexObsidianRelations(db, memoryId, text) {
69
+ const parsed = parseObsidian(text);
70
+ const insLink = db.prepare(
71
+ 'INSERT OR IGNORE INTO memory_links (from_id, to_target, line) VALUES (?, ?, ?)',
72
+ );
73
+ const insTag = db.prepare(
74
+ 'INSERT OR IGNORE INTO memory_tags (memory_id, tag_path, depth) VALUES (?, ?, ?)',
75
+ );
76
+ const insMeta = db.prepare(
77
+ 'INSERT OR IGNORE INTO memory_meta (memory_id, key, value) VALUES (?, ?, ?)',
78
+ );
79
+ const tx = db.transaction(() => {
80
+ db.prepare('DELETE FROM memory_links WHERE from_id=?').run(memoryId);
81
+ db.prepare('DELETE FROM memory_tags WHERE memory_id=?').run(memoryId);
82
+ db.prepare('DELETE FROM memory_meta WHERE memory_id=?').run(memoryId);
83
+ for (const l of parsed.links) insLink.run(memoryId, l.target, l.line);
84
+ for (const t of parsed.tags) insTag.run(memoryId, t.path, t.depth);
85
+ for (const m of parsed.meta) insMeta.run(memoryId, m.key, m.value);
86
+ });
87
+ tx();
88
+ return parsed;
89
+ }
90
+
91
+ // v1.5.1 R5-1.2 -- one-time M1 backfill for memory written during v1.5.0,
92
+ // when indexObsidianRelations was NOT wired into the production write path.
93
+ // Round-4 Fix-1 (commit 3218812) wired M1+M2 into handleStore/autoIndex but
94
+ // forward-only: rows already in memory_entries have empty memory_links /
95
+ // memory_tags / memory_meta. This walks EVERY row and re-runs M1 over it.
96
+ //
97
+ // Safe to run over everything:
98
+ // - free -- pure markdown parse, zero LLM / network
99
+ // - idempotent-- indexObsidianRelations clears prior aux rows per id before
100
+ // re-inserting, so a re-run produces identical state
101
+ //
102
+ // The walk reads ids in batches so a very large memory_entries doesn't pin
103
+ // the whole table in memory; each row's indexObsidianRelations call carries
104
+ // its own transaction (DELETE-then-INSERT) so a single bad row never aborts
105
+ // the rest of the backfill.
106
+ //
107
+ // Returns { rows, links, tags, meta } -- counts re-indexed across the run.
108
+ export function backfillObsidianIndex(db, opts = {}) {
109
+ if (!db || typeof db.prepare !== 'function') {
110
+ throw new Error('backfillObsidianIndex: db handle is invalid.');
111
+ }
112
+ const batchSize = Math.max(1, opts.batchSize || 500);
113
+ const result = { rows: 0, links: 0, tags: 0, meta: 0, errors: 0 };
114
+ let lastId = 0;
115
+ // eslint-disable-next-line no-constant-condition
116
+ while (true) {
117
+ let batch;
118
+ try {
119
+ batch = db
120
+ .prepare(
121
+ 'SELECT id, body FROM memory_entries WHERE id > ? ORDER BY id ASC LIMIT ?',
122
+ )
123
+ .all(lastId, batchSize);
124
+ } catch {
125
+ // memory_entries missing (fresh db before migration 001) -- nothing to do.
126
+ break;
127
+ }
128
+ if (!batch || batch.length === 0) break;
129
+ for (const row of batch) {
130
+ lastId = row.id;
131
+ if (typeof row.body !== 'string' || row.body.length === 0) continue;
132
+ try {
133
+ const parsed = indexObsidianRelations(db, String(row.id), row.body);
134
+ result.rows += 1;
135
+ result.links += parsed.links.length;
136
+ result.tags += parsed.tags.length;
137
+ result.meta += parsed.meta.length;
138
+ } catch (e) {
139
+ result.errors += 1;
140
+ try {
141
+ console.error(
142
+ '[obsidian] backfill failed for id', row.id, ':', e?.message || e,
143
+ );
144
+ } catch { /* never throw out of the backfill */ }
145
+ }
146
+ }
147
+ if (batch.length < batchSize) break;
148
+ }
149
+ return result;
150
+ }
151
+
152
+ export default { parseObsidian, indexObsidianRelations, backfillObsidianIndex };
@@ -0,0 +1,86 @@
1
+ // IJFW v1.5.0 -- Dataview-grade declarative query for memory_entries.
2
+ //
3
+ // Supported grammar (v1 — intentionally minimal; expand in v1.6+):
4
+ // tag = #path[/sub]* prefix-matches memory_tags.tag_path
5
+ // linked_to = "target" matches memory_links.to_target
6
+ // created_after = <unix-secs> memory_entries.created_at > N
7
+ // created_before = <unix-secs> memory_entries.created_at < N
8
+ //
9
+ // Multiple filters AND together via case-insensitive " and ".
10
+ // Whitespace-tolerant. Unrecognised clauses are silently skipped
11
+ // (caller can inspect parsed.filters for __unrecognised entries).
12
+
13
+ const CLAUSE_AND = /\s+and\s+/i;
14
+ const TAG_RE = /^\s*tag\s*=\s*#?([\w/_-]+)\s*$/i;
15
+ const LINKED_RE = /^\s*linked_to\s*=\s*"([^"\n]+)"\s*$/i;
16
+ const CREATED_AFTER_RE = /^\s*created_after\s*=\s*(\d+)\s*$/i;
17
+ const CREATED_BEFORE_RE = /^\s*created_before\s*=\s*(\d+)\s*$/i;
18
+
19
+ export function parseDataviewQuery(input) {
20
+ const clauses = String(input || '').split(CLAUSE_AND);
21
+ const out = { filters: [] };
22
+ for (const raw of clauses) {
23
+ const c = raw.trim();
24
+ if (!c) continue;
25
+ let m;
26
+ if ((m = c.match(TAG_RE))) {
27
+ out.tag = m[1].toLowerCase().replace(/^\/+|\/+$/g, '');
28
+ continue;
29
+ }
30
+ if ((m = c.match(LINKED_RE))) {
31
+ out.filters.push({ field: 'linked_to', op: '=', value: m[1] });
32
+ continue;
33
+ }
34
+ if ((m = c.match(CREATED_AFTER_RE))) {
35
+ out.filters.push({ field: 'created_after', op: '=', value: Number(m[1]) });
36
+ continue;
37
+ }
38
+ if ((m = c.match(CREATED_BEFORE_RE))) {
39
+ out.filters.push({ field: 'created_before', op: '=', value: Number(m[1]) });
40
+ continue;
41
+ }
42
+ out.filters.push({ field: '__unrecognised', op: '=', value: c });
43
+ }
44
+ return out;
45
+ }
46
+
47
+ export function runDataviewQuery(db, parsed) {
48
+ const where = [];
49
+ const params = [];
50
+ let join = '';
51
+ if (parsed.tag) {
52
+ join += ' JOIN memory_tags t ON t.memory_id = e.id ';
53
+ where.push('(t.tag_path = ? OR t.tag_path LIKE ?)');
54
+ params.push(parsed.tag, `${parsed.tag}/%`);
55
+ }
56
+ for (const f of parsed.filters) {
57
+ switch (f.field) {
58
+ case 'linked_to':
59
+ join += ' JOIN memory_links l ON l.from_id = e.id ';
60
+ where.push('l.to_target = ?');
61
+ params.push(f.value);
62
+ break;
63
+ case 'created_after':
64
+ where.push('e.created_at > ?');
65
+ params.push(f.value);
66
+ break;
67
+ case 'created_before':
68
+ where.push('e.created_at < ?');
69
+ params.push(f.value);
70
+ break;
71
+ default: /* __unrecognised silently skipped */
72
+ }
73
+ }
74
+ // Note: memory_entries (migration 001 schema) has columns:
75
+ // id (INTEGER PK), body, source, session_id, created_at. No title column.
76
+ // memory_links/_tags/_meta store TEXT memory_id — SQLite coerces across
77
+ // the type boundary at JOIN time (SQLite is permissive without strict FKs).
78
+ const sql = `SELECT DISTINCT e.id, e.body, e.source, e.created_at
79
+ FROM memory_entries e ${join}
80
+ ${where.length ? 'WHERE ' + where.join(' AND ') : ''}
81
+ ORDER BY e.created_at DESC`;
82
+ const rows = db.prepare(sql).all(...params);
83
+ return { rows, rowCount: rows.length, parsed, sql };
84
+ }
85
+
86
+ export default { parseDataviewQuery, runDataviewQuery };
@@ -31,6 +31,13 @@ import { readFileSync, existsSync, mkdirSync } from 'node:fs';
31
31
  import { dirname, join, resolve, normalize, isAbsolute } from 'node:path';
32
32
 
33
33
  import { expandQuery } from '../compute/synonyms.js';
34
+ import { loadMigrations } from './migration-runner.js';
35
+ // v1.5.1 R4-H2 — auto-index rows must flow through indexEntry so the
36
+ // v1.5.0 memory-moat (M1 Obsidian indexing + M2 A-Mem auto-linking) fires
37
+ // for warm-tier rebuilds, not just the benchmark harness. obsidian-parser
38
+ // is imported directly so M1 runs synchronously inside the same txn batch.
39
+ import { indexObsidianRelations } from './obsidian-parser.js';
40
+ import { autoLink } from './auto-linker.js';
34
41
 
35
42
  const MAX_RESULTS = 50;
36
43
  const SNIPPET_HALF = 60;
@@ -50,20 +57,16 @@ try {
50
57
  }
51
58
 
52
59
  // Resolve migration modules synchronously at module load via top-level
53
- // await. Replayed inside searchMemory's sync path. Keep in lockstep with
54
- // ./migrations/.
55
- const MEMORY_MIGRATIONS = await loadMemoryMigrationsSync();
56
-
57
- async function loadMemoryMigrationsSync() {
58
- const v1 = await import('./migrations/001-fts5-init.js');
59
- const v2 = await import('./migrations/002-tier-semantic.js');
60
- const v3 = await import('./migrations/003-stale-candidate.js');
61
- return [
62
- { version: v1.VERSION, description: v1.DESCRIPTION, up: v1.up },
63
- { version: v2.VERSION, description: v2.DESCRIPTION, up: v2.up },
64
- { version: v3.VERSION, description: v3.DESCRIPTION, up: v3.up },
65
- ].sort((a, b) => a.version - b.version);
66
- }
60
+ // await. Replayed inside searchMemory's sync path.
61
+ //
62
+ // v1.5.1 W3.B: discovery is delegated to memory/migration-runner.js
63
+ // (readdirSync over ./migrations/) so a single source of truth governs
64
+ // which migrations search.js knows about. Prior to this, search.js
65
+ // carried its OWN hardcoded list -- the v1.5.0 INT.7 hotfix patched
66
+ // the symptom (006/007/008 missing); this kills the dual-registry bug
67
+ // class outright. Drop migration 009 into ./migrations/, and search.js
68
+ // will pick it up automatically.
69
+ const MEMORY_MIGRATIONS = await loadMigrations();
67
70
 
68
71
  function highestMigrationVersion() {
69
72
  if (!MEMORY_MIGRATIONS.length) return 0;
@@ -210,12 +213,20 @@ function runMemoryMigrationsSync(db, currentVersion, targetVersion) {
210
213
 
211
214
  function autoIndex(db, files) {
212
215
  let n = 0;
216
+ // v1.5.1 R4-H2 — capture the rowid of every inserted entry so the
217
+ // memory-moat aux indexing (M1 Obsidian relations, M2 auto-link) can run
218
+ // over the warm-tier rebuild, not just the benchmark harness. The bulk
219
+ // INSERT stays in one transaction for FTS write performance; M1/M2 run
220
+ // AFTER commit so a parse/link failure can never abort the rebuild.
221
+ const inserted = [];
213
222
  const txfn = db.transaction((batch) => {
214
223
  const stmt = db.prepare(
215
224
  'INSERT INTO memory_entries (body, source, session_id, created_at) VALUES (?, ?, ?, ?)'
216
225
  );
217
226
  for (const item of batch) {
218
- stmt.run(item.body, item.source, null, item.created_at);
227
+ const info = stmt.run(item.body, item.source, null, item.created_at);
228
+ const id = info && info.lastInsertRowid != null ? Number(info.lastInsertRowid) : null;
229
+ inserted.push({ id, body: item.body });
219
230
  n++;
220
231
  }
221
232
  });
@@ -232,6 +243,26 @@ function autoIndex(db, files) {
232
243
  }
233
244
  if (batch.length === 0) return 0;
234
245
  try { txfn.immediate(batch); } catch { /* one bad batch should not abort the search */ }
246
+
247
+ // v1.5.1 R4-H2 — M1: Obsidian wikilink/tag/meta indexing into
248
+ // memory_links/_tags/_meta. Synchronous + idempotent (indexObsidianRelations
249
+ // clears prior rows for the id before re-inserting). Best-effort: a missing
250
+ // migration-006 schema or a parse failure must never break the search path.
251
+ // M2: A-Mem auto-linking — fire-and-forget, env-gated (IJFW_AUTOLINK_OFF),
252
+ // budget-capped (IJFW_AUTOLINK_BUDGET_USD); returns skipped cleanly when no
253
+ // API key, so a bulk rebuild without credentials does no LLM work.
254
+ for (const row of inserted) {
255
+ if (row.id == null) continue;
256
+ try {
257
+ indexObsidianRelations(db, String(row.id), row.body);
258
+ } catch { /* M1 best-effort -- never abort the search */ }
259
+ try {
260
+ const p = autoLink(db, { id: row.id, body: row.body });
261
+ if (p && typeof p.catch === 'function') p.catch(() => {});
262
+ // expose for tests that want deterministic completion
263
+ autoIndex.__lastAutoLinkPromise = p;
264
+ } catch { /* M2 dispatch best-effort */ }
265
+ }
235
266
  return n;
236
267
  }
237
268