foottrace 1.1.0 → 1.3.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 +121 -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 +6 -0
- package/dist/esm/index.js +1 -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/queryMemo.d.ts +18 -0
- package/dist/esm/lib/cause/queryMemo.js +46 -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 +42 -0
- package/dist/esm/lib/cause/recordChildren.d.ts +4 -0
- package/dist/esm/lib/cause/recordChildren.js +78 -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/recordInspection.d.ts +4 -0
- package/dist/esm/lib/cause/recordInspection.js +86 -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/recordQuery.d.ts +9 -0
- package/dist/esm/lib/cause/recordQuery.js +22 -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 +164 -0
- package/dist/esm/lib/cause/storedRecordAdapter.d.ts +11 -0
- package/dist/esm/lib/cause/storedRecordAdapter.js +63 -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 +91 -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 +117 -0
- package/dist/esm/lib/cause/walk.d.ts +12 -0
- package/dist/esm/lib/cause/walk.js +174 -0
- package/dist/esm/lib/evidence/admission.d.ts +31 -0
- package/dist/esm/lib/evidence/admission.js +72 -0
- package/dist/esm/lib/evidence/codes.d.ts +33 -0
- package/dist/esm/lib/evidence/codes.js +28 -0
- package/dist/esm/lib/evidence/requirementTypes.d.ts +46 -0
- package/dist/esm/lib/evidence/requirementTypes.js +1 -0
- package/dist/esm/lib/evidence/requirements.d.ts +24 -0
- package/dist/esm/lib/evidence/requirements.js +185 -0
- package/dist/esm/lib/memory/types.d.ts +4 -1
- package/dist/esm/lib/memory/types.js +1 -1
- package/dist/index.js +5 -1
- package/dist/lib/cause/explain.js +119 -0
- package/dist/lib/cause/queryMemo.js +52 -0
- package/dist/lib/cause/ranking.js +14 -0
- package/dist/lib/cause/recordAdapter.js +48 -0
- package/dist/lib/cause/recordChildren.js +82 -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/recordInspection.js +90 -0
- package/dist/lib/cause/recordLoading.js +121 -0
- package/dist/lib/cause/recordQuery.js +27 -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 +168 -0
- package/dist/lib/cause/storedRecordAdapter.js +67 -0
- package/dist/lib/cause/strategies.js +26 -0
- package/dist/lib/cause/types.js +2 -0
- package/dist/lib/cause/validation.js +133 -0
- package/dist/lib/cause/walk.js +180 -0
- package/dist/lib/evidence/admission.js +80 -0
- package/dist/lib/evidence/codes.js +33 -0
- package/dist/lib/evidence/requirementTypes.js +2 -0
- package/dist/lib/evidence/requirements.js +190 -0
- package/dist/lib/memory/types.js +1 -1
- package/dist/types/cause.d.ts +8 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/lib/cause/explain.d.ts +5 -0
- package/dist/types/lib/cause/queryMemo.d.ts +18 -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/recordInspection.d.ts +4 -0
- package/dist/types/lib/cause/recordLoading.d.ts +7 -0
- package/dist/types/lib/cause/recordQuery.d.ts +9 -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 +91 -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/evidence/admission.d.ts +31 -0
- package/dist/types/lib/evidence/codes.d.ts +33 -0
- package/dist/types/lib/evidence/requirementTypes.d.ts +46 -0
- package/dist/types/lib/evidence/requirements.d.ts +24 -0
- package/dist/types/lib/memory/types.d.ts +4 -1
- package/package.json +67 -13
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { attention as marksOf, AdmissionError, observePromise } from './validation.js';
|
|
2
|
+
import { boundedList, boundedName, EvidenceRefusal, ownData, spendFields } from '../evidence/admission.js';
|
|
3
|
+
export { marksOf };
|
|
4
|
+
/** Loader refusals stay distinct from graph-port refusals so a failed host load keeps its original classification. */
|
|
5
|
+
export class RecordRefusal extends EvidenceRefusal {
|
|
6
|
+
code;
|
|
7
|
+
constructor(code = 'invalid-data') {
|
|
8
|
+
super(code, 'cause: record unavailable');
|
|
9
|
+
this.code = code;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const refuseRecord = (code) => new RecordRefusal(code);
|
|
13
|
+
const recordDescriptors = { translateTrapErrors: true, requireOwnValue: true };
|
|
14
|
+
export function recordOwn(value, key) {
|
|
15
|
+
return ownData(value, key, refuseRecord, recordDescriptors);
|
|
16
|
+
}
|
|
17
|
+
export function names(value, maximum = 128, path = false) {
|
|
18
|
+
return recordList(value, maximum).map((item) => recordName(item, path));
|
|
19
|
+
}
|
|
20
|
+
export function recordName(value, path = false) {
|
|
21
|
+
return boundedName(value, 1024, refuseRecord, path);
|
|
22
|
+
}
|
|
23
|
+
export function recordList(value, maximum) {
|
|
24
|
+
return boundedList(value, maximum, (item) => item, {
|
|
25
|
+
refuse: refuseRecord,
|
|
26
|
+
overflow: 'node-limit',
|
|
27
|
+
freeze: false,
|
|
28
|
+
descriptors: recordDescriptors,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export function addressOf(value) {
|
|
32
|
+
const logRunId = recordName(recordOwn(value, 'logRunId')), drillPath = names(recordOwn(value, 'drillPath'), 16);
|
|
33
|
+
if (logRunId.length + drillPath.reduce((n, s) => n + s.length, 0) > 1536)
|
|
34
|
+
throw new RecordRefusal();
|
|
35
|
+
return Object.freeze({ logRunId, drillPath: Object.freeze(drillPath) });
|
|
36
|
+
}
|
|
37
|
+
export function spend(budget, count) {
|
|
38
|
+
spendFields(budget, count, refuseRecord);
|
|
39
|
+
}
|
|
40
|
+
export const addressKey = (a) => JSON.stringify([a.logRunId, ...a.drillPath]);
|
|
41
|
+
/** Iterative bounded copy: data descriptors only, no getters/toJSON/prototypes or opaque class instances. */
|
|
42
|
+
export function dataCopy(value, budget) {
|
|
43
|
+
const leaf = (v) => v === null || typeof v !== 'object';
|
|
44
|
+
const check = (v) => {
|
|
45
|
+
spend(budget, 1);
|
|
46
|
+
if (typeof v === 'function' || typeof v === 'symbol' || (typeof v === 'string' && v.length > 65536))
|
|
47
|
+
throw new RecordRefusal();
|
|
48
|
+
};
|
|
49
|
+
check(value);
|
|
50
|
+
if (leaf(value))
|
|
51
|
+
return value;
|
|
52
|
+
const make = (v) => {
|
|
53
|
+
let p;
|
|
54
|
+
try {
|
|
55
|
+
p = Object.getPrototypeOf(v);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
throw new RecordRefusal();
|
|
59
|
+
}
|
|
60
|
+
if (!Array.isArray(v) && p !== Object.prototype && p !== null)
|
|
61
|
+
throw new RecordRefusal();
|
|
62
|
+
if (Array.isArray(v)) {
|
|
63
|
+
const length = recordOwn(v, 'length');
|
|
64
|
+
if (typeof length !== 'number' || !Number.isSafeInteger(length) || length < 0)
|
|
65
|
+
throw new RecordRefusal();
|
|
66
|
+
spend(budget, length);
|
|
67
|
+
const array = [];
|
|
68
|
+
array.length = length;
|
|
69
|
+
return array;
|
|
70
|
+
}
|
|
71
|
+
return Object.create(null);
|
|
72
|
+
};
|
|
73
|
+
const root = make(value), seen = new Set([value]), work = [{ from: value, to: root }];
|
|
74
|
+
while (work.length) {
|
|
75
|
+
const { from, to } = work.pop();
|
|
76
|
+
let keys;
|
|
77
|
+
try {
|
|
78
|
+
keys = Object.keys(from);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
throw new RecordRefusal();
|
|
82
|
+
}
|
|
83
|
+
if (keys.length > budget.left)
|
|
84
|
+
throw new RecordRefusal('node-limit');
|
|
85
|
+
for (const key of keys) {
|
|
86
|
+
if (key.length > 1024)
|
|
87
|
+
throw new RecordRefusal('node-limit');
|
|
88
|
+
const v = recordOwn(from, key);
|
|
89
|
+
check(v);
|
|
90
|
+
let copy = v;
|
|
91
|
+
if (!leaf(v)) {
|
|
92
|
+
if (seen.has(v))
|
|
93
|
+
throw new RecordRefusal();
|
|
94
|
+
seen.add(v);
|
|
95
|
+
copy = make(v);
|
|
96
|
+
work.push({ from: v, to: copy });
|
|
97
|
+
}
|
|
98
|
+
Object.defineProperty(to, key, { value: copy, enumerable: true, writable: true, configurable: true });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return root;
|
|
102
|
+
}
|
|
103
|
+
export function bundleOf(value, idx, budget) {
|
|
104
|
+
const runtimeStageId = recordName(recordOwn(value, 'runtimeStageId')), stageId = recordName(recordOwn(value, 'stageId'));
|
|
105
|
+
const trace = recordList(recordOwn(value, 'trace'), 128).map((_, i) => {
|
|
106
|
+
const t = recordOwn(recordOwn(value, 'trace'), String(i)), path = recordName(recordOwn(t, 'path'), true), verb = recordOwn(t, 'verb');
|
|
107
|
+
if (!['set', 'merge', 'append', 'delete'].includes(verb))
|
|
108
|
+
throw new RecordRefusal();
|
|
109
|
+
const readKeys = recordOwn(t, 'readKeys');
|
|
110
|
+
return { path, verb: verb, ...(readKeys === undefined ? {} : { readKeys: names(readKeys, 128, true) }) };
|
|
111
|
+
});
|
|
112
|
+
spend(budget, trace.length + trace.reduce((n, t) => n + (t.readKeys?.length ?? 0), 0) + 1);
|
|
113
|
+
const overwrite = dataCopy(recordOwn(value, 'overwrite'), budget), updates = dataCopy(recordOwn(value, 'updates'), budget);
|
|
114
|
+
if (!overwrite ||
|
|
115
|
+
typeof overwrite !== 'object' ||
|
|
116
|
+
Array.isArray(overwrite) ||
|
|
117
|
+
!updates ||
|
|
118
|
+
typeof updates !== 'object' ||
|
|
119
|
+
Array.isArray(updates))
|
|
120
|
+
throw new RecordRefusal();
|
|
121
|
+
const redactedPaths = names(recordOwn(value, 'redactedPaths') ?? [], 128, true);
|
|
122
|
+
const untracked = recordOwn(value, 'untrackedSources'), phase = recordOwn(value, 'phase');
|
|
123
|
+
spend(budget, redactedPaths.length + (untracked === undefined ? 0 : recordList(untracked, 128).length));
|
|
124
|
+
return {
|
|
125
|
+
idx,
|
|
126
|
+
stage: stageId,
|
|
127
|
+
stageId,
|
|
128
|
+
runtimeStageId,
|
|
129
|
+
trace,
|
|
130
|
+
overwrite,
|
|
131
|
+
updates,
|
|
132
|
+
redactedPaths,
|
|
133
|
+
...(untracked === undefined
|
|
134
|
+
? {}
|
|
135
|
+
: {
|
|
136
|
+
untrackedSources: names(untracked).map((s) => {
|
|
137
|
+
if (!['args', 'env', 'silent'].includes(s))
|
|
138
|
+
throw new RecordRefusal();
|
|
139
|
+
return s;
|
|
140
|
+
}),
|
|
141
|
+
}),
|
|
142
|
+
...(phase === undefined
|
|
143
|
+
? {}
|
|
144
|
+
: {
|
|
145
|
+
phase: (() => {
|
|
146
|
+
if (phase !== 'exit' && phase !== 'repeat')
|
|
147
|
+
throw new RecordRefusal();
|
|
148
|
+
return phase;
|
|
149
|
+
})(),
|
|
150
|
+
}),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
export function mapOf(value, limit, read, budget) {
|
|
154
|
+
if (value === undefined)
|
|
155
|
+
return undefined;
|
|
156
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
157
|
+
throw new RecordRefusal();
|
|
158
|
+
let keys;
|
|
159
|
+
try {
|
|
160
|
+
keys = Object.keys(value);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
throw new RecordRefusal();
|
|
164
|
+
}
|
|
165
|
+
if (keys.length > limit)
|
|
166
|
+
throw new RecordRefusal('node-limit');
|
|
167
|
+
if (budget)
|
|
168
|
+
spend(budget, keys.length);
|
|
169
|
+
return new Map(keys.map((k) => [recordName(k), read(recordOwn(value, k))]));
|
|
170
|
+
}
|
|
171
|
+
/** Observe native promises by intrinsic without assimilating a data object's then accessor. */
|
|
172
|
+
export function projectLoaded(work, project) {
|
|
173
|
+
return new Promise((resolve, reject) => {
|
|
174
|
+
const accept = (value) => {
|
|
175
|
+
try {
|
|
176
|
+
resolve(project(value));
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
reject(error);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
let returned;
|
|
183
|
+
try {
|
|
184
|
+
returned = work();
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
reject(error);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
if (!observePromise(returned, (value) => accept(value), reject))
|
|
192
|
+
accept(returned);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
reject(error instanceof AdmissionError ? new RecordRefusal(error.code) : error);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CauseGraph, ExplainOptions, Gap, GraphContext, NodeId, WalkLimits } from './types.js';
|
|
2
|
+
export interface Read<T> {
|
|
3
|
+
readonly value?: T;
|
|
4
|
+
readonly gap?: Gap;
|
|
5
|
+
}
|
|
6
|
+
type Method = 'parents' | 'children' | 'attention' | 'gaps' | 'roots';
|
|
7
|
+
/** One deadline, memo table and work budget per public query, never per graph or store. */
|
|
8
|
+
export declare class Session {
|
|
9
|
+
readonly limits: Required<WalkLimits>;
|
|
10
|
+
readonly context: GraphContext;
|
|
11
|
+
readonly nodes: Set<string>;
|
|
12
|
+
readonly controller: AbortController;
|
|
13
|
+
private readonly cache;
|
|
14
|
+
private calls;
|
|
15
|
+
edges: number;
|
|
16
|
+
private timedOut;
|
|
17
|
+
private readonly deadline;
|
|
18
|
+
private readonly timer;
|
|
19
|
+
private readonly external?;
|
|
20
|
+
private readonly onAbort;
|
|
21
|
+
private readonly methods;
|
|
22
|
+
constructor(graph: CauseGraph, options?: ExplainOptions);
|
|
23
|
+
dispose(): void;
|
|
24
|
+
stopGap(id?: NodeId): Gap | undefined;
|
|
25
|
+
admit(id: NodeId): Gap | undefined;
|
|
26
|
+
hasRoots(): boolean;
|
|
27
|
+
read<T>(method: Method, id: NodeId | undefined, project: (raw: unknown) => T): Promise<Read<T>>;
|
|
28
|
+
invoke<T>(id: NodeId | undefined, work: () => unknown, project: (raw: unknown) => T): Promise<Read<T>>;
|
|
29
|
+
}
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { AdmissionError, gap, graphName, observePromise, graphOwn, plain } from './validation.js';
|
|
2
|
+
import { beginQueryMemo, endQueryMemo } from './queryMemo.js';
|
|
3
|
+
const defaults = { maxNodes: 100, maxDepth: 20, maxEdges: 1000, maxCalls: 1000, timeoutMs: 1000 };
|
|
4
|
+
const ceilings = { maxNodes: 10000, maxDepth: 1000, maxEdges: 100000, maxCalls: 100000, timeoutMs: 30000 };
|
|
5
|
+
/** One deadline, memo table and work budget per public query, never per graph or store. */
|
|
6
|
+
export class Session {
|
|
7
|
+
limits;
|
|
8
|
+
context;
|
|
9
|
+
nodes = new Set();
|
|
10
|
+
controller = new AbortController();
|
|
11
|
+
cache = new Map();
|
|
12
|
+
calls = 0;
|
|
13
|
+
edges = 0;
|
|
14
|
+
timedOut = false;
|
|
15
|
+
deadline;
|
|
16
|
+
timer;
|
|
17
|
+
external;
|
|
18
|
+
onAbort = () => this.controller.abort();
|
|
19
|
+
methods = {};
|
|
20
|
+
constructor(graph, options = {}) {
|
|
21
|
+
try {
|
|
22
|
+
plain(options);
|
|
23
|
+
const limits = { ...defaults };
|
|
24
|
+
for (const key of Object.keys(defaults)) {
|
|
25
|
+
const value = graphOwn(options, key);
|
|
26
|
+
if (value !== undefined) {
|
|
27
|
+
if (typeof value !== 'number' ||
|
|
28
|
+
!Number.isSafeInteger(value) ||
|
|
29
|
+
value < (key === 'maxDepth' ? 0 : 1) ||
|
|
30
|
+
value > ceilings[key])
|
|
31
|
+
throw new TypeError();
|
|
32
|
+
limits[key] = value;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
this.limits = Object.freeze(limits);
|
|
36
|
+
const rank = graphOwn(options, 'rankRoots');
|
|
37
|
+
if (rank !== undefined && typeof rank !== 'function')
|
|
38
|
+
throw new TypeError();
|
|
39
|
+
const impactRank = graphOwn(options, 'rankImpact');
|
|
40
|
+
if (impactRank !== undefined && typeof impactRank !== 'function')
|
|
41
|
+
throw new TypeError();
|
|
42
|
+
const signal = graphOwn(options, 'signal');
|
|
43
|
+
if (signal !== undefined && !(signal instanceof AbortSignal))
|
|
44
|
+
throw new TypeError();
|
|
45
|
+
this.external = signal;
|
|
46
|
+
for (const key of ['parents', 'children', 'attention', 'gaps', 'roots']) {
|
|
47
|
+
let cursor = graph, method;
|
|
48
|
+
for (let depth = 0; cursor && cursor !== Object.prototype && depth < 8; depth++, cursor = Object.getPrototypeOf(cursor)) {
|
|
49
|
+
const field = Object.getOwnPropertyDescriptor(cursor, key);
|
|
50
|
+
if (field) {
|
|
51
|
+
if (!('value' in field))
|
|
52
|
+
throw new TypeError();
|
|
53
|
+
method = field.value;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (typeof method !== 'function' && key !== 'roots')
|
|
58
|
+
throw new TypeError();
|
|
59
|
+
if (typeof method === 'function')
|
|
60
|
+
this.methods[key] = method.bind(graph);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw new TypeError('CAUSE_CONFIGURATION_INVALID');
|
|
65
|
+
}
|
|
66
|
+
this.context = Object.freeze({ signal: this.controller.signal });
|
|
67
|
+
this.deadline = performance.now() + this.limits.timeoutMs;
|
|
68
|
+
this.external?.addEventListener('abort', this.onAbort, { once: true });
|
|
69
|
+
if (this.external?.aborted)
|
|
70
|
+
this.onAbort();
|
|
71
|
+
this.timer = setTimeout(() => {
|
|
72
|
+
this.timedOut = true;
|
|
73
|
+
this.controller.abort();
|
|
74
|
+
}, this.limits.timeoutMs);
|
|
75
|
+
beginQueryMemo(this.context, this.limits.maxNodes);
|
|
76
|
+
}
|
|
77
|
+
dispose() {
|
|
78
|
+
endQueryMemo(this.context);
|
|
79
|
+
clearTimeout(this.timer);
|
|
80
|
+
this.external?.removeEventListener('abort', this.onAbort);
|
|
81
|
+
}
|
|
82
|
+
stopGap(id) {
|
|
83
|
+
// Resolved Promise chains can starve timers. Check the monotonic deadline too.
|
|
84
|
+
if (!this.controller.signal.aborted && performance.now() >= this.deadline) {
|
|
85
|
+
this.timedOut = true;
|
|
86
|
+
this.controller.abort();
|
|
87
|
+
}
|
|
88
|
+
return this.controller.signal.aborted ? gap(this.timedOut ? 'timeout' : 'cancelled', id) : undefined;
|
|
89
|
+
}
|
|
90
|
+
admit(id) {
|
|
91
|
+
graphName(id);
|
|
92
|
+
if (this.nodes.has(id))
|
|
93
|
+
return undefined;
|
|
94
|
+
if (this.nodes.size >= this.limits.maxNodes)
|
|
95
|
+
return gap('node-limit', id);
|
|
96
|
+
this.nodes.add(id);
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
hasRoots() {
|
|
100
|
+
return !!this.methods.roots;
|
|
101
|
+
}
|
|
102
|
+
read(method, id, project) {
|
|
103
|
+
const key = JSON.stringify([method, id]);
|
|
104
|
+
const cached = this.cache.get(key);
|
|
105
|
+
if (cached)
|
|
106
|
+
return cached;
|
|
107
|
+
const pending = this.invoke(id, () => {
|
|
108
|
+
const fn = this.methods[method];
|
|
109
|
+
if (!fn)
|
|
110
|
+
throw new AdmissionError('not-collected');
|
|
111
|
+
return id === undefined ? fn(this.context) : fn(id, this.context);
|
|
112
|
+
}, project);
|
|
113
|
+
this.cache.set(key, pending);
|
|
114
|
+
return pending;
|
|
115
|
+
}
|
|
116
|
+
invoke(id, work, project) {
|
|
117
|
+
const stopped = this.stopGap(id);
|
|
118
|
+
if (stopped)
|
|
119
|
+
return Promise.resolve({ gap: stopped });
|
|
120
|
+
if (++this.calls > this.limits.maxCalls)
|
|
121
|
+
return Promise.resolve({ gap: gap('call-limit', id) });
|
|
122
|
+
return new Promise((resolve) => {
|
|
123
|
+
let finished = false;
|
|
124
|
+
const finish = (result) => {
|
|
125
|
+
if (finished)
|
|
126
|
+
return;
|
|
127
|
+
finished = true;
|
|
128
|
+
this.controller.signal.removeEventListener('abort', abort);
|
|
129
|
+
resolve(result);
|
|
130
|
+
};
|
|
131
|
+
const abort = () => finish({ gap: this.stopGap(id) ?? gap('cancelled', id) });
|
|
132
|
+
const accept = (raw) => {
|
|
133
|
+
if (finished)
|
|
134
|
+
return;
|
|
135
|
+
const stopped = this.stopGap(id);
|
|
136
|
+
if (stopped) {
|
|
137
|
+
finish({ gap: stopped });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const value = project(raw), expired = this.stopGap(id);
|
|
142
|
+
finish(expired ? { gap: expired } : { value });
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
finish({ gap: gap(error instanceof AdmissionError ? error.code : 'invalid-data', id) });
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
this.controller.signal.addEventListener('abort', abort, { once: true });
|
|
149
|
+
if (this.controller.signal.aborted) {
|
|
150
|
+
abort();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const returned = work();
|
|
155
|
+
// Observe native Promises by intrinsic, never assimilate a payload's `then` getter.
|
|
156
|
+
if (!observePromise(returned, accept, () => finish({ gap: gap('unavailable', id) })))
|
|
157
|
+
accept(returned);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
finish({ gap: gap(error instanceof AdmissionError ? error.code : 'unavailable', id) });
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LogAddress } from '../memory/eventPosition.js';
|
|
2
|
+
import { type RecordCauseGraph, type RecordGraphOptions, type RecordScope } from './recordAdapter.js';
|
|
3
|
+
import type { GraphContext, MaybePromise } from './types.js';
|
|
4
|
+
export interface StoredRecordScope extends Omit<RecordScope, 'address' | 'commitLog' | 'complete'> {
|
|
5
|
+
readonly entries: readonly unknown[];
|
|
6
|
+
}
|
|
7
|
+
export interface StoredRecordHost {
|
|
8
|
+
loadLog(address: LogAddress, context: GraphContext): MaybePromise<StoredRecordScope>;
|
|
9
|
+
}
|
|
10
|
+
/** Load one address only. The host owns auth/storage; cancellation is forwarded and waiting is independently bounded. */
|
|
11
|
+
export declare function createStoredRecordGraph(host: StoredRecordHost, options?: RecordGraphOptions): RecordCauseGraph;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { readStoredRecord } from '../time-travel/storedRecord.js';
|
|
2
|
+
import { createLazyRecordGraph, recordOptions, } from './recordAdapter.js';
|
|
3
|
+
import { addressKey, addressOf, bundleOf, dataCopy, recordList, recordOwn, projectLoaded, RecordRefusal, } from './recordValidation.js';
|
|
4
|
+
/** Load one address only. The host owns auth/storage; cancellation is forwarded and waiting is independently bounded. */
|
|
5
|
+
export function createStoredRecordGraph(host, options = {}) {
|
|
6
|
+
const limits = recordOptions(options);
|
|
7
|
+
return createLazyRecordGraph((address, context) => projectLoaded(() => host.loadLog(address, context), (loaded) => {
|
|
8
|
+
const max = limits.maxCommits, budget = { left: limits.maxFields };
|
|
9
|
+
const raw = recordList(recordOwn(loaded, 'entries'), max * 4 + 16), entries = [];
|
|
10
|
+
for (let i = 0; i < raw.length; i++) {
|
|
11
|
+
if (context.signal.aborted)
|
|
12
|
+
throw new RecordRefusal('cancelled');
|
|
13
|
+
const entry = recordOwn(raw, String(i)), a = addressOf(recordOwn(entry, 'address'));
|
|
14
|
+
if (addressKey(a) !== addressKey(address))
|
|
15
|
+
throw new RecordRefusal();
|
|
16
|
+
const kind = recordOwn(entry, 'kind'), at = recordOwn(entry, 'at');
|
|
17
|
+
if (typeof at !== 'number' || !Number.isFinite(at))
|
|
18
|
+
throw new RecordRefusal();
|
|
19
|
+
if (kind === 'commit') {
|
|
20
|
+
const b = recordOwn(entry, 'bundle'), idx = recordOwn(b, 'idx');
|
|
21
|
+
if (!Number.isSafeInteger(idx) || idx < 0 || idx >= max)
|
|
22
|
+
throw new RecordRefusal('node-limit');
|
|
23
|
+
entries.push({ kind, address: a, at, bundle: bundleOf(b, idx, budget) });
|
|
24
|
+
}
|
|
25
|
+
else if (kind === 'opened')
|
|
26
|
+
entries.push({ kind, address: a, at, initialState: dataCopy(recordOwn(entry, 'initialState'), budget) });
|
|
27
|
+
else if (kind === 'closed')
|
|
28
|
+
entries.push({
|
|
29
|
+
kind,
|
|
30
|
+
address: a,
|
|
31
|
+
at,
|
|
32
|
+
outcome: recordOwn(entry, 'outcome'),
|
|
33
|
+
commits: recordOwn(entry, 'commits'),
|
|
34
|
+
});
|
|
35
|
+
else if (kind === 'note') {
|
|
36
|
+
/* opaque note bodies never enter dependency projection */
|
|
37
|
+
}
|
|
38
|
+
else
|
|
39
|
+
throw new RecordRefusal();
|
|
40
|
+
}
|
|
41
|
+
const record = readStoredRecord(entries, {
|
|
42
|
+
entries: max * 4 + 16,
|
|
43
|
+
missingCommits: max,
|
|
44
|
+
drillDepth: 16,
|
|
45
|
+
addressLength: 1536,
|
|
46
|
+
}), log = record.log(address);
|
|
47
|
+
if (!log)
|
|
48
|
+
throw new RecordRefusal();
|
|
49
|
+
if (log.gaps.length)
|
|
50
|
+
throw new RecordRefusal('not-collected');
|
|
51
|
+
return {
|
|
52
|
+
scope: {
|
|
53
|
+
address,
|
|
54
|
+
commitLog: log.commitLog,
|
|
55
|
+
reads: recordOwn(loaded, 'reads'),
|
|
56
|
+
controlDeps: recordOwn(loaded, 'controlDeps'),
|
|
57
|
+
attention: recordOwn(loaded, 'attention'),
|
|
58
|
+
complete: log.complete,
|
|
59
|
+
},
|
|
60
|
+
gaps: record.unread.length || log.gaps.length ? ['not-collected'] : [],
|
|
61
|
+
};
|
|
62
|
+
}), limits);
|
|
63
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Host-computed quality drops or impact counts: scores rank recorded candidates, never create evidence. */
|
|
2
|
+
function maximum(scores) {
|
|
3
|
+
return (candidates) => {
|
|
4
|
+
let best = -Infinity;
|
|
5
|
+
const tied = [];
|
|
6
|
+
for (const candidate of candidates) {
|
|
7
|
+
const score = scores.get(candidate.nodeId);
|
|
8
|
+
if (score === undefined || !Number.isFinite(score))
|
|
9
|
+
throw new TypeError('CAUSE_STRATEGY_EVIDENCE_MISSING');
|
|
10
|
+
if (score > best) {
|
|
11
|
+
best = score;
|
|
12
|
+
tied.length = 0;
|
|
13
|
+
}
|
|
14
|
+
if (score === best)
|
|
15
|
+
tied.push(candidate.nodeId);
|
|
16
|
+
}
|
|
17
|
+
return Object.freeze(tied);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export const largestDrop = (scores) => maximum(scores);
|
|
21
|
+
export const mostAffected = (counts) => maximum(counts);
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { GapCode } from '../evidence/codes.js';
|
|
2
|
+
export type { GapCode } from '../evidence/codes.js';
|
|
3
|
+
/** Names and evidence references only. No state values, messages, credentials or business payloads. */
|
|
4
|
+
export type NodeId = string;
|
|
5
|
+
export type AttentionKind = 'error' | 'unknown-effect' | 'gave-up' | 'timeout' | 'retried' | 'quality-drop' | 'not-collected' | 'outlier' | (string & {});
|
|
6
|
+
export type AttentionReason = 'ERROR_RECORDED' | 'UNKNOWN_EFFECT_RECORDED' | 'RETRY_STOP_RECORDED' | 'TIMEOUT_RECORDED' | 'RETRY_RECORDED' | 'QUALITY_DROP_RECORDED' | 'EVIDENCE_NOT_COLLECTED' | 'OUTLIER_RECORDED' | 'BUSINESS_RULE_MATCHED';
|
|
7
|
+
export interface Attention {
|
|
8
|
+
readonly kind: AttentionKind;
|
|
9
|
+
readonly reason: AttentionReason;
|
|
10
|
+
readonly evidence: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
export type Confidence = 'complete' | 'incomplete';
|
|
13
|
+
export interface Gap {
|
|
14
|
+
readonly code: GapCode;
|
|
15
|
+
readonly nodeId?: NodeId;
|
|
16
|
+
readonly evidence: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
export interface GraphEdge {
|
|
19
|
+
readonly nodeId: NodeId;
|
|
20
|
+
readonly kind: 'data' | 'control';
|
|
21
|
+
readonly attribution: 'per-write' | 'stage' | 'recorded-control';
|
|
22
|
+
readonly evidence: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
export interface Adjacency {
|
|
25
|
+
readonly edges: readonly GraphEdge[];
|
|
26
|
+
readonly complete: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface GraphContext {
|
|
29
|
+
readonly signal: AbortSignal;
|
|
30
|
+
}
|
|
31
|
+
export type MaybePromise<T> = T | Promise<T>;
|
|
32
|
+
/** A host owns storage, authentication and service resolution. A node may represent a key-qualified write. */
|
|
33
|
+
export interface CauseGraph {
|
|
34
|
+
parents(id: NodeId, context: GraphContext): MaybePromise<Adjacency>;
|
|
35
|
+
children(id: NodeId, context: GraphContext): MaybePromise<Adjacency>;
|
|
36
|
+
attention(id: NodeId, context: GraphContext): MaybePromise<readonly Attention[]>;
|
|
37
|
+
gaps(id: NodeId, context: GraphContext): MaybePromise<readonly Gap[]>;
|
|
38
|
+
/** Optional, bounded discovery seeds; attentionList can instead receive explicit nodeIds. */
|
|
39
|
+
roots?(context: GraphContext): MaybePromise<readonly NodeId[]>;
|
|
40
|
+
}
|
|
41
|
+
export interface TraversedEdge extends Omit<GraphEdge, 'nodeId'> {
|
|
42
|
+
readonly from: NodeId;
|
|
43
|
+
readonly to: NodeId;
|
|
44
|
+
}
|
|
45
|
+
export interface MarkedNode {
|
|
46
|
+
readonly nodeId: NodeId;
|
|
47
|
+
readonly attention: readonly Attention[];
|
|
48
|
+
}
|
|
49
|
+
export interface ImpactResult {
|
|
50
|
+
readonly nodes: readonly NodeId[];
|
|
51
|
+
readonly edges: readonly TraversedEdge[];
|
|
52
|
+
readonly confidence: Confidence;
|
|
53
|
+
readonly gaps: readonly Gap[];
|
|
54
|
+
}
|
|
55
|
+
export interface CauseResult {
|
|
56
|
+
readonly nodeId: NodeId;
|
|
57
|
+
readonly attention: readonly Attention[];
|
|
58
|
+
/** Present only for one supported candidate and complete backward coverage. Not provider-internal proof. */
|
|
59
|
+
readonly rootCause?: NodeId;
|
|
60
|
+
/** Selected node → candidate, following recorded dependency edges. */
|
|
61
|
+
readonly path: readonly NodeId[];
|
|
62
|
+
readonly alternatives: readonly NodeId[];
|
|
63
|
+
readonly impact: ImpactResult;
|
|
64
|
+
readonly confidence: Confidence;
|
|
65
|
+
readonly gaps: readonly Gap[];
|
|
66
|
+
}
|
|
67
|
+
export interface AttentionListResult {
|
|
68
|
+
readonly nodes: readonly MarkedNode[];
|
|
69
|
+
readonly confidence: Confidence;
|
|
70
|
+
readonly gaps: readonly Gap[];
|
|
71
|
+
}
|
|
72
|
+
export interface WalkLimits {
|
|
73
|
+
readonly maxNodes?: number;
|
|
74
|
+
readonly maxDepth?: number;
|
|
75
|
+
readonly maxEdges?: number;
|
|
76
|
+
readonly maxCalls?: number;
|
|
77
|
+
readonly timeoutMs?: number;
|
|
78
|
+
}
|
|
79
|
+
/** A strategy selects a tied best tier of already recorded marked nodes; it cannot introduce identities. */
|
|
80
|
+
export type RootStrategy = (candidates: readonly MarkedNode[], context: GraphContext) => MaybePromise<readonly NodeId[]>;
|
|
81
|
+
/** Reorder all already discovered impact references; never inject or drop evidence. */
|
|
82
|
+
export type ImpactStrategy = (nodes: readonly NodeId[], context: GraphContext) => MaybePromise<readonly NodeId[]>;
|
|
83
|
+
export interface ExplainOptions extends WalkLimits {
|
|
84
|
+
readonly signal?: AbortSignal;
|
|
85
|
+
readonly rankRoots?: RootStrategy;
|
|
86
|
+
readonly rankImpact?: ImpactStrategy;
|
|
87
|
+
}
|
|
88
|
+
export interface AttentionListOptions extends WalkLimits {
|
|
89
|
+
readonly signal?: AbortSignal;
|
|
90
|
+
readonly nodeIds?: readonly NodeId[];
|
|
91
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Adjacency, Attention, Gap, GapCode, NodeId } from './types.js';
|
|
2
|
+
import { EvidenceRefusal } from '../evidence/admission.js';
|
|
3
|
+
/** Graph-port refusals carry their code through Session; loader refusals are handled at the record boundary. */
|
|
4
|
+
export declare class AdmissionError extends EvidenceRefusal {
|
|
5
|
+
readonly code: GapCode;
|
|
6
|
+
constructor(code?: GapCode);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Only ordinary native promise wrappers belong to the async host contract.
|
|
10
|
+
* An own constructor could run an accessor/species during intrinsic then().
|
|
11
|
+
* Exotic wrappers remain the trusted host's responsibility, including rejection handling.
|
|
12
|
+
*/
|
|
13
|
+
export declare function observePromise(raw: unknown, accept: (raw: unknown) => void, reject: (reason: unknown) => void): boolean;
|
|
14
|
+
export declare function graphOwn(value: unknown, key: string): unknown;
|
|
15
|
+
export declare function plain(value: unknown): void;
|
|
16
|
+
export declare function graphName(value: unknown, maximum?: number): string;
|
|
17
|
+
export declare function graphList<T>(value: unknown, maximum: number, read: (item: unknown) => T, overflow?: GapCode): readonly T[];
|
|
18
|
+
export declare const refs: (value: unknown) => readonly string[];
|
|
19
|
+
export declare function attention(value: unknown): readonly Attention[];
|
|
20
|
+
export declare function gaps(value: unknown): readonly Gap[];
|
|
21
|
+
export declare function adjacency(value: unknown, maximum: number): Adjacency;
|
|
22
|
+
export declare const nodeIds: (value: unknown, max: number) => readonly NodeId[];
|
|
23
|
+
export declare const gap: (code: GapCode, nodeId?: NodeId) => Gap;
|
|
24
|
+
export declare function uniqueGaps(items: readonly Gap[]): readonly Gap[];
|