@reconcrap/boss-recommend-mcp 2.0.45 → 2.0.47

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 (56) hide show
  1. package/bin/boss-recommend-mcp.js +4 -4
  2. package/config/screening-config.example.json +27 -27
  3. package/package.json +1 -1
  4. package/scripts/postinstall.cjs +44 -44
  5. package/skills/boss-chat/README.md +39 -39
  6. package/skills/boss-chat/SKILL.md +93 -93
  7. package/skills/boss-recommend-pipeline/README.md +12 -12
  8. package/skills/boss-recommend-pipeline/SKILL.md +180 -180
  9. package/skills/boss-recruit-pipeline/README.md +17 -17
  10. package/skills/boss-recruit-pipeline/SKILL.md +58 -58
  11. package/src/chat-mcp.js +1780 -1780
  12. package/src/chat-runtime-config.js +749 -749
  13. package/src/cli.js +3054 -3054
  14. package/src/core/boss-cards/index.js +199 -199
  15. package/src/core/browser/index.js +1453 -1446
  16. package/src/core/capture/index.js +1201 -1201
  17. package/src/core/cv-acquisition/index.js +238 -238
  18. package/src/core/cv-capture-target/index.js +299 -299
  19. package/src/core/greet-quota/index.js +54 -54
  20. package/src/core/infinite-list/index.js +1326 -1326
  21. package/src/core/reporting/legacy-csv.js +341 -341
  22. package/src/core/run/timing.js +33 -33
  23. package/src/core/screening/index.js +50 -3
  24. package/src/core/self-heal/index.js +973 -973
  25. package/src/core/self-heal/viewport.js +564 -564
  26. package/src/domains/chat/cards.js +137 -137
  27. package/src/domains/chat/constants.js +221 -221
  28. package/src/domains/chat/detail.js +1668 -1661
  29. package/src/domains/chat/index.js +7 -7
  30. package/src/domains/chat/jobs.js +592 -588
  31. package/src/domains/chat/page-guard.js +98 -98
  32. package/src/domains/chat/roots.js +56 -56
  33. package/src/domains/chat/run-service.js +1977 -1955
  34. package/src/domains/recommend/actions.js +457 -457
  35. package/src/domains/recommend/cards.js +243 -243
  36. package/src/domains/recommend/constants.js +165 -165
  37. package/src/domains/recommend/detail.js +36 -28
  38. package/src/domains/recommend/filters.js +610 -581
  39. package/src/domains/recommend/index.js +10 -10
  40. package/src/domains/recommend/jobs.js +316 -263
  41. package/src/domains/recommend/refresh.js +472 -472
  42. package/src/domains/recommend/roots.js +80 -80
  43. package/src/domains/recommend/run-service.js +75 -35
  44. package/src/domains/recommend/scopes.js +246 -245
  45. package/src/domains/recruit/actions.js +277 -277
  46. package/src/domains/recruit/cards.js +74 -74
  47. package/src/domains/recruit/constants.js +167 -167
  48. package/src/domains/recruit/detail.js +461 -460
  49. package/src/domains/recruit/index.js +9 -9
  50. package/src/domains/recruit/instruction-parser.js +451 -451
  51. package/src/domains/recruit/refresh.js +44 -44
  52. package/src/domains/recruit/roots.js +68 -68
  53. package/src/domains/recruit/run-service.js +1207 -1161
  54. package/src/domains/recruit/search.js +1202 -1149
  55. package/src/recommend-mcp.js +22 -22
  56. package/src/recruit-mcp.js +1338 -1338
@@ -1,564 +1,564 @@
1
- import {
2
- getNodeBox,
3
- querySelector,
4
- sleep
5
- } from "../browser/index.js";
6
-
7
- export const VIEWPORT_COLLAPSE_RATIO_THRESHOLD = 0.6;
8
- export const VIEWPORT_COLLAPSE_MIN_EXPECTED_WIDTH = 1000;
9
- export const VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO = 0.85;
10
-
11
- const ABSOLUTE_COLLAPSE_LIMITS = Object.freeze({
12
- clientHeight: 260,
13
- clientWidth: 280,
14
- frameHeight: 320,
15
- frameWidth: 460,
16
- viewportHeight: 260,
17
- viewportWidth: 360
18
- });
19
-
20
- function normalizeText(value) {
21
- return String(value ?? "").replace(/\s+/g, " ").trim();
22
- }
23
-
24
- function getPositiveNumber(...values) {
25
- for (const value of values) {
26
- const number = Number(value);
27
- if (Number.isFinite(number) && number > 0) return number;
28
- }
29
- return 0;
30
- }
31
-
32
- function rootNodeId(roots = {}, name) {
33
- const root = roots[name];
34
- if (typeof root === "number") return root;
35
- if (root?.nodeId) return root.nodeId;
36
- if (root?.documentNodeId) return root.documentNodeId;
37
- return 0;
38
- }
39
-
40
- function compactRect(rect = {}) {
41
- return {
42
- width: getPositiveNumber(rect.width),
43
- height: getPositiveNumber(rect.height)
44
- };
45
- }
46
-
47
- function pickViewportSize(layoutMetrics = {}, axis = "width") {
48
- const clientKey = axis === "width" ? "clientWidth" : "clientHeight";
49
- return getPositiveNumber(
50
- layoutMetrics?.cssVisualViewport?.[clientKey],
51
- layoutMetrics?.cssLayoutViewport?.[clientKey],
52
- layoutMetrics?.visualViewport?.[clientKey],
53
- layoutMetrics?.layoutViewport?.[clientKey]
54
- );
55
- }
56
-
57
- async function getLayoutMetrics(client) {
58
- if (typeof client?.Page?.getLayoutMetrics !== "function") return null;
59
- try {
60
- return await client.Page.getLayoutMetrics();
61
- } catch {
62
- return null;
63
- }
64
- }
65
-
66
- export async function getCurrentWindowInfo(client) {
67
- if (typeof client?.Browser?.getWindowForTarget !== "function") {
68
- return {
69
- ok: false,
70
- unsupported: true,
71
- error: "Browser.getWindowForTarget is not available"
72
- };
73
- }
74
-
75
- try {
76
- const targetWindow = await client.Browser.getWindowForTarget({});
77
- let bounds = targetWindow?.bounds || null;
78
- if (
79
- targetWindow?.windowId
80
- && typeof client?.Browser?.getWindowBounds === "function"
81
- ) {
82
- const currentBounds = await client.Browser.getWindowBounds({
83
- windowId: targetWindow.windowId
84
- });
85
- bounds = currentBounds?.bounds || bounds;
86
- }
87
- return {
88
- ok: true,
89
- windowId: targetWindow?.windowId || null,
90
- bounds
91
- };
92
- } catch (error) {
93
- return {
94
- ok: false,
95
- error: error?.message || String(error)
96
- };
97
- }
98
- }
99
-
100
- async function readBox(client, nodeId) {
101
- if (!nodeId) return null;
102
- try {
103
- return await getNodeBox(client, nodeId);
104
- } catch {
105
- return null;
106
- }
107
- }
108
-
109
- async function readBestContentBox(client, rootNodeIdValue) {
110
- const directBox = await readBox(client, rootNodeIdValue);
111
- if (directBox?.rect?.width && directBox?.rect?.height) return directBox;
112
-
113
- for (const selector of ["body", "html"]) {
114
- const nodeId = await querySelector(client, rootNodeIdValue, selector).catch(() => 0);
115
- const box = await readBox(client, nodeId);
116
- if (box?.rect?.width && box?.rect?.height) return box;
117
- }
118
- return directBox;
119
- }
120
-
121
- export function buildViewportHealthDiagnostics(state, windowInfo = null, layoutMetrics = null) {
122
- const topViewport = state?.topViewport || {};
123
- const bounds = windowInfo?.bounds || null;
124
- const windowState = normalizeText(bounds?.windowState || "").toLowerCase() || null;
125
- const windowWidth = getPositiveNumber(bounds?.width);
126
- const screenAvailWidth = getPositiveNumber(topViewport.screenAvailWidth);
127
- const topOuterWidth = getPositiveNumber(topViewport.outerWidth);
128
- const actualWidth = getPositiveNumber(
129
- layoutMetrics?.cssVisualViewport?.clientWidth,
130
- layoutMetrics?.cssLayoutViewport?.clientWidth,
131
- topViewport.visualWidth,
132
- topViewport.innerWidth,
133
- state?.viewport?.width,
134
- state?.clientWidth,
135
- state?.frameRect?.width
136
- );
137
- const actualHeight = getPositiveNumber(
138
- layoutMetrics?.cssVisualViewport?.clientHeight,
139
- layoutMetrics?.cssLayoutViewport?.clientHeight,
140
- topViewport.visualHeight,
141
- topViewport.innerHeight,
142
- state?.viewport?.height,
143
- state?.clientHeight,
144
- state?.frameRect?.height
145
- );
146
- const hasScreenWidth = screenAvailWidth > 0;
147
- const nearFullscreen = Boolean(
148
- windowState === "maximized"
149
- || (
150
- windowWidth > 0
151
- && hasScreenWidth
152
- && windowWidth >= screenAvailWidth * VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO
153
- )
154
- || (
155
- topOuterWidth > 0
156
- && hasScreenWidth
157
- && topOuterWidth >= screenAvailWidth * VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO
158
- )
159
- );
160
- const fallbackExpectedWidth = getPositiveNumber(screenAvailWidth, windowWidth, topOuterWidth);
161
- let expectedWidth = 0;
162
- if (windowWidth > 0) {
163
- expectedWidth = hasScreenWidth && windowWidth >= screenAvailWidth * VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO
164
- ? Math.min(windowWidth, screenAvailWidth)
165
- : windowWidth;
166
- } else if (topOuterWidth > 0) {
167
- expectedWidth = hasScreenWidth && topOuterWidth >= screenAvailWidth * VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO
168
- ? Math.min(topOuterWidth, screenAvailWidth)
169
- : topOuterWidth;
170
- } else {
171
- expectedWidth = fallbackExpectedWidth;
172
- }
173
- const widthRatio = actualWidth > 0 && expectedWidth > 0
174
- ? actualWidth / expectedWidth
175
- : null;
176
- const relativeCollapsed = Boolean(
177
- nearFullscreen
178
- && expectedWidth >= VIEWPORT_COLLAPSE_MIN_EXPECTED_WIDTH
179
- && actualWidth > 0
180
- && widthRatio !== null
181
- && widthRatio <= VIEWPORT_COLLAPSE_RATIO_THRESHOLD
182
- );
183
-
184
- return {
185
- threshold: VIEWPORT_COLLAPSE_RATIO_THRESHOLD,
186
- minExpectedWidth: VIEWPORT_COLLAPSE_MIN_EXPECTED_WIDTH,
187
- nearFullscreen,
188
- windowState,
189
- windowWidth,
190
- screenAvailWidth,
191
- topOuterWidth,
192
- actualWidth,
193
- actualHeight,
194
- expectedWidth,
195
- widthRatio,
196
- relativeCollapsed
197
- };
198
- }
199
-
200
- export function isListViewportCollapsed(state) {
201
- if (!state?.ok) return false;
202
- if (state.viewportDiagnostics?.relativeCollapsed === true) return true;
203
- const clientHeight = Number(state.clientHeight || 0);
204
- const clientWidth = Number(state.clientWidth || 0);
205
- const frameWidth = Number(state.frameRect?.width || 0);
206
- const frameHeight = Number(state.frameRect?.height || 0);
207
- const viewportWidth = Number(state.viewport?.width || 0);
208
- const viewportHeight = Number(state.viewport?.height || 0);
209
-
210
- return (
211
- (clientHeight > 0 && clientHeight < ABSOLUTE_COLLAPSE_LIMITS.clientHeight)
212
- || (clientWidth > 0 && clientWidth < ABSOLUTE_COLLAPSE_LIMITS.clientWidth)
213
- || (frameHeight > 0 && frameHeight < ABSOLUTE_COLLAPSE_LIMITS.frameHeight)
214
- || (frameWidth > 0 && frameWidth < ABSOLUTE_COLLAPSE_LIMITS.frameWidth)
215
- || (viewportHeight > 0 && viewportHeight < ABSOLUTE_COLLAPSE_LIMITS.viewportHeight)
216
- || (viewportWidth > 0 && viewportWidth < ABSOLUTE_COLLAPSE_LIMITS.viewportWidth)
217
- );
218
- }
219
-
220
- export async function readViewportState(client, {
221
- roots = {},
222
- root = "frame",
223
- frameOwnerRoot = "frameOwner"
224
- } = {}) {
225
- const targetRootNodeId = rootNodeId(roots, root);
226
- if (!targetRootNodeId) {
227
- return {
228
- ok: false,
229
- root,
230
- error: `Root not found: ${root}`
231
- };
232
- }
233
-
234
- const layoutMetrics = await getLayoutMetrics(client);
235
- const windowInfo = await getCurrentWindowInfo(client);
236
- const contentBox = await readBestContentBox(client, targetRootNodeId);
237
- const ownerNodeId = rootNodeId(roots, frameOwnerRoot);
238
- const ownerBox = ownerNodeId ? await readBox(client, ownerNodeId) : null;
239
- const frameRect = compactRect(ownerBox?.rect || contentBox?.rect || {});
240
- const clientWidth = getPositiveNumber(
241
- contentBox?.rect?.width,
242
- frameRect.width,
243
- pickViewportSize(layoutMetrics, "width")
244
- );
245
- const clientHeight = getPositiveNumber(
246
- contentBox?.rect?.height,
247
- frameRect.height,
248
- pickViewportSize(layoutMetrics, "height")
249
- );
250
- const viewportWidth = pickViewportSize(layoutMetrics, "width") || clientWidth;
251
- const viewportHeight = pickViewportSize(layoutMetrics, "height") || clientHeight;
252
- const bounds = windowInfo?.bounds || {};
253
- const topViewport = {
254
- innerWidth: viewportWidth,
255
- innerHeight: viewportHeight,
256
- outerWidth: getPositiveNumber(bounds.width, viewportWidth),
257
- outerHeight: getPositiveNumber(bounds.height, viewportHeight),
258
- visualWidth: getPositiveNumber(layoutMetrics?.cssVisualViewport?.clientWidth, viewportWidth),
259
- visualHeight: getPositiveNumber(layoutMetrics?.cssVisualViewport?.clientHeight, viewportHeight),
260
- screenAvailWidth: getPositiveNumber(bounds.width),
261
- screenAvailHeight: getPositiveNumber(bounds.height),
262
- devicePixelRatio: getPositiveNumber(layoutMetrics?.cssVisualViewport?.scale, 1)
263
- };
264
- const state = {
265
- ok: true,
266
- root,
267
- rootNodeId: targetRootNodeId,
268
- frameOwnerRoot,
269
- frameOwnerNodeId: ownerNodeId || null,
270
- clientWidth,
271
- clientHeight,
272
- frameRect,
273
- viewport: {
274
- width: viewportWidth,
275
- height: viewportHeight
276
- },
277
- topViewport,
278
- windowInfo
279
- };
280
- state.viewportDiagnostics = buildViewportHealthDiagnostics(state, windowInfo, layoutMetrics);
281
- state.collapsed = isListViewportCollapsed(state);
282
- return state;
283
- }
284
-
285
- export async function setWindowStateIfPossible(client, windowState, reason = "viewport_recovery") {
286
- const windowInfo = await getCurrentWindowInfo(client);
287
- if (!windowInfo.ok || !windowInfo.windowId || typeof client?.Browser?.setWindowBounds !== "function") {
288
- return {
289
- ok: false,
290
- reason,
291
- windowState,
292
- error: windowInfo.error || "Browser.setWindowBounds is not available"
293
- };
294
- }
295
-
296
- try {
297
- await client.Browser.setWindowBounds({
298
- windowId: windowInfo.windowId,
299
- bounds: {
300
- windowState
301
- }
302
- });
303
- return {
304
- ok: true,
305
- reason,
306
- windowState,
307
- windowId: windowInfo.windowId,
308
- before: windowInfo.bounds || null
309
- };
310
- } catch (error) {
311
- return {
312
- ok: false,
313
- reason,
314
- windowState,
315
- windowId: windowInfo.windowId,
316
- error: error?.message || String(error)
317
- };
318
- }
319
- }
320
-
321
- export async function toggleWindowStateForViewportRecovery(client, {
322
- reason = "viewport_recovery",
323
- settleMs = 520,
324
- bringToFront = true
325
- } = {}) {
326
- const currentInfo = await getCurrentWindowInfo(client);
327
- const currentState = normalizeText(currentInfo?.bounds?.windowState || "").toLowerCase();
328
- const sequence = currentState === "normal"
329
- ? ["maximized", "normal"]
330
- : ["normal", "maximized"];
331
- const attempts = [];
332
-
333
- for (const windowState of sequence) {
334
- const attempt = await setWindowStateIfPossible(client, windowState, reason);
335
- attempts.push(attempt);
336
- if (attempt.ok && settleMs > 0) await sleep(settleMs);
337
- }
338
-
339
- if (bringToFront && typeof client?.Page?.bringToFront === "function") {
340
- await client.Page.bringToFront();
341
- }
342
-
343
- return {
344
- ok: attempts.some((attempt) => attempt.ok),
345
- applied: attempts.some((attempt) => attempt.ok),
346
- reason,
347
- current_state: currentState || null,
348
- sequence,
349
- attempts
350
- };
351
- }
352
-
353
- export function compactViewportState(state = null) {
354
- if (!state) return null;
355
- return {
356
- ok: Boolean(state.ok),
357
- root: state.root || null,
358
- error: state.error || null,
359
- clientWidth: state.clientWidth || 0,
360
- clientHeight: state.clientHeight || 0,
361
- frameRect: state.frameRect || null,
362
- viewport: state.viewport || null,
363
- topViewport: state.topViewport
364
- ? {
365
- innerWidth: state.topViewport.innerWidth || 0,
366
- innerHeight: state.topViewport.innerHeight || 0,
367
- outerWidth: state.topViewport.outerWidth || 0,
368
- outerHeight: state.topViewport.outerHeight || 0,
369
- visualWidth: state.topViewport.visualWidth || 0,
370
- visualHeight: state.topViewport.visualHeight || 0,
371
- screenAvailWidth: state.topViewport.screenAvailWidth || 0,
372
- screenAvailHeight: state.topViewport.screenAvailHeight || 0,
373
- devicePixelRatio: state.topViewport.devicePixelRatio || 0
374
- }
375
- : null,
376
- viewportDiagnostics: state.viewportDiagnostics || null,
377
- collapsed: Boolean(state.collapsed)
378
- };
379
- }
380
-
381
- export function compactViewportHealthResult(result = null) {
382
- if (!result) return null;
383
- return {
384
- ok: Boolean(result.ok),
385
- collapsed: Boolean(result.collapsed),
386
- recovered: Boolean(result.recovered),
387
- reason: result.reason || null,
388
- state: compactViewportState(result.state),
389
- before: compactViewportState(result.before),
390
- repair: result.repair
391
- ? {
392
- ok: Boolean(result.repair.ok),
393
- applied: Boolean(result.repair.applied),
394
- current_state: result.repair.current_state || null,
395
- sequence: result.repair.sequence || [],
396
- attempts: (result.repair.attempts || []).map((attempt) => ({
397
- ok: Boolean(attempt.ok),
398
- windowState: attempt.windowState,
399
- error: attempt.error || null
400
- }))
401
- }
402
- : null,
403
- error: result.error || null
404
- };
405
- }
406
-
407
- export async function ensureHealthyViewport(client, {
408
- roots = {},
409
- root = "frame",
410
- frameOwnerRoot = "frameOwner",
411
- reason = "viewport_recovery",
412
- repair = true,
413
- recoveryDelayMs = 900
414
- } = {}) {
415
- const before = await readViewportState(client, {
416
- roots,
417
- root,
418
- frameOwnerRoot
419
- });
420
- if (!before.ok) {
421
- return {
422
- ok: false,
423
- collapsed: false,
424
- recovered: false,
425
- reason,
426
- state: before,
427
- error: before.error || "viewport state could not be read"
428
- };
429
- }
430
-
431
- if (!isListViewportCollapsed(before)) {
432
- return {
433
- ok: true,
434
- collapsed: false,
435
- recovered: false,
436
- reason,
437
- state: before
438
- };
439
- }
440
-
441
- if (!repair) {
442
- return {
443
- ok: false,
444
- collapsed: true,
445
- recovered: false,
446
- reason,
447
- before,
448
- state: before,
449
- error: "viewport collapsed and repair disabled"
450
- };
451
- }
452
-
453
- const repairResult = await toggleWindowStateForViewportRecovery(client, { reason });
454
- if (recoveryDelayMs > 0) await sleep(recoveryDelayMs);
455
- const after = await readViewportState(client, {
456
- roots,
457
- root,
458
- frameOwnerRoot
459
- });
460
- const stillCollapsed = isListViewportCollapsed(after);
461
- return {
462
- ok: after.ok && !stillCollapsed,
463
- collapsed: stillCollapsed,
464
- recovered: after.ok && !stillCollapsed && repairResult.applied,
465
- reason,
466
- before,
467
- state: after,
468
- repair: repairResult,
469
- error: after.ok && !stillCollapsed
470
- ? null
471
- : "viewport collapsed after recovery attempt"
472
- };
473
- }
474
-
475
- export function createViewportRunGuard({
476
- client,
477
- domain = "boss",
478
- root = "frame",
479
- frameOwnerRoot = "frameOwner",
480
- runControl = null,
481
- getRoots = null,
482
- rootNodesFromState = (rootState) => rootState?.rootNodes || rootState?.roots || rootState || {},
483
- repair = true,
484
- maxEvents = 10
485
- } = {}) {
486
- if (!client) throw new Error("createViewportRunGuard requires a guarded CDP client");
487
- const events = [];
488
- const stats = {
489
- checks: 0,
490
- recoveries: 0,
491
- failures: 0
492
- };
493
-
494
- function recordEvent(phase, health) {
495
- const compact = compactViewportHealthResult(health);
496
- const shouldRecord = Boolean(health?.recovered || !health?.ok || health?.collapsed);
497
- if (!shouldRecord) return compact;
498
- const event = {
499
- phase,
500
- at: new Date().toISOString(),
501
- ...compact
502
- };
503
- events.push(event);
504
- if (events.length > maxEvents) events.shift();
505
- if (runControl) {
506
- runControl.checkpoint({
507
- viewport_health: event,
508
- viewport_health_events: events.slice(),
509
- viewport_health_stats: { ...stats }
510
- });
511
- }
512
- return compact;
513
- }
514
-
515
- async function ensure(rootState, {
516
- phase = "run",
517
- reason = `${domain}:${phase}`
518
- } = {}) {
519
- let currentRootState = rootState;
520
- if (!currentRootState && typeof getRoots === "function") {
521
- currentRootState = await getRoots(client);
522
- }
523
- const roots = rootNodesFromState(currentRootState);
524
- stats.checks += 1;
525
- const health = await ensureHealthyViewport(client, {
526
- roots,
527
- root,
528
- frameOwnerRoot,
529
- reason,
530
- repair
531
- });
532
- if (health.recovered) stats.recoveries += 1;
533
- if (!health.ok) stats.failures += 1;
534
- const compact = recordEvent(phase, health);
535
- if (!health.ok) {
536
- const error = new Error(`${String(domain).toUpperCase()}_LIST_VIEWPORT_COLLAPSED`);
537
- error.code = "LIST_VIEWPORT_COLLAPSED";
538
- error.domain = domain;
539
- error.phase = phase;
540
- error.viewport_health = compact;
541
- throw error;
542
- }
543
- if (health.recovered && typeof getRoots === "function") {
544
- currentRootState = await getRoots(client);
545
- }
546
- return {
547
- rootState: currentRootState,
548
- health,
549
- compact,
550
- stats: { ...stats },
551
- events: events.slice()
552
- };
553
- }
554
-
555
- return {
556
- ensure,
557
- getStats() {
558
- return { ...stats };
559
- },
560
- getEvents() {
561
- return events.slice();
562
- }
563
- };
564
- }
1
+ import {
2
+ getNodeBox,
3
+ querySelector,
4
+ sleep
5
+ } from "../browser/index.js";
6
+
7
+ export const VIEWPORT_COLLAPSE_RATIO_THRESHOLD = 0.6;
8
+ export const VIEWPORT_COLLAPSE_MIN_EXPECTED_WIDTH = 1000;
9
+ export const VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO = 0.85;
10
+
11
+ const ABSOLUTE_COLLAPSE_LIMITS = Object.freeze({
12
+ clientHeight: 260,
13
+ clientWidth: 280,
14
+ frameHeight: 320,
15
+ frameWidth: 460,
16
+ viewportHeight: 260,
17
+ viewportWidth: 360
18
+ });
19
+
20
+ function normalizeText(value) {
21
+ return String(value ?? "").replace(/\s+/g, " ").trim();
22
+ }
23
+
24
+ function getPositiveNumber(...values) {
25
+ for (const value of values) {
26
+ const number = Number(value);
27
+ if (Number.isFinite(number) && number > 0) return number;
28
+ }
29
+ return 0;
30
+ }
31
+
32
+ function rootNodeId(roots = {}, name) {
33
+ const root = roots[name];
34
+ if (typeof root === "number") return root;
35
+ if (root?.nodeId) return root.nodeId;
36
+ if (root?.documentNodeId) return root.documentNodeId;
37
+ return 0;
38
+ }
39
+
40
+ function compactRect(rect = {}) {
41
+ return {
42
+ width: getPositiveNumber(rect.width),
43
+ height: getPositiveNumber(rect.height)
44
+ };
45
+ }
46
+
47
+ function pickViewportSize(layoutMetrics = {}, axis = "width") {
48
+ const clientKey = axis === "width" ? "clientWidth" : "clientHeight";
49
+ return getPositiveNumber(
50
+ layoutMetrics?.cssVisualViewport?.[clientKey],
51
+ layoutMetrics?.cssLayoutViewport?.[clientKey],
52
+ layoutMetrics?.visualViewport?.[clientKey],
53
+ layoutMetrics?.layoutViewport?.[clientKey]
54
+ );
55
+ }
56
+
57
+ async function getLayoutMetrics(client) {
58
+ if (typeof client?.Page?.getLayoutMetrics !== "function") return null;
59
+ try {
60
+ return await client.Page.getLayoutMetrics();
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+
66
+ export async function getCurrentWindowInfo(client) {
67
+ if (typeof client?.Browser?.getWindowForTarget !== "function") {
68
+ return {
69
+ ok: false,
70
+ unsupported: true,
71
+ error: "Browser.getWindowForTarget is not available"
72
+ };
73
+ }
74
+
75
+ try {
76
+ const targetWindow = await client.Browser.getWindowForTarget({});
77
+ let bounds = targetWindow?.bounds || null;
78
+ if (
79
+ targetWindow?.windowId
80
+ && typeof client?.Browser?.getWindowBounds === "function"
81
+ ) {
82
+ const currentBounds = await client.Browser.getWindowBounds({
83
+ windowId: targetWindow.windowId
84
+ });
85
+ bounds = currentBounds?.bounds || bounds;
86
+ }
87
+ return {
88
+ ok: true,
89
+ windowId: targetWindow?.windowId || null,
90
+ bounds
91
+ };
92
+ } catch (error) {
93
+ return {
94
+ ok: false,
95
+ error: error?.message || String(error)
96
+ };
97
+ }
98
+ }
99
+
100
+ async function readBox(client, nodeId) {
101
+ if (!nodeId) return null;
102
+ try {
103
+ return await getNodeBox(client, nodeId);
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ async function readBestContentBox(client, rootNodeIdValue) {
110
+ const directBox = await readBox(client, rootNodeIdValue);
111
+ if (directBox?.rect?.width && directBox?.rect?.height) return directBox;
112
+
113
+ for (const selector of ["body", "html"]) {
114
+ const nodeId = await querySelector(client, rootNodeIdValue, selector).catch(() => 0);
115
+ const box = await readBox(client, nodeId);
116
+ if (box?.rect?.width && box?.rect?.height) return box;
117
+ }
118
+ return directBox;
119
+ }
120
+
121
+ export function buildViewportHealthDiagnostics(state, windowInfo = null, layoutMetrics = null) {
122
+ const topViewport = state?.topViewport || {};
123
+ const bounds = windowInfo?.bounds || null;
124
+ const windowState = normalizeText(bounds?.windowState || "").toLowerCase() || null;
125
+ const windowWidth = getPositiveNumber(bounds?.width);
126
+ const screenAvailWidth = getPositiveNumber(topViewport.screenAvailWidth);
127
+ const topOuterWidth = getPositiveNumber(topViewport.outerWidth);
128
+ const actualWidth = getPositiveNumber(
129
+ layoutMetrics?.cssVisualViewport?.clientWidth,
130
+ layoutMetrics?.cssLayoutViewport?.clientWidth,
131
+ topViewport.visualWidth,
132
+ topViewport.innerWidth,
133
+ state?.viewport?.width,
134
+ state?.clientWidth,
135
+ state?.frameRect?.width
136
+ );
137
+ const actualHeight = getPositiveNumber(
138
+ layoutMetrics?.cssVisualViewport?.clientHeight,
139
+ layoutMetrics?.cssLayoutViewport?.clientHeight,
140
+ topViewport.visualHeight,
141
+ topViewport.innerHeight,
142
+ state?.viewport?.height,
143
+ state?.clientHeight,
144
+ state?.frameRect?.height
145
+ );
146
+ const hasScreenWidth = screenAvailWidth > 0;
147
+ const nearFullscreen = Boolean(
148
+ windowState === "maximized"
149
+ || (
150
+ windowWidth > 0
151
+ && hasScreenWidth
152
+ && windowWidth >= screenAvailWidth * VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO
153
+ )
154
+ || (
155
+ topOuterWidth > 0
156
+ && hasScreenWidth
157
+ && topOuterWidth >= screenAvailWidth * VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO
158
+ )
159
+ );
160
+ const fallbackExpectedWidth = getPositiveNumber(screenAvailWidth, windowWidth, topOuterWidth);
161
+ let expectedWidth = 0;
162
+ if (windowWidth > 0) {
163
+ expectedWidth = hasScreenWidth && windowWidth >= screenAvailWidth * VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO
164
+ ? Math.min(windowWidth, screenAvailWidth)
165
+ : windowWidth;
166
+ } else if (topOuterWidth > 0) {
167
+ expectedWidth = hasScreenWidth && topOuterWidth >= screenAvailWidth * VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO
168
+ ? Math.min(topOuterWidth, screenAvailWidth)
169
+ : topOuterWidth;
170
+ } else {
171
+ expectedWidth = fallbackExpectedWidth;
172
+ }
173
+ const widthRatio = actualWidth > 0 && expectedWidth > 0
174
+ ? actualWidth / expectedWidth
175
+ : null;
176
+ const relativeCollapsed = Boolean(
177
+ nearFullscreen
178
+ && expectedWidth >= VIEWPORT_COLLAPSE_MIN_EXPECTED_WIDTH
179
+ && actualWidth > 0
180
+ && widthRatio !== null
181
+ && widthRatio <= VIEWPORT_COLLAPSE_RATIO_THRESHOLD
182
+ );
183
+
184
+ return {
185
+ threshold: VIEWPORT_COLLAPSE_RATIO_THRESHOLD,
186
+ minExpectedWidth: VIEWPORT_COLLAPSE_MIN_EXPECTED_WIDTH,
187
+ nearFullscreen,
188
+ windowState,
189
+ windowWidth,
190
+ screenAvailWidth,
191
+ topOuterWidth,
192
+ actualWidth,
193
+ actualHeight,
194
+ expectedWidth,
195
+ widthRatio,
196
+ relativeCollapsed
197
+ };
198
+ }
199
+
200
+ export function isListViewportCollapsed(state) {
201
+ if (!state?.ok) return false;
202
+ if (state.viewportDiagnostics?.relativeCollapsed === true) return true;
203
+ const clientHeight = Number(state.clientHeight || 0);
204
+ const clientWidth = Number(state.clientWidth || 0);
205
+ const frameWidth = Number(state.frameRect?.width || 0);
206
+ const frameHeight = Number(state.frameRect?.height || 0);
207
+ const viewportWidth = Number(state.viewport?.width || 0);
208
+ const viewportHeight = Number(state.viewport?.height || 0);
209
+
210
+ return (
211
+ (clientHeight > 0 && clientHeight < ABSOLUTE_COLLAPSE_LIMITS.clientHeight)
212
+ || (clientWidth > 0 && clientWidth < ABSOLUTE_COLLAPSE_LIMITS.clientWidth)
213
+ || (frameHeight > 0 && frameHeight < ABSOLUTE_COLLAPSE_LIMITS.frameHeight)
214
+ || (frameWidth > 0 && frameWidth < ABSOLUTE_COLLAPSE_LIMITS.frameWidth)
215
+ || (viewportHeight > 0 && viewportHeight < ABSOLUTE_COLLAPSE_LIMITS.viewportHeight)
216
+ || (viewportWidth > 0 && viewportWidth < ABSOLUTE_COLLAPSE_LIMITS.viewportWidth)
217
+ );
218
+ }
219
+
220
+ export async function readViewportState(client, {
221
+ roots = {},
222
+ root = "frame",
223
+ frameOwnerRoot = "frameOwner"
224
+ } = {}) {
225
+ const targetRootNodeId = rootNodeId(roots, root);
226
+ if (!targetRootNodeId) {
227
+ return {
228
+ ok: false,
229
+ root,
230
+ error: `Root not found: ${root}`
231
+ };
232
+ }
233
+
234
+ const layoutMetrics = await getLayoutMetrics(client);
235
+ const windowInfo = await getCurrentWindowInfo(client);
236
+ const contentBox = await readBestContentBox(client, targetRootNodeId);
237
+ const ownerNodeId = rootNodeId(roots, frameOwnerRoot);
238
+ const ownerBox = ownerNodeId ? await readBox(client, ownerNodeId) : null;
239
+ const frameRect = compactRect(ownerBox?.rect || contentBox?.rect || {});
240
+ const clientWidth = getPositiveNumber(
241
+ contentBox?.rect?.width,
242
+ frameRect.width,
243
+ pickViewportSize(layoutMetrics, "width")
244
+ );
245
+ const clientHeight = getPositiveNumber(
246
+ contentBox?.rect?.height,
247
+ frameRect.height,
248
+ pickViewportSize(layoutMetrics, "height")
249
+ );
250
+ const viewportWidth = pickViewportSize(layoutMetrics, "width") || clientWidth;
251
+ const viewportHeight = pickViewportSize(layoutMetrics, "height") || clientHeight;
252
+ const bounds = windowInfo?.bounds || {};
253
+ const topViewport = {
254
+ innerWidth: viewportWidth,
255
+ innerHeight: viewportHeight,
256
+ outerWidth: getPositiveNumber(bounds.width, viewportWidth),
257
+ outerHeight: getPositiveNumber(bounds.height, viewportHeight),
258
+ visualWidth: getPositiveNumber(layoutMetrics?.cssVisualViewport?.clientWidth, viewportWidth),
259
+ visualHeight: getPositiveNumber(layoutMetrics?.cssVisualViewport?.clientHeight, viewportHeight),
260
+ screenAvailWidth: getPositiveNumber(bounds.width),
261
+ screenAvailHeight: getPositiveNumber(bounds.height),
262
+ devicePixelRatio: getPositiveNumber(layoutMetrics?.cssVisualViewport?.scale, 1)
263
+ };
264
+ const state = {
265
+ ok: true,
266
+ root,
267
+ rootNodeId: targetRootNodeId,
268
+ frameOwnerRoot,
269
+ frameOwnerNodeId: ownerNodeId || null,
270
+ clientWidth,
271
+ clientHeight,
272
+ frameRect,
273
+ viewport: {
274
+ width: viewportWidth,
275
+ height: viewportHeight
276
+ },
277
+ topViewport,
278
+ windowInfo
279
+ };
280
+ state.viewportDiagnostics = buildViewportHealthDiagnostics(state, windowInfo, layoutMetrics);
281
+ state.collapsed = isListViewportCollapsed(state);
282
+ return state;
283
+ }
284
+
285
+ export async function setWindowStateIfPossible(client, windowState, reason = "viewport_recovery") {
286
+ const windowInfo = await getCurrentWindowInfo(client);
287
+ if (!windowInfo.ok || !windowInfo.windowId || typeof client?.Browser?.setWindowBounds !== "function") {
288
+ return {
289
+ ok: false,
290
+ reason,
291
+ windowState,
292
+ error: windowInfo.error || "Browser.setWindowBounds is not available"
293
+ };
294
+ }
295
+
296
+ try {
297
+ await client.Browser.setWindowBounds({
298
+ windowId: windowInfo.windowId,
299
+ bounds: {
300
+ windowState
301
+ }
302
+ });
303
+ return {
304
+ ok: true,
305
+ reason,
306
+ windowState,
307
+ windowId: windowInfo.windowId,
308
+ before: windowInfo.bounds || null
309
+ };
310
+ } catch (error) {
311
+ return {
312
+ ok: false,
313
+ reason,
314
+ windowState,
315
+ windowId: windowInfo.windowId,
316
+ error: error?.message || String(error)
317
+ };
318
+ }
319
+ }
320
+
321
+ export async function toggleWindowStateForViewportRecovery(client, {
322
+ reason = "viewport_recovery",
323
+ settleMs = 520,
324
+ bringToFront = true
325
+ } = {}) {
326
+ const currentInfo = await getCurrentWindowInfo(client);
327
+ const currentState = normalizeText(currentInfo?.bounds?.windowState || "").toLowerCase();
328
+ const sequence = currentState === "normal"
329
+ ? ["maximized", "normal"]
330
+ : ["normal", "maximized"];
331
+ const attempts = [];
332
+
333
+ for (const windowState of sequence) {
334
+ const attempt = await setWindowStateIfPossible(client, windowState, reason);
335
+ attempts.push(attempt);
336
+ if (attempt.ok && settleMs > 0) await sleep(settleMs);
337
+ }
338
+
339
+ if (bringToFront && typeof client?.Page?.bringToFront === "function") {
340
+ await client.Page.bringToFront();
341
+ }
342
+
343
+ return {
344
+ ok: attempts.some((attempt) => attempt.ok),
345
+ applied: attempts.some((attempt) => attempt.ok),
346
+ reason,
347
+ current_state: currentState || null,
348
+ sequence,
349
+ attempts
350
+ };
351
+ }
352
+
353
+ export function compactViewportState(state = null) {
354
+ if (!state) return null;
355
+ return {
356
+ ok: Boolean(state.ok),
357
+ root: state.root || null,
358
+ error: state.error || null,
359
+ clientWidth: state.clientWidth || 0,
360
+ clientHeight: state.clientHeight || 0,
361
+ frameRect: state.frameRect || null,
362
+ viewport: state.viewport || null,
363
+ topViewport: state.topViewport
364
+ ? {
365
+ innerWidth: state.topViewport.innerWidth || 0,
366
+ innerHeight: state.topViewport.innerHeight || 0,
367
+ outerWidth: state.topViewport.outerWidth || 0,
368
+ outerHeight: state.topViewport.outerHeight || 0,
369
+ visualWidth: state.topViewport.visualWidth || 0,
370
+ visualHeight: state.topViewport.visualHeight || 0,
371
+ screenAvailWidth: state.topViewport.screenAvailWidth || 0,
372
+ screenAvailHeight: state.topViewport.screenAvailHeight || 0,
373
+ devicePixelRatio: state.topViewport.devicePixelRatio || 0
374
+ }
375
+ : null,
376
+ viewportDiagnostics: state.viewportDiagnostics || null,
377
+ collapsed: Boolean(state.collapsed)
378
+ };
379
+ }
380
+
381
+ export function compactViewportHealthResult(result = null) {
382
+ if (!result) return null;
383
+ return {
384
+ ok: Boolean(result.ok),
385
+ collapsed: Boolean(result.collapsed),
386
+ recovered: Boolean(result.recovered),
387
+ reason: result.reason || null,
388
+ state: compactViewportState(result.state),
389
+ before: compactViewportState(result.before),
390
+ repair: result.repair
391
+ ? {
392
+ ok: Boolean(result.repair.ok),
393
+ applied: Boolean(result.repair.applied),
394
+ current_state: result.repair.current_state || null,
395
+ sequence: result.repair.sequence || [],
396
+ attempts: (result.repair.attempts || []).map((attempt) => ({
397
+ ok: Boolean(attempt.ok),
398
+ windowState: attempt.windowState,
399
+ error: attempt.error || null
400
+ }))
401
+ }
402
+ : null,
403
+ error: result.error || null
404
+ };
405
+ }
406
+
407
+ export async function ensureHealthyViewport(client, {
408
+ roots = {},
409
+ root = "frame",
410
+ frameOwnerRoot = "frameOwner",
411
+ reason = "viewport_recovery",
412
+ repair = true,
413
+ recoveryDelayMs = 900
414
+ } = {}) {
415
+ const before = await readViewportState(client, {
416
+ roots,
417
+ root,
418
+ frameOwnerRoot
419
+ });
420
+ if (!before.ok) {
421
+ return {
422
+ ok: false,
423
+ collapsed: false,
424
+ recovered: false,
425
+ reason,
426
+ state: before,
427
+ error: before.error || "viewport state could not be read"
428
+ };
429
+ }
430
+
431
+ if (!isListViewportCollapsed(before)) {
432
+ return {
433
+ ok: true,
434
+ collapsed: false,
435
+ recovered: false,
436
+ reason,
437
+ state: before
438
+ };
439
+ }
440
+
441
+ if (!repair) {
442
+ return {
443
+ ok: false,
444
+ collapsed: true,
445
+ recovered: false,
446
+ reason,
447
+ before,
448
+ state: before,
449
+ error: "viewport collapsed and repair disabled"
450
+ };
451
+ }
452
+
453
+ const repairResult = await toggleWindowStateForViewportRecovery(client, { reason });
454
+ if (recoveryDelayMs > 0) await sleep(recoveryDelayMs);
455
+ const after = await readViewportState(client, {
456
+ roots,
457
+ root,
458
+ frameOwnerRoot
459
+ });
460
+ const stillCollapsed = isListViewportCollapsed(after);
461
+ return {
462
+ ok: after.ok && !stillCollapsed,
463
+ collapsed: stillCollapsed,
464
+ recovered: after.ok && !stillCollapsed && repairResult.applied,
465
+ reason,
466
+ before,
467
+ state: after,
468
+ repair: repairResult,
469
+ error: after.ok && !stillCollapsed
470
+ ? null
471
+ : "viewport collapsed after recovery attempt"
472
+ };
473
+ }
474
+
475
+ export function createViewportRunGuard({
476
+ client,
477
+ domain = "boss",
478
+ root = "frame",
479
+ frameOwnerRoot = "frameOwner",
480
+ runControl = null,
481
+ getRoots = null,
482
+ rootNodesFromState = (rootState) => rootState?.rootNodes || rootState?.roots || rootState || {},
483
+ repair = true,
484
+ maxEvents = 10
485
+ } = {}) {
486
+ if (!client) throw new Error("createViewportRunGuard requires a guarded CDP client");
487
+ const events = [];
488
+ const stats = {
489
+ checks: 0,
490
+ recoveries: 0,
491
+ failures: 0
492
+ };
493
+
494
+ function recordEvent(phase, health) {
495
+ const compact = compactViewportHealthResult(health);
496
+ const shouldRecord = Boolean(health?.recovered || !health?.ok || health?.collapsed);
497
+ if (!shouldRecord) return compact;
498
+ const event = {
499
+ phase,
500
+ at: new Date().toISOString(),
501
+ ...compact
502
+ };
503
+ events.push(event);
504
+ if (events.length > maxEvents) events.shift();
505
+ if (runControl) {
506
+ runControl.checkpoint({
507
+ viewport_health: event,
508
+ viewport_health_events: events.slice(),
509
+ viewport_health_stats: { ...stats }
510
+ });
511
+ }
512
+ return compact;
513
+ }
514
+
515
+ async function ensure(rootState, {
516
+ phase = "run",
517
+ reason = `${domain}:${phase}`
518
+ } = {}) {
519
+ let currentRootState = rootState;
520
+ if (!currentRootState && typeof getRoots === "function") {
521
+ currentRootState = await getRoots(client);
522
+ }
523
+ const roots = rootNodesFromState(currentRootState);
524
+ stats.checks += 1;
525
+ const health = await ensureHealthyViewport(client, {
526
+ roots,
527
+ root,
528
+ frameOwnerRoot,
529
+ reason,
530
+ repair
531
+ });
532
+ if (health.recovered) stats.recoveries += 1;
533
+ if (!health.ok) stats.failures += 1;
534
+ const compact = recordEvent(phase, health);
535
+ if (!health.ok) {
536
+ const error = new Error(`${String(domain).toUpperCase()}_LIST_VIEWPORT_COLLAPSED`);
537
+ error.code = "LIST_VIEWPORT_COLLAPSED";
538
+ error.domain = domain;
539
+ error.phase = phase;
540
+ error.viewport_health = compact;
541
+ throw error;
542
+ }
543
+ if (health.recovered && typeof getRoots === "function") {
544
+ currentRootState = await getRoots(client);
545
+ }
546
+ return {
547
+ rootState: currentRootState,
548
+ health,
549
+ compact,
550
+ stats: { ...stats },
551
+ events: events.slice()
552
+ };
553
+ }
554
+
555
+ return {
556
+ ensure,
557
+ getStats() {
558
+ return { ...stats };
559
+ },
560
+ getEvents() {
561
+ return events.slice();
562
+ }
563
+ };
564
+ }