foottrace 1.0.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +137 -4
  2. package/dist/cause.js +14 -0
  3. package/dist/esm/cause.d.ts +8 -0
  4. package/dist/esm/cause.js +5 -0
  5. package/dist/esm/index.d.ts +7 -2
  6. package/dist/esm/index.js +2 -1
  7. package/dist/esm/lib/cause/admission.d.ts +27 -0
  8. package/dist/esm/lib/cause/admission.js +59 -0
  9. package/dist/esm/lib/cause/explain.d.ts +5 -0
  10. package/dist/esm/lib/cause/explain.js +114 -0
  11. package/dist/esm/lib/cause/ranking.d.ts +6 -0
  12. package/dist/esm/lib/cause/ranking.js +10 -0
  13. package/dist/esm/lib/cause/recordAdapter.d.ts +9 -0
  14. package/dist/esm/lib/cause/recordAdapter.js +49 -0
  15. package/dist/esm/lib/cause/recordChildren.d.ts +4 -0
  16. package/dist/esm/lib/cause/recordChildren.js +79 -0
  17. package/dist/esm/lib/cause/recordControl.d.ts +13 -0
  18. package/dist/esm/lib/cause/recordControl.js +66 -0
  19. package/dist/esm/lib/cause/recordCoverage.d.ts +24 -0
  20. package/dist/esm/lib/cause/recordCoverage.js +87 -0
  21. package/dist/esm/lib/cause/recordIdentity.d.ts +9 -0
  22. package/dist/esm/lib/cause/recordIdentity.js +67 -0
  23. package/dist/esm/lib/cause/recordLoading.d.ts +7 -0
  24. package/dist/esm/lib/cause/recordLoading.js +116 -0
  25. package/dist/esm/lib/cause/recordParents.d.ts +4 -0
  26. package/dist/esm/lib/cause/recordParents.js +86 -0
  27. package/dist/esm/lib/cause/recordScope.d.ts +8 -0
  28. package/dist/esm/lib/cause/recordScope.js +60 -0
  29. package/dist/esm/lib/cause/recordTypes.d.ts +67 -0
  30. package/dist/esm/lib/cause/recordTypes.js +1 -0
  31. package/dist/esm/lib/cause/recordValidation.d.ts +33 -0
  32. package/dist/esm/lib/cause/recordValidation.js +198 -0
  33. package/dist/esm/lib/cause/session.d.ts +30 -0
  34. package/dist/esm/lib/cause/session.js +161 -0
  35. package/dist/esm/lib/cause/storedRecordAdapter.d.ts +11 -0
  36. package/dist/esm/lib/cause/storedRecordAdapter.js +57 -0
  37. package/dist/esm/lib/cause/strategies.d.ts +3 -0
  38. package/dist/esm/lib/cause/strategies.js +21 -0
  39. package/dist/esm/lib/cause/types.d.ts +90 -0
  40. package/dist/esm/lib/cause/types.js +1 -0
  41. package/dist/esm/lib/cause/validation.d.ts +24 -0
  42. package/dist/esm/lib/cause/validation.js +134 -0
  43. package/dist/esm/lib/cause/walk.d.ts +12 -0
  44. package/dist/esm/lib/cause/walk.js +174 -0
  45. package/dist/esm/lib/ids/attemptId.d.ts +63 -0
  46. package/dist/esm/lib/ids/attemptId.js +91 -0
  47. package/dist/esm/lib/memory/EventLog.d.ts +89 -3
  48. package/dist/esm/lib/memory/EventLog.js +242 -3
  49. package/dist/esm/lib/memory/honesty.d.ts +6 -4
  50. package/dist/esm/lib/memory/honesty.js +7 -4
  51. package/dist/esm/lib/memory/sink.d.ts +148 -0
  52. package/dist/esm/lib/memory/sink.js +92 -0
  53. package/dist/esm/lib/memory/types.d.ts +4 -1
  54. package/dist/esm/lib/memory/types.js +1 -1
  55. package/dist/esm/lib/time-travel/bundles.d.ts +2 -0
  56. package/dist/esm/lib/time-travel/bundles.js +16 -0
  57. package/dist/esm/lib/time-travel/commitStops.js +21 -2
  58. package/dist/esm/lib/time-travel/index.d.ts +2 -0
  59. package/dist/esm/lib/time-travel/index.js +1 -0
  60. package/dist/esm/lib/time-travel/storedRecord.d.ts +195 -0
  61. package/dist/esm/lib/time-travel/storedRecord.js +494 -0
  62. package/dist/esm/lib/time-travel/timeTravel.js +11 -0
  63. package/dist/esm/lib/time-travel/types.d.ts +4 -1
  64. package/dist/esm/write.d.ts +7 -0
  65. package/dist/esm/write.js +6 -0
  66. package/dist/index.js +6 -2
  67. package/dist/lib/cause/admission.js +67 -0
  68. package/dist/lib/cause/explain.js +119 -0
  69. package/dist/lib/cause/ranking.js +14 -0
  70. package/dist/lib/cause/recordAdapter.js +55 -0
  71. package/dist/lib/cause/recordChildren.js +83 -0
  72. package/dist/lib/cause/recordControl.js +74 -0
  73. package/dist/lib/cause/recordCoverage.js +99 -0
  74. package/dist/lib/cause/recordIdentity.js +73 -0
  75. package/dist/lib/cause/recordLoading.js +121 -0
  76. package/dist/lib/cause/recordParents.js +90 -0
  77. package/dist/lib/cause/recordScope.js +66 -0
  78. package/dist/lib/cause/recordTypes.js +2 -0
  79. package/dist/lib/cause/recordValidation.js +213 -0
  80. package/dist/lib/cause/session.js +165 -0
  81. package/dist/lib/cause/storedRecordAdapter.js +61 -0
  82. package/dist/lib/cause/strategies.js +26 -0
  83. package/dist/lib/cause/types.js +2 -0
  84. package/dist/lib/cause/validation.js +150 -0
  85. package/dist/lib/cause/walk.js +180 -0
  86. package/dist/lib/ids/attemptId.js +96 -0
  87. package/dist/lib/memory/EventLog.js +242 -3
  88. package/dist/lib/memory/honesty.js +7 -4
  89. package/dist/lib/memory/sink.js +98 -0
  90. package/dist/lib/memory/types.js +1 -1
  91. package/dist/lib/time-travel/bundles.js +18 -1
  92. package/dist/lib/time-travel/commitStops.js +21 -2
  93. package/dist/lib/time-travel/index.js +3 -1
  94. package/dist/lib/time-travel/storedRecord.js +498 -0
  95. package/dist/lib/time-travel/timeTravel.js +11 -0
  96. package/dist/types/cause.d.ts +8 -0
  97. package/dist/types/index.d.ts +7 -2
  98. package/dist/types/lib/cause/admission.d.ts +27 -0
  99. package/dist/types/lib/cause/explain.d.ts +5 -0
  100. package/dist/types/lib/cause/ranking.d.ts +6 -0
  101. package/dist/types/lib/cause/recordAdapter.d.ts +9 -0
  102. package/dist/types/lib/cause/recordChildren.d.ts +4 -0
  103. package/dist/types/lib/cause/recordControl.d.ts +13 -0
  104. package/dist/types/lib/cause/recordCoverage.d.ts +24 -0
  105. package/dist/types/lib/cause/recordIdentity.d.ts +9 -0
  106. package/dist/types/lib/cause/recordLoading.d.ts +7 -0
  107. package/dist/types/lib/cause/recordParents.d.ts +4 -0
  108. package/dist/types/lib/cause/recordScope.d.ts +8 -0
  109. package/dist/types/lib/cause/recordTypes.d.ts +67 -0
  110. package/dist/types/lib/cause/recordValidation.d.ts +33 -0
  111. package/dist/types/lib/cause/session.d.ts +30 -0
  112. package/dist/types/lib/cause/storedRecordAdapter.d.ts +11 -0
  113. package/dist/types/lib/cause/strategies.d.ts +3 -0
  114. package/dist/types/lib/cause/types.d.ts +90 -0
  115. package/dist/types/lib/cause/validation.d.ts +24 -0
  116. package/dist/types/lib/cause/walk.d.ts +12 -0
  117. package/dist/types/lib/ids/attemptId.d.ts +63 -0
  118. package/dist/types/lib/memory/EventLog.d.ts +89 -3
  119. package/dist/types/lib/memory/honesty.d.ts +6 -4
  120. package/dist/types/lib/memory/sink.d.ts +148 -0
  121. package/dist/types/lib/memory/types.d.ts +4 -1
  122. package/dist/types/lib/time-travel/bundles.d.ts +2 -0
  123. package/dist/types/lib/time-travel/index.d.ts +2 -0
  124. package/dist/types/lib/time-travel/storedRecord.d.ts +195 -0
  125. package/dist/types/lib/time-travel/types.d.ts +4 -1
  126. package/dist/types/write.d.ts +7 -0
  127. package/dist/write.js +8 -1
  128. package/package.json +67 -13
package/README.md CHANGED
@@ -6,15 +6,16 @@ This package extracts FootPrint's established record layer. It does not introduc
6
6
 
7
7
  Foottrace owns the record layer used by footprintjs 10. Older stored records remain readable without installing their original engine.
8
8
 
9
- ## Three entry points
9
+ ## Four entry points
10
10
 
11
11
  | Import | Responsibility |
12
12
  | --- | --- |
13
- | `foottrace` | Record types, runtime IDs, replay, time-travel cursors, causal and variable slices, honesty codes |
14
- | `foottrace/write` | `SharedMemory`, `EventLog`, `RecordFrame`, encoding and write types, writer-owned freezing |
13
+ | `foottrace` | Record types, runtime and attempt IDs, replay, time-travel cursors, causal and variable slices, honesty codes, the stored-record reader |
14
+ | `foottrace/write` | `SharedMemory`, `EventLog`, `RecordFrame`, encoding and write types, writer-owned freezing, the record sink port and `memorySink` |
15
15
  | `foottrace/paths` | Record-path encoding/decoding, nested access and safe diagnostic writes |
16
+ | `foottrace/cause` (ESM) | Optional bounded attention, cause and impact queries; in-memory and lazily loaded stored-record adapters |
16
17
 
17
- Every declaration has one public entry point. There are no public deep imports or `/testing` entry point. FootPrint's engine, observer channels, recorders and stores remain in `footprintjs`.
18
+ Every declaration has one public entry point. Producer-facing `Attention` and `AttentionKind` are type-only root exports; the cause algorithms live only on `/cause`. Existing root, `/write` and `/paths` imports retain their ESM and CommonJS doors. There are no public deep imports or `/testing` entry point. FootPrint's engine, observer channels, recorders and stores remain in `footprintjs`.
18
19
 
19
20
  ## Write and replay
20
21
 
@@ -42,6 +43,45 @@ Writers record during execution. Readers query what was recorded; they do not re
42
43
 
43
44
  See the [record contract](docs/guides/record-contract.md), [time-travel contract](src/lib/time-travel/README.md), and [slice contract](src/lib/slice/README.md).
44
45
 
46
+ ## A record that survives the run
47
+
48
+ A log can also hand every entry to a sink **as it lands**, so a run that is killed still leaves its record behind. The sink sees four entries — `opened` (the base), `commit` (each bundle, after it is frozen), `note` (anything the writer wants kept beside the log, as it happens) and `closed` (how the log ended) — and `memorySink` is the reference adapter. Write your own for a file, a bucket or a table by implementing those four methods.
49
+
50
+ ```ts
51
+ import { readStoredRecord, timeTravel } from 'foottrace';
52
+ import { EventLog, RecordFrame, SharedMemory, memorySink } from 'foottrace/write';
53
+
54
+ const sink = memorySink();
55
+ const state = new SharedMemory(undefined, { count: 0 });
56
+ const log = new EventLog(state.getState(), { sink, address: { logRunId: 'run-1', drillPath: [] } });
57
+ const frame = new RecordFrame(state, log);
58
+ frame.write(frame.at([], 'count'), 1, 'set');
59
+ frame.commit(() => ({ stage: 'Increment', stageId: 'increment', runtimeStageId: 'increment#0' }));
60
+ log.note('attempt', { attempt: 1, outcome: 'timeout' });
61
+ // … the process dies here: no log.close('ok') …
62
+
63
+ const run = readStoredRecord(sink.entries).root('run-1')!;
64
+ run.complete; // false — the log never closed
65
+ run.notes; // [{ kind: 'attempt', at, data: { attempt: 1, outcome: 'timeout' } }]
66
+ timeTravel(run.source()).last(); // the cursor over what landed
67
+ ```
68
+
69
+ A failing sink never stops the writer: a throw or a rejected promise is counted (`log.sinkFailures`) and reported to `sink.onFailure`. The reader treats what storage gives back as untrusted (`Iterable<unknown>`) and checks each entry: one it does not read into a log is listed in `unread` by name, and a commit sits at its own index, so a commit the store lost is a gap the fold reports by index — never a shifted log. `complete` means the writer closed the log and the reader read it all. Its work is bounded by four documented limits (`StoredRecordLimits`, raised per call). A sink adds nothing to the log's bytes; `meta` and a note's data are opaque, stored and handed back, never read. See [time travel](src/lib/time-travel/README.md#6-a-stored-record-reads-back--even-a-run-that-never-ended-110) and the [record contract](docs/guides/record-contract.md#a-stored-record).
70
+
71
+ ## Attempt IDs
72
+
73
+ An attempt under a stage execution has its own ID: `charge#4@2` is attempt 2 of `charge#4`, and `charge#4@2.1@3` is attempt 3 of the first call that stage attempt made. Counts start at 1.
74
+
75
+ ```ts
76
+ import { buildAttemptId, parseAttemptId } from 'foottrace';
77
+
78
+ buildAttemptId('charge#4', 2, { ordinal: 1, attempt: 3 }); // 'charge#4@2.1@3'
79
+ parseAttemptId('charge#4@2'); // { runtimeStageId: 'charge#4', attempt: 2 }
80
+ parseAttemptId('charge#4'); // undefined — a stage execution, not an attempt
81
+ ```
82
+
83
+ The parser reads from the right, so a stage ID may contain `@` or `.` and no character is newly reserved. See [ids/](src/lib/ids/README.md#attempt-ids-110).
84
+
45
85
  ## Paths are not interchangeable
46
86
 
47
87
  - `idPathSegments` (root entry) splits runtime IDs on `/` and drops empty segments. `isExecutionKey` detects `#`; it is not a complete validator.
@@ -72,3 +112,96 @@ No engine is installed in this package's test environment. Engine integration an
72
112
  Record bytes are part of the versioning promise: refactors never re-pin; a named law fix requires a documented minor release; any other byte change requires a major. See the [fixture policy](test/fixtures/README.md).
73
113
 
74
114
  MIT license. Contact: sanjay@footprintjs.dev.
115
+
116
+
117
+ ## Cause and impact — 1.2.0 candidate
118
+
119
+ `foottrace/cause` reads recorded dependencies; it never reruns business code, calls a model, or accesses storage by itself. An application supplies a graph or a stored-record host. `explain(graph, nodeId)` walks the selected node's recorded data and control dependencies, identifies the earliest marked ancestors in that dependency order, and walks forward to report impact. Independent candidates remain `alternatives`; timestamps and ID order never break a causal tie. A missing or cut backward dependency withholds `rootCause`; missing evidence in either direction returns `confidence: 'incomplete'` with fixed gap codes. Confidence describes evidence coverage, not proof of a provider's internal failure.
120
+
121
+ **For precise per-write cause queries, enable `writeProvenance: 'reads-prefix'` when recording the flow.** Step-level data edges can add false ancestors; the record adapter marks that fallback `conservative`, so those queries return incomplete evidence and alternatives rather than a definite root. This setting cannot restore provenance in old records and does not guarantee a root: complete recorded read/control and attention coverage, one supported candidate and no backward gaps are still required. Other graph adapters can provide their own precise dependency evidence.
122
+
123
+ **Module compatibility:** `/cause` has an `import` export condition only. CommonJS consumers must use native `import('foottrace/cause')`; `require('foottrace/cause')` is not exported, even on Node versions that support requiring ESM. Preserve native `import()` in the compiled CommonJS loader: compilers that rewrite it to `require()` need a dedicated import-preserving bridge. Existing root, `/write` and `/paths` CommonJS imports remain supported.
124
+
125
+ ```ts
126
+ import type { Attention } from 'foottrace';
127
+ import { attentionList, explain, type CauseGraph } from 'foottrace/cause';
128
+
129
+ // The host owns authentication, storage, graph identities and attention rules.
130
+ // Each method receives a cancellation signal and may load only the requested node.
131
+ declare const graph: CauseGraph;
132
+ declare const selectedNodeId: string;
133
+ const marked = await attentionList(graph, {
134
+ nodeIds: [selectedNodeId], maxNodes: 50, maxCalls: 100, timeoutMs: 500,
135
+ });
136
+ const finding = await explain(graph, selectedNodeId, {
137
+ maxNodes: 50, maxDepth: 10, maxEdges: 100, maxCalls: 200, timeoutMs: 500,
138
+ });
139
+ // Display finding.attention, rootCause or alternatives, path, impact and gaps.
140
+ // An LLM can phrase these validated findings without inventing a cause.
141
+ ```
142
+
143
+ Attention marks contain a fixed reason code and evidence references, never a raw error or state value. Every mark needs a proof reference except `not-collected`, which records absence. The application decides which business conditions deserve a mark. `error`, `unknown-effect`, `gave-up`, `timeout`, `retried`, `quality-drop`, `not-collected` and `outlier` are standard kinds; future bounded kind names remain readable. An absent mark does not prove a node was healthy. `largestDrop(scores)` and `mostAffected(scores)` return optional root strategies over host-supplied, already recorded numeric scores; equal best scores remain tied and missing scores become gaps. The default uses the earliest marked ancestor, with no scoring model. An optional `rankImpact(nodes, context)` may reorder the discovered descendants, but cannot add, omit or duplicate references; failed ranking preserves the unranked references and reports a gap.
144
+
145
+ The default earliest-ancestor rule does not restrict a custom `rankRoots`: that callback receives all supported marked nodes in the backward walk, including a marked selected node, and may select a subset containing a nearer ancestor. It cannot introduce foreign or duplicate identities. This differs from `rankImpact`, which must return an exact permutation of every discovered descendant. Name the chosen root strategy when presenting its findings; a custom choice is not the default earliest-ancestor conclusion.
146
+
147
+ Query defaults are 100 nodes, depth 20, 1,000 edges, 1,000 calls and one second. Hard ceilings are 10,000 nodes, depth 1,000, 100,000 edges/calls and 30 seconds. The same budget covers both directions and ranking. Callbacks are trusted executable code: asynchronous waiting is bounded and receives an abort signal, while synchronous callbacks must cooperate. A late synchronous result is rejected after it returns. Hosts must use non-secret node and evidence IDs; the reader does not classify arbitrary ID strings for personal information.
148
+
149
+ Async hosts must return ordinary current-realm native Promises with no own `constructor`; promise wrappers are trusted host control objects, while their resolved data is untrusted. Custom thenables, subclasses and constructor/species overrides are outside this contract and the host must handle their rejections itself. The reader never mutates a promise to observe it. Ordinary native rejections are consumed and become fixed gaps; synchronous data objects' `then` accessors are never invoked.
150
+
151
+ ### Per-write provenance recipe
152
+
153
+ For precise edges between individual writes, configure the producer's record encoding with `writeProvenance: 'reads-prefix'`. `RecordFrame` captures the reads that preceded each write. For an execution that reads `a`, writes `x`, then reads `b` and writes `y`, the `x` query uses its recorded read prefix; the `y` query uses the later prefix. Without per-write provenance, the reader must conservatively consider all reads of that stage for either value and report that fallback. Later reads cannot be retroactively assigned to an earlier write. An overwritten version is distinct from the newer value, so impact follows the selected write rather than every later use of the same key name.
154
+
155
+ ```ts
156
+ import { EventLog, RecordFrame, SharedMemory } from 'foottrace/write';
157
+ const state = new SharedMemory(undefined, { a: 1, b: 2 });
158
+ const log = new EventLog(state.getState());
159
+ const frame = new RecordFrame(state, log);
160
+ frame.useEncoding({ commitValues: 'full', writeProvenance: 'reads-prefix' });
161
+ frame.noteRead([], 'a');
162
+ frame.write(frame.at([], 'x'), 1, 'set');
163
+ frame.noteRead([], 'b');
164
+ frame.write(frame.at([], 'y'), 3, 'set');
165
+ frame.commit(() => ({ stage: 'Derive', stageId: 'derive', runtimeStageId: 'derive#0' }));
166
+ frame.release();
167
+ ```
168
+
169
+ A stored host can expose one authorized log at a time:
170
+
171
+ ```ts
172
+ import { createStoredRecordGraph, explain, type StoredRecordHost } from 'foottrace/cause';
173
+
174
+ // loadLog(address, { signal }) returns native entries for exactly that address,
175
+ // plus recorded read/control/attention maps when available. It checks access.
176
+ declare const host: StoredRecordHost;
177
+ const graph = createStoredRecordGraph(host, {
178
+ maxScopes: 8, maxCommits: 256, maxFields: 20_000, maxEdges: 64, loadTimeoutMs: 500,
179
+ });
180
+ const address = { logRunId: 'run-1', drillPath: [] };
181
+ const selected = graph.nodeId(address, { runtimeStageId: 'derive#0', commitIdx: 0, key: 'y' });
182
+ const finding = await explain(graph, selected, { timeoutMs: 750, maxNodes: 50 });
183
+ // Only the requested scope is loaded; a different subflow is opened on demand.
184
+ ```
185
+
186
+ The stored adapter loads a requested log through the host, reuses `readStoredRecord`, and retains incomplete-record and gap evidence. A graph instance caches admitted scope snapshots; create a new instance to refresh a record that is still being written. Bind each host/graph/cache to one authorization domain or authorize before every query: a cache hit does not call `loadLog` again. Never share a global graph whose loader consults a changing request identity. Subflow containment is a navigation relationship, not automatically a causal dependency. Scope, execution, write position and optional key qualify node identities. The host must authorize each storage scope; cross-service and cross-run causal stitching remain outside this candidate. Lazy loading is real only when the host loader fetches a requested scope: expanding a cursor over an already loaded snapshot is an in-memory drill.
187
+
188
+ ### Established algorithms
189
+
190
+ Foundations: [Weiser, *Program slicing* (1981)](https://dl.acm.org/doi/10.5555/800078.802557), [Korel and Laski, *Dynamic program slicing* (1988)](https://doi.org/10.1016/0020-0190(88)90054-3), [Agrawal and Horgan, *Dynamic program slicing* (1990)](https://doi.org/10.1145/93542.93576), [Ferrante, Ottenstein and Warren, *The program dependence graph and its use in optimization* (1987)](https://doi.org/10.1145/24039.24041), and [Buneman, Khanna and Tan, *Why and Where: A Characterization of Data Provenance* (2001)](https://www.pure.ed.ac.uk/ws/files/16509989/Why_and_Where_A_Characterization_of_Data_Provenance.pdf). These are the historical techniques; this reader applies them only to dependencies present in a recording.
191
+
192
+ | Query | Established name |
193
+ | --- | --- |
194
+ | Recorded ancestors influencing a value or node | **Backward dynamic program slicing** (Weiser, 1981; Korel and Laski, 1988; Agrawal and Horgan, 1990) |
195
+ | Recorded data plus decision/control edges | **Program dependence graph** (Ferrante, Ottenstein and Warren, 1987) |
196
+ | Recorded downstream effects | **Forward slicing**, or change-impact analysis |
197
+ | Why a value exists and where it came from | **Why- and where-provenance** (Buneman, Khanna and Tan, 2001) |
198
+ | Loading only queried graph nodes/scopes | **Demand-driven, or lazy, graph traversal** |
199
+ | Default candidate selection | **Root-cause localisation on a dependency DAG**, using the earliest anomalous ancestor |
200
+
201
+ These names describe the techniques, not a claim of complete language-level instrumentation. Findings are bounded by the dependencies the producer recorded. Cause queries share one deadline and node/edge/call budget across reverse traversal, ranking and forward impact; limits, cancellation and unavailable reads become gaps. Output carries references and fixed codes, never a serialized DAG or business payload. `/cause` is ESM-only and `sideEffects: false`; its unused-import browser bundle is tested to match a bundle without the import. The package has no runtime dependencies. This is an unpublished candidate; later engine stamping, viewer integration and agent tools are separate changes.
202
+
203
+ Direct `graph.openScope(address, { signal })` discovery rejects with a fixed `cause: record unavailable` error and a gap `code` on failure; an unavailable scope is never reported as an empty successful log. Public `explain` and `attentionList` queries turn unavailable reads into bounded incomplete findings.
204
+
205
+ Record-adapter coverage is conservative: its shared scope gaps can withhold a cause when a local forward edge cap is reached, even if the visible backward path is covered. A withheld cause is unknown under those bounds; it is not proof that no cause exists.
206
+
207
+ Package verification retains strict automatic entrypoint discovery for every door except `/cause`, then checks only `/cause` with Are the Types Wrong’s `esm-only` profile. No resolution error is ignored globally; the regression test fixes that exception to this one ESM-only subpath.
package/dist/cause.js ADDED
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createStoredRecordGraph = exports.createRecordGraph = exports.mostAffected = exports.largestDrop = exports.attentionList = exports.explain = void 0;
4
+ /** Optional read-only cause and impact queries over recorded dependency graphs. */
5
+ var explain_js_1 = require("./lib/cause/explain.js");
6
+ Object.defineProperty(exports, "explain", { enumerable: true, get: function () { return explain_js_1.explain; } });
7
+ Object.defineProperty(exports, "attentionList", { enumerable: true, get: function () { return explain_js_1.attentionList; } });
8
+ var strategies_js_1 = require("./lib/cause/strategies.js");
9
+ Object.defineProperty(exports, "largestDrop", { enumerable: true, get: function () { return strategies_js_1.largestDrop; } });
10
+ Object.defineProperty(exports, "mostAffected", { enumerable: true, get: function () { return strategies_js_1.mostAffected; } });
11
+ var recordAdapter_js_1 = require("./lib/cause/recordAdapter.js");
12
+ Object.defineProperty(exports, "createRecordGraph", { enumerable: true, get: function () { return recordAdapter_js_1.createRecordGraph; } });
13
+ var storedRecordAdapter_js_1 = require("./lib/cause/storedRecordAdapter.js");
14
+ Object.defineProperty(exports, "createStoredRecordGraph", { enumerable: true, get: function () { return storedRecordAdapter_js_1.createStoredRecordGraph; } });
@@ -0,0 +1,8 @@
1
+ /** Optional read-only cause and impact queries over recorded dependency graphs. */
2
+ export { explain, attentionList } from './lib/cause/explain.js';
3
+ export { largestDrop, mostAffected } from './lib/cause/strategies.js';
4
+ export { createRecordGraph } from './lib/cause/recordAdapter.js';
5
+ export { createStoredRecordGraph } from './lib/cause/storedRecordAdapter.js';
6
+ export type { Adjacency, AttentionListOptions, AttentionListResult, AttentionReason, CauseGraph, CauseResult, Confidence, ExplainOptions, Gap, GapCode, GraphContext, GraphEdge, ImpactResult, ImpactStrategy, MarkedNode, MaybePromise, NodeId, RootStrategy, TraversedEdge, WalkLimits, } from './lib/cause/types.js';
7
+ export type { RecordScope, RecordGraphOptions, RecordCauseGraph, RecordNode } from './lib/cause/recordAdapter.js';
8
+ export type { StoredRecordScope, StoredRecordHost } from './lib/cause/storedRecordAdapter.js';
@@ -0,0 +1,5 @@
1
+ /** Optional read-only cause and impact queries over recorded dependency graphs. */
2
+ export { explain, attentionList } from './lib/cause/explain.js';
3
+ export { largestDrop, mostAffected } from './lib/cause/strategies.js';
4
+ export { createRecordGraph } from './lib/cause/recordAdapter.js';
5
+ export { createStoredRecordGraph } from './lib/cause/storedRecordAdapter.js';
@@ -1,6 +1,8 @@
1
1
  /** Read a recorded execution: replay, time travel, slicing and attribution. */
2
2
  export type { CommitIdx, ExecutionCounter, ExecutionIndex, RuntimeStageId } from './lib/ids/runtimeStageId.js';
3
3
  export { buildRuntimeStageId, createExecutionCounter, parseRuntimeStageId, splitStageId, } from './lib/ids/runtimeStageId.js';
4
+ export type { AttemptId } from './lib/ids/attemptId.js';
5
+ export { buildAttemptId, parseAttemptId } from './lib/ids/attemptId.js';
4
6
  export { UnknownVerbError } from './lib/memory/verbs.js';
5
7
  export type { CommitBundle, MemoryPatch, TraceEntry } from './lib/memory/types.js';
6
8
  export type { ExecutionTree } from './lib/memory/types.js';
@@ -8,8 +10,9 @@ export { applySmartMerge } from './lib/memory/verbs.js';
8
10
  export type { ValueBasis, ValueBasisOptions, ValueWithBasis, WriterBasis, WriterWithBasis, } from './lib/memory/commitLogUtils.js';
9
11
  export { buildCommitIndex, commitIndexOf, commitValueAt, commitValueAtWithBasis, findCommit, findCommits, findLastWriter, findLastWriterWithBasis, inferLegacyPhases, recordsPhases, } from './lib/memory/commitLogUtils.js';
10
12
  export type { CommitPhase } from './lib/memory/types.js';
11
- export type { AxisRefusal, AxisSplit, BookendedAxis, FoldBasis, FoldedState, FoldSource, LogGap, Mark, Move, MoveRefusal, Stop, StopFilter, StopKind, TimeTravel, TimeTravelOptions, TimeTravelSource, TimeTravelStrategy, } from './lib/time-travel/index.js';
12
- export { commitStops, commitStopsStrategy, filterStops, isCommitBundle, splitAxis, stateAt, tagStops, timeTravel, } from './lib/time-travel/index.js';
13
+ export type { AxisRefusal, AxisSplit, BookendedAxis, FoldBasis, FoldedState, FoldSource, LogGap, Mark, Move, MoveRefusal, Stop, StopFilter, StopKind, StoredLog, StoredNote, StoredRecord, StoredRecordLimits, TimeTravel, TimeTravelOptions, TimeTravelSource, TimeTravelStrategy, } from './lib/time-travel/index.js';
14
+ export { commitStops, commitStopsStrategy, filterStops, isCommitBundle, readStoredRecord, splitAxis, stateAt, tagStops, timeTravel, } from './lib/time-travel/index.js';
15
+ export type { LogOutcome, Meta } from './lib/memory/sink.js';
13
16
  export type { CausalChainOptions, CausalEdge, CausalNode, ControlDependency, ControlDepLookup, EdgeWeigher, KeysReadLookup, } from './lib/memory/backtrack.js';
14
17
  export { causalChain, flattenCausalDAG, formatCausalChain } from './lib/memory/backtrack.js';
15
18
  export type { UntrackedSource } from './lib/memory/types.js';
@@ -25,3 +28,5 @@ export type { EmitSourcePosition, LogAddress } from './lib/memory/eventPosition.
25
28
  export { EXECUTION_DELIMITER, PATH_DELIMITER, isExecutionKey, joinPath, pathSegments as idPathSegments, stageIdOf, subflowSegmentsOf, } from './lib/ids/runtimeStageId.js';
26
29
  export { deepEqual } from './lib/memory/equality.js';
27
30
  export { serveRecord } from './lib/capture/freeze.js';
31
+ /** Record attention types are shared with producers; the optional cause reader has its own door. */
32
+ export type { Attention, AttentionKind } from './lib/cause/types.js';
package/dist/esm/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  export { buildRuntimeStageId, createExecutionCounter, parseRuntimeStageId, splitStageId, } from './lib/ids/runtimeStageId.js';
2
+ export { buildAttemptId, parseAttemptId } from './lib/ids/attemptId.js';
2
3
  export { UnknownVerbError } from './lib/memory/verbs.js';
3
4
  export { applySmartMerge } from './lib/memory/verbs.js';
4
5
  export { buildCommitIndex, commitIndexOf, commitValueAt, commitValueAtWithBasis, findCommit, findCommits, findLastWriter, findLastWriterWithBasis, inferLegacyPhases, recordsPhases, } from './lib/memory/commitLogUtils.js';
5
- export { commitStops, commitStopsStrategy, filterStops, isCommitBundle, splitAxis, stateAt, tagStops, timeTravel, } from './lib/time-travel/index.js';
6
+ export { commitStops, commitStopsStrategy, filterStops, isCommitBundle, readStoredRecord, splitAxis, stateAt, tagStops, timeTravel, } from './lib/time-travel/index.js';
6
7
  export { causalChain, flattenCausalDAG, formatCausalChain } from './lib/memory/backtrack.js';
7
8
  export { arrayProvenance, elementProvenance, formatForwardSlice, formatSlice, formatTimeline, forwardSliceForKey, forwardSliceToJSON, keysReadFromExecutionTree, keysReadFromMap, keyTimeline, resolveKeysReadSource, sliceForKey, sliceToJSON, } from './lib/slice/index.js';
8
9
  export { HONESTY_CODES } from './lib/memory/honesty.js';
@@ -0,0 +1,27 @@
1
+ import type { GapCode } from './types.js';
2
+ /** One refusal representation; subclasses preserve the graph-port and record-loader boundaries. */
3
+ export declare class EvidenceRefusal extends Error {
4
+ readonly code: GapCode;
5
+ constructor(code: GapCode, message: string);
6
+ }
7
+ export type RefuseEvidence = (code?: GapCode) => Error;
8
+ interface DescriptorPolicy {
9
+ readonly translateTrapErrors: boolean;
10
+ readonly requireOwnValue: boolean;
11
+ }
12
+ /** Read data descriptors only; each boundary keeps its existing treatment of proxy traps. */
13
+ export declare function ownData(value: unknown, key: string, refuse: RefuseEvidence, policy: DescriptorPolicy): unknown;
14
+ /** Names share one grammar, with explicit limits and the record path separator exception. */
15
+ export declare function boundedName(value: unknown, maximum: number, refuse: RefuseEvidence, path?: boolean): string;
16
+ /** Bound iteration before reading entries, without invoking element accessors. */
17
+ export declare function boundedList<T>(value: unknown, maximum: number, read: (item: unknown) => T, options: {
18
+ readonly refuse: RefuseEvidence;
19
+ readonly overflow: GapCode;
20
+ readonly freeze: boolean;
21
+ readonly descriptors: DescriptorPolicy;
22
+ }): readonly T[];
23
+ /** Charge retained fields against the same mutable allowance across admission helpers. */
24
+ export declare function spendFields(budget: {
25
+ left: number;
26
+ }, count: number, refuse: RefuseEvidence): void;
27
+ export {};
@@ -0,0 +1,59 @@
1
+ /** One refusal representation; subclasses preserve the graph-port and record-loader boundaries. */
2
+ export class EvidenceRefusal extends Error {
3
+ code;
4
+ constructor(code, message) {
5
+ super(message);
6
+ this.code = code;
7
+ }
8
+ }
9
+ /** Read data descriptors only; each boundary keeps its existing treatment of proxy traps. */
10
+ export function ownData(value, key, refuse, policy) {
11
+ if (!value || typeof value !== 'object')
12
+ throw refuse();
13
+ let descriptor;
14
+ if (policy.translateTrapErrors) {
15
+ try {
16
+ descriptor = Object.getOwnPropertyDescriptor(value, key);
17
+ }
18
+ catch {
19
+ throw refuse();
20
+ }
21
+ }
22
+ else
23
+ descriptor = Object.getOwnPropertyDescriptor(value, key);
24
+ if (!descriptor)
25
+ return undefined;
26
+ if (policy.requireOwnValue ? !Object.hasOwn(descriptor, 'value') : !('value' in descriptor))
27
+ throw refuse();
28
+ return descriptor.value;
29
+ }
30
+ /** Names share one grammar, with explicit limits and the record path separator exception. */
31
+ export function boundedName(value, maximum, refuse, path = false) {
32
+ if (typeof value !== 'string' ||
33
+ !value.length ||
34
+ value.length > maximum ||
35
+ (path ? /[\u0000-\u001e\u007f-\u009f]/ : /[\u0000-\u001f\u007f-\u009f]/).test(value))
36
+ throw refuse();
37
+ return value;
38
+ }
39
+ /** Bound iteration before reading entries, without invoking element accessors. */
40
+ export function boundedList(value, maximum, read, options) {
41
+ const { refuse, overflow, freeze, descriptors } = options;
42
+ if (!Array.isArray(value))
43
+ throw refuse();
44
+ const length = ownData(value, 'length', refuse, descriptors);
45
+ if (typeof length !== 'number' || !Number.isSafeInteger(length) || length < 0)
46
+ throw refuse();
47
+ if (length > maximum)
48
+ throw refuse(overflow);
49
+ const result = [];
50
+ for (let index = 0; index < length; index++)
51
+ result.push(read(ownData(value, String(index), refuse, descriptors)));
52
+ return freeze ? Object.freeze(result) : result;
53
+ }
54
+ /** Charge retained fields against the same mutable allowance across admission helpers. */
55
+ export function spendFields(budget, count, refuse) {
56
+ budget.left -= count;
57
+ if (budget.left < 0)
58
+ throw refuse('node-limit');
59
+ }
@@ -0,0 +1,5 @@
1
+ import type { AttentionListOptions, AttentionListResult, CauseGraph, CauseResult, ExplainOptions, NodeId } from './types.js';
2
+ /** Read-only deterministic cause localisation and impact. No inference from timestamps or structural ancestry. */
3
+ export declare function explain(graph: CauseGraph, nodeId: NodeId, options?: ExplainOptions): Promise<CauseResult>;
4
+ /** Discover only bounded host seeds; this is not a global scan of arbitrary storage. */
5
+ export declare function attentionList(graph: CauseGraph, options?: AttentionListOptions): Promise<AttentionListResult>;
@@ -0,0 +1,114 @@
1
+ import { Session } from './session.js';
2
+ import { frontier, pathTo, walk } from './walk.js';
3
+ import { gap, name, nodeIds, own, uniqueGaps } from './validation.js';
4
+ import { admitRanking } from './ranking.js';
5
+ /** Read-only deterministic cause localisation and impact. No inference from timestamps or structural ancestry. */
6
+ export async function explain(graph, nodeId, options = {}) {
7
+ name(nodeId);
8
+ const session = new Session(graph, options);
9
+ try {
10
+ const backward = await walk(session, [nodeId], 'parents');
11
+ const missing = [...backward.gaps];
12
+ let candidates = frontier(backward).map((item) => item.nodeId);
13
+ const rank = own(options, 'rankRoots');
14
+ if (rank !== undefined) {
15
+ if (typeof rank !== 'function')
16
+ throw new TypeError('CAUSE_CONFIGURATION_INVALID');
17
+ const eligible = backward.marked.filter((item) => item.attention.some((mark) => mark.kind !== 'not-collected'));
18
+ const selection = await session.invoke(undefined, () => rank(Object.freeze(eligible), session.context), (raw) => admitRanking(raw, new Set(eligible.map((item) => item.nodeId)), {
19
+ exactPermutation: false,
20
+ maximum: session.limits.maxNodes,
21
+ }));
22
+ if (selection.gap)
23
+ missing.push(selection.gap, gap('strategy-failed'));
24
+ else
25
+ candidates = [...(selection.value ?? [])];
26
+ }
27
+ const stopped = session.stopGap(nodeId);
28
+ if (stopped)
29
+ missing.push(stopped);
30
+ const causeComplete = missing.length === 0;
31
+ const rootCause = causeComplete && candidates.length === 1 ? candidates[0] : undefined;
32
+ const forward = await walk(session, [nodeId], 'children');
33
+ let impactNodes = Object.freeze(forward.nodes.filter((id) => id !== nodeId));
34
+ const impactGaps = [...forward.gaps], rankImpact = own(options, 'rankImpact');
35
+ if (typeof rankImpact === 'function') {
36
+ const selection = await session.invoke(undefined, () => rankImpact(impactNodes, session.context), (raw) => admitRanking(raw, new Set(impactNodes), {
37
+ exactPermutation: true,
38
+ maximum: session.limits.maxNodes,
39
+ }));
40
+ if (selection.gap)
41
+ impactGaps.push(selection.gap, gap('strategy-failed'));
42
+ else
43
+ impactNodes = selection.value ?? impactNodes;
44
+ }
45
+ const impact = Object.freeze({
46
+ nodes: impactNodes,
47
+ edges: forward.edges,
48
+ confidence: impactGaps.length ? 'incomplete' : 'complete',
49
+ gaps: uniqueGaps(impactGaps),
50
+ });
51
+ const allGaps = uniqueGaps([...missing, ...impactGaps]);
52
+ return Object.freeze({
53
+ nodeId,
54
+ attention: backward.marked.find((item) => item.nodeId === nodeId)?.attention ?? Object.freeze([]),
55
+ ...(rootCause ? { rootCause } : {}),
56
+ path: rootCause ? pathTo(backward.edges, nodeId, rootCause) : Object.freeze([]),
57
+ alternatives: Object.freeze(rootCause ? [] : candidates),
58
+ impact,
59
+ confidence: allGaps.length ? 'incomplete' : 'complete',
60
+ gaps: allGaps,
61
+ });
62
+ }
63
+ finally {
64
+ session.dispose();
65
+ }
66
+ }
67
+ /** Discover only bounded host seeds; this is not a global scan of arbitrary storage. */
68
+ export async function attentionList(graph, options = {}) {
69
+ const session = new Session(graph, options);
70
+ try {
71
+ let seeds = [], missing = [];
72
+ const requested = own(options, 'nodeIds');
73
+ const admitSeeds = (raw) => {
74
+ // Only inspect the bounded prefix; the unread tail is explicitly incomplete.
75
+ if (!Array.isArray(raw))
76
+ return nodeIds(raw, session.limits.maxNodes);
77
+ const length = own(raw, 'length');
78
+ if (typeof length !== 'number' || !Number.isSafeInteger(length) || length < 0)
79
+ return nodeIds(raw, session.limits.maxNodes);
80
+ if (length > session.limits.maxNodes)
81
+ missing.push(gap('node-limit'));
82
+ const prefix = [];
83
+ for (let i = 0; i < Math.min(length, session.limits.maxNodes); i++)
84
+ prefix.push(own(raw, String(i)));
85
+ return nodeIds(prefix, session.limits.maxNodes);
86
+ };
87
+ if (requested !== undefined) {
88
+ const read = await session.invoke(undefined, () => requested, admitSeeds);
89
+ if (read.gap)
90
+ missing.push(read.gap);
91
+ else
92
+ seeds = read.value ?? [];
93
+ }
94
+ else if (session.hasRoots()) {
95
+ const read = await session.read('roots', undefined, admitSeeds);
96
+ if (read.gap)
97
+ missing.push(read.gap);
98
+ else
99
+ seeds = read.value ?? [];
100
+ }
101
+ else
102
+ missing.push(gap('not-collected'));
103
+ const result = await walk(session, seeds, 'children');
104
+ missing = [...missing, ...result.gaps];
105
+ return Object.freeze({
106
+ nodes: result.marked,
107
+ confidence: missing.length ? 'incomplete' : 'complete',
108
+ gaps: uniqueGaps(missing),
109
+ });
110
+ }
111
+ finally {
112
+ session.dispose();
113
+ }
114
+ }
@@ -0,0 +1,6 @@
1
+ import type { NodeId } from './types.js';
2
+ /** Strategies may only select admitted nodes; impact ranking must preserve the full set. */
3
+ export declare function admitRanking(raw: unknown, allowed: ReadonlySet<NodeId>, options: {
4
+ readonly exactPermutation: boolean;
5
+ readonly maximum: number;
6
+ }): readonly NodeId[];
@@ -0,0 +1,10 @@
1
+ import { nodeIds } from './validation.js';
2
+ /** Strategies may only select admitted nodes; impact ranking must preserve the full set. */
3
+ export function admitRanking(raw, allowed, options) {
4
+ const ids = nodeIds(raw, options.maximum);
5
+ if ((options.exactPermutation && ids.length !== allowed.size) ||
6
+ new Set(ids).size !== ids.length ||
7
+ ids.some((id) => !allowed.has(id)))
8
+ throw new TypeError();
9
+ return ids;
10
+ }
@@ -0,0 +1,9 @@
1
+ import type { LogAddress } from '../memory/eventPosition.js';
2
+ import type { RecordCauseGraph, RecordGraphOptions, RecordLoaderResult, RecordScope } from './recordTypes.js';
3
+ import type { GraphContext } from './types.js';
4
+ export type { RecordCauseGraph, RecordGraphOptions, RecordLoaderResult, RecordNode, RecordScope, } from './recordTypes.js';
5
+ export { recordOptions } from './recordScope.js';
6
+ /** Common engine for memory and one-scope-at-a-time host storage. Loader functions are trusted executable code. */
7
+ export declare function createLazyRecordGraph(load: (address: LogAddress, context: GraphContext) => RecordLoaderResult | Promise<RecordLoaderResult>, options?: RecordGraphOptions): RecordCauseGraph;
8
+ /** A finished record already in memory; payload admission is lazy per requested scope. */
9
+ export declare function createRecordGraph(scopes: readonly RecordScope[], options?: RecordGraphOptions): RecordCauseGraph;
@@ -0,0 +1,49 @@
1
+ import { childrenForRecordNode } from './recordChildren.js';
2
+ import { attentionForRecordNode, gapForRecordFailure } from './recordCoverage.js';
3
+ import { nodeIdFor, resolveRecordNode } from './recordIdentity.js';
4
+ import { createRecordScopeLoader, openRecordScope } from './recordLoading.js';
5
+ import { inspectRecordNode } from './recordParents.js';
6
+ import { recordOptions } from './recordScope.js';
7
+ import { addressKey, addressOf, list, own, RecordRefusal } from './recordValidation.js';
8
+ export { recordOptions } from './recordScope.js';
9
+ /** Common engine for memory and one-scope-at-a-time host storage. Loader functions are trusted executable code. */
10
+ export function createLazyRecordGraph(load, options = {}) {
11
+ const limits = recordOptions(options), loader = createRecordScopeLoader(load, limits);
12
+ const resolve = (nodeId, context) => resolveRecordNode(nodeId, context, loader);
13
+ return Object.freeze({
14
+ nodeId: (address, node) => nodeIdFor(address, node),
15
+ openScope: (address, context) => openRecordScope(address, context, loader),
16
+ async parents(nodeId, context) {
17
+ const node = await resolve(nodeId, context);
18
+ return 'failure' in node
19
+ ? { edges: [], complete: false }
20
+ : inspectRecordNode(node, limits.maxEdges, loader.extraGapCodes(node.scope)).adjacency;
21
+ },
22
+ async gaps(nodeId, context) {
23
+ const node = await resolve(nodeId, context);
24
+ return 'failure' in node
25
+ ? gapForRecordFailure(node.failure)
26
+ : inspectRecordNode(node, limits.maxEdges, loader.extraGapCodes(node.scope)).gaps;
27
+ },
28
+ async attention(nodeId, context) {
29
+ return attentionForRecordNode(await resolve(nodeId, context));
30
+ },
31
+ async children(nodeId, context) {
32
+ const node = await resolve(nodeId, context);
33
+ return 'failure' in node
34
+ ? { edges: [], complete: false }
35
+ : childrenForRecordNode(node, limits.maxEdges, loader.extraGapCodes(node.scope));
36
+ },
37
+ });
38
+ }
39
+ /** A finished record already in memory; payload admission is lazy per requested scope. */
40
+ export function createRecordGraph(scopes, options = {}) {
41
+ const limits = recordOptions(options);
42
+ return createLazyRecordGraph((address) => {
43
+ for (const raw of list(scopes, limits.maxScopes)) {
44
+ if (addressKey(addressOf(own(raw, 'address'))) === addressKey(address))
45
+ return { scope: raw };
46
+ }
47
+ throw new RecordRefusal('unavailable');
48
+ }, limits);
49
+ }
@@ -0,0 +1,4 @@
1
+ import type { ResolvedRecordNode } from './recordTypes.js';
2
+ import type { Adjacency, Gap } from './types.js';
3
+ /** Share parent-side coverage checks before appending data then control children in their established order. */
4
+ export declare function childrenForRecordNode(node: ResolvedRecordNode, maximumEdges: number, extraGapCodes: readonly Gap['code'][] | undefined): Adjacency;
@@ -0,0 +1,79 @@
1
+ import { forwardSliceForKey } from '../slice/forwardSliceForKey.js';
2
+ import { appendControlChildren } from './recordControl.js';
3
+ import { allStageReadsCollected, writtenKeysForNode } from './recordCoverage.js';
4
+ import { nodeIdFor } from './recordIdentity.js';
5
+ import { inspectRecordNode } from './recordParents.js';
6
+ import { readKeysForStage } from './recordScope.js';
7
+ /** Keep per-write and conservative fed edges distinct while retaining existing cap and deduplication order. */
8
+ function appendFedChildren(scope, root, maximumEdges, edges) {
9
+ let complete = true;
10
+ for (const edge of root.fedEdges) {
11
+ const child = edge.child;
12
+ if (child.commitIdx === undefined || !child.runtimeStageId) {
13
+ complete = false;
14
+ continue;
15
+ }
16
+ if (edges.length >= maximumEdges) {
17
+ complete = false;
18
+ break;
19
+ }
20
+ const nodeId = nodeIdFor(scope.address, {
21
+ runtimeStageId: child.runtimeStageId,
22
+ commitIdx: child.commitIdx,
23
+ key: child.key,
24
+ });
25
+ if (!edges.some((existing) => existing.nodeId === nodeId))
26
+ edges.push({ nodeId, kind: 'data', attribution: edge.basis, evidence: ['commit:' + child.commitIdx] });
27
+ if (edge.basis === 'stage')
28
+ complete = false;
29
+ }
30
+ return complete;
31
+ }
32
+ /** A consumer with no writes still needs a qualified node so its recorded control flow remains reachable. */
33
+ function appendReadOnlyChildren(scope, root, key, maximumEdges, edges) {
34
+ let complete = true;
35
+ for (const read of root.reads) {
36
+ if (scope.log[read.commitIdx]?.trace.length)
37
+ continue;
38
+ if (edges.length >= maximumEdges) {
39
+ complete = false;
40
+ break;
41
+ }
42
+ const nodeId = nodeIdFor(scope.address, { runtimeStageId: read.runtimeStageId, commitIdx: read.commitIdx, key }, true);
43
+ if (!edges.some((existing) => existing.nodeId === nodeId))
44
+ edges.push({ nodeId, kind: 'data', attribution: 'per-write', evidence: ['commit:' + read.commitIdx] });
45
+ }
46
+ return complete;
47
+ }
48
+ /** Query each written value at its original commit boundary instead of the value's latest writer. */
49
+ function appendDataChildren(scope, identity, commitIndex, maximumEdges, edges) {
50
+ let complete = true;
51
+ const lookup = (runtimeStageId) => readKeysForStage(scope, runtimeStageId);
52
+ for (const key of writtenKeysForNode(scope.log[commitIndex], identity)) {
53
+ const forward = forwardSliceForKey(scope.log, key, lookup, {
54
+ before: commitIndex + 1,
55
+ maxDepth: 1,
56
+ maxNodes: maximumEdges + 1,
57
+ });
58
+ if (forward.root?.truncated?.byNodes)
59
+ complete = false;
60
+ if (forward.root?.commitIdx !== commitIndex) {
61
+ complete = false;
62
+ continue;
63
+ }
64
+ complete = appendFedChildren(scope, forward.root, maximumEdges, edges) && complete;
65
+ complete = appendReadOnlyChildren(scope, forward.root, key, maximumEdges, edges) && complete;
66
+ }
67
+ return complete;
68
+ }
69
+ /** Share parent-side coverage checks before appending data then control children in their established order. */
70
+ export function childrenForRecordNode(node, maximumEdges, extraGapCodes) {
71
+ const { scope, identity, commitIndex } = node;
72
+ const base = inspectRecordNode(node, maximumEdges, extraGapCodes), edges = [];
73
+ let complete = base.gaps.length === 0;
74
+ if (!allStageReadsCollected(scope))
75
+ complete = false;
76
+ complete = appendDataChildren(scope, identity, commitIndex, maximumEdges, edges) && complete;
77
+ complete = appendControlChildren(scope, identity, commitIndex, maximumEdges, edges) && complete;
78
+ return { edges, complete };
79
+ }