@rasensio/aidlc 1.66.0 → 1.72.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 (149) hide show
  1. package/README.md +2 -1
  2. package/dist/autopilot/driver.d.ts +35 -2
  3. package/dist/autopilot/driver.d.ts.map +1 -1
  4. package/dist/autopilot/driver.js +650 -27
  5. package/dist/autopilot/driver.js.map +1 -1
  6. package/dist/autopilot/presets.d.ts.map +1 -1
  7. package/dist/autopilot/presets.js +5 -0
  8. package/dist/autopilot/presets.js.map +1 -1
  9. package/dist/autopilot/result.d.ts +133 -0
  10. package/dist/autopilot/result.d.ts.map +1 -0
  11. package/dist/autopilot/result.js +431 -0
  12. package/dist/autopilot/result.js.map +1 -0
  13. package/dist/autopilot/run.d.ts +2 -0
  14. package/dist/autopilot/run.d.ts.map +1 -1
  15. package/dist/autopilot/run.js +33 -6
  16. package/dist/autopilot/run.js.map +1 -1
  17. package/dist/autopilot/state.d.ts +51 -0
  18. package/dist/autopilot/state.d.ts.map +1 -1
  19. package/dist/autopilot/state.js.map +1 -1
  20. package/dist/autopilot/typed.d.ts +98 -0
  21. package/dist/autopilot/typed.d.ts.map +1 -0
  22. package/dist/autopilot/typed.js +122 -0
  23. package/dist/autopilot/typed.js.map +1 -0
  24. package/dist/cli.d.ts.map +1 -1
  25. package/dist/cli.js +2 -0
  26. package/dist/cli.js.map +1 -1
  27. package/dist/commands/hook.d.ts +37 -0
  28. package/dist/commands/hook.d.ts.map +1 -0
  29. package/dist/commands/hook.js +208 -0
  30. package/dist/commands/hook.js.map +1 -0
  31. package/dist/commands/init.d.ts +18 -0
  32. package/dist/commands/init.d.ts.map +1 -1
  33. package/dist/commands/init.js +66 -3
  34. package/dist/commands/init.js.map +1 -1
  35. package/dist/commands/knowledge.d.ts.map +1 -1
  36. package/dist/commands/knowledge.js +8 -4
  37. package/dist/commands/knowledge.js.map +1 -1
  38. package/dist/commands/roadmap.d.ts +20 -3
  39. package/dist/commands/roadmap.d.ts.map +1 -1
  40. package/dist/commands/roadmap.js +60 -15
  41. package/dist/commands/roadmap.js.map +1 -1
  42. package/dist/commands/update.d.ts.map +1 -1
  43. package/dist/commands/update.js +8 -2
  44. package/dist/commands/update.js.map +1 -1
  45. package/dist/compile/adapters/claude-code.d.ts.map +1 -1
  46. package/dist/compile/adapters/claude-code.js +35 -4
  47. package/dist/compile/adapters/claude-code.js.map +1 -1
  48. package/dist/compile/adapters/codex.d.ts +12 -0
  49. package/dist/compile/adapters/codex.d.ts.map +1 -1
  50. package/dist/compile/adapters/codex.js +58 -8
  51. package/dist/compile/adapters/codex.js.map +1 -1
  52. package/dist/compile/loaders.d.ts.map +1 -1
  53. package/dist/compile/loaders.js +2 -0
  54. package/dist/compile/loaders.js.map +1 -1
  55. package/dist/compile/skill-metadata.d.ts +82 -0
  56. package/dist/compile/skill-metadata.d.ts.map +1 -0
  57. package/dist/compile/skill-metadata.js +156 -0
  58. package/dist/compile/skill-metadata.js.map +1 -0
  59. package/dist/concurrency/claim-store.d.ts +17 -0
  60. package/dist/concurrency/claim-store.d.ts.map +1 -1
  61. package/dist/concurrency/claim-store.js +42 -2
  62. package/dist/concurrency/claim-store.js.map +1 -1
  63. package/dist/concurrency/common-dir.d.ts +15 -0
  64. package/dist/concurrency/common-dir.d.ts.map +1 -1
  65. package/dist/concurrency/common-dir.js +39 -0
  66. package/dist/concurrency/common-dir.js.map +1 -1
  67. package/dist/core/types.d.ts +17 -0
  68. package/dist/core/types.d.ts.map +1 -1
  69. package/dist/cost/hooks/arm.d.ts +9 -1
  70. package/dist/cost/hooks/arm.d.ts.map +1 -1
  71. package/dist/cost/hooks/arm.js +16 -2
  72. package/dist/cost/hooks/arm.js.map +1 -1
  73. package/dist/cost/providers/codex.d.ts +13 -0
  74. package/dist/cost/providers/codex.d.ts.map +1 -1
  75. package/dist/cost/providers/codex.js +74 -2
  76. package/dist/cost/providers/codex.js.map +1 -1
  77. package/dist/doctor/migrations/index.d.ts.map +1 -1
  78. package/dist/doctor/migrations/index.js +5 -0
  79. package/dist/doctor/migrations/index.js.map +1 -1
  80. package/dist/doctor/migrations/roadmap-mermaid-graphs.d.ts +22 -0
  81. package/dist/doctor/migrations/roadmap-mermaid-graphs.d.ts.map +1 -0
  82. package/dist/doctor/migrations/roadmap-mermaid-graphs.js +74 -0
  83. package/dist/doctor/migrations/roadmap-mermaid-graphs.js.map +1 -0
  84. package/dist/hooks/guard.d.ts +41 -0
  85. package/dist/hooks/guard.d.ts.map +1 -0
  86. package/dist/hooks/guard.js +108 -0
  87. package/dist/hooks/guard.js.map +1 -0
  88. package/dist/hooks/install.d.ts +78 -0
  89. package/dist/hooks/install.d.ts.map +1 -0
  90. package/dist/hooks/install.js +207 -0
  91. package/dist/hooks/install.js.map +1 -0
  92. package/dist/hooks/lifecycle-context.d.ts +65 -0
  93. package/dist/hooks/lifecycle-context.d.ts.map +1 -0
  94. package/dist/hooks/lifecycle-context.js +212 -0
  95. package/dist/hooks/lifecycle-context.js.map +1 -0
  96. package/dist/knowledge/engine.d.ts +19 -2
  97. package/dist/knowledge/engine.d.ts.map +1 -1
  98. package/dist/knowledge/engine.js +35 -11
  99. package/dist/knowledge/engine.js.map +1 -1
  100. package/dist/knowledge/index/knowledge-index.d.ts +28 -2
  101. package/dist/knowledge/index/knowledge-index.d.ts.map +1 -1
  102. package/dist/knowledge/index/sqlite-index.d.ts +19 -1
  103. package/dist/knowledge/index/sqlite-index.d.ts.map +1 -1
  104. package/dist/knowledge/index/sqlite-index.js +66 -5
  105. package/dist/knowledge/index/sqlite-index.js.map +1 -1
  106. package/dist/knowledge/source-fingerprint.d.ts +71 -0
  107. package/dist/knowledge/source-fingerprint.d.ts.map +1 -0
  108. package/dist/knowledge/source-fingerprint.js +105 -0
  109. package/dist/knowledge/source-fingerprint.js.map +1 -0
  110. package/dist/knowledge/sync.d.ts +12 -1
  111. package/dist/knowledge/sync.d.ts.map +1 -1
  112. package/dist/knowledge/sync.js +23 -3
  113. package/dist/knowledge/sync.js.map +1 -1
  114. package/dist/knowledge/types.d.ts +10 -1
  115. package/dist/knowledge/types.d.ts.map +1 -1
  116. package/dist/knowledge/types.js +10 -1
  117. package/dist/knowledge/types.js.map +1 -1
  118. package/dist/menu/roster.d.ts.map +1 -1
  119. package/dist/menu/roster.js +2 -0
  120. package/dist/menu/roster.js.map +1 -1
  121. package/dist/roadmap/graph.d.ts +73 -28
  122. package/dist/roadmap/graph.d.ts.map +1 -1
  123. package/dist/roadmap/graph.js +184 -125
  124. package/dist/roadmap/graph.js.map +1 -1
  125. package/dist/roadmap/next.d.ts +5 -0
  126. package/dist/roadmap/next.d.ts.map +1 -1
  127. package/dist/roadmap/next.js +7 -1
  128. package/dist/roadmap/next.js.map +1 -1
  129. package/dist/roadmap/open-browser.d.ts +34 -0
  130. package/dist/roadmap/open-browser.d.ts.map +1 -0
  131. package/dist/roadmap/open-browser.js +40 -0
  132. package/dist/roadmap/open-browser.js.map +1 -0
  133. package/dist/roadmap/paths.d.ts +5 -1
  134. package/dist/roadmap/paths.d.ts.map +1 -1
  135. package/dist/roadmap/paths.js +5 -9
  136. package/dist/roadmap/paths.js.map +1 -1
  137. package/dist/roadmap/view-assets.d.ts +18 -0
  138. package/dist/roadmap/view-assets.d.ts.map +1 -0
  139. package/dist/roadmap/view-assets.js +301 -0
  140. package/dist/roadmap/view-assets.js.map +1 -0
  141. package/dist/roadmap/view-html.d.ts +37 -0
  142. package/dist/roadmap/view-html.d.ts.map +1 -0
  143. package/dist/roadmap/view-html.js +317 -0
  144. package/dist/roadmap/view-html.js.map +1 -0
  145. package/dist/roadmap/view-model.d.ts +93 -0
  146. package/dist/roadmap/view-model.d.ts.map +1 -0
  147. package/dist/roadmap/view-model.js +194 -0
  148. package/dist/roadmap/view-model.js.map +1 -0
  149. package/package.json +2 -2
@@ -0,0 +1,212 @@
1
+ /**
2
+ * What a harness hook needs to know about the lifecycle: which instance is
3
+ * claimed in this checkout, what phase it is in, and what comes next.
4
+ *
5
+ * Read-only by contract (hooks-carry-lifecycle-state/AC-8). Everything here runs
6
+ * inside a `SessionStart` or `PreToolUse` hook, on every session start or every
7
+ * edit, so it reads only the claims and the claimed instances' own files — never
8
+ * every instance, ledger or cache. `aidlc status --json` reads all of those and
9
+ * took 3.5 s on this repository; the CLI itself starts in 0.16 s (AC-10).
10
+ *
11
+ * Requirements: hooks-carry-lifecycle-state/AC-1..AC-9
12
+ * Design: design.md C2
13
+ *
14
+ * @module
15
+ */
16
+ import { existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs';
17
+ import { join, resolve } from 'node:path';
18
+ import { loadTemplates as loadBuiltInTemplates } from '@rasensio/aidlc-content';
19
+ import { nextStep } from '../core/lifecycle.js';
20
+ import { loadTemplates, resolveTemplate } from '../core/template-resolver.js';
21
+ import { loadSnapshot } from '../state/snapshot-store.js';
22
+ import { parseTasks } from '../tasks/parser.js';
23
+ import { gitDirs } from '../concurrency/common-dir.js';
24
+ import { readClaims } from '../concurrency/claim-store.js';
25
+ /** Phases in which code is not yet written — what the phase guard asks about. */
26
+ export const PLANNING_PHASES = ['ideation', 'requirements', 'design'];
27
+ /** Longest line either hook prints (AC-9). */
28
+ export const MAX_LINE = 200;
29
+ function canonical(path) {
30
+ try {
31
+ return realpathSync(path);
32
+ }
33
+ catch {
34
+ return resolve(path);
35
+ }
36
+ }
37
+ /**
38
+ * Instances claimed in this checkout, sorted by name (AC-6).
39
+ *
40
+ * Held here means recorded from this working directory: the claim's
41
+ * `worktree_path` is this checkout, or it has none and this is the primary
42
+ * checkout. Staleness is deliberately not consulted — claims renew only at phase
43
+ * transitions, and one phase routinely outlasts the 60-minute window, so a
44
+ * live-only rule would go quiet on a compaction deep into implementation, the
45
+ * case this hook exists for (design D3). Session ids are not compared either:
46
+ * they change across `startup`.
47
+ */
48
+ export function claimsHere(projectRoot) {
49
+ const git = gitDirs(projectRoot);
50
+ const here = canonical(projectRoot);
51
+ const held = [];
52
+ for (const [name, claim] of readClaims(projectRoot, git?.common ?? null)) {
53
+ if (!existsSync(join(projectRoot, '.aidlc', 'state', name, 'instance.yaml')))
54
+ continue;
55
+ // `aidlc claim` records `worktree_path` only from a linked worktree, so a
56
+ // claim without one was taken in the primary checkout. Outside git the claims
57
+ // live in this checkout's own instance.yaml files. Not `dirname(common dir)`:
58
+ // the project can sit below the repository root, and then that is not here.
59
+ const heldHere = claim.worktree_path ? canonical(claim.worktree_path) === here : git === null || !git.linked;
60
+ if (heldHere)
61
+ held.push(name);
62
+ }
63
+ return held.sort();
64
+ }
65
+ /** `current_phase` of one instance, or null when its state cannot be read. */
66
+ export function currentPhase(projectRoot, name) {
67
+ try {
68
+ const state = loadSnapshot(join(projectRoot, '.aidlc', 'state'), name, 'instance.yaml');
69
+ return typeof state.current_phase === 'string' ? state.current_phase : null;
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ }
75
+ /** Built-in templates plus the project's own, as `aidlc continue` loads them. */
76
+ function loadAllTemplates(projectRoot) {
77
+ const templates = new Map();
78
+ for (const tpl of loadBuiltInTemplates()) {
79
+ templates.set(tpl.name, tpl.raw);
80
+ }
81
+ const customDir = join(projectRoot, '.aidlc', 'templates');
82
+ if (existsSync(customDir)) {
83
+ for (const [name, tpl] of loadTemplates(customDir))
84
+ templates.set(name, tpl);
85
+ }
86
+ return templates;
87
+ }
88
+ /**
89
+ * Phase files keyed by the phase named in the file name, an unreadable one
90
+ * skipped — both exactly as `aidlc continue` does, so the next step stays the one
91
+ * it reports (AC-2).
92
+ */
93
+ function loadPhaseStates(stateDir, name) {
94
+ const phases = new Map();
95
+ for (const entry of readdirSync(join(stateDir, name))) {
96
+ if (!entry.startsWith('phase-') || !entry.endsWith('.yaml'))
97
+ continue;
98
+ try {
99
+ phases.set(entry.slice('phase-'.length, -'.yaml'.length), loadSnapshot(stateDir, name, entry));
100
+ }
101
+ catch {
102
+ // skipped, as continue.ts skips it
103
+ }
104
+ }
105
+ return phases;
106
+ }
107
+ /**
108
+ * One instance's phase and next step, or null when any of its state cannot be
109
+ * read (AC-7) — a hook must never stop a session starting.
110
+ */
111
+ export function instanceContext(projectRoot, name) {
112
+ try {
113
+ const stateDir = join(projectRoot, '.aidlc', 'state');
114
+ const state = loadSnapshot(stateDir, name, 'instance.yaml');
115
+ const templates = loadAllTemplates(projectRoot);
116
+ const raw = templates.get(state.template);
117
+ if (!raw)
118
+ return null;
119
+ const step = nextStep(state, loadPhaseStates(stateDir, name), resolveTemplate(raw, templates));
120
+ return {
121
+ name,
122
+ template: state.template,
123
+ scope: state.scope,
124
+ phase: String(state.current_phase),
125
+ step,
126
+ openTask: state.current_phase === 'implementation' ? lowestOpenTask(stateDir, name) : null,
127
+ };
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ }
133
+ function lowestOpenTask(stateDir, name) {
134
+ let content;
135
+ try {
136
+ content = readFileSync(join(stateDir, name, 'tasks.md'), 'utf8');
137
+ }
138
+ catch {
139
+ return null;
140
+ }
141
+ // parseTasks orders by id, so the first open one is the lowest-numbered.
142
+ const task = parseTasks(content).tasks.find((t) => t.state === 'open');
143
+ return task ? { id: task.id, title: task.title } : null;
144
+ }
145
+ /**
146
+ * One printable line: control characters (escape codes included) become spaces,
147
+ * and anything past `max` is cut with an ellipsis (AC-9).
148
+ */
149
+ export function plainLine(text, max = MAX_LINE) {
150
+ const plain = text.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ');
151
+ return plain.length <= max ? plain : `${plain.slice(0, max - 1)}…`;
152
+ }
153
+ const RESUME = 'Resume with the aidlc-continue skill.';
154
+ function nextStepLine(ctx) {
155
+ if (ctx.openTask !== null) {
156
+ // The title is what gets shortened, so the pointer at the end survives (AC-9).
157
+ const head = `Next step: T${ctx.openTask.id} — `;
158
+ const tail = `, the lowest open task in tasks.md. ${RESUME}`;
159
+ const room = MAX_LINE - head.length - tail.length;
160
+ const title = plainLine(ctx.openTask.title, Math.max(room, 1));
161
+ return `${head}${title}${tail}`;
162
+ }
163
+ switch (ctx.step.kind) {
164
+ case 'artifact':
165
+ return `Next step: produce ${ctx.step.artifact}. ${RESUME}`;
166
+ case 'phase':
167
+ return (`Next step: the ${ctx.phase} phase is complete — enter ${ctx.step.phase} ` +
168
+ `(aidlc transition ${ctx.name}). ${RESUME}`);
169
+ case 'complete':
170
+ return 'Next step: none — every phase is complete.';
171
+ }
172
+ }
173
+ /** The three lines for one claimed instance (AC-1). */
174
+ function singleLines(projectRoot, ctx) {
175
+ const skill = `.aidlc/skills/aidlc-${ctx.phase}.md`;
176
+ const pointer = existsSync(join(projectRoot, skill)) ? ` Phase instructions: ${skill}` : '';
177
+ return [
178
+ `AIDLC: instance ${ctx.name} is claimed in this checkout (${ctx.template} template, ${ctx.scope} scope).`,
179
+ `Phase: ${ctx.phase}.${pointer}`,
180
+ nextStepLine(ctx),
181
+ ].map((line) => plainLine(line));
182
+ }
183
+ /** One line naming every claimed instance, or as many as fit (AC-5, AC-9). */
184
+ function severalLine(contexts) {
185
+ const head = `AIDLC: ${contexts.length} instances are claimed in this checkout — `;
186
+ const tail = '. Run aidlc status to see which to continue.';
187
+ const named = contexts.map((c) => `${c.name} (${c.phase})`);
188
+ for (let shown = named.length; shown > 0; shown--) {
189
+ const rest = named.length - shown;
190
+ const list = named.slice(0, shown).join(', ') + (rest > 0 ? `, and ${rest} more` : '');
191
+ const line = `${head}${list}${tail}`;
192
+ if (line.length <= MAX_LINE)
193
+ return line;
194
+ }
195
+ return plainLine(`${head}${named.join(', ')}${tail}`);
196
+ }
197
+ /**
198
+ * What `aidlc hook session-start` prints: three lines for one claim, one line
199
+ * for several, nothing for none (AC-1, AC-4, AC-5). An instance whose state
200
+ * cannot be read is left out (AC-7).
201
+ */
202
+ export function sessionStartLines(projectRoot) {
203
+ const contexts = claimsHere(projectRoot)
204
+ .map((name) => instanceContext(projectRoot, name))
205
+ .filter((ctx) => ctx !== null);
206
+ if (contexts.length === 0)
207
+ return [];
208
+ if (contexts.length === 1)
209
+ return singleLines(projectRoot, contexts[0]);
210
+ return [severalLine(contexts)];
211
+ }
212
+ //# sourceMappingURL=lifecycle-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle-context.js","sourceRoot":"","sources":["../../src/hooks/lifecycle-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC9E,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,IAAI,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAGhF,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAC9E,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,8BAA8B,CAAC;AACvD,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAE3D,iFAAiF;AACjF,MAAM,CAAC,MAAM,eAAe,GAAsB,CAAC,UAAU,EAAE,cAAc,EAAE,QAAQ,CAAC,CAAC;AAEzF,8CAA8C;AAC9C,MAAM,CAAC,MAAM,QAAQ,GAAG,GAAG,CAAC;AAc5B,SAAS,SAAS,CAAC,IAAY;IAC7B,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,CAAC;AACH,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,UAAU,CAAC,WAAmB;IAC5C,MAAM,GAAG,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IACjC,MAAM,IAAI,GAAG,SAAS,CAAC,WAAW,CAAC,CAAC;IACpC,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,UAAU,CAAC,WAAW,EAAE,GAAG,EAAE,MAAM,IAAI,IAAI,CAAC,EAAE,CAAC;QACzE,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,WAAW,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,eAAe,CAAC,CAAC;YAAE,SAAS;QACvF,0EAA0E;QAC1E,8EAA8E;QAC9E,8EAA8E;QAC9E,4EAA4E;QAC5E,MAAM,QAAQ,GAAG,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,aAAa,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,GAAG,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC;QAC7G,IAAI,QAAQ;YAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChC,CAAC;IACD,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC;AACrB,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,YAAY,CAAC,WAAmB,EAAE,IAAY;IAC5D,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,YAAY,CAAgB,IAAI,CAAC,WAAW,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,eAAe,CAAC,CAAC;QACvG,OAAO,OAAO,KAAK,CAAC,aAAa,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC;IAC9E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,iFAAiF;AACjF,SAAS,gBAAgB,CAAC,WAAmB;IAC3C,MAAM,SAAS,GAAG,IAAI,GAAG,EAA4B,CAAC;IACtD,KAAK,MAAM,GAAG,IAAI,oBAAoB,EAAE,EAAE,CAAC;QACzC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,GAAkC,CAAC,CAAC;IAClE,CAAC;IACD,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC;IAC3D,IAAI,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC1B,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,aAAa,CAAC,SAAS,CAAC;YAAE,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC/E,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;GAIG;AACH,SAAS,eAAe,CAAC,QAAgB,EAAE,IAAY;IACrD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAsB,CAAC;IAC7C,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC;QACtD,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,SAAS;QACtE,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,YAAY,CAAa,QAAQ,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;QAC7G,CAAC;QAAC,MAAM,CAAC;YACP,mCAAmC;QACrC,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,WAAmB,EAAE,IAAY;IAC/D,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;QACtD,MAAM,KAAK,GAAG,YAAY,CAAgB,QAAQ,EAAE,IAAI,EAAE,eAAe,CAAC,CAAC;QAC3E,MAAM,SAAS,GAAG,gBAAgB,CAAC,WAAW,CAAC,CAAC;QAChD,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC1C,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAC;QACtB,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,eAAe,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC,CAAC;QAC/F,OAAO;YACL,IAAI;YACJ,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC;YAClC,IAAI;YACJ,QAAQ,EAAE,KAAK,CAAC,aAAa,KAAK,gBAAgB,CAAC,CAAC,CAAC,cAAc,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI;SAC3F,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CAAC,QAAgB,EAAE,IAAY;IACpD,IAAI,OAAe,CAAC;IACpB,IAAI,CAAC;QACH,OAAO,GAAG,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,CAAC;IACnE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,yEAAyE;IACzE,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC;IACvE,OAAO,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC1D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,GAAG,GAAG,QAAQ;IACpD,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,+BAA+B,EAAE,GAAG,CAAC,CAAC;IACjE,OAAO,KAAK,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC;AACrE,CAAC;AAED,MAAM,MAAM,GAAG,uCAAuC,CAAC;AAEvD,SAAS,YAAY,CAAC,GAAoB;IACxC,IAAI,GAAG,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;QAC1B,+EAA+E;QAC/E,MAAM,IAAI,GAAG,eAAe,GAAG,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAC;QACjD,MAAM,IAAI,GAAG,uCAAuC,MAAM,EAAE,CAAC;QAC7D,MAAM,IAAI,GAAG,QAAQ,GAAG,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAClD,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;QAC/D,OAAO,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,EAAE,CAAC;IAClC,CAAC;IACD,QAAQ,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACtB,KAAK,UAAU;YACb,OAAO,sBAAsB,GAAG,CAAC,IAAI,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;QAC9D,KAAK,OAAO;YACV,OAAO,CACL,kBAAkB,GAAG,CAAC,KAAK,8BAA8B,GAAG,CAAC,IAAI,CAAC,KAAK,GAAG;gBAC1E,qBAAqB,GAAG,CAAC,IAAI,MAAM,MAAM,EAAE,CAC5C,CAAC;QACJ,KAAK,UAAU;YACb,OAAO,4CAA4C,CAAC;IACxD,CAAC;AACH,CAAC;AAED,uDAAuD;AACvD,SAAS,WAAW,CAAC,WAAmB,EAAE,GAAoB;IAC5D,MAAM,KAAK,GAAG,uBAAuB,GAAG,CAAC,KAAK,KAAK,CAAC;IACpD,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,wBAAwB,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC5F,OAAO;QACL,mBAAmB,GAAG,CAAC,IAAI,iCAAiC,GAAG,CAAC,QAAQ,cAAc,GAAG,CAAC,KAAK,UAAU;QACzG,UAAU,GAAG,CAAC,KAAK,IAAI,OAAO,EAAE;QAChC,YAAY,CAAC,GAAG,CAAC;KAClB,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;AACnC,CAAC;AAED,8EAA8E;AAC9E,SAAS,WAAW,CAAC,QAA2B;IAC9C,MAAM,IAAI,GAAG,UAAU,QAAQ,CAAC,MAAM,4CAA4C,CAAC;IACnF,MAAM,IAAI,GAAG,8CAA8C,CAAC;IAC5D,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC;IAC5D,KAAK,IAAI,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,GAAG,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;QAClD,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC;QAClC,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACvF,MAAM,IAAI,GAAG,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC;QACrC,IAAI,IAAI,CAAC,MAAM,IAAI,QAAQ;YAAE,OAAO,IAAI,CAAC;IAC3C,CAAC;IACD,OAAO,SAAS,CAAC,GAAG,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC;AACxD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,WAAmB;IACnD,MAAM,QAAQ,GAAG,UAAU,CAAC,WAAW,CAAC;SACrC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,eAAe,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;SACjD,MAAM,CAAC,CAAC,GAAG,EAA0B,EAAE,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC;IACzD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACrC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,WAAW,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IACxE,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC;AACjC,CAAC"}
@@ -2,11 +2,15 @@
2
2
  * Knowledge engine facade — opens the YAML store and SQLite index together
3
3
  * and wires up the standard recovery paths:
4
4
  *
5
- * - Missing/empty index with YAML present → auto-rebuild from sources.
5
+ * - Index whose recorded source fingerprint disagrees with the sources on disk
6
+ * → auto-rebuild from sources. This covers the missing index, the empty one,
7
+ * and — the case it was added for — sources that arrived by `git pull`, merge
8
+ * or branch switch rather than through this CLI.
6
9
  * - Outdated index schema → delete and rebuild (the index is a disposable
7
10
  * cache; YAML is the source of truth — design DD2).
8
11
  *
9
- * Requirements: KG 3.2, 3.3, 11.4
12
+ * Requirements: KG 3.2, 3.3, 11.4; knowledge-index-misses-merged-yaml/AC-1, AC-2,
13
+ * AC-6, AC-11, AC-13
10
14
  *
11
15
  * @module
12
16
  */
@@ -17,6 +21,7 @@ import { KnowledgeReader } from './reader.js';
17
21
  import { ContextGenerator } from './context-generator.js';
18
22
  import { StalenessDetector } from './staleness.js';
19
23
  import { KNOWLEDGE_DIR } from './types.js';
24
+ import type { Entity } from './types.js';
20
25
  /**
21
26
  * Relative path of the knowledge directory within a project.
22
27
  *
@@ -34,6 +39,18 @@ export interface KnowledgeEngine {
34
39
  staleness: StalenessDetector;
35
40
  /** Parse warnings emitted by an auto-rebuild, if one ran. */
36
41
  warnings: string[];
42
+ /**
43
+ * The entities the open-time rebuild replaced, or `null` when the cache was served.
44
+ *
45
+ * `knowledge sync` exists to report what a merge did (KG Req 12.4), and it does that
46
+ * by diffing the pre-pull index against the post-pull YAML. Now that opening the
47
+ * engine rebuilds a stale index, that diff would always be empty — the "before" it
48
+ * read from the index would already be the "after". So the one thing that can still
49
+ * see the pre-pull state keeps it: it is captured here, on the rebuild branch only,
50
+ * where the whole YAML was about to be re-read anyway. A cache hit pays nothing
51
+ * (knowledge-index-misses-merged-yaml/AC-8, design DD7).
52
+ */
53
+ replaced: Entity[] | null;
37
54
  close(): void;
38
55
  }
39
56
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/knowledge/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,WAAW,EAAiB,MAAM,yBAAyB,CAAC;AAErE,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;;;GAMG;AACH,OAAO,EAAE,aAAa,EAAE,CAAC;AAEzB,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,eAAe,CAAC;IACxB,KAAK,EAAE,WAAW,CAAC;IACnB,MAAM,EAAE,eAAe,CAAC;IACxB,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,gBAAgB,CAAC;IAC1B,SAAS,EAAE,iBAAiB,CAAC;IAC7B,6DAA6D;IAC7D,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,IAAI,IAAI,CAAC;CACf;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,eAAe,CAgDlE;AAED,2DAA2D;AAC3D,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAE5D"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/knowledge/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,WAAW,EAAiB,MAAM,yBAAyB,CAAC;AAGrE,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC;;;;;;GAMG;AACH,OAAO,EAAE,aAAa,EAAE,CAAC;AAEzB,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,eAAe,CAAC;IACxB,KAAK,EAAE,WAAW,CAAC;IACnB,MAAM,EAAE,eAAe,CAAC;IACxB,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,gBAAgB,CAAC;IAC1B,SAAS,EAAE,iBAAiB,CAAC;IAC7B,6DAA6D;IAC7D,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB;;;;;;;;;;OAUG;IACH,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC1B,KAAK,IAAI,IAAI,CAAC;CACf;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,eAAe,CAmElE;AAED,2DAA2D;AAC3D,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAE5D"}
@@ -2,19 +2,24 @@
2
2
  * Knowledge engine facade — opens the YAML store and SQLite index together
3
3
  * and wires up the standard recovery paths:
4
4
  *
5
- * - Missing/empty index with YAML present → auto-rebuild from sources.
5
+ * - Index whose recorded source fingerprint disagrees with the sources on disk
6
+ * → auto-rebuild from sources. This covers the missing index, the empty one,
7
+ * and — the case it was added for — sources that arrived by `git pull`, merge
8
+ * or branch switch rather than through this CLI.
6
9
  * - Outdated index schema → delete and rebuild (the index is a disposable
7
10
  * cache; YAML is the source of truth — design DD2).
8
11
  *
9
- * Requirements: KG 3.2, 3.3, 11.4
12
+ * Requirements: KG 3.2, 3.3, 11.4; knowledge-index-misses-merged-yaml/AC-1, AC-2,
13
+ * AC-6, AC-11, AC-13
10
14
  *
11
15
  * @module
12
16
  */
13
17
  import { join } from 'node:path';
14
- import { existsSync, rmSync } from 'node:fs';
18
+ import { rmSync } from 'node:fs';
15
19
  import { YamlSourceStore } from './yaml-source.js';
16
20
  import { SqliteIndex, INDEX_DB_FILE } from './index/sqlite-index.js';
17
21
  import { IndexOutdatedError } from './index/knowledge-index.js';
22
+ import { fingerprintSources, rebuildFromSources } from './source-fingerprint.js';
18
23
  import { KnowledgeWriter } from './writer.js';
19
24
  import { KnowledgeReader } from './reader.js';
20
25
  import { ContextGenerator } from './context-generator.js';
@@ -38,7 +43,6 @@ export function openKnowledge(projectRoot) {
38
43
  const source = new YamlSourceStore(knowledgeDir);
39
44
  const warnings = [];
40
45
  const dbPath = join(knowledgeDir, INDEX_DB_FILE);
41
- const dbExisted = existsSync(dbPath);
42
46
  let index;
43
47
  try {
44
48
  index = new SqliteIndex(knowledgeDir);
@@ -56,13 +60,32 @@ export function openKnowledge(projectRoot) {
56
60
  throw err;
57
61
  }
58
62
  }
59
- // Fresh index but sources exist → populate from YAML (KG Req 3.2).
60
- if ((!dbExisted || index.getAllEntities().length === 0) && source.exists()) {
61
- const loaded = source.loadAll();
62
- if (loaded.entities.length > 0 || loaded.relationships.length > 0) {
63
- index.rebuild(loaded.entities, loaded.relationships);
64
- }
65
- warnings.push(...loaded.warnings);
63
+ // The sources moved since the index was built → rebuild before anything is served
64
+ // (AC-1, AC-2). Checked here, at open, rather than lazily on first query, so there
65
+ // is no window in which a reader sees a stale row.
66
+ //
67
+ // This one condition replaces `(!dbExisted || index.getAllEntities().length === 0)
68
+ // && source.exists()`, and each half of that is worth a line on why it is gone:
69
+ //
70
+ // - `!dbExisted || getAllEntities().length === 0` only ever asked "is the cache
71
+ // empty", which is why YAML arriving by merge was invisible. It also read the
72
+ // whole entities table on *every* open — `||` does not short-circuit in the
73
+ // direction it needed — so dropping it is where AC-16's headroom comes from
74
+ // (design DD7). A matching fingerprint already means these contents were built
75
+ // from these sources, so an empty index with a matching fingerprint means the
76
+ // sources are empty, and serving empty is correct.
77
+ // - `source.exists()` skipped the rebuild precisely when there was nothing to
78
+ // rebuild from, so deleting the YAML left the index serving every deleted fact
79
+ // forever. An empty tree fingerprints differently from a populated one, so the
80
+ // deletion is now indexed like any other change (AC-3, design DD8).
81
+ //
82
+ // A project that has no graph at all stamps the empty-tree fingerprint on its
83
+ // first open and is a cache hit from then on (AC-13).
84
+ let replaced = null;
85
+ if (index.getSourceFingerprint() !== fingerprintSources(source)) {
86
+ // Kept before it is overwritten, for `knowledge sync` — see `replaced` above.
87
+ replaced = index.getAllEntities();
88
+ warnings.push(...rebuildFromSources(source, index).warnings);
66
89
  }
67
90
  const writer = new KnowledgeWriter(source, index);
68
91
  const reader = new KnowledgeReader(index);
@@ -76,6 +99,7 @@ export function openKnowledge(projectRoot) {
76
99
  context,
77
100
  staleness,
78
101
  warnings,
102
+ replaced,
79
103
  close: () => index.close(),
80
104
  };
81
105
  }
@@ -1 +1 @@
1
- {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/knowledge/engine.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/knowledge/engine.ts"],"names":[],"mappings":"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"}
@@ -38,8 +38,34 @@ export interface KnowledgeIndex {
38
38
  relationshipsTo(id: string): Relationship[];
39
39
  relationshipsFor(ids: string[]): Relationship[];
40
40
  countRelationshipsByType(type: string): number;
41
- /** Replace all index contents from parsed YAML sources (KG Req 3.3). */
42
- rebuild(entities: Entity[], relationships: Relationship[]): void;
41
+ /**
42
+ * Replace all index contents from parsed YAML sources (KG Req 3.3).
43
+ *
44
+ * **An implementation MUST discard any recorded source fingerprint it is not
45
+ * given**, atomically with the contents. The freshness invariant rests on it: a
46
+ * fingerprint may exist only because a caller vouched for *these* contents, so
47
+ * that the way to get a stale cache hit is to stamp a fingerprint you did not
48
+ * earn — a much smaller thing to keep right than remembering to clear one. An
49
+ * implementation that keeps the old record instead lets `compact`, which rebuilds
50
+ * from deduplicated entities rather than a plain load, serve contents the
51
+ * fingerprint does not describe (knowledge-index-misses-merged-yaml/AC-4).
52
+ *
53
+ * @param fingerprint - Source fingerprint these contents were built from, recorded
54
+ * in the same transaction. Omit — or pass `null` — when the caller cannot vouch
55
+ * for a source state; the record is then cleared and the next open rebuilds.
56
+ */
57
+ rebuild(entities: Entity[], relationships: Relationship[], fingerprint?: string | null): void;
58
+ /**
59
+ * Fingerprint of the YAML sources these contents were built from, or `null` when
60
+ * none is recorded.
61
+ *
62
+ * "Which source state am I a cache of" is a property of a cache, so it belongs on
63
+ * this interface rather than beside it. `null` means rebuild, never "assume fresh"
64
+ * (knowledge-index-misses-merged-yaml/AC-4).
65
+ */
66
+ getSourceFingerprint(): string | null;
67
+ /** Record the fingerprint the current contents were built from. */
68
+ setSourceFingerprint(fingerprint: string): void;
43
69
  close(): void;
44
70
  }
45
71
  //# sourceMappingURL=knowledge-index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"knowledge-index.d.ts","sourceRoot":"","sources":["../../../src/knowledge/index/knowledge-index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAExD,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,8FAA8F;AAC9F,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;CAO5C;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAC1C,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAE/B,oFAAoF;IACpF,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,MAAM,EAAE,CAAC;IAChE,cAAc,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG,MAAM,EAAE,CAAC;IAChD,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,MAAM,CAAC,EAAE,YAAY,GAAG,MAAM,EAAE,CAAC;IACrE,+EAA+E;IAC/E,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC9C,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAE1C,kBAAkB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAAC;IAC5C,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,EAAE,CAAC;IAC9C,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,EAAE,CAAC;IAC5C,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,YAAY,EAAE,CAAC;IAChD,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAE/C,wEAAwE;IACxE,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,aAAa,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;IAEjE,KAAK,IAAI,IAAI,CAAC;CACf"}
1
+ {"version":3,"file":"knowledge-index.d.ts","sourceRoot":"","sources":["../../../src/knowledge/index/knowledge-index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAExD,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,8FAA8F;AAC9F,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;CAO5C;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAC1C,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAE/B,oFAAoF;IACpF,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,MAAM,EAAE,CAAC;IAChE,cAAc,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG,MAAM,EAAE,CAAC;IAChD,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,MAAM,CAAC,EAAE,YAAY,GAAG,MAAM,EAAE,CAAC;IACrE,+EAA+E;IAC/E,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC9C,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAE1C,kBAAkB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAAC;IAC5C,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,EAAE,CAAC;IAC9C,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,EAAE,CAAC;IAC5C,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,YAAY,EAAE,CAAC;IAChD,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAE/C;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,aAAa,EAAE,YAAY,EAAE,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;IAE9F;;;;;;;OAOG;IACH,oBAAoB,IAAI,MAAM,GAAG,IAAI,CAAC;IAEtC,mEAAmE;IACnE,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAEhD,KAAK,IAAI,IAAI,CAAC;CACf"}
@@ -15,6 +15,21 @@ import type { Entity, Relationship } from '../types.js';
15
15
  import type { KnowledgeIndex, SearchOptions, EntityFilter } from './knowledge-index.js';
16
16
  /** Filename of the index database inside the knowledge directory. */
17
17
  export declare const INDEX_DB_FILE = "index.db";
18
+ /**
19
+ * Schema version of the index **cache**, deliberately separate from
20
+ * `KNOWLEDGE_SCHEMA_VERSION`, which versions the YAML **sources**.
21
+ *
22
+ * One constant used to serve both. Bumping it to add this version's
23
+ * `source_fingerprint` table would also have stamped `schemaVersion: 2` into every
24
+ * YAML `_meta` the next `compact` rewrote, and an older CLI reading those files warns
25
+ * about them (`yaml-source.ts`) — user-visible churn in the source of truth caused by
26
+ * a change to a cache. Splitting them costs this comment and one import
27
+ * (knowledge-index-misses-merged-yaml/AC-14, design DD3).
28
+ *
29
+ * Version 2 adds `source_fingerprint`. An index at version 1 is discarded and rebuilt
30
+ * by the `IndexOutdatedError` path `openKnowledge` already owns (AC-11).
31
+ */
32
+ export declare const INDEX_SCHEMA_VERSION = 2;
18
33
  export declare class SqliteIndex implements KnowledgeIndex {
19
34
  private readonly db;
20
35
  private ftsAvailable;
@@ -42,7 +57,10 @@ export declare class SqliteIndex implements KnowledgeIndex {
42
57
  relationshipsTo(id: string): Relationship[];
43
58
  relationshipsFor(ids: string[]): Relationship[];
44
59
  countRelationshipsByType(type: string): number;
45
- rebuild(entities: Entity[], relationships: Relationship[]): void;
60
+ rebuild(entities: Entity[], relationships: Relationship[], fingerprint?: string | null): void;
61
+ getSourceFingerprint(): string | null;
62
+ setSourceFingerprint(fingerprint: string): void;
63
+ private writeFingerprint;
46
64
  close(): void;
47
65
  private syncFts;
48
66
  private syncFileMapping;
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite-index.d.ts","sourceRoot":"","sources":["../../../src/knowledge/index/sqlite-index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAOH,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAExD,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAExF,qEAAqE;AACrE,eAAO,MAAM,aAAa,aAAa,CAAC;AAwBxC,qBAAa,WAAY,YAAW,cAAc;IAChD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAe;IAClC,OAAO,CAAC,YAAY,CAAQ;IAC5B,OAAO,CAAC,SAAS,CAAS;IAE1B;;;OAGG;gBACS,YAAY,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM;IAYzD,OAAO,CAAC,YAAY;IAUpB,OAAO,CAAC,iBAAiB;IAWzB,OAAO,CAAC,YAAY;IAgEpB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAOzC,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAwBlC,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAQ9B,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB,GAAG,MAAM,EAAE;IAenE,OAAO,CAAC,SAAS;IA2BjB,OAAO,CAAC,UAAU;IAkBlB,cAAc,CAAC,MAAM,GAAE,YAAiB,GAAG,MAAM,EAAE;IAYnD,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,MAAM,GAAE,YAAiB,GAAG,MAAM,EAAE;IAcxE,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE;IAW7C,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAWzC,kBAAkB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI;IAU3C,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,EAAE;IAO7C,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,EAAE;IAO3C,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,YAAY,EAAE;IAY/C,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAW9C,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,aAAa,EAAE,YAAY,EAAE,GAAG,IAAI;IAiBhE,KAAK,IAAI,IAAI;IAQb,OAAO,CAAC,OAAO;IAcf,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,SAAS;CAOlB;AAED,8EAA8E;AAC9E,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKxD"}
1
+ {"version":3,"file":"sqlite-index.d.ts","sourceRoot":"","sources":["../../../src/knowledge/index/sqlite-index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAOH,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAExD,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAExF,qEAAqE;AACrE,eAAO,MAAM,aAAa,aAAa,CAAC;AAExC;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAwBtC,qBAAa,WAAY,YAAW,cAAc;IAChD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAe;IAClC,OAAO,CAAC,YAAY,CAAQ;IAC5B,OAAO,CAAC,SAAS,CAAS;IAE1B;;;OAGG;gBACS,YAAY,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM;IAYzD,OAAO,CAAC,YAAY;IAUpB,OAAO,CAAC,iBAAiB;IAWzB,OAAO,CAAC,YAAY;IAwEpB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAOzC,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAwBlC,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAQ9B,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB,GAAG,MAAM,EAAE;IAenE,OAAO,CAAC,SAAS;IA2BjB,OAAO,CAAC,UAAU;IAkBlB,cAAc,CAAC,MAAM,GAAE,YAAiB,GAAG,MAAM,EAAE;IAYnD,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,MAAM,GAAE,YAAiB,GAAG,MAAM,EAAE;IAcxE,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE;IAW7C,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAWzC,kBAAkB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI;IAU3C,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,EAAE;IAO7C,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,EAAE;IAO3C,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,YAAY,EAAE;IAY/C,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAW9C,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,aAAa,EAAE,YAAY,EAAE,EAAE,WAAW,GAAE,MAAM,GAAG,IAAW,GAAG,IAAI;IAiCnG,oBAAoB,IAAI,MAAM,GAAG,IAAI;IAcrC,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAI/C,OAAO,CAAC,gBAAgB;IAMxB,KAAK,IAAI,IAAI;IAQb,OAAO,CAAC,OAAO;IAcf,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,SAAS;CAOlB;AAED,8EAA8E;AAC9E,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKxD"}
@@ -14,10 +14,25 @@
14
14
  import { DatabaseSync } from 'node:sqlite';
15
15
  import { join } from 'node:path';
16
16
  import { mkdirSync } from 'node:fs';
17
- import { AGENT_INFERRED_SOURCE, KNOWLEDGE_SCHEMA_VERSION } from '../types.js';
17
+ import { AGENT_INFERRED_SOURCE } from '../types.js';
18
18
  import { IndexOutdatedError } from './knowledge-index.js';
19
19
  /** Filename of the index database inside the knowledge directory. */
20
20
  export const INDEX_DB_FILE = 'index.db';
21
+ /**
22
+ * Schema version of the index **cache**, deliberately separate from
23
+ * `KNOWLEDGE_SCHEMA_VERSION`, which versions the YAML **sources**.
24
+ *
25
+ * One constant used to serve both. Bumping it to add this version's
26
+ * `source_fingerprint` table would also have stamped `schemaVersion: 2` into every
27
+ * YAML `_meta` the next `compact` rewrote, and an older CLI reading those files warns
28
+ * about them (`yaml-source.ts`) — user-visible churn in the source of truth caused by
29
+ * a change to a cache. Splitting them costs this comment and one import
30
+ * (knowledge-index-misses-merged-yaml/AC-14, design DD3).
31
+ *
32
+ * Version 2 adds `source_fingerprint`. An index at version 1 is discarded and rebuilt
33
+ * by the `IndexOutdatedError` path `openKnowledge` already owns (AC-11).
34
+ */
35
+ export const INDEX_SCHEMA_VERSION = 2;
21
36
  export class SqliteIndex {
22
37
  db;
23
38
  ftsAvailable = true;
@@ -42,9 +57,9 @@ export class SqliteIndex {
42
57
  if (version === 0) {
43
58
  this.createSchema();
44
59
  }
45
- else if (version < KNOWLEDGE_SCHEMA_VERSION) {
60
+ else if (version < INDEX_SCHEMA_VERSION) {
46
61
  // Refuse to serve; migration is rebuild-from-YAML (KG Req 11.4)
47
- throw new IndexOutdatedError(version, KNOWLEDGE_SCHEMA_VERSION);
62
+ throw new IndexOutdatedError(version, INDEX_SCHEMA_VERSION);
48
63
  }
49
64
  }
50
65
  readSchemaVersion() {
@@ -94,6 +109,14 @@ export class SqliteIndex {
94
109
  PRIMARY KEY (file_path, entity_id)
95
110
  );
96
111
 
112
+ -- Which source state this cache mirrors. One row, pinned by the CHECK so a
113
+ -- second fingerprint cannot accumulate behind the first and be read instead
114
+ -- (knowledge-index-misses-merged-yaml/AC-4).
115
+ CREATE TABLE IF NOT EXISTS source_fingerprint (
116
+ id INTEGER PRIMARY KEY CHECK (id = 1),
117
+ fingerprint TEXT NOT NULL
118
+ );
119
+
97
120
  CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type);
98
121
  CREATE INDEX IF NOT EXISTS idx_entities_confidence ON entities(confidence);
99
122
  CREATE INDEX IF NOT EXISTS idx_relationships_from ON relationships(from_entity);
@@ -114,7 +137,7 @@ export class SqliteIndex {
114
137
  }
115
138
  this.db
116
139
  .prepare('INSERT INTO _schema_version (version, created_at) VALUES (?, ?)')
117
- .run(KNOWLEDGE_SCHEMA_VERSION, new Date().toISOString());
140
+ .run(INDEX_SCHEMA_VERSION, new Date().toISOString());
118
141
  }
119
142
  // -------------------------------------------------------------------------
120
143
  // Entities
@@ -273,7 +296,7 @@ export class SqliteIndex {
273
296
  // -------------------------------------------------------------------------
274
297
  // Rebuild
275
298
  // -------------------------------------------------------------------------
276
- rebuild(entities, relationships) {
299
+ rebuild(entities, relationships, fingerprint = null) {
277
300
  this.db.exec('BEGIN');
278
301
  try {
279
302
  this.db.exec('DELETE FROM relationships');
@@ -281,10 +304,23 @@ export class SqliteIndex {
281
304
  this.db.exec('DELETE FROM entities');
282
305
  if (this.ftsAvailable)
283
306
  this.db.exec('DELETE FROM entities_fts');
307
+ // Replacing the contents invalidates any claim about which sources they came
308
+ // from, so the old record goes unconditionally and the caller's — if it has
309
+ // one — is written in the same transaction.
310
+ //
311
+ // Inside the transaction rather than after it, because two processes rebuilding
312
+ // at once could otherwise pair one's contents with the other's fingerprint:
313
+ // A loads sources S1, B loads S2 and commits, A commits S1 over it, then the two
314
+ // stamp in either order. Stamping separately leaves S1's rows under S2's
315
+ // fingerprint — a stale read that no later open would notice, because the
316
+ // fingerprint matches. One transaction makes contents and record move together.
317
+ this.db.exec('DELETE FROM source_fingerprint');
284
318
  for (const entity of entities)
285
319
  this.upsertEntity(entity);
286
320
  for (const rel of relationships)
287
321
  this.upsertRelationship(rel);
322
+ if (fingerprint !== null)
323
+ this.writeFingerprint(fingerprint);
288
324
  this.db.exec('COMMIT');
289
325
  }
290
326
  catch (err) {
@@ -292,6 +328,31 @@ export class SqliteIndex {
292
328
  throw err;
293
329
  }
294
330
  }
331
+ // -------------------------------------------------------------------------
332
+ // Source freshness
333
+ // -------------------------------------------------------------------------
334
+ getSourceFingerprint() {
335
+ try {
336
+ const row = this.db
337
+ .prepare('SELECT fingerprint FROM source_fingerprint WHERE id = 1')
338
+ .get();
339
+ return row?.fingerprint ?? null;
340
+ }
341
+ catch {
342
+ // No such table: an index from before this schema version, reached by a path
343
+ // that skipped the version check. `null` is the fail-safe answer — it rebuilds
344
+ // (AC-4) — and it is why this returns a value rather than throwing.
345
+ return null;
346
+ }
347
+ }
348
+ setSourceFingerprint(fingerprint) {
349
+ this.writeFingerprint(fingerprint);
350
+ }
351
+ writeFingerprint(fingerprint) {
352
+ this.db
353
+ .prepare('INSERT OR REPLACE INTO source_fingerprint (id, fingerprint) VALUES (1, ?)')
354
+ .run(fingerprint);
355
+ }
295
356
  close() {
296
357
  this.db.close();
297
358
  }