@sema-agent/core 5.31.0 → 5.33.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 (42) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/dist/agents/cascade.d.ts +49 -1
  3. package/dist/agents/cascade.js +2 -2
  4. package/dist/agents/verify.d.ts +70 -4
  5. package/dist/agents/verify.js +62 -16
  6. package/dist/core/checkpoint-store.d.ts +95 -0
  7. package/dist/core/checkpoint-store.js +40 -0
  8. package/dist/core/hooks.d.ts +14 -6
  9. package/dist/core/hooks.js +14 -3
  10. package/dist/core/memory-engine/file-backend.d.ts +172 -22
  11. package/dist/core/memory-engine/file-backend.js +877 -79
  12. package/dist/core/memory-engine/memory-backend-contract.js +33 -0
  13. package/dist/core/runner/assemble-result.d.ts +7 -0
  14. package/dist/core/runner/assemble-result.js +1 -1
  15. package/dist/core/runner/prepare-acquire-reconcile.d.ts +72 -0
  16. package/dist/core/runner/prepare-acquire-reconcile.js +126 -0
  17. package/dist/core/runner/prepare-config-doors.d.ts +140 -0
  18. package/dist/core/runner/prepare-config-doors.js +250 -0
  19. package/dist/core/runner/prepare-safety-scan.d.ts +53 -0
  20. package/dist/core/runner/prepare-safety-scan.js +80 -0
  21. package/dist/core/runner/prepare-task.d.ts +28 -80
  22. package/dist/core/runner/prepare-task.js +102 -586
  23. package/dist/core/runner/prepare-workspace-restore.d.ts +102 -0
  24. package/dist/core/runner/prepare-workspace-restore.js +144 -0
  25. package/dist/core/runner/runtask.js +8 -2
  26. package/dist/core/tool-policy.d.ts +25 -0
  27. package/dist/core/types.d.ts +149 -13
  28. package/dist/index.d.ts +5 -4
  29. package/dist/index.js +2 -2
  30. package/dist/orchestration/workflow-governance.d.ts +6 -4
  31. package/dist/tools/fs/bash-readonly-classifier.d.ts +9 -3
  32. package/dist/tools/fs/bash-readonly-classifier.js +4 -1
  33. package/dist/tools/fs/fs-bash.d.ts +19 -3
  34. package/dist/tools/fs/fs-bash.js +26 -1
  35. package/dist/tools/fs/index.d.ts +27 -7
  36. package/dist/tools/fs/index.js +7 -2
  37. package/dist/tools/fs/read-deny.d.ts +66 -8
  38. package/dist/tools/fs/read-deny.js +75 -39
  39. package/dist/tools/fs/read-face.d.ts +24 -2
  40. package/dist/tools/fs/read-face.js +9 -0
  41. package/dist/tools/fs/search.js +2 -0
  42. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import { decisionText, refuseOutOfContractDecision } from "./tool-policy.js";
2
2
  import { brandPolicyAskClass } from "./ask-class.js";
3
3
  import { inlineUntrusted } from "./untrusted-text.js";
4
+ import { PROBE_REASON_MAX, normalizeProbeCause } from "./checkpoint-store.js";
4
5
  import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
5
6
  import { createSafeNotifier } from "./safe-notify.js";
6
7
  import { ORG_ADJUDICATION_TIMEOUT_MS, ORG_RULE_DECISION_REASON, ORG_UNAVAILABLE_DECISION_REASON, settleOrgVerdictWithin } from "./permission-rule-org.js";
@@ -256,12 +257,20 @@ export async function runToolGate(input) {
256
257
  }
257
258
  if (decision.action === "allow" && (input.irreversibility === "always" || input.irreversibility === "maybe")) {
258
259
  let tighten = true;
260
+ let probeReason;
261
+ let probeCause;
259
262
  if (input.irreversibility === "maybe" && input.reversibilityProbe) {
260
263
  let reversible = false;
261
264
  const probeArgs = policyRewrite !== undefined ? policyRewrite : currentInput;
262
265
  try {
263
266
  const verdict = await withProbeTimeout(Promise.resolve(input.reversibilityProbe(probeArgs)), input.approvalTimeoutMs, input.abortSignal);
264
267
  reversible = verdict?.reversible === true;
268
+ if (!reversible) {
269
+ const raw = verdict?.reason;
270
+ if (typeof raw === "string" && raw.trim().length > 0)
271
+ probeReason = inlineUntrusted(raw, PROBE_REASON_MAX);
272
+ probeCause = normalizeProbeCause(verdict?.cause);
273
+ }
265
274
  }
266
275
  catch {
267
276
  reversible = false;
@@ -273,6 +282,8 @@ export async function runToolGate(input) {
273
282
  action: "ask",
274
283
  message: `tool "${toolName}" performs an irreversible action — explicit approval required`,
275
284
  decisionReason: "safety",
285
+ ...(probeReason !== undefined ? { probeReason } : {}),
286
+ ...(probeCause !== undefined ? { probeCause } : {}),
276
287
  };
277
288
  denySource = input.shellGated === true ? "shellGate" : "safety";
278
289
  }
@@ -436,7 +447,7 @@ export async function runToolGate(input) {
436
447
  ? { origin: orgAskOrigin !== undefined ? `org_${orgAskOrigin}` : "policy" }
437
448
  : undefined;
438
449
  if (suspendAsk && decision.action === "ask") {
439
- const suspended = await suspendAsk(req, currentInput, safety, undefined, realApprovalOf(decision), decision.action === "ask" ? decision.persistedRuleShadowed : undefined, decision.action === "ask" ? decision.decisionReason : undefined);
450
+ const suspended = await suspendAsk(req, currentInput, safety, undefined, realApprovalOf(decision), decision.action === "ask" ? decision.persistedRuleShadowed : undefined, decision.action === "ask" ? decision.decisionReason : undefined, decision.action === "ask" ? decision.probeReason : undefined, decision.action === "ask" ? decision.probeCause : undefined);
440
451
  if (suspended) {
441
452
  if ("parkFailed" in suspended)
442
453
  parkFailed = suspended.parkFailed;
@@ -455,7 +466,7 @@ export async function runToolGate(input) {
455
466
  req.args = outcome.presentedInput;
456
467
  }
457
468
  if (suspendAsk && outcome.parkDeclined && parkFailed === undefined) {
458
- const suspended = await suspendAsk(req, currentInput, safety, true, realApprovalOf(decision), decision.action === "ask" ? decision.persistedRuleShadowed : undefined, decision.action === "ask" ? decision.decisionReason : undefined);
469
+ const suspended = await suspendAsk(req, currentInput, safety, true, realApprovalOf(decision), decision.action === "ask" ? decision.persistedRuleShadowed : undefined, decision.action === "ask" ? decision.decisionReason : undefined, decision.action === "ask" ? decision.probeReason : undefined, decision.action === "ask" ? decision.probeCause : undefined);
459
470
  if (suspended) {
460
471
  if ("parkFailed" in suspended)
461
472
  parkFailed = suspended.parkFailed;
@@ -483,7 +494,7 @@ export async function runToolGate(input) {
483
494
  const resolved = await resolveAsk(decision, req);
484
495
  decision = resolved;
485
496
  if (resolved.action === "deny" && resolved.approverUnavailable === true && suspendAsk && parkFailed === undefined) {
486
- const suspended = await suspendAsk(req, currentInput, safety, true, realApprovalOf(askBeforeResolve), askBeforeResolve.action === "ask" ? askBeforeResolve.persistedRuleShadowed : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.decisionReason : undefined);
497
+ const suspended = await suspendAsk(req, currentInput, safety, true, realApprovalOf(askBeforeResolve), askBeforeResolve.action === "ask" ? askBeforeResolve.persistedRuleShadowed : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.decisionReason : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeReason : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeCause : undefined);
487
498
  if (suspended) {
488
499
  if ("parkFailed" in suspended)
489
500
  parkFailed = suspended.parkFailed;
@@ -61,11 +61,15 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
61
61
  /** Injected clock (opus 1.258 LOW: match the engine's `this.now` determinism discipline). */
62
62
  private readonly now;
63
63
  /**
64
- * The COMMITTED-rev ledger (`revs.json`, id → rev). Zero-copy makes it load-bearing for CAS: the
65
- * entry files are the SAME files the session edits, so "read the current rev from the file" would
66
- * make every legitimate update self-conflict (the file already holds the new content). The ledger
67
- * records the rev as of the last COMMIT (applyPatches) — a CAS compares against IT, so an in-session
68
- * edit passes (baseRev == ledger) while a concurrently-committed change conflicts (ledger advanced).
64
+ * The COMMITTED-rev ledger (`revs.json`, id → row; design/186 schema v2). Zero-copy makes it
65
+ * load-bearing for CAS: the entry files are the SAME files the session edits, so "read the current
66
+ * rev from the file" would make every legitimate update self-conflict (the file already holds the
67
+ * new content). Each row records the rev as of the last COMMIT (applyPatches) — a CAS compares
68
+ * against IT, so an in-session edit passes (baseRev == ledger) while a concurrently-committed
69
+ * change conflicts (ledger advanced) — PLUS the projection BINDING (owning scope + slug): bytes
70
+ * are model-plane material anyone can copy or relocate, so the account — model-invisible, outside
71
+ * every file-tool root — is what holds ownership. "rev matches" alone is no longer a serve
72
+ * credential; "rev matches AND the coordinates match the binding" is.
69
73
  * Read-side (`listHeaders`/`getByIds`/`search`) SYNCS the ledger to disk THROUGH the inbound gate
70
74
  * (B2 — out-of-session edits, e.g. a git pull into an in-repo memory dir, are secret/cap-scanned;
71
75
  * rejects are quarantined + shadow-restored, never adopted); the applyPatches lookup does NOT sync
@@ -73,6 +77,33 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
73
77
  * (fail-closed), never a silent empty-ledger downgrade.
74
78
  */
75
79
  private ledger;
80
+ /** Top-level unknown keys of the loaded v2 envelope — write-preserved (§1.1). */
81
+ private envelopeExtras;
82
+ /**
83
+ * §3.1 (r3-④) — the migration retry judge: the PERSISTED schema form, set at every real disk read
84
+ * of the ledger and flipped only by the locked migration. Deliberately NOT derived from the
85
+ * in-memory ledger shape: the v1 compat arm upgrades rows in memory (§3.2), so the memory shape
86
+ * reads "already v2" after the first stand-down and the retry would die. `"v1"` arms the
87
+ * per-entry opportunistic retry (construction / reads / checkControlPlane / every locked
88
+ * mutation); undefined = the disk form has not been observed yet.
89
+ */
90
+ private persistedSchemaVersion;
91
+ /** §3.1 — the v1-compat window is loud ONCE per instance (per-row unbound noise stays suppressed;
92
+ * the OVERALL state does not). */
93
+ private announcedV1Compat;
94
+ /** §3.4 — O(1) retry judge for the READ entries' unbound-convergence attempts: recomputed at
95
+ * every disk load and every persist (both already iterate the rows), never by scanning per read.
96
+ * Slightly stale-TRUE is harmless (one try-lock + one early-return reload); stale-FALSE cannot
97
+ * happen (every seat that changes the rows passes through a maintaining seat). While a blocker
98
+ * is genuinely stuck (a standing duplicate), each read pays one census — the same bounded cost
99
+ * the v1 read retry accepts for an incomplete store, in a state the sweeps warn about loudly. */
100
+ private unboundRowsKnown;
101
+ /** Adopting-face notice dedup (copy / undecidable-probe / ambiguous / census-incomplete): one
102
+ * still-pending state announces once per instance; changed bytes mint a new key and re-announce
103
+ * (same posture as the restricted finding keys, which are per-MOUNT — this face has no mount). */
104
+ private adoptionNoticeKeys;
105
+ /** Test seam (§7.4 r4-④ arm): force a post-commit-point transfer-append failure. */
106
+ private transfersAppendFault?;
76
107
  /** B2 — inbound-gate findings accumulated by read-side syncs; the engine drains them into
77
108
  * `HarvestReport.inboundFindings` at harvest. */
78
109
  private inboundFindings;
@@ -94,8 +125,77 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
94
125
  private shadowPath;
95
126
  private loadLedger;
96
127
  /** B3: atomic + fail-closed — a ledger that cannot be persisted throws (the commit is incomplete
97
- * and the caller must know), never a silent swallow. */
128
+ * and the caller must know), never a silent swallow. Always writes the v2 envelope; writing over
129
+ * an unmigrated v1 disk is refused loudly (mutations migrate FIRST — §3.1's "变异必先迁" is an
130
+ * invariant here, not a convention: a v2 write minus the census would strand every row unbound). */
98
131
  private saveLedger;
132
+ /** v1-journal restore ONLY (§1.2 dual-read): the snapshot predates the upgrade and is persisted in
133
+ * ITS OWN form — the migration (which runs after recovery, under the lock) owns the v1→v2 swap
134
+ * together with its backup + evidence protocol. */
135
+ private persistLedgerV1;
136
+ /** Fail-open announcement convenience (S2-B queue posture — an unwritable queue never fails the
137
+ * operation that had something to say; the fail-CLOSED evidence lane is `transfers.jsonl`). */
138
+ private enqueueExternalItems;
139
+ /** One-shot dedup for adopting-face notices (see {@link adoptionNoticeKeys}). */
140
+ private announceAdoptionNotice;
141
+ /** Resolve a binding's absolute projection path WITHOUT registering anything (probes must stay
142
+ * side-effect-free; `scopeDir` would register an unknown scope name from a hand-edited row). */
143
+ private bindingAbsPath;
144
+ private bindingRelPath;
145
+ /**
146
+ * §3.3/§2.2 — the strict projection census: EVERY id-bearing file under EVERY registered scope,
147
+ * with `{complete}` honesty. Absence is ENOENT-only AT EVERY LEVEL — deliberately NOT
148
+ * `scanEntryFiles`, whose root arm folds an unreadable scope root into "empty scope" (`onSkip`
149
+ * fires for non-root only): copied here, one transient root EACCES would turn "only B was
150
+ * visible" into "only B exists" and mint a permanent wrong binding (bound rows are never
151
+ * re-derived). Symlinks / depth overflow / non-.md are exclusions by the entry-scan contract,
152
+ * not census gaps.
153
+ */
154
+ private censusProjectionsLocked;
155
+ /** §3.6 — capture the pre-migration v1 bytes exactly once (`wx`: the first scene is never
156
+ * overwritten by a retry). */
157
+ private writeV1BackupOnce;
158
+ /** §3.1 — non-blocking txn-mutex acquisition for the opportunistic migration entries (the
159
+ * constructor cannot await). LIVE contention stands down — wait semantics belong to the async
160
+ * writers; every entry retries later. A STALE lock (past its deadline, owner token present) gets
161
+ * ONE synchronous single-winner steal, same judge and same token discipline as the async
162
+ * waiter's steal: without it, a crashed holder's leftover lock would stand down every sync entry
163
+ * forever, and a READ-ONLY deployment — which never runs an async writer to do the stealing —
164
+ * would sit in v1-compat indefinitely (the very suspension §3.1 bounds to seconds). Steal-safety
165
+ * is unchanged: the caller runs journal recovery INSIDE the acquired lock before touching state. */
166
+ private tryAcquireTxnLockSync;
167
+ /** §3.1 — one opportunistic migration attempt (read entries / construction / checkControlPlane):
168
+ * try-lock, recover, migrate, release. A busy lock is a clean stand-down (the mutation entries
169
+ * bootstrap the migration under their own lock regardless). */
170
+ private tryOpportunisticMigrate;
171
+ /**
172
+ * §3.3-§3.5 — the migration body. MUST hold the txn mutex. Reloads the DISK truth (the retry
173
+ * judge is {@link persistedSchemaVersion}, set by that reload — never the in-memory shape), then:
174
+ * v1 ⇒ full census-derived rebind + one-shot backup + journaled swap + one migration-summary
175
+ * evidence event; v2-with-unbound ⇒ re-derive the unbound rows only (bound rows are FINAL — only
176
+ * explicit transactions rewrite a binding), each new bind carrying a `migration-bind` evidence
177
+ * event. An INCOMPLETE census abandons the pass with ZERO writes ("only what was visible" is not
178
+ * "all that exists"; a wrong binding is worse than no binding) and retries at the next entry.
179
+ * P=0 rows stay unbound (「缺失≠删除」— dropping the row would endorse an unattributed delete);
180
+ * P≥2 rows stay unbound (the migration never adjudicates which projection is real — an existing
181
+ * dup audit keeps ringing until a harvest or a human resolves it). Idempotent: v2 + zero unbound
182
+ * is a no-op version check.
183
+ */
184
+ private migrateLockedIfNeeded;
185
+ /** Read the evidence log's event ids, healing a TORN TAIL (a crash mid-append): the fragment is
186
+ * quarantined for evidence, the sound prefix stands (lines are self-contained — damage never
187
+ * spreads backward). Any NON-tail corruption is fail-closed (B3 family). */
188
+ private readTransferEvs;
189
+ /** §1.1 commit-point honesty: any failure BEFORE the journal write must refuse the whole
190
+ * transaction cleanly — so the append target's writability (and the log's soundness) is probed
191
+ * here, never discovered after the point of no return. */
192
+ private precheckTransfersAppendable;
193
+ /** Append evidence events, idempotent by `ev` (the recovery leg re-runs this for a journal whose
194
+ * append crashed). The ONLY failure class here is I/O — never capacity: a size threshold would
195
+ * freeze legitimate moves behind a full log, so growth is disclosed, not refused. A failure
196
+ * AFTER the commit point is COMMITTED-PENDING (the caller keeps the journal and throws — calling
197
+ * it "refused" would invite a retry of a transaction the next recovery completes anyway). */
198
+ private appendTransfers;
99
199
  /**
100
200
  * @param opts.unlocked RB-447 (2026-07-31, hardening review) — set by the two callers that run
101
201
  * WITHOUT the txn mutex (the constructor and {@link checkControlPlane}). `applyPatches` serializes
@@ -120,7 +220,9 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
120
220
  /** RB-447 — is another writer mid-transaction right now? True while the txn lock dir exists and is
121
221
  * not past its stale line (a stale lock is a crashed holder: its artifacts ARE ours to recover).
122
222
  * Only consulted by the two UNLOCKED callers — inside `applyPatches` the lock is ours by
123
- * construction. Cheap: one `stat` on the lock dir, no lock acquisition, never blocks. */
223
+ * construction. Cheap: one `stat` on the lock dir, no lock acquisition, never blocks.
224
+ * §3.1 ⓪ (r5-①): "no lock" is ENOENT-only — a stat FAILURE folded into "nobody is committing"
225
+ * let a transient EIO run the unlocked recovery (and its staged sweep) over a live transaction. */
124
226
  private txnInFlight;
125
227
  /** Remove orphaned `.stage-*` files (a crash BEFORE the journal rename = clean rollback: nothing
126
228
  * was committed, the staged tmps are litter). Dotfiles are invisible to entry scans regardless. */
@@ -152,11 +254,46 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
152
254
  * Ledger-write discipline: the sync=true adoption path WRITES `revs.json`, and that write is
153
255
  * wholesale (atomic replace) — unserialized against applyPatches' transaction it is a
154
256
  * lost update (the later writer drops every row the earlier one added). sync=true is therefore
155
- * reachable ONLY through {@link readScopeAdopting}: `probeOnly` runs the scan with NO adoption
156
- * side effects and returns undefined at the FIRST divergent entry (adoption required the
157
- * caller re-runs the scan under the txn mutex against a freshly reloaded ledger); a completed
158
- * probe pass returns the entries (nothing to adopt, no lock ever taken). */
257
+ * reachable ONLY through {@link readScopeAdopting}: `probeOnly` runs the scan with NO ledger
258
+ * side effects and returns undefined at the FIRST entry that needs an ACCOUNT transaction (an
259
+ * edit adoption, or a move candidate — the caller re-runs the scan under the txn mutex against
260
+ * a freshly reloaded ledger); a completed probe pass returns the entries. A COPY or an
261
+ * undecidable binding probe needs no transaction (withheld + announced, account untouched), so
262
+ * the probe classifies those inline and keeps going — a store with a lingering copy never
263
+ * contends for the writer lock on every read. */
159
264
  private readScope;
265
+ /**
266
+ * design-A RB-5 ([A3]) — the read-side whitewash gate, shared by the bound-here divergence lane
267
+ * and the move+edit lane so the two can never drift: an out-of-session disk change (git pull /
268
+ * hand edit) of a repo-ingested entry that strips `provenance`, or strips a present `trust`
269
+ * marker, is the same whitewash applyPatches' I-2 clause refuses — adoption must not be the
270
+ * laundering back door. Baseline = the COMMITTED state (control-plane shadow; the disk file is
271
+ * the very thing under suspicion). A shadow-less legacy entry has no independent baseline, so the
272
+ * gate cannot fire for it — but every adoption/commit backfills the shadow, closing that window
273
+ * on first contact. `containment` keeps the reason honest per lane (the move lane quarantines
274
+ * WITHOUT restoring at the never-bound path).
275
+ */
276
+ private whitewashInboundFinding;
277
+ /**
278
+ * §2.2/§2.3 — one MOVE candidate under the txn mutex: a committed entry's bytes at non-bound
279
+ * coordinates whose bound projection is CONFIRMED absent (ENOENT, probed under this same lock).
280
+ * Adoption needs all three conditions — candidate present + bound path ENOENT + store-wide
281
+ * uniqueness through a census that proves its own completeness: with B and C both planted,
282
+ * single-condition ENOENT would let READ ORDER pick the owner; with a half census, "only B was
283
+ * visible" is not "only B exists" (r4-③/r5-②). Gate-clean adoption is an EXPLICIT transaction:
284
+ * row rebind + `prev` trace in one atomic ledger write, an `adopted-move` evidence event in the
285
+ * same journaled commit, and a loud announcement — external move/rename is the writeScope-null
286
+ * plane's PRIMARY legitimate input channel (git pull), so it is upgraded to a traced transaction,
287
+ * never refused wholesale. Candid boundary (D-1, ruled): a byte-identical delete-then-plant is
288
+ * THIS SAME disk observation — the adoption is a TRACED acceptance, not a rejection; what v2
289
+ * removes is the delete's invisibility (missing now rings), the relocation's tracelessness
290
+ * (transfers row + prev), and the silence. Out-of-band writers do not take this lock, so a
291
+ * TOCTOU window between the ENOENT re-verification and the commit remains for an adversary that
292
+ * can recreate the original mid-window — that actor is the delete-then-plant actor (same candid
293
+ * boundary), not a new capability; the tri-state probe closes FAULT misjudgment, and claims no
294
+ * more. Returns the entry to serve, or undefined (refused/deferred this pass).
295
+ */
296
+ private adoptMoveCandidateLocked;
160
297
  /**
161
298
  * The adopting read: {@link readScope} sync=true with its ledger write serialized behind the
162
299
  * SAME cross-process txn mutex applyPatches holds (acquisition/steal/timeout semantics shared,
@@ -208,12 +345,15 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
208
345
  * WRITES ARE REFUSED, loudly — a view handed to a write path is a defect, not a fallback.
209
346
  */
210
347
  retrievalView(): MemoryBackend;
211
- /** Dedup keys for divergence findings the restricted view already recorded on THIS instance: one
212
- * materialize reads a scope several times (listHeaders, then getByIds per scope), and a
213
- * checkpoint-cadence harvest re-audits the same still-pending state — without the key, one
214
- * untouched divergence would mint a finding per read. Per-process on purpose (no durable state to
215
- * lifecycle: a cross-process resume re-discloses once, which is the desired "still pending"
216
- * notice); a divergence whose bytes CHANGE mints a new key and is reported again. */
348
+ /** Dedup keys for divergence findings the restricted view already recorded: one materialize reads
349
+ * a scope several times (listHeaders, then getByIds per scope), and a checkpoint-cadence harvest
350
+ * re-audits the same still-pending state — without the key, one untouched divergence would mint
351
+ * a finding per read. Per AUDITED MOUNT (design/186 §6): the keys live and die with the mount,
352
+ * same lifecycle as the id-less scope/baseline pair one mount discloses a still-pending
353
+ * divergence once, and the NEXT audited mount (a later restricted session in this process, or a
354
+ * cross-process resume) re-discloses once, which is the desired "still pending" notice; the
355
+ * in-process and cross-process postures agree. A divergence whose bytes CHANGE mints a new key
356
+ * and is reported again within the mount. */
217
357
  private restrictedFindingKeys;
218
358
  private recordRestrictedFinding;
219
359
  /** Id-less audit state for an adoption-restricted session's WRITE scope. Armed only by the
@@ -262,7 +402,11 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
262
402
  private readScopeCommitted;
263
403
  /** Does `id` have a projection file anywhere in the store OTHER than `excludeAbsPath`? Returns
264
404
  * that projection's rel path, or undefined. Early-exits on the first hit; only consulted on the
265
- * rare divergent-with-shadow path (the scope-binding guard above), never on clean reads. */
405
+ * rare divergent-with-shadow path (the scope-binding guard above) for UNBOUND rows bound rows
406
+ * answer the same question in O(1) from the account binding (design/186 §2.4). RETIREMENT
407
+ * CONDITION: unbound rows are a migration transitional; when they are gone this scan form has no
408
+ * caller left and retires WITH them (delete the function, not just the call — a dead scan arm is
409
+ * the residue class the retirement registry exists for). */
266
410
  private idProjectionElsewhere;
267
411
  /**
268
412
  * The ADOPTION-RESTRICTED projection of this backend (see {@link readScopeCommitted}) — what a
@@ -287,9 +431,13 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
287
431
  * over the session's scopes — bytes landed AFTER materialize (a mid-session shell write) are
288
432
  * detected here, at the very boundary where an unrestricted harvest would have admitted them —
289
433
  * then check the committed account for entries MISSING from disk entirely (an unattributed delete
290
- * is divergence too). Ledger rows carry no scope, so presence is judged across every registered
291
- * scope, the same resolution `getByIds` uses; a rename or cross-scope move therefore never reads
292
- * as missing. Non-destructive throughout: nothing is restored, forgotten, or adopted here.
434
+ * is divergence too). BOUND rows (design/186 §2.2) are judged AT THEIR COMMITTED PROJECTION: a
435
+ * single-copy move / delete-then-plant empties the bound address and rings here even though the
436
+ * id is still "somewhere" (the v1 any-scope presence test read exactly that as fine — the
437
+ * registered residual this schema retires); byte-carriers of the id at other addresses are named
438
+ * in the same finding. UNBOUND rows (migration transitional) keep the v1 store-wide presence
439
+ * test — no false alarms while their address is underivable. Non-destructive throughout: nothing
440
+ * is restored, forgotten, or adopted here.
293
441
  *
294
442
  * `opts.idlessWriteScope` is the CALLER's per-call session verdict (same posture as the mount:
295
443
  * the backend never guesses): when the audited session HAS a write scope, id-less files under it
@@ -345,7 +493,9 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
345
493
  private assertTxnLockOwnership;
346
494
  private applyPatchesLocked;
347
495
  /** Plan one patch: decide CAS/conflicts against CURRENT state, emit journal ops (no entry-file
348
- * writes here — staging/execution happen in {@link applyPatches}). */
496
+ * writes here — staging/execution happen in {@link applyPatches}). Every row write is BOUND
497
+ * (§1.1) at the coordinates the batch ACTUALLY lands — a slug collision's `-n` suffix binds the
498
+ * suffixed slug, the same one `applied[].slug` reports (v1's ledger had no landing awareness). */
349
499
  private planOne;
350
500
  private locateById;
351
501
  getConsolidationCursor(scope: string): Promise<string | undefined>;