@flrande/bak-extension 0.6.11 → 0.6.13

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/src/background.ts CHANGED
@@ -1,19 +1,30 @@
1
- import type {
2
- ConsoleEntry,
3
- DebugDumpSection,
4
- Locator,
5
- NetworkEntry,
6
- PageExecutionScope,
7
- PageFetchResponse,
8
- PageFrameResult,
9
- PageFreshnessResult,
10
- TableHandle,
11
- TableSchema
12
- } from '@flrande/bak-protocol';
13
- import {
14
- clearNetworkEntries,
15
- dropNetworkCapture,
16
- ensureNetworkDebugger,
1
+ import type {
2
+ ConsoleEntry,
3
+ DebugDumpSection,
4
+ InspectFreshnessResult,
5
+ InspectLiveUpdatesResult,
6
+ InspectPageDataCandidateProbe,
7
+ InspectPageDataResult,
8
+ Locator,
9
+ NetworkEntry,
10
+ PageExecutionScope,
11
+ PageFetchResponse,
12
+ PageFrameResult,
13
+ PageFreshnessResult,
14
+ TableHandle,
15
+ TableSchema
16
+ } from '@flrande/bak-protocol';
17
+ import {
18
+ buildInspectPageDataResult,
19
+ buildPageDataProbe,
20
+ selectReplaySchemaMatch,
21
+ type InlineJsonInspectionSource,
22
+ type TableAnalysis
23
+ } from './dynamic-data-tools.js';
24
+ import {
25
+ clearNetworkEntries,
26
+ dropNetworkCapture,
27
+ ensureNetworkDebugger,
17
28
  exportHar,
18
29
  getNetworkEntry,
19
30
  latestNetworkTimestamp,
@@ -65,15 +76,19 @@ interface RuntimeErrorDetails {
65
76
  at: number;
66
77
  }
67
78
 
68
- interface PopupSessionBindingSummary {
69
- id: string;
70
- label: string;
71
- tabCount: number;
72
- activeTabId: number | null;
73
- windowId: number | null;
74
- groupId: number | null;
75
- detached: boolean;
76
- }
79
+ interface PopupSessionBindingSummary {
80
+ id: string;
81
+ label: string;
82
+ tabCount: number;
83
+ activeTabId: number | null;
84
+ activeTabTitle: string | null;
85
+ activeTabUrl: string | null;
86
+ windowId: number | null;
87
+ groupId: number | null;
88
+ detached: boolean;
89
+ lastBindingUpdateAt: number | null;
90
+ lastBindingUpdateReason: string | null;
91
+ }
77
92
 
78
93
  interface PopupState {
79
94
  ok: true;
@@ -114,24 +129,48 @@ const CONTRACT_TIMESTAMP_CONTEXT_PATTERN =
114
129
  /\b(expiry|expiration|expires|option|contract|strike|maturity|dte|call|put|exercise)\b/i;
115
130
  const EVENT_TIMESTAMP_CONTEXT_PATTERN = /\b(earnings|event|report|dividend|split|meeting|fomc|release|filing)\b/i;
116
131
 
117
- interface TimestampEvidenceCandidate {
132
+ interface TimestampEvidenceCandidate {
118
133
  value: string;
119
134
  source: 'visible' | 'inline' | 'page-data' | 'network';
120
135
  context?: string;
121
136
  path?: string;
122
137
  category?: 'data' | 'contract' | 'event' | 'unknown';
123
- }
124
-
125
- interface PageDataCandidateProbe {
126
- name: string;
127
- resolver: 'globalThis' | 'lexical';
128
- sample: unknown;
129
- timestamps: Array<{
130
- path: string;
131
- value: string;
132
- category: 'data' | 'contract' | 'event' | 'unknown';
133
- }>;
134
- }
138
+ }
139
+
140
+ interface PageInspectionState {
141
+ url?: string;
142
+ title?: string;
143
+ html?: string;
144
+ visibleText?: Array<{ chunkId: string; text: string; sourceTag: string }>;
145
+ suspiciousGlobals?: string[];
146
+ globalsPreview?: string[];
147
+ visibleTimestamps?: string[];
148
+ visibleTimestampCandidates?: Array<Record<string, unknown>>;
149
+ inlineTimestamps?: string[];
150
+ inlineTimestampCandidates?: Array<Record<string, unknown>>;
151
+ tables?: TableHandle[];
152
+ inlineJsonSources?: InlineJsonInspectionSource[];
153
+ cookies?: Array<{ name: string }>;
154
+ lastMutationAt?: number;
155
+ pageLoadedAt?: number;
156
+ scripts?: {
157
+ inlineCount: number;
158
+ suspectedDataVars: string[];
159
+ };
160
+ storage?: {
161
+ localStorageKeys: string[];
162
+ sessionStorageKeys: string[];
163
+ };
164
+ frames?: Array<{ framePath: string[]; url: string }>;
165
+ context?: {
166
+ framePath: string[];
167
+ shadowPath: string[];
168
+ };
169
+ timers?: {
170
+ timeouts: number;
171
+ intervals: number;
172
+ };
173
+ }
135
174
  const REPLAY_FORBIDDEN_HEADER_NAMES = new Set([
136
175
  'accept-encoding',
137
176
  'authorization',
@@ -152,10 +191,11 @@ let nextReconnectAt: number | null = null;
152
191
  let reconnectAttempt = 0;
153
192
  let lastError: RuntimeErrorDetails | null = null;
154
193
  let manualDisconnect = false;
155
- let sessionBindingStateMutationQueue: Promise<void> = Promise.resolve();
156
- let preserveHumanFocusDepth = 0;
157
- let lastBindingUpdateAt: number | null = null;
158
- let lastBindingUpdateReason: string | null = null;
194
+ let sessionBindingStateMutationQueue: Promise<void> = Promise.resolve();
195
+ let preserveHumanFocusDepth = 0;
196
+ let lastBindingUpdateAt: number | null = null;
197
+ let lastBindingUpdateReason: string | null = null;
198
+ const bindingUpdateMetadata = new Map<string, { at: number; reason: string }>();
159
199
 
160
200
  async function getConfig(): Promise<ExtensionConfig> {
161
201
  const stored = await chrome.storage.local.get([STORAGE_KEY_TOKEN, STORAGE_KEY_PORT, STORAGE_KEY_DEBUG_RICH_TEXT]);
@@ -308,31 +348,40 @@ async function listSessionBindingStates(): Promise<SessionBindingRecord[]> {
308
348
  return Object.values(await loadSessionBindingStateMap());
309
349
  }
310
350
 
311
- function summarizeSessionBindings(states: SessionBindingRecord[]): PopupState['sessionBindings'] {
312
- const items = states.map((state) => {
313
- const detached = state.windowId === null || state.tabIds.length === 0;
314
- return {
315
- id: state.id,
316
- label: state.label,
317
- tabCount: state.tabIds.length,
318
- activeTabId: state.activeTabId,
319
- windowId: state.windowId,
320
- groupId: state.groupId,
321
- detached
322
- } satisfies PopupSessionBindingSummary;
323
- });
324
- return {
325
- count: items.length,
326
- attachedCount: items.filter((item) => !item.detached).length,
351
+ async function summarizeSessionBindings(states: SessionBindingRecord[]): Promise<PopupState['sessionBindings']> {
352
+ const items = await Promise.all(
353
+ states.map(async (state) => {
354
+ const detached = state.windowId === null || state.tabIds.length === 0;
355
+ const activeTab =
356
+ typeof state.activeTabId === 'number' ? await sessionBindingBrowser.getTab(state.activeTabId) : null;
357
+ const bindingUpdate = bindingUpdateMetadata.get(state.id);
358
+ return {
359
+ id: state.id,
360
+ label: state.label,
361
+ tabCount: state.tabIds.length,
362
+ activeTabId: state.activeTabId,
363
+ activeTabTitle: activeTab?.title ?? null,
364
+ activeTabUrl: activeTab?.url ?? null,
365
+ windowId: state.windowId,
366
+ groupId: state.groupId,
367
+ detached,
368
+ lastBindingUpdateAt: bindingUpdate?.at ?? null,
369
+ lastBindingUpdateReason: bindingUpdate?.reason ?? null
370
+ } satisfies PopupSessionBindingSummary;
371
+ })
372
+ );
373
+ return {
374
+ count: items.length,
375
+ attachedCount: items.filter((item) => !item.detached).length,
327
376
  detachedCount: items.filter((item) => item.detached).length,
328
377
  tabCount: items.reduce((sum, item) => sum + item.tabCount, 0),
329
378
  items
330
379
  };
331
380
  }
332
381
 
333
- async function buildPopupState(): Promise<PopupState> {
334
- const config = await getConfig();
335
- const sessionBindings = summarizeSessionBindings(await listSessionBindingStates());
382
+ async function buildPopupState(): Promise<PopupState> {
383
+ const config = await getConfig();
384
+ const sessionBindings = await summarizeSessionBindings(await listSessionBindingStates());
336
385
  const reconnectRemainingMs = nextReconnectAt === null ? null : Math.max(0, nextReconnectAt - Date.now());
337
386
  let connectionState: PopupState['connectionState'];
338
387
  if (!config.token) {
@@ -395,16 +444,20 @@ function toSessionBindingEventBrowser(state: SessionBindingRecord | null): Recor
395
444
  };
396
445
  }
397
446
 
398
- function emitSessionBindingUpdated(
399
- bindingId: string,
400
- reason: string,
401
- state: SessionBindingRecord | null,
402
- extras: Record<string, unknown> = {}
403
- ): void {
404
- lastBindingUpdateAt = Date.now();
405
- lastBindingUpdateReason = reason;
406
- sendEvent('sessionBinding.updated', {
407
- bindingId,
447
+ function emitSessionBindingUpdated(
448
+ bindingId: string,
449
+ reason: string,
450
+ state: SessionBindingRecord | null,
451
+ extras: Record<string, unknown> = {}
452
+ ): void {
453
+ lastBindingUpdateAt = Date.now();
454
+ lastBindingUpdateReason = reason;
455
+ bindingUpdateMetadata.set(bindingId, {
456
+ at: lastBindingUpdateAt,
457
+ reason
458
+ });
459
+ sendEvent('sessionBinding.updated', {
460
+ bindingId,
408
461
  reason,
409
462
  browser: toSessionBindingEventBrowser(state),
410
463
  ...extras
@@ -1391,118 +1444,91 @@ function computeFreshnessAssessment(input: {
1391
1444
  return 'unknown';
1392
1445
  }
1393
1446
 
1394
- async function collectPageInspection(tabId: number, params: Record<string, unknown> = {}): Promise<Record<string, unknown>> {
1395
- return (await forwardContentRpc(tabId, 'bak.internal.inspectState', params)) as Record<string, unknown>;
1396
- }
1397
-
1398
- async function probePageDataCandidatesForTab(tabId: number, inspection: Record<string, unknown>): Promise<PageDataCandidateProbe[]> {
1399
- const candidateNames = [
1400
- ...(Array.isArray(inspection.suspiciousGlobals) ? inspection.suspiciousGlobals.map(String) : []),
1401
- ...(Array.isArray(inspection.globalsPreview) ? inspection.globalsPreview.map(String) : [])
1402
- ]
1403
- .filter((name, index, array) => /^[A-Za-z_$][\w$]*$/.test(name) && array.indexOf(name) === index)
1404
- .slice(0, 16);
1447
+ async function collectPageInspection(tabId: number, params: Record<string, unknown> = {}): Promise<PageInspectionState> {
1448
+ return (await forwardContentRpc(tabId, 'bak.internal.inspectState', params)) as PageInspectionState;
1449
+ }
1450
+
1451
+ async function probePageDataCandidatesForTab(tabId: number, inspection: PageInspectionState): Promise<InspectPageDataCandidateProbe[]> {
1452
+ const candidateNames = [
1453
+ ...(Array.isArray(inspection.suspiciousGlobals) ? inspection.suspiciousGlobals.map(String) : []),
1454
+ ...(Array.isArray(inspection.globalsPreview) ? inspection.globalsPreview.map(String) : [])
1455
+ ]
1456
+ .filter((name, index, array) => /^[A-Za-z_$][\w$]*$/.test(name) && array.indexOf(name) === index)
1457
+ .slice(0, 16);
1405
1458
  if (candidateNames.length === 0) {
1406
1459
  return [];
1407
1460
  }
1408
1461
 
1409
- const expr = `(() => {
1410
- const candidates = ${JSON.stringify(candidateNames)};
1411
- const dataPattern = /\\b(updated|update|updatedat|asof|timestamp|generated|generatedat|refresh|latest|last|quote|trade|price|flow|market|time|snapshot|signal)\\b/i;
1412
- const contractPattern = /\\b(expiry|expiration|expires|option|contract|strike|maturity|dte|call|put|exercise)\\b/i;
1413
- const eventPattern = /\\b(earnings|event|report|dividend|split|meeting|fomc|release|filing)\\b/i;
1414
- const isTimestampString = (value) => typeof value === 'string' && value.trim().length > 0 && !Number.isNaN(Date.parse(value.trim()));
1415
- const classify = (path, value) => {
1416
- const normalized = String(path || '').toLowerCase();
1417
- if (dataPattern.test(normalized)) return 'data';
1418
- if (contractPattern.test(normalized)) return 'contract';
1419
- if (eventPattern.test(normalized)) return 'event';
1420
- const parsed = Date.parse(String(value || '').trim());
1421
- return Number.isFinite(parsed) && parsed > Date.now() + 36 * 60 * 60 * 1000 ? 'contract' : 'unknown';
1422
- };
1423
- const sampleValue = (value, depth = 0) => {
1424
- if (depth >= 2 || value == null || typeof value !== 'object') {
1425
- if (typeof value === 'string') {
1426
- return value.length > 160 ? value.slice(0, 160) : value;
1427
- }
1428
- if (typeof value === 'function') {
1429
- return '[Function]';
1430
- }
1431
- return value;
1432
- }
1433
- if (Array.isArray(value)) {
1434
- return value.slice(0, 3).map((item) => sampleValue(item, depth + 1));
1435
- }
1436
- const sampled = {};
1437
- for (const key of Object.keys(value).slice(0, 8)) {
1438
- try {
1439
- sampled[key] = sampleValue(value[key], depth + 1);
1440
- } catch {
1441
- sampled[key] = '[Unreadable]';
1442
- }
1443
- }
1444
- return sampled;
1445
- };
1446
- const collectTimestamps = (value, path, depth, collected) => {
1447
- if (collected.length >= 16) return;
1448
- if (isTimestampString(value)) {
1449
- collected.push({ path, value: String(value), category: classify(path, value) });
1450
- return;
1451
- }
1452
- if (depth >= 3) return;
1453
- if (Array.isArray(value)) {
1454
- value.slice(0, 3).forEach((item, index) => collectTimestamps(item, path + '[' + index + ']', depth + 1, collected));
1455
- return;
1456
- }
1457
- if (value && typeof value === 'object') {
1458
- Object.keys(value)
1459
- .slice(0, 8)
1460
- .forEach((key) => {
1461
- try {
1462
- collectTimestamps(value[key], path ? path + '.' + key : key, depth + 1, collected);
1463
- } catch {
1464
- // Ignore unreadable nested properties.
1465
- }
1466
- });
1467
- }
1468
- };
1469
- const readCandidate = (name) => {
1470
- if (name in globalThis) {
1471
- return { resolver: 'globalThis', value: globalThis[name] };
1472
- }
1473
- return { resolver: 'lexical', value: globalThis.eval(name) };
1462
+ const expr = `(() => {
1463
+ const candidates = ${JSON.stringify(candidateNames)};
1464
+ const dataPattern = /\\b(updated|update|updatedat|asof|timestamp|generated|generatedat|refresh|latest|last|quote|trade|price|flow|market|time|snapshot|signal)\\b/i;
1465
+ const contractPattern = /\\b(expiry|expiration|expires|option|contract|strike|maturity|dte|call|put|exercise)\\b/i;
1466
+ const eventPattern = /\\b(earnings|event|report|dividend|split|meeting|fomc|release|filing)\\b/i;
1467
+ const sampleValue = (value, depth = 0) => {
1468
+ if (depth >= 2 || value == null || typeof value !== 'object') {
1469
+ if (typeof value === 'string') {
1470
+ return value.length > 160 ? value.slice(0, 160) : value;
1471
+ }
1472
+ if (typeof value === 'function') {
1473
+ return '[Function]';
1474
+ }
1475
+ return value;
1476
+ }
1477
+ if (Array.isArray(value)) {
1478
+ return value.slice(0, 3).map((item) => sampleValue(item, depth + 1));
1479
+ }
1480
+ const sampled = {};
1481
+ for (const key of Object.keys(value).slice(0, 8)) {
1482
+ try {
1483
+ sampled[key] = sampleValue(value[key], depth + 1);
1484
+ } catch {
1485
+ sampled[key] = '[Unreadable]';
1486
+ }
1487
+ }
1488
+ return sampled;
1489
+ };
1490
+ const readCandidate = (name) => {
1491
+ if (name in globalThis) {
1492
+ return { resolver: 'globalThis', value: globalThis[name] };
1493
+ }
1494
+ return { resolver: 'lexical', value: globalThis.eval(name) };
1474
1495
  };
1475
1496
  const results = [];
1476
- for (const name of candidates) {
1477
- try {
1478
- const resolved = readCandidate(name);
1479
- const timestamps = [];
1480
- collectTimestamps(resolved.value, name, 0, timestamps);
1481
- results.push({
1482
- name,
1483
- resolver: resolved.resolver,
1484
- sample: sampleValue(resolved.value),
1485
- timestamps
1486
- });
1487
- } catch {
1488
- // Ignore inaccessible candidates.
1489
- }
1497
+ for (const name of candidates) {
1498
+ try {
1499
+ const resolved = readCandidate(name);
1500
+ results.push({
1501
+ name,
1502
+ resolver: resolved.resolver,
1503
+ sample: sampleValue(resolved.value)
1504
+ });
1505
+ } catch {
1506
+ // Ignore inaccessible candidates.
1507
+ }
1490
1508
  }
1491
1509
  return results;
1492
1510
  })()`;
1493
-
1494
- try {
1495
- const evaluated = await executePageWorld<PageDataCandidateProbe[]>(tabId, 'eval', {
1496
- expr,
1497
- scope: 'current',
1498
- maxBytes: 64 * 1024
1499
- });
1500
- const frameResult = evaluated.result ?? evaluated.results?.find((candidate) => candidate.value || candidate.error);
1501
- return Array.isArray(frameResult?.value) ? frameResult.value : [];
1502
- } catch {
1503
- return [];
1504
- }
1505
- }
1511
+
1512
+ try {
1513
+ const evaluated = await executePageWorld<Array<{ name: string; resolver: 'globalThis' | 'lexical'; sample: unknown }>>(tabId, 'eval', {
1514
+ expr,
1515
+ scope: 'current',
1516
+ maxBytes: 64 * 1024
1517
+ });
1518
+ const frameResult = evaluated.result ?? evaluated.results?.find((candidate) => candidate.value || candidate.error);
1519
+ if (!Array.isArray(frameResult?.value)) {
1520
+ return [];
1521
+ }
1522
+ return frameResult.value
1523
+ .filter(
1524
+ (candidate): candidate is { name: string; resolver: 'globalThis' | 'lexical'; sample: unknown } =>
1525
+ typeof candidate === 'object' && candidate !== null && typeof candidate.name === 'string'
1526
+ )
1527
+ .map((candidate) => buildPageDataProbe(candidate.name, candidate.resolver, candidate.sample));
1528
+ } catch {
1529
+ return [];
1530
+ }
1531
+ }
1506
1532
 
1507
1533
  async function buildFreshnessForTab(tabId: number, params: Record<string, unknown> = {}): Promise<PageFreshnessResult> {
1508
1534
  const inspection = await collectPageInspection(tabId, params);
@@ -1598,7 +1624,7 @@ async function buildFreshnessForTab(tabId: number, params: Record<string, unknow
1598
1624
  };
1599
1625
  }
1600
1626
 
1601
- function summarizeNetworkCadence(entries: NetworkEntry[]): Record<string, unknown> {
1627
+ function summarizeNetworkCadence(entries: NetworkEntry[]): InspectLiveUpdatesResult['networkCadence'] {
1602
1628
  const relevant = entries
1603
1629
  .filter((entry) => entry.kind === 'fetch' || entry.kind === 'xhr')
1604
1630
  .slice()
@@ -1639,10 +1665,10 @@ function summarizeNetworkCadence(entries: NetworkEntry[]): Record<string, unknow
1639
1665
  };
1640
1666
  }
1641
1667
 
1642
- function extractReplayRowsCandidate(json: unknown): { rows: unknown[]; source: string } | null {
1643
- if (Array.isArray(json)) {
1644
- return { rows: json, source: '$' };
1645
- }
1668
+ function extractReplayRowsCandidate(json: unknown): { rows: unknown[]; source: string } | null {
1669
+ if (Array.isArray(json)) {
1670
+ return { rows: json, source: '$' };
1671
+ }
1646
1672
  if (typeof json !== 'object' || json === null) {
1647
1673
  return null;
1648
1674
  }
@@ -1652,71 +1678,77 @@ function extractReplayRowsCandidate(json: unknown): { rows: unknown[]; source: s
1652
1678
  if (Array.isArray(record[key])) {
1653
1679
  return { rows: record[key] as unknown[], source: `$.${key}` };
1654
1680
  }
1655
- }
1656
- return null;
1657
- }
1658
-
1659
- async function enrichReplayWithSchema(tabId: number, response: PageFetchResponse): Promise<PageFetchResponse> {
1660
- const candidate = extractReplayRowsCandidate(response.json);
1661
- if (!candidate || candidate.rows.length === 0) {
1662
- return response;
1663
- }
1664
-
1665
- const firstRow = candidate.rows[0];
1666
- const tablesResult = (await forwardContentRpc(tabId, 'table.list', {})) as { tables?: TableHandle[] };
1667
- const tables = Array.isArray(tablesResult.tables) ? tablesResult.tables : [];
1668
- if (tables.length === 0) {
1669
- return response;
1670
- }
1671
-
1672
- const schemas: Array<{ table: TableHandle; schema: TableSchema }> = [];
1673
- for (const table of tables) {
1674
- const schemaResult = (await forwardContentRpc(tabId, 'table.schema', { table: table.id })) as {
1675
- table?: TableHandle;
1676
- schema?: TableSchema;
1677
- };
1678
- if (schemaResult.schema && Array.isArray(schemaResult.schema.columns)) {
1679
- schemas.push({ table: schemaResult.table ?? table, schema: schemaResult.schema });
1680
- }
1681
- }
1682
-
1683
- if (schemas.length === 0) {
1684
- return response;
1685
- }
1686
-
1687
- if (Array.isArray(firstRow)) {
1688
- const matchingSchema = schemas.find(({ schema }) => schema.columns.length === firstRow.length) ?? schemas[0];
1689
- if (!matchingSchema) {
1690
- return response;
1691
- }
1692
- const mappedRows = candidate.rows
1693
- .filter((row): row is unknown[] => Array.isArray(row))
1694
- .map((row) => {
1695
- const mapped: Record<string, unknown> = {};
1696
- matchingSchema.schema.columns.forEach((column, index) => {
1697
- mapped[column.label] = row[index];
1698
- });
1699
- return mapped;
1700
- });
1701
- return {
1702
- ...response,
1703
- table: matchingSchema.table,
1704
- schema: matchingSchema.schema,
1705
- mappedRows,
1706
- mappingSource: candidate.source
1707
- };
1708
- }
1709
-
1710
- if (typeof firstRow === 'object' && firstRow !== null) {
1711
- return {
1712
- ...response,
1713
- mappedRows: candidate.rows.filter((row): row is Record<string, unknown> => typeof row === 'object' && row !== null),
1714
- mappingSource: candidate.source
1715
- };
1716
- }
1717
-
1718
- return response;
1719
- }
1681
+ }
1682
+ return null;
1683
+ }
1684
+
1685
+ async function collectTableAnalyses(tabId: number): Promise<TableAnalysis[]> {
1686
+ const tablesResult = (await forwardContentRpc(tabId, 'table.list', {})) as { tables?: TableHandle[] };
1687
+ const tables = Array.isArray(tablesResult.tables) ? tablesResult.tables : [];
1688
+ const analyses: TableAnalysis[] = [];
1689
+ for (const table of tables) {
1690
+ const schemaResult = (await forwardContentRpc(tabId, 'table.schema', { table: table.id })) as {
1691
+ table?: TableHandle;
1692
+ schema?: TableSchema;
1693
+ };
1694
+ const rowsResult = (await forwardContentRpc(tabId, 'table.rows', {
1695
+ table: table.id,
1696
+ limit: 8
1697
+ })) as {
1698
+ table?: TableHandle;
1699
+ rows?: Array<Record<string, unknown>>;
1700
+ };
1701
+ if (schemaResult.schema && Array.isArray(schemaResult.schema.columns)) {
1702
+ analyses.push({
1703
+ table: schemaResult.table ?? rowsResult.table ?? table,
1704
+ schema: schemaResult.schema,
1705
+ sampleRows: Array.isArray(rowsResult.rows) ? rowsResult.rows.slice(0, 8) : []
1706
+ });
1707
+ }
1708
+ }
1709
+ return analyses;
1710
+ }
1711
+
1712
+ async function enrichReplayWithSchema(tabId: number, requestId: string, response: PageFetchResponse): Promise<PageFetchResponse> {
1713
+ const candidate = extractReplayRowsCandidate(response.json);
1714
+ if (!candidate || candidate.rows.length === 0) {
1715
+ return response;
1716
+ }
1717
+
1718
+ const tables = await collectTableAnalyses(tabId);
1719
+ if (tables.length === 0) {
1720
+ return response;
1721
+ }
1722
+
1723
+ const inspection = await collectPageInspection(tabId, {});
1724
+ const pageDataCandidates = await probePageDataCandidatesForTab(tabId, inspection);
1725
+ const recentNetwork = listNetworkEntries(tabId, { limit: 25 });
1726
+ const pageDataReport = buildInspectPageDataResult({
1727
+ suspiciousGlobals: inspection.suspiciousGlobals ?? [],
1728
+ tables: inspection.tables ?? [],
1729
+ visibleTimestamps: inspection.visibleTimestamps ?? [],
1730
+ inlineTimestamps: inspection.inlineTimestamps ?? [],
1731
+ pageDataCandidates,
1732
+ recentNetwork,
1733
+ tableAnalyses: tables,
1734
+ inlineJsonSources: Array.isArray(inspection.inlineJsonSources) ? inspection.inlineJsonSources : []
1735
+ });
1736
+ const matched = selectReplaySchemaMatch(response.json, tables, {
1737
+ preferredSourceId: `networkResponse:${requestId}`,
1738
+ mappings: pageDataReport.sourceMappings
1739
+ });
1740
+ if (matched) {
1741
+ return {
1742
+ ...response,
1743
+ table: matched.table,
1744
+ schema: matched.schema,
1745
+ mappedRows: matched.mappedRows,
1746
+ mappingSource: matched.mappingSource
1747
+ };
1748
+ }
1749
+
1750
+ return response;
1751
+ }
1720
1752
 
1721
1753
  async function handleRequest(request: CliRequest): Promise<unknown> {
1722
1754
  const params = request.params ?? {};
@@ -2241,9 +2273,11 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
2241
2273
  if (!first) {
2242
2274
  throw toError('E_EXECUTION', 'network replay returned no response payload');
2243
2275
  }
2244
- return params.withSchema === 'auto' && params.mode === 'json' ? await enrichReplayWithSchema(tab.id!, first) : first;
2245
- });
2246
- }
2276
+ return params.withSchema === 'auto' && params.mode === 'json'
2277
+ ? await enrichReplayWithSchema(tab.id!, String(params.id ?? ''), first)
2278
+ : first;
2279
+ });
2280
+ }
2247
2281
  case 'page.freshness': {
2248
2282
  return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
2249
2283
  const tab = await withTab(target);
@@ -2312,61 +2346,75 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
2312
2346
  return filtered;
2313
2347
  });
2314
2348
  }
2315
- case 'inspect.pageData': {
2316
- return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
2317
- const tab = await withTab(target);
2318
- await ensureNetworkDebugger(tab.id!).catch(() => undefined);
2319
- const inspection = await collectPageInspection(tab.id!, params);
2320
- const pageDataCandidates = await probePageDataCandidatesForTab(tab.id!, inspection);
2321
- const network = listNetworkEntries(tab.id!, { limit: 10 });
2322
- return {
2323
- suspiciousGlobals: inspection.suspiciousGlobals ?? [],
2324
- tables: inspection.tables ?? [],
2325
- visibleTimestamps: inspection.visibleTimestamps ?? [],
2326
- inlineTimestamps: inspection.inlineTimestamps ?? [],
2327
- pageDataCandidates,
2328
- recentNetwork: network,
2329
- recommendedNextSteps: [
2330
- 'bak page extract --path table_data --resolver auto',
2331
- 'bak network search --pattern table_data',
2332
- 'bak page freshness'
2333
- ]
2334
- };
2335
- });
2336
- }
2337
- case 'inspect.liveUpdates': {
2338
- return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
2339
- const tab = await withTab(target);
2340
- await ensureNetworkDebugger(tab.id!).catch(() => undefined);
2341
- const inspection = await collectPageInspection(tab.id!, params);
2342
- const network = listNetworkEntries(tab.id!, { limit: 25 });
2343
- return {
2344
- lastMutationAt: inspection.lastMutationAt ?? null,
2345
- timers: inspection.timers ?? { timeouts: 0, intervals: 0 },
2346
- networkCount: network.length,
2347
- networkCadence: summarizeNetworkCadence(network),
2348
- recentNetwork: network.slice(0, 10)
2349
- };
2350
- });
2351
- }
2352
- case 'inspect.freshness': {
2353
- return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
2354
- const tab = await withTab(target);
2355
- const freshness = await buildFreshnessForTab(tab.id!, params);
2356
- return {
2349
+ case 'inspect.pageData': {
2350
+ return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
2351
+ const tab = await withTab(target);
2352
+ await ensureNetworkDebugger(tab.id!).catch(() => undefined);
2353
+ const inspection = await collectPageInspection(tab.id!, params);
2354
+ const pageDataCandidates = await probePageDataCandidatesForTab(tab.id!, inspection);
2355
+ const network = listNetworkEntries(tab.id!, { limit: 10 });
2356
+ const tableAnalyses = await collectTableAnalyses(tab.id!);
2357
+ const enriched = buildInspectPageDataResult({
2358
+ suspiciousGlobals: inspection.suspiciousGlobals ?? [],
2359
+ tables: inspection.tables ?? [],
2360
+ visibleTimestamps: inspection.visibleTimestamps ?? [],
2361
+ inlineTimestamps: inspection.inlineTimestamps ?? [],
2362
+ pageDataCandidates,
2363
+ recentNetwork: network,
2364
+ tableAnalyses,
2365
+ inlineJsonSources: Array.isArray(inspection.inlineJsonSources) ? inspection.inlineJsonSources : []
2366
+ });
2367
+ const recommendedNextSteps = enriched.recommendedNextActions.map((action) => action.command);
2368
+ return {
2369
+ suspiciousGlobals: inspection.suspiciousGlobals ?? [],
2370
+ tables: inspection.tables ?? [],
2371
+ visibleTimestamps: inspection.visibleTimestamps ?? [],
2372
+ inlineTimestamps: inspection.inlineTimestamps ?? [],
2373
+ pageDataCandidates,
2374
+ recentNetwork: network,
2375
+ dataSources: enriched.dataSources,
2376
+ sourceMappings: enriched.sourceMappings,
2377
+ recommendedNextActions: enriched.recommendedNextActions,
2378
+ recommendedNextSteps: [
2379
+ ...recommendedNextSteps,
2380
+ ...['bak page freshness'].filter((command) => !recommendedNextSteps.includes(command))
2381
+ ]
2382
+ } satisfies InspectPageDataResult;
2383
+ });
2384
+ }
2385
+ case 'inspect.liveUpdates': {
2386
+ return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
2387
+ const tab = await withTab(target);
2388
+ await ensureNetworkDebugger(tab.id!).catch(() => undefined);
2389
+ const inspection = await collectPageInspection(tab.id!, params);
2390
+ const network = listNetworkEntries(tab.id!, { limit: 25 });
2391
+ return {
2392
+ lastMutationAt: inspection.lastMutationAt ?? null,
2393
+ timers: inspection.timers ?? { timeouts: 0, intervals: 0 },
2394
+ networkCount: network.length,
2395
+ networkCadence: summarizeNetworkCadence(network),
2396
+ recentNetwork: network.slice(0, 10)
2397
+ } satisfies InspectLiveUpdatesResult;
2398
+ });
2399
+ }
2400
+ case 'inspect.freshness': {
2401
+ return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
2402
+ const tab = await withTab(target);
2403
+ const freshness = await buildFreshnessForTab(tab.id!, params);
2404
+ return {
2357
2405
  ...freshness,
2358
2406
  lagMs:
2359
2407
  typeof freshness.latestNetworkTimestamp === 'number' &&
2360
2408
  typeof (freshness.latestPageDataTimestamp ?? freshness.latestInlineDataTimestamp) === 'number'
2361
2409
  ? Math.max(
2362
2410
  0,
2363
- freshness.latestNetworkTimestamp -
2364
- (freshness.latestPageDataTimestamp ?? freshness.latestInlineDataTimestamp ?? freshness.latestNetworkTimestamp)
2365
- )
2366
- : null
2367
- };
2368
- });
2369
- }
2411
+ freshness.latestNetworkTimestamp -
2412
+ (freshness.latestPageDataTimestamp ?? freshness.latestInlineDataTimestamp ?? freshness.latestNetworkTimestamp)
2413
+ )
2414
+ : null
2415
+ } satisfies InspectFreshnessResult;
2416
+ });
2417
+ }
2370
2418
  case 'capture.snapshot': {
2371
2419
  return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
2372
2420
  const tab = await withTab(target);