proteum 2.2.8 → 2.3.0
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 +5 -3
- package/README.md +50 -12
- package/agents/project/AGENTS.md +47 -10
- package/agents/project/CODING_STYLE.md +5 -1
- package/agents/project/client/AGENTS.md +2 -0
- package/agents/project/diagnostics.md +8 -5
- package/agents/project/optimizations.md +1 -0
- package/agents/project/root/AGENTS.md +18 -10
- package/agents/project/tests/AGENTS.md +6 -1
- package/agents/project/tests/e2e/AGENTS.md +13 -0
- package/agents/project/tests/e2e/REAL_WORLD_JOURNEY_TESTS.md +192 -0
- package/cli/commands/check.ts +21 -3
- package/cli/commands/configure.ts +1 -0
- package/cli/commands/connect.ts +40 -4
- package/cli/commands/diagnose.ts +136 -5
- package/cli/commands/doctor.ts +24 -4
- package/cli/commands/explain.ts +105 -6
- package/cli/commands/mcp.ts +16 -0
- package/cli/commands/orient.ts +66 -3
- package/cli/commands/perf.ts +118 -13
- package/cli/commands/runtime.ts +151 -0
- package/cli/commands/trace.ts +116 -21
- package/cli/mcp/provider.ts +365 -0
- package/cli/mcp/stdio.ts +16 -0
- package/cli/presentation/commands.ts +79 -22
- package/cli/presentation/devSession.ts +2 -0
- package/cli/runtime/commands.ts +95 -12
- package/cli/utils/agentOutput.ts +46 -0
- package/cli/utils/agents.ts +225 -48
- package/common/dev/inspection.ts +30 -9
- package/common/dev/mcpPayloads.ts +736 -0
- package/common/dev/mcpServer.ts +254 -0
- package/docs/agent-routing.md +126 -0
- package/docs/dev-commands.md +2 -0
- package/docs/dev-sessions.md +2 -1
- package/docs/diagnostics.md +68 -23
- package/docs/mcp.md +149 -0
- package/docs/migrate-from-2.1.3.md +15 -5
- package/docs/request-tracing.md +12 -6
- package/eslint.js +220 -0
- package/package.json +2 -1
- package/server/app/devMcp.ts +159 -0
- package/server/services/router/http/cache.ts +116 -0
- package/server/services/router/http/index.ts +94 -35
- package/server/services/router/index.ts +8 -11
- package/tests/agents-utils.test.cjs +89 -11
- package/tests/dev-transpile-watch.test.cjs +117 -8
- package/tests/eslint-rules.test.cjs +110 -0
- package/tests/inspection.test.cjs +67 -0
- package/tests/mcp.test.cjs +127 -0
- package/tests/router-cache-config.test.cjs +74 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Write Real World Journey Tests
|
|
2
|
+
|
|
3
|
+
Source: `/Users/gaetan/.codex/skills/write-real-world-journey-tests/SKILL.md`
|
|
4
|
+
|
|
5
|
+
## Goal
|
|
6
|
+
|
|
7
|
+
Create journey tests that read like an executable product spec. Prefer a realistic sequence of actions by real roles over a collection of disconnected UI checks.
|
|
8
|
+
|
|
9
|
+
The journey should cover the requested feature area broadly, but every step must be justified by normal user behavior and by state created earlier in the journey.
|
|
10
|
+
|
|
11
|
+
## Workflow
|
|
12
|
+
|
|
13
|
+
1. Inspect the feature surface before designing the test.
|
|
14
|
+
- Read existing journey tests, page objects, factories, workflows, fixtures, selectors, and test utilities.
|
|
15
|
+
- Identify local conventions for auth, data creation, navigation, assertions, cleanup, retries, and timeouts.
|
|
16
|
+
- Reuse existing helpers before adding new ones.
|
|
17
|
+
- Preserve the repository's test-file organization instead of putting every helper inside the journey spec.
|
|
18
|
+
|
|
19
|
+
2. Follow the local E2E file organization.
|
|
20
|
+
- Put complete user journeys in `tests/e2e/journeys/*.spec.ts` or the repository's equivalent journey/spec directory.
|
|
21
|
+
- Put reusable screen abstractions in `tests/e2e/pages/**`, grouped by product area; use focused page objects for pages, modals, filter bars, pickers, and detail panels.
|
|
22
|
+
- Put reusable multi-step business flows in `tests/e2e/workflows/**`, such as login, signup via invite, create entity with invite link, or other flows shared by several journeys.
|
|
23
|
+
- Put generated domain data in `tests/e2e/factories/**`, with separate minimum, maximum, and edited payload factories when the journey needs create/edit coverage.
|
|
24
|
+
- Put shared technical/domain helpers in `tests/e2e/utils/**`, such as context creation, constants, API response parsing, date formatting, KPI expectations, number/currency parsers, display-name builders, and journey tree logging.
|
|
25
|
+
- Add a helper to the nearest existing page/workflow/factory/utils module when it is reusable; keep it local to the spec only when it is specific to that one journey's memory or assertions.
|
|
26
|
+
|
|
27
|
+
3. Map the real-world story.
|
|
28
|
+
- List the roles involved, their permissions, and why each role appears in the scenario.
|
|
29
|
+
- Identify the entities that move through the journey: account, team, organization, customer, order, lead, report, subscription, etc.
|
|
30
|
+
- Define the natural order: setup, invite/signup/login, empty state, creation, assignment, action by another role, management, reporting, audit/review.
|
|
31
|
+
- Prefer one coherent story over feature stuffing. Cover many features only when they belong to the same user journey.
|
|
32
|
+
|
|
33
|
+
4. Make the report-visible hierarchy explicit.
|
|
34
|
+
- Group journeys with `describe` blocks or the framework equivalent by business phase, such as acquisition, activation, setup, core workflow, limits, billing, and admin review.
|
|
35
|
+
- Split each journey test into named substeps with `test.step(...)`, Cypress command logs, or the local framework equivalent so CI reports and traces show what business phase failed.
|
|
36
|
+
- Name steps from the user or business perspective, such as `Launch plan blocks the third filter`, not from implementation details, such as `Click button`.
|
|
37
|
+
- Use nested substeps for repeated matrices like roles, plans, permissions, limits, or locales, with one visible step per case and smaller substeps for settings, allowed actions, and blocked actions.
|
|
38
|
+
- Preserve local file organization and helper conventions; hierarchy should improve readability without moving unrelated code or hiding assertions.
|
|
39
|
+
|
|
40
|
+
5. Use serial state only for real dependency chains.
|
|
41
|
+
- Use a serial describe block when later tests intentionally depend on state created by earlier actors.
|
|
42
|
+
- Split the journey into tests by actor/session or meaningful product phase.
|
|
43
|
+
- Use fresh browser contexts or sessions for different roles.
|
|
44
|
+
- Keep setup assertions at the top of each dependent test so failures explain the missing prerequisite.
|
|
45
|
+
|
|
46
|
+
6. Keep journey memory as the source of truth.
|
|
47
|
+
- Store created IDs, invite links, generated users, current payloads, assignment state, statuses, dates, and monetary values in typed in-memory objects.
|
|
48
|
+
- Update memory immediately after successful UI/API responses.
|
|
49
|
+
- Compute expectations from memory instead of duplicating constants in assertions.
|
|
50
|
+
- Track both `createdPayload` and `currentPayload` when edits are part of the journey.
|
|
51
|
+
|
|
52
|
+
7. Assert the product contract at each phase.
|
|
53
|
+
- Empty states and initial KPIs for new users.
|
|
54
|
+
- Role-specific navigation, tabs, menu labels, and restricted access.
|
|
55
|
+
- Created rows/cards/details match generated input.
|
|
56
|
+
- Default ownership, assignment, channel/source, payment terms, status, or permission behavior.
|
|
57
|
+
- Cross-role visibility after another role acts.
|
|
58
|
+
- Details modals, edit flows, notes/activity timelines, status changes, derived fields, filters, date ranges, and aggregate KPIs.
|
|
59
|
+
- Final manager/admin review across the whole organization or feature scope.
|
|
60
|
+
|
|
61
|
+
8. Make derived assertions resilient.
|
|
62
|
+
- Use polling for eventually consistent UI totals, KPI cards, async table updates, revenue/pipeline calculations, closing rates, and status text.
|
|
63
|
+
- Parse formatted numbers, currency, and percentages with shared utilities.
|
|
64
|
+
- Use deterministic date helpers for UI date formatting and date-range assertions.
|
|
65
|
+
- Wait for important create/update responses when IDs or persistence are needed.
|
|
66
|
+
|
|
67
|
+
9. Prefer expressive helpers.
|
|
68
|
+
- Introduce small helpers for repeated domain assertions, such as row basics, row KPI checks, snapshot comparison, filter setup, and derived totals.
|
|
69
|
+
- Keep helper names product-facing, not implementation-facing.
|
|
70
|
+
- Use page objects and workflow helpers to keep the test readable at the story level.
|
|
71
|
+
|
|
72
|
+
10. Log or snapshot the journey shape when useful.
|
|
73
|
+
- For long multi-role journeys, optionally keep a small tree or timeline logger showing created roles and entities.
|
|
74
|
+
- Use it for debugging and readability, not as a substitute for assertions.
|
|
75
|
+
|
|
76
|
+
## Implementation Pattern
|
|
77
|
+
|
|
78
|
+
Use this shape as a guide, adapting to the repository's test framework:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
test.describe.serial('Domain - Feature journey', () => {
|
|
82
|
+
test.describe.configure({ timeout: 300_000 });
|
|
83
|
+
|
|
84
|
+
let organization = createEmptyOrganizationMemory();
|
|
85
|
+
let manager = createEmptyAccountMemory();
|
|
86
|
+
let operator = createEmptyAccountMemory();
|
|
87
|
+
let externalPartner = createEmptyPartnerMemory();
|
|
88
|
+
let itemA = createItemMemory(createMinimumPayload(), { type: 'operator' });
|
|
89
|
+
let itemB = createItemMemory(createMaximumPayload(), { type: 'external-partner' });
|
|
90
|
+
|
|
91
|
+
const getCreatedItems = () => [itemA, itemB].filter((item) => item.id);
|
|
92
|
+
const buildKpis = () => ({
|
|
93
|
+
'Total Items': getCreatedItems().length,
|
|
94
|
+
'Completed': getCreatedItems().filter((item) => item.status === 'Completed').length,
|
|
95
|
+
});
|
|
96
|
+
const matrixCases = [{ name: 'Manager' }, { name: 'Operator' }];
|
|
97
|
+
|
|
98
|
+
test.describe('Setup and activation', () => {
|
|
99
|
+
test('Admin or owner creates the parent scope', async ({ browser }) => {
|
|
100
|
+
await test.step('Create the organization through the normal product path', async () => {
|
|
101
|
+
// Store IDs, invite links, and display data in memory.
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('Manager signs up and prepares the working structure', async ({ browser }) => {
|
|
106
|
+
await test.step('Assert restricted navigation and empty state', async () => {});
|
|
107
|
+
await test.step('Create the team and invite the next role', async () => {});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test.describe('Core workflow', () => {
|
|
112
|
+
test('Primary role performs the core workflow and delegates work', async ({ browser }) => {
|
|
113
|
+
await test.step('Create a minimum item', async () => {});
|
|
114
|
+
await test.step('Assert default ownership and visible table state', async () => {});
|
|
115
|
+
await test.step('Delegate the item to another role', async () => {});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('Secondary role acts on assigned state', async ({ browser }) => {
|
|
119
|
+
await test.step('Assert this role only sees assigned work', async () => {});
|
|
120
|
+
await test.step('Create or update a maximum-data item', async () => {});
|
|
121
|
+
await test.step('Assert role-specific defaults and KPIs', async () => {});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('Primary role manages resulting activity', async ({ browser }) => {
|
|
125
|
+
await test.step('Open details and validate data from the secondary role', async () => {});
|
|
126
|
+
await test.step('Add notes, edit fields, and change status', async () => {});
|
|
127
|
+
await test.step('Assert recalculated derived values', async () => {});
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test.describe('Review and reporting', () => {
|
|
132
|
+
test('Manager or auditor reviews aggregate state', async ({ browser }) => {
|
|
133
|
+
await test.step('Assert cross-role visibility and filters', async () => {});
|
|
134
|
+
await test.step('Assert aggregate KPIs and final business outcomes', async () => {});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('Plan, role, or permission matrix exposes the correct behavior', async ({ browser }) => {
|
|
138
|
+
for (const caseItem of matrixCases) {
|
|
139
|
+
await test.step(`${caseItem.name} shows the right settings and limits`, async () => {
|
|
140
|
+
await test.step(`${caseItem.name} settings match contract`, async () => {});
|
|
141
|
+
await test.step(`${caseItem.name} allowed actions succeed`, async () => {});
|
|
142
|
+
await test.step(`${caseItem.name} blocked actions fail with the expected reason`, async () => {});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Coverage Heuristics
|
|
151
|
+
|
|
152
|
+
Include a feature when it naturally belongs to the journey and creates a durable assertion later. Strong candidates:
|
|
153
|
+
|
|
154
|
+
- Invite/signup/login boundaries.
|
|
155
|
+
- Permission differences between roles.
|
|
156
|
+
- Empty state to populated state.
|
|
157
|
+
- Minimum-data and maximum-data creation paths.
|
|
158
|
+
- Assignment, reassignment, ownership, membership, or sharing.
|
|
159
|
+
- Details view, edit view, notes, activity history, and status changes.
|
|
160
|
+
- Derived totals, dashboards, reporting cards, and tables.
|
|
161
|
+
- Filters, search, date ranges, sorting, and snapshots.
|
|
162
|
+
- Cross-role consistency: what one actor creates, another actor sees or manages.
|
|
163
|
+
|
|
164
|
+
Exclude or move to narrower tests:
|
|
165
|
+
|
|
166
|
+
- Pure component styling.
|
|
167
|
+
- Exhaustive validation errors.
|
|
168
|
+
- Every filter permutation.
|
|
169
|
+
- Rare edge cases that interrupt the main story.
|
|
170
|
+
- Admin-only setup that is unrelated to the requested feature.
|
|
171
|
+
|
|
172
|
+
## Quality Bar
|
|
173
|
+
|
|
174
|
+
- The scenario should be explainable as a real customer workflow.
|
|
175
|
+
- Each role should do work that role would actually do.
|
|
176
|
+
- Assertions should prove business behavior, not only that buttons are clickable.
|
|
177
|
+
- Later assertions should depend on earlier created state.
|
|
178
|
+
- Names, IDs, and dates should be generated uniquely enough for shared test environments.
|
|
179
|
+
- The test should fail near the broken product behavior, with step names that explain the business phase.
|
|
180
|
+
- The test report should expose a clear hierarchy of phases, tests, and substeps so a reader can understand the journey without opening the source file.
|
|
181
|
+
- Comments should clarify product intent or non-obvious timing behavior, not narrate obvious code.
|
|
182
|
+
|
|
183
|
+
## Anti-Patterns
|
|
184
|
+
|
|
185
|
+
- One huge test with no actor boundaries.
|
|
186
|
+
- Flat journey tests with no report-visible phases or substeps.
|
|
187
|
+
- Serial dependency without explicit prerequisite assertions.
|
|
188
|
+
- Hardcoded duplicate KPI values that drift after edits.
|
|
189
|
+
- Creating data directly in the database when the product flow under test is creation, signup, invite, assignment, or editing.
|
|
190
|
+
- Covering unrelated features just to increase line count.
|
|
191
|
+
- Hiding flakiness with arbitrary sleeps instead of waiting on UI state, network responses, or polling derived values.
|
|
192
|
+
- Testing only the creator role when the feature's value appears through another role's visibility or management flow.
|
package/cli/commands/check.ts
CHANGED
|
@@ -19,19 +19,37 @@ export const run = async (): Promise<void> => {
|
|
|
19
19
|
|
|
20
20
|
console.info(
|
|
21
21
|
[
|
|
22
|
-
await renderTitle('PROTEUM CHECK', 'Refreshing contracts, running TypeScript
|
|
22
|
+
await renderTitle('PROTEUM CHECK', 'Refreshing contracts, then running TypeScript and ESLint.'),
|
|
23
23
|
renderRows([{ label: 'app', value: cli.paths.appRoot === process.cwd() ? '.' : cli.paths.appRoot }]),
|
|
24
24
|
].join('\n\n'),
|
|
25
25
|
);
|
|
26
|
+
|
|
26
27
|
if (hasAppConfig()) {
|
|
27
28
|
console.info(await renderStep('[1/3]', 'Refreshing generated typings.'));
|
|
28
29
|
await refreshGeneratedTypings();
|
|
29
30
|
} else {
|
|
30
31
|
console.info(await renderStep('[1/3]', 'Skipping generated typings: no Proteum app config found.'));
|
|
31
32
|
}
|
|
33
|
+
|
|
34
|
+
const failures: Error[] = [];
|
|
35
|
+
|
|
32
36
|
console.info(await renderStep('[2/3]', 'Running TypeScript typechecking.'));
|
|
33
|
-
|
|
37
|
+
try {
|
|
38
|
+
await runAppTypecheck();
|
|
39
|
+
} catch (error) {
|
|
40
|
+
failures.push(error instanceof Error ? error : new Error(String(error)));
|
|
41
|
+
}
|
|
42
|
+
|
|
34
43
|
console.info(await renderStep('[3/3]', 'Running ESLint.'));
|
|
35
|
-
|
|
44
|
+
try {
|
|
45
|
+
await runAppLint();
|
|
46
|
+
} catch (error) {
|
|
47
|
+
failures.push(error instanceof Error ? error : new Error(String(error)));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (failures.length > 0) {
|
|
51
|
+
throw new AggregateError(failures, 'Proteum check failed. See TypeScript and ESLint output above.');
|
|
52
|
+
}
|
|
53
|
+
|
|
36
54
|
console.info(await renderSuccess('All checks passed.'));
|
|
37
55
|
};
|
|
@@ -119,6 +119,7 @@ const renderConfigureResultSections = (result: TConfigureProjectAgentInstruction
|
|
|
119
119
|
if (result.updated.length > 0) sections.push(['Updated:', ...result.updated.map((entry) => `- ${entry}`)].join('\n'));
|
|
120
120
|
if (result.overwritten.length > 0)
|
|
121
121
|
sections.push(['Overwritten:', ...result.overwritten.map((entry) => `- ${entry}`)].join('\n'));
|
|
122
|
+
if (result.removed.length > 0) sections.push(['Removed:', ...result.removed.map((entry) => `- ${entry}`)].join('\n'));
|
|
122
123
|
if (result.updatedGitignores.length > 0)
|
|
123
124
|
sections.push(['Updated .gitignore:', ...result.updatedGitignores.map((entry) => `- ${entry}`)].join('\n'));
|
|
124
125
|
if (result.blocked.length > 0)
|
package/cli/commands/connect.ts
CHANGED
|
@@ -2,8 +2,9 @@ import cli from '..';
|
|
|
2
2
|
import Compiler from '../compiler';
|
|
3
3
|
import { readProteumManifest } from '../compiler/common/proteumManifest';
|
|
4
4
|
import { buildConnectResponse, renderConnectHuman } from '@common/dev/connect';
|
|
5
|
+
import { compactList, printAgentResponse, printJson, truncateForAgent } from '../utils/agentOutput';
|
|
5
6
|
|
|
6
|
-
const allowedConnectArgs = new Set(['controllers', 'json', 'strict']);
|
|
7
|
+
const allowedConnectArgs = new Set(['controllers', 'full', 'human', 'json', 'strict']);
|
|
7
8
|
|
|
8
9
|
const validateConnectArgs = () => {
|
|
9
10
|
const enabledArgs = Object.entries(cli.args)
|
|
@@ -19,6 +20,29 @@ const validateConnectArgs = () => {
|
|
|
19
20
|
}
|
|
20
21
|
};
|
|
21
22
|
|
|
23
|
+
const compactDiagnostic = (diagnostic: ReturnType<typeof buildConnectResponse>['diagnostics'][number]) => ({
|
|
24
|
+
level: diagnostic.level,
|
|
25
|
+
code: diagnostic.code,
|
|
26
|
+
message: truncateForAgent(diagnostic.message),
|
|
27
|
+
filepath: diagnostic.filepath,
|
|
28
|
+
sourceLocation: diagnostic.sourceLocation,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const compactProject = (project: ReturnType<typeof buildConnectResponse>['projects'][number]) => ({
|
|
32
|
+
namespace: project.namespace,
|
|
33
|
+
identityIdentifier: project.identityIdentifier,
|
|
34
|
+
identityName: project.identityName,
|
|
35
|
+
sourceKind: project.sourceKind,
|
|
36
|
+
sourceConfigured: project.sourceConfigured,
|
|
37
|
+
urlInternalConfigured: project.urlInternalConfigured,
|
|
38
|
+
urlInternal: project.urlInternal,
|
|
39
|
+
controllerCount: project.controllerCount,
|
|
40
|
+
cachedContractExists: project.cachedContractExists,
|
|
41
|
+
cachedContractFilepath: project.cachedContractFilepath,
|
|
42
|
+
typingMode: project.typingMode,
|
|
43
|
+
controllers: project.controllers ? compactList(project.controllers, 8) : undefined,
|
|
44
|
+
});
|
|
45
|
+
|
|
22
46
|
export const run = async (): Promise<void> => {
|
|
23
47
|
validateConnectArgs();
|
|
24
48
|
|
|
@@ -31,10 +55,22 @@ export const run = async (): Promise<void> => {
|
|
|
31
55
|
strict: cli.args.strict === true,
|
|
32
56
|
});
|
|
33
57
|
|
|
34
|
-
if (cli.args.
|
|
35
|
-
|
|
36
|
-
} else {
|
|
58
|
+
if (cli.args.full === true) {
|
|
59
|
+
printJson(response);
|
|
60
|
+
} else if (cli.args.human === true) {
|
|
37
61
|
console.log(renderConnectHuman(manifest, response));
|
|
62
|
+
} else {
|
|
63
|
+
printAgentResponse({
|
|
64
|
+
summary: `Connect: ${response.summary.connectedProjects} projects, ${response.summary.importedControllers} controllers, ${response.summary.errors} errors, ${response.summary.warnings} warnings`,
|
|
65
|
+
data: {
|
|
66
|
+
app: response.app,
|
|
67
|
+
summary: response.summary,
|
|
68
|
+
projects: response.projects.map(compactProject),
|
|
69
|
+
diagnostics: compactList(response.diagnostics, 10).map(compactDiagnostic),
|
|
70
|
+
totalDiagnostics: response.diagnostics.length,
|
|
71
|
+
},
|
|
72
|
+
fullDetailCommand: `proteum connect${cli.args.controllers === true ? ' --controllers' : ''} --full`,
|
|
73
|
+
});
|
|
38
74
|
}
|
|
39
75
|
|
|
40
76
|
if (cli.args.strict === true && response.diagnostics.length > 0) {
|
package/cli/commands/diagnose.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { TRequestTraceErrorResponse, TRequestTraceArmResponse } from '../..
|
|
|
12
12
|
import type { TDevSessionErrorResponse, TDevSessionStartResponse } from '../../common/dev/session';
|
|
13
13
|
import { summarizeTraceForDiagnose } from '@common/dev/inspection';
|
|
14
14
|
import { readProteumManifest } from '../compiler/common/proteumManifest';
|
|
15
|
+
import { compactList, printAgentResponse, printJson, quoteCommandArgument, truncateForAgent } from '../utils/agentOutput';
|
|
15
16
|
|
|
16
17
|
const normalizeBaseUrl = (value: string) => value.replace(/\/+$/, '');
|
|
17
18
|
const truncate = (value: string, max = 160) => (value.length <= max ? value : `${value.slice(0, max)}...`);
|
|
@@ -237,6 +238,132 @@ const renderHuman = (manifest: ReturnType<typeof readProteumManifest>, response:
|
|
|
237
238
|
renderLogs(response.serverLogs),
|
|
238
239
|
].join('\n');
|
|
239
240
|
|
|
241
|
+
const compactOwnerMatch = (match: TDiagnoseResponse['owner']['matches'][number]) => ({
|
|
242
|
+
kind: match.kind,
|
|
243
|
+
label: match.label,
|
|
244
|
+
score: match.score,
|
|
245
|
+
scope: match.scopeLabel,
|
|
246
|
+
origin: match.originHint,
|
|
247
|
+
source: match.source,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const compactDiagnostic = (diagnostic: TDiagnoseResponse['doctor']['diagnostics'][number]) => ({
|
|
251
|
+
level: diagnostic.level,
|
|
252
|
+
code: diagnostic.code,
|
|
253
|
+
message: truncateForAgent(diagnostic.message),
|
|
254
|
+
filepath: diagnostic.filepath,
|
|
255
|
+
sourceLocation: diagnostic.sourceLocation,
|
|
256
|
+
fixHint: diagnostic.fixHint ? truncateForAgent(diagnostic.fixHint) : undefined,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const compactRequest = (request: TDiagnoseResponse['request']) =>
|
|
260
|
+
request
|
|
261
|
+
? {
|
|
262
|
+
id: request.id,
|
|
263
|
+
method: request.method,
|
|
264
|
+
path: request.path,
|
|
265
|
+
statusCode: request.statusCode,
|
|
266
|
+
durationMs: request.durationMs,
|
|
267
|
+
capture: request.capture,
|
|
268
|
+
user: request.user,
|
|
269
|
+
errorMessage: request.errorMessage,
|
|
270
|
+
counts: {
|
|
271
|
+
calls: request.calls.length,
|
|
272
|
+
events: request.events.length,
|
|
273
|
+
sqlQueries: request.sqlQueries.length,
|
|
274
|
+
droppedEvents: request.droppedEvents,
|
|
275
|
+
},
|
|
276
|
+
}
|
|
277
|
+
: undefined;
|
|
278
|
+
|
|
279
|
+
const buildDiagnoseFullDetailCommand = ({
|
|
280
|
+
hitPath,
|
|
281
|
+
query,
|
|
282
|
+
}: {
|
|
283
|
+
hitPath: string;
|
|
284
|
+
query: string;
|
|
285
|
+
}) =>
|
|
286
|
+
[
|
|
287
|
+
'proteum diagnose',
|
|
288
|
+
quoteCommandArgument(query),
|
|
289
|
+
hitPath ? `--hit ${quoteCommandArgument(hitPath)}` : '',
|
|
290
|
+
typeof cli.args.port === 'string' && cli.args.port ? `--port ${cli.args.port}` : '',
|
|
291
|
+
typeof cli.args.url === 'string' && cli.args.url ? `--url ${quoteCommandArgument(cli.args.url)}` : '',
|
|
292
|
+
'--full',
|
|
293
|
+
]
|
|
294
|
+
.filter(Boolean)
|
|
295
|
+
.join(' ');
|
|
296
|
+
|
|
297
|
+
const printCompactDiagnose = ({
|
|
298
|
+
hitPath,
|
|
299
|
+
query,
|
|
300
|
+
response,
|
|
301
|
+
}: {
|
|
302
|
+
hitPath: string;
|
|
303
|
+
query: string;
|
|
304
|
+
response: TDiagnoseResponse;
|
|
305
|
+
}) => {
|
|
306
|
+
const request = compactRequest(response.request);
|
|
307
|
+
const traceSummary = summarizeTraceForDiagnose(response.request);
|
|
308
|
+
const doctorSummary = `${response.doctor.summary.errors} doctor errors/${response.doctor.summary.warnings} warnings`;
|
|
309
|
+
const contractsSummary = `${response.contracts.summary.errors} contract errors/${response.contracts.summary.warnings} warnings`;
|
|
310
|
+
const summary = `${response.query || query || 'request'}: ${traceSummary}; ${doctorSummary}; ${contractsSummary}`;
|
|
311
|
+
const nextActions = response.orientation?.nextSteps || [];
|
|
312
|
+
const requestId = response.request?.id;
|
|
313
|
+
|
|
314
|
+
printAgentResponse({
|
|
315
|
+
summary,
|
|
316
|
+
data: {
|
|
317
|
+
query: response.query,
|
|
318
|
+
request,
|
|
319
|
+
owner: {
|
|
320
|
+
top: response.owner.matches[0] ? compactOwnerMatch(response.owner.matches[0]) : undefined,
|
|
321
|
+
matches: compactList(response.owner.matches, 4).map(compactOwnerMatch),
|
|
322
|
+
totalReturned: response.owner.matches.length,
|
|
323
|
+
},
|
|
324
|
+
suspects: compactList(response.suspects, 6),
|
|
325
|
+
chain: compactList(response.chain || [], 8),
|
|
326
|
+
diagnostics: {
|
|
327
|
+
doctor: {
|
|
328
|
+
summary: response.doctor.summary,
|
|
329
|
+
top: compactList(response.doctor.diagnostics, 6).map(compactDiagnostic),
|
|
330
|
+
total: response.doctor.diagnostics.length,
|
|
331
|
+
},
|
|
332
|
+
contracts: {
|
|
333
|
+
summary: response.contracts.summary,
|
|
334
|
+
top: compactList(response.contracts.diagnostics, 6).map(compactDiagnostic),
|
|
335
|
+
total: response.contracts.diagnostics.length,
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
logs: compactList(response.serverLogs.logs, 10).map((entry) => ({
|
|
339
|
+
level: entry.level,
|
|
340
|
+
time: entry.time,
|
|
341
|
+
text: truncateForAgent(entry.text),
|
|
342
|
+
})),
|
|
343
|
+
instructions: response.orientation
|
|
344
|
+
? {
|
|
345
|
+
mustRead: [...new Set([response.orientation.guidance.agents, ...response.orientation.guidance.areaAgents])],
|
|
346
|
+
diagnostics: response.orientation.guidance.diagnostics,
|
|
347
|
+
codingStyle: response.orientation.guidance.codingStyle,
|
|
348
|
+
optimizations: response.orientation.guidance.optimizations,
|
|
349
|
+
}
|
|
350
|
+
: undefined,
|
|
351
|
+
},
|
|
352
|
+
nextActions,
|
|
353
|
+
fullDetailCommand: buildDiagnoseFullDetailCommand({ hitPath, query }),
|
|
354
|
+
omitted: [
|
|
355
|
+
...(requestId
|
|
356
|
+
? [
|
|
357
|
+
{
|
|
358
|
+
reason: 'Full request events, API call payloads, and SQL text are omitted from the default diagnose response.',
|
|
359
|
+
command: `proteum trace show ${quoteCommandArgument(requestId)} --events`,
|
|
360
|
+
},
|
|
361
|
+
]
|
|
362
|
+
: []),
|
|
363
|
+
],
|
|
364
|
+
});
|
|
365
|
+
};
|
|
366
|
+
|
|
240
367
|
const resolveManifest = async () => {
|
|
241
368
|
try {
|
|
242
369
|
return readProteumManifest(cli.paths.appRoot);
|
|
@@ -258,7 +385,6 @@ export const run = async () => {
|
|
|
258
385
|
const method = typeof cli.args.method === 'string' && cli.args.method ? cli.args.method.trim().toUpperCase() : 'GET';
|
|
259
386
|
const logsLevel = typeof cli.args.logsLevel === 'string' && cli.args.logsLevel ? cli.args.logsLevel.trim() : 'warn';
|
|
260
387
|
const logsLimit = typeof cli.args.logsLimit === 'string' && cli.args.logsLimit ? cli.args.logsLimit.trim() : '40';
|
|
261
|
-
const shouldPrintJson = cli.args.json === true;
|
|
262
388
|
const hitPath = hit || (target.startsWith('/') ? target : '');
|
|
263
389
|
const query = target || hitPath;
|
|
264
390
|
let parsedDataJson: unknown;
|
|
@@ -308,11 +434,16 @@ export const run = async () => {
|
|
|
308
434
|
const diagnose = await requestJson<TDiagnoseResponse>(
|
|
309
435
|
`/__proteum/diagnose?${new URLSearchParams(diagnoseRequest).toString()}`,
|
|
310
436
|
);
|
|
311
|
-
if (
|
|
312
|
-
|
|
437
|
+
if (cli.args.full === true) {
|
|
438
|
+
printJson(diagnose.body);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (cli.args.human === true) {
|
|
443
|
+
const manifest = await resolveManifest();
|
|
444
|
+
console.log(renderHuman(manifest, diagnose.body));
|
|
313
445
|
return;
|
|
314
446
|
}
|
|
315
447
|
|
|
316
|
-
|
|
317
|
-
console.log(renderHuman(manifest, diagnose.body));
|
|
448
|
+
printCompactDiagnose({ hitPath, query, response: diagnose.body });
|
|
318
449
|
};
|
package/cli/commands/doctor.ts
CHANGED
|
@@ -3,8 +3,9 @@ import Compiler from '../compiler';
|
|
|
3
3
|
import { readProteumManifest } from '../compiler/common/proteumManifest';
|
|
4
4
|
import { buildContractsDoctorResponse } from '@common/dev/contractsDoctor';
|
|
5
5
|
import { buildDoctorResponse, renderDoctorHuman, renderDoctorResponseHuman } from '@common/dev/diagnostics';
|
|
6
|
+
import { compactList, printAgentResponse, printJson, truncateForAgent } from '../utils/agentOutput';
|
|
6
7
|
|
|
7
|
-
const allowedDoctorArgs = new Set(['contracts', 'json', 'strict']);
|
|
8
|
+
const allowedDoctorArgs = new Set(['contracts', 'full', 'human', 'json', 'strict']);
|
|
8
9
|
|
|
9
10
|
const validateDoctorArgs = () => {
|
|
10
11
|
const enabledArgs = Object.entries(cli.args)
|
|
@@ -20,6 +21,15 @@ const validateDoctorArgs = () => {
|
|
|
20
21
|
}
|
|
21
22
|
};
|
|
22
23
|
|
|
24
|
+
const compactDiagnostic = (diagnostic: ReturnType<typeof buildDoctorResponse>['diagnostics'][number]) => ({
|
|
25
|
+
level: diagnostic.level,
|
|
26
|
+
code: diagnostic.code,
|
|
27
|
+
message: truncateForAgent(diagnostic.message),
|
|
28
|
+
filepath: diagnostic.filepath,
|
|
29
|
+
sourceLocation: diagnostic.sourceLocation,
|
|
30
|
+
fixHint: diagnostic.fixHint ? truncateForAgent(diagnostic.fixHint) : undefined,
|
|
31
|
+
});
|
|
32
|
+
|
|
23
33
|
export const run = async (): Promise<void> => {
|
|
24
34
|
validateDoctorArgs();
|
|
25
35
|
|
|
@@ -32,9 +42,9 @@ export const run = async (): Promise<void> => {
|
|
|
32
42
|
? buildContractsDoctorResponse(manifest, cli.args.strict === true)
|
|
33
43
|
: buildDoctorResponse(manifest, cli.args.strict === true);
|
|
34
44
|
|
|
35
|
-
if (cli.args.
|
|
36
|
-
|
|
37
|
-
} else {
|
|
45
|
+
if (cli.args.full === true) {
|
|
46
|
+
printJson(response);
|
|
47
|
+
} else if (cli.args.human === true) {
|
|
38
48
|
console.log(
|
|
39
49
|
cli.args.contracts === true
|
|
40
50
|
? renderDoctorResponseHuman({
|
|
@@ -45,6 +55,16 @@ export const run = async (): Promise<void> => {
|
|
|
45
55
|
})
|
|
46
56
|
: renderDoctorHuman(manifest, cli.args.strict === true),
|
|
47
57
|
);
|
|
58
|
+
} else {
|
|
59
|
+
printAgentResponse({
|
|
60
|
+
summary: `${cli.args.contracts === true ? 'Doctor contracts' : 'Doctor'}: ${response.summary.errors} errors, ${response.summary.warnings} warnings`,
|
|
61
|
+
data: {
|
|
62
|
+
summary: response.summary,
|
|
63
|
+
diagnostics: compactList(response.diagnostics, 10).map(compactDiagnostic),
|
|
64
|
+
totalDiagnostics: response.diagnostics.length,
|
|
65
|
+
},
|
|
66
|
+
fullDetailCommand: `proteum doctor${cli.args.contracts === true ? ' --contracts' : ''} --full`,
|
|
67
|
+
});
|
|
48
68
|
}
|
|
49
69
|
|
|
50
70
|
if (cli.args.strict === true && response.diagnostics.length > 0) {
|