foottrace 1.0.1 → 1.0.3

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.
@@ -9,7 +9,6 @@
9
9
  * the call site instead: `(id) => recorder.getByKey(id)?.keysRead ?? []`.
10
10
  */
11
11
  import type { KeysReadLookup } from '../memory/backtrack.js';
12
- import type { ExecutionTree } from '../memory/types.js';
13
12
  import type { KeysReadSource } from './types.js';
14
13
  /**
15
14
  * Post-hoc reads from a finished run's execution tree — ZERO setup.
@@ -36,8 +35,17 @@ import type { KeysReadSource } from './types.js';
36
35
  * Passing multiple trees widens READ resolution only (useful when one log
37
36
  * genuinely spans them); it does not let a root-log slice cross a subflow
38
37
  * mount — see README.md § Subflow boundaries.
38
+ *
39
+ * A live {@link ExecutionTree} or unknown saved JSON uses this same traversal.
40
+ * Malformed reader fields throw a TypeError naming their source path; no partial
41
+ * reads strategy is returned. Optional fields may be absent or undefined, but
42
+ * null nodes, sparse arrays and wrongly shaped fields are not missing evidence
43
+ * to silently skip. Only read-map KEYS are inspected, never their stored values.
44
+ * Cycles/shared objects are visited once; existing traversal order is retained.
45
+ * Coverage counts observed nodes and nonempty read sets, not capture completeness.
46
+ * This is an ordinary-data boundary, not a sandbox for hostile proxies/accessors.
39
47
  */
40
- export declare function keysReadFromExecutionTree(tree: ExecutionTree | readonly ExecutionTree[]): KeysReadSource;
48
+ export declare function keysReadFromExecutionTree(tree: unknown): KeysReadSource;
41
49
  /**
42
50
  * Reads from a prebuilt map — e.g. collected live from
43
51
  * `ScopeRecorder.onRead` events, or deserialized from a stored trace.
@@ -8,6 +8,10 @@
8
8
  * drag this leaf library up the dependency DAG. The adapter is a one-liner at
9
9
  * the call site instead: `(id) => recorder.getByKey(id)?.keysRead ?? []`.
10
10
  */
11
+ /** Narrow only the object shape this reader uses; stored values stay opaque. */
12
+ function isReadTreeObject(value) {
13
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
14
+ }
11
15
  /**
12
16
  * Post-hoc reads from a finished run's execution tree — ZERO setup.
13
17
  *
@@ -33,33 +37,63 @@
33
37
  * Passing multiple trees widens READ resolution only (useful when one log
34
38
  * genuinely spans them); it does not let a root-log slice cross a subflow
35
39
  * mount — see README.md § Subflow boundaries.
40
+ *
41
+ * A live {@link ExecutionTree} or unknown saved JSON uses this same traversal.
42
+ * Malformed reader fields throw a TypeError naming their source path; no partial
43
+ * reads strategy is returned. Optional fields may be absent or undefined, but
44
+ * null nodes, sparse arrays and wrongly shaped fields are not missing evidence
45
+ * to silently skip. Only read-map KEYS are inspected, never their stored values.
46
+ * Cycles/shared objects are visited once; existing traversal order is retained.
47
+ * Coverage counts observed nodes and nonempty read sets, not capture completeness.
48
+ * This is an ordinary-data boundary, not a sandbox for hostile proxies/accessors.
36
49
  */
37
50
  export function keysReadFromExecutionTree(tree) {
38
51
  const byStep = new Map();
39
52
  let steps = 0;
40
53
  // The tree is acyclic by construction (next/children), but this walker is
41
- // also handed CONSUMER-provided data — a visited set makes a malformed or
54
+ // also handed CONSUMER-provided data — a visited set makes a cycle in a
42
55
  // hand-built tree a non-event instead of an infinite loop.
43
56
  const visited = new Set();
44
- const stack = [tree].flat(); // one root, or several (subflow trees); a hole is skipped
57
+ const stack = [];
58
+ if (Array.isArray(tree)) {
59
+ // Indexing retains holes as invalid evidence, unlike flat()/map().
60
+ for (let i = 0; i < tree.length; i++)
61
+ stack.push({ node: tree[i], path: `$[${i}]` });
62
+ }
63
+ else {
64
+ stack.push({ node: tree, path: '$' });
65
+ }
45
66
  while (stack.length > 0) {
46
- const node = stack.pop();
67
+ const { node, path } = stack.pop();
68
+ if (!isReadTreeObject(node))
69
+ throw new TypeError(`keysReadFromExecutionTree: ${path} must be an object`);
47
70
  if (visited.has(node))
48
71
  continue;
49
72
  visited.add(node);
50
- if (node.runtimeStageId) {
73
+ const { runtimeStageId, stageReads, next, children } = node;
74
+ if (runtimeStageId !== undefined && typeof runtimeStageId !== 'string') {
75
+ throw new TypeError(`keysReadFromExecutionTree: ${path}.runtimeStageId must be a string when present`);
76
+ }
77
+ if (stageReads !== undefined && !isReadTreeObject(stageReads)) {
78
+ throw new TypeError(`keysReadFromExecutionTree: ${path}.stageReads must be an object when present`);
79
+ }
80
+ if (children !== undefined && !Array.isArray(children)) {
81
+ throw new TypeError(`keysReadFromExecutionTree: ${path}.children must be an array when present`);
82
+ }
83
+ if (runtimeStageId) {
51
84
  steps++;
52
- if (node.stageReads) {
53
- const keys = Object.keys(node.stageReads);
85
+ if (stageReads !== undefined) {
86
+ const keys = Object.keys(stageReads);
54
87
  if (keys.length > 0)
55
- byStep.set(node.runtimeStageId, keys);
88
+ byStep.set(runtimeStageId, keys);
56
89
  }
57
90
  }
58
- if (node.next)
59
- stack.push(node.next);
60
- if (node.children)
61
- for (const c of node.children)
62
- stack.push(c);
91
+ if (next !== undefined)
92
+ stack.push({ node: next, path: `${path}.next` });
93
+ if (children !== undefined) {
94
+ for (let i = 0; i < children.length; i++)
95
+ stack.push({ node: children[i], path: `${path}.children[${i}]` });
96
+ }
63
97
  }
64
98
  return {
65
99
  kind: 'execution-tree',
@@ -41,6 +41,10 @@ export declare function readTree(value: unknown): ExecutionTree | undefined;
41
41
  * `changedSince` reads its paths), so a row without one is not a bundle the
42
42
  * fold can survive — it is the crash a gap exists to prevent. `CommitBundle`
43
43
  * declares it required, so no live log is affected.
44
+ * - Each trace entry needs a known verb and a string `path`. Readers use
45
+ * string path operations; accepting a number can silently lose a causal
46
+ * writer in a short log and crash the indexed lookup in a longer one.
47
+ * Empty strings and historical path spellings remain valid strings.
44
48
  * - `updates` / `overwrite`, when present, must be plain objects. They are
45
49
  * read only AT THE PATHS THE TRACE NAMES, so an absent one is harmless and
46
50
  * is tolerated, exactly as the fold tolerates an absent `redactedPaths`.
@@ -63,6 +63,10 @@ export function readTree(value) {
63
63
  * `changedSince` reads its paths), so a row without one is not a bundle the
64
64
  * fold can survive — it is the crash a gap exists to prevent. `CommitBundle`
65
65
  * declares it required, so no live log is affected.
66
+ * - Each trace entry needs a known verb and a string `path`. Readers use
67
+ * string path operations; accepting a number can silently lose a causal
68
+ * writer in a short log and crash the indexed lookup in a longer one.
69
+ * Empty strings and historical path spellings remain valid strings.
66
70
  * - `updates` / `overwrite`, when present, must be plain objects. They are
67
71
  * read only AT THE PATHS THE TRACE NAMES, so an absent one is harmless and
68
72
  * is tolerated, exactly as the fold tolerates an absent `redactedPaths`.
@@ -95,6 +99,11 @@ export function bundleRefusal(row) {
95
99
  if (!isVerb(verb)) {
96
100
  return `trace[${i}].verb is ${verb === undefined ? 'missing' : JSON.stringify(verb)}, not ${VERBS.join(' | ')}`;
97
101
  }
102
+ const path = isPlainish(entry) ? entry.path : undefined;
103
+ if (typeof path !== 'string') {
104
+ const kind = path === undefined ? 'missing' : path === null ? 'null' : Array.isArray(path) ? 'an array' : `a ${typeof path}`;
105
+ return `trace[${i}].path is ${kind}, not a string`;
106
+ }
98
107
  }
99
108
  return undefined;
100
109
  }
@@ -11,6 +11,10 @@
11
11
  */
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.resolveKeysReadSource = exports.keysReadFromMap = exports.keysReadFromExecutionTree = void 0;
14
+ /** Narrow only the object shape this reader uses; stored values stay opaque. */
15
+ function isReadTreeObject(value) {
16
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
17
+ }
14
18
  /**
15
19
  * Post-hoc reads from a finished run's execution tree — ZERO setup.
16
20
  *
@@ -36,33 +40,63 @@ exports.resolveKeysReadSource = exports.keysReadFromMap = exports.keysReadFromEx
36
40
  * Passing multiple trees widens READ resolution only (useful when one log
37
41
  * genuinely spans them); it does not let a root-log slice cross a subflow
38
42
  * mount — see README.md § Subflow boundaries.
43
+ *
44
+ * A live {@link ExecutionTree} or unknown saved JSON uses this same traversal.
45
+ * Malformed reader fields throw a TypeError naming their source path; no partial
46
+ * reads strategy is returned. Optional fields may be absent or undefined, but
47
+ * null nodes, sparse arrays and wrongly shaped fields are not missing evidence
48
+ * to silently skip. Only read-map KEYS are inspected, never their stored values.
49
+ * Cycles/shared objects are visited once; existing traversal order is retained.
50
+ * Coverage counts observed nodes and nonempty read sets, not capture completeness.
51
+ * This is an ordinary-data boundary, not a sandbox for hostile proxies/accessors.
39
52
  */
40
53
  function keysReadFromExecutionTree(tree) {
41
54
  const byStep = new Map();
42
55
  let steps = 0;
43
56
  // The tree is acyclic by construction (next/children), but this walker is
44
- // also handed CONSUMER-provided data — a visited set makes a malformed or
57
+ // also handed CONSUMER-provided data — a visited set makes a cycle in a
45
58
  // hand-built tree a non-event instead of an infinite loop.
46
59
  const visited = new Set();
47
- const stack = [tree].flat(); // one root, or several (subflow trees); a hole is skipped
60
+ const stack = [];
61
+ if (Array.isArray(tree)) {
62
+ // Indexing retains holes as invalid evidence, unlike flat()/map().
63
+ for (let i = 0; i < tree.length; i++)
64
+ stack.push({ node: tree[i], path: `$[${i}]` });
65
+ }
66
+ else {
67
+ stack.push({ node: tree, path: '$' });
68
+ }
48
69
  while (stack.length > 0) {
49
- const node = stack.pop();
70
+ const { node, path } = stack.pop();
71
+ if (!isReadTreeObject(node))
72
+ throw new TypeError(`keysReadFromExecutionTree: ${path} must be an object`);
50
73
  if (visited.has(node))
51
74
  continue;
52
75
  visited.add(node);
53
- if (node.runtimeStageId) {
76
+ const { runtimeStageId, stageReads, next, children } = node;
77
+ if (runtimeStageId !== undefined && typeof runtimeStageId !== 'string') {
78
+ throw new TypeError(`keysReadFromExecutionTree: ${path}.runtimeStageId must be a string when present`);
79
+ }
80
+ if (stageReads !== undefined && !isReadTreeObject(stageReads)) {
81
+ throw new TypeError(`keysReadFromExecutionTree: ${path}.stageReads must be an object when present`);
82
+ }
83
+ if (children !== undefined && !Array.isArray(children)) {
84
+ throw new TypeError(`keysReadFromExecutionTree: ${path}.children must be an array when present`);
85
+ }
86
+ if (runtimeStageId) {
54
87
  steps++;
55
- if (node.stageReads) {
56
- const keys = Object.keys(node.stageReads);
88
+ if (stageReads !== undefined) {
89
+ const keys = Object.keys(stageReads);
57
90
  if (keys.length > 0)
58
- byStep.set(node.runtimeStageId, keys);
91
+ byStep.set(runtimeStageId, keys);
59
92
  }
60
93
  }
61
- if (node.next)
62
- stack.push(node.next);
63
- if (node.children)
64
- for (const c of node.children)
65
- stack.push(c);
94
+ if (next !== undefined)
95
+ stack.push({ node: next, path: `${path}.next` });
96
+ if (children !== undefined) {
97
+ for (let i = 0; i < children.length; i++)
98
+ stack.push({ node: children[i], path: `${path}.children[${i}]` });
99
+ }
66
100
  }
67
101
  return {
68
102
  kind: 'execution-tree',
@@ -68,6 +68,10 @@ exports.readTree = readTree;
68
68
  * `changedSince` reads its paths), so a row without one is not a bundle the
69
69
  * fold can survive — it is the crash a gap exists to prevent. `CommitBundle`
70
70
  * declares it required, so no live log is affected.
71
+ * - Each trace entry needs a known verb and a string `path`. Readers use
72
+ * string path operations; accepting a number can silently lose a causal
73
+ * writer in a short log and crash the indexed lookup in a longer one.
74
+ * Empty strings and historical path spellings remain valid strings.
71
75
  * - `updates` / `overwrite`, when present, must be plain objects. They are
72
76
  * read only AT THE PATHS THE TRACE NAMES, so an absent one is harmless and
73
77
  * is tolerated, exactly as the fold tolerates an absent `redactedPaths`.
@@ -100,6 +104,11 @@ function bundleRefusal(row) {
100
104
  if (!(0, verbs_js_1.isVerb)(verb)) {
101
105
  return `trace[${i}].verb is ${verb === undefined ? 'missing' : JSON.stringify(verb)}, not ${verbs_js_1.VERBS.join(' | ')}`;
102
106
  }
107
+ const path = isPlainish(entry) ? entry.path : undefined;
108
+ if (typeof path !== 'string') {
109
+ const kind = path === undefined ? 'missing' : path === null ? 'null' : Array.isArray(path) ? 'an array' : `a ${typeof path}`;
110
+ return `trace[${i}].path is ${kind}, not a string`;
111
+ }
103
112
  }
104
113
  return undefined;
105
114
  }
@@ -9,7 +9,6 @@
9
9
  * the call site instead: `(id) => recorder.getByKey(id)?.keysRead ?? []`.
10
10
  */
11
11
  import type { KeysReadLookup } from '../memory/backtrack.js';
12
- import type { ExecutionTree } from '../memory/types.js';
13
12
  import type { KeysReadSource } from './types.js';
14
13
  /**
15
14
  * Post-hoc reads from a finished run's execution tree — ZERO setup.
@@ -36,8 +35,17 @@ import type { KeysReadSource } from './types.js';
36
35
  * Passing multiple trees widens READ resolution only (useful when one log
37
36
  * genuinely spans them); it does not let a root-log slice cross a subflow
38
37
  * mount — see README.md § Subflow boundaries.
38
+ *
39
+ * A live {@link ExecutionTree} or unknown saved JSON uses this same traversal.
40
+ * Malformed reader fields throw a TypeError naming their source path; no partial
41
+ * reads strategy is returned. Optional fields may be absent or undefined, but
42
+ * null nodes, sparse arrays and wrongly shaped fields are not missing evidence
43
+ * to silently skip. Only read-map KEYS are inspected, never their stored values.
44
+ * Cycles/shared objects are visited once; existing traversal order is retained.
45
+ * Coverage counts observed nodes and nonempty read sets, not capture completeness.
46
+ * This is an ordinary-data boundary, not a sandbox for hostile proxies/accessors.
39
47
  */
40
- export declare function keysReadFromExecutionTree(tree: ExecutionTree | readonly ExecutionTree[]): KeysReadSource;
48
+ export declare function keysReadFromExecutionTree(tree: unknown): KeysReadSource;
41
49
  /**
42
50
  * Reads from a prebuilt map — e.g. collected live from
43
51
  * `ScopeRecorder.onRead` events, or deserialized from a stored trace.
@@ -41,6 +41,10 @@ export declare function readTree(value: unknown): ExecutionTree | undefined;
41
41
  * `changedSince` reads its paths), so a row without one is not a bundle the
42
42
  * fold can survive — it is the crash a gap exists to prevent. `CommitBundle`
43
43
  * declares it required, so no live log is affected.
44
+ * - Each trace entry needs a known verb and a string `path`. Readers use
45
+ * string path operations; accepting a number can silently lose a causal
46
+ * writer in a short log and crash the indexed lookup in a longer one.
47
+ * Empty strings and historical path spellings remain valid strings.
44
48
  * - `updates` / `overwrite`, when present, must be plain objects. They are
45
49
  * read only AT THE PATHS THE TRACE NAMES, so an absent one is harmless and
46
50
  * is tolerated, exactly as the fold tolerates an absent `redactedPaths`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foottrace",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "type": "commonjs",
5
5
  "description": "Execution records, replay, time travel and honest attribution — independent of the engine that produced them",
6
6
  "license": "MIT",