foottrace 1.0.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -4
- package/dist/cause.js +14 -0
- package/dist/esm/cause.d.ts +8 -0
- package/dist/esm/cause.js +5 -0
- package/dist/esm/index.d.ts +7 -2
- package/dist/esm/index.js +2 -1
- package/dist/esm/lib/cause/admission.d.ts +27 -0
- package/dist/esm/lib/cause/admission.js +59 -0
- package/dist/esm/lib/cause/explain.d.ts +5 -0
- package/dist/esm/lib/cause/explain.js +114 -0
- package/dist/esm/lib/cause/ranking.d.ts +6 -0
- package/dist/esm/lib/cause/ranking.js +10 -0
- package/dist/esm/lib/cause/recordAdapter.d.ts +9 -0
- package/dist/esm/lib/cause/recordAdapter.js +49 -0
- package/dist/esm/lib/cause/recordChildren.d.ts +4 -0
- package/dist/esm/lib/cause/recordChildren.js +79 -0
- package/dist/esm/lib/cause/recordControl.d.ts +13 -0
- package/dist/esm/lib/cause/recordControl.js +66 -0
- package/dist/esm/lib/cause/recordCoverage.d.ts +24 -0
- package/dist/esm/lib/cause/recordCoverage.js +87 -0
- package/dist/esm/lib/cause/recordIdentity.d.ts +9 -0
- package/dist/esm/lib/cause/recordIdentity.js +67 -0
- package/dist/esm/lib/cause/recordLoading.d.ts +7 -0
- package/dist/esm/lib/cause/recordLoading.js +116 -0
- package/dist/esm/lib/cause/recordParents.d.ts +4 -0
- package/dist/esm/lib/cause/recordParents.js +86 -0
- package/dist/esm/lib/cause/recordScope.d.ts +8 -0
- package/dist/esm/lib/cause/recordScope.js +60 -0
- package/dist/esm/lib/cause/recordTypes.d.ts +67 -0
- package/dist/esm/lib/cause/recordTypes.js +1 -0
- package/dist/esm/lib/cause/recordValidation.d.ts +33 -0
- package/dist/esm/lib/cause/recordValidation.js +198 -0
- package/dist/esm/lib/cause/session.d.ts +30 -0
- package/dist/esm/lib/cause/session.js +161 -0
- package/dist/esm/lib/cause/storedRecordAdapter.d.ts +11 -0
- package/dist/esm/lib/cause/storedRecordAdapter.js +57 -0
- package/dist/esm/lib/cause/strategies.d.ts +3 -0
- package/dist/esm/lib/cause/strategies.js +21 -0
- package/dist/esm/lib/cause/types.d.ts +90 -0
- package/dist/esm/lib/cause/types.js +1 -0
- package/dist/esm/lib/cause/validation.d.ts +24 -0
- package/dist/esm/lib/cause/validation.js +134 -0
- package/dist/esm/lib/cause/walk.d.ts +12 -0
- package/dist/esm/lib/cause/walk.js +174 -0
- package/dist/esm/lib/ids/attemptId.d.ts +63 -0
- package/dist/esm/lib/ids/attemptId.js +91 -0
- package/dist/esm/lib/memory/EventLog.d.ts +89 -3
- package/dist/esm/lib/memory/EventLog.js +242 -3
- package/dist/esm/lib/memory/honesty.d.ts +6 -4
- package/dist/esm/lib/memory/honesty.js +7 -4
- package/dist/esm/lib/memory/sink.d.ts +148 -0
- package/dist/esm/lib/memory/sink.js +92 -0
- package/dist/esm/lib/memory/types.d.ts +4 -1
- package/dist/esm/lib/memory/types.js +1 -1
- package/dist/esm/lib/time-travel/bundles.d.ts +2 -0
- package/dist/esm/lib/time-travel/bundles.js +16 -0
- package/dist/esm/lib/time-travel/commitStops.js +21 -2
- package/dist/esm/lib/time-travel/index.d.ts +2 -0
- package/dist/esm/lib/time-travel/index.js +1 -0
- package/dist/esm/lib/time-travel/storedRecord.d.ts +195 -0
- package/dist/esm/lib/time-travel/storedRecord.js +494 -0
- package/dist/esm/lib/time-travel/timeTravel.js +11 -0
- package/dist/esm/lib/time-travel/types.d.ts +4 -1
- package/dist/esm/write.d.ts +7 -0
- package/dist/esm/write.js +6 -0
- package/dist/index.js +6 -2
- package/dist/lib/cause/admission.js +67 -0
- package/dist/lib/cause/explain.js +119 -0
- package/dist/lib/cause/ranking.js +14 -0
- package/dist/lib/cause/recordAdapter.js +55 -0
- package/dist/lib/cause/recordChildren.js +83 -0
- package/dist/lib/cause/recordControl.js +74 -0
- package/dist/lib/cause/recordCoverage.js +99 -0
- package/dist/lib/cause/recordIdentity.js +73 -0
- package/dist/lib/cause/recordLoading.js +121 -0
- package/dist/lib/cause/recordParents.js +90 -0
- package/dist/lib/cause/recordScope.js +66 -0
- package/dist/lib/cause/recordTypes.js +2 -0
- package/dist/lib/cause/recordValidation.js +213 -0
- package/dist/lib/cause/session.js +165 -0
- package/dist/lib/cause/storedRecordAdapter.js +61 -0
- package/dist/lib/cause/strategies.js +26 -0
- package/dist/lib/cause/types.js +2 -0
- package/dist/lib/cause/validation.js +150 -0
- package/dist/lib/cause/walk.js +180 -0
- package/dist/lib/ids/attemptId.js +96 -0
- package/dist/lib/memory/EventLog.js +242 -3
- package/dist/lib/memory/honesty.js +7 -4
- package/dist/lib/memory/sink.js +98 -0
- package/dist/lib/memory/types.js +1 -1
- package/dist/lib/time-travel/bundles.js +18 -1
- package/dist/lib/time-travel/commitStops.js +21 -2
- package/dist/lib/time-travel/index.js +3 -1
- package/dist/lib/time-travel/storedRecord.js +498 -0
- package/dist/lib/time-travel/timeTravel.js +11 -0
- package/dist/types/cause.d.ts +8 -0
- package/dist/types/index.d.ts +7 -2
- package/dist/types/lib/cause/admission.d.ts +27 -0
- package/dist/types/lib/cause/explain.d.ts +5 -0
- package/dist/types/lib/cause/ranking.d.ts +6 -0
- package/dist/types/lib/cause/recordAdapter.d.ts +9 -0
- package/dist/types/lib/cause/recordChildren.d.ts +4 -0
- package/dist/types/lib/cause/recordControl.d.ts +13 -0
- package/dist/types/lib/cause/recordCoverage.d.ts +24 -0
- package/dist/types/lib/cause/recordIdentity.d.ts +9 -0
- package/dist/types/lib/cause/recordLoading.d.ts +7 -0
- package/dist/types/lib/cause/recordParents.d.ts +4 -0
- package/dist/types/lib/cause/recordScope.d.ts +8 -0
- package/dist/types/lib/cause/recordTypes.d.ts +67 -0
- package/dist/types/lib/cause/recordValidation.d.ts +33 -0
- package/dist/types/lib/cause/session.d.ts +30 -0
- package/dist/types/lib/cause/storedRecordAdapter.d.ts +11 -0
- package/dist/types/lib/cause/strategies.d.ts +3 -0
- package/dist/types/lib/cause/types.d.ts +90 -0
- package/dist/types/lib/cause/validation.d.ts +24 -0
- package/dist/types/lib/cause/walk.d.ts +12 -0
- package/dist/types/lib/ids/attemptId.d.ts +63 -0
- package/dist/types/lib/memory/EventLog.d.ts +89 -3
- package/dist/types/lib/memory/honesty.d.ts +6 -4
- package/dist/types/lib/memory/sink.d.ts +148 -0
- package/dist/types/lib/memory/types.d.ts +4 -1
- package/dist/types/lib/time-travel/bundles.d.ts +2 -0
- package/dist/types/lib/time-travel/index.d.ts +2 -0
- package/dist/types/lib/time-travel/storedRecord.d.ts +195 -0
- package/dist/types/lib/time-travel/types.d.ts +4 -1
- package/dist/types/write.d.ts +7 -0
- package/dist/write.js +8 -1
- package/package.json +67 -13
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sink.ts — the record's sink port: where a log's entries go AS THEY LAND, so a run that never ends
|
|
3
|
+
* still leaves its record behind.
|
|
4
|
+
*
|
|
5
|
+
* A log that is given a sink (`new EventLog(base, { sink, address })`) hands it four kinds of entry,
|
|
6
|
+
* each stamped with the log's address and the time it was written (`at`, epoch ms):
|
|
7
|
+
*
|
|
8
|
+
* - `opened` — once, from the constructor: the fold base (`initialState`, the sink's own copy);
|
|
9
|
+
* - `commit` — per `record()`, AFTER the bundle is frozen: the log's own bundle, as served;
|
|
10
|
+
* - `note` — per `note(kind, data)`: something the writer wants kept beside the log, as it happens;
|
|
11
|
+
* - `closed` — per `close(outcome)`: how the log ended, and how many commits it held then (`commits`).
|
|
12
|
+
*
|
|
13
|
+
* These are RECORD facts. What the writer means by them rides `meta` (on `opened` / `closed`) and a
|
|
14
|
+
* note's `kind` and `data`: opaque here — stored and handed back, never read or interpreted. Nothing in
|
|
15
|
+
* this file names an engine.
|
|
16
|
+
*
|
|
17
|
+
* THE STORED FORM. A sink stores each entry as a {@link RecordEntry}: the method's entry plus the
|
|
18
|
+
* method's name as `kind`. A note's own kind cannot also be called `kind`, so it is stored as `note`.
|
|
19
|
+
* `memorySink` is the reference adapter; `readStoredRecord` (`foottrace`) reads the entries back.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* import { EventLog, memorySink } from 'foottrace/write';
|
|
24
|
+
* import { readStoredRecord } from 'foottrace';
|
|
25
|
+
*
|
|
26
|
+
* const sink = memorySink();
|
|
27
|
+
* const log = new EventLog({ count: 0 }, { sink, address: { logRunId: 'run-1', drillPath: [] } });
|
|
28
|
+
* log.note('attempt', { attempt: 1, outcome: 'failed' });
|
|
29
|
+
* // … the process dies here: no `closed` entry …
|
|
30
|
+
* readStoredRecord(sink.entries).root('run-1')?.complete; // false — and its note is still there
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
import type { LogAddress } from './eventPosition.js';
|
|
34
|
+
import type { CommitBundle } from './types.js';
|
|
35
|
+
/**
|
|
36
|
+
* What a writer puts beside the record — on a log's `opened` / `closed` entry, or as a note's data.
|
|
37
|
+
* A plain object that survives `structuredClone`; opaque to foottrace: stored and returned, never read.
|
|
38
|
+
*/
|
|
39
|
+
export type Meta = Readonly<Record<string, unknown>>;
|
|
40
|
+
/** How a log's writer said it ended. A pause ends this log; it is not a failure. */
|
|
41
|
+
export type LogOutcome = 'ok' | 'failed' | 'paused';
|
|
42
|
+
/** The outcomes a `closed` entry may carry, in one place for the writer's and the reader's checks. */
|
|
43
|
+
export declare const LOG_OUTCOMES: readonly LogOutcome[];
|
|
44
|
+
/**
|
|
45
|
+
* One stored entry: what a sink keeps per call, discriminated by `kind` (the method that received it).
|
|
46
|
+
* A note's own kind is stored as `note`.
|
|
47
|
+
*/
|
|
48
|
+
export type RecordEntry = {
|
|
49
|
+
readonly kind: 'opened';
|
|
50
|
+
readonly address: LogAddress;
|
|
51
|
+
readonly at: number;
|
|
52
|
+
/** The fold base — the state before the log's first commit. */
|
|
53
|
+
readonly initialState: Record<string, unknown>;
|
|
54
|
+
readonly meta?: Meta;
|
|
55
|
+
} | {
|
|
56
|
+
readonly kind: 'commit';
|
|
57
|
+
readonly address: LogAddress;
|
|
58
|
+
readonly at: number;
|
|
59
|
+
/** The log's own bundle, frozen, its position stamped as `idx`. */
|
|
60
|
+
readonly bundle: CommitBundle;
|
|
61
|
+
} | {
|
|
62
|
+
readonly kind: 'note';
|
|
63
|
+
readonly address: LogAddress;
|
|
64
|
+
readonly at: number;
|
|
65
|
+
/** The note's own kind, as the writer named it (`note('attempt', …)` → `'attempt'`). */
|
|
66
|
+
readonly note: string;
|
|
67
|
+
readonly data: Meta;
|
|
68
|
+
} | {
|
|
69
|
+
readonly kind: 'closed';
|
|
70
|
+
readonly address: LogAddress;
|
|
71
|
+
readonly at: number;
|
|
72
|
+
readonly outcome: LogOutcome;
|
|
73
|
+
/** The log's length when it closed — how many commits the stored log must hold to be whole. */
|
|
74
|
+
readonly commits: number;
|
|
75
|
+
readonly meta?: Meta;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Where a log's entries go as they land — the port an adapter implements (memory, a file, S3, a table).
|
|
79
|
+
*
|
|
80
|
+
* Every method is called synchronously from the writer; an adapter that writes somewhere slow queues
|
|
81
|
+
* the entry and drains in `flush`. A method may throw, or return a promise that rejects: the log
|
|
82
|
+
* counts it (`EventLog · sinkFailures`), reports it to `onFailure`, and goes on — a failing sink never
|
|
83
|
+
* stops the writer. The entries an adapter is handed are its to keep: the bundle is frozen and shared
|
|
84
|
+
* with the log, everything else is the adapter's own copy.
|
|
85
|
+
*/
|
|
86
|
+
export interface RecordSink {
|
|
87
|
+
/** A log opened at `address` with its fold base. Called once, from the log's constructor. */
|
|
88
|
+
opened(entry: {
|
|
89
|
+
readonly address: LogAddress;
|
|
90
|
+
readonly at: number;
|
|
91
|
+
readonly initialState: Record<string, unknown>;
|
|
92
|
+
readonly meta?: Meta;
|
|
93
|
+
}): void;
|
|
94
|
+
/** A bundle landed in the log at `address` (its position is `bundle.idx`). */
|
|
95
|
+
commit(entry: {
|
|
96
|
+
readonly address: LogAddress;
|
|
97
|
+
readonly at: number;
|
|
98
|
+
readonly bundle: CommitBundle;
|
|
99
|
+
}): void;
|
|
100
|
+
/** The writer noted something beside the log — `kind` and `data` are opaque. */
|
|
101
|
+
note(entry: {
|
|
102
|
+
readonly address: LogAddress;
|
|
103
|
+
readonly at: number;
|
|
104
|
+
readonly kind: string;
|
|
105
|
+
readonly data: Meta;
|
|
106
|
+
}): void;
|
|
107
|
+
/** The log at `address` closed with `outcome`, holding `commits` bundles. */
|
|
108
|
+
closed(entry: {
|
|
109
|
+
readonly address: LogAddress;
|
|
110
|
+
readonly at: number;
|
|
111
|
+
readonly outcome: LogOutcome;
|
|
112
|
+
readonly commits: number;
|
|
113
|
+
readonly meta?: Meta;
|
|
114
|
+
}): void;
|
|
115
|
+
/** Drain whatever the adapter queued. The log never calls it; whoever owns the sink does. */
|
|
116
|
+
flush?(): Promise<void>;
|
|
117
|
+
/** Told of every entry this sink did not take — the entry and what went wrong. A throw here is ignored. */
|
|
118
|
+
onFailure?(failure: {
|
|
119
|
+
readonly entry: RecordEntry;
|
|
120
|
+
readonly error: unknown;
|
|
121
|
+
}): void;
|
|
122
|
+
}
|
|
123
|
+
/** A plain object — not null, not an array, not a primitive. */
|
|
124
|
+
export declare function isEntryObject(value: unknown): value is Record<string, unknown>;
|
|
125
|
+
/**
|
|
126
|
+
* Hand `entry` to the sink's method for its kind, as that method's entry; returns what the method
|
|
127
|
+
* returned (a promise from an async adapter). Throws whatever the method throws — the log catches.
|
|
128
|
+
*/
|
|
129
|
+
export declare function sendEntry(sink: RecordSink, entry: RecordEntry): unknown;
|
|
130
|
+
/**
|
|
131
|
+
* A sink that keeps every entry in memory, in arrival order — the reference adapter: what any adapter
|
|
132
|
+
* must give back for `readStoredRecord` to read the same record. `entries` is a new array on each read.
|
|
133
|
+
* What it keeps is frozen as far as freezing reaches — the base, `meta` and a note's `data` are the
|
|
134
|
+
* copies a log hands it, and the bundle is already frozen — so no reader can rewrite its containers. A
|
|
135
|
+
* Date's time, a Map's or Set's entries and a buffer's bytes are not sealed by freezing (`deepFreeze`'s
|
|
136
|
+
* stated limit). A caller that hands it objects directly hands them over: they are frozen in place.
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```typescript
|
|
140
|
+
* const sink = memorySink();
|
|
141
|
+
* const log = new EventLog({}, { sink, address: { logRunId: 'run-1', drillPath: [] } });
|
|
142
|
+
* log.close('ok');
|
|
143
|
+
* sink.entries.map((e) => e.kind); // ['opened', 'closed']
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
export declare function memorySink(): RecordSink & {
|
|
147
|
+
readonly entries: readonly RecordEntry[];
|
|
148
|
+
};
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* types.ts — the record's types: a bundle, its rows, its patches, its encoding, and the
|
|
3
3
|
* execution tree as the record's readers read it.
|
|
4
4
|
*
|
|
5
|
-
* Imports
|
|
5
|
+
* Imports only the record's leaf attention type. The record names nothing outside itself: the engine's frame
|
|
6
6
|
* types — the stage snapshot, flow messages and retention dials — remain in FootPrint's
|
|
7
7
|
* `frameTypes.ts`. The closed record graph is enforced by scripts/check-layering.mjs.
|
|
8
8
|
*/
|
|
9
|
+
import type { Attention } from '../cause/types.js';
|
|
9
10
|
/** A flat key-value bag representing a state patch (overwrite or merge). */
|
|
10
11
|
export interface MemoryPatch {
|
|
11
12
|
[key: string]: any;
|
|
@@ -194,6 +195,8 @@ export type CommitValuesMode = 'full' | 'delta';
|
|
|
194
195
|
* ```
|
|
195
196
|
*/
|
|
196
197
|
export interface ExecutionTree {
|
|
198
|
+
/** Optional recorded business attention; absence does not assert health. */
|
|
199
|
+
readonly attention?: readonly Attention[];
|
|
197
200
|
/** The stage's id (prefixed inside a subflow). `commitStops` reads the root's to place a subflow's seed. */
|
|
198
201
|
readonly id?: string;
|
|
199
202
|
/** `[subflowPath/]stageId#executionIndex` — what a reader keys on. A node without one is not a stage. */
|
|
@@ -19,6 +19,8 @@ import type { CommitBundle, ExecutionTree } from '../memory/types.js';
|
|
|
19
19
|
import type { LogGap } from './types.js';
|
|
20
20
|
/** A plain object — not null, not an array, not a primitive. */
|
|
21
21
|
export declare function isPlainish(value: unknown): value is Record<string, unknown>;
|
|
22
|
+
/** A row every reader refuses with `reason` — a gap that keeps its index. */
|
|
23
|
+
export declare function absentRow(reason: string): object;
|
|
22
24
|
/**
|
|
23
25
|
* Read a source's `executionTree` as an {@link ExecutionTree}, or as absent.
|
|
24
26
|
*
|
|
@@ -17,6 +17,8 @@ export { filterStops, splitAxis } from './axis.js';
|
|
|
17
17
|
export { isCommitBundle } from './bundles.js';
|
|
18
18
|
export { commitStops, commitStopsStrategy } from './commitStops.js';
|
|
19
19
|
export { stateAt } from './stateAt.js';
|
|
20
|
+
export type { StoredLog, StoredNote, StoredRecord, StoredRecordLimits } from './storedRecord.js';
|
|
21
|
+
export { readStoredRecord } from './storedRecord.js';
|
|
20
22
|
export { tagStops } from './tagStops.js';
|
|
21
23
|
export { timeTravel } from './timeTravel.js';
|
|
22
24
|
export type { AxisRefusal, AxisSplit, BookendedAxis, FoldBasis, FoldedState, FoldSource, LogGap, Mark, Move, MoveRefusal, Stop, StopKind, TimeTravel, TimeTravelOptions, TimeTravelSource, TimeTravelStrategy, } from './types.js';
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* storedRecord.ts — reading back what a sink stored (1.1.0).
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS FILE EXISTS. A log that writes to a `RecordSink` (`foottrace/write`) leaves its record in
|
|
5
|
+
* storage AS IT LANDS: an `opened` entry with the fold base, one `commit` entry per bundle, `note`
|
|
6
|
+
* entries, and a `closed` entry with how the log ended. A run that is killed never writes its
|
|
7
|
+
* `closed` entry. This reader turns stored entries back into logs a person or a model can read —
|
|
8
|
+
* and says, before any fold, which of them never finished (`complete: false`) and what each one is
|
|
9
|
+
* missing.
|
|
10
|
+
*
|
|
11
|
+
* WHAT IT READS. Entries come back from storage unvalidated (a parsed file, a table scan) — untrusted
|
|
12
|
+
* input — so it takes `Iterable<unknown>` and checks each one where it is read: the 9.18.0 law for
|
|
13
|
+
* stored recordings, no cast at the seam. An entry it does not read into a log is never dropped in
|
|
14
|
+
* silence: it is listed in `unread` with its position and the reason.
|
|
15
|
+
*
|
|
16
|
+
* THE LAWS IT KEEPS.
|
|
17
|
+
* 1. Grouped by address. Every entry names its log (`logRunId` + `drillPath`); entries of many
|
|
18
|
+
* logs may arrive interleaved, and each log keeps its own entries in arrival order.
|
|
19
|
+
* 2. A commit sits at its own index. `EventLog · record` stamps `bundle.idx` before the sink sees
|
|
20
|
+
* the bundle, so a commit lost on the way (a sink that failed once — the writer goes on) leaves a
|
|
21
|
+
* GAP at its index instead of shifting every commit after it; so does a bundle that is not a
|
|
22
|
+
* readable bundle. Every gap keeps its index, and the fold reports it (`skipped`) with the same
|
|
23
|
+
* reason the stored log gives (`gaps`). One place, one entry: the first READABLE commit entry
|
|
24
|
+
* for an index stands (a torn first delivery yields to a readable redelivery), the first
|
|
25
|
+
* `opened` stands, and every other entry for a place already held — a redelivery or a conflict,
|
|
26
|
+
* the very same object included — is listed in `unread`. Payloads are never compared, so no
|
|
27
|
+
* stored value is walked.
|
|
28
|
+
* 3. Complete means the writer finished — and the reader read it all. A log is `complete` when a
|
|
29
|
+
* `closed` entry covers every stored commit (`closed.commits` is past the last index); the
|
|
30
|
+
* covering close with the largest `commits` gives the `outcome` and the length, so no loss an
|
|
31
|
+
* earlier close showed can hide behind a later one. A log that was closed and then went on (a
|
|
32
|
+
* later commit) is not complete until it is closed again. A closed log whose store lost commits
|
|
33
|
+
* is complete AND has gaps (`closed.commits` makes even a lost LAST commit a gap). A log a limit
|
|
34
|
+
* cut, and every log once reading stopped at the `entries` limit, is not complete: what the
|
|
35
|
+
* reader did not read could have gone on.
|
|
36
|
+
* 4. Opaque stays opaque. `meta` and a note's `kind` and `data` are handed back as stored, never
|
|
37
|
+
* read or merged.
|
|
38
|
+
* 5. Read, never touched. Nothing here copies, freezes or edits the entries it is given — not the
|
|
39
|
+
* base, not `meta`: the stored log hands back their own values.
|
|
40
|
+
* 6. Bounded work (see {@link StoredRecordLimits}). Every walk is a loop over the input, never a
|
|
41
|
+
* recursion, and never through an input's own iterator except the input itself; the work per
|
|
42
|
+
* entry is bounded by the address limits; the rows a log holds are its stored commits plus gaps
|
|
43
|
+
* from ONE budget for the whole record, spent on the logs that need the fewest gaps first (so a
|
|
44
|
+
* corrupt log asking for the most is the one cut); and reading stops after `entries` entries. Whatever a
|
|
45
|
+
* limit stops is listed in `unread` by name — never a hang, never a silent cut.
|
|
46
|
+
*
|
|
47
|
+
* Entries are data. An entry whose fields throw while it is checked (a getter, a revoked proxy) is
|
|
48
|
+
* listed in `unread`, and what the reader computes with — the kind, a commit's index and bundle, a
|
|
49
|
+
* close's length — is taken once, as checked. An in-memory accessor that changes or throws on a later
|
|
50
|
+
* read is out of contract, as everywhere in the record (docs/extraction.md): parsed JSON and structured
|
|
51
|
+
* clones carry none.
|
|
52
|
+
*
|
|
53
|
+
* DAG position: memory ← time-travel; this file is a reader beside `timeTravel` and imports only
|
|
54
|
+
* record files.
|
|
55
|
+
*/
|
|
56
|
+
import type { LogAddress } from '../memory/eventPosition.js';
|
|
57
|
+
import { type LogOutcome, type Meta, type RecordEntry } from '../memory/sink.js';
|
|
58
|
+
import type { CommitBundle } from '../memory/types.js';
|
|
59
|
+
import type { LogGap, TimeTravelSource } from './types.js';
|
|
60
|
+
/** A note the writer kept beside a log (`EventLog · note`), as stored. */
|
|
61
|
+
export interface StoredNote {
|
|
62
|
+
/** The note's kind, as the writer named it. */
|
|
63
|
+
readonly kind: string;
|
|
64
|
+
/** When it was written (epoch ms). */
|
|
65
|
+
readonly at: number;
|
|
66
|
+
/** What the writer put in it — opaque. */
|
|
67
|
+
readonly data: Meta;
|
|
68
|
+
}
|
|
69
|
+
/** One log read back from a sink's entries. */
|
|
70
|
+
export interface StoredLog {
|
|
71
|
+
/** Which log this is: the run leg that owned it and the mounts it sits under (root: `drillPath: []`). */
|
|
72
|
+
readonly address: LogAddress;
|
|
73
|
+
/** The fold base the `opened` entry carried; ABSENT when no readable `opened` entry was stored. */
|
|
74
|
+
readonly initialState?: Record<string, unknown>;
|
|
75
|
+
/**
|
|
76
|
+
* The bundles, each at its own index (`bundle.idx`). An index with no readable bundle holds an empty
|
|
77
|
+
* stand-in (it belongs to no stage and carries nothing) and is named in {@link gaps} — check `gaps`
|
|
78
|
+
* before a causal query, which takes the bundles as they are; fold {@link source}, not this, so the
|
|
79
|
+
* fold reports them.
|
|
80
|
+
*/
|
|
81
|
+
readonly commitLog: readonly CommitBundle[];
|
|
82
|
+
/** Every index of {@link commitLog} that holds no readable bundle, with why — empty when the log is whole. */
|
|
83
|
+
readonly gaps: readonly LogGap[];
|
|
84
|
+
/**
|
|
85
|
+
* The notes, in arrival order. A store that delivers an entry twice as two copies repeats its note:
|
|
86
|
+
* payloads are never compared (the very same object twice is listed in `unread` instead).
|
|
87
|
+
*/
|
|
88
|
+
readonly notes: readonly StoredNote[];
|
|
89
|
+
/**
|
|
90
|
+
* `true` when a `closed` entry covers every stored commit and the reader read the log whole: the
|
|
91
|
+
* writer said it ended. `false` for a log that never closed — a crashed run, or one still going —
|
|
92
|
+
* for one that went on after its last close, and for one the reader did not read whole (a limit
|
|
93
|
+
* cut it, or reading stopped at the `entries` limit — see `StoredRecord.unread`).
|
|
94
|
+
*/
|
|
95
|
+
readonly complete: boolean;
|
|
96
|
+
/** How the covering `closed` entry said the log ended; ABSENT while the log is not complete. */
|
|
97
|
+
readonly outcome?: LogOutcome;
|
|
98
|
+
/**
|
|
99
|
+
* What the writer put on the `opened` entry and on the covering `closed` entry (present only while
|
|
100
|
+
* the log is complete) — opaque, never merged.
|
|
101
|
+
*/
|
|
102
|
+
readonly meta: {
|
|
103
|
+
readonly opened?: Meta;
|
|
104
|
+
readonly closed?: Meta;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* The entries this log was read from, in arrival order, as stored — the evidence the rest is read
|
|
108
|
+
* from (each entry's `at`, every `closed` entry). An entry listed in `StoredRecord.unread` is not
|
|
109
|
+
* here. A commit's bundle is the stored row: {@link gaps} names one that is not a readable bundle.
|
|
110
|
+
*/
|
|
111
|
+
readonly entries: readonly RecordEntry[];
|
|
112
|
+
/**
|
|
113
|
+
* The log as `timeTravel` and `stateAt` read it: `{ commitLog, initialState?, subflowResults? }`. A
|
|
114
|
+
* gap is a row the fold refuses with the same reason {@link gaps} gives (`skipped`), so a fold that
|
|
115
|
+
* crosses one says so. `subflowResults` holds every stored mount log of the run leg — this one too,
|
|
116
|
+
* when it is a mount — keyed by each mount's runtimeStageId (the last segment of its `drillPath`):
|
|
117
|
+
* one flat map per leg, as a live snapshot's, so the cursor marks those stops as mounts and
|
|
118
|
+
* `drill(mountRuntimeStageId)` opens the mount's own stored log. A mount log's head commit stamped
|
|
119
|
+
* with its own mount id is its seed (it opens `start`, as on a live cursor). The same object on
|
|
120
|
+
* every call.
|
|
121
|
+
*/
|
|
122
|
+
source(): TimeTravelSource;
|
|
123
|
+
}
|
|
124
|
+
/** Every log a sink's entries hold, and every entry no log holds. */
|
|
125
|
+
export interface StoredRecord {
|
|
126
|
+
/** Every log, keyed by its address as a JSON array — `["<logRunId>", ...drillPath]` — in first-arrival order. */
|
|
127
|
+
readonly logs: ReadonlyMap<string, StoredLog>;
|
|
128
|
+
/** The root log of the run leg `logRunId` (`drillPath: []`), or `undefined` when none was stored. */
|
|
129
|
+
root(logRunId: string): StoredLog | undefined;
|
|
130
|
+
/** The log at `address`, or `undefined` when none was stored. */
|
|
131
|
+
log(address: LogAddress): StoredLog | undefined;
|
|
132
|
+
/**
|
|
133
|
+
* Entries not read into any log, by position in the input, with the reason: an entry that could not
|
|
134
|
+
* be read, one past a limit of the reader, or one for a place another entry holds (a second `opened`,
|
|
135
|
+
* a second commit for an index, the same entry again). In input order; empty when every entry was
|
|
136
|
+
* read. A commit that could not be read at all, or that a limit cut, leaves no row: its index is a
|
|
137
|
+
* gap only when a later stored commit or a close reaches past it.
|
|
138
|
+
*/
|
|
139
|
+
readonly unread: readonly {
|
|
140
|
+
readonly index: number;
|
|
141
|
+
readonly reason: string;
|
|
142
|
+
}[];
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The reader's limits — what bounds its work on untrusted input. Each defaults to the value below; a
|
|
146
|
+
* caller that reads larger records raises it. Whatever a limit stops is listed in `unread` by name.
|
|
147
|
+
*/
|
|
148
|
+
export interface StoredRecordLimits {
|
|
149
|
+
/**
|
|
150
|
+
* Entries read; past it, reading stops, one `unread` item says so, and no log reads complete.
|
|
151
|
+
* Default 16,777,216 (2^24).
|
|
152
|
+
*/
|
|
153
|
+
readonly entries?: number;
|
|
154
|
+
/**
|
|
155
|
+
* Gaps held across the WHOLE record — rows for commits the store does not have — spent on the logs
|
|
156
|
+
* that need the fewest first. A commit (or a close) that would need more is unread. Default
|
|
157
|
+
* 1,048,576 (2^20). The other rows a log holds are its stored commits.
|
|
158
|
+
*/
|
|
159
|
+
readonly missingCommits?: number;
|
|
160
|
+
/** Segments in an address's `drillPath`. Default 1,024. */
|
|
161
|
+
readonly drillDepth?: number;
|
|
162
|
+
/** Characters in an address — its `logRunId` and `drillPath` segments together. Default 65,536. */
|
|
163
|
+
readonly addressLength?: number;
|
|
164
|
+
}
|
|
165
|
+
/** The defaults — internal to the package; documented on {@link StoredRecordLimits}. */
|
|
166
|
+
export declare const DEFAULT_LIMITS: Required<StoredRecordLimits>;
|
|
167
|
+
/**
|
|
168
|
+
* Read a sink's stored entries back into logs.
|
|
169
|
+
*
|
|
170
|
+
* Bounded on untrusted input: no recursion, no payload walk, work per entry bounded by the address
|
|
171
|
+
* limits, at most `limits.missingCommits` gap rows across the record, and at most `limits.entries`
|
|
172
|
+
* entries read — whatever a limit stops is listed in `unread` by name.
|
|
173
|
+
*
|
|
174
|
+
* @param entries What a `RecordSink` stored (`memorySink().entries`, or entries parsed back from a
|
|
175
|
+
* file or a table), in the order they were written — typed `unknown` because stored entries come
|
|
176
|
+
* back unvalidated; each is checked here.
|
|
177
|
+
* @param limits Raise a default to read a larger record ({@link StoredRecordLimits}).
|
|
178
|
+
* @returns Every log by address, the run-leg roots, and every entry not read into a log.
|
|
179
|
+
* @throws RangeError when a limit is not a non-negative integer or `Infinity`; TypeError when
|
|
180
|
+
* `entries` is not iterable; whatever the input's own iterator throws. Never for an entry that is
|
|
181
|
+
* data (parsed JSON, a structured clone).
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```typescript
|
|
185
|
+
* import { readStoredRecord, stateAt, timeTravel } from 'foottrace';
|
|
186
|
+
*
|
|
187
|
+
* const stored = readStoredRecord(entries); // e.g. JSON lines read back after a crash
|
|
188
|
+
* const run = stored.root('run-1');
|
|
189
|
+
* run?.complete; // false — it never closed
|
|
190
|
+
* run?.notes.map((n) => n.kind); // ['attempt', 'attempt'] — kept as they happened
|
|
191
|
+
* timeTravel(run!.source()).last(); // the cursor over what landed
|
|
192
|
+
* stateAt(run!.source(), run!.commitLog.length - 1).skipped; // a lost commit, by index and reason
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
195
|
+
export declare function readStoredRecord(entries: Iterable<unknown>, limits?: StoredRecordLimits): StoredRecord;
|
|
@@ -452,7 +452,10 @@ export interface TimeTravel<TMeta = unknown> {
|
|
|
452
452
|
/**
|
|
453
453
|
* A cursor over the subflow mounted at `mountRuntimeStageId` — the same
|
|
454
454
|
* interface, its own log, its own fold base. `undefined` when that mount is
|
|
455
|
-
* not in this run's subflow results
|
|
455
|
+
* not in this run's subflow results, and when it is this cursor's own mount,
|
|
456
|
+
* one it was drilled out of, or the log it already reads (1.1.0: a stored or
|
|
457
|
+
* hand-built log can name one, and a viewer that opens every mount must not
|
|
458
|
+
* loop).
|
|
456
459
|
*
|
|
457
460
|
* A stop inside a subflow is NOT a stop of this cursor: subflows run in
|
|
458
461
|
* isolated runtimes and commit to their own log, so the outer axis holds
|
package/dist/types/write.d.ts
CHANGED
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
* The step's encoding is two dials (`RecordEncoding`): `commitValues` (`'full'` | `'delta'`) and
|
|
16
16
|
* `writeProvenance` (`'off'` | `'reads-prefix'`, `WriteProvenanceMode`).
|
|
17
17
|
*
|
|
18
|
+
* A log can also hand every entry to a `RecordSink` AS IT LANDS (`new EventLog(base, { sink,
|
|
19
|
+
* address })`): `opened`, `commit`, `note`, `closed` — so a run that never ends still leaves its
|
|
20
|
+
* record. `memorySink` is the reference adapter; `readStoredRecord` (`foottrace`) reads the stored
|
|
21
|
+
* entries back. A failing sink never stops the writer (`EventLog · sinkFailures`).
|
|
22
|
+
*
|
|
18
23
|
* Every type this door names is the record's own: nothing here loads or names the engine
|
|
19
24
|
* (test/architecture/layering.test.ts). Its names and the bytes they write are promised under the
|
|
20
25
|
* record fixtures' re-pin policy (test/fixtures/README.md): a refactor never moves a byte.
|
|
@@ -41,6 +46,8 @@
|
|
|
41
46
|
*/
|
|
42
47
|
export { EventLog } from './lib/memory/EventLog.js';
|
|
43
48
|
export { SharedMemory } from './lib/memory/SharedMemory.js';
|
|
49
|
+
export type { RecordEntry, RecordSink } from './lib/memory/sink.js';
|
|
50
|
+
export { memorySink } from './lib/memory/sink.js';
|
|
44
51
|
export type { CommitStamp } from './lib/memory/recordCommit.js';
|
|
45
52
|
export type { RecordEncoding, WriteScrub, WriteVerb } from './lib/memory/RecordFrame.js';
|
|
46
53
|
export { RecordFrame } from './lib/memory/RecordFrame.js';
|
package/dist/write.js
CHANGED
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
* The step's encoding is two dials (`RecordEncoding`): `commitValues` (`'full'` | `'delta'`) and
|
|
17
17
|
* `writeProvenance` (`'off'` | `'reads-prefix'`, `WriteProvenanceMode`).
|
|
18
18
|
*
|
|
19
|
+
* A log can also hand every entry to a `RecordSink` AS IT LANDS (`new EventLog(base, { sink,
|
|
20
|
+
* address })`): `opened`, `commit`, `note`, `closed` — so a run that never ends still leaves its
|
|
21
|
+
* record. `memorySink` is the reference adapter; `readStoredRecord` (`foottrace`) reads the stored
|
|
22
|
+
* entries back. A failing sink never stops the writer (`EventLog · sinkFailures`).
|
|
23
|
+
*
|
|
19
24
|
* Every type this door names is the record's own: nothing here loads or names the engine
|
|
20
25
|
* (test/architecture/layering.test.ts). Its names and the bytes they write are promised under the
|
|
21
26
|
* record fixtures' re-pin policy (test/fixtures/README.md): a refactor never moves a byte.
|
|
@@ -41,12 +46,14 @@
|
|
|
41
46
|
* @module write
|
|
42
47
|
*/
|
|
43
48
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
-
exports.LOG_PLACEHOLDER = exports.freezeRecord = exports.deepFreeze = exports.RecordFrame = exports.SharedMemory = exports.EventLog = void 0;
|
|
49
|
+
exports.LOG_PLACEHOLDER = exports.freezeRecord = exports.deepFreeze = exports.RecordFrame = exports.memorySink = exports.SharedMemory = exports.EventLog = void 0;
|
|
45
50
|
// The heap and the log.
|
|
46
51
|
var EventLog_js_1 = require("./lib/memory/EventLog.js");
|
|
47
52
|
Object.defineProperty(exports, "EventLog", { enumerable: true, get: function () { return EventLog_js_1.EventLog; } });
|
|
48
53
|
var SharedMemory_js_1 = require("./lib/memory/SharedMemory.js");
|
|
49
54
|
Object.defineProperty(exports, "SharedMemory", { enumerable: true, get: function () { return SharedMemory_js_1.SharedMemory; } });
|
|
55
|
+
var sink_js_1 = require("./lib/memory/sink.js");
|
|
56
|
+
Object.defineProperty(exports, "memorySink", { enumerable: true, get: function () { return sink_js_1.memorySink; } });
|
|
50
57
|
var RecordFrame_js_1 = require("./lib/memory/RecordFrame.js");
|
|
51
58
|
Object.defineProperty(exports, "RecordFrame", { enumerable: true, get: function () { return RecordFrame_js_1.RecordFrame; } });
|
|
52
59
|
var freeze_js_1 = require("./lib/capture/freeze.js");
|
package/package.json
CHANGED
|
@@ -1,45 +1,99 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "foottrace",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"type": "commonjs",
|
|
5
|
-
"description": "Execution records, replay, time travel and honest attribution
|
|
5
|
+
"description": "Execution records, replay, time travel and honest attribution \u2014 independent of the engine that produced them",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Sanjay Krishna Anbalagan <sanjay@footprintjs.dev>",
|
|
8
|
-
"repository": {
|
|
9
|
-
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/footprintjs/foottrace.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/footprintjs/foottrace/issues"
|
|
14
|
+
},
|
|
10
15
|
"homepage": "https://github.com/footprintjs/foottrace#readme",
|
|
11
|
-
"engines": {
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=22"
|
|
18
|
+
},
|
|
12
19
|
"sideEffects": false,
|
|
13
20
|
"main": "./dist/index.js",
|
|
14
21
|
"module": "./dist/esm/index.js",
|
|
15
22
|
"types": "./dist/types/index.d.ts",
|
|
16
|
-
"files": [
|
|
23
|
+
"files": [
|
|
24
|
+
"dist/"
|
|
25
|
+
],
|
|
17
26
|
"exports": {
|
|
18
|
-
".": {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
".": {
|
|
28
|
+
"import": {
|
|
29
|
+
"types": "./dist/esm/index.d.ts",
|
|
30
|
+
"default": "./dist/esm/index.js"
|
|
31
|
+
},
|
|
32
|
+
"require": {
|
|
33
|
+
"types": "./dist/types/index.d.ts",
|
|
34
|
+
"default": "./dist/index.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"./write": {
|
|
38
|
+
"import": {
|
|
39
|
+
"types": "./dist/esm/write.d.ts",
|
|
40
|
+
"default": "./dist/esm/write.js"
|
|
41
|
+
},
|
|
42
|
+
"require": {
|
|
43
|
+
"types": "./dist/types/write.d.ts",
|
|
44
|
+
"default": "./dist/write.js"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"./paths": {
|
|
48
|
+
"import": {
|
|
49
|
+
"types": "./dist/esm/paths.d.ts",
|
|
50
|
+
"default": "./dist/esm/paths.js"
|
|
51
|
+
},
|
|
52
|
+
"require": {
|
|
53
|
+
"types": "./dist/types/paths.d.ts",
|
|
54
|
+
"default": "./dist/paths.js"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"./package.json": "./package.json",
|
|
58
|
+
"./cause": {
|
|
59
|
+
"import": {
|
|
60
|
+
"types": "./dist/esm/cause.d.ts",
|
|
61
|
+
"default": "./dist/esm/cause.js"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"typesVersions": {
|
|
66
|
+
"*": {
|
|
67
|
+
"write": [
|
|
68
|
+
"dist/types/write.d.ts"
|
|
69
|
+
],
|
|
70
|
+
"paths": [
|
|
71
|
+
"dist/types/paths.d.ts"
|
|
72
|
+
],
|
|
73
|
+
"cause": [
|
|
74
|
+
"dist/esm/cause.d.ts"
|
|
75
|
+
]
|
|
76
|
+
}
|
|
22
77
|
},
|
|
23
|
-
"typesVersions": { "*": { "write": ["dist/types/write.d.ts"], "paths": ["dist/types/paths.d.ts"] } },
|
|
24
78
|
"scripts": {
|
|
25
79
|
"build": "tsc -p tsconfig.json && tsc -p tsconfig.esm.json && node scripts/postbuild-esm.mjs",
|
|
26
80
|
"test": "vitest run",
|
|
27
81
|
"test:coverage": "vitest run --coverage",
|
|
28
82
|
"typecheck": "tsc -p tsconfig.test.json",
|
|
29
83
|
"check:layering": "node scripts/check-layering.mjs",
|
|
30
|
-
"check:package": "publint && attw --pack",
|
|
84
|
+
"check:package": "publint && attw --pack --exclude-entrypoints cause --profile strict && attw --pack --entrypoints cause --profile esm-only",
|
|
31
85
|
"lint": "eslint 'src/**/*.ts'",
|
|
32
86
|
"format": "prettier --check 'src/**/*.ts' 'test/**/*.ts'",
|
|
33
87
|
"test:examples": "tsc -p examples/tsconfig.json",
|
|
34
88
|
"check": "npm run build && npm run typecheck && npm test && npm run check:layering && npm run lint && npm run format && npm run check:package && npm run test:examples"
|
|
35
89
|
},
|
|
36
|
-
"dependencies": {},
|
|
37
90
|
"devDependencies": {
|
|
38
91
|
"@arethetypeswrong/cli": "^0.18.1",
|
|
39
92
|
"@types/node": "^22.0.0",
|
|
40
93
|
"@typescript-eslint/eslint-plugin": "^8.71.1",
|
|
41
94
|
"@typescript-eslint/parser": "^8.71.1",
|
|
42
95
|
"@vitest/coverage-v8": "4.1.11",
|
|
96
|
+
"esbuild": "0.28.2",
|
|
43
97
|
"eslint": "^8.57.1",
|
|
44
98
|
"fast-check": "^4.4.0",
|
|
45
99
|
"prettier": "^2.8.1",
|