foottrace 1.2.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 +27 -2
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/lib/cause/explain.js +7 -7
- package/dist/esm/lib/cause/queryMemo.d.ts +18 -0
- package/dist/esm/lib/cause/queryMemo.js +46 -0
- package/dist/esm/lib/cause/recordAdapter.js +12 -19
- package/dist/esm/lib/cause/recordChildren.d.ts +3 -3
- package/dist/esm/lib/cause/recordChildren.js +3 -4
- package/dist/esm/lib/cause/recordCoverage.d.ts +2 -2
- package/dist/esm/lib/cause/recordCoverage.js +1 -1
- package/dist/esm/lib/cause/recordIdentity.js +5 -5
- package/dist/esm/lib/cause/recordLoading.js +3 -3
- 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.js +13 -13
- package/dist/esm/lib/cause/recordValidation.d.ts +4 -4
- package/dist/esm/lib/cause/recordValidation.js +17 -17
- package/dist/esm/lib/cause/session.js +9 -6
- package/dist/esm/lib/cause/storedRecordAdapter.js +16 -10
- package/dist/esm/lib/cause/types.d.ts +2 -1
- package/dist/esm/lib/cause/validation.d.ts +4 -4
- package/dist/esm/lib/cause/validation.js +19 -36
- package/dist/esm/lib/{cause → evidence}/admission.d.ts +5 -1
- package/dist/esm/lib/{cause → evidence}/admission.js +14 -1
- 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/index.js +5 -1
- package/dist/lib/cause/explain.js +6 -6
- package/dist/lib/cause/queryMemo.js +52 -0
- package/dist/lib/cause/recordAdapter.js +10 -17
- package/dist/lib/cause/recordChildren.js +3 -4
- package/dist/lib/cause/recordCoverage.js +1 -1
- package/dist/lib/cause/recordIdentity.js +4 -4
- package/dist/lib/cause/recordLoading.js +2 -2
- package/dist/lib/cause/recordQuery.js +27 -0
- package/dist/lib/cause/recordScope.js +12 -12
- package/dist/lib/cause/recordValidation.js +21 -21
- package/dist/lib/cause/session.js +8 -5
- package/dist/lib/cause/storedRecordAdapter.js +15 -9
- package/dist/lib/cause/validation.js +23 -40
- package/dist/lib/{cause → evidence}/admission.js +14 -1
- 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/types/index.d.ts +4 -0
- package/dist/types/lib/cause/queryMemo.d.ts +18 -0
- package/dist/types/lib/cause/recordChildren.d.ts +3 -3
- package/dist/types/lib/cause/recordCoverage.d.ts +2 -2
- package/dist/types/lib/cause/recordQuery.d.ts +9 -0
- package/dist/types/lib/cause/recordValidation.d.ts +4 -4
- package/dist/types/lib/cause/types.d.ts +2 -1
- package/dist/types/lib/cause/validation.d.ts +4 -4
- package/dist/types/lib/{cause → evidence}/admission.d.ts +5 -1
- 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/package.json +1 -1
- /package/dist/esm/lib/cause/{recordParents.d.ts → recordInspection.d.ts} +0 -0
- /package/dist/esm/lib/cause/{recordParents.js → recordInspection.js} +0 -0
- /package/dist/lib/cause/{recordParents.js → recordInspection.js} +0 -0
- /package/dist/types/lib/cause/{recordParents.d.ts → recordInspection.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -43,6 +43,31 @@ Writers record during execution. Readers query what was recorded; they do not re
|
|
|
43
43
|
|
|
44
44
|
See the [record contract](docs/guides/record-contract.md), [time-travel contract](src/lib/time-travel/README.md), and [slice contract](src/lib/slice/README.md).
|
|
45
45
|
|
|
46
|
+
## Evidence requirements — 1.3.0
|
|
47
|
+
|
|
48
|
+
`RequirementResult` and `readRequirementResults(unknown)` on the root door carry
|
|
49
|
+
one producer-issued requirement result per finding/window, the claims it
|
|
50
|
+
`affects`, explicit unresolved `RequirementReason` codes, check/evidence/attempt
|
|
51
|
+
references and available action references. Domain details are namespaced IDs;
|
|
52
|
+
raw subjects, payloads, credentials and executable actions do not enter this
|
|
53
|
+
contract. The bounded reader rejects malformed or oversized batches atomically
|
|
54
|
+
and returns detached frozen records. It works through the root ESM and CommonJS
|
|
55
|
+
doors without loading the optional cause reader.
|
|
56
|
+
|
|
57
|
+
`RequirementReason` is the twelve-code domain-evidence subset of the single
|
|
58
|
+
`GapCode` registry. Result and check reasons refuse trace-walk codes such as
|
|
59
|
+
`cycle` and `node-limit`; generic cause gaps and typed admission errors retain
|
|
60
|
+
their broader vocabulary.
|
|
61
|
+
|
|
62
|
+
Structural admission never authenticates a producer or establishes that a
|
|
63
|
+
`satisfied` assertion is true. The host verifies current query, run, caller scope,
|
|
64
|
+
artifact provenance and domain validity before rendering each claim. An unresolved
|
|
65
|
+
requirement blocks only its affected conclusions; independently supported facts
|
|
66
|
+
can remain. Valid-time and learned-time references are separate, and missing
|
|
67
|
+
validity is unknown. Action references resolve through a host-owned closed
|
|
68
|
+
registry, with permissions checked again at execution. See the [contract and
|
|
69
|
+
limits](src/lib/evidence/README.md) and [reference-only example](examples/post-execution/time-travel/08-evidence-requirements.ts).
|
|
70
|
+
|
|
46
71
|
## A record that survives the run
|
|
47
72
|
|
|
48
73
|
A log can also hand every entry to a sink **as it lands**, so a run that is killed still leaves its record behind. The sink sees four entries — `opened` (the base), `commit` (each bundle, after it is frozen), `note` (anything the writer wants kept beside the log, as it happens) and `closed` (how the log ended) — and `memorySink` is the reference adapter. Write your own for a file, a bucket or a table by implementing those four methods.
|
|
@@ -114,7 +139,7 @@ Record bytes are part of the versioning promise: refactors never re-pin; a named
|
|
|
114
139
|
MIT license. Contact: sanjay@footprintjs.dev.
|
|
115
140
|
|
|
116
141
|
|
|
117
|
-
## Cause and impact — 1.2.0
|
|
142
|
+
## Cause and impact — 1.2.0
|
|
118
143
|
|
|
119
144
|
`foottrace/cause` reads recorded dependencies; it never reruns business code, calls a model, or accesses storage by itself. An application supplies a graph or a stored-record host. `explain(graph, nodeId)` walks the selected node's recorded data and control dependencies, identifies the earliest marked ancestors in that dependency order, and walks forward to report impact. Independent candidates remain `alternatives`; timestamps and ID order never break a causal tie. A missing or cut backward dependency withholds `rootCause`; missing evidence in either direction returns `confidence: 'incomplete'` with fixed gap codes. Confidence describes evidence coverage, not proof of a provider's internal failure.
|
|
120
145
|
|
|
@@ -198,7 +223,7 @@ Foundations: [Weiser, *Program slicing* (1981)](https://dl.acm.org/doi/10.5555/8
|
|
|
198
223
|
| Loading only queried graph nodes/scopes | **Demand-driven, or lazy, graph traversal** |
|
|
199
224
|
| Default candidate selection | **Root-cause localisation on a dependency DAG**, using the earliest anomalous ancestor |
|
|
200
225
|
|
|
201
|
-
These names describe the techniques, not a claim of complete language-level instrumentation. Findings are bounded by the dependencies the producer recorded. Cause queries share one deadline and node/edge/call budget across reverse traversal, ranking and forward impact; limits, cancellation and unavailable reads become gaps. Output carries references and fixed codes, never a serialized DAG or business payload. `/cause` is ESM-only and `sideEffects: false`; its unused-import browser bundle is tested to match a bundle without the import. The package has no runtime dependencies.
|
|
226
|
+
These names describe the techniques, not a claim of complete language-level instrumentation. Findings are bounded by the dependencies the producer recorded. Cause queries share one deadline and node/edge/call budget across reverse traversal, ranking and forward impact; limits, cancellation and unavailable reads become gaps. Output carries references and fixed codes, never a serialized DAG or business payload. `/cause` is ESM-only and `sideEffects: false`; its unused-import browser bundle is tested to match a bundle without the import. The package has no runtime dependencies. The cause reader shipped in 1.2.0; later engine stamping, viewer integration and agent tools are separate changes.
|
|
202
227
|
|
|
203
228
|
Direct `graph.openScope(address, { signal })` discovery rejects with a fixed `cause: record unavailable` error and a gap `code` on failure; an unavailable scope is never reported as an empty successful log. Public `explain` and `attentionList` queries turn unavailable reads into bounded incomplete findings.
|
|
204
229
|
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -30,3 +30,7 @@ export { deepEqual } from './lib/memory/equality.js';
|
|
|
30
30
|
export { serveRecord } from './lib/capture/freeze.js';
|
|
31
31
|
/** Record attention types are shared with producers; the optional cause reader has its own door. */
|
|
32
32
|
export type { Attention, AttentionKind } from './lib/cause/types.js';
|
|
33
|
+
/** Reference-only requirement records: structural admission never grants authority or verifies domain truth. */
|
|
34
|
+
export type { RequirementReason } from './lib/evidence/codes.js';
|
|
35
|
+
export type { RequirementCheck, RequirementCheckDisposition, RequirementResult, } from './lib/evidence/requirementTypes.js';
|
|
36
|
+
export { readRequirementResults, RequirementValidationError, REQUIREMENT_RESULT_LIMITS, } from './lib/evidence/requirements.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -11,3 +11,4 @@ export { CommitRangeIndex } from './lib/recorder/CommitRangeIndex.js';
|
|
|
11
11
|
export { EXECUTION_DELIMITER, PATH_DELIMITER, isExecutionKey, joinPath, pathSegments as idPathSegments, stageIdOf, subflowSegmentsOf, } from './lib/ids/runtimeStageId.js';
|
|
12
12
|
export { deepEqual } from './lib/memory/equality.js';
|
|
13
13
|
export { serveRecord } from './lib/capture/freeze.js';
|
|
14
|
+
export { readRequirementResults, RequirementValidationError, REQUIREMENT_RESULT_LIMITS, } from './lib/evidence/requirements.js';
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { Session } from './session.js';
|
|
2
2
|
import { frontier, pathTo, walk } from './walk.js';
|
|
3
|
-
import { gap,
|
|
3
|
+
import { gap, graphName, nodeIds, graphOwn, uniqueGaps } from './validation.js';
|
|
4
4
|
import { admitRanking } from './ranking.js';
|
|
5
5
|
/** Read-only deterministic cause localisation and impact. No inference from timestamps or structural ancestry. */
|
|
6
6
|
export async function explain(graph, nodeId, options = {}) {
|
|
7
|
-
|
|
7
|
+
graphName(nodeId);
|
|
8
8
|
const session = new Session(graph, options);
|
|
9
9
|
try {
|
|
10
10
|
const backward = await walk(session, [nodeId], 'parents');
|
|
11
11
|
const missing = [...backward.gaps];
|
|
12
12
|
let candidates = frontier(backward).map((item) => item.nodeId);
|
|
13
|
-
const rank =
|
|
13
|
+
const rank = graphOwn(options, 'rankRoots');
|
|
14
14
|
if (rank !== undefined) {
|
|
15
15
|
if (typeof rank !== 'function')
|
|
16
16
|
throw new TypeError('CAUSE_CONFIGURATION_INVALID');
|
|
@@ -31,7 +31,7 @@ export async function explain(graph, nodeId, options = {}) {
|
|
|
31
31
|
const rootCause = causeComplete && candidates.length === 1 ? candidates[0] : undefined;
|
|
32
32
|
const forward = await walk(session, [nodeId], 'children');
|
|
33
33
|
let impactNodes = Object.freeze(forward.nodes.filter((id) => id !== nodeId));
|
|
34
|
-
const impactGaps = [...forward.gaps], rankImpact =
|
|
34
|
+
const impactGaps = [...forward.gaps], rankImpact = graphOwn(options, 'rankImpact');
|
|
35
35
|
if (typeof rankImpact === 'function') {
|
|
36
36
|
const selection = await session.invoke(undefined, () => rankImpact(impactNodes, session.context), (raw) => admitRanking(raw, new Set(impactNodes), {
|
|
37
37
|
exactPermutation: true,
|
|
@@ -69,19 +69,19 @@ export async function attentionList(graph, options = {}) {
|
|
|
69
69
|
const session = new Session(graph, options);
|
|
70
70
|
try {
|
|
71
71
|
let seeds = [], missing = [];
|
|
72
|
-
const requested =
|
|
72
|
+
const requested = graphOwn(options, 'nodeIds');
|
|
73
73
|
const admitSeeds = (raw) => {
|
|
74
74
|
// Only inspect the bounded prefix; the unread tail is explicitly incomplete.
|
|
75
75
|
if (!Array.isArray(raw))
|
|
76
76
|
return nodeIds(raw, session.limits.maxNodes);
|
|
77
|
-
const length =
|
|
77
|
+
const length = graphOwn(raw, 'length');
|
|
78
78
|
if (typeof length !== 'number' || !Number.isSafeInteger(length) || length < 0)
|
|
79
79
|
return nodeIds(raw, session.limits.maxNodes);
|
|
80
80
|
if (length > session.limits.maxNodes)
|
|
81
81
|
missing.push(gap('node-limit'));
|
|
82
82
|
const prefix = [];
|
|
83
83
|
for (let i = 0; i < Math.min(length, session.limits.maxNodes); i++)
|
|
84
|
-
prefix.push(
|
|
84
|
+
prefix.push(graphOwn(raw, String(i)));
|
|
85
85
|
return nodeIds(prefix, session.limits.maxNodes);
|
|
86
86
|
};
|
|
87
87
|
if (requested !== undefined) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { GraphContext } from './types.js';
|
|
2
|
+
/** Share bounded derived work within one query without retaining it in the graph's storage cache. */
|
|
3
|
+
declare class QueryMemo {
|
|
4
|
+
private readonly maximumEntries;
|
|
5
|
+
private readonly owners;
|
|
6
|
+
private entries;
|
|
7
|
+
private closed;
|
|
8
|
+
constructor(maximumEntries: number);
|
|
9
|
+
remember<T>(owner: object, key: unknown, create: () => T): T;
|
|
10
|
+
close(): void;
|
|
11
|
+
}
|
|
12
|
+
/** Reserve one resolution and one inspection per admitted node, shared across all adapter owners. */
|
|
13
|
+
export declare function beginQueryMemo(context: GraphContext, maximumNodes: number): void;
|
|
14
|
+
/** Direct graph calls have no managed query lifetime and deliberately remain uncached. */
|
|
15
|
+
export declare function queryMemo(context: GraphContext): QueryMemo | undefined;
|
|
16
|
+
/** Clear synchronously without aborting an otherwise successful query's public signal. */
|
|
17
|
+
export declare function endQueryMemo(context: GraphContext): void;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Share bounded derived work within one query without retaining it in the graph's storage cache. */
|
|
2
|
+
class QueryMemo {
|
|
3
|
+
maximumEntries;
|
|
4
|
+
owners = new Map();
|
|
5
|
+
entries = 0;
|
|
6
|
+
closed = false;
|
|
7
|
+
constructor(maximumEntries) {
|
|
8
|
+
this.maximumEntries = maximumEntries;
|
|
9
|
+
}
|
|
10
|
+
remember(owner, key, create) {
|
|
11
|
+
const previous = this.owners.get(owner);
|
|
12
|
+
if (previous?.has(key))
|
|
13
|
+
return previous.get(key);
|
|
14
|
+
if (this.closed || this.entries >= this.maximumEntries)
|
|
15
|
+
return create();
|
|
16
|
+
const value = create();
|
|
17
|
+
// Creation can finish after disposal or re-enter other readers; neither may refill a closed/full memo.
|
|
18
|
+
if (!this.closed && this.entries < this.maximumEntries) {
|
|
19
|
+
const bucket = this.owners.get(owner) ?? new Map();
|
|
20
|
+
if (!bucket.has(key))
|
|
21
|
+
this.entries++;
|
|
22
|
+
bucket.set(key, value);
|
|
23
|
+
this.owners.set(owner, bucket);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
close() {
|
|
28
|
+
this.closed = true;
|
|
29
|
+
this.owners.clear();
|
|
30
|
+
this.entries = 0;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const queries = new WeakMap();
|
|
34
|
+
/** Reserve one resolution and one inspection per admitted node, shared across all adapter owners. */
|
|
35
|
+
export function beginQueryMemo(context, maximumNodes) {
|
|
36
|
+
queries.set(context, new QueryMemo(maximumNodes * 2));
|
|
37
|
+
}
|
|
38
|
+
/** Direct graph calls have no managed query lifetime and deliberately remain uncached. */
|
|
39
|
+
export function queryMemo(context) {
|
|
40
|
+
return queries.get(context);
|
|
41
|
+
}
|
|
42
|
+
/** Clear synchronously without aborting an otherwise successful query's public signal. */
|
|
43
|
+
export function endQueryMemo(context) {
|
|
44
|
+
queries.get(context)?.close();
|
|
45
|
+
queries.delete(context);
|
|
46
|
+
}
|
|
@@ -1,38 +1,31 @@
|
|
|
1
1
|
import { childrenForRecordNode } from './recordChildren.js';
|
|
2
2
|
import { attentionForRecordNode, gapForRecordFailure } from './recordCoverage.js';
|
|
3
|
-
import { nodeIdFor
|
|
3
|
+
import { nodeIdFor } from './recordIdentity.js';
|
|
4
4
|
import { createRecordScopeLoader, openRecordScope } from './recordLoading.js';
|
|
5
|
-
import {
|
|
5
|
+
import { createRecordQuery, whenResolved } from './recordQuery.js';
|
|
6
6
|
import { recordOptions } from './recordScope.js';
|
|
7
|
-
import { addressKey, addressOf,
|
|
7
|
+
import { addressKey, addressOf, recordList, recordOwn, RecordRefusal } from './recordValidation.js';
|
|
8
|
+
/** Give each failed port its own incomplete page rather than sharing a mutable edge array. */
|
|
9
|
+
const unavailableAdjacency = () => ({ edges: [], complete: false });
|
|
8
10
|
export { recordOptions } from './recordScope.js';
|
|
9
11
|
/** Common engine for memory and one-scope-at-a-time host storage. Loader functions are trusted executable code. */
|
|
10
12
|
export function createLazyRecordGraph(load, options = {}) {
|
|
11
13
|
const limits = recordOptions(options), loader = createRecordScopeLoader(load, limits);
|
|
12
|
-
const
|
|
14
|
+
const query = createRecordQuery(loader, limits.maxEdges);
|
|
13
15
|
return Object.freeze({
|
|
14
16
|
nodeId: (address, node) => nodeIdFor(address, node),
|
|
15
17
|
openScope: (address, context) => openRecordScope(address, context, loader),
|
|
16
18
|
async parents(nodeId, context) {
|
|
17
|
-
|
|
18
|
-
return 'failure' in node
|
|
19
|
-
? { edges: [], complete: false }
|
|
20
|
-
: inspectRecordNode(node, limits.maxEdges, loader.extraGapCodes(node.scope)).adjacency;
|
|
19
|
+
return whenResolved(await query.resolve(nodeId, context), (node) => query.inspect(node, context).adjacency, unavailableAdjacency);
|
|
21
20
|
},
|
|
22
21
|
async gaps(nodeId, context) {
|
|
23
|
-
|
|
24
|
-
return 'failure' in node
|
|
25
|
-
? gapForRecordFailure(node.failure)
|
|
26
|
-
: inspectRecordNode(node, limits.maxEdges, loader.extraGapCodes(node.scope)).gaps;
|
|
22
|
+
return whenResolved(await query.resolve(nodeId, context), (node) => query.inspect(node, context).gaps, (failure) => gapForRecordFailure(failure.failure));
|
|
27
23
|
},
|
|
28
24
|
async attention(nodeId, context) {
|
|
29
|
-
return
|
|
25
|
+
return whenResolved(await query.resolve(nodeId, context), attentionForRecordNode, () => []);
|
|
30
26
|
},
|
|
31
27
|
async children(nodeId, context) {
|
|
32
|
-
|
|
33
|
-
return 'failure' in node
|
|
34
|
-
? { edges: [], complete: false }
|
|
35
|
-
: childrenForRecordNode(node, limits.maxEdges, loader.extraGapCodes(node.scope));
|
|
28
|
+
return whenResolved(await query.resolve(nodeId, context), (node) => childrenForRecordNode(node, limits.maxEdges, query.inspect(node, context)), unavailableAdjacency);
|
|
36
29
|
},
|
|
37
30
|
});
|
|
38
31
|
}
|
|
@@ -40,8 +33,8 @@ export function createLazyRecordGraph(load, options = {}) {
|
|
|
40
33
|
export function createRecordGraph(scopes, options = {}) {
|
|
41
34
|
const limits = recordOptions(options);
|
|
42
35
|
return createLazyRecordGraph((address) => {
|
|
43
|
-
for (const raw of
|
|
44
|
-
if (addressKey(addressOf(
|
|
36
|
+
for (const raw of recordList(scopes, limits.maxScopes)) {
|
|
37
|
+
if (addressKey(addressOf(recordOwn(raw, 'address'))) === addressKey(address))
|
|
45
38
|
return { scope: raw };
|
|
46
39
|
}
|
|
47
40
|
throw new RecordRefusal('unavailable');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ResolvedRecordNode } from './recordTypes.js';
|
|
2
|
-
import type { Adjacency
|
|
1
|
+
import type { RecordInspection, ResolvedRecordNode } from './recordTypes.js';
|
|
2
|
+
import type { Adjacency } from './types.js';
|
|
3
3
|
/** Share parent-side coverage checks before appending data then control children in their established order. */
|
|
4
|
-
export declare function childrenForRecordNode(node: ResolvedRecordNode, maximumEdges: number,
|
|
4
|
+
export declare function childrenForRecordNode(node: ResolvedRecordNode, maximumEdges: number, inspection: RecordInspection): Adjacency;
|
|
@@ -2,7 +2,6 @@ import { forwardSliceForKey } from '../slice/forwardSliceForKey.js';
|
|
|
2
2
|
import { appendControlChildren } from './recordControl.js';
|
|
3
3
|
import { allStageReadsCollected, writtenKeysForNode } from './recordCoverage.js';
|
|
4
4
|
import { nodeIdFor } from './recordIdentity.js';
|
|
5
|
-
import { inspectRecordNode } from './recordParents.js';
|
|
6
5
|
import { readKeysForStage } from './recordScope.js';
|
|
7
6
|
/** Keep per-write and conservative fed edges distinct while retaining existing cap and deduplication order. */
|
|
8
7
|
function appendFedChildren(scope, root, maximumEdges, edges) {
|
|
@@ -67,10 +66,10 @@ function appendDataChildren(scope, identity, commitIndex, maximumEdges, edges) {
|
|
|
67
66
|
return complete;
|
|
68
67
|
}
|
|
69
68
|
/** Share parent-side coverage checks before appending data then control children in their established order. */
|
|
70
|
-
export function childrenForRecordNode(node, maximumEdges,
|
|
69
|
+
export function childrenForRecordNode(node, maximumEdges, inspection) {
|
|
71
70
|
const { scope, identity, commitIndex } = node;
|
|
72
|
-
const
|
|
73
|
-
let complete =
|
|
71
|
+
const edges = [];
|
|
72
|
+
let complete = inspection.gaps.length === 0;
|
|
74
73
|
if (!allStageReadsCollected(scope))
|
|
75
74
|
complete = false;
|
|
76
75
|
complete = appendDataChildren(scope, identity, commitIndex, maximumEdges, edges) && complete;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CommitBundle } from '../memory/types.js';
|
|
2
|
-
import type { AddRecordGap, PreparedRecordScope, RecordIdentity,
|
|
2
|
+
import type { AddRecordGap, PreparedRecordScope, RecordIdentity, ResolvedRecordNode } from './recordTypes.js';
|
|
3
3
|
import type { Attention, Gap } from './types.js';
|
|
4
4
|
/** Preserve first-observed coverage order while collapsing repeated codes from different evidence paths. */
|
|
5
5
|
export declare function recordGapCollector(): {
|
|
@@ -9,7 +9,7 @@ export declare function recordGapCollector(): {
|
|
|
9
9
|
/** Failed loading exposes a fixed category, never the host error or an apparently complete empty graph. */
|
|
10
10
|
export declare function gapForRecordFailure(code: Gap['code']): readonly Gap[];
|
|
11
11
|
/** Return captured mark references without treating an omitted attention map as collected evidence. */
|
|
12
|
-
export declare function attentionForRecordNode(node: ResolvedRecordNode
|
|
12
|
+
export declare function attentionForRecordNode(node: ResolvedRecordNode): readonly Attention[];
|
|
13
13
|
/** Emit host and capture gaps before query-derived gaps so existing evidence order stays stable. */
|
|
14
14
|
export declare function collectScopeCoverage(scope: PreparedRecordScope, bundle: CommitBundle, extraGapCodes: readonly Gap['code'][] | undefined, addGap: AddRecordGap): void;
|
|
15
15
|
/** A read-only identity has no new value life; unqualified executions retain written-key encounter order. */
|
|
@@ -20,7 +20,7 @@ export function gapForRecordFailure(code) {
|
|
|
20
20
|
/** Return captured mark references without treating an omitted attention map as collected evidence. */
|
|
21
21
|
export function attentionForRecordNode(node) {
|
|
22
22
|
// An omitted map stays unknown coverage; it is never replaced by an affirmative empty map.
|
|
23
|
-
return
|
|
23
|
+
return node.scope.marks?.get(node.identity.runtimeStageId) ?? [];
|
|
24
24
|
}
|
|
25
25
|
/** Emit host and capture gaps before query-derived gaps so existing evidence order stays stable. */
|
|
26
26
|
export function collectScopeCoverage(scope, bundle, extraGapCodes, addGap) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readKeysForStage } from './recordScope.js';
|
|
2
|
-
import { addressOf,
|
|
2
|
+
import { addressOf, recordName, recordOwn, RecordRefusal } from './recordValidation.js';
|
|
3
3
|
/** Parse only the bounded identity encoding; payload fields never become graph addresses. */
|
|
4
4
|
export function readNodeIdentity(nodeId) {
|
|
5
5
|
if (typeof nodeId !== 'string' || nodeId.length > 2048 || !nodeId.startsWith('record:'))
|
|
@@ -11,7 +11,7 @@ export function readNodeIdentity(nodeId) {
|
|
|
11
11
|
catch {
|
|
12
12
|
throw new RecordRefusal();
|
|
13
13
|
}
|
|
14
|
-
const address = addressOf(
|
|
14
|
+
const address = addressOf(recordOwn(fields, 'address')), runtimeStageId = recordName(recordOwn(fields, 'runtimeStageId')), commitIndex = recordOwn(fields, 'commitIdx'), key = recordOwn(fields, 'key'), read = recordOwn(fields, 'read');
|
|
15
15
|
if (commitIndex !== undefined && (!Number.isSafeInteger(commitIndex) || commitIndex < 0))
|
|
16
16
|
throw new RecordRefusal();
|
|
17
17
|
if ((read !== undefined && read !== true) || (read === true && key === undefined))
|
|
@@ -20,14 +20,14 @@ export function readNodeIdentity(nodeId) {
|
|
|
20
20
|
address,
|
|
21
21
|
runtimeStageId,
|
|
22
22
|
...(commitIndex === undefined ? {} : { commitIdx: commitIndex }),
|
|
23
|
-
...(key === undefined ? {} : { key:
|
|
23
|
+
...(key === undefined ? {} : { key: recordName(key, true) }),
|
|
24
24
|
...(read === true ? { read: true } : {}),
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
27
|
/** Stable field order preserves existing address/execution/key IDs across storage and memory readers. */
|
|
28
28
|
export function nodeIdFor(addressInput, node, read) {
|
|
29
29
|
try {
|
|
30
|
-
const address = addressOf(addressInput), runtimeStageId =
|
|
30
|
+
const address = addressOf(addressInput), runtimeStageId = recordName(recordOwn(node, 'runtimeStageId')), commitIndex = recordOwn(node, 'commitIdx'), key = recordOwn(node, 'key');
|
|
31
31
|
if (commitIndex !== undefined && (!Number.isSafeInteger(commitIndex) || commitIndex < 0))
|
|
32
32
|
throw new RecordRefusal();
|
|
33
33
|
const nodeId = 'record:' +
|
|
@@ -35,7 +35,7 @@ export function nodeIdFor(addressInput, node, read) {
|
|
|
35
35
|
address,
|
|
36
36
|
runtimeStageId,
|
|
37
37
|
...(commitIndex === undefined ? {} : { commitIdx: commitIndex }),
|
|
38
|
-
...(key === undefined ? {} : { key:
|
|
38
|
+
...(key === undefined ? {} : { key: recordName(key, true) }),
|
|
39
39
|
...(read ? { read: true } : {}),
|
|
40
40
|
});
|
|
41
41
|
return nodeId.length <= 2048 ? nodeId : 'record:invalid';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { gaps as admitGaps } from './validation.js';
|
|
2
2
|
import { nodeIdFor } from './recordIdentity.js';
|
|
3
3
|
import { prepareRecordScope } from './recordScope.js';
|
|
4
|
-
import { addressKey, addressOf,
|
|
4
|
+
import { addressKey, addressOf, recordList, recordOwn, projectLoaded, RecordRefusal } from './recordValidation.js';
|
|
5
5
|
/** Admit before caching so a cache hit cannot bypass payload limits or lose scope-owned coverage. */
|
|
6
6
|
function admitLoadedScope(result, request, limits, state) {
|
|
7
7
|
if (request.controller.signal.aborted || performance.now() >= request.deadline) {
|
|
@@ -10,8 +10,8 @@ function admitLoadedScope(result, request, limits, state) {
|
|
|
10
10
|
}
|
|
11
11
|
let prepared;
|
|
12
12
|
try {
|
|
13
|
-
prepared = prepareRecordScope(
|
|
14
|
-
const extra =
|
|
13
|
+
prepared = prepareRecordScope(recordOwn(result, 'scope'), request.address, limits.maxCommits, limits.maxFields);
|
|
14
|
+
const extra = recordList(recordOwn(result, 'gaps') ?? [], 32);
|
|
15
15
|
// Coverage belongs to this admitted object, so cached scopes retain the same gap ordering and ownership.
|
|
16
16
|
state.extraGaps.set(prepared, admitGaps(extra.map((code) => ({ code, evidence: [] }))).map((gap) => gap.code));
|
|
17
17
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RecordLoadFailure, RecordScopeLoader, ResolvedRecordNode } from './recordTypes.js';
|
|
2
|
+
import type { GraphContext, NodeId } from './types.js';
|
|
3
|
+
/** Both dependency directions share the same admitted node and canonical inspection within a query. */
|
|
4
|
+
export declare function createRecordQuery(loader: RecordScopeLoader, maximumEdges: number): {
|
|
5
|
+
resolve(nodeId: NodeId, context: GraphContext): Promise<RecordLoadFailure | ResolvedRecordNode>;
|
|
6
|
+
inspect(node: ResolvedRecordNode, context: GraphContext): import("./recordTypes.js").RecordInspection;
|
|
7
|
+
};
|
|
8
|
+
/** Every port maps the same fixed loading failure while projecting only admitted nodes. */
|
|
9
|
+
export declare function whenResolved<T>(node: ResolvedRecordNode | RecordLoadFailure, onNode: (node: ResolvedRecordNode) => T, onFailure: (failure: RecordLoadFailure) => T): T;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { queryMemo } from './queryMemo.js';
|
|
2
|
+
import { resolveRecordNode } from './recordIdentity.js';
|
|
3
|
+
import { inspectRecordNode } from './recordInspection.js';
|
|
4
|
+
/** Both dependency directions share the same admitted node and canonical inspection within a query. */
|
|
5
|
+
export function createRecordQuery(loader, maximumEdges) {
|
|
6
|
+
const resolutionOwner = {}, inspectionOwner = {};
|
|
7
|
+
return {
|
|
8
|
+
resolve(nodeId, context) {
|
|
9
|
+
const resolve = () => resolveRecordNode(nodeId, context, loader), memo = queryMemo(context);
|
|
10
|
+
// A retained success must never bypass the loader's existing cancellation check.
|
|
11
|
+
return memo && !context.signal.aborted ? memo.remember(resolutionOwner, nodeId, resolve) : resolve();
|
|
12
|
+
},
|
|
13
|
+
inspect(node, context) {
|
|
14
|
+
const inspect = () => inspectRecordNode(node, maximumEdges, loader.extraGapCodes(node.scope));
|
|
15
|
+
return queryMemo(context)?.remember(inspectionOwner, node, inspect) ?? inspect();
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Every port maps the same fixed loading failure while projecting only admitted nodes. */
|
|
20
|
+
export function whenResolved(node, onNode, onFailure) {
|
|
21
|
+
return 'failure' in node ? onFailure(node) : onNode(node);
|
|
22
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { addressKey, addressOf, bundleOf,
|
|
1
|
+
import { addressKey, addressOf, bundleOf, recordList, mapOf, marksOf, recordName, names, recordOwn, RecordRefusal, spend, } from './recordValidation.js';
|
|
2
2
|
/** Refuse an invalid limit before loading a scope or allocating its preparation budget. */
|
|
3
3
|
function boundedOption(value, defaultValue, maximum) {
|
|
4
4
|
const configuredValue = value ?? defaultValue;
|
|
@@ -16,11 +16,11 @@ export function recordOptions(options) {
|
|
|
16
16
|
if (prototype !== Object.prototype && prototype !== null)
|
|
17
17
|
throw new TypeError();
|
|
18
18
|
return Object.freeze({
|
|
19
|
-
maxCommits: boundedOption(
|
|
20
|
-
maxFields: boundedOption(
|
|
21
|
-
maxEdges: boundedOption(
|
|
22
|
-
maxScopes: boundedOption(
|
|
23
|
-
loadTimeoutMs: boundedOption(
|
|
19
|
+
maxCommits: boundedOption(recordOwn(options, 'maxCommits'), 256, 4096),
|
|
20
|
+
maxFields: boundedOption(recordOwn(options, 'maxFields'), 20000, 1000000),
|
|
21
|
+
maxEdges: boundedOption(recordOwn(options, 'maxEdges'), 64, 256),
|
|
22
|
+
maxScopes: boundedOption(recordOwn(options, 'maxScopes'), 32, 256),
|
|
23
|
+
loadTimeoutMs: boundedOption(recordOwn(options, 'loadTimeoutMs'), 1000, 30000),
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
26
|
catch {
|
|
@@ -29,22 +29,22 @@ export function recordOptions(options) {
|
|
|
29
29
|
}
|
|
30
30
|
/** Spend one shared field budget before canonical readers see any untrusted scope data. */
|
|
31
31
|
export function prepareRecordScope(value, address, maximumCommits, maximumFields) {
|
|
32
|
-
if (addressKey(addressOf(
|
|
32
|
+
if (addressKey(addressOf(recordOwn(value, 'address'))) !== addressKey(address))
|
|
33
33
|
throw new RecordRefusal();
|
|
34
|
-
const rawLog =
|
|
35
|
-
const log = rawLog.map((_, commitIndex) => bundleOf(
|
|
36
|
-
const reads = mapOf(
|
|
34
|
+
const rawLog = recordList(recordOwn(value, 'commitLog'), maximumCommits), budget = { left: maximumFields };
|
|
35
|
+
const log = rawLog.map((_, commitIndex) => bundleOf(recordOwn(rawLog, String(commitIndex)), commitIndex, budget));
|
|
36
|
+
const reads = mapOf(recordOwn(value, 'reads'), maximumCommits, (value) => {
|
|
37
37
|
const readKeys = names(value, 128, true);
|
|
38
38
|
spend(budget, readKeys.length);
|
|
39
39
|
return readKeys;
|
|
40
40
|
}, budget);
|
|
41
|
-
const control = mapOf(
|
|
42
|
-
const marks = mapOf(
|
|
41
|
+
const control = mapOf(recordOwn(value, 'controlDeps'), maximumCommits, (value) => ({ deciderId: recordName(recordOwn(value, 'deciderId')) }), budget);
|
|
42
|
+
const marks = mapOf(recordOwn(value, 'attention'), maximumCommits, (value) => {
|
|
43
43
|
const attention = marksOf(value);
|
|
44
44
|
spend(budget, attention.length + attention.reduce((count, mark) => count + mark.evidence.length, 0));
|
|
45
45
|
return attention;
|
|
46
46
|
}, budget);
|
|
47
|
-
return { address, log, reads, control, marks, complete:
|
|
47
|
+
return { address, log, reads, control, marks, complete: recordOwn(value, 'complete') === true };
|
|
48
48
|
}
|
|
49
49
|
/** Preserve both stage reads and stronger per-write prefixes, including their original encounter order. */
|
|
50
50
|
export function readKeysForStage(scope, runtimeStageId) {
|
|
@@ -3,17 +3,17 @@ import type { LogAddress } from '../memory/eventPosition.js';
|
|
|
3
3
|
import type { CommitBundle } from '../memory/types.js';
|
|
4
4
|
import type { GapCode } from './types.js';
|
|
5
5
|
import { attention as marksOf } from './validation.js';
|
|
6
|
-
import { EvidenceRefusal } from '
|
|
6
|
+
import { EvidenceRefusal } from '../evidence/admission.js';
|
|
7
7
|
export { marksOf };
|
|
8
8
|
/** Loader refusals stay distinct from graph-port refusals so a failed host load keeps its original classification. */
|
|
9
9
|
export declare class RecordRefusal extends EvidenceRefusal {
|
|
10
10
|
readonly code: GapCode;
|
|
11
11
|
constructor(code?: GapCode);
|
|
12
12
|
}
|
|
13
|
-
export declare function
|
|
13
|
+
export declare function recordOwn(value: unknown, key: string): unknown;
|
|
14
14
|
export declare function names(value: unknown, maximum?: number, path?: boolean): string[];
|
|
15
|
-
export declare function
|
|
16
|
-
export declare function
|
|
15
|
+
export declare function recordName(value: unknown, path?: boolean): string;
|
|
16
|
+
export declare function recordList(value: unknown, maximum: number): readonly unknown[];
|
|
17
17
|
export declare function addressOf(value: unknown): LogAddress;
|
|
18
18
|
export declare function spend(budget: {
|
|
19
19
|
left: number;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { attention as marksOf, AdmissionError, observePromise } from './validation.js';
|
|
2
|
-
import { boundedList, boundedName, EvidenceRefusal, ownData, spendFields } from '
|
|
2
|
+
import { boundedList, boundedName, EvidenceRefusal, ownData, spendFields } from '../evidence/admission.js';
|
|
3
3
|
export { marksOf };
|
|
4
4
|
/** Loader refusals stay distinct from graph-port refusals so a failed host load keeps its original classification. */
|
|
5
5
|
export class RecordRefusal extends EvidenceRefusal {
|
|
@@ -11,16 +11,16 @@ export class RecordRefusal extends EvidenceRefusal {
|
|
|
11
11
|
}
|
|
12
12
|
const refuseRecord = (code) => new RecordRefusal(code);
|
|
13
13
|
const recordDescriptors = { translateTrapErrors: true, requireOwnValue: true };
|
|
14
|
-
export function
|
|
14
|
+
export function recordOwn(value, key) {
|
|
15
15
|
return ownData(value, key, refuseRecord, recordDescriptors);
|
|
16
16
|
}
|
|
17
17
|
export function names(value, maximum = 128, path = false) {
|
|
18
|
-
return
|
|
18
|
+
return recordList(value, maximum).map((item) => recordName(item, path));
|
|
19
19
|
}
|
|
20
|
-
export function
|
|
20
|
+
export function recordName(value, path = false) {
|
|
21
21
|
return boundedName(value, 1024, refuseRecord, path);
|
|
22
22
|
}
|
|
23
|
-
export function
|
|
23
|
+
export function recordList(value, maximum) {
|
|
24
24
|
return boundedList(value, maximum, (item) => item, {
|
|
25
25
|
refuse: refuseRecord,
|
|
26
26
|
overflow: 'node-limit',
|
|
@@ -29,7 +29,7 @@ export function list(value, maximum) {
|
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
31
|
export function addressOf(value) {
|
|
32
|
-
const logRunId =
|
|
32
|
+
const logRunId = recordName(recordOwn(value, 'logRunId')), drillPath = names(recordOwn(value, 'drillPath'), 16);
|
|
33
33
|
if (logRunId.length + drillPath.reduce((n, s) => n + s.length, 0) > 1536)
|
|
34
34
|
throw new RecordRefusal();
|
|
35
35
|
return Object.freeze({ logRunId, drillPath: Object.freeze(drillPath) });
|
|
@@ -60,7 +60,7 @@ export function dataCopy(value, budget) {
|
|
|
60
60
|
if (!Array.isArray(v) && p !== Object.prototype && p !== null)
|
|
61
61
|
throw new RecordRefusal();
|
|
62
62
|
if (Array.isArray(v)) {
|
|
63
|
-
const length =
|
|
63
|
+
const length = recordOwn(v, 'length');
|
|
64
64
|
if (typeof length !== 'number' || !Number.isSafeInteger(length) || length < 0)
|
|
65
65
|
throw new RecordRefusal();
|
|
66
66
|
spend(budget, length);
|
|
@@ -85,7 +85,7 @@ export function dataCopy(value, budget) {
|
|
|
85
85
|
for (const key of keys) {
|
|
86
86
|
if (key.length > 1024)
|
|
87
87
|
throw new RecordRefusal('node-limit');
|
|
88
|
-
const v =
|
|
88
|
+
const v = recordOwn(from, key);
|
|
89
89
|
check(v);
|
|
90
90
|
let copy = v;
|
|
91
91
|
if (!leaf(v)) {
|
|
@@ -101,16 +101,16 @@ export function dataCopy(value, budget) {
|
|
|
101
101
|
return root;
|
|
102
102
|
}
|
|
103
103
|
export function bundleOf(value, idx, budget) {
|
|
104
|
-
const runtimeStageId =
|
|
105
|
-
const trace =
|
|
106
|
-
const t =
|
|
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
107
|
if (!['set', 'merge', 'append', 'delete'].includes(verb))
|
|
108
108
|
throw new RecordRefusal();
|
|
109
|
-
const readKeys =
|
|
109
|
+
const readKeys = recordOwn(t, 'readKeys');
|
|
110
110
|
return { path, verb: verb, ...(readKeys === undefined ? {} : { readKeys: names(readKeys, 128, true) }) };
|
|
111
111
|
});
|
|
112
112
|
spend(budget, trace.length + trace.reduce((n, t) => n + (t.readKeys?.length ?? 0), 0) + 1);
|
|
113
|
-
const overwrite = dataCopy(
|
|
113
|
+
const overwrite = dataCopy(recordOwn(value, 'overwrite'), budget), updates = dataCopy(recordOwn(value, 'updates'), budget);
|
|
114
114
|
if (!overwrite ||
|
|
115
115
|
typeof overwrite !== 'object' ||
|
|
116
116
|
Array.isArray(overwrite) ||
|
|
@@ -118,9 +118,9 @@ export function bundleOf(value, idx, budget) {
|
|
|
118
118
|
typeof updates !== 'object' ||
|
|
119
119
|
Array.isArray(updates))
|
|
120
120
|
throw new RecordRefusal();
|
|
121
|
-
const redactedPaths = names(
|
|
122
|
-
const untracked =
|
|
123
|
-
spend(budget, redactedPaths.length + (untracked === undefined ? 0 :
|
|
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
124
|
return {
|
|
125
125
|
idx,
|
|
126
126
|
stage: stageId,
|
|
@@ -166,7 +166,7 @@ export function mapOf(value, limit, read, budget) {
|
|
|
166
166
|
throw new RecordRefusal('node-limit');
|
|
167
167
|
if (budget)
|
|
168
168
|
spend(budget, keys.length);
|
|
169
|
-
return new Map(keys.map((k) => [
|
|
169
|
+
return new Map(keys.map((k) => [recordName(k), read(recordOwn(value, k))]));
|
|
170
170
|
}
|
|
171
171
|
/** Observe native promises by intrinsic without assimilating a data object's then accessor. */
|
|
172
172
|
export function projectLoaded(work, project) {
|