@broberg/lens-daemon 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/README.md +14 -2
- package/package.json +52 -3
- package/src/artifact-backfill.ts +253 -0
- package/src/artifact-uploads.ts +370 -0
- package/src/auth/index.ts +294 -0
- package/src/browser.ts +440 -0
- package/src/build-marker.ts +197 -0
- package/src/capture-session.ts +873 -0
- package/src/capture.ts +348 -0
- package/src/critic-dom.ts +951 -0
- package/src/critic-vision.ts +205 -0
- package/src/critic.ts +73 -0
- package/src/daemon-routes.ts +333 -0
- package/src/durability.ts +18 -0
- package/src/ensure-chromium.ts +118 -0
- package/src/export.ts +256 -0
- package/src/fill.ts +149 -0
- package/src/flow-store.ts +207 -0
- package/src/flow.ts +2869 -0
- package/src/frames.ts +195 -0
- package/src/gate.ts +123 -0
- package/src/http.ts +175 -0
- package/src/index.ts +24 -0
- package/src/manifest.ts +741 -0
- package/src/mcp.ts +562 -0
- package/src/motion-probe.ts +653 -0
- package/src/password-in-url.ts +106 -0
- package/src/pdf-text.ts +514 -0
- package/src/pixelmatch.d.ts +21 -0
- package/src/preflight.ts +76 -0
- package/src/readiness.ts +282 -0
- package/src/record-run.ts +427 -0
- package/src/record.ts +387 -0
- package/src/sandbox.ts +22 -0
- package/src/schema.ts +300 -0
- package/src/scrape.ts +116 -0
- package/src/seed-storage.ts +149 -0
- package/src/service.ts +1483 -0
- package/src/storage.ts +159 -0
- package/src/store.ts +267 -0
- package/src/swallowed-errors.ts +252 -0
- package/src/validate-vision.ts +97 -0
- package/src/verify.ts +411 -0
- package/src/video.ts +211 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// F082 — Lens flow-run persistence.
|
|
2
|
+
//
|
|
3
|
+
// Mirrors store.ts (captures): per-step screenshot BYTES go to the storage
|
|
4
|
+
// adapter; the run report (one row, per-step status in steps_json) goes to the
|
|
5
|
+
// daemon's SQLite. Hard-scoped to project_slug.
|
|
6
|
+
|
|
7
|
+
import type { Database } from 'bun:sqlite';
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
9
|
+
import type { LensFlowRunRow } from './schema.ts';
|
|
10
|
+
import { artifactKeys, type LensStorage } from './storage.ts';
|
|
11
|
+
import { enqueueArtifacts } from './artifact-uploads.ts';
|
|
12
|
+
import type { FlowResult, FailedResponse } from './flow.ts';
|
|
13
|
+
|
|
14
|
+
/** A per-step entry as persisted (screenshot bytes already offloaded to an
|
|
15
|
+
* artifact key — null when the step took no shot). */
|
|
16
|
+
export interface StoredFlowStep {
|
|
17
|
+
index: number;
|
|
18
|
+
type: string;
|
|
19
|
+
label: string;
|
|
20
|
+
status: 'pass' | 'fail' | 'skip';
|
|
21
|
+
testid: string | null;
|
|
22
|
+
detail: string | null;
|
|
23
|
+
artifact_key: string | null;
|
|
24
|
+
/** F074.29 — set when the step failed because the PROBE was broken (an assert
|
|
25
|
+
* body that cannot compile, or one that threw every time) rather than because
|
|
26
|
+
* the app was wrong. `status` stays 'fail' either way, so no consumer can
|
|
27
|
+
* accidentally stop counting it. */
|
|
28
|
+
fault?: 'instrument';
|
|
29
|
+
/** F074.49 — the failed HTTP responses the app gave around this step, so the
|
|
30
|
+
* report carries the ANSWER and not only the assertion. Persisted, because a
|
|
31
|
+
* flow-run is read back from this row long after the browser is gone: a field
|
|
32
|
+
* produced in flow.ts but not written here reaches nobody. */
|
|
33
|
+
network?: FailedResponse[];
|
|
34
|
+
/** F126.7 — the composition report for a screenshot step. Persisted for the
|
|
35
|
+
* same reason `network` is: a flow-run is read back from this row long after
|
|
36
|
+
* the browser is gone, and a field produced in flow.ts but not written here
|
|
37
|
+
* reaches nobody. xrt81 (#21050) measured the cost of the string-only version
|
|
38
|
+
* — a caller fixed the one finding it was shown, went green, and the rest were
|
|
39
|
+
* silently recorded as resolved. */
|
|
40
|
+
critic?: unknown;
|
|
41
|
+
/** F213.12 — how long after the last navigation this step ran. THE THIRD TIME
|
|
42
|
+
* this file's rule caught someone, and this time it was the author of the
|
|
43
|
+
* rule's most recent entry: F213.11 produced the field in flow.ts, proved it
|
|
44
|
+
* against `runFlow`'s return value, and never wrote it here — so the number
|
|
45
|
+
* reached nobody through the MCP, which is the ONLY surface the session that
|
|
46
|
+
* asked for it can read. Measured on the wire, not reasoned about.
|
|
47
|
+
*
|
|
48
|
+
* UNDEFINED means the step never ran (skip). ZERO is a real, common reading —
|
|
49
|
+
* a `goto` runs at 0ms after its own navigation — so this must be written on
|
|
50
|
+
* a `typeof === 'number'` test, never on truthiness. Every other optional
|
|
51
|
+
* field here is correctly gated on truthiness; copying that pattern for a
|
|
52
|
+
* number silently reports the strongest evidence of an early write as
|
|
53
|
+
* "not measured". */
|
|
54
|
+
ms_after_navigation?: number;
|
|
55
|
+
/** F213.13 — how long the step itself took. Written here IN THE SAME COMMIT as
|
|
56
|
+
* the producer, which is the entire lesson of F213.12 one card earlier: a field
|
|
57
|
+
* that exists only in flow.ts reaches nobody. Same `typeof` gate — a step that
|
|
58
|
+
* completed in under a millisecond reports 0, and that is a reading, not a gap. */
|
|
59
|
+
ms_step_duration?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function recordFlowRun(
|
|
63
|
+
db: Database,
|
|
64
|
+
storage: LensStorage,
|
|
65
|
+
projectSlug: string,
|
|
66
|
+
result: FlowResult,
|
|
67
|
+
/** F074.36 — which credential the run used, or why it had none. Persisted so a
|
|
68
|
+
* past GREEN run can still be asked "were you even logged in?". */
|
|
69
|
+
auth?: { source: string; adapter: string | null; note?: string },
|
|
70
|
+
): Promise<LensFlowRunRow> {
|
|
71
|
+
const id = randomUUID();
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
|
|
74
|
+
const storedSteps: StoredFlowStep[] = [];
|
|
75
|
+
for (const s of result.steps) {
|
|
76
|
+
let artifactKey: string | null = null;
|
|
77
|
+
if (s.screenshot) {
|
|
78
|
+
artifactKey = artifactKeys.flowStep(projectSlug, id, s.index);
|
|
79
|
+
await storage.put(artifactKey, s.screenshot);
|
|
80
|
+
}
|
|
81
|
+
storedSteps.push({
|
|
82
|
+
index: s.index,
|
|
83
|
+
type: s.type,
|
|
84
|
+
label: s.label,
|
|
85
|
+
status: s.status,
|
|
86
|
+
testid: s.testid,
|
|
87
|
+
detail: s.detail,
|
|
88
|
+
artifact_key: artifactKey,
|
|
89
|
+
...(s.fault ? { fault: s.fault } : {}),
|
|
90
|
+
...(s.network?.length ? { network: s.network } : {}),
|
|
91
|
+
...(s.critic ? { critic: s.critic } : {}),
|
|
92
|
+
// `typeof === 'number'`, NOT truthiness — 0 is the reading that matters most.
|
|
93
|
+
...(typeof s.ms_after_navigation === 'number' ? { ms_after_navigation: s.ms_after_navigation } : {}),
|
|
94
|
+
...(typeof s.ms_step_duration === 'number' ? { ms_step_duration: s.ms_step_duration } : {}),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// F255.5 — store the run's recording next to its step stills, under the same
|
|
99
|
+
// flow-run prefix so both are cleaned up together.
|
|
100
|
+
let videoKey: string | null = null;
|
|
101
|
+
if (result.video && result.video.length > 0) {
|
|
102
|
+
videoKey = artifactKeys.flowVideo(projectSlug, id);
|
|
103
|
+
await storage.put(videoKey, result.video);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const row: LensFlowRunRow = {
|
|
107
|
+
id,
|
|
108
|
+
project_slug: projectSlug,
|
|
109
|
+
flow_name: result.name,
|
|
110
|
+
status: result.status,
|
|
111
|
+
failed_at: result.failed_at,
|
|
112
|
+
reason: result.reason,
|
|
113
|
+
steps_json: JSON.stringify(storedSteps),
|
|
114
|
+
step_count: storedSteps.length,
|
|
115
|
+
duration_ms: result.duration_ms,
|
|
116
|
+
created_at: now,
|
|
117
|
+
video_key: videoKey,
|
|
118
|
+
build: result.build ?? null,
|
|
119
|
+
final_url: result.final_url ?? null,
|
|
120
|
+
// F074.59 — which motion state the run executed under. Absent on an older row.
|
|
121
|
+
reduced_motion: result.reduced_motion ?? null,
|
|
122
|
+
// F272.2 — WHERE the bytes above actually went, read off the storage adapter
|
|
123
|
+
// that just wrote them rather than from config at read time. This is the only
|
|
124
|
+
// moment the answer is certainly true.
|
|
125
|
+
artifact_backend: storage.backend,
|
|
126
|
+
// Only {source, adapter, note} — never the credential object itself. The
|
|
127
|
+
// caller already knows what it passed, and echoing statePath/url back into
|
|
128
|
+
// every report is noise in a surface many sessions read.
|
|
129
|
+
auth_json: auth
|
|
130
|
+
? JSON.stringify({
|
|
131
|
+
source: auth.source,
|
|
132
|
+
adapter: auth.adapter,
|
|
133
|
+
...(auth.note ? { note: auth.note } : {}),
|
|
134
|
+
// F074.36 — the measured verdict, not the intent: "a credential was
|
|
135
|
+
// configured" and "a credential can reach this host" are different
|
|
136
|
+
// facts, and only the second one means anything.
|
|
137
|
+
...(result.auth_reach ? { reach: result.auth_reach } : {}),
|
|
138
|
+
})
|
|
139
|
+
: null,
|
|
140
|
+
// F074.99 — the run-level findings, persisted for the same reason `network`
|
|
141
|
+
// and `critic` above are: a flow-run is read back from this row long after
|
|
142
|
+
// the browser is gone. ALWAYS written, even when empty — '{}' means the run
|
|
143
|
+
// looked and found nothing, and NULL means the row predates the column. Both
|
|
144
|
+
// findings were produced in flow.ts and written nowhere until fd-sundhed
|
|
145
|
+
// measured it from a consumer's seat: correct instrument, nobody told.
|
|
146
|
+
findings_json: JSON.stringify({
|
|
147
|
+
...(result.password_in_url ? { password_in_url: result.password_in_url } : {}),
|
|
148
|
+
...(result.not_listening ? { not_listening: result.not_listening } : {}),
|
|
149
|
+
}),
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
db.prepare(
|
|
153
|
+
`INSERT INTO lens_flow_runs (
|
|
154
|
+
id, project_slug, flow_name, status, failed_at, reason,
|
|
155
|
+
steps_json, step_count, duration_ms, created_at, video_key, build,
|
|
156
|
+
final_url, auth_json, reduced_motion, artifact_backend, findings_json
|
|
157
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
|
158
|
+
).run(
|
|
159
|
+
row.id, row.project_slug, row.flow_name, row.status, row.failed_at, row.reason,
|
|
160
|
+
row.steps_json, row.step_count, row.duration_ms, row.created_at, row.video_key ?? null,
|
|
161
|
+
row.build ?? null, row.final_url ?? null, row.auth_json ?? null,
|
|
162
|
+
row.reduced_motion ?? null,
|
|
163
|
+
row.artifact_backend ?? null,
|
|
164
|
+
row.findings_json ?? null,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// F276.1 — queue every still for the cloud, in the SAME function that wrote it
|
|
168
|
+
// locally. Doing it at the three call sites instead would mean a fourth call site
|
|
169
|
+
// added tomorrow silently produces phone-invisible evidence — the F213.12 lesson
|
|
170
|
+
// this file has already learned three times: a field produced in one place and not
|
|
171
|
+
// written in the other reaches nobody.
|
|
172
|
+
enqueueArtifacts(
|
|
173
|
+
db,
|
|
174
|
+
projectSlug,
|
|
175
|
+
[...storedSteps.map((s) => s.artifact_key), videoKey],
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return row;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface ListFlowRunsFilter {
|
|
182
|
+
flow_name?: string;
|
|
183
|
+
status?: string;
|
|
184
|
+
limit?: number;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function listFlowRuns(db: Database, projectSlug: string, filter: ListFlowRunsFilter = {}): LensFlowRunRow[] {
|
|
188
|
+
const clauses = ['project_slug = ?'];
|
|
189
|
+
const params: (string | number)[] = [projectSlug];
|
|
190
|
+
if (filter.flow_name) {
|
|
191
|
+
clauses.push('flow_name = ?');
|
|
192
|
+
params.push(filter.flow_name);
|
|
193
|
+
}
|
|
194
|
+
if (filter.status) {
|
|
195
|
+
clauses.push('status = ?');
|
|
196
|
+
params.push(filter.status);
|
|
197
|
+
}
|
|
198
|
+
const limit = Math.min(Math.max(filter.limit ?? 50, 1), 500);
|
|
199
|
+
params.push(limit);
|
|
200
|
+
return db
|
|
201
|
+
.query(`SELECT * FROM lens_flow_runs WHERE ${clauses.join(' AND ')} ORDER BY created_at DESC LIMIT ?`)
|
|
202
|
+
.all(...params) as LensFlowRunRow[];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function getFlowRun(db: Database, id: string): LensFlowRunRow | null {
|
|
206
|
+
return (db.query('SELECT * FROM lens_flow_runs WHERE id = ?').get(id) as LensFlowRunRow | null) ?? null;
|
|
207
|
+
}
|