foottrace 1.1.0 → 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 (88) hide show
  1. package/README.md +96 -2
  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 +2 -0
  6. package/dist/esm/lib/cause/admission.d.ts +27 -0
  7. package/dist/esm/lib/cause/admission.js +59 -0
  8. package/dist/esm/lib/cause/explain.d.ts +5 -0
  9. package/dist/esm/lib/cause/explain.js +114 -0
  10. package/dist/esm/lib/cause/ranking.d.ts +6 -0
  11. package/dist/esm/lib/cause/ranking.js +10 -0
  12. package/dist/esm/lib/cause/recordAdapter.d.ts +9 -0
  13. package/dist/esm/lib/cause/recordAdapter.js +49 -0
  14. package/dist/esm/lib/cause/recordChildren.d.ts +4 -0
  15. package/dist/esm/lib/cause/recordChildren.js +79 -0
  16. package/dist/esm/lib/cause/recordControl.d.ts +13 -0
  17. package/dist/esm/lib/cause/recordControl.js +66 -0
  18. package/dist/esm/lib/cause/recordCoverage.d.ts +24 -0
  19. package/dist/esm/lib/cause/recordCoverage.js +87 -0
  20. package/dist/esm/lib/cause/recordIdentity.d.ts +9 -0
  21. package/dist/esm/lib/cause/recordIdentity.js +67 -0
  22. package/dist/esm/lib/cause/recordLoading.d.ts +7 -0
  23. package/dist/esm/lib/cause/recordLoading.js +116 -0
  24. package/dist/esm/lib/cause/recordParents.d.ts +4 -0
  25. package/dist/esm/lib/cause/recordParents.js +86 -0
  26. package/dist/esm/lib/cause/recordScope.d.ts +8 -0
  27. package/dist/esm/lib/cause/recordScope.js +60 -0
  28. package/dist/esm/lib/cause/recordTypes.d.ts +67 -0
  29. package/dist/esm/lib/cause/recordTypes.js +1 -0
  30. package/dist/esm/lib/cause/recordValidation.d.ts +33 -0
  31. package/dist/esm/lib/cause/recordValidation.js +198 -0
  32. package/dist/esm/lib/cause/session.d.ts +30 -0
  33. package/dist/esm/lib/cause/session.js +161 -0
  34. package/dist/esm/lib/cause/storedRecordAdapter.d.ts +11 -0
  35. package/dist/esm/lib/cause/storedRecordAdapter.js +57 -0
  36. package/dist/esm/lib/cause/strategies.d.ts +3 -0
  37. package/dist/esm/lib/cause/strategies.js +21 -0
  38. package/dist/esm/lib/cause/types.d.ts +90 -0
  39. package/dist/esm/lib/cause/types.js +1 -0
  40. package/dist/esm/lib/cause/validation.d.ts +24 -0
  41. package/dist/esm/lib/cause/validation.js +134 -0
  42. package/dist/esm/lib/cause/walk.d.ts +12 -0
  43. package/dist/esm/lib/cause/walk.js +174 -0
  44. package/dist/esm/lib/memory/types.d.ts +4 -1
  45. package/dist/esm/lib/memory/types.js +1 -1
  46. package/dist/lib/cause/admission.js +67 -0
  47. package/dist/lib/cause/explain.js +119 -0
  48. package/dist/lib/cause/ranking.js +14 -0
  49. package/dist/lib/cause/recordAdapter.js +55 -0
  50. package/dist/lib/cause/recordChildren.js +83 -0
  51. package/dist/lib/cause/recordControl.js +74 -0
  52. package/dist/lib/cause/recordCoverage.js +99 -0
  53. package/dist/lib/cause/recordIdentity.js +73 -0
  54. package/dist/lib/cause/recordLoading.js +121 -0
  55. package/dist/lib/cause/recordParents.js +90 -0
  56. package/dist/lib/cause/recordScope.js +66 -0
  57. package/dist/lib/cause/recordTypes.js +2 -0
  58. package/dist/lib/cause/recordValidation.js +213 -0
  59. package/dist/lib/cause/session.js +165 -0
  60. package/dist/lib/cause/storedRecordAdapter.js +61 -0
  61. package/dist/lib/cause/strategies.js +26 -0
  62. package/dist/lib/cause/types.js +2 -0
  63. package/dist/lib/cause/validation.js +150 -0
  64. package/dist/lib/cause/walk.js +180 -0
  65. package/dist/lib/memory/types.js +1 -1
  66. package/dist/types/cause.d.ts +8 -0
  67. package/dist/types/index.d.ts +2 -0
  68. package/dist/types/lib/cause/admission.d.ts +27 -0
  69. package/dist/types/lib/cause/explain.d.ts +5 -0
  70. package/dist/types/lib/cause/ranking.d.ts +6 -0
  71. package/dist/types/lib/cause/recordAdapter.d.ts +9 -0
  72. package/dist/types/lib/cause/recordChildren.d.ts +4 -0
  73. package/dist/types/lib/cause/recordControl.d.ts +13 -0
  74. package/dist/types/lib/cause/recordCoverage.d.ts +24 -0
  75. package/dist/types/lib/cause/recordIdentity.d.ts +9 -0
  76. package/dist/types/lib/cause/recordLoading.d.ts +7 -0
  77. package/dist/types/lib/cause/recordParents.d.ts +4 -0
  78. package/dist/types/lib/cause/recordScope.d.ts +8 -0
  79. package/dist/types/lib/cause/recordTypes.d.ts +67 -0
  80. package/dist/types/lib/cause/recordValidation.d.ts +33 -0
  81. package/dist/types/lib/cause/session.d.ts +30 -0
  82. package/dist/types/lib/cause/storedRecordAdapter.d.ts +11 -0
  83. package/dist/types/lib/cause/strategies.d.ts +3 -0
  84. package/dist/types/lib/cause/types.d.ts +90 -0
  85. package/dist/types/lib/cause/validation.d.ts +24 -0
  86. package/dist/types/lib/cause/walk.d.ts +12 -0
  87. package/dist/types/lib/memory/types.d.ts +4 -1
  88. package/package.json +67 -13
@@ -0,0 +1,24 @@
1
+ import type { CommitBundle } from '../memory/types.js';
2
+ import type { AddRecordGap, PreparedRecordScope, RecordIdentity, RecordLoadFailure, ResolvedRecordNode } from './recordTypes.js';
3
+ import type { Attention, Gap } from './types.js';
4
+ /** Preserve first-observed coverage order while collapsing repeated codes from different evidence paths. */
5
+ export declare function recordGapCollector(): {
6
+ gaps: Gap[];
7
+ addGap: AddRecordGap;
8
+ };
9
+ /** Failed loading exposes a fixed category, never the host error or an apparently complete empty graph. */
10
+ export declare function gapForRecordFailure(code: Gap['code']): readonly Gap[];
11
+ /** Return captured mark references without treating an omitted attention map as collected evidence. */
12
+ export declare function attentionForRecordNode(node: ResolvedRecordNode | RecordLoadFailure): readonly Attention[];
13
+ /** Emit host and capture gaps before query-derived gaps so existing evidence order stays stable. */
14
+ export declare function collectScopeCoverage(scope: PreparedRecordScope, bundle: CommitBundle, extraGapCodes: readonly Gap['code'][] | undefined, addGap: AddRecordGap): void;
15
+ /** A read-only identity has no new value life; unqualified executions retain written-key encounter order. */
16
+ export declare function writtenKeysForNode(bundle: CommitBundle, identity: RecordIdentity): string[];
17
+ /** Immediate adjacency caps affect graph coverage even when the caller asks only for parents. */
18
+ export declare function collectForwardCoverage(scope: PreparedRecordScope, identity: RecordIdentity, commitIndex: number, firstCommitIndex: number, maximumEdges: number, addGap: AddRecordGap): void;
19
+ /** Exact attribution requires captured per-write provenance or a validated read-only key context. */
20
+ export declare function readAttributionIsPrecise(scope: PreparedRecordScope, identity: RecordIdentity, bundle: CommitBundle): boolean;
21
+ /** Distinguish conservative attribution from missing collection without marking a proven empty leaf. */
22
+ export declare function collectReadCoverage(scope: PreparedRecordScope, identity: RecordIdentity, precise: boolean, addGap: AddRecordGap): void;
23
+ /** Forward absence is trustworthy only when every potentially consuming execution collected its reads. */
24
+ export declare function allStageReadsCollected(scope: PreparedRecordScope): boolean;
@@ -0,0 +1,87 @@
1
+ import { relation } from '../memory/keyPaths.js';
2
+ import { forwardSliceForKey } from '../slice/forwardSliceForKey.js';
3
+ import { collectControlChildCoverage } from './recordControl.js';
4
+ import { readKeysForStage } from './recordScope.js';
5
+ /** Preserve first-observed coverage order while collapsing repeated codes from different evidence paths. */
6
+ export function recordGapCollector() {
7
+ const gaps = [];
8
+ return {
9
+ gaps,
10
+ addGap: (code) => {
11
+ if (!gaps.some((gap) => gap.code === code))
12
+ gaps.push({ code, evidence: [] });
13
+ },
14
+ };
15
+ }
16
+ /** Failed loading exposes a fixed category, never the host error or an apparently complete empty graph. */
17
+ export function gapForRecordFailure(code) {
18
+ return [{ code, evidence: [] }];
19
+ }
20
+ /** Return captured mark references without treating an omitted attention map as collected evidence. */
21
+ export function attentionForRecordNode(node) {
22
+ // An omitted map stays unknown coverage; it is never replaced by an affirmative empty map.
23
+ return 'failure' in node ? [] : node.scope.marks?.get(node.identity.runtimeStageId) ?? [];
24
+ }
25
+ /** Emit host and capture gaps before query-derived gaps so existing evidence order stays stable. */
26
+ export function collectScopeCoverage(scope, bundle, extraGapCodes, addGap) {
27
+ for (const code of extraGapCodes ?? [])
28
+ addGap(code);
29
+ if (!scope.complete || scope.control === undefined || scope.marks === undefined)
30
+ addGap('not-collected');
31
+ if (bundle.untrackedSources?.length)
32
+ addGap('untracked-input');
33
+ if (bundle.redactedPaths.length)
34
+ addGap('redacted');
35
+ }
36
+ /** A read-only identity has no new value life; unqualified executions retain written-key encounter order. */
37
+ export function writtenKeysForNode(bundle, identity) {
38
+ return identity.read
39
+ ? []
40
+ : identity.key === undefined
41
+ ? [...new Set(bundle.trace.map((entry) => entry.path))]
42
+ : [identity.key];
43
+ }
44
+ /** Immediate adjacency caps affect graph coverage even when the caller asks only for parents. */
45
+ export function collectForwardCoverage(scope, identity, commitIndex, firstCommitIndex, maximumEdges, addGap) {
46
+ const neighbors = new Set();
47
+ const lookup = (runtimeStageId) => readKeysForStage(scope, runtimeStageId);
48
+ for (const key of writtenKeysForNode(scope.log[commitIndex], identity)) {
49
+ const forward = forwardSliceForKey(scope.log, key, lookup, {
50
+ before: commitIndex + 1,
51
+ maxDepth: 1,
52
+ maxNodes: maximumEdges + 1,
53
+ });
54
+ // A one-hop query intentionally stops at depth one; only a node cap hides immediate adjacency.
55
+ if (forward.root?.truncated?.byNodes)
56
+ addGap('edge-limit');
57
+ for (const edge of forward.root?.fedEdges ?? [])
58
+ neighbors.add(JSON.stringify(['data', edge.child.commitIdx, edge.child.key]));
59
+ for (const read of forward.root?.reads ?? [])
60
+ if (!scope.log[read.commitIdx]?.trace.length)
61
+ neighbors.add(JSON.stringify(['read', read.commitIdx, key]));
62
+ }
63
+ collectControlChildCoverage(scope, identity, firstCommitIndex, neighbors, addGap);
64
+ if (neighbors.size > maximumEdges)
65
+ addGap('edge-limit');
66
+ }
67
+ /** Exact attribution requires captured per-write provenance or a validated read-only key context. */
68
+ export function readAttributionIsPrecise(scope, identity, bundle) {
69
+ const relevant = identity.key === undefined
70
+ ? bundle.trace
71
+ : bundle.trace.filter((entry) => relation(entry.path, identity.key) !== undefined);
72
+ return identity.read === true
73
+ ? identity.key !== undefined && readKeysForStage(scope, identity.runtimeStageId).includes(identity.key)
74
+ : identity.key !== undefined && relevant.length > 0 && relevant.every((entry) => entry.readKeys !== undefined);
75
+ }
76
+ /** Distinguish conservative attribution from missing collection without marking a proven empty leaf. */
77
+ export function collectReadCoverage(scope, identity, precise, addGap) {
78
+ if (!precise && (identity.key !== undefined || readKeysForStage(scope, identity.runtimeStageId).length > 0))
79
+ addGap('conservative');
80
+ if (!scope.reads?.has(identity.runtimeStageId) && !precise)
81
+ addGap('not-collected');
82
+ }
83
+ /** Forward absence is trustworthy only when every potentially consuming execution collected its reads. */
84
+ export function allStageReadsCollected(scope) {
85
+ return !scope.log.some((bundle) => !scope.reads?.has(bundle.runtimeStageId) &&
86
+ (bundle.trace.length === 0 || bundle.trace.some((entry) => entry.readKeys === undefined)));
87
+ }
@@ -0,0 +1,9 @@
1
+ import type { LogAddress } from '../memory/eventPosition.js';
2
+ import type { GraphContext, NodeId } from './types.js';
3
+ import type { RecordIdentity, RecordLoadFailure, RecordNode, RecordScopeLoader, ResolvedRecordNode } from './recordTypes.js';
4
+ /** Parse only the bounded identity encoding; payload fields never become graph addresses. */
5
+ export declare function readNodeIdentity(nodeId: string): RecordIdentity;
6
+ /** Stable field order preserves existing address/execution/key IDs across storage and memory readers. */
7
+ export declare function nodeIdFor(addressInput: LogAddress, node: RecordNode, read?: true): NodeId;
8
+ /** An admitted ID must also match captured evidence before it can introduce a dependency. */
9
+ export declare function resolveRecordNode(nodeId: NodeId, context: GraphContext, loader: RecordScopeLoader): Promise<ResolvedRecordNode | RecordLoadFailure>;
@@ -0,0 +1,67 @@
1
+ import { readKeysForStage } from './recordScope.js';
2
+ import { addressOf, name, own, RecordRefusal } from './recordValidation.js';
3
+ /** Parse only the bounded identity encoding; payload fields never become graph addresses. */
4
+ export function readNodeIdentity(nodeId) {
5
+ if (typeof nodeId !== 'string' || nodeId.length > 2048 || !nodeId.startsWith('record:'))
6
+ throw new RecordRefusal();
7
+ let fields;
8
+ try {
9
+ fields = JSON.parse(nodeId.slice(7));
10
+ }
11
+ catch {
12
+ throw new RecordRefusal();
13
+ }
14
+ const address = addressOf(own(fields, 'address')), runtimeStageId = name(own(fields, 'runtimeStageId')), commitIndex = own(fields, 'commitIdx'), key = own(fields, 'key'), read = own(fields, 'read');
15
+ if (commitIndex !== undefined && (!Number.isSafeInteger(commitIndex) || commitIndex < 0))
16
+ throw new RecordRefusal();
17
+ if ((read !== undefined && read !== true) || (read === true && key === undefined))
18
+ throw new RecordRefusal();
19
+ return {
20
+ address,
21
+ runtimeStageId,
22
+ ...(commitIndex === undefined ? {} : { commitIdx: commitIndex }),
23
+ ...(key === undefined ? {} : { key: name(key, true) }),
24
+ ...(read === true ? { read: true } : {}),
25
+ };
26
+ }
27
+ /** Stable field order preserves existing address/execution/key IDs across storage and memory readers. */
28
+ export function nodeIdFor(addressInput, node, read) {
29
+ try {
30
+ const address = addressOf(addressInput), runtimeStageId = name(own(node, 'runtimeStageId')), commitIndex = own(node, 'commitIdx'), key = own(node, 'key');
31
+ if (commitIndex !== undefined && (!Number.isSafeInteger(commitIndex) || commitIndex < 0))
32
+ throw new RecordRefusal();
33
+ const nodeId = 'record:' +
34
+ JSON.stringify({
35
+ address,
36
+ runtimeStageId,
37
+ ...(commitIndex === undefined ? {} : { commitIdx: commitIndex }),
38
+ ...(key === undefined ? {} : { key: name(key, true) }),
39
+ ...(read ? { read: true } : {}),
40
+ });
41
+ return nodeId.length <= 2048 ? nodeId : 'record:invalid';
42
+ }
43
+ catch {
44
+ return 'record:invalid';
45
+ }
46
+ }
47
+ /** An admitted ID must also match captured evidence before it can introduce a dependency. */
48
+ export async function resolveRecordNode(nodeId, context, loader) {
49
+ try {
50
+ const identity = readNodeIdentity(nodeId), scope = await loader.loadScope(identity.address, context);
51
+ if ('failure' in scope)
52
+ return scope;
53
+ const commitIndex = identity.commitIdx ?? scope.log.findIndex((bundle) => bundle.runtimeStageId === identity.runtimeStageId);
54
+ if (commitIndex < 0 ||
55
+ commitIndex >= scope.log.length ||
56
+ scope.log[commitIndex].runtimeStageId !== identity.runtimeStageId)
57
+ throw new RecordRefusal();
58
+ // A qualified read is a captured fact, never a caller-supplied request to invent a dependency.
59
+ if (identity.read &&
60
+ (identity.key === undefined || !readKeysForStage(scope, identity.runtimeStageId).includes(identity.key)))
61
+ throw new RecordRefusal();
62
+ return { scope, identity, commitIndex };
63
+ }
64
+ catch (error) {
65
+ return { failure: error instanceof RecordRefusal ? error.code : 'invalid-data' };
66
+ }
67
+ }
@@ -0,0 +1,7 @@
1
+ import type { LogAddress } from '../memory/eventPosition.js';
2
+ import type { GraphContext, NodeId } from './types.js';
3
+ import type { RecordGraphOptions, RecordScopeHost, RecordScopeLoader } from './recordTypes.js';
4
+ /** Bound caller waiting independently of host cooperation without sharing cancellation between requests. */
5
+ export declare function createRecordScopeLoader(hostLoad: RecordScopeHost, limits: Required<RecordGraphOptions>): RecordScopeLoader;
6
+ /** Discovery must distinguish an unavailable scope from a successfully loaded empty record. */
7
+ export declare function openRecordScope(addressInput: LogAddress, context: GraphContext, loader: RecordScopeLoader): Promise<readonly NodeId[]>;
@@ -0,0 +1,116 @@
1
+ import { gaps as admitGaps } from './validation.js';
2
+ import { nodeIdFor } from './recordIdentity.js';
3
+ import { prepareRecordScope } from './recordScope.js';
4
+ import { addressKey, addressOf, list, own, projectLoaded, RecordRefusal } from './recordValidation.js';
5
+ /** Admit before caching so a cache hit cannot bypass payload limits or lose scope-owned coverage. */
6
+ function admitLoadedScope(result, request, limits, state) {
7
+ if (request.controller.signal.aborted || performance.now() >= request.deadline) {
8
+ request.controller.abort();
9
+ return { failure: request.context.signal.aborted ? 'cancelled' : 'timeout' };
10
+ }
11
+ let prepared;
12
+ try {
13
+ prepared = prepareRecordScope(own(result, 'scope'), request.address, limits.maxCommits, limits.maxFields);
14
+ const extra = list(own(result, 'gaps') ?? [], 32);
15
+ // Coverage belongs to this admitted object, so cached scopes retain the same gap ordering and ownership.
16
+ state.extraGaps.set(prepared, admitGaps(extra.map((code) => ({ code, evidence: [] }))).map((gap) => gap.code));
17
+ }
18
+ catch (error) {
19
+ throw error instanceof RecordRefusal ? error : new RecordRefusal();
20
+ }
21
+ if (performance.now() >= request.deadline) {
22
+ request.controller.abort();
23
+ return { failure: 'timeout' };
24
+ }
25
+ if (state.cache.size >= limits.maxScopes && !state.cache.has(request.cacheKey))
26
+ return { failure: 'node-limit' };
27
+ state.cache.set(request.cacheKey, prepared);
28
+ return prepared;
29
+ }
30
+ /** Host failures become fixed gaps while unfinished host work continues to occupy its bounded slot. */
31
+ function startScopeWork(hostLoad, request, limits, state) {
32
+ const work = projectLoaded(() => {
33
+ if (request.controller.signal.aborted)
34
+ throw new RecordRefusal(request.context.signal.aborted ? 'cancelled' : 'timeout');
35
+ return hostLoad(request.address, { signal: request.controller.signal });
36
+ }, (result) => admitLoadedScope(result, request, limits, state)).catch((error) => {
37
+ if (request.context.signal.aborted)
38
+ return { failure: 'cancelled' };
39
+ if (performance.now() >= request.deadline) {
40
+ request.controller.abort();
41
+ return { failure: 'timeout' };
42
+ }
43
+ return { failure: error instanceof RecordRefusal ? error.code : 'unavailable' };
44
+ });
45
+ // A timed-out host operation retains its slot until it settles; repeated timeouts cannot grow an unbounded I/O pool.
46
+ void work.then(() => {
47
+ state.activeLoads--;
48
+ }, () => {
49
+ state.activeLoads--;
50
+ });
51
+ return work;
52
+ }
53
+ /** Bound caller waiting independently of host cooperation without sharing cancellation between requests. */
54
+ export function createRecordScopeLoader(hostLoad, limits) {
55
+ const state = { activeLoads: 0, cache: new Map(), extraGaps: new WeakMap() };
56
+ return {
57
+ async loadScope(address, context) {
58
+ if (context.signal.aborted)
59
+ return { failure: 'cancelled' };
60
+ const cacheKey = addressKey(address);
61
+ const cached = state.cache.get(cacheKey);
62
+ if (cached)
63
+ return cached;
64
+ if (state.cache.size + state.activeLoads >= limits.maxScopes)
65
+ return { failure: 'node-limit' };
66
+ state.activeLoads++;
67
+ const deadline = performance.now() + limits.loadTimeoutMs;
68
+ const controller = new AbortController();
69
+ let timer;
70
+ let finish;
71
+ const interrupted = new Promise((resolve) => {
72
+ finish = resolve;
73
+ });
74
+ const cancel = () => {
75
+ controller.abort();
76
+ finish?.({ failure: 'cancelled' });
77
+ };
78
+ context.signal.addEventListener('abort', cancel, { once: true });
79
+ timer = setTimeout(() => {
80
+ controller.abort();
81
+ finish?.({ failure: 'timeout' });
82
+ }, limits.loadTimeoutMs);
83
+ try {
84
+ const work = startScopeWork(hostLoad, { address, context, controller, deadline, cacheKey }, limits, state);
85
+ return await Promise.race([work, interrupted]);
86
+ }
87
+ finally {
88
+ if (timer !== undefined)
89
+ clearTimeout(timer);
90
+ context.signal.removeEventListener('abort', cancel);
91
+ }
92
+ },
93
+ extraGapCodes: (scope) => state.extraGaps.get(scope),
94
+ };
95
+ }
96
+ /** Discovery must distinguish an unavailable scope from a successfully loaded empty record. */
97
+ export async function openRecordScope(addressInput, context, loader) {
98
+ let address;
99
+ try {
100
+ address = addressOf(addressInput);
101
+ }
102
+ catch {
103
+ throw new RecordRefusal();
104
+ }
105
+ const scope = await loader.loadScope(address, context);
106
+ if ('failure' in scope)
107
+ throw new RecordRefusal(scope.failure);
108
+ // Empty unfinished evidence has no node on which to report its gap; discovery must reject instead.
109
+ if (!scope.log.length &&
110
+ (!scope.complete ||
111
+ scope.control === undefined ||
112
+ scope.marks === undefined ||
113
+ (loader.extraGapCodes(scope)?.length ?? 0) > 0))
114
+ throw new RecordRefusal('not-collected');
115
+ return scope.log.map((bundle, commitIndex) => nodeIdFor(address, { runtimeStageId: bundle.runtimeStageId, commitIdx: commitIndex }));
116
+ }
@@ -0,0 +1,4 @@
1
+ import type { RecordInspection, ResolvedRecordNode } from './recordTypes.js';
2
+ import type { Gap } from './types.js';
3
+ /** Derive parent adjacency and its ordered gaps together from the same bounded canonical query. */
4
+ export declare function inspectRecordNode(node: ResolvedRecordNode, maximumEdges: number, extraGapCodes: readonly Gap['code'][] | undefined): RecordInspection;
@@ -0,0 +1,86 @@
1
+ import { causalChain } from '../memory/backtrack.js';
2
+ import { findLastWriter } from '../memory/commitLogUtils.js';
3
+ import { sliceForKey } from '../slice/sliceForKey.js';
4
+ import { collectControlParentCoverage, controlParentLookup } from './recordControl.js';
5
+ import { collectForwardCoverage, collectReadCoverage, collectScopeCoverage, readAttributionIsPrecise, recordGapCollector, } from './recordCoverage.js';
6
+ import { nodeIdFor } from './recordIdentity.js';
7
+ import { readKeysForStage } from './recordScope.js';
8
+ /** Anchor writers before the execution so later commits cannot rewrite the recorded causal edge. */
9
+ function projectParentEdges(scope, parentEdges, firstCommitIndex, precise, maximumEdges, addGap) {
10
+ const edges = [];
11
+ for (const edge of parentEdges) {
12
+ if (edges.length >= maximumEdges) {
13
+ addGap('edge-limit');
14
+ break;
15
+ }
16
+ const writer = edge.kind === 'data' && edge.key
17
+ ? findLastWriter(scope.log, edge.key, firstCommitIndex)
18
+ : scope.log.find((bundle) => bundle.runtimeStageId === edge.parent.runtimeStageId);
19
+ if (!writer) {
20
+ addGap('untracked-input');
21
+ continue;
22
+ }
23
+ const writerIndex = scope.log.indexOf(writer);
24
+ edges.push({
25
+ nodeId: nodeIdFor(scope.address, {
26
+ runtimeStageId: writer.runtimeStageId,
27
+ commitIdx: writerIndex,
28
+ ...(edge.kind === 'data' && edge.key ? { key: edge.key } : {}),
29
+ }),
30
+ kind: edge.kind,
31
+ attribution: edge.kind === 'control' ? 'recorded-control' : precise ? 'per-write' : 'stage',
32
+ evidence: ['commit:' + writerIndex],
33
+ });
34
+ if (edge.basis)
35
+ addGap('conservative');
36
+ }
37
+ return edges;
38
+ }
39
+ /** Derive parent adjacency and its ordered gaps together from the same bounded canonical query. */
40
+ export function inspectRecordNode(node, maximumEdges, extraGapCodes) {
41
+ const { scope, identity, commitIndex } = node;
42
+ const bundle = scope.log[commitIndex], firstCommitIndex = scope.log.findIndex((entry) => entry.runtimeStageId === identity.runtimeStageId), coverage = recordGapCollector();
43
+ collectScopeCoverage(scope, bundle, extraGapCodes, coverage.addGap);
44
+ const lookup = (runtimeStageId) => readKeysForStage(scope, runtimeStageId);
45
+ collectForwardCoverage(scope, identity, commitIndex, firstCommitIndex, maximumEdges, coverage.addGap);
46
+ const precise = readAttributionIsPrecise(scope, identity, bundle);
47
+ collectReadCoverage(scope, identity, precise, coverage.addGap);
48
+ collectControlParentCoverage(scope, identity, firstCommitIndex, coverage.addGap);
49
+ const control = controlParentLookup(scope);
50
+ const slice = identity.key === undefined || identity.read
51
+ ? undefined
52
+ : sliceForKey(scope.log, identity.key, lookup, {
53
+ before: commitIndex + 1,
54
+ maxDepth: 1,
55
+ maxNodes: maximumEdges + 1,
56
+ controlDeps: control,
57
+ });
58
+ if (identity.key !== undefined &&
59
+ !identity.read &&
60
+ (!slice?.writer || scope.log.indexOf(slice.writer) !== commitIndex)) {
61
+ coverage.addGap('invalid-data');
62
+ return { adjacency: { edges: [], complete: false }, gaps: coverage.gaps };
63
+ }
64
+ // Read-only identities retain both the read's data writer and the execution's recorded control parent.
65
+ const root = identity.read && identity.key !== undefined
66
+ ? causalChain(scope.log, identity.runtimeStageId, (runtimeStageId) => (runtimeStageId === identity.runtimeStageId ? [identity.key] : lookup(runtimeStageId)), { maxDepth: 1, maxNodes: maximumEdges + 1, controlDeps: control })
67
+ : slice?.root ??
68
+ (identity.key === undefined
69
+ ? causalChain(scope.log, identity.runtimeStageId, lookup, {
70
+ maxDepth: 1,
71
+ maxNodes: maximumEdges + 1,
72
+ controlDeps: control,
73
+ })
74
+ : undefined);
75
+ if (root?.preRunReads?.keys.length)
76
+ coverage.addGap('untracked-input');
77
+ if (root?.truncated?.byNodes)
78
+ coverage.addGap('edge-limit');
79
+ for (const note of slice?.notes ?? [])
80
+ if (note.code === 'redacted')
81
+ coverage.addGap('redacted');
82
+ else if (note.code === 'nested-rows' || note.code === 'from-initial-state')
83
+ coverage.addGap('conservative');
84
+ const edges = projectParentEdges(scope, root?.parentEdges ?? [], firstCommitIndex, precise, maximumEdges, coverage.addGap);
85
+ return { adjacency: { edges, complete: coverage.gaps.length === 0 }, gaps: coverage.gaps };
86
+ }
@@ -0,0 +1,8 @@
1
+ import type { LogAddress } from '../memory/eventPosition.js';
2
+ import type { PreparedRecordScope, RecordGraphOptions, RecordScope } from './recordTypes.js';
3
+ /** Read configuration as data so option accessors cannot execute during adapter construction. */
4
+ export declare function recordOptions(options: RecordGraphOptions): Required<RecordGraphOptions>;
5
+ /** Spend one shared field budget before canonical readers see any untrusted scope data. */
6
+ export declare function prepareRecordScope(value: RecordScope, address: LogAddress, maximumCommits: number, maximumFields: number): PreparedRecordScope;
7
+ /** Preserve both stage reads and stronger per-write prefixes, including their original encounter order. */
8
+ export declare function readKeysForStage(scope: PreparedRecordScope, runtimeStageId: string): string[];
@@ -0,0 +1,60 @@
1
+ import { addressKey, addressOf, bundleOf, list, mapOf, marksOf, name, names, own, RecordRefusal, spend, } from './recordValidation.js';
2
+ /** Refuse an invalid limit before loading a scope or allocating its preparation budget. */
3
+ function boundedOption(value, defaultValue, maximum) {
4
+ const configuredValue = value ?? defaultValue;
5
+ if (typeof configuredValue !== 'number' ||
6
+ !Number.isSafeInteger(configuredValue) ||
7
+ configuredValue < 1 ||
8
+ configuredValue > maximum)
9
+ throw new TypeError('cause: invalid record configuration');
10
+ return configuredValue;
11
+ }
12
+ /** Read configuration as data so option accessors cannot execute during adapter construction. */
13
+ export function recordOptions(options) {
14
+ try {
15
+ const prototype = Object.getPrototypeOf(options);
16
+ if (prototype !== Object.prototype && prototype !== null)
17
+ throw new TypeError();
18
+ return Object.freeze({
19
+ maxCommits: boundedOption(own(options, 'maxCommits'), 256, 4096),
20
+ maxFields: boundedOption(own(options, 'maxFields'), 20000, 1000000),
21
+ maxEdges: boundedOption(own(options, 'maxEdges'), 64, 256),
22
+ maxScopes: boundedOption(own(options, 'maxScopes'), 32, 256),
23
+ loadTimeoutMs: boundedOption(own(options, 'loadTimeoutMs'), 1000, 30000),
24
+ });
25
+ }
26
+ catch {
27
+ throw new TypeError('cause: invalid record configuration');
28
+ }
29
+ }
30
+ /** Spend one shared field budget before canonical readers see any untrusted scope data. */
31
+ export function prepareRecordScope(value, address, maximumCommits, maximumFields) {
32
+ if (addressKey(addressOf(own(value, 'address'))) !== addressKey(address))
33
+ throw new RecordRefusal();
34
+ const rawLog = list(own(value, 'commitLog'), maximumCommits), budget = { left: maximumFields };
35
+ const log = rawLog.map((_, commitIndex) => bundleOf(own(rawLog, String(commitIndex)), commitIndex, budget));
36
+ const reads = mapOf(own(value, 'reads'), maximumCommits, (value) => {
37
+ const readKeys = names(value, 128, true);
38
+ spend(budget, readKeys.length);
39
+ return readKeys;
40
+ }, budget);
41
+ const control = mapOf(own(value, 'controlDeps'), maximumCommits, (value) => ({ deciderId: name(own(value, 'deciderId')) }), budget);
42
+ const marks = mapOf(own(value, 'attention'), maximumCommits, (value) => {
43
+ const attention = marksOf(value);
44
+ spend(budget, attention.length + attention.reduce((count, mark) => count + mark.evidence.length, 0));
45
+ return attention;
46
+ }, budget);
47
+ return { address, log, reads, control, marks, complete: own(value, 'complete') === true };
48
+ }
49
+ /** Preserve both stage reads and stronger per-write prefixes, including their original encounter order. */
50
+ export function readKeysForStage(scope, runtimeStageId) {
51
+ // Captured per-write prefixes remain evidence even when a supplied stage-read map omits them.
52
+ return [
53
+ ...new Set([
54
+ ...(scope.reads?.get(runtimeStageId) ?? []),
55
+ ...scope.log
56
+ .filter((bundle) => bundle.runtimeStageId === runtimeStageId)
57
+ .flatMap((bundle) => bundle.trace.flatMap((entry) => entry.readKeys ?? [])),
58
+ ]),
59
+ ];
60
+ }
@@ -0,0 +1,67 @@
1
+ import type { ControlDependency } from '../memory/backtrack.js';
2
+ import type { LogAddress } from '../memory/eventPosition.js';
3
+ import type { CommitBundle } from '../memory/types.js';
4
+ import type { Adjacency, Attention, CauseGraph, Gap, GraphContext, NodeId } from './types.js';
5
+ /** Explicit complete maps record absence too; an omitted map means it was not collected. */
6
+ export interface RecordScope {
7
+ readonly address: LogAddress;
8
+ readonly commitLog: readonly unknown[];
9
+ readonly reads?: Readonly<Record<string, readonly string[]>>;
10
+ readonly controlDeps?: Readonly<Record<string, {
11
+ readonly deciderId: string;
12
+ }>>;
13
+ readonly attention?: Readonly<Record<string, readonly Attention[]>>;
14
+ readonly complete?: boolean;
15
+ }
16
+ export interface RecordNode {
17
+ readonly runtimeStageId: string;
18
+ readonly commitIdx?: number;
19
+ readonly key?: string;
20
+ }
21
+ export interface RecordGraphOptions {
22
+ readonly maxCommits?: number;
23
+ readonly maxFields?: number;
24
+ readonly maxEdges?: number;
25
+ readonly maxScopes?: number;
26
+ readonly loadTimeoutMs?: number;
27
+ }
28
+ export interface RecordCauseGraph extends CauseGraph {
29
+ nodeId(address: LogAddress, node: RecordNode): NodeId;
30
+ /** Rejects with fixed message/code on discovery failure; never reports an unavailable scope as empty. */
31
+ openScope(address: LogAddress, context: GraphContext): Promise<readonly NodeId[]>;
32
+ }
33
+ export interface RecordLoaderResult {
34
+ readonly scope: RecordScope;
35
+ readonly gaps?: readonly Gap['code'][];
36
+ }
37
+ export interface RecordIdentity extends RecordNode {
38
+ readonly address: LogAddress;
39
+ readonly read?: true;
40
+ }
41
+ export interface PreparedRecordScope {
42
+ address: LogAddress;
43
+ log: CommitBundle[];
44
+ reads?: Map<string, string[]>;
45
+ control?: Map<string, ControlDependency>;
46
+ marks?: Map<string, readonly Attention[]>;
47
+ complete: boolean;
48
+ }
49
+ export interface RecordLoadFailure {
50
+ failure: Gap['code'];
51
+ }
52
+ export type LoadedRecordScope = PreparedRecordScope | RecordLoadFailure;
53
+ export interface ResolvedRecordNode {
54
+ scope: PreparedRecordScope;
55
+ identity: RecordIdentity;
56
+ commitIndex: number;
57
+ }
58
+ export interface RecordInspection {
59
+ adjacency: Adjacency;
60
+ gaps: readonly Gap[];
61
+ }
62
+ export type RecordScopeHost = (address: LogAddress, context: GraphContext) => RecordLoaderResult | Promise<RecordLoaderResult>;
63
+ export interface RecordScopeLoader {
64
+ loadScope(address: LogAddress, context: GraphContext): Promise<LoadedRecordScope>;
65
+ extraGapCodes(scope: PreparedRecordScope): readonly Gap['code'][] | undefined;
66
+ }
67
+ export type AddRecordGap = (code: Gap['code']) => void;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,33 @@
1
+ /** Admission before the existing readers touch untrusted record fields. Payloads never leave this module's graph. */
2
+ import type { LogAddress } from '../memory/eventPosition.js';
3
+ import type { CommitBundle } from '../memory/types.js';
4
+ import type { GapCode } from './types.js';
5
+ import { attention as marksOf } from './validation.js';
6
+ import { EvidenceRefusal } from './admission.js';
7
+ export { marksOf };
8
+ /** Loader refusals stay distinct from graph-port refusals so a failed host load keeps its original classification. */
9
+ export declare class RecordRefusal extends EvidenceRefusal {
10
+ readonly code: GapCode;
11
+ constructor(code?: GapCode);
12
+ }
13
+ export declare function own(value: unknown, key: string): unknown;
14
+ export declare function names(value: unknown, maximum?: number, path?: boolean): string[];
15
+ export declare function name(value: unknown, path?: boolean): string;
16
+ export declare function list(value: unknown, maximum: number): readonly unknown[];
17
+ export declare function addressOf(value: unknown): LogAddress;
18
+ export declare function spend(budget: {
19
+ left: number;
20
+ }, count: number): void;
21
+ export declare const addressKey: (a: LogAddress) => string;
22
+ /** Iterative bounded copy: data descriptors only, no getters/toJSON/prototypes or opaque class instances. */
23
+ export declare function dataCopy(value: unknown, budget: {
24
+ left: number;
25
+ }): unknown;
26
+ export declare function bundleOf(value: unknown, idx: number, budget: {
27
+ left: number;
28
+ }): CommitBundle;
29
+ export declare function mapOf<T>(value: unknown, limit: number, read: (v: unknown) => T, budget?: {
30
+ left: number;
31
+ }): Map<string, T> | undefined;
32
+ /** Observe native promises by intrinsic without assimilating a data object's then accessor. */
33
+ export declare function projectLoaded<T, R>(work: () => T | Promise<T>, project: (value: T) => R): Promise<R>;