@monster-orange/den 0.0.0 → 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.
Files changed (231) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/LICENSE +21 -0
  3. package/README.md +165 -1
  4. package/bin/den +61 -0
  5. package/docs/distributions.md +256 -0
  6. package/make/demo.mk +82 -0
  7. package/make/den.mk +80 -0
  8. package/make/journeys.mk +57 -0
  9. package/make/local_dev.mk +154 -0
  10. package/make/security.mk +59 -0
  11. package/make/test.mk +405 -0
  12. package/package.json +99 -4
  13. package/scripts/auth-configure.ts +415 -0
  14. package/scripts/heal-config.ts +92 -0
  15. package/scripts/jsonl-playwright-reporter.js +70 -0
  16. package/scripts/manifest-read.cjs +91 -0
  17. package/scripts/setup-1password.ts +570 -0
  18. package/scripts/setup-secrets.ts +149 -0
  19. package/scripts/view-results.js +156 -0
  20. package/service/install.sh +112 -0
  21. package/src/pillars/api/fixtures/index.ts +128 -0
  22. package/src/pillars/api/fixtures/openapi-validator.ts +111 -0
  23. package/src/pillars/api/global-setup.ts +103 -0
  24. package/src/pillars/api/playwright.config.ts +105 -0
  25. package/src/pillars/api/schemas/example-api.json +110 -0
  26. package/src/pillars/api/tests/health/health.spec.ts +19 -0
  27. package/src/pillars/api/tests/health/manifest.yml +6 -0
  28. package/src/pillars/browser/fixtures/index.ts +232 -0
  29. package/src/pillars/browser/fixtures/package.json +10 -0
  30. package/src/pillars/browser/global-setup.ts +140 -0
  31. package/src/pillars/browser/global-teardown.ts +48 -0
  32. package/src/pillars/browser/playwright.config.ts +212 -0
  33. package/src/pillars/browser/setup.ts +219 -0
  34. package/src/pillars/journeys/app-config.ts +237 -0
  35. package/src/pillars/journeys/discover.ts +160 -0
  36. package/src/pillars/journeys/hooks/deny-shell.cjs +44 -0
  37. package/src/pillars/journeys/observability/events.ts +288 -0
  38. package/src/pillars/journeys/observability/sdk-mapper.ts +306 -0
  39. package/src/pillars/journeys/report/friction.ts +304 -0
  40. package/src/pillars/journeys/report/junit.ts +105 -0
  41. package/src/pillars/journeys/report/render.ts +273 -0
  42. package/src/pillars/journeys/report/results.ts +323 -0
  43. package/src/pillars/journeys/run.ts +673 -0
  44. package/src/pillars/journeys/runner.ts +1025 -0
  45. package/src/pillars/journeys/sandbox.ts +406 -0
  46. package/src/pillars/journeys/spec/loader.ts +152 -0
  47. package/src/pillars/journeys/spec/schema.ts +208 -0
  48. package/src/pillars/journeys/teardown.ts +98 -0
  49. package/src/pillars/journeys/tools/browser-session.ts +219 -0
  50. package/src/pillars/journeys/tools/browser-tool.ts +914 -0
  51. package/src/pillars/journeys/tools/http-tool.ts +564 -0
  52. package/src/pillars/journeys/tools/ledger.ts +278 -0
  53. package/src/pillars/journeys/tools/snapshot-store.ts +117 -0
  54. package/src/pillars/journeys/verify/assertions.ts +502 -0
  55. package/src/pillars/journeys/verify/verdict.ts +71 -0
  56. package/src/pillars/perf/build-report.ts +36 -0
  57. package/src/pillars/perf/k6.config.js +112 -0
  58. package/src/pillars/perf/run.ts +345 -0
  59. package/src/pillars/perf/scenarios/load-profile.js +97 -0
  60. package/src/pillars/perf/staging.ts +73 -0
  61. package/src/pillars/security/discover.ts +108 -0
  62. package/src/pillars/security/findings.ts +151 -0
  63. package/src/pillars/security/plan.ts +355 -0
  64. package/src/pillars/security/run.ts +729 -0
  65. package/src/pillars/smoke/admin-probe.ts +68 -0
  66. package/src/pillars/smoke/base-url.ts +56 -0
  67. package/src/pillars/smoke/cleanup.ts +8 -0
  68. package/src/pillars/smoke/discover.ts +237 -0
  69. package/src/pillars/smoke/ensure-smoke-teardown.ts +40 -0
  70. package/src/pillars/smoke/fixture-manager.ts +217 -0
  71. package/src/pillars/smoke/fixtures.ts +468 -0
  72. package/src/pillars/smoke/global-setup-all.ts +262 -0
  73. package/src/pillars/smoke/global-setup-global.ts +262 -0
  74. package/src/pillars/smoke/global-setup.ts +273 -0
  75. package/src/pillars/smoke/global-teardown-all.ts +8 -0
  76. package/src/pillars/smoke/global-teardown-global.ts +8 -0
  77. package/src/pillars/smoke/global-teardown.ts +139 -0
  78. package/src/pillars/smoke/playwright-all.config.ts +147 -0
  79. package/src/pillars/smoke/playwright-global.config.ts +141 -0
  80. package/src/pillars/smoke/playwright.config.ts +162 -0
  81. package/src/pillars/smoke/setup-env.ts +132 -0
  82. package/src/pillars/smoke/setup-global.ts +126 -0
  83. package/src/pillars/smoke/setup.ts +115 -0
  84. package/src/pillars/smoke/smoke-repo.ts +16 -0
  85. package/src/pillars/smoke/staging.ts +49 -0
  86. package/src/pillars/suites/run.ts +147 -0
  87. package/src/scripts/configure-aws-profiles.ts +114 -0
  88. package/src/scripts/register-repos.ts +307 -0
  89. package/src/scripts/setup-default-workspace.ts +203 -0
  90. package/src/shared/applications.ts +138 -0
  91. package/src/shared/auth/api-token.ts +416 -0
  92. package/src/shared/auth/auth0-helper.ts +284 -0
  93. package/src/shared/auth/headless-login.ts +265 -0
  94. package/src/shared/auth/jwt-helper.ts +186 -0
  95. package/src/shared/auth/sudo-vault.ts +69 -0
  96. package/src/shared/auth/test-accounts.ts +98 -0
  97. package/src/shared/auth/token-injection.ts +184 -0
  98. package/src/shared/auth/token-providers/auth0.ts +80 -0
  99. package/src/shared/auth/token-providers/index.ts +182 -0
  100. package/src/shared/auth/token-refresh.ts +273 -0
  101. package/src/shared/banner.ts +15 -0
  102. package/src/shared/brand.ts +79 -0
  103. package/src/shared/cli-tabs.ts +131 -0
  104. package/src/shared/constants.ts +128 -0
  105. package/src/shared/den-config.ts +566 -0
  106. package/src/shared/dev-metrics/emit.ts +354 -0
  107. package/src/shared/edge/config.ts +229 -0
  108. package/src/shared/edge/render.ts +221 -0
  109. package/src/shared/edge/runtime.ts +268 -0
  110. package/src/shared/edge/types.ts +62 -0
  111. package/src/shared/engine.ts +67 -0
  112. package/src/shared/environment/aws-profiles.ts +49 -0
  113. package/src/shared/environment/env-selector.ts +161 -0
  114. package/src/shared/environment/playwright-env.ts +82 -0
  115. package/src/shared/environment-manager/catalog.ts +77 -0
  116. package/src/shared/environment-manager/clone-guard.ts +163 -0
  117. package/src/shared/environment-manager/dag.ts +286 -0
  118. package/src/shared/environment-manager/default-workspace.ts +182 -0
  119. package/src/shared/environment-manager/derivation/alias.ts +90 -0
  120. package/src/shared/environment-manager/derivation/cache.ts +97 -0
  121. package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
  122. package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
  123. package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
  124. package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
  125. package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
  126. package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
  127. package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
  128. package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
  129. package/src/shared/environment-manager/derivation/scan.ts +160 -0
  130. package/src/shared/environment-manager/derivation/text.ts +40 -0
  131. package/src/shared/environment-manager/derivation/types.ts +69 -0
  132. package/src/shared/environment-manager/health.ts +589 -0
  133. package/src/shared/environment-manager/index.ts +6 -0
  134. package/src/shared/environment-manager/load-applications.ts +78 -0
  135. package/src/shared/environment-manager/log-tail.ts +212 -0
  136. package/src/shared/environment-manager/make-targets.ts +78 -0
  137. package/src/shared/environment-manager/opctl.ts +334 -0
  138. package/src/shared/environment-manager/readiness.ts +93 -0
  139. package/src/shared/environment-manager/register-local.ts +72 -0
  140. package/src/shared/environment-manager/secrets-check.ts +260 -0
  141. package/src/shared/environment-manager/service-env.ts +110 -0
  142. package/src/shared/environment-manager/service-runner.ts +1588 -0
  143. package/src/shared/environment-manager/up-plan.ts +302 -0
  144. package/src/shared/environment-manager/workspace-manager.ts +905 -0
  145. package/src/shared/environment-manager/workspaces.ts +860 -0
  146. package/src/shared/manifest/deps.ts +27 -0
  147. package/src/shared/manifest/migrate-manifest.ts +101 -0
  148. package/src/shared/manifest/read-manifest.ts +160 -0
  149. package/src/shared/manifest/schema.ts +299 -0
  150. package/src/shared/manifest/service-links.ts +93 -0
  151. package/src/shared/manifest/validate-manifest.ts +140 -0
  152. package/src/shared/manifest/workspace-manifests.ts +92 -0
  153. package/src/shared/open-url.ts +122 -0
  154. package/src/shared/org.ts +44 -0
  155. package/src/shared/paths.ts +178 -0
  156. package/src/shared/perf/optional-fields.ts +105 -0
  157. package/src/shared/perf/report-normalizer.ts +404 -0
  158. package/src/shared/perf/report-paths.ts +19 -0
  159. package/src/shared/perf/report-store.ts +110 -0
  160. package/src/shared/perf/report-types.ts +70 -0
  161. package/src/shared/perf/test-paths.ts +25 -0
  162. package/src/shared/process/log-rotation.ts +147 -0
  163. package/src/shared/process/spawn-streaming.ts +229 -0
  164. package/src/shared/product.ts +92 -0
  165. package/src/shared/profile.ts +141 -0
  166. package/src/shared/repo-kind.ts +32 -0
  167. package/src/shared/reporting/jsonl-reporter.ts +131 -0
  168. package/src/shared/reporting/slack-reporter.ts +340 -0
  169. package/src/shared/results/result-reader.ts +266 -0
  170. package/src/shared/results/result-writer.ts +168 -0
  171. package/src/shared/secrets/doppler.ts +126 -0
  172. package/src/shared/secrets/env.ts +48 -0
  173. package/src/shared/secrets/index.ts +197 -0
  174. package/src/shared/secrets/keys.ts +33 -0
  175. package/src/shared/secrets/onepassword.ts +75 -0
  176. package/src/shared/secrets/types.ts +52 -0
  177. package/src/shared/service-url.ts +119 -0
  178. package/src/shared/smoke/branch-resolver.ts +141 -0
  179. package/src/shared/smoke/git-remote.ts +70 -0
  180. package/src/shared/smoke/repo-cloner.ts +151 -0
  181. package/src/shared/stacks/config.ts +137 -0
  182. package/src/shared/stacks/manifest-commands.ts +88 -0
  183. package/src/shared/stacks/render.ts +253 -0
  184. package/src/shared/stacks/runtime.ts +334 -0
  185. package/src/shared/stacks/types.ts +53 -0
  186. package/src/shared/terminal/tabs.ts +212 -0
  187. package/src/shared/test-discovery/module-scanner.ts +569 -0
  188. package/src/shared/types/index.ts +678 -0
  189. package/src/shared/ui-marker.ts +19 -0
  190. package/src/ui/cli/applications-command.ts +116 -0
  191. package/src/ui/cli/config-command.ts +68 -0
  192. package/src/ui/cli/create-main.ts +9 -0
  193. package/src/ui/cli/create.ts +360 -0
  194. package/src/ui/cli/edge-command.ts +91 -0
  195. package/src/ui/cli/flag-parser.ts +246 -0
  196. package/src/ui/cli/graph-command.ts +524 -0
  197. package/src/ui/cli/health-display.ts +63 -0
  198. package/src/ui/cli/index.ts +3656 -0
  199. package/src/ui/cli/init.ts +855 -0
  200. package/src/ui/cli/invocation-target.ts +175 -0
  201. package/src/ui/cli/logs-command.ts +104 -0
  202. package/src/ui/cli/module-selection.ts +31 -0
  203. package/src/ui/cli/prompts.ts +767 -0
  204. package/src/ui/cli/results-viewer.ts +436 -0
  205. package/src/ui/cli/secrets-preflight.ts +91 -0
  206. package/src/ui/cli/stack-command.ts +144 -0
  207. package/src/ui/cli/stalled-setup-recovery.ts +129 -0
  208. package/src/ui/cli/test-runner.ts +1312 -0
  209. package/src/ui/cli/web-command.ts +1073 -0
  210. package/src/ui/server/changelog-parser.ts +94 -0
  211. package/src/ui/server/daemon-reaper.ts +129 -0
  212. package/src/ui/server/editor-manager.ts +301 -0
  213. package/src/ui/server/github-service.ts +223 -0
  214. package/src/ui/server/http-helpers.ts +98 -0
  215. package/src/ui/server/index.ts +3236 -0
  216. package/src/ui/server/log-history.ts +452 -0
  217. package/src/ui/server/perf-report.ts +42 -0
  218. package/src/ui/server/public/environments.html +494 -0
  219. package/src/ui/server/public/favicon.ico +0 -0
  220. package/src/ui/server/public/header.js +1371 -0
  221. package/src/ui/server/public/index.html +2595 -0
  222. package/src/ui/server/public/local-dev.html +7405 -0
  223. package/src/ui/server/public/logs.html +1489 -0
  224. package/src/ui/server/public/perf-report.js +359 -0
  225. package/src/ui/server/public/release.html +2111 -0
  226. package/src/ui/server/public/services.html +1098 -0
  227. package/src/ui/server/public/workspace-picker.js +69 -0
  228. package/src/ui/server/release-deploy-session.ts +246 -0
  229. package/src/ui/server/release-service.ts +1125 -0
  230. package/src/ui/server/sudo-store.ts +111 -0
  231. package/src/ui/server/workspace-routes.ts +2154 -0
@@ -0,0 +1,502 @@
1
+ /**
2
+ * Journeys deterministic assertion engine (plan: jrn-verifier packet,
3
+ * Approach §Assertion contract).
4
+ *
5
+ * Plain code, zero LLM involvement: every JourneyAssertion is evaluated by
6
+ * executing its probe request ({method, path, body?}) through an
7
+ * allowlisted + JWT-authenticated probe client (mirroring the scoped HTTP
8
+ * tool's contract — the credential is injected harness-side and never
9
+ * appears in errors or results), then checking the response
10
+ * deterministically.
11
+ *
12
+ * `{placeholders}` in probe paths bind to ledger-extracted resource ids
13
+ * from EITHER feed (http-tool or browser network interception). Binding
14
+ * convention: `{<resourceType>_id}` binds to the MOST RECENT ledger entry
15
+ * of that resourceType. A missing binding fails the assertion with a clear
16
+ * message — it never crashes the verifier and never issues the probe.
17
+ *
18
+ * Assertions are API probes even for browser-surface journeys: the UI
19
+ * writes app state; the verifier reads it back through the API, which
20
+ * keeps the verdict independent of UI markup.
21
+ */
22
+
23
+ import type { JourneyAssertion, JourneyProbeRequest } from "../spec/schema.js";
24
+ import { resolveExtraHeaders } from "../tools/http-tool.js";
25
+ import type { RunLedger } from "../tools/ledger.js";
26
+
27
+ // ── Probe client (http-tool contract mirror, harness-side) ──────────────────
28
+
29
+ // Per-request transport cap applied to every probe fetch even when no
30
+ // deadline signal is provided (review finding 6) — verification and
31
+ // teardown requests can never hang a journey run indefinitely.
32
+ export const DEFAULT_PROBE_TIMEOUT_MS = 30_000;
33
+
34
+ /** Response the probe client hands to the evaluator. */
35
+ export interface ProbeResponse {
36
+ status: number;
37
+ /** Parsed JSON body when the response is JSON; raw text otherwise. */
38
+ body: unknown;
39
+ }
40
+
41
+ /** Executes one bound probe request. Thrown errors never carry the token. */
42
+ export type VerifierProbeClient = (
43
+ request: JourneyProbeRequest,
44
+ ) => Promise<ProbeResponse>;
45
+
46
+ export interface VerifierProbeClientOptions {
47
+ /**
48
+ * Base URLs the probes may reach; relative paths resolve against the
49
+ * first entry (the API base URL by convention) — same contract as the
50
+ * scoped HTTP tool.
51
+ */
52
+ allowedBaseUrls: string[];
53
+ /** Harness-held JWT source; the value never leaves the client. */
54
+ tokenProvider: () => Promise<string>;
55
+ /**
56
+ * Optional extra-header source (the app's session-derived scoping
57
+ * headers), matching the scoped HTTP tool. Derived from the same browser
58
+ * session as the token.
59
+ */
60
+ headersProvider?: () => Promise<Record<string, string>>;
61
+ /** Injectable fetch (unit tests stub it; defaults to global fetch). */
62
+ fetchImpl?: typeof fetch;
63
+ /**
64
+ * Optional deadline AbortSignal; combined with the per-request timeout
65
+ * so in-flight probes abort when the caller's budget expires.
66
+ */
67
+ signal?: AbortSignal;
68
+ /** Per-request transport cap; defaults to DEFAULT_PROBE_TIMEOUT_MS. */
69
+ requestTimeoutMs?: number;
70
+ }
71
+
72
+ /**
73
+ * Build the verifier's probe client: allowlist resolution + harness-side
74
+ * `Authorization: Bearer <jwt>` (plus any session-derived headers)
75
+ * injection, mirroring the scoped HTTP tool. Unlike the tool (agent-facing,
76
+ * never throws), this client is
77
+ * harness-facing and throws typed errors — the assertion engine converts
78
+ * them into structured failures; the token is redacted from any
79
+ * transport-error message before it propagates.
80
+ */
81
+ export function buildVerifierProbeClient(
82
+ options: VerifierProbeClientOptions,
83
+ ): VerifierProbeClient {
84
+ const {
85
+ allowedBaseUrls,
86
+ tokenProvider,
87
+ headersProvider,
88
+ fetchImpl = fetch,
89
+ signal: deadlineSignal,
90
+ requestTimeoutMs = DEFAULT_PROBE_TIMEOUT_MS,
91
+ } = options;
92
+ if (allowedBaseUrls.length === 0) {
93
+ throw new Error(
94
+ "buildVerifierProbeClient requires at least one allowlisted base URL",
95
+ );
96
+ }
97
+
98
+ return async (request) => {
99
+ const isAbsolute = /^https?:\/\//i.test(request.path);
100
+ const url = isAbsolute
101
+ ? new URL(request.path)
102
+ : new URL(request.path, allowedBaseUrls[0]);
103
+
104
+ const allowed = allowedBaseUrls.some(
105
+ (base) => url.origin === new URL(base).origin,
106
+ );
107
+ if (!allowed) {
108
+ throw new Error(
109
+ `Probe target "${url.origin}" is not an allowlisted base URL ` +
110
+ "(verifier probes carry the same allowlist as the HTTP tool).",
111
+ );
112
+ }
113
+
114
+ const token = await tokenProvider();
115
+ const headers: Record<string, string> = {
116
+ ...(await resolveExtraHeaders(headersProvider)),
117
+ Authorization: `Bearer ${token}`,
118
+ };
119
+ // Every probe fetch is bounded (finding 6): per-request timeout
120
+ // always, plus the caller's deadline signal when provided.
121
+ const timeoutSignal = AbortSignal.timeout(requestTimeoutMs);
122
+ const init: RequestInit = {
123
+ method: request.method,
124
+ headers,
125
+ signal: deadlineSignal
126
+ ? AbortSignal.any([deadlineSignal, timeoutSignal])
127
+ : timeoutSignal,
128
+ };
129
+ if (request.body !== undefined) {
130
+ headers["Content-Type"] = "application/json";
131
+ init.body = JSON.stringify(request.body);
132
+ }
133
+
134
+ let response: Response;
135
+ try {
136
+ response = await fetchImpl(url.toString(), init);
137
+ } catch (error) {
138
+ // Redact the credential from transport-error messages before they
139
+ // propagate into assertion diagnostics.
140
+ const message = error instanceof Error ? error.message : String(error);
141
+ throw new Error(
142
+ `Probe ${request.method} ${url.pathname} failed: ` +
143
+ message.split(token).join("[REDACTED]"),
144
+ );
145
+ }
146
+
147
+ const rawBody = await response.text();
148
+ const contentType = response.headers.get("content-type") ?? "";
149
+ let body: unknown = rawBody;
150
+ if (contentType.includes("application/json")) {
151
+ try {
152
+ body = JSON.parse(rawBody);
153
+ } catch {
154
+ body = rawBody;
155
+ }
156
+ }
157
+ return { status: response.status, body };
158
+ };
159
+ }
160
+
161
+ // ── Placeholder binding ──────────────────────────────────────────────────────
162
+
163
+ const PLACEHOLDER_PATTERN = /\{([A-Za-z0-9_]+)\}/g;
164
+ const ID_SUFFIX = "_id";
165
+
166
+ export type PlaceholderBinding =
167
+ | { ok: true; path: string }
168
+ | { ok: false; missing: string[] };
169
+
170
+ /**
171
+ * Bind every `{placeholder}` in a probe path from the run ledger.
172
+ * `{<resourceType>_id}` binds to the MOST RECENT ledger entry of that
173
+ * resourceType, regardless of which feed recorded it. Unresolvable
174
+ * placeholders are reported (assertion failure), never thrown.
175
+ */
176
+ export function bindPlaceholders(
177
+ path: string,
178
+ ledger: RunLedger,
179
+ ): PlaceholderBinding {
180
+ const missing: string[] = [];
181
+
182
+ const bound = path.replace(PLACEHOLDER_PATTERN, (whole, name: string) => {
183
+ const resourceType = name.endsWith(ID_SUFFIX)
184
+ ? name.slice(0, -ID_SUFFIX.length)
185
+ : name;
186
+ // Most recent entry wins when a journey created several resources of
187
+ // one type — the assertion targets what the agent just finished with.
188
+ for (let i = ledger.entries.length - 1; i >= 0; i -= 1) {
189
+ if (ledger.entries[i].resourceType === resourceType) {
190
+ return ledger.entries[i].id;
191
+ }
192
+ }
193
+ missing.push(name);
194
+ return whole;
195
+ });
196
+
197
+ return missing.length > 0
198
+ ? { ok: false, missing }
199
+ : { ok: true, path: bound };
200
+ }
201
+
202
+ // ── Minimal JSONPath evaluator ───────────────────────────────────────────────
203
+
204
+ /**
205
+ * Result of resolving a JSONPath: the value at the path, or a reason it
206
+ * could not be resolved. Filter expressions resolve to the ARRAY of
207
+ * matching items (so array_* assertion kinds operate on the match set).
208
+ */
209
+ export type JsonPathResult =
210
+ | { resolved: true; value: unknown }
211
+ | { resolved: false; reason: string };
212
+
213
+ type PathSegment =
214
+ | { kind: "property"; name: string }
215
+ | { kind: "index"; index: number }
216
+ | { kind: "filter"; field: string; equals: string | number | boolean };
217
+
218
+ // Segment grammar: .prop | [n] | [?(@.field=='literal')] (single-field
219
+ // equality filters — the subset the journey specs use).
220
+ const SEGMENT_PATTERN =
221
+ /\.([A-Za-z0-9_-]+)|\[(\d+)\]|\[\?\(@\.([A-Za-z0-9_-]+)\s*==\s*(?:'([^']*)'|"([^"]*)"|(true|false)|(-?\d+(?:\.\d+)?))\)\]/y;
222
+
223
+ function parseJsonPath(path: string): PathSegment[] | undefined {
224
+ if (!path.startsWith("$")) {
225
+ return undefined;
226
+ }
227
+ const segments: PathSegment[] = [];
228
+ SEGMENT_PATTERN.lastIndex = 1;
229
+ while (SEGMENT_PATTERN.lastIndex < path.length) {
230
+ const match = SEGMENT_PATTERN.exec(path);
231
+ if (match === null) {
232
+ return undefined;
233
+ }
234
+ const [, property, index, field, single, double, boolean, numeric] = match;
235
+ if (property !== undefined) {
236
+ segments.push({ kind: "property", name: property });
237
+ } else if (index !== undefined) {
238
+ segments.push({ kind: "index", index: Number(index) });
239
+ } else if (field !== undefined) {
240
+ const equals =
241
+ single ??
242
+ double ??
243
+ (boolean !== undefined ? boolean === "true" : Number(numeric));
244
+ segments.push({ kind: "filter", field, equals });
245
+ }
246
+ }
247
+ return segments;
248
+ }
249
+
250
+ /**
251
+ * Evaluate the JSONPath subset used by journey assertions against a
252
+ * response body: `$`, dotted properties (`$.items`), numeric indices
253
+ * (`$.items[0]`), and single-field equality filters over arrays
254
+ * (`$[?(@.role=='assistant')]` — resolves to the array of matches).
255
+ * Never throws: malformed paths and unresolvable steps report a reason.
256
+ */
257
+ export function evaluateJsonPath(body: unknown, path: string): JsonPathResult {
258
+ const segments = parseJsonPath(path);
259
+ if (segments === undefined) {
260
+ return { resolved: false, reason: `Malformed JSONPath: ${path}` };
261
+ }
262
+
263
+ let current: unknown = body;
264
+ for (const segment of segments) {
265
+ switch (segment.kind) {
266
+ case "property": {
267
+ if (
268
+ typeof current !== "object" ||
269
+ current === null ||
270
+ Array.isArray(current) ||
271
+ !(segment.name in current)
272
+ ) {
273
+ return {
274
+ resolved: false,
275
+ reason: `Property "${segment.name}" not found while resolving ${path}`,
276
+ };
277
+ }
278
+ current = (current as Record<string, unknown>)[segment.name];
279
+ break;
280
+ }
281
+ case "index": {
282
+ if (!Array.isArray(current) || segment.index >= current.length) {
283
+ return {
284
+ resolved: false,
285
+ reason: `Index [${segment.index}] out of range while resolving ${path}`,
286
+ };
287
+ }
288
+ current = current[segment.index];
289
+ break;
290
+ }
291
+ case "filter": {
292
+ if (!Array.isArray(current)) {
293
+ return {
294
+ resolved: false,
295
+ reason: `Filter applied to a non-array while resolving ${path}`,
296
+ };
297
+ }
298
+ current = current.filter(
299
+ (item) =>
300
+ typeof item === "object" &&
301
+ item !== null &&
302
+ (item as Record<string, unknown>)[segment.field] === segment.equals,
303
+ );
304
+ break;
305
+ }
306
+ }
307
+ }
308
+ return { resolved: true, value: current };
309
+ }
310
+
311
+ // ── Assertion evaluation ─────────────────────────────────────────────────────
312
+
313
+ /** Structured outcome of one assertion (diagnostics for the report). */
314
+ export interface AssertionResult {
315
+ assertion: JourneyAssertion;
316
+ passed: boolean;
317
+ expected?: unknown;
318
+ actual?: unknown;
319
+ message?: string;
320
+ }
321
+
322
+ function failed(
323
+ assertion: JourneyAssertion,
324
+ message: string,
325
+ expected?: unknown,
326
+ actual?: unknown,
327
+ ): AssertionResult {
328
+ return { assertion, passed: false, message, expected, actual };
329
+ }
330
+
331
+ function passed(
332
+ assertion: JourneyAssertion,
333
+ expected?: unknown,
334
+ actual?: unknown,
335
+ ): AssertionResult {
336
+ return { assertion, passed: true, expected, actual };
337
+ }
338
+
339
+ function resolvePath(
340
+ assertion: JourneyAssertion & { path: string },
341
+ response: ProbeResponse,
342
+ ): { value: unknown } | { failure: AssertionResult } {
343
+ const result = evaluateJsonPath(response.body, assertion.path);
344
+ if (!result.resolved) {
345
+ return { failure: failed(assertion, result.reason) };
346
+ }
347
+ return { value: result.value };
348
+ }
349
+
350
+ function evaluateAssertion(
351
+ assertion: JourneyAssertion,
352
+ response: ProbeResponse,
353
+ ): AssertionResult {
354
+ switch (assertion.kind) {
355
+ case "http_status": {
356
+ return response.status === assertion.equals
357
+ ? passed(assertion, assertion.equals, response.status)
358
+ : failed(
359
+ assertion,
360
+ `Expected HTTP ${assertion.equals}, got ${response.status}`,
361
+ assertion.equals,
362
+ response.status,
363
+ );
364
+ }
365
+ case "json_path_exists": {
366
+ const result = evaluateJsonPath(response.body, assertion.path);
367
+ return result.resolved
368
+ ? passed(assertion, assertion.path, result.value)
369
+ : failed(
370
+ assertion,
371
+ `JSONPath ${assertion.path} did not resolve: ${result.reason}`,
372
+ assertion.path,
373
+ );
374
+ }
375
+ case "json_path_equals": {
376
+ const resolved = resolvePath(assertion, response);
377
+ if ("failure" in resolved) return resolved.failure;
378
+ return resolved.value === assertion.equals
379
+ ? passed(assertion, assertion.equals, resolved.value)
380
+ : failed(
381
+ assertion,
382
+ `Value at ${assertion.path} does not equal the expected literal`,
383
+ assertion.equals,
384
+ resolved.value,
385
+ );
386
+ }
387
+ case "json_path_contains": {
388
+ const resolved = resolvePath(assertion, response);
389
+ if ("failure" in resolved) return resolved.failure;
390
+ const value = resolved.value;
391
+ const contains =
392
+ typeof value === "string" && typeof assertion.contains === "string"
393
+ ? value.includes(assertion.contains)
394
+ : Array.isArray(value) && value.includes(assertion.contains);
395
+ return contains
396
+ ? passed(assertion, assertion.contains, value)
397
+ : failed(
398
+ assertion,
399
+ `Value at ${assertion.path} does not contain the expected target`,
400
+ assertion.contains,
401
+ value,
402
+ );
403
+ }
404
+ case "array_min_length": {
405
+ const resolved = resolvePath(assertion, response);
406
+ if ("failure" in resolved) return resolved.failure;
407
+ if (!Array.isArray(resolved.value)) {
408
+ return failed(
409
+ assertion,
410
+ `Value at ${assertion.path} is not an array`,
411
+ assertion.min,
412
+ resolved.value,
413
+ );
414
+ }
415
+ const length = resolved.value.length;
416
+ return length >= assertion.min
417
+ ? passed(assertion, assertion.min, length)
418
+ : failed(
419
+ assertion,
420
+ `Array at ${assertion.path} has ${length} item(s); expected >= ${assertion.min}`,
421
+ assertion.min,
422
+ length,
423
+ );
424
+ }
425
+ case "array_contains_match": {
426
+ const resolved = resolvePath(assertion, response);
427
+ if ("failure" in resolved) return resolved.failure;
428
+ if (!Array.isArray(resolved.value)) {
429
+ return failed(
430
+ assertion,
431
+ `Value at ${assertion.path} is not an array`,
432
+ assertion.match,
433
+ resolved.value,
434
+ );
435
+ }
436
+ const matches = resolved.value.some(
437
+ (item) =>
438
+ typeof item === "object" &&
439
+ item !== null &&
440
+ Object.entries(assertion.match).every(
441
+ ([field, literal]) =>
442
+ (item as Record<string, unknown>)[field] === literal,
443
+ ),
444
+ );
445
+ return matches
446
+ ? passed(assertion, assertion.match)
447
+ : failed(
448
+ assertion,
449
+ `No item in the array at ${assertion.path} matches every field predicate`,
450
+ assertion.match,
451
+ resolved.value,
452
+ );
453
+ }
454
+ }
455
+ }
456
+
457
+ /**
458
+ * Evaluate every assertion against live probe responses. ALL assertions
459
+ * run (a failure never short-circuits the rest — the report wants the
460
+ * full picture); the verdict composer applies all-must-pass semantics.
461
+ * Probe failures — missing placeholder bindings, thrown client errors —
462
+ * become structured assertion failures, never crashes.
463
+ */
464
+ export async function runAssertions(
465
+ assertions: readonly JourneyAssertion[],
466
+ ledger: RunLedger,
467
+ client: VerifierProbeClient,
468
+ ): Promise<AssertionResult[]> {
469
+ const results: AssertionResult[] = [];
470
+
471
+ for (const assertion of assertions) {
472
+ const binding = bindPlaceholders(assertion.request.path, ledger);
473
+ if (!binding.ok) {
474
+ results.push(
475
+ failed(
476
+ assertion,
477
+ `Unbound placeholder(s) [${binding.missing.join(", ")}] in probe ` +
478
+ `path "${assertion.request.path}" — no matching resource was ` +
479
+ "recorded in the run ledger (either feed).",
480
+ ),
481
+ );
482
+ continue;
483
+ }
484
+
485
+ let response: ProbeResponse;
486
+ try {
487
+ response = await client({ ...assertion.request, path: binding.path });
488
+ } catch (error) {
489
+ results.push(
490
+ failed(
491
+ assertion,
492
+ `Probe request failed: ${error instanceof Error ? error.message : String(error)}`,
493
+ ),
494
+ );
495
+ continue;
496
+ }
497
+
498
+ results.push(evaluateAssertion(assertion, response));
499
+ }
500
+
501
+ return results;
502
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Journeys verdict composition (plan: jrn-verifier packet, Approach
3
+ * §Verdict discipline and packet ordering).
4
+ *
5
+ * The deterministic assertions are the SOLE pass/fail authority — agent
6
+ * completion status alone never passes a journey. Composition rule: a
7
+ * journey passes only when ALL assertions pass AND the RunOutcome carries
8
+ * no failure_kind. Any failure_kind (budget_exceeded, budget_unverifiable,
9
+ * infra_error, agent_error) can NEVER compose to pass, even with every
10
+ * assertion green. The advisory rubric is NOT part of the verdict.
11
+ */
12
+
13
+ import type { FailureKind } from "../runner.js";
14
+ import type { JourneyAssertion } from "../spec/schema.js";
15
+ import type { RunLedger } from "../tools/ledger.js";
16
+ import {
17
+ runAssertions,
18
+ type AssertionResult,
19
+ type VerifierProbeClient,
20
+ } from "./assertions.js";
21
+
22
+ /** Structural subset of RunOutcome the verdict composer consumes. */
23
+ export interface VerifiableOutcome {
24
+ failure_kind?: FailureKind;
25
+ }
26
+
27
+ /** The journey's final deterministic verdict. */
28
+ export interface Verdict {
29
+ passed: boolean;
30
+ /** Propagated from the RunOutcome when the run itself failed. */
31
+ failure_kind?: FailureKind;
32
+ /** Every assertion's structured result, for reporting. */
33
+ results: AssertionResult[];
34
+ }
35
+
36
+ /**
37
+ * Compose the verdict: pass only if ALL assertions passed and the run
38
+ * carried no failure_kind. Pure and synchronous — probes have already run.
39
+ */
40
+ export function composeVerdict(
41
+ outcome: VerifiableOutcome,
42
+ results: AssertionResult[],
43
+ ): Verdict {
44
+ const verdict: Verdict = {
45
+ passed:
46
+ outcome.failure_kind === undefined &&
47
+ results.every((result) => result.passed),
48
+ results,
49
+ };
50
+ if (outcome.failure_kind !== undefined) {
51
+ verdict.failure_kind = outcome.failure_kind;
52
+ }
53
+ return verdict;
54
+ }
55
+
56
+ /**
57
+ * Verify one finished journey: execute its assertions as API probes
58
+ * against the run's ledger (placeholder binding from either feed), then
59
+ * compose the verdict. Called by the entrypoint strictly AFTER runJourney
60
+ * and BEFORE any teardown — probes must observe the created state. Even
61
+ * when the run carried a failure_kind the probes still execute (their
62
+ * results are diagnostics for the report), but the verdict can never pass.
63
+ */
64
+ export async function verifyJourney(
65
+ assertions: readonly JourneyAssertion[],
66
+ outcome: VerifiableOutcome & { ledger: RunLedger },
67
+ client: VerifierProbeClient,
68
+ ): Promise<Verdict> {
69
+ const results = await runAssertions(assertions, outcome.ledger, client);
70
+ return composeVerdict(outcome, results);
71
+ }
@@ -0,0 +1,36 @@
1
+ import { writeCurrentPerfRunReport } from "../../shared/perf/report-store.js";
2
+
3
+ function main(): void {
4
+ const environment = process.env.PERF_ENVIRONMENT;
5
+ const rawModules = process.env.PERF_MODULES ?? "";
6
+ const modules = rawModules
7
+ .split(",")
8
+ .map((moduleName) => moduleName.trim())
9
+ .filter(Boolean);
10
+
11
+ if (!environment) {
12
+ throw new Error("PERF_ENVIRONMENT is required");
13
+ }
14
+
15
+ if (modules.length === 0) {
16
+ throw new Error("PERF_MODULES must include at least one module");
17
+ }
18
+
19
+ const report = writeCurrentPerfRunReport({
20
+ environment,
21
+ modules,
22
+ finishedAt: new Date().toISOString(),
23
+ });
24
+
25
+ console.log(
26
+ `Generated perf report for ${report.modules.length} module(s) in ${report.environment}`,
27
+ );
28
+ }
29
+
30
+ try {
31
+ main();
32
+ } catch (error) {
33
+ const message = error instanceof Error ? error.message : String(error);
34
+ console.error(`Failed to build perf report: ${message}`);
35
+ process.exit(1);
36
+ }