@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.
- package/CHANGELOG.md +67 -0
- package/LICENSE +21 -0
- package/README.md +165 -1
- 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 +99 -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,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
|
+
}
|