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
|
@@ -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
|
-
-
|
|
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
|
-
-
|
|
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,
|
|
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
|
|
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.
|
|
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
|
-
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
+
};
|
package/cli/commands/session.ts
CHANGED
|
@@ -4,7 +4,13 @@ import { spawn } from 'child_process';
|
|
|
4
4
|
import { UsageError } from 'clipanion';
|
|
5
5
|
|
|
6
6
|
import cli from '..';
|
|
7
|
-
import
|
|
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}
|
|
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}
|
|
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}
|
|
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
|
-
|
|
272
|
+
{
|
|
273
|
+
...(shouldUseRemoteServer ? await requestSession(email, role) : await runLocalSession(email, role)),
|
|
274
|
+
redirect,
|
|
275
|
+
role,
|
|
276
|
+
},
|
|
246
277
|
);
|
|
247
278
|
|
|
248
279
|
if (shouldPrintJson) {
|
package/cli/commands/verify.ts
CHANGED
|
@@ -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: [
|
|
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 = (
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
+
};
|