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.
- package/README.md +137 -4
- package/dist/cause.js +14 -0
- package/dist/esm/cause.d.ts +8 -0
- package/dist/esm/cause.js +5 -0
- package/dist/esm/index.d.ts +7 -2
- package/dist/esm/index.js +2 -1
- package/dist/esm/lib/cause/admission.d.ts +27 -0
- package/dist/esm/lib/cause/admission.js +59 -0
- package/dist/esm/lib/cause/explain.d.ts +5 -0
- package/dist/esm/lib/cause/explain.js +114 -0
- package/dist/esm/lib/cause/ranking.d.ts +6 -0
- package/dist/esm/lib/cause/ranking.js +10 -0
- package/dist/esm/lib/cause/recordAdapter.d.ts +9 -0
- package/dist/esm/lib/cause/recordAdapter.js +49 -0
- package/dist/esm/lib/cause/recordChildren.d.ts +4 -0
- package/dist/esm/lib/cause/recordChildren.js +79 -0
- package/dist/esm/lib/cause/recordControl.d.ts +13 -0
- package/dist/esm/lib/cause/recordControl.js +66 -0
- package/dist/esm/lib/cause/recordCoverage.d.ts +24 -0
- package/dist/esm/lib/cause/recordCoverage.js +87 -0
- package/dist/esm/lib/cause/recordIdentity.d.ts +9 -0
- package/dist/esm/lib/cause/recordIdentity.js +67 -0
- package/dist/esm/lib/cause/recordLoading.d.ts +7 -0
- package/dist/esm/lib/cause/recordLoading.js +116 -0
- package/dist/esm/lib/cause/recordParents.d.ts +4 -0
- package/dist/esm/lib/cause/recordParents.js +86 -0
- package/dist/esm/lib/cause/recordScope.d.ts +8 -0
- package/dist/esm/lib/cause/recordScope.js +60 -0
- package/dist/esm/lib/cause/recordTypes.d.ts +67 -0
- package/dist/esm/lib/cause/recordTypes.js +1 -0
- package/dist/esm/lib/cause/recordValidation.d.ts +33 -0
- package/dist/esm/lib/cause/recordValidation.js +198 -0
- package/dist/esm/lib/cause/session.d.ts +30 -0
- package/dist/esm/lib/cause/session.js +161 -0
- package/dist/esm/lib/cause/storedRecordAdapter.d.ts +11 -0
- package/dist/esm/lib/cause/storedRecordAdapter.js +57 -0
- package/dist/esm/lib/cause/strategies.d.ts +3 -0
- package/dist/esm/lib/cause/strategies.js +21 -0
- package/dist/esm/lib/cause/types.d.ts +90 -0
- package/dist/esm/lib/cause/types.js +1 -0
- package/dist/esm/lib/cause/validation.d.ts +24 -0
- package/dist/esm/lib/cause/validation.js +134 -0
- package/dist/esm/lib/cause/walk.d.ts +12 -0
- package/dist/esm/lib/cause/walk.js +174 -0
- package/dist/esm/lib/ids/attemptId.d.ts +63 -0
- package/dist/esm/lib/ids/attemptId.js +91 -0
- package/dist/esm/lib/memory/EventLog.d.ts +89 -3
- package/dist/esm/lib/memory/EventLog.js +242 -3
- package/dist/esm/lib/memory/honesty.d.ts +6 -4
- package/dist/esm/lib/memory/honesty.js +7 -4
- package/dist/esm/lib/memory/sink.d.ts +148 -0
- package/dist/esm/lib/memory/sink.js +92 -0
- package/dist/esm/lib/memory/types.d.ts +4 -1
- package/dist/esm/lib/memory/types.js +1 -1
- package/dist/esm/lib/time-travel/bundles.d.ts +2 -0
- package/dist/esm/lib/time-travel/bundles.js +16 -0
- package/dist/esm/lib/time-travel/commitStops.js +21 -2
- package/dist/esm/lib/time-travel/index.d.ts +2 -0
- package/dist/esm/lib/time-travel/index.js +1 -0
- package/dist/esm/lib/time-travel/storedRecord.d.ts +195 -0
- package/dist/esm/lib/time-travel/storedRecord.js +494 -0
- package/dist/esm/lib/time-travel/timeTravel.js +11 -0
- package/dist/esm/lib/time-travel/types.d.ts +4 -1
- package/dist/esm/write.d.ts +7 -0
- package/dist/esm/write.js +6 -0
- package/dist/index.js +6 -2
- package/dist/lib/cause/admission.js +67 -0
- package/dist/lib/cause/explain.js +119 -0
- package/dist/lib/cause/ranking.js +14 -0
- package/dist/lib/cause/recordAdapter.js +55 -0
- package/dist/lib/cause/recordChildren.js +83 -0
- package/dist/lib/cause/recordControl.js +74 -0
- package/dist/lib/cause/recordCoverage.js +99 -0
- package/dist/lib/cause/recordIdentity.js +73 -0
- package/dist/lib/cause/recordLoading.js +121 -0
- package/dist/lib/cause/recordParents.js +90 -0
- package/dist/lib/cause/recordScope.js +66 -0
- package/dist/lib/cause/recordTypes.js +2 -0
- package/dist/lib/cause/recordValidation.js +213 -0
- package/dist/lib/cause/session.js +165 -0
- package/dist/lib/cause/storedRecordAdapter.js +61 -0
- package/dist/lib/cause/strategies.js +26 -0
- package/dist/lib/cause/types.js +2 -0
- package/dist/lib/cause/validation.js +150 -0
- package/dist/lib/cause/walk.js +180 -0
- package/dist/lib/ids/attemptId.js +96 -0
- package/dist/lib/memory/EventLog.js +242 -3
- package/dist/lib/memory/honesty.js +7 -4
- package/dist/lib/memory/sink.js +98 -0
- package/dist/lib/memory/types.js +1 -1
- package/dist/lib/time-travel/bundles.js +18 -1
- package/dist/lib/time-travel/commitStops.js +21 -2
- package/dist/lib/time-travel/index.js +3 -1
- package/dist/lib/time-travel/storedRecord.js +498 -0
- package/dist/lib/time-travel/timeTravel.js +11 -0
- package/dist/types/cause.d.ts +8 -0
- package/dist/types/index.d.ts +7 -2
- package/dist/types/lib/cause/admission.d.ts +27 -0
- package/dist/types/lib/cause/explain.d.ts +5 -0
- package/dist/types/lib/cause/ranking.d.ts +6 -0
- package/dist/types/lib/cause/recordAdapter.d.ts +9 -0
- package/dist/types/lib/cause/recordChildren.d.ts +4 -0
- package/dist/types/lib/cause/recordControl.d.ts +13 -0
- package/dist/types/lib/cause/recordCoverage.d.ts +24 -0
- package/dist/types/lib/cause/recordIdentity.d.ts +9 -0
- package/dist/types/lib/cause/recordLoading.d.ts +7 -0
- package/dist/types/lib/cause/recordParents.d.ts +4 -0
- package/dist/types/lib/cause/recordScope.d.ts +8 -0
- package/dist/types/lib/cause/recordTypes.d.ts +67 -0
- package/dist/types/lib/cause/recordValidation.d.ts +33 -0
- package/dist/types/lib/cause/session.d.ts +30 -0
- package/dist/types/lib/cause/storedRecordAdapter.d.ts +11 -0
- package/dist/types/lib/cause/strategies.d.ts +3 -0
- package/dist/types/lib/cause/types.d.ts +90 -0
- package/dist/types/lib/cause/validation.d.ts +24 -0
- package/dist/types/lib/cause/walk.d.ts +12 -0
- package/dist/types/lib/ids/attemptId.d.ts +63 -0
- package/dist/types/lib/memory/EventLog.d.ts +89 -3
- package/dist/types/lib/memory/honesty.d.ts +6 -4
- package/dist/types/lib/memory/sink.d.ts +148 -0
- package/dist/types/lib/memory/types.d.ts +4 -1
- package/dist/types/lib/time-travel/bundles.d.ts +2 -0
- package/dist/types/lib/time-travel/index.d.ts +2 -0
- package/dist/types/lib/time-travel/storedRecord.d.ts +195 -0
- package/dist/types/lib/time-travel/types.d.ts +4 -1
- package/dist/types/write.d.ts +7 -0
- package/dist/write.js +8 -1
- package/package.json +67 -13
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.attentionList = exports.explain = void 0;
|
|
4
|
+
const session_js_1 = require("./session.js");
|
|
5
|
+
const walk_js_1 = require("./walk.js");
|
|
6
|
+
const validation_js_1 = require("./validation.js");
|
|
7
|
+
const ranking_js_1 = require("./ranking.js");
|
|
8
|
+
/** Read-only deterministic cause localisation and impact. No inference from timestamps or structural ancestry. */
|
|
9
|
+
async function explain(graph, nodeId, options = {}) {
|
|
10
|
+
(0, validation_js_1.name)(nodeId);
|
|
11
|
+
const session = new session_js_1.Session(graph, options);
|
|
12
|
+
try {
|
|
13
|
+
const backward = await (0, walk_js_1.walk)(session, [nodeId], 'parents');
|
|
14
|
+
const missing = [...backward.gaps];
|
|
15
|
+
let candidates = (0, walk_js_1.frontier)(backward).map((item) => item.nodeId);
|
|
16
|
+
const rank = (0, validation_js_1.own)(options, 'rankRoots');
|
|
17
|
+
if (rank !== undefined) {
|
|
18
|
+
if (typeof rank !== 'function')
|
|
19
|
+
throw new TypeError('CAUSE_CONFIGURATION_INVALID');
|
|
20
|
+
const eligible = backward.marked.filter((item) => item.attention.some((mark) => mark.kind !== 'not-collected'));
|
|
21
|
+
const selection = await session.invoke(undefined, () => rank(Object.freeze(eligible), session.context), (raw) => (0, ranking_js_1.admitRanking)(raw, new Set(eligible.map((item) => item.nodeId)), {
|
|
22
|
+
exactPermutation: false,
|
|
23
|
+
maximum: session.limits.maxNodes,
|
|
24
|
+
}));
|
|
25
|
+
if (selection.gap)
|
|
26
|
+
missing.push(selection.gap, (0, validation_js_1.gap)('strategy-failed'));
|
|
27
|
+
else
|
|
28
|
+
candidates = [...(selection.value ?? [])];
|
|
29
|
+
}
|
|
30
|
+
const stopped = session.stopGap(nodeId);
|
|
31
|
+
if (stopped)
|
|
32
|
+
missing.push(stopped);
|
|
33
|
+
const causeComplete = missing.length === 0;
|
|
34
|
+
const rootCause = causeComplete && candidates.length === 1 ? candidates[0] : undefined;
|
|
35
|
+
const forward = await (0, walk_js_1.walk)(session, [nodeId], 'children');
|
|
36
|
+
let impactNodes = Object.freeze(forward.nodes.filter((id) => id !== nodeId));
|
|
37
|
+
const impactGaps = [...forward.gaps], rankImpact = (0, validation_js_1.own)(options, 'rankImpact');
|
|
38
|
+
if (typeof rankImpact === 'function') {
|
|
39
|
+
const selection = await session.invoke(undefined, () => rankImpact(impactNodes, session.context), (raw) => (0, ranking_js_1.admitRanking)(raw, new Set(impactNodes), {
|
|
40
|
+
exactPermutation: true,
|
|
41
|
+
maximum: session.limits.maxNodes,
|
|
42
|
+
}));
|
|
43
|
+
if (selection.gap)
|
|
44
|
+
impactGaps.push(selection.gap, (0, validation_js_1.gap)('strategy-failed'));
|
|
45
|
+
else
|
|
46
|
+
impactNodes = selection.value ?? impactNodes;
|
|
47
|
+
}
|
|
48
|
+
const impact = Object.freeze({
|
|
49
|
+
nodes: impactNodes,
|
|
50
|
+
edges: forward.edges,
|
|
51
|
+
confidence: impactGaps.length ? 'incomplete' : 'complete',
|
|
52
|
+
gaps: (0, validation_js_1.uniqueGaps)(impactGaps),
|
|
53
|
+
});
|
|
54
|
+
const allGaps = (0, validation_js_1.uniqueGaps)([...missing, ...impactGaps]);
|
|
55
|
+
return Object.freeze({
|
|
56
|
+
nodeId,
|
|
57
|
+
attention: backward.marked.find((item) => item.nodeId === nodeId)?.attention ?? Object.freeze([]),
|
|
58
|
+
...(rootCause ? { rootCause } : {}),
|
|
59
|
+
path: rootCause ? (0, walk_js_1.pathTo)(backward.edges, nodeId, rootCause) : Object.freeze([]),
|
|
60
|
+
alternatives: Object.freeze(rootCause ? [] : candidates),
|
|
61
|
+
impact,
|
|
62
|
+
confidence: allGaps.length ? 'incomplete' : 'complete',
|
|
63
|
+
gaps: allGaps,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
session.dispose();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.explain = explain;
|
|
71
|
+
/** Discover only bounded host seeds; this is not a global scan of arbitrary storage. */
|
|
72
|
+
async function attentionList(graph, options = {}) {
|
|
73
|
+
const session = new session_js_1.Session(graph, options);
|
|
74
|
+
try {
|
|
75
|
+
let seeds = [], missing = [];
|
|
76
|
+
const requested = (0, validation_js_1.own)(options, 'nodeIds');
|
|
77
|
+
const admitSeeds = (raw) => {
|
|
78
|
+
// Only inspect the bounded prefix; the unread tail is explicitly incomplete.
|
|
79
|
+
if (!Array.isArray(raw))
|
|
80
|
+
return (0, validation_js_1.nodeIds)(raw, session.limits.maxNodes);
|
|
81
|
+
const length = (0, validation_js_1.own)(raw, 'length');
|
|
82
|
+
if (typeof length !== 'number' || !Number.isSafeInteger(length) || length < 0)
|
|
83
|
+
return (0, validation_js_1.nodeIds)(raw, session.limits.maxNodes);
|
|
84
|
+
if (length > session.limits.maxNodes)
|
|
85
|
+
missing.push((0, validation_js_1.gap)('node-limit'));
|
|
86
|
+
const prefix = [];
|
|
87
|
+
for (let i = 0; i < Math.min(length, session.limits.maxNodes); i++)
|
|
88
|
+
prefix.push((0, validation_js_1.own)(raw, String(i)));
|
|
89
|
+
return (0, validation_js_1.nodeIds)(prefix, session.limits.maxNodes);
|
|
90
|
+
};
|
|
91
|
+
if (requested !== undefined) {
|
|
92
|
+
const read = await session.invoke(undefined, () => requested, admitSeeds);
|
|
93
|
+
if (read.gap)
|
|
94
|
+
missing.push(read.gap);
|
|
95
|
+
else
|
|
96
|
+
seeds = read.value ?? [];
|
|
97
|
+
}
|
|
98
|
+
else if (session.hasRoots()) {
|
|
99
|
+
const read = await session.read('roots', undefined, admitSeeds);
|
|
100
|
+
if (read.gap)
|
|
101
|
+
missing.push(read.gap);
|
|
102
|
+
else
|
|
103
|
+
seeds = read.value ?? [];
|
|
104
|
+
}
|
|
105
|
+
else
|
|
106
|
+
missing.push((0, validation_js_1.gap)('not-collected'));
|
|
107
|
+
const result = await (0, walk_js_1.walk)(session, seeds, 'children');
|
|
108
|
+
missing = [...missing, ...result.gaps];
|
|
109
|
+
return Object.freeze({
|
|
110
|
+
nodes: result.marked,
|
|
111
|
+
confidence: missing.length ? 'incomplete' : 'complete',
|
|
112
|
+
gaps: (0, validation_js_1.uniqueGaps)(missing),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
session.dispose();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
exports.attentionList = attentionList;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.admitRanking = void 0;
|
|
4
|
+
const validation_js_1 = require("./validation.js");
|
|
5
|
+
/** Strategies may only select admitted nodes; impact ranking must preserve the full set. */
|
|
6
|
+
function admitRanking(raw, allowed, options) {
|
|
7
|
+
const ids = (0, validation_js_1.nodeIds)(raw, options.maximum);
|
|
8
|
+
if ((options.exactPermutation && ids.length !== allowed.size) ||
|
|
9
|
+
new Set(ids).size !== ids.length ||
|
|
10
|
+
ids.some((id) => !allowed.has(id)))
|
|
11
|
+
throw new TypeError();
|
|
12
|
+
return ids;
|
|
13
|
+
}
|
|
14
|
+
exports.admitRanking = admitRanking;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRecordGraph = exports.createLazyRecordGraph = exports.recordOptions = void 0;
|
|
4
|
+
const recordChildren_js_1 = require("./recordChildren.js");
|
|
5
|
+
const recordCoverage_js_1 = require("./recordCoverage.js");
|
|
6
|
+
const recordIdentity_js_1 = require("./recordIdentity.js");
|
|
7
|
+
const recordLoading_js_1 = require("./recordLoading.js");
|
|
8
|
+
const recordParents_js_1 = require("./recordParents.js");
|
|
9
|
+
const recordScope_js_1 = require("./recordScope.js");
|
|
10
|
+
const recordValidation_js_1 = require("./recordValidation.js");
|
|
11
|
+
var recordScope_js_2 = require("./recordScope.js");
|
|
12
|
+
Object.defineProperty(exports, "recordOptions", { enumerable: true, get: function () { return recordScope_js_2.recordOptions; } });
|
|
13
|
+
/** Common engine for memory and one-scope-at-a-time host storage. Loader functions are trusted executable code. */
|
|
14
|
+
function createLazyRecordGraph(load, options = {}) {
|
|
15
|
+
const limits = (0, recordScope_js_1.recordOptions)(options), loader = (0, recordLoading_js_1.createRecordScopeLoader)(load, limits);
|
|
16
|
+
const resolve = (nodeId, context) => (0, recordIdentity_js_1.resolveRecordNode)(nodeId, context, loader);
|
|
17
|
+
return Object.freeze({
|
|
18
|
+
nodeId: (address, node) => (0, recordIdentity_js_1.nodeIdFor)(address, node),
|
|
19
|
+
openScope: (address, context) => (0, recordLoading_js_1.openRecordScope)(address, context, loader),
|
|
20
|
+
async parents(nodeId, context) {
|
|
21
|
+
const node = await resolve(nodeId, context);
|
|
22
|
+
return 'failure' in node
|
|
23
|
+
? { edges: [], complete: false }
|
|
24
|
+
: (0, recordParents_js_1.inspectRecordNode)(node, limits.maxEdges, loader.extraGapCodes(node.scope)).adjacency;
|
|
25
|
+
},
|
|
26
|
+
async gaps(nodeId, context) {
|
|
27
|
+
const node = await resolve(nodeId, context);
|
|
28
|
+
return 'failure' in node
|
|
29
|
+
? (0, recordCoverage_js_1.gapForRecordFailure)(node.failure)
|
|
30
|
+
: (0, recordParents_js_1.inspectRecordNode)(node, limits.maxEdges, loader.extraGapCodes(node.scope)).gaps;
|
|
31
|
+
},
|
|
32
|
+
async attention(nodeId, context) {
|
|
33
|
+
return (0, recordCoverage_js_1.attentionForRecordNode)(await resolve(nodeId, context));
|
|
34
|
+
},
|
|
35
|
+
async children(nodeId, context) {
|
|
36
|
+
const node = await resolve(nodeId, context);
|
|
37
|
+
return 'failure' in node
|
|
38
|
+
? { edges: [], complete: false }
|
|
39
|
+
: (0, recordChildren_js_1.childrenForRecordNode)(node, limits.maxEdges, loader.extraGapCodes(node.scope));
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
exports.createLazyRecordGraph = createLazyRecordGraph;
|
|
44
|
+
/** A finished record already in memory; payload admission is lazy per requested scope. */
|
|
45
|
+
function createRecordGraph(scopes, options = {}) {
|
|
46
|
+
const limits = (0, recordScope_js_1.recordOptions)(options);
|
|
47
|
+
return createLazyRecordGraph((address) => {
|
|
48
|
+
for (const raw of (0, recordValidation_js_1.list)(scopes, limits.maxScopes)) {
|
|
49
|
+
if ((0, recordValidation_js_1.addressKey)((0, recordValidation_js_1.addressOf)((0, recordValidation_js_1.own)(raw, 'address'))) === (0, recordValidation_js_1.addressKey)(address))
|
|
50
|
+
return { scope: raw };
|
|
51
|
+
}
|
|
52
|
+
throw new recordValidation_js_1.RecordRefusal('unavailable');
|
|
53
|
+
}, limits);
|
|
54
|
+
}
|
|
55
|
+
exports.createRecordGraph = createRecordGraph;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.childrenForRecordNode = void 0;
|
|
4
|
+
const forwardSliceForKey_js_1 = require("../slice/forwardSliceForKey.js");
|
|
5
|
+
const recordControl_js_1 = require("./recordControl.js");
|
|
6
|
+
const recordCoverage_js_1 = require("./recordCoverage.js");
|
|
7
|
+
const recordIdentity_js_1 = require("./recordIdentity.js");
|
|
8
|
+
const recordParents_js_1 = require("./recordParents.js");
|
|
9
|
+
const recordScope_js_1 = require("./recordScope.js");
|
|
10
|
+
/** Keep per-write and conservative fed edges distinct while retaining existing cap and deduplication order. */
|
|
11
|
+
function appendFedChildren(scope, root, maximumEdges, edges) {
|
|
12
|
+
let complete = true;
|
|
13
|
+
for (const edge of root.fedEdges) {
|
|
14
|
+
const child = edge.child;
|
|
15
|
+
if (child.commitIdx === undefined || !child.runtimeStageId) {
|
|
16
|
+
complete = false;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (edges.length >= maximumEdges) {
|
|
20
|
+
complete = false;
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
const nodeId = (0, recordIdentity_js_1.nodeIdFor)(scope.address, {
|
|
24
|
+
runtimeStageId: child.runtimeStageId,
|
|
25
|
+
commitIdx: child.commitIdx,
|
|
26
|
+
key: child.key,
|
|
27
|
+
});
|
|
28
|
+
if (!edges.some((existing) => existing.nodeId === nodeId))
|
|
29
|
+
edges.push({ nodeId, kind: 'data', attribution: edge.basis, evidence: ['commit:' + child.commitIdx] });
|
|
30
|
+
if (edge.basis === 'stage')
|
|
31
|
+
complete = false;
|
|
32
|
+
}
|
|
33
|
+
return complete;
|
|
34
|
+
}
|
|
35
|
+
/** A consumer with no writes still needs a qualified node so its recorded control flow remains reachable. */
|
|
36
|
+
function appendReadOnlyChildren(scope, root, key, maximumEdges, edges) {
|
|
37
|
+
let complete = true;
|
|
38
|
+
for (const read of root.reads) {
|
|
39
|
+
if (scope.log[read.commitIdx]?.trace.length)
|
|
40
|
+
continue;
|
|
41
|
+
if (edges.length >= maximumEdges) {
|
|
42
|
+
complete = false;
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
const nodeId = (0, recordIdentity_js_1.nodeIdFor)(scope.address, { runtimeStageId: read.runtimeStageId, commitIdx: read.commitIdx, key }, true);
|
|
46
|
+
if (!edges.some((existing) => existing.nodeId === nodeId))
|
|
47
|
+
edges.push({ nodeId, kind: 'data', attribution: 'per-write', evidence: ['commit:' + read.commitIdx] });
|
|
48
|
+
}
|
|
49
|
+
return complete;
|
|
50
|
+
}
|
|
51
|
+
/** Query each written value at its original commit boundary instead of the value's latest writer. */
|
|
52
|
+
function appendDataChildren(scope, identity, commitIndex, maximumEdges, edges) {
|
|
53
|
+
let complete = true;
|
|
54
|
+
const lookup = (runtimeStageId) => (0, recordScope_js_1.readKeysForStage)(scope, runtimeStageId);
|
|
55
|
+
for (const key of (0, recordCoverage_js_1.writtenKeysForNode)(scope.log[commitIndex], identity)) {
|
|
56
|
+
const forward = (0, forwardSliceForKey_js_1.forwardSliceForKey)(scope.log, key, lookup, {
|
|
57
|
+
before: commitIndex + 1,
|
|
58
|
+
maxDepth: 1,
|
|
59
|
+
maxNodes: maximumEdges + 1,
|
|
60
|
+
});
|
|
61
|
+
if (forward.root?.truncated?.byNodes)
|
|
62
|
+
complete = false;
|
|
63
|
+
if (forward.root?.commitIdx !== commitIndex) {
|
|
64
|
+
complete = false;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
complete = appendFedChildren(scope, forward.root, maximumEdges, edges) && complete;
|
|
68
|
+
complete = appendReadOnlyChildren(scope, forward.root, key, maximumEdges, edges) && complete;
|
|
69
|
+
}
|
|
70
|
+
return complete;
|
|
71
|
+
}
|
|
72
|
+
/** Share parent-side coverage checks before appending data then control children in their established order. */
|
|
73
|
+
function childrenForRecordNode(node, maximumEdges, extraGapCodes) {
|
|
74
|
+
const { scope, identity, commitIndex } = node;
|
|
75
|
+
const base = (0, recordParents_js_1.inspectRecordNode)(node, maximumEdges, extraGapCodes), edges = [];
|
|
76
|
+
let complete = base.gaps.length === 0;
|
|
77
|
+
if (!(0, recordCoverage_js_1.allStageReadsCollected)(scope))
|
|
78
|
+
complete = false;
|
|
79
|
+
complete = appendDataChildren(scope, identity, commitIndex, maximumEdges, edges) && complete;
|
|
80
|
+
complete = (0, recordControl_js_1.appendControlChildren)(scope, identity, commitIndex, maximumEdges, edges) && complete;
|
|
81
|
+
return { edges, complete };
|
|
82
|
+
}
|
|
83
|
+
exports.childrenForRecordNode = childrenForRecordNode;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.appendControlChildren = exports.collectControlChildCoverage = exports.collectControlParentCoverage = exports.controlParentLookup = exports.controlChildren = void 0;
|
|
4
|
+
const recordIdentity_js_1 = require("./recordIdentity.js");
|
|
5
|
+
/** Only an execution or captured read may govern control flow; a written key alone cannot. */
|
|
6
|
+
function controlChildren(scope, identity) {
|
|
7
|
+
// A read-only execution may govern later work; a particular written key is not itself a decision.
|
|
8
|
+
return identity.key === undefined || identity.read ? scope.control ?? [] : [];
|
|
9
|
+
}
|
|
10
|
+
exports.controlChildren = controlChildren;
|
|
11
|
+
/** Canonical slicing receives only control parents that existed before the dependent execution. */
|
|
12
|
+
function controlParentLookup(scope) {
|
|
13
|
+
return scope.control === undefined
|
|
14
|
+
? undefined
|
|
15
|
+
: (runtimeStageId) => {
|
|
16
|
+
const dependency = scope.control.get(runtimeStageId);
|
|
17
|
+
if (!dependency)
|
|
18
|
+
return undefined;
|
|
19
|
+
const parentIndex = scope.log.findIndex((bundle) => bundle.runtimeStageId === dependency.deciderId), childIndex = scope.log.findIndex((bundle) => bundle.runtimeStageId === runtimeStageId);
|
|
20
|
+
return parentIndex >= 0 && parentIndex < childIndex ? dependency : undefined;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
exports.controlParentLookup = controlParentLookup;
|
|
24
|
+
/** Keep missing or future deciders visible rather than silently dropping their recorded dependency. */
|
|
25
|
+
function collectControlParentCoverage(scope, identity, firstCommitIndex, addGap) {
|
|
26
|
+
const dependency = scope.control?.get(identity.runtimeStageId);
|
|
27
|
+
if (!dependency)
|
|
28
|
+
return;
|
|
29
|
+
const parentIndex = scope.log.findIndex((bundle) => bundle.runtimeStageId === dependency.deciderId);
|
|
30
|
+
if (parentIndex < 0)
|
|
31
|
+
addGap('scope-boundary');
|
|
32
|
+
else if (parentIndex >= firstCommitIndex)
|
|
33
|
+
addGap('invalid-data');
|
|
34
|
+
}
|
|
35
|
+
exports.collectControlParentCoverage = collectControlParentCoverage;
|
|
36
|
+
/** Include control fan-out in the same immediate-neighbor cap used for data adjacency. */
|
|
37
|
+
function collectControlChildCoverage(scope, identity, firstCommitIndex, neighbors, addGap) {
|
|
38
|
+
for (const [childStageId, dependency] of controlChildren(scope, identity))
|
|
39
|
+
if (dependency.deciderId === identity.runtimeStageId) {
|
|
40
|
+
const childIndex = scope.log.findIndex((bundle) => bundle.runtimeStageId === childStageId);
|
|
41
|
+
if (childIndex < 0)
|
|
42
|
+
addGap('scope-boundary');
|
|
43
|
+
else if (childIndex <= firstCommitIndex)
|
|
44
|
+
addGap('invalid-data');
|
|
45
|
+
else
|
|
46
|
+
neighbors.add(JSON.stringify(['control', childStageId]));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.collectControlChildCoverage = collectControlChildCoverage;
|
|
50
|
+
/** Preserve recorded control order and coverage when a dependent execution is absent or capped. */
|
|
51
|
+
function appendControlChildren(scope, identity, commitIndex, maximumEdges, edges) {
|
|
52
|
+
let complete = true;
|
|
53
|
+
for (const [childStageId, dependency] of controlChildren(scope, identity)) {
|
|
54
|
+
if (dependency.deciderId !== identity.runtimeStageId)
|
|
55
|
+
continue;
|
|
56
|
+
const childIndex = scope.log.findIndex((bundle) => bundle.runtimeStageId === childStageId);
|
|
57
|
+
if (childIndex <= commitIndex) {
|
|
58
|
+
complete = false;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (edges.length >= maximumEdges) {
|
|
62
|
+
complete = false;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
edges.push({
|
|
66
|
+
nodeId: (0, recordIdentity_js_1.nodeIdFor)(scope.address, { runtimeStageId: childStageId, commitIdx: childIndex }),
|
|
67
|
+
kind: 'control',
|
|
68
|
+
attribution: 'recorded-control',
|
|
69
|
+
evidence: ['commit:' + childIndex],
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return complete;
|
|
73
|
+
}
|
|
74
|
+
exports.appendControlChildren = appendControlChildren;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.allStageReadsCollected = exports.collectReadCoverage = exports.readAttributionIsPrecise = exports.collectForwardCoverage = exports.writtenKeysForNode = exports.collectScopeCoverage = exports.attentionForRecordNode = exports.gapForRecordFailure = exports.recordGapCollector = void 0;
|
|
4
|
+
const keyPaths_js_1 = require("../memory/keyPaths.js");
|
|
5
|
+
const forwardSliceForKey_js_1 = require("../slice/forwardSliceForKey.js");
|
|
6
|
+
const recordControl_js_1 = require("./recordControl.js");
|
|
7
|
+
const recordScope_js_1 = require("./recordScope.js");
|
|
8
|
+
/** Preserve first-observed coverage order while collapsing repeated codes from different evidence paths. */
|
|
9
|
+
function recordGapCollector() {
|
|
10
|
+
const gaps = [];
|
|
11
|
+
return {
|
|
12
|
+
gaps,
|
|
13
|
+
addGap: (code) => {
|
|
14
|
+
if (!gaps.some((gap) => gap.code === code))
|
|
15
|
+
gaps.push({ code, evidence: [] });
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
exports.recordGapCollector = recordGapCollector;
|
|
20
|
+
/** Failed loading exposes a fixed category, never the host error or an apparently complete empty graph. */
|
|
21
|
+
function gapForRecordFailure(code) {
|
|
22
|
+
return [{ code, evidence: [] }];
|
|
23
|
+
}
|
|
24
|
+
exports.gapForRecordFailure = gapForRecordFailure;
|
|
25
|
+
/** Return captured mark references without treating an omitted attention map as collected evidence. */
|
|
26
|
+
function attentionForRecordNode(node) {
|
|
27
|
+
// An omitted map stays unknown coverage; it is never replaced by an affirmative empty map.
|
|
28
|
+
return 'failure' in node ? [] : node.scope.marks?.get(node.identity.runtimeStageId) ?? [];
|
|
29
|
+
}
|
|
30
|
+
exports.attentionForRecordNode = attentionForRecordNode;
|
|
31
|
+
/** Emit host and capture gaps before query-derived gaps so existing evidence order stays stable. */
|
|
32
|
+
function collectScopeCoverage(scope, bundle, extraGapCodes, addGap) {
|
|
33
|
+
for (const code of extraGapCodes ?? [])
|
|
34
|
+
addGap(code);
|
|
35
|
+
if (!scope.complete || scope.control === undefined || scope.marks === undefined)
|
|
36
|
+
addGap('not-collected');
|
|
37
|
+
if (bundle.untrackedSources?.length)
|
|
38
|
+
addGap('untracked-input');
|
|
39
|
+
if (bundle.redactedPaths.length)
|
|
40
|
+
addGap('redacted');
|
|
41
|
+
}
|
|
42
|
+
exports.collectScopeCoverage = collectScopeCoverage;
|
|
43
|
+
/** A read-only identity has no new value life; unqualified executions retain written-key encounter order. */
|
|
44
|
+
function writtenKeysForNode(bundle, identity) {
|
|
45
|
+
return identity.read
|
|
46
|
+
? []
|
|
47
|
+
: identity.key === undefined
|
|
48
|
+
? [...new Set(bundle.trace.map((entry) => entry.path))]
|
|
49
|
+
: [identity.key];
|
|
50
|
+
}
|
|
51
|
+
exports.writtenKeysForNode = writtenKeysForNode;
|
|
52
|
+
/** Immediate adjacency caps affect graph coverage even when the caller asks only for parents. */
|
|
53
|
+
function collectForwardCoverage(scope, identity, commitIndex, firstCommitIndex, maximumEdges, addGap) {
|
|
54
|
+
const neighbors = new Set();
|
|
55
|
+
const lookup = (runtimeStageId) => (0, recordScope_js_1.readKeysForStage)(scope, runtimeStageId);
|
|
56
|
+
for (const key of writtenKeysForNode(scope.log[commitIndex], identity)) {
|
|
57
|
+
const forward = (0, forwardSliceForKey_js_1.forwardSliceForKey)(scope.log, key, lookup, {
|
|
58
|
+
before: commitIndex + 1,
|
|
59
|
+
maxDepth: 1,
|
|
60
|
+
maxNodes: maximumEdges + 1,
|
|
61
|
+
});
|
|
62
|
+
// A one-hop query intentionally stops at depth one; only a node cap hides immediate adjacency.
|
|
63
|
+
if (forward.root?.truncated?.byNodes)
|
|
64
|
+
addGap('edge-limit');
|
|
65
|
+
for (const edge of forward.root?.fedEdges ?? [])
|
|
66
|
+
neighbors.add(JSON.stringify(['data', edge.child.commitIdx, edge.child.key]));
|
|
67
|
+
for (const read of forward.root?.reads ?? [])
|
|
68
|
+
if (!scope.log[read.commitIdx]?.trace.length)
|
|
69
|
+
neighbors.add(JSON.stringify(['read', read.commitIdx, key]));
|
|
70
|
+
}
|
|
71
|
+
(0, recordControl_js_1.collectControlChildCoverage)(scope, identity, firstCommitIndex, neighbors, addGap);
|
|
72
|
+
if (neighbors.size > maximumEdges)
|
|
73
|
+
addGap('edge-limit');
|
|
74
|
+
}
|
|
75
|
+
exports.collectForwardCoverage = collectForwardCoverage;
|
|
76
|
+
/** Exact attribution requires captured per-write provenance or a validated read-only key context. */
|
|
77
|
+
function readAttributionIsPrecise(scope, identity, bundle) {
|
|
78
|
+
const relevant = identity.key === undefined
|
|
79
|
+
? bundle.trace
|
|
80
|
+
: bundle.trace.filter((entry) => (0, keyPaths_js_1.relation)(entry.path, identity.key) !== undefined);
|
|
81
|
+
return identity.read === true
|
|
82
|
+
? identity.key !== undefined && (0, recordScope_js_1.readKeysForStage)(scope, identity.runtimeStageId).includes(identity.key)
|
|
83
|
+
: identity.key !== undefined && relevant.length > 0 && relevant.every((entry) => entry.readKeys !== undefined);
|
|
84
|
+
}
|
|
85
|
+
exports.readAttributionIsPrecise = readAttributionIsPrecise;
|
|
86
|
+
/** Distinguish conservative attribution from missing collection without marking a proven empty leaf. */
|
|
87
|
+
function collectReadCoverage(scope, identity, precise, addGap) {
|
|
88
|
+
if (!precise && (identity.key !== undefined || (0, recordScope_js_1.readKeysForStage)(scope, identity.runtimeStageId).length > 0))
|
|
89
|
+
addGap('conservative');
|
|
90
|
+
if (!scope.reads?.has(identity.runtimeStageId) && !precise)
|
|
91
|
+
addGap('not-collected');
|
|
92
|
+
}
|
|
93
|
+
exports.collectReadCoverage = collectReadCoverage;
|
|
94
|
+
/** Forward absence is trustworthy only when every potentially consuming execution collected its reads. */
|
|
95
|
+
function allStageReadsCollected(scope) {
|
|
96
|
+
return !scope.log.some((bundle) => !scope.reads?.has(bundle.runtimeStageId) &&
|
|
97
|
+
(bundle.trace.length === 0 || bundle.trace.some((entry) => entry.readKeys === undefined)));
|
|
98
|
+
}
|
|
99
|
+
exports.allStageReadsCollected = allStageReadsCollected;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveRecordNode = exports.nodeIdFor = exports.readNodeIdentity = void 0;
|
|
4
|
+
const recordScope_js_1 = require("./recordScope.js");
|
|
5
|
+
const recordValidation_js_1 = require("./recordValidation.js");
|
|
6
|
+
/** Parse only the bounded identity encoding; payload fields never become graph addresses. */
|
|
7
|
+
function readNodeIdentity(nodeId) {
|
|
8
|
+
if (typeof nodeId !== 'string' || nodeId.length > 2048 || !nodeId.startsWith('record:'))
|
|
9
|
+
throw new recordValidation_js_1.RecordRefusal();
|
|
10
|
+
let fields;
|
|
11
|
+
try {
|
|
12
|
+
fields = JSON.parse(nodeId.slice(7));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
throw new recordValidation_js_1.RecordRefusal();
|
|
16
|
+
}
|
|
17
|
+
const address = (0, recordValidation_js_1.addressOf)((0, recordValidation_js_1.own)(fields, 'address')), runtimeStageId = (0, recordValidation_js_1.name)((0, recordValidation_js_1.own)(fields, 'runtimeStageId')), commitIndex = (0, recordValidation_js_1.own)(fields, 'commitIdx'), key = (0, recordValidation_js_1.own)(fields, 'key'), read = (0, recordValidation_js_1.own)(fields, 'read');
|
|
18
|
+
if (commitIndex !== undefined && (!Number.isSafeInteger(commitIndex) || commitIndex < 0))
|
|
19
|
+
throw new recordValidation_js_1.RecordRefusal();
|
|
20
|
+
if ((read !== undefined && read !== true) || (read === true && key === undefined))
|
|
21
|
+
throw new recordValidation_js_1.RecordRefusal();
|
|
22
|
+
return {
|
|
23
|
+
address,
|
|
24
|
+
runtimeStageId,
|
|
25
|
+
...(commitIndex === undefined ? {} : { commitIdx: commitIndex }),
|
|
26
|
+
...(key === undefined ? {} : { key: (0, recordValidation_js_1.name)(key, true) }),
|
|
27
|
+
...(read === true ? { read: true } : {}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
exports.readNodeIdentity = readNodeIdentity;
|
|
31
|
+
/** Stable field order preserves existing address/execution/key IDs across storage and memory readers. */
|
|
32
|
+
function nodeIdFor(addressInput, node, read) {
|
|
33
|
+
try {
|
|
34
|
+
const address = (0, recordValidation_js_1.addressOf)(addressInput), runtimeStageId = (0, recordValidation_js_1.name)((0, recordValidation_js_1.own)(node, 'runtimeStageId')), commitIndex = (0, recordValidation_js_1.own)(node, 'commitIdx'), key = (0, recordValidation_js_1.own)(node, 'key');
|
|
35
|
+
if (commitIndex !== undefined && (!Number.isSafeInteger(commitIndex) || commitIndex < 0))
|
|
36
|
+
throw new recordValidation_js_1.RecordRefusal();
|
|
37
|
+
const nodeId = 'record:' +
|
|
38
|
+
JSON.stringify({
|
|
39
|
+
address,
|
|
40
|
+
runtimeStageId,
|
|
41
|
+
...(commitIndex === undefined ? {} : { commitIdx: commitIndex }),
|
|
42
|
+
...(key === undefined ? {} : { key: (0, recordValidation_js_1.name)(key, true) }),
|
|
43
|
+
...(read ? { read: true } : {}),
|
|
44
|
+
});
|
|
45
|
+
return nodeId.length <= 2048 ? nodeId : 'record:invalid';
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return 'record:invalid';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.nodeIdFor = nodeIdFor;
|
|
52
|
+
/** An admitted ID must also match captured evidence before it can introduce a dependency. */
|
|
53
|
+
async function resolveRecordNode(nodeId, context, loader) {
|
|
54
|
+
try {
|
|
55
|
+
const identity = readNodeIdentity(nodeId), scope = await loader.loadScope(identity.address, context);
|
|
56
|
+
if ('failure' in scope)
|
|
57
|
+
return scope;
|
|
58
|
+
const commitIndex = identity.commitIdx ?? scope.log.findIndex((bundle) => bundle.runtimeStageId === identity.runtimeStageId);
|
|
59
|
+
if (commitIndex < 0 ||
|
|
60
|
+
commitIndex >= scope.log.length ||
|
|
61
|
+
scope.log[commitIndex].runtimeStageId !== identity.runtimeStageId)
|
|
62
|
+
throw new recordValidation_js_1.RecordRefusal();
|
|
63
|
+
// A qualified read is a captured fact, never a caller-supplied request to invent a dependency.
|
|
64
|
+
if (identity.read &&
|
|
65
|
+
(identity.key === undefined || !(0, recordScope_js_1.readKeysForStage)(scope, identity.runtimeStageId).includes(identity.key)))
|
|
66
|
+
throw new recordValidation_js_1.RecordRefusal();
|
|
67
|
+
return { scope, identity, commitIndex };
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
return { failure: error instanceof recordValidation_js_1.RecordRefusal ? error.code : 'invalid-data' };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
exports.resolveRecordNode = resolveRecordNode;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.openRecordScope = exports.createRecordScopeLoader = void 0;
|
|
4
|
+
const validation_js_1 = require("./validation.js");
|
|
5
|
+
const recordIdentity_js_1 = require("./recordIdentity.js");
|
|
6
|
+
const recordScope_js_1 = require("./recordScope.js");
|
|
7
|
+
const recordValidation_js_1 = require("./recordValidation.js");
|
|
8
|
+
/** Admit before caching so a cache hit cannot bypass payload limits or lose scope-owned coverage. */
|
|
9
|
+
function admitLoadedScope(result, request, limits, state) {
|
|
10
|
+
if (request.controller.signal.aborted || performance.now() >= request.deadline) {
|
|
11
|
+
request.controller.abort();
|
|
12
|
+
return { failure: request.context.signal.aborted ? 'cancelled' : 'timeout' };
|
|
13
|
+
}
|
|
14
|
+
let prepared;
|
|
15
|
+
try {
|
|
16
|
+
prepared = (0, recordScope_js_1.prepareRecordScope)((0, recordValidation_js_1.own)(result, 'scope'), request.address, limits.maxCommits, limits.maxFields);
|
|
17
|
+
const extra = (0, recordValidation_js_1.list)((0, recordValidation_js_1.own)(result, 'gaps') ?? [], 32);
|
|
18
|
+
// Coverage belongs to this admitted object, so cached scopes retain the same gap ordering and ownership.
|
|
19
|
+
state.extraGaps.set(prepared, (0, validation_js_1.gaps)(extra.map((code) => ({ code, evidence: [] }))).map((gap) => gap.code));
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
throw error instanceof recordValidation_js_1.RecordRefusal ? error : new recordValidation_js_1.RecordRefusal();
|
|
23
|
+
}
|
|
24
|
+
if (performance.now() >= request.deadline) {
|
|
25
|
+
request.controller.abort();
|
|
26
|
+
return { failure: 'timeout' };
|
|
27
|
+
}
|
|
28
|
+
if (state.cache.size >= limits.maxScopes && !state.cache.has(request.cacheKey))
|
|
29
|
+
return { failure: 'node-limit' };
|
|
30
|
+
state.cache.set(request.cacheKey, prepared);
|
|
31
|
+
return prepared;
|
|
32
|
+
}
|
|
33
|
+
/** Host failures become fixed gaps while unfinished host work continues to occupy its bounded slot. */
|
|
34
|
+
function startScopeWork(hostLoad, request, limits, state) {
|
|
35
|
+
const work = (0, recordValidation_js_1.projectLoaded)(() => {
|
|
36
|
+
if (request.controller.signal.aborted)
|
|
37
|
+
throw new recordValidation_js_1.RecordRefusal(request.context.signal.aborted ? 'cancelled' : 'timeout');
|
|
38
|
+
return hostLoad(request.address, { signal: request.controller.signal });
|
|
39
|
+
}, (result) => admitLoadedScope(result, request, limits, state)).catch((error) => {
|
|
40
|
+
if (request.context.signal.aborted)
|
|
41
|
+
return { failure: 'cancelled' };
|
|
42
|
+
if (performance.now() >= request.deadline) {
|
|
43
|
+
request.controller.abort();
|
|
44
|
+
return { failure: 'timeout' };
|
|
45
|
+
}
|
|
46
|
+
return { failure: error instanceof recordValidation_js_1.RecordRefusal ? error.code : 'unavailable' };
|
|
47
|
+
});
|
|
48
|
+
// A timed-out host operation retains its slot until it settles; repeated timeouts cannot grow an unbounded I/O pool.
|
|
49
|
+
void work.then(() => {
|
|
50
|
+
state.activeLoads--;
|
|
51
|
+
}, () => {
|
|
52
|
+
state.activeLoads--;
|
|
53
|
+
});
|
|
54
|
+
return work;
|
|
55
|
+
}
|
|
56
|
+
/** Bound caller waiting independently of host cooperation without sharing cancellation between requests. */
|
|
57
|
+
function createRecordScopeLoader(hostLoad, limits) {
|
|
58
|
+
const state = { activeLoads: 0, cache: new Map(), extraGaps: new WeakMap() };
|
|
59
|
+
return {
|
|
60
|
+
async loadScope(address, context) {
|
|
61
|
+
if (context.signal.aborted)
|
|
62
|
+
return { failure: 'cancelled' };
|
|
63
|
+
const cacheKey = (0, recordValidation_js_1.addressKey)(address);
|
|
64
|
+
const cached = state.cache.get(cacheKey);
|
|
65
|
+
if (cached)
|
|
66
|
+
return cached;
|
|
67
|
+
if (state.cache.size + state.activeLoads >= limits.maxScopes)
|
|
68
|
+
return { failure: 'node-limit' };
|
|
69
|
+
state.activeLoads++;
|
|
70
|
+
const deadline = performance.now() + limits.loadTimeoutMs;
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
let timer;
|
|
73
|
+
let finish;
|
|
74
|
+
const interrupted = new Promise((resolve) => {
|
|
75
|
+
finish = resolve;
|
|
76
|
+
});
|
|
77
|
+
const cancel = () => {
|
|
78
|
+
controller.abort();
|
|
79
|
+
finish?.({ failure: 'cancelled' });
|
|
80
|
+
};
|
|
81
|
+
context.signal.addEventListener('abort', cancel, { once: true });
|
|
82
|
+
timer = setTimeout(() => {
|
|
83
|
+
controller.abort();
|
|
84
|
+
finish?.({ failure: 'timeout' });
|
|
85
|
+
}, limits.loadTimeoutMs);
|
|
86
|
+
try {
|
|
87
|
+
const work = startScopeWork(hostLoad, { address, context, controller, deadline, cacheKey }, limits, state);
|
|
88
|
+
return await Promise.race([work, interrupted]);
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
if (timer !== undefined)
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
context.signal.removeEventListener('abort', cancel);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
extraGapCodes: (scope) => state.extraGaps.get(scope),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
exports.createRecordScopeLoader = createRecordScopeLoader;
|
|
100
|
+
/** Discovery must distinguish an unavailable scope from a successfully loaded empty record. */
|
|
101
|
+
async function openRecordScope(addressInput, context, loader) {
|
|
102
|
+
let address;
|
|
103
|
+
try {
|
|
104
|
+
address = (0, recordValidation_js_1.addressOf)(addressInput);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
throw new recordValidation_js_1.RecordRefusal();
|
|
108
|
+
}
|
|
109
|
+
const scope = await loader.loadScope(address, context);
|
|
110
|
+
if ('failure' in scope)
|
|
111
|
+
throw new recordValidation_js_1.RecordRefusal(scope.failure);
|
|
112
|
+
// Empty unfinished evidence has no node on which to report its gap; discovery must reject instead.
|
|
113
|
+
if (!scope.log.length &&
|
|
114
|
+
(!scope.complete ||
|
|
115
|
+
scope.control === undefined ||
|
|
116
|
+
scope.marks === undefined ||
|
|
117
|
+
(loader.extraGapCodes(scope)?.length ?? 0) > 0))
|
|
118
|
+
throw new recordValidation_js_1.RecordRefusal('not-collected');
|
|
119
|
+
return scope.log.map((bundle, commitIndex) => (0, recordIdentity_js_1.nodeIdFor)(address, { runtimeStageId: bundle.runtimeStageId, commitIdx: commitIndex }));
|
|
120
|
+
}
|
|
121
|
+
exports.openRecordScope = openRecordScope;
|