@mfittko/repo-wiki 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/.llmwiki/schema.md +107 -0
  2. package/AGENTS.md +42 -0
  3. package/CHANGELOG.md +91 -0
  4. package/LICENSE +21 -0
  5. package/README.md +254 -0
  6. package/dist/bin/repo-wiki.d.ts +2 -0
  7. package/dist/bin/repo-wiki.js +7 -0
  8. package/dist/bin/repo-wiki.js.map +1 -0
  9. package/dist/src/cli.d.ts +1 -0
  10. package/dist/src/cli.js +404 -0
  11. package/dist/src/cli.js.map +1 -0
  12. package/dist/src/compiler.d.ts +55 -0
  13. package/dist/src/compiler.js +2046 -0
  14. package/dist/src/compiler.js.map +1 -0
  15. package/dist/src/config.d.ts +63 -0
  16. package/dist/src/config.js +86 -0
  17. package/dist/src/config.js.map +1 -0
  18. package/dist/src/context-assembler.d.ts +68 -0
  19. package/dist/src/context-assembler.js +378 -0
  20. package/dist/src/context-assembler.js.map +1 -0
  21. package/dist/src/data-model-signals.d.ts +1 -0
  22. package/dist/src/data-model-signals.js +13 -0
  23. package/dist/src/data-model-signals.js.map +1 -0
  24. package/dist/src/docs-ingestor.d.ts +138 -0
  25. package/dist/src/docs-ingestor.js +844 -0
  26. package/dist/src/docs-ingestor.js.map +1 -0
  27. package/dist/src/docs-linter.d.ts +14 -0
  28. package/dist/src/docs-linter.js +164 -0
  29. package/dist/src/docs-linter.js.map +1 -0
  30. package/dist/src/docs-validation.d.ts +36 -0
  31. package/dist/src/docs-validation.js +297 -0
  32. package/dist/src/docs-validation.js.map +1 -0
  33. package/dist/src/extractors.d.ts +50 -0
  34. package/dist/src/extractors.js +2275 -0
  35. package/dist/src/extractors.js.map +1 -0
  36. package/dist/src/frontmatter.d.ts +46 -0
  37. package/dist/src/frontmatter.js +377 -0
  38. package/dist/src/frontmatter.js.map +1 -0
  39. package/dist/src/index.d.ts +26 -0
  40. package/dist/src/index.js +18 -0
  41. package/dist/src/index.js.map +1 -0
  42. package/dist/src/init.d.ts +12 -0
  43. package/dist/src/init.js +121 -0
  44. package/dist/src/init.js.map +1 -0
  45. package/dist/src/language.d.ts +2 -0
  46. package/dist/src/language.js +62 -0
  47. package/dist/src/language.js.map +1 -0
  48. package/dist/src/linter.d.ts +33 -0
  49. package/dist/src/linter.js +398 -0
  50. package/dist/src/linter.js.map +1 -0
  51. package/dist/src/llm-provider.d.ts +267 -0
  52. package/dist/src/llm-provider.js +474 -0
  53. package/dist/src/llm-provider.js.map +1 -0
  54. package/dist/src/page-ownership.d.ts +38 -0
  55. package/dist/src/page-ownership.js +96 -0
  56. package/dist/src/page-ownership.js.map +1 -0
  57. package/dist/src/planner.d.ts +55 -0
  58. package/dist/src/planner.js +422 -0
  59. package/dist/src/planner.js.map +1 -0
  60. package/dist/src/prompts.d.ts +103 -0
  61. package/dist/src/prompts.js +344 -0
  62. package/dist/src/prompts.js.map +1 -0
  63. package/dist/src/publisher.d.ts +68 -0
  64. package/dist/src/publisher.js +662 -0
  65. package/dist/src/publisher.js.map +1 -0
  66. package/dist/src/repository-analysis.d.ts +88 -0
  67. package/dist/src/repository-analysis.js +485 -0
  68. package/dist/src/repository-analysis.js.map +1 -0
  69. package/dist/src/scanner.d.ts +122 -0
  70. package/dist/src/scanner.js +309 -0
  71. package/dist/src/scanner.js.map +1 -0
  72. package/dist/src/search.d.ts +71 -0
  73. package/dist/src/search.js +410 -0
  74. package/dist/src/search.js.map +1 -0
  75. package/dist/src/secret-patterns.d.ts +3 -0
  76. package/dist/src/secret-patterns.js +14 -0
  77. package/dist/src/secret-patterns.js.map +1 -0
  78. package/dist/src/utils/args.d.ts +2 -0
  79. package/dist/src/utils/args.js +19 -0
  80. package/dist/src/utils/args.js.map +1 -0
  81. package/dist/src/utils/dotenv.d.ts +7 -0
  82. package/dist/src/utils/dotenv.js +73 -0
  83. package/dist/src/utils/dotenv.js.map +1 -0
  84. package/dist/src/utils/fs.d.ts +22 -0
  85. package/dist/src/utils/fs.js +83 -0
  86. package/dist/src/utils/fs.js.map +1 -0
  87. package/dist/src/utils/git.d.ts +13 -0
  88. package/dist/src/utils/git.js +39 -0
  89. package/dist/src/utils/git.js.map +1 -0
  90. package/dist/src/wiki-graph.d.ts +74 -0
  91. package/dist/src/wiki-graph.js +335 -0
  92. package/dist/src/wiki-graph.js.map +1 -0
  93. package/dist/src/wiki-patch.d.ts +152 -0
  94. package/dist/src/wiki-patch.js +489 -0
  95. package/dist/src/wiki-patch.js.map +1 -0
  96. package/dist/src/wiki-query.d.ts +63 -0
  97. package/dist/src/wiki-query.js +255 -0
  98. package/dist/src/wiki-query.js.map +1 -0
  99. package/dist/test/cli.test.d.ts +1 -0
  100. package/dist/test/cli.test.js +514 -0
  101. package/dist/test/cli.test.js.map +1 -0
  102. package/dist/test/compiler-eval.test.d.ts +1 -0
  103. package/dist/test/compiler-eval.test.js +234 -0
  104. package/dist/test/compiler-eval.test.js.map +1 -0
  105. package/dist/test/compiler.test.d.ts +1 -0
  106. package/dist/test/compiler.test.js +2537 -0
  107. package/dist/test/compiler.test.js.map +1 -0
  108. package/dist/test/context-assembler.test.d.ts +1 -0
  109. package/dist/test/context-assembler.test.js +379 -0
  110. package/dist/test/context-assembler.test.js.map +1 -0
  111. package/dist/test/docs-linter.test.d.ts +1 -0
  112. package/dist/test/docs-linter.test.js +900 -0
  113. package/dist/test/docs-linter.test.js.map +1 -0
  114. package/dist/test/dotenv.test.d.ts +1 -0
  115. package/dist/test/dotenv.test.js +77 -0
  116. package/dist/test/dotenv.test.js.map +1 -0
  117. package/dist/test/extractors-go.test.d.ts +1 -0
  118. package/dist/test/extractors-go.test.js +393 -0
  119. package/dist/test/extractors-go.test.js.map +1 -0
  120. package/dist/test/extractors-rust.test.d.ts +1 -0
  121. package/dist/test/extractors-rust.test.js +219 -0
  122. package/dist/test/extractors-rust.test.js.map +1 -0
  123. package/dist/test/extractors-utils.test.d.ts +1 -0
  124. package/dist/test/extractors-utils.test.js +786 -0
  125. package/dist/test/extractors-utils.test.js.map +1 -0
  126. package/dist/test/fixtures/compiler-e2e/basic-node-service/repo/infra/deploy.d.ts +1 -0
  127. package/dist/test/fixtures/compiler-e2e/basic-node-service/repo/infra/deploy.js +4 -0
  128. package/dist/test/fixtures/compiler-e2e/basic-node-service/repo/infra/deploy.js.map +1 -0
  129. package/dist/test/frontmatter.test.d.ts +1 -0
  130. package/dist/test/frontmatter.test.js +287 -0
  131. package/dist/test/frontmatter.test.js.map +1 -0
  132. package/dist/test/init-planner.test.d.ts +1 -0
  133. package/dist/test/init-planner.test.js +688 -0
  134. package/dist/test/init-planner.test.js.map +1 -0
  135. package/dist/test/linter.test.d.ts +1 -0
  136. package/dist/test/linter.test.js +426 -0
  137. package/dist/test/linter.test.js.map +1 -0
  138. package/dist/test/llm-provider.test.d.ts +1 -0
  139. package/dist/test/llm-provider.test.js +783 -0
  140. package/dist/test/llm-provider.test.js.map +1 -0
  141. package/dist/test/page-ownership.test.d.ts +1 -0
  142. package/dist/test/page-ownership.test.js +247 -0
  143. package/dist/test/page-ownership.test.js.map +1 -0
  144. package/dist/test/publisher.test.d.ts +1 -0
  145. package/dist/test/publisher.test.js +1297 -0
  146. package/dist/test/publisher.test.js.map +1 -0
  147. package/dist/test/repository-analysis.test.d.ts +1 -0
  148. package/dist/test/repository-analysis.test.js +182 -0
  149. package/dist/test/repository-analysis.test.js.map +1 -0
  150. package/dist/test/run-compiled-tests.d.ts +1 -0
  151. package/dist/test/run-compiled-tests.js +48 -0
  152. package/dist/test/run-compiled-tests.js.map +1 -0
  153. package/dist/test/scanner.test.d.ts +1 -0
  154. package/dist/test/scanner.test.js +551 -0
  155. package/dist/test/scanner.test.js.map +1 -0
  156. package/dist/test/search.test.d.ts +1 -0
  157. package/dist/test/search.test.js +92 -0
  158. package/dist/test/search.test.js.map +1 -0
  159. package/dist/test/update-changelog.test.d.ts +1 -0
  160. package/dist/test/update-changelog.test.js +125 -0
  161. package/dist/test/update-changelog.test.js.map +1 -0
  162. package/dist/test/wiki-graph.test.d.ts +1 -0
  163. package/dist/test/wiki-graph.test.js +164 -0
  164. package/dist/test/wiki-graph.test.js.map +1 -0
  165. package/dist/test/wiki-patch.test.d.ts +1 -0
  166. package/dist/test/wiki-patch.test.js +610 -0
  167. package/dist/test/wiki-patch.test.js.map +1 -0
  168. package/dist/test/wiki-query.test.d.ts +1 -0
  169. package/dist/test/wiki-query.test.js +163 -0
  170. package/dist/test/wiki-query.test.js.map +1 -0
  171. package/docs/PLAN.md +993 -0
  172. package/docs/WHY.md +61 -0
  173. package/docs/plans/agent-integration.md +85 -0
  174. package/docs/plans/ci-publishing.md +111 -0
  175. package/docs/plans/doc-validation.md +92 -0
  176. package/docs/plans/github-action.md +113 -0
  177. package/docs/plans/incremental-mode.md +98 -0
  178. package/docs/plans/karpathy-llm-wiki-alignment.md +84 -0
  179. package/docs/plans/llm-compiler.md +160 -0
  180. package/docs/plans/production-scanner.md +104 -0
  181. package/docs/plans/query-and-file-back.md +103 -0
  182. package/docs/plans/search-index.md +118 -0
  183. package/docs/plans/trust-hardening.md +74 -0
  184. package/docs/plans/wiki-graph.md +183 -0
  185. package/docs/plans/wiki-health.md +76 -0
  186. package/package.json +83 -0
  187. package/prompts/compiler.md +16 -0
  188. package/prompts/lint.md +18 -0
  189. package/prompts/page-templates.md +25 -0
  190. package/skills/repo-wiki-cli/SKILL.md +139 -0
@@ -0,0 +1,688 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtemp, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { initProject } from '../src/init.js';
7
+ import { createBootstrapPlan } from '../src/planner.js';
8
+ import { readJson } from '../src/utils/fs.js';
9
+ async function exists(filePath) {
10
+ try {
11
+ await stat(filePath);
12
+ return true;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ test('initProject writes defaults, agent pointer, skip behavior, and force overwrite', async () => {
19
+ const repoDir = await mkdtemp(path.join(os.tmpdir(), 'repo-wiki-init-'));
20
+ try {
21
+ const first = await initProject({ repoPath: repoDir, writeAgents: true });
22
+ assert.deepEqual(first.summary.skipped, []);
23
+ assert.ok(first.summary.written.includes('.llmwiki/config.json'));
24
+ assert.ok(first.summary.written.includes('.llmwiki/schema.md'));
25
+ assert.ok(first.summary.written.includes('AGENTS.repo-wiki.md'));
26
+ assert.equal(await exists(path.join(repoDir, '.llmwiki', 'config.json')), true);
27
+ assert.equal(await exists(path.join(repoDir, '.llmwiki', 'schema.md')), true);
28
+ assert.equal(await exists(path.join(repoDir, 'AGENTS.repo-wiki.md')), true);
29
+ const originalPointer = await readFile(path.join(repoDir, 'AGENTS.repo-wiki.md'), 'utf8');
30
+ await writeFile(path.join(repoDir, 'AGENTS.repo-wiki.md'), 'custom\n', 'utf8');
31
+ const second = await initProject({ repoPath: repoDir, writeAgents: true });
32
+ assert.equal(second.summary.written.length, 0);
33
+ assert.ok(second.summary.skipped.includes('AGENTS.repo-wiki.md'));
34
+ assert.equal(await readFile(path.join(repoDir, 'AGENTS.repo-wiki.md'), 'utf8'), 'custom\n');
35
+ const forced = await initProject({ repoPath: repoDir, writeAgents: true, force: true });
36
+ assert.ok(forced.summary.written.includes('AGENTS.repo-wiki.md'));
37
+ assert.equal(await readFile(path.join(repoDir, 'AGENTS.repo-wiki.md'), 'utf8'), originalPointer);
38
+ }
39
+ finally {
40
+ await rm(repoDir, { recursive: true, force: true });
41
+ }
42
+ });
43
+ test('createBootstrapPlan groups modules and emits cross-cutting pages from manifest signals', async () => {
44
+ const dir = await mkdtemp(path.join(os.tmpdir(), 'repo-wiki-plan-'));
45
+ const scanDir = path.join(dir, 'scan');
46
+ const outFile = path.join(dir, 'bootstrap-plan.json');
47
+ try {
48
+ await mkdir(scanDir, { recursive: true });
49
+ await writeFile(path.join(scanDir, 'manifest.json'), JSON.stringify({
50
+ mode: 'bootstrap',
51
+ repo_path: dir,
52
+ remote: 'origin',
53
+ commit: 'abc123',
54
+ totals: {
55
+ runtime_hints: { 'http-route': 1 },
56
+ categories: { data: 1 }
57
+ },
58
+ files: [
59
+ { path: 'apps/web/server.ts', category: 'source', language: 'TypeScript', runtime_hints: ['http-route'], reasons: ['api-surface'], bytes: 100 },
60
+ { path: 'services/auth/index.ts', category: 'source', language: 'TypeScript', runtime_hints: [], reasons: ['auth'], bytes: 100 },
61
+ { path: 'packages/core/index.ts', category: 'source', language: 'TypeScript', runtime_hints: [], reasons: ['source'], bytes: 100 },
62
+ { path: 'src/compiler.ts', category: 'source', language: 'TypeScript', runtime_hints: [], reasons: ['source'], bytes: 100 },
63
+ { path: 'lib/helpers.ts', category: 'source', language: 'TypeScript', runtime_hints: [], reasons: ['source'], bytes: 100 },
64
+ { path: 'infra/docker.ts', category: 'infra', language: 'TypeScript', runtime_hints: ['deployment'], reasons: ['infra'], bytes: 100 },
65
+ { path: 'docs/guide.md', category: 'docs', language: 'Markdown', runtime_hints: [], reasons: ['docs'], bytes: 100 },
66
+ { path: '.github/workflows/ci.yml', category: 'ci', language: 'YAML', runtime_hints: [], reasons: ['ci'], bytes: 100 },
67
+ { path: 'root-file.ts', category: 'source', language: 'TypeScript', runtime_hints: [], reasons: ['source'], bytes: 100 },
68
+ { path: 'db/migrations/001.sql', category: 'data', language: 'SQL', runtime_hints: [], reasons: ['data-model'], bytes: 100 },
69
+ { path: 'src/ignored.ts', category: 'source', language: 'TypeScript', runtime_hints: [], reasons: ['source'], bytes: 1_000_001, skipped_content: true }
70
+ ]
71
+ }, null, 2), 'utf8');
72
+ const result = await createBootstrapPlan({ scanDir, outFile });
73
+ const plan = await readJson(outFile);
74
+ assert.equal(result.summary.outFile, outFile);
75
+ assert.ok(plan.modules.some((module) => module.name === 'Service web'));
76
+ assert.ok(plan.modules.some((module) => module.name === 'Service auth'));
77
+ assert.ok(plan.modules.some((module) => module.name === 'Package core'));
78
+ assert.ok(plan.modules.some((module) => module.name === 'Module compiler.ts' || module.name === 'Module compiler'));
79
+ assert.ok(plan.modules.some((module) => module.name === 'Module helpers.ts' || module.name === 'Module helpers'));
80
+ assert.ok(plan.modules.some((module) => module.name === 'Infrastructure'));
81
+ assert.ok(plan.modules.some((module) => module.name === 'Documentation'));
82
+ assert.ok(plan.modules.some((module) => module.name === 'CI and Automation'));
83
+ assert.ok(plan.modules.some((module) => module.name === 'Repository Root'));
84
+ assert.ok(!plan.modules.some((module) => module.files.includes('src/ignored.ts')));
85
+ assert.ok(plan.pages.some((page) => page.path === 'API-HTTP-Routes.md'));
86
+ assert.ok(plan.pages.some((page) => page.path === 'Data-Model-and-Migrations.md'));
87
+ assert.ok(plan.phases.some((phase) => phase.name === 'link-and-lint'));
88
+ }
89
+ finally {
90
+ await rm(dir, { recursive: true, force: true });
91
+ }
92
+ });
93
+ test('createBootstrapPlan emits data-model page for ORM-only signal paths', async () => {
94
+ const dir = await mkdtemp(path.join(os.tmpdir(), 'repo-wiki-plan-orm-'));
95
+ const scanDir = path.join(dir, 'scan');
96
+ const outFile = path.join(dir, 'bootstrap-plan.json');
97
+ try {
98
+ await mkdir(scanDir, { recursive: true });
99
+ await writeFile(path.join(scanDir, 'manifest.json'), JSON.stringify({
100
+ mode: 'bootstrap',
101
+ repo_path: dir,
102
+ remote: 'origin',
103
+ commit: 'abc123',
104
+ totals: {
105
+ runtime_hints: { 'orm-model': 1 },
106
+ categories: { source: 1 }
107
+ },
108
+ files: [
109
+ {
110
+ path: 'src/models/user.entity.ts',
111
+ category: 'source',
112
+ language: 'TypeScript',
113
+ runtime_hints: ['data-model', 'orm-model'],
114
+ reasons: ['data-model', 'orm-model'],
115
+ model_surfaces: [{ name: 'UserEntity', kind: 'entity', framework: 'typeorm' }],
116
+ bytes: 100
117
+ }
118
+ ]
119
+ }, null, 2), 'utf8');
120
+ const result = await createBootstrapPlan({ scanDir, outFile });
121
+ const plan = await readJson(outFile);
122
+ assert.equal(result.summary.outFile, outFile);
123
+ assert.ok(plan.pages.some((page) => page.path === 'Data-Model-and-Migrations.md'));
124
+ }
125
+ finally {
126
+ await rm(dir, { recursive: true, force: true });
127
+ }
128
+ });
129
+ test('createBootstrapPlan builds affected_page_graph mapping source files to wiki pages', async () => {
130
+ const dir = await mkdtemp(path.join(os.tmpdir(), 'repo-wiki-plan-affected-'));
131
+ const scanDir = path.join(dir, 'scan');
132
+ const outFile = path.join(dir, 'bootstrap-plan.json');
133
+ try {
134
+ await mkdir(scanDir, { recursive: true });
135
+ // Fixture:
136
+ // apps/api/server.ts → Service api module (no imports; imported by client + test + routes)
137
+ // apps/api/routes.ts → Service api module (has route surfaces; imports server.ts - same module)
138
+ // apps/api/config.ts → Service api module (has environment variables + auth reason)
139
+ // apps/web/client.ts → Service web module (imports apps/api/server.ts cross-module)
140
+ // test/server.test.ts → test file (imports apps/api/server.ts; covers apps/api/server.ts)
141
+ // docs/guide.md → doc card (Documentation-Debt-Report.md)
142
+ // prisma/schema.prisma→ data file (has model surfaces → Data-Model-and-Migrations.md)
143
+ await writeFile(path.join(scanDir, 'manifest.json'), JSON.stringify({
144
+ mode: 'bootstrap',
145
+ repo_path: dir,
146
+ remote: 'origin',
147
+ commit: 'abc123',
148
+ totals: {
149
+ runtime_hints: { 'http-route': 2, 'orm-model': 1 },
150
+ categories: { source: 4, test: 1, data: 1 }
151
+ },
152
+ files: [
153
+ {
154
+ path: 'apps/api/server.ts',
155
+ category: 'source',
156
+ language: 'TypeScript',
157
+ imports: [],
158
+ route_surfaces: [],
159
+ migration_surfaces: [],
160
+ model_surfaces: [],
161
+ environment_variables: [],
162
+ runtime_hints: [],
163
+ reasons: ['source'],
164
+ bytes: 100
165
+ },
166
+ {
167
+ path: 'apps/api/routes.ts',
168
+ category: 'source',
169
+ language: 'TypeScript',
170
+ imports: [],
171
+ route_surfaces: [{ kind: 'http-route', framework: 'express', target: 'router', methods: ['GET'], path: '/health', handler: null }],
172
+ migration_surfaces: [],
173
+ model_surfaces: [],
174
+ environment_variables: [],
175
+ runtime_hints: ['http-route'],
176
+ reasons: ['api-surface'],
177
+ bytes: 100
178
+ },
179
+ {
180
+ path: 'apps/api/config.ts',
181
+ category: 'source',
182
+ language: 'TypeScript',
183
+ imports: [],
184
+ route_surfaces: [],
185
+ migration_surfaces: [],
186
+ model_surfaces: [],
187
+ environment_variables: ['APP_SECRET', 'DB_URL'],
188
+ runtime_hints: ['environment-variable'],
189
+ reasons: ['auth', 'configuration'],
190
+ bytes: 100
191
+ },
192
+ {
193
+ path: 'apps/web/client.ts',
194
+ category: 'source',
195
+ language: 'TypeScript',
196
+ imports: ['../api/server'],
197
+ route_surfaces: [],
198
+ migration_surfaces: [],
199
+ model_surfaces: [],
200
+ environment_variables: [],
201
+ runtime_hints: [],
202
+ reasons: ['source'],
203
+ bytes: 100
204
+ },
205
+ {
206
+ path: 'test/server.test.ts',
207
+ category: 'test',
208
+ language: 'TypeScript',
209
+ imports: ['../apps/api/server'],
210
+ route_surfaces: [],
211
+ migration_surfaces: [],
212
+ model_surfaces: [],
213
+ environment_variables: [],
214
+ runtime_hints: [],
215
+ reasons: ['test'],
216
+ bytes: 100
217
+ },
218
+ {
219
+ path: 'docs/guide.md',
220
+ category: 'docs',
221
+ language: 'Markdown',
222
+ imports: [],
223
+ route_surfaces: [],
224
+ migration_surfaces: [],
225
+ model_surfaces: [],
226
+ environment_variables: [],
227
+ runtime_hints: [],
228
+ reasons: ['docs'],
229
+ bytes: 100
230
+ },
231
+ {
232
+ path: 'prisma/schema.prisma',
233
+ category: 'data',
234
+ language: 'Text',
235
+ imports: [],
236
+ route_surfaces: [],
237
+ migration_surfaces: [],
238
+ model_surfaces: [{ name: 'User', kind: 'model', framework: 'prisma' }],
239
+ environment_variables: [],
240
+ runtime_hints: ['orm-model'],
241
+ reasons: ['data-model', 'orm-model'],
242
+ bytes: 100
243
+ }
244
+ ],
245
+ analysis: {
246
+ dependency_graph: {
247
+ edges: [
248
+ // Cross-module import: web/client.ts (Service web) imports api/server.ts (Service api)
249
+ { from: 'apps/web/client.ts', to: 'apps/api/server.ts', specifier: '../api/server' },
250
+ // Test file imports: test file imports api/server.ts
251
+ { from: 'test/server.test.ts', to: 'apps/api/server.ts', specifier: '../apps/api/server' },
252
+ // Same-module import: api/routes.ts (Service api) imports api/server.ts (same module)
253
+ { from: 'apps/api/routes.ts', to: 'apps/api/server.ts', specifier: './server' }
254
+ ]
255
+ },
256
+ test_to_source: {
257
+ mappings: [
258
+ {
259
+ test: 'test/server.test.ts',
260
+ sources: ['apps/api/server.ts'],
261
+ heuristics: ['imports']
262
+ }
263
+ ]
264
+ }
265
+ },
266
+ documentation: {
267
+ files: [
268
+ {
269
+ kind: 'documentation_card',
270
+ path: 'docs/guide.md',
271
+ authority: 'secondary',
272
+ status: 'unvalidated',
273
+ stale: false,
274
+ claims: [],
275
+ validation: { contradictions: [], validated: [], commands: [], env_vars: [] }
276
+ }
277
+ ]
278
+ }
279
+ }, null, 2), 'utf8');
280
+ await createBootstrapPlan({ scanDir, outFile });
281
+ const plan = await readJson(outFile);
282
+ assert.ok(plan.affected_page_graph, 'plan should include affected_page_graph');
283
+ assert.ok(Array.isArray(plan.affected_page_graph.source_to_pages), 'source_to_pages should be an array');
284
+ assert.ok(typeof plan.affected_page_graph.summary.mapped_sources === 'number');
285
+ assert.ok(typeof plan.affected_page_graph.summary.total_page_references === 'number');
286
+ // Helper: look up a source entry and build a page→reasons map for easy assertions
287
+ const bySource = new Map(plan.affected_page_graph.source_to_pages.map((e) => [e.source, e]));
288
+ function pageReasons(entry, pageName) {
289
+ const found = entry?.pages.find((p) => p.page === pageName);
290
+ return found ? found.reasons : [];
291
+ }
292
+ function pageNames(entry) {
293
+ return (entry?.pages || []).map((p) => p.page);
294
+ }
295
+ // Direct module change: apps/api/server.ts belongs to Service-api module
296
+ const apiServer = bySource.get('apps/api/server.ts');
297
+ assert.ok(apiServer, 'apps/api/server.ts should have affected pages');
298
+ assert.ok(pageReasons(apiServer, 'Service-api.md').includes('direct_module'), 'direct_module: server.ts is in Service api');
299
+ // Import-transitive: client.ts (in Service web) imports server.ts → Service-web.md affected
300
+ assert.ok(pageReasons(apiServer, 'Service-web.md').includes('import_transitive'), 'import_transitive: importing module page affected when importer is in a different module');
301
+ // Same-module import (routes.ts → server.ts, both in Service api) must NOT produce import_transitive
302
+ // for Service-api.md; the page appears only once with only direct_module
303
+ assert.equal(pageNames(apiServer).filter((pg) => pg === 'Service-api.md').length, 1, 'Service-api.md must appear exactly once even when imported within the same module');
304
+ assert.ok(!pageReasons(apiServer, 'Service-api.md').includes('import_transitive'), 'import_transitive must not appear on Service-api.md when the importer (routes.ts) is in the same module');
305
+ // Dependency map: server.ts is imported by others
306
+ assert.ok(pageReasons(apiServer, 'Dependency-Map.md').includes('dependency_change'), 'dependency_change: server.ts is imported so it participates in the dep graph');
307
+ // Web module: client.ts has imports → direct + dependency_change
308
+ const webClient = bySource.get('apps/web/client.ts');
309
+ assert.ok(webClient, 'apps/web/client.ts should have affected pages');
310
+ assert.ok(pageReasons(webClient, 'Service-web.md').includes('direct_module'), 'direct_module: client.ts is in Service web');
311
+ assert.ok(pageReasons(webClient, 'Dependency-Map.md').includes('dependency_change'), 'dependency_change: client.ts has imports');
312
+ assert.ok(!pageNames(webClient).includes('Service-api.md'), 'client.ts should not directly affect api module page');
313
+ // Cross-cutting routes: routes.ts has route surfaces → API-HTTP-Routes.md
314
+ const apiRoutes = bySource.get('apps/api/routes.ts');
315
+ assert.ok(apiRoutes, 'apps/api/routes.ts should have affected pages');
316
+ assert.ok(pageReasons(apiRoutes, 'API-HTTP-Routes.md').includes('cross_cutting_routes'), 'cross_cutting_routes: routes file affects HTTP routes page');
317
+ // Cross-cutting config + security: config.ts has env vars and auth reason
318
+ const apiConfig = bySource.get('apps/api/config.ts');
319
+ assert.ok(apiConfig, 'apps/api/config.ts should have affected pages');
320
+ assert.ok(pageReasons(apiConfig, 'Configuration-and-Environment.md').includes('cross_cutting_config'), 'cross_cutting_config: env vars present');
321
+ assert.ok(pageReasons(apiConfig, 'Security-and-Secrets.md').includes('cross_cutting_security'), 'cross_cutting_security: auth reason present');
322
+ // Test file change → Testing-Strategy.md AND covered source module pages
323
+ const testFile = bySource.get('test/server.test.ts');
324
+ assert.ok(testFile, 'test file should have affected pages');
325
+ assert.ok(pageReasons(testFile, 'Testing-Strategy.md').includes('test_coverage'), 'test_coverage: test file affects Testing-Strategy.md');
326
+ // test_to_source maps server.test.ts → apps/api/server.ts (in Service api)
327
+ // so changing the test should also flag the covered module page
328
+ assert.ok(pageReasons(testFile, 'Service-api.md').includes('test_covered_module'), 'test_covered_module: test file affects the module page of the source files it covers');
329
+ assert.ok(!pageReasons(testFile, 'Service-api.md').includes('direct_module'), 'test file should not claim direct_module on the covered source module page');
330
+ // Documentation file change → Documentation-Debt-Report.md
331
+ const docFile = bySource.get('docs/guide.md');
332
+ assert.ok(docFile, 'documentation file should have affected pages');
333
+ assert.ok(pageReasons(docFile, 'Documentation-Debt-Report.md').includes('docs_debt'), 'docs_debt: doc file affects Documentation-Debt-Report.md');
334
+ // Data model file change → Data-Model-and-Migrations.md
335
+ const schemaFile = bySource.get('prisma/schema.prisma');
336
+ assert.ok(schemaFile, 'data model file should have affected pages');
337
+ assert.ok(pageReasons(schemaFile, 'Data-Model-and-Migrations.md').includes('cross_cutting_data_model'), 'cross_cutting_data_model: model surfaces present');
338
+ // source_to_pages is deterministically sorted by source path
339
+ const sources = plan.affected_page_graph.source_to_pages.map((e) => e.source);
340
+ assert.deepEqual(sources, [...sources].sort(), 'source_to_pages must be sorted by source path');
341
+ // Each page entry's reasons array must itself be sorted
342
+ for (const entry of plan.affected_page_graph.source_to_pages) {
343
+ for (const pageEntry of entry.pages) {
344
+ assert.deepEqual(pageEntry.reasons, [...pageEntry.reasons].sort(), `reasons for ${entry.source} → ${pageEntry.page} must be sorted`);
345
+ }
346
+ }
347
+ // pages within each source entry must be sorted by page name
348
+ for (const entry of plan.affected_page_graph.source_to_pages) {
349
+ const names = entry.pages.map((p) => p.page);
350
+ assert.deepEqual(names, [...names].sort(), `pages for ${entry.source} must be sorted by page name`);
351
+ }
352
+ // Architecture.md architecture-relevant signals:
353
+ // Module membership: files that are in modules should mark Architecture.md
354
+ const apiServerArch = pageReasons(apiServer, 'Architecture.md');
355
+ assert.ok(apiServerArch.includes('module_membership'), 'module_membership: apps/api/server.ts is in Service api → Architecture.md affected');
356
+ // Cross-module dependency: server.ts is imported by web/client.ts (different module)
357
+ assert.ok(apiServerArch.includes('cross_module_dependency'), 'cross_module_dependency: server.ts imported by client.ts (different module) → Architecture.md affected');
358
+ // Cross-cutting routes: routes.ts has route surfaces → Architecture.md affected
359
+ const apiRoutesArch = pageReasons(apiRoutes, 'Architecture.md');
360
+ assert.ok(apiRoutesArch.includes('cross_cutting_routes'), 'cross_cutting_routes: routes file has route surfaces → Architecture.md affected');
361
+ // Cross-cutting config: config.ts has env vars → Architecture.md affected
362
+ assert.ok(pageReasons(apiConfig, 'Architecture.md').includes('cross_cutting_config'), 'cross_cutting_config: config.ts has env vars → Architecture.md affected');
363
+ // Cross-cutting security: config.ts has auth reason → Architecture.md affected
364
+ assert.ok(pageReasons(apiConfig, 'Architecture.md').includes('cross_cutting_security'), 'cross_cutting_security: config.ts has auth reason → Architecture.md affected');
365
+ // Cross-cutting data model: schema.prisma has model surfaces → Architecture.md affected
366
+ assert.ok(pageReasons(schemaFile, 'Architecture.md').includes('cross_cutting_data_model'), 'cross_cutting_data_model: schema.prisma has model surfaces → Architecture.md affected');
367
+ // Architecture.md should appear for test files that have module membership
368
+ // (test/server.test.ts resolves to 'Repository Root' module in this fixture)
369
+ const testFileArchEntry = pageReasons(testFile, 'Architecture.md');
370
+ assert.ok(testFileArchEntry.includes('module_membership'), 'test file in Repository Root module should have module_membership for Architecture.md');
371
+ }
372
+ finally {
373
+ await rm(dir, { recursive: true, force: true });
374
+ }
375
+ });
376
+ test('createBootstrapPlan incremental mode selects affected pages from graph and always includes global pages', async () => {
377
+ const dir = await mkdtemp(path.join(os.tmpdir(), 'repo-wiki-plan-incremental-graph-'));
378
+ const llmwikiDir = path.join(dir, '.llmwiki');
379
+ const scanDir = path.join(llmwikiDir, 'run');
380
+ const outFile = path.join(llmwikiDir, 'incremental-plan.json');
381
+ try {
382
+ await mkdir(scanDir, { recursive: true });
383
+ await writeFile(path.join(scanDir, 'manifest.json'), JSON.stringify({
384
+ mode: 'incremental',
385
+ repo_path: dir,
386
+ remote: 'origin',
387
+ commit: 'abc123',
388
+ changed_paths: ['docs/guide.md', 'apps/api/server.ts'],
389
+ totals: {
390
+ runtime_hints: {},
391
+ categories: { source: 1, docs: 1 }
392
+ },
393
+ files: [
394
+ { path: 'apps/api/server.ts', category: 'source', language: 'TypeScript', runtime_hints: [], reasons: ['source'], bytes: 100 },
395
+ { path: 'docs/guide.md', category: 'docs', language: 'Markdown', runtime_hints: [], reasons: ['docs'], bytes: 100 }
396
+ ],
397
+ documentation: {
398
+ files: [
399
+ {
400
+ kind: 'documentation_card',
401
+ path: 'docs/guide.md',
402
+ authority: 'secondary',
403
+ status: 'unvalidated',
404
+ stale: false,
405
+ claims: [],
406
+ validation: { contradictions: [], validated: [], commands: [], env_vars: [] }
407
+ }
408
+ ]
409
+ }
410
+ }, null, 2), 'utf8');
411
+ await writeFile(path.join(llmwikiDir, 'graph.json'), JSON.stringify({
412
+ schema_version: 1,
413
+ nodes: [
414
+ { id: 'page:Service-api.md', kind: 'page', path: 'Service-api.md', page_state: 'generated' },
415
+ { id: 'page:Documentation-Debt-Report.md', kind: 'page', path: 'Documentation-Debt-Report.md', page_state: 'mixed' },
416
+ { id: 'page:Security-and-Secrets.md', kind: 'page', path: 'Security-and-Secrets.md', page_state: 'human-owned' },
417
+ { id: 'source:apps/api/server.ts', kind: 'source', path: 'apps/api/server.ts' },
418
+ { id: 'source:docs/guide.md', kind: 'documentation', path: 'docs/guide.md' }
419
+ ],
420
+ edges: [
421
+ { type: 'affects', from: 'source:apps/api/server.ts', to: 'page:Security-and-Secrets.md' },
422
+ { type: 'affects', from: 'source:apps/api/server.ts', to: 'page:Service-api.md' },
423
+ { type: 'affects', from: 'source:docs/guide.md', to: 'page:Documentation-Debt-Report.md' }
424
+ ]
425
+ }, null, 2), 'utf8');
426
+ await createBootstrapPlan({ scanDir, outFile });
427
+ const plan = await readJson(outFile);
428
+ assert.ok(plan.incremental_selection, 'incremental plan should include incremental_selection');
429
+ assert.equal(plan.incremental_selection.summary.graph_available, true);
430
+ assert.equal(plan.incremental_selection.summary.graph_used, true);
431
+ assert.deepEqual(plan.incremental_selection.changed_paths, ['apps/api/server.ts', 'docs/guide.md']);
432
+ const selectedByPage = new Map(plan.incremental_selection.selected_pages.map((entry) => [entry.page, entry]));
433
+ assert.ok(selectedByPage.has('Service-api.md'), 'source change should affect module page');
434
+ assert.deepEqual(selectedByPage.get('Service-api.md').changed_paths, ['apps/api/server.ts']);
435
+ assert.ok(selectedByPage.get('Service-api.md').reasons.includes('graph_affects'));
436
+ assert.ok(selectedByPage.has('Documentation-Debt-Report.md'), 'docs change should affect docs debt page');
437
+ assert.deepEqual(selectedByPage.get('Documentation-Debt-Report.md').changed_paths, ['docs/guide.md']);
438
+ assert.ok(selectedByPage.get('Documentation-Debt-Report.md').reasons.includes('graph_affects'));
439
+ assert.ok(!selectedByPage.has('Security-and-Secrets.md'), 'human-owned page should be excluded from graph-selected incremental regeneration set');
440
+ for (const page of ['Index.md', '_Sidebar.md', 'Log.md', 'Agent-Context-Pack.md']) {
441
+ const selected = selectedByPage.get(page);
442
+ assert.ok(selected, `${page} must always be selected in incremental mode`);
443
+ assert.ok(selected.reasons.includes('always_affected_global'));
444
+ }
445
+ const selectedPageNames = plan.incremental_selection.selected_pages.map((entry) => entry.page);
446
+ assert.deepEqual(selectedPageNames, [...selectedPageNames].sort((a, b) => a.localeCompare(b)), 'selected_pages must be sorted by page path');
447
+ }
448
+ finally {
449
+ await rm(dir, { recursive: true, force: true });
450
+ }
451
+ });
452
+ test('createBootstrapPlan incremental mode supports documentation graph node IDs', async () => {
453
+ const dir = await mkdtemp(path.join(os.tmpdir(), 'repo-wiki-plan-incremental-doc-node-'));
454
+ const llmwikiDir = path.join(dir, '.llmwiki');
455
+ const scanDir = path.join(llmwikiDir, 'run');
456
+ const outFile = path.join(llmwikiDir, 'incremental-plan.json');
457
+ try {
458
+ await mkdir(scanDir, { recursive: true });
459
+ await writeFile(path.join(scanDir, 'manifest.json'), JSON.stringify({
460
+ mode: 'incremental',
461
+ repo_path: dir,
462
+ remote: 'origin',
463
+ commit: 'abc123',
464
+ changed_paths: ['docs/guide.md'],
465
+ totals: {
466
+ runtime_hints: {},
467
+ categories: { docs: 1 }
468
+ },
469
+ files: [
470
+ { path: 'docs/guide.md', category: 'docs', language: 'Markdown', runtime_hints: [], reasons: ['docs'], bytes: 100 }
471
+ ],
472
+ documentation: {
473
+ files: [
474
+ {
475
+ kind: 'documentation_card',
476
+ path: 'docs/guide.md',
477
+ authority: 'secondary',
478
+ status: 'unvalidated',
479
+ stale: false,
480
+ claims: [],
481
+ validation: { contradictions: [], validated: [], commands: [], env_vars: [] }
482
+ }
483
+ ]
484
+ }
485
+ }, null, 2), 'utf8');
486
+ await writeFile(path.join(llmwikiDir, 'graph.json'), JSON.stringify({
487
+ schema_version: 1,
488
+ nodes: [
489
+ { id: 'page:Documentation-Debt-Report.md', kind: 'page', path: 'Documentation-Debt-Report.md', page_state: 'generated' },
490
+ { id: 'documentation:docs/guide.md', kind: 'documentation', path: 'docs/guide.md' }
491
+ ],
492
+ edges: [
493
+ { type: 'affects', from: 'documentation:docs/guide.md', to: 'page:Documentation-Debt-Report.md' }
494
+ ]
495
+ }, null, 2), 'utf8');
496
+ await createBootstrapPlan({ scanDir, outFile });
497
+ const plan = await readJson(outFile);
498
+ const selectedByPage = new Map(plan.incremental_selection.selected_pages.map((entry) => [entry.page, entry]));
499
+ assert.ok(selectedByPage.has('Documentation-Debt-Report.md'), 'documentation node ids should resolve docs changes to affected pages');
500
+ assert.deepEqual(selectedByPage.get('Documentation-Debt-Report.md').changed_paths, ['docs/guide.md']);
501
+ assert.ok(selectedByPage.get('Documentation-Debt-Report.md').reasons.includes('graph_affects'));
502
+ }
503
+ finally {
504
+ await rm(dir, { recursive: true, force: true });
505
+ }
506
+ });
507
+ test('createBootstrapPlan incremental mode falls back when graph is present but changed-path attribution is absent', async () => {
508
+ const dir = await mkdtemp(path.join(os.tmpdir(), 'repo-wiki-plan-incremental-missing-changed-paths-'));
509
+ const llmwikiDir = path.join(dir, '.llmwiki');
510
+ const scanDir = path.join(llmwikiDir, 'run');
511
+ const outFile = path.join(llmwikiDir, 'incremental-plan.json');
512
+ try {
513
+ await mkdir(scanDir, { recursive: true });
514
+ await writeFile(path.join(scanDir, 'manifest.json'), JSON.stringify({
515
+ mode: 'incremental',
516
+ repo_path: dir,
517
+ remote: 'origin',
518
+ commit: 'abc123',
519
+ totals: {
520
+ runtime_hints: {},
521
+ categories: { source: 1 }
522
+ },
523
+ files: [
524
+ { path: 'apps/api/server.ts', category: 'source', language: 'TypeScript', runtime_hints: [], reasons: ['source'], bytes: 100 }
525
+ ],
526
+ documentation: { files: [] }
527
+ }, null, 2), 'utf8');
528
+ await writeFile(path.join(llmwikiDir, 'graph.json'), JSON.stringify({
529
+ schema_version: 1,
530
+ nodes: [
531
+ { id: 'page:Service-api.md', kind: 'page', path: 'Service-api.md', page_state: 'generated' },
532
+ { id: 'source:apps/api/server.ts', kind: 'source', path: 'apps/api/server.ts' }
533
+ ],
534
+ edges: [
535
+ { type: 'affects', from: 'source:apps/api/server.ts', to: 'page:Service-api.md' }
536
+ ]
537
+ }, null, 2), 'utf8');
538
+ await createBootstrapPlan({ scanDir, outFile });
539
+ const plan = await readJson(outFile);
540
+ assert.ok(plan.incremental_selection, 'incremental plan should include incremental_selection');
541
+ assert.equal(plan.incremental_selection.summary.graph_available, true);
542
+ assert.equal(plan.incremental_selection.summary.graph_used, false);
543
+ assert.equal(plan.incremental_selection.summary.changed_paths_available, false);
544
+ assert.equal(plan.incremental_selection.summary.fallback_reason, 'fallback_missing_changed_paths');
545
+ assert.deepEqual(plan.incremental_selection.changed_paths, []);
546
+ const selectedByPage = new Map(plan.incremental_selection.selected_pages.map((entry) => [entry.page, entry]));
547
+ for (const plannedPage of plan.pages.map((page) => page.path)) {
548
+ assert.ok(selectedByPage.has(plannedPage), `fallback must include planned page ${plannedPage}`);
549
+ assert.ok(selectedByPage.get(plannedPage).reasons.includes('fallback_missing_changed_paths'));
550
+ }
551
+ for (const page of ['Index.md', '_Sidebar.md', 'Log.md', 'Agent-Context-Pack.md']) {
552
+ const selected = selectedByPage.get(page);
553
+ assert.ok(selected, `${page} must always be selected when changed-path attribution is absent`);
554
+ assert.ok(selected.reasons.includes('always_affected_global'));
555
+ }
556
+ }
557
+ finally {
558
+ await rm(dir, { recursive: true, force: true });
559
+ }
560
+ });
561
+ test('createBootstrapPlan incremental mode falls back when changed paths are absent from the graph artifact', async () => {
562
+ const dir = await mkdtemp(path.join(os.tmpdir(), 'repo-wiki-plan-incremental-unmatched-changed-paths-'));
563
+ const llmwikiDir = path.join(dir, '.llmwiki');
564
+ const scanDir = path.join(llmwikiDir, 'run');
565
+ const outFile = path.join(llmwikiDir, 'incremental-plan.json');
566
+ try {
567
+ await mkdir(scanDir, { recursive: true });
568
+ await writeFile(path.join(scanDir, 'manifest.json'), JSON.stringify({
569
+ mode: 'incremental',
570
+ repo_path: dir,
571
+ remote: 'origin',
572
+ commit: 'abc123',
573
+ changed_paths: ['apps/api/new.ts'],
574
+ totals: {
575
+ runtime_hints: {},
576
+ categories: { source: 1 }
577
+ },
578
+ files: [
579
+ { path: 'apps/api/new.ts', category: 'source', language: 'TypeScript', runtime_hints: [], reasons: ['source'], bytes: 100 }
580
+ ],
581
+ documentation: { files: [] }
582
+ }, null, 2), 'utf8');
583
+ await writeFile(path.join(llmwikiDir, 'graph.json'), JSON.stringify({
584
+ schema_version: 1,
585
+ nodes: [
586
+ { id: 'page:Service-api.md', kind: 'page', path: 'Service-api.md', page_state: 'generated' },
587
+ { id: 'source:apps/api/old.ts', kind: 'source', path: 'apps/api/old.ts' }
588
+ ],
589
+ edges: [
590
+ { type: 'affects', from: 'source:apps/api/old.ts', to: 'page:Service-api.md' }
591
+ ]
592
+ }, null, 2), 'utf8');
593
+ await createBootstrapPlan({ scanDir, outFile });
594
+ const plan = await readJson(outFile);
595
+ assert.ok(plan.incremental_selection, 'incremental plan should include incremental_selection');
596
+ assert.equal(plan.incremental_selection.summary.graph_available, true);
597
+ assert.equal(plan.incremental_selection.summary.graph_used, false);
598
+ assert.equal(plan.incremental_selection.summary.changed_paths_available, true);
599
+ assert.equal(plan.incremental_selection.summary.fallback_reason, 'fallback_unmatched_changed_paths');
600
+ assert.deepEqual(plan.incremental_selection.changed_paths, ['apps/api/new.ts']);
601
+ const selectedByPage = new Map(plan.incremental_selection.selected_pages.map((entry) => [entry.page, entry]));
602
+ for (const plannedPage of plan.pages.map((page) => page.path)) {
603
+ assert.ok(selectedByPage.has(plannedPage), `fallback must include planned page ${plannedPage}`);
604
+ assert.ok(selectedByPage.get(plannedPage).reasons.includes('fallback_unmatched_changed_paths'));
605
+ }
606
+ for (const page of ['Index.md', '_Sidebar.md', 'Log.md', 'Agent-Context-Pack.md']) {
607
+ const selected = selectedByPage.get(page);
608
+ assert.ok(selected, `${page} must always be selected when changed paths are absent from the graph artifact`);
609
+ assert.ok(selected.reasons.includes('always_affected_global'));
610
+ }
611
+ }
612
+ finally {
613
+ await rm(dir, { recursive: true, force: true });
614
+ }
615
+ });
616
+ test('createBootstrapPlan incremental mode rejects malformed graph artifacts', async () => {
617
+ const dir = await mkdtemp(path.join(os.tmpdir(), 'repo-wiki-plan-incremental-bad-graph-'));
618
+ const llmwikiDir = path.join(dir, '.llmwiki');
619
+ const scanDir = path.join(llmwikiDir, 'run');
620
+ const outFile = path.join(llmwikiDir, 'incremental-plan.json');
621
+ try {
622
+ await mkdir(scanDir, { recursive: true });
623
+ await writeFile(path.join(scanDir, 'manifest.json'), JSON.stringify({
624
+ mode: 'incremental',
625
+ repo_path: dir,
626
+ remote: 'origin',
627
+ commit: 'abc123',
628
+ changed_paths: ['apps/api/server.ts'],
629
+ totals: {
630
+ runtime_hints: {},
631
+ categories: { source: 1 }
632
+ },
633
+ files: [
634
+ { path: 'apps/api/server.ts', category: 'source', language: 'TypeScript', runtime_hints: [], reasons: ['source'], bytes: 100 }
635
+ ],
636
+ documentation: { files: [] }
637
+ }, null, 2), 'utf8');
638
+ await writeFile(path.join(llmwikiDir, 'graph.json'), '{ not valid json\n', 'utf8');
639
+ await assert.rejects(createBootstrapPlan({ scanDir, outFile }), SyntaxError);
640
+ }
641
+ finally {
642
+ await rm(dir, { recursive: true, force: true });
643
+ }
644
+ });
645
+ test('createBootstrapPlan incremental mode falls back deterministically when graph artifact is missing', async () => {
646
+ const dir = await mkdtemp(path.join(os.tmpdir(), 'repo-wiki-plan-incremental-fallback-'));
647
+ const llmwikiDir = path.join(dir, '.llmwiki');
648
+ const scanDir = path.join(llmwikiDir, 'run');
649
+ const outFile = path.join(llmwikiDir, 'incremental-plan.json');
650
+ try {
651
+ await mkdir(scanDir, { recursive: true });
652
+ await writeFile(path.join(scanDir, 'manifest.json'), JSON.stringify({
653
+ mode: 'incremental',
654
+ repo_path: dir,
655
+ remote: 'origin',
656
+ commit: 'abc123',
657
+ changed_paths: ['apps/api/server.ts'],
658
+ totals: {
659
+ runtime_hints: {},
660
+ categories: { source: 1 }
661
+ },
662
+ files: [
663
+ { path: 'apps/api/server.ts', category: 'source', language: 'TypeScript', runtime_hints: [], reasons: ['source'], bytes: 100 }
664
+ ],
665
+ documentation: { files: [] }
666
+ }, null, 2), 'utf8');
667
+ await createBootstrapPlan({ scanDir, outFile });
668
+ const plan = await readJson(outFile);
669
+ assert.ok(plan.incremental_selection, 'incremental plan should include incremental_selection');
670
+ assert.equal(plan.incremental_selection.summary.graph_available, false);
671
+ assert.equal(plan.incremental_selection.summary.graph_used, false);
672
+ assert.equal(plan.incremental_selection.summary.fallback_reason, 'fallback_missing_graph');
673
+ const selectedByPage = new Map(plan.incremental_selection.selected_pages.map((entry) => [entry.page, entry]));
674
+ for (const plannedPage of plan.pages.map((page) => page.path)) {
675
+ assert.ok(selectedByPage.has(plannedPage), `fallback must include planned page ${plannedPage}`);
676
+ assert.ok(selectedByPage.get(plannedPage).reasons.includes('fallback_missing_graph'));
677
+ }
678
+ for (const page of ['Index.md', '_Sidebar.md', 'Log.md', 'Agent-Context-Pack.md']) {
679
+ const selected = selectedByPage.get(page);
680
+ assert.ok(selected, `${page} must always be selected in fallback mode`);
681
+ assert.ok(selected.reasons.includes('always_affected_global'));
682
+ }
683
+ }
684
+ finally {
685
+ await rm(dir, { recursive: true, force: true });
686
+ }
687
+ });
688
+ //# sourceMappingURL=init-planner.test.js.map