foottrace 1.0.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +137 -4
  2. package/dist/cause.js +14 -0
  3. package/dist/esm/cause.d.ts +8 -0
  4. package/dist/esm/cause.js +5 -0
  5. package/dist/esm/index.d.ts +7 -2
  6. package/dist/esm/index.js +2 -1
  7. package/dist/esm/lib/cause/admission.d.ts +27 -0
  8. package/dist/esm/lib/cause/admission.js +59 -0
  9. package/dist/esm/lib/cause/explain.d.ts +5 -0
  10. package/dist/esm/lib/cause/explain.js +114 -0
  11. package/dist/esm/lib/cause/ranking.d.ts +6 -0
  12. package/dist/esm/lib/cause/ranking.js +10 -0
  13. package/dist/esm/lib/cause/recordAdapter.d.ts +9 -0
  14. package/dist/esm/lib/cause/recordAdapter.js +49 -0
  15. package/dist/esm/lib/cause/recordChildren.d.ts +4 -0
  16. package/dist/esm/lib/cause/recordChildren.js +79 -0
  17. package/dist/esm/lib/cause/recordControl.d.ts +13 -0
  18. package/dist/esm/lib/cause/recordControl.js +66 -0
  19. package/dist/esm/lib/cause/recordCoverage.d.ts +24 -0
  20. package/dist/esm/lib/cause/recordCoverage.js +87 -0
  21. package/dist/esm/lib/cause/recordIdentity.d.ts +9 -0
  22. package/dist/esm/lib/cause/recordIdentity.js +67 -0
  23. package/dist/esm/lib/cause/recordLoading.d.ts +7 -0
  24. package/dist/esm/lib/cause/recordLoading.js +116 -0
  25. package/dist/esm/lib/cause/recordParents.d.ts +4 -0
  26. package/dist/esm/lib/cause/recordParents.js +86 -0
  27. package/dist/esm/lib/cause/recordScope.d.ts +8 -0
  28. package/dist/esm/lib/cause/recordScope.js +60 -0
  29. package/dist/esm/lib/cause/recordTypes.d.ts +67 -0
  30. package/dist/esm/lib/cause/recordTypes.js +1 -0
  31. package/dist/esm/lib/cause/recordValidation.d.ts +33 -0
  32. package/dist/esm/lib/cause/recordValidation.js +198 -0
  33. package/dist/esm/lib/cause/session.d.ts +30 -0
  34. package/dist/esm/lib/cause/session.js +161 -0
  35. package/dist/esm/lib/cause/storedRecordAdapter.d.ts +11 -0
  36. package/dist/esm/lib/cause/storedRecordAdapter.js +57 -0
  37. package/dist/esm/lib/cause/strategies.d.ts +3 -0
  38. package/dist/esm/lib/cause/strategies.js +21 -0
  39. package/dist/esm/lib/cause/types.d.ts +90 -0
  40. package/dist/esm/lib/cause/types.js +1 -0
  41. package/dist/esm/lib/cause/validation.d.ts +24 -0
  42. package/dist/esm/lib/cause/validation.js +134 -0
  43. package/dist/esm/lib/cause/walk.d.ts +12 -0
  44. package/dist/esm/lib/cause/walk.js +174 -0
  45. package/dist/esm/lib/ids/attemptId.d.ts +63 -0
  46. package/dist/esm/lib/ids/attemptId.js +91 -0
  47. package/dist/esm/lib/memory/EventLog.d.ts +89 -3
  48. package/dist/esm/lib/memory/EventLog.js +242 -3
  49. package/dist/esm/lib/memory/honesty.d.ts +6 -4
  50. package/dist/esm/lib/memory/honesty.js +7 -4
  51. package/dist/esm/lib/memory/sink.d.ts +148 -0
  52. package/dist/esm/lib/memory/sink.js +92 -0
  53. package/dist/esm/lib/memory/types.d.ts +4 -1
  54. package/dist/esm/lib/memory/types.js +1 -1
  55. package/dist/esm/lib/time-travel/bundles.d.ts +2 -0
  56. package/dist/esm/lib/time-travel/bundles.js +16 -0
  57. package/dist/esm/lib/time-travel/commitStops.js +21 -2
  58. package/dist/esm/lib/time-travel/index.d.ts +2 -0
  59. package/dist/esm/lib/time-travel/index.js +1 -0
  60. package/dist/esm/lib/time-travel/storedRecord.d.ts +195 -0
  61. package/dist/esm/lib/time-travel/storedRecord.js +494 -0
  62. package/dist/esm/lib/time-travel/timeTravel.js +11 -0
  63. package/dist/esm/lib/time-travel/types.d.ts +4 -1
  64. package/dist/esm/write.d.ts +7 -0
  65. package/dist/esm/write.js +6 -0
  66. package/dist/index.js +6 -2
  67. package/dist/lib/cause/admission.js +67 -0
  68. package/dist/lib/cause/explain.js +119 -0
  69. package/dist/lib/cause/ranking.js +14 -0
  70. package/dist/lib/cause/recordAdapter.js +55 -0
  71. package/dist/lib/cause/recordChildren.js +83 -0
  72. package/dist/lib/cause/recordControl.js +74 -0
  73. package/dist/lib/cause/recordCoverage.js +99 -0
  74. package/dist/lib/cause/recordIdentity.js +73 -0
  75. package/dist/lib/cause/recordLoading.js +121 -0
  76. package/dist/lib/cause/recordParents.js +90 -0
  77. package/dist/lib/cause/recordScope.js +66 -0
  78. package/dist/lib/cause/recordTypes.js +2 -0
  79. package/dist/lib/cause/recordValidation.js +213 -0
  80. package/dist/lib/cause/session.js +165 -0
  81. package/dist/lib/cause/storedRecordAdapter.js +61 -0
  82. package/dist/lib/cause/strategies.js +26 -0
  83. package/dist/lib/cause/types.js +2 -0
  84. package/dist/lib/cause/validation.js +150 -0
  85. package/dist/lib/cause/walk.js +180 -0
  86. package/dist/lib/ids/attemptId.js +96 -0
  87. package/dist/lib/memory/EventLog.js +242 -3
  88. package/dist/lib/memory/honesty.js +7 -4
  89. package/dist/lib/memory/sink.js +98 -0
  90. package/dist/lib/memory/types.js +1 -1
  91. package/dist/lib/time-travel/bundles.js +18 -1
  92. package/dist/lib/time-travel/commitStops.js +21 -2
  93. package/dist/lib/time-travel/index.js +3 -1
  94. package/dist/lib/time-travel/storedRecord.js +498 -0
  95. package/dist/lib/time-travel/timeTravel.js +11 -0
  96. package/dist/types/cause.d.ts +8 -0
  97. package/dist/types/index.d.ts +7 -2
  98. package/dist/types/lib/cause/admission.d.ts +27 -0
  99. package/dist/types/lib/cause/explain.d.ts +5 -0
  100. package/dist/types/lib/cause/ranking.d.ts +6 -0
  101. package/dist/types/lib/cause/recordAdapter.d.ts +9 -0
  102. package/dist/types/lib/cause/recordChildren.d.ts +4 -0
  103. package/dist/types/lib/cause/recordControl.d.ts +13 -0
  104. package/dist/types/lib/cause/recordCoverage.d.ts +24 -0
  105. package/dist/types/lib/cause/recordIdentity.d.ts +9 -0
  106. package/dist/types/lib/cause/recordLoading.d.ts +7 -0
  107. package/dist/types/lib/cause/recordParents.d.ts +4 -0
  108. package/dist/types/lib/cause/recordScope.d.ts +8 -0
  109. package/dist/types/lib/cause/recordTypes.d.ts +67 -0
  110. package/dist/types/lib/cause/recordValidation.d.ts +33 -0
  111. package/dist/types/lib/cause/session.d.ts +30 -0
  112. package/dist/types/lib/cause/storedRecordAdapter.d.ts +11 -0
  113. package/dist/types/lib/cause/strategies.d.ts +3 -0
  114. package/dist/types/lib/cause/types.d.ts +90 -0
  115. package/dist/types/lib/cause/validation.d.ts +24 -0
  116. package/dist/types/lib/cause/walk.d.ts +12 -0
  117. package/dist/types/lib/ids/attemptId.d.ts +63 -0
  118. package/dist/types/lib/memory/EventLog.d.ts +89 -3
  119. package/dist/types/lib/memory/honesty.d.ts +6 -4
  120. package/dist/types/lib/memory/sink.d.ts +148 -0
  121. package/dist/types/lib/memory/types.d.ts +4 -1
  122. package/dist/types/lib/time-travel/bundles.d.ts +2 -0
  123. package/dist/types/lib/time-travel/index.d.ts +2 -0
  124. package/dist/types/lib/time-travel/storedRecord.d.ts +195 -0
  125. package/dist/types/lib/time-travel/types.d.ts +4 -1
  126. package/dist/types/write.d.ts +7 -0
  127. package/dist/write.js +8 -1
  128. 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
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * attemptId — the address of one attempt under a stage execution.
3
+ *
4
+ * Format:
5
+ * <runtimeStageId>@<n> attempt n of the stage execution 'charge#4@2'
6
+ * <runtimeStageId>@<n>.<k>@<m> attempt m of the k-th call made 'charge#4@2.1@3'
7
+ * during stage attempt n
8
+ *
9
+ * Every number after the runtimeStageId is a positive integer, counted from 1: attempt 1 is the first
10
+ * try, call 1 the first call the stage attempt made. Written without leading zeros, so one attempt has
11
+ * one spelling and an id is a safe join key.
12
+ *
13
+ * PARSED FROM THE RIGHT. A runtimeStageId always ends in `#<execution index>`, and the suffix grammar
14
+ * holds no `#`, so the LAST `#` of an attempt id is its execution delimiter whatever the stage id
15
+ * contains — `@`, `.` and `~` stay legal in stage ids, and no byte is newly reserved (the id doors'
16
+ * refusal, FootPrint's `refuseReservedId`, is untouched). The delimiter is read from the grammar's one
17
+ * owner, `runtimeStageId.ts`.
18
+ *
19
+ * Imports nothing but the runtime-ID grammar: a leaf of the record, checked by the package fence.
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * buildAttemptId('charge#4', 2); // 'charge#4@2'
24
+ * buildAttemptId('charge#4', 2, { ordinal: 1, attempt: 3 }); // 'charge#4@2.1@3'
25
+ * parseAttemptId('sf/v1.2@x#7@3'); // { runtimeStageId: 'sf/v1.2@x#7', attempt: 3 }
26
+ * parseAttemptId('charge#4'); // undefined — a stage execution, not an attempt
27
+ * ```
28
+ */
29
+ import { type RuntimeStageId } from './runtimeStageId.js';
30
+ declare const ATTEMPT_ID: unique symbol;
31
+ /** A `<runtimeStageId>@<n>[.<k>@<m>]` string the grammar produced. Assignable to `string`. */
32
+ export type AttemptId = string & {
33
+ readonly [ATTEMPT_ID]: true;
34
+ };
35
+ /**
36
+ * Build the id of one attempt.
37
+ *
38
+ * @param runtimeStageId The stage execution the attempt belongs to (`[subflowPath/]stageId#index`).
39
+ * @param attempt The stage attempt, from 1.
40
+ * @param call For an attempt of a call made inside the stage attempt: the call's `ordinal` (1 for the
41
+ * first call that stage attempt made) and its own `attempt` (from 1).
42
+ * @throws TypeError when `runtimeStageId` does not end in `#<execution index>`, and RangeError when a
43
+ * count is not a positive integer — what `parseAttemptId` could not give back is never written.
44
+ */
45
+ export declare function buildAttemptId(runtimeStageId: string, attempt: number, call?: {
46
+ readonly ordinal: number;
47
+ readonly attempt: number;
48
+ }): AttemptId;
49
+ /**
50
+ * Parse an attempt id into its parts, or `undefined` when `id` is not one (a bare runtimeStageId, a
51
+ * count of 0, a leading zero, a missing part). Never throws.
52
+ *
53
+ * `parseAttemptId(buildAttemptId(...parts))` gives the parts back, whatever the stage id contains.
54
+ */
55
+ export declare function parseAttemptId(id: string): {
56
+ runtimeStageId: RuntimeStageId;
57
+ attempt: number;
58
+ call?: {
59
+ ordinal: number;
60
+ attempt: number;
61
+ };
62
+ } | undefined;
63
+ export {};
@@ -0,0 +1,91 @@
1
+ /**
2
+ * attemptId — the address of one attempt under a stage execution.
3
+ *
4
+ * Format:
5
+ * <runtimeStageId>@<n> attempt n of the stage execution 'charge#4@2'
6
+ * <runtimeStageId>@<n>.<k>@<m> attempt m of the k-th call made 'charge#4@2.1@3'
7
+ * during stage attempt n
8
+ *
9
+ * Every number after the runtimeStageId is a positive integer, counted from 1: attempt 1 is the first
10
+ * try, call 1 the first call the stage attempt made. Written without leading zeros, so one attempt has
11
+ * one spelling and an id is a safe join key.
12
+ *
13
+ * PARSED FROM THE RIGHT. A runtimeStageId always ends in `#<execution index>`, and the suffix grammar
14
+ * holds no `#`, so the LAST `#` of an attempt id is its execution delimiter whatever the stage id
15
+ * contains — `@`, `.` and `~` stay legal in stage ids, and no byte is newly reserved (the id doors'
16
+ * refusal, FootPrint's `refuseReservedId`, is untouched). The delimiter is read from the grammar's one
17
+ * owner, `runtimeStageId.ts`.
18
+ *
19
+ * Imports nothing but the runtime-ID grammar: a leaf of the record, checked by the package fence.
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * buildAttemptId('charge#4', 2); // 'charge#4@2'
24
+ * buildAttemptId('charge#4', 2, { ordinal: 1, attempt: 3 }); // 'charge#4@2.1@3'
25
+ * parseAttemptId('sf/v1.2@x#7@3'); // { runtimeStageId: 'sf/v1.2@x#7', attempt: 3 }
26
+ * parseAttemptId('charge#4'); // undefined — a stage execution, not an attempt
27
+ * ```
28
+ */
29
+ import { EXECUTION_DELIMITER } from './runtimeStageId.js';
30
+ /**
31
+ * What follows the last `#`: the execution index, `@` and the attempt, then — for a call — `.`, the
32
+ * call's ordinal, `@` and the call's attempt. Counts are canonical positive integers.
33
+ */
34
+ const AFTER_EXECUTION_DELIMITER = /^(\d+)@([1-9]\d*)(?:\.([1-9]\d*)@([1-9]\d*))?$/;
35
+ const EXECUTION_INDEX = /^\d+$/;
36
+ /** True when `id` ends in `#<digits>` — the one shape every runtimeStageId has. */
37
+ function endsInExecutionIndex(id) {
38
+ const at = id.lastIndexOf(EXECUTION_DELIMITER);
39
+ return at !== -1 && EXECUTION_INDEX.test(id.slice(at + 1));
40
+ }
41
+ /** Refuse a count the parser could not give back: a positive safe integer, or a `RangeError`. */
42
+ function requireCount(name, value) {
43
+ if (!Number.isSafeInteger(value) || value < 1) {
44
+ throw new RangeError(`buildAttemptId: ${name} must be a positive integer (counted from 1), got ${String(value)}`);
45
+ }
46
+ }
47
+ /**
48
+ * Build the id of one attempt.
49
+ *
50
+ * @param runtimeStageId The stage execution the attempt belongs to (`[subflowPath/]stageId#index`).
51
+ * @param attempt The stage attempt, from 1.
52
+ * @param call For an attempt of a call made inside the stage attempt: the call's `ordinal` (1 for the
53
+ * first call that stage attempt made) and its own `attempt` (from 1).
54
+ * @throws TypeError when `runtimeStageId` does not end in `#<execution index>`, and RangeError when a
55
+ * count is not a positive integer — what `parseAttemptId` could not give back is never written.
56
+ */
57
+ export function buildAttemptId(runtimeStageId, attempt, call) {
58
+ if (typeof runtimeStageId !== 'string' || !endsInExecutionIndex(runtimeStageId)) {
59
+ throw new TypeError(`buildAttemptId: ${JSON.stringify(runtimeStageId)} is not a runtimeStageId (it must end in #<execution index>)`);
60
+ }
61
+ requireCount('attempt', attempt);
62
+ if (call === undefined)
63
+ return `${runtimeStageId}@${attempt}`;
64
+ requireCount('call ordinal', call.ordinal);
65
+ requireCount('call attempt', call.attempt);
66
+ return `${runtimeStageId}@${attempt}.${call.ordinal}@${call.attempt}`;
67
+ }
68
+ /**
69
+ * Parse an attempt id into its parts, or `undefined` when `id` is not one (a bare runtimeStageId, a
70
+ * count of 0, a leading zero, a missing part). Never throws.
71
+ *
72
+ * `parseAttemptId(buildAttemptId(...parts))` gives the parts back, whatever the stage id contains.
73
+ */
74
+ export function parseAttemptId(id) {
75
+ if (typeof id !== 'string')
76
+ return undefined;
77
+ const at = id.lastIndexOf(EXECUTION_DELIMITER);
78
+ if (at === -1)
79
+ return undefined;
80
+ const parts = AFTER_EXECUTION_DELIMITER.exec(id.slice(at + 1));
81
+ if (!parts)
82
+ return undefined;
83
+ const [, index, attempt, ordinal, callAttempt] = parts;
84
+ const counts = [attempt, ordinal, callAttempt].filter((n) => n !== undefined).map(Number);
85
+ if (!counts.every(Number.isSafeInteger))
86
+ return undefined;
87
+ const runtimeStageId = id.slice(0, at + 1 + index.length);
88
+ if (ordinal === undefined)
89
+ return { runtimeStageId, attempt: counts[0] };
90
+ return { runtimeStageId, attempt: counts[0], call: { ordinal: counts[1], attempt: counts[2] } };
91
+ }
@@ -24,8 +24,20 @@
24
24
  * contract for state) and a `SharedArrayBuffer` (every copy shares its memory). A frozen RegExp's
25
25
  * `lastIndex` is read-only, so a `/g` or `/y` regex read from a bundle throws when `exec` or
26
26
  * `replace` advance it.
27
+ *
28
+ * A SINK (1.1.0, optional). `new EventLog(base, { sink, address, meta? })` also hands every entry to a
29
+ * `RecordSink` AS IT LANDS (`sink.ts`): `opened` from the constructor (the base, as the sink's own
30
+ * copy), `commit` from `record` after the bundle is frozen and appended (the bundle as served),
31
+ * `note` from `note`, `closed` from `close`. The log's own record never depends on the sink: the
32
+ * bundle is in `steps` before the sink sees it, and nothing the sink does reaches the writer — a
33
+ * throw, a rejected promise or an entry the log refuses to send (a `note` whose data is not a plain
34
+ * object that survives `structuredClone`) is counted (`sinkFailures`) and reported to
35
+ * `sink.onFailure`, and the writer goes on; a `meta` that will not copy is lost on its own and its
36
+ * entry still goes. Without a sink, nothing here runs: the one-argument log writes the same bytes,
37
+ * and has the same own fields, it always did.
27
38
  */
28
39
  import type { EmitSourcePosition, LogAddress } from './eventPosition.js';
40
+ import { type LogOutcome, type Meta, type RecordSink } from './sink.js';
29
41
  import type { CommitBundle } from './types.js';
30
42
  export declare class EventLog {
31
43
  /** Base snapshot BEFORE the first stage mutates anything. */
@@ -34,7 +46,26 @@ export declare class EventLog {
34
46
  private steps;
35
47
  private sourceAddress?;
36
48
  private sourceAddressRetired;
37
- constructor(initialMemory: any);
49
+ /** Where entries go as they land; absent for the one-argument log. */
50
+ private readonly sink?;
51
+ /** Entries the sink did not take; set with the sink. */
52
+ private lostEntries?;
53
+ /** True while `onFailure` runs (until its promise settles): a loss it causes is counted, not reported again. */
54
+ private reporting?;
55
+ /**
56
+ * @param initialMemory The fold base — the state before the first commit (copied).
57
+ * @param options With a `sink`: every entry goes to it as it lands, under `address` — which also
58
+ * becomes this log's {@link address} — and `meta` rides the `opened` entry. The sink is told
59
+ * `opened` before the constructor returns.
60
+ * @throws TypeError when `options` names no sink, a malformed address, or a base that is not a plain
61
+ * object (the `opened` entry carries it) — a writer's mistake, refused at the door. Nothing the sink
62
+ * itself does is ever thrown.
63
+ */
64
+ constructor(initialMemory: any, options?: {
65
+ readonly sink: RecordSink;
66
+ readonly address: LogAddress;
67
+ readonly meta?: Meta;
68
+ });
38
69
  /**
39
70
  * The fold BASE — the state as it stood before the first commit.
40
71
  *
@@ -78,6 +109,53 @@ export declare class EventLog {
78
109
  * will no longer edit — its own object is the one frozen.
79
110
  */
80
111
  record(bundle: CommitBundle): void;
112
+ /**
113
+ * Note something beside the log, in the sink, as it happens — for evidence that must survive a run
114
+ * that never closes (an attempt that ended, before the next one starts). `kind` and `data` are the
115
+ * writer's: opaque to the record. Notes never enter the log's bundles, so the log's bytes are the
116
+ * same with or without them. Without a sink, nothing happens.
117
+ *
118
+ * Never throws: a `kind` that is not a string, `data` that is not a plain object surviving
119
+ * `structuredClone`, or a sink that fails is counted in {@link sinkFailures} and reported to
120
+ * `sink.onFailure`. The sink keeps its own copy of `data`.
121
+ */
122
+ note(kind: string, data: Meta): void;
123
+ /**
124
+ * Tell the sink this log ended — `outcome` and, for the reader's check that the stored log is
125
+ * whole, how many commits it holds now. `meta` rides the entry, copied. Without a sink, nothing
126
+ * happens. The log itself stays as it is: a later `record` still lands, and still reaches the sink
127
+ * (a reader then reads the log as not complete until it is closed again).
128
+ *
129
+ * Never throws: an unknown outcome, a `meta` that is not a plain object surviving `structuredClone`,
130
+ * or a sink that fails is counted in {@link sinkFailures} and reported to `sink.onFailure`.
131
+ */
132
+ close(outcome: LogOutcome, meta?: Meta): void;
133
+ /**
134
+ * How many entries the sink did not take: a sink method that threw or whose promise rejected, and an
135
+ * entry the log refused to send — plus each `meta` the log could not copy (that entry went without
136
+ * it). Counted as each happens (a rejection, when it settles); 0 without a sink. The stored record
137
+ * says the same from its side: a missing commit is a gap.
138
+ */
139
+ get sinkFailures(): number;
140
+ /**
141
+ * One entry to the sink. Nothing here reaches the writer: a refusal, a throw or a rejection is a lost
142
+ * entry; a `meta` that cannot be copied is lost on its own — counted and reported — and the entry goes
143
+ * without it.
144
+ */
145
+ private send;
146
+ /**
147
+ * Count the rejection of an adapter's promise once. A native promise is watched through the real
148
+ * `Promise.prototype.then` (an own `then` on it is not trusted); any other thenable is adopted into a
149
+ * fresh promise first, whose settling functions run once. Either way the rejection is handled.
150
+ */
151
+ private watch;
152
+ /**
153
+ * Count a lost entry and tell the sink's `onFailure`, if it listens; a report that fails is ignored.
154
+ * A loss caused while `onFailure` runs — until the promise it returns settles, when it returns one —
155
+ * (it wrote to this log again, through the same failing sink) is counted and not reported again: one
156
+ * report deep, never a runaway.
157
+ */
158
+ private lost;
81
159
  /**
82
160
  * Every recorded commit bundle, AS SERVED (9.44.2): a new array each call; a bundle freezing sealed
83
161
  * whole is the log's own (frozen) object, one holding a Date, Map, buffer … is a copy of its open
@@ -94,12 +172,20 @@ export declare class EventLog {
94
172
  recorded(): CommitBundle[];
95
173
  /** Number of recorded commits. */
96
174
  get length(): number;
97
- /** Immutable identity of this log, absent until the engine binds it. */
175
+ /**
176
+ * Immutable identity of this log: the sink's address when the log was built with one, otherwise
177
+ * absent until the engine binds it.
178
+ */
98
179
  get address(): LogAddress | undefined;
99
180
  /** @internal Bind once; resuming the same log must not rename its earlier positions. */
100
181
  bindAddress(logRunId: string, drillPath: readonly string[]): void;
101
182
  /** O(1) source-time capture. No counter, observer bookkeeping, or future commit prediction. */
102
183
  capturePosition(runId: string): EmitSourcePosition | undefined;
103
- /** Wipes history (useful for test resets). */
184
+ /**
185
+ * Wipes history (useful for test resets).
186
+ *
187
+ * @throws Error on a log built with a sink: what the sink stored cannot be wiped, and a second
188
+ * history under the same address would read back as one corrupted log. Start a fresh EventLog.
189
+ */
104
190
  clear(): void;
105
191
  }