proteum 2.5.10 → 2.5.12
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/agents/project/CODING_STYLE.md +28 -0
- package/agents/project/DOCUMENTATION.md +19 -2
- package/cli/commands/docs.ts +223 -0
- package/cli/presentation/commands.ts +14 -0
- package/cli/runtime/commands.ts +18 -0
- package/cli/verification/changed.ts +21 -0
- package/common/dev/mcpPayloads.ts +86 -8
- package/docAnchors.js +135 -0
- package/eslint.js +368 -1
- package/package.json +1 -1
- package/tests/doc-anchors.test.cjs +115 -0
- package/tests/docs-check.test.cjs +138 -0
- package/tests/eslint-rules.test.cjs +318 -2
- package/tests/mcp.test.cjs +109 -0
- package/tests/verify-changed.test.cjs +51 -3
|
@@ -1,17 +1,53 @@
|
|
|
1
1
|
const assert = require('node:assert/strict');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
2
4
|
const { Linter } = require('eslint');
|
|
3
5
|
|
|
4
6
|
const { createProteumEslintConfig } = require('../eslint.js');
|
|
5
7
|
|
|
6
|
-
const lint = (code, filename = 'client/example.tsx') => {
|
|
8
|
+
const lint = (code, filename = 'client/example.tsx', configOptions) => {
|
|
7
9
|
const linter = new Linter({ configType: 'flat' });
|
|
8
|
-
return linter.verify(code, createProteumEslintConfig(), {
|
|
10
|
+
return linter.verify(code, createProteumEslintConfig(configOptions), {
|
|
9
11
|
filename,
|
|
10
12
|
});
|
|
11
13
|
};
|
|
12
14
|
|
|
13
15
|
const swallowedErrorRuleId = 'proteum/no-swallowed-caught-error';
|
|
14
16
|
const noAppImportRuleId = 'proteum/no-app-import';
|
|
17
|
+
const requireDocAnchorRuleId = 'proteum/require-doc-anchor';
|
|
18
|
+
const validDocAnchorRuleId = 'proteum/valid-doc-anchor';
|
|
19
|
+
|
|
20
|
+
const messagesFor = (messages, ruleId) => messages.filter((message) => message.ruleId === ruleId);
|
|
21
|
+
|
|
22
|
+
// The fixture lives inside the repository `.temp` directory rather than the OS
|
|
23
|
+
// temp directory: on macOS the latter sits under `/var/folders`, which the
|
|
24
|
+
// shared `**/var/**` ignore would exclude from linting entirely.
|
|
25
|
+
const docProjectParent = path.resolve(__dirname, '..', '.temp');
|
|
26
|
+
const docProjectRoots = [];
|
|
27
|
+
|
|
28
|
+
afterAll(() => {
|
|
29
|
+
docProjectRoots.forEach((root) => fs.rmSync(root, { force: true, recursive: true }));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Build a throwaway project whose documentation corpus really exists on disk,
|
|
34
|
+
* because the anchor rules resolve their paths against the filesystem.
|
|
35
|
+
*/
|
|
36
|
+
const createDocProject = () => {
|
|
37
|
+
fs.mkdirSync(docProjectParent, { recursive: true });
|
|
38
|
+
const root = fs.mkdtempSync(path.join(docProjectParent, 'proteum-doc-anchor-'));
|
|
39
|
+
docProjectRoots.push(root);
|
|
40
|
+
|
|
41
|
+
fs.mkdirSync(path.join(root, 'docs', 'features', 'search'), { recursive: true });
|
|
42
|
+
fs.writeFileSync(path.join(root, 'docs', 'features', 'search', 'README.md'), '# Search\n');
|
|
43
|
+
fs.mkdirSync(path.join(root, 'docs', 'decisions'), { recursive: true });
|
|
44
|
+
fs.writeFileSync(path.join(root, 'docs', 'decisions', 'ADR-0004-page-query-contracts.md'), '# ADR-0004\n');
|
|
45
|
+
fs.mkdirSync(path.join(root, 'docs', 'fixes'), { recursive: true });
|
|
46
|
+
fs.writeFileSync(path.join(root, 'docs', 'fixes', '2026-06-09-keyword-order.md'), '# Fix\n');
|
|
47
|
+
fs.mkdirSync(path.join(root, 'client', 'pages'), { recursive: true });
|
|
48
|
+
|
|
49
|
+
return { pageFile: path.join(root, 'client', 'pages', 'browse.tsx'), root };
|
|
50
|
+
};
|
|
15
51
|
|
|
16
52
|
test('proteum lint rejects contextual @app imports', () => {
|
|
17
53
|
const messages = lint(`
|
|
@@ -384,3 +420,283 @@ test('proteum lint allows direct reject promise catch handlers', () => {
|
|
|
384
420
|
|
|
385
421
|
assert.equal(messages.filter((message) => message.ruleId === swallowedErrorRuleId).length, 0);
|
|
386
422
|
});
|
|
423
|
+
|
|
424
|
+
test('proteum lint requires a doc anchor on definition files', () => {
|
|
425
|
+
const { pageFile } = createDocProject();
|
|
426
|
+
const messages = lint(`export default definePageRoute({ path: '/browse' });`, pageFile);
|
|
427
|
+
|
|
428
|
+
assert.equal(messagesFor(messages, requireDocAnchorRuleId).length, 1);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test('proteum lint accepts a definition file that anchors its feature pack', () => {
|
|
432
|
+
const { pageFile } = createDocProject();
|
|
433
|
+
const messages = lint(
|
|
434
|
+
`
|
|
435
|
+
/**
|
|
436
|
+
* @docs docs/features/search
|
|
437
|
+
*/
|
|
438
|
+
export default definePageRoute({ path: '/browse' });
|
|
439
|
+
`,
|
|
440
|
+
pageFile,
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
assert.equal(messagesFor(messages, requireDocAnchorRuleId).length, 0);
|
|
444
|
+
assert.equal(messagesFor(messages, validDocAnchorRuleId).length, 0);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test('proteum lint requires a doc anchor on every Proteum definition kind', () => {
|
|
448
|
+
const { pageFile } = createDocProject();
|
|
449
|
+
|
|
450
|
+
for (const definition of [
|
|
451
|
+
'defineController',
|
|
452
|
+
'definePageRoute',
|
|
453
|
+
'defineServerRoute',
|
|
454
|
+
'defineServerRoutes',
|
|
455
|
+
]) {
|
|
456
|
+
const messages = lint(`export default ${definition}({ path: '/browse' });`, pageFile);
|
|
457
|
+
assert.equal(messagesFor(messages, requireDocAnchorRuleId).length, 1, definition);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test('proteum lint requires a doc anchor on exported service classes', () => {
|
|
462
|
+
const { root } = createDocProject();
|
|
463
|
+
const serviceFile = path.join(root, 'server', 'services', 'Domains', 'search', 'index.ts');
|
|
464
|
+
|
|
465
|
+
const missing = lint(
|
|
466
|
+
`export default class DomainsSearchService extends Service<Config, {}, Application, object> {}`,
|
|
467
|
+
serviceFile,
|
|
468
|
+
);
|
|
469
|
+
assert.equal(messagesFor(missing, requireDocAnchorRuleId).length, 1);
|
|
470
|
+
|
|
471
|
+
const anchored = lint(
|
|
472
|
+
`
|
|
473
|
+
/**
|
|
474
|
+
* @docs docs/features/search
|
|
475
|
+
*/
|
|
476
|
+
export default class DomainsSearchService extends Service<Config, {}, Application, object> {}
|
|
477
|
+
`,
|
|
478
|
+
serviceFile,
|
|
479
|
+
);
|
|
480
|
+
assert.equal(messagesFor(anchored, requireDocAnchorRuleId).length, 0);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test('proteum lint covers app-specific service base classes and named exports', () => {
|
|
484
|
+
const { root } = createDocProject();
|
|
485
|
+
const messages = lint(
|
|
486
|
+
`export class AuthManagement extends UsersManagementService<TUser, AuthApplication, TJwtSession> {}`,
|
|
487
|
+
path.join(root, 'server', 'services', 'Users', 'Auth', 'index.ts'),
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
assert.equal(messagesFor(messages, requireDocAnchorRuleId).length, 1);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test('proteum lint leaves non-service classes alone', () => {
|
|
494
|
+
const { root } = createDocProject();
|
|
495
|
+
const messages = lint(
|
|
496
|
+
`export default class DomainCard extends React.Component {}`,
|
|
497
|
+
path.join(root, 'client', 'components', 'DomainCard.tsx'),
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
assert.equal(messagesFor(messages, requireDocAnchorRuleId).length, 0);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test('proteum lint reports a service class only once per file', () => {
|
|
504
|
+
const { root } = createDocProject();
|
|
505
|
+
const messages = lint(
|
|
506
|
+
`
|
|
507
|
+
export class FirstService extends Service {}
|
|
508
|
+
export default class SecondService extends Service {}
|
|
509
|
+
`,
|
|
510
|
+
path.join(root, 'server', 'services', 'Two', 'index.ts'),
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
assert.equal(messagesFor(messages, requireDocAnchorRuleId).length, 1);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test('proteum lint opts extra files in through include globs', () => {
|
|
517
|
+
const { root } = createDocProject();
|
|
518
|
+
const paywallFile = path.join(root, 'client', 'components', 'paywall', 'PaywallModal', 'index.tsx');
|
|
519
|
+
const iconFile = path.join(root, 'client', 'components', 'Icon.tsx');
|
|
520
|
+
const source = `const Modal = () => null;\nexport default Modal;\n`;
|
|
521
|
+
const options = { docAnchors: 'warn', includeDocAnchors: ['client/components/paywall/**'] };
|
|
522
|
+
|
|
523
|
+
assert.equal(messagesFor(lint(source, paywallFile, options), requireDocAnchorRuleId).length, 1);
|
|
524
|
+
assert.equal(messagesFor(lint(source, iconFile, options), requireDocAnchorRuleId).length, 0);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test('proteum lint accepts an included file once it carries an anchor', () => {
|
|
528
|
+
const { root } = createDocProject();
|
|
529
|
+
const messages = lint(
|
|
530
|
+
`
|
|
531
|
+
/**
|
|
532
|
+
* @docs docs/features/search
|
|
533
|
+
*/
|
|
534
|
+
const Modal = () => null;
|
|
535
|
+
export default Modal;
|
|
536
|
+
`,
|
|
537
|
+
path.join(root, 'client', 'components', 'paywall', 'PaywallModal', 'index.tsx'),
|
|
538
|
+
{ docAnchors: 'warn', includeDocAnchors: ['client/components/paywall/**'] },
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
assert.equal(messagesFor(messages, requireDocAnchorRuleId).length, 0);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test('proteum lint does not require a doc anchor on error routes', () => {
|
|
545
|
+
const { root } = createDocProject();
|
|
546
|
+
const messages = lint(
|
|
547
|
+
`export default defineErrorRoute({ code: 404 });`,
|
|
548
|
+
path.join(root, 'client', 'pages', '_messages', '404.tsx'),
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
assert.equal(messagesFor(messages, requireDocAnchorRuleId).length, 0);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test('proteum lint ignores files that export no Proteum definition', () => {
|
|
555
|
+
const { pageFile } = createDocProject();
|
|
556
|
+
const messages = lint(`export default { path: '/browse' };`, pageFile);
|
|
557
|
+
|
|
558
|
+
assert.equal(messagesFor(messages, requireDocAnchorRuleId).length, 0);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
test('proteum lint reports a definition file whose anchors omit the feature pack', () => {
|
|
562
|
+
const { pageFile } = createDocProject();
|
|
563
|
+
const messages = lint(
|
|
564
|
+
`
|
|
565
|
+
/**
|
|
566
|
+
* @rule Browse rows never expose raw score values.
|
|
567
|
+
*/
|
|
568
|
+
export default definePageRoute({ path: '/browse' });
|
|
569
|
+
`,
|
|
570
|
+
pageFile,
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
assert.equal(messagesFor(messages, requireDocAnchorRuleId).length, 1);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test('proteum lint resolves anchors against the repo corpus when the app has its own docs directory', () => {
|
|
577
|
+
const { root } = createDocProject();
|
|
578
|
+
|
|
579
|
+
// Mirrors the monorepo layout: apps/<app>/docs/ sits between the source file
|
|
580
|
+
// and the repository-level corpus that the anchor actually points at.
|
|
581
|
+
const appRoot = path.join(root, 'apps', 'website');
|
|
582
|
+
fs.mkdirSync(path.join(appRoot, 'docs', 'fixes'), { recursive: true });
|
|
583
|
+
fs.mkdirSync(path.join(appRoot, 'client', 'pages'), { recursive: true });
|
|
584
|
+
|
|
585
|
+
const messages = lint(
|
|
586
|
+
`
|
|
587
|
+
/**
|
|
588
|
+
* @docs docs/features/search
|
|
589
|
+
*/
|
|
590
|
+
export default definePageRoute({ path: '/browse' });
|
|
591
|
+
`,
|
|
592
|
+
path.join(appRoot, 'client', 'pages', 'browse.tsx'),
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
assert.equal(messagesFor(messages, validDocAnchorRuleId).length, 0);
|
|
596
|
+
assert.equal(messagesFor(messages, requireDocAnchorRuleId).length, 0);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
test('proteum lint rejects a doc anchor pointing at a missing document', () => {
|
|
600
|
+
const { pageFile } = createDocProject();
|
|
601
|
+
const messages = lint(
|
|
602
|
+
`
|
|
603
|
+
/**
|
|
604
|
+
* @docs docs/features/deleted-feature
|
|
605
|
+
*/
|
|
606
|
+
export default definePageRoute({ path: '/browse' });
|
|
607
|
+
`,
|
|
608
|
+
pageFile,
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
const reported = messagesFor(messages, validDocAnchorRuleId);
|
|
612
|
+
assert.equal(reported.length, 1);
|
|
613
|
+
assert.equal(/docs\/features\/deleted-feature/.test(reported[0].message), true);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
test('proteum lint resolves fix and decision anchors against the documentation corpus', () => {
|
|
617
|
+
const { pageFile } = createDocProject();
|
|
618
|
+
const messages = lint(
|
|
619
|
+
`
|
|
620
|
+
/**
|
|
621
|
+
* @docs docs/features/search
|
|
622
|
+
* @adr ADR-0004
|
|
623
|
+
* @fix docs/fixes/2026-06-09-keyword-order.md
|
|
624
|
+
* @rule Composite ordering stays alias-aware.
|
|
625
|
+
*/
|
|
626
|
+
export default definePageRoute({ path: '/browse' });
|
|
627
|
+
`,
|
|
628
|
+
pageFile,
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
assert.equal(messagesFor(messages, validDocAnchorRuleId).length, 0);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
test('proteum lint rejects a decision anchor that matches no decision record', () => {
|
|
635
|
+
const { pageFile } = createDocProject();
|
|
636
|
+
const messages = lint(
|
|
637
|
+
`
|
|
638
|
+
/**
|
|
639
|
+
* @docs docs/features/search
|
|
640
|
+
* @adr ADR-9999
|
|
641
|
+
*/
|
|
642
|
+
export default definePageRoute({ path: '/browse' });
|
|
643
|
+
`,
|
|
644
|
+
pageFile,
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
assert.equal(messagesFor(messages, validDocAnchorRuleId).length, 1);
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
test('proteum lint rejects a rule anchor that states no invariant', () => {
|
|
651
|
+
const { pageFile } = createDocProject();
|
|
652
|
+
const messages = lint(
|
|
653
|
+
`
|
|
654
|
+
/**
|
|
655
|
+
* @docs docs/features/search
|
|
656
|
+
* @rule todo
|
|
657
|
+
*/
|
|
658
|
+
export default definePageRoute({ path: '/browse' });
|
|
659
|
+
`,
|
|
660
|
+
pageFile,
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
assert.equal(messagesFor(messages, validDocAnchorRuleId).length, 1);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
test('proteum lint validates anchors on files that export no definition', () => {
|
|
667
|
+
const { root } = createDocProject();
|
|
668
|
+
const messages = lint(
|
|
669
|
+
`
|
|
670
|
+
/**
|
|
671
|
+
* @docs docs/features/deleted-feature
|
|
672
|
+
*/
|
|
673
|
+
export const helper = () => null;
|
|
674
|
+
`,
|
|
675
|
+
path.join(root, 'server', 'services', 'search.ts'),
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
assert.equal(messagesFor(messages, validDocAnchorRuleId).length, 1);
|
|
679
|
+
assert.equal(messagesFor(messages, requireDocAnchorRuleId).length, 0);
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
test('proteum lint escalates and disables doc anchor rules through config options', () => {
|
|
683
|
+
const { pageFile } = createDocProject();
|
|
684
|
+
const source = `
|
|
685
|
+
/**
|
|
686
|
+
* @docs docs/features/deleted-feature
|
|
687
|
+
*/
|
|
688
|
+
export default definePageRoute({ path: '/browse' });
|
|
689
|
+
`;
|
|
690
|
+
|
|
691
|
+
const warned = lint(`export default definePageRoute({ path: '/browse' });`, pageFile);
|
|
692
|
+
assert.equal(messagesFor(warned, requireDocAnchorRuleId)[0].severity, 1);
|
|
693
|
+
|
|
694
|
+
const escalated = lint(`export default definePageRoute({ path: '/browse' });`, pageFile, {
|
|
695
|
+
docAnchors: 'error',
|
|
696
|
+
});
|
|
697
|
+
assert.equal(messagesFor(escalated, requireDocAnchorRuleId)[0].severity, 2);
|
|
698
|
+
|
|
699
|
+
const disabled = lint(source, pageFile, { docAnchors: 'off' });
|
|
700
|
+
assert.equal(messagesFor(disabled, requireDocAnchorRuleId).length, 0);
|
|
701
|
+
assert.equal(messagesFor(disabled, validDocAnchorRuleId).length, 0);
|
|
702
|
+
});
|
package/tests/mcp.test.cjs
CHANGED
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
compactRouteCandidatesResponse,
|
|
19
19
|
compactTraceResponse,
|
|
20
20
|
compactWorkflowStartResponse,
|
|
21
|
+
readOwnerDocAnchors,
|
|
21
22
|
resolveInstructionRouting,
|
|
22
23
|
} = require('../common/dev/mcpPayloads.ts');
|
|
23
24
|
const { createProteumMcpServer } = require('../common/dev/mcpServer.ts');
|
|
@@ -241,6 +242,114 @@ test('instruction routing promotes triggered full instruction files', () => {
|
|
|
241
242
|
);
|
|
242
243
|
});
|
|
243
244
|
|
|
245
|
+
test('owner doc anchors resolve the documentation that governs a source file', () => {
|
|
246
|
+
const appRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-mcp-doc-anchor-'));
|
|
247
|
+
const anchoredFile = path.join(appRoot, 'client/pages/browse.tsx');
|
|
248
|
+
const plainFile = path.join(appRoot, 'client/pages/plain.tsx');
|
|
249
|
+
|
|
250
|
+
writeFile(
|
|
251
|
+
anchoredFile,
|
|
252
|
+
[
|
|
253
|
+
'/**',
|
|
254
|
+
' * @docs docs/features/search',
|
|
255
|
+
' * @adr ADR-0004',
|
|
256
|
+
' * @fix docs/fixes/2026-06-09-keyword-order.md',
|
|
257
|
+
' * @rule Composite ordering stays alias-aware.',
|
|
258
|
+
' */',
|
|
259
|
+
"export default definePageRoute({ path: '/browse' });",
|
|
260
|
+
].join('\n'),
|
|
261
|
+
);
|
|
262
|
+
writeFile(plainFile, "export default definePageRoute({ path: '/plain' });\n");
|
|
263
|
+
|
|
264
|
+
const anchors = readOwnerDocAnchors(anchoredFile);
|
|
265
|
+
|
|
266
|
+
assert.deepEqual(anchors.docs, ['docs/features/search']);
|
|
267
|
+
assert.deepEqual(anchors.adr, ['ADR-0004']);
|
|
268
|
+
assert.deepEqual(anchors.fix, ['docs/fixes/2026-06-09-keyword-order.md']);
|
|
269
|
+
assert.deepEqual(anchors.rules, ['Composite ordering stays alias-aware.']);
|
|
270
|
+
|
|
271
|
+
assert.equal(readOwnerDocAnchors(plainFile), undefined);
|
|
272
|
+
assert.equal(readOwnerDocAnchors(path.join(appRoot, 'missing.tsx')), undefined);
|
|
273
|
+
assert.equal(readOwnerDocAnchors(undefined), undefined);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test('owner payloads carry the doc anchors declared by the owning file', () => {
|
|
277
|
+
const appRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-mcp-owner-docs-'));
|
|
278
|
+
const pageFile = path.join(appRoot, 'client/pages/domains.tsx');
|
|
279
|
+
|
|
280
|
+
writeFile(path.join(appRoot, 'AGENTS.md'), '# App Agents\n\n- root\n');
|
|
281
|
+
writeFile(
|
|
282
|
+
pageFile,
|
|
283
|
+
[
|
|
284
|
+
'/**',
|
|
285
|
+
' * @docs docs/features/product-domain-listings',
|
|
286
|
+
' * @rule Public rows never expose raw score values.',
|
|
287
|
+
' */',
|
|
288
|
+
'export default function Domains() { return null; }',
|
|
289
|
+
].join('\n'),
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const manifest = {
|
|
293
|
+
version: 10,
|
|
294
|
+
app: {
|
|
295
|
+
root: appRoot,
|
|
296
|
+
coreRoot,
|
|
297
|
+
identityFilepath: path.join(appRoot, 'identity.config.ts'),
|
|
298
|
+
setupFilepath: path.join(appRoot, 'proteum.config.ts'),
|
|
299
|
+
identity: { name: 'Owner Docs App', identifier: 'OwnerDocsApp', description: '' },
|
|
300
|
+
setup: {},
|
|
301
|
+
},
|
|
302
|
+
conventions: { routeOptionKeys: [], reservedRouteOptionKeys: [] },
|
|
303
|
+
env: {
|
|
304
|
+
source: 'test',
|
|
305
|
+
loadedVariableKeys: [],
|
|
306
|
+
requiredVariables: [],
|
|
307
|
+
resolved: {
|
|
308
|
+
name: 'test',
|
|
309
|
+
profile: 'dev',
|
|
310
|
+
routerPort: 3105,
|
|
311
|
+
routerCurrentDomain: 'localhost',
|
|
312
|
+
routerInternalUrl: 'http://localhost:3105',
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
connectedProjects: [],
|
|
316
|
+
services: { app: [], routerPlugins: [] },
|
|
317
|
+
controllers: [],
|
|
318
|
+
commands: [],
|
|
319
|
+
routes: { client: [], server: [] },
|
|
320
|
+
layouts: [],
|
|
321
|
+
diagnostics: [],
|
|
322
|
+
};
|
|
323
|
+
const doctor = { summary: { errors: 0, warnings: 0, strictFailed: false }, diagnostics: [] };
|
|
324
|
+
const payload = compactWorkflowStartResponse({
|
|
325
|
+
contracts: doctor,
|
|
326
|
+
doctor,
|
|
327
|
+
manifest,
|
|
328
|
+
owner: {
|
|
329
|
+
matches: [
|
|
330
|
+
{
|
|
331
|
+
details: [],
|
|
332
|
+
kind: 'route',
|
|
333
|
+
label: '/domains',
|
|
334
|
+
matchedOn: ['path'],
|
|
335
|
+
originHint: 'manifest',
|
|
336
|
+
scopeLabel: 'local',
|
|
337
|
+
score: 100,
|
|
338
|
+
source: { filepath: pageFile, line: 1, column: 1 },
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
normalizedQuery: '/domains',
|
|
342
|
+
query: '/domains',
|
|
343
|
+
},
|
|
344
|
+
route: '/domains',
|
|
345
|
+
runtime: { publicUrl: 'http://localhost:3105', mcpUrl: 'http://localhost:3105/__proteum/mcp' },
|
|
346
|
+
task: 'read-only runtime health pass',
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
assert.deepEqual(payload.data.owner.top.docs.docs, ['docs/features/product-domain-listings']);
|
|
350
|
+
assert.deepEqual(payload.data.owner.top.docs.rules, ['Public rows never expose raw score values.']);
|
|
351
|
+
});
|
|
352
|
+
|
|
244
353
|
test('workflow start payload combines compact runtime, instructions, owner, and duplicate guidance', () => {
|
|
245
354
|
const appRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-mcp-workflow-app-'));
|
|
246
355
|
const pageFile = path.join(appRoot, 'client/pages/domains.tsx');
|
|
@@ -46,7 +46,10 @@ test('changed verification planner runs related tests for source files', () => {
|
|
|
46
46
|
changedFiles: ['packages/auth/src/session.ts'],
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
assert.deepEqual(planCommands(plan), [
|
|
49
|
+
assert.deepEqual(planCommands(plan), [
|
|
50
|
+
"npx vitest related 'packages/auth/src/session.ts'",
|
|
51
|
+
'npx proteum docs check',
|
|
52
|
+
]);
|
|
50
53
|
assert.deepEqual(plan.selectedChecks[0].matchedFiles, ['packages/auth/src/session.ts']);
|
|
51
54
|
});
|
|
52
55
|
|
|
@@ -115,7 +118,9 @@ test('changed verification planner skips tests for docs-only changes', () => {
|
|
|
115
118
|
changedFiles: ['docs/testing.md'],
|
|
116
119
|
});
|
|
117
120
|
|
|
118
|
-
|
|
121
|
+
// Test suites are skipped, but the doc-anchor check still runs: moving or
|
|
122
|
+
// renaming a document is precisely what leaves an anchor pointing nowhere.
|
|
123
|
+
assert.deepEqual(planIds(plan), ['builtin:doc-anchors']);
|
|
119
124
|
assert.equal(plan.docsOnly, true);
|
|
120
125
|
assert.deepEqual(plan.skippedChecks.map((check) => check.id), ['builtin:docs-only']);
|
|
121
126
|
});
|
|
@@ -195,6 +200,49 @@ test('verify changed CLI JSON output keeps the planner and execution shape stabl
|
|
|
195
200
|
assert.ok(Array.isArray(output.skippedChecks));
|
|
196
201
|
assert.ok(Array.isArray(output.executions));
|
|
197
202
|
assert.equal(typeof output.result.ok, 'boolean');
|
|
198
|
-
|
|
203
|
+
// A docs-only change still skips the test suites, but it does run the
|
|
204
|
+
// doc-anchor check: renaming a document is exactly what orphans an anchor.
|
|
205
|
+
assert.equal(output.result.selectedChecks, 1);
|
|
206
|
+
assert.deepEqual(
|
|
207
|
+
output.selectedChecks.map((check) => check.id),
|
|
208
|
+
['builtin:doc-anchors'],
|
|
209
|
+
);
|
|
199
210
|
assert.equal(output.result.failedChecks, 0);
|
|
200
211
|
});
|
|
212
|
+
|
|
213
|
+
test('changed verification planner checks doc anchors when documentation moves', () => {
|
|
214
|
+
const root = createRoot();
|
|
215
|
+
writeFile(root, 'docs/features/search/README.md', '# Search\n');
|
|
216
|
+
|
|
217
|
+
const plan = buildChangedVerificationPlan({
|
|
218
|
+
cwd: root,
|
|
219
|
+
changedFiles: ['docs/features/search/README.md'],
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
assert.deepEqual(planIds(plan), ['builtin:doc-anchors']);
|
|
223
|
+
assert.deepEqual(planCommands(plan), ['npx proteum docs check']);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('changed verification planner checks doc anchors when a source file changes', () => {
|
|
227
|
+
const root = createRoot();
|
|
228
|
+
writeFile(root, 'apps/product/server/controllers/Domains/search.ts', 'export default {};\n');
|
|
229
|
+
|
|
230
|
+
const plan = buildChangedVerificationPlan({
|
|
231
|
+
cwd: root,
|
|
232
|
+
changedFiles: ['apps/product/server/controllers/Domains/search.ts'],
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
assert.ok(planIds(plan).includes('builtin:doc-anchors'));
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('changed verification planner leaves doc anchors alone for unrelated changes', () => {
|
|
239
|
+
const root = createRoot();
|
|
240
|
+
writeFile(root, 'README.md', '# Root\n');
|
|
241
|
+
|
|
242
|
+
const plan = buildChangedVerificationPlan({
|
|
243
|
+
cwd: root,
|
|
244
|
+
changedFiles: ['README.md'],
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
assert.equal(planIds(plan).includes('builtin:doc-anchors'), false);
|
|
248
|
+
});
|