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.
- package/README.md +96 -2
- 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 +2 -0
- 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/memory/types.d.ts +4 -1
- package/dist/esm/lib/memory/types.js +1 -1
- 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/memory/types.js +1 -1
- package/dist/types/cause.d.ts +8 -0
- package/dist/types/index.d.ts +2 -0
- 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/memory/types.d.ts +4 -1
- package/package.json +67 -13
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { boundedList, boundedName, EvidenceRefusal, ownData } from './admission.js';
|
|
2
|
+
/** Graph-port refusals carry their code through Session; loader refusals are handled at the record boundary. */
|
|
3
|
+
export class AdmissionError extends EvidenceRefusal {
|
|
4
|
+
code;
|
|
5
|
+
constructor(code = 'invalid-data') {
|
|
6
|
+
super(code, 'Invalid cause evidence');
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Only ordinary native promise wrappers belong to the async host contract.
|
|
12
|
+
* An own constructor could run an accessor/species during intrinsic then().
|
|
13
|
+
* Exotic wrappers remain the trusted host's responsibility, including rejection handling.
|
|
14
|
+
*/
|
|
15
|
+
export function observePromise(raw, accept, reject) {
|
|
16
|
+
if (!raw || typeof raw !== 'object' || Object.getPrototypeOf(raw) !== Promise.prototype)
|
|
17
|
+
return false;
|
|
18
|
+
if (Object.getOwnPropertyDescriptor(raw, 'constructor'))
|
|
19
|
+
throw new AdmissionError();
|
|
20
|
+
try {
|
|
21
|
+
Promise.prototype.then.call(raw, accept, reject);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new AdmissionError();
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
const refuseGraph = (code) => new AdmissionError(code);
|
|
29
|
+
const graphDescriptors = { translateTrapErrors: false, requireOwnValue: false };
|
|
30
|
+
export function own(value, key) {
|
|
31
|
+
return ownData(value, key, refuseGraph, graphDescriptors);
|
|
32
|
+
}
|
|
33
|
+
export function plain(value) {
|
|
34
|
+
if (!value || typeof value !== 'object' || ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
|
|
35
|
+
throw new AdmissionError();
|
|
36
|
+
}
|
|
37
|
+
export function name(value, maximum = 2048) {
|
|
38
|
+
return boundedName(value, maximum, refuseGraph);
|
|
39
|
+
}
|
|
40
|
+
export function list(value, maximum, read, overflow = 'invalid-data') {
|
|
41
|
+
return boundedList(value, maximum, read, {
|
|
42
|
+
refuse: refuseGraph,
|
|
43
|
+
overflow,
|
|
44
|
+
freeze: true,
|
|
45
|
+
descriptors: graphDescriptors,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const reasons = new Set([
|
|
49
|
+
'ERROR_RECORDED',
|
|
50
|
+
'UNKNOWN_EFFECT_RECORDED',
|
|
51
|
+
'RETRY_STOP_RECORDED',
|
|
52
|
+
'TIMEOUT_RECORDED',
|
|
53
|
+
'RETRY_RECORDED',
|
|
54
|
+
'QUALITY_DROP_RECORDED',
|
|
55
|
+
'EVIDENCE_NOT_COLLECTED',
|
|
56
|
+
'OUTLIER_RECORDED',
|
|
57
|
+
'BUSINESS_RULE_MATCHED',
|
|
58
|
+
]);
|
|
59
|
+
const codes = new Set([
|
|
60
|
+
'not-collected',
|
|
61
|
+
'untracked-input',
|
|
62
|
+
'redacted',
|
|
63
|
+
'conservative',
|
|
64
|
+
'scope-boundary',
|
|
65
|
+
'invalid-data',
|
|
66
|
+
'unavailable',
|
|
67
|
+
'timeout',
|
|
68
|
+
'cancelled',
|
|
69
|
+
'cycle',
|
|
70
|
+
'node-limit',
|
|
71
|
+
'depth-limit',
|
|
72
|
+
'edge-limit',
|
|
73
|
+
'call-limit',
|
|
74
|
+
'truncated',
|
|
75
|
+
'strategy-failed',
|
|
76
|
+
]);
|
|
77
|
+
export const refs = (value) => list(value, 16, (item) => name(item, 512));
|
|
78
|
+
export function attention(value) {
|
|
79
|
+
return list(value, 32, (item) => {
|
|
80
|
+
plain(item);
|
|
81
|
+
const kind = own(item, 'kind'), reason = own(item, 'reason');
|
|
82
|
+
if (typeof kind !== 'string' ||
|
|
83
|
+
!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(kind) ||
|
|
84
|
+
typeof reason !== 'string' ||
|
|
85
|
+
!reasons.has(reason))
|
|
86
|
+
throw new AdmissionError();
|
|
87
|
+
const evidence = refs(own(item, 'evidence'));
|
|
88
|
+
if (kind !== 'not-collected' && evidence.length === 0)
|
|
89
|
+
throw new AdmissionError();
|
|
90
|
+
return Object.freeze({ kind, reason: reason, evidence });
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
export function gaps(value) {
|
|
94
|
+
return list(value, 32, (item) => {
|
|
95
|
+
plain(item);
|
|
96
|
+
const code = own(item, 'code'), nodeId = own(item, 'nodeId');
|
|
97
|
+
if (typeof code !== 'string' || !codes.has(code))
|
|
98
|
+
throw new AdmissionError();
|
|
99
|
+
return Object.freeze({
|
|
100
|
+
code: code,
|
|
101
|
+
...(nodeId === undefined ? {} : { nodeId: name(nodeId) }),
|
|
102
|
+
evidence: refs(own(item, 'evidence')),
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
export function adjacency(value, maximum) {
|
|
107
|
+
plain(value);
|
|
108
|
+
const complete = own(value, 'complete');
|
|
109
|
+
if (typeof complete !== 'boolean')
|
|
110
|
+
throw new AdmissionError();
|
|
111
|
+
const edges = list(own(value, 'edges'), maximum, (item) => {
|
|
112
|
+
plain(item);
|
|
113
|
+
const nodeId = name(own(item, 'nodeId')), kind = own(item, 'kind'), attribution = own(item, 'attribution');
|
|
114
|
+
if ((kind !== 'data' && kind !== 'control') ||
|
|
115
|
+
(attribution !== 'per-write' && attribution !== 'stage' && attribution !== 'recorded-control') ||
|
|
116
|
+
(kind === 'control' && attribution !== 'recorded-control') ||
|
|
117
|
+
(kind === 'data' && attribution === 'recorded-control'))
|
|
118
|
+
throw new AdmissionError();
|
|
119
|
+
return Object.freeze({ nodeId, kind, attribution, evidence: refs(own(item, 'evidence')) });
|
|
120
|
+
}, 'edge-limit');
|
|
121
|
+
return Object.freeze({ edges, complete });
|
|
122
|
+
}
|
|
123
|
+
export const nodeIds = (value, max) => list(value, max, (item) => name(item), 'node-limit');
|
|
124
|
+
export const gap = (code, nodeId) => Object.freeze({ code, ...(nodeId === undefined ? {} : { nodeId }), evidence: Object.freeze([]) });
|
|
125
|
+
export function uniqueGaps(items) {
|
|
126
|
+
const seen = new Set();
|
|
127
|
+
return Object.freeze(items.filter((item) => {
|
|
128
|
+
const key = JSON.stringify(item);
|
|
129
|
+
if (seen.has(key))
|
|
130
|
+
return false;
|
|
131
|
+
seen.add(key);
|
|
132
|
+
return true;
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Gap, MarkedNode, NodeId, TraversedEdge } from './types.js';
|
|
2
|
+
import { Session } from './session.js';
|
|
3
|
+
export interface Walk {
|
|
4
|
+
readonly nodes: readonly NodeId[];
|
|
5
|
+
readonly marked: readonly MarkedNode[];
|
|
6
|
+
readonly edges: readonly TraversedEdge[];
|
|
7
|
+
readonly gaps: readonly Gap[];
|
|
8
|
+
}
|
|
9
|
+
/** BFS discovers the shallowest arrival; a repeated diamond node is not a cycle. */
|
|
10
|
+
export declare function walk(session: Session, seeds: readonly NodeId[], direction: 'parents' | 'children'): Promise<Walk>;
|
|
11
|
+
export declare function frontier(result: Walk): readonly MarkedNode[];
|
|
12
|
+
export declare function pathTo(edges: readonly TraversedEdge[], start: NodeId, target: NodeId): readonly NodeId[];
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { adjacency, attention, gap, gaps, uniqueGaps } from './validation.js';
|
|
2
|
+
/** BFS discovers the shallowest arrival; a repeated diamond node is not a cycle. */
|
|
3
|
+
export async function walk(session, seeds, direction) {
|
|
4
|
+
const queue = [], seen = new Set(), marks = [], edges = [], missing = [];
|
|
5
|
+
const enqueue = (id, depth) => {
|
|
6
|
+
if (seen.has(id))
|
|
7
|
+
return;
|
|
8
|
+
const refusal = session.admit(id);
|
|
9
|
+
if (refusal) {
|
|
10
|
+
missing.push(refusal);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
seen.add(id);
|
|
14
|
+
queue.push([id, depth]);
|
|
15
|
+
};
|
|
16
|
+
for (const id of seeds)
|
|
17
|
+
enqueue(id, 0);
|
|
18
|
+
for (let index = 0; index < queue.length; index++) {
|
|
19
|
+
const [id, depth] = queue[index], stopped = session.stopGap(id);
|
|
20
|
+
if (stopped) {
|
|
21
|
+
missing.push(stopped);
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
const marked = await session.read('attention', id, attention);
|
|
25
|
+
if (marked.gap)
|
|
26
|
+
missing.push(marked.gap);
|
|
27
|
+
if (marked.value?.length) {
|
|
28
|
+
marks.push(Object.freeze({ nodeId: id, attention: marked.value }));
|
|
29
|
+
if (marked.value.some((item) => item.kind === 'not-collected'))
|
|
30
|
+
missing.push(gap('not-collected', id));
|
|
31
|
+
}
|
|
32
|
+
const coverage = await session.read('gaps', id, gaps);
|
|
33
|
+
if (coverage.gap)
|
|
34
|
+
missing.push(coverage.gap);
|
|
35
|
+
if (coverage.value)
|
|
36
|
+
missing.push(...coverage.value);
|
|
37
|
+
const page = await session.read(direction, id, (raw) => adjacency(raw, session.limits.maxEdges));
|
|
38
|
+
if (page.gap) {
|
|
39
|
+
missing.push(page.gap);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (!page.value)
|
|
43
|
+
continue;
|
|
44
|
+
if (!page.value.complete)
|
|
45
|
+
missing.push(gap('not-collected', id));
|
|
46
|
+
if (depth >= session.limits.maxDepth) {
|
|
47
|
+
if (page.value.edges.length)
|
|
48
|
+
missing.push(gap('depth-limit', id));
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
for (const edge of page.value.edges) {
|
|
52
|
+
if (session.edges >= session.limits.maxEdges) {
|
|
53
|
+
missing.push(gap('edge-limit', id));
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
session.edges++;
|
|
57
|
+
const refusal = session.admit(edge.nodeId);
|
|
58
|
+
if (refusal) {
|
|
59
|
+
missing.push(refusal);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
edges.push(Object.freeze({
|
|
63
|
+
from: id,
|
|
64
|
+
to: edge.nodeId,
|
|
65
|
+
kind: edge.kind,
|
|
66
|
+
attribution: edge.attribution,
|
|
67
|
+
evidence: edge.evidence,
|
|
68
|
+
}));
|
|
69
|
+
if (edge.attribution === 'stage')
|
|
70
|
+
missing.push(gap('conservative', id));
|
|
71
|
+
enqueue(edge.nodeId, depth + 1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (hasCycle(edges))
|
|
75
|
+
missing.push(gap('cycle'));
|
|
76
|
+
return Object.freeze({
|
|
77
|
+
nodes: Object.freeze(queue.map((row) => row[0])),
|
|
78
|
+
marked: Object.freeze(marks),
|
|
79
|
+
edges: Object.freeze(edges),
|
|
80
|
+
gaps: uniqueGaps(missing),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/** Iterative three-colour walk; shared nodes remain distinguishable from back edges. */
|
|
84
|
+
function hasCycle(edges) {
|
|
85
|
+
const next = new Map();
|
|
86
|
+
for (const edge of edges) {
|
|
87
|
+
if (!next.has(edge.from))
|
|
88
|
+
next.set(edge.from, []);
|
|
89
|
+
next.get(edge.from).push(edge.to);
|
|
90
|
+
}
|
|
91
|
+
const state = new Map();
|
|
92
|
+
for (const seed of next.keys()) {
|
|
93
|
+
if (state.has(seed))
|
|
94
|
+
continue;
|
|
95
|
+
const stack = [[seed, 0]];
|
|
96
|
+
state.set(seed, 1);
|
|
97
|
+
while (stack.length) {
|
|
98
|
+
const frame = stack[stack.length - 1], children = next.get(frame[0]) ?? [];
|
|
99
|
+
if (frame[1] >= children.length) {
|
|
100
|
+
state.set(frame[0], 2);
|
|
101
|
+
stack.pop();
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const child = children[frame[1]++];
|
|
105
|
+
if (state.get(child) === 1)
|
|
106
|
+
return true;
|
|
107
|
+
if (!state.has(child)) {
|
|
108
|
+
state.set(child, 1);
|
|
109
|
+
stack.push([child, 0]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
export function frontier(result) {
|
|
116
|
+
const marked = new Set(result.marked
|
|
117
|
+
.filter((item) => item.attention.some((mark) => mark.kind !== 'not-collected'))
|
|
118
|
+
.map((item) => item.nodeId));
|
|
119
|
+
const pending = new Map(result.nodes.map((id) => [id, 0]));
|
|
120
|
+
const dependents = new Map();
|
|
121
|
+
for (const edge of result.edges) {
|
|
122
|
+
pending.set(edge.from, (pending.get(edge.from) ?? 0) + 1);
|
|
123
|
+
if (!dependents.has(edge.to))
|
|
124
|
+
dependents.set(edge.to, []);
|
|
125
|
+
dependents.get(edge.to).push(edge.from);
|
|
126
|
+
}
|
|
127
|
+
// Process causal ancestors first, propagating one boolean through the DAG.
|
|
128
|
+
// This is O(nodes + edges), rather than a fresh ancestry scan per marked node.
|
|
129
|
+
const queue = [...pending].filter(([, count]) => count === 0).map(([id]) => id);
|
|
130
|
+
const earlierMark = new Set(), eligible = new Set();
|
|
131
|
+
for (let i = 0; i < queue.length; i++) {
|
|
132
|
+
const id = queue[i];
|
|
133
|
+
if (marked.has(id) && !earlierMark.has(id))
|
|
134
|
+
eligible.add(id);
|
|
135
|
+
for (const dependent of dependents.get(id) ?? []) {
|
|
136
|
+
if (marked.has(id) || earlierMark.has(id))
|
|
137
|
+
earlierMark.add(dependent);
|
|
138
|
+
const count = pending.get(dependent) - 1;
|
|
139
|
+
pending.set(dependent, count);
|
|
140
|
+
if (count === 0)
|
|
141
|
+
queue.push(dependent);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Nodes left in a cycle cannot supply a validated earliest ancestor.
|
|
145
|
+
return result.marked.filter((item) => eligible.has(item.nodeId));
|
|
146
|
+
}
|
|
147
|
+
export function pathTo(edges, start, target) {
|
|
148
|
+
const queue = [start], previous = new Map([[start, undefined]]);
|
|
149
|
+
const next = new Map();
|
|
150
|
+
for (const edge of edges) {
|
|
151
|
+
if (!next.has(edge.from))
|
|
152
|
+
next.set(edge.from, []);
|
|
153
|
+
next.get(edge.from).push(edge.to);
|
|
154
|
+
}
|
|
155
|
+
for (let i = 0; i < queue.length; i++) {
|
|
156
|
+
const id = queue[i];
|
|
157
|
+
if (id === target)
|
|
158
|
+
break;
|
|
159
|
+
for (const child of next.get(id) ?? [])
|
|
160
|
+
if (!previous.has(child)) {
|
|
161
|
+
previous.set(child, id);
|
|
162
|
+
queue.push(child);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (!previous.has(target))
|
|
166
|
+
return Object.freeze([]);
|
|
167
|
+
const path = [];
|
|
168
|
+
let id = target;
|
|
169
|
+
while (id !== undefined) {
|
|
170
|
+
path.push(id);
|
|
171
|
+
id = previous.get(id);
|
|
172
|
+
}
|
|
173
|
+
return Object.freeze(path.reverse());
|
|
174
|
+
}
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* types.ts — the record's types: a bundle, its rows, its patches, its encoding, and the
|
|
3
3
|
* execution tree as the record's readers read it.
|
|
4
4
|
*
|
|
5
|
-
* Imports
|
|
5
|
+
* Imports only the record's leaf attention type. The record names nothing outside itself: the engine's frame
|
|
6
6
|
* types — the stage snapshot, flow messages and retention dials — remain in FootPrint's
|
|
7
7
|
* `frameTypes.ts`. The closed record graph is enforced by scripts/check-layering.mjs.
|
|
8
8
|
*/
|
|
9
|
+
import type { Attention } from '../cause/types.js';
|
|
9
10
|
/** A flat key-value bag representing a state patch (overwrite or merge). */
|
|
10
11
|
export interface MemoryPatch {
|
|
11
12
|
[key: string]: any;
|
|
@@ -194,6 +195,8 @@ export type CommitValuesMode = 'full' | 'delta';
|
|
|
194
195
|
* ```
|
|
195
196
|
*/
|
|
196
197
|
export interface ExecutionTree {
|
|
198
|
+
/** Optional recorded business attention; absence does not assert health. */
|
|
199
|
+
readonly attention?: readonly Attention[];
|
|
197
200
|
/** The stage's id (prefixed inside a subflow). `commitStops` reads the root's to place a subflow's seed. */
|
|
198
201
|
readonly id?: string;
|
|
199
202
|
/** `[subflowPath/]stageId#executionIndex` — what a reader keys on. A node without one is not a stage. */
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* types.ts — the record's types: a bundle, its rows, its patches, its encoding, and the
|
|
3
3
|
* execution tree as the record's readers read it.
|
|
4
4
|
*
|
|
5
|
-
* Imports
|
|
5
|
+
* Imports only the record's leaf attention type. The record names nothing outside itself: the engine's frame
|
|
6
6
|
* types — the stage snapshot, flow messages and retention dials — remain in FootPrint's
|
|
7
7
|
* `frameTypes.ts`. The closed record graph is enforced by scripts/check-layering.mjs.
|
|
8
8
|
*/
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.spendFields = exports.boundedList = exports.boundedName = exports.ownData = exports.EvidenceRefusal = void 0;
|
|
4
|
+
/** One refusal representation; subclasses preserve the graph-port and record-loader boundaries. */
|
|
5
|
+
class EvidenceRefusal extends Error {
|
|
6
|
+
code;
|
|
7
|
+
constructor(code, message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
exports.EvidenceRefusal = EvidenceRefusal;
|
|
13
|
+
/** Read data descriptors only; each boundary keeps its existing treatment of proxy traps. */
|
|
14
|
+
function ownData(value, key, refuse, policy) {
|
|
15
|
+
if (!value || typeof value !== 'object')
|
|
16
|
+
throw refuse();
|
|
17
|
+
let descriptor;
|
|
18
|
+
if (policy.translateTrapErrors) {
|
|
19
|
+
try {
|
|
20
|
+
descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
throw refuse();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else
|
|
27
|
+
descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
28
|
+
if (!descriptor)
|
|
29
|
+
return undefined;
|
|
30
|
+
if (policy.requireOwnValue ? !Object.hasOwn(descriptor, 'value') : !('value' in descriptor))
|
|
31
|
+
throw refuse();
|
|
32
|
+
return descriptor.value;
|
|
33
|
+
}
|
|
34
|
+
exports.ownData = ownData;
|
|
35
|
+
/** Names share one grammar, with explicit limits and the record path separator exception. */
|
|
36
|
+
function boundedName(value, maximum, refuse, path = false) {
|
|
37
|
+
if (typeof value !== 'string' ||
|
|
38
|
+
!value.length ||
|
|
39
|
+
value.length > maximum ||
|
|
40
|
+
(path ? /[\u0000-\u001e\u007f-\u009f]/ : /[\u0000-\u001f\u007f-\u009f]/).test(value))
|
|
41
|
+
throw refuse();
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
exports.boundedName = boundedName;
|
|
45
|
+
/** Bound iteration before reading entries, without invoking element accessors. */
|
|
46
|
+
function boundedList(value, maximum, read, options) {
|
|
47
|
+
const { refuse, overflow, freeze, descriptors } = options;
|
|
48
|
+
if (!Array.isArray(value))
|
|
49
|
+
throw refuse();
|
|
50
|
+
const length = ownData(value, 'length', refuse, descriptors);
|
|
51
|
+
if (typeof length !== 'number' || !Number.isSafeInteger(length) || length < 0)
|
|
52
|
+
throw refuse();
|
|
53
|
+
if (length > maximum)
|
|
54
|
+
throw refuse(overflow);
|
|
55
|
+
const result = [];
|
|
56
|
+
for (let index = 0; index < length; index++)
|
|
57
|
+
result.push(read(ownData(value, String(index), refuse, descriptors)));
|
|
58
|
+
return freeze ? Object.freeze(result) : result;
|
|
59
|
+
}
|
|
60
|
+
exports.boundedList = boundedList;
|
|
61
|
+
/** Charge retained fields against the same mutable allowance across admission helpers. */
|
|
62
|
+
function spendFields(budget, count, refuse) {
|
|
63
|
+
budget.left -= count;
|
|
64
|
+
if (budget.left < 0)
|
|
65
|
+
throw refuse('node-limit');
|
|
66
|
+
}
|
|
67
|
+
exports.spendFields = spendFields;
|
|
@@ -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;
|