proteum 2.5.9 → 2.5.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/AGENTS.md +4 -4
  2. package/agents/project/AGENTS.md +131 -91
  3. package/agents/project/CODING_STYLE.md +69 -40
  4. package/agents/project/DOCUMENTATION.md +19 -2
  5. package/agents/project/client/AGENTS.md +0 -1
  6. package/agents/project/diagnostics.md +6 -5
  7. package/agents/project/optimizations.md +1 -7
  8. package/agents/project/server/services/AGENTS.md +1 -3
  9. package/agents/project/tests/AGENTS.md +3 -3
  10. package/cli/commands/docs.ts +223 -0
  11. package/cli/commands/session.ts +36 -5
  12. package/cli/commands/verify.ts +6 -1
  13. package/cli/compiler/client/index.ts +11 -4
  14. package/cli/compiler/common/uiSingletons.ts +76 -0
  15. package/cli/compiler/server/index.ts +34 -12
  16. package/cli/presentation/commands.ts +20 -1
  17. package/cli/runtime/commands.ts +20 -0
  18. package/cli/scaffold/index.ts +3 -0
  19. package/cli/scaffold/templates.ts +62 -6
  20. package/cli/utils/agents.ts +2 -2
  21. package/cli/verification/changed.ts +21 -0
  22. package/client/dev/profiler/index.tsx +761 -455
  23. package/common/dev/mcpPayloads.ts +86 -8
  24. package/common/dev/session.ts +32 -0
  25. package/common/errors/index.tsx +0 -1
  26. package/docAnchors.js +135 -0
  27. package/docs/agent-routing.md +2 -2
  28. package/eslint.js +264 -1
  29. package/package.json +1 -1
  30. package/server/app/container/console/index.ts +0 -17
  31. package/server/services/router/http/index.ts +130 -33
  32. package/tests/agents-utils.test.cjs +0 -4
  33. package/tests/dev-session-login-url.test.cjs +33 -0
  34. package/tests/doc-anchors.test.cjs +115 -0
  35. package/tests/docs-check.test.cjs +138 -0
  36. package/tests/eslint-rules.test.cjs +235 -2
  37. package/tests/mcp.test.cjs +109 -0
  38. package/tests/ui-singletons.test.cjs +104 -0
  39. package/tests/verify-changed.test.cjs +51 -3
  40. package/agents/project/app-root/AGENTS.md +0 -14
  41. package/agents/project/root/AGENTS.md +0 -399
@@ -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,200 @@ 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 does not require a doc anchor on error routes', () => {
462
+ const { root } = createDocProject();
463
+ const messages = lint(
464
+ `export default defineErrorRoute({ code: 404 });`,
465
+ path.join(root, 'client', 'pages', '_messages', '404.tsx'),
466
+ );
467
+
468
+ assert.equal(messagesFor(messages, requireDocAnchorRuleId).length, 0);
469
+ });
470
+
471
+ test('proteum lint ignores files that export no Proteum definition', () => {
472
+ const { pageFile } = createDocProject();
473
+ const messages = lint(`export default { path: '/browse' };`, pageFile);
474
+
475
+ assert.equal(messagesFor(messages, requireDocAnchorRuleId).length, 0);
476
+ });
477
+
478
+ test('proteum lint reports a definition file whose anchors omit the feature pack', () => {
479
+ const { pageFile } = createDocProject();
480
+ const messages = lint(
481
+ `
482
+ /**
483
+ * @rule Browse rows never expose raw score values.
484
+ */
485
+ export default definePageRoute({ path: '/browse' });
486
+ `,
487
+ pageFile,
488
+ );
489
+
490
+ assert.equal(messagesFor(messages, requireDocAnchorRuleId).length, 1);
491
+ });
492
+
493
+ test('proteum lint resolves anchors against the repo corpus when the app has its own docs directory', () => {
494
+ const { root } = createDocProject();
495
+
496
+ // Mirrors the monorepo layout: apps/<app>/docs/ sits between the source file
497
+ // and the repository-level corpus that the anchor actually points at.
498
+ const appRoot = path.join(root, 'apps', 'website');
499
+ fs.mkdirSync(path.join(appRoot, 'docs', 'fixes'), { recursive: true });
500
+ fs.mkdirSync(path.join(appRoot, 'client', 'pages'), { recursive: true });
501
+
502
+ const messages = lint(
503
+ `
504
+ /**
505
+ * @docs docs/features/search
506
+ */
507
+ export default definePageRoute({ path: '/browse' });
508
+ `,
509
+ path.join(appRoot, 'client', 'pages', 'browse.tsx'),
510
+ );
511
+
512
+ assert.equal(messagesFor(messages, validDocAnchorRuleId).length, 0);
513
+ assert.equal(messagesFor(messages, requireDocAnchorRuleId).length, 0);
514
+ });
515
+
516
+ test('proteum lint rejects a doc anchor pointing at a missing document', () => {
517
+ const { pageFile } = createDocProject();
518
+ const messages = lint(
519
+ `
520
+ /**
521
+ * @docs docs/features/deleted-feature
522
+ */
523
+ export default definePageRoute({ path: '/browse' });
524
+ `,
525
+ pageFile,
526
+ );
527
+
528
+ const reported = messagesFor(messages, validDocAnchorRuleId);
529
+ assert.equal(reported.length, 1);
530
+ assert.equal(/docs\/features\/deleted-feature/.test(reported[0].message), true);
531
+ });
532
+
533
+ test('proteum lint resolves fix and decision anchors against the documentation corpus', () => {
534
+ const { pageFile } = createDocProject();
535
+ const messages = lint(
536
+ `
537
+ /**
538
+ * @docs docs/features/search
539
+ * @adr ADR-0004
540
+ * @fix docs/fixes/2026-06-09-keyword-order.md
541
+ * @rule Composite ordering stays alias-aware.
542
+ */
543
+ export default definePageRoute({ path: '/browse' });
544
+ `,
545
+ pageFile,
546
+ );
547
+
548
+ assert.equal(messagesFor(messages, validDocAnchorRuleId).length, 0);
549
+ });
550
+
551
+ test('proteum lint rejects a decision anchor that matches no decision record', () => {
552
+ const { pageFile } = createDocProject();
553
+ const messages = lint(
554
+ `
555
+ /**
556
+ * @docs docs/features/search
557
+ * @adr ADR-9999
558
+ */
559
+ export default definePageRoute({ path: '/browse' });
560
+ `,
561
+ pageFile,
562
+ );
563
+
564
+ assert.equal(messagesFor(messages, validDocAnchorRuleId).length, 1);
565
+ });
566
+
567
+ test('proteum lint rejects a rule anchor that states no invariant', () => {
568
+ const { pageFile } = createDocProject();
569
+ const messages = lint(
570
+ `
571
+ /**
572
+ * @docs docs/features/search
573
+ * @rule todo
574
+ */
575
+ export default definePageRoute({ path: '/browse' });
576
+ `,
577
+ pageFile,
578
+ );
579
+
580
+ assert.equal(messagesFor(messages, validDocAnchorRuleId).length, 1);
581
+ });
582
+
583
+ test('proteum lint validates anchors on files that export no definition', () => {
584
+ const { root } = createDocProject();
585
+ const messages = lint(
586
+ `
587
+ /**
588
+ * @docs docs/features/deleted-feature
589
+ */
590
+ export const helper = () => null;
591
+ `,
592
+ path.join(root, 'server', 'services', 'search.ts'),
593
+ );
594
+
595
+ assert.equal(messagesFor(messages, validDocAnchorRuleId).length, 1);
596
+ assert.equal(messagesFor(messages, requireDocAnchorRuleId).length, 0);
597
+ });
598
+
599
+ test('proteum lint escalates and disables doc anchor rules through config options', () => {
600
+ const { pageFile } = createDocProject();
601
+ const source = `
602
+ /**
603
+ * @docs docs/features/deleted-feature
604
+ */
605
+ export default definePageRoute({ path: '/browse' });
606
+ `;
607
+
608
+ const warned = lint(`export default definePageRoute({ path: '/browse' });`, pageFile);
609
+ assert.equal(messagesFor(warned, requireDocAnchorRuleId)[0].severity, 1);
610
+
611
+ const escalated = lint(`export default definePageRoute({ path: '/browse' });`, pageFile, {
612
+ docAnchors: 'error',
613
+ });
614
+ assert.equal(messagesFor(escalated, requireDocAnchorRuleId)[0].severity, 2);
615
+
616
+ const disabled = lint(source, pageFile, { docAnchors: 'off' });
617
+ assert.equal(messagesFor(disabled, requireDocAnchorRuleId).length, 0);
618
+ assert.equal(messagesFor(disabled, validDocAnchorRuleId).length, 0);
619
+ });
@@ -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');
@@ -0,0 +1,104 @@
1
+ const assert = require('node:assert/strict');
2
+ const path = require('node:path');
3
+
4
+ const coreRoot = path.resolve(__dirname, '..');
5
+ process.env.TS_NODE_PROJECT = path.join(coreRoot, 'cli', 'tsconfig.json');
6
+ process.env.TS_NODE_TRANSPILE_ONLY = '1';
7
+
8
+ require('ts-node/register/transpile-only');
9
+
10
+ const {
11
+ isUiSingletonRequest,
12
+ resolveUiSingletonAliases,
13
+ resolveUiSingletonServerExternalRequest,
14
+ } = require('../cli/compiler/common/uiSingletons.ts');
15
+
16
+ test('UI singleton matcher includes React and Preact runtime packages', () => {
17
+ assert.equal(isUiSingletonRequest('preact'), true);
18
+ assert.equal(isUiSingletonRequest('preact/hooks'), true);
19
+ assert.equal(isUiSingletonRequest('preact-render-to-string'), true);
20
+ assert.equal(isUiSingletonRequest('react'), true);
21
+ assert.equal(isUiSingletonRequest('react-dom/client'), true);
22
+ });
23
+
24
+ test('UI singleton matcher does not match unrelated React-prefixed packages', () => {
25
+ assert.equal(isUiSingletonRequest('react-number-format'), false);
26
+ assert.equal(isUiSingletonRequest('reactive-stream'), false);
27
+ assert.equal(isUiSingletonRequest('@radix-ui/react-popover'), false);
28
+ assert.equal(isUiSingletonRequest(undefined), false);
29
+ });
30
+
31
+ test('UI singleton aliases resolve React imports to Preact runtime requests', () => {
32
+ const seenRequests = [];
33
+ const seenPackageRoots = [];
34
+ const aliases = resolveUiSingletonAliases({
35
+ resolvePackageRoot: (packageName) => {
36
+ seenPackageRoots.push(packageName);
37
+ return `/app/node_modules/${packageName}`;
38
+ },
39
+ resolveRequest: (request) => {
40
+ seenRequests.push(request);
41
+ return `/app/node_modules/${request}`;
42
+ },
43
+ });
44
+
45
+ assert.equal(aliases['preact'], '/app/node_modules/preact');
46
+ assert.equal(aliases['preact$'], '/app/node_modules/preact');
47
+ assert.equal(aliases['preact/hooks$'], '/app/node_modules/preact/hooks');
48
+ assert.equal(aliases['react$'], '/app/node_modules/preact/compat');
49
+ assert.equal(aliases['react-dom$'], '/app/node_modules/preact/compat');
50
+ assert.equal(aliases['react-dom/client$'], '/app/node_modules/preact/compat/client');
51
+ assert.equal(aliases['react/jsx-runtime$'], '/app/node_modules/preact/jsx-runtime');
52
+ assert.equal(aliases['preact-render-to-string'], '/app/node_modules/preact-render-to-string');
53
+ assert.equal(aliases['preact-render-to-string$'], '/app/node_modules/preact-render-to-string');
54
+ assert.deepEqual(seenPackageRoots, ['preact', 'preact-render-to-string']);
55
+ assert.equal(
56
+ Object.keys(aliases).indexOf('preact/jsx-dev-runtime$') < Object.keys(aliases).indexOf('preact'),
57
+ true,
58
+ );
59
+ assert.deepEqual(
60
+ seenRequests.filter((request) => request === 'preact-render-to-string'),
61
+ ['preact-render-to-string'],
62
+ );
63
+ });
64
+
65
+ test('UI singleton server externals resolve Preact and React compat requests from the app', () => {
66
+ const seenRequests = [];
67
+ const resolveRequest = (request) => {
68
+ seenRequests.push(request);
69
+ return `/app/node_modules/${request}`;
70
+ };
71
+
72
+ assert.equal(resolveUiSingletonServerExternalRequest('preact', resolveRequest), '/app/node_modules/preact');
73
+ assert.equal(
74
+ resolveUiSingletonServerExternalRequest('preact/hooks', resolveRequest),
75
+ '/app/node_modules/preact/hooks',
76
+ );
77
+ assert.equal(
78
+ resolveUiSingletonServerExternalRequest('react', resolveRequest),
79
+ '/app/node_modules/preact/compat',
80
+ );
81
+ assert.equal(
82
+ resolveUiSingletonServerExternalRequest('react/jsx-runtime', resolveRequest),
83
+ '/app/node_modules/preact/jsx-runtime',
84
+ );
85
+ assert.equal(
86
+ resolveUiSingletonServerExternalRequest('react-dom/client', resolveRequest),
87
+ '/app/node_modules/preact/compat/client',
88
+ );
89
+ assert.deepEqual(seenRequests, [
90
+ 'preact',
91
+ 'preact/hooks',
92
+ 'preact/compat',
93
+ 'preact/jsx-runtime',
94
+ 'preact/compat/client',
95
+ ]);
96
+ });
97
+
98
+ test('UI singleton server externals keep the SSR renderer compiled', () => {
99
+ const resolveRequest = () => {
100
+ throw new Error('should not resolve');
101
+ };
102
+
103
+ assert.equal(resolveUiSingletonServerExternalRequest('preact-render-to-string', resolveRequest), undefined);
104
+ });
@@ -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), ["npx vitest related 'packages/auth/src/session.ts'"]);
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
- assert.deepEqual(plan.selectedChecks, []);
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
- assert.equal(output.result.selectedChecks, 0);
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
+ });
@@ -1,14 +0,0 @@
1
- # Proteum App-Root Addendum
2
-
3
- This file is the app-root-only addendum for a Proteum app that lives inside a larger monorepo.
4
- Keep the reusable Proteum contract in the nearest ancestor root `AGENTS.md`, and keep this file only for instructions that depend on the current directory being the Proteum app root.
5
- Role: keep only app-root workflow and local project-semantic rules here.
6
- Do not put here: reusable Proteum architecture contracts, shared verification rules, documentation-driven coding workflow, diagnostics workflow, optimization checklists, coding-style details, or area-specific rules already covered by broader `AGENTS.md` files or the root-level `DOCUMENTATION.md`, `diagnostics.md`, `optimizations.md`, and `CODING_STYLE.md`.
7
-
8
- ## App-Root Triggers
9
-
10
- - If you are working in a newly created Proteum worktree, before following the rest of these instructions:
11
- - Run `npx proteum worktree init --source <source-app-root>`.
12
- - Read and acknowledge the applicable `AGENTS.md` files.
13
- - Run the dev server with the task-safe elevated-permissions launch workflow from the reusable root `AGENTS.md`, keep it running so user can see the results by himself, and print the live server URL as a clickable Markdown link. If `proteum dev` reports blocked instruction paths, resolve them before continuing.
14
- - If the task changes UX, copy, onboarding, pricing, product semantics, or commercial positioning, use root-level `DOCUMENTATION.md` to choose the smallest relevant `./docs/` pack before editing. If a dev server is already running, print the live dev server URL as a clickable Markdown link.