doc-freshness-checker 2.0.23 → 2.2.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 +19 -0
- package/dist/config/defaults.js +9 -3
- package/dist/config/defaults.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 +67 -63
- package/dist/runner.js.map +1 -1
- package/dist/runner.test.js +444 -42
- package/dist/runner.test.js.map +1 -1
- package/dist/semantic/vectorSearch.js +2 -1
- package/dist/semantic/vectorSearch.js.map +1 -1
- package/dist/semantic/vectorSearch.test.js +2 -2
- 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 +34 -2
- 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 +4 -4
package/dist/runner.test.js
CHANGED
|
@@ -4,11 +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';
|
|
11
15
|
import { CacheManager } from './cache/cacheManager.js';
|
|
16
|
+
import { FreshnessScorer } from './scoring/freshnessScorer.js';
|
|
12
17
|
import { withOutputFile } from './test-utils/tempFiles.js';
|
|
13
18
|
import { captureConsoleLog, captureConsoleWarn } from './test-utils/console.js';
|
|
14
19
|
vi.mock('fastembed', () => ({
|
|
@@ -139,15 +144,58 @@ describe('runner', () => {
|
|
|
139
144
|
readSpy.mockRestore();
|
|
140
145
|
await fs.promises.rm(path.join(process.cwd(), cacheDir), { recursive: true, force: true });
|
|
141
146
|
});
|
|
142
|
-
it('
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
+
});
|
|
149
198
|
});
|
|
150
|
-
expect(true).toBe(true);
|
|
151
199
|
});
|
|
152
200
|
it.each(['supportsFormat', 'extract'])('propagates custom extractor %s failures', async (hook) => {
|
|
153
201
|
vi.mocked(glob).mockResolvedValueOnce([path.join(process.cwd(), 'README.md')]);
|
|
@@ -194,9 +242,7 @@ describe('runner', () => {
|
|
|
194
242
|
await expect(run({
|
|
195
243
|
...baseConfig,
|
|
196
244
|
rules: { ...baseConfig.rules, 'file-path': { enabled: true } },
|
|
197
|
-
customExtractors: [
|
|
198
|
-
{ extract: () => [reference], supportsFormat: () => true },
|
|
199
|
-
],
|
|
245
|
+
customExtractors: [{ extract: () => [reference], supportsFormat: () => true }],
|
|
200
246
|
})).rejects.toThrow('file validator crashed');
|
|
201
247
|
}
|
|
202
248
|
finally {
|
|
@@ -237,6 +283,73 @@ describe('runner', () => {
|
|
|
237
283
|
});
|
|
238
284
|
});
|
|
239
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
|
+
});
|
|
240
353
|
describe('verbose mode', () => {
|
|
241
354
|
it('logs config file path and source patterns', async () => {
|
|
242
355
|
const spy = captureLog();
|
|
@@ -271,9 +384,11 @@ describe('runner', () => {
|
|
|
271
384
|
});
|
|
272
385
|
});
|
|
273
386
|
describe('reporters', () => {
|
|
274
|
-
it('
|
|
387
|
+
it('routes the Console emitter to stdout incrementally', async () => {
|
|
275
388
|
const spy = captureLog();
|
|
276
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);
|
|
277
392
|
expect(spy.mock.calls.flat().join('\n')).toContain('Documentation Freshness Report');
|
|
278
393
|
});
|
|
279
394
|
it('falls back to console when the reporter list is empty', async () => {
|
|
@@ -331,6 +446,95 @@ describe('runner', () => {
|
|
|
331
446
|
expect(spy.mock.calls.flat().join('\n')).toContain('written to');
|
|
332
447
|
});
|
|
333
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
|
+
});
|
|
334
538
|
});
|
|
335
539
|
describe('graph and scoring', () => {
|
|
336
540
|
it('builds graph without persisting an unused graph cache', async () => {
|
|
@@ -345,33 +549,152 @@ describe('runner', () => {
|
|
|
345
549
|
await fs.promises.rm(path.join(process.cwd(), cacheDir), { recursive: true, force: true }).catch(() => { });
|
|
346
550
|
}
|
|
347
551
|
});
|
|
348
|
-
it('
|
|
349
|
-
|
|
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 {
|
|
350
575
|
captureLog();
|
|
351
|
-
await run({
|
|
576
|
+
await expect(run({
|
|
352
577
|
...baseConfig,
|
|
353
578
|
reporters: ['json'],
|
|
354
|
-
outputPath,
|
|
355
579
|
graph: { enabled: true },
|
|
356
580
|
freshnessScoring: { enabled: true },
|
|
357
581
|
cache: { enabled: false },
|
|
358
|
-
});
|
|
359
|
-
expect(
|
|
360
|
-
}
|
|
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
|
+
}
|
|
361
590
|
});
|
|
362
|
-
it('
|
|
363
|
-
|
|
364
|
-
|
|
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();
|
|
365
635
|
await run({
|
|
366
636
|
...baseConfig,
|
|
367
637
|
reporters: ['markdown'],
|
|
368
|
-
outputPath,
|
|
369
638
|
graph: { enabled: true },
|
|
370
639
|
freshnessScoring: { enabled: true },
|
|
371
640
|
cache: { enabled: false },
|
|
372
641
|
});
|
|
373
|
-
|
|
374
|
-
|
|
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
|
+
}
|
|
375
698
|
});
|
|
376
699
|
it('generates enhanced with scores to file', async () => {
|
|
377
700
|
await withOutputFile(cacheRoot, 'test-enhanced-scored.md', async (outputPath) => {
|
|
@@ -941,6 +1264,7 @@ describe('runner', () => {
|
|
|
941
1264
|
const output = spy.mock.calls.flat().join('\n');
|
|
942
1265
|
expect(output).toContain('Indexing documentation');
|
|
943
1266
|
expect(output).toContain('Finding semantic mismatches');
|
|
1267
|
+
expect(output).not.toContain('Building source code index');
|
|
944
1268
|
});
|
|
945
1269
|
});
|
|
946
1270
|
describe('runWithConfig', () => {
|
|
@@ -956,6 +1280,7 @@ describe('runner', () => {
|
|
|
956
1280
|
});
|
|
957
1281
|
});
|
|
958
1282
|
describe('vector search without prior graph', () => {
|
|
1283
|
+
afterEach(() => vi.restoreAllMocks());
|
|
959
1284
|
it('builds source index independently when graph not enabled', async () => {
|
|
960
1285
|
const spy = captureLog();
|
|
961
1286
|
await run({
|
|
@@ -968,36 +1293,113 @@ describe('runner', () => {
|
|
|
968
1293
|
const output = spy.mock.calls.flat().join('\n');
|
|
969
1294
|
expect(output).toContain('Building source code index');
|
|
970
1295
|
});
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
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
|
+
]);
|
|
974
1307
|
const spy = captureLog();
|
|
975
1308
|
await run({
|
|
976
1309
|
...baseConfig,
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1310
|
+
rules: { ...baseConfig.rules, 'code-pattern': { enabled: true } },
|
|
1311
|
+
vectorSearch: { enabled: true },
|
|
1312
|
+
graph: { enabled: false },
|
|
1313
|
+
verbose: true,
|
|
981
1314
|
});
|
|
982
|
-
expect(spy.mock.calls.flat().join('\n')).toContain('
|
|
1315
|
+
expect(spy.mock.calls.flat().join('\n')).not.toContain('Building source code index');
|
|
983
1316
|
});
|
|
984
|
-
|
|
985
|
-
|
|
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');
|
|
986
1387
|
await run({
|
|
987
1388
|
...baseConfig,
|
|
988
|
-
reporters: ['json'],
|
|
989
1389
|
graph: { enabled: true },
|
|
990
|
-
|
|
991
|
-
|
|
1390
|
+
rules: { ...baseConfig.rules, 'code-pattern': { enabled: true } },
|
|
1391
|
+
customValidators: { 'code-pattern': { validateBatch } },
|
|
992
1392
|
});
|
|
993
|
-
|
|
994
|
-
expect(
|
|
1393
|
+
expect(validateBatch).toHaveBeenCalledOnce();
|
|
1394
|
+
expect(load).toHaveBeenCalledOnce();
|
|
995
1395
|
});
|
|
996
|
-
|
|
1396
|
+
});
|
|
1397
|
+
describe('reporters with freshness scores', () => {
|
|
1398
|
+
it('console reporter uses generateWithScores when scores available', async () => {
|
|
997
1399
|
const spy = captureLog();
|
|
998
1400
|
await run({
|
|
999
1401
|
...baseConfig,
|
|
1000
|
-
reporters: ['
|
|
1402
|
+
reporters: ['console'],
|
|
1001
1403
|
graph: { enabled: true },
|
|
1002
1404
|
freshnessScoring: { enabled: true },
|
|
1003
1405
|
cache: { enabled: false },
|