@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.
Files changed (231) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/LICENSE +21 -0
  3. package/README.md +166 -2
  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 +104 -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,278 @@
1
+ /**
2
+ * Journeys resource ledger — descriptor-based tracking of resources a
3
+ * journey creates, so the caller-orchestrated teardown can delete exactly
4
+ * what was created (and nothing it merely guesses at).
5
+ *
6
+ * Two feeds share this module: the scoped HTTP tool records its own calls
7
+ * directly, and the browser toolset's network-interception feed (next
8
+ * packet) reuses the same pure `matchResourceCreation` matcher.
9
+ *
10
+ * Data-minimization contract: ledger entries and warnings retain ONLY
11
+ * {method, sanitized path (query/fragment/origin stripped), status,
12
+ * extracted descriptor id, source tag}. Bodies are inspected transiently
13
+ * for id extraction and discarded; headers, cookies, and credential
14
+ * material never enter this module's stored state.
15
+ */
16
+
17
+ /** Feed that produced a ledger record. */
18
+ export type LedgerSource = "http-tool" | "browser";
19
+
20
+ /** HTTP methods the matcher distinguishes. */
21
+ export type HttpMethod =
22
+ | "GET"
23
+ | "HEAD"
24
+ | "OPTIONS"
25
+ | "POST"
26
+ | "PUT"
27
+ | "PATCH"
28
+ | "DELETE";
29
+
30
+ const MUTATING_METHODS: ReadonlySet<HttpMethod> = new Set([
31
+ "POST",
32
+ "PUT",
33
+ "PATCH",
34
+ "DELETE",
35
+ ]);
36
+
37
+ /** Explicit cleanup request bound to a recorded resource. */
38
+ export interface CleanupRequest {
39
+ method: HttpMethod;
40
+ path: string;
41
+ }
42
+
43
+ /**
44
+ * An observed API call, as seen by either feed. `body` is the parsed
45
+ * response body; it is read transiently for id extraction and never stored.
46
+ */
47
+ export interface ObservedCall {
48
+ method: HttpMethod;
49
+ /** Raw path or absolute URL; sanitized before matching/storage. */
50
+ pathOrUrl: string;
51
+ status: number;
52
+ /** Parsed response body (transient — id extraction only). */
53
+ body?: unknown;
54
+ }
55
+
56
+ /**
57
+ * Registry entry describing one recognized resource-creating endpoint and
58
+ * how to clean the resource up afterwards.
59
+ */
60
+ export interface ResourceDescriptor {
61
+ resourceType: string;
62
+ creation: {
63
+ method: HttpMethod;
64
+ /** Matched against the sanitized path (origin + query stripped). */
65
+ pathPattern: RegExp;
66
+ };
67
+ /** Extract the created resource's id from the observed call. */
68
+ extractId: (call: ObservedCall) => string | undefined;
69
+ /** Build the exact cleanup request for a created resource. */
70
+ cleanup: (id: string) => CleanupRequest;
71
+ }
72
+
73
+ /**
74
+ * Registry the matcher runs against: recognized creations plus known
75
+ * action-POST patterns (RPC-style endpoints that mutate nothing we must
76
+ * track — never recorded as resources NOR as unmanaged warnings).
77
+ */
78
+ export interface LedgerRegistry {
79
+ descriptors: ResourceDescriptor[];
80
+ actionPatterns: RegExp[];
81
+ }
82
+
83
+ /** Result of matching one observed call against the registry. */
84
+ export type ResourceMatch =
85
+ | {
86
+ kind: "resource";
87
+ resourceType: string;
88
+ id: string;
89
+ cleanup: CleanupRequest;
90
+ }
91
+ | { kind: "unmanaged-mutation" }
92
+ | { kind: "ignored" };
93
+
94
+ /** A recorded, cleanup-bound resource creation (data-minimized). */
95
+ export interface LedgerEntry {
96
+ resourceType: string;
97
+ id: string;
98
+ method: HttpMethod;
99
+ path: string;
100
+ status: number;
101
+ source: LedgerSource;
102
+ cleanup: CleanupRequest;
103
+ }
104
+
105
+ /**
106
+ * A successful mutation on an endpoint with no registered descriptor —
107
+ * reported for humans to review, never guessed-deleted (data-minimized).
108
+ */
109
+ export interface UnmanagedMutationWarning {
110
+ method: HttpMethod;
111
+ path: string;
112
+ status: number;
113
+ source: LedgerSource;
114
+ }
115
+
116
+ /**
117
+ * Strip origin, query string, and fragment from a path or absolute URL so
118
+ * matching and storage only ever see the bare path (data minimization:
119
+ * query strings can carry tokens).
120
+ */
121
+ export function sanitizePath(pathOrUrl: string): string {
122
+ // Absolute URLs parse directly; bare paths get a placeholder origin.
123
+ const url = new URL(pathOrUrl, "http://placeholder.invalid");
124
+ return url.pathname;
125
+ }
126
+
127
+ /**
128
+ * The registry comes from the app under test (`journeys.app.resources` and
129
+ * `journeys.app.actions` in the spec-hosting repo's manifest): see
130
+ * ledgerRegistryFromApp in ../app-config.ts. The engine ships none of its
131
+ * own; this empty registry reports every successful mutation as unmanaged.
132
+ */
133
+ export function emptyLedgerRegistry(): LedgerRegistry {
134
+ return { descriptors: [], actionPatterns: [] };
135
+ }
136
+
137
+ /**
138
+ * Pure matcher shared by both feeds (HTTP tool now, browser network
139
+ * interception in the next packet). Rules:
140
+ * - non-mutating methods and non-2xx responses are ignored;
141
+ * - known action POSTs are ignored (proven-by-test never recorded);
142
+ * - a registered creation with an extractable id becomes a resource match
143
+ * carrying the descriptor's exact cleanup request;
144
+ * - any other successful mutation — including a registered creation whose
145
+ * id cannot be extracted — is an unmanaged mutation (reported, never
146
+ * guessed-deleted).
147
+ */
148
+ export function matchResourceCreation(
149
+ registry: LedgerRegistry,
150
+ call: ObservedCall,
151
+ ): ResourceMatch {
152
+ if (!MUTATING_METHODS.has(call.method)) {
153
+ return { kind: "ignored" };
154
+ }
155
+ if (call.status < 200 || call.status >= 300) {
156
+ return { kind: "ignored" };
157
+ }
158
+
159
+ const path = sanitizePath(call.pathOrUrl);
160
+
161
+ if (registry.actionPatterns.some((pattern) => pattern.test(path))) {
162
+ return { kind: "ignored" };
163
+ }
164
+
165
+ for (const descriptor of registry.descriptors) {
166
+ if (
167
+ descriptor.creation.method === call.method &&
168
+ descriptor.creation.pathPattern.test(path)
169
+ ) {
170
+ const id = descriptor.extractId(call);
171
+ if (id === undefined) {
172
+ // Recognized creation but no id — cleanup is impossible, so this
173
+ // degrades to a reported warning instead of a guessed delete.
174
+ return { kind: "unmanaged-mutation" };
175
+ }
176
+ return {
177
+ kind: "resource",
178
+ resourceType: descriptor.resourceType,
179
+ id,
180
+ cleanup: descriptor.cleanup(id),
181
+ };
182
+ }
183
+ }
184
+
185
+ return { kind: "unmanaged-mutation" };
186
+ }
187
+
188
+ /**
189
+ * Observer notified the instant the ledger recognizes a resource creation,
190
+ * an unmanaged mutation, or an interception failure — the observability
191
+ * event bus subscribes here so bindings (the `{item_id}` question) surface
192
+ * on the live stream and in events.jsonl, not just in the teardown section.
193
+ * Data-minimized by construction: only the already-sanitized record fields
194
+ * cross this boundary (no bodies/headers).
195
+ */
196
+ export interface LedgerObserver {
197
+ onResource?(entry: LedgerEntry): void;
198
+ onUnmanagedMutation?(warning: UnmanagedMutationWarning): void;
199
+ onInterceptionError?(message: string): void;
200
+ }
201
+
202
+ /**
203
+ * Multi-feed run ledger. Records recognized resource creations with their
204
+ * cleanup descriptors and collects unmanaged-mutation warnings; consumed by
205
+ * the caller-orchestrated teardown executor (jrn-runner packet).
206
+ */
207
+ export class RunLedger {
208
+ private readonly registry: LedgerRegistry;
209
+ private readonly recordedEntries: LedgerEntry[] = [];
210
+ private readonly recordedWarnings: UnmanagedMutationWarning[] = [];
211
+ private readonly recordedInterceptionErrors: string[] = [];
212
+ private readonly observer?: LedgerObserver;
213
+
214
+ constructor(registry: LedgerRegistry, observer?: LedgerObserver) {
215
+ this.registry = registry;
216
+ this.observer = observer;
217
+ }
218
+
219
+ /** Recorded resource creations, in creation order. */
220
+ get entries(): readonly LedgerEntry[] {
221
+ return this.recordedEntries;
222
+ }
223
+
224
+ /** Unmanaged-mutation warnings, in observation order. */
225
+ get warnings(): readonly UnmanagedMutationWarning[] {
226
+ return this.recordedWarnings;
227
+ }
228
+
229
+ /**
230
+ * Interception-processing failures (review finding 7): a failed
231
+ * response handler means a creation MAY have gone unrecorded, so the
232
+ * failure is reported loudly instead of being swallowed.
233
+ */
234
+ get interceptionErrors(): readonly string[] {
235
+ return this.recordedInterceptionErrors;
236
+ }
237
+
238
+ /** Record one interception-processing failure (message only — no bodies). */
239
+ recordInterceptionError(message: string): void {
240
+ this.recordedInterceptionErrors.push(message);
241
+ this.observer?.onInterceptionError?.(message);
242
+ }
243
+
244
+ /**
245
+ * Feed one observed call into the ledger. The call's body is only passed
246
+ * through the matcher for id extraction; stored records are minimized to
247
+ * method + sanitized path + status + id + source.
248
+ */
249
+ record(call: ObservedCall, source: LedgerSource): ResourceMatch {
250
+ const match = matchResourceCreation(this.registry, call);
251
+ const path = sanitizePath(call.pathOrUrl);
252
+
253
+ if (match.kind === "resource") {
254
+ const entry: LedgerEntry = {
255
+ resourceType: match.resourceType,
256
+ id: match.id,
257
+ method: call.method,
258
+ path,
259
+ status: call.status,
260
+ source,
261
+ cleanup: match.cleanup,
262
+ };
263
+ this.recordedEntries.push(entry);
264
+ this.observer?.onResource?.(entry);
265
+ } else if (match.kind === "unmanaged-mutation") {
266
+ const warning: UnmanagedMutationWarning = {
267
+ method: call.method,
268
+ path,
269
+ status: call.status,
270
+ source,
271
+ };
272
+ this.recordedWarnings.push(warning);
273
+ this.observer?.onUnmanagedMutation?.(warning);
274
+ }
275
+
276
+ return match;
277
+ }
278
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Journeys snapshot store — deterministic per-action PNG persistence plus
3
+ * the SnapshotIndex consumed by the rendered report/gallery and friction
4
+ * evidence (jrn-reporting packet).
5
+ *
6
+ * Screenshots live under `<reportDir>/snapshots/NNN-<action>.png` (the run
7
+ * entrypoint passes `reports/journeys/<run-id>` as the report dir). Entries
8
+ * for sensitive actions can be *suppressed*: the capture callback is never
9
+ * invoked — so the frame is never serialized — and the index records a
10
+ * masked entry with a null path (critique finding D2-1).
11
+ */
12
+
13
+ import * as fs from "fs";
14
+ import * as path from "path";
15
+
16
+ /** One SnapshotIndex row. `path` is null when capture was suppressed. */
17
+ export interface SnapshotIndexEntry {
18
+ seq: number;
19
+ action: string;
20
+ url: string;
21
+ path: string | null;
22
+ ts: string;
23
+ /** Present (true) when the action failed and this is its failure frame. */
24
+ failure?: boolean;
25
+ /** Present (true) when the action's input was masked as sensitive. */
26
+ masked?: boolean;
27
+ /** Present (true) when the screenshot was suppressed pre-capture. */
28
+ suppressed?: boolean;
29
+ /**
30
+ * Present when the capture callback (or the snapshots-dir mkdir)
31
+ * failed — the entry is still recorded (path null) so the missing
32
+ * evidence surfaces as a report warning (review finding 9).
33
+ */
34
+ captureError?: string;
35
+ }
36
+
37
+ export interface PersistOptions {
38
+ /** Short action label, e.g. "navigate", "click"; slugged into the name. */
39
+ action: string;
40
+ /** Page URL at capture time (already policy-checked by the caller). */
41
+ url: string;
42
+ /**
43
+ * Writes the PNG to the given absolute path (typically
44
+ * `page.screenshot({path})`). NOT invoked when `suppress` is set.
45
+ */
46
+ capture: (filePath: string) => Promise<void>;
47
+ failure?: boolean;
48
+ masked?: boolean;
49
+ suppress?: boolean;
50
+ }
51
+
52
+ /** Slug an action label into a filesystem-safe fragment. */
53
+ function slugify(action: string): string {
54
+ return (
55
+ action
56
+ .toLowerCase()
57
+ .replace(/[^a-z0-9]+/g, "-")
58
+ .replace(/^-+|-+$/g, "") || "action"
59
+ );
60
+ }
61
+
62
+ export class SnapshotStore {
63
+ private readonly entries: SnapshotIndexEntry[] = [];
64
+ readonly snapshotsDir: string;
65
+
66
+ constructor(reportDir: string) {
67
+ this.snapshotsDir = path.join(reportDir, "snapshots");
68
+ }
69
+
70
+ /** SnapshotIndex entries in capture order. */
71
+ get index(): readonly SnapshotIndexEntry[] {
72
+ return this.entries;
73
+ }
74
+
75
+ /**
76
+ * Persist one action's screenshot and append its index entry. Ordering
77
+ * contract: when `suppress` is set, `capture` is NEVER called — the entry
78
+ * is recorded with a null path, so no sensitive frame can be serialized.
79
+ *
80
+ * Capture failures never throw (review finding 9): the entry is still
81
+ * recorded with `captureError` set, so a passing run with missing
82
+ * required evidence is reported loudly instead of silently.
83
+ */
84
+ async persist(options: PersistOptions): Promise<SnapshotIndexEntry> {
85
+ const seq = this.entries.length + 1;
86
+ const entry: SnapshotIndexEntry = {
87
+ seq,
88
+ action: options.action,
89
+ url: options.url,
90
+ path: null,
91
+ ts: new Date().toISOString(),
92
+ };
93
+ if (options.failure) entry.failure = true;
94
+ if (options.masked) entry.masked = true;
95
+
96
+ if (options.suppress) {
97
+ entry.suppressed = true;
98
+ this.entries.push(entry);
99
+ return entry;
100
+ }
101
+
102
+ const name = `${String(seq).padStart(3, "0")}-${slugify(options.action)}${
103
+ options.failure ? "-failure" : ""
104
+ }.png`;
105
+ try {
106
+ fs.mkdirSync(this.snapshotsDir, { recursive: true });
107
+ const filePath = path.join(this.snapshotsDir, name);
108
+ await options.capture(filePath);
109
+ entry.path = filePath;
110
+ } catch (error) {
111
+ entry.captureError =
112
+ error instanceof Error ? error.message : String(error);
113
+ }
114
+ this.entries.push(entry);
115
+ return entry;
116
+ }
117
+ }