@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
package/src/record.ts
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
// F312.1 — record a live surface ON DEMAND, with nothing to script.
|
|
2
|
+
//
|
|
3
|
+
// `record: true` has existed on a flow since F255.5, and it is unreachable for
|
|
4
|
+
// this purpose: a flow REQUIRES steps, and you must know what to drive before you
|
|
5
|
+
// know what is wrong. The defect that motivated F312 only appears while an agent
|
|
6
|
+
// writes into the stream — the reproduction is literally "open it and sit still
|
|
7
|
+
// while something else happens". So the verb is: open, watch, stop.
|
|
8
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import type { BrowserContext, Page } from 'playwright';
|
|
12
|
+
import { withPage } from './browser.ts';
|
|
13
|
+
import {
|
|
14
|
+
PROBE_READ,
|
|
15
|
+
type ScrollNote,
|
|
16
|
+
findUncausedMoves,
|
|
17
|
+
probeSource,
|
|
18
|
+
rankFindings,
|
|
19
|
+
type FindOptions,
|
|
20
|
+
type MotionFinding,
|
|
21
|
+
type MotionSample,
|
|
22
|
+
type ProbeConfig,
|
|
23
|
+
} from './motion-probe.ts';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* F312.11 — put the view back where a reader would keep it, and STAMP IT AS OURS.
|
|
27
|
+
*
|
|
28
|
+
* The driver stamp is the load-bearing half. Without it the re-scroll is itself
|
|
29
|
+
* an uncaused move and the run reports its own instrument; with it, the seconds
|
|
30
|
+
* that follow are a genuine "he is sitting still, high up, while content
|
|
31
|
+
* arrives" — which is the exact situation being complained about and the one a
|
|
32
|
+
* bottom-anchored recording can never observe.
|
|
33
|
+
*/
|
|
34
|
+
export async function scrollUpBy(page: Page, selector: string | null, px: number): Promise<void> {
|
|
35
|
+
// STAMP BEFORE, NOT ONLY AFTER. The scroll and the stamp are two round-trips
|
|
36
|
+
// to the page and the probe samples every 50ms, so a stamp that only follows
|
|
37
|
+
// the scroll can arrive after the move has already been sampled — measured 6
|
|
38
|
+
// Sept: 2 of 17 driver re-scrolls were reported as uncaused moves, i.e. the
|
|
39
|
+
// instrument filing its own action as the defect. Opening the grace window
|
|
40
|
+
// first closes the race in the safe direction.
|
|
41
|
+
await page.evaluate('window.__lensDriverActed && window.__lensDriverActed()').catch(() => {});
|
|
42
|
+
await page.evaluate(
|
|
43
|
+
([s, n]) => {
|
|
44
|
+
const el = s ? document.querySelector(s as string) : null;
|
|
45
|
+
if (el) (el as HTMLElement).scrollTop = Math.max(0, (el as HTMLElement).scrollTop - (n as number));
|
|
46
|
+
},
|
|
47
|
+
[selector, px] as const,
|
|
48
|
+
);
|
|
49
|
+
await page.evaluate('window.__lensDriverActed && window.__lensDriverActed()').catch(() => {});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** F312.21 — what the tab seeding did, or why it could not. Never inferred from
|
|
53
|
+
* a later click failing: "the tab was not there" and "the click missed" are
|
|
54
|
+
* different faults with different fixes. */
|
|
55
|
+
/**
|
|
56
|
+
* F312.17 — wait until the surface has stopped ARRIVING, not merely stopped
|
|
57
|
+
* growing for a moment. Three consecutive equal readings of height AND row
|
|
58
|
+
* count; a transcript renders a few rows, pauses while it fetches, then jumps,
|
|
59
|
+
* so one quiet moment passes for finished.
|
|
60
|
+
*
|
|
61
|
+
* Extracted because I reintroduced the very bug this fixes, one function over:
|
|
62
|
+
* F312.21's tab seeding reloads the page and waited a FIXED 2500ms, so a run
|
|
63
|
+
* began with 56 rows on screen and ended with 133. A fix that lives in one call
|
|
64
|
+
* site is a fix the next call site does not get.
|
|
65
|
+
*/
|
|
66
|
+
async function waitForSettled(page: Page, selector: string | null, rowAttr: string): Promise<void> {
|
|
67
|
+
let last = '';
|
|
68
|
+
let stable = 0;
|
|
69
|
+
const deadline = Date.now() + 25_000;
|
|
70
|
+
while (Date.now() < deadline) {
|
|
71
|
+
const sig = (await page
|
|
72
|
+
.evaluate(
|
|
73
|
+
([s2, ra]) => {
|
|
74
|
+
const el = s2 ? document.querySelector(s2 as string) : null;
|
|
75
|
+
if (!el) return '';
|
|
76
|
+
const e = el as HTMLElement;
|
|
77
|
+
return `${e.scrollHeight}:${e.querySelectorAll('[' + ra + ']').length}`;
|
|
78
|
+
},
|
|
79
|
+
[selector, rowAttr] as const,
|
|
80
|
+
)
|
|
81
|
+
.catch(() => '')) as string;
|
|
82
|
+
stable = sig !== '' && sig === last ? stable + 1 : 0;
|
|
83
|
+
last = sig;
|
|
84
|
+
if (stable >= 2) break;
|
|
85
|
+
await page.waitForTimeout(700);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type SeedResult = { ok: true; edge: string; tabs: string[] } | { ok: false; reason: string } | null;
|
|
90
|
+
|
|
91
|
+
export interface RecordOptions extends ProbeConfig, FindOptions {
|
|
92
|
+
url: string;
|
|
93
|
+
/**
|
|
94
|
+
* F312.10 — SCROLL UP BY THIS MANY PIXELS BEFORE WATCHING.
|
|
95
|
+
*
|
|
96
|
+
* The experiment was measuring the wrong situation. Parked at the live bottom,
|
|
97
|
+
* the browser keeps you pinned there as content arrives — every "move" tracks
|
|
98
|
+
* the growing maximum and nothing is wrong. His complaint is the opposite: he
|
|
99
|
+
* has scrolled UP to read, and THEN it moves. A recording that never leaves the
|
|
100
|
+
* bottom cannot show it.
|
|
101
|
+
*/
|
|
102
|
+
scrollUpPx?: number;
|
|
103
|
+
/**
|
|
104
|
+
* F312.21 — SEED THE OPEN TABS before measuring a tab switch.
|
|
105
|
+
*
|
|
106
|
+
* Lens runs a fresh browser profile, so the chat's tab strip is empty and there
|
|
107
|
+
* is nothing to switch BETWEEN. Measured: a three-step switch sequence reported
|
|
108
|
+
* `pressed: false` on its first press, and the frame confirmed the picker was
|
|
109
|
+
* not open — the experiment had simply never happened.
|
|
110
|
+
*
|
|
111
|
+
* The edge is NOT guessed. It is read back from the tab the app itself wrote
|
|
112
|
+
* once it had picked a session, so the seeded tabs carry the same edge the
|
|
113
|
+
* running app uses. A guessed edge produces a tab that renders and cannot be
|
|
114
|
+
* switched to, which is worse than no tab at all: it looks like the scenario ran.
|
|
115
|
+
*/
|
|
116
|
+
seedTabs?: { session: string; channel?: string; label?: string }[];
|
|
117
|
+
/** How long to sit and watch. */
|
|
118
|
+
seconds: number;
|
|
119
|
+
viewport?: { width: number; height: number };
|
|
120
|
+
engine?: 'chromium' | 'webkit' | 'firefox';
|
|
121
|
+
locale?: string;
|
|
122
|
+
timezone?: string;
|
|
123
|
+
reducedMotion?: 'reduce' | 'no-preference';
|
|
124
|
+
prepareContext?: (context: BrowserContext) => Promise<void>;
|
|
125
|
+
/** Optional — the point of this verb is that it works with none. Each entry is
|
|
126
|
+
* run before the watch begins, and each stamps the driver clock so its own
|
|
127
|
+
* scrolling is never reported as a defect. */
|
|
128
|
+
before?: (page: Page) => Promise<void>;
|
|
129
|
+
/** F312.5 — run WHILE the probe watches. This is how the typing case is
|
|
130
|
+
* reproduced: a browser sitting still cannot show a defect that only appears
|
|
131
|
+
* when the caret is in a field. Its keystrokes deliberately do NOT stamp the
|
|
132
|
+
* driver clock — typing is not a request to move the view, so a move during it
|
|
133
|
+
* must stay reportable. */
|
|
134
|
+
during?: (page: Page, seconds: number) => Promise<void>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface RecordResult {
|
|
138
|
+
video: Buffer | null;
|
|
139
|
+
durationMs: number;
|
|
140
|
+
/** The scroller the probe actually watched — never assumed, always reported, so
|
|
141
|
+
* "no findings" can be told apart from "watched the wrong element". */
|
|
142
|
+
watched: string | null;
|
|
143
|
+
samples: MotionSample[];
|
|
144
|
+
findings: MotionFinding[];
|
|
145
|
+
/** F312.12 — the page's own record of who moved the view, on the probe's clock.
|
|
146
|
+
* Empty on a surface that keeps no ledger. */
|
|
147
|
+
ledger: ScrollNote[] | null;
|
|
148
|
+
/** F312.16 — marker → ms after probe start when it first appeared on screen. */
|
|
149
|
+
markers: Record<string, number>;
|
|
150
|
+
/** UTC epoch ms at which the probe started, so a marker's arrival on the probe
|
|
151
|
+
* clock can be turned back into a wall-clock time and compared with when the
|
|
152
|
+
* message was actually sent. Without it `landed at 4213ms` cannot be paired
|
|
153
|
+
* with anything. */
|
|
154
|
+
probeStartedAt: number;
|
|
155
|
+
/**
|
|
156
|
+
* F312.18 — did the probe DEMONSTRABLY see a movement it was given on purpose?
|
|
157
|
+
*
|
|
158
|
+
* False means the run proves nothing: zero findings from a blind camera and
|
|
159
|
+
* zero from a fixed product are the same number, and only this tells them
|
|
160
|
+
* apart. Never inferred from the absence of findings — that is the very
|
|
161
|
+
* confusion it exists to remove.
|
|
162
|
+
*/
|
|
163
|
+
detectorAlive: boolean;
|
|
164
|
+
/** F312.21 — the outcome of seeding the open-tab set, or null when not asked. */
|
|
165
|
+
seed: SeedResult;
|
|
166
|
+
/** F312.18 — the working behind `detectorAlive`, so a false can be attributed
|
|
167
|
+
* instead of merely believed. */
|
|
168
|
+
selfTest: { from: number; sampledBefore3s: number; maxDeviation: number | null };
|
|
169
|
+
probeError: string | null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Open `url`, attach the motion probe, record for `seconds`, and return the video
|
|
174
|
+
* with a measured timeline.
|
|
175
|
+
*
|
|
176
|
+
* A MISSING RECORDING NEVER FAILS THE RUN (F255.5's rule, inherited on purpose):
|
|
177
|
+
* the probe data is the primary result, and it survives a video that did not
|
|
178
|
+
* finalise.
|
|
179
|
+
*/
|
|
180
|
+
export async function recordSurface(opts: RecordOptions): Promise<RecordResult> {
|
|
181
|
+
const videoDir = await mkdtemp(join(tmpdir(), 'lens-record-'));
|
|
182
|
+
let videoPage: Page | null = null;
|
|
183
|
+
const startedAt = Date.now();
|
|
184
|
+
let samples: MotionSample[] = [];
|
|
185
|
+
let ledger: ScrollNote[] | null = null;
|
|
186
|
+
let markers: Record<string, number> = {};
|
|
187
|
+
let probeStartedAt = 0;
|
|
188
|
+
let selfTestFrom = -1;
|
|
189
|
+
let seedResult: SeedResult = null;
|
|
190
|
+
let probeError: string | null = null;
|
|
191
|
+
let watched: string | null = null;
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
await withPage(
|
|
195
|
+
{
|
|
196
|
+
recordVideoDir: videoDir,
|
|
197
|
+
...(opts.viewport ? { viewport: opts.viewport } : {}),
|
|
198
|
+
...(opts.engine ? { engine: opts.engine } : {}),
|
|
199
|
+
...(opts.locale ? { locale: opts.locale } : {}),
|
|
200
|
+
...(opts.timezone ? { timezone: opts.timezone } : {}),
|
|
201
|
+
...(opts.reducedMotion ? { reducedMotion: opts.reducedMotion } : {}),
|
|
202
|
+
...(opts.prepareContext ? { prepareContext: opts.prepareContext } : {}),
|
|
203
|
+
},
|
|
204
|
+
async (page) => {
|
|
205
|
+
videoPage = page;
|
|
206
|
+
await page.goto(opts.url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
|
|
207
|
+
// Let the surface settle BEFORE the probe starts, or the first-load
|
|
208
|
+
// landing — which is not a defect — becomes finding number one.
|
|
209
|
+
//
|
|
210
|
+
// F312.17 — WAIT FOR IT TO STOP GROWING, don't guess a number. Measured 6
|
|
211
|
+
// Sept: a run began with 6 rows on screen and ended with 131, so the
|
|
212
|
+
// reading position was taken against a transcript that had not loaded and
|
|
213
|
+
// the whole recording measured the load instead of the defect. A fixed
|
|
214
|
+
// 2500ms is a guess about someone else's network; two consecutive equal
|
|
215
|
+
// heights is the actual question.
|
|
216
|
+
await page.waitForTimeout(1200);
|
|
217
|
+
await waitForSettled(page, opts.selector ?? null, opts.rowAttr ?? 'data-ev-uuid');
|
|
218
|
+
|
|
219
|
+
// F312.21 — seed the open-tab set, then reload so the strip renders it.
|
|
220
|
+
// The edge comes from the tab the APP wrote after it picked a session;
|
|
221
|
+
// guessing it would produce a tab that renders and cannot be switched to,
|
|
222
|
+
// which looks like the scenario ran.
|
|
223
|
+
if (opts.seedTabs && opts.seedTabs.length > 0) {
|
|
224
|
+
const seeded = await page
|
|
225
|
+
.evaluate((want) => {
|
|
226
|
+
const KEY = 'cardmem.chat.tabs';
|
|
227
|
+
let cur: { edge?: string; session?: string; channel?: string; label?: string }[] = [];
|
|
228
|
+
try {
|
|
229
|
+
const raw = localStorage.getItem(KEY);
|
|
230
|
+
if (raw) cur = JSON.parse(raw);
|
|
231
|
+
} catch {
|
|
232
|
+
cur = [];
|
|
233
|
+
}
|
|
234
|
+
const edge = Array.isArray(cur) && cur[0]?.edge ? String(cur[0].edge) : null;
|
|
235
|
+
if (!edge) return { ok: false, reason: 'no edge — the app has not picked a session yet' };
|
|
236
|
+
const have = new Set((cur || []).map((t) => String(t.channel ?? t.session)));
|
|
237
|
+
for (const w of want as { session: string; channel?: string; label?: string }[]) {
|
|
238
|
+
const channel = w.channel ?? w.session;
|
|
239
|
+
if (have.has(channel)) continue;
|
|
240
|
+
cur.push({ edge, session: w.session, channel, label: w.label ?? channel });
|
|
241
|
+
}
|
|
242
|
+
localStorage.setItem(KEY, JSON.stringify(cur));
|
|
243
|
+
return { ok: true, edge, tabs: cur.map((t) => String(t.channel ?? t.session)) };
|
|
244
|
+
}, opts.seedTabs)
|
|
245
|
+
.catch(() => ({ ok: false, reason: 'evaluate failed' }));
|
|
246
|
+
seedResult = seeded as SeedResult;
|
|
247
|
+
if ((seeded as { ok?: boolean }).ok) {
|
|
248
|
+
await page.reload({ waitUntil: 'domcontentloaded', timeout: 30_000 });
|
|
249
|
+
await page.waitForTimeout(1200);
|
|
250
|
+
await waitForSettled(page, opts.selector ?? null, opts.rowAttr ?? 'data-ev-uuid');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// F312.10 — take up the reading position BEFORE the probe starts, so the
|
|
255
|
+
// deliberate scroll is not itself the first finding.
|
|
256
|
+
if (opts.scrollUpPx && opts.scrollUpPx > 0) {
|
|
257
|
+
await scrollUpBy(page, opts.selector ?? null, opts.scrollUpPx);
|
|
258
|
+
await page.waitForTimeout(1200);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (opts.before) {
|
|
262
|
+
await opts.before(page);
|
|
263
|
+
// Whatever that did was OURS. Stamp it so the grace window covers it.
|
|
264
|
+
await page.evaluate('window.__lensDriverActed && window.__lensDriverActed()').catch(() => {});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const started = await page.evaluate(
|
|
268
|
+
probeSource({
|
|
269
|
+
...(opts.selector ? { selector: opts.selector } : {}),
|
|
270
|
+
...(opts.intervalMs ? { intervalMs: opts.intervalMs } : {}),
|
|
271
|
+
...(opts.rowAttr ? { rowAttr: opts.rowAttr } : {}),
|
|
272
|
+
}),
|
|
273
|
+
);
|
|
274
|
+
probeStartedAt = Date.now();
|
|
275
|
+
if (started === 'no-scroller') probeError = 'no scrollable element found';
|
|
276
|
+
|
|
277
|
+
// F312.18 — DOES THE DETECTOR STILL WORK? A deliberate, stamped nudge a
|
|
278
|
+
// moment after the probe starts, whose only job is to prove the camera is
|
|
279
|
+
// alive. Raised by the trail session from two failures of exactly this
|
|
280
|
+
// shape in their own day: a coverage metric that only counted the chunks
|
|
281
|
+
// that EXISTED, and an indexing job whose `WHERE enabled = 1` selected
|
|
282
|
+
// zero rows — so it neither failed nor reported. Both read as success.
|
|
283
|
+
//
|
|
284
|
+
// The same hole was open here. `exercised` answers "did the surface
|
|
285
|
+
// change"; nothing answered "would this probe have SEEN a change". A run
|
|
286
|
+
// whose probe never attached, whose selector matched a static element, or
|
|
287
|
+
// whose sampling died reports zero findings — identical to a fixed
|
|
288
|
+
// product. The nudge is classified `driver`, so it is never a finding;
|
|
289
|
+
// its absence from the samples is the alarm.
|
|
290
|
+
await page.waitForTimeout(600);
|
|
291
|
+
const before = (await page
|
|
292
|
+
.evaluate((s2) => {
|
|
293
|
+
const el = s2 ? document.querySelector(s2 as string) : null;
|
|
294
|
+
return el ? (el as HTMLElement).scrollTop : -1;
|
|
295
|
+
}, opts.selector ?? null)
|
|
296
|
+
.catch(() => -1)) as number;
|
|
297
|
+
await scrollUpBy(page, opts.selector ?? null, 140);
|
|
298
|
+
await page.waitForTimeout(250);
|
|
299
|
+
await scrollUpBy(page, opts.selector ?? null, -140);
|
|
300
|
+
await page.waitForTimeout(250);
|
|
301
|
+
selfTestFrom = before;
|
|
302
|
+
|
|
303
|
+
const secs = Math.max(1, opts.seconds);
|
|
304
|
+
if (opts.during) await opts.during(page, secs);
|
|
305
|
+
else await page.waitForTimeout(secs * 1000);
|
|
306
|
+
|
|
307
|
+
const read = (await page.evaluate(PROBE_READ)) as {
|
|
308
|
+
error?: string | null;
|
|
309
|
+
selector?: string | null;
|
|
310
|
+
samples?: MotionSample[];
|
|
311
|
+
ledger?: ScrollNote[] | null;
|
|
312
|
+
markers?: Record<string, number>;
|
|
313
|
+
};
|
|
314
|
+
samples = read?.samples ?? [];
|
|
315
|
+
ledger = read?.ledger ?? null;
|
|
316
|
+
markers = read?.markers ?? {};
|
|
317
|
+
watched = read?.selector ?? null;
|
|
318
|
+
if (read?.error) probeError = read.error;
|
|
319
|
+
},
|
|
320
|
+
);
|
|
321
|
+
} finally {
|
|
322
|
+
// Playwright only finalises the .webm when the context closes, so this read
|
|
323
|
+
// has to happen AFTER withPage resolves.
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
let video: Buffer | null = null;
|
|
327
|
+
try {
|
|
328
|
+
// `saveAs`, NOT `path()`. Measured: path() resolves immediately and the file
|
|
329
|
+
// is still 0 bytes — Playwright flushes the webm asynchronously after the
|
|
330
|
+
// context closes, so reading the path is a race that loses quietly. saveAs
|
|
331
|
+
// WAITS for the write, which is the difference between a video and an empty
|
|
332
|
+
// file that ffmpeg then rejects with an unhelpful exit code.
|
|
333
|
+
const out = join(videoDir, 'run.webm');
|
|
334
|
+
const v = (videoPage as Page | null)?.video();
|
|
335
|
+
if (v) {
|
|
336
|
+
await v.saveAs(out);
|
|
337
|
+
video = await readFile(out);
|
|
338
|
+
}
|
|
339
|
+
} catch {
|
|
340
|
+
video = null; // a missing recording must never fail an otherwise-good run
|
|
341
|
+
} finally {
|
|
342
|
+
await rm(videoDir, { recursive: true, force: true }).catch(() => {});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const findings = rankFindings(
|
|
346
|
+
findUncausedMoves(samples, {
|
|
347
|
+
...(opts.minDeltaPx != null ? { minDeltaPx: opts.minDeltaPx } : {}),
|
|
348
|
+
...(opts.inputGraceMs != null ? { inputGraceMs: opts.inputGraceMs } : {}),
|
|
349
|
+
...(opts.driverGraceMs != null ? { driverGraceMs: opts.driverGraceMs } : {}),
|
|
350
|
+
}),
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
video,
|
|
355
|
+
durationMs: Date.now() - startedAt,
|
|
356
|
+
watched,
|
|
357
|
+
samples,
|
|
358
|
+
findings,
|
|
359
|
+
ledger,
|
|
360
|
+
markers,
|
|
361
|
+
probeStartedAt,
|
|
362
|
+
// The nudge moved the position by 140px and back within the first second, so
|
|
363
|
+
// a probe that was watching MUST have sampled something other than its
|
|
364
|
+
// starting point. Judged on the samples, not on whether the scroll call
|
|
365
|
+
// resolved — a resolved call proves the message was sent, not that anyone
|
|
366
|
+
// was listening.
|
|
367
|
+
seed: seedResult,
|
|
368
|
+
detectorAlive:
|
|
369
|
+
selfTestFrom >= 0 && samples.some((x) => x.t < 3000 && Math.abs(x.top - selfTestFrom) > 40),
|
|
370
|
+
// F312.18 — WHY it says what it says. A bare false is the same shape as the
|
|
371
|
+
// silence it exists to detect: it could mean the probe was blind, or that the
|
|
372
|
+
// nudge never moved anything, or that the position could not be read at all.
|
|
373
|
+
// Measured twice today, and I could not tell those apart from the field alone
|
|
374
|
+
// — so the working is reported beside the verdict.
|
|
375
|
+
selfTest: {
|
|
376
|
+
from: selfTestFrom,
|
|
377
|
+
sampledBefore3s: samples.filter((x) => x.t < 3000).length,
|
|
378
|
+
maxDeviation:
|
|
379
|
+
selfTestFrom < 0
|
|
380
|
+
? null
|
|
381
|
+
: samples
|
|
382
|
+
.filter((x) => x.t < 3000)
|
|
383
|
+
.reduce((m, x) => Math.max(m, Math.abs(x.top - selfTestFrom)), 0),
|
|
384
|
+
},
|
|
385
|
+
probeError,
|
|
386
|
+
};
|
|
387
|
+
}
|
package/src/sandbox.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// lens F001.5 — the daemon's sandbox, INJECTED, never copied.
|
|
2
|
+
//
|
|
3
|
+
// resolveSandboxedPath is the cardmem daemon's security boundary (it also guards
|
|
4
|
+
// non-Lens routes there). A second copy inside this package would be two
|
|
5
|
+
// security checks that can drift apart, so the package holds no implementation:
|
|
6
|
+
// the daemon hands its own function in through LensDeps when it mounts the
|
|
7
|
+
// router. Until then every call refuses — fail closed, never "allow".
|
|
8
|
+
|
|
9
|
+
export type SandboxResolver = (path: string, sandboxRoots: string[]) => string;
|
|
10
|
+
|
|
11
|
+
let resolver: SandboxResolver | null = null;
|
|
12
|
+
|
|
13
|
+
export function setSandboxResolver(fn: SandboxResolver): void {
|
|
14
|
+
resolver = fn;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function resolveSandboxedPath(path: string, sandboxRoots: string[]): string {
|
|
18
|
+
if (!resolver) {
|
|
19
|
+
throw new Error('lens-daemon: no sandbox resolver installed — the daemon must pass resolveSandboxedPath in LensDeps');
|
|
20
|
+
}
|
|
21
|
+
return resolver(path, sandboxRoots);
|
|
22
|
+
}
|