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
@@ -18,7 +18,7 @@ This file is the canonical source of truth for diagnostics, temporary instrument
18
18
 
19
19
  - For long-lived dev reproductions, always request elevated permissions and run `npx proteum dev` outside the sandbox. Use an explicit task/thread-scoped session file, inspect `npx proteum runtime status` first, then use its exact next action so occupied router/HMR ports and untracked same-app runtimes are handled without page-body probes. After the server is ready, print the live server URL as a clickable Markdown link.
20
20
  - Use `--replace-existing` only when restarting the exact session file started by the current thread/task. Never replace another live session that belongs to a user, another thread, or an unknown owner.
21
- - Only the bare `npx proteum build` and bare `npx proteum dev` commands print the welcome banner and active Proteum installation method. Any extra argument or option skips the welcome banner. Terminal `npx proteum mcp` may print a compact central MCP ready banner when it starts or reuses the managed daemon. Only `npx proteum dev` clears the interactive terminal before rendering and reports connected app names plus successful connected `/ping` checks in the ready banner; keep that in mind when capturing or comparing command logs during diagnosis. Every `npx proteum dev` start ensures tracked instruction files contain the current managed `# Proteum Instructions` section and `CLAUDE.md` symlinks point to sibling `AGENTS.md` files before the dev loop begins.
21
+ - When capturing or comparing command logs: only the bare `npx proteum build` and bare `npx proteum dev` commands print the welcome banner; any extra argument or option skips it. Only `npx proteum dev` clears the interactive terminal before rendering and reports connected app names plus successful connected `/ping` checks in its ready banner.
22
22
  - During `npx proteum dev`, the running app exposes the read-only Proteum MCP transport at `/__proteum/mcp`. Use it for runtime-adjacent agent reads instead of repeatedly spawning equivalent CLI diagnostics.
23
23
  - If machine MCP routing fails, run `npx proteum mcp status` and `npx proteum runtime status` from the intended app root. If no live session exists, use the exact MCP offline or runtime-status next action so occupied router/HMR ports are avoided. If the same app already responds on the configured port without live tracking, use or repair that runtime instead of starting another server. Do not `curl` normal page routes to identify which app owns a port; use runtime status or Proteum dev-only endpoints. If a live session exists but runtime/MCP is unreachable, stop the listed session file first, then start dev again. Do not start a second dev server in the same worktree, and do not start a second managed MCP daemon. Do not run diagnose, trace, or perf reads while runtime health is unreachable. Then retry MCP `workflow_start` and use the returned `projectId`.
24
24
  - For ownership or repo discovery questions, start with MCP `workflow_start`; use MCP `route_candidates { projectId, query }`, MCP `orient { projectId, query }`, and MCP `explain_summary { projectId, query }` only when the bootstrap owner summary is insufficient. Use `npx proteum orient <query>` or `npx proteum explain owner <query>` only when MCP is unavailable or terminal evidence is required.
@@ -34,7 +34,8 @@ This file is the canonical source of truth for diagnostics, temporary instrument
34
34
  - If existing traces are insufficient, arm `npx proteum trace arm --capture deep`, reproduce once, then inspect the new request with compact `npx proteum trace latest`; use `npx proteum trace show <requestId> --events` only when raw event detail is still required.
35
35
  - Use the browser MCP to inspect browser console errors and warnings for frontend, SSR, hydration, and controller-call issues.
36
36
  - Inspect server startup and runtime errors.
37
- - For protected browser or API flows in dev, prefer `npx proteum session <email> --role <role>` over driving the login UI, then use that session for browser MCP validation. Use `npx proteum e2e --session-email <email> --session-role <role>` only when Playwright end-to-end suites need the auth token through the child process environment. Use the login UI only when auth UX itself is under test.
37
+ - Before every browser MCP validation pass in dev, create an admin session from the project instructions with `npx proteum session <admin-email> --role GOD` and use its `browserLoginUrl` or emitted cookie state before opening the target page. Skip this only when the task is explicitly testing auth, login, anonymous, or non-admin behavior; if no admin email is declared in the project instructions, stop and ask before browser validation.
38
+ - For protected API flows in dev, prefer `npx proteum session <email> --role <role>` over driving the login UI, then use that session for browser MCP validation. Use `npx proteum e2e --session-email <email> --session-role <role>` only when Playwright end-to-end suites need the auth token through the child process environment. Use the login UI only when auth UX itself is under test.
38
39
 
39
40
  ## Temporary Instrumentation
40
41
 
@@ -55,16 +56,16 @@ This file is the canonical source of truth for diagnostics, temporary instrument
55
56
  ## Verification And Testing
56
57
 
57
58
  - Use the cheapest trustworthy verification that matches the failing layer.
58
- - After implementing a change, verify at the smallest trustworthy layer required by the changed surface first, including targeted tests when behavior changed. When the project defines `proteum.verify.config.ts`, prefer `npx proteum verify changed` for the first post-edit verification plan. Do not run coverage by default, and do not default to a running app, browser MCP, or Playwright while iterating when a narrower static or request-level verification is enough.
59
+ - After implementing a change, verify at the smallest trustworthy layer required by the changed surface first, following the root contract `Verification Policy`. Do not default to a running app, browser MCP, or Playwright while iterating when a narrower static or request-level verification is enough.
59
60
  - For compile-time or type-safety issues, start with the relevant targeted typecheck or build command. Do not run them by default for unrelated runtime, copy, docs, or local refactor changes.
60
61
  - For request/runtime issues, verify through the real page, route, generated controller call, or command on a running app.
61
- - Start the smallest trustworthy runtime surface first: MCP `workflow_start`, then MCP `route_candidates { projectId, query }`, MCP `orient { projectId, query }`, or MCP `explain_summary { projectId, query }` only when more owner detail is needed. If runtime health is unreachable, repair/start dev before any diagnose, trace, or perf read. Once runtime is reachable, use the relevant real URL, generated controller call, command, or MCP `diagnose { projectId, path }`. Use CLI equivalents only when MCP is unavailable or terminal evidence is required. Use browser MCP validation only when request-level verification is insufficient or the change is browser-visible.
62
+ - Start the smallest trustworthy runtime surface first: MCP `workflow_start`, then MCP `route_candidates { projectId, query }`, MCP `orient { projectId, query }`, or MCP `explain_summary { projectId, query }` only when more owner detail is needed. If runtime health is unreachable, repair/start dev before any diagnose, trace, or perf read. Once runtime is reachable, use the relevant real URL, generated controller call, command, or MCP `diagnose { projectId, path }`. Use CLI equivalents only when MCP is unavailable or terminal evidence is required. Use browser MCP validation when request-level verification is insufficient, when the change is browser-visible, or when the root `Verification Policy` requires the mandatory browser pass for a new UI-visible feature.
62
63
  - When automated browser assertions or suite coverage are required, use `npx proteum e2e --port <port>` for targeted or full Playwright suites. Do not use direct Playwright for browser validation outside the E2E wrapper, and do not launch raw browser automation against a shared persistent profile.
63
64
  - Focused verification should treat unrelated global diagnostics as visible but non-blocking by default. Use `--strict-global` only when the task explicitly requires broad clean-room validation.
64
65
  - For browser regressions, prefer a browser MCP repro first and add targeted Playwright E2E coverage only when the user asks for automated coverage, when a stable regression path needs automation, or when browser MCP verification is insufficient.
65
66
  - Only the final verifier agent should usually run browser flows. Earlier agents should stay on `orient`, `verify owner`, `verify request`, `diagnose`, and command-level checks unless browser execution is the only trustworthy reproducer.
66
67
  - Treat server startup failures, runtime errors, browser console errors or warnings, and Playwright failures as blocking unless they are clearly unrelated to the change.
67
- - When the touched surface can affect coding-style enforcement, run the targeted lint or typecheck command for that surface before finishing. Downstream app commit-only workflows run only `proteum refresh`, then targeted lint, typecheck, and test commands in parallel; framework-repo commit workflows skip this downstream app verification. Run the full `npm run check` gate before pushing or when explicitly requested.
68
+ - When the touched surface can affect coding-style enforcement, run the targeted lint or typecheck command for that surface before finishing. Commit-time and push-time gates follow the root contract `Verification Policy` and `Commit Workflow`.
68
69
  - If the task started any long-lived `proteum dev` server, stop it explicitly with `npx proteum dev stop --session-file <path>` or `npx proteum dev stop --all --stale`, then confirm the remaining tracked sessions with `npx proteum dev list --json`.
69
70
  - Add `data-testid` when stable selectors are missing instead of relying on brittle text or DOM-shape selectors.
70
71
  - If an isolated test misses prerequisite state, run the smallest broader scope that reproduces the real setup.
@@ -23,12 +23,7 @@ When tradeoffs exist inside optimization work, optimize in this order:
23
23
 
24
24
  ## SSR And Page Size
25
25
 
26
- - SSR page data belongs in the explicit `definePageRoute({ path, options, data, render })` `data` function, not in `api.fetch(...)`.
27
- - `options` carries route behavior. `data` returns one flat object or is `null` when the page has no SSR data loader.
28
- - Route-option keys and `_`-prefixed route-option aliases are forbidden in page data and must live in `options`.
29
- - If a page needs route data, return it from `data` and read it in `render`.
30
- - Controller fetchers and promises returned from `data` resolve before render.
31
- - Never use `api.fetch(...)` in page files for SSR loading.
26
+ - The page `data` / `options` / `render` contract is defined in `client/pages/AGENTS.md`; SSR page data belongs in the route definition `data` function, never in `api.fetch(...)`.
32
27
  - Synchronous or SSR data calls must return only the strictly necessary data for the current render path to minimize SSR payload size.
33
28
  - If an existing controller or data method returns a broader shape than the SSR path needs, create a dedicated proxy controller method with a narrower typed contract instead of reusing the oversized payload.
34
29
  - Keep Prisma runtime access inside services when possible and prefer explicit `select` or narrow `include` in database queries.
@@ -46,4 +41,3 @@ When tradeoffs exist inside optimization work, optimize in this order:
46
41
  - For browser or SSR changes, use the browser MCP to load the real page, inspect the rendered HTML, and confirm the change does not ship unnecessary client code or oversized SSR payloads.
47
42
  - Treat clearly worse bundle size, runtime cost, or crawlable HTML quality as regressions to fix or justify explicitly, not as optional follow-up cleanup.
48
43
  - Build-only checks are supplementary.
49
- - For SSR changes, use the browser MCP to load the real page and inspect the rendered HTML plus browser console.
@@ -29,9 +29,7 @@ Diagnostics source of truth: root-level `diagnostics.md`.
29
29
  - Use runtime models through `this.models` or the app model accessors.
30
30
  - Use Prisma typings through `@models/types` only.
31
31
  - In database queries, prefer explicit `select` or narrow `include`.
32
- - For database structure changes, edit the app's `schema.prisma` only. Never create or edit migration files manually.
33
- - Never use raw SQL DDL or other schema-mutating SQL to change database structure.
34
- - For read-only SQL diagnosis, use MCP `db_query` or `npx proteum db query "<sql>"`; only one capped `SELECT`, `SHOW`, or `EXPLAIN` statement is allowed.
32
+ - For database structure changes, edit the app's `schema.prisma` only. Never create or edit migration files manually; the full database safety rules live in the root contract `Hard Stops`.
35
33
  - Prefer inferred return types such as `Awaited<ReturnType<MyService['methodName']>>` over manual DTO duplication.
36
34
 
37
35
  ## Errors
@@ -9,9 +9,9 @@ Diagnostics source of truth: root-level `diagnostics.md`.
9
9
 
10
10
  - Understand the real user flow and the main feature branches before writing tests.
11
11
  - Test the current controller/page runtime model, not legacy `@Route` or `api.fetch(...)` behavior.
12
- - For every production change, add or update focused unit tests and run the targeted test command that matches the changed behavior. When the repository defines `proteum.verify.config.ts`, use `npx proteum verify changed` first so changed test files, related source tests, and project-specific suites are selected consistently. Do not run whole-project coverage after every ordinary change by default. Downstream app commit-only workflows run only `proteum refresh`, then targeted lint, typecheck, and test commands in parallel; framework-repo commit workflows skip this downstream app verification. Use `npm run check` as the full gate before push or when the user explicitly asks for it, and document any generated files, migrations, framework shims, unreachable defensive branches, or changes that cannot reasonably be unit-tested as explicit exceptions.
12
+ - For every production change, add or update focused unit tests and run the targeted test command that matches the changed behavior. When the repository defines `proteum.verify.config.ts`, use `npx proteum verify changed` first so changed test files, related source tests, and project-specific suites are selected consistently. Coverage, commit-time, and push-time gates follow the root contract `Verification Policy` and `Commit Workflow`.
13
13
  - Verify routing, controllers, SSR, and router plugins against a running app when behavior depends on real request handling.
14
- - After implementing a new feature or changing existing feature behavior, update the end-to-end coverage for that behavior and run the full Playwright suite before finishing. Prefer `npx proteum e2e --port <port>` for Playwright runs so base URLs and auth tokens are passed through Proteum-managed child env instead of shell-leading environment assignments. Use a browser MCP repro against a running app during iteration when it is the fastest trustworthy loop.
14
+ - After implementing a new feature or changing existing feature behavior, update the end-to-end coverage for that behavior and run the targeted Playwright scenarios that cover it before finishing; reserve the full suite for push workflows or explicit requests per the root contract `Verification Policy`. Prefer `npx proteum e2e --port <port>` for Playwright runs so base URLs and auth tokens are passed through Proteum-managed child env instead of shell-leading environment assignments. For new UI-visible features, the root `Verification Policy` still requires a browser MCP pass against the real page before finishing.
15
15
  - Exercise real URLs, generated controller calls, or real browser flows instead of re-deriving framework internals in tests.
16
16
  - Organize end-to-end tests following the Crosspath platform layout under `tests/e2e/**`.
17
17
  - Put runnable scenario entrypoints in `tests/e2e/features/**`, `tests/e2e/specs/<domain>/**`, or `tests/e2e/journeys/**` depending on scope.
@@ -23,7 +23,7 @@ Diagnostics source of truth: root-level `diagnostics.md`.
23
23
  - Add `data-testid` where needed instead of relying on brittle selectors.
24
24
  - Keep end-to-end tests clean, well organized, and non-redundant. Prefer extending or reshaping the most relevant existing scenario over duplicating coverage, and remove or consolidate overlap when the suite becomes repetitive.
25
25
  - Reuse root catalog files from `/client/catalogs/**`, `/server/catalogs/**`, or `/common/catalogs/**` instead of duplicating catalog constants in tests.
26
- - For protected dev flows, prefer `npx proteum e2e --session-email <email> --session-role <role>` or `npx proteum session <email> --role <role>` over automating login unless the login flow itself is under test.
26
+ - For browser MCP validation in dev, start from the project instructions' admin account with `npx proteum session <admin-email> --role GOD` or its `browserLoginUrl` unless the scenario intentionally covers auth, anonymous, or non-admin behavior. For protected E2E dev flows, prefer `npx proteum e2e --session-email <email> --session-role <role>` over automating login unless the login flow itself is under test.
27
27
 
28
28
  ### Real-World Journey E2E
29
29
 
@@ -0,0 +1,223 @@
1
+ import { UsageError } from 'clipanion';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+
5
+ import cli from '..';
6
+ import { renderRows } from '../presentation/layout';
7
+ import { renderStep, renderSuccess, renderTitle, renderWarning } from '../presentation/ink';
8
+
9
+ const { collectDocAnchors } = require('../../docAnchors.js') as {
10
+ collectDocAnchors: (sourceText: string) => {
11
+ docs: string[];
12
+ fix: string[];
13
+ entries: { line: number; tag: string; value: string }[];
14
+ };
15
+ };
16
+
17
+ /*----------------------------------
18
+ - TYPES
19
+ ----------------------------------*/
20
+
21
+ type TDocsCheckFinding = {
22
+ detail: string;
23
+ kind: 'unresolved-anchor' | 'unanchored-fix-note' | 'orphan-feature-pack';
24
+ subject: string;
25
+ };
26
+
27
+ type TDocsCheckReport = {
28
+ anchoredFiles: number;
29
+ findings: TDocsCheckFinding[];
30
+ scannedFiles: number;
31
+ };
32
+
33
+ /*----------------------------------
34
+ - HELPERS
35
+ ----------------------------------*/
36
+
37
+ const skippedDirectories = new Set([
38
+ '.generated',
39
+ '.git',
40
+ '.proteum',
41
+ 'bin',
42
+ 'bin-dev',
43
+ 'dist',
44
+ 'node_modules',
45
+ 'var',
46
+ ]);
47
+
48
+ const isSourceFile = (name: string) => /\.(ts|tsx|mts|cts)$/.test(name);
49
+
50
+ const walkSourceFiles = (directory: string, collected: string[] = []) => {
51
+ let entries: { isDirectory: () => boolean; name: string }[] = [];
52
+ try {
53
+ entries = fs.readdirSync(directory, { withFileTypes: true });
54
+ } catch (error) {
55
+ // An unreadable directory is not a documentation failure; skip it and
56
+ // keep scanning so one permission problem cannot mask real findings.
57
+ if (cli.verbose) console.warn(`docs check: skipping ${directory} (${(error as Error).message})`);
58
+ return collected;
59
+ }
60
+
61
+ entries.forEach((entry) => {
62
+ if (entry.name.startsWith('.') && entry.name !== '.') return;
63
+ const full = path.join(directory, entry.name);
64
+ if (entry.isDirectory()) {
65
+ if (skippedDirectories.has(entry.name)) return;
66
+ walkSourceFiles(full, collected);
67
+ return;
68
+ }
69
+ if (isSourceFile(entry.name)) collected.push(full);
70
+ });
71
+
72
+ return collected;
73
+ };
74
+
75
+ /**
76
+ * Resolve an anchor value the same way the `proteum/valid-doc-anchor` lint rule
77
+ * does: against every ancestor of the file that holds a `docs/` directory, so a
78
+ * monorepo app can point at the repository-level corpus.
79
+ */
80
+ const resolveAnchorValue = (value: string, fromDirectory: string, root: string) => {
81
+ if (path.isAbsolute(value)) return fs.existsSync(value);
82
+
83
+ const roots: string[] = [];
84
+ let current = fromDirectory;
85
+ // The walk deliberately continues past `root`: running this from an app root
86
+ // inside a monorepo must still resolve anchors aimed at the repository-level
87
+ // corpus, exactly as the `proteum/valid-doc-anchor` lint rule does.
88
+ while (current) {
89
+ if (fs.existsSync(path.join(current, 'docs'))) roots.push(current);
90
+ const parent = path.dirname(current);
91
+ if (parent === current) break;
92
+ current = parent;
93
+ }
94
+ if (!roots.includes(root)) roots.push(root);
95
+
96
+ return roots.some((candidate) => fs.existsSync(path.resolve(candidate, value)));
97
+ };
98
+
99
+ const listFeaturePacks = (root: string) => {
100
+ const featuresDir = path.join(root, 'docs', 'features');
101
+ if (!fs.existsSync(featuresDir)) return [];
102
+
103
+ return fs
104
+ .readdirSync(featuresDir, { withFileTypes: true })
105
+ .filter((entry) => entry.isDirectory())
106
+ .map((entry) => entry.name);
107
+ };
108
+
109
+ const listFixNotesRequiringAnchor = (root: string) => {
110
+ const fixesDir = path.join(root, 'docs', 'fixes');
111
+ if (!fs.existsSync(fixesDir)) return [];
112
+
113
+ return fs
114
+ .readdirSync(fixesDir)
115
+ .filter((name) => name.endsWith('.md'))
116
+ .filter((name) => /^## Agent warning\s*$/m.test(fs.readFileSync(path.join(fixesDir, name), 'utf8')));
117
+ };
118
+
119
+ export const buildDocsCheckReport = (root: string): TDocsCheckReport => {
120
+ const files = walkSourceFiles(root);
121
+ const findings: TDocsCheckFinding[] = [];
122
+ const referencedPacks = new Set<string>();
123
+ const referencedFixNotes = new Set<string>();
124
+ let anchoredFiles = 0;
125
+
126
+ files.forEach((filepath) => {
127
+ const anchors = collectDocAnchors(fs.readFileSync(filepath, 'utf8'));
128
+ if (anchors.entries.length === 0) return;
129
+
130
+ anchoredFiles += 1;
131
+ const relative = path.relative(root, filepath);
132
+
133
+ anchors.docs.forEach((value) => {
134
+ referencedPacks.add(path.basename(value.replace(/\/+$/, '')));
135
+ if (!resolveAnchorValue(value, path.dirname(filepath), root)) {
136
+ findings.push({ detail: `@docs ${value}`, kind: 'unresolved-anchor', subject: relative });
137
+ }
138
+ });
139
+
140
+ anchors.fix.forEach((value) => {
141
+ referencedFixNotes.add(path.basename(value));
142
+ if (!resolveAnchorValue(value, path.dirname(filepath), root)) {
143
+ findings.push({ detail: `@fix ${value}`, kind: 'unresolved-anchor', subject: relative });
144
+ }
145
+ });
146
+ });
147
+
148
+ listFixNotesRequiringAnchor(root).forEach((note) => {
149
+ if (referencedFixNotes.has(note)) return;
150
+ findings.push({
151
+ detail: 'carries an Agent warning that no source file anchors',
152
+ kind: 'unanchored-fix-note',
153
+ subject: `docs/fixes/${note}`,
154
+ });
155
+ });
156
+
157
+ listFeaturePacks(root).forEach((pack) => {
158
+ if (referencedPacks.has(pack)) return;
159
+ findings.push({
160
+ detail: 'no source file points at this pack with @docs',
161
+ kind: 'orphan-feature-pack',
162
+ subject: `docs/features/${pack}`,
163
+ });
164
+ });
165
+
166
+ return { anchoredFiles, findings, scannedFiles: files.length };
167
+ };
168
+
169
+ /*----------------------------------
170
+ - COMMAND
171
+ ----------------------------------*/
172
+
173
+ const renderFindings = (findings: TDocsCheckFinding[], kind: TDocsCheckFinding['kind'], label: string) => {
174
+ const matching = findings.filter((finding) => finding.kind === kind);
175
+ if (matching.length === 0) return `${label}: none`;
176
+
177
+ return [`${label}: ${matching.length}`, ...matching.map((finding) => ` ${finding.subject} | ${finding.detail}`)].join(
178
+ '\n',
179
+ );
180
+ };
181
+
182
+ export const run = async (): Promise<void> => {
183
+ if (cli.args.action !== 'check') throw new UsageError('Usage: `proteum docs check`');
184
+
185
+ const root = cli.paths.appRoot;
186
+
187
+ console.info(
188
+ [
189
+ await renderTitle('PROTEUM DOCS CHECK', 'Checking that code and documentation still point at each other.'),
190
+ renderRows([{ label: 'root', value: root === process.cwd() ? '.' : root }]),
191
+ await renderStep('[1/1]', 'Resolving doc anchors.'),
192
+ ].join('\n\n'),
193
+ );
194
+
195
+ const report = buildDocsCheckReport(root);
196
+ const unresolved = report.findings.filter((finding) => finding.kind === 'unresolved-anchor');
197
+
198
+ console.info(
199
+ [
200
+ renderRows([
201
+ { label: 'files scanned', value: String(report.scannedFiles) },
202
+ { label: 'files anchored', value: String(report.anchoredFiles) },
203
+ ]),
204
+ renderFindings(report.findings, 'unresolved-anchor', 'Unresolved anchors'),
205
+ renderFindings(report.findings, 'unanchored-fix-note', 'Fix notes with no code anchor'),
206
+ renderFindings(report.findings, 'orphan-feature-pack', 'Feature packs with no inbound anchor'),
207
+ ].join('\n\n'),
208
+ );
209
+
210
+ // Only a broken pointer fails the command. Missing coverage is reported as a
211
+ // backlog so a project can adopt anchors without a red build on day one.
212
+ if (unresolved.length > 0)
213
+ throw new Error(
214
+ `Proteum docs check failed: ${unresolved.length} doc anchor(s) do not resolve. Update or remove them.`,
215
+ );
216
+
217
+ const backlog = report.findings.length;
218
+ console.info(
219
+ backlog > 0
220
+ ? await renderWarning(`Anchors resolve. ${backlog} coverage gap(s) reported above.`)
221
+ : await renderSuccess('All doc anchors resolve, and coverage is complete.'),
222
+ );
223
+ };
@@ -4,7 +4,13 @@ import { spawn } from 'child_process';
4
4
  import { UsageError } from 'clipanion';
5
5
 
6
6
  import cli from '..';
7
- import type { TDevSessionErrorResponse, TDevSessionStartResponse } from '../../common/dev/session';
7
+ import {
8
+ buildDevSessionLoginUrl,
9
+ devSessionStartPath,
10
+ normalizeDevSessionRedirectPath,
11
+ type TDevSessionErrorResponse,
12
+ type TDevSessionStartResponse,
13
+ } from '../../common/dev/session';
8
14
 
9
15
  const localSessionResultMarker = '__PROTEUM_SESSION_RESULT__';
10
16
 
@@ -13,6 +19,7 @@ type TResolvedSessionOutput = {
13
19
  user: TDevSessionStartResponse['user'];
14
20
  session: TDevSessionStartResponse['session'];
15
21
  browserCookie: string;
22
+ browserLoginUrl: string;
16
23
  curlCookieHeader: string;
17
24
  playwright: {
18
25
  cookies: Array<{
@@ -28,6 +35,13 @@ type TResolvedSessionOutput = {
28
35
  };
29
36
 
30
37
  const normalizeBaseUrl = (value: string) => value.replace(/\/+$/, '');
38
+ const normalizeRedirectPath = (value: string): string => {
39
+ try {
40
+ return normalizeDevSessionRedirectPath(value);
41
+ } catch (error) {
42
+ throw new UsageError(error instanceof Error ? error.message : String(error));
43
+ }
44
+ };
31
45
 
32
46
  const getRouterPortFromManifest = () => {
33
47
  const manifestFilepath = path.join(cli.args.workdir as string, '.proteum', 'manifest.json');
@@ -82,7 +96,7 @@ const requestSession = async (email: string, role: string) => {
82
96
 
83
97
  for (const baseUrl of getRouterBaseUrls()) {
84
98
  try {
85
- const response = await got(`${baseUrl}/__proteum/session/start`, {
99
+ const response = await got(`${baseUrl}${devSessionStartPath}`, {
86
100
  method: 'POST',
87
101
  json: role ? { email, role } : { email },
88
102
  responseType: 'json',
@@ -92,7 +106,7 @@ const requestSession = async (email: string, role: string) => {
92
106
 
93
107
  if (response.statusCode >= 400) {
94
108
  if (response.statusCode === 404 && !hasStructuredSessionError(response.body as TDevSessionErrorResponse | object | string | undefined)) {
95
- attempts.push(`${baseUrl}/__proteum/session/start: returned 404`);
109
+ attempts.push(`${baseUrl}${devSessionStartPath}: returned 404`);
96
110
  continue;
97
111
  }
98
112
 
@@ -109,7 +123,7 @@ const requestSession = async (email: string, role: string) => {
109
123
  if (error instanceof UsageError) throw error;
110
124
 
111
125
  const message = error instanceof Error ? error.message : String(error);
112
- attempts.push(`${baseUrl}/__proteum/session/start: ${message}`);
126
+ attempts.push(`${baseUrl}${devSessionStartPath}: ${message}`);
113
127
  }
114
128
  }
115
129
 
@@ -124,9 +138,13 @@ const requestSession = async (email: string, role: string) => {
124
138
 
125
139
  const buildSessionOutput = ({
126
140
  baseUrl,
141
+ redirect,
142
+ role,
127
143
  response,
128
144
  }: {
129
145
  baseUrl: string;
146
+ redirect: string;
147
+ role: string;
130
148
  response: TDevSessionStartResponse;
131
149
  }): TResolvedSessionOutput => {
132
150
  const expires = Math.floor(Date.parse(response.session.expiresAt) / 1000);
@@ -137,6 +155,12 @@ const buildSessionOutput = ({
137
155
  user: response.user,
138
156
  session: response.session,
139
157
  browserCookie: `${response.session.cookieName}=${response.session.token}; Path=/`,
158
+ browserLoginUrl: buildDevSessionLoginUrl({
159
+ baseUrl,
160
+ email: response.user.email,
161
+ redirect,
162
+ role,
163
+ }),
140
164
  curlCookieHeader: `Cookie: ${response.session.cookieName}=${response.session.token}`,
141
165
  playwright: {
142
166
  cookies: [
@@ -170,6 +194,8 @@ const renderSession = (value: TResolvedSessionOutput) =>
170
194
  JSON.stringify(value.playwright, null, 2),
171
195
  'Browser Cookie',
172
196
  value.browserCookie,
197
+ 'Browser Login URL',
198
+ value.browserLoginUrl,
173
199
  ].join('\n');
174
200
 
175
201
  const runLocalSession = async (email: string, role: string) => {
@@ -232,6 +258,7 @@ const runLocalSession = async (email: string, role: string) => {
232
258
  export const run = async () => {
233
259
  const email = typeof cli.args.email === 'string' ? cli.args.email.trim() : '';
234
260
  const role = typeof cli.args.role === 'string' ? cli.args.role.trim() : '';
261
+ const redirect = normalizeRedirectPath(typeof cli.args.redirect === 'string' ? cli.args.redirect : '/');
235
262
  const shouldPrintJson = cli.args.json === true;
236
263
  const shouldUseRemoteServer =
237
264
  (typeof cli.args.port === 'string' && cli.args.port.length > 0) ||
@@ -242,7 +269,11 @@ export const run = async () => {
242
269
  }
243
270
 
244
271
  const resolved = buildSessionOutput(
245
- shouldUseRemoteServer ? await requestSession(email, role) : await runLocalSession(email, role),
272
+ {
273
+ ...(shouldUseRemoteServer ? await requestSession(email, role) : await runLocalSession(email, role)),
274
+ redirect,
275
+ role,
276
+ },
246
277
  );
247
278
 
248
279
  if (shouldPrintJson) {
@@ -1149,7 +1149,12 @@ const runChangedVerify = async () => {
1149
1149
  code: 'changed/check-failed',
1150
1150
  message: `Verification check "${execution.checkId}" failed with exit code ${execution.exitCode ?? 'unknown'}.`,
1151
1151
  source: 'changed',
1152
- details: [`command=${execution.command}`, `cwd=${execution.cwd}`, `durationMs=${execution.durationMs}`],
1152
+ details: [
1153
+ `command=${execution.command}`,
1154
+ `cwd=${execution.cwd}`,
1155
+ `durationMs=${execution.durationMs}`,
1156
+ 'fix=Fix the failing surface and re-run npx proteum verify changed; broader gates only per the root AGENTS.md Verification Policy.',
1157
+ ],
1153
1158
  }));
1154
1159
 
1155
1160
  return finalizeResult({
@@ -12,6 +12,7 @@ import { rspack, type Configuration, type Module } from '@rspack/core';
12
12
  import createCommonConfig, { TCompileMode, TCompileOutputTarget, regex } from '../common';
13
13
  import { createClientBundleAnalysisPlugins } from '../common/bundleAnalysis';
14
14
  import { toRspackAliases } from '../common/rspackAliases';
15
+ import { resolveUiSingletonAliases } from '../common/uiSingletons';
15
16
  import identityAssets from './identite';
16
17
  import cli from '../..';
17
18
  import { logVerbose } from '../../runtime/verbose';
@@ -35,7 +36,9 @@ const getFrameworkSourceRoot = () => {
35
36
  return activeCoreRoot;
36
37
  };
37
38
 
38
- const resolveFromAppOrCore = (_app: App, request: string) => cli.paths.resolveRequest(request);
39
+ const resolveFromAppOrCore = (request: string) => cli.paths.resolveRequest(request, { preferApp: true });
40
+ const resolvePackageRootFromAppOrCore = (packageName: string) =>
41
+ cli.paths.resolvePackageRoot(packageName, { preferApp: true });
39
42
  const rewriteFrameworkAliasTargets = (aliases: Record<string, string | string[]>) => {
40
43
  const visibleFrameworkRoots = [
41
44
  ...cli.paths.getVisiblePackageInstallRoots('proteum'),
@@ -145,9 +148,13 @@ export default function createCompiler(
145
148
  const rspackAliases = toRspackAliases(resolvedAliases);
146
149
  rspackAliases['proteum'] = frameworkSourceRoot;
147
150
  rspackAliases['@/client/router$'] = frameworkSourceRoot + '/client/router.ts';
148
- rspackAliases['preact/jsx-runtime$'] = resolveFromAppOrCore(app, 'preact/jsx-runtime');
149
- rspackAliases['react/jsx-runtime$'] = resolveFromAppOrCore(app, 'preact/jsx-runtime');
150
- rspackAliases['react/jsx-dev-runtime$'] = resolveFromAppOrCore(app, 'preact/jsx-dev-runtime');
151
+ Object.assign(
152
+ rspackAliases,
153
+ resolveUiSingletonAliases({
154
+ resolvePackageRoot: resolvePackageRootFromAppOrCore,
155
+ resolveRequest: resolveFromAppOrCore,
156
+ }),
157
+ );
151
158
 
152
159
  debug && console.log('client aliases', rspackAliases);
153
160
  const config: Configuration = {
@@ -0,0 +1,76 @@
1
+ type TUiSingletonResolvers = {
2
+ resolvePackageRoot: (packageName: string) => string;
3
+ resolveRequest: (request: string) => string;
4
+ };
5
+
6
+ const uiSingletonPackages = ['preact', 'preact-render-to-string', 'react', 'react-dom'];
7
+
8
+ const uiSingletonPackageAliases: Array<{ alias: string; packageName: string }> = [
9
+ { alias: 'preact', packageName: 'preact' },
10
+ { alias: 'preact-render-to-string', packageName: 'preact-render-to-string' },
11
+ ];
12
+
13
+ const uiSingletonRequestAliases: Array<{ alias: string; request: string }> = [
14
+ { alias: 'preact$', request: 'preact' },
15
+ { alias: 'preact/hooks$', request: 'preact/hooks' },
16
+ { alias: 'preact/compat$', request: 'preact/compat' },
17
+ { alias: 'preact/compat/client$', request: 'preact/compat/client' },
18
+ { alias: 'preact/jsx-runtime$', request: 'preact/jsx-runtime' },
19
+ { alias: 'preact/jsx-dev-runtime$', request: 'preact/jsx-dev-runtime' },
20
+ { alias: 'react$', request: 'preact/compat' },
21
+ { alias: 'react-dom$', request: 'preact/compat' },
22
+ { alias: 'react-dom/client$', request: 'preact/compat/client' },
23
+ { alias: 'react-dom/test-utils$', request: 'preact/test-utils' },
24
+ { alias: 'react/jsx-runtime$', request: 'preact/jsx-runtime' },
25
+ { alias: 'react/jsx-dev-runtime$', request: 'preact/jsx-dev-runtime' },
26
+ { alias: 'preact-render-to-string$', request: 'preact-render-to-string' },
27
+ ];
28
+
29
+ const uiSingletonServerExternalRequests: Array<{ request: string; resolvedRequest: string }> = [
30
+ { request: 'preact', resolvedRequest: 'preact' },
31
+ { request: 'preact/hooks', resolvedRequest: 'preact/hooks' },
32
+ { request: 'preact/compat', resolvedRequest: 'preact/compat' },
33
+ { request: 'preact/compat/client', resolvedRequest: 'preact/compat/client' },
34
+ { request: 'preact/jsx-runtime', resolvedRequest: 'preact/jsx-runtime' },
35
+ { request: 'preact/jsx-dev-runtime', resolvedRequest: 'preact/jsx-dev-runtime' },
36
+ { request: 'preact/test-utils', resolvedRequest: 'preact/test-utils' },
37
+ { request: 'react', resolvedRequest: 'preact/compat' },
38
+ { request: 'react-dom', resolvedRequest: 'preact/compat' },
39
+ { request: 'react-dom/client', resolvedRequest: 'preact/compat/client' },
40
+ { request: 'react-dom/test-utils', resolvedRequest: 'preact/test-utils' },
41
+ { request: 'react/jsx-runtime', resolvedRequest: 'preact/jsx-runtime' },
42
+ { request: 'react/jsx-dev-runtime', resolvedRequest: 'preact/jsx-dev-runtime' },
43
+ ];
44
+
45
+ export const isUiSingletonRequest = (request: string | undefined) =>
46
+ typeof request === 'string' &&
47
+ uiSingletonPackages.some((packageName) => request === packageName || request.startsWith(`${packageName}/`));
48
+
49
+ export const resolveUiSingletonAliases = ({ resolvePackageRoot, resolveRequest }: TUiSingletonResolvers) => ({
50
+ ...Object.fromEntries(
51
+ uiSingletonRequestAliases.map(({ alias, request }) => [
52
+ alias,
53
+ resolveRequest(request),
54
+ ]),
55
+ ),
56
+ ...Object.fromEntries(
57
+ uiSingletonPackageAliases.map(({ alias, packageName }) => [
58
+ alias,
59
+ resolvePackageRoot(packageName),
60
+ ]),
61
+ ),
62
+ });
63
+
64
+ export const resolveUiSingletonServerExternalRequest = (
65
+ request: string | undefined,
66
+ resolveRequest: TUiSingletonResolvers['resolveRequest'],
67
+ ) => {
68
+ if (request === undefined) return undefined;
69
+
70
+ const exactExternalRequest = uiSingletonServerExternalRequests.find((entry) => entry.request === request);
71
+ if (exactExternalRequest) return resolveRequest(exactExternalRequest.resolvedRequest);
72
+
73
+ if (request.startsWith('preact/')) return resolveRequest(request);
74
+
75
+ return undefined;
76
+ };