@monster-orange/den 0.0.0-stage → 0.1.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/CHANGELOG.md +67 -0
- package/LICENSE +21 -0
- package/README.md +166 -2
- package/bin/den +61 -0
- package/docs/distributions.md +256 -0
- package/make/demo.mk +82 -0
- package/make/den.mk +80 -0
- package/make/journeys.mk +57 -0
- package/make/local_dev.mk +154 -0
- package/make/security.mk +59 -0
- package/make/test.mk +405 -0
- package/package.json +104 -4
- package/scripts/auth-configure.ts +415 -0
- package/scripts/heal-config.ts +92 -0
- package/scripts/jsonl-playwright-reporter.js +70 -0
- package/scripts/manifest-read.cjs +91 -0
- package/scripts/setup-1password.ts +570 -0
- package/scripts/setup-secrets.ts +149 -0
- package/scripts/view-results.js +156 -0
- package/service/install.sh +112 -0
- package/src/pillars/api/fixtures/index.ts +128 -0
- package/src/pillars/api/fixtures/openapi-validator.ts +111 -0
- package/src/pillars/api/global-setup.ts +103 -0
- package/src/pillars/api/playwright.config.ts +105 -0
- package/src/pillars/api/schemas/example-api.json +110 -0
- package/src/pillars/api/tests/health/health.spec.ts +19 -0
- package/src/pillars/api/tests/health/manifest.yml +6 -0
- package/src/pillars/browser/fixtures/index.ts +232 -0
- package/src/pillars/browser/fixtures/package.json +10 -0
- package/src/pillars/browser/global-setup.ts +140 -0
- package/src/pillars/browser/global-teardown.ts +48 -0
- package/src/pillars/browser/playwright.config.ts +212 -0
- package/src/pillars/browser/setup.ts +219 -0
- package/src/pillars/journeys/app-config.ts +237 -0
- package/src/pillars/journeys/discover.ts +160 -0
- package/src/pillars/journeys/hooks/deny-shell.cjs +44 -0
- package/src/pillars/journeys/observability/events.ts +288 -0
- package/src/pillars/journeys/observability/sdk-mapper.ts +306 -0
- package/src/pillars/journeys/report/friction.ts +304 -0
- package/src/pillars/journeys/report/junit.ts +105 -0
- package/src/pillars/journeys/report/render.ts +273 -0
- package/src/pillars/journeys/report/results.ts +323 -0
- package/src/pillars/journeys/run.ts +673 -0
- package/src/pillars/journeys/runner.ts +1025 -0
- package/src/pillars/journeys/sandbox.ts +406 -0
- package/src/pillars/journeys/spec/loader.ts +152 -0
- package/src/pillars/journeys/spec/schema.ts +208 -0
- package/src/pillars/journeys/teardown.ts +98 -0
- package/src/pillars/journeys/tools/browser-session.ts +219 -0
- package/src/pillars/journeys/tools/browser-tool.ts +914 -0
- package/src/pillars/journeys/tools/http-tool.ts +564 -0
- package/src/pillars/journeys/tools/ledger.ts +278 -0
- package/src/pillars/journeys/tools/snapshot-store.ts +117 -0
- package/src/pillars/journeys/verify/assertions.ts +502 -0
- package/src/pillars/journeys/verify/verdict.ts +71 -0
- package/src/pillars/perf/build-report.ts +36 -0
- package/src/pillars/perf/k6.config.js +112 -0
- package/src/pillars/perf/run.ts +345 -0
- package/src/pillars/perf/scenarios/load-profile.js +97 -0
- package/src/pillars/perf/staging.ts +73 -0
- package/src/pillars/security/discover.ts +108 -0
- package/src/pillars/security/findings.ts +151 -0
- package/src/pillars/security/plan.ts +355 -0
- package/src/pillars/security/run.ts +729 -0
- package/src/pillars/smoke/admin-probe.ts +68 -0
- package/src/pillars/smoke/base-url.ts +56 -0
- package/src/pillars/smoke/cleanup.ts +8 -0
- package/src/pillars/smoke/discover.ts +237 -0
- package/src/pillars/smoke/ensure-smoke-teardown.ts +40 -0
- package/src/pillars/smoke/fixture-manager.ts +217 -0
- package/src/pillars/smoke/fixtures.ts +468 -0
- package/src/pillars/smoke/global-setup-all.ts +262 -0
- package/src/pillars/smoke/global-setup-global.ts +262 -0
- package/src/pillars/smoke/global-setup.ts +273 -0
- package/src/pillars/smoke/global-teardown-all.ts +8 -0
- package/src/pillars/smoke/global-teardown-global.ts +8 -0
- package/src/pillars/smoke/global-teardown.ts +139 -0
- package/src/pillars/smoke/playwright-all.config.ts +147 -0
- package/src/pillars/smoke/playwright-global.config.ts +141 -0
- package/src/pillars/smoke/playwright.config.ts +162 -0
- package/src/pillars/smoke/setup-env.ts +132 -0
- package/src/pillars/smoke/setup-global.ts +126 -0
- package/src/pillars/smoke/setup.ts +115 -0
- package/src/pillars/smoke/smoke-repo.ts +16 -0
- package/src/pillars/smoke/staging.ts +49 -0
- package/src/pillars/suites/run.ts +147 -0
- package/src/scripts/configure-aws-profiles.ts +114 -0
- package/src/scripts/register-repos.ts +307 -0
- package/src/scripts/setup-default-workspace.ts +203 -0
- package/src/shared/applications.ts +138 -0
- package/src/shared/auth/api-token.ts +416 -0
- package/src/shared/auth/auth0-helper.ts +284 -0
- package/src/shared/auth/headless-login.ts +265 -0
- package/src/shared/auth/jwt-helper.ts +186 -0
- package/src/shared/auth/sudo-vault.ts +69 -0
- package/src/shared/auth/test-accounts.ts +98 -0
- package/src/shared/auth/token-injection.ts +184 -0
- package/src/shared/auth/token-providers/auth0.ts +80 -0
- package/src/shared/auth/token-providers/index.ts +182 -0
- package/src/shared/auth/token-refresh.ts +273 -0
- package/src/shared/banner.ts +15 -0
- package/src/shared/brand.ts +79 -0
- package/src/shared/cli-tabs.ts +131 -0
- package/src/shared/constants.ts +128 -0
- package/src/shared/den-config.ts +566 -0
- package/src/shared/dev-metrics/emit.ts +354 -0
- package/src/shared/edge/config.ts +229 -0
- package/src/shared/edge/render.ts +221 -0
- package/src/shared/edge/runtime.ts +268 -0
- package/src/shared/edge/types.ts +62 -0
- package/src/shared/engine.ts +67 -0
- package/src/shared/environment/aws-profiles.ts +49 -0
- package/src/shared/environment/env-selector.ts +161 -0
- package/src/shared/environment/playwright-env.ts +82 -0
- package/src/shared/environment-manager/catalog.ts +77 -0
- package/src/shared/environment-manager/clone-guard.ts +163 -0
- package/src/shared/environment-manager/dag.ts +286 -0
- package/src/shared/environment-manager/default-workspace.ts +182 -0
- package/src/shared/environment-manager/derivation/alias.ts +90 -0
- package/src/shared/environment-manager/derivation/cache.ts +97 -0
- package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
- package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
- package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
- package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
- package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
- package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
- package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
- package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
- package/src/shared/environment-manager/derivation/scan.ts +160 -0
- package/src/shared/environment-manager/derivation/text.ts +40 -0
- package/src/shared/environment-manager/derivation/types.ts +69 -0
- package/src/shared/environment-manager/health.ts +589 -0
- package/src/shared/environment-manager/index.ts +6 -0
- package/src/shared/environment-manager/load-applications.ts +78 -0
- package/src/shared/environment-manager/log-tail.ts +212 -0
- package/src/shared/environment-manager/make-targets.ts +78 -0
- package/src/shared/environment-manager/opctl.ts +334 -0
- package/src/shared/environment-manager/readiness.ts +93 -0
- package/src/shared/environment-manager/register-local.ts +72 -0
- package/src/shared/environment-manager/secrets-check.ts +260 -0
- package/src/shared/environment-manager/service-env.ts +110 -0
- package/src/shared/environment-manager/service-runner.ts +1588 -0
- package/src/shared/environment-manager/up-plan.ts +302 -0
- package/src/shared/environment-manager/workspace-manager.ts +905 -0
- package/src/shared/environment-manager/workspaces.ts +860 -0
- package/src/shared/manifest/deps.ts +27 -0
- package/src/shared/manifest/migrate-manifest.ts +101 -0
- package/src/shared/manifest/read-manifest.ts +160 -0
- package/src/shared/manifest/schema.ts +299 -0
- package/src/shared/manifest/service-links.ts +93 -0
- package/src/shared/manifest/validate-manifest.ts +140 -0
- package/src/shared/manifest/workspace-manifests.ts +92 -0
- package/src/shared/open-url.ts +122 -0
- package/src/shared/org.ts +44 -0
- package/src/shared/paths.ts +178 -0
- package/src/shared/perf/optional-fields.ts +105 -0
- package/src/shared/perf/report-normalizer.ts +404 -0
- package/src/shared/perf/report-paths.ts +19 -0
- package/src/shared/perf/report-store.ts +110 -0
- package/src/shared/perf/report-types.ts +70 -0
- package/src/shared/perf/test-paths.ts +25 -0
- package/src/shared/process/log-rotation.ts +147 -0
- package/src/shared/process/spawn-streaming.ts +229 -0
- package/src/shared/product.ts +92 -0
- package/src/shared/profile.ts +141 -0
- package/src/shared/repo-kind.ts +32 -0
- package/src/shared/reporting/jsonl-reporter.ts +131 -0
- package/src/shared/reporting/slack-reporter.ts +340 -0
- package/src/shared/results/result-reader.ts +266 -0
- package/src/shared/results/result-writer.ts +168 -0
- package/src/shared/secrets/doppler.ts +126 -0
- package/src/shared/secrets/env.ts +48 -0
- package/src/shared/secrets/index.ts +197 -0
- package/src/shared/secrets/keys.ts +33 -0
- package/src/shared/secrets/onepassword.ts +75 -0
- package/src/shared/secrets/types.ts +52 -0
- package/src/shared/service-url.ts +119 -0
- package/src/shared/smoke/branch-resolver.ts +141 -0
- package/src/shared/smoke/git-remote.ts +70 -0
- package/src/shared/smoke/repo-cloner.ts +151 -0
- package/src/shared/stacks/config.ts +137 -0
- package/src/shared/stacks/manifest-commands.ts +88 -0
- package/src/shared/stacks/render.ts +253 -0
- package/src/shared/stacks/runtime.ts +334 -0
- package/src/shared/stacks/types.ts +53 -0
- package/src/shared/terminal/tabs.ts +212 -0
- package/src/shared/test-discovery/module-scanner.ts +569 -0
- package/src/shared/types/index.ts +678 -0
- package/src/shared/ui-marker.ts +19 -0
- package/src/ui/cli/applications-command.ts +116 -0
- package/src/ui/cli/config-command.ts +68 -0
- package/src/ui/cli/create-main.ts +9 -0
- package/src/ui/cli/create.ts +360 -0
- package/src/ui/cli/edge-command.ts +91 -0
- package/src/ui/cli/flag-parser.ts +246 -0
- package/src/ui/cli/graph-command.ts +524 -0
- package/src/ui/cli/health-display.ts +63 -0
- package/src/ui/cli/index.ts +3656 -0
- package/src/ui/cli/init.ts +855 -0
- package/src/ui/cli/invocation-target.ts +175 -0
- package/src/ui/cli/logs-command.ts +104 -0
- package/src/ui/cli/module-selection.ts +31 -0
- package/src/ui/cli/prompts.ts +767 -0
- package/src/ui/cli/results-viewer.ts +436 -0
- package/src/ui/cli/secrets-preflight.ts +91 -0
- package/src/ui/cli/stack-command.ts +144 -0
- package/src/ui/cli/stalled-setup-recovery.ts +129 -0
- package/src/ui/cli/test-runner.ts +1312 -0
- package/src/ui/cli/web-command.ts +1073 -0
- package/src/ui/server/changelog-parser.ts +94 -0
- package/src/ui/server/daemon-reaper.ts +129 -0
- package/src/ui/server/editor-manager.ts +301 -0
- package/src/ui/server/github-service.ts +223 -0
- package/src/ui/server/http-helpers.ts +98 -0
- package/src/ui/server/index.ts +3236 -0
- package/src/ui/server/log-history.ts +452 -0
- package/src/ui/server/perf-report.ts +42 -0
- package/src/ui/server/public/environments.html +494 -0
- package/src/ui/server/public/favicon.ico +0 -0
- package/src/ui/server/public/header.js +1371 -0
- package/src/ui/server/public/index.html +2595 -0
- package/src/ui/server/public/local-dev.html +7405 -0
- package/src/ui/server/public/logs.html +1489 -0
- package/src/ui/server/public/perf-report.js +359 -0
- package/src/ui/server/public/release.html +2111 -0
- package/src/ui/server/public/services.html +1098 -0
- package/src/ui/server/public/workspace-picker.js +69 -0
- package/src/ui/server/release-deploy-session.ts +246 -0
- package/src/ui/server/release-service.ts +1125 -0
- package/src/ui/server/sudo-store.ts +111 -0
- package/src/ui/server/workspace-routes.ts +2154 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Journeys friction extraction — deterministic, harness-side, zero LLM
|
|
3
|
+
* involvement (plan: jrn-reporting packet, Approach §Reporting).
|
|
4
|
+
*
|
|
5
|
+
* Extracts friction_points[] from the persisted transcript (parsed JSONL
|
|
6
|
+
* lines), optional tool telemetry, and the SnapshotIndex. Friction is
|
|
7
|
+
* OBSERVABILITY ONLY: nothing in this module touches the verdict, and the
|
|
8
|
+
* report row's status is computed exclusively from the deterministic
|
|
9
|
+
* verifier's Verdict (pinned by tests in reporting.test.ts).
|
|
10
|
+
*
|
|
11
|
+
* Categories:
|
|
12
|
+
* - tool_error — a tool call finished with status "error";
|
|
13
|
+
* - tool_retry — a same-name call immediately after a failed one;
|
|
14
|
+
* - http_error — a structured tool result carrying HTTP 4xx/5xx
|
|
15
|
+
* (both surfaces: scoped HTTP tool and browser);
|
|
16
|
+
* - error_page — error-page markers in a browser page summary;
|
|
17
|
+
* - dead_end_navigation — navigation back to an already-visited URL;
|
|
18
|
+
* - latency_outlier — a tool call far slower than the per-step budget.
|
|
19
|
+
*
|
|
20
|
+
* Every point carries evidence {transcriptRef, snapshotPath?}: the 1-based
|
|
21
|
+
* transcript line and, for browser actions, the nearest SnapshotIndex frame
|
|
22
|
+
* (falling back to the closest earlier non-suppressed frame).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { JourneyBudgets } from "../spec/schema.js";
|
|
26
|
+
import type { SnapshotIndexEntry } from "../tools/snapshot-store.js";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Markers that identify an error page in a browser tool's text page summary.
|
|
30
|
+
* Deliberately conservative: generic web-app failure copy only, so ordinary
|
|
31
|
+
* content ("error handling docs") does not false-positive.
|
|
32
|
+
*/
|
|
33
|
+
const ERROR_PAGE_MARKERS =
|
|
34
|
+
/something went wrong|an error occurred|page not found|internal server error|service unavailable/i;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A step is a latency outlier when it exceeds this multiple of the per-step
|
|
38
|
+
* budget (timeout_s * 1000 / max_turns). Factor 2 because the per-step
|
|
39
|
+
* budget is an average — individual steps legitimately vary around it, and
|
|
40
|
+
* friction is advisory, so we only flag clear outliers.
|
|
41
|
+
*/
|
|
42
|
+
const LATENCY_OUTLIER_FACTOR = 2;
|
|
43
|
+
|
|
44
|
+
/** Tool-name prefix that assigns an event to the browser surface. */
|
|
45
|
+
const BROWSER_TOOL_PREFIX = "browser_";
|
|
46
|
+
|
|
47
|
+
export type FrictionSurface = "browser" | "api";
|
|
48
|
+
export type FrictionSeverity = "low" | "medium" | "high";
|
|
49
|
+
export type FrictionCategory =
|
|
50
|
+
| "tool_error"
|
|
51
|
+
| "tool_retry"
|
|
52
|
+
| "http_error"
|
|
53
|
+
| "error_page"
|
|
54
|
+
| "dead_end_navigation"
|
|
55
|
+
| "latency_outlier";
|
|
56
|
+
|
|
57
|
+
export interface FrictionEvidence {
|
|
58
|
+
/** Transcript position, e.g. "transcript.jsonl:12" (1-based line). */
|
|
59
|
+
transcriptRef: string;
|
|
60
|
+
/** Nearest snapshot frame, when one exists for the action. */
|
|
61
|
+
snapshotPath?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface FrictionPoint {
|
|
65
|
+
/** 1-based transcript line of the triggering event. */
|
|
66
|
+
step: number;
|
|
67
|
+
surface: FrictionSurface;
|
|
68
|
+
severity: FrictionSeverity;
|
|
69
|
+
category: FrictionCategory;
|
|
70
|
+
evidence: FrictionEvidence;
|
|
71
|
+
description: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Per-call duration telemetry, keyed by the transcript's call_id. */
|
|
75
|
+
export interface ToolCallTelemetry {
|
|
76
|
+
callId: string;
|
|
77
|
+
durationMs: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface FrictionInputs {
|
|
81
|
+
/** Parsed transcript JSONL lines, in persisted order. */
|
|
82
|
+
transcript: readonly unknown[];
|
|
83
|
+
snapshotIndex: readonly SnapshotIndexEntry[];
|
|
84
|
+
/** Spec budgets — the per-step latency baseline derives from these. */
|
|
85
|
+
budgets: JourneyBudgets;
|
|
86
|
+
toolTelemetry?: readonly ToolCallTelemetry[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Structural shape of a terminal tool_call transcript line. */
|
|
90
|
+
interface TerminalToolCall {
|
|
91
|
+
line: number;
|
|
92
|
+
callId?: string;
|
|
93
|
+
name: string;
|
|
94
|
+
status: "completed" | "error";
|
|
95
|
+
result?: unknown;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function asTerminalToolCall(
|
|
99
|
+
message: unknown,
|
|
100
|
+
line: number,
|
|
101
|
+
): TerminalToolCall | undefined {
|
|
102
|
+
if (typeof message !== "object" || message === null) return undefined;
|
|
103
|
+
const candidate = message as {
|
|
104
|
+
type?: unknown;
|
|
105
|
+
call_id?: unknown;
|
|
106
|
+
name?: unknown;
|
|
107
|
+
status?: unknown;
|
|
108
|
+
result?: unknown;
|
|
109
|
+
};
|
|
110
|
+
if (candidate.type !== "tool_call") return undefined;
|
|
111
|
+
if (typeof candidate.name !== "string") return undefined;
|
|
112
|
+
if (candidate.status !== "completed" && candidate.status !== "error") {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
line,
|
|
117
|
+
callId:
|
|
118
|
+
typeof candidate.call_id === "string" ? candidate.call_id : undefined,
|
|
119
|
+
name: candidate.name,
|
|
120
|
+
status: candidate.status,
|
|
121
|
+
result: candidate.result,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function surfaceOf(call: TerminalToolCall): FrictionSurface {
|
|
126
|
+
return call.name.startsWith(BROWSER_TOOL_PREFIX) ? "browser" : "api";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function resultField(call: TerminalToolCall, field: string): unknown {
|
|
130
|
+
if (typeof call.result !== "object" || call.result === null) return undefined;
|
|
131
|
+
return (call.result as Record<string, unknown>)[field];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function httpStatusOf(call: TerminalToolCall): number | undefined {
|
|
135
|
+
const status = resultField(call, "status");
|
|
136
|
+
return typeof status === "number" ? status : undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function pageSummaryOf(call: TerminalToolCall): string | undefined {
|
|
140
|
+
const summary = resultField(call, "summary");
|
|
141
|
+
if (typeof summary === "string") return summary;
|
|
142
|
+
return typeof call.result === "string" ? call.result : undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Did this terminal call fail (tool error or HTTP >= 400 result)? */
|
|
146
|
+
function callFailed(call: TerminalToolCall): boolean {
|
|
147
|
+
if (call.status === "error") return true;
|
|
148
|
+
const status = httpStatusOf(call);
|
|
149
|
+
return status !== undefined && status >= 400;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Nearest snapshot for the browser action at `ordinal` (1-based count of
|
|
154
|
+
* terminal browser_* calls so far): the entry with the matching seq when it
|
|
155
|
+
* has a persisted frame, else the closest earlier non-suppressed frame.
|
|
156
|
+
*/
|
|
157
|
+
function nearestSnapshotPath(
|
|
158
|
+
snapshotIndex: readonly SnapshotIndexEntry[],
|
|
159
|
+
ordinal: number,
|
|
160
|
+
): string | undefined {
|
|
161
|
+
for (let seq = ordinal; seq >= 1; seq -= 1) {
|
|
162
|
+
const entry = snapshotIndex.find((candidate) => candidate.seq === seq);
|
|
163
|
+
if (entry?.path != null) return entry.path;
|
|
164
|
+
}
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Extract deterministic friction points from one run's persisted artifacts.
|
|
170
|
+
* Output order follows transcript order; identical inputs always produce
|
|
171
|
+
* identical output.
|
|
172
|
+
*/
|
|
173
|
+
export function extractFrictionPoints(inputs: FrictionInputs): FrictionPoint[] {
|
|
174
|
+
const points: FrictionPoint[] = [];
|
|
175
|
+
const telemetryByCallId = new Map<string, number>(
|
|
176
|
+
(inputs.toolTelemetry ?? []).map((t) => [t.callId, t.durationMs]),
|
|
177
|
+
);
|
|
178
|
+
// Per-step baseline: the budgets guarantee max_turns > 0 (schema-enforced).
|
|
179
|
+
const stepBudgetMs =
|
|
180
|
+
(inputs.budgets.timeout_s * 1000) / inputs.budgets.max_turns;
|
|
181
|
+
const latencyThresholdMs = stepBudgetMs * LATENCY_OUTLIER_FACTOR;
|
|
182
|
+
|
|
183
|
+
const visitedUrls = new Set<string>();
|
|
184
|
+
let previousCall: TerminalToolCall | undefined;
|
|
185
|
+
let browserOrdinal = 0;
|
|
186
|
+
|
|
187
|
+
const push = (
|
|
188
|
+
call: TerminalToolCall,
|
|
189
|
+
category: FrictionCategory,
|
|
190
|
+
severity: FrictionSeverity,
|
|
191
|
+
description: string,
|
|
192
|
+
): void => {
|
|
193
|
+
const evidence: FrictionEvidence = {
|
|
194
|
+
transcriptRef: `transcript.jsonl:${call.line}`,
|
|
195
|
+
};
|
|
196
|
+
if (surfaceOf(call) === "browser") {
|
|
197
|
+
const snapshotPath = nearestSnapshotPath(
|
|
198
|
+
inputs.snapshotIndex,
|
|
199
|
+
browserOrdinal,
|
|
200
|
+
);
|
|
201
|
+
if (snapshotPath !== undefined) evidence.snapshotPath = snapshotPath;
|
|
202
|
+
}
|
|
203
|
+
points.push({
|
|
204
|
+
step: call.line,
|
|
205
|
+
surface: surfaceOf(call),
|
|
206
|
+
severity,
|
|
207
|
+
category,
|
|
208
|
+
evidence,
|
|
209
|
+
description,
|
|
210
|
+
});
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
inputs.transcript.forEach((message, index) => {
|
|
214
|
+
const call = asTerminalToolCall(message, index + 1);
|
|
215
|
+
if (call === undefined) return;
|
|
216
|
+
|
|
217
|
+
if (surfaceOf(call) === "browser") browserOrdinal += 1;
|
|
218
|
+
|
|
219
|
+
if (call.status === "error") {
|
|
220
|
+
push(
|
|
221
|
+
call,
|
|
222
|
+
"tool_error",
|
|
223
|
+
"high",
|
|
224
|
+
`${call.name} failed: ${JSON.stringify(call.result ?? null)}`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const httpStatus = httpStatusOf(call);
|
|
229
|
+
if (call.status === "completed" && httpStatus !== undefined) {
|
|
230
|
+
if (httpStatus >= 500) {
|
|
231
|
+
push(
|
|
232
|
+
call,
|
|
233
|
+
"http_error",
|
|
234
|
+
"high",
|
|
235
|
+
`${call.name} returned HTTP ${httpStatus}`,
|
|
236
|
+
);
|
|
237
|
+
} else if (httpStatus >= 400) {
|
|
238
|
+
push(
|
|
239
|
+
call,
|
|
240
|
+
"http_error",
|
|
241
|
+
"medium",
|
|
242
|
+
`${call.name} returned HTTP ${httpStatus}`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (call.status === "completed" && surfaceOf(call) === "browser") {
|
|
248
|
+
const summary = pageSummaryOf(call);
|
|
249
|
+
if (summary !== undefined && ERROR_PAGE_MARKERS.test(summary)) {
|
|
250
|
+
push(
|
|
251
|
+
call,
|
|
252
|
+
"error_page",
|
|
253
|
+
"medium",
|
|
254
|
+
`${call.name} landed on an error page: ${summary.slice(0, 200)}`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (
|
|
260
|
+
previousCall !== undefined &&
|
|
261
|
+
previousCall.name === call.name &&
|
|
262
|
+
callFailed(previousCall)
|
|
263
|
+
) {
|
|
264
|
+
push(
|
|
265
|
+
call,
|
|
266
|
+
"tool_retry",
|
|
267
|
+
"low",
|
|
268
|
+
`${call.name} retried after a failed attempt`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (call.name === "browser_navigate" && call.status === "completed") {
|
|
273
|
+
const url = resultField(call, "url");
|
|
274
|
+
if (typeof url === "string") {
|
|
275
|
+
if (visitedUrls.has(url)) {
|
|
276
|
+
push(
|
|
277
|
+
call,
|
|
278
|
+
"dead_end_navigation",
|
|
279
|
+
"low",
|
|
280
|
+
`navigated back to already-visited ${url} (backtracking)`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
visitedUrls.add(url);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (call.callId !== undefined) {
|
|
288
|
+
const durationMs = telemetryByCallId.get(call.callId);
|
|
289
|
+
if (durationMs !== undefined && durationMs > latencyThresholdMs) {
|
|
290
|
+
push(
|
|
291
|
+
call,
|
|
292
|
+
"latency_outlier",
|
|
293
|
+
"medium",
|
|
294
|
+
`${call.name} took ${durationMs}ms (>${Math.round(latencyThresholdMs)}ms, ` +
|
|
295
|
+
`${LATENCY_OUTLIER_FACTOR}x the per-step budget)`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
previousCall = call;
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
return points;
|
|
304
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Journeys JUnit XML emitter (plan: jrn-reporting packet).
|
|
3
|
+
*
|
|
4
|
+
* Journeys are not Playwright-run, so no reporter emits JUnit for them —
|
|
5
|
+
* this is a small hand-rolled writer producing one <testcase> per journey
|
|
6
|
+
* under a single "journeys" <testsuite>. Failure detail (failure kind +
|
|
7
|
+
* assertion diagnostics) goes into <failure>; teardown warnings are
|
|
8
|
+
* NON-GATING and surface in <system-out>.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
12
|
+
import * as path from "path";
|
|
13
|
+
|
|
14
|
+
/** Conventional output path (repo-relative), mirroring the smoke/api junit files under reports/. */
|
|
15
|
+
export const JUNIT_OUTPUT_PATH = "reports/junit-journeys-results.xml";
|
|
16
|
+
|
|
17
|
+
/** One journey's JUnit row, produced by finalizeJourneyReport. */
|
|
18
|
+
export interface JourneysJUnitCase {
|
|
19
|
+
name: string;
|
|
20
|
+
environment: string;
|
|
21
|
+
durationMs: number;
|
|
22
|
+
passed: boolean;
|
|
23
|
+
/** RunOutcome failure kind, when the run itself failed. */
|
|
24
|
+
failureKind?: string;
|
|
25
|
+
/** Structured assertion diagnostics for the <failure> body. */
|
|
26
|
+
failureDetails: string[];
|
|
27
|
+
/** Teardown failures + unmanaged-mutation warnings (non-gating). */
|
|
28
|
+
teardownWarnings: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function escapeXml(value: string): string {
|
|
32
|
+
return value
|
|
33
|
+
.replace(/&/g, "&")
|
|
34
|
+
.replace(/</g, "<")
|
|
35
|
+
.replace(/>/g, ">")
|
|
36
|
+
.replace(/"/g, """)
|
|
37
|
+
.replace(/'/g, "'");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function renderTestCase(testCase: JourneysJUnitCase): string {
|
|
41
|
+
const timeS = testCase.durationMs / 1000;
|
|
42
|
+
const lines: string[] = [
|
|
43
|
+
` <testcase name="${escapeXml(testCase.name)}" ` +
|
|
44
|
+
`classname="journeys.${escapeXml(testCase.environment)}" ` +
|
|
45
|
+
`time="${timeS}">`,
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
if (!testCase.passed) {
|
|
49
|
+
const message =
|
|
50
|
+
testCase.failureKind !== undefined
|
|
51
|
+
? `failure_kind=${testCase.failureKind}`
|
|
52
|
+
: "journey assertions failed";
|
|
53
|
+
const body = [message, ...testCase.failureDetails]
|
|
54
|
+
.map(escapeXml)
|
|
55
|
+
.join("\n");
|
|
56
|
+
lines.push(
|
|
57
|
+
` <failure message="${escapeXml(message)}">${body}</failure>`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (testCase.teardownWarnings.length > 0) {
|
|
62
|
+
lines.push(
|
|
63
|
+
` <system-out>${testCase.teardownWarnings
|
|
64
|
+
.map(escapeXml)
|
|
65
|
+
.join("\n")}</system-out>`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
lines.push(" </testcase>");
|
|
70
|
+
return lines.join("\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Render the full JUnit document for one run's journeys. */
|
|
74
|
+
export function renderJourneysJUnitXml(
|
|
75
|
+
cases: readonly JourneysJUnitCase[],
|
|
76
|
+
): string {
|
|
77
|
+
const failures = cases.filter((testCase) => !testCase.passed).length;
|
|
78
|
+
const totalTimeS =
|
|
79
|
+
cases.reduce((sum, testCase) => sum + testCase.durationMs, 0) / 1000;
|
|
80
|
+
|
|
81
|
+
return [
|
|
82
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
83
|
+
`<testsuites tests="${cases.length}" failures="${failures}" time="${totalTimeS}">`,
|
|
84
|
+
` <testsuite name="journeys" tests="${cases.length}" ` +
|
|
85
|
+
`failures="${failures}" time="${totalTimeS}">`,
|
|
86
|
+
...cases.map(renderTestCase),
|
|
87
|
+
" </testsuite>",
|
|
88
|
+
"</testsuites>",
|
|
89
|
+
"",
|
|
90
|
+
].join("\n");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Write the JUnit document to disk (default: reports/junit-journeys-
|
|
95
|
+
* results.xml). Returns the resolved file path.
|
|
96
|
+
*/
|
|
97
|
+
export async function writeJourneysJUnitXml(
|
|
98
|
+
cases: readonly JourneysJUnitCase[],
|
|
99
|
+
filePath: string = JUNIT_OUTPUT_PATH,
|
|
100
|
+
): Promise<string> {
|
|
101
|
+
const resolved = path.resolve(filePath);
|
|
102
|
+
await mkdir(path.dirname(resolved), { recursive: true });
|
|
103
|
+
await writeFile(resolved, renderJourneysJUnitXml(cases));
|
|
104
|
+
return resolved;
|
|
105
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Journeys per-run markdown report (plan: jrn-reporting packet, Approach
|
|
3
|
+
* §Browser snapshots in every report).
|
|
4
|
+
*
|
|
5
|
+
* Renders the human-facing run report: verdict summary, budget/usage
|
|
6
|
+
* numbers, the embedded snapshot gallery (image links RELATIVE to the
|
|
7
|
+
* report dir so the report stays portable), the friction section with
|
|
8
|
+
* evidence references, assertion diagnostics, and teardown warnings.
|
|
9
|
+
* Friction and teardown sections are informational — the verdict line
|
|
10
|
+
* reflects the deterministic verifier only.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
import type { JourneyEvent } from "../observability/events.js";
|
|
15
|
+
import type { TeardownReport } from "../teardown.js";
|
|
16
|
+
import type { SnapshotIndexEntry } from "../tools/snapshot-store.js";
|
|
17
|
+
import type { CollectedJourneyReport } from "./results.js";
|
|
18
|
+
|
|
19
|
+
function snapshotAlt(entry: SnapshotIndexEntry): string {
|
|
20
|
+
return `${String(entry.seq).padStart(3, "0")}-${entry.action}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function renderGallery(collected: CollectedJourneyReport): string[] {
|
|
24
|
+
if (collected.snapshotIndex.length === 0) {
|
|
25
|
+
return ["_No snapshots captured (api-surface journey)._", ""];
|
|
26
|
+
}
|
|
27
|
+
const lines: string[] = [];
|
|
28
|
+
for (const entry of collected.snapshotIndex) {
|
|
29
|
+
if (entry.captureError !== undefined) {
|
|
30
|
+
lines.push(
|
|
31
|
+
`⚠️ _Snapshot ${String(entry.seq).padStart(3, "0")} ` +
|
|
32
|
+
`(${entry.action}) capture FAILED — evidence missing: ` +
|
|
33
|
+
`${entry.captureError}_`,
|
|
34
|
+
);
|
|
35
|
+
} else if (entry.path === null) {
|
|
36
|
+
lines.push(
|
|
37
|
+
`_Snapshot ${String(entry.seq).padStart(3, "0")} (${entry.action}) ` +
|
|
38
|
+
`suppressed${entry.masked ? " — sensitive input masked" : ""}._`,
|
|
39
|
+
);
|
|
40
|
+
} else {
|
|
41
|
+
const relative = path.relative(collected.reportDir, entry.path);
|
|
42
|
+
lines.push(``);
|
|
43
|
+
}
|
|
44
|
+
lines.push("");
|
|
45
|
+
}
|
|
46
|
+
return lines;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function renderFriction(collected: CollectedJourneyReport): string[] {
|
|
50
|
+
if (collected.frictionPoints.length === 0) {
|
|
51
|
+
return ["_No friction points detected._", ""];
|
|
52
|
+
}
|
|
53
|
+
const lines: string[] = [];
|
|
54
|
+
for (const point of collected.frictionPoints) {
|
|
55
|
+
const snapshotSuffix =
|
|
56
|
+
point.evidence.snapshotPath !== undefined
|
|
57
|
+
? ` (snapshot: ${path.relative(collected.reportDir, point.evidence.snapshotPath)})`
|
|
58
|
+
: "";
|
|
59
|
+
lines.push(
|
|
60
|
+
`- **${point.severity}** \`${point.category}\` @ ` +
|
|
61
|
+
`${point.evidence.transcriptRef} — ${point.description}${snapshotSuffix}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
lines.push("");
|
|
65
|
+
return lines;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function renderAssertions(collected: CollectedJourneyReport): string[] {
|
|
69
|
+
if (collected.assertionResults.length === 0) {
|
|
70
|
+
return ["_No assertion diagnostics recorded._", ""];
|
|
71
|
+
}
|
|
72
|
+
const lines: string[] = [];
|
|
73
|
+
for (const result of collected.assertionResults) {
|
|
74
|
+
const marker = result.passed ? "✅" : "❌";
|
|
75
|
+
const detail = result.passed
|
|
76
|
+
? ""
|
|
77
|
+
: ` — ${result.message ?? `expected ${JSON.stringify(result.expected)}, actual ${JSON.stringify(result.actual)}`}`;
|
|
78
|
+
lines.push(`- ${marker} \`${result.assertion.kind}\`${detail}`);
|
|
79
|
+
}
|
|
80
|
+
lines.push("");
|
|
81
|
+
return lines;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function renderTeardown(teardown: TeardownReport | undefined): string[] {
|
|
85
|
+
if (teardown === undefined) {
|
|
86
|
+
return ["_No teardown report (teardown did not run)._", ""];
|
|
87
|
+
}
|
|
88
|
+
const lines: string[] = [
|
|
89
|
+
`Attempted deletions: ${teardown.results.length}, ` +
|
|
90
|
+
`failed: ${teardown.failures.length}.`,
|
|
91
|
+
"",
|
|
92
|
+
];
|
|
93
|
+
for (const failure of teardown.failures) {
|
|
94
|
+
lines.push(
|
|
95
|
+
`- ⚠️ delete failed for ${failure.entry.resourceType} ` +
|
|
96
|
+
`${failure.entry.id}: ${failure.entry.cleanup.method} ` +
|
|
97
|
+
`${failure.entry.cleanup.path}` +
|
|
98
|
+
(failure.status !== undefined ? ` (${failure.status})` : "") +
|
|
99
|
+
(failure.error !== undefined ? ` — ${failure.error}` : ""),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
for (const warning of teardown.unmanagedWarnings) {
|
|
103
|
+
lines.push(
|
|
104
|
+
`- ⚠️ unmanaged mutation (never guessed-deleted): ` +
|
|
105
|
+
`${warning.method} ${warning.path} (${warning.status}, ` +
|
|
106
|
+
`source: ${warning.source})`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
for (const message of teardown.interceptionErrors) {
|
|
110
|
+
lines.push(
|
|
111
|
+
`- ⚠️ browser interception error (a creation may be untracked): ` +
|
|
112
|
+
message,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
if (
|
|
116
|
+
teardown.failures.length > 0 ||
|
|
117
|
+
teardown.unmanagedWarnings.length > 0 ||
|
|
118
|
+
teardown.interceptionErrors.length > 0
|
|
119
|
+
) {
|
|
120
|
+
lines.push("");
|
|
121
|
+
}
|
|
122
|
+
return lines;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function ts(event: JourneyEvent): string {
|
|
126
|
+
return `${(event.tMs / 1000).toFixed(1)}s`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Condensed chronological timeline of what the agent actually did —
|
|
131
|
+
* tool calls (with results), bindings, denials, thinking/assistant text.
|
|
132
|
+
* This is the section that turns a black-box run into a readable story.
|
|
133
|
+
*/
|
|
134
|
+
function renderTimeline(collected: CollectedJourneyReport): string[] {
|
|
135
|
+
if (collected.events.length === 0) {
|
|
136
|
+
return ["_No events recorded._", ""];
|
|
137
|
+
}
|
|
138
|
+
const lines: string[] = [];
|
|
139
|
+
for (const event of collected.events) {
|
|
140
|
+
switch (event.kind) {
|
|
141
|
+
case "turn_started":
|
|
142
|
+
lines.push(`- \`${ts(event)}\` **turn ${event.turn}**`);
|
|
143
|
+
break;
|
|
144
|
+
case "tool_call": {
|
|
145
|
+
const icon =
|
|
146
|
+
event.status === "completed"
|
|
147
|
+
? "✓"
|
|
148
|
+
: event.status === "error"
|
|
149
|
+
? "✗"
|
|
150
|
+
: "…";
|
|
151
|
+
const summary = event.summary ? ` \`${event.summary}\`` : "";
|
|
152
|
+
const result = event.result ? ` → ${event.result}` : "";
|
|
153
|
+
lines.push(
|
|
154
|
+
`- \`${ts(event)}\` ${icon} ${event.tool}${summary}${result}`,
|
|
155
|
+
);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
case "ledger_binding":
|
|
159
|
+
lines.push(
|
|
160
|
+
`- \`${ts(event)}\` 🔗 bound **${event.resourceType}** = ` +
|
|
161
|
+
`\`${event.id}\` (${event.method} ${event.pathValue} → ${event.status})`,
|
|
162
|
+
);
|
|
163
|
+
break;
|
|
164
|
+
case "unmanaged_mutation":
|
|
165
|
+
lines.push(
|
|
166
|
+
`- \`${ts(event)}\` ⚠️ unmanaged ${event.method} ` +
|
|
167
|
+
`${event.pathValue} → ${event.status}`,
|
|
168
|
+
);
|
|
169
|
+
break;
|
|
170
|
+
case "interception_error":
|
|
171
|
+
lines.push(
|
|
172
|
+
`- \`${ts(event)}\` ⚠️ interception error: ${event.message}`,
|
|
173
|
+
);
|
|
174
|
+
break;
|
|
175
|
+
case "agent_error":
|
|
176
|
+
lines.push(
|
|
177
|
+
`- \`${ts(event)}\` ✗ **agent error**` +
|
|
178
|
+
(event.code ? ` [${event.code}]` : "") +
|
|
179
|
+
`: ${event.message}`,
|
|
180
|
+
);
|
|
181
|
+
break;
|
|
182
|
+
case "assistant_text":
|
|
183
|
+
lines.push(`- \`${ts(event)}\` 🗣 ${truncate(event.text, 160)}`);
|
|
184
|
+
break;
|
|
185
|
+
default:
|
|
186
|
+
// thinking / turn_ended / run_started / run_ended are omitted from
|
|
187
|
+
// the human timeline (they live in events.jsonl); keeps it readable.
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
lines.push("");
|
|
192
|
+
return lines;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Resource bindings — the direct answer to "did {<type>_id} bind, and to
|
|
197
|
+
* what?". Empty for a journey that created nothing (or failed before it).
|
|
198
|
+
*/
|
|
199
|
+
function renderBindings(collected: CollectedJourneyReport): string[] {
|
|
200
|
+
const bindings = collected.events.filter(
|
|
201
|
+
(e): e is Extract<JourneyEvent, { kind: "ledger_binding" }> =>
|
|
202
|
+
e.kind === "ledger_binding",
|
|
203
|
+
);
|
|
204
|
+
if (bindings.length === 0) {
|
|
205
|
+
return [
|
|
206
|
+
"_No resources bound (nothing created, or created pre-binding)._",
|
|
207
|
+
"",
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
const lines = bindings.map(
|
|
211
|
+
(b) =>
|
|
212
|
+
`- **${b.resourceType}** = \`${b.id}\` ` +
|
|
213
|
+
`(${b.method} ${b.pathValue} → ${b.status}, via ${b.source})`,
|
|
214
|
+
);
|
|
215
|
+
lines.push("");
|
|
216
|
+
return lines;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function truncate(text: string, max: number): string {
|
|
220
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
221
|
+
return flat.length <= max ? flat : `${flat.slice(0, max - 1)}…`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Render the full per-run markdown report. */
|
|
225
|
+
export function renderJourneyReportMarkdown(
|
|
226
|
+
collected: CollectedJourneyReport,
|
|
227
|
+
teardown?: TeardownReport,
|
|
228
|
+
): string {
|
|
229
|
+
const verdictLine = collected.status === "passed" ? "✅ passed" : "❌ failed";
|
|
230
|
+
|
|
231
|
+
return [
|
|
232
|
+
`# Journey report — ${collected.journeyName}`,
|
|
233
|
+
"",
|
|
234
|
+
`- **Run id**: ${collected.runId}`,
|
|
235
|
+
`- **Environment**: ${collected.environment}`,
|
|
236
|
+
`- **Surface**: ${collected.surface}`,
|
|
237
|
+
`- **Verdict**: ${verdictLine}` +
|
|
238
|
+
(collected.failureKind !== undefined
|
|
239
|
+
? ` (failure_kind: ${collected.failureKind})`
|
|
240
|
+
: ""),
|
|
241
|
+
`- **Duration**: ${collected.durationMs}ms`,
|
|
242
|
+
`- **Tokens used**: ${collected.usageTotalTokens ?? "unreported"} / ` +
|
|
243
|
+
`${collected.budgetConsumption.maxTotalTokens} budgeted`,
|
|
244
|
+
`- **Budgets**: max_turns=${collected.budgetConsumption.maxTurns}, ` +
|
|
245
|
+
`timeout_s=${collected.budgetConsumption.timeoutS}`,
|
|
246
|
+
`- **Transcript**: ${path.basename(collected.transcriptPath)}`,
|
|
247
|
+
`- **Events**: ${collected.eventsFile}`,
|
|
248
|
+
`- **Snapshots**: ${collected.snapshotCount} in ` +
|
|
249
|
+
`${path.relative(collected.reportDir, collected.snapshotsDir) || "."}/`,
|
|
250
|
+
...(collected.agentError !== undefined
|
|
251
|
+
? [`- **Agent error**: ${collected.agentError}`]
|
|
252
|
+
: []),
|
|
253
|
+
"",
|
|
254
|
+
"## Timeline",
|
|
255
|
+
"",
|
|
256
|
+
...renderTimeline(collected),
|
|
257
|
+
"## Bindings",
|
|
258
|
+
"",
|
|
259
|
+
...renderBindings(collected),
|
|
260
|
+
"## Assertions",
|
|
261
|
+
"",
|
|
262
|
+
...renderAssertions(collected),
|
|
263
|
+
"## Snapshots",
|
|
264
|
+
"",
|
|
265
|
+
...renderGallery(collected),
|
|
266
|
+
"## Friction",
|
|
267
|
+
"",
|
|
268
|
+
...renderFriction(collected),
|
|
269
|
+
"## Teardown",
|
|
270
|
+
"",
|
|
271
|
+
...renderTeardown(teardown),
|
|
272
|
+
].join("\n");
|
|
273
|
+
}
|