doc-freshness-checker 2.0.22 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -3
- package/dist/cache/cacheManager.d.ts +20 -2
- package/dist/cache/cacheManager.js +137 -39
- package/dist/cache/cacheManager.js.map +1 -1
- package/dist/cache/cacheManager.test.js +132 -4
- package/dist/cache/cacheManager.test.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/manifests/manifestInventory.d.ts +28 -0
- package/dist/manifests/manifestInventory.js +225 -0
- package/dist/manifests/manifestInventory.js.map +1 -0
- package/dist/manifests/manifestInventory.test.d.ts +1 -0
- package/dist/manifests/manifestInventory.test.js +176 -0
- package/dist/manifests/manifestInventory.test.js.map +1 -0
- package/dist/manifests/validatorConstructors.test.d.ts +1 -0
- package/dist/manifests/validatorConstructors.test.js +17 -0
- package/dist/manifests/validatorConstructors.test.js.map +1 -0
- package/dist/parsers/documentParser.d.ts +2 -3
- package/dist/parsers/documentParser.js.map +1 -1
- package/dist/parsers/extractorInterface.test.d.ts +1 -0
- package/dist/parsers/extractorInterface.test.js +34 -0
- package/dist/parsers/extractorInterface.test.js.map +1 -0
- package/dist/parsers/extractors/codeSnippetExtractor.js.map +1 -1
- package/dist/plugins/plugin.d.ts +5 -1
- package/dist/plugins/plugin.js +5 -1
- package/dist/plugins/plugin.js.map +1 -1
- package/dist/reporters/consoleReporter.d.ts +5 -11
- package/dist/reporters/consoleReporter.js +74 -76
- package/dist/reporters/consoleReporter.js.map +1 -1
- package/dist/reporters/consoleReporter.test.js +22 -27
- package/dist/reporters/consoleReporter.test.js.map +1 -1
- package/dist/reporters/enhancedReporter.d.ts +51 -7
- package/dist/reporters/enhancedReporter.js +128 -74
- package/dist/reporters/enhancedReporter.js.map +1 -1
- package/dist/reporters/enhancedReporter.test.js +110 -122
- package/dist/reporters/enhancedReporter.test.js.map +1 -1
- package/dist/reporters/jsonReporter.d.ts +6 -6
- package/dist/reporters/jsonReporter.js +24 -14
- package/dist/reporters/jsonReporter.js.map +1 -1
- package/dist/reporters/jsonReporter.test.js +26 -11
- package/dist/reporters/jsonReporter.test.js.map +1 -1
- package/dist/reporters/markdownIssueCells.d.ts +7 -0
- package/dist/reporters/markdownIssueCells.js +9 -0
- package/dist/reporters/markdownIssueCells.js.map +1 -0
- package/dist/reporters/markdownReporter.d.ts +2 -7
- package/dist/reporters/markdownReporter.js +57 -56
- package/dist/reporters/markdownReporter.js.map +1 -1
- package/dist/reporters/markdownReporter.test.js +104 -76
- package/dist/reporters/markdownReporter.test.js.map +1 -1
- package/dist/reporters/publicReporterApi.test.d.ts +1 -0
- package/dist/reporters/publicReporterApi.test.js +28 -0
- package/dist/reporters/publicReporterApi.test.js.map +1 -0
- package/dist/reporters/reportContext.d.ts +12 -0
- package/dist/reporters/reportContext.js +8 -0
- package/dist/reporters/reportContext.js.map +1 -0
- package/dist/reporters/reportContext.test.d.ts +1 -0
- package/dist/reporters/reportContext.test.js +30 -0
- package/dist/reporters/reportContext.test.js.map +1 -0
- package/dist/reporters/reporters.characterization.test.d.ts +1 -0
- package/dist/reporters/reporters.characterization.test.js +208 -0
- package/dist/reporters/reporters.characterization.test.js.map +1 -0
- package/dist/runner.js +88 -85
- package/dist/runner.js.map +1 -1
- package/dist/runner.test.js +545 -56
- package/dist/runner.test.js.map +1 -1
- package/dist/semantic/vectorSearch.d.ts +3 -3
- package/dist/semantic/vectorSearch.js +29 -30
- package/dist/semantic/vectorSearch.js.map +1 -1
- package/dist/semantic/vectorSearch.test.js +53 -4
- package/dist/semantic/vectorSearch.test.js.map +1 -1
- package/dist/source/sourceIndex.d.ts +39 -0
- package/dist/source/sourceIndex.js +444 -0
- package/dist/source/sourceIndex.js.map +1 -0
- package/dist/source/sourceIndex.largeGlob.test.d.ts +1 -0
- package/dist/source/sourceIndex.largeGlob.test.js +29 -0
- package/dist/source/sourceIndex.largeGlob.test.js.map +1 -0
- package/dist/source/sourceIndex.test.d.ts +1 -0
- package/dist/source/sourceIndex.test.js +263 -0
- package/dist/source/sourceIndex.test.js.map +1 -0
- package/dist/source/sourceValidators.d.ts +7 -0
- package/dist/source/sourceValidators.js +8 -0
- package/dist/source/sourceValidators.js.map +1 -0
- package/dist/source/sourceValidators.test.d.ts +1 -0
- package/dist/source/sourceValidators.test.js +17 -0
- package/dist/source/sourceValidators.test.js.map +1 -0
- package/dist/types.d.ts +12 -2
- package/dist/utils/incremental.d.ts +3 -1
- package/dist/utils/incremental.js +32 -8
- package/dist/utils/incremental.js.map +1 -1
- package/dist/utils/incremental.test.js +17 -0
- package/dist/utils/incremental.test.js.map +1 -1
- package/dist/validators/codePatternValidator.d.ts +5 -11
- package/dist/validators/codePatternValidator.js +17 -145
- package/dist/validators/codePatternValidator.js.map +1 -1
- package/dist/validators/codeSnippetValidator.d.ts +5 -18
- package/dist/validators/codeSnippetValidator.js +19 -312
- package/dist/validators/codeSnippetValidator.js.map +1 -1
- package/dist/validators/dependencyValidator.d.ts +0 -5
- package/dist/validators/dependencyValidator.js +7 -64
- package/dist/validators/dependencyValidator.js.map +1 -1
- package/dist/validators/versionValidator.d.ts +1 -10
- package/dist/validators/versionValidator.js +7 -157
- package/dist/validators/versionValidator.js.map +1 -1
- package/dist/validators/versionValidator.test.js +1 -101
- package/dist/validators/versionValidator.test.js.map +1 -1
- package/package.json +3 -3
package/dist/runner.test.js
CHANGED
|
@@ -4,10 +4,16 @@ import path from 'path';
|
|
|
4
4
|
import { glob } from 'glob';
|
|
5
5
|
import { run, runWithConfig } from './runner.js';
|
|
6
6
|
import { BUILT_IN_RULE_TYPES } from './config/defaults.js';
|
|
7
|
+
import { GraphBuilder } from './graph/graphBuilder.js';
|
|
7
8
|
import { VectorSearch } from './semantic/vectorSearch.js';
|
|
9
|
+
import { SourceIndex } from './source/sourceIndex.js';
|
|
10
|
+
import { DocumentParser } from './parsers/documentParser.js';
|
|
11
|
+
import { ManifestInventory } from './manifests/manifestInventory.js';
|
|
8
12
|
import { ValidationEngine } from './validators/validationEngine.js';
|
|
9
13
|
import { IncrementalChecker } from './utils/incremental.js';
|
|
10
14
|
import { FileValidator } from './validators/fileValidator.js';
|
|
15
|
+
import { CacheManager } from './cache/cacheManager.js';
|
|
16
|
+
import { FreshnessScorer } from './scoring/freshnessScorer.js';
|
|
11
17
|
import { withOutputFile } from './test-utils/tempFiles.js';
|
|
12
18
|
import { captureConsoleLog, captureConsoleWarn } from './test-utils/console.js';
|
|
13
19
|
vi.mock('fastembed', () => ({
|
|
@@ -38,6 +44,9 @@ describe('runner', () => {
|
|
|
38
44
|
'.doc-freshness-cache/runner-vs-nograph',
|
|
39
45
|
'.doc-freshness-cache/runner-vs-reporters',
|
|
40
46
|
'.doc-freshness-cache/rv-clear',
|
|
47
|
+
'.doc-freshness-cache/runner-clear-root',
|
|
48
|
+
'.doc-freshness-cache/runner-policy-root',
|
|
49
|
+
'.doc-freshness-cache/runner-incremental-disabled',
|
|
41
50
|
];
|
|
42
51
|
const captureLog = captureConsoleLog;
|
|
43
52
|
const captureWarn = captureConsoleWarn;
|
|
@@ -76,7 +85,7 @@ describe('runner', () => {
|
|
|
76
85
|
sourcePatterns: [],
|
|
77
86
|
manifestFiles: [],
|
|
78
87
|
incremental: { enabled: true },
|
|
79
|
-
cache: { enabled:
|
|
88
|
+
cache: { enabled: true, dir: '.cache' },
|
|
80
89
|
...overrides,
|
|
81
90
|
rules: { ...baseConfig.rules, ...overrides.rules },
|
|
82
91
|
});
|
|
@@ -106,12 +115,14 @@ describe('runner', () => {
|
|
|
106
115
|
expect(results.documents).toEqual([]);
|
|
107
116
|
});
|
|
108
117
|
it('clears cache when clearCache is set', async () => {
|
|
109
|
-
const
|
|
118
|
+
const rootDir = path.join(cacheRoot, 'runner-clear-root');
|
|
119
|
+
const cacheDir = path.join(rootDir, '.result-cache');
|
|
110
120
|
await fs.promises.mkdir(cacheDir, { recursive: true });
|
|
111
121
|
await fs.promises.writeFile(path.join(cacheDir, 'dummy.json'), '{}');
|
|
112
122
|
await run({
|
|
113
123
|
...baseConfig,
|
|
114
|
-
|
|
124
|
+
rootDir,
|
|
125
|
+
cache: { enabled: false, dir: '.result-cache' },
|
|
115
126
|
clearCache: true,
|
|
116
127
|
});
|
|
117
128
|
const exists = await fs.promises
|
|
@@ -120,15 +131,71 @@ describe('runner', () => {
|
|
|
120
131
|
.catch(() => false);
|
|
121
132
|
expect(exists).toBe(false);
|
|
122
133
|
});
|
|
123
|
-
it('
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
await
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
134
|
+
it('ignores an unused outside cache directory when caching is disabled', async () => {
|
|
135
|
+
const outsideDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'doc-freshness-disabled-cache-'));
|
|
136
|
+
await expect(run({ ...baseConfig, cache: { enabled: false, dir: outsideDir } })).resolves.toBeDefined();
|
|
137
|
+
expect(await fs.promises.readdir(outsideDir)).toEqual([]);
|
|
138
|
+
await fs.promises.rm(outsideDir, { recursive: true, force: true });
|
|
139
|
+
});
|
|
140
|
+
it('continues when an enabled cache file is unreadable', async () => {
|
|
141
|
+
const cacheDir = '.doc-freshness-cache/runner-unreadable';
|
|
142
|
+
const readSpy = vi.spyOn(fs.promises, 'readFile').mockRejectedValueOnce(Object.assign(new Error('denied'), { code: 'EACCES' }));
|
|
143
|
+
await expect(run({ ...baseConfig, cache: { enabled: true, dir: cacheDir } })).resolves.toBeDefined();
|
|
144
|
+
readSpy.mockRestore();
|
|
145
|
+
await fs.promises.rm(path.join(process.cwd(), cacheDir), { recursive: true, force: true });
|
|
146
|
+
});
|
|
147
|
+
it('round-trips a two-method custom extractor through an overriding custom validator', async () => {
|
|
148
|
+
await fs.promises.mkdir(cacheRoot, { recursive: true });
|
|
149
|
+
await withOutputFile(cacheRoot, 'custom-extractor.md', async (filePath) => {
|
|
150
|
+
const content = '# Custom\n\nprose';
|
|
151
|
+
await fs.promises.writeFile(filePath, content, 'utf-8');
|
|
152
|
+
vi.mocked(glob).mockResolvedValueOnce([filePath]);
|
|
153
|
+
const reference = {
|
|
154
|
+
type: 'file-path',
|
|
155
|
+
value: 'prose',
|
|
156
|
+
lineNumber: 3,
|
|
157
|
+
raw: 'prose',
|
|
158
|
+
sourceFile: 'custom-extractor.md',
|
|
159
|
+
};
|
|
160
|
+
const customExtractor = {
|
|
161
|
+
supportsFormat: vi.fn((format) => format === 'markdown'),
|
|
162
|
+
extract: vi.fn(() => [reference]),
|
|
163
|
+
};
|
|
164
|
+
const invalidResult = {
|
|
165
|
+
reference,
|
|
166
|
+
valid: false,
|
|
167
|
+
severity: 'error',
|
|
168
|
+
message: 'Custom reference is stale',
|
|
169
|
+
};
|
|
170
|
+
const customValidator = {
|
|
171
|
+
validateBatch: vi.fn(async () => [invalidResult]),
|
|
172
|
+
};
|
|
173
|
+
const config = {
|
|
174
|
+
...baseConfig,
|
|
175
|
+
rootDir: cacheRoot,
|
|
176
|
+
include: ['**/*.md'],
|
|
177
|
+
rules: { ...baseConfig.rules, 'file-path': { enabled: true } },
|
|
178
|
+
customExtractors: [customExtractor],
|
|
179
|
+
customValidators: { 'file-path': customValidator },
|
|
180
|
+
};
|
|
181
|
+
const results = await run(config);
|
|
182
|
+
const document = {
|
|
183
|
+
path: 'custom-extractor.md',
|
|
184
|
+
absolutePath: filePath,
|
|
185
|
+
content,
|
|
186
|
+
format: 'markdown',
|
|
187
|
+
lines: ['# Custom', '', 'prose'],
|
|
188
|
+
references: [reference],
|
|
189
|
+
};
|
|
190
|
+
expect(vi.mocked(glob).mock.calls).toEqual([[['**/*.md'], { ignore: [], cwd: cacheRoot, absolute: true }]]);
|
|
191
|
+
expect(customExtractor.supportsFormat.mock.calls).toEqual([['markdown']]);
|
|
192
|
+
expect(customExtractor.extract.mock.calls).toEqual([[document]]);
|
|
193
|
+
expect(customValidator.validateBatch.mock.calls).toEqual([[[reference], document, config]]);
|
|
194
|
+
expect(results).toEqual({
|
|
195
|
+
documents: [{ path: 'custom-extractor.md', issues: [invalidResult] }],
|
|
196
|
+
summary: { total: 1, valid: 0, errors: 1, warnings: 0, info: 0, skipped: 0 },
|
|
197
|
+
});
|
|
130
198
|
});
|
|
131
|
-
expect(true).toBe(true);
|
|
132
199
|
});
|
|
133
200
|
it.each(['supportsFormat', 'extract'])('propagates custom extractor %s failures', async (hook) => {
|
|
134
201
|
vi.mocked(glob).mockResolvedValueOnce([path.join(process.cwd(), 'README.md')]);
|
|
@@ -175,9 +242,7 @@ describe('runner', () => {
|
|
|
175
242
|
await expect(run({
|
|
176
243
|
...baseConfig,
|
|
177
244
|
rules: { ...baseConfig.rules, 'file-path': { enabled: true } },
|
|
178
|
-
customExtractors: [
|
|
179
|
-
{ extract: () => [reference], supportsFormat: () => true },
|
|
180
|
-
],
|
|
245
|
+
customExtractors: [{ extract: () => [reference], supportsFormat: () => true }],
|
|
181
246
|
})).rejects.toThrow('file validator crashed');
|
|
182
247
|
}
|
|
183
248
|
finally {
|
|
@@ -218,6 +283,73 @@ describe('runner', () => {
|
|
|
218
283
|
});
|
|
219
284
|
});
|
|
220
285
|
});
|
|
286
|
+
describe('shared manifest inventory', () => {
|
|
287
|
+
afterEach(() => vi.restoreAllMocks());
|
|
288
|
+
const manifestDocument = {
|
|
289
|
+
path: 'doc.md',
|
|
290
|
+
absolutePath: '/doc.md',
|
|
291
|
+
content: '',
|
|
292
|
+
format: 'markdown',
|
|
293
|
+
lines: [],
|
|
294
|
+
references: [
|
|
295
|
+
{
|
|
296
|
+
type: 'version',
|
|
297
|
+
value: 'TypeScript 5.0',
|
|
298
|
+
technology: 'typescript',
|
|
299
|
+
version: '5.0',
|
|
300
|
+
lineNumber: 1,
|
|
301
|
+
raw: 'TypeScript 5.0',
|
|
302
|
+
sourceFile: 'doc.md',
|
|
303
|
+
},
|
|
304
|
+
{ type: 'dependency', value: 'typescript', lineNumber: 2, raw: 'typescript', sourceFile: 'doc.md' },
|
|
305
|
+
],
|
|
306
|
+
};
|
|
307
|
+
it('reads a manifest once for both built-in validators', async () => {
|
|
308
|
+
const rootDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'doc-freshness-runner-manifest-'));
|
|
309
|
+
await fs.promises.writeFile(path.join(rootDir, 'package.json'), JSON.stringify({ dependencies: { typescript: '5.9.0' } }));
|
|
310
|
+
vi.spyOn(DocumentParser.prototype, 'scanDocuments').mockResolvedValue([manifestDocument]);
|
|
311
|
+
const readFile = vi.spyOn(fs.promises, 'readFile');
|
|
312
|
+
try {
|
|
313
|
+
const results = await run({
|
|
314
|
+
...baseConfig,
|
|
315
|
+
rootDir,
|
|
316
|
+
manifestFiles: ['package.json'],
|
|
317
|
+
rules: { ...baseConfig.rules, version: { enabled: true }, dependency: { enabled: true } },
|
|
318
|
+
});
|
|
319
|
+
expect(results.summary).toMatchObject({ total: 2, valid: 2 });
|
|
320
|
+
expect(readFile).toHaveBeenCalledOnce();
|
|
321
|
+
}
|
|
322
|
+
finally {
|
|
323
|
+
await fs.promises.rm(rootDir, { recursive: true, force: true });
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
it('does no manifest work when there are no references or both rules are disabled', async () => {
|
|
327
|
+
const dependencyNames = vi.spyOn(ManifestInventory.prototype, 'dependencyNames');
|
|
328
|
+
const packageVersions = vi.spyOn(ManifestInventory.prototype, 'packageVersions');
|
|
329
|
+
await run(baseConfig);
|
|
330
|
+
vi.spyOn(DocumentParser.prototype, 'scanDocuments').mockResolvedValue([manifestDocument]);
|
|
331
|
+
await run(baseConfig);
|
|
332
|
+
expect(dependencyNames).not.toHaveBeenCalled();
|
|
333
|
+
expect(packageVersions).not.toHaveBeenCalled();
|
|
334
|
+
});
|
|
335
|
+
it('does no built-in manifest work when custom validators replace both types', async () => {
|
|
336
|
+
vi.spyOn(DocumentParser.prototype, 'scanDocuments').mockResolvedValue([manifestDocument]);
|
|
337
|
+
const dependencyNames = vi.spyOn(ManifestInventory.prototype, 'dependencyNames');
|
|
338
|
+
const packageVersions = vi.spyOn(ManifestInventory.prototype, 'packageVersions');
|
|
339
|
+
const validateBatch = vi.fn().mockResolvedValue([]);
|
|
340
|
+
await run({
|
|
341
|
+
...baseConfig,
|
|
342
|
+
rules: { ...baseConfig.rules, version: { enabled: true }, dependency: { enabled: true } },
|
|
343
|
+
customValidators: {
|
|
344
|
+
version: { validateBatch },
|
|
345
|
+
dependency: { validateBatch },
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
expect(validateBatch).toHaveBeenCalledTimes(2);
|
|
349
|
+
expect(dependencyNames).not.toHaveBeenCalled();
|
|
350
|
+
expect(packageVersions).not.toHaveBeenCalled();
|
|
351
|
+
});
|
|
352
|
+
});
|
|
221
353
|
describe('verbose mode', () => {
|
|
222
354
|
it('logs config file path and source patterns', async () => {
|
|
223
355
|
const spy = captureLog();
|
|
@@ -252,9 +384,11 @@ describe('runner', () => {
|
|
|
252
384
|
});
|
|
253
385
|
});
|
|
254
386
|
describe('reporters', () => {
|
|
255
|
-
it('
|
|
387
|
+
it('routes the Console emitter to stdout incrementally', async () => {
|
|
256
388
|
const spy = captureLog();
|
|
257
389
|
await run({ ...baseConfig, reporters: ['console'] });
|
|
390
|
+
expect(spy.mock.calls.length).toBeGreaterThan(1);
|
|
391
|
+
expect(spy.mock.calls.every((call) => call.length === 1)).toBe(true);
|
|
258
392
|
expect(spy.mock.calls.flat().join('\n')).toContain('Documentation Freshness Report');
|
|
259
393
|
});
|
|
260
394
|
it('falls back to console when the reporter list is empty', async () => {
|
|
@@ -312,50 +446,255 @@ describe('runner', () => {
|
|
|
312
446
|
expect(spy.mock.calls.flat().join('\n')).toContain('written to');
|
|
313
447
|
});
|
|
314
448
|
});
|
|
449
|
+
it('defaults an absent reporter list to Console', async () => {
|
|
450
|
+
const log = captureLog();
|
|
451
|
+
await run({ ...baseConfig, reporters: undefined });
|
|
452
|
+
expect(log.mock.calls.flat().join('\n')).toContain('Documentation Freshness Report');
|
|
453
|
+
});
|
|
454
|
+
it('preserves configured reporter order and duplicates', async () => {
|
|
455
|
+
const log = captureLog();
|
|
456
|
+
await run({ ...baseConfig, reporters: ['json', 'markdown', 'json'] });
|
|
457
|
+
const reports = log.mock.calls
|
|
458
|
+
.map(([value]) => value)
|
|
459
|
+
.filter((value) => typeof value === 'string' && (value.startsWith('{') || value.startsWith('#')));
|
|
460
|
+
expect(reports).toHaveLength(3);
|
|
461
|
+
expect(reports[0]).toMatch(/^\{/);
|
|
462
|
+
expect(reports[1]).toMatch(/^# Documentation Freshness Report/);
|
|
463
|
+
expect(reports[2]).toMatch(/^\{/);
|
|
464
|
+
});
|
|
465
|
+
it('treats inherited registry keys as unknown', async () => {
|
|
466
|
+
const inheritedKeys = ['toString', 'constructor', '__proto__'];
|
|
467
|
+
const log = captureLog();
|
|
468
|
+
const warn = captureWarn();
|
|
469
|
+
await expect(run({ ...baseConfig, reporters: inheritedKeys })).resolves.toBeDefined();
|
|
470
|
+
expect(warn.mock.calls).toEqual([
|
|
471
|
+
['Unknown reporter type: toString'],
|
|
472
|
+
['Unknown reporter type: constructor'],
|
|
473
|
+
['Unknown reporter type: __proto__'],
|
|
474
|
+
]);
|
|
475
|
+
expect(log.mock.calls.some(([value]) => typeof value === 'string' && value.startsWith('{'))).toBe(false);
|
|
476
|
+
const output = log.mock.calls.flat().join('\n');
|
|
477
|
+
expect(output).not.toContain('Documentation Freshness Report');
|
|
478
|
+
expect(output).not.toContain('Documentation Freshness Scan Report');
|
|
479
|
+
});
|
|
480
|
+
it('keeps Console on stdout and ignores outputPath', async () => {
|
|
481
|
+
await withOutputFile(cacheRoot, 'console-must-not-write.out', async (outputPath) => {
|
|
482
|
+
const log = captureLog();
|
|
483
|
+
await run({ ...baseConfig, reporters: ['console'], outputPath });
|
|
484
|
+
expect(log.mock.calls.flat().join('\n')).toContain('Documentation Freshness Report');
|
|
485
|
+
await expect(fs.promises.access(outputPath)).rejects.toThrow();
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
it('overwrites a shared outputPath sequentially so the last string reporter wins', async () => {
|
|
489
|
+
await withOutputFile(cacheRoot, 'shared-reporter.out', async (outputPath) => {
|
|
490
|
+
captureLog();
|
|
491
|
+
await run({ ...baseConfig, reporters: ['json', 'markdown'], outputPath });
|
|
492
|
+
expect(await fs.promises.readFile(outputPath, 'utf-8')).toMatch(/^# Documentation Freshness Report/);
|
|
493
|
+
await run({ ...baseConfig, reporters: ['markdown', 'json'], outputPath });
|
|
494
|
+
expect(await fs.promises.readFile(outputPath, 'utf-8')).toMatch(/^\{/);
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
it('creates recursive output directories and reports each verbose label', async () => {
|
|
498
|
+
const outputRoot = path.join(cacheRoot, 'reporter-routing');
|
|
499
|
+
const outputPath = path.join(outputRoot, 'nested', 'report.out');
|
|
500
|
+
const log = captureLog();
|
|
501
|
+
try {
|
|
502
|
+
await run({ ...baseConfig, reporters: ['json', 'markdown'], outputPath, verbose: true });
|
|
503
|
+
expect(await fs.promises.readFile(outputPath, 'utf-8')).toMatch(/^# Documentation Freshness Report/);
|
|
504
|
+
expect(log.mock.calls.flat()).toContain(`JSON report written to ${outputPath}`);
|
|
505
|
+
expect(log.mock.calls.flat()).toContain(`Markdown report written to ${outputPath}`);
|
|
506
|
+
}
|
|
507
|
+
finally {
|
|
508
|
+
await fs.promises.rm(outputRoot, { recursive: true, force: true });
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
it('rejects when writing a string report fails', async () => {
|
|
512
|
+
const outputPath = path.join(cacheRoot, 'reporter-write-directory');
|
|
513
|
+
await fs.promises.mkdir(outputPath, { recursive: true });
|
|
514
|
+
try {
|
|
515
|
+
captureLog();
|
|
516
|
+
await expect(run({ ...baseConfig, reporters: ['json'], outputPath })).rejects.toThrow();
|
|
517
|
+
}
|
|
518
|
+
finally {
|
|
519
|
+
await fs.promises.rm(outputPath, { recursive: true, force: true });
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
it('reads the clock only for timestamped reporter paths and propagates clock failures', async () => {
|
|
523
|
+
const toISOString = vi.spyOn(Date.prototype, 'toISOString').mockImplementation(() => {
|
|
524
|
+
throw new Error('timestamp failed');
|
|
525
|
+
});
|
|
526
|
+
const log = captureLog();
|
|
527
|
+
try {
|
|
528
|
+
await expect(run({ ...baseConfig, reporters: ['console'] })).resolves.toBeDefined();
|
|
529
|
+
await expect(run({ ...baseConfig, reporters: ['json'] })).resolves.toBeDefined();
|
|
530
|
+
expect(toISOString).not.toHaveBeenCalled();
|
|
531
|
+
expect(log).toHaveBeenCalled();
|
|
532
|
+
await expect(run({ ...baseConfig, reporters: ['markdown'] })).rejects.toThrow('timestamp failed');
|
|
533
|
+
}
|
|
534
|
+
finally {
|
|
535
|
+
toISOString.mockRestore();
|
|
536
|
+
}
|
|
537
|
+
});
|
|
315
538
|
});
|
|
316
539
|
describe('graph and scoring', () => {
|
|
317
|
-
it('builds graph
|
|
540
|
+
it('builds graph without persisting an unused graph cache', async () => {
|
|
318
541
|
captureLog();
|
|
319
542
|
const cacheDir = '.doc-freshness-cache/runner-graph';
|
|
320
543
|
try {
|
|
321
544
|
await run({ ...baseConfig, graph: { enabled: true }, cache: { enabled: true, dir: cacheDir } });
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
.then(() => true)
|
|
325
|
-
.catch(() => false);
|
|
326
|
-
expect(exists).toBe(true);
|
|
545
|
+
await expect(fs.promises.access(path.join(process.cwd(), cacheDir, 'graph-cache.json'))).rejects.toThrow();
|
|
546
|
+
await expect(fs.promises.access(path.join(process.cwd(), cacheDir, 'url-cache.json'))).resolves.toBeUndefined();
|
|
327
547
|
}
|
|
328
548
|
finally {
|
|
329
549
|
await fs.promises.rm(path.join(process.cwd(), cacheDir), { recursive: true, force: true }).catch(() => { });
|
|
330
550
|
}
|
|
331
551
|
});
|
|
332
|
-
it('
|
|
333
|
-
|
|
552
|
+
it('does not read the clock when runner scored-result spreading fails', async () => {
|
|
553
|
+
const validationResults = {
|
|
554
|
+
documents: [],
|
|
555
|
+
summary: { total: 0, valid: 0, errors: 0, warnings: 0, skipped: 0 },
|
|
556
|
+
};
|
|
557
|
+
const proxiedResults = new Proxy(validationResults, {
|
|
558
|
+
get(target, property, receiver) {
|
|
559
|
+
if (property === 'documents') {
|
|
560
|
+
throw new Error('runner result getter failed');
|
|
561
|
+
}
|
|
562
|
+
return Reflect.get(target, property, receiver);
|
|
563
|
+
},
|
|
564
|
+
});
|
|
565
|
+
const scores = {
|
|
566
|
+
projectScore: 100,
|
|
567
|
+
projectGrade: 'A',
|
|
568
|
+
documents: [],
|
|
569
|
+
summary: { total: 0, gradeA: 0, gradeB: 0, gradeC: 0, gradeD: 0, gradeF: 0 },
|
|
570
|
+
};
|
|
571
|
+
const validate = vi.spyOn(ValidationEngine.prototype, 'validate').mockResolvedValue(proxiedResults);
|
|
572
|
+
const calculateScores = vi.spyOn(FreshnessScorer.prototype, 'calculateProjectScores').mockReturnValue(scores);
|
|
573
|
+
const clock = vi.spyOn(Date.prototype, 'toISOString');
|
|
574
|
+
try {
|
|
334
575
|
captureLog();
|
|
335
|
-
await run({
|
|
576
|
+
await expect(run({
|
|
336
577
|
...baseConfig,
|
|
337
578
|
reporters: ['json'],
|
|
338
|
-
outputPath,
|
|
339
579
|
graph: { enabled: true },
|
|
340
580
|
freshnessScoring: { enabled: true },
|
|
341
581
|
cache: { enabled: false },
|
|
342
|
-
});
|
|
343
|
-
expect(
|
|
344
|
-
}
|
|
582
|
+
})).rejects.toThrow('runner result getter failed');
|
|
583
|
+
expect(clock).not.toHaveBeenCalled();
|
|
584
|
+
}
|
|
585
|
+
finally {
|
|
586
|
+
validate.mockRestore();
|
|
587
|
+
calculateScores.mockRestore();
|
|
588
|
+
clock.mockRestore();
|
|
589
|
+
}
|
|
345
590
|
});
|
|
346
|
-
it('
|
|
347
|
-
|
|
348
|
-
|
|
591
|
+
it('captures Markdown result references before the clock, then renders the base before scores', async () => {
|
|
592
|
+
const events = [];
|
|
593
|
+
const summary = { total: 1, valid: 1, errors: 0, warnings: 0, skipped: 0 };
|
|
594
|
+
const documents = [];
|
|
595
|
+
let currentSummary = summary;
|
|
596
|
+
let currentDocuments = documents;
|
|
597
|
+
const validationResults = {
|
|
598
|
+
get summary() {
|
|
599
|
+
events.push('results:summary');
|
|
600
|
+
return currentSummary;
|
|
601
|
+
},
|
|
602
|
+
set summary(value) {
|
|
603
|
+
currentSummary = value;
|
|
604
|
+
},
|
|
605
|
+
get documents() {
|
|
606
|
+
events.push('results:documents');
|
|
607
|
+
return currentDocuments;
|
|
608
|
+
},
|
|
609
|
+
set documents(value) {
|
|
610
|
+
currentDocuments = value;
|
|
611
|
+
},
|
|
612
|
+
};
|
|
613
|
+
const scores = {
|
|
614
|
+
get projectScore() {
|
|
615
|
+
events.push('scores:projectScore');
|
|
616
|
+
summary.total = 99;
|
|
617
|
+
return 100;
|
|
618
|
+
},
|
|
619
|
+
projectGrade: 'A',
|
|
620
|
+
documents: [],
|
|
621
|
+
summary: { total: 0, gradeA: 0, gradeB: 0, gradeC: 0, gradeD: 0, gradeF: 0 },
|
|
622
|
+
};
|
|
623
|
+
const validate = vi.spyOn(ValidationEngine.prototype, 'validate').mockResolvedValue(validationResults);
|
|
624
|
+
const calculateScores = vi.spyOn(FreshnessScorer.prototype, 'calculateProjectScores').mockReturnValue(scores);
|
|
625
|
+
const clock = vi.spyOn(Date.prototype, 'toISOString').mockImplementation(() => {
|
|
626
|
+
events.push('clock');
|
|
627
|
+
summary.total = 7;
|
|
628
|
+
documents.push({ path: 'docs/captured.md', issues: [] });
|
|
629
|
+
validationResults.summary = { total: 50, valid: 50, errors: 0, warnings: 0, skipped: 0 };
|
|
630
|
+
validationResults.documents = [];
|
|
631
|
+
return '2025-01-02T03:04:05.678Z';
|
|
632
|
+
});
|
|
633
|
+
try {
|
|
634
|
+
const log = captureLog();
|
|
349
635
|
await run({
|
|
350
636
|
...baseConfig,
|
|
351
637
|
reporters: ['markdown'],
|
|
352
|
-
outputPath,
|
|
353
638
|
graph: { enabled: true },
|
|
354
639
|
freshnessScoring: { enabled: true },
|
|
355
640
|
cache: { enabled: false },
|
|
356
641
|
});
|
|
357
|
-
|
|
358
|
-
|
|
642
|
+
const report = log.mock.calls.flat().find((value) => typeof value === 'string' && value.startsWith('#'));
|
|
643
|
+
expect(events).toEqual(['results:summary', 'results:documents', 'clock', 'scores:projectScore']);
|
|
644
|
+
expect(report).toContain('| Total Checked | 7 |');
|
|
645
|
+
expect(report).toContain('docs/captured.md');
|
|
646
|
+
expect(report).not.toContain('| Total Checked | 99 |');
|
|
647
|
+
expect(report).not.toContain('| Total Checked | 50 |');
|
|
648
|
+
}
|
|
649
|
+
finally {
|
|
650
|
+
validate.mockRestore();
|
|
651
|
+
calculateScores.mockRestore();
|
|
652
|
+
clock.mockRestore();
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
it('does not inspect Markdown documents, clock, or scores when the summary getter fails', async () => {
|
|
656
|
+
const events = [];
|
|
657
|
+
let scoreRead = false;
|
|
658
|
+
const validationResults = {
|
|
659
|
+
get summary() {
|
|
660
|
+
events.push('results:summary');
|
|
661
|
+
throw new Error('Markdown summary failed');
|
|
662
|
+
},
|
|
663
|
+
get documents() {
|
|
664
|
+
events.push('results:documents');
|
|
665
|
+
return [];
|
|
666
|
+
},
|
|
667
|
+
};
|
|
668
|
+
const scores = {
|
|
669
|
+
get projectScore() {
|
|
670
|
+
scoreRead = true;
|
|
671
|
+
return 100;
|
|
672
|
+
},
|
|
673
|
+
projectGrade: 'A',
|
|
674
|
+
documents: [],
|
|
675
|
+
summary: { total: 0, gradeA: 0, gradeB: 0, gradeC: 0, gradeD: 0, gradeF: 0 },
|
|
676
|
+
};
|
|
677
|
+
const validate = vi.spyOn(ValidationEngine.prototype, 'validate').mockResolvedValue(validationResults);
|
|
678
|
+
const calculateScores = vi.spyOn(FreshnessScorer.prototype, 'calculateProjectScores').mockReturnValue(scores);
|
|
679
|
+
const clock = vi.spyOn(Date.prototype, 'toISOString');
|
|
680
|
+
try {
|
|
681
|
+
captureLog();
|
|
682
|
+
await expect(run({
|
|
683
|
+
...baseConfig,
|
|
684
|
+
reporters: ['markdown'],
|
|
685
|
+
graph: { enabled: true },
|
|
686
|
+
freshnessScoring: { enabled: true },
|
|
687
|
+
cache: { enabled: false },
|
|
688
|
+
})).rejects.toThrow('Markdown summary failed');
|
|
689
|
+
expect(events).toEqual(['results:summary']);
|
|
690
|
+
expect(clock).not.toHaveBeenCalled();
|
|
691
|
+
expect(scoreRead).toBe(false);
|
|
692
|
+
}
|
|
693
|
+
finally {
|
|
694
|
+
validate.mockRestore();
|
|
695
|
+
calculateScores.mockRestore();
|
|
696
|
+
clock.mockRestore();
|
|
697
|
+
}
|
|
359
698
|
});
|
|
360
699
|
it('generates enhanced with scores to file', async () => {
|
|
361
700
|
await withOutputFile(cacheRoot, 'test-enhanced-scored.md', async (outputPath) => {
|
|
@@ -372,6 +711,50 @@ describe('runner', () => {
|
|
|
372
711
|
});
|
|
373
712
|
});
|
|
374
713
|
});
|
|
714
|
+
describe('result cache policy', () => {
|
|
715
|
+
const modes = [false, true].flatMap((cache) => [false, true].flatMap((graph) => [false, true].flatMap((incremental) => [false, true].map((vector) => ({ cache, graph, incremental, vector })))));
|
|
716
|
+
it.each(modes)('applies cache=$cache graph=$graph incremental=$incremental vector=$vector under rootDir', async ({ cache, graph, incremental, vector }) => {
|
|
717
|
+
captureLog();
|
|
718
|
+
const rootDir = path.join(cacheRoot, 'runner-policy-root', `${Number(cache)}${Number(graph)}${Number(incremental)}${Number(vector)}`);
|
|
719
|
+
const resultDir = path.join(rootDir, '.result-cache');
|
|
720
|
+
const docPath = path.join(rootDir, 'README.md');
|
|
721
|
+
await fs.promises.mkdir(rootDir, { recursive: true });
|
|
722
|
+
await fs.promises.writeFile(docPath, '# Test');
|
|
723
|
+
vi.mocked(glob).mockResolvedValueOnce([docPath]);
|
|
724
|
+
await run({
|
|
725
|
+
...baseConfig,
|
|
726
|
+
rootDir,
|
|
727
|
+
include: ['README.md'],
|
|
728
|
+
cache: { enabled: cache, dir: '.result-cache' },
|
|
729
|
+
graph: { enabled: graph },
|
|
730
|
+
incremental: { enabled: incremental },
|
|
731
|
+
vectorSearch: { enabled: vector },
|
|
732
|
+
});
|
|
733
|
+
const exists = (file) => fs.promises
|
|
734
|
+
.access(path.join(resultDir, file))
|
|
735
|
+
.then(() => true)
|
|
736
|
+
.catch(() => false);
|
|
737
|
+
expect(await exists('url-cache.json')).toBe(cache);
|
|
738
|
+
expect(await exists('file-hashes.json')).toBe(cache && incremental);
|
|
739
|
+
expect(await exists('embedding-cache.json')).toBe(cache && vector);
|
|
740
|
+
expect(await exists('graph-cache.json')).toBe(false);
|
|
741
|
+
if (!cache) {
|
|
742
|
+
await expect(fs.promises.access(resultDir)).rejects.toThrow();
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
it('continues when the URL cache cannot be saved', async () => {
|
|
746
|
+
const saveSpy = vi.spyOn(CacheManager.prototype, 'saveUrlCache').mockRejectedValueOnce(new Error('read only'));
|
|
747
|
+
const warnSpy = captureWarn();
|
|
748
|
+
try {
|
|
749
|
+
await expect(run({ ...baseConfig, cache: { enabled: true }, verbose: true })).resolves.toBeDefined();
|
|
750
|
+
expect(warnSpy).toHaveBeenCalledWith('Could not save the URL cache: read only');
|
|
751
|
+
}
|
|
752
|
+
finally {
|
|
753
|
+
saveSpy.mockRestore();
|
|
754
|
+
warnSpy.mockRestore();
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
});
|
|
375
758
|
describe('incremental mode', () => {
|
|
376
759
|
it('reports changed files in verbose mode', async () => {
|
|
377
760
|
const spy = captureLog();
|
|
@@ -394,7 +777,7 @@ describe('runner', () => {
|
|
|
394
777
|
include: ['*.md'],
|
|
395
778
|
incremental: { enabled: true },
|
|
396
779
|
graph: { enabled: false },
|
|
397
|
-
cache: { enabled:
|
|
780
|
+
cache: { enabled: true, dir: '.cache' },
|
|
398
781
|
reporters: [],
|
|
399
782
|
};
|
|
400
783
|
mockDocumentScan(docPath);
|
|
@@ -629,7 +1012,7 @@ describe('runner', () => {
|
|
|
629
1012
|
const config = incrementalConfig(rootDir, {
|
|
630
1013
|
manifestFiles: ['package.json'],
|
|
631
1014
|
rules: { dependency: { enabled: true, severity: 'info' } },
|
|
632
|
-
cache: { enabled:
|
|
1015
|
+
cache: { enabled: true, dir: cacheDir },
|
|
633
1016
|
});
|
|
634
1017
|
mockDocumentScan(docPath);
|
|
635
1018
|
expect((await run(config)).summary.total).toBe(1);
|
|
@@ -760,7 +1143,7 @@ describe('runner', () => {
|
|
|
760
1143
|
await fs.promises.writeFile(docPath, 'No references');
|
|
761
1144
|
const config = incrementalConfig(rootDir, {
|
|
762
1145
|
rules: { 'file-path': { enabled: true } },
|
|
763
|
-
cache: { enabled:
|
|
1146
|
+
cache: { enabled: true, dir: cacheDir },
|
|
764
1147
|
});
|
|
765
1148
|
mockDocumentScan(docPath);
|
|
766
1149
|
await run(config);
|
|
@@ -787,14 +1170,41 @@ describe('runner', () => {
|
|
|
787
1170
|
expect((await run(config)).summary.errors).toBe(1);
|
|
788
1171
|
});
|
|
789
1172
|
});
|
|
1173
|
+
it('checks every document without reading or saving state when caching is disabled', async () => {
|
|
1174
|
+
const rootDir = path.join(cacheRoot, 'runner-incremental-disabled');
|
|
1175
|
+
const docPath = path.join(rootDir, 'README.md');
|
|
1176
|
+
const stateFile = path.join(rootDir, '.result-cache', 'file-hashes.json');
|
|
1177
|
+
await fs.promises.mkdir(path.dirname(stateFile), { recursive: true });
|
|
1178
|
+
await fs.promises.writeFile(docPath, '# Test');
|
|
1179
|
+
await fs.promises.writeFile(stateFile, 'sentinel');
|
|
1180
|
+
vi.mocked(glob).mockResolvedValueOnce([docPath]);
|
|
1181
|
+
const readSpy = vi.spyOn(fs.promises, 'readFile');
|
|
1182
|
+
const spy = captureLog();
|
|
1183
|
+
await run({
|
|
1184
|
+
...baseConfig,
|
|
1185
|
+
rootDir,
|
|
1186
|
+
include: ['README.md'],
|
|
1187
|
+
incremental: { enabled: true },
|
|
1188
|
+
cache: { enabled: false, dir: '.result-cache' },
|
|
1189
|
+
verbose: true,
|
|
1190
|
+
});
|
|
1191
|
+
expect(spy.mock.calls.flat().join('\n')).toContain('checking 1 changed files, skipping 0 unchanged');
|
|
1192
|
+
expect(readSpy).not.toHaveBeenCalledWith(stateFile, 'utf-8');
|
|
1193
|
+
expect(await fs.promises.readFile(stateFile, 'utf-8')).toBe('sentinel');
|
|
1194
|
+
readSpy.mockRestore();
|
|
1195
|
+
await fs.promises.rm(rootDir, { recursive: true, force: true });
|
|
1196
|
+
});
|
|
790
1197
|
});
|
|
791
|
-
it('loads URL cache when
|
|
1198
|
+
it('loads and saves URL cache when graph is disabled', async () => {
|
|
792
1199
|
const cacheDir = '.doc-freshness-cache/runner-url';
|
|
793
1200
|
const fullDir = path.join(process.cwd(), cacheDir);
|
|
1201
|
+
const cacheFile = path.join(fullDir, 'url-cache.json');
|
|
1202
|
+
const cached = { 'https://cached.example': { result: { valid: true }, timestamp: Date.now() } };
|
|
794
1203
|
await fs.promises.mkdir(fullDir, { recursive: true });
|
|
795
|
-
await fs.promises.writeFile(
|
|
1204
|
+
await fs.promises.writeFile(cacheFile, JSON.stringify(cached));
|
|
796
1205
|
try {
|
|
797
1206
|
expect(await run({ ...baseConfig, cache: { enabled: true, dir: cacheDir } })).toBeDefined();
|
|
1207
|
+
expect(await fs.promises.readFile(cacheFile, 'utf-8')).toBe(JSON.stringify(cached, null, 2));
|
|
798
1208
|
}
|
|
799
1209
|
finally {
|
|
800
1210
|
await fs.promises.rm(fullDir, { recursive: true, force: true }).catch(() => { });
|
|
@@ -854,6 +1264,7 @@ describe('runner', () => {
|
|
|
854
1264
|
const output = spy.mock.calls.flat().join('\n');
|
|
855
1265
|
expect(output).toContain('Indexing documentation');
|
|
856
1266
|
expect(output).toContain('Finding semantic mismatches');
|
|
1267
|
+
expect(output).not.toContain('Building source code index');
|
|
857
1268
|
});
|
|
858
1269
|
});
|
|
859
1270
|
describe('runWithConfig', () => {
|
|
@@ -869,6 +1280,7 @@ describe('runner', () => {
|
|
|
869
1280
|
});
|
|
870
1281
|
});
|
|
871
1282
|
describe('vector search without prior graph', () => {
|
|
1283
|
+
afterEach(() => vi.restoreAllMocks());
|
|
872
1284
|
it('builds source index independently when graph not enabled', async () => {
|
|
873
1285
|
const spy = captureLog();
|
|
874
1286
|
await run({
|
|
@@ -881,36 +1293,113 @@ describe('runner', () => {
|
|
|
881
1293
|
const output = spy.mock.calls.flat().join('\n');
|
|
882
1294
|
expect(output).toContain('Building source code index');
|
|
883
1295
|
});
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1296
|
+
it('does not report index building when code-pattern validation already loaded it', async () => {
|
|
1297
|
+
vi.spyOn(DocumentParser.prototype, 'scanDocuments').mockResolvedValue([
|
|
1298
|
+
{
|
|
1299
|
+
path: 'doc.md',
|
|
1300
|
+
absolutePath: '/doc.md',
|
|
1301
|
+
content: '',
|
|
1302
|
+
format: 'markdown',
|
|
1303
|
+
lines: [],
|
|
1304
|
+
references: [{ type: 'code-pattern', value: 'RealSymbol', lineNumber: 1, raw: 'RealSymbol', sourceFile: 'doc.md' }],
|
|
1305
|
+
},
|
|
1306
|
+
]);
|
|
887
1307
|
const spy = captureLog();
|
|
888
1308
|
await run({
|
|
889
1309
|
...baseConfig,
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1310
|
+
rules: { ...baseConfig.rules, 'code-pattern': { enabled: true } },
|
|
1311
|
+
vectorSearch: { enabled: true },
|
|
1312
|
+
graph: { enabled: false },
|
|
1313
|
+
verbose: true,
|
|
894
1314
|
});
|
|
895
|
-
expect(spy.mock.calls.flat().join('\n')).toContain('
|
|
1315
|
+
expect(spy.mock.calls.flat().join('\n')).not.toContain('Building source code index');
|
|
896
1316
|
});
|
|
897
|
-
|
|
898
|
-
|
|
1317
|
+
});
|
|
1318
|
+
describe('shared source index', () => {
|
|
1319
|
+
afterEach(() => vi.restoreAllMocks());
|
|
1320
|
+
it('does not load source when no validator, graph, or vector consumer needs it', async () => {
|
|
1321
|
+
const load = vi.spyOn(SourceIndex.prototype, 'load');
|
|
1322
|
+
await run(baseConfig);
|
|
1323
|
+
expect(load).not.toHaveBeenCalled();
|
|
1324
|
+
});
|
|
1325
|
+
it('uses one index instance for both built-in validators', async () => {
|
|
1326
|
+
vi.spyOn(DocumentParser.prototype, 'scanDocuments').mockResolvedValue([
|
|
1327
|
+
{
|
|
1328
|
+
path: 'doc.md',
|
|
1329
|
+
absolutePath: '/doc.md',
|
|
1330
|
+
content: '',
|
|
1331
|
+
format: 'markdown',
|
|
1332
|
+
lines: [],
|
|
1333
|
+
references: [
|
|
1334
|
+
{ type: 'code-pattern', value: 'RealSymbol', lineNumber: 1, raw: 'RealSymbol', sourceFile: 'doc.md' },
|
|
1335
|
+
{ type: 'code-snippet', kind: 'function-call', value: 'realFunction', lineNumber: 2, raw: '', sourceFile: 'doc.md' },
|
|
1336
|
+
],
|
|
1337
|
+
},
|
|
1338
|
+
]);
|
|
1339
|
+
const load = vi.spyOn(SourceIndex.prototype, 'load');
|
|
1340
|
+
await run({
|
|
1341
|
+
...baseConfig,
|
|
1342
|
+
rules: {
|
|
1343
|
+
...baseConfig.rules,
|
|
1344
|
+
'code-pattern': { enabled: true },
|
|
1345
|
+
'code-snippet': { enabled: true },
|
|
1346
|
+
},
|
|
1347
|
+
});
|
|
1348
|
+
expect(load).toHaveBeenCalledTimes(2);
|
|
1349
|
+
expect(new Set(load.mock.contexts).size).toBe(1);
|
|
1350
|
+
});
|
|
1351
|
+
it('feeds symbols to graph and all pattern files to vector search', async () => {
|
|
1352
|
+
const symbols = new Map();
|
|
1353
|
+
const patternFiles = new Map([['src/no-symbol.ts', { content: '// a useful comment', language: 'typescript' }]]);
|
|
1354
|
+
const snapshot = {
|
|
1355
|
+
symbols,
|
|
1356
|
+
patternFiles,
|
|
1357
|
+
snippetFiles: new Map(),
|
|
1358
|
+
functionSignatures: new Map(),
|
|
1359
|
+
interfaceKeys: new Map(),
|
|
1360
|
+
exportsByFile: new Map(),
|
|
1361
|
+
patternInputs: [],
|
|
1362
|
+
};
|
|
1363
|
+
const load = vi.spyOn(SourceIndex.prototype, 'load').mockResolvedValue(snapshot);
|
|
1364
|
+
const buildGraph = vi.spyOn(GraphBuilder.prototype, 'buildGraph');
|
|
1365
|
+
const indexCodeComments = vi.spyOn(VectorSearch.prototype, 'indexCodeComments');
|
|
1366
|
+
captureLog();
|
|
1367
|
+
await run({ ...baseConfig, graph: { enabled: true }, vectorSearch: { enabled: true } });
|
|
1368
|
+
expect(buildGraph).toHaveBeenCalledWith([], symbols);
|
|
1369
|
+
expect(indexCodeComments).toHaveBeenCalledWith([
|
|
1370
|
+
{ path: 'src/no-symbol.ts', content: '// a useful comment', language: 'typescript' },
|
|
1371
|
+
]);
|
|
1372
|
+
expect(new Set(load.mock.contexts).size).toBe(1);
|
|
1373
|
+
});
|
|
1374
|
+
it('keeps custom validator overwrite independent from graph source loading', async () => {
|
|
1375
|
+
const validateBatch = vi.fn().mockResolvedValue([]);
|
|
1376
|
+
vi.spyOn(DocumentParser.prototype, 'scanDocuments').mockResolvedValue([
|
|
1377
|
+
{
|
|
1378
|
+
path: 'doc.md',
|
|
1379
|
+
absolutePath: '/doc.md',
|
|
1380
|
+
content: '',
|
|
1381
|
+
format: 'markdown',
|
|
1382
|
+
lines: [],
|
|
1383
|
+
references: [{ type: 'code-pattern', value: 'Custom', lineNumber: 1, raw: 'Custom', sourceFile: 'doc.md' }],
|
|
1384
|
+
},
|
|
1385
|
+
]);
|
|
1386
|
+
const load = vi.spyOn(SourceIndex.prototype, 'load');
|
|
899
1387
|
await run({
|
|
900
1388
|
...baseConfig,
|
|
901
|
-
reporters: ['json'],
|
|
902
1389
|
graph: { enabled: true },
|
|
903
|
-
|
|
904
|
-
|
|
1390
|
+
rules: { ...baseConfig.rules, 'code-pattern': { enabled: true } },
|
|
1391
|
+
customValidators: { 'code-pattern': { validateBatch } },
|
|
905
1392
|
});
|
|
906
|
-
|
|
907
|
-
expect(
|
|
1393
|
+
expect(validateBatch).toHaveBeenCalledOnce();
|
|
1394
|
+
expect(load).toHaveBeenCalledOnce();
|
|
908
1395
|
});
|
|
909
|
-
|
|
1396
|
+
});
|
|
1397
|
+
describe('reporters with freshness scores', () => {
|
|
1398
|
+
it('console reporter uses generateWithScores when scores available', async () => {
|
|
910
1399
|
const spy = captureLog();
|
|
911
1400
|
await run({
|
|
912
1401
|
...baseConfig,
|
|
913
|
-
reporters: ['
|
|
1402
|
+
reporters: ['console'],
|
|
914
1403
|
graph: { enabled: true },
|
|
915
1404
|
freshnessScoring: { enabled: true },
|
|
916
1405
|
cache: { enabled: false },
|