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.
- package/AGENTS.md +4 -4
- package/agents/project/AGENTS.md +131 -91
- package/agents/project/CODING_STYLE.md +69 -40
- package/agents/project/DOCUMENTATION.md +19 -2
- package/agents/project/client/AGENTS.md +0 -1
- package/agents/project/diagnostics.md +6 -5
- package/agents/project/optimizations.md +1 -7
- package/agents/project/server/services/AGENTS.md +1 -3
- package/agents/project/tests/AGENTS.md +3 -3
- package/cli/commands/docs.ts +223 -0
- package/cli/commands/session.ts +36 -5
- package/cli/commands/verify.ts +6 -1
- package/cli/compiler/client/index.ts +11 -4
- package/cli/compiler/common/uiSingletons.ts +76 -0
- package/cli/compiler/server/index.ts +34 -12
- package/cli/presentation/commands.ts +20 -1
- package/cli/runtime/commands.ts +20 -0
- package/cli/scaffold/index.ts +3 -0
- package/cli/scaffold/templates.ts +62 -6
- package/cli/utils/agents.ts +2 -2
- package/cli/verification/changed.ts +21 -0
- package/client/dev/profiler/index.tsx +761 -455
- package/common/dev/mcpPayloads.ts +86 -8
- package/common/dev/session.ts +32 -0
- package/common/errors/index.tsx +0 -1
- package/docAnchors.js +135 -0
- package/docs/agent-routing.md +2 -2
- package/eslint.js +264 -1
- package/package.json +1 -1
- package/server/app/container/console/index.ts +0 -17
- package/server/services/router/http/index.ts +130 -33
- package/tests/agents-utils.test.cjs +0 -4
- package/tests/dev-session-login-url.test.cjs +33 -0
- package/tests/doc-anchors.test.cjs +115 -0
- package/tests/docs-check.test.cjs +138 -0
- package/tests/eslint-rules.test.cjs +235 -2
- package/tests/mcp.test.cjs +109 -0
- package/tests/ui-singletons.test.cjs +104 -0
- package/tests/verify-changed.test.cjs +51 -3
- package/agents/project/app-root/AGENTS.md +0 -14
- 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
|
+
});
|
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');
|
|
@@ -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), [
|
|
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
|
+
});
|
|
@@ -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.
|