@agent-scope/core 1.17.0 → 1.17.2

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 (2) hide show
  1. package/README.md +590 -0
  2. package/package.json +3 -2
package/README.md ADDED
@@ -0,0 +1,590 @@
1
+ # @agent-scope/core
2
+
3
+ Zero-dependency type foundation for the Scope React instrumentation platform.
4
+
5
+ `@agent-scope/core` defines every shared TypeScript type, the `serialize()` function, a complete set of runtime type-guards, and schema version constants. Nothing in this package references the DOM, React, or any browser API — it is safe to import in any environment (browser, Node.js, edge runtimes, test runners).
6
+
7
+ ---
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install @agent-scope/core
13
+ ```
14
+
15
+ ## What it does / when to use it
16
+
17
+ `@agent-scope/core` is the contract layer of the Scope monorepo. You need it when:
18
+
19
+ - **Building a consumer** of `PageReport` objects (analytics, DevTools, CI reporters) — import the types and type-guards.
20
+ - **Serialising arbitrary React props or state** — call `serialize()` to produce a safely-serialised `SerializedValue` that handles circular references, depth limiting, and every JavaScript type.
21
+ - **Validating untrusted data** — use the `is*` guards to verify that a stored report still matches the current schema before processing it.
22
+
23
+ `@agent-scope/runtime` and `@agent-scope/sourcemap` both depend on this package and re-export the types they need.
24
+
25
+ ---
26
+
27
+ ## API Reference
28
+
29
+ ### `PageReport`
30
+
31
+ The top-level transport and storage unit. Every other type is a node or sub-node of this structure.
32
+
33
+ ```ts
34
+ interface PageReport {
35
+ /** Full URL at capture time (no fragment). */
36
+ url: string;
37
+
38
+ /**
39
+ * Router-level metadata. null when no routing adapter is installed or
40
+ * the URL could not be matched to a known route.
41
+ */
42
+ route: RouteInfo | null;
43
+
44
+ /** Unix timestamp (ms) when the capture was initiated. */
45
+ timestamp: number;
46
+
47
+ /**
48
+ * Wall-clock time (ms) from capture start to PageReport assembly.
49
+ * Use this to monitor Scope runtime overhead.
50
+ */
51
+ capturedIn: number;
52
+
53
+ /** Root node of the captured React component tree. */
54
+ tree: ComponentNode;
55
+
56
+ /**
57
+ * All JS errors observed during the capture window:
58
+ * unhandled exceptions, unhandled rejections, ErrorBoundary catches,
59
+ * and console.error calls attributed to an error.
60
+ */
61
+ errors: CapturedError[];
62
+
63
+ /** All <Suspense> boundaries, flattened for O(1) access. */
64
+ suspenseBoundaries: SuspenseBoundaryInfo[];
65
+
66
+ /** All console.* calls intercepted during the capture window, ascending by timestamp. */
67
+ consoleEntries: ConsoleEntry[];
68
+ }
69
+ ```
70
+
71
+ **Example payload** (from `src/__tests__/types.test.ts`):
72
+
73
+ ```ts
74
+ const report: PageReport = {
75
+ url: "https://example.com/dashboard",
76
+ route: {
77
+ pattern: "/dashboard",
78
+ params: null,
79
+ query: {},
80
+ name: "dashboard",
81
+ },
82
+ timestamp: 1700000000000,
83
+ capturedIn: 42,
84
+ tree: {
85
+ id: 1,
86
+ name: "App",
87
+ type: "function",
88
+ source: { fileName: "/app/src/Foo.tsx", lineNumber: 10, columnNumber: 5 },
89
+ props: { type: "object", value: {}, preview: "{}" },
90
+ state: [
91
+ {
92
+ type: "useState",
93
+ name: null,
94
+ value: { type: "string", value: "hello", preview: '"hello"' },
95
+ deps: null,
96
+ hasCleanup: null,
97
+ },
98
+ ],
99
+ context: [
100
+ {
101
+ contextName: "ThemeContext",
102
+ value: { type: "string", value: "hello", preview: '"hello"' },
103
+ didTriggerRender: false,
104
+ },
105
+ ],
106
+ renderCount: 1,
107
+ renderDuration: 0.5,
108
+ children: [],
109
+ },
110
+ errors: [
111
+ {
112
+ message: "Something went wrong",
113
+ name: "Error",
114
+ stack: "Error: Something\n at App.tsx:42",
115
+ source: { fileName: "/app/src/Foo.tsx", lineNumber: 10, columnNumber: 5 },
116
+ componentName: "App",
117
+ timestamp: 1700000000000,
118
+ capturedBy: "boundary",
119
+ },
120
+ ],
121
+ suspenseBoundaries: [
122
+ {
123
+ id: 5,
124
+ componentName: "LazySection",
125
+ isSuspended: false,
126
+ suspendedDuration: null,
127
+ fallbackName: "Spinner",
128
+ source: { fileName: "/app/src/Foo.tsx", lineNumber: 10, columnNumber: 5 },
129
+ },
130
+ ],
131
+ consoleEntries: [
132
+ {
133
+ level: "warn",
134
+ args: [{ type: "string", value: "hello", preview: '"hello"' }],
135
+ timestamp: 1700000000001,
136
+ componentName: "App",
137
+ preview: "Something is odd",
138
+ },
139
+ ],
140
+ };
141
+ ```
142
+
143
+ ---
144
+
145
+ ### `ComponentNode`
146
+
147
+ One node in the captured component tree, corresponding to a single React fiber.
148
+
149
+ ```ts
150
+ interface ComponentNode {
151
+ /** Fiber's internal numeric ID. Stable for the lifetime of a mounted component. */
152
+ id: number;
153
+
154
+ /**
155
+ * Resolved display name.
156
+ * Resolution order: displayName → function/class name → JSX tag → "Anonymous".
157
+ */
158
+ name: string;
159
+
160
+ /** Component implementation strategy. */
161
+ type: ComponentType; // "function" | "class" | "forward_ref" | "memo" | "host"
162
+
163
+ /**
164
+ * Source location from babel's __source prop or DevTools fiber data.
165
+ * null in production builds or for host elements.
166
+ */
167
+ source: SourceLocation | null;
168
+
169
+ /** Serialized snapshot of the component's props at capture time. */
170
+ props: SerializedValue;
171
+
172
+ /**
173
+ * Ordered hook slots (hook call-order, slot 0 = first use* call).
174
+ * Empty for class components and host elements.
175
+ */
176
+ state: HookState[];
177
+
178
+ /** All React contexts consumed by this component during the capture window. */
179
+ context: ContextConsumption[];
180
+
181
+ /** Number of renders during the capture window (>1 indicates re-renders). */
182
+ renderCount: number;
183
+
184
+ /**
185
+ * Total wall-clock time (ms) in this component's render, excluding children.
186
+ * Measured from beginWork to completeWork.
187
+ */
188
+ renderDuration: number;
189
+
190
+ /** Ordered child nodes (depth-first). Empty array = leaf node. */
191
+ children: ComponentNode[];
192
+ }
193
+
194
+ type ComponentType = "function" | "class" | "forward_ref" | "memo" | "host";
195
+ ```
196
+
197
+ ---
198
+
199
+ ### `LightweightComponentNode`
200
+
201
+ A stripped-down node emitted when `capture({ lightweight: true })` is used. Omits `props`, `state`, `context`, `source`, `renderCount`, and `renderDuration`, producing ~99% smaller payloads for large trees.
202
+
203
+ ```ts
204
+ interface LightweightComponentNode {
205
+ id: number;
206
+ name: string;
207
+ type: ComponentType;
208
+ /** Total hooks count (no values serialised). */
209
+ hookCount: number;
210
+ /** Hook types in call order, no values. */
211
+ hookTypes: HookType[];
212
+ /** Equals children.length. */
213
+ childCount: number;
214
+ /** Tree depth; root = 0. */
215
+ depth: number;
216
+ children: LightweightComponentNode[];
217
+ }
218
+ ```
219
+
220
+ ---
221
+
222
+ ### `HookState`
223
+
224
+ A snapshot of a single hook slot.
225
+
226
+ ```ts
227
+ interface HookState {
228
+ /** Hook type. "custom" for third-party / project-local hooks. */
229
+ type: HookType;
230
+
231
+ /**
232
+ * Inferred name of a custom hook (e.g. "useTheme").
233
+ * null for built-in hooks where type is already informative.
234
+ */
235
+ name: string | null;
236
+
237
+ /**
238
+ * Serialized current value.
239
+ * - useState/useReducer: current state
240
+ * - useMemo/useCallback: memoized value / function ref
241
+ * - useRef: .current value
242
+ * - useContext: current context value
243
+ * - effect hooks: undefined (no meaningful value)
244
+ */
245
+ value: SerializedValue;
246
+
247
+ /**
248
+ * Serialized dependency array.
249
+ * Present for useEffect, useLayoutEffect, useMemo, useCallback, useSyncExternalStore.
250
+ * null for hooks that do not accept deps.
251
+ */
252
+ deps: SerializedValue[] | null;
253
+
254
+ /**
255
+ * Whether the effect registered a cleanup function on its last run.
256
+ * true = cleanup returned. false = no cleanup. null = not applicable.
257
+ */
258
+ hasCleanup: boolean | null;
259
+ }
260
+
261
+ type HookType =
262
+ | "useState" | "useReducer" | "useEffect" | "useLayoutEffect"
263
+ | "useMemo" | "useCallback" | "useRef" | "useContext"
264
+ | "useId" | "useSyncExternalStore" | "useTransition" | "useDeferredValue"
265
+ | "custom";
266
+ ```
267
+
268
+ **Example** (from `src/__tests__/types.test.ts`):
269
+
270
+ ```ts
271
+ // useState
272
+ { type: "useState", name: null, value: { type: "number", value: 42 }, deps: null, hasCleanup: null }
273
+
274
+ // useEffect with deps and cleanup
275
+ { type: "useEffect", name: null, value: { type: "undefined" }, deps: [{ type: "string", value: "dep1" }], hasCleanup: true }
276
+
277
+ // custom hook
278
+ { type: "custom", name: "useTheme", value: { type: "object", preview: '{ mode: "dark" }' }, deps: null, hasCleanup: null }
279
+ ```
280
+
281
+ ---
282
+
283
+ ### `SerializedValue`
284
+
285
+ An envelope for any JavaScript value that may not round-trip through JSON (functions, symbols, circular references, etc.).
286
+
287
+ ```ts
288
+ interface SerializedValue {
289
+ /** Original JavaScript type discriminant. */
290
+ type: SerializedValueType;
291
+
292
+ /**
293
+ * Round-trippable value for primitives and simple composites.
294
+ * Absent for function, symbol, circular, truncated — use preview instead.
295
+ */
296
+ value?: unknown;
297
+
298
+ /**
299
+ * Human-readable string for display. Always present when value is absent.
300
+ */
301
+ preview?: string;
302
+ }
303
+
304
+ type SerializedValueType =
305
+ | "string" | "number" | "boolean" | "null" | "undefined"
306
+ | "object" | "array" | "function" | "symbol" | "bigint"
307
+ | "date" | "map" | "set"
308
+ | "circular" // circular reference was detected at this position
309
+ | "truncated"; // value exceeded depth/size limits and was cut off
310
+ ```
311
+
312
+ **Examples** (from `src/__tests__/serialization.test.ts`):
313
+
314
+ ```ts
315
+ // Primitives
316
+ { type: "null", value: null, preview: "null" }
317
+ { type: "boolean", value: true, preview: "true" }
318
+ { type: "number", value: 42, preview: "42" }
319
+ { type: "string", value: "hi", preview: '"hi"' }
320
+ { type: "bigint", value: "123n", preview: "123n" }
321
+
322
+ // Function (no value)
323
+ { type: "function", preview: "function myFunc(a, b) { return a + …" }
324
+
325
+ // Date
326
+ { type: "date", value: "2024-01-01T00:00:00.000Z", preview: "2024-01-01T00:00:00.000Z" }
327
+
328
+ // Map
329
+ { type: "map", value: [/* entry objects */], preview: "Map(2)" }
330
+
331
+ // Set
332
+ { type: "set", value: [/* items */], preview: "Set(3)" }
333
+
334
+ // Circular reference
335
+ { type: "circular" }
336
+
337
+ // Depth-exceeded
338
+ { type: "truncated", preview: "Array(50)" }
339
+ ```
340
+
341
+ ---
342
+
343
+ ### `serialize(value, options?)`
344
+
345
+ Convert any JavaScript value to a `SerializedValue` snapshot.
346
+
347
+ ```ts
348
+ function serialize(value: unknown, options?: SerializeOptions): SerializedValue;
349
+
350
+ interface SerializeOptions {
351
+ /** Maximum recursion depth for nested objects/arrays. Default: 5. */
352
+ maxDepth?: number;
353
+ /** Maximum string length before truncation. Default: 200. */
354
+ maxStringLength?: number;
355
+ /** Maximum array items to serialize. Default: 100. */
356
+ maxArrayLength?: number;
357
+ /** Maximum object properties to serialize. Default: 50. */
358
+ maxProperties?: number;
359
+ }
360
+ ```
361
+
362
+ **Behaviour summary:**
363
+
364
+ | Input | Output type | Notes |
365
+ |---|---|---|
366
+ | `null` | `"null"` | `value: null` |
367
+ | `undefined` | `"undefined"` | no `value` field |
368
+ | `boolean` | `"boolean"` | |
369
+ | `number` | `"number"` | `NaN`, `Infinity` included |
370
+ | `string` | `"string"` | truncated at `maxStringLength` with `"..."` suffix |
371
+ | `bigint` | `"bigint"` | stored as `"123n"` string |
372
+ | `Symbol` | `"symbol"` | `preview` only, no `value` |
373
+ | `function` | `"function"` | raw source preview, truncated at 50 chars |
374
+ | `Date` | `"date"` | ISO 8601 string in `value` and `preview` |
375
+ | `Map` | `"map"` | entries as objects, truncated at `maxProperties` |
376
+ | `Set` | `"set"` | items as array, truncated at `maxArrayLength` |
377
+ | `Array` | `"array"` | truncated at `maxArrayLength` |
378
+ | `object` | `"object"` | `{key: SerializedValue}` map, truncated at `maxProperties` |
379
+ | `Error` | `"object"` | `{name, message, stack}` extracted |
380
+ | `WeakMap/WeakSet/WeakRef` | `"object"` | opaque, no entries |
381
+ | _circular ref_ | `"circular"` | no `value` or `preview` |
382
+ | _depth exceeded_ | `"truncated"` | `preview` describes the cut-off value |
383
+
384
+ **Circular reference detection** uses a `WeakSet` that tracks the current ancestor chain. The same object can legitimately appear in sibling branches without being flagged as circular (verified by tests).
385
+
386
+ **Depth counting** — depth 0 is the root call. An object value encountered at `depth >= maxDepth` is returned as `{ type: "truncated" }` rather than being expanded.
387
+
388
+ ```ts
389
+ // Self-referential object
390
+ const obj: Record<string, unknown> = { a: 1 };
391
+ obj.self = obj;
392
+ serialize(obj);
393
+ // → { type: "object", value: { a: { type: "number", ... }, self: { type: "circular" } } }
394
+
395
+ // Deep nesting with maxDepth: 2
396
+ const deep = { a: { b: { c: "leaf" } } };
397
+ serialize(deep, { maxDepth: 2 });
398
+ // root(depth=0) → a(depth=1) → b is { type: "truncated" } at depth=2
399
+
400
+ // Shared object in sibling branches (NOT flagged as circular)
401
+ const shared = { x: 1 };
402
+ serialize({ left: shared, right: shared });
403
+ // → { left: { type: "object", ... }, right: { type: "object", ... } } ← both are "object"
404
+ ```
405
+
406
+ ---
407
+
408
+ ### Type Guards
409
+
410
+ All guards accept `unknown` and return a type predicate. Use them to validate data crossing process or storage boundaries.
411
+
412
+ ```ts
413
+ import {
414
+ isPageReport, isPageReportDeep,
415
+ isComponentNode, isComponentNodeDeep,
416
+ isCapturedError, isSuspenseBoundaryInfo,
417
+ isConsoleEntry, isContextConsumption,
418
+ isHookState, isRouteInfo,
419
+ isSerializedValue, isSourceLocation,
420
+ } from "@agent-scope/core";
421
+ ```
422
+
423
+ | Guard | Validates | Depth |
424
+ |---|---|---|
425
+ | `isPageReport(v)` | Full `PageReport` shape | Shallow `ComponentNode` |
426
+ | `isPageReportDeep(v)` | Full `PageReport` + entire tree | Recursive |
427
+ | `isComponentNode(v)` | Single `ComponentNode` (children not recursed) | O(1) per node |
428
+ | `isComponentNodeDeep(v)` | `ComponentNode` + all descendants | O(n) |
429
+ | `isCapturedError(v)` | `CapturedError` | — |
430
+ | `isSuspenseBoundaryInfo(v)` | `SuspenseBoundaryInfo` | — |
431
+ | `isConsoleEntry(v)` | `ConsoleEntry` | — |
432
+ | `isContextConsumption(v)` | `ContextConsumption` | — |
433
+ | `isHookState(v)` | `HookState` | — |
434
+ | `isRouteInfo(v)` | `RouteInfo` | — |
435
+ | `isSerializedValue(v)` | `SerializedValue` | — |
436
+ | `isSourceLocation(v)` | `SourceLocation` | — |
437
+
438
+ ---
439
+
440
+ ### Constants
441
+
442
+ Readonly arrays useful for exhaustive `switch`/`if` checks and validation loops.
443
+
444
+ ```ts
445
+ import {
446
+ SCHEMA_VERSION, // "0.1.0" — bump on breaking schema changes
447
+ HOOK_TYPES, // readonly HookType[]
448
+ COMPONENT_TYPES, // readonly ComponentType[]
449
+ SERIALIZED_VALUE_TYPES, // readonly SerializedValueType[]
450
+ CONSOLE_LEVELS, // readonly ConsoleLevel[]
451
+ } from "@agent-scope/core";
452
+ ```
453
+
454
+ `SCHEMA_VERSION` follows semver; store it in `PageReport` metadata to detect schema drift when loading reports from storage.
455
+
456
+ ---
457
+
458
+ ### Supporting Types
459
+
460
+ <details>
461
+ <summary><code>SourceLocation</code></summary>
462
+
463
+ ```ts
464
+ interface SourceLocation {
465
+ /** Absolute or relative path to the source file. */
466
+ fileName: string;
467
+ /** 1-based line number of the JSX element. */
468
+ lineNumber: number;
469
+ /** 1-based column number of the JSX element. */
470
+ columnNumber: number;
471
+ }
472
+ ```
473
+
474
+ Populated from React's `__source` prop (injected by `babel-plugin-transform-react-jsx-source` in development mode).
475
+ </details>
476
+
477
+ <details>
478
+ <summary><code>ContextConsumption</code></summary>
479
+
480
+ ```ts
481
+ interface ContextConsumption {
482
+ /**
483
+ * The context's displayName. null when the context has no display name.
484
+ */
485
+ contextName: string | null;
486
+
487
+ /** Serialized snapshot of the context value at capture time. */
488
+ value: SerializedValue;
489
+
490
+ /**
491
+ * Whether this context consumption triggered a re-render during the
492
+ * capture window (i.e. value changed between previous and current render).
493
+ */
494
+ didTriggerRender: boolean;
495
+ }
496
+ ```
497
+ </details>
498
+
499
+ <details>
500
+ <summary><code>CapturedError</code></summary>
501
+
502
+ ```ts
503
+ interface CapturedError {
504
+ message: string;
505
+ name: string; // e.g. "TypeError", "RangeError"
506
+ stack: string | null; // raw V8 stack; not source-mapped
507
+ source: SourceLocation | null; // parsed from top stack frame
508
+ componentName: string | null; // nearest React component
509
+ timestamp: number; // Unix ms
510
+ capturedBy: "boundary" | "unhandled" | "rejection" | "console";
511
+ }
512
+ ```
513
+ </details>
514
+
515
+ <details>
516
+ <summary><code>RouteInfo</code></summary>
517
+
518
+ ```ts
519
+ interface RouteInfo {
520
+ pattern: string | null; // e.g. "/users/:id"
521
+ params: Record<string, string> | null; // e.g. { id: "42" }
522
+ query: Record<string, string>; // parsed query string
523
+ name: string | null; // named route from router config
524
+ }
525
+ ```
526
+ </details>
527
+
528
+ <details>
529
+ <summary><code>SuspenseBoundaryInfo</code></summary>
530
+
531
+ ```ts
532
+ interface SuspenseBoundaryInfo {
533
+ id: number;
534
+ componentName: string; // nearest named ancestor
535
+ isSuspended: boolean;
536
+ suspendedDuration: number | null; // ms suspended; null if not triggered
537
+ fallbackName: string | null; // name of the fallback element
538
+ source: SourceLocation | null;
539
+ }
540
+ ```
541
+ </details>
542
+
543
+ <details>
544
+ <summary><code>ConsoleEntry</code></summary>
545
+
546
+ ```ts
547
+ interface ConsoleEntry {
548
+ level: ConsoleLevel; // "log" | "warn" | "error" | "info" | "debug" | "group" | ...
549
+ args: SerializedValue[]; // each argument, serialized
550
+ timestamp: number; // Unix ms
551
+ componentName: string | null; // attributed React component
552
+ preview: string; // single-line condensed text, trimmed to 200 chars
553
+ }
554
+ ```
555
+ </details>
556
+
557
+ ---
558
+
559
+ ## Internal Architecture
560
+
561
+ ```
562
+ src/
563
+ ├── index.ts ← barrel export (types, guards, serialize, constants)
564
+ ├── serialization.ts ← serialize() + SerializeOptions
565
+ ├── guards.ts ← all is*() type-guard functions
566
+ ├── constants.ts ← SCHEMA_VERSION, HOOK_TYPES, COMPONENT_TYPES, …
567
+ └── types/
568
+ ├── page-report.ts ← PageReport
569
+ ├── component-node.ts ← ComponentNode, ComponentType
570
+ ├── lightweight-component-node.ts ← LightweightComponentNode
571
+ ├── hook-state.ts ← HookState, HookType
572
+ ├── serialized-value.ts ← SerializedValue, SerializedValueType
573
+ ├── source-location.ts ← SourceLocation
574
+ ├── context.ts ← ContextConsumption
575
+ ├── errors.ts ← CapturedError
576
+ ├── route.ts ← RouteInfo
577
+ ├── console.ts ← ConsoleEntry, ConsoleLevel
578
+ └── suspense.ts ← SuspenseBoundaryInfo
579
+ ```
580
+
581
+ The package has **zero runtime dependencies**. It ships both ESM (`dist/index.js`) and CJS (`dist/index.cjs`) builds via `tsup`.
582
+
583
+ ---
584
+
585
+ ## Used by
586
+
587
+ | Package | How |
588
+ |---|---|
589
+ | `@agent-scope/runtime` | Imports all types; calls `serialize()` for props, state, and context values |
590
+ | `@agent-scope/sourcemap` | Imports `ComponentNode`, `PageReport` to type the resolution input/output |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-scope/core",
3
- "version": "1.17.0",
3
+ "version": "1.17.2",
4
4
  "description": "Core types, schemas, and serialization for Scope — zero dependencies",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -20,7 +20,8 @@
20
20
  "module": "./dist/index.js",
21
21
  "types": "./dist/index.d.ts",
22
22
  "files": [
23
- "dist"
23
+ "dist",
24
+ "README.md"
24
25
  ],
25
26
  "scripts": {
26
27
  "build": "tsup",