@rasensio/aidlc 1.67.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.
- package/README.md +2 -1
- package/dist/autopilot/driver.d.ts +35 -2
- package/dist/autopilot/driver.d.ts.map +1 -1
- package/dist/autopilot/driver.js +650 -27
- package/dist/autopilot/driver.js.map +1 -1
- package/dist/autopilot/presets.d.ts.map +1 -1
- package/dist/autopilot/presets.js +5 -0
- package/dist/autopilot/presets.js.map +1 -1
- package/dist/autopilot/result.d.ts +133 -0
- package/dist/autopilot/result.d.ts.map +1 -0
- package/dist/autopilot/result.js +431 -0
- package/dist/autopilot/result.js.map +1 -0
- package/dist/autopilot/run.d.ts +2 -0
- package/dist/autopilot/run.d.ts.map +1 -1
- package/dist/autopilot/run.js +33 -6
- package/dist/autopilot/run.js.map +1 -1
- package/dist/autopilot/state.d.ts +51 -0
- package/dist/autopilot/state.d.ts.map +1 -1
- package/dist/autopilot/state.js.map +1 -1
- package/dist/autopilot/typed.d.ts +98 -0
- package/dist/autopilot/typed.d.ts.map +1 -0
- package/dist/autopilot/typed.js +122 -0
- package/dist/autopilot/typed.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/hook.d.ts +37 -0
- package/dist/commands/hook.d.ts.map +1 -0
- package/dist/commands/hook.js +208 -0
- package/dist/commands/hook.js.map +1 -0
- package/dist/commands/init.d.ts +18 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +66 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/knowledge.d.ts.map +1 -1
- package/dist/commands/knowledge.js +8 -4
- package/dist/commands/knowledge.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +8 -2
- package/dist/commands/update.js.map +1 -1
- package/dist/compile/adapters/claude-code.d.ts.map +1 -1
- package/dist/compile/adapters/claude-code.js +35 -4
- package/dist/compile/adapters/claude-code.js.map +1 -1
- package/dist/compile/adapters/codex.d.ts +12 -0
- package/dist/compile/adapters/codex.d.ts.map +1 -1
- package/dist/compile/adapters/codex.js +58 -8
- package/dist/compile/adapters/codex.js.map +1 -1
- package/dist/compile/loaders.d.ts.map +1 -1
- package/dist/compile/loaders.js +2 -0
- package/dist/compile/loaders.js.map +1 -1
- package/dist/compile/skill-metadata.d.ts +82 -0
- package/dist/compile/skill-metadata.d.ts.map +1 -0
- package/dist/compile/skill-metadata.js +156 -0
- package/dist/compile/skill-metadata.js.map +1 -0
- package/dist/concurrency/claim-store.d.ts +17 -0
- package/dist/concurrency/claim-store.d.ts.map +1 -1
- package/dist/concurrency/claim-store.js +42 -2
- package/dist/concurrency/claim-store.js.map +1 -1
- package/dist/concurrency/common-dir.d.ts +15 -0
- package/dist/concurrency/common-dir.d.ts.map +1 -1
- package/dist/concurrency/common-dir.js +39 -0
- package/dist/concurrency/common-dir.js.map +1 -1
- package/dist/core/types.d.ts +17 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/cost/hooks/arm.d.ts +9 -1
- package/dist/cost/hooks/arm.d.ts.map +1 -1
- package/dist/cost/hooks/arm.js +16 -2
- package/dist/cost/hooks/arm.js.map +1 -1
- package/dist/cost/providers/codex.d.ts +13 -0
- package/dist/cost/providers/codex.d.ts.map +1 -1
- package/dist/cost/providers/codex.js +74 -2
- package/dist/cost/providers/codex.js.map +1 -1
- package/dist/hooks/guard.d.ts +41 -0
- package/dist/hooks/guard.d.ts.map +1 -0
- package/dist/hooks/guard.js +108 -0
- package/dist/hooks/guard.js.map +1 -0
- package/dist/hooks/install.d.ts +78 -0
- package/dist/hooks/install.d.ts.map +1 -0
- package/dist/hooks/install.js +207 -0
- package/dist/hooks/install.js.map +1 -0
- package/dist/hooks/lifecycle-context.d.ts +65 -0
- package/dist/hooks/lifecycle-context.d.ts.map +1 -0
- package/dist/hooks/lifecycle-context.js +212 -0
- package/dist/hooks/lifecycle-context.js.map +1 -0
- package/dist/knowledge/engine.d.ts +19 -2
- package/dist/knowledge/engine.d.ts.map +1 -1
- package/dist/knowledge/engine.js +35 -11
- package/dist/knowledge/engine.js.map +1 -1
- package/dist/knowledge/index/knowledge-index.d.ts +28 -2
- package/dist/knowledge/index/knowledge-index.d.ts.map +1 -1
- package/dist/knowledge/index/sqlite-index.d.ts +19 -1
- package/dist/knowledge/index/sqlite-index.d.ts.map +1 -1
- package/dist/knowledge/index/sqlite-index.js +66 -5
- package/dist/knowledge/index/sqlite-index.js.map +1 -1
- package/dist/knowledge/source-fingerprint.d.ts +71 -0
- package/dist/knowledge/source-fingerprint.d.ts.map +1 -0
- package/dist/knowledge/source-fingerprint.js +105 -0
- package/dist/knowledge/source-fingerprint.js.map +1 -0
- package/dist/knowledge/sync.d.ts +12 -1
- package/dist/knowledge/sync.d.ts.map +1 -1
- package/dist/knowledge/sync.js +23 -3
- package/dist/knowledge/sync.js.map +1 -1
- package/dist/knowledge/types.d.ts +10 -1
- package/dist/knowledge/types.d.ts.map +1 -1
- package/dist/knowledge/types.js +10 -1
- package/dist/knowledge/types.js.map +1 -1
- package/dist/menu/roster.d.ts.map +1 -1
- package/dist/menu/roster.js +2 -0
- package/dist/menu/roster.js.map +1 -1
- 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":"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"}
|
|
@@ -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
|
-
* -
|
|
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
|
|
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"}
|
package/dist/knowledge/engine.js
CHANGED
|
@@ -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
|
-
* -
|
|
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 {
|
|
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
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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":"AAAA
|
|
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
|
-
/**
|
|
42
|
-
|
|
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
|
|
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;
|
|
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
|
|
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 <
|
|
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,
|
|
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(
|
|
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
|
}
|