@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.
- package/.llmwiki/schema.md +107 -0
- package/AGENTS.md +42 -0
- package/CHANGELOG.md +91 -0
- package/LICENSE +21 -0
- package/README.md +254 -0
- package/dist/bin/repo-wiki.d.ts +2 -0
- package/dist/bin/repo-wiki.js +7 -0
- package/dist/bin/repo-wiki.js.map +1 -0
- package/dist/src/cli.d.ts +1 -0
- package/dist/src/cli.js +404 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/compiler.d.ts +55 -0
- package/dist/src/compiler.js +2046 -0
- package/dist/src/compiler.js.map +1 -0
- package/dist/src/config.d.ts +63 -0
- package/dist/src/config.js +86 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/context-assembler.d.ts +68 -0
- package/dist/src/context-assembler.js +378 -0
- package/dist/src/context-assembler.js.map +1 -0
- package/dist/src/data-model-signals.d.ts +1 -0
- package/dist/src/data-model-signals.js +13 -0
- package/dist/src/data-model-signals.js.map +1 -0
- package/dist/src/docs-ingestor.d.ts +138 -0
- package/dist/src/docs-ingestor.js +844 -0
- package/dist/src/docs-ingestor.js.map +1 -0
- package/dist/src/docs-linter.d.ts +14 -0
- package/dist/src/docs-linter.js +164 -0
- package/dist/src/docs-linter.js.map +1 -0
- package/dist/src/docs-validation.d.ts +36 -0
- package/dist/src/docs-validation.js +297 -0
- package/dist/src/docs-validation.js.map +1 -0
- package/dist/src/extractors.d.ts +50 -0
- package/dist/src/extractors.js +2275 -0
- package/dist/src/extractors.js.map +1 -0
- package/dist/src/frontmatter.d.ts +46 -0
- package/dist/src/frontmatter.js +377 -0
- package/dist/src/frontmatter.js.map +1 -0
- package/dist/src/index.d.ts +26 -0
- package/dist/src/index.js +18 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/init.d.ts +12 -0
- package/dist/src/init.js +121 -0
- package/dist/src/init.js.map +1 -0
- package/dist/src/language.d.ts +2 -0
- package/dist/src/language.js +62 -0
- package/dist/src/language.js.map +1 -0
- package/dist/src/linter.d.ts +33 -0
- package/dist/src/linter.js +398 -0
- package/dist/src/linter.js.map +1 -0
- package/dist/src/llm-provider.d.ts +267 -0
- package/dist/src/llm-provider.js +474 -0
- package/dist/src/llm-provider.js.map +1 -0
- package/dist/src/page-ownership.d.ts +38 -0
- package/dist/src/page-ownership.js +96 -0
- package/dist/src/page-ownership.js.map +1 -0
- package/dist/src/planner.d.ts +55 -0
- package/dist/src/planner.js +422 -0
- package/dist/src/planner.js.map +1 -0
- package/dist/src/prompts.d.ts +103 -0
- package/dist/src/prompts.js +344 -0
- package/dist/src/prompts.js.map +1 -0
- package/dist/src/publisher.d.ts +68 -0
- package/dist/src/publisher.js +662 -0
- package/dist/src/publisher.js.map +1 -0
- package/dist/src/repository-analysis.d.ts +88 -0
- package/dist/src/repository-analysis.js +485 -0
- package/dist/src/repository-analysis.js.map +1 -0
- package/dist/src/scanner.d.ts +122 -0
- package/dist/src/scanner.js +309 -0
- package/dist/src/scanner.js.map +1 -0
- package/dist/src/search.d.ts +71 -0
- package/dist/src/search.js +410 -0
- package/dist/src/search.js.map +1 -0
- package/dist/src/secret-patterns.d.ts +3 -0
- package/dist/src/secret-patterns.js +14 -0
- package/dist/src/secret-patterns.js.map +1 -0
- package/dist/src/utils/args.d.ts +2 -0
- package/dist/src/utils/args.js +19 -0
- package/dist/src/utils/args.js.map +1 -0
- package/dist/src/utils/dotenv.d.ts +7 -0
- package/dist/src/utils/dotenv.js +73 -0
- package/dist/src/utils/dotenv.js.map +1 -0
- package/dist/src/utils/fs.d.ts +22 -0
- package/dist/src/utils/fs.js +83 -0
- package/dist/src/utils/fs.js.map +1 -0
- package/dist/src/utils/git.d.ts +13 -0
- package/dist/src/utils/git.js +39 -0
- package/dist/src/utils/git.js.map +1 -0
- package/dist/src/wiki-graph.d.ts +74 -0
- package/dist/src/wiki-graph.js +335 -0
- package/dist/src/wiki-graph.js.map +1 -0
- package/dist/src/wiki-patch.d.ts +152 -0
- package/dist/src/wiki-patch.js +489 -0
- package/dist/src/wiki-patch.js.map +1 -0
- package/dist/src/wiki-query.d.ts +63 -0
- package/dist/src/wiki-query.js +255 -0
- package/dist/src/wiki-query.js.map +1 -0
- package/dist/test/cli.test.d.ts +1 -0
- package/dist/test/cli.test.js +514 -0
- package/dist/test/cli.test.js.map +1 -0
- package/dist/test/compiler-eval.test.d.ts +1 -0
- package/dist/test/compiler-eval.test.js +234 -0
- package/dist/test/compiler-eval.test.js.map +1 -0
- package/dist/test/compiler.test.d.ts +1 -0
- package/dist/test/compiler.test.js +2537 -0
- package/dist/test/compiler.test.js.map +1 -0
- package/dist/test/context-assembler.test.d.ts +1 -0
- package/dist/test/context-assembler.test.js +379 -0
- package/dist/test/context-assembler.test.js.map +1 -0
- package/dist/test/docs-linter.test.d.ts +1 -0
- package/dist/test/docs-linter.test.js +900 -0
- package/dist/test/docs-linter.test.js.map +1 -0
- package/dist/test/dotenv.test.d.ts +1 -0
- package/dist/test/dotenv.test.js +77 -0
- package/dist/test/dotenv.test.js.map +1 -0
- package/dist/test/extractors-go.test.d.ts +1 -0
- package/dist/test/extractors-go.test.js +393 -0
- package/dist/test/extractors-go.test.js.map +1 -0
- package/dist/test/extractors-rust.test.d.ts +1 -0
- package/dist/test/extractors-rust.test.js +219 -0
- package/dist/test/extractors-rust.test.js.map +1 -0
- package/dist/test/extractors-utils.test.d.ts +1 -0
- package/dist/test/extractors-utils.test.js +786 -0
- package/dist/test/extractors-utils.test.js.map +1 -0
- package/dist/test/fixtures/compiler-e2e/basic-node-service/repo/infra/deploy.d.ts +1 -0
- package/dist/test/fixtures/compiler-e2e/basic-node-service/repo/infra/deploy.js +4 -0
- package/dist/test/fixtures/compiler-e2e/basic-node-service/repo/infra/deploy.js.map +1 -0
- package/dist/test/frontmatter.test.d.ts +1 -0
- package/dist/test/frontmatter.test.js +287 -0
- package/dist/test/frontmatter.test.js.map +1 -0
- package/dist/test/init-planner.test.d.ts +1 -0
- package/dist/test/init-planner.test.js +688 -0
- package/dist/test/init-planner.test.js.map +1 -0
- package/dist/test/linter.test.d.ts +1 -0
- package/dist/test/linter.test.js +426 -0
- package/dist/test/linter.test.js.map +1 -0
- package/dist/test/llm-provider.test.d.ts +1 -0
- package/dist/test/llm-provider.test.js +783 -0
- package/dist/test/llm-provider.test.js.map +1 -0
- package/dist/test/page-ownership.test.d.ts +1 -0
- package/dist/test/page-ownership.test.js +247 -0
- package/dist/test/page-ownership.test.js.map +1 -0
- package/dist/test/publisher.test.d.ts +1 -0
- package/dist/test/publisher.test.js +1297 -0
- package/dist/test/publisher.test.js.map +1 -0
- package/dist/test/repository-analysis.test.d.ts +1 -0
- package/dist/test/repository-analysis.test.js +182 -0
- package/dist/test/repository-analysis.test.js.map +1 -0
- package/dist/test/run-compiled-tests.d.ts +1 -0
- package/dist/test/run-compiled-tests.js +48 -0
- package/dist/test/run-compiled-tests.js.map +1 -0
- package/dist/test/scanner.test.d.ts +1 -0
- package/dist/test/scanner.test.js +551 -0
- package/dist/test/scanner.test.js.map +1 -0
- package/dist/test/search.test.d.ts +1 -0
- package/dist/test/search.test.js +92 -0
- package/dist/test/search.test.js.map +1 -0
- package/dist/test/update-changelog.test.d.ts +1 -0
- package/dist/test/update-changelog.test.js +125 -0
- package/dist/test/update-changelog.test.js.map +1 -0
- package/dist/test/wiki-graph.test.d.ts +1 -0
- package/dist/test/wiki-graph.test.js +164 -0
- package/dist/test/wiki-graph.test.js.map +1 -0
- package/dist/test/wiki-patch.test.d.ts +1 -0
- package/dist/test/wiki-patch.test.js +610 -0
- package/dist/test/wiki-patch.test.js.map +1 -0
- package/dist/test/wiki-query.test.d.ts +1 -0
- package/dist/test/wiki-query.test.js +163 -0
- package/dist/test/wiki-query.test.js.map +1 -0
- package/docs/PLAN.md +993 -0
- package/docs/WHY.md +61 -0
- package/docs/plans/agent-integration.md +85 -0
- package/docs/plans/ci-publishing.md +111 -0
- package/docs/plans/doc-validation.md +92 -0
- package/docs/plans/github-action.md +113 -0
- package/docs/plans/incremental-mode.md +98 -0
- package/docs/plans/karpathy-llm-wiki-alignment.md +84 -0
- package/docs/plans/llm-compiler.md +160 -0
- package/docs/plans/production-scanner.md +104 -0
- package/docs/plans/query-and-file-back.md +103 -0
- package/docs/plans/search-index.md +118 -0
- package/docs/plans/trust-hardening.md +74 -0
- package/docs/plans/wiki-graph.md +183 -0
- package/docs/plans/wiki-health.md +76 -0
- package/package.json +83 -0
- package/prompts/compiler.md +16 -0
- package/prompts/lint.md +18 -0
- package/prompts/page-templates.md +25 -0
- package/skills/repo-wiki-cli/SKILL.md +139 -0
|
@@ -0,0 +1,2537 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { promises as fs } from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { compileWiki, computeArchDecision } from '../src/compiler.js';
|
|
7
|
+
import { lintWiki } from '../src/linter.js';
|
|
8
|
+
import { extractHumanNotes } from '../src/page-ownership.js';
|
|
9
|
+
import { MockLLMProvider } from '../src/llm-provider.js';
|
|
10
|
+
function createPlan() {
|
|
11
|
+
return {
|
|
12
|
+
pages: [
|
|
13
|
+
{ path: 'Home.md', phase: 'foundation', purpose: 'Entry point.' },
|
|
14
|
+
{ path: '_Sidebar.md', phase: 'foundation', purpose: 'Sidebar.' },
|
|
15
|
+
{ path: 'Index.md', phase: 'foundation', purpose: 'Index.' },
|
|
16
|
+
{ path: 'Log.md', phase: 'foundation', purpose: 'Log.' },
|
|
17
|
+
{ path: 'Agent-Context-Pack.md', phase: 'foundation', purpose: 'Agent entry.' },
|
|
18
|
+
{ path: 'Repository-Overview.md', phase: 'foundation', purpose: 'Overview.' },
|
|
19
|
+
{ path: 'Architecture.md', phase: 'foundation', purpose: 'Architecture.' },
|
|
20
|
+
{ path: 'Build-Test-and-Run.md', phase: 'foundation', purpose: 'Commands.' },
|
|
21
|
+
{ path: 'Open-Questions.md', phase: 'foundation', purpose: 'Gaps.' },
|
|
22
|
+
{ path: 'Documentation-Debt-Report.md', phase: 'foundation', purpose: 'Docs debt.' },
|
|
23
|
+
{ path: 'Dependency-Map.md', phase: 'cross-cutting', purpose: 'Dependencies.' },
|
|
24
|
+
{ path: 'Testing-Strategy.md', phase: 'cross-cutting', purpose: 'Tests.' },
|
|
25
|
+
{ path: 'Configuration-and-Environment.md', phase: 'cross-cutting', purpose: 'Config.' },
|
|
26
|
+
{ path: 'Security-and-Secrets.md', phase: 'cross-cutting', purpose: 'Security.' },
|
|
27
|
+
{ path: 'Operational-Runbook.md', phase: 'cross-cutting', purpose: 'Operations.' },
|
|
28
|
+
{ path: 'API-HTTP-Routes.md', phase: 'cross-cutting', purpose: 'Routes.' }
|
|
29
|
+
],
|
|
30
|
+
modules: []
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
async function writeFixture({ manifest, plan }) {
|
|
34
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'repo-wiki-compiler-'));
|
|
35
|
+
const scanDir = path.join(dir, 'scan');
|
|
36
|
+
const wikiDir = path.join(dir, 'wiki');
|
|
37
|
+
const planFile = path.join(dir, 'plan.json');
|
|
38
|
+
await fs.mkdir(scanDir, { recursive: true });
|
|
39
|
+
await fs.writeFile(path.join(scanDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
40
|
+
await fs.writeFile(planFile, JSON.stringify(plan, null, 2));
|
|
41
|
+
return { dir, scanDir, wikiDir, planFile };
|
|
42
|
+
}
|
|
43
|
+
function assertNoWallClockFields(value) {
|
|
44
|
+
const forbidden = new Set(['generated_at', 'compiled_at', 'timestamp']);
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
for (const entry of value) {
|
|
47
|
+
assertNoWallClockFields(entry);
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!value || typeof value !== 'object') {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
55
|
+
assert.ok(!forbidden.has(key), `unexpected wall-clock field "${key}" in graph artifact`);
|
|
56
|
+
assertNoWallClockFields(nested);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
test('compileWiki renders richer scanner analysis into wiki pages', async () => {
|
|
60
|
+
const manifest = {
|
|
61
|
+
remote: 'origin',
|
|
62
|
+
commit: 'abc123456789',
|
|
63
|
+
mode: 'bootstrap',
|
|
64
|
+
totals: {
|
|
65
|
+
languages: { JavaScript: 3, JSON: 1 },
|
|
66
|
+
categories: { source: 2, test: 1, config: 1 },
|
|
67
|
+
runtime_hints: { 'http-route': 1 }
|
|
68
|
+
},
|
|
69
|
+
files: [
|
|
70
|
+
{
|
|
71
|
+
path: 'package.json',
|
|
72
|
+
category: 'config',
|
|
73
|
+
language: 'JSON',
|
|
74
|
+
imports: [],
|
|
75
|
+
runtime_hints: [],
|
|
76
|
+
reasons: ['config']
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
path: 'src/index.js',
|
|
80
|
+
category: 'source',
|
|
81
|
+
language: 'JavaScript',
|
|
82
|
+
imports: ['./utils.js'],
|
|
83
|
+
environment_variables: ['APP_MODE', 'PORT'],
|
|
84
|
+
route_surfaces: [
|
|
85
|
+
{
|
|
86
|
+
framework: 'express',
|
|
87
|
+
target: 'app',
|
|
88
|
+
methods: ['GET'],
|
|
89
|
+
path: '/health',
|
|
90
|
+
handler: 'healthCheck'
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
runtime_hints: ['environment-variable', 'http-route'],
|
|
94
|
+
reasons: ['api-surface', 'configuration']
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
path: 'src/utils.js',
|
|
98
|
+
category: 'source',
|
|
99
|
+
language: 'JavaScript',
|
|
100
|
+
imports: [],
|
|
101
|
+
environment_variables: [],
|
|
102
|
+
route_surfaces: [],
|
|
103
|
+
runtime_hints: [],
|
|
104
|
+
reasons: ['source']
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
path: 'test/index.test.js',
|
|
108
|
+
category: 'test',
|
|
109
|
+
language: 'JavaScript',
|
|
110
|
+
imports: ['../src/index.js'],
|
|
111
|
+
environment_variables: [],
|
|
112
|
+
route_surfaces: [],
|
|
113
|
+
runtime_hints: [],
|
|
114
|
+
reasons: ['test']
|
|
115
|
+
}
|
|
116
|
+
],
|
|
117
|
+
analysis: {
|
|
118
|
+
package_scripts: [
|
|
119
|
+
{
|
|
120
|
+
path: 'package.json',
|
|
121
|
+
name: 'fixture-repo',
|
|
122
|
+
scripts: {
|
|
123
|
+
build: 'node build.js',
|
|
124
|
+
test: 'node --test'
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
dependency_graph: {
|
|
129
|
+
edges: [
|
|
130
|
+
{ from: 'src/index.js', to: 'src/utils.js', specifier: './utils.js' },
|
|
131
|
+
{ from: 'test/index.test.js', to: 'src/index.js', specifier: '../src/index.js' }
|
|
132
|
+
],
|
|
133
|
+
summary: {
|
|
134
|
+
edges: 2,
|
|
135
|
+
importers: 2,
|
|
136
|
+
imported_files: 2
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
test_to_source: {
|
|
140
|
+
mappings: [
|
|
141
|
+
{
|
|
142
|
+
test: 'test/index.test.js',
|
|
143
|
+
sources: ['src/index.js'],
|
|
144
|
+
heuristics: ['imports']
|
|
145
|
+
}
|
|
146
|
+
],
|
|
147
|
+
summary: {
|
|
148
|
+
mapped_tests: 1,
|
|
149
|
+
source_files: 1
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest, plan: createPlan() });
|
|
155
|
+
try {
|
|
156
|
+
await compileWiki({ scanDir, planFile, wikiDir });
|
|
157
|
+
const buildPage = await fs.readFile(path.join(wikiDir, 'Build-Test-and-Run.md'), 'utf8');
|
|
158
|
+
const dependencyPage = await fs.readFile(path.join(wikiDir, 'Dependency-Map.md'), 'utf8');
|
|
159
|
+
const configPage = await fs.readFile(path.join(wikiDir, 'Configuration-and-Environment.md'), 'utf8');
|
|
160
|
+
const routesPage = await fs.readFile(path.join(wikiDir, 'API-HTTP-Routes.md'), 'utf8');
|
|
161
|
+
const testingPage = await fs.readFile(path.join(wikiDir, 'Testing-Strategy.md'), 'utf8');
|
|
162
|
+
assert.match(buildPage, /source_paths: \["package\.json"\]/);
|
|
163
|
+
assert.match(buildPage, /Package scripts/);
|
|
164
|
+
assert.match(buildPage, /node --test/);
|
|
165
|
+
assert.match(dependencyPage, /source_paths: \["src\/index\.js","src\/utils\.js","test\/index\.test\.js"\]/);
|
|
166
|
+
assert.match(dependencyPage, /Resolved internal dependency edges/);
|
|
167
|
+
assert.match(dependencyPage, /src\/utils\.js/);
|
|
168
|
+
assert.match(configPage, /source_paths: \["src\/index\.js"\]/);
|
|
169
|
+
assert.match(configPage, /APP_MODE/);
|
|
170
|
+
assert.match(configPage, /PORT/);
|
|
171
|
+
assert.match(routesPage, /source_paths: \["src\/index\.js"\]/);
|
|
172
|
+
assert.match(routesPage, /\/health/);
|
|
173
|
+
assert.match(routesPage, /healthCheck/);
|
|
174
|
+
assert.match(testingPage, /source_paths: \["src\/index\.js","test\/index\.test\.js"\]/);
|
|
175
|
+
assert.match(testingPage, /Test-to-source mappings/);
|
|
176
|
+
assert.match(testingPage, /test\/index\.test\.js/);
|
|
177
|
+
assert.match(testingPage, /src\/index\.js/);
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
test('compileWiki falls back cleanly when richer analysis is absent', async () => {
|
|
184
|
+
const manifest = {
|
|
185
|
+
remote: 'origin',
|
|
186
|
+
commit: 'def987654321',
|
|
187
|
+
mode: 'bootstrap',
|
|
188
|
+
totals: {
|
|
189
|
+
languages: { JavaScript: 1 },
|
|
190
|
+
categories: { source: 1 },
|
|
191
|
+
runtime_hints: {}
|
|
192
|
+
},
|
|
193
|
+
files: [
|
|
194
|
+
{
|
|
195
|
+
path: 'src/index.js',
|
|
196
|
+
category: 'source',
|
|
197
|
+
language: 'JavaScript',
|
|
198
|
+
imports: ['./utils.js'],
|
|
199
|
+
runtime_hints: [],
|
|
200
|
+
reasons: ['source']
|
|
201
|
+
}
|
|
202
|
+
]
|
|
203
|
+
};
|
|
204
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest, plan: createPlan() });
|
|
205
|
+
try {
|
|
206
|
+
await compileWiki({ scanDir, planFile, wikiDir });
|
|
207
|
+
const buildPage = await fs.readFile(path.join(wikiDir, 'Build-Test-and-Run.md'), 'utf8');
|
|
208
|
+
const dependencyPage = await fs.readFile(path.join(wikiDir, 'Dependency-Map.md'), 'utf8');
|
|
209
|
+
const configPage = await fs.readFile(path.join(wikiDir, 'Configuration-and-Environment.md'), 'utf8');
|
|
210
|
+
const testingPage = await fs.readFile(path.join(wikiDir, 'Testing-Strategy.md'), 'utf8');
|
|
211
|
+
assert.match(buildPage, /No package scripts were extracted/);
|
|
212
|
+
assert.match(dependencyPage, /Source file \| Imports/);
|
|
213
|
+
assert.match(configPage, /No explicit environment variable names were extracted/);
|
|
214
|
+
assert.match(testingPage, /The compiler will add direct test-to-source mappings/);
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
test('compileWiki redacts sensitive CI command arguments in Build-Test-and-Run output', async () => {
|
|
221
|
+
const manifest = {
|
|
222
|
+
remote: 'origin',
|
|
223
|
+
commit: 'aa11bb22cc33',
|
|
224
|
+
mode: 'bootstrap',
|
|
225
|
+
totals: {
|
|
226
|
+
languages: { YAML: 1, JSON: 1 },
|
|
227
|
+
categories: { ci: 1, config: 1 },
|
|
228
|
+
runtime_hints: {}
|
|
229
|
+
},
|
|
230
|
+
files: [
|
|
231
|
+
{
|
|
232
|
+
path: '.github/workflows/ci.yml',
|
|
233
|
+
category: 'ci',
|
|
234
|
+
language: 'YAML',
|
|
235
|
+
imports: [],
|
|
236
|
+
runtime_hints: [],
|
|
237
|
+
reasons: ['ci']
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
path: 'package.json',
|
|
241
|
+
category: 'config',
|
|
242
|
+
language: 'JSON',
|
|
243
|
+
imports: [],
|
|
244
|
+
runtime_hints: [],
|
|
245
|
+
reasons: ['config']
|
|
246
|
+
}
|
|
247
|
+
],
|
|
248
|
+
analysis: {
|
|
249
|
+
package_scripts: [],
|
|
250
|
+
ci_workflow_command_sources: [
|
|
251
|
+
{
|
|
252
|
+
path: '.github/workflows/ci.yml',
|
|
253
|
+
command: 'curl -H "authorization: bearer super-secret-token" https://example.test',
|
|
254
|
+
line: 12
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
path: '.github/workflows/ci.yml',
|
|
258
|
+
command: 'npm run deploy --token abc123',
|
|
259
|
+
line: 18
|
|
260
|
+
}
|
|
261
|
+
]
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest, plan: createPlan() });
|
|
265
|
+
try {
|
|
266
|
+
await compileWiki({ scanDir, planFile, wikiDir });
|
|
267
|
+
const buildPage = await fs.readFile(path.join(wikiDir, 'Build-Test-and-Run.md'), 'utf8');
|
|
268
|
+
assert.match(buildPage, /authorization: bearer \[REDACTED\]/i);
|
|
269
|
+
assert.doesNotMatch(buildPage, /super-secret-token/);
|
|
270
|
+
assert.match(buildPage, /--token \[REDACTED\]/);
|
|
271
|
+
assert.doesNotMatch(buildPage, /--token abc123/);
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
test('compileWiki renders related tests in module pages', async () => {
|
|
278
|
+
const manifest = {
|
|
279
|
+
remote: 'origin',
|
|
280
|
+
commit: 'abc123456789',
|
|
281
|
+
mode: 'bootstrap',
|
|
282
|
+
totals: {
|
|
283
|
+
languages: { JavaScript: 2 },
|
|
284
|
+
categories: { source: 1, test: 1 },
|
|
285
|
+
runtime_hints: {}
|
|
286
|
+
},
|
|
287
|
+
files: [
|
|
288
|
+
{
|
|
289
|
+
path: 'src/utils.js',
|
|
290
|
+
category: 'source',
|
|
291
|
+
language: 'JavaScript',
|
|
292
|
+
imports: [],
|
|
293
|
+
runtime_hints: [],
|
|
294
|
+
reasons: ['source']
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
path: 'test/utils.test.js',
|
|
298
|
+
category: 'test',
|
|
299
|
+
language: 'JavaScript',
|
|
300
|
+
imports: ['../src/utils.js'],
|
|
301
|
+
runtime_hints: [],
|
|
302
|
+
reasons: ['test']
|
|
303
|
+
}
|
|
304
|
+
],
|
|
305
|
+
analysis: {
|
|
306
|
+
package_scripts: [],
|
|
307
|
+
dependency_graph: {
|
|
308
|
+
edges: [{ from: 'test/utils.test.js', to: 'src/utils.js', specifier: '../src/utils.js' }],
|
|
309
|
+
summary: { edges: 1, importers: 1, imported_files: 1, imported_packages: 0 }
|
|
310
|
+
},
|
|
311
|
+
test_to_source: {
|
|
312
|
+
mappings: [
|
|
313
|
+
{ test: 'test/utils.test.js', sources: ['src/utils.js'], heuristics: ['imports'] }
|
|
314
|
+
],
|
|
315
|
+
summary: { mapped_tests: 1, source_files: 1 }
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
const plan = {
|
|
320
|
+
pages: createPlan().pages,
|
|
321
|
+
modules: [
|
|
322
|
+
{
|
|
323
|
+
slug: 'Module-Utils',
|
|
324
|
+
name: 'Utils',
|
|
325
|
+
files: ['src/utils.js'],
|
|
326
|
+
categories: { source: 1 },
|
|
327
|
+
languages: { JavaScript: 1 },
|
|
328
|
+
runtime_hints: {},
|
|
329
|
+
important_reasons: ['source']
|
|
330
|
+
}
|
|
331
|
+
]
|
|
332
|
+
};
|
|
333
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest, plan });
|
|
334
|
+
try {
|
|
335
|
+
await compileWiki({ scanDir, planFile, wikiDir });
|
|
336
|
+
const modulePage = await fs.readFile(path.join(wikiDir, 'Module-Utils.md'), 'utf8');
|
|
337
|
+
assert.match(modulePage, /Related tests/);
|
|
338
|
+
assert.match(modulePage, /test\/utils\.test\.js/);
|
|
339
|
+
}
|
|
340
|
+
finally {
|
|
341
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
test('compileWiki renders source file paths as commit-pinned GitHub links when remote is GitHub', async () => {
|
|
345
|
+
const manifest = {
|
|
346
|
+
remote: 'git@github.com:owner/example.git',
|
|
347
|
+
commit: 'abc123456789',
|
|
348
|
+
mode: 'bootstrap',
|
|
349
|
+
totals: {
|
|
350
|
+
languages: { TypeScript: 2, JSON: 1 },
|
|
351
|
+
categories: { source: 1, test: 1, config: 1 },
|
|
352
|
+
runtime_hints: {}
|
|
353
|
+
},
|
|
354
|
+
files: [
|
|
355
|
+
{ path: 'package.json', category: 'config', language: 'JSON', imports: [], runtime_hints: [], reasons: ['config'] },
|
|
356
|
+
{ path: 'src/file [with] spaces.ts', category: 'source', language: 'TypeScript', imports: [], runtime_hints: [], reasons: ['source'] },
|
|
357
|
+
{ path: 'test/file with spaces.test.ts', category: 'test', language: 'TypeScript', imports: ['../src/file [with] spaces.ts'], runtime_hints: [], reasons: ['test'] }
|
|
358
|
+
],
|
|
359
|
+
analysis: {
|
|
360
|
+
package_scripts: [{
|
|
361
|
+
path: 'package.json',
|
|
362
|
+
name: 'example',
|
|
363
|
+
scripts: { test: 'node --test' },
|
|
364
|
+
script_sources: [{ name: 'test', line: 7 }]
|
|
365
|
+
}],
|
|
366
|
+
ci_workflow_command_sources: [
|
|
367
|
+
{ path: '.github/workflows/ci.yml', command: 'npm ci', line: 12, end_line: 13 }
|
|
368
|
+
],
|
|
369
|
+
dependency_graph: {
|
|
370
|
+
edges: [{ from: 'test/file with spaces.test.ts', to: 'src/file [with] spaces.ts', specifier: '../src/file [with] spaces.ts' }],
|
|
371
|
+
summary: { edges: 1, importers: 1, imported_files: 1 }
|
|
372
|
+
},
|
|
373
|
+
test_to_source: {
|
|
374
|
+
mappings: [{ test: 'test/file with spaces.test.ts', sources: ['src/file [with] spaces.ts'], heuristics: ['imports'] }],
|
|
375
|
+
summary: { mapped_tests: 1, source_files: 1 }
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
const plan = {
|
|
380
|
+
pages: createPlan().pages,
|
|
381
|
+
modules: [
|
|
382
|
+
{
|
|
383
|
+
slug: 'Module-Source',
|
|
384
|
+
name: 'Source',
|
|
385
|
+
files: ['src/file [with] spaces.ts'],
|
|
386
|
+
categories: { source: 1 },
|
|
387
|
+
languages: { TypeScript: 1 },
|
|
388
|
+
runtime_hints: {},
|
|
389
|
+
important_reasons: ['source']
|
|
390
|
+
}
|
|
391
|
+
]
|
|
392
|
+
};
|
|
393
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest, plan });
|
|
394
|
+
try {
|
|
395
|
+
await compileWiki({ scanDir, planFile, wikiDir });
|
|
396
|
+
const modulePage = await fs.readFile(path.join(wikiDir, 'Module-Source.md'), 'utf8');
|
|
397
|
+
const buildPage = await fs.readFile(path.join(wikiDir, 'Build-Test-and-Run.md'), 'utf8');
|
|
398
|
+
const testingPage = await fs.readFile(path.join(wikiDir, 'Testing-Strategy.md'), 'utf8');
|
|
399
|
+
const dependencyPage = await fs.readFile(path.join(wikiDir, 'Dependency-Map.md'), 'utf8');
|
|
400
|
+
assert.ok(modulePage.includes('[src/file \\[with\\] spaces.ts](https://github.com/owner/example/blob/abc123456789/src/file%20%5Bwith%5D%20spaces.ts)'));
|
|
401
|
+
assert.match(buildPage, /\[package\.json\]\(https:\/\/github\.com\/owner\/example\/blob\/abc123456789\/package\.json#L7\)/);
|
|
402
|
+
assert.match(buildPage, /\[\.github\/workflows\/ci\.yml\]\(https:\/\/github\.com\/owner\/example\/blob\/abc123456789\/\.github\/workflows\/ci\.yml#L12-L13\)/);
|
|
403
|
+
assert.match(testingPage, /\[test\/file with spaces\.test\.ts\]\(https:\/\/github\.com\/owner\/example\/blob\/abc123456789\/test\/file%20with%20spaces\.test\.ts\)/);
|
|
404
|
+
assert.ok(dependencyPage.includes('[src/file \\[with\\] spaces.ts](https://github.com/owner/example/blob/abc123456789/src/file%20%5Bwith%5D%20spaces.ts)'));
|
|
405
|
+
}
|
|
406
|
+
finally {
|
|
407
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
test('compileWiki omits Related tests section when no test mappings exist for the module', async () => {
|
|
411
|
+
const manifest = {
|
|
412
|
+
remote: 'origin',
|
|
413
|
+
commit: 'abc123456789',
|
|
414
|
+
mode: 'bootstrap',
|
|
415
|
+
totals: {
|
|
416
|
+
languages: { JavaScript: 1 },
|
|
417
|
+
categories: { source: 1 },
|
|
418
|
+
runtime_hints: {}
|
|
419
|
+
},
|
|
420
|
+
files: [
|
|
421
|
+
{
|
|
422
|
+
path: 'src/utils.js',
|
|
423
|
+
category: 'source',
|
|
424
|
+
language: 'JavaScript',
|
|
425
|
+
imports: [],
|
|
426
|
+
runtime_hints: [],
|
|
427
|
+
reasons: ['source']
|
|
428
|
+
}
|
|
429
|
+
]
|
|
430
|
+
};
|
|
431
|
+
const plan = {
|
|
432
|
+
pages: createPlan().pages,
|
|
433
|
+
modules: [
|
|
434
|
+
{
|
|
435
|
+
slug: 'Module-Utils',
|
|
436
|
+
name: 'Utils',
|
|
437
|
+
files: ['src/utils.js'],
|
|
438
|
+
categories: { source: 1 },
|
|
439
|
+
languages: { JavaScript: 1 },
|
|
440
|
+
runtime_hints: {},
|
|
441
|
+
important_reasons: ['source']
|
|
442
|
+
}
|
|
443
|
+
]
|
|
444
|
+
};
|
|
445
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest, plan });
|
|
446
|
+
try {
|
|
447
|
+
await compileWiki({ scanDir, planFile, wikiDir });
|
|
448
|
+
const modulePage = await fs.readFile(path.join(wikiDir, 'Module-Utils.md'), 'utf8');
|
|
449
|
+
assert.doesNotMatch(modulePage, /Related tests/);
|
|
450
|
+
}
|
|
451
|
+
finally {
|
|
452
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
test('compileWiki renders data-model page for ORM-only manifests', async () => {
|
|
456
|
+
const manifest = {
|
|
457
|
+
mode: 'bootstrap',
|
|
458
|
+
totals: {
|
|
459
|
+
languages: { TypeScript: 1 },
|
|
460
|
+
categories: { source: 1 },
|
|
461
|
+
runtime_hints: { 'data-model': 1, 'orm-model': 1 }
|
|
462
|
+
},
|
|
463
|
+
files: [
|
|
464
|
+
{
|
|
465
|
+
path: 'src/models/user.entity.ts',
|
|
466
|
+
category: 'source',
|
|
467
|
+
language: 'TypeScript',
|
|
468
|
+
imports: [],
|
|
469
|
+
runtime_hints: ['data-model', 'orm-model'],
|
|
470
|
+
reasons: ['data-model', 'orm-model'],
|
|
471
|
+
model_surfaces: [{ name: 'UserEntity', kind: 'entity', framework: 'typeorm' }]
|
|
472
|
+
}
|
|
473
|
+
]
|
|
474
|
+
};
|
|
475
|
+
const plan = createPlan();
|
|
476
|
+
plan.pages.push({ path: 'Data-Model-and-Migrations.md', phase: 'cross-cutting', purpose: 'Data models and migrations.' });
|
|
477
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest, plan });
|
|
478
|
+
try {
|
|
479
|
+
await compileWiki({ scanDir, planFile, wikiDir });
|
|
480
|
+
const dataPage = await fs.readFile(path.join(wikiDir, 'Data-Model-and-Migrations.md'), 'utf8');
|
|
481
|
+
assert.match(dataPage, /Data Model and Migrations/);
|
|
482
|
+
assert.match(dataPage, /src\/models\/user\.entity\.ts/);
|
|
483
|
+
}
|
|
484
|
+
finally {
|
|
485
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
// Frontmatter: page_state metadata
|
|
490
|
+
// ---------------------------------------------------------------------------
|
|
491
|
+
test('compileWiki adds page_state: "generated" frontmatter to new pages', async () => {
|
|
492
|
+
const manifest = {
|
|
493
|
+
remote: 'origin',
|
|
494
|
+
commit: 'abc123',
|
|
495
|
+
mode: 'bootstrap',
|
|
496
|
+
totals: { languages: {}, categories: {}, runtime_hints: {} },
|
|
497
|
+
files: []
|
|
498
|
+
};
|
|
499
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest, plan: createPlan() });
|
|
500
|
+
try {
|
|
501
|
+
await compileWiki({ scanDir, planFile, wikiDir });
|
|
502
|
+
const homePage = await fs.readFile(path.join(wikiDir, 'Home.md'), 'utf8');
|
|
503
|
+
const sidebarPage = await fs.readFile(path.join(wikiDir, '_Sidebar.md'), 'utf8');
|
|
504
|
+
assert.match(homePage, /page_state: "generated"/);
|
|
505
|
+
assert.match(homePage, /confidence: "medium"/);
|
|
506
|
+
assert.doesNotMatch(homePage, /Last compiled:/);
|
|
507
|
+
assert.doesNotMatch(homePage, /Generated from `origin` at commit `abc123`/);
|
|
508
|
+
assert.match(sidebarPage, /source_commit: "abc123"/);
|
|
509
|
+
assert.match(sidebarPage, /page_state: "generated"/);
|
|
510
|
+
assert.match(sidebarPage, /confidence: "medium"/);
|
|
511
|
+
}
|
|
512
|
+
finally {
|
|
513
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
test('compileWiki sets high-confidence grounded metadata for generated module pages', async () => {
|
|
517
|
+
const manifest = {
|
|
518
|
+
remote: 'origin',
|
|
519
|
+
commit: 'abc123',
|
|
520
|
+
mode: 'bootstrap',
|
|
521
|
+
totals: { languages: { JavaScript: 1 }, categories: { source: 1 }, runtime_hints: {} },
|
|
522
|
+
files: [{ path: 'src/core.js', category: 'source', language: 'JavaScript', imports: [], runtime_hints: [], reasons: ['source'] }],
|
|
523
|
+
analysis: { package_scripts: [], dependency_graph: { edges: [], summary: {} }, test_to_source: { mappings: [], summary: {} } }
|
|
524
|
+
};
|
|
525
|
+
const plan = {
|
|
526
|
+
pages: createPlan().pages,
|
|
527
|
+
modules: [
|
|
528
|
+
{
|
|
529
|
+
slug: 'Module-Core',
|
|
530
|
+
name: 'Core',
|
|
531
|
+
files: ['src/core.js'],
|
|
532
|
+
categories: { source: 1 },
|
|
533
|
+
languages: { JavaScript: 1 },
|
|
534
|
+
runtime_hints: {},
|
|
535
|
+
important_reasons: ['source']
|
|
536
|
+
}
|
|
537
|
+
]
|
|
538
|
+
};
|
|
539
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest, plan });
|
|
540
|
+
try {
|
|
541
|
+
await compileWiki({ scanDir, planFile, wikiDir });
|
|
542
|
+
const modulePage = await fs.readFile(path.join(wikiDir, 'Module-Core.md'), 'utf8');
|
|
543
|
+
assert.match(modulePage, /kind: "module"/);
|
|
544
|
+
assert.match(modulePage, /confidence: "high"/);
|
|
545
|
+
assert.match(modulePage, /claim_status: "grounded"/);
|
|
546
|
+
assert.match(modulePage, /source_paths: \["src\/core\.js"\]/);
|
|
547
|
+
}
|
|
548
|
+
finally {
|
|
549
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
test('compileWiki routes stale, contradicted, and unvalidated documentation cards to Open Questions', async () => {
|
|
553
|
+
const manifest = {
|
|
554
|
+
remote: 'origin',
|
|
555
|
+
commit: 'abc123',
|
|
556
|
+
mode: 'bootstrap',
|
|
557
|
+
totals: { languages: { Markdown: 3 }, categories: { docs: 3 }, runtime_hints: {} },
|
|
558
|
+
files: [],
|
|
559
|
+
documentation: {
|
|
560
|
+
enabled: true,
|
|
561
|
+
authority: 'secondary',
|
|
562
|
+
summary: { files: 3, claims: 3, stale: 1, commands: 0, env_vars: 0, file_paths: 0 },
|
|
563
|
+
files: [
|
|
564
|
+
{ path: 'docs/stale.md', status: 'validated', stale: true, age_days: 400, claims: [{ text: 'old claim' }], validation: { contradictions: [] } },
|
|
565
|
+
{ path: 'docs/contradicted.md', status: 'validated', stale: false, age_days: 2, claims: [{ text: 'conflict claim' }], validation: { contradictions: ['conflict'] } },
|
|
566
|
+
{ path: 'docs/unvalidated.md', status: 'unvalidated', stale: false, age_days: 1, claims: [{ text: 'unknown claim' }], validation: { contradictions: [] } }
|
|
567
|
+
]
|
|
568
|
+
},
|
|
569
|
+
analysis: { package_scripts: [], dependency_graph: { edges: [], summary: {} }, test_to_source: { mappings: [], summary: {} } }
|
|
570
|
+
};
|
|
571
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest, plan: createPlan() });
|
|
572
|
+
try {
|
|
573
|
+
await compileWiki({ scanDir, planFile, wikiDir });
|
|
574
|
+
const openQuestions = await fs.readFile(path.join(wikiDir, 'Open-Questions.md'), 'utf8');
|
|
575
|
+
const debtReport = await fs.readFile(path.join(wikiDir, 'Documentation-Debt-Report.md'), 'utf8');
|
|
576
|
+
assert.match(openQuestions, /claim_status: "review-needed"/);
|
|
577
|
+
assert.match(openQuestions, /confidence: "low"/);
|
|
578
|
+
assert.match(openQuestions, /source_paths:/);
|
|
579
|
+
assert.match(openQuestions, /docs\/stale\.md/);
|
|
580
|
+
assert.match(openQuestions, /docs\/contradicted\.md/);
|
|
581
|
+
assert.match(openQuestions, /docs\/unvalidated\.md/);
|
|
582
|
+
assert.match(openQuestions, /`docs\/stale\.md` - .*stale/);
|
|
583
|
+
assert.match(openQuestions, /`docs\/contradicted\.md` - .*contradicted/);
|
|
584
|
+
assert.match(openQuestions, /`docs\/unvalidated\.md` - .*unvalidated/);
|
|
585
|
+
assert.match(openQuestions, /Do not promote these items as authoritative wiki claims until validated/);
|
|
586
|
+
assert.match(debtReport, /source_paths:/);
|
|
587
|
+
assert.match(debtReport, /docs\/stale\.md/);
|
|
588
|
+
assert.match(debtReport, /docs\/contradicted\.md/);
|
|
589
|
+
assert.match(debtReport, /docs\/unvalidated\.md/);
|
|
590
|
+
assert.match(debtReport, /## Findings by category/);
|
|
591
|
+
}
|
|
592
|
+
finally {
|
|
593
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
test('compileWiki Documentation Debt Report surfaces ADR-specific findings', async () => {
|
|
597
|
+
const manifest = {
|
|
598
|
+
remote: 'origin',
|
|
599
|
+
commit: 'abc123',
|
|
600
|
+
mode: 'bootstrap',
|
|
601
|
+
totals: { languages: { Markdown: 4 }, categories: { docs: 4 }, runtime_hints: {} },
|
|
602
|
+
files: [],
|
|
603
|
+
documentation: {
|
|
604
|
+
enabled: true,
|
|
605
|
+
authority: 'secondary',
|
|
606
|
+
summary: { files: 4, claims: 0, stale: 1, commands: 0, env_vars: 0, file_paths: 0 },
|
|
607
|
+
files: [
|
|
608
|
+
{
|
|
609
|
+
path: 'ADR/0001-current.md',
|
|
610
|
+
status: 'unvalidated',
|
|
611
|
+
authority: 'secondary',
|
|
612
|
+
stale: false,
|
|
613
|
+
age_days: 1,
|
|
614
|
+
claims: [],
|
|
615
|
+
validation: { contradictions: [] },
|
|
616
|
+
adr: { detected: true, status: 'Accepted', superseded: false, superseded_by: null, replaces: null, has_status_metadata: true }
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
path: 'docs/adrs/0002-superseded.md',
|
|
620
|
+
status: 'stale',
|
|
621
|
+
authority: 'secondary',
|
|
622
|
+
stale: true,
|
|
623
|
+
age_days: 300,
|
|
624
|
+
claims: [],
|
|
625
|
+
validation: { contradictions: [] },
|
|
626
|
+
adr: { detected: true, status: 'Superseded', superseded: true, superseded_by: 'ADR-0003', replaces: null, has_status_metadata: true }
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
path: 'docs/adrs/0000-legacy.md',
|
|
630
|
+
status: 'stale',
|
|
631
|
+
authority: 'secondary',
|
|
632
|
+
stale: true,
|
|
633
|
+
age_days: 400,
|
|
634
|
+
claims: [],
|
|
635
|
+
validation: { contradictions: [] },
|
|
636
|
+
adr: { detected: true, status: null, superseded: false, superseded_by: null, replaces: null, has_status_metadata: false }
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
path: 'docs/guide.md',
|
|
640
|
+
status: 'unvalidated',
|
|
641
|
+
authority: 'secondary',
|
|
642
|
+
stale: false,
|
|
643
|
+
age_days: 1,
|
|
644
|
+
claims: [],
|
|
645
|
+
validation: { contradictions: [] },
|
|
646
|
+
adr: { detected: false, status: null, superseded: false, superseded_by: null, replaces: null, has_status_metadata: false }
|
|
647
|
+
}
|
|
648
|
+
]
|
|
649
|
+
},
|
|
650
|
+
analysis: { package_scripts: [], dependency_graph: { edges: [], summary: {} }, test_to_source: { mappings: [], summary: {} } }
|
|
651
|
+
};
|
|
652
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest, plan: createPlan() });
|
|
653
|
+
try {
|
|
654
|
+
await compileWiki({ scanDir, planFile, wikiDir });
|
|
655
|
+
const debtReport = await fs.readFile(path.join(wikiDir, 'Documentation-Debt-Report.md'), 'utf8');
|
|
656
|
+
assert.match(debtReport, /## ADR validation/);
|
|
657
|
+
assert.match(debtReport, /ADR files detected: 3/);
|
|
658
|
+
assert.match(debtReport, /Superseded ADRs: 1/);
|
|
659
|
+
assert.match(debtReport, /Old ADRs missing status metadata: 1/);
|
|
660
|
+
assert.match(debtReport, /\| `docs\/adrs\/0002-superseded\.md` \| `Superseded` \| `ADR-0003` \| 300 \| ⚠ superseded \|/);
|
|
661
|
+
assert.match(debtReport, /\| `docs\/adrs\/0000-legacy\.md` \| `unknown` \| `-` \| 400 \| ⚠ old without status metadata \|/);
|
|
662
|
+
assert.match(debtReport, /### ADR-specific/);
|
|
663
|
+
assert.match(debtReport, /`docs\/adrs\/0002-superseded\.md` - superseded ADR \(superseded by ADR-0003\)\./);
|
|
664
|
+
assert.match(debtReport, /`docs\/adrs\/0000-legacy\.md` - stale ADR missing explicit status metadata\./);
|
|
665
|
+
}
|
|
666
|
+
finally {
|
|
667
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
// ---------------------------------------------------------------------------
|
|
671
|
+
// Human-notes preservation
|
|
672
|
+
// ---------------------------------------------------------------------------
|
|
673
|
+
test('compileWiki preserves HUMAN_NOTES content byte-for-byte on recompilation', async () => {
|
|
674
|
+
const manifest = {
|
|
675
|
+
remote: 'origin',
|
|
676
|
+
commit: 'abc123',
|
|
677
|
+
mode: 'bootstrap',
|
|
678
|
+
totals: { languages: { JavaScript: 1 }, categories: { source: 1 }, runtime_hints: {} },
|
|
679
|
+
files: [{ path: 'src/utils.js', category: 'source', language: 'JavaScript', imports: [], runtime_hints: [], reasons: ['source'] }],
|
|
680
|
+
analysis: { package_scripts: [], dependency_graph: { edges: [], summary: {} }, test_to_source: { mappings: [], summary: {} } }
|
|
681
|
+
};
|
|
682
|
+
const plan = {
|
|
683
|
+
pages: createPlan().pages,
|
|
684
|
+
modules: [
|
|
685
|
+
{
|
|
686
|
+
slug: 'Module-Utils',
|
|
687
|
+
name: 'Utils',
|
|
688
|
+
files: ['src/utils.js'],
|
|
689
|
+
categories: { source: 1 },
|
|
690
|
+
languages: { JavaScript: 1 },
|
|
691
|
+
runtime_hints: {},
|
|
692
|
+
important_reasons: ['source']
|
|
693
|
+
}
|
|
694
|
+
]
|
|
695
|
+
};
|
|
696
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest, plan });
|
|
697
|
+
try {
|
|
698
|
+
// First compilation – produces a clean generated page.
|
|
699
|
+
await compileWiki({ scanDir, planFile, wikiDir });
|
|
700
|
+
const firstPage = await fs.readFile(path.join(wikiDir, 'Module-Utils.md'), 'utf8');
|
|
701
|
+
assert.match(firstPage, /page_state: "generated"/);
|
|
702
|
+
// Simulate a human adding notes between the markers.
|
|
703
|
+
const humanNotes = '\n## My custom section\n\nThis was written by a human.\n';
|
|
704
|
+
const pageWithNotes = firstPage.replace('<!-- HUMAN_NOTES_START -->\n<!-- HUMAN_NOTES_END -->', `<!-- HUMAN_NOTES_START -->${humanNotes}<!-- HUMAN_NOTES_END -->`);
|
|
705
|
+
await fs.writeFile(path.join(wikiDir, 'Module-Utils.md'), pageWithNotes, 'utf8');
|
|
706
|
+
// Second compilation – must preserve the human notes.
|
|
707
|
+
await compileWiki({ scanDir, planFile, wikiDir });
|
|
708
|
+
const secondPage = await fs.readFile(path.join(wikiDir, 'Module-Utils.md'), 'utf8');
|
|
709
|
+
assert.equal(extractHumanNotes(secondPage), humanNotes);
|
|
710
|
+
assert.match(secondPage, /page_state: "mixed"/);
|
|
711
|
+
}
|
|
712
|
+
finally {
|
|
713
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
test('compileWiki preserves whitespace-only HUMAN_NOTES without marking the page mixed', async () => {
|
|
717
|
+
const manifest = {
|
|
718
|
+
remote: 'origin',
|
|
719
|
+
commit: 'abc123',
|
|
720
|
+
mode: 'bootstrap',
|
|
721
|
+
totals: { languages: { JavaScript: 1 }, categories: { source: 1 }, runtime_hints: {} },
|
|
722
|
+
files: [{ path: 'src/utils.js', category: 'source', language: 'JavaScript', imports: [], runtime_hints: [], reasons: ['source'] }],
|
|
723
|
+
analysis: { package_scripts: [], dependency_graph: { edges: [], summary: {} }, test_to_source: { mappings: [], summary: {} } }
|
|
724
|
+
};
|
|
725
|
+
const plan = {
|
|
726
|
+
pages: createPlan().pages,
|
|
727
|
+
modules: [
|
|
728
|
+
{
|
|
729
|
+
slug: 'Module-Utils',
|
|
730
|
+
name: 'Utils',
|
|
731
|
+
files: ['src/utils.js'],
|
|
732
|
+
categories: { source: 1 },
|
|
733
|
+
languages: { JavaScript: 1 },
|
|
734
|
+
runtime_hints: {},
|
|
735
|
+
important_reasons: ['source']
|
|
736
|
+
}
|
|
737
|
+
]
|
|
738
|
+
};
|
|
739
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest, plan });
|
|
740
|
+
try {
|
|
741
|
+
await compileWiki({ scanDir, planFile, wikiDir });
|
|
742
|
+
const firstPage = await fs.readFile(path.join(wikiDir, 'Module-Utils.md'), 'utf8');
|
|
743
|
+
const whitespaceNotes = '\n \t \n';
|
|
744
|
+
const pageWithWhitespaceNotes = firstPage.replace('<!-- HUMAN_NOTES_START -->\n<!-- HUMAN_NOTES_END -->', `<!-- HUMAN_NOTES_START -->${whitespaceNotes}<!-- HUMAN_NOTES_END -->`);
|
|
745
|
+
await fs.writeFile(path.join(wikiDir, 'Module-Utils.md'), pageWithWhitespaceNotes, 'utf8');
|
|
746
|
+
await compileWiki({ scanDir, planFile, wikiDir });
|
|
747
|
+
const secondPage = await fs.readFile(path.join(wikiDir, 'Module-Utils.md'), 'utf8');
|
|
748
|
+
assert.equal(extractHumanNotes(secondPage), whitespaceNotes);
|
|
749
|
+
assert.match(secondPage, /page_state: "generated"/);
|
|
750
|
+
}
|
|
751
|
+
finally {
|
|
752
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
test('compileWiki does not overwrite a human-owned page and summarizes owned_by skips', async () => {
|
|
756
|
+
const manifest = {
|
|
757
|
+
remote: 'origin',
|
|
758
|
+
commit: 'abc123',
|
|
759
|
+
mode: 'bootstrap',
|
|
760
|
+
totals: { languages: {}, categories: {}, runtime_hints: {} },
|
|
761
|
+
files: []
|
|
762
|
+
};
|
|
763
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest, plan: createPlan() });
|
|
764
|
+
try {
|
|
765
|
+
// First compilation.
|
|
766
|
+
await compileWiki({ scanDir, planFile, wikiDir });
|
|
767
|
+
// A human claims ownership of the Home page using owned_by.
|
|
768
|
+
const originalHome = await fs.readFile(path.join(wikiDir, 'Home.md'), 'utf8');
|
|
769
|
+
const ownedHome = originalHome.replace('page_state: "generated"', 'page_state: "generated"\nowned_by: "human"');
|
|
770
|
+
await fs.writeFile(path.join(wikiDir, 'Home.md'), ownedHome, 'utf8');
|
|
771
|
+
// Second compilation – must not touch the human-owned page.
|
|
772
|
+
const result = await compileWiki({ scanDir, planFile, wikiDir });
|
|
773
|
+
const afterRecompile = await fs.readFile(path.join(wikiDir, 'Home.md'), 'utf8');
|
|
774
|
+
assert.equal(afterRecompile, ownedHome);
|
|
775
|
+
assert.equal(result.summary.skipped, 1);
|
|
776
|
+
assert.equal(result.summary.skipped_by_state['human-owned'], 1);
|
|
777
|
+
}
|
|
778
|
+
finally {
|
|
779
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
test('compileWiki does not overwrite unmanaged pages by default', async () => {
|
|
783
|
+
const manifest = {
|
|
784
|
+
remote: 'origin',
|
|
785
|
+
commit: 'abc123',
|
|
786
|
+
mode: 'bootstrap',
|
|
787
|
+
totals: { languages: { JavaScript: 1 }, categories: { source: 1 }, runtime_hints: {} },
|
|
788
|
+
files: [{ path: 'src/index.js', category: 'source', language: 'JavaScript', imports: [], runtime_hints: [], reasons: ['source'] }],
|
|
789
|
+
analysis: { package_scripts: [], dependency_graph: { edges: [], summary: {} }, test_to_source: { mappings: [], summary: {} } }
|
|
790
|
+
};
|
|
791
|
+
const plan = {
|
|
792
|
+
pages: createPlan().pages,
|
|
793
|
+
modules: [
|
|
794
|
+
{
|
|
795
|
+
slug: 'Module-Index',
|
|
796
|
+
name: 'Index',
|
|
797
|
+
files: ['src/index.js'],
|
|
798
|
+
categories: { source: 1 },
|
|
799
|
+
languages: { JavaScript: 1 },
|
|
800
|
+
runtime_hints: {},
|
|
801
|
+
important_reasons: ['source']
|
|
802
|
+
}
|
|
803
|
+
]
|
|
804
|
+
};
|
|
805
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest, plan });
|
|
806
|
+
try {
|
|
807
|
+
await fs.mkdir(wikiDir, { recursive: true });
|
|
808
|
+
// Pre-existing unmanaged page with HUMAN_NOTES markers but no source_commit.
|
|
809
|
+
const unmanagedContent = [
|
|
810
|
+
'# Module Index',
|
|
811
|
+
'',
|
|
812
|
+
'Some description.',
|
|
813
|
+
'',
|
|
814
|
+
'<!-- HUMAN_NOTES_START -->',
|
|
815
|
+
'Notes left by a human.',
|
|
816
|
+
'<!-- HUMAN_NOTES_END -->',
|
|
817
|
+
''
|
|
818
|
+
].join('\n');
|
|
819
|
+
await fs.writeFile(path.join(wikiDir, 'Module-Index.md'), unmanagedContent, 'utf8');
|
|
820
|
+
// Compilation must not implicitly adopt or overwrite the unmanaged page.
|
|
821
|
+
const compileResult = await compileWiki({ scanDir, planFile, wikiDir });
|
|
822
|
+
const result = await fs.readFile(path.join(wikiDir, 'Module-Index.md'), 'utf8');
|
|
823
|
+
assert.equal(result, unmanagedContent);
|
|
824
|
+
assert.equal(compileResult.summary.skipped, 1);
|
|
825
|
+
assert.equal(compileResult.summary.skipped_by_state.unmanaged, 1);
|
|
826
|
+
}
|
|
827
|
+
finally {
|
|
828
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
// ---------------------------------------------------------------------------
|
|
832
|
+
// LLM compiler mode
|
|
833
|
+
// ---------------------------------------------------------------------------
|
|
834
|
+
function createLLMPlan() {
|
|
835
|
+
return {
|
|
836
|
+
pages: createPlan().pages,
|
|
837
|
+
modules: [
|
|
838
|
+
{
|
|
839
|
+
slug: 'Module-Auth',
|
|
840
|
+
name: 'Auth',
|
|
841
|
+
files: ['src/auth.ts'],
|
|
842
|
+
categories: { source: 1 },
|
|
843
|
+
languages: { TypeScript: 1 },
|
|
844
|
+
runtime_hints: {},
|
|
845
|
+
important_reasons: ['source']
|
|
846
|
+
}
|
|
847
|
+
]
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
function validLLMTestContent(req) {
|
|
851
|
+
const sourcePaths = req.sourcePaths?.length ? req.sourcePaths : ['src/auth.ts'];
|
|
852
|
+
const frontmatter = [
|
|
853
|
+
'---',
|
|
854
|
+
`kind: ${JSON.stringify(req.archetype)}`,
|
|
855
|
+
'compiled_at: "mock"',
|
|
856
|
+
'source_repo: "mock"',
|
|
857
|
+
'source_commit: "mock"',
|
|
858
|
+
'page_state: "generated"',
|
|
859
|
+
...(req.archetype === 'architecture' ? ['confidence: "medium"', 'claim_status: "grounded"'] : []),
|
|
860
|
+
`source_paths: ${JSON.stringify(sourcePaths)}`,
|
|
861
|
+
'---',
|
|
862
|
+
'',
|
|
863
|
+
`# ${req.pageTitle}`,
|
|
864
|
+
'',
|
|
865
|
+
`> Archetype: ${req.archetype}`,
|
|
866
|
+
'',
|
|
867
|
+
];
|
|
868
|
+
if (req.archetype === 'architecture') {
|
|
869
|
+
frontmatter.push('## Executive Architecture Summary', '', 'Test architecture summary.', '', '## System and Repository Context', '', 'Test repository context.', '', '## Major Modules and Responsibilities', '', 'Test module responsibilities.', '', '## Runtime, Data, and Control-Flow Relationships', '', 'Test runtime relationships.', '', '## Build, Test, Deployment, and Operational Surfaces', '', 'Test build surfaces.', '', '## Cross-Cutting Concerns', '', 'Test cross-cutting concerns.', '', '## Caveats and Open Questions', '', 'Test caveats.', '');
|
|
870
|
+
}
|
|
871
|
+
frontmatter.push('<!-- HUMAN_NOTES_START -->', '<!-- HUMAN_NOTES_END -->', '');
|
|
872
|
+
return frontmatter.join('\n');
|
|
873
|
+
}
|
|
874
|
+
const defaultLLMManifest = {
|
|
875
|
+
remote: 'origin',
|
|
876
|
+
commit: 'llm-test-commit',
|
|
877
|
+
mode: 'bootstrap',
|
|
878
|
+
totals: { languages: { TypeScript: 1 }, categories: { source: 1 }, runtime_hints: {} },
|
|
879
|
+
files: [
|
|
880
|
+
{
|
|
881
|
+
path: 'src/auth.ts',
|
|
882
|
+
category: 'source',
|
|
883
|
+
language: 'TypeScript',
|
|
884
|
+
imports: [],
|
|
885
|
+
runtime_hints: [],
|
|
886
|
+
reasons: ['source']
|
|
887
|
+
}
|
|
888
|
+
],
|
|
889
|
+
analysis: {
|
|
890
|
+
package_scripts: [],
|
|
891
|
+
dependency_graph: { edges: [], summary: {} },
|
|
892
|
+
test_to_source: { mappings: [], summary: {} }
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
test('compileWiki in LLM mode synthesizes module pages through the mock provider', async () => {
|
|
896
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest: defaultLLMManifest, plan: createLLMPlan() });
|
|
897
|
+
const config = { compiler: { mode: 'llm' } };
|
|
898
|
+
try {
|
|
899
|
+
const result = await compileWiki({ scanDir, planFile, wikiDir, config, _provider: new MockLLMProvider() });
|
|
900
|
+
const modulePage = await fs.readFile(path.join(wikiDir, 'Module-Auth.md'), 'utf8');
|
|
901
|
+
// Mock provider output: valid frontmatter with kind, source_commit, compiled_at, source_paths
|
|
902
|
+
assert.match(modulePage, /kind: "module"/);
|
|
903
|
+
assert.match(modulePage, /source_commit: "llm-test-commit"/);
|
|
904
|
+
assert.match(modulePage, /page_state: "generated"/);
|
|
905
|
+
assert.match(modulePage, /source_paths: \["src\/auth\.ts"\]/);
|
|
906
|
+
assert.match(modulePage, /# Auth/);
|
|
907
|
+
assert.match(modulePage, /Generated by the mock LLM provider/);
|
|
908
|
+
// Module page counted
|
|
909
|
+
assert.ok(result.summary.pages > 0);
|
|
910
|
+
// Summary must report compiler mode and per-renderer page counts.
|
|
911
|
+
assert.equal(result.summary.compiler_mode, 'llm');
|
|
912
|
+
// Architecture.md is also synthesized through LLM in LLM mode.
|
|
913
|
+
assert.equal(result.summary.llm_pages, 2);
|
|
914
|
+
assert.ok(result.summary.deterministic_pages >= 1);
|
|
915
|
+
}
|
|
916
|
+
finally {
|
|
917
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
test('compileWiki normalizes LLM block-list source_paths without leaving sequence entries', async () => {
|
|
921
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest: defaultLLMManifest, plan: createLLMPlan() });
|
|
922
|
+
const config = { compiler: { mode: 'llm' } };
|
|
923
|
+
const blockListProvider = {
|
|
924
|
+
name: 'block-list-mock',
|
|
925
|
+
async complete(req) {
|
|
926
|
+
if (req.archetype === 'architecture') {
|
|
927
|
+
return { provider: 'block-list-mock', content: validLLMTestContent(req) };
|
|
928
|
+
}
|
|
929
|
+
return {
|
|
930
|
+
provider: 'block-list-mock',
|
|
931
|
+
content: [
|
|
932
|
+
'---',
|
|
933
|
+
'kind: "module"',
|
|
934
|
+
'compiled_at: "2026-05-10T00:00:00.000Z"',
|
|
935
|
+
'source_repo: "provider-origin"',
|
|
936
|
+
'source_commit: "provider-commit"',
|
|
937
|
+
'source_paths:',
|
|
938
|
+
' - "src/provider-a.ts"',
|
|
939
|
+
' - "src/provider-b.ts"',
|
|
940
|
+
'page_state: "generated"',
|
|
941
|
+
'custom_field: "keep-me"',
|
|
942
|
+
'---',
|
|
943
|
+
'',
|
|
944
|
+
'# Auth',
|
|
945
|
+
'',
|
|
946
|
+
'Provider generated body.',
|
|
947
|
+
''
|
|
948
|
+
].join('\n')
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
try {
|
|
953
|
+
await compileWiki({ scanDir, planFile, wikiDir, config, _provider: blockListProvider });
|
|
954
|
+
const modulePage = await fs.readFile(path.join(wikiDir, 'Module-Auth.md'), 'utf8');
|
|
955
|
+
const frontmatterBlock = modulePage.slice(0, modulePage.indexOf('\n---', 4));
|
|
956
|
+
assert.match(modulePage, /source_repo: "origin"/);
|
|
957
|
+
assert.match(modulePage, /source_commit: "llm-test-commit"/);
|
|
958
|
+
assert.match(modulePage, /page_state: "generated"/);
|
|
959
|
+
assert.match(modulePage, /source_paths: \["src\/auth\.ts"\]/);
|
|
960
|
+
assert.match(modulePage, /custom_field: "keep-me"/);
|
|
961
|
+
assert.doesNotMatch(frontmatterBlock, /^\s+- "src\/provider-[ab]\.ts"/m);
|
|
962
|
+
assert.match(modulePage, /Provider generated body/);
|
|
963
|
+
}
|
|
964
|
+
finally {
|
|
965
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
test('compileWiki in LLM mode normalizes docs-only module evidence conservatively', async () => {
|
|
969
|
+
const manifest = {
|
|
970
|
+
remote: 'origin',
|
|
971
|
+
commit: 'docs-only-commit',
|
|
972
|
+
mode: 'bootstrap',
|
|
973
|
+
totals: { languages: { Markdown: 2, JSON: 1 }, categories: { docs: 2, package: 1 }, runtime_hints: {} },
|
|
974
|
+
files: [
|
|
975
|
+
{ path: 'package.json', category: 'package', language: 'JSON', imports: [], runtime_hints: [], reasons: ['package'] },
|
|
976
|
+
{ path: 'README.md', category: 'docs', language: 'Markdown', imports: [], runtime_hints: [], reasons: ['docs', 'readme'] },
|
|
977
|
+
{ path: 'docs/operations.md', category: 'docs', language: 'Markdown', imports: [], runtime_hints: [], reasons: ['docs'] }
|
|
978
|
+
],
|
|
979
|
+
documentation: {
|
|
980
|
+
enabled: true,
|
|
981
|
+
authority: 'secondary',
|
|
982
|
+
summary: { files: 2, claims: 1, stale: 0, commands: 0, env_vars: 0, file_paths: 0 },
|
|
983
|
+
files: [
|
|
984
|
+
{ path: 'README.md', status: 'unvalidated', authority: 'secondary', stale: false, claims: [{ text: 'The docs describe usage.' }] },
|
|
985
|
+
{ path: 'docs/operations.md', status: 'unvalidated', authority: 'secondary', stale: false, claims: [{ text: 'The docs describe operations.' }] }
|
|
986
|
+
]
|
|
987
|
+
},
|
|
988
|
+
analysis: { package_scripts: [], dependency_graph: { edges: [], summary: {} }, test_to_source: { mappings: [], summary: {} } }
|
|
989
|
+
};
|
|
990
|
+
const plan = {
|
|
991
|
+
pages: createPlan().pages,
|
|
992
|
+
modules: [
|
|
993
|
+
{
|
|
994
|
+
slug: 'Documentation',
|
|
995
|
+
name: 'Documentation',
|
|
996
|
+
files: ['README.md', 'docs/operations.md'],
|
|
997
|
+
categories: { docs: 2 },
|
|
998
|
+
languages: { Markdown: 2 },
|
|
999
|
+
runtime_hints: {},
|
|
1000
|
+
important_reasons: ['docs', 'readme']
|
|
1001
|
+
}
|
|
1002
|
+
]
|
|
1003
|
+
};
|
|
1004
|
+
const provider = {
|
|
1005
|
+
name: 'docs-only-mock',
|
|
1006
|
+
async complete(req) {
|
|
1007
|
+
if (req.archetype === 'architecture') {
|
|
1008
|
+
return { provider: 'docs-only-mock', content: validLLMTestContent(req) };
|
|
1009
|
+
}
|
|
1010
|
+
return {
|
|
1011
|
+
provider: 'docs-only-mock',
|
|
1012
|
+
content: [
|
|
1013
|
+
'---',
|
|
1014
|
+
'kind: "module"',
|
|
1015
|
+
'compiled_at: "2026-05-10T00:00:00.000Z"',
|
|
1016
|
+
'source_repo: "provider-origin"',
|
|
1017
|
+
'source_commit: "provider-commit"',
|
|
1018
|
+
'source_paths: ["README.md", "docs/operations.md"]',
|
|
1019
|
+
'page_state: "generated"',
|
|
1020
|
+
'confidence: "high"',
|
|
1021
|
+
'claim_status: "source-grounded"',
|
|
1022
|
+
'---',
|
|
1023
|
+
'',
|
|
1024
|
+
'# Documentation',
|
|
1025
|
+
'',
|
|
1026
|
+
'This page summarizes operational behavior from the documentation set.',
|
|
1027
|
+
'',
|
|
1028
|
+
'<!-- HUMAN_NOTES_START -->',
|
|
1029
|
+
'<!-- HUMAN_NOTES_END -->',
|
|
1030
|
+
''
|
|
1031
|
+
].join('\n')
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest, plan });
|
|
1036
|
+
const config = { compiler: { mode: 'llm' } };
|
|
1037
|
+
try {
|
|
1038
|
+
await compileWiki({ scanDir, planFile, wikiDir, config, _provider: provider });
|
|
1039
|
+
const modulePage = await fs.readFile(path.join(wikiDir, 'Documentation.md'), 'utf8');
|
|
1040
|
+
assert.match(modulePage, /source_paths: \["README\.md","docs\/operations\.md"\]/);
|
|
1041
|
+
assert.match(modulePage, /claim_status: "review-needed"/);
|
|
1042
|
+
assert.match(modulePage, /confidence: "low"/);
|
|
1043
|
+
assert.doesNotMatch(modulePage, /claim_status: "source-grounded"/);
|
|
1044
|
+
assert.doesNotMatch(modulePage, /confidence: "high"/);
|
|
1045
|
+
assert.match(modulePage, /markdown documentation is secondary evidence/i);
|
|
1046
|
+
assert.match(modulePage, /validated against source code, tests, CI workflows, runtime configuration, or schemas/i);
|
|
1047
|
+
const lint = await lintWiki({ wikiDir, scanDir });
|
|
1048
|
+
const moduleProvenanceWarning = lint.issues.find((issue) => issue.code === 'missing-source-provenance' && issue.message.includes('Documentation.md'));
|
|
1049
|
+
const moduleSourcePathsWarning = lint.issues.find((issue) => issue.code === 'missing-source-paths' && issue.message.includes('Documentation.md'));
|
|
1050
|
+
assert.equal(moduleProvenanceWarning, undefined);
|
|
1051
|
+
assert.equal(moduleSourcePathsWarning, undefined);
|
|
1052
|
+
}
|
|
1053
|
+
finally {
|
|
1054
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
test('compileWiki in LLM mode does not overwrite existing page when provider output is invalid', async () => {
|
|
1058
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest: defaultLLMManifest, plan: createLLMPlan() });
|
|
1059
|
+
const config = { compiler: { mode: 'llm' } };
|
|
1060
|
+
// Create a pre-existing "generated" module page that should be preserved on failure.
|
|
1061
|
+
await fs.mkdir(wikiDir, { recursive: true });
|
|
1062
|
+
const existingContent = [
|
|
1063
|
+
'---',
|
|
1064
|
+
'source_commit: "old-commit"',
|
|
1065
|
+
'kind: "module"',
|
|
1066
|
+
'compiled_at: "2024-01-01T00:00:00Z"',
|
|
1067
|
+
'source_paths: []',
|
|
1068
|
+
'page_state: "generated"',
|
|
1069
|
+
'---',
|
|
1070
|
+
'',
|
|
1071
|
+
'# Auth',
|
|
1072
|
+
'',
|
|
1073
|
+
'Old deterministic content.',
|
|
1074
|
+
''
|
|
1075
|
+
].join('\n');
|
|
1076
|
+
await fs.writeFile(path.join(wikiDir, 'Module-Auth.md'), existingContent, 'utf8');
|
|
1077
|
+
// Provider that returns content that fails wiki patch validation (no frontmatter).
|
|
1078
|
+
const badProvider = {
|
|
1079
|
+
name: 'bad-mock',
|
|
1080
|
+
async complete(_request) {
|
|
1081
|
+
return { content: 'just prose, no frontmatter block', provider: 'bad-mock' };
|
|
1082
|
+
}
|
|
1083
|
+
};
|
|
1084
|
+
try {
|
|
1085
|
+
await assert.rejects(() => compileWiki({ scanDir, planFile, wikiDir, config, _provider: badProvider }), /LLM compilation failed.*Module-Auth\.md/s);
|
|
1086
|
+
// Existing page must be preserved byte-for-byte, and fail-fast semantics
|
|
1087
|
+
// must prevent partial writes of deterministic pages from the same run.
|
|
1088
|
+
const afterCompile = await fs.readFile(path.join(wikiDir, 'Module-Auth.md'), 'utf8');
|
|
1089
|
+
assert.equal(afterCompile, existingContent);
|
|
1090
|
+
await assert.rejects(() => fs.readFile(path.join(wikiDir, 'Home.md'), 'utf8'), (error) => error?.code === 'ENOENT');
|
|
1091
|
+
}
|
|
1092
|
+
finally {
|
|
1093
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1096
|
+
test('compileWiki uses validation retries independently from provider transport retries', async () => {
|
|
1097
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest: defaultLLMManifest, plan: createLLMPlan() });
|
|
1098
|
+
let calls = 0;
|
|
1099
|
+
const provider = {
|
|
1100
|
+
name: 'validation-retry-mock',
|
|
1101
|
+
async complete(req) {
|
|
1102
|
+
calls += 1;
|
|
1103
|
+
if (calls === 1) {
|
|
1104
|
+
return { content: '# Invalid - no frontmatter', provider: 'validation-retry-mock' };
|
|
1105
|
+
}
|
|
1106
|
+
if (req.archetype === 'architecture') {
|
|
1107
|
+
return { provider: 'validation-retry-mock', content: validLLMTestContent(req) };
|
|
1108
|
+
}
|
|
1109
|
+
return {
|
|
1110
|
+
provider: 'validation-retry-mock',
|
|
1111
|
+
content: [
|
|
1112
|
+
'---',
|
|
1113
|
+
'kind: "module"',
|
|
1114
|
+
'compiled_at: "2026-05-10T00:00:00.000Z"',
|
|
1115
|
+
'source_repo: "provider-origin"',
|
|
1116
|
+
'source_commit: "provider-commit"',
|
|
1117
|
+
'source_paths: ["src/auth.ts"]',
|
|
1118
|
+
'page_state: "generated"',
|
|
1119
|
+
'---',
|
|
1120
|
+
'',
|
|
1121
|
+
'# Auth',
|
|
1122
|
+
'',
|
|
1123
|
+
'Valid retry output.',
|
|
1124
|
+
''
|
|
1125
|
+
].join('\n')
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
try {
|
|
1130
|
+
await compileWiki({
|
|
1131
|
+
scanDir,
|
|
1132
|
+
planFile,
|
|
1133
|
+
wikiDir,
|
|
1134
|
+
config: { compiler: { mode: 'llm', llm: { provider: 'mock', retries: 0, validation_retries: 1 } } },
|
|
1135
|
+
_provider: provider
|
|
1136
|
+
});
|
|
1137
|
+
const modulePage = await fs.readFile(path.join(wikiDir, 'Module-Auth.md'), 'utf8');
|
|
1138
|
+
// calls: 1 invalid (module) + 1 valid (module retry) + 1 valid (architecture) = 3
|
|
1139
|
+
assert.equal(calls, 3);
|
|
1140
|
+
assert.match(modulePage, /Valid retry output/);
|
|
1141
|
+
}
|
|
1142
|
+
finally {
|
|
1143
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
test('compileWiki does not use provider transport retries for validation correction', async () => {
|
|
1147
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest: defaultLLMManifest, plan: createLLMPlan() });
|
|
1148
|
+
let calls = 0;
|
|
1149
|
+
const provider = {
|
|
1150
|
+
name: 'invalid-once-mock',
|
|
1151
|
+
async complete(_request) {
|
|
1152
|
+
calls += 1;
|
|
1153
|
+
return { content: '# Invalid - no frontmatter', provider: 'invalid-once-mock' };
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
try {
|
|
1157
|
+
await assert.rejects(() => compileWiki({
|
|
1158
|
+
scanDir,
|
|
1159
|
+
planFile,
|
|
1160
|
+
wikiDir,
|
|
1161
|
+
config: { compiler: { mode: 'llm', llm: { provider: 'mock', retries: 5, validation_retries: 0 } } },
|
|
1162
|
+
_provider: provider
|
|
1163
|
+
}), /LLM compilation failed.*Module-Auth\.md/s);
|
|
1164
|
+
// calls: 1 module attempt (fails, no retries) + 1 architecture attempt (fails, no retries) = 2
|
|
1165
|
+
assert.equal(calls, 2);
|
|
1166
|
+
}
|
|
1167
|
+
finally {
|
|
1168
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
test('compileWiki in LLM mode preserves human notes on successful synthesis', async () => {
|
|
1172
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest: defaultLLMManifest, plan: createLLMPlan() });
|
|
1173
|
+
const config = { compiler: { mode: 'llm' } };
|
|
1174
|
+
try {
|
|
1175
|
+
// First compile in deterministic mode to get a baseline page with HUMAN_NOTES markers.
|
|
1176
|
+
await compileWiki({ scanDir, planFile, wikiDir });
|
|
1177
|
+
const firstPage = await fs.readFile(path.join(wikiDir, 'Module-Auth.md'), 'utf8');
|
|
1178
|
+
assert.match(firstPage, /HUMAN_NOTES_START/);
|
|
1179
|
+
// Simulate a human adding notes.
|
|
1180
|
+
const humanNotes = '\n## Custom auth notes\n\nThis was written by a human.\n';
|
|
1181
|
+
const pageWithNotes = firstPage.replace('<!-- HUMAN_NOTES_START -->\n<!-- HUMAN_NOTES_END -->', `<!-- HUMAN_NOTES_START -->${humanNotes}<!-- HUMAN_NOTES_END -->`);
|
|
1182
|
+
await fs.writeFile(path.join(wikiDir, 'Module-Auth.md'), pageWithNotes, 'utf8');
|
|
1183
|
+
// Recompile in LLM mode using the mock provider.
|
|
1184
|
+
await compileWiki({ scanDir, planFile, wikiDir, config, _provider: new MockLLMProvider() });
|
|
1185
|
+
const afterLLM = await fs.readFile(path.join(wikiDir, 'Module-Auth.md'), 'utf8');
|
|
1186
|
+
// Human notes must be preserved.
|
|
1187
|
+
assert.equal(extractHumanNotes(afterLLM), humanNotes);
|
|
1188
|
+
// Page state must be "mixed" because human notes are non-empty.
|
|
1189
|
+
assert.match(afterLLM, /page_state: "mixed"/);
|
|
1190
|
+
// LLM content (mock output) must be present.
|
|
1191
|
+
assert.match(afterLLM, /Generated by the mock LLM provider/);
|
|
1192
|
+
}
|
|
1193
|
+
finally {
|
|
1194
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
1195
|
+
}
|
|
1196
|
+
});
|
|
1197
|
+
test('compileWiki in LLM mode does not overwrite human-owned module pages', async () => {
|
|
1198
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest: defaultLLMManifest, plan: createLLMPlan() });
|
|
1199
|
+
const config = { compiler: { mode: 'llm' } };
|
|
1200
|
+
// Create a human-owned module page.
|
|
1201
|
+
await fs.mkdir(wikiDir, { recursive: true });
|
|
1202
|
+
const humanOwnedContent = [
|
|
1203
|
+
'---',
|
|
1204
|
+
'source_commit: "hand-written"',
|
|
1205
|
+
'kind: "module"',
|
|
1206
|
+
'compiled_at: "2024-01-01T00:00:00Z"',
|
|
1207
|
+
'source_paths: []',
|
|
1208
|
+
'page_state: "human-owned"',
|
|
1209
|
+
'---',
|
|
1210
|
+
'',
|
|
1211
|
+
'# Auth',
|
|
1212
|
+
'',
|
|
1213
|
+
'Human-maintained content.',
|
|
1214
|
+
''
|
|
1215
|
+
].join('\n');
|
|
1216
|
+
await fs.writeFile(path.join(wikiDir, 'Module-Auth.md'), humanOwnedContent, 'utf8');
|
|
1217
|
+
try {
|
|
1218
|
+
const result = await compileWiki({ scanDir, planFile, wikiDir, config, _provider: new MockLLMProvider() });
|
|
1219
|
+
const afterCompile = await fs.readFile(path.join(wikiDir, 'Module-Auth.md'), 'utf8');
|
|
1220
|
+
assert.equal(afterCompile, humanOwnedContent);
|
|
1221
|
+
assert.equal(result.summary.skipped, 1);
|
|
1222
|
+
assert.equal(result.summary.skipped_by_state['human-owned'], 1);
|
|
1223
|
+
// No errors – human-owned skip is expected behavior, not an LLM failure.
|
|
1224
|
+
}
|
|
1225
|
+
finally {
|
|
1226
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
1227
|
+
}
|
|
1228
|
+
});
|
|
1229
|
+
test('compileWiki honors LLMWIKI_COMPILER_MODE=llm from the environment', async () => {
|
|
1230
|
+
const previousMode = process.env.LLMWIKI_COMPILER_MODE;
|
|
1231
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest: defaultLLMManifest, plan: createLLMPlan() });
|
|
1232
|
+
try {
|
|
1233
|
+
process.env.LLMWIKI_COMPILER_MODE = 'llm';
|
|
1234
|
+
await compileWiki({
|
|
1235
|
+
scanDir,
|
|
1236
|
+
planFile,
|
|
1237
|
+
wikiDir,
|
|
1238
|
+
config: { compiler: { mode: 'deterministic' } },
|
|
1239
|
+
_provider: new MockLLMProvider()
|
|
1240
|
+
});
|
|
1241
|
+
const modulePage = await fs.readFile(path.join(wikiDir, 'Module-Auth.md'), 'utf8');
|
|
1242
|
+
assert.match(modulePage, /Generated by the mock LLM provider/);
|
|
1243
|
+
assert.match(modulePage, /source_commit: "llm-test-commit"/);
|
|
1244
|
+
assert.match(modulePage, /page_state: "generated"/);
|
|
1245
|
+
assert.match(modulePage, /source_paths: \["src\/auth\.ts"\]/);
|
|
1246
|
+
}
|
|
1247
|
+
finally {
|
|
1248
|
+
if (previousMode === undefined) {
|
|
1249
|
+
delete process.env.LLMWIKI_COMPILER_MODE;
|
|
1250
|
+
}
|
|
1251
|
+
else {
|
|
1252
|
+
process.env.LLMWIKI_COMPILER_MODE = previousMode;
|
|
1253
|
+
}
|
|
1254
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
1255
|
+
}
|
|
1256
|
+
});
|
|
1257
|
+
test('compileWiki in LLM mode honors explicit mock provider config without API key', async () => {
|
|
1258
|
+
const previousProvider = process.env.LLMWIKI_LLM_PROVIDER;
|
|
1259
|
+
const previousKey = process.env.LLMWIKI_LLM_API_KEY;
|
|
1260
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest: defaultLLMManifest, plan: createLLMPlan() });
|
|
1261
|
+
try {
|
|
1262
|
+
delete process.env.LLMWIKI_LLM_PROVIDER;
|
|
1263
|
+
delete process.env.LLMWIKI_LLM_API_KEY;
|
|
1264
|
+
const result = await compileWiki({
|
|
1265
|
+
scanDir,
|
|
1266
|
+
planFile,
|
|
1267
|
+
wikiDir,
|
|
1268
|
+
config: { compiler: { mode: 'llm', llm: { provider: 'mock' } } }
|
|
1269
|
+
});
|
|
1270
|
+
const modulePage = await fs.readFile(path.join(wikiDir, 'Module-Auth.md'), 'utf8');
|
|
1271
|
+
assert.equal(result.summary.compiler_mode, 'llm');
|
|
1272
|
+
// Module-Auth + Architecture.md are both synthesized via LLM.
|
|
1273
|
+
assert.equal(result.summary.llm_pages, 2);
|
|
1274
|
+
assert.match(modulePage, /Generated by the mock LLM provider/);
|
|
1275
|
+
}
|
|
1276
|
+
finally {
|
|
1277
|
+
if (previousProvider === undefined) {
|
|
1278
|
+
delete process.env.LLMWIKI_LLM_PROVIDER;
|
|
1279
|
+
}
|
|
1280
|
+
else {
|
|
1281
|
+
process.env.LLMWIKI_LLM_PROVIDER = previousProvider;
|
|
1282
|
+
}
|
|
1283
|
+
if (previousKey === undefined) {
|
|
1284
|
+
delete process.env.LLMWIKI_LLM_API_KEY;
|
|
1285
|
+
}
|
|
1286
|
+
else {
|
|
1287
|
+
process.env.LLMWIKI_LLM_API_KEY = previousKey;
|
|
1288
|
+
}
|
|
1289
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
test('compileWiki in hosted LLM mode fails when the configured API key is missing', async () => {
|
|
1293
|
+
const previousMode = process.env.LLMWIKI_COMPILER_MODE;
|
|
1294
|
+
const previousKey = process.env.REPO_WIKI_MISSING_TEST_KEY;
|
|
1295
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest: defaultLLMManifest, plan: createLLMPlan() });
|
|
1296
|
+
try {
|
|
1297
|
+
delete process.env.LLMWIKI_COMPILER_MODE;
|
|
1298
|
+
delete process.env.REPO_WIKI_MISSING_TEST_KEY;
|
|
1299
|
+
await assert.rejects(() => compileWiki({
|
|
1300
|
+
scanDir,
|
|
1301
|
+
planFile,
|
|
1302
|
+
wikiDir,
|
|
1303
|
+
config: { compiler: { mode: 'llm', llm: { provider: 'openai-compatible', api_key_env: 'REPO_WIKI_MISSING_TEST_KEY' } } }
|
|
1304
|
+
}), /requires an API key/);
|
|
1305
|
+
}
|
|
1306
|
+
finally {
|
|
1307
|
+
if (previousMode === undefined) {
|
|
1308
|
+
delete process.env.LLMWIKI_COMPILER_MODE;
|
|
1309
|
+
}
|
|
1310
|
+
else {
|
|
1311
|
+
process.env.LLMWIKI_COMPILER_MODE = previousMode;
|
|
1312
|
+
}
|
|
1313
|
+
if (previousKey === undefined) {
|
|
1314
|
+
delete process.env.REPO_WIKI_MISSING_TEST_KEY;
|
|
1315
|
+
}
|
|
1316
|
+
else {
|
|
1317
|
+
process.env.REPO_WIKI_MISSING_TEST_KEY = previousKey;
|
|
1318
|
+
}
|
|
1319
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
test('compileWiki skips human-owned and unmanaged module pages before creating an LLM provider', async () => {
|
|
1323
|
+
const plan = {
|
|
1324
|
+
pages: createPlan().pages,
|
|
1325
|
+
modules: [
|
|
1326
|
+
{ ...createLLMPlan().modules[0], slug: 'Module-Human', name: 'Human' },
|
|
1327
|
+
{ ...createLLMPlan().modules[0], slug: 'Module-Unmanaged', name: 'Unmanaged' }
|
|
1328
|
+
]
|
|
1329
|
+
};
|
|
1330
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest: defaultLLMManifest, plan });
|
|
1331
|
+
try {
|
|
1332
|
+
await fs.mkdir(wikiDir, { recursive: true });
|
|
1333
|
+
const humanOwned = [
|
|
1334
|
+
'---',
|
|
1335
|
+
'source_commit: "old"',
|
|
1336
|
+
'kind: "module"',
|
|
1337
|
+
'compiled_at: "2024-01-01T00:00:00Z"',
|
|
1338
|
+
'source_paths: ["src/auth.ts"]',
|
|
1339
|
+
'page_state: "human-owned"',
|
|
1340
|
+
'---',
|
|
1341
|
+
'',
|
|
1342
|
+
'# Human page',
|
|
1343
|
+
''
|
|
1344
|
+
].join('\n');
|
|
1345
|
+
const unmanaged = '# Unmanaged page\n\nHuman content.\n';
|
|
1346
|
+
const humanOwnedArch = [
|
|
1347
|
+
'---',
|
|
1348
|
+
'source_commit: "old"',
|
|
1349
|
+
'kind: "architecture"',
|
|
1350
|
+
'compiled_at: "2024-01-01T00:00:00Z"',
|
|
1351
|
+
'source_paths: ["src/auth.ts"]',
|
|
1352
|
+
'page_state: "human-owned"',
|
|
1353
|
+
'---',
|
|
1354
|
+
'',
|
|
1355
|
+
'# Architecture',
|
|
1356
|
+
'',
|
|
1357
|
+
'Human-maintained architecture page.',
|
|
1358
|
+
''
|
|
1359
|
+
].join('\n');
|
|
1360
|
+
await fs.writeFile(path.join(wikiDir, 'Module-Human.md'), humanOwned, 'utf8');
|
|
1361
|
+
await fs.writeFile(path.join(wikiDir, 'Module-Unmanaged.md'), unmanaged, 'utf8');
|
|
1362
|
+
await fs.writeFile(path.join(wikiDir, 'Architecture.md'), humanOwnedArch, 'utf8');
|
|
1363
|
+
const result = await compileWiki({
|
|
1364
|
+
scanDir,
|
|
1365
|
+
planFile,
|
|
1366
|
+
wikiDir,
|
|
1367
|
+
config: { compiler: { mode: 'llm', llm: { provider: 'openai-compatible', api_key_env: 'REPO_WIKI_MISSING_TEST_KEY' } } }
|
|
1368
|
+
});
|
|
1369
|
+
assert.equal(await fs.readFile(path.join(wikiDir, 'Module-Human.md'), 'utf8'), humanOwned);
|
|
1370
|
+
assert.equal(await fs.readFile(path.join(wikiDir, 'Module-Unmanaged.md'), 'utf8'), unmanaged);
|
|
1371
|
+
assert.equal(await fs.readFile(path.join(wikiDir, 'Architecture.md'), 'utf8'), humanOwnedArch);
|
|
1372
|
+
assert.equal(result.summary.skipped_by_state['human-owned'], 2);
|
|
1373
|
+
assert.equal(result.summary.skipped_by_state.unmanaged, 1);
|
|
1374
|
+
}
|
|
1375
|
+
finally {
|
|
1376
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
1377
|
+
}
|
|
1378
|
+
});
|
|
1379
|
+
test('compileWiki deterministic mode is unaffected by config presence', async () => {
|
|
1380
|
+
const manifest = {
|
|
1381
|
+
remote: 'origin',
|
|
1382
|
+
commit: 'abc123',
|
|
1383
|
+
mode: 'bootstrap',
|
|
1384
|
+
totals: { languages: { TypeScript: 1 }, categories: { source: 1 }, runtime_hints: {} },
|
|
1385
|
+
files: [{ path: 'src/core.ts', category: 'source', language: 'TypeScript', imports: [], runtime_hints: [], reasons: ['source'] }],
|
|
1386
|
+
analysis: { package_scripts: [], dependency_graph: { edges: [], summary: {} }, test_to_source: { mappings: [], summary: {} } }
|
|
1387
|
+
};
|
|
1388
|
+
const plan = {
|
|
1389
|
+
pages: createPlan().pages,
|
|
1390
|
+
modules: [
|
|
1391
|
+
{
|
|
1392
|
+
slug: 'Module-Core',
|
|
1393
|
+
name: 'Core',
|
|
1394
|
+
files: ['src/core.ts'],
|
|
1395
|
+
categories: { source: 1 },
|
|
1396
|
+
languages: { TypeScript: 1 },
|
|
1397
|
+
runtime_hints: {},
|
|
1398
|
+
important_reasons: ['source']
|
|
1399
|
+
}
|
|
1400
|
+
]
|
|
1401
|
+
};
|
|
1402
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest, plan });
|
|
1403
|
+
try {
|
|
1404
|
+
// Explicit deterministic mode config
|
|
1405
|
+
const result = await compileWiki({
|
|
1406
|
+
scanDir,
|
|
1407
|
+
planFile,
|
|
1408
|
+
wikiDir,
|
|
1409
|
+
config: { compiler: { mode: 'deterministic' } }
|
|
1410
|
+
});
|
|
1411
|
+
const modulePage = await fs.readFile(path.join(wikiDir, 'Module-Core.md'), 'utf8');
|
|
1412
|
+
// Deterministic renderer output (not mock LLM output)
|
|
1413
|
+
assert.match(modulePage, /source_paths: \["src\/core\.ts"\]/);
|
|
1414
|
+
assert.doesNotMatch(modulePage, /Generated by the mock LLM provider/);
|
|
1415
|
+
// Summary must report deterministic mode and zero LLM-generated pages.
|
|
1416
|
+
assert.equal(result.summary.compiler_mode, 'deterministic');
|
|
1417
|
+
assert.equal(result.summary.llm_pages, 0);
|
|
1418
|
+
assert.ok(result.summary.deterministic_pages >= 1);
|
|
1419
|
+
}
|
|
1420
|
+
finally {
|
|
1421
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
1422
|
+
}
|
|
1423
|
+
});
|
|
1424
|
+
// ---------------------------------------------------------------------------
|
|
1425
|
+
// Architecture LLM synthesis
|
|
1426
|
+
// ---------------------------------------------------------------------------
|
|
1427
|
+
test('compileWiki in LLM mode synthesizes Architecture.md through the mock provider', async () => {
|
|
1428
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest: defaultLLMManifest, plan: createLLMPlan() });
|
|
1429
|
+
const config = { compiler: { mode: 'llm' } };
|
|
1430
|
+
try {
|
|
1431
|
+
const result = await compileWiki({ scanDir, planFile, wikiDir, config, _provider: new MockLLMProvider() });
|
|
1432
|
+
const archPage = await fs.readFile(path.join(wikiDir, 'Architecture.md'), 'utf8');
|
|
1433
|
+
// Architecture page must come from the LLM provider (mock output marker).
|
|
1434
|
+
assert.match(archPage, /Generated by the mock LLM provider/);
|
|
1435
|
+
assert.match(archPage, /kind: "architecture"/);
|
|
1436
|
+
assert.match(archPage, /page_state: "generated"/);
|
|
1437
|
+
assert.match(archPage, /source_commit: "llm-test-commit"/);
|
|
1438
|
+
// Architecture is counted as an LLM page together with the module page.
|
|
1439
|
+
assert.equal(result.summary.llm_pages, 2);
|
|
1440
|
+
}
|
|
1441
|
+
finally {
|
|
1442
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
1443
|
+
}
|
|
1444
|
+
});
|
|
1445
|
+
test('compileWiki in deterministic mode renders Architecture.md without LLM synthesis', async () => {
|
|
1446
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest: defaultLLMManifest, plan: createLLMPlan() });
|
|
1447
|
+
try {
|
|
1448
|
+
const result = await compileWiki({ scanDir, planFile, wikiDir });
|
|
1449
|
+
const archPage = await fs.readFile(path.join(wikiDir, 'Architecture.md'), 'utf8');
|
|
1450
|
+
// Deterministic renderer output — not from the LLM.
|
|
1451
|
+
assert.doesNotMatch(archPage, /Generated by the mock LLM provider/);
|
|
1452
|
+
assert.match(archPage, /# Architecture/);
|
|
1453
|
+
assert.match(archPage, /first-pass architecture summary/);
|
|
1454
|
+
assert.equal(result.summary.llm_pages, 0);
|
|
1455
|
+
assert.equal(result.summary.compiler_mode, 'deterministic');
|
|
1456
|
+
}
|
|
1457
|
+
finally {
|
|
1458
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
1459
|
+
}
|
|
1460
|
+
});
|
|
1461
|
+
test('compileWiki in LLM mode reports architecture_decision skipped for human-owned Architecture.md', async () => {
|
|
1462
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest: defaultLLMManifest, plan: createLLMPlan() });
|
|
1463
|
+
const config = { compiler: { mode: 'llm' } };
|
|
1464
|
+
const humanOwnedArch = [
|
|
1465
|
+
'---',
|
|
1466
|
+
'source_repo: "origin"',
|
|
1467
|
+
'source_commit: "abc123"',
|
|
1468
|
+
'page_state: "human-owned"',
|
|
1469
|
+
'kind: "architecture"',
|
|
1470
|
+
'---',
|
|
1471
|
+
'# Architecture',
|
|
1472
|
+
'',
|
|
1473
|
+
'Human-owned architecture page.'
|
|
1474
|
+
].join('\n');
|
|
1475
|
+
await fs.mkdir(wikiDir, { recursive: true });
|
|
1476
|
+
await fs.writeFile(path.join(wikiDir, 'Architecture.md'), humanOwnedArch, 'utf8');
|
|
1477
|
+
try {
|
|
1478
|
+
const result = await compileWiki({ scanDir, planFile, wikiDir, config, _provider: new MockLLMProvider() });
|
|
1479
|
+
assert.equal(result.summary.architecture_decision, 'skipped');
|
|
1480
|
+
assert.equal(await fs.readFile(path.join(wikiDir, 'Architecture.md'), 'utf8'), humanOwnedArch);
|
|
1481
|
+
}
|
|
1482
|
+
finally {
|
|
1483
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
1484
|
+
}
|
|
1485
|
+
});
|
|
1486
|
+
test('compileWiki in LLM mode skips human-owned Architecture.md without overwriting', async () => {
|
|
1487
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest: defaultLLMManifest, plan: createLLMPlan() });
|
|
1488
|
+
const config = { compiler: { mode: 'llm' } };
|
|
1489
|
+
const humanOwnedArch = [
|
|
1490
|
+
'---',
|
|
1491
|
+
'source_commit: "hand-written"',
|
|
1492
|
+
'kind: "architecture"',
|
|
1493
|
+
'compiled_at: "2024-01-01T00:00:00Z"',
|
|
1494
|
+
'source_paths: ["src/auth.ts"]',
|
|
1495
|
+
'page_state: "human-owned"',
|
|
1496
|
+
'---',
|
|
1497
|
+
'',
|
|
1498
|
+
'# Architecture',
|
|
1499
|
+
'',
|
|
1500
|
+
'Human-maintained architecture page.',
|
|
1501
|
+
''
|
|
1502
|
+
].join('\n');
|
|
1503
|
+
await fs.mkdir(wikiDir, { recursive: true });
|
|
1504
|
+
await fs.writeFile(path.join(wikiDir, 'Architecture.md'), humanOwnedArch, 'utf8');
|
|
1505
|
+
try {
|
|
1506
|
+
const result = await compileWiki({ scanDir, planFile, wikiDir, config, _provider: new MockLLMProvider() });
|
|
1507
|
+
const afterCompile = await fs.readFile(path.join(wikiDir, 'Architecture.md'), 'utf8');
|
|
1508
|
+
assert.equal(afterCompile, humanOwnedArch);
|
|
1509
|
+
assert.equal(result.summary.skipped_by_state['human-owned'], 1);
|
|
1510
|
+
}
|
|
1511
|
+
finally {
|
|
1512
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
1513
|
+
}
|
|
1514
|
+
});
|
|
1515
|
+
test('compileWiki in LLM mode uses architecture archetype for Architecture.md prompt', async () => {
|
|
1516
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest: defaultLLMManifest, plan: createLLMPlan() });
|
|
1517
|
+
const archetypes = [];
|
|
1518
|
+
const capturingProvider = {
|
|
1519
|
+
name: 'archetype-capturing-mock',
|
|
1520
|
+
async complete(req) {
|
|
1521
|
+
archetypes.push(req.archetype);
|
|
1522
|
+
return {
|
|
1523
|
+
provider: 'archetype-capturing-mock',
|
|
1524
|
+
content: validLLMTestContent(req)
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
};
|
|
1528
|
+
try {
|
|
1529
|
+
await compileWiki({
|
|
1530
|
+
scanDir,
|
|
1531
|
+
planFile,
|
|
1532
|
+
wikiDir,
|
|
1533
|
+
config: { compiler: { mode: 'llm' } },
|
|
1534
|
+
_provider: capturingProvider
|
|
1535
|
+
});
|
|
1536
|
+
assert.ok(archetypes.includes('architecture'), 'Architecture archetype must be used for Architecture.md');
|
|
1537
|
+
assert.ok(archetypes.includes('module'), 'Module archetype must be used for module pages');
|
|
1538
|
+
}
|
|
1539
|
+
finally {
|
|
1540
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1543
|
+
test('compileWiki in LLM mode keeps architecture-specific system prompt guardrails', async () => {
|
|
1544
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest: defaultLLMManifest, plan: createLLMPlan() });
|
|
1545
|
+
const capturedSystemPrompts = {};
|
|
1546
|
+
const capturingProvider = {
|
|
1547
|
+
name: 'system-prompt-capturing-mock',
|
|
1548
|
+
async complete(req) {
|
|
1549
|
+
capturedSystemPrompts[req.archetype] = req.systemPrompt;
|
|
1550
|
+
return {
|
|
1551
|
+
provider: 'system-prompt-capturing-mock',
|
|
1552
|
+
content: validLLMTestContent(req)
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
};
|
|
1556
|
+
try {
|
|
1557
|
+
await compileWiki({
|
|
1558
|
+
scanDir,
|
|
1559
|
+
planFile,
|
|
1560
|
+
wikiDir,
|
|
1561
|
+
config: { compiler: { mode: 'llm', llm: { system_prompt: 'Global custom system prompt.' } } },
|
|
1562
|
+
_provider: capturingProvider
|
|
1563
|
+
});
|
|
1564
|
+
assert.equal(capturedSystemPrompts.module, 'Global custom system prompt.');
|
|
1565
|
+
assert.match(capturedSystemPrompts.architecture, /Architecture synthesis rules:/);
|
|
1566
|
+
assert.match(capturedSystemPrompts.architecture, /Do not invent unsupported relationships or architectural layers/);
|
|
1567
|
+
}
|
|
1568
|
+
finally {
|
|
1569
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
1570
|
+
}
|
|
1571
|
+
});
|
|
1572
|
+
test('compileWiki in LLM mode normalizes Architecture.md source_paths from the prompt context', async () => {
|
|
1573
|
+
const manifest = {
|
|
1574
|
+
remote: 'origin',
|
|
1575
|
+
commit: 'architecture-budget-test',
|
|
1576
|
+
mode: 'bootstrap',
|
|
1577
|
+
totals: { languages: { TypeScript: 31 }, categories: { source: 31 }, runtime_hints: {} },
|
|
1578
|
+
files: [
|
|
1579
|
+
{
|
|
1580
|
+
path: 'src/auth.ts',
|
|
1581
|
+
category: 'source',
|
|
1582
|
+
language: 'TypeScript',
|
|
1583
|
+
imports: Array.from({ length: 20 }, (_, index) => `./dep-${index}.js`),
|
|
1584
|
+
exported_symbols: Array.from({ length: 20 }, (_, index) => ({ name: `authSymbol${index}${'X'.repeat(40)}` })),
|
|
1585
|
+
runtime_hints: [],
|
|
1586
|
+
reasons: ['source']
|
|
1587
|
+
},
|
|
1588
|
+
...Array.from({ length: 30 }, (_, index) => ({
|
|
1589
|
+
path: `src/feature-${String(index).padStart(2, '0')}.ts`,
|
|
1590
|
+
category: 'source',
|
|
1591
|
+
language: 'TypeScript',
|
|
1592
|
+
imports: Array.from({ length: 20 }, (_, depIndex) => `./feature-${index}-dep-${depIndex}.js`),
|
|
1593
|
+
exported_symbols: Array.from({ length: 20 }, (_, symbolIndex) => ({ name: `feature${index}Symbol${symbolIndex}${'Y'.repeat(40)}` })),
|
|
1594
|
+
runtime_hints: [],
|
|
1595
|
+
reasons: ['source']
|
|
1596
|
+
}))
|
|
1597
|
+
],
|
|
1598
|
+
analysis: { package_scripts: [], dependency_graph: { edges: [], summary: {} }, test_to_source: { mappings: [], summary: {} } }
|
|
1599
|
+
};
|
|
1600
|
+
const plan = createLLMPlan();
|
|
1601
|
+
let capturedArchitectureSourcePaths = [];
|
|
1602
|
+
const capturingProvider = {
|
|
1603
|
+
name: 'source-path-capturing-mock',
|
|
1604
|
+
async complete(req) {
|
|
1605
|
+
if (req.archetype === 'architecture') {
|
|
1606
|
+
capturedArchitectureSourcePaths = req.sourcePaths ?? [];
|
|
1607
|
+
}
|
|
1608
|
+
return {
|
|
1609
|
+
provider: 'source-path-capturing-mock',
|
|
1610
|
+
content: validLLMTestContent(req)
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
};
|
|
1614
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest, plan });
|
|
1615
|
+
try {
|
|
1616
|
+
await compileWiki({
|
|
1617
|
+
scanDir,
|
|
1618
|
+
planFile,
|
|
1619
|
+
wikiDir,
|
|
1620
|
+
config: { compiler: { mode: 'llm' } },
|
|
1621
|
+
_provider: capturingProvider
|
|
1622
|
+
});
|
|
1623
|
+
assert.ok(capturedArchitectureSourcePaths.length > 0);
|
|
1624
|
+
assert.ok(capturedArchitectureSourcePaths.length < 20, 'test fixture should exercise a budgeted architecture context');
|
|
1625
|
+
const archPage = await fs.readFile(path.join(wikiDir, 'Architecture.md'), 'utf8');
|
|
1626
|
+
const sourcePathsMatch = /^source_paths: (\[[^\n]+\])$/m.exec(archPage);
|
|
1627
|
+
assert.ok(sourcePathsMatch, 'Architecture.md should contain normalized source_paths JSON');
|
|
1628
|
+
assert.deepEqual(JSON.parse(sourcePathsMatch[1]), capturedArchitectureSourcePaths.slice(0, 20));
|
|
1629
|
+
}
|
|
1630
|
+
finally {
|
|
1631
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
1632
|
+
}
|
|
1633
|
+
});
|
|
1634
|
+
test('compileWiki in LLM mode applies architecture request overrides', async () => {
|
|
1635
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest: defaultLLMManifest, plan: createLLMPlan() });
|
|
1636
|
+
const capturedRequests = {};
|
|
1637
|
+
const capturingProvider = {
|
|
1638
|
+
name: 'token-capturing-mock',
|
|
1639
|
+
async complete(req) {
|
|
1640
|
+
capturedRequests[req.archetype] = {
|
|
1641
|
+
maxTokens: req.maxTokens,
|
|
1642
|
+
reasoningEffort: req.reasoningEffort,
|
|
1643
|
+
};
|
|
1644
|
+
return {
|
|
1645
|
+
provider: 'token-capturing-mock',
|
|
1646
|
+
content: validLLMTestContent(req)
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
};
|
|
1650
|
+
const config = {
|
|
1651
|
+
compiler: {
|
|
1652
|
+
mode: 'llm',
|
|
1653
|
+
llm: {
|
|
1654
|
+
provider: 'mock',
|
|
1655
|
+
max_output_tokens: 4000,
|
|
1656
|
+
reasoning_effort: 'medium',
|
|
1657
|
+
page_budgets: {
|
|
1658
|
+
architecture: { max_output_tokens: 12000, reasoning_effort: 'low' }
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1663
|
+
try {
|
|
1664
|
+
await compileWiki({ scanDir, planFile, wikiDir, config, _provider: capturingProvider });
|
|
1665
|
+
// Architecture page must receive the architecture-specific request overrides.
|
|
1666
|
+
assert.equal(capturedRequests['architecture'].maxTokens, 12000);
|
|
1667
|
+
assert.equal(capturedRequests['architecture'].reasoningEffort, 'low');
|
|
1668
|
+
// Module pages must receive the global request settings.
|
|
1669
|
+
assert.equal(capturedRequests['module'].maxTokens, 4000);
|
|
1670
|
+
assert.equal(capturedRequests['module'].reasoningEffort, 'medium');
|
|
1671
|
+
}
|
|
1672
|
+
finally {
|
|
1673
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1676
|
+
test('compileWiki in LLM mode preserves human notes on Architecture.md synthesis', async () => {
|
|
1677
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest: defaultLLMManifest, plan: createLLMPlan() });
|
|
1678
|
+
const config = { compiler: { mode: 'llm' } };
|
|
1679
|
+
// Compile in deterministic mode first to create a generated Architecture.md.
|
|
1680
|
+
await compileWiki({ scanDir, planFile, wikiDir });
|
|
1681
|
+
const firstPage = await fs.readFile(path.join(wikiDir, 'Architecture.md'), 'utf8');
|
|
1682
|
+
// Simulate a human adding notes to the architecture page.
|
|
1683
|
+
const humanNotes = '\n## Custom architecture notes\n\nThis was written by a human.\n';
|
|
1684
|
+
const pageWithNotes = firstPage + `<!-- HUMAN_NOTES_START -->${humanNotes}<!-- HUMAN_NOTES_END -->\n`;
|
|
1685
|
+
await fs.writeFile(path.join(wikiDir, 'Architecture.md'), pageWithNotes, 'utf8');
|
|
1686
|
+
try {
|
|
1687
|
+
await compileWiki({ scanDir, planFile, wikiDir, config, _provider: new MockLLMProvider() });
|
|
1688
|
+
const afterLLM = await fs.readFile(path.join(wikiDir, 'Architecture.md'), 'utf8');
|
|
1689
|
+
// Human notes must be preserved.
|
|
1690
|
+
assert.equal(extractHumanNotes(afterLLM), humanNotes);
|
|
1691
|
+
// Page state must be "mixed" because human notes are non-empty.
|
|
1692
|
+
assert.match(afterLLM, /page_state: "mixed"/);
|
|
1693
|
+
// LLM content must be present.
|
|
1694
|
+
assert.match(afterLLM, /Generated by the mock LLM provider/);
|
|
1695
|
+
}
|
|
1696
|
+
finally {
|
|
1697
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
1698
|
+
}
|
|
1699
|
+
});
|
|
1700
|
+
// ---------------------------------------------------------------------------
|
|
1701
|
+
// Architecture.md incremental gating – deterministic mode
|
|
1702
|
+
// ---------------------------------------------------------------------------
|
|
1703
|
+
function buildArchManifest(extra = {}) {
|
|
1704
|
+
return {
|
|
1705
|
+
remote: 'origin',
|
|
1706
|
+
commit: 'arch-test-abc1234',
|
|
1707
|
+
mode: 'bootstrap',
|
|
1708
|
+
totals: { languages: { TypeScript: 2 }, categories: { source: 2 }, runtime_hints: {} },
|
|
1709
|
+
files: [
|
|
1710
|
+
{ path: 'src/core.ts', category: 'source', language: 'TypeScript', imports: [], runtime_hints: [], reasons: ['source'] },
|
|
1711
|
+
{ path: 'src/utils.ts', category: 'source', language: 'TypeScript', imports: [], runtime_hints: [], reasons: ['source'] }
|
|
1712
|
+
],
|
|
1713
|
+
analysis: { package_scripts: [], dependency_graph: { edges: [], summary: {} }, test_to_source: { mappings: [], summary: {} } },
|
|
1714
|
+
...extra
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
function buildArchPlan(modules = []) {
|
|
1718
|
+
return {
|
|
1719
|
+
pages: createPlan().pages,
|
|
1720
|
+
modules: modules.length > 0 ? modules : [
|
|
1721
|
+
{ slug: 'Module-Core', name: 'Core', files: ['src/core.ts'], categories: { source: 1 }, languages: { TypeScript: 1 }, runtime_hints: {}, important_reasons: ['source'] },
|
|
1722
|
+
{ slug: 'Module-Utils', name: 'Utils', files: ['src/utils.ts'], categories: { source: 1 }, languages: { TypeScript: 1 }, runtime_hints: {}, important_reasons: ['source'] }
|
|
1723
|
+
]
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
test('computeArchDecision returns full-regenerated when no existing content', () => {
|
|
1727
|
+
const newContent = '---\nsource_commit: "abc"\ncompiled_at: "T"\n---\n# Architecture\n\n## Structural map\n\n```mermaid\nflowchart TD\n Repo[Repository at abc1234]\n Repo --> M0[Core]\n```\n\n## Module groups\n\n### Core\n\n- Files: 1\n';
|
|
1728
|
+
assert.equal(computeArchDecision(newContent, null), 'full-regenerated');
|
|
1729
|
+
});
|
|
1730
|
+
test('computeArchDecision returns skipped when content unchanged after normalizing volatile fields', () => {
|
|
1731
|
+
const body = '# Architecture\n\n## Structural map\n\n```mermaid\nflowchart TD\n Repo[Repository at abc1234]\n Repo --> M0[Core]\n```\n\n## Module groups\n\n### Core\n\n- Files: 1\n- Dominant categories: source\n- Dominant languages: TypeScript\n- Important reasons: source\n';
|
|
1732
|
+
const existing = `---\nsource_commit: "abc"\ncompiled_at: "2025-01-01T00:00:00Z"\n---\n${body}`;
|
|
1733
|
+
// New content has a different compiled_at (timestamp) but same source_commit and same body
|
|
1734
|
+
const newContent = `---\nsource_commit: "abc"\ncompiled_at: "2025-02-01T00:00:00Z"\n---\n${body}`;
|
|
1735
|
+
assert.equal(computeArchDecision(newContent, existing), 'skipped');
|
|
1736
|
+
});
|
|
1737
|
+
test('computeArchDecision returns section-patched when module list unchanged but details changed', () => {
|
|
1738
|
+
const existing = '---\ncompiled_at: "T1"\n---\n# Architecture\n\n## Structural map\n\n```mermaid\nflowchart TD\n Repo[Repository at abc1234]\n Repo --> M0[Core]\n```\n\n## Module groups\n\n### Core\n\n- Files: 1\n- Dominant categories: source\n';
|
|
1739
|
+
const newContent = '---\ncompiled_at: "T2"\n---\n# Architecture\n\n## Structural map\n\n```mermaid\nflowchart TD\n Repo[Repository at def5678]\n Repo --> M0[Core]\n```\n\n## Module groups\n\n### Core\n\n- Files: 5\n- Dominant categories: source\n';
|
|
1740
|
+
assert.equal(computeArchDecision(newContent, existing), 'section-patched');
|
|
1741
|
+
});
|
|
1742
|
+
test('computeArchDecision returns full-regenerated when module list changes', () => {
|
|
1743
|
+
const existing = '---\ncompiled_at: "T1"\n---\n# Architecture\n\n## Structural map\n\n```mermaid\nflowchart TD\n Repo[Repository at abc1234]\n Repo --> M0[Core]\n```\n\n## Module groups\n\n### Core\n\n- Files: 1\n';
|
|
1744
|
+
// New content adds a second module
|
|
1745
|
+
const newContent = '---\ncompiled_at: "T2"\n---\n# Architecture\n\n## Structural map\n\n```mermaid\nflowchart TD\n Repo[Repository at def5678]\n Repo --> M0[Core]\n Repo --> M1[Utils]\n```\n\n## Module groups\n\n### Core\n\n- Files: 1\n\n### Utils\n\n- Files: 2\n';
|
|
1746
|
+
assert.equal(computeArchDecision(newContent, existing), 'full-regenerated');
|
|
1747
|
+
});
|
|
1748
|
+
test('computeArchDecision ignores HUMAN_NOTES headings when comparing module lists', () => {
|
|
1749
|
+
const base = `---
|
|
1750
|
+
compiled_at: "T1"
|
|
1751
|
+
---
|
|
1752
|
+
# Architecture
|
|
1753
|
+
|
|
1754
|
+
## Structural map
|
|
1755
|
+
|
|
1756
|
+
dummy
|
|
1757
|
+
|
|
1758
|
+
## Module groups
|
|
1759
|
+
|
|
1760
|
+
### Core
|
|
1761
|
+
|
|
1762
|
+
- Files: 1
|
|
1763
|
+
|
|
1764
|
+
## Architecture signals
|
|
1765
|
+
|
|
1766
|
+
- Module groups: 1
|
|
1767
|
+
|
|
1768
|
+
<!-- HUMAN_NOTES_START -->
|
|
1769
|
+
### Human heading
|
|
1770
|
+
<!-- HUMAN_NOTES_END -->
|
|
1771
|
+
`.replace('\tdummy', '```mermaid\nflowchart TD\n Repo[Repository at abc1234]\n Repo --> M0[Core]\n```');
|
|
1772
|
+
const updated = base.replace('compiled_at: "T1"', 'compiled_at: "T2"');
|
|
1773
|
+
assert.equal(computeArchDecision(updated, base), 'skipped');
|
|
1774
|
+
});
|
|
1775
|
+
test('computeArchDecision ignores HUMAN_NOTES and mixed page_state when generated portions are unchanged', () => {
|
|
1776
|
+
const generated = `---
|
|
1777
|
+
page_state: "generated"
|
|
1778
|
+
compiled_at: "T1"
|
|
1779
|
+
---
|
|
1780
|
+
# Architecture
|
|
1781
|
+
|
|
1782
|
+
## Structural map
|
|
1783
|
+
|
|
1784
|
+
dummy
|
|
1785
|
+
|
|
1786
|
+
## Module groups
|
|
1787
|
+
|
|
1788
|
+
### Core
|
|
1789
|
+
|
|
1790
|
+
- Files: 1
|
|
1791
|
+
|
|
1792
|
+
## Architecture signals
|
|
1793
|
+
|
|
1794
|
+
- Module groups: 1
|
|
1795
|
+
|
|
1796
|
+
<!-- HUMAN_NOTES_START -->
|
|
1797
|
+
<!-- HUMAN_NOTES_END -->
|
|
1798
|
+
`.replace('\tdummy', '```mermaid\nflowchart TD\n Repo[Repository at abc1234]\n Repo --> M0[Core]\n```');
|
|
1799
|
+
const mixed = generated
|
|
1800
|
+
.replace('page_state: "generated"', 'page_state: "mixed"')
|
|
1801
|
+
.replace('<!-- HUMAN_NOTES_START -->\n<!-- HUMAN_NOTES_END -->', '<!-- HUMAN_NOTES_START -->\nCustom note\n<!-- HUMAN_NOTES_END -->');
|
|
1802
|
+
assert.equal(computeArchDecision(generated.replace('compiled_at: "T1"', 'compiled_at: "T2"'), mixed), 'skipped');
|
|
1803
|
+
});
|
|
1804
|
+
test('computeArchDecision treats module heading changes beyond structural-map truncation as full regeneration', () => {
|
|
1805
|
+
const existing = `---
|
|
1806
|
+
compiled_at: "T1"
|
|
1807
|
+
---
|
|
1808
|
+
# Architecture
|
|
1809
|
+
|
|
1810
|
+
## Structural map
|
|
1811
|
+
|
|
1812
|
+
dummy
|
|
1813
|
+
|
|
1814
|
+
## Module groups
|
|
1815
|
+
|
|
1816
|
+
### Core
|
|
1817
|
+
|
|
1818
|
+
- Files: 1
|
|
1819
|
+
|
|
1820
|
+
### Extra
|
|
1821
|
+
|
|
1822
|
+
- Files: 1
|
|
1823
|
+
|
|
1824
|
+
## Architecture signals
|
|
1825
|
+
|
|
1826
|
+
- Module groups: 2
|
|
1827
|
+
`.replace('\tdummy', '```mermaid\nflowchart TD\n Repo[Repository at abc1234]\n Repo --> M0[Core]\n```');
|
|
1828
|
+
const newer = `---
|
|
1829
|
+
compiled_at: "T2"
|
|
1830
|
+
---
|
|
1831
|
+
# Architecture
|
|
1832
|
+
|
|
1833
|
+
## Structural map
|
|
1834
|
+
|
|
1835
|
+
dummy
|
|
1836
|
+
|
|
1837
|
+
## Module groups
|
|
1838
|
+
|
|
1839
|
+
### Core
|
|
1840
|
+
|
|
1841
|
+
- Files: 1
|
|
1842
|
+
|
|
1843
|
+
### Changed
|
|
1844
|
+
|
|
1845
|
+
- Files: 1
|
|
1846
|
+
|
|
1847
|
+
## Architecture signals
|
|
1848
|
+
|
|
1849
|
+
- Module groups: 2
|
|
1850
|
+
`.replace('\tdummy', '```mermaid\nflowchart TD\n Repo[Repository at abc1234]\n Repo --> M0[Core]\n```');
|
|
1851
|
+
assert.equal(computeArchDecision(newer, existing), 'full-regenerated');
|
|
1852
|
+
});
|
|
1853
|
+
test('computeArchDecision returns full-regenerated when existing has no module list', () => {
|
|
1854
|
+
const existing = '---\ncompiled_at: "T1"\n---\n# Architecture\n\nSome content without structural map.\n';
|
|
1855
|
+
const newContent = '---\ncompiled_at: "T2"\n---\n# Architecture\n\n## Structural map\n\n```mermaid\nflowchart TD\n Repo[Repository at abc1234]\n Repo --> M0[Core]\n```\n';
|
|
1856
|
+
assert.equal(computeArchDecision(newContent, existing), 'full-regenerated');
|
|
1857
|
+
});
|
|
1858
|
+
test('compileWiki deterministic mode keeps Architecture.md byte-stable on re-compile with unchanged manifest', async () => {
|
|
1859
|
+
const manifest = buildArchManifest();
|
|
1860
|
+
const plan = buildArchPlan();
|
|
1861
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest, plan });
|
|
1862
|
+
try {
|
|
1863
|
+
// First compile – creates Architecture.md
|
|
1864
|
+
const result1 = await compileWiki({ scanDir, planFile, wikiDir });
|
|
1865
|
+
const after1 = await fs.readFile(path.join(wikiDir, 'Architecture.md'), 'utf8');
|
|
1866
|
+
assert.equal(result1.summary.architecture_decision, 'full-regenerated');
|
|
1867
|
+
// Second compile – same manifest, same plan: should skip writing
|
|
1868
|
+
const result2 = await compileWiki({ scanDir, planFile, wikiDir });
|
|
1869
|
+
const after2 = await fs.readFile(path.join(wikiDir, 'Architecture.md'), 'utf8');
|
|
1870
|
+
// File must be byte-stable (not rewritten)
|
|
1871
|
+
assert.equal(after2, after1, 'Architecture.md must be byte-stable when inputs are unchanged');
|
|
1872
|
+
assert.equal(result2.summary.architecture_decision, 'skipped');
|
|
1873
|
+
}
|
|
1874
|
+
finally {
|
|
1875
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
1876
|
+
}
|
|
1877
|
+
});
|
|
1878
|
+
test('compileWiki deterministic mode updates Architecture signals for cross-cutting changes without full regeneration', async () => {
|
|
1879
|
+
const manifest = buildArchManifest();
|
|
1880
|
+
const plan = buildArchPlan();
|
|
1881
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest, plan });
|
|
1882
|
+
try {
|
|
1883
|
+
await compileWiki({ scanDir, planFile, wikiDir });
|
|
1884
|
+
const updatedManifest = buildArchManifest({
|
|
1885
|
+
totals: { languages: { TypeScript: 2 }, categories: { source: 2 }, runtime_hints: { 'http-route': 1 } },
|
|
1886
|
+
files: [
|
|
1887
|
+
{ path: 'src/core.ts', category: 'source', language: 'TypeScript', imports: [], runtime_hints: ['http-route'], route_surfaces: [{ methods: ['GET'], path: '/health', handler: 'health' }], environment_variables: ['PORT'], reasons: ['auth'] },
|
|
1888
|
+
{ path: 'src/utils.ts', category: 'source', language: 'TypeScript', imports: [], runtime_hints: ['deployment'], reasons: ['source'] },
|
|
1889
|
+
{ path: 'db/schema.ts', category: 'source', language: 'TypeScript', imports: [], runtime_hints: [], migration_surfaces: [{ kind: 'migration', id: '001', name: 'init' }], reasons: ['source'] }
|
|
1890
|
+
],
|
|
1891
|
+
analysis: { package_scripts: [], dependency_graph: { edges: [], summary: { edges: 0 } }, test_to_source: { mappings: [], summary: {} } }
|
|
1892
|
+
});
|
|
1893
|
+
await fs.writeFile(path.join(scanDir, 'manifest.json'), JSON.stringify(updatedManifest, null, 2));
|
|
1894
|
+
const result2 = await compileWiki({ scanDir, planFile, wikiDir });
|
|
1895
|
+
const after2 = await fs.readFile(path.join(wikiDir, 'Architecture.md'), 'utf8');
|
|
1896
|
+
assert.equal(result2.summary.architecture_decision, 'section-patched');
|
|
1897
|
+
assert.match(after2, /Route-bearing files: 1/);
|
|
1898
|
+
assert.match(after2, /Config-bearing files: 1/);
|
|
1899
|
+
assert.match(after2, /Data-model files: 1/);
|
|
1900
|
+
assert.match(after2, /Security-sensitive files: 1/);
|
|
1901
|
+
assert.match(after2, /Infrastructure files: 1/);
|
|
1902
|
+
}
|
|
1903
|
+
finally {
|
|
1904
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
1905
|
+
}
|
|
1906
|
+
});
|
|
1907
|
+
test('compileWiki deterministic mode still section-patches when Architecture.md contains human notes', async () => {
|
|
1908
|
+
const manifest = buildArchManifest();
|
|
1909
|
+
const plan = buildArchPlan();
|
|
1910
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest, plan });
|
|
1911
|
+
try {
|
|
1912
|
+
await compileWiki({ scanDir, planFile, wikiDir });
|
|
1913
|
+
const firstPage = await fs.readFile(path.join(wikiDir, 'Architecture.md'), 'utf8');
|
|
1914
|
+
const pageWithNotes = `${firstPage}\n<!-- HUMAN_NOTES_START -->\n### Operator note\nKeep this context.\n<!-- HUMAN_NOTES_END -->\n`;
|
|
1915
|
+
await fs.writeFile(path.join(wikiDir, 'Architecture.md'), pageWithNotes, 'utf8');
|
|
1916
|
+
const planWithMoreFiles = {
|
|
1917
|
+
...plan,
|
|
1918
|
+
modules: [
|
|
1919
|
+
{ ...plan.modules[0], files: ['src/core.ts', 'src/extra.ts'] },
|
|
1920
|
+
plan.modules[1]
|
|
1921
|
+
]
|
|
1922
|
+
};
|
|
1923
|
+
await fs.writeFile(planFile, JSON.stringify(planWithMoreFiles, null, 2));
|
|
1924
|
+
const result2 = await compileWiki({ scanDir, planFile, wikiDir });
|
|
1925
|
+
assert.equal(result2.summary.architecture_decision, 'section-patched');
|
|
1926
|
+
}
|
|
1927
|
+
finally {
|
|
1928
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
1929
|
+
}
|
|
1930
|
+
});
|
|
1931
|
+
test('compileWiki deterministic mode applies section-patched decision when module details change within same module list', async () => {
|
|
1932
|
+
const manifest = buildArchManifest();
|
|
1933
|
+
const plan = buildArchPlan();
|
|
1934
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest, plan });
|
|
1935
|
+
try {
|
|
1936
|
+
// First compile
|
|
1937
|
+
const result1 = await compileWiki({ scanDir, planFile, wikiDir });
|
|
1938
|
+
assert.equal(result1.summary.architecture_decision, 'full-regenerated');
|
|
1939
|
+
// Simulate a change that alters module details (more files in Core) but NOT the module list
|
|
1940
|
+
const planWithMoreFiles = {
|
|
1941
|
+
...plan,
|
|
1942
|
+
modules: [
|
|
1943
|
+
{ ...plan.modules[0], files: ['src/core.ts', 'src/extra.ts'] },
|
|
1944
|
+
plan.modules[1]
|
|
1945
|
+
]
|
|
1946
|
+
};
|
|
1947
|
+
await fs.writeFile(planFile, JSON.stringify(planWithMoreFiles, null, 2));
|
|
1948
|
+
const result2 = await compileWiki({ scanDir, planFile, wikiDir });
|
|
1949
|
+
const after2 = await fs.readFile(path.join(wikiDir, 'Architecture.md'), 'utf8');
|
|
1950
|
+
assert.equal(result2.summary.architecture_decision, 'section-patched');
|
|
1951
|
+
// Content must reflect the new file count
|
|
1952
|
+
assert.match(after2, /Files: 2/);
|
|
1953
|
+
}
|
|
1954
|
+
finally {
|
|
1955
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
1956
|
+
}
|
|
1957
|
+
});
|
|
1958
|
+
test('compileWiki deterministic mode applies full-regenerated when module list changes', async () => {
|
|
1959
|
+
const manifest = buildArchManifest();
|
|
1960
|
+
const plan = buildArchPlan();
|
|
1961
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest, plan });
|
|
1962
|
+
try {
|
|
1963
|
+
// First compile
|
|
1964
|
+
await compileWiki({ scanDir, planFile, wikiDir });
|
|
1965
|
+
// Add a new module (changes the module list)
|
|
1966
|
+
const planWithExtraModule = {
|
|
1967
|
+
...plan,
|
|
1968
|
+
modules: [
|
|
1969
|
+
...plan.modules,
|
|
1970
|
+
{ slug: 'Module-Api', name: 'Api', files: ['src/api.ts'], categories: { source: 1 }, languages: { TypeScript: 1 }, runtime_hints: {}, important_reasons: ['api-surface'] }
|
|
1971
|
+
]
|
|
1972
|
+
};
|
|
1973
|
+
await fs.writeFile(planFile, JSON.stringify(planWithExtraModule, null, 2));
|
|
1974
|
+
const result2 = await compileWiki({ scanDir, planFile, wikiDir });
|
|
1975
|
+
assert.equal(result2.summary.architecture_decision, 'full-regenerated');
|
|
1976
|
+
const after2 = await fs.readFile(path.join(wikiDir, 'Architecture.md'), 'utf8');
|
|
1977
|
+
assert.match(after2, /### Api/);
|
|
1978
|
+
}
|
|
1979
|
+
finally {
|
|
1980
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
1981
|
+
}
|
|
1982
|
+
});
|
|
1983
|
+
test('compileWiki summary includes architecture_decision field', async () => {
|
|
1984
|
+
const manifest = buildArchManifest();
|
|
1985
|
+
const plan = buildArchPlan();
|
|
1986
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest, plan });
|
|
1987
|
+
try {
|
|
1988
|
+
const result = await compileWiki({ scanDir, planFile, wikiDir });
|
|
1989
|
+
assert.ok('architecture_decision' in result.summary, 'summary must have architecture_decision');
|
|
1990
|
+
assert.ok(['skipped', 'section-patched', 'full-regenerated'].includes(result.summary.architecture_decision), 'architecture_decision must be a valid status');
|
|
1991
|
+
}
|
|
1992
|
+
finally {
|
|
1993
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
1994
|
+
}
|
|
1995
|
+
});
|
|
1996
|
+
// ---------------------------------------------------------------------------
|
|
1997
|
+
// Architecture.md incremental gating – LLM mode
|
|
1998
|
+
// ---------------------------------------------------------------------------
|
|
1999
|
+
test('compileWiki in LLM mode skips Architecture.md LLM call when fingerprint unchanged', async () => {
|
|
2000
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest: defaultLLMManifest, plan: createLLMPlan() });
|
|
2001
|
+
const config = { compiler: { mode: 'llm' } };
|
|
2002
|
+
let archCallCount = 0;
|
|
2003
|
+
const countingProvider = {
|
|
2004
|
+
name: 'counting-mock',
|
|
2005
|
+
async complete(req) {
|
|
2006
|
+
if (req.archetype === 'architecture') {
|
|
2007
|
+
archCallCount++;
|
|
2008
|
+
}
|
|
2009
|
+
return { provider: 'counting-mock', content: validLLMTestContent(req) };
|
|
2010
|
+
}
|
|
2011
|
+
};
|
|
2012
|
+
try {
|
|
2013
|
+
// First compile – architecture LLM call expected
|
|
2014
|
+
const result1 = await compileWiki({ scanDir, planFile, wikiDir, config, _provider: countingProvider });
|
|
2015
|
+
assert.equal(archCallCount, 1, 'Architecture LLM call expected on first compile');
|
|
2016
|
+
assert.equal(result1.summary.architecture_decision, 'full-regenerated');
|
|
2017
|
+
const archAfter1 = await fs.readFile(path.join(wikiDir, 'Architecture.md'), 'utf8');
|
|
2018
|
+
assert.match(archAfter1, /arch_inputs_fingerprint: "[a-f0-9]{16}"/);
|
|
2019
|
+
// Second compile – same manifest/plan: fingerprint matches, LLM call should be skipped
|
|
2020
|
+
const result2 = await compileWiki({ scanDir, planFile, wikiDir, config, _provider: countingProvider });
|
|
2021
|
+
assert.equal(archCallCount, 1, 'Architecture LLM call must NOT be made when fingerprint matches');
|
|
2022
|
+
assert.equal(result2.summary.architecture_decision, 'skipped');
|
|
2023
|
+
// Existing LLM-generated Architecture.md must remain unchanged (byte-stable)
|
|
2024
|
+
const archAfter2 = await fs.readFile(path.join(wikiDir, 'Architecture.md'), 'utf8');
|
|
2025
|
+
assert.equal(archAfter2, archAfter1, 'Architecture.md must be byte-stable when fingerprint matches');
|
|
2026
|
+
}
|
|
2027
|
+
finally {
|
|
2028
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
2029
|
+
}
|
|
2030
|
+
});
|
|
2031
|
+
test('compileWiki in LLM mode makes Architecture.md LLM call when fingerprint changes', async () => {
|
|
2032
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest: defaultLLMManifest, plan: createLLMPlan() });
|
|
2033
|
+
const config = { compiler: { mode: 'llm' } };
|
|
2034
|
+
let archCallCount = 0;
|
|
2035
|
+
const countingProvider = {
|
|
2036
|
+
name: 'counting-mock-2',
|
|
2037
|
+
async complete(req) {
|
|
2038
|
+
if (req.archetype === 'architecture') {
|
|
2039
|
+
archCallCount++;
|
|
2040
|
+
}
|
|
2041
|
+
return { provider: 'counting-mock-2', content: validLLMTestContent(req) };
|
|
2042
|
+
}
|
|
2043
|
+
};
|
|
2044
|
+
try {
|
|
2045
|
+
// First compile
|
|
2046
|
+
await compileWiki({ scanDir, planFile, wikiDir, config, _provider: countingProvider });
|
|
2047
|
+
assert.equal(archCallCount, 1);
|
|
2048
|
+
// Change the plan by adding a new module (changes architecture fingerprint)
|
|
2049
|
+
const updatedPlan = {
|
|
2050
|
+
...createLLMPlan(),
|
|
2051
|
+
modules: [
|
|
2052
|
+
...createLLMPlan().modules,
|
|
2053
|
+
{ slug: 'Module-Extra', name: 'Extra', files: ['src/extra.ts'], categories: { source: 1 }, languages: { TypeScript: 1 }, runtime_hints: {}, important_reasons: ['source'] }
|
|
2054
|
+
]
|
|
2055
|
+
};
|
|
2056
|
+
await fs.writeFile(planFile, JSON.stringify(updatedPlan, null, 2));
|
|
2057
|
+
// Second compile – fingerprint changed, LLM call expected
|
|
2058
|
+
const result2 = await compileWiki({ scanDir, planFile, wikiDir, config, _provider: countingProvider });
|
|
2059
|
+
assert.equal(archCallCount, 2, 'Architecture LLM call expected when fingerprint changes');
|
|
2060
|
+
assert.equal(result2.summary.architecture_decision, 'full-regenerated');
|
|
2061
|
+
}
|
|
2062
|
+
finally {
|
|
2063
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
2064
|
+
}
|
|
2065
|
+
});
|
|
2066
|
+
test('compileWiki in LLM mode does not skip architecture when commit changed but fingerprint stayed stable', async () => {
|
|
2067
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest: defaultLLMManifest, plan: createLLMPlan() });
|
|
2068
|
+
const config = { compiler: { mode: 'llm' } };
|
|
2069
|
+
let archCallCount = 0;
|
|
2070
|
+
const countingProvider = {
|
|
2071
|
+
name: 'counting-mock-commit',
|
|
2072
|
+
async complete(req) {
|
|
2073
|
+
if (req.archetype === 'architecture') {
|
|
2074
|
+
archCallCount++;
|
|
2075
|
+
}
|
|
2076
|
+
return { provider: 'counting-mock-commit', content: validLLMTestContent(req) };
|
|
2077
|
+
}
|
|
2078
|
+
};
|
|
2079
|
+
try {
|
|
2080
|
+
await compileWiki({ scanDir, planFile, wikiDir, config, _provider: countingProvider });
|
|
2081
|
+
assert.equal(archCallCount, 1);
|
|
2082
|
+
const updatedManifest = { ...defaultLLMManifest, commit: 'new-commit-222' };
|
|
2083
|
+
await fs.writeFile(path.join(scanDir, 'manifest.json'), JSON.stringify(updatedManifest, null, 2));
|
|
2084
|
+
const result2 = await compileWiki({ scanDir, planFile, wikiDir, config, _provider: countingProvider });
|
|
2085
|
+
assert.equal(archCallCount, 2, 'architecture provider should rerun when source_commit changes');
|
|
2086
|
+
assert.equal(result2.summary.architecture_decision, 'full-regenerated');
|
|
2087
|
+
const archPage = await fs.readFile(path.join(wikiDir, 'Architecture.md'), 'utf8');
|
|
2088
|
+
assert.match(archPage, /source_commit: "new-commit-222"/);
|
|
2089
|
+
}
|
|
2090
|
+
finally {
|
|
2091
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
2092
|
+
}
|
|
2093
|
+
});
|
|
2094
|
+
test('compileWiki in LLM mode reruns architecture when route details change without changing route-bearing file count', async () => {
|
|
2095
|
+
const manifest = {
|
|
2096
|
+
...defaultLLMManifest,
|
|
2097
|
+
totals: { languages: { TypeScript: 1 }, categories: { source: 1 }, runtime_hints: { 'http-route': 1 } },
|
|
2098
|
+
files: [
|
|
2099
|
+
{
|
|
2100
|
+
path: 'src/auth.ts',
|
|
2101
|
+
category: 'source',
|
|
2102
|
+
language: 'TypeScript',
|
|
2103
|
+
imports: [],
|
|
2104
|
+
route_surfaces: [{ framework: 'express', methods: ['GET'], path: '/health', handler: 'health' }],
|
|
2105
|
+
runtime_hints: ['http-route'],
|
|
2106
|
+
reasons: ['source']
|
|
2107
|
+
}
|
|
2108
|
+
],
|
|
2109
|
+
analysis: { package_scripts: [], dependency_graph: { edges: [], summary: {} }, test_to_source: { mappings: [], summary: {} } }
|
|
2110
|
+
};
|
|
2111
|
+
const plan = {
|
|
2112
|
+
...createLLMPlan(),
|
|
2113
|
+
modules: [
|
|
2114
|
+
{ slug: 'Module-Auth', name: 'Auth', files: ['src/auth.ts'], categories: { source: 1 }, languages: { TypeScript: 1 }, runtime_hints: {}, important_reasons: ['source'] }
|
|
2115
|
+
]
|
|
2116
|
+
};
|
|
2117
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest, plan });
|
|
2118
|
+
const config = { compiler: { mode: 'llm' } };
|
|
2119
|
+
let archCallCount = 0;
|
|
2120
|
+
const countingProvider = {
|
|
2121
|
+
name: 'counting-mock-routes',
|
|
2122
|
+
async complete(req) {
|
|
2123
|
+
if (req.archetype === 'architecture') {
|
|
2124
|
+
archCallCount++;
|
|
2125
|
+
}
|
|
2126
|
+
return { provider: 'counting-mock-routes', content: validLLMTestContent(req) };
|
|
2127
|
+
}
|
|
2128
|
+
};
|
|
2129
|
+
try {
|
|
2130
|
+
await compileWiki({ scanDir, planFile, wikiDir, config, _provider: countingProvider });
|
|
2131
|
+
assert.equal(archCallCount, 1);
|
|
2132
|
+
const updatedManifest = {
|
|
2133
|
+
...manifest,
|
|
2134
|
+
files: [
|
|
2135
|
+
{
|
|
2136
|
+
...manifest.files[0],
|
|
2137
|
+
route_surfaces: [{ framework: 'express', methods: ['POST'], path: '/status', handler: 'status' }]
|
|
2138
|
+
}
|
|
2139
|
+
]
|
|
2140
|
+
};
|
|
2141
|
+
await fs.writeFile(path.join(scanDir, 'manifest.json'), JSON.stringify(updatedManifest, null, 2));
|
|
2142
|
+
const result2 = await compileWiki({ scanDir, planFile, wikiDir, config, _provider: countingProvider });
|
|
2143
|
+
assert.equal(archCallCount, 2, 'architecture provider should rerun when route details change');
|
|
2144
|
+
assert.equal(result2.summary.architecture_decision, 'full-regenerated');
|
|
2145
|
+
}
|
|
2146
|
+
finally {
|
|
2147
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
2148
|
+
}
|
|
2149
|
+
});
|
|
2150
|
+
test('compileWiki in LLM mode embeds arch_inputs_fingerprint in Architecture.md frontmatter', async () => {
|
|
2151
|
+
const { dir, scanDir, wikiDir, planFile } = await writeFixture({ manifest: defaultLLMManifest, plan: createLLMPlan() });
|
|
2152
|
+
const config = { compiler: { mode: 'llm' } };
|
|
2153
|
+
try {
|
|
2154
|
+
await compileWiki({ scanDir, planFile, wikiDir, config, _provider: new MockLLMProvider() });
|
|
2155
|
+
const archPage = await fs.readFile(path.join(wikiDir, 'Architecture.md'), 'utf8');
|
|
2156
|
+
assert.match(archPage, /^arch_inputs_fingerprint: "[a-f0-9]{16}"$/m);
|
|
2157
|
+
}
|
|
2158
|
+
finally {
|
|
2159
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
2160
|
+
}
|
|
2161
|
+
});
|
|
2162
|
+
test('compileWiki writes deterministic enriched graph.json while preserving source→page affects parity', async () => {
|
|
2163
|
+
const manifest = {
|
|
2164
|
+
remote: 'origin',
|
|
2165
|
+
commit: 'graph-test-commit',
|
|
2166
|
+
mode: 'bootstrap',
|
|
2167
|
+
totals: {
|
|
2168
|
+
languages: { TypeScript: 1, Markdown: 1 },
|
|
2169
|
+
categories: { source: 1, docs: 1 },
|
|
2170
|
+
runtime_hints: {}
|
|
2171
|
+
},
|
|
2172
|
+
files: [
|
|
2173
|
+
{
|
|
2174
|
+
path: 'src/server.ts',
|
|
2175
|
+
category: 'source',
|
|
2176
|
+
language: 'TypeScript',
|
|
2177
|
+
imports: [],
|
|
2178
|
+
runtime_hints: [],
|
|
2179
|
+
reasons: ['source']
|
|
2180
|
+
},
|
|
2181
|
+
{
|
|
2182
|
+
path: 'docs/guide.md',
|
|
2183
|
+
category: 'docs',
|
|
2184
|
+
language: 'Markdown',
|
|
2185
|
+
imports: [],
|
|
2186
|
+
runtime_hints: [],
|
|
2187
|
+
reasons: ['docs']
|
|
2188
|
+
}
|
|
2189
|
+
],
|
|
2190
|
+
documentation: {
|
|
2191
|
+
files: [
|
|
2192
|
+
{
|
|
2193
|
+
kind: 'documentation_card',
|
|
2194
|
+
path: 'docs/guide.md',
|
|
2195
|
+
authority: 'secondary',
|
|
2196
|
+
status: 'unvalidated',
|
|
2197
|
+
stale: false,
|
|
2198
|
+
claims: [],
|
|
2199
|
+
validation: { contradictions: [], validated: [], commands: [], env_vars: [] }
|
|
2200
|
+
}
|
|
2201
|
+
]
|
|
2202
|
+
}
|
|
2203
|
+
};
|
|
2204
|
+
const plan = {
|
|
2205
|
+
...createPlan(),
|
|
2206
|
+
pages: [
|
|
2207
|
+
...createPlan().pages,
|
|
2208
|
+
{ path: 'Service-api.md', phase: 'modules', purpose: 'Service module page.', moduleName: 'Service api' }
|
|
2209
|
+
],
|
|
2210
|
+
modules: [
|
|
2211
|
+
{
|
|
2212
|
+
name: 'Service api',
|
|
2213
|
+
slug: 'Service-api',
|
|
2214
|
+
files: ['src/server.ts', 'docs/guide.md'],
|
|
2215
|
+
categories: { source: 1, docs: 1 },
|
|
2216
|
+
languages: { TypeScript: 1, Markdown: 1 },
|
|
2217
|
+
runtime_hints: {},
|
|
2218
|
+
important_reasons: []
|
|
2219
|
+
}
|
|
2220
|
+
],
|
|
2221
|
+
affected_page_graph: {
|
|
2222
|
+
source_to_pages: [
|
|
2223
|
+
{
|
|
2224
|
+
source: 'src/server.ts',
|
|
2225
|
+
pages: [
|
|
2226
|
+
{ page: 'Architecture.md', reasons: ['module_membership'] },
|
|
2227
|
+
{ page: 'Dependency-Map.md', reasons: ['dependency_change'] }
|
|
2228
|
+
]
|
|
2229
|
+
},
|
|
2230
|
+
{
|
|
2231
|
+
source: 'docs/guide.md',
|
|
2232
|
+
pages: [
|
|
2233
|
+
{ page: 'Documentation-Debt-Report.md', reasons: ['docs_debt'] },
|
|
2234
|
+
{ page: 'Architecture.md', reasons: ['context'] }
|
|
2235
|
+
]
|
|
2236
|
+
}
|
|
2237
|
+
],
|
|
2238
|
+
summary: {
|
|
2239
|
+
mapped_sources: 2,
|
|
2240
|
+
total_page_references: 4
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
};
|
|
2244
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'repo-wiki-graph-'));
|
|
2245
|
+
const llmwikiDir = path.join(dir, '.llmwiki');
|
|
2246
|
+
const scanDir = path.join(llmwikiDir, 'run');
|
|
2247
|
+
const wikiDir = path.join(dir, 'custom-wiki');
|
|
2248
|
+
const planFile = path.join(llmwikiDir, 'bootstrap-plan.json');
|
|
2249
|
+
await fs.mkdir(scanDir, { recursive: true });
|
|
2250
|
+
await fs.writeFile(path.join(scanDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
2251
|
+
await fs.writeFile(planFile, JSON.stringify(plan, null, 2));
|
|
2252
|
+
try {
|
|
2253
|
+
await compileWiki({ scanDir, planFile, wikiDir });
|
|
2254
|
+
const graphPath = path.join(llmwikiDir, 'graph.json');
|
|
2255
|
+
await assert.doesNotReject(fs.access(graphPath), 'graph.json should be written to the fixed .llmwiki artifact path');
|
|
2256
|
+
await assert.rejects(fs.access(path.join(dir, 'graph.json')));
|
|
2257
|
+
const firstBytes = await fs.readFile(graphPath, 'utf8');
|
|
2258
|
+
const graph = JSON.parse(firstBytes);
|
|
2259
|
+
assert.deepEqual(Object.keys(graph), ['schema_version', 'nodes', 'edges']);
|
|
2260
|
+
assert.equal(graph.schema_version, 1);
|
|
2261
|
+
const expectedPagePaths = [...new Set(plan.pages.map((page) => page.path))]
|
|
2262
|
+
.sort((left, right) => left.localeCompare(right));
|
|
2263
|
+
const actualPagePaths = graph.nodes
|
|
2264
|
+
.filter((node) => node.kind === 'page')
|
|
2265
|
+
.map((node) => node.path);
|
|
2266
|
+
assert.deepEqual(actualPagePaths, expectedPagePaths);
|
|
2267
|
+
const docsNode = graph.nodes.find((node) => node.id === 'documentation:docs/guide.md');
|
|
2268
|
+
const sourceNode = graph.nodes.find((node) => node.id === 'source:src/server.ts');
|
|
2269
|
+
const moduleNode = graph.nodes.find((node) => node.id === 'module:Service-api');
|
|
2270
|
+
assert.equal(docsNode?.kind, 'documentation');
|
|
2271
|
+
assert.equal(sourceNode?.kind, 'source');
|
|
2272
|
+
assert.equal(moduleNode?.kind, 'module');
|
|
2273
|
+
const pageNodes = graph.nodes.filter((node) => node.kind === 'page');
|
|
2274
|
+
const sourceNodes = graph.nodes.filter((node) => node.kind !== 'page');
|
|
2275
|
+
assert.deepEqual(pageNodes.map((node) => node.path), [...pageNodes.map((node) => node.path)].sort((left, right) => left.localeCompare(right)), 'page nodes must be sorted deterministically by canonical path');
|
|
2276
|
+
assert.deepEqual(sourceNodes.map((node) => node.path), [...sourceNodes.map((node) => node.path)].sort((left, right) => left.localeCompare(right)), 'source nodes must be sorted deterministically by canonical path');
|
|
2277
|
+
const documentationPaths = new Set([
|
|
2278
|
+
...(manifest.documentation?.files || []).map((file) => file.path),
|
|
2279
|
+
...(manifest.files || []).filter((file) => file.category === 'docs').map((file) => file.path)
|
|
2280
|
+
]);
|
|
2281
|
+
const isDocumentationGraphPath = (sourcePath) => (documentationPaths.has(sourcePath)
|
|
2282
|
+
|| /\.(?:md|mdx|markdown)$/i.test(sourcePath)
|
|
2283
|
+
|| /(?:^|\/)docs(?:\/|$)/i.test(sourcePath)
|
|
2284
|
+
|| /(?:^|\/)documentation(?:\/|$)/i.test(sourcePath));
|
|
2285
|
+
const expectedPairs = plan.affected_page_graph.source_to_pages
|
|
2286
|
+
.flatMap((entry) => (entry.pages || []).map((pageEntry) => `${isDocumentationGraphPath(entry.source) ? 'documentation' : 'source'}:${entry.source}→page:${pageEntry.page}`))
|
|
2287
|
+
.sort();
|
|
2288
|
+
const actualPairs = graph.edges
|
|
2289
|
+
.filter((edge) => edge.type === 'affects')
|
|
2290
|
+
.map((edge) => `${edge.from}→${edge.to}`)
|
|
2291
|
+
.sort();
|
|
2292
|
+
assert.deepEqual(actualPairs, expectedPairs, 'edge pairs must exactly match plan.affected_page_graph.source_to_pages');
|
|
2293
|
+
const serializedEdges = graph.edges.map((edge) => `${edge.type}|${edge.from}|${edge.to}`);
|
|
2294
|
+
assert.deepEqual(serializedEdges, [...serializedEdges].sort((left, right) => left.localeCompare(right)), 'edges must be sorted deterministically by (kind, from, to)');
|
|
2295
|
+
assert.equal(new Set(serializedEdges).size, serializedEdges.length, 'edges must be deduplicated');
|
|
2296
|
+
const graphNodeIdSet = new Set(graph.nodes.map((node) => node.id));
|
|
2297
|
+
for (const edge of graph.edges) {
|
|
2298
|
+
assert.ok(graphNodeIdSet.has(edge.from), `missing from-node for ${edge.from} -> ${edge.to}`);
|
|
2299
|
+
assert.ok(graphNodeIdSet.has(edge.to), `missing to-node for ${edge.from} -> ${edge.to}`);
|
|
2300
|
+
}
|
|
2301
|
+
const ownershipEdges = graph.edges
|
|
2302
|
+
.filter((edge) => edge.type === 'owns')
|
|
2303
|
+
.map((edge) => `${edge.from}→${edge.to}`)
|
|
2304
|
+
.sort();
|
|
2305
|
+
assert.deepEqual(ownershipEdges, [
|
|
2306
|
+
'module:Service-api→documentation:docs/guide.md',
|
|
2307
|
+
'module:Service-api→page:Service-api.md',
|
|
2308
|
+
'module:Service-api→source:src/server.ts'
|
|
2309
|
+
]);
|
|
2310
|
+
assertNoWallClockFields(graph);
|
|
2311
|
+
await compileWiki({ scanDir, planFile, wikiDir });
|
|
2312
|
+
const secondBytes = await fs.readFile(graphPath, 'utf8');
|
|
2313
|
+
assert.equal(secondBytes, firstBytes, 'graph.json must be byte-identical across repeated compile runs with unchanged inputs');
|
|
2314
|
+
}
|
|
2315
|
+
finally {
|
|
2316
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
2317
|
+
}
|
|
2318
|
+
});
|
|
2319
|
+
test('compileWiki graph enrichment normalizes page states, wiki links, and provenance edges', async () => {
|
|
2320
|
+
const manifest = {
|
|
2321
|
+
remote: 'origin',
|
|
2322
|
+
commit: 'graph-enrichment-commit',
|
|
2323
|
+
mode: 'bootstrap',
|
|
2324
|
+
totals: {
|
|
2325
|
+
languages: { TypeScript: 1, Markdown: 1 },
|
|
2326
|
+
categories: { source: 1, docs: 1 },
|
|
2327
|
+
runtime_hints: {}
|
|
2328
|
+
},
|
|
2329
|
+
files: [
|
|
2330
|
+
{
|
|
2331
|
+
path: 'src/a.ts',
|
|
2332
|
+
category: 'source',
|
|
2333
|
+
language: 'TypeScript',
|
|
2334
|
+
imports: [],
|
|
2335
|
+
runtime_hints: [],
|
|
2336
|
+
reasons: ['source']
|
|
2337
|
+
},
|
|
2338
|
+
{
|
|
2339
|
+
path: 'docs/readme.md',
|
|
2340
|
+
category: 'docs',
|
|
2341
|
+
language: 'Markdown',
|
|
2342
|
+
imports: [],
|
|
2343
|
+
runtime_hints: [],
|
|
2344
|
+
reasons: ['docs']
|
|
2345
|
+
}
|
|
2346
|
+
],
|
|
2347
|
+
documentation: {
|
|
2348
|
+
files: [
|
|
2349
|
+
{
|
|
2350
|
+
kind: 'documentation_card',
|
|
2351
|
+
path: 'docs/readme.md',
|
|
2352
|
+
authority: 'secondary',
|
|
2353
|
+
status: 'unvalidated',
|
|
2354
|
+
stale: false,
|
|
2355
|
+
claims: [],
|
|
2356
|
+
validation: { contradictions: [], validated: [], commands: [], env_vars: [] }
|
|
2357
|
+
}
|
|
2358
|
+
]
|
|
2359
|
+
}
|
|
2360
|
+
};
|
|
2361
|
+
const plan = {
|
|
2362
|
+
pages: [
|
|
2363
|
+
{ path: 'Alpha.md', phase: 'module', purpose: 'Alpha' },
|
|
2364
|
+
{ path: 'Beta.md', phase: 'module', purpose: 'Beta' },
|
|
2365
|
+
{ path: 'Delta.md', phase: 'module', purpose: 'Delta' },
|
|
2366
|
+
{ path: 'Epsilon.md', phase: 'module', purpose: 'Epsilon' },
|
|
2367
|
+
{ path: 'Eta.md', phase: 'module', purpose: 'Eta' },
|
|
2368
|
+
{ path: 'Gamma.md', phase: 'module', purpose: 'Gamma' },
|
|
2369
|
+
{ path: 'Iota.md', phase: 'module', purpose: 'Iota' },
|
|
2370
|
+
{ path: 'Kappa.md', phase: 'module', purpose: 'Kappa' },
|
|
2371
|
+
{ path: 'Lambda.md', phase: 'module', purpose: 'Lambda' },
|
|
2372
|
+
{ path: 'Nu.md', phase: 'module', purpose: 'Nu' },
|
|
2373
|
+
{ path: 'Omicron.md', phase: 'module', purpose: 'Omicron' },
|
|
2374
|
+
{ path: 'Pi.md', phase: 'module', purpose: 'Pi' },
|
|
2375
|
+
{ path: 'Theta.md', phase: 'module', purpose: 'Theta' },
|
|
2376
|
+
{ path: 'Xi.md', phase: 'module', purpose: 'Xi' },
|
|
2377
|
+
{ path: 'Zeta.md', phase: 'module', purpose: 'Zeta' }
|
|
2378
|
+
],
|
|
2379
|
+
modules: [],
|
|
2380
|
+
affected_page_graph: {
|
|
2381
|
+
source_to_pages: [
|
|
2382
|
+
{
|
|
2383
|
+
source: 'src/a.ts',
|
|
2384
|
+
pages: [
|
|
2385
|
+
{ page: 'Alpha.md', reasons: ['module_membership'] },
|
|
2386
|
+
{ page: 'Beta.md', reasons: ['api_change'] }
|
|
2387
|
+
]
|
|
2388
|
+
}
|
|
2389
|
+
],
|
|
2390
|
+
summary: {
|
|
2391
|
+
mapped_sources: 1,
|
|
2392
|
+
total_page_references: 2
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
};
|
|
2396
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'repo-wiki-graph-enrichment-'));
|
|
2397
|
+
const llmwikiDir = path.join(dir, '.llmwiki');
|
|
2398
|
+
const scanDir = path.join(llmwikiDir, 'run');
|
|
2399
|
+
const wikiDir = path.join(dir, 'wiki');
|
|
2400
|
+
const planFile = path.join(llmwikiDir, 'bootstrap-plan.json');
|
|
2401
|
+
await fs.mkdir(scanDir, { recursive: true });
|
|
2402
|
+
await fs.mkdir(wikiDir, { recursive: true });
|
|
2403
|
+
await fs.writeFile(path.join(scanDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
2404
|
+
await fs.writeFile(planFile, JSON.stringify(plan, null, 2));
|
|
2405
|
+
await fs.writeFile(path.join(wikiDir, 'Alpha.md'), `---
|
|
2406
|
+
owned_by: "human"
|
|
2407
|
+
source_paths: ["src/a.ts", "", "./src/a.ts", "src/./a.ts"]
|
|
2408
|
+
---
|
|
2409
|
+
|
|
2410
|
+
[Beta](Beta)
|
|
2411
|
+
[Beta 2](Beta.md)
|
|
2412
|
+
[Beta 3](./Beta.md)
|
|
2413
|
+
[Beta 4](Beta.md#section)
|
|
2414
|
+
[Beta 5](Beta.md "details")
|
|
2415
|
+
[Epsilon](<Epsilon.md> "details")
|
|
2416
|
+
[Zeta][zeta-ref]
|
|
2417
|
+
[See [Theta] details](Theta.md)
|
|
2418
|
+
\\[Escaped](Gamma.md)
|
|
2419
|
+
Inline code \`[Inline](Gamma.md)\` should not count.
|
|
2420
|
+
<!-- [Comment](Gamma.md) -->
|
|
2421
|
+
<!--
|
|
2422
|
+
[Hidden](Gamma.md)
|
|
2423
|
+
-->
|
|
2424
|
+
[Eta](Eta.md)
|
|
2425
|
+
\`\`\`\`md
|
|
2426
|
+
\`\`\`\`ts
|
|
2427
|
+
[Code sample](Gamma.md)
|
|
2428
|
+
\`\`\`\`
|
|
2429
|
+
[External](https://example.com)
|
|
2430
|
+
[Anchor](#local)
|
|
2431
|
+
[Asset](diagram.png)
|
|
2432
|
+
[Nested](nested/Beta.md)
|
|
2433
|
+
- Parent
|
|
2434
|
+
- [Iota](Iota.md)
|
|
2435
|
+
- Parent 2
|
|
2436
|
+
[Kappa](Kappa.md)
|
|
2437
|
+
|
|
2438
|
+
[Indented code link](Gamma.md)
|
|
2439
|
+
\`\`\`md
|
|
2440
|
+
[Lambda](Lambda.md)
|
|
2441
|
+
> \`\`\`md
|
|
2442
|
+
> [Quoted code](Nu.md)
|
|
2443
|
+
> \`\`\`
|
|
2444
|
+
> [Quoted indented code](Xi.md)
|
|
2445
|
+
> [Eta again](Eta.md)
|
|
2446
|
+
![diagram][omicron-image]
|
|
2447
|
+
[omicron-image]: Omicron.md
|
|
2448
|
+
[def-with-inline]: docs/readme.md "see [Pi](Pi.md)"
|
|
2449
|
+
[zeta-ref]: Zeta.md
|
|
2450
|
+
[zeta-ref]: Gamma.md
|
|
2451
|
+
`);
|
|
2452
|
+
await fs.writeFile(path.join(wikiDir, 'Beta.md'), `---
|
|
2453
|
+
title: "beta"
|
|
2454
|
+
source_paths: src/a.ts
|
|
2455
|
+
---
|
|
2456
|
+
|
|
2457
|
+
no source commit keeps this unmanaged
|
|
2458
|
+
`);
|
|
2459
|
+
await fs.writeFile(path.join(wikiDir, 'Delta.md'), `---
|
|
2460
|
+
source_repo: "origin"
|
|
2461
|
+
source_commit: "graph-enrichment-commit"
|
|
2462
|
+
source_paths: [src/a.ts, "", ./src/a.ts, docs/readme.md, src//a.ts] # primary
|
|
2463
|
+
---
|
|
2464
|
+
|
|
2465
|
+
<!-- HUMAN_NOTES_START -->
|
|
2466
|
+
|
|
2467
|
+
<!-- HUMAN_NOTES_END -->
|
|
2468
|
+
`);
|
|
2469
|
+
await fs.writeFile(path.join(wikiDir, 'Gamma.md'), `---
|
|
2470
|
+
source_repo: "origin"
|
|
2471
|
+
source_commit: "graph-enrichment-commit"
|
|
2472
|
+
source_paths: # primary sources
|
|
2473
|
+
- src/./gamma.ts # primary
|
|
2474
|
+
- "./src/gamma.ts"
|
|
2475
|
+
- "src//gamma.ts"
|
|
2476
|
+
# keep comments inside source_paths lists
|
|
2477
|
+
- ""
|
|
2478
|
+
- docs/readme.md
|
|
2479
|
+
---
|
|
2480
|
+
|
|
2481
|
+
<!-- HUMAN_NOTES_START -->
|
|
2482
|
+
Needs review.
|
|
2483
|
+
<!-- HUMAN_NOTES_END -->
|
|
2484
|
+
`);
|
|
2485
|
+
try {
|
|
2486
|
+
await compileWiki({ scanDir, planFile, wikiDir });
|
|
2487
|
+
const graphPath = path.join(llmwikiDir, 'graph.json');
|
|
2488
|
+
const graphBytes = await fs.readFile(graphPath, 'utf8');
|
|
2489
|
+
const graph = JSON.parse(graphBytes);
|
|
2490
|
+
const pageStateByPath = new Map(graph.nodes
|
|
2491
|
+
.filter((node) => node.kind === 'page')
|
|
2492
|
+
.map((node) => [node.path, node.page_state]));
|
|
2493
|
+
assert.equal(pageStateByPath.get('Alpha.md'), 'human-owned');
|
|
2494
|
+
assert.equal(pageStateByPath.get('Beta.md'), 'unmanaged');
|
|
2495
|
+
assert.equal(pageStateByPath.get('Gamma.md'), 'mixed');
|
|
2496
|
+
assert.equal(pageStateByPath.get('Delta.md'), 'generated');
|
|
2497
|
+
assert.equal(pageStateByPath.get('Epsilon.md'), 'generated');
|
|
2498
|
+
assert.equal(pageStateByPath.get('Eta.md'), 'generated');
|
|
2499
|
+
assert.equal(pageStateByPath.get('Iota.md'), 'generated');
|
|
2500
|
+
assert.equal(pageStateByPath.get('Kappa.md'), 'generated');
|
|
2501
|
+
assert.equal(pageStateByPath.get('Lambda.md'), 'generated');
|
|
2502
|
+
assert.equal(pageStateByPath.get('Theta.md'), 'generated');
|
|
2503
|
+
assert.equal(pageStateByPath.get('Zeta.md'), 'generated');
|
|
2504
|
+
const wikiLinks = graph.edges
|
|
2505
|
+
.filter((edge) => edge.type === 'wiki_link')
|
|
2506
|
+
.map((edge) => `${edge.from}->${edge.to}`);
|
|
2507
|
+
assert.deepEqual(wikiLinks, [
|
|
2508
|
+
'page:Alpha.md->page:Beta.md',
|
|
2509
|
+
'page:Alpha.md->page:Epsilon.md',
|
|
2510
|
+
'page:Alpha.md->page:Eta.md',
|
|
2511
|
+
'page:Alpha.md->page:Iota.md',
|
|
2512
|
+
'page:Alpha.md->page:Kappa.md',
|
|
2513
|
+
'page:Alpha.md->page:Lambda.md',
|
|
2514
|
+
'page:Alpha.md->page:Theta.md',
|
|
2515
|
+
'page:Alpha.md->page:Zeta.md'
|
|
2516
|
+
]);
|
|
2517
|
+
const provenanceEdges = graph.edges
|
|
2518
|
+
.filter((edge) => edge.type === 'provenance')
|
|
2519
|
+
.map((edge) => `${edge.from}->${edge.to}`)
|
|
2520
|
+
.sort();
|
|
2521
|
+
assert.deepEqual(provenanceEdges, [
|
|
2522
|
+
'page:Alpha.md->source:src/a.ts',
|
|
2523
|
+
'page:Beta.md->source:src/a.ts',
|
|
2524
|
+
'page:Delta.md->documentation:docs/readme.md',
|
|
2525
|
+
'page:Delta.md->source:src/a.ts',
|
|
2526
|
+
'page:Gamma.md->documentation:docs/readme.md',
|
|
2527
|
+
'page:Gamma.md->source:src/gamma.ts'
|
|
2528
|
+
]);
|
|
2529
|
+
const sourceNodes = graph.nodes.filter((node) => node.kind !== 'page');
|
|
2530
|
+
const sourcePaths = sourceNodes.map((node) => node.path);
|
|
2531
|
+
assert.deepEqual(sourcePaths, [...sourcePaths].sort((left, right) => left.localeCompare(right)));
|
|
2532
|
+
}
|
|
2533
|
+
finally {
|
|
2534
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
2535
|
+
}
|
|
2536
|
+
});
|
|
2537
|
+
//# sourceMappingURL=compiler.test.js.map
|