foottrace 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +73 -2
- package/dist/esm/index.d.ts +27 -0
- package/dist/esm/index.js +12 -0
- package/dist/esm/lib/capture/freeze.d.ts +91 -0
- package/dist/esm/lib/capture/freeze.js +313 -0
- package/dist/esm/lib/capture/ownData.d.ts +18 -0
- package/dist/esm/lib/capture/ownData.js +35 -0
- package/dist/esm/lib/capture/valueKinds.d.ts +57 -0
- package/dist/esm/lib/capture/valueKinds.js +141 -0
- package/dist/esm/lib/ids/runtimeStageId.d.ts +173 -0
- package/dist/esm/lib/ids/runtimeStageId.js +193 -0
- package/dist/esm/lib/memory/EventLog.d.ts +105 -0
- package/dist/esm/lib/memory/EventLog.js +147 -0
- package/dist/esm/lib/memory/RecordFrame.d.ts +232 -0
- package/dist/esm/lib/memory/RecordFrame.js +295 -0
- package/dist/esm/lib/memory/SharedMemory.d.ts +46 -0
- package/dist/esm/lib/memory/SharedMemory.js +87 -0
- package/dist/esm/lib/memory/TransactionBuffer.d.ts +521 -0
- package/dist/esm/lib/memory/TransactionBuffer.js +946 -0
- package/dist/esm/lib/memory/admission.d.ts +81 -0
- package/dist/esm/lib/memory/admission.js +152 -0
- package/dist/esm/lib/memory/backtrack.d.ts +286 -0
- package/dist/esm/lib/memory/backtrack.js +456 -0
- package/dist/esm/lib/memory/commitLogUtils.d.ts +217 -0
- package/dist/esm/lib/memory/commitLogUtils.js +383 -0
- package/dist/esm/lib/memory/deltaEncoding.d.ts +66 -0
- package/dist/esm/lib/memory/deltaEncoding.js +121 -0
- package/dist/esm/lib/memory/equality.d.ts +93 -0
- package/dist/esm/lib/memory/equality.js +261 -0
- package/dist/esm/lib/memory/eventPosition.d.ts +18 -0
- package/dist/esm/lib/memory/eventPosition.js +1 -0
- package/dist/esm/lib/memory/honesty.d.ts +93 -0
- package/dist/esm/lib/memory/honesty.js +91 -0
- package/dist/esm/lib/memory/keyPaths.d.ts +125 -0
- package/dist/esm/lib/memory/keyPaths.js +226 -0
- package/dist/esm/lib/memory/logModel.d.ts +76 -0
- package/dist/esm/lib/memory/logModel.js +363 -0
- package/dist/esm/lib/memory/merge.d.ts +26 -0
- package/dist/esm/lib/memory/merge.js +51 -0
- package/dist/esm/lib/memory/pathOps.d.ts +118 -0
- package/dist/esm/lib/memory/pathOps.js +292 -0
- package/dist/esm/lib/memory/paths.d.ts +56 -0
- package/dist/esm/lib/memory/paths.js +60 -0
- package/dist/esm/lib/memory/placeholders.d.ts +39 -0
- package/dist/esm/lib/memory/placeholders.js +39 -0
- package/dist/esm/lib/memory/recordCommit.d.ts +45 -0
- package/dist/esm/lib/memory/recordCommit.js +60 -0
- package/dist/esm/lib/memory/scrub.d.ts +52 -0
- package/dist/esm/lib/memory/scrub.js +72 -0
- package/dist/esm/lib/memory/types.d.ts +209 -0
- package/dist/esm/lib/memory/types.js +9 -0
- package/dist/esm/lib/memory/utils.d.ts +73 -0
- package/dist/esm/lib/memory/utils.js +150 -0
- package/dist/esm/lib/memory/verbs.d.ts +333 -0
- package/dist/esm/lib/memory/verbs.js +426 -0
- package/dist/esm/lib/recorder/CommitRangeIndex.d.ts +147 -0
- package/dist/esm/lib/recorder/CommitRangeIndex.js +206 -0
- package/dist/esm/lib/slice/elementProvenance.d.ts +81 -0
- package/dist/esm/lib/slice/elementProvenance.js +189 -0
- package/dist/esm/lib/slice/forwardSliceForKey.d.ts +88 -0
- package/dist/esm/lib/slice/forwardSliceForKey.js +242 -0
- package/dist/esm/lib/slice/index.d.ts +26 -0
- package/dist/esm/lib/slice/index.js +25 -0
- package/dist/esm/lib/slice/keyIndex.d.ts +137 -0
- package/dist/esm/lib/slice/keyIndex.js +292 -0
- package/dist/esm/lib/slice/keyTimeline.d.ts +49 -0
- package/dist/esm/lib/slice/keyTimeline.js +112 -0
- package/dist/esm/lib/slice/keysReadSources.d.ts +51 -0
- package/dist/esm/lib/slice/keysReadSources.js +94 -0
- package/dist/esm/lib/slice/serialize.d.ts +78 -0
- package/dist/esm/lib/slice/serialize.js +277 -0
- package/dist/esm/lib/slice/sliceForKey.d.ts +40 -0
- package/dist/esm/lib/slice/sliceForKey.js +78 -0
- package/dist/esm/lib/slice/types.d.ts +567 -0
- package/dist/esm/lib/slice/types.js +24 -0
- package/dist/esm/lib/time-travel/axis.d.ts +119 -0
- package/dist/esm/lib/time-travel/axis.js +178 -0
- package/dist/esm/lib/time-travel/bundles.d.ts +77 -0
- package/dist/esm/lib/time-travel/bundles.js +135 -0
- package/dist/esm/lib/time-travel/chain.d.ts +93 -0
- package/dist/esm/lib/time-travel/chain.js +187 -0
- package/dist/esm/lib/time-travel/commitStops.d.ts +55 -0
- package/dist/esm/lib/time-travel/commitStops.js +201 -0
- package/dist/esm/lib/time-travel/index.d.ts +22 -0
- package/dist/esm/lib/time-travel/index.js +20 -0
- package/dist/esm/lib/time-travel/stateAt.d.ts +102 -0
- package/dist/esm/lib/time-travel/stateAt.js +152 -0
- package/dist/esm/lib/time-travel/tagStops.d.ts +72 -0
- package/dist/esm/lib/time-travel/tagStops.js +101 -0
- package/dist/esm/lib/time-travel/timeTravel.d.ts +70 -0
- package/dist/esm/lib/time-travel/timeTravel.js +359 -0
- package/dist/esm/lib/time-travel/types.d.ts +470 -0
- package/dist/esm/lib/time-travel/types.js +22 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/paths.d.ts +4 -0
- package/dist/esm/paths.js +4 -0
- package/dist/esm/write.d.ts +49 -0
- package/dist/esm/write.js +47 -0
- package/dist/index.js +67 -0
- package/dist/lib/capture/freeze.js +319 -0
- package/dist/lib/capture/ownData.js +40 -0
- package/dist/lib/capture/valueKinds.js +145 -0
- package/dist/lib/ids/runtimeStageId.js +209 -0
- package/dist/lib/memory/EventLog.js +151 -0
- package/dist/lib/memory/RecordFrame.js +299 -0
- package/dist/lib/memory/SharedMemory.js +91 -0
- package/dist/lib/memory/TransactionBuffer.js +950 -0
- package/dist/lib/memory/admission.js +158 -0
- package/dist/lib/memory/backtrack.js +462 -0
- package/dist/lib/memory/commitLogUtils.js +398 -0
- package/dist/lib/memory/deltaEncoding.js +127 -0
- package/dist/lib/memory/equality.js +265 -0
- package/dist/lib/memory/eventPosition.js +2 -0
- package/dist/lib/memory/honesty.js +94 -0
- package/dist/lib/memory/keyPaths.js +242 -0
- package/dist/lib/memory/logModel.js +371 -0
- package/dist/lib/memory/merge.js +55 -0
- package/dist/lib/memory/pathOps.js +308 -0
- package/dist/lib/memory/paths.js +66 -0
- package/dist/lib/memory/placeholders.js +42 -0
- package/dist/lib/memory/recordCommit.js +64 -0
- package/dist/lib/memory/scrub.js +77 -0
- package/dist/lib/memory/types.js +10 -0
- package/dist/lib/memory/utils.js +169 -0
- package/dist/lib/memory/verbs.js +443 -0
- package/dist/lib/recorder/CommitRangeIndex.js +210 -0
- package/dist/lib/slice/elementProvenance.js +194 -0
- package/dist/lib/slice/forwardSliceForKey.js +246 -0
- package/dist/lib/slice/index.js +42 -0
- package/dist/lib/slice/keyIndex.js +312 -0
- package/dist/lib/slice/keyTimeline.js +116 -0
- package/dist/lib/slice/keysReadSources.js +100 -0
- package/dist/lib/slice/serialize.js +285 -0
- package/dist/lib/slice/sliceForKey.js +82 -0
- package/dist/lib/slice/types.js +25 -0
- package/dist/lib/time-travel/axis.js +183 -0
- package/dist/lib/time-travel/bundles.js +143 -0
- package/dist/lib/time-travel/chain.js +192 -0
- package/dist/lib/time-travel/commitStops.js +205 -0
- package/dist/lib/time-travel/index.js +31 -0
- package/dist/lib/time-travel/stateAt.js +159 -0
- package/dist/lib/time-travel/tagStops.js +105 -0
- package/dist/lib/time-travel/timeTravel.js +363 -0
- package/dist/lib/time-travel/types.js +23 -0
- package/dist/paths.js +14 -0
- package/dist/types/index.d.ts +27 -0
- package/dist/types/lib/capture/freeze.d.ts +91 -0
- package/dist/types/lib/capture/ownData.d.ts +18 -0
- package/dist/types/lib/capture/valueKinds.d.ts +57 -0
- package/dist/types/lib/ids/runtimeStageId.d.ts +173 -0
- package/dist/types/lib/memory/EventLog.d.ts +105 -0
- package/dist/types/lib/memory/RecordFrame.d.ts +232 -0
- package/dist/types/lib/memory/SharedMemory.d.ts +46 -0
- package/dist/types/lib/memory/TransactionBuffer.d.ts +521 -0
- package/dist/types/lib/memory/admission.d.ts +81 -0
- package/dist/types/lib/memory/backtrack.d.ts +286 -0
- package/dist/types/lib/memory/commitLogUtils.d.ts +217 -0
- package/dist/types/lib/memory/deltaEncoding.d.ts +66 -0
- package/dist/types/lib/memory/equality.d.ts +93 -0
- package/dist/types/lib/memory/eventPosition.d.ts +18 -0
- package/dist/types/lib/memory/honesty.d.ts +93 -0
- package/dist/types/lib/memory/keyPaths.d.ts +125 -0
- package/dist/types/lib/memory/logModel.d.ts +76 -0
- package/dist/types/lib/memory/merge.d.ts +26 -0
- package/dist/types/lib/memory/pathOps.d.ts +118 -0
- package/dist/types/lib/memory/paths.d.ts +56 -0
- package/dist/types/lib/memory/placeholders.d.ts +39 -0
- package/dist/types/lib/memory/recordCommit.d.ts +45 -0
- package/dist/types/lib/memory/scrub.d.ts +52 -0
- package/dist/types/lib/memory/types.d.ts +209 -0
- package/dist/types/lib/memory/utils.d.ts +73 -0
- package/dist/types/lib/memory/verbs.d.ts +333 -0
- package/dist/types/lib/recorder/CommitRangeIndex.d.ts +147 -0
- package/dist/types/lib/slice/elementProvenance.d.ts +81 -0
- package/dist/types/lib/slice/forwardSliceForKey.d.ts +88 -0
- package/dist/types/lib/slice/index.d.ts +26 -0
- package/dist/types/lib/slice/keyIndex.d.ts +137 -0
- package/dist/types/lib/slice/keyTimeline.d.ts +49 -0
- package/dist/types/lib/slice/keysReadSources.d.ts +51 -0
- package/dist/types/lib/slice/serialize.d.ts +78 -0
- package/dist/types/lib/slice/sliceForKey.d.ts +40 -0
- package/dist/types/lib/slice/types.d.ts +567 -0
- package/dist/types/lib/time-travel/axis.d.ts +119 -0
- package/dist/types/lib/time-travel/bundles.d.ts +77 -0
- package/dist/types/lib/time-travel/chain.d.ts +93 -0
- package/dist/types/lib/time-travel/commitStops.d.ts +55 -0
- package/dist/types/lib/time-travel/index.d.ts +22 -0
- package/dist/types/lib/time-travel/stateAt.d.ts +102 -0
- package/dist/types/lib/time-travel/tagStops.d.ts +72 -0
- package/dist/types/lib/time-travel/timeTravel.d.ts +70 -0
- package/dist/types/lib/time-travel/types.d.ts +470 -0
- package/dist/types/paths.d.ts +4 -0
- package/dist/types/write.d.ts +49 -0
- package/dist/write.js +56 -0
- package/package.json +48 -4
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Coordinates of an existing log within a recording/session, not a second event timeline. */
|
|
2
|
+
export interface LogAddress {
|
|
3
|
+
/** Engine leg that first owned this log; unchanged by a same-executor root resume. */
|
|
4
|
+
readonly logRunId: string;
|
|
5
|
+
/** Actual mount runtime IDs, outermost first. Root log: []. Never static subflow IDs. */
|
|
6
|
+
readonly drillPath: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The committed prefix present when an event was emitted. Does NOT describe
|
|
10
|
+
* the emitter's staged writes, first-touch read view, or eventual own commit.
|
|
11
|
+
* Several events can share a position; this is not a total event order.
|
|
12
|
+
*/
|
|
13
|
+
export interface EmitSourcePosition extends LogAddress {
|
|
14
|
+
/** Engine leg that created the emitting frame (not the scope's memory namespace). */
|
|
15
|
+
readonly runId: string;
|
|
16
|
+
/** Inclusive index in the addressed log; -1 means only its fold base existed. */
|
|
17
|
+
readonly committedThroughIdx: number;
|
|
18
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* honesty.ts — the ONE vocabulary for what a reader cannot see.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS FILE EXISTS. A recording cannot always answer what it is asked, and the library says
|
|
5
|
+
* so in several places, each in its own words: a slice carries `HonestyNote`s and, when it has no
|
|
6
|
+
* answer, a `missing` reason; a fed edge and an element birth carry a `basis`; a fold carries a
|
|
7
|
+
* `basis` and its `redacted` paths; a stored log reports `LogGap`s; a causal node carries
|
|
8
|
+
* `incompleteSources` and `truncated`. A reader that wants to EXPLAIN any of them to a person (a
|
|
9
|
+
* why-panel, an agent tool) kept its own table of code → sentence, and tables kept apart drift.
|
|
10
|
+
* This file is that table, once.
|
|
11
|
+
*
|
|
12
|
+
* WHAT IT HOLDS.
|
|
13
|
+
* - `HONESTY_CODES` — a frozen, closed registry: code → the ONE sentence that says what the code
|
|
14
|
+
* means. The shape of rustc's `--explain` and LSP's `Diagnostic.code`: the code is the stable
|
|
15
|
+
* key a consumer BRANCHES on; the sentence is for whoever reads the screen. It is not the
|
|
16
|
+
* per-instance `detail` a note carries (that names the key, the budget, the rows).
|
|
17
|
+
* - `HonestyCode` / `RegisteredCode` — what makes the registry the owner and not a suggestion:
|
|
18
|
+
* each public union of these codes (`HonestyNoteCode`, `MissingSliceReason`,
|
|
19
|
+
* `MissingProvenanceReason`, `FedBasis`, `AttributionBasis`, `FoldBasis`, and since 9.33.0 `ValueBasis` /
|
|
20
|
+
* `WriterBasis` — the basis twins of `commitValueAt` / `findLastWriter`) declares its members
|
|
21
|
+
* through `RegisteredCode`, so a member the registry does not hold fails to COMPILE. And
|
|
22
|
+
* `test/architecture/honesty-vocabulary.test.ts` asks the type checker for every exported type
|
|
23
|
+
* alias of `foottrace` that is a union of string literals: each one is inside the
|
|
24
|
+
* registry or is named, with its reason, as not an honesty vocabulary.
|
|
25
|
+
*
|
|
26
|
+
* NO OBJECT GAINS A FIELD from being registered. Three signals carry no code of their own — a
|
|
27
|
+
* `LogGap` (`'log-gap'`), `CausalNode.incompleteSources` (`'incomplete-sources'`) and
|
|
28
|
+
* `FoldedState.redacted` / `redactedPaths` (`'redacted'`) — and a `truncated: { byDepth, byNodes }`
|
|
29
|
+
* field means `'truncated'`; they are registered so a reader explains them from the same place.
|
|
30
|
+
*
|
|
31
|
+
* NOT HERE: the two strings a redaction leaves where a value was. The engine writes those on every
|
|
32
|
+
* redacted run; the log's lives in its own leaf, `memory/placeholders.ts` (the scope channel's with
|
|
33
|
+
* the verdict, in FootPrint's `memory/redaction.ts`), so that an app which only runs charts does not carry these
|
|
34
|
+
* sentences in its bundle (`HONESTY_CODES` is a pure expression, so a bundler drops it wherever
|
|
35
|
+
* nothing reads it).
|
|
36
|
+
*
|
|
37
|
+
* WHY L0. This file imports NOTHING, and `slice/` and `time-travel/` (L3) type their codes through
|
|
38
|
+
* it, so it sits below them — the layer table is `scripts/layers.json`.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* import { HONESTY_CODES } from 'foottrace';
|
|
43
|
+
*
|
|
44
|
+
* for (const { code } of forwardSlice.notes) console.log(code, '→', HONESTY_CODES[code]);
|
|
45
|
+
* // reads-not-recorded → This log carries no recorded read at all (the readTracking: 'off' signature), …
|
|
46
|
+
* HONESTY_CODES[foldedState.basis]; // 'log-only' → No initialState travelled with this log (…), …
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
/**
|
|
50
|
+
* Every honesty code the trace readers (`foottrace`) speak, with the one sentence that says
|
|
51
|
+
* what it means.
|
|
52
|
+
*
|
|
53
|
+
* Frozen and closed: the keys are exactly the codes below, and {@link HonestyCode} is derived from
|
|
54
|
+
* them. A new code is one new line here: a union declared through {@link RegisteredCode} cannot hold
|
|
55
|
+
* a code that is not on it, and `test/architecture/honesty-vocabulary.test.ts` fails on an exported
|
|
56
|
+
* type alias of `foottrace` that is a union of string literals neither inside the registry
|
|
57
|
+
* nor named there as not an honesty vocabulary. The values are typed `string`, not literal types,
|
|
58
|
+
* on purpose: the sentences are for a screen and may be reworded; a consumer branches on the KEY.
|
|
59
|
+
*/
|
|
60
|
+
export declare const HONESTY_CODES: Readonly<{
|
|
61
|
+
'conservative-fed-edges': string;
|
|
62
|
+
'nested-rows': string;
|
|
63
|
+
'pre-run-origin': string;
|
|
64
|
+
'reads-not-recorded': string;
|
|
65
|
+
'unknown-key': string;
|
|
66
|
+
truncated: string;
|
|
67
|
+
'empty-log': string;
|
|
68
|
+
'never-written': string;
|
|
69
|
+
'not-an-array': string;
|
|
70
|
+
'per-write': string;
|
|
71
|
+
stage: string;
|
|
72
|
+
'append-verb': string;
|
|
73
|
+
'prefix-inference': string;
|
|
74
|
+
'whole-value': string;
|
|
75
|
+
'initial+log': string;
|
|
76
|
+
'log-only': string;
|
|
77
|
+
'log-gap': string;
|
|
78
|
+
'incomplete-sources': string;
|
|
79
|
+
redacted: string;
|
|
80
|
+
deleted: string;
|
|
81
|
+
'from-initial-state': string;
|
|
82
|
+
}>;
|
|
83
|
+
/** Any code in {@link HONESTY_CODES} — the key a reader looks an explanation up by. */
|
|
84
|
+
export type HonestyCode = keyof typeof HONESTY_CODES;
|
|
85
|
+
/**
|
|
86
|
+
* A union of honesty codes a type DECLARES: `T` must be a subset of {@link HonestyCode}, or the
|
|
87
|
+
* declaration fails to compile (TS2344).
|
|
88
|
+
*
|
|
89
|
+
* `RegisteredCode<T>` is `T` — an identity at the type level — so the union keeps exactly the
|
|
90
|
+
* members it was written with and does NOT widen to every code:
|
|
91
|
+
* `type FoldBasis = RegisteredCode<'initial+log' | 'log-only'>`.
|
|
92
|
+
*/
|
|
93
|
+
export type RegisteredCode<T extends HonestyCode> = T;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* keyPaths.ts — which rows of a commit log touch a state key. ONE answer (F3, 9.33.0).
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS FILE EXISTS. A key query (`commitValueAt`, `findLastWriter`, `causalChain`, the slice
|
|
5
|
+
* layer) asks "which rows wrote `key`?". Each reader used to answer with its own `row.path === key`,
|
|
6
|
+
* so a row written INSIDE the key — `cfg␟b`, which every subflow seed of an object, every
|
|
7
|
+
* outputMapper merge-back and every fork child's namespace produces — was invisible to all of
|
|
8
|
+
* them, while the fold (`stateAt`, the live commit) applied it. The fold said `{a:1,b:2}`; the key
|
|
9
|
+
* queries said "never written". Every reader now asks here.
|
|
10
|
+
*
|
|
11
|
+
* THE PATH HALF (this file, L0 — it imports only the path codec and types):
|
|
12
|
+
* - {@link relation} — how a row's path sits against a key's path. A segment prefix on the
|
|
13
|
+
* DELIM-joined path, the way RFC 6901 prefixes a JSON Pointer: `cfg` is a prefix of `cfg␟a`,
|
|
14
|
+
* never of `cfgX`. A row is `'exact'` (on the key), `'inside'` (below it — it wrote part of the
|
|
15
|
+
* key's value) or `'around'` (above it — it wrote a container that holds the key).
|
|
16
|
+
* - {@link buildWriterIndex} / {@link writerCandidates} — one pass over a log, then the rows that
|
|
17
|
+
* can have written a key, found through its top-level key instead of a scan.
|
|
18
|
+
*
|
|
19
|
+
* THE RULES (defined here; the fold is L1 and the log readers L3, so `commitLogUtils.ts` applies
|
|
20
|
+
* them — `writersOf`, `findLastWriter`, `commitValueAt`):
|
|
21
|
+
* - THE WRITER RULE — commit `c` WRITES key `K` iff `c` has a row `'exact'` or `'inside'` `K`,
|
|
22
|
+
* or a row `'around'` `K` across which the value at `K` differs (compared over the whole commit,
|
|
23
|
+
* not row by row: a value that changes and changes back inside one commit was not written).
|
|
24
|
+
* A top-level key has no `'around'` rows, so for it the rule is the path check alone.
|
|
25
|
+
* Why not every `'around'` row: `$update('cfg', { list: [3] })` is a `merge` row on `cfg`, but
|
|
26
|
+
* it never reaches `cfg␟a` — counting it named the wrong writer of `cfg␟a`.
|
|
27
|
+
* - THE VALUE RULE — the value of `K` at commit `i` is the fold of EVERY row under `K`'s top-level
|
|
28
|
+
* key up to `i`, anchored at that key's last `set`/`delete`, read at `K`: the fold `stateAt`
|
|
29
|
+
* does, restricted to one top-level key. Top-level keys never interact under the step, so the
|
|
30
|
+
* restriction is exact. Folding only the rows that touch `K` is NOT: an array union
|
|
31
|
+
* (`deepSmartMerge`) dedups the whole array, so a sibling element written beside `a␟1` changes
|
|
32
|
+
* where a later merge on `a` leaves `a[1]`.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* relation('cfg\u001Fb', 'cfg'); // 'inside' — the row wrote part of cfg
|
|
37
|
+
* relation('cfg', 'cfg\u001Fa'); // 'around' — the row wrote the container cfg.a lives in
|
|
38
|
+
* relation('cfgX', 'cfg'); // undefined — a different key, not a prefix match
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
import type { CommitBundle } from './types.js';
|
|
42
|
+
/**
|
|
43
|
+
* The WORK every key query has done — rows indexed and scanned, fold steps, around-verdicts, trie and
|
|
44
|
+
* ancestor steps, binary searches, a causal walk's own loops — summed across calls. A deterministic cost
|
|
45
|
+
* measure: `test/lib/memory/boundary/key-query-scaling.test.ts` asserts a causal walk's work grows linearly
|
|
46
|
+
* in the log by COUNTING it (a wall-clock ratio failed on a busy machine). Never read by the library; one
|
|
47
|
+
* integer add per unit of work it already does. Here, at L0, so every layer that does that work can count it.
|
|
48
|
+
*/
|
|
49
|
+
export declare const queryWork: {
|
|
50
|
+
units: number;
|
|
51
|
+
};
|
|
52
|
+
/** How a row's path sits against a key's path. `undefined` from {@link relation} means disjoint. */
|
|
53
|
+
export type PathRelation = 'exact' | 'inside' | 'around';
|
|
54
|
+
/**
|
|
55
|
+
* How the row path `rowPath` sits against the key `key` — both DELIM-joined `TraceEntry.path`
|
|
56
|
+
* strings (a top-level key is a one-segment path). `'exact'` when they are equal, `'inside'` when
|
|
57
|
+
* the row is below the key, `'around'` when the row is above it, `undefined` when neither is a
|
|
58
|
+
* segment prefix of the other.
|
|
59
|
+
*/
|
|
60
|
+
export declare function relation(rowPath: string, key: string): PathRelation | undefined;
|
|
61
|
+
/** The top-level key a DELIM-joined path belongs to — its first segment. */
|
|
62
|
+
export declare function rootOf(path: string): string;
|
|
63
|
+
/**
|
|
64
|
+
* One written path in a {@link WriterIndex} — a node of the path TRIE built over a log. A node exists for
|
|
65
|
+
* every written path and every prefix of one.
|
|
66
|
+
*/
|
|
67
|
+
export interface PathNode {
|
|
68
|
+
/** The DELIM-joined path this node stands for. */
|
|
69
|
+
readonly path: string;
|
|
70
|
+
/** ARRAY positions of the commits with a row exactly on this path — ascending, one per commit. */
|
|
71
|
+
readonly positions: number[];
|
|
72
|
+
/** The subset of {@link positions} whose row here is a `set` or a `delete` (it decides the value alone). */
|
|
73
|
+
readonly totals: number[];
|
|
74
|
+
/** Positions of commits whose row with a non-`delete` verb sits on this path (the rest are deletes only). */
|
|
75
|
+
readonly writes: number[];
|
|
76
|
+
/** The next segment → its node. */
|
|
77
|
+
readonly children: Map<string, PathNode>;
|
|
78
|
+
/** Memo of {@link subtreePositions}. */
|
|
79
|
+
subtree?: readonly number[];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The positions in a log where each written path was written: a path trie per top-level key. Built in ONE
|
|
83
|
+
* pass (`O(total path segments)`); a key's candidates then cost `O(depth + matches)` — its node, its
|
|
84
|
+
* ancestors, its subtree — never a scan of every path under its top-level key.
|
|
85
|
+
*/
|
|
86
|
+
export interface WriterIndex {
|
|
87
|
+
/** Every top-level key → the root of its trie. */
|
|
88
|
+
readonly roots: ReadonlyMap<string, PathNode>;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* One pass over `commitLog`: the path trie of every written path. Build it ONCE when a reader asks about
|
|
92
|
+
* many keys (a causal walk, a forward slice) — or let `commitLogUtils · logModel` memoise it for a frozen log.
|
|
93
|
+
*/
|
|
94
|
+
export declare function buildWriterIndex(commitLog: readonly CommitBundle[]): WriterIndex;
|
|
95
|
+
/** Every path a row was written on, in trie order. */
|
|
96
|
+
export declare function pathsWritten(index: WriterIndex): string[];
|
|
97
|
+
/** The node for `key`, or undefined when no row was written on it or below it. */
|
|
98
|
+
export declare function nodeAt(index: WriterIndex, key: string): PathNode | undefined;
|
|
99
|
+
/** The nodes of `key`'s PROPER ancestors that exist in the index, top-level key first. */
|
|
100
|
+
export declare function ancestorNodes(index: WriterIndex, key: string): PathNode[];
|
|
101
|
+
/** Every commit with a row on `node`'s path or below it — ascending, memoised on the node. */
|
|
102
|
+
export declare function subtreePositions(node: PathNode): readonly number[];
|
|
103
|
+
/** The first index in an ascending list holding a value >= `value`. */
|
|
104
|
+
export declare function lowerBound(sorted: readonly number[], value: number): number;
|
|
105
|
+
/** The last value in an ascending list that is < `before`, or -1. */
|
|
106
|
+
export declare function lastBefore(sorted: readonly number[], before: number): number;
|
|
107
|
+
/** The values of an ascending list in the OPEN range (`after`, `before`). */
|
|
108
|
+
export declare function between(sorted: readonly number[], after: number, before: number): readonly number[];
|
|
109
|
+
/** The commits that can have written a key, split by what the writer rule still has to ask. */
|
|
110
|
+
export interface WriterCandidates {
|
|
111
|
+
/** Commits with a row `'exact'` or `'inside'` the key — writers by the path alone. Ascending. */
|
|
112
|
+
readonly atOrInside: readonly number[];
|
|
113
|
+
/** Commits with a row `'around'` the key and none at or inside it — writers only if the value at the key changed. Ascending. */
|
|
114
|
+
readonly aroundOnly: readonly number[];
|
|
115
|
+
}
|
|
116
|
+
/** The path half of the writer rule over an index: every commit that can have written `key`. `O(depth + matches)`. */
|
|
117
|
+
export declare function writerCandidates(index: WriterIndex, key: string): WriterCandidates;
|
|
118
|
+
/** The ascending, de-duplicated union of ascending position lists. */
|
|
119
|
+
export declare function ascendingUnion(lists: readonly (readonly number[])[]): number[];
|
|
120
|
+
/**
|
|
121
|
+
* Did this commit reach `key` ONLY through rows inside it — at least one row `'inside'` the key, and no
|
|
122
|
+
* row on it or around it? Such a write changed part of the key's value, not the whole: the one test
|
|
123
|
+
* behind every `'nested-rows'` code (a slice note, a causal edge's `basis`, `findLastWriterWithBasis`).
|
|
124
|
+
*/
|
|
125
|
+
export declare function writesOnlyInside(bundle: CommitBundle, key: string): boolean;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* logModel.ts — the read model of ONE commit log: the writer rule and the value rule of `keyPaths.ts`,
|
|
3
|
+
* answered at a cost proportional to the answer, not to the log (F3, 9.33.0).
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS FILE EXISTS. The rules are simple to state and expensive to apply naively: a key's writers
|
|
6
|
+
* include the commits AROUND it whose change reached it, and deciding that needs the value at the key
|
|
7
|
+
* before and after such a commit — a fold of its top-level key. Folding once per KEY made a causal walk
|
|
8
|
+
* over nested keys cubic in the log (every key re-scanned the log and re-folded its root). Here:
|
|
9
|
+
*
|
|
10
|
+
* - the log is indexed ONCE (`keyPaths · buildWriterIndex`, a path trie): a key's candidates cost
|
|
11
|
+
* O(depth + matches) — its node, its ancestors, its subtree — and the rows under a top-level key are
|
|
12
|
+
* reached through the index, never a scan;
|
|
13
|
+
* - each top-level key is folded ONCE (`verbs · foldKey`, copy-on-write), keeping its value before and
|
|
14
|
+
* after every commit a verdict can ask about: a commit with a row on a path that has written paths
|
|
15
|
+
* below it (it can be AROUND a written key), and a commit that writes THROUGH a string-valued
|
|
16
|
+
* container (`nativeSet` replaces the string, and the characters a key read from it vanish — the one
|
|
17
|
+
* way a row BESIDE a key moves it). A key's verdict is then two `nativeGet`s and a `deepEqual`;
|
|
18
|
+
* - the last writer before a point is found LAZILY: the last write on or inside the key, then only the
|
|
19
|
+
* around-candidates after it — most questions need no verdict at all.
|
|
20
|
+
*
|
|
21
|
+
* One shape keeps a per-key fold (memoised per key): a key that was never written, under a container that
|
|
22
|
+
* was only ever written WHOLE (its node has no written children, so no generation is kept there).
|
|
23
|
+
*
|
|
24
|
+
* A model of a FROZEN log (every engine log since F3: `EventLog · record` freezes each bundle and
|
|
25
|
+
* `getSnapshot` freezes the array) is memoised on the log — the record cannot change under it. A log that
|
|
26
|
+
* is not frozen (hand-built, parsed from JSON) gets a fresh model per question.
|
|
27
|
+
*/
|
|
28
|
+
import { type WriterIndex } from './keyPaths.js';
|
|
29
|
+
import type { CommitBundle } from './types.js';
|
|
30
|
+
import { type Touch } from './verbs.js';
|
|
31
|
+
/** The read model of `log` — memoised when the log is frozen, fresh otherwise. */
|
|
32
|
+
export declare function logModel(log: readonly CommitBundle[]): LogModel;
|
|
33
|
+
/** The memoised model of a frozen log (built on first ask), or undefined for a log that is not frozen. */
|
|
34
|
+
export declare function memoisedModel(log: readonly CommitBundle[]): LogModel | undefined;
|
|
35
|
+
/** Does the commit hold a non-`delete` row on `key` or inside it? (A `delete` cannot replace a string container.) */
|
|
36
|
+
export declare function writesAtOrInside(bundle: CommitBundle, key: string): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Can this commit leave a STRING-valued container above `key` — a row around the key whose recorded value
|
|
39
|
+
* holds a string at the row's path or anywhere between it and the key? Only then can a later write beside the
|
|
40
|
+
* key move it (`nativeSet` replaces the string). A conservative, payload-only check: no fold.
|
|
41
|
+
*/
|
|
42
|
+
export declare function leavesStringAbove(bundle: CommitBundle, key: string): boolean;
|
|
43
|
+
export declare class LogModel {
|
|
44
|
+
readonly log: readonly CommitBundle[];
|
|
45
|
+
readonly index: WriterIndex;
|
|
46
|
+
private readonly roots;
|
|
47
|
+
private readonly histories;
|
|
48
|
+
private readonly changesByKey;
|
|
49
|
+
private readonly writersByKey;
|
|
50
|
+
constructor(log: readonly CommitBundle[]);
|
|
51
|
+
private rootRows;
|
|
52
|
+
/** Refuse an answer that reaches a row whose verb the law does not know (one in commits `after` < c ≤ `end`). */
|
|
53
|
+
private refuseThrough;
|
|
54
|
+
/** Every row under `key`'s top-level key up to `end`, with its relation to `key` — through the index. */
|
|
55
|
+
rowsUnderRoot(key: string, end: number): Touch[];
|
|
56
|
+
private history;
|
|
57
|
+
/** A key never written, under a container only ever written whole: no generation is kept for it. */
|
|
58
|
+
private needsOwnFold;
|
|
59
|
+
/** Every commit across which the value at `key` changed — one fold for this key (the fallback shape). */
|
|
60
|
+
private ownChanges;
|
|
61
|
+
/**
|
|
62
|
+
* The commits in the OPEN range (`after`, `before`) with no row on or inside `key` across which the value
|
|
63
|
+
* at `key` changed — the AROUND half of the writer rule (and the string-container case beside it).
|
|
64
|
+
*/
|
|
65
|
+
private changedAround;
|
|
66
|
+
/** Every commit that WROTE `key`, ascending (memoised per key). */
|
|
67
|
+
writersOf(key: string): readonly number[];
|
|
68
|
+
/** The ARRAY position of the last commit before `before` that wrote `key`, or -1. */
|
|
69
|
+
lastWriterBefore(key: string, before: number): number;
|
|
70
|
+
/**
|
|
71
|
+
* The value of `key` after commit `idx` (inclusive). The fold of the rows under its top-level key — but
|
|
72
|
+
* when its last total row up to `idx` is a `set` ON the key and nothing around it was written since, only
|
|
73
|
+
* the rows on and inside the key from that `set` on can move it, and only they are folded.
|
|
74
|
+
*/
|
|
75
|
+
valueAt(key: string, idx: number): unknown;
|
|
76
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* merge.ts — the union merge the `merge` verb applies. A leaf (L0, imports
|
|
3
|
+
* only the own-slot writer): `deepSmartMerge`, the law every replay, fold and reader
|
|
4
|
+
* of a recorded `merge` row runs. Re-exported from `utils.ts`, so no importer
|
|
5
|
+
* moved.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Deep union merge helper.
|
|
9
|
+
* - Arrays (non-empty): union without duplicates (encounter order preserved)
|
|
10
|
+
* - Arrays (empty): replace — src `[]` clears the destination array.
|
|
11
|
+
* Rationale: writing `scope.tags = []` means "clear tags", not "append nothing".
|
|
12
|
+
* Without this rule, an empty-array write silently becomes a no-op which is
|
|
13
|
+
* impossible to distinguish from a bug.
|
|
14
|
+
* - Objects: recursive merge of own enumerable string keys, including special keys;
|
|
15
|
+
* inherited destination values and setters never participate.
|
|
16
|
+
* - Primitives: source wins
|
|
17
|
+
*
|
|
18
|
+
* Terminates on a CYCLIC `src` (9.18.1, same law as {@link deepEqual}: a
|
|
19
|
+
* state value survives `structuredClone`, which preserves cycles). A `src`
|
|
20
|
+
* object re-entered while it is still being merged higher up the stack hands
|
|
21
|
+
* back the output being built for it, so the merged value mirrors the cycle
|
|
22
|
+
* instead of unrolling it forever. Only the ancestors on the stack are
|
|
23
|
+
* guarded — a shared (acyclic) `src` reference met again at a different
|
|
24
|
+
* `dst` merges against THAT `dst`, exactly as before.
|
|
25
|
+
*/
|
|
26
|
+
export declare function deepSmartMerge(dst: any, src: any): any;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pathOps.ts — Native nested-path helpers (replaces lodash.get/set/has/mergewith)
|
|
3
|
+
*
|
|
4
|
+
* The DENIED set is the shared rule for three prototype-pollution selector
|
|
5
|
+
* segments (__proto__, constructor, prototype). Each operation's traversal
|
|
6
|
+
* contract is explicit below; merely using this module is not a sandbox.
|
|
7
|
+
*
|
|
8
|
+
* Selector operations read only own fields. Writers preflight the whole path
|
|
9
|
+
* before creating or copying any container, then place through putOwn.
|
|
10
|
+
* - nativeGet — DENIED check + hasOwnProperty at every step.
|
|
11
|
+
* - nativeHas — own-property existence only, including own denied names
|
|
12
|
+
* and own undefined values; it does not use the DENIED rule.
|
|
13
|
+
* - mergeContextWins — own-enumerable source keys, DENIED check, own-only
|
|
14
|
+
* destination precedence. Inherited fields are never destination data.
|
|
15
|
+
*
|
|
16
|
+
* Diagnostic writers retain their distinct creation/merge laws in utils.ts,
|
|
17
|
+
* using the same isDeniedSegment predicate and shared own-slot writer.
|
|
18
|
+
*
|
|
19
|
+
* Paths may be dot-notation strings or pre-split (string|number)[] arrays.
|
|
20
|
+
*/
|
|
21
|
+
/** Is `segment` one of the three denied prototype-pollution selector names? */
|
|
22
|
+
export declare function isDeniedSegment(segment: string | number): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Get the value at `path` in `obj`, returning `defaultValue` if absent.
|
|
25
|
+
*
|
|
26
|
+
* Security: each path segment is checked against the DENIED list and requires
|
|
27
|
+
* an own property at every step, preventing prototype-chain reads.
|
|
28
|
+
* e.g. nativeGet({}, '__proto__') and nativeGet({}, 'constructor') both return
|
|
29
|
+
* `defaultValue` instead of leaking Object.prototype / the Object constructor.
|
|
30
|
+
*/
|
|
31
|
+
export declare function nativeGet(obj: any, path: string | (string | number)[], defaultValue?: any): any;
|
|
32
|
+
/**
|
|
33
|
+
* Set an own path in a writer-owned container, creating missing intermediates.
|
|
34
|
+
* A denied selector is a no-op BEFORE any prefix is created. Inherited values
|
|
35
|
+
* and accessors do not participate; existing own accessors keep normal behavior.
|
|
36
|
+
*/
|
|
37
|
+
export declare function nativeSet(obj: any, path: string | (string | number)[], value: any): any;
|
|
38
|
+
/**
|
|
39
|
+
* Remove the own property at `path` in `obj` (the replay primitive for the
|
|
40
|
+
* `delete` commit verb, #13c-B). No-op when any intermediate segment is
|
|
41
|
+
* missing — deleting an absent key has nothing to remove.
|
|
42
|
+
*
|
|
43
|
+
* Security: same DENIED + hasOwnProperty discipline as nativeGet — every
|
|
44
|
+
* segment is checked, so `nativeDelete(obj, '__proto__x')` and
|
|
45
|
+
* prototype-chain walks are inert.
|
|
46
|
+
*/
|
|
47
|
+
export declare function nativeDelete(obj: any, path: string | (string | number)[]): void;
|
|
48
|
+
/** A container a writer may own: anything `typeof 'object'` and not null. */
|
|
49
|
+
export declare function isContainer(value: unknown): value is object;
|
|
50
|
+
/**
|
|
51
|
+
* `parent[key]` when `parent` is a container that OWNS `key` — the slot
|
|
52
|
+
* {@link nativeGet} would read, refusing the same prototype-pollution
|
|
53
|
+
* segments — else `undefined`. One step of a path walk.
|
|
54
|
+
*/
|
|
55
|
+
export declare function ownChild(parent: unknown, key: string | number): unknown;
|
|
56
|
+
/**
|
|
57
|
+
* A SHALLOW copy of one container that keeps every slot the whole-state
|
|
58
|
+
* `structuredClone` this replaced kept — so a path copy and a deep clone give
|
|
59
|
+
* the same value at every path:
|
|
60
|
+
* - a plain object (or a null-prototype one): its own enumerable keys, in
|
|
61
|
+
* order, by spread — an own `__proto__` key stays a key; the result has
|
|
62
|
+
* `Object.prototype`, as a clone's does;
|
|
63
|
+
* - an array: its indices, holes and length (`slice`) AND any named
|
|
64
|
+
* property a nested write hung off it (`slice` alone would drop those);
|
|
65
|
+
* - anything else — a `Date`, `Map`, `Set`, typed array, a class instance —
|
|
66
|
+
* is deep-cloned, which is what the whole-state clone did to it; and the
|
|
67
|
+
* own enumerable properties the clone cannot carry (an expando a nested
|
|
68
|
+
* write hung on a `Date`) are carried over by reference, so a later write
|
|
69
|
+
* through it changes exactly its own path. Rare on a write path.
|
|
70
|
+
*/
|
|
71
|
+
export declare function shallowCopy<T extends object>(container: T): T;
|
|
72
|
+
/**
|
|
73
|
+
* Make the containers on the way to `path`'s LEAF ones the writer owns, so
|
|
74
|
+
* the write that follows (`nativeSet` / `nativeDelete` / a verb arm) edits
|
|
75
|
+
* nothing it did not create.
|
|
76
|
+
*
|
|
77
|
+
* `root` must already be owned. Walking down, every container the writer did
|
|
78
|
+
* not create (`owned` does not hold it) is replaced, in its parent, by its
|
|
79
|
+
* {@link shallowCopy}, which the writer then owns. A denied segment anywhere
|
|
80
|
+
* refuses the entire walk before copying. Otherwise the walk stops at a
|
|
81
|
+
* missing / inherited / primitive intermediate (`nativeSet` makes a fresh
|
|
82
|
+
* own container from that point on). The leaf itself is never copied — the
|
|
83
|
+
* write replaces it.
|
|
84
|
+
*
|
|
85
|
+
* Cost: one shallow copy per container on the path that this writer has not
|
|
86
|
+
* already copied — O(depth × width), independent of everything the write
|
|
87
|
+
* does not pass through. A path of one segment (a root key, the common case)
|
|
88
|
+
* copies nothing here.
|
|
89
|
+
*/
|
|
90
|
+
export declare function ownSpine(root: any, path: string | (string | number)[], owned: WeakSet<object>): void;
|
|
91
|
+
/**
|
|
92
|
+
* The root of a new generation: a shallow copy of `base` when it is a
|
|
93
|
+
* container (owned by the writer), else the historical deep clone of a
|
|
94
|
+
* non-container base (`undefined`, a primitive) so the callers' behaviour on
|
|
95
|
+
* such a base is unchanged.
|
|
96
|
+
*/
|
|
97
|
+
export declare function ownedRootOf(base: any, owned?: WeakSet<object>): any;
|
|
98
|
+
/** Mark a value the writer just created (a fresh clone, a merge result) as owned. */
|
|
99
|
+
export declare function own(value: unknown, owned: WeakSet<object> | undefined): void;
|
|
100
|
+
/**
|
|
101
|
+
* Mark EVERY container of a tree the writer just created whole (a
|
|
102
|
+
* `structuredClone` result) as owned, so a later write through any of them
|
|
103
|
+
* edits it in place instead of copying it again. Walks own enumerable keys —
|
|
104
|
+
* the slots a path can address; a `Map`'s or `Set`'s entries are not. O(the
|
|
105
|
+
* tree's containers); a container already owned is not walked twice (a cycle
|
|
106
|
+
* inside the clone ends there).
|
|
107
|
+
*/
|
|
108
|
+
export declare function adopt(tree: unknown, owned: WeakSet<object>): void;
|
|
109
|
+
/** Returns true if `obj` has an own property at every segment of `path`. */
|
|
110
|
+
export declare function nativeHas(obj: any, path: string | (string | number)[]): boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Deep merge where destination wins for any defined value.
|
|
113
|
+
* Fills missing keys from `src`, but never overwrites defined keys in `dst`.
|
|
114
|
+
* Arrays are not recursed — dst array always wins when present.
|
|
115
|
+
*
|
|
116
|
+
* Replaces: `mergeWith(dst, src, (objValue) => objValue !== undefined ? objValue : undefined)`
|
|
117
|
+
*/
|
|
118
|
+
export declare function mergeContextWins(dst: any, src: any): any;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* paths.ts — the path codec: how a path of segments is written into a
|
|
3
|
+
* `TraceEntry.path` and read back. A leaf (L0, imports nothing): the commit
|
|
4
|
+
* log's address format, one owner. Re-exported from `utils.ts`, so no importer
|
|
5
|
+
* moved.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* The separator that joins path SEGMENTS into a `TraceEntry.path`.
|
|
9
|
+
*
|
|
10
|
+
* WHY NOT `'.'` — the ambiguity this exists to prevent: a state key may itself
|
|
11
|
+
* CONTAIN a dot, and this library creates such keys routinely. `$setValue`
|
|
12
|
+
* takes a KEY, not a path, so `$setValue('a.b', v)` makes one top-level key
|
|
13
|
+
* literally named `a.b`. With a dot separator that key's path and the nested
|
|
14
|
+
* path `['a', 'b']` would both encode as `"a.b"`, and every reader of the
|
|
15
|
+
* commit log — `applySmartMerge`, `commitValueAt`, `redactPatch`,
|
|
16
|
+
* `findLastWriter`, the slice layer — would have to guess which one a bundle
|
|
17
|
+
* meant. Splitting the wrong way writes into (or reads from) the wrong place.
|
|
18
|
+
*
|
|
19
|
+
* ASCII Unit-Separator is the choice because it cannot appear in a JS
|
|
20
|
+
* identifier and is vanishingly unlikely in a hand-written key, so the
|
|
21
|
+
* encoding stays unambiguous. It is NOT a display character: a path rendered
|
|
22
|
+
* straight to a UI or a log looks like one broken word. Split it with
|
|
23
|
+
* {@link pathSegments} — that, not this constant, is the contract consumers
|
|
24
|
+
* should hold.
|
|
25
|
+
*/
|
|
26
|
+
export declare const DELIM = "\u001F";
|
|
27
|
+
/** A canonical encoded path, or its literal segments (dots inside a segment stay literal). */
|
|
28
|
+
export type StateKey = string | ReadonlyArray<string | number>;
|
|
29
|
+
/** Normalise a consumer key with the same codec the writer uses. */
|
|
30
|
+
export declare function normaliseStateKey(key: StateKey): string;
|
|
31
|
+
/**
|
|
32
|
+
* Normalises an array path into a stable string key using DELIM.
|
|
33
|
+
*/
|
|
34
|
+
export declare function normalisePath(path: (string | number)[]): string;
|
|
35
|
+
/**
|
|
36
|
+
* The inverse of {@link normalisePath}: the SEGMENTS of a `TraceEntry.path`.
|
|
37
|
+
*
|
|
38
|
+
* A path in the commit log is DELIM-joined (see {@link DELIM}), which is not a
|
|
39
|
+
* display encoding — printing one verbatim shows a single broken-looking word,
|
|
40
|
+
* as a consumer rendering a path discovered. This is the supported way to take
|
|
41
|
+
* it apart; the delimiter itself stays an implementation detail, so a path can
|
|
42
|
+
* be read, rendered or re-joined without anyone hard-coding a control
|
|
43
|
+
* character.
|
|
44
|
+
*
|
|
45
|
+
* A single-segment path (the common case — a top-level state key) returns a
|
|
46
|
+
* one-element array, so callers need no special case.
|
|
47
|
+
*
|
|
48
|
+
* ```ts
|
|
49
|
+
* import { pathSegments } from 'foottrace/paths';
|
|
50
|
+
*
|
|
51
|
+
* for (const entry of bundle.trace) {
|
|
52
|
+
* console.log(pathSegments(entry.path).join(' › ')); // 'order › lines'
|
|
53
|
+
* }
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export declare function pathSegments(path: string): string[];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* placeholders.ts — the string a redaction leaves where a value was, in the RECORD.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS FILE EXISTS. Before F4a six files each spelled a placeholder as a literal, and a spelling
|
|
5
|
+
* kept in six places is a placeholder a reader can no longer match on. Every `src/` file that writes
|
|
6
|
+
* the log's placeholder imports it from here, and `test/architecture/placeholders.test.ts` pins that
|
|
7
|
+
* no other `src/` file spells it.
|
|
8
|
+
*
|
|
9
|
+
* ONE STRING HERE, ITS TWIN ON THE VERDICT'S SIDE (C4, review N2). A redaction leaves one of two
|
|
10
|
+
* strings, and both are historical, so neither may change (stored recordings hold this one). Which
|
|
11
|
+
* one a reader sees depends on WHERE it reads, and each has the owner its reader has:
|
|
12
|
+
* - {@link LOG_PLACEHOLDER} (`'REDACTED'`, here) is a RECORD byte: the commit log and everything
|
|
13
|
+
* served from it — written into the log by the record's scrub (`memory/scrub.ts`, L2), and
|
|
14
|
+
* passed by the two places that serve a mirror (`runner/ExecutionRuntime.ts`, the mirror's
|
|
15
|
+
* seed; `engine/handlers/SubflowExecutor.ts`, a subflow's served state). The readers
|
|
16
|
+
* (`slice/`, `time-travel/`) only name it in their docs: they meet it as data in the log;
|
|
17
|
+
* - `SCOPE_PLACEHOLDER` (`'[REDACTED]'`) is the ENGINE's: the scope channel's retained reads and
|
|
18
|
+
* writes, recorder events, the narrative. It lives beside the verdict that writes it
|
|
19
|
+
* (FootPrint's `memory/redaction.ts`). Until C4 both sat in this file ("two placeholders, one leaf");
|
|
20
|
+
* the record took the engine's string with it.
|
|
21
|
+
*
|
|
22
|
+
* WHY A LEAF OF ITS OWN (and not beside `honesty.ts · HONESTY_CODES`). Different readers, different
|
|
23
|
+
* reasons to change: this string is written on the engine's WRITE path on every redacted run, the
|
|
24
|
+
* honesty registry is read by a reader that explains a recording — and the registry's sentences have
|
|
25
|
+
* no business in the bundle of an app that only runs charts. This file imports NOTHING, so it is L0
|
|
26
|
+
* (the layer table is `scripts/layers.json`).
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* What the COMMIT LOG and the redacted MIRROR carry where a value was scrubbed.
|
|
30
|
+
*
|
|
31
|
+
* `memory/scrub.ts · scrubPatch` writes it into the patch the log records (both encodings), and the
|
|
32
|
+
* mirror's seed is scrubbed with the same string (`runner/ExecutionRuntime.ts`), so a fold of the log
|
|
33
|
+
* reproduces it (`stateAt` → `FoldedState.redactedPaths`) and a slice or an element birth, which
|
|
34
|
+
* re-serve the log's bytes, show it too. A subflow's SERVED state is the subflow's own nested mirror
|
|
35
|
+
* (9.20.0): `subflowResults[*].treeContext.globalContext` under `getSnapshot({ redact: true })`, and
|
|
36
|
+
* `onSubflowExit.outputState` whenever a policy keeps that mirror — so those carry this string as
|
|
37
|
+
* well, not the scope channel's `SCOPE_PLACEHOLDER` (`memory/redaction.ts`).
|
|
38
|
+
*/
|
|
39
|
+
export declare const LOG_PLACEHOLDER = "REDACTED";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recordCommit — one stage's commit onto the record (C1, L3). The one owner of a bundle's bytes and
|
|
3
|
+
* of where a commit lands: live state, the redacted mirror, the log.
|
|
4
|
+
*
|
|
5
|
+
* THE LAW:
|
|
6
|
+
* 1. No payload (the stage staged no write) → an EMPTY bundle goes to the log, and nothing else
|
|
7
|
+
* moves: live state and the mirror keep their generation. Every executed stage is a cursor stop.
|
|
8
|
+
* 2. A payload → its names are read first, so a stamp that cannot be read fails the commit before
|
|
9
|
+
* anything moves; then its raw rows build live state's next generation, `scrubPatch`
|
|
10
|
+
* (`scrub.ts`, the record's own) puts the placeholder at each redacted path that holds a value
|
|
11
|
+
* (in a spine copy, never in the raw patch), and the mirror and the log take the scrubbed rows —
|
|
12
|
+
* never the raw ones. The redacted paths are data by now: the engine decided them per write
|
|
13
|
+
* (`RecordFrame · write`); nothing here imports the engine's verdict (C4).
|
|
14
|
+
* 3. The bundle's KEY ORDER is the record's bytes: `overwrite`, `updates`, `redactedPaths`, `trace`,
|
|
15
|
+
* `stage`, `stageId`, `runtimeStageId`, then `untrackedSources`, `tags` and `phase`, each only
|
|
16
|
+
* when it has something to say (absent, never empty). `EventLog · record` appends `idx`. The
|
|
17
|
+
* names are names: no redaction reads them, and no encoding (`commitValues`) changes them.
|
|
18
|
+
*
|
|
19
|
+
* The frame (`StageContext`) decides WHAT is committed — the payload, the names, whether the bundle
|
|
20
|
+
* is a continuation — and keeps what is not the record: retention, diagnostics, the dev-mode
|
|
21
|
+
* warnings and the commit observer.
|
|
22
|
+
*/
|
|
23
|
+
import type { EventLog } from './EventLog.js';
|
|
24
|
+
import type { SharedMemory } from './SharedMemory.js';
|
|
25
|
+
import type { TransactionBuffer } from './TransactionBuffer.js';
|
|
26
|
+
import type { CommitPhase, UntrackedSource } from './types.js';
|
|
27
|
+
/** One stage's net change, as `TransactionBuffer · commit` hands it over. */
|
|
28
|
+
export type CommitPayload = ReturnType<TransactionBuffer['commit']>;
|
|
29
|
+
/** What names a commit on the record, beside its payload (law 3). */
|
|
30
|
+
export interface CommitStamp {
|
|
31
|
+
stage: string;
|
|
32
|
+
stageId: string;
|
|
33
|
+
runtimeStageId: string;
|
|
34
|
+
untrackedSources?: ReadonlySet<UntrackedSource>;
|
|
35
|
+
tags?: readonly string[];
|
|
36
|
+
phase?: CommitPhase;
|
|
37
|
+
}
|
|
38
|
+
/** Where a commit lands: live state always; the mirror and the log when the run keeps them. */
|
|
39
|
+
export interface CommitTargets {
|
|
40
|
+
state: SharedMemory;
|
|
41
|
+
mirror?: SharedMemory;
|
|
42
|
+
log?: EventLog;
|
|
43
|
+
}
|
|
44
|
+
/** Record one commit (the module law). `payload` absent = the empty commit. */
|
|
45
|
+
export declare function recordCommit(payload: CommitPayload | undefined, stamp: CommitStamp, into: CommitTargets): void;
|