PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
CR-150 P00 declaration contracts

DESIGN coordinate basis: core-cr150-design/work/DESIGN.md, revision 6 text identified by the P00-7 gating read's SHA-256: 68bda6e37931c096522a348603623c7f3cba2b1371a9c0a442edbf53d25ab0ad. DESIGN line citations below use that pinned text; the amendments below take precedence. Dispatch is the external design lane's PARALLEL-PLAN.md, lines 27–30 and 63. C++ declarations spell the operations described by DESIGN; OPEN items below preserve its unspecified policies.

CORE-TOP's rev 6 amendment (10-18 16:04) in the external design lane's core-cr150-design/work/REVISION.md, lines 342–348, resolves S1 and T1. References to amendment S1 or T1 below name that ruling, which binds L6b-1 and L8a respectively.

CORE-TOP's rev 6 amendment recorded at 10-18 19:24 in the same external core-cr150-design/work/REVISION.md, lines 351–358, resolves V1 and V2. It adopts core-cr150-l15a/PROPOSAL-V1-V2.md, SHA-256 b17ff9f2dab20d876df2e3148ca1c2aba2b5d78f7ccf8c4f026643cfdb14674e, with required changes 1–9 of core-cr150-read-v1v2/REVIEW.md. That ruling also approves eager provider/feed component digests at configure, superseding the lazy timing in DESIGN 5:1243-1246. The review notes that the later 19:11 converter amendment shifts live DESIGN coordinates at/after 1173 by +3; the pinned coordinate basis here remains explicit.

The freeze adds declarations, value-type defaults and the provider's approved defaulted virtual destructor. Existing execution paths, feature probes, versions and caller-visible layouts stay unchanged. Operation definitions and lab proof belong to CORE-TOP and the implementation lanes. Nothing is built, compiled or tested on the P00 machine.

Sink ABI and line writer

File / declaration DESIGN / amendment authority Implementing lane
include/pineforge/report_sink.h: pf_report_sink_v1 6:1283-1289,1307-1309 L6b-1
pf_report_sink_fd_v1 and strategy_set_report_sink_fd_v1(pf_strategy_t,const pf_report_sink_fd_v1*) CORE-TOP CX-SINK-B1/B2 inherited-fd amendment, 10-20 L22a
pf_report_sink_receipt_v1 and its layout assertions 6:1291-1298,1305-1306 L6b-1
pf_report_sink_summary_v1 and strategy_report_sink_summary_v1(pf_strategy_t,pf_report_sink_summary_v1*) CORE-TOP CX-SINK-B5a ordered T-summary amendment, 10-20; layout and lifecycle L22q
pf_report_sink_version(void) 6:1300 L6b-1
strategy_set_report_sink_v1(pf_strategy_t,const pf_report_sink_v1*) 6:1301,1307-1317 L6b-1
strategy_report_sink_receipt_v1(pf_strategy_t,pf_report_sink_receipt_v1*) 6:1302; 3.7:791-793; amendment S1 L6b-1
include/pineforge/report_sink_writer.hpp: report_sink::RecordSet 3.7:795-809,823-831; 6:1310-1311 L6a/L6b-3
report_sink::Trailer 3.7:833-837; 6:1291-1298 L6a
report_sink::f64(double) 3.7:850-853,858-875 L6a
report_sink::escaped_string(std::string_view) 3.7:878-879 L6a
report_sink::LineWriter constructor/destructor and opaque State 3.7:881-888; 6:1307 L6a
LineWriter::write_header(const RecordSet&) 3.7:786-809,881-883 L6a/L6b-1
LineWriter::write_e(const pf_equity_point_t&) 3.7:825; 3.2:490-509; 3.6:756-773 L6a/L6b-1/2
LineWriter::write_t(...) 3.7:826; 3.3:547-551; 3.4:639-652 L6a/L6b-1/L8c
LineWriter::write_trailer() 3.7:833-837,881-888 L6a/L6b-1

report_sink.h follows the additive runtime-header pattern used by execution_observer.h. scripts/check_c_abi_runtime.py currently names pineforge.h, execution_observer.h, selected_window.h in RUNTIME_HEADERS. P00 adds no definitions, so it leaves that inventory and its counts intact. L6b-1 must enroll report_sink.h, the three definitions and their counts together, and add PINEFORGE_HAS_REPORT_SINK_V1 only with runtime support (DESIGN 6:1268-1271,1333-1338). There is no transitive include from an existing header in P00. O/V/K/X/H operations remain L6b-3's extension.

CORE-TOP's CX-SINK-B1/B2 amendment supersedes DESIGN 3.11's path-only statement for jailed children. LineWriter also accepts a borrowed pf_report_sink_fd_v1, with no path or scratch directory; both modes share the canonical v1 builder and receipt. The fd route retries sequential writes and waits on back-pressure without growing its buffer. It never closes the caller's fd or changes SIGPIPE handling. The trusted finalizer owns file creation. See the fd ABI contract.

CX-SINK-B5a adds a separate 64-byte, 8-byte-aligned summary receipt without changing the 144-byte trailer receipt. Report-only sealed-prefix folds and a copy folded over the resident/presented suffix preserve the Python T-fold bits, strict win/loss counts, first zero sign and first nonfinite T-field index (5..12). Neither the fold nor the published receipt enters broker state hashes; completion is published with the successful trailer.

TradeLedger

File / declaration DESIGN / amendment authority Implementing lane
include/pineforge/trade_ledger.hpp: engine_script_run_v19::TradeLedger, type aliases, lifetime/copy/move declarations, opaque State 3.4:612-620; 6:1344-1347 L8a
TradeLedger::clear() Amendment T1: size 0, base 0 L8a
TradeLedger::reserve(size_type) Amendment T1: a capacity hint L8a
TradeLedger::size() const, base() const 3.4:613-618 L8a
TradeLedger::empty() const Amendment T1: size() == 0 L8a
TradeLedger::operator[](size_type) (mutable/const), back() (mutable/const) 3.4:613-616,664-670 L8a/L8d
TradeLedger::at(size_type global) Amendment T1: engine_invariant below base() or at/after size() L8a
TradeLedger::at(size_type global) const Amendment T1: the same checked bounds and const result L8a
TradeLedger::push_back(const Trade&), push_back(Trade&&) 3.4:613-616 L8a
TradeLedger::resize(size_type) 3.4:614,664-665 L8a
TradeLedger::begin()/end() (mutable/const) 3.4:615 L8a
TradeLedger::seal(size_type) and its caller's five-condition proof / fold-write-spill-before-drop contract 3.4:622-688 L8b/L8c/L18

The total-count versus resident-window distinction and closedtrades.* retention exception are binding doc contracts (3.4:613-620,690-711). set_trade_history_streamable and its feature probe remain the engine/codegen lanes' work; P00 neither advertises them nor changes trades_.

References and iterators invalidate as a std::vector's (amendment T1).

Metric accumulator and point sources

File / declaration DESIGN section and lines Implementing lane
include/pineforge/metrics_stream.hpp: 24-byte pf_equity_point_t layout assertions 3.3:541-543; 3.8:906-907 L6b-4/L7c
metrics::TradeStatsAccumulator constructor/destructor, copy declarations, opaque State 3.3:518-529,547-553 L7a
TradeStatsAccumulator::fold(const TradeC&), finish() const 3.3:529,547-551 L7a
metrics::ArrayCurve constructor, size() const, operator[](int64_t) const 3.3:535-543 L7b
metrics::ScratchCurveRegion 3.8:902-909; 3.3:537,541-543 L6b-4
metrics::SpillCurve constructor/destructor, size() const, operator[](int64_t) const, opaque State 3.3:535-546; 3.8:904-909 L6b-4/L7c
metrics::compute_equity_stats<PointSource>(...) (ordinary denominator) 3.3:531-546 L7b/L7c
metrics::compute_equity_stats<PointSource>(...,observation_count) 3.3:531-546; 3.6:745-749,769-773 L7b/L7c/L17b

Only the named accumulator/point-source operations are frozen here. The month/per-bar generators are the metric implementation's internal walks; their unprescribed helper names and storage are not made a second API. Scratch curve/position-tail placement remains either regions or separate private files, as DESIGN permits. No mapped scratch or text-based metrics.

Bar and close views

File / declaration DESIGN / V1-V2 amendment authority Implementing lane
include/pineforge/native_bar_views.hpp: inline namespace native_bar_views_v1 2.2:339-347; 6:1328-1329 L15a
NativeBarView { const Bar* data; std::size_t size; } 2.2:348 L15a
NativeInstrumentFeedView { NativeBarView bars; const int64_t* close_ms; } 2.2:349 L15a/L15b
NativeInstrumentFeedReadView { NativeBarView bars; const int64_t* close_ms; size_t close_count; } 2.2:354-375; proposal V1 L15a
NativeBarViews with nullable indexed feed slots, const provider and current segment 2.2:352-375; 5:1250-1259; changes 1-4 L15a
native_instrument_feed_view(owned,borrowed) noexcept (no default argument) 2.2:355-375; change 3 L15a
normalize_native_run_spec(spec,views) noexcept 2.2:361-363; change 5 also covers projection L15a
validate_native_instrument_feeds(feeds,views) noexcept 2.2:361-363; proposal V1 L15a/L15b
validate_native_timeframe_subscriptions(...,views) noexcept 2.2:361-363; proposal V1 L15a
native_instrument_feeds_digest(feeds,views) noexcept 2.2:364-375; proposal V1 L15a
native_run_spec_digest(spec,views) (may throw) 2.2:364-375; eager-digest amendment / change 8 L15a
class NativeLowerPathSource (forward declaration retained and class completed) 2.2:350-351; 5:1237-1262; proposal V2 as amended L15a/L16
NativeLowerPathSource::Visitor = bool (*)(void*,const Bar&) noexcept 5:1241-1243; proposal V2 L16
virtual ~NativeLowerPathSource() noexcept = default Proposal V2: exclusive consumer ownership / base deletion L15a/L16
virtual size_t NativeLowerPathSource::count() const = 0 5:1241-1247; change 7: lifetime count, O(1) after first call L16
virtual bool NativeLowerPathSource::for_each(Visitor,void*) const = 0 5:1241-1247; proposal V2 L16
virtual NativeBarView NativeLowerPathSource::segment(int64_t,int64_t) = 0 5:1250-1257; change 6: repeated/overlapping windows L16
virtual const Bar* NativeLowerPathSource::next_after_segment() const noexcept = 0 5:1251-1257; proposal V2 L16
native_intrabar_path_digest(path,source) (may throw) 5:1241-1247; proposal V2 / change 8 L15a/L16

Staging and the active query are public members of the private NativeExecutionConsumer in src/native_execution_consumer.hpp (change 1):

Member declaration Frozen contract Implementing lane
void stage_instrument_feed_views(const NativeInstrumentFeedView* const*,size_t) Copy pending descriptors; no lifecycle judgement or active mutation L15a
void stage_lower_path_source(unique_ptr<NativeLowerPathSource>) noexcept Transfer exclusive ownership to pending; null clears pending source L15a
NativeBarViews bar_views() const noexcept Allocation-free active binding only; pending is invisible L15a

L15a implements staging and the common accessor, with no caller. It also adds the new header/source and namespace check to the native ownership guard (2.2:352-378). Neither the existing v3 run-spec nor native-host declarations change here. The borrowed-feed C export/probe belongs to L15b (6:1318-1329).

V1/V2 binding and provider rules

The two original view structs stay byte-for-byte the same. A zero context feed count selects all owned vectors. Otherwise the count matches metadata entries by index; a null slot selects its owned feed, while a nonnull slot always selects borrowed data, including a present empty feed. The read type preserves the independent owned close count, so validation still detects length mismatches and the feed digest still folds zero for a missing close. Borrowed slots require empty owning bar/close vectors, with metadata and columns retained. Descriptors/table are copied; immutable arrays are borrowed through every accepted-binding use. Binding adds no pointer registry or duplicate key/timeframe strings.

Staging changes pending only. Descriptor allocation throws std::bad_alloc with the previous pending slot preserved; a nonzero count with a null table throws coded engine_invariant. Only an applied configure binds and judges the pending descriptors/provider against candidate metadata. Refused configure or unwound preparation discards pending; no Ready restaging. Binding checks keep the proposal's typed bar/close/metadata refusals and existing validation order. bar_views() exposes active Ready/Running state, never pending, and returns an empty context when unbound or detached. A detached Ready spec cannot be admitted, and a provider bound at stream begin is engine_invariant. Run exit, including error/abort, detaches borrowed pointers after retaining scalar counts/content digests. The provider destructor never reads its input.

The visitor is synchronous, ordered and nonthrowing; false stops normally. Whole walks do not overwrite the active segment or separate look-ahead. Count is fixed for provider life: the first call may walk or use a pre-scan, later calls return its cached scalar in O(1). Segment bounds are individually non-decreasing; repeated or overlapping windows return every row in the half-open interval, including overlap rows. Equal bounds give empty data; reversed/decreasing bounds are coded engine_invariant / std::logic_error. Repeats keep the same storage. A different segment replaces storage only after its script bar finishes, preparing look-ahead before exposing it. Look-ahead is the first row at/after the end, even for an empty segment; its separate buffer stays fixed until a different segment and queries do not advance. The const context prevents scheduler callbacks selecting segments.

DESIGN 5 eager-digest amendment: after normalization succeeds and before configure's first member write, compute immutable feed/provider component digests. Lower-walk exceptions propagate unchanged and nothing is published; they are not mapped to validation refusals. Keep components outside combined cache invalidation and through detachment so the noexcept continuation query never walks freed input. Standalone context digest overloads may walk/throw. Folded bytes/order stay the same: feed NaNs canonicalize as before, while the lower-path digest preserves raw double bits. Null source uses the vector digest; nonnull source is present even if empty. No failure becomes empty data, a fallback or a sentinel hash. Unexpected visitor stop/count mismatch is an invariant error. Runtime segment failures retain the existing intrabar option rejection; allocation exceptions use the run-entry handler.

All nine required changes are frozen: (1) consumer staging/query, (2) pending only and configure binding, (3) explicit accessor argument, (4) const source, (5) projection validation routing below, (6) overlapping windows, (7) cached count, (8) propagating eager-digest failures, and (9) RESOLVED V1/V2 text. The implementations and source guards remain L15a/L15b/L16's work.

L15a read inventory

This is DESIGN's required source-guard read set (2.2:357-371; 5:1221-1228), including the indirect count/hash consumers. Each source range below names a symbol in that window. The DESIGN column preserves the revision-6 inventory references; L15a must maintain these anchors and mechanically pin every current .bars/.close_ms read when it installs the accessor/guard.

Reader to route through the view/source Source anchors in this tree DESIGN lines
Kernel instrument-feed pump: current bar, close, next open, count NativeExecutionConsumer::pump_instrument_subscription src/native_execution_consumer.hpp:1230 357-358
Host feed lookup/delivery and column reads installed_symbol_feed src/source/pine_security_eval.cpp:1059-1068; installed_symbol_feed src/source/pine_security_eval.cpp:1291-1301; installed_symbol_feed src/source/pine_security_eval.cpp:1320-1328 359-360
Feed validation: bar values/order, close length/bounds, column length; configure and consumer entry points feed.bars src/native_run_spec.cpp:259-290; instrument_feed_shapes src/native_run_spec.cpp:743; validate_native_instrument_feeds src/native_run_spec.cpp:1019-1022; validate_native_timeframe_subscriptions src/native_execution_consumer.cpp:9997; validate_native_timeframe_subscriptions src/native_execution_consumer.cpp:10022 361-363
Adapter projection validation: use the context normalizer; host stages after lending and before projection, using a context built from its store normalize_projected_spec src/source/pine_adapter.cpp:3189 361-363; V1/V2 change 5
Instrument-feed digest and cached/uncached continuation folds digest_instrument_feed_views src/native_run_spec.cpp:1162-1205; native_instrument_feeds_digest src/native_execution_consumer.cpp:328; native_instrument_feeds_digest src/native_execution_consumer.cpp:2907; native_run_spec_digest src/native_execution_consumer.cpp:14181 364-365
Staged feed counts in host state and its hash installed_symbol_feed src/source/pine_strategy_host.cpp:535-545; staged_.instrument_feeds src/source/pine_state_hash.cpp:339-346 366-367
Refresh of symbol-data digest and host hash fold refresh_symbol_data_digest src/source/pine_security_eval.cpp:1553-1566; symbol_data_digest_ src/source/pine_state_hash.cpp:1238-1244 367-368
Restore/store upkeep and column-setter length check held.bars src/source/pine_security_eval.cpp:1091-1101; data.bars src/source/pine_security_eval.cpp:1433-1439 369-370
Lower-path preflight/validation NativeExecutionConsumer::preflight_intrabar_path src/native_execution_consumer.hpp:1189 371,1221-1222
Lower-path digest (count first) and continuation hash native_intrabar_path_digest src/native_run_spec.cpp:891-919; native_intrabar_path_digest src/native_execution_consumer.cpp:293 371,1223-1224
Per-script-bar lower-path binary search/walk candidate src/native_execution_consumer.cpp:9094-9127 371,1225
Scheduler sub-bar lookup and one-bar look-ahead PineScheduler::lower_path_bar_at src/source/pine_scheduler_native.cpp:265-287; next_open src/source/pine_scheduler_native.cpp:328-329 371,1226-1227
Lower-path emptiness test inactive_sampler_path src/source/pine_adapter.cpp:3620 371,1228

Every count/digest must fold the same bytes/order for copied and borrowed feeds, including cached and uncached paths (2.2:372-375). Views replace storage; metadata, validation and continuation identity remain the existing contract.

Contract item status

ID Status Contract / remaining gap Owner
S1 RESOLVED Amendment S1: caller sets struct_size and version; 0 with the receipt filled; -1 with *out unchanged for a bad handle, struct_size or version. No sink installed: 0, counts 0, sha256_hex "", complete 0. L6b-1
S2 OPEN Internal writer error transport, oversized-line/unsafe-integer failure details, and what a repeated fill/capture does after a trailer; DESIGN requires repeatable metric reads but gives no writer reset/idempotence contract (3.3:550-551; 3.7:790,881-888). CORE-TOP / L6a/L6b-1
T1 RESOLVED Amendment T1: clear() resets size/base to 0; reserve(size_type) is a hint; both at(size_type global) forms throw engine_invariant below base() or at/after size(); empty() const means size() == 0. References and iterators invalidate as a std::vector's. L8a
M1 OPEN Point-source malformed-region/bounds/short-read error transport and concurrent-reader rules are not specified; the 24-byte binary points, 64 KiB pread window and repeated forward walks ARE fixed (3.3:535-546; 3.8:902-909). CORE-TOP / L6b-4/L7c
V1 RESOLVED CORE-TOP 19:24 amendment: consumer pending-only staging, active query, indexed nullable slots, explicit accessor and context validation/digest overloads, as amended by required changes 1-5 and 8-9. L15a/L15b
V2 RESOLVED CORE-TOP 19:24 amendment: four-operation virtual provider with nonthrowing visitor, const read context, cached count, overlapping/repeated segments, separate look-ahead and exclusive consumer ownership; eager configure component digests propagate failures (changes 1-2,4,6-9). L15a/L16

S1's model is the observation query, strategy_execution_observation_v1 include/pineforge/execution_observer.h:144-151.

Lab handoff

CORE-TOP runs the headers' compile proof, scripts/check_c_abi_runtime.py, and scripts/check_native_cpp_versions.py on the lab queue. P00 supplies no local test or guard verdict. Repository-wide unit/corpus and merge-gate proof remain required before integration is declared verified.

Report-sink presentation

An ordinary report sink, including an observed run without an admitted selected window, presents a one-point copy of the editable equity tail so a hook cannot rewrite prefix records that have already been emitted. A selected sink presents only the last min(W, M + 1) points to the hook, with W = kSelectedPresentationWindowPoints (2) and equity_curve_len equal to that tail-window length. The logical M + 1 stays capture-internal; hooks see only the window, with no ReportC or C ABI extension. Interior points are immutable to hooks and stream to scratch unchanged before presentation; the edited window follows them. The published count is the logical length minus the number of window points the hook cut. Metrics retain all M + 1 presented points, including any hidden suffix. Sink OFF keeps the full array unchanged. At capture, the existing admitted selected-window flag chooses the presentation once; subsequent report reads reuse that capture.

Ordinary sink removal (set_report_sink_v1(NULL)) returns the typed setter refusal -3 while that run's scratch curve is live and keeps the sink, scratch-backed summary report and bounded observer capture unchanged; the scratch is dropped at the next run begin or handle destruction without restoring a curve array. Sealed trade rows (TradeLedger::base() > 0) also refuse sink removal with -3 and set_trade_history_streamable(false) with coded<std::logic_error>(engine_invariant) before any state change, preserving the sink, receipt and report without rehydrating rows until a new run begin clears size/base to 0 (ordinary scratch ownership still independently refuses removal).

On the sink path, both ordinary and selected present_report hooks receive only the open-at-end trade rows, in the same lot order as the suffix of the sink-OFF array. trades_len is this open-lot count; total_trades still includes all closed rows. The hook may edit these rows in place, keeping their pointer and count. Its incoming net_profit still includes all rows. A hook that re-sums edited rows must continue report_closed_net_profit() one open row at a time, preserving the original left fold. Trade metrics fold directly over the ledger and then the presented open rows. T emission projects one ledger row at a time, followed by that same edited view; neither capture nor read allocates a closed-history TradeC[]. Sink OFF keeps the full array and is byte-identical.

The sink's presentation_ stores scalar report values with null trade and equity pointers; an ordinary observer capture also owns one inline presented equity point. The independent open-lot view is retained only to preserve the hook's edits across reads and explicit sink removal; its size depends on open lots, never closed trades. A completed selected capture owns its metric scratch separately. With a resident base of zero and no ordinary scratch owner, removing the sink explicitly requests full report arrays again, without dispatching a captured hook a second time. After sealing, removal is refused before any state change: retired rows remain in the sink and cannot be recovered from resident storage.

L17c/d and L8c use one report path. fill_trades_section (src/engine_report.cpp:162) keeps the logical global count and copies only open rows for a sink. report_closed_net_profit (src/engine_report.cpp:262) starts at ClosedTradePrefix::pnl and adds the resident suffix in order. finish_streamed_trade_metrics (src/native_execution_consumer.cpp:14001) finishes copies of the sealed accumulators over resident closed rows, then the presented open rows. The source host's terminal-quote presentation continues that same closed-row PnL fold before adding its edited open rows (net_profit src/source/pine_strategy_host.cpp:2248-2249).

Selected carry-in validation includes the minimum entry time folded at seal (first_entry_time src/native_execution_consumer.cpp:12942), so a window starting after a sealed entry refuses with the existing typed engine_invariant. Validation never indexes a retired row. write_report_sink_trades (src/engine_report.cpp:1350) resumes at the global T cursor: sealed rows already in the sink are emitted exactly once, followed by the resident suffix and open rows. The sink remains the complete row report.

HostRecorded contract, also to be carried into DESIGN 3.6 and the release-notes draft: a host that owns and pushes the curve owns its memory; the production Pine host uses KernelRecordedAtHostMarks (src/source/pine_adapter.cpp:2870), whose recorded curve, with a report sink installed, is held as one point and streamed to the scratch file; without a sink the curve stays in memory. HostRecorded's host-owned curve and the sink-OFF curve path retain their existing behavior.

The kernel recorder uses the ordinary sink's scratch owner and single editable execution point for selected runs too. Released pre-window points update the existing selected validation latches; only the anchor and observations in [T, E) enter scratch. Capture judges those latches and the final execution point before presenting, then reads the bounded window from the scratch suffix and held point. It replaces that suffix with the edited window and transfers the same scratch owner to the selected metric snapshot. No selected history vector or second scratch writer is built on this recorder path.

A live fill_report (src/engine_report.cpp:74) during an open selected kernel-recorder sink run refuses with a coded std::logic_error, report_shape_unsupported and {"shape":"live_selected_sink"}. It clears the output before allocating report rows, calling presentation or staging a point. The host may catch this read refusal and continue; the recorder, run-failure state and later selected capture remain unchanged. Sink OFF retains its live full-prefix array report.

Auxiliary-table fold across storage backends

The consumed-cycle table retains both the fixed scratch-slab backend and WL-PTAILFIX's incremental disk radix fold. Both fold the live count followed by the same lexically ordered key/value bytes for every incoming hash seed. The disk backend keeps its radix buckets, journal, pending header/slot overlay and constant-time hash read.

The slab's existing 40-byte records and 16-byte slots have no radix-node region. Its mutation path rebuilds an independent 257-word root transform with repeated ordered scans and fixed comparison buffers, then publishes the admitted header/slot and root. This costs quadratic key comparisons per mutation; hashing remains constant-time, with no file IO or allocation. The slab layout, admission limits, refusal-before-mutation rule and first-seen iteration stay unchanged. The root is reconstructible physical storage of the existing hash leaf, not a new logical hash field. Clear resets both roots; move carries the slab owner, collision seam and both fold states.