@zosmaai/pi-llm-wiki 0.12.1 → 0.12.2

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 (48) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/README.md +16 -8
  3. package/dist/extensions/llm-wiki/lib/bootstrap.js +2 -0
  4. package/dist/extensions/llm-wiki/lib/indexing.js +24 -1
  5. package/dist/extensions/llm-wiki/lib/ingest-worker.js +3 -1
  6. package/dist/extensions/llm-wiki/lib/knowledge-links.js +41 -6
  7. package/dist/extensions/llm-wiki/lib/model-command.js +45 -8
  8. package/dist/extensions/llm-wiki/lib/qmd-indexing.js +1024 -0
  9. package/dist/extensions/llm-wiki/lib/qmd-mirror.js +418 -0
  10. package/dist/extensions/llm-wiki/lib/qmd-store.js +112 -0
  11. package/dist/extensions/llm-wiki/lib/recall.js +77 -3
  12. package/dist/extensions/llm-wiki/lib/runtime.js +25 -1
  13. package/dist/extensions/llm-wiki/lib/subagent.js +47 -7
  14. package/dist/extensions/llm-wiki/lib/tools.js +165 -5
  15. package/dist/extensions/llm-wiki/lib/utils.js +16 -2
  16. package/dist/extensions/llm-wiki/lib/wiki-service.js +104 -5
  17. package/dist/mcp/index.js +66 -2
  18. package/dist/mcp/operations.js +26 -2
  19. package/docs/api.md +43 -1
  20. package/docs/architecture.md +28 -0
  21. package/docs/commands.md +1 -0
  22. package/docs/qmd-compatibility.md +47 -0
  23. package/docs/retrieval-benchmark.md +47 -0
  24. package/docs/superpowers/benchmarks/phase-1-current-baseline.json +53 -0
  25. package/docs/superpowers/plans/2026-08-09-qmd-retrieval-phase-2-remediation.md +549 -0
  26. package/docs/superpowers/plans/2026-08-09-qmd-retrieval-phase-2-validated-indexing.md +1493 -0
  27. package/docs/superpowers/plans/2026-08-11-qmd-retrieval-phase-3-retrieval-modes-and-recall-cutover.md +678 -0
  28. package/docs/superpowers/plans/2026-09-05-wikilink-alias-pipe-table-only.md +257 -0
  29. package/extensions/llm-wiki/index.ts +14 -1
  30. package/extensions/llm-wiki/lib/bootstrap.ts +2 -0
  31. package/extensions/llm-wiki/lib/indexing.ts +24 -1
  32. package/extensions/llm-wiki/lib/ingest-worker.ts +10 -2
  33. package/extensions/llm-wiki/lib/knowledge-document.ts +8 -1
  34. package/extensions/llm-wiki/lib/knowledge-links.ts +39 -7
  35. package/extensions/llm-wiki/lib/model-command.ts +57 -12
  36. package/extensions/llm-wiki/lib/qmd-indexing.ts +1304 -0
  37. package/extensions/llm-wiki/lib/qmd-mirror.ts +496 -0
  38. package/extensions/llm-wiki/lib/qmd-store.ts +222 -0
  39. package/extensions/llm-wiki/lib/recall.ts +77 -3
  40. package/extensions/llm-wiki/lib/runtime.ts +57 -5
  41. package/extensions/llm-wiki/lib/subagent.ts +73 -10
  42. package/extensions/llm-wiki/lib/tools.ts +188 -4
  43. package/extensions/llm-wiki/lib/utils.ts +21 -2
  44. package/extensions/llm-wiki/lib/wiki-service.ts +160 -4
  45. package/mcp/index.ts +78 -1
  46. package/mcp/operations.ts +41 -2
  47. package/package.json +9 -6
  48. package/skills/llm-wiki/SKILL.md +7 -1
@@ -0,0 +1,1493 @@
1
+ # QMD Retrieval Phase 2: Validated Indexing Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use /skill:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Build independently repairable per-vault QMD indexes from parser-valid Markdown, with validated canonical/evidence mirrors, incremental updates, deletion handling, vector indexing, status diagnostics, and crash-safe replacement, without changing active recall.
6
+
7
+ **Architecture:** Authoritative Markdown remains under `.llm-wiki/wiki/**`. A mirror module serializes only documents accepted by the shared parser into `.llm-wiki/meta/qmd/documents/{canonical,evidence}/**/*.md` and publishes a manifest that maps generated paths back to stable `(vault_id, page_id)` identities. A QMD-only adapter wraps the pinned public SDK, while an indexing coordinator serializes each vault through a filesystem lock, mutates a copied or fresh staging store, closes and validates it, and promotes the whole artifact directory through a recoverable journal. Normal metadata-triggered work updates lexical state without loading models; explicit `wiki_reindex` owns vector downloads and progress.
8
+
9
+ **Tech Stack:** TypeScript 5.9, Node.js 22 `node:fs/promises`, `node:crypto`, `@tobilu/qmd` 2.5.3 public SDK, TypeBox, Zod, Vitest.
10
+
11
+ **Roadmap:** `docs/superpowers/roadmaps/2026-08-09-qmd-retrieval-roadmap.md`
12
+
13
+ **Phase:** Phase 2: Validated QMD Indexing
14
+
15
+ ---
16
+
17
+ ## Phase Boundary
18
+
19
+ This plan starts from Phase 1's pinned QMD dependency and green SDK contract. It intentionally does **not**:
20
+
21
+ - change `extensions/llm-wiki/lib/recall.ts` ranking or result rendering;
22
+ - change automatic `before_agent_start` recall;
23
+ - add lexical, hybrid, adaptive, or quality query execution;
24
+ - add typed relation expansion, card/evidence bundles, conflict resolution, or feedback;
25
+ - deprecate `wiki_reindex_embeddings` yet; the roadmap assigns that compatibility step to Phase 3;
26
+ - edit QMD tables or import QMD internals.
27
+
28
+ At completion, existing heuristic recall and its page-level embedding sidecar remain active. QMD indexing is independently observable and repairable, but no recall path depends on it.
29
+
30
+ ## Execution Prerequisites
31
+
32
+ 1. Upstream Phase 1 PR `#144` must be merged, or the implementation branch must contain its exact commits.
33
+ 2. Rebase the implementation branch onto current upstream `main` before changing production code.
34
+ 3. Preserve the exact `@tobilu/qmd` `2.5.3` pin and Node.js `>=22.0.0` floor.
35
+ 4. Run model-backed tests only with `QMD_MODEL_SMOKE=1`; ordinary tests must not download or load a model.
36
+
37
+ ## File Map
38
+
39
+ ### New production files
40
+
41
+ - `extensions/llm-wiki/lib/qmd-store.ts` — only production module allowed to import `@tobilu/qmd`; normalizes SDK update, embedding, status, model identity, and close behavior.
42
+ - `extensions/llm-wiki/lib/qmd-mirror.ts` — canonical/evidence role classification, manifest validation, deterministic hashing, parser-valid mirror reconciliation, and unsafe-entry invalidation.
43
+ - `extensions/llm-wiki/lib/qmd-indexing.ts` — stable vault ID backfill, per-vault locking, staging/copy-on-write indexing, journaled swap/recovery, reindex orchestration, cancellation, and generated status.
44
+
45
+ ### New tests
46
+
47
+ - `test/qmd-mirror.test.ts` — valid/invalid/reserved page filtering, role mapping, manifest mapping, updates, role changes, and deletion.
48
+ - `test/qmd-indexing.test.ts` — real model-free QMD add/update/delete, vector delegation with a fake adapter, vault isolation, status, and forced rebuild.
49
+ - `test/qmd-indexing-recovery.test.ts` — every recoverable journal phase, failed reopen rollback, dead/live lock behavior, and no-rename-while-open contract.
50
+ - `test/qmd-reindex-tool.test.ts` — Pi tool validation, progress, cancellation, structured result, and no-model lexical path.
51
+
52
+ ### Modified production files
53
+
54
+ - `extensions/llm-wiki/lib/utils.ts` — expose generated QMD paths on `VaultPaths`.
55
+ - `extensions/llm-wiki/lib/bootstrap.ts` — create and preserve stable `vault_id` values.
56
+ - `extensions/llm-wiki/lib/knowledge-document.ts` — add stable diagnostics for invalid vault IDs and QMD health.
57
+ - `extensions/llm-wiki/lib/indexing.ts` — schedule QMD only after successful metadata projection; on projection failure perform safety invalidation only.
58
+ - `extensions/llm-wiki/lib/wiki-service.ts` — shared vault selection, reindex operation, and QMD status shape.
59
+ - `extensions/llm-wiki/lib/tools.ts` — register `wiki_reindex`; include QMD state in `wiki_status` and `wiki_lint`; chain `wiki_rebuild_meta` into lexical QMD update.
60
+ - `extensions/llm-wiki/index.ts` — register `wiki_reindex` and run startup recovery without touching recall hooks.
61
+ - `mcp/operations.ts` — shared reindex and expanded status operations.
62
+ - `mcp/index.ts` — expose `wiki_reindex` and run startup recovery.
63
+
64
+ ### Modified tests and docs
65
+
66
+ - `test/bootstrap.test.ts`, `test/mcp-parity.test.ts`, `test/mcp-package.test.ts`, `test/package-structure.test.ts`, `test/indexing.test.ts`, `test/indexing-fail-closed.test.ts`, `test/guardrails.test.ts`, and `test/background-tools.test.ts`.
67
+ - `README.md`, `docs/api.md`, `docs/architecture.md`, `docs/commands.md`, and `skills/llm-wiki/SKILL.md`.
68
+
69
+ ---
70
+
71
+ ### Task 1: Stable Vault Identity and Generated Paths
72
+
73
+ **Files:**
74
+ - Modify: `extensions/llm-wiki/lib/utils.ts`
75
+ - Modify: `extensions/llm-wiki/lib/bootstrap.ts`
76
+ - Modify: `extensions/llm-wiki/lib/knowledge-document.ts`
77
+ - Modify: `test/bootstrap.test.ts`
78
+ - Modify: `test/mcp-parity.test.ts`
79
+
80
+ - [ ] **Step 1: Write failing vault-path and UUID tests**
81
+
82
+ Add assertions equivalent to:
83
+
84
+ ```ts
85
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
86
+
87
+ it("creates a stable vault_id and QMD paths", () => {
88
+ const first = bootstrapVault(paths, { topic: "Identity", mode: "personal" });
89
+ expect(first.ok).toBe(true);
90
+ const firstConfig = JSON.parse(readFileSync(join(paths.dotWiki, "config.json"), "utf8"));
91
+ expect(firstConfig.vault_id).toMatch(UUID);
92
+
93
+ bootstrapVault(paths, { topic: "Renamed", mode: "personal" });
94
+ const secondConfig = JSON.parse(readFileSync(join(paths.dotWiki, "config.json"), "utf8"));
95
+ expect(secondConfig.vault_id).toBe(firstConfig.vault_id);
96
+
97
+ expect(paths.qmd).toBe(join(paths.meta, "qmd"));
98
+ expect(paths.qmdCurrent).toBe(join(paths.meta, "qmd", "current"));
99
+ expect(paths.qmdDocuments).toBe(join(paths.meta, "qmd", "documents"));
100
+ expect(paths.qmdManifest).toBe(join(paths.meta, "qmd", "manifest.json"));
101
+ expect(paths.qmdSwap).toBe(join(paths.meta, "qmd", "swap.json"));
102
+ });
103
+ ```
104
+
105
+ In MCP parity, stop comparing independently generated UUID values byte-for-byte. Assert that both are valid, then compare configs after omitting `vault_id`:
106
+
107
+ ```ts
108
+ const { vault_id: mcpVaultId, ...mcpConfig } = JSON.parse(readFileSync(mcpConfigPath, "utf8"));
109
+ const { vault_id: piVaultId, ...piConfig } = JSON.parse(readFileSync(piConfigPath, "utf8"));
110
+ expect(mcpVaultId).toMatch(UUID);
111
+ expect(piVaultId).toMatch(UUID);
112
+ expect(mcpConfig).toEqual(piConfig);
113
+ ```
114
+
115
+ Do not install a UUID package; Node supplies `randomUUID()`.
116
+
117
+ - [ ] **Step 2: Run tests and verify failure**
118
+
119
+ Run:
120
+
121
+ ```bash
122
+ pnpm exec vitest run test/bootstrap.test.ts test/mcp-parity.test.ts --reporter=verbose
123
+ ```
124
+
125
+ Expected: failure because `vault_id` and QMD path fields do not exist.
126
+
127
+ - [ ] **Step 3: Extend `VaultPaths` in one place**
128
+
129
+ Add these required fields:
130
+
131
+ ```ts
132
+ export interface VaultPaths {
133
+ root: string;
134
+ raw: string;
135
+ rawSources: string;
136
+ rawTrajectories: string;
137
+ wiki: string;
138
+ meta: string;
139
+ dotWiki: string;
140
+ outputs: string;
141
+ discoveries: string;
142
+ qmd: string;
143
+ qmdCurrent: string;
144
+ qmdDocuments: string;
145
+ qmdManifest: string;
146
+ qmdSwap: string;
147
+ }
148
+ ```
149
+
150
+ Build them in both `getVaultPaths` and `getLegacyVaultPaths`. Use local `meta` and `qmd` constants so paths are not repeated inconsistently:
151
+
152
+ ```ts
153
+ const meta = join(root, ".llm-wiki", "meta");
154
+ const qmd = join(meta, "qmd");
155
+ // ...existing fields...
156
+ qmd,
157
+ qmdCurrent: join(qmd, "current"),
158
+ qmdDocuments: join(qmd, "documents"),
159
+ qmdManifest: join(qmd, "manifest.json"),
160
+ qmdSwap: join(qmd, "swap.json"),
161
+ ```
162
+
163
+ Do not create QMD directories from `ensureVaultStructure`; stores remain lazy.
164
+
165
+ - [ ] **Step 4: Create and preserve `vault_id` during bootstrap**
166
+
167
+ Import `randomUUID` from `node:crypto` and add:
168
+
169
+ ```ts
170
+ const config: Record<string, unknown> = {
171
+ ...existing,
172
+ name: input.topic,
173
+ mode: input.mode,
174
+ topic: input.topic,
175
+ created: existing.created ?? fmtDate(),
176
+ version: existing.version ?? "1.0",
177
+ vault_id: existing.vault_id ?? randomUUID(),
178
+ ...(created ? { knowledge_format: "okf-0.2" } : {}),
179
+ };
180
+ ```
181
+
182
+ An existing non-empty `vault_id` is preserved. Indexing validates it later and must never silently replace an invalid identity.
183
+
184
+ Add diagnostic codes now so later tasks do not use untyped strings:
185
+
186
+ ```ts
187
+ | "config_invalid_vault_id"
188
+ | "qmd_index_missing"
189
+ | "qmd_index_stale"
190
+ | "qmd_index_error"
191
+ | "qmd_index_busy"
192
+ | "qmd_manifest_invalid"
193
+ | "qmd_swap_interrupted";
194
+ ```
195
+
196
+ - [ ] **Step 5: Run focused tests**
197
+
198
+ Run:
199
+
200
+ ```bash
201
+ pnpm exec vitest run test/bootstrap.test.ts test/mcp-parity.test.ts --reporter=verbose
202
+ pnpm typecheck
203
+ ```
204
+
205
+ Expected: all pass. Bootstrap reruns preserve page content and `vault_id`.
206
+
207
+ - [ ] **Step 6: Commit**
208
+
209
+ ```bash
210
+ git add extensions/llm-wiki/lib/utils.ts extensions/llm-wiki/lib/bootstrap.ts extensions/llm-wiki/lib/knowledge-document.ts test/bootstrap.test.ts test/mcp-parity.test.ts
211
+ git commit -m "feat: add stable wiki vault identities"
212
+ ```
213
+
214
+ ---
215
+
216
+ ### Task 2: Parser-Validated Mirror and Manifest
217
+
218
+ **Files:**
219
+ - Create: `extensions/llm-wiki/lib/qmd-mirror.ts`
220
+ - Create: `test/qmd-mirror.test.ts`
221
+
222
+ - [ ] **Step 1: Write failing mirror contract tests**
223
+
224
+ Use a temporary vault under `node:os.tmpdir()`. Create these pages:
225
+
226
+ | Path | Type/content | Expected role |
227
+ |---|---|---|
228
+ | `concepts/card.md` | `type: concept` | canonical |
229
+ | `entities/person.md` | `type: entity` | canonical |
230
+ | `analyses/decision.md` | `type: analysis` | canonical |
231
+ | `syntheses/summary.md` | `type: synthesis` | canonical |
232
+ | `requirements/rule.md` | `type: requirement` | canonical |
233
+ | `skills/procedure.md` | `type: skill` | canonical |
234
+ | `cases/example.md` | `type: case` | canonical |
235
+ | `sources/source.md` | `type: source` | evidence |
236
+ | `misc/unknown.md` | `type: custom` | evidence |
237
+ | `concepts/bad.md` | malformed frontmatter | absent |
238
+ | `concepts/index.md` | reserved generated name | absent |
239
+ | `log.md` | reserved generated name | absent |
240
+
241
+ Use a concrete fixture so the engineer is never guessing content. For example, `concepts/card.md` is exactly:
242
+
243
+ ```
244
+ ---
245
+ type: concept
246
+ title: Retrieval Card
247
+ created: 2026-08-09
248
+ updated: 2026-08-09
249
+ ---
250
+
251
+ # Retrieval Card
252
+
253
+ QMD mirrors only parser-valid Markdown.
254
+ ```
255
+
256
+ And `concepts/bad.md` is exactly:
257
+
258
+ ```
259
+ this file has no frontmatter at all
260
+ ```
261
+
262
+ The core assertions must be:
263
+
264
+ ```ts
265
+ const result = await reconcileQmdMirror(paths, vaultId, "changed");
266
+ expect(result.counts).toEqual({ indexed: 9, updated: 0, unchanged: 0, removed: 0 });
267
+ expect(result.diagnostics.map((d) => d.code)).toContain("frontmatter_missing");
268
+
269
+ const manifest = await readQmdManifest(paths, vaultId);
270
+ expect(Object.keys(manifest.entries).sort()).toEqual([
271
+ "documents/canonical/analyses/decision.md",
272
+ "documents/canonical/cases/example.md",
273
+ "documents/canonical/concepts/card.md",
274
+ "documents/canonical/entities/person.md",
275
+ "documents/canonical/requirements/rule.md",
276
+ "documents/canonical/skills/procedure.md",
277
+ "documents/canonical/syntheses/summary.md",
278
+ "documents/evidence/misc/unknown.md",
279
+ "documents/evidence/sources/source.md",
280
+ ]);
281
+ expect(manifest.entries["documents/canonical/concepts/card.md"]).toMatchObject({
282
+ vaultId,
283
+ pageId: "concepts/card",
284
+ role: "canonical",
285
+ });
286
+ ```
287
+
288
+ Add separate cases proving:
289
+
290
+ 1. unchanged reconciliation does not rewrite mirror files and returns `unchanged`;
291
+ 2. body edits return `updated` and change the SHA-256 hash;
292
+ 3. changing `type: concept` to `type: source` removes the canonical path and creates the evidence path;
293
+ 4. deleting a page removes its manifest entry and mirror file;
294
+ 5. making a previously valid page malformed removes its old mirror entry;
295
+ 6. invalid or mismatched manifest `vaultId` returns `qmd_manifest_invalid` and rebuilds from authoritative pages;
296
+ 7. every manifest `sourcePath` is absolute and every manifest key uses `/`, including on Windows.
297
+
298
+ - [ ] **Step 2: Run test and verify failure**
299
+
300
+ Run:
301
+
302
+ ```bash
303
+ pnpm exec vitest run test/qmd-mirror.test.ts --reporter=verbose
304
+ ```
305
+
306
+ Expected: module-not-found failure for `qmd-mirror.js`.
307
+
308
+ - [ ] **Step 3: Define manifest and reconciliation types**
309
+
310
+ Create `qmd-mirror.ts` with no QMD import:
311
+
312
+ ```ts
313
+ import { createHash, randomUUID } from "node:crypto";
314
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
315
+ import { dirname, isAbsolute, join, relative, sep } from "node:path";
316
+ import { serializeKnowledgeDocument } from "./knowledge-document.js";
317
+ import type { KnowledgeDiagnostic } from "./knowledge-document.js";
318
+ import type { VaultPaths } from "./utils.js";
319
+ import { discoverKnowledgeDocuments } from "./vault-format.js";
320
+
321
+ export const QMD_MANIFEST_VERSION = 1;
322
+ export type QmdRole = "canonical" | "evidence";
323
+
324
+ export interface QmdManifestEntry {
325
+ sourcePath: string;
326
+ vaultId: string;
327
+ pageId: string;
328
+ contentHash: string;
329
+ role: QmdRole;
330
+ type: string;
331
+ }
332
+
333
+ export interface QmdManifest {
334
+ version: 1;
335
+ vaultId: string;
336
+ entries: Record<string, QmdManifestEntry>;
337
+ }
338
+
339
+ export interface QmdMirrorCounts {
340
+ indexed: number;
341
+ updated: number;
342
+ unchanged: number;
343
+ removed: number;
344
+ }
345
+
346
+ export interface QmdMirrorResult {
347
+ manifest: QmdManifest;
348
+ manifestHash: string;
349
+ counts: QmdMirrorCounts;
350
+ diagnostics: KnowledgeDiagnostic[];
351
+ }
352
+
353
+ const CANONICAL_TYPES = new Set([
354
+ "concept",
355
+ "entity",
356
+ "analysis",
357
+ "synthesis",
358
+ "requirement",
359
+ "skill",
360
+ "case",
361
+ ]);
362
+ ```
363
+
364
+ Export `roleForDocumentType(type: string): QmdRole`; normalize with `trim().toLowerCase()` and default unknown types to evidence. Also export `readQmdManifest(paths, expectedVaultId): Promise<QmdManifest>` for status and tests; it must reject malformed, wrong-version, wrong-vault, absolute-key, and traversal-key data rather than returning an untrusted partial manifest.
365
+
366
+ - [ ] **Step 4: Implement deterministic keys, hashes, and atomic writes**
367
+
368
+ Use these contracts:
369
+
370
+ ```ts
371
+ export function manifestKey(role: QmdRole, pageId: string): string {
372
+ return ["documents", role, ...pageId.split("/")].join("/") + ".md";
373
+ }
374
+
375
+ export function hashQmdContent(content: string): string {
376
+ return createHash("sha256").update(content, "utf8").digest("hex");
377
+ }
378
+
379
+ export function hashQmdManifest(manifest: QmdManifest): string {
380
+ const entries = Object.fromEntries(
381
+ Object.entries(manifest.entries).sort(([left], [right]) =>
382
+ left < right ? -1 : left > right ? 1 : 0,
383
+ ),
384
+ );
385
+ return hashQmdContent(JSON.stringify({ version: manifest.version, vaultId: manifest.vaultId, entries }));
386
+ }
387
+
388
+ async function atomicWrite(path: string, content: string): Promise<void> {
389
+ await mkdir(dirname(path), { recursive: true });
390
+ const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
391
+ await writeFile(temporary, content, { encoding: "utf8", flag: "wx" });
392
+ await rename(temporary, path);
393
+ }
394
+ ```
395
+
396
+ Convert manifest keys to physical paths only through a containment-checked helper. Reject absolute keys, `..`, and any resolved path outside `paths.qmd`; never trust paths loaded from JSON.
397
+
398
+ - [ ] **Step 5: Implement fail-safe mirror publication**
399
+
400
+ `reconcileQmdMirror(paths, vaultId, scope)` must:
401
+
402
+ 1. call `discoverKnowledgeDocuments(paths)` and retain its diagnostics even when discovery is blocking;
403
+ 2. use only `discovery.documents`, which already passed the shared parser and reserved-name/symlink/collision checks;
404
+ 3. serialize each accepted document with `serializeKnowledgeDocument`, hash that exact mirror string, and derive its role;
405
+ 4. load a version- and UUID-validated prior manifest or use an empty manifest plus `qmd_manifest_invalid` diagnostic;
406
+ 5. build the complete desired manifest in code-point key order;
407
+ 6. atomically publish an intermediate manifest that removes deleted, malformed, or role-moved entries **before** changing files, ensuring future retrieval cannot map an unsafe old candidate;
408
+ 7. atomically write new/changed mirror files; for `scope: "all"`, rewrite all accepted files; for `changed`, skip equal hashes;
409
+ 8. atomically publish the final desired manifest;
410
+ 9. remove orphaned generated mirror files and empty generated directories without following symlinks;
411
+ 10. return exact indexed/updated/unchanged/removed counts and the stable entries-only manifest hash.
412
+
413
+ A mirror write failure must leave either the intermediate or previous valid manifest, never a manifest entry that points to missing or unvalidated content. Generated leftovers not referenced by the manifest are harmless and removed by the next reconciliation.
414
+
415
+ Also export:
416
+
417
+ ```ts
418
+ export async function invalidateUnsafeQmdEntries(
419
+ paths: VaultPaths,
420
+ vaultId: string,
421
+ ): Promise<QmdMirrorResult>;
422
+ ```
423
+
424
+ It performs the same complete parser scan but may only remove manifest entries for missing or rejected pages. It must not add or update valid documents. This is the safety-only path used when metadata projection fails.
425
+
426
+ - [ ] **Step 6: Run focused tests**
427
+
428
+ Run:
429
+
430
+ ```bash
431
+ pnpm exec vitest run test/qmd-mirror.test.ts test/knowledge-document.test.ts test/indexing-fail-closed.test.ts --reporter=verbose
432
+ pnpm typecheck
433
+ pnpm lint
434
+ ```
435
+
436
+ Expected: all pass; no model cache files are created.
437
+
438
+ - [ ] **Step 7: Commit**
439
+
440
+ ```bash
441
+ git add extensions/llm-wiki/lib/qmd-mirror.ts test/qmd-mirror.test.ts
442
+ git commit -m "feat: build validated QMD document mirrors"
443
+ ```
444
+
445
+ ---
446
+
447
+ ### Task 3: Public-SDK QMD Store Adapter
448
+
449
+ **Files:**
450
+ - Create: `extensions/llm-wiki/lib/qmd-store.ts`
451
+ - Modify: `test/qmd-contract.test.ts`
452
+
453
+ - [ ] **Step 1: Add failing normalized-adapter tests**
454
+
455
+ Extend `test/qmd-contract.test.ts` to use canonical and evidence directories, then assert:
456
+
457
+ ```ts
458
+ const handle = await openQmdIndexStore({
459
+ dbPath,
460
+ documentsPath,
461
+ });
462
+ const updated = await handle.update();
463
+ expect(updated).toEqual({
464
+ collections: 2,
465
+ indexed: 2,
466
+ updated: 0,
467
+ unchanged: 0,
468
+ removed: 0,
469
+ needsEmbedding: 2,
470
+ });
471
+ expect(await handle.status()).toMatchObject({
472
+ totalDocuments: 2,
473
+ needsEmbedding: 2,
474
+ hasVectorIndex: false,
475
+ });
476
+ await handle.close();
477
+ ```
478
+
479
+ Delete one evidence file, reopen, update, and expect `removed: 1`. Verify lexical update leaves QMD model cache contents unchanged.
480
+
481
+ - [ ] **Step 2: Run contract test and verify failure**
482
+
483
+ Run:
484
+
485
+ ```bash
486
+ QMD_FORCE_CPU=1 pnpm exec vitest run test/qmd-contract.test.ts --reporter=verbose
487
+ ```
488
+
489
+ Expected: failure because normalized adapter does not exist.
490
+
491
+ - [ ] **Step 3: Define package-private normalized adapter**
492
+
493
+ `qmd-store.ts` is the only production file importing QMD:
494
+
495
+ ```ts
496
+ import { createStore } from "@tobilu/qmd";
497
+ import type { QMDStore } from "@tobilu/qmd";
498
+ import { join } from "node:path";
499
+
500
+ export const QMD_PACKAGE_VERSION = "2.5.3";
501
+ export const QMD_DEFAULT_MODELS = {
502
+ embed: "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf",
503
+ generate: "hf:tobil/qmd-query-expansion-1.7B-gguf/qmd-query-expansion-1.7B-q4_k_m.gguf",
504
+ rerank: "hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf",
505
+ } as const;
506
+
507
+ export interface QmdResolvedModels {
508
+ embed: string;
509
+ generate: string;
510
+ rerank: string;
511
+ }
512
+
513
+ export interface QmdStoreUpdateResult {
514
+ collections: number;
515
+ indexed: number;
516
+ updated: number;
517
+ unchanged: number;
518
+ removed: number;
519
+ needsEmbedding: number;
520
+ }
521
+
522
+ export interface QmdStoreEmbedResult {
523
+ docsProcessed: number;
524
+ chunksEmbedded: number;
525
+ errors: number;
526
+ durationMs: number;
527
+ }
528
+
529
+ export interface QmdStoreStatus {
530
+ totalDocuments: number;
531
+ needsEmbedding: number;
532
+ hasVectorIndex: boolean;
533
+ canonicalDocuments: number;
534
+ evidenceDocuments: number;
535
+ }
536
+
537
+ export interface QmdIndexStore {
538
+ update(onProgress?: (progress: { collection: string; file: string; current: number; total: number }) => void): Promise<QmdStoreUpdateResult>;
539
+ embed(options: { force: boolean; onProgress?: (progress: { chunksEmbedded: number; totalChunks: number; errors: number }) => void }): Promise<QmdStoreEmbedResult>;
540
+ status(): Promise<QmdStoreStatus>;
541
+ close(): Promise<void>;
542
+ }
543
+
544
+ export type QmdStoreFactory = (input: {
545
+ dbPath: string;
546
+ documentsPath: string;
547
+ }) => Promise<QmdIndexStore>;
548
+ ```
549
+
550
+ No `QMDStore`, `IndexStatus`, `UpdateResult`, or other package type may appear outside this file.
551
+
552
+ - [ ] **Step 4: Implement collection config and guaranteed close behavior**
553
+
554
+ Use exact non-overlapping collection paths:
555
+
556
+ ```ts
557
+ export async function openQmdIndexStore(input: {
558
+ dbPath: string;
559
+ documentsPath: string;
560
+ }): Promise<QmdIndexStore> {
561
+ const store: QMDStore = await createStore({
562
+ dbPath: input.dbPath,
563
+ config: {
564
+ global_context: "Validated LLM Wiki knowledge",
565
+ collections: {
566
+ canonical: {
567
+ path: join(input.documentsPath, "canonical"),
568
+ pattern: "**/*.md",
569
+ context: { "/": "Reusable conclusions, entities, requirements, and procedures" },
570
+ },
571
+ evidence: {
572
+ path: join(input.documentsPath, "evidence"),
573
+ pattern: "**/*.md",
574
+ context: { "/": "Source evidence, observations, trajectories, and unpromoted notes" },
575
+ },
576
+ },
577
+ },
578
+ });
579
+
580
+ return {
581
+ update: (onProgress) => store.update({ onProgress }),
582
+ embed: ({ force, onProgress }) =>
583
+ store.embed({ force, chunkStrategy: "regex", onProgress }),
584
+ status: async () => {
585
+ const status = await store.getStatus();
586
+ const counts = Object.fromEntries(status.collections.map((collection) => [collection.name, collection.documents]));
587
+ return {
588
+ totalDocuments: status.totalDocuments,
589
+ needsEmbedding: status.needsEmbedding,
590
+ hasVectorIndex: status.hasVectorIndex,
591
+ canonicalDocuments: counts.canonical ?? 0,
592
+ evidenceDocuments: counts.evidence ?? 0,
593
+ };
594
+ },
595
+ close: () => store.close(),
596
+ };
597
+ }
598
+ ```
599
+
600
+ QMD 2.5.3 omits empty collections from `getStatus().collections`; default absent counts to zero. Never inspect `store.internal` or QMD SQLite tables.
601
+
602
+ Resolve model identities without loading models:
603
+
604
+ ```ts
605
+ export function resolveQmdModels(env: NodeJS.ProcessEnv = process.env): QmdResolvedModels {
606
+ return {
607
+ embed: env.QMD_EMBED_MODEL?.trim() || QMD_DEFAULT_MODELS.embed,
608
+ generate: env.QMD_GENERATE_MODEL?.trim() || QMD_DEFAULT_MODELS.generate,
609
+ rerank: env.QMD_RERANK_MODEL?.trim() || QMD_DEFAULT_MODELS.rerank,
610
+ };
611
+ }
612
+ ```
613
+
614
+ - [ ] **Step 5: Run model-free SDK tests**
615
+
616
+ Run:
617
+
618
+ ```bash
619
+ QMD_FORCE_CPU=1 pnpm exec vitest run test/qmd-contract.test.ts --reporter=verbose
620
+ pnpm typecheck
621
+ pnpm lint
622
+ ```
623
+
624
+ Expected: lexical tests pass, model smoke remains skipped, and cache file listing is unchanged.
625
+
626
+ - [ ] **Step 6: Commit**
627
+
628
+ ```bash
629
+ git add extensions/llm-wiki/lib/qmd-store.ts test/qmd-contract.test.ts
630
+ git commit -m "feat: add QMD index store adapter"
631
+ ```
632
+
633
+ ---
634
+
635
+ ### Task 4: Copy-on-Write Indexing, Locking, Swap, and Recovery
636
+
637
+ **Files:**
638
+ - Create: `extensions/llm-wiki/lib/qmd-indexing.ts`
639
+ - Create: `test/qmd-indexing.test.ts`
640
+ - Create: `test/qmd-indexing-recovery.test.ts`
641
+
642
+ - [ ] **Step 1: Write failing model-free indexing tests**
643
+
644
+ Use real QMD with `components: ["lexical"]` and temporary vaults. Cover:
645
+
646
+ ```ts
647
+ const first = await reindexQmdVault(paths, {
648
+ scope: "changed",
649
+ components: ["lexical"],
650
+ force: false,
651
+ });
652
+ expect(first.ok).toBe(true);
653
+ expect(first.documents).toMatchObject({ indexed: 2, removed: 0 });
654
+ expect(first.vectors.generated).toBe(0);
655
+ expect(first.status.state).toBe("ready");
656
+ expect(existsSync(join(paths.qmdCurrent, "index.sqlite"))).toBe(true);
657
+
658
+ // edit, add, delete, then run changed again
659
+ expect(second.documents).toMatchObject({ indexed: 1, updated: 1, removed: 1 });
660
+ expect(second.status.totalDocuments).toBe(2);
661
+ ```
662
+
663
+ Also assert:
664
+
665
+ - `components: ["vectors"]` still performs document update before calling fake adapter `embed`;
666
+ - `force: true` with lexical starts from an empty staging store instead of copying current;
667
+ - `force: true` with vectors passes `force: true` to `embed`;
668
+ - lexical-only indexing never calls `embed`;
669
+ - identical page IDs in two vaults produce different `vaultId` values and independent stores;
670
+ - invalid existing `vault_id` returns `config_invalid_vault_id` without replacing it;
671
+ - a pre-QMD existing config receives one UUID backfill while every unrelated key remains byte-for-byte equivalent as parsed JSON;
672
+ - failed staging update leaves current store and `index-state.json` unchanged while status becomes stale/error with the failed manifest hash recorded.
673
+
674
+ - [ ] **Step 2: Write failing recovery-state tests**
675
+
676
+ Construct generated directories and `swap.json` directly; no production fault-injection option is needed. Cover each journal phase:
677
+
678
+ | Phase | Disk state | Recovery result |
679
+ |---|---|---|
680
+ | `prepared` | current + staging | remove staging, retain current |
681
+ | `previous-moved` | previous + staging, no current | restore previous to current, remove staging |
682
+ | `current-promoted` valid | current + previous | validate current, remove previous |
683
+ | `current-promoted` invalid | broken current + previous | remove broken generated current, restore previous |
684
+ | `validated` | current + previous | retain current, remove previous |
685
+ | malformed journal | current present | retain current, report `qmd_swap_interrupted`; do not guess destructive recovery |
686
+
687
+ Use a fake adapter that tracks `open` and `close`. Assert every `rename` recorded by an injected filesystem test seam occurs only when open-handle count is zero.
688
+
689
+ Add lock cases:
690
+
691
+ - live same-host PID returns `qmd_index_busy`;
692
+ - dead same-host PID lock is recovered;
693
+ - other-host or malformed lock is never broken automatically;
694
+ - lock is removed in `finally` after update, embed, validation, cancellation, and thrown errors.
695
+
696
+ - [ ] **Step 3: Run tests and verify failure**
697
+
698
+ Run:
699
+
700
+ ```bash
701
+ QMD_FORCE_CPU=1 pnpm exec vitest run test/qmd-indexing.test.ts test/qmd-indexing-recovery.test.ts --reporter=verbose
702
+ ```
703
+
704
+ Expected: module-not-found failure for `qmd-indexing.js`.
705
+
706
+ - [ ] **Step 4: Define public reindex, state, journal, and status contracts**
707
+
708
+ Create these normalized contracts in `qmd-indexing.ts`:
709
+
710
+ ```ts
711
+ export type QmdComponent = "lexical" | "vectors";
712
+ export type QmdReindexScope = "changed" | "all";
713
+ export type QmdIndexState = "missing" | "ready" | "stale" | "recovering" | "error";
714
+
715
+ export interface QmdReindexOptions {
716
+ scope: QmdReindexScope;
717
+ components: QmdComponent[];
718
+ force: boolean;
719
+ signal?: AbortSignal;
720
+ onProgress?: (progress: QmdIndexProgress) => void;
721
+ }
722
+
723
+ export interface QmdIndexProgress {
724
+ stage: "mirror" | "copy" | "lexical" | "vectors" | "validate" | "swap";
725
+ message: string;
726
+ current?: number;
727
+ total?: number;
728
+ }
729
+
730
+ export interface QmdIndexIssue {
731
+ code: string;
732
+ message: string;
733
+ path?: string;
734
+ }
735
+
736
+ export interface QmdGeneratedStatus {
737
+ state: QmdIndexState;
738
+ vaultId?: string;
739
+ qmdVersion: string;
740
+ models: QmdResolvedModels;
741
+ totalDocuments: number;
742
+ canonicalDocuments: number;
743
+ evidenceDocuments: number;
744
+ needsEmbedding: number;
745
+ hasVectorIndex: boolean;
746
+ manifestHash?: string;
747
+ indexedManifestHash?: string;
748
+ lastIndexedAt?: string;
749
+ swapPhase?: QmdSwapPhase;
750
+ issues: QmdIndexIssue[];
751
+ }
752
+
753
+ export interface QmdReindexResult {
754
+ ok: boolean;
755
+ vaultId?: string;
756
+ scope: QmdReindexScope;
757
+ components: QmdComponent[];
758
+ documents: { indexed: number; updated: number; unchanged: number; removed: number };
759
+ vectors: { generated: number; skipped: number; errors: number };
760
+ elapsedMs: number;
761
+ status: QmdGeneratedStatus;
762
+ warnings: QmdIndexIssue[];
763
+ errors: QmdIndexIssue[];
764
+ }
765
+
766
+ export type QmdSwapPhase = "prepared" | "previous-moved" | "current-promoted" | "validated";
767
+
768
+ interface QmdSwapJournal {
769
+ version: 1;
770
+ operationId: string;
771
+ stagingName: string;
772
+ phase: QmdSwapPhase;
773
+ startedAt: string;
774
+ }
775
+
776
+ interface QmdIndexStateFile {
777
+ version: 1;
778
+ vaultId: string;
779
+ qmdVersion: string;
780
+ models: QmdResolvedModels;
781
+ manifestHash: string;
782
+ indexedAt: string;
783
+ status: QmdStoreStatus;
784
+ }
785
+ ```
786
+
787
+ `components` must be de-duplicated and non-empty. Check `signal.aborted` before each stage and in every QMD progress callback; throw a private `QmdIndexCancelledError` so cancellation never promotes staging. Map vector counts without guessing: `generated` is `embedResult.docsProcessed`, `errors` is `embedResult.errors`, and `skipped` is `Math.max(0, storeStatus.totalDocuments - embedResult.docsProcessed)` when vectors are selected (otherwise all three are zero).
788
+
789
+ - [ ] **Step 5: Implement stable existing-vault ID backfill**
790
+
791
+ Under the QMD lock, read `config.json` as an object. Use Node's `randomUUID` and `UUID` validation matching Task 1:
792
+
793
+ ```ts
794
+ export async function ensureVaultId(paths: VaultPaths): Promise<string> {
795
+ const configPath = join(paths.dotWiki, "config.json");
796
+ const config = JSON.parse(await readFile(configPath, "utf8")) as Record<string, unknown>;
797
+ if (typeof config.vault_id === "string") {
798
+ if (!UUID.test(config.vault_id)) throw new QmdIndexError("config_invalid_vault_id", "config.json contains an invalid vault_id");
799
+ return config.vault_id;
800
+ }
801
+ if (config.vault_id !== undefined) {
802
+ throw new QmdIndexError("config_invalid_vault_id", "config.json contains a non-string vault_id");
803
+ }
804
+ const vaultId = randomUUID();
805
+ await atomicWriteJson(configPath, { ...config, vault_id: vaultId });
806
+ return vaultId;
807
+ }
808
+ ```
809
+
810
+ Do not rewrite a valid ID and do not replace an invalid ID.
811
+
812
+ - [ ] **Step 6: Implement cross-process index lock**
813
+
814
+ Use atomic directory creation at `meta/qmd/index.lock`, with `owner.json` containing `{ pid, hostname, acquiredAt }`.
815
+
816
+ ```ts
817
+ async function processExists(pid: number): Promise<boolean> {
818
+ try {
819
+ process.kill(pid, 0);
820
+ return true;
821
+ } catch (error: unknown) {
822
+ return (error as NodeJS.ErrnoException).code === "EPERM";
823
+ }
824
+ }
825
+ ```
826
+
827
+ On `EEXIST`, recover only if owner JSON is valid, hostname equals `node:os.hostname()`, and the PID is no longer alive. Never use an age-only timeout: vector indexing can legitimately run for minutes. Other-host, malformed, or live locks return `qmd_index_busy`. Always remove a lock acquired by this process in `finally`.
828
+
829
+ Keep one in-process promise queue per physical `paths.root` in addition to the lock, so calls from the same extension do not race or spuriously report busy.
830
+
831
+ - [ ] **Step 7: Implement staging and promotion**
832
+
833
+ For every mutation, including ordinary changed indexing:
834
+
835
+ 1. recover an interrupted prior swap;
836
+ 2. reconcile mirror (`all` rewrites accepted files, `changed` hashes and skips);
837
+ 3. create `staging-<randomUUID>` under `paths.qmd`;
838
+ 4. if lexical is not forced and current exists, recursively `cp(paths.qmdCurrent, staging, { recursive: true, errorOnExist: true })`; otherwise create empty staging;
839
+ 5. open only `staging/index.sqlite` using `openQmdIndexStore` and the committed `paths.qmdDocuments`;
840
+ 6. call `update` whenever lexical or vectors are selected; vectors require fresh document state;
841
+ 7. call `embed({ force })` only when vectors are selected;
842
+ 8. call `status`, require `totalDocuments === Object.keys(manifest.entries).length`, then close in `finally`;
843
+ 9. write `index-state.json` inside staging after close;
844
+ 10. reopen staging, validate status/counts again, and close before any rename;
845
+ 11. atomically write journal phase `prepared`;
846
+ 12. remove only a stale generated `previous` already proven safe by recovery;
847
+ 13. rename current to fixed `previous` when current exists, then journal `previous-moved`;
848
+ 14. rename staging to current, then journal `current-promoted`;
849
+ 15. reopen current, validate state/counts, close, then journal `validated`;
850
+ 16. remove previous and journal.
851
+
852
+ Use one helper to open, run, and close so all paths release QMD resources:
853
+
854
+ ```ts
855
+ async function withStore<T>(
856
+ factory: QmdStoreFactory,
857
+ input: { dbPath: string; documentsPath: string },
858
+ work: (store: QmdIndexStore) => Promise<T>,
859
+ ): Promise<T> {
860
+ const store = await factory(input);
861
+ try {
862
+ return await work(store);
863
+ } finally {
864
+ await store.close();
865
+ }
866
+ }
867
+ ```
868
+
869
+ No rename may run inside `work` or before `withStore` resolves.
870
+
871
+ - [ ] **Step 8: Implement deterministic recovery**
872
+
873
+ Expose the single injection seam used by recovery/indexing tests so they never touch QMD globals or real handles:
874
+
875
+ ```ts
876
+ export interface QmdIndexDeps {
877
+ factory: QmdStoreFactory;
878
+ fs?: {
879
+ exists(path: string): Promise<boolean>;
880
+ rename(from: string, to: string): Promise<void>;
881
+ rm(path: string, options: { recursive: boolean; force: boolean }): Promise<void>;
882
+ cp(from: string, to: string, options: { recursive: boolean; errorOnExist: boolean }): Promise<void>;
883
+ };
884
+ }
885
+ ```
886
+
887
+ `reindexQmdVault` and `recoverQmdIndex` accept optional `deps?: Partial<QmdIndexDeps>` and default to real `node:fs/promises`. Recovery tests use `deps` for the open-handle assertion: the fake `fs.rename` throws if the fake factory reports any store still open. The wrapper factory in `deps.factory` increments a counter on open and decrements on close, so every rename is provably closed-store-only.
888
+
889
+ `recoverQmdIndex(paths, deps?)` must acquire the same lock and validate journal names as basenames matching `staging-<uuid>`. Never use absolute or parent-relative paths from journal JSON.
890
+
891
+ Recovery behavior:
892
+
893
+ ```ts
894
+ switch (journal.phase) {
895
+ case "prepared":
896
+ await rm(staging, { recursive: true, force: true });
897
+ break;
898
+ case "previous-moved":
899
+ if (!(await pathExists(current)) && (await pathExists(previous))) {
900
+ await rename(previous, current);
901
+ }
902
+ await rm(staging, { recursive: true, force: true });
903
+ break;
904
+ case "current-promoted":
905
+ if (await validateCurrent(paths, factory)) {
906
+ await rm(previous, { recursive: true, force: true });
907
+ } else if (await pathExists(previous)) {
908
+ await rm(current, { recursive: true, force: true });
909
+ await rename(previous, current);
910
+ } else {
911
+ await rm(current, { recursive: true, force: true });
912
+ }
913
+ break;
914
+ case "validated":
915
+ await rm(previous, { recursive: true, force: true });
916
+ break;
917
+ }
918
+ await rm(paths.qmdSwap, { force: true });
919
+ ```
920
+
921
+ Before cleanup in `current-promoted`, close validation store. A malformed journal produces a diagnostic and leaves current/previous/staging untouched for human inspection.
922
+
923
+ - [ ] **Step 9: Implement generated status without opening QMD**
924
+
925
+ `readQmdIndexStatus(paths)` reads only manifest, `current/index-state.json`, optional `last-error.json`, lock, and swap journal. State rules:
926
+
927
+ 1. `recovering` when a valid swap journal exists;
928
+ 2. `missing` when current state is absent;
929
+ 3. `error` for invalid manifest/state or recorded last error with no usable current;
930
+ 4. `stale` when manifest hash, QMD package version, vault ID, or resolved embedding model differs from indexed state, or last error exists beside a usable current;
931
+ 5. `ready` otherwise.
932
+
933
+ Generation/rerank model changes are reported but do not invalidate lexical/vector index content. Embedding model mismatch marks vectors stale. Clear `last-error.json` only after successful promotion. Record errors atomically without deleting current.
934
+
935
+ - [ ] **Step 10: Run focused indexing and recovery tests**
936
+
937
+ Run:
938
+
939
+ ```bash
940
+ QMD_FORCE_CPU=1 pnpm exec vitest run test/qmd-indexing.test.ts test/qmd-indexing-recovery.test.ts test/qmd-contract.test.ts --reporter=verbose
941
+ pnpm typecheck
942
+ pnpm lint
943
+ ```
944
+
945
+ Expected: all model-free tests pass; model smoke skipped; failed and cancelled updates retain current.
946
+
947
+ - [ ] **Step 11: Commit**
948
+
949
+ ```bash
950
+ git add extensions/llm-wiki/lib/qmd-indexing.ts test/qmd-indexing.test.ts test/qmd-indexing-recovery.test.ts
951
+ git commit -m "feat: add recoverable QMD index lifecycle"
952
+ ```
953
+
954
+ ---
955
+
956
+ ### Task 5: Shared `wiki_reindex` for Pi and MCP
957
+
958
+ **Files:**
959
+ - Modify: `extensions/llm-wiki/lib/wiki-service.ts`
960
+ - Modify: `extensions/llm-wiki/lib/tools.ts`
961
+ - Modify: `extensions/llm-wiki/index.ts`
962
+ - Modify: `mcp/operations.ts`
963
+ - Modify: `mcp/index.ts`
964
+ - Create: `test/qmd-reindex-tool.test.ts`
965
+ - Modify: `test/mcp-parity.test.ts`
966
+ - Modify: `test/mcp-package.test.ts`
967
+ - Modify: `test/package-structure.test.ts`
968
+
969
+ - [ ] **Step 1: Write failing shared-operation and tool tests**
970
+
971
+ Capture the Pi tool registration and assert exact schema behavior:
972
+
973
+ ```ts
974
+ expect(tool.name).toBe("wiki_reindex");
975
+ const result = await tool.execute(
976
+ "id",
977
+ { scope: "changed", components: ["lexical"], force: false, vault: "active" },
978
+ new AbortController().signal,
979
+ onUpdate,
980
+ ctx,
981
+ );
982
+ expect(result.isError).not.toBe(true);
983
+ expect(result.details).toMatchObject({
984
+ scope: "changed",
985
+ components: ["lexical"],
986
+ vault: "active",
987
+ });
988
+ expect(result.content[0].text).toContain("QMD indexing complete");
989
+ ```
990
+
991
+ Test validation for empty components, unavailable `project` scope from a personal vault, and invalid writable-vault config. Abort before start and expect structured cancellation without current-store deletion.
992
+
993
+ For vault selection, assert:
994
+
995
+ - `active` processes only resolved active paths;
996
+ - `personal` processes only `getPersonalWikiPaths()`;
997
+ - `project` processes only active non-personal paths;
998
+ - `all` processes project and personal independently, deduplicating identical roots;
999
+ - one vault failure does not prevent the other result from being returned.
1000
+
1001
+ Update MCP parity to expect a seventh tool and compare the normalized `reindexWiki` result with `reindexOperation` using lexical-only temporary vaults.
1002
+
1003
+ Update packaged MCP test title and expected tool list, then invoke:
1004
+
1005
+ ```json
1006
+ {
1007
+ "name": "wiki_reindex",
1008
+ "arguments": {
1009
+ "scope": "changed",
1010
+ "components": ["lexical"],
1011
+ "force": false,
1012
+ "vault": "active"
1013
+ }
1014
+ }
1015
+ ```
1016
+
1017
+ Assert success and `.llm-wiki/meta/qmd/current/index.sqlite` existence.
1018
+
1019
+ - [ ] **Step 2: Run tests and verify failure**
1020
+
1021
+ Run:
1022
+
1023
+ ```bash
1024
+ pnpm exec vitest run test/qmd-reindex-tool.test.ts test/mcp-parity.test.ts test/mcp-package.test.ts test/package-structure.test.ts --reporter=verbose
1025
+ ```
1026
+
1027
+ Expected: missing operation/tool and six-vs-seven registration failures.
1028
+
1029
+ - [ ] **Step 3: Add shared vault selection and operation**
1030
+
1031
+ In `wiki-service.ts`, add:
1032
+
1033
+ ```ts
1034
+ export type WikiReindexVault = "active" | "personal" | "project" | "all";
1035
+
1036
+ export interface WikiReindexInput {
1037
+ scope?: "changed" | "all";
1038
+ components?: Array<"lexical" | "vectors">;
1039
+ force?: boolean;
1040
+ vault?: WikiReindexVault;
1041
+ signal?: AbortSignal;
1042
+ onProgress?: (progress: { vault: string; progress: QmdIndexProgress }) => void;
1043
+ }
1044
+
1045
+ export interface WikiReindexResult {
1046
+ vault: WikiReindexVault;
1047
+ results: Array<{ root: string; label: "active" | "personal" | "project"; result: QmdReindexResult }>;
1048
+ }
1049
+ ```
1050
+
1051
+ `reindexWiki(activePaths, input)` validates defaults exactly:
1052
+
1053
+ ```ts
1054
+ scope: input.scope ?? "changed"
1055
+ components: input.components ?? ["lexical", "vectors"]
1056
+ force: input.force ?? false
1057
+ vault: input.vault ?? "active"
1058
+ ```
1059
+
1060
+ Process selected vaults sequentially. Sequential execution avoids simultaneous model loads and gives deterministic progress/result order. Validate each vault with `inspectWritableVault` immediately before work.
1061
+
1062
+ - [ ] **Step 4: Register Pi tool**
1063
+
1064
+ Add `registerWikiReindex` in `tools.ts` with all required extension-tool fields:
1065
+
1066
+ ```ts
1067
+ parameters: Type.Object({
1068
+ scope: Type.Optional(Type.Union([Type.Literal("changed"), Type.Literal("all")], { default: "changed" })),
1069
+ components: Type.Optional(Type.Array(
1070
+ Type.Union([Type.Literal("lexical"), Type.Literal("vectors")]),
1071
+ { minItems: 1, uniqueItems: true, default: ["lexical", "vectors"] },
1072
+ )),
1073
+ force: Type.Optional(Type.Boolean({ default: false })),
1074
+ vault: Type.Optional(Type.Union([
1075
+ Type.Literal("active"),
1076
+ Type.Literal("personal"),
1077
+ Type.Literal("project"),
1078
+ Type.Literal("all"),
1079
+ ], { default: "active" })),
1080
+ }),
1081
+ ```
1082
+
1083
+ Use the tool's `signal`. Forward progress through `_onUpdate` as a short text block plus structured details. This explicit repair command runs foreground so cancellation remains connected; do not dispatch it through `Runtime.launchTask`.
1084
+
1085
+ Return the complete shared result in `details`. Selecting only lexical must say `model-free lexical indexing`; selecting vectors must warn before work that QMD may download approximately 2 GB of models on first use.
1086
+
1087
+ Register it in `extensions/llm-wiki/index.ts`. Update the entry comment from 13 to 14 standard tools.
1088
+
1089
+ - [ ] **Step 5: Add MCP operation and tool**
1090
+
1091
+ `mcp/operations.ts` adds a thin `reindexOperation(paths, input)` that returns `reindexWiki(paths, input)` with no duplicate indexing logic.
1092
+
1093
+ Register `wiki_reindex` in `mcp/index.ts` using Zod enums and a non-empty components array. Pass MCP cancellation signal when available. Return shared result as JSON. Update the module comment that describes the other tools as “the other five” to say “the other tools”.
1094
+
1095
+ - [ ] **Step 6: Update package structure and parity expectations**
1096
+
1097
+ Add `registerWikiReindex` to the production-registration assertion. Change MCP lists to exactly:
1098
+
1099
+ ```ts
1100
+ [
1101
+ "wiki_bootstrap",
1102
+ "wiki_recall",
1103
+ "wiki_search",
1104
+ "wiki_status",
1105
+ "wiki_reindex",
1106
+ "wiki_retro",
1107
+ "wiki_capture_source",
1108
+ ]
1109
+ ```
1110
+
1111
+ Use sorted order where the test already sorts.
1112
+
1113
+ - [ ] **Step 7: Run Pi/MCP tests and package build**
1114
+
1115
+ Run:
1116
+
1117
+ ```bash
1118
+ QMD_FORCE_CPU=1 pnpm exec vitest run test/qmd-reindex-tool.test.ts test/mcp-parity.test.ts test/mcp-package.test.ts test/package-structure.test.ts --reporter=verbose
1119
+ pnpm build:mcp
1120
+ pnpm typecheck
1121
+ pnpm lint
1122
+ ```
1123
+
1124
+ Expected: seven MCP tools; lexical reindex succeeds without model download.
1125
+
1126
+ - [ ] **Step 8: Commit**
1127
+
1128
+ ```bash
1129
+ git add extensions/llm-wiki/lib/wiki-service.ts extensions/llm-wiki/lib/tools.ts extensions/llm-wiki/index.ts mcp/operations.ts mcp/index.ts test/qmd-reindex-tool.test.ts test/mcp-parity.test.ts test/mcp-package.test.ts test/package-structure.test.ts
1130
+ git commit -m "feat: expose shared QMD reindex operation"
1131
+ ```
1132
+
1133
+ ---
1134
+
1135
+ ### Task 6: Post-Projection Scheduling and Startup Recovery
1136
+
1137
+ **Files:**
1138
+ - Modify: `extensions/llm-wiki/lib/indexing.ts`
1139
+ - Modify: `extensions/llm-wiki/lib/tools.ts`
1140
+ - Modify: `extensions/llm-wiki/index.ts`
1141
+ - Modify: `mcp/operations.ts`
1142
+ - Modify: `mcp/index.ts`
1143
+ - Modify: `test/indexing.test.ts`
1144
+ - Modify: `test/indexing-fail-closed.test.ts`
1145
+ - Modify: `test/background-tools.test.ts`
1146
+ - Modify: `test/guardrails.test.ts`
1147
+
1148
+ - [ ] **Step 1: Write failing scheduler tests**
1149
+
1150
+ Mock only `qmd-indexing.js`, not QMD. Extend indexing tests to prove:
1151
+
1152
+ ```ts
1153
+ expect(reindexQmdVault).toHaveBeenCalledWith(paths, expect.objectContaining({
1154
+ scope: "changed",
1155
+ components: ["lexical"],
1156
+ force: false,
1157
+ }));
1158
+ ```
1159
+
1160
+ Required cases:
1161
+
1162
+ 1. successful metadata projection schedules exactly one coalesced lexical QMD pass;
1163
+ 2. no automatic vector call occurs;
1164
+ 3. writes arriving during QMD work cause one trailing pass and are not lost;
1165
+ 4. QMD failure does not reject the page write or skip the legacy embedding refresh;
1166
+ 5. blocking metadata projection does **not** call full reconciliation or index valid additions;
1167
+ 6. blocking projection calls only unsafe-entry invalidation and then a lexical removal update when entries were removed;
1168
+ 7. `wiki_rebuild_meta` awaits lexical QMD indexing only after successful projection and reports QMD failure as a warning;
1169
+ 8. direct writes under `meta/qmd/**` remain blocked by existing meta guardrail;
1170
+ 9. no recall function is imported or changed.
1171
+
1172
+ - [ ] **Step 2: Run scheduler tests and verify failure**
1173
+
1174
+ Run:
1175
+
1176
+ ```bash
1177
+ pnpm exec vitest run test/indexing.test.ts test/indexing-fail-closed.test.ts test/background-tools.test.ts test/guardrails.test.ts --reporter=verbose
1178
+ ```
1179
+
1180
+ Expected: QMD scheduling assertions fail.
1181
+
1182
+ - [ ] **Step 3: Chain model-free indexing after metadata**
1183
+
1184
+ In the existing `scheduleReindex` drain loop:
1185
+
1186
+ ```ts
1187
+ const projection = rebuildMetadataLight(paths);
1188
+ if (!projection.ok) {
1189
+ await invalidateQmdAfterProjectionFailure(paths);
1190
+ continue;
1191
+ }
1192
+
1193
+ try {
1194
+ await reindexQmdVault(paths, {
1195
+ scope: "changed",
1196
+ components: ["lexical"],
1197
+ force: false,
1198
+ });
1199
+ } catch {
1200
+ // Generated search indexing is repairable and must not fail the authoritative write.
1201
+ }
1202
+
1203
+ runtime.ensureConfig(root);
1204
+ const embedder = resolveEmbedder(runtime.config);
1205
+ if (embedder) await reindexEmbeddings(paths, embedder);
1206
+ ```
1207
+
1208
+ `invalidateQmdAfterProjectionFailure` backfills/validates `vault_id`, removes only unsafe entries, and runs a staging lexical update only when removals occurred. It must never add/update valid mirror pages after a projection failure.
1209
+
1210
+ Keep the existing dirty flag set until metadata, QMD, and legacy embedding work all finish so writes during any awaited stage trigger the trailing pass.
1211
+
1212
+ - [ ] **Step 4: Update explicit metadata rebuild and MCP writers**
1213
+
1214
+ Inside `wiki_rebuild_meta`'s existing background work, after `result.ok`, await changed lexical QMD indexing. Return metadata success plus structured QMD warning if indexing fails; the projection remains successful.
1215
+
1216
+ After successful `rebuildMetadata` in MCP authoritative write operations, enqueue changed lexical QMD indexing through the same per-vault in-process queue and catch errors. Do not await model work and do not return an authoritative write as failed because generated QMD state failed. Add a test-only `awaitQmdIndexQueue(paths.root)` export so MCP parity tests can drain queued work deterministically.
1217
+
1218
+ - [ ] **Step 5: Wire startup recovery**
1219
+
1220
+ In Pi `session_start`, after writable-vault validation, launch one background recovery task labeled `qmd-recovery:<root>`. Recovery is generated-state repair and must not block existing heuristic recall.
1221
+
1222
+ In MCP `main`, connect the transport first so clients are never blocked by recovery, then run `recoverQmdIndex(getPaths())` as a fire-and-forget task whenever a configured vault exists. Log all outcomes to stderr. A busy/live lock logs one warning and MCP continues with current state untouched; malformed recovery state stays visible through `wiki_status` and `wiki_lint` instead of stalling startup.
1223
+
1224
+ Every explicit `reindexQmdVault` also calls recovery first, so repair remains correct when startup hooks were skipped in direct-library tests.
1225
+
1226
+ - [ ] **Step 6: Run lifecycle tests**
1227
+
1228
+ Run:
1229
+
1230
+ ```bash
1231
+ QMD_FORCE_CPU=1 pnpm exec vitest run test/indexing.test.ts test/indexing-fail-closed.test.ts test/background-tools.test.ts test/guardrails.test.ts test/mcp-parity.test.ts --reporter=verbose
1232
+ pnpm typecheck
1233
+ pnpm lint
1234
+ ```
1235
+
1236
+ Expected: successful projections schedule lexical indexing; malformed pages can only remove old QMD candidates; existing recall tests remain untouched.
1237
+
1238
+ - [ ] **Step 7: Commit**
1239
+
1240
+ ```bash
1241
+ git add extensions/llm-wiki/lib/indexing.ts extensions/llm-wiki/lib/tools.ts extensions/llm-wiki/index.ts mcp/operations.ts mcp/index.ts test/indexing.test.ts test/indexing-fail-closed.test.ts test/background-tools.test.ts test/guardrails.test.ts test/mcp-parity.test.ts
1242
+ git commit -m "feat: maintain QMD indexes after metadata rebuilds"
1243
+ ```
1244
+
1245
+ ---
1246
+
1247
+ ### Task 7: Status and Lint Diagnostics
1248
+
1249
+ **Files:**
1250
+ - Modify: `extensions/llm-wiki/lib/wiki-service.ts`
1251
+ - Modify: `extensions/llm-wiki/lib/tools.ts`
1252
+ - Modify: `mcp/operations.ts`
1253
+ - Modify: `test/mcp-parity.test.ts`
1254
+ - Modify: `test/lint-okf.test.ts`
1255
+ - Modify: `test/background-tools.test.ts`
1256
+
1257
+ - [ ] **Step 1: Write failing status and lint tests**
1258
+
1259
+ Extend shared status expectations:
1260
+
1261
+ ```ts
1262
+ expect(status.qmd).toMatchObject({
1263
+ state: "ready",
1264
+ totalDocuments: 2,
1265
+ canonicalDocuments: 1,
1266
+ evidenceDocuments: 1,
1267
+ needsEmbedding: 2,
1268
+ hasVectorIndex: false,
1269
+ qmdVersion: "2.5.3",
1270
+ });
1271
+ ```
1272
+
1273
+ Test `missing`, `stale` manifest hash, embedding-model mismatch, valid interrupted swap, malformed state, and recorded last error.
1274
+
1275
+ Lint must print one finding for stale/error/recovering QMD state, include stable code, and recommend the exact repair command:
1276
+
1277
+ ```text
1278
+ wiki_reindex(scope="changed", components=["lexical"], vault="active")
1279
+ ```
1280
+
1281
+ Missing QMD state is informational before first indexing, not a blocking lint failure. A stale vector-only state recommends components `vectors`.
1282
+
1283
+ MCP status must equal shared `getWikiStatus`, including QMD object and diagnostics.
1284
+
1285
+ - [ ] **Step 2: Run tests and verify failure**
1286
+
1287
+ Run:
1288
+
1289
+ ```bash
1290
+ pnpm exec vitest run test/lint-okf.test.ts test/background-tools.test.ts test/mcp-parity.test.ts --reporter=verbose
1291
+ ```
1292
+
1293
+ Expected: missing `qmd` status and lint findings.
1294
+
1295
+ - [ ] **Step 3: Extend shared status**
1296
+
1297
+ Make `getWikiStatus` asynchronous and add:
1298
+
1299
+ ```ts
1300
+ export interface WikiStatusSnapshot {
1301
+ knowledgeFormat: KnowledgeFormat;
1302
+ totalPages: number;
1303
+ byType: Record<string, number>;
1304
+ blockingDiagnostics: KnowledgeDiagnostic[];
1305
+ lastUpdated: string;
1306
+ qmd: QmdGeneratedStatus;
1307
+ }
1308
+ ```
1309
+
1310
+ Await `readQmdIndexStatus(paths)` once. Update Pi, MCP, and tests to await `getWikiStatus`. Keep registry counts and knowledge-format behavior unchanged.
1311
+
1312
+ Pi status text adds:
1313
+
1314
+ ```text
1315
+ QMD index: ready|missing|stale|recovering|error
1316
+ QMD documents: <total> (<canonical> canonical, <evidence> evidence)
1317
+ QMD embeddings pending: <count>
1318
+ QMD package: 2.5.3
1319
+ ```
1320
+
1321
+ Tool `details.qmd` is the complete shared status object.
1322
+
1323
+ - [ ] **Step 4: Extend lint without mutating QMD**
1324
+
1325
+ Make `runWikiLint` async and append findings derived from `readQmdIndexStatus`. Lint may inspect state but must not recover, reindex, remove files, or download models. Keep existing `auto_fix` behavior limited to its current page/metadata fixes.
1326
+
1327
+ Map states to diagnostics:
1328
+
1329
+ - `stale` → `qmd_index_stale` warning and component-specific repair command;
1330
+ - `recovering` → `qmd_swap_interrupted` warning and restart/reindex guidance;
1331
+ - `error` → `qmd_index_error` warning with stored safe message;
1332
+ - `missing` → no failure, one informational status line.
1333
+
1334
+ Do not include absolute source paths or model-cache paths in chat text. Structured local details may retain generated artifact paths.
1335
+
1336
+ - [ ] **Step 5: Run status/lint tests**
1337
+
1338
+ Run:
1339
+
1340
+ ```bash
1341
+ pnpm exec vitest run test/lint-okf.test.ts test/background-tools.test.ts test/mcp-parity.test.ts test/qmd-indexing.test.ts --reporter=verbose
1342
+ pnpm typecheck
1343
+ pnpm lint
1344
+ ```
1345
+
1346
+ Expected: Pi and MCP status objects match; lint is read-only for QMD.
1347
+
1348
+ - [ ] **Step 6: Commit**
1349
+
1350
+ ```bash
1351
+ git add extensions/llm-wiki/lib/wiki-service.ts extensions/llm-wiki/lib/tools.ts mcp/operations.ts test/mcp-parity.test.ts test/lint-okf.test.ts test/background-tools.test.ts
1352
+ git commit -m "feat: report QMD index health"
1353
+ ```
1354
+
1355
+ ---
1356
+
1357
+ ### Task 8: Operator Documentation and Final Phase Verification
1358
+
1359
+ **Files:**
1360
+ - Modify: `README.md`
1361
+ - Modify: `docs/api.md`
1362
+ - Modify: `docs/architecture.md`
1363
+ - Modify: `docs/commands.md`
1364
+ - Modify: `skills/llm-wiki/SKILL.md`
1365
+ - Modify: `test/package-structure.test.ts`
1366
+
1367
+ - [ ] **Step 1: Write failing documentation assertions**
1368
+
1369
+ Add package-structure checks for:
1370
+
1371
+ ```ts
1372
+ for (const path of ["README.md", "docs/api.md", "docs/architecture.md", "docs/commands.md"]) {
1373
+ const content = readFile(join(rootDir, path));
1374
+ expect(content, path).toContain("wiki_reindex");
1375
+ expect(content, path).toContain("meta/qmd");
1376
+ }
1377
+ expect(readFile(join(rootDir, "docs/api.md"))).toContain('components: ["lexical", "vectors"]');
1378
+ expect(readFile(join(rootDir, "docs/architecture.md"))).toContain("generated and rebuildable");
1379
+ ```
1380
+
1381
+ - [ ] **Step 2: Run documentation test and verify failure**
1382
+
1383
+ Run:
1384
+
1385
+ ```bash
1386
+ pnpm exec vitest run test/package-structure.test.ts --reporter=verbose
1387
+ ```
1388
+
1389
+ Expected: missing Phase 2 documentation.
1390
+
1391
+ - [ ] **Step 3: Document ownership and operational semantics**
1392
+
1393
+ Document these facts consistently:
1394
+
1395
+ - `.llm-wiki/wiki/**` remains authoritative and user editable;
1396
+ - `.llm-wiki/meta/qmd/**` is extension-owned, generated, local, and rebuildable;
1397
+ - QMD never scans authoritative Markdown directly;
1398
+ - `manifest.json` maps validated mirrors back to `(vault_id, page_id)`;
1399
+ - canonical and evidence collections never overlap;
1400
+ - ordinary write-triggered updates are lexical and model-free;
1401
+ - vector selection may trigger approximately 2 GB of first-use downloads;
1402
+ - cancellation and failures retain the last usable current store;
1403
+ - stale/error/recovering status is repaired with `wiki_reindex`;
1404
+ - full-vault backups include generated searchable text, while OKF-only exports do not;
1405
+ - users must not edit, copy partially, or restore individual SQLite/WAL/SHM files inside current; restore the whole generated directory or rebuild;
1406
+ - active recall remains the old heuristic until Phase 3.
1407
+
1408
+ Add the complete tool signature to `docs/api.md`:
1409
+
1410
+ ```text
1411
+ wiki_reindex(
1412
+ scope: "changed" | "all" = "changed",
1413
+ components: ("lexical" | "vectors")[] = ["lexical", "vectors"],
1414
+ force: boolean = false,
1415
+ vault: "active" | "personal" | "project" | "all" = "active"
1416
+ )
1417
+ ```
1418
+
1419
+ Explain that `vectors` first refreshes documents, lexical-only never loads models, `force` applies only to selected components, and `all` vault scope reports each vault independently.
1420
+
1421
+ Add `wiki_reindex` to Pi and MCP tool tables. Do not claim QMD powers recall yet.
1422
+
1423
+ - [ ] **Step 4: Run all model-free verification**
1424
+
1425
+ Run exactly:
1426
+
1427
+ ```bash
1428
+ QMD_FORCE_CPU=1 pnpm exec vitest run test/qmd-mirror.test.ts test/qmd-contract.test.ts test/qmd-indexing.test.ts test/qmd-indexing-recovery.test.ts test/qmd-reindex-tool.test.ts --reporter=verbose
1429
+ pnpm test
1430
+ pnpm typecheck
1431
+ pnpm lint
1432
+ pnpm build:mcp
1433
+ pnpm benchmark:retrieval
1434
+ ```
1435
+
1436
+ Expected:
1437
+
1438
+ - all tests pass;
1439
+ - QMD model smoke remains skipped;
1440
+ - retrieval baseline metrics remain identical to Phase 1 because active recall is unchanged;
1441
+ - MCP package exposes seven tools;
1442
+ - `git diff --check` passes.
1443
+
1444
+ - [ ] **Step 5: Run optional cached model smoke**
1445
+
1446
+ Only when pinned models are already cached or the operator explicitly accepts the download:
1447
+
1448
+ ```bash
1449
+ QMD_MODEL_SMOKE=1 QMD_FORCE_CPU=1 pnpm exec vitest run test/qmd-contract.test.ts test/qmd-indexing.test.ts --reporter=verbose
1450
+ ```
1451
+
1452
+ Expected: embedding succeeds, vector status becomes fresh, and no reranking/query behavior is wired into recall.
1453
+
1454
+ - [ ] **Step 6: Verify phase scope mechanically**
1455
+
1456
+ Run:
1457
+
1458
+ ```bash
1459
+ git diff main...HEAD -- extensions/llm-wiki/lib/recall.ts extensions/llm-wiki/lib/inject.ts
1460
+
1461
+ git grep -n "@tobilu/qmd" -- extensions/llm-wiki | grep -v "lib/qmd-store.ts"
1462
+
1463
+ git diff --check
1464
+
1465
+ git status --short
1466
+ ```
1467
+
1468
+ Expected:
1469
+
1470
+ - no diff in active recall or injection files;
1471
+ - no production QMD import outside `qmd-store.ts`;
1472
+ - no whitespace errors;
1473
+ - only planned files changed.
1474
+
1475
+ - [ ] **Step 7: Commit**
1476
+
1477
+ ```bash
1478
+ git add README.md docs/api.md docs/architecture.md docs/commands.md skills/llm-wiki/SKILL.md test/package-structure.test.ts
1479
+ git commit -m "docs: document validated QMD indexing"
1480
+ ```
1481
+
1482
+ - [ ] **Step 8: Inspect final history and diff**
1483
+
1484
+ Run:
1485
+
1486
+ ```bash
1487
+ git status --short
1488
+ git log --oneline -8
1489
+ git diff --stat main...HEAD
1490
+ git diff --check main...HEAD
1491
+ ```
1492
+
1493
+ Expected: clean implementation tree and eight focused Phase 2 commits after the planning commit. Existing recall remains functional and unchanged; QMD indexes are independently buildable, updateable, inspectable, cancellable, and recoverable. Phase 3 may then plan retrieval modes and recall cutover.