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,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 nothing. The standalone record names nothing outside itself: the engine's frame
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
@@ -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",
3
+ "version": "1.2.0",
4
4
  "type": "commonjs",
5
- "description": "Execution records, replay, time travel and honest attribution — independent of the engine that produced them",
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": { "type": "git", "url": "git+https://github.com/footprintjs/foottrace.git" },
9
- "bugs": { "url": "https://github.com/footprintjs/foottrace/issues" },
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": { "node": ">=22" },
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": ["dist/"],
23
+ "files": [
24
+ "dist/"
25
+ ],
17
26
  "exports": {
18
- ".": { "import": { "types": "./dist/esm/index.d.ts", "default": "./dist/esm/index.js" }, "require": { "types": "./dist/types/index.d.ts", "default": "./dist/index.js" } },
19
- "./write": { "import": { "types": "./dist/esm/write.d.ts", "default": "./dist/esm/write.js" }, "require": { "types": "./dist/types/write.d.ts", "default": "./dist/write.js" } },
20
- "./paths": { "import": { "types": "./dist/esm/paths.d.ts", "default": "./dist/esm/paths.js" }, "require": { "types": "./dist/types/paths.d.ts", "default": "./dist/paths.js" } },
21
- "./package.json": "./package.json"
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",