@staix/agent-mem 0.20.3 → 0.22.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 (71) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +30 -3
  3. package/package.json +2 -2
  4. package/src/cli.ts +36 -3
  5. package/src/commands-table.ts +3 -0
  6. package/src/hook-spool.ts +45 -3
  7. package/src/index.ts +31 -2
  8. package/src/memory/bridge.ts +66 -18
  9. package/src/memory/candidates.ts +71 -9
  10. package/src/memory/commands.ts +3 -2
  11. package/src/memory/generation-index.ts +95 -18
  12. package/src/memory/generation-sync.ts +3 -0
  13. package/src/memory/generation.ts +33 -15
  14. package/src/memory/platform.ts +38 -3
  15. package/src/memory/policy.ts +28 -15
  16. package/src/memory/prune-publisher-lineage.ts +50 -12
  17. package/src/memory/public-prune-activation.ts +4 -4
  18. package/src/memory/public-prune-advance.ts +11 -5
  19. package/src/memory/public-prune-inputs.ts +17 -16
  20. package/src/memory/shard-sync-applications.ts +153 -0
  21. package/src/memory/shard-sync-format.ts +15 -0
  22. package/src/memory/shard-sync-graph.ts +6 -4
  23. package/src/memory/shard-sync-history.ts +238 -0
  24. package/src/memory/shard-sync.ts +66 -151
  25. package/src/memory/shard.ts +32 -6
  26. package/src/memory/store.ts +14 -1
  27. package/src/memory/sync-binding.ts +63 -0
  28. package/src/memory/sync-cadence.ts +2 -1
  29. package/src/memory/sync-placement-abandon.ts +142 -0
  30. package/src/memory/sync-placement-activation-locks.ts +28 -0
  31. package/src/memory/sync-placement-activation-path.ts +2 -0
  32. package/src/memory/sync-placement-activation-proof.ts +68 -0
  33. package/src/memory/sync-placement-activation-storage.ts +167 -0
  34. package/src/memory/sync-placement-activation.ts +185 -0
  35. package/src/memory/sync-placement-active.ts +75 -0
  36. package/src/memory/sync-placement-cache.ts +32 -0
  37. package/src/memory/sync-placement-capture.ts +373 -0
  38. package/src/memory/sync-placement-dependencies.ts +255 -0
  39. package/src/memory/sync-placement-effective-writers.ts +84 -0
  40. package/src/memory/sync-placement-format.ts +182 -0
  41. package/src/memory/sync-placement-git.ts +95 -0
  42. package/src/memory/sync-placement-guard.ts +104 -0
  43. package/src/memory/sync-placement-inspection-evidence.ts +257 -0
  44. package/src/memory/sync-placement-inspection-pending.ts +66 -0
  45. package/src/memory/sync-placement-inspection.ts +240 -0
  46. package/src/memory/sync-placement-journal-format.ts +114 -0
  47. package/src/memory/sync-placement-journal-storage.ts +557 -0
  48. package/src/memory/sync-placement-journal.ts +160 -0
  49. package/src/memory/sync-placement-materialize.ts +149 -0
  50. package/src/memory/sync-placement-plan.ts +171 -0
  51. package/src/memory/sync-placement-prerequisites.ts +50 -0
  52. package/src/memory/sync-placement-projection.ts +79 -0
  53. package/src/memory/sync-placement-publication-plan.ts +189 -0
  54. package/src/memory/sync-placement-publication-storage.ts +87 -0
  55. package/src/memory/sync-placement-transport.ts +500 -0
  56. package/src/memory/sync-placement-writer-history.ts +278 -0
  57. package/src/memory/sync-placement-writer-renewal.ts +179 -0
  58. package/src/memory/sync-placement-writer-revision-format.ts +122 -0
  59. package/src/memory/sync-placement-writer-roots.ts +167 -0
  60. package/src/memory/sync-placement-writers.ts +135 -0
  61. package/src/memory/sync-policy-inspection.ts +126 -0
  62. package/src/memory/sync-screen.ts +128 -33
  63. package/src/memory/sync.ts +19 -64
  64. package/src/setup.ts +101 -14
  65. package/src/sync-placement-writer-context.ts +136 -0
  66. package/src/ui/inbox-cache.ts +219 -0
  67. package/src/ui/inbox-worker-version.ts +14 -0
  68. package/src/ui/inbox-worker.ts +32 -0
  69. package/src/ui/inbox.ts +281 -0
  70. package/src/ui/panel.js +7 -3
  71. package/src/ui/plugin.ts +26 -53
@@ -0,0 +1,257 @@
1
+ /** Read-only retained placement evidence. Neither a planned object nor a cached ref is a publication receipt. */
2
+ import { capturePlacementPublicationMetadata, type PlacementJournalRead, type PlacementPublicationStored } from "./sync-placement-journal-storage.ts";
3
+ import { rebuildPlacementProjection, type PlacementProjection } from "./sync-placement-projection.ts";
4
+ import { SyncPlacementJournalError } from "./sync-placement-journal-format.ts";
5
+ import { createHash } from "node:crypto";
6
+ import { lstatSync, opendirSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { canonicalJSON, hashJSON } from "./generation.ts";
9
+ import { memoryGitEnv, memoryGitSync } from "./platform.ts";
10
+ import { placementCacheConfigStamp, placementCacheRootStamp, validatePlacementCacheConfig } from "./sync-placement-cache.ts";
11
+ import { SYNC_SCREEN_MAX_BYTES, SYNC_SCREEN_MAX_ROWS } from "./sync-screen.ts";
12
+ import { PUBLIC_PRUNE_REPLAY_MAX_BYTES } from "./public-prune-input-chain.ts";
13
+ import { PUBLIC_PRUNE_GRAPH_MAX_BYTES } from "./public-prune-inputs.ts";
14
+ import { ShardHistoryAuditor } from "./shard-sync-history.ts";
15
+ import { SHARD_PACK_MAX_BYTES, shardSyncRef, type ShardSyncRealm } from "./shard-sync-format.ts";
16
+ import type { PlacementRealm } from "./sync-placement-journal-format.ts";
17
+ import { placementKeyId } from "./sync-placement-journal-format.ts";
18
+ import type { ShardHistory } from "./shard-sync-history.ts";
19
+ import type { PlacementPublicationDecision, PlacementPublicationObject } from "./sync-placement-publication-plan.ts";
20
+
21
+ export interface InspectionJournal {
22
+ journal: PlacementJournalRead;
23
+ projection: PlacementProjection;
24
+ publications: ReadonlyMap<string, PlacementPublicationStored>;
25
+ assertCurrent(): void;
26
+ }
27
+
28
+ export function captureInspectionJournal(home: string, epochHash: string): InspectionJournal {
29
+ const captured = capturePlacementPublicationMetadata(home, epochHash, io => {
30
+ const journal = io.journal();
31
+ if (journal.pending) throw new SyncPlacementJournalError("pending");
32
+ const attempts = new Set<string>();
33
+ for (const name of io.names()) {
34
+ const match = /^publication-([a-f0-9]{64})-/.exec(name);
35
+ if (match) attempts.add(match[1]!);
36
+ }
37
+ const publications = new Map<string, PlacementPublicationStored>();
38
+ for (const attempt of [...attempts].sort()) {
39
+ const publication = io.publication(attempt);
40
+ if (publication.pending) throw new SyncPlacementJournalError("pending");
41
+ const frontier = (sequence: number) => sequence === 0 ? publication.bundle.root.epochHash : journal.records[sequence - 1]?.seal;
42
+ if (frontier(publication.bundle.root.journal.sequence) !== publication.bundle.root.journal.headHash) throw new SyncPlacementJournalError("corrupt");
43
+ const receipt = publication.facts.find(fact => fact.phase === "completed");
44
+ if (receipt?.phase === "completed" && frontier(receipt.journal.sequence) !== receipt.journal.headHash) throw new SyncPlacementJournalError("corrupt");
45
+ const root = publication.bundle.root, artifact = publication.facts.find(fact => fact.phase === "artifact-built");
46
+ if (artifact?.phase === "artifact-built" && artifact.exportHash !== hashJSON(root.targets.map(target => target.exportSetHash))) throw new SyncPlacementJournalError("corrupt");
47
+ if (receipt?.phase === "completed" && receipt.outcomeHash !== (receipt.outcome === "committed"
48
+ ? hashJSON({ attempt: root.seal, commits: root.targets.map(target => target.commit) }) : hashJSON({ attempt: root.seal, outcome: "unknown" }))) throw new SyncPlacementJournalError("corrupt");
49
+ publications.set(attempt, publication);
50
+ }
51
+ return { journal, projection: rebuildPlacementProjection(journal.epoch, journal.records), publications };
52
+ });
53
+ return { ...captured.value, assertCurrent: captured.assertCurrent };
54
+ }
55
+
56
+ export interface InspectionExposureProof { remoteHash: string; refHash: string; commit: string }
57
+ export interface InspectionTarget { realm: ShardSyncRealm; generation: string }
58
+ export interface RetainedInspectionHistories {
59
+ old: (ShardHistory | undefined)[];
60
+ tips: (string | undefined)[];
61
+ published: Map<string, InspectionExposureProof[]>;
62
+ }
63
+ const physicalRealm = (realm: ShardSyncRealm): PlacementRealm => realm.scope === "user" ? { scope: "user" } : { scope: "project", keyHash: hash(realm.key) };
64
+
65
+ /** Historical core acknowledgement plus exact immutable membership, never a current remote-head observation. */
66
+ export async function inspectRetainedPublicationHistories(snapshot: InspectionJournal, targets: readonly InspectionTarget[], objects?: PlacementInspectionObjects): Promise<RetainedInspectionHistories> {
67
+ const result: RetainedInspectionHistories = { old: targets.map(() => undefined), tips: targets.map(() => undefined), published: new Map() };
68
+ const selected = new Map(targets.map((target, index) => [canonicalJSON(physicalRealm(target.realm)), index]));
69
+ const publications = [...snapshot.publications.values()].sort((a, b) => a.bundle.root.journal.sequence - b.bundle.root.journal.sequence || a.bundle.root.seal.localeCompare(b.bundle.root.seal));
70
+ for (const publication of publications) {
71
+ const root = publication.bundle.root, decisions = publication.bundle.chunks.filter(chunk => chunk.kind === "decisions").flatMap(chunk => chunk.items as PlacementPublicationDecision[]);
72
+ const decisionByKey = new Map(decisions.map(decision => [placementKeyId(decision.key), decision]));
73
+ const receipt = publication.facts.find(fact => fact.phase === "completed"), artifact = publication.facts.find(fact => fact.phase === "artifact-built");
74
+ if (!objects || publication.phase !== "completed" || receipt?.phase !== "completed" || receipt.outcome !== "committed") continue;
75
+ if (artifact?.phase !== "artifact-built") unavailable();
76
+ for (const [targetIndex, planned] of root.targets.entries()) {
77
+ const index = selected.get(canonicalJSON(planned.realm)); if (index === undefined) continue;
78
+ const target = targets[index]!, tip = planned.commit ?? planned.expectedRemoteHead;
79
+ if (!tip) continue;
80
+ if (planned.refHash !== hash(shardSyncRef(target.realm))) unavailable();
81
+ const auditor = objects.auditor(target.realm, target.generation), history = await auditor.history(tip);
82
+ if (history.rows.size > SYNC_SCREEN_MAX_ROWS) bound();
83
+ const membership = [...history.rows.values()].map(row => ({ id: row.id, rowHash: hashJSON(row) })).sort((a, b) => a.id.localeCompare(b.id));
84
+ if (hashJSON(membership) !== planned.exportSetHash || hash(history.files.get("shard.json") ?? Buffer.alloc(0)) !== planned.descriptorHash
85
+ || planned.tree !== objects.tree(tip)) unavailable();
86
+ const retained = publication.bundle.chunks.filter(chunk => chunk.kind === "objects").flatMap(chunk => chunk.items as PlacementPublicationObject[]);
87
+ for (const object of retained) if (object.targetIndex === targetIndex) {
88
+ const body = objects.object(object.oid, object.type, object.type === "commit" ? 65536 : SHARD_PACK_MAX_BYTES);
89
+ if (body.length !== object.size || hash(body) !== object.sha256) unavailable();
90
+ }
91
+ const acknowledgedHistories = new Map<string, ShardHistory>([[tip, history]]);
92
+ for (const record of snapshot.journal.records) {
93
+ if (record.kind !== "publication-committed" || record.outcome.attemptHash !== root.seal
94
+ || record.sequence <= root.journal.sequence || record.sequence > root.journal.sequence + root.reservedRecords
95
+ || canonicalJSON(record.key.realm) !== canonicalJSON(planned.realm)) continue;
96
+ const id = placementKeyId(record.key), decision = decisionByKey.get(id), commit = record.outcome.commit;
97
+ if (!decision || record.sourcePlacement !== decision.sourcePlacement || record.outcome.remoteHash !== root.remoteHash || record.outcome.refHash !== planned.refHash
98
+ || !(commit === planned.expectedRemoteHead || decision.eligible && planned.publish && commit === planned.commit)
99
+ || record.requestId !== hashJSON({ attempt: root.seal, key: record.key, kind: record.kind, commit })
100
+ || record.effectHash !== hashJSON({ attempt: root.seal, context: root.contextHash, key: record.key, kind: record.kind, commit })
101
+ || record.outcome.proofHash !== hashJSON({ attempt: root.seal, key: record.key, commit, remote: root.remoteHash, ref: planned.refHash })) unavailable();
102
+ let atCommit = acknowledgedHistories.get(commit);
103
+ if (!atCommit) {
104
+ if (!await auditor.verifiedAncestor(commit, tip)) unavailable();
105
+ atCommit = await auditor.history(commit); acknowledgedHistories.set(commit, atCommit);
106
+ }
107
+ const row = atCommit.rows.get(record.key.id);
108
+ if (!row || hashJSON(row) !== record.key.rowHash) unavailable();
109
+ const proof = { remoteHash: root.remoteHash, refHash: planned.refHash, commit };
110
+ const prior = result.published.get(id) ?? [];
111
+ const previous = prior.findIndex(value => value.remoteHash === proof.remoteHash && value.refHash === proof.refHash);
112
+ if (previous < 0) prior.push(proof); else prior[previous] = proof;
113
+ result.published.set(id, prior);
114
+ }
115
+ // Only the retained selected origin supplies publication dependencies. A receipt from another remote
116
+ // is historical exposure, never a reason to hydrate its bodies into the current publication target.
117
+ if (root.remoteHash === objects.remoteHash) { result.old[index] = history; result.tips[index] = tip; }
118
+ }
119
+ }
120
+ for (const proofs of result.published.values()) proofs.sort((a, b) => canonicalJSON(a).localeCompare(canonicalJSON(b)));
121
+ objects?.assertCurrent(); snapshot.assertCurrent(); return result;
122
+ }
123
+
124
+ function unavailable(): never { throw new SyncPlacementJournalError("corrupt"); }
125
+ function bound(): never { throw new SyncPlacementJournalError("bound"); }
126
+ const hash = (bytes: string | Uint8Array) => createHash("sha256").update(bytes).digest("hex");
127
+ const oid = (value: string) => /^[a-f0-9]{40}$/.test(value);
128
+ const forbidden = ["commondir", "shallow", "info/grafts", "objects/info/alternates", "objects/info/http-alternates", "config.worktree"];
129
+ function readCommand(args: readonly string[]): boolean {
130
+ const ref = (value: string | undefined) => !!value && /^refs\/heads\/amem\/(?:u|p\/[a-f0-9]{16})$/.test(value);
131
+ if (args[0] === "config") return args.join("\0") === "config\0--local\0--no-includes\0--null\0--list";
132
+ if (args[0] === "cat-file") return args.length === 3 && ["-t", "-s", "blob", "commit", "tree"].includes(args[1] ?? "") && oid(args[2] ?? "");
133
+ if (args[0] === "rev-list") return args.length === 5 && args.slice(0, 4).join("\0") === "rev-list\0--topo-order\0--reverse\0--parents" && oid(args[4] ?? "");
134
+ if (args[0] === "symbolic-ref") return args.length === 3 && args[1] === "--quiet" && ref(args[2]);
135
+ return args[0] === "rev-parse" && (args.length === 2 && ["--is-bare-repository", "--show-object-format"].includes(args[1] ?? "")
136
+ || args.length === 4 && args[1] === "--verify" && args[2] === "--quiet" && ref(args[3]));
137
+ }
138
+
139
+ /** Bind the complete private cache inventory, not a mutable ref as evidence of a remote acknowledgement.
140
+ * ponytail: diagnostic cache metadata shares the 20000-entry/8MiB ceiling and the existing 256MiB replay-disk
141
+ * ceiling. Larger inventories need a reviewed immutable object inventory; no entry is silently skipped. */
142
+ function cacheInventory(cache: string): string {
143
+ const pending = [cache], rows: unknown[] = []; let metadataBytes = 0, diskBytes = 0;
144
+ const record = (file: string) => {
145
+ if (rows.length >= SYNC_SCREEN_MAX_ROWS) return bound();
146
+ const stat = lstatSync(file, { bigint: true });
147
+ if (stat.isSymbolicLink() || stat.uid !== BigInt(process.getuid?.() ?? -1) || (stat.mode & 0o077n) !== 0n
148
+ || !stat.isDirectory() && (!stat.isFile() || stat.nlink !== 1n) || file.endsWith(".promisor")) unavailable();
149
+ if (stat.isFile()) { diskBytes += Number(stat.size); if (diskBytes > PUBLIC_PRUNE_REPLAY_MAX_BYTES) bound(); }
150
+ const row = [file, stat.isDirectory() ? "directory" : "file", ...[stat.dev, stat.ino, stat.mode, stat.size, stat.mtimeNs, stat.ctimeNs].map(String)];
151
+ metadataBytes += Buffer.byteLength(canonicalJSON(row)); if (metadataBytes > SYNC_SCREEN_MAX_BYTES) bound(); rows.push(row);
152
+ return stat.isDirectory();
153
+ };
154
+ record(cache);
155
+ while (pending.length) {
156
+ const reader = opendirSync(pending.pop()!);
157
+ try { for (;;) { const entry = reader.readSync(); if (!entry) break; const file = join(reader.path, entry.name); if (record(file)) pending.push(file); } }
158
+ finally { reader.closeSync(); }
159
+ }
160
+ return hash(canonicalJSON(rows.sort((a, b) => canonicalJSON(a).localeCompare(canonicalJSON(b)))));
161
+ }
162
+
163
+ /** Fixed local Git reads only. No initialization, fetch, remote probing, write command, credential or lazy fetch. */
164
+ export class PlacementInspectionObjects {
165
+ readonly cache: string;
166
+ readonly remoteHash: string;
167
+ private readonly home: string;
168
+ private readonly stamp: string;
169
+ private readonly rootStamp: string;
170
+ private readonly inventory: string;
171
+ private readonly check: () => void;
172
+ private bytes = 0;
173
+ private sourceRows = 0;
174
+ private sourceBytes = 0;
175
+ private readonly sourcePacks = new Set<string>();
176
+ private readonly objects = new Map<string, { type: string; bytes: Buffer }>();
177
+ constructor(home: string, check: () => void) {
178
+ this.home = home; this.cache = join(home, "shard-sync-placement", "objects.git"); this.check = check;
179
+ this.rootStamp = placementCacheRootStamp(home);
180
+ this.inventory = cacheInventory(this.cache); this.stamp = placementCacheConfigStamp(this.cache);
181
+ this.quick();
182
+ const config = validatePlacementCacheConfig(this.text(["config", "--local", "--no-includes", "--null", "--list"]));
183
+ const origin = config.get("remote.origin.url");
184
+ if (!origin || origin.length > 4096 || origin.startsWith("-") || /[\r\n\0]/.test(origin) || config.has("remote.origin.pushurl") && config.get("remote.origin.pushurl") !== origin) unavailable();
185
+ this.remoteHash = hash(origin);
186
+ if (this.text(["rev-parse", "--is-bare-repository"]) !== "true" || this.text(["rev-parse", "--show-object-format"]) !== "sha1") unavailable();
187
+ this.assertCurrent();
188
+ }
189
+ private quick(): void {
190
+ this.check(); this.cacheCheck();
191
+ }
192
+ private cacheCheck(): void {
193
+ if (placementCacheRootStamp(this.home) !== this.rootStamp) throw new SyncPlacementJournalError("changed");
194
+ if (placementCacheConfigStamp(this.cache) !== this.stamp) throw new SyncPlacementJournalError("changed");
195
+ for (const file of forbidden) if (lstatSync(join(this.cache, file), { throwIfNoEntry: false })) unavailable();
196
+ }
197
+ private run(args: string[], maximum: number): { code: number; bytes: Buffer } {
198
+ this.quick();
199
+ if (!readCommand(args)) unavailable();
200
+ const env = memoryGitEnv(process.env, { repositoryIsolation: true, readOnlyDiagnostic: true });
201
+ const result = memoryGitSync(["--no-replace-objects", "--git-dir", this.cache, "-c", "protocol.allow=never", "-c", "maintenance.auto=false", "-c", "gc.auto=0", "-c", "core.commitGraph=false", ...args],
202
+ { encoding: "buffer", cwd: this.home, env, timeout: 10000, killSignal: "SIGKILL", maxBuffer: maximum });
203
+ this.quick();
204
+ if (result.error) { if ((result.error as NodeJS.ErrnoException).code === "ENOBUFS") bound(); unavailable(); }
205
+ return { code: result.status ?? -1, bytes: result.stdout };
206
+ }
207
+ private text(args: string[]): string {
208
+ const result = this.run(args, SYNC_SCREEN_MAX_BYTES); if (result.code !== 0) unavailable();
209
+ return new TextDecoder("utf-8", { fatal: true }).decode(result.bytes).trim();
210
+ }
211
+ ref(realm: ShardSyncRealm): string | undefined {
212
+ const ref = shardSyncRef(realm), symbolic = this.run(["symbolic-ref", "--quiet", ref], 65536);
213
+ if (symbolic.code !== 1) unavailable();
214
+ const result = this.run(["rev-parse", "--verify", "--quiet", ref], 65536), value = result.bytes.toString("utf8").trim();
215
+ if (result.code === 1) return undefined;
216
+ if (result.code !== 0 || !oid(value)) unavailable(); return value;
217
+ }
218
+ object(object: string, type: "commit" | "tree" | "blob", maximum: number): Buffer {
219
+ // ponytail: a diagnostic retains at most 20000 object identities within the existing 80MiB graph bound.
220
+ // Larger retained histories need a reviewed immutable digest inventory; physical row-pack limits stay separate.
221
+ if (!oid(object)) unavailable();
222
+ const prior = this.objects.get(object); if (prior) { if (prior.type !== type || prior.bytes.length > maximum) unavailable(); return Buffer.from(prior.bytes); }
223
+ if (this.objects.size >= SYNC_SCREEN_MAX_ROWS) bound();
224
+ if (this.text(["cat-file", "-t", object]) !== type) unavailable();
225
+ const size = Number(this.text(["cat-file", "-s", object]));
226
+ if (!Number.isSafeInteger(size) || size < 0 || size > maximum || this.bytes + size > PUBLIC_PRUNE_GRAPH_MAX_BYTES) bound();
227
+ const result = this.run(["cat-file", type, object], Math.max(1, maximum));
228
+ if (result.code !== 0 || result.bytes.length !== size || createHash("sha1").update(`${type} ${size}\0`).update(result.bytes).digest("hex") !== object) unavailable();
229
+ this.bytes += size; this.objects.set(object, { type, bytes: result.bytes }); return Buffer.from(result.bytes);
230
+ }
231
+ auditor(realm: ShardSyncRealm, generation: string): ShardHistoryAuditor {
232
+ // The byte cache is shared and bounded. Do not retain a full decoded body map for every historical receipt.
233
+ return new ShardHistoryAuditor({ realm, generation, placement: true, publicPrune: true, git: async args => this.text(args),
234
+ object: async (id, type) => this.object(id, type, type === "commit" ? 65536 : SHARD_PACK_MAX_BYTES),
235
+ blob: async (id, path) => {
236
+ const bytes = this.object(id, "blob", path === "shard.json" || path.startsWith("prune-witnesses/") ? 65536 : SHARD_PACK_MAX_BYTES);
237
+ const physical = canonicalJSON([realm, id]);
238
+ if (path.startsWith("rows/") && !this.sourcePacks.has(physical)) {
239
+ let lines = 0; for (const byte of bytes) if (byte === 10) lines++;
240
+ if (this.sourceRows + lines > SYNC_SCREEN_MAX_ROWS || this.sourceBytes + bytes.length > SYNC_SCREEN_MAX_BYTES) bound();
241
+ this.sourceRows += lines; this.sourceBytes += bytes.length; this.sourcePacks.add(physical);
242
+ }
243
+ return bytes;
244
+ } });
245
+ }
246
+ tree(commit: string): string {
247
+ const body = this.object(commit, "commit", 65536), first = body.subarray(0, body.indexOf(10)).toString("ascii");
248
+ if (!/^tree [a-f0-9]{40}$/.test(first)) unavailable(); return first.slice(5);
249
+ }
250
+ assertCurrent(): void {
251
+ this.check(); this.assertRetained();
252
+ }
253
+ /** Passive final proof: never invokes an attestation/policy callback after another input's final check. */
254
+ assertRetained(): void {
255
+ this.cacheCheck(); if (cacheInventory(this.cache) !== this.inventory) throw new SyncPlacementJournalError("changed");
256
+ }
257
+ }
@@ -0,0 +1,66 @@
1
+ /** Passive pending-prune namespace capture. It grants no recovery, writer or publication authority. */
2
+ import { lstatSync, opendirSync, type BigIntStats } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { types } from "node:util";
5
+ import { canonicalJSON, hashJSON } from "./generation.ts";
6
+ import { realPath } from "./platform.ts";
7
+ import { SYNC_SCREEN_MAX_BYTES, SYNC_SCREEN_MAX_ROWS } from "./sync-screen.ts";
8
+ import { SyncPlacementJournalError, type SyncPlacementJournalErrorCode } from "./sync-placement-journal-format.ts";
9
+
10
+ function fail(code: SyncPlacementJournalErrorCode = "corrupt"): never { throw new SyncPlacementJournalError(code); }
11
+ function owned(stat: BigIntStats, directory: boolean): void {
12
+ if (stat.isSymbolicLink() || stat.uid !== BigInt(process.getuid?.() ?? -1) || (stat.mode & 0o077n) !== 0n
13
+ || (directory ? !stat.isDirectory() : !stat.isFile() || stat.nlink !== 1n)) fail();
14
+ }
15
+ const stamp = (stat: BigIntStats) => [stat.dev, stat.ino, stat.uid, stat.mode, stat.size, stat.mtimeNs, stat.ctimeNs].map(String);
16
+ function parentIdentity(target: string): string {
17
+ const stat = lstatSync(target, { bigint: true }); owned(stat, true);
18
+ // Derived indexes may change the target directory; its identity/ownership, rather than unrelated contents, is binding.
19
+ return canonicalJSON([stat.dev, stat.ino, stat.uid, stat.mode].map(String));
20
+ }
21
+ function snapshot(target: string): string {
22
+ const dir = join(target, "prune-plans"), before = lstatSync(dir, { bigint: true, throwIfNoEntry: false });
23
+ if (!before) return "absent";
24
+ owned(before, true);
25
+ const rows: string[][] = []; let count = 0, metadataBytes = Buffer.byteLength(canonicalJSON({ directory: stamp(before), entries: [] }));
26
+ const iterator = opendirSync(dir);
27
+ try {
28
+ for (;;) {
29
+ const entry = iterator.readSync(); if (!entry) break;
30
+ if (++count > SYNC_SCREEN_MAX_ROWS) fail("bound");
31
+ // Refuse incomplete/malformed claim names too. No content is read, decoded, completed or removed.
32
+ if (entry.name.startsWith("pending-local-") || entry.name.startsWith("pending-log-")) fail("pending");
33
+ const stat = lstatSync(join(dir, entry.name), { bigint: true }); owned(stat, stat.isDirectory());
34
+ const row = [entry.name, stat.isDirectory() ? "directory" : "file", ...stamp(stat)];
35
+ metadataBytes += Buffer.byteLength(canonicalJSON(row)) + (rows.length ? 1 : 0); if (metadataBytes > SYNC_SCREEN_MAX_BYTES) fail("bound");
36
+ rows.push(row);
37
+ }
38
+ } finally { iterator.closeSync(); }
39
+ const after = lstatSync(dir, { bigint: true }); owned(after, true);
40
+ if (canonicalJSON(stamp(before)) !== canonicalJSON(stamp(after))) fail("changed");
41
+ rows.sort((a, b) => a[0]! < b[0]! ? -1 : a[0]! > b[0]! ? 1 : 0);
42
+ return hashJSON({ directory: stamp(before), entries: rows });
43
+ }
44
+ function safeError(error: unknown, final: boolean): never {
45
+ if (error && typeof error === "object" && !types.isProxy(error) && error instanceof SyncPlacementJournalError) {
46
+ const code = Object.getOwnPropertyDescriptor(error, "code");
47
+ if (code && "value" in code && ["bound", "pending", "changed", "corrupt"].includes(code.value))
48
+ return fail(final && code.value !== "bound" ? "changed" : code.value);
49
+ }
50
+ return fail(final ? "changed" : "corrupt");
51
+ }
52
+ /** Captures the selected target's namespace and returns a no-effect final recheck.
53
+ * ponytail: at most20000 direct metadata entries/8MiB of canonical name/stat metadata may be captured. Saved plan
54
+ * bodies are never read; a reviewed streaming namespace proof is required before this inventory ceiling can grow. */
55
+ export function captureInspectionPendingPrunes(targetDir: string): () => void {
56
+ try {
57
+ const original = parentIdentity(targetDir), target = realPath(targetDir);
58
+ if (parentIdentity(target) !== original) fail("changed");
59
+ const before = snapshot(target);
60
+ if (parentIdentity(target) !== original) fail("changed");
61
+ return () => {
62
+ try { if (parentIdentity(target) !== original || snapshot(target) !== before || parentIdentity(target) !== original) fail("changed"); }
63
+ catch (error) { safeError(error, true); }
64
+ };
65
+ } catch (error) { return safeError(error, false); }
66
+ }
@@ -0,0 +1,240 @@
1
+ /** IDs-only current placement decisions and separately verified historical exposure. No effects or network. */
2
+ import { createHash } from "node:crypto";
3
+ import { lstatSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { types } from "node:util";
6
+ import { SegmentReadBudgetError } from "./bridge.ts";
7
+ import { canonicalJSON, hashJSON, selectorRevision } from "./generation.ts";
8
+ import { assertFormatFloor, FormatFloorError } from "./formats.ts";
9
+ import { readPlacementRuntimeMarkers, SyncPlacementRuntimeError } from "./sync-placement-guard.ts";
10
+ import { readActivePlacementBinding } from "./sync-placement-active.ts";
11
+ import { PLACEMENT_FORMAT } from "./sync-placement-format.ts";
12
+ import { captureSyncPlacementContext, type PlacementCapturePort } from "./sync-placement-capture.ts";
13
+ import { placementKeyId, SyncPlacementJournalError, type PlacementKey, type PlacementHoldReason } from "./sync-placement-journal-format.ts";
14
+ import { materializePlacementRows, verifiedPlacementPrunes } from "./sync-placement-materialize.ts";
15
+ import { placementCacheRootStamp } from "./sync-placement-cache.ts";
16
+ import { captureInspectionJournal, inspectRetainedPublicationHistories, PlacementInspectionObjects, type InspectionExposureProof } from "./sync-placement-inspection-evidence.ts";
17
+ import { outboundPolicyEvaluator, type RowScreen, type SyncPolicyCategory } from "./sync-screen.ts";
18
+ import { explicitExpirySnapshot } from "./generation-index.ts";
19
+ import { reducePruneRows, type PruneRow } from "./prune.ts";
20
+ import { readPublicPruneViewForActivation } from "./public-prune-view.ts";
21
+ import { reducePublicPruneInputs } from "./public-prune-inputs.ts";
22
+ import { inspectPublisherPruneLineages } from "./prune-publisher-lineage.ts";
23
+ import type { PublicPruneLineage } from "./public-prune-witness.ts";
24
+ import type { PlacementPublicationDecisionReason } from "./sync-placement-publication-plan.ts";
25
+ import type { ShardSyncRealm, ShardSyncRow } from "./shard-sync-format.ts";
26
+ import type { MemoryStore } from "./store.ts";
27
+ import { captureInspectionPendingPrunes } from "./sync-placement-inspection-pending.ts";
28
+ import { SharedWriterRevisionError } from "./sync-placement-writer-revision-format.ts";
29
+
30
+ export interface SyncPlacementInspectionOptions {
31
+ screen?: RowScreen;
32
+ policyVersion?: () => string;
33
+ /** Same fresh writer-pin binding as v3 sync; this read-only port grants no effect or activation authority. */
34
+ placementAttestation?: PlacementCapturePort["attest"];
35
+ }
36
+ export type SyncPlacementDisposition = "eligible" | "policy-local" | "person-local" | "held";
37
+ export type SyncPlacementExposureEvidence = InspectionExposureProof;
38
+ export type SyncPlacementExposure = { certainty: "verified-published"; evidence: SyncPlacementExposureEvidence[] }
39
+ | { certainty: "not-observed" | "unknown" };
40
+ export interface SyncPlacementInspectionRow extends PlacementKey {
41
+ sourcePlacement: "local" | "synced";
42
+ disposition: SyncPlacementDisposition;
43
+ reason: PlacementPublicationDecisionReason;
44
+ categories: SyncPolicyCategory[];
45
+ holdReasons: PlacementHoldReason[];
46
+ exposure: SyncPlacementExposure;
47
+ }
48
+ export interface SyncPlacementIntrinsicLocalRow extends PlacementKey {
49
+ sourcePlacement: "local";
50
+ reason: "intrinsic-local";
51
+ categories: SyncPolicyCategory[];
52
+ holdReasons: PlacementHoldReason[];
53
+ exposure: SyncPlacementExposure;
54
+ }
55
+ export type SyncPlacementInspectionIncompleteReason = "placement-not-active" | "binding-changed" | "input-unavailable" | "input-bound" | "input-changed"
56
+ | "journal-unavailable" | "policy-unavailable" | "attestation-unavailable" | "evidence-unavailable" | "reader-upgrade-required";
57
+ export type SyncPlacementInspection = {
58
+ complete: true; rows: SyncPlacementInspectionRow[]; intrinsicLocal: SyncPlacementIntrinsicLocalRow[]; fingerprint: string;
59
+ counts: { selected: number; rows: number; intrinsicLocal: number; eligible: number; policyLocal: number; personLocal: number; held: number; verifiedPublished: number; notObserved: number; unknown: number };
60
+ } | { complete: false; reason: SyncPlacementInspectionIncompleteReason; nextStep: string };
61
+
62
+ const categories: readonly SyncPolicyCategory[] = ["secret", "personal-data", "deny-path", "private-or-invalid", "host"];
63
+ const remedies: Record<SyncPlacementInspectionIncompleteReason, string> = {
64
+ "placement-not-active": "Placement is not active; complete the person-reviewed shard/public-prune migrations and separately authorized placement activation before inspecting placement.",
65
+ "binding-changed": "Selected placement bindings changed; inspect the retained activation to restore matching markers when required, then reopen the selected project/user handles and inspect again.",
66
+ "input-unavailable": "Selected source or placement-context inputs are unreadable, incomplete or unsupported; inspect retained source metadata and any recorded partial request, verify compatible writer pins, then inspect again.",
67
+ "input-bound": "Placement inspection exceeds its supported 8 MiB/20000-row or retained-evidence bounds; request a person-reviewed bounded epoch transition before inspecting again.",
68
+ "input-changed": "The selected placement inspection context changed; wait for stable source, journal and policy bindings, then inspect again.",
69
+ "journal-unavailable": "The retained placement journal is incomplete or inconsistent; preserve it and resume its exact recorded request with a compatible runtime before inspecting again.",
70
+ "policy-unavailable": "The policy callback/version or configured/default-deny epoch is unavailable or changed; restore a stable policy and policyVersion before inspecting again.",
71
+ "attestation-unavailable": "Fresh placement writer-pin/quiescence attestation is missing or invalid; supply the matching placementAttestation callback and inspect again.",
72
+ "evidence-unavailable": "Retained publication evidence is unreadable or inconsistent; check the private cache's ownership, permissions, links and exact objects, restore compatible retained evidence, then use a separately authorized sync to reverify before inspecting again.",
73
+ "reader-upgrade-required": "The configured sync format floor refuses the loaded reader; install a compatible newer runtime, reopen the selected handles and inspect again.",
74
+ };
75
+ class Incomplete extends Error {
76
+ constructor(readonly reason: SyncPlacementInspectionIncompleteReason) { super(remedies[reason]); }
77
+ }
78
+ function fail(reason: SyncPlacementInspectionIncompleteReason): never { throw new Incomplete(reason); }
79
+ const digest = (value: string) => createHash("sha256").update(value).digest("hex");
80
+ function physical(realm: ShardSyncRealm): PlacementKey["realm"] { return realm.scope === "user" ? { scope: "user" } : { scope: "project", keyHash: digest(realm.key) }; }
81
+ function keyId(key: PlacementKey): string { return placementKeyId({ realm: key.realm, id: key.id, rowHash: key.rowHash }); }
82
+ function callbacks(input: SyncPlacementInspectionOptions): SyncPlacementInspectionOptions {
83
+ if (!input || typeof input !== "object" || types.isProxy(input) || ![Object.prototype, null].includes(Object.getPrototypeOf(input))) fail("policy-unavailable");
84
+ const fields = Object.getOwnPropertyDescriptors(input);
85
+ const read = <K extends keyof SyncPlacementInspectionOptions>(key: K): SyncPlacementInspectionOptions[K] => {
86
+ const field = fields[key]; if (!field) return undefined;
87
+ if (!("value" in field) || !field.enumerable || field.value !== undefined && typeof field.value !== "function") fail("policy-unavailable");
88
+ return field.value as SyncPlacementInspectionOptions[K];
89
+ };
90
+ const result = { screen: read("screen"), policyVersion: read("policyVersion"), placementAttestation: read("placementAttestation") };
91
+ if (result.screen && !result.policyVersion) fail("policy-unavailable"); return result;
92
+ }
93
+ function inactive(store: MemoryStore): Extract<SyncPlacementInspection, { complete: false }> {
94
+ const steps: string[] = [];
95
+ if (!store.profile.sharded) steps.push("Have the person run amem migrate --shards --dry-run, review the plan, then amem migrate --shards --apply.");
96
+ if (!store.profile.foreignPruneFormat) steps.push("Have the person run amem migrate --public-prune-view --dry-run, review the plan, then amem migrate --public-prune-view --apply.");
97
+ steps.push("Complete separately person-authorized placement activation with compatible exact writer pins; this stage provides no placement activation command or marker writer.");
98
+ return { complete: false, reason: "placement-not-active", nextStep: "Placement is not active. " + steps.join(" ") };
99
+ }
100
+
101
+ export async function inspectSyncPlacement(store: MemoryStore, input: SyncPlacementInspectionOptions = {}): Promise<SyncPlacementInspection> {
102
+ let phase: SyncPlacementInspectionIncompleteReason = "input-unavailable";
103
+ let captured = false;
104
+ try {
105
+ const options = callbacks(input), home = store.profile.home;
106
+ assertFormatFloor(home, "sync");
107
+ const opened = [...store.openedSyncTargets()];
108
+ const binding = () => hashJSON(store.openedSyncTargets().map(target => ({ profile: target.profile, machine: target.machine,
109
+ selector: selectorRevision(target.profile.home) })));
110
+ if (opened.some(target => selectorRevision(target.profile.home) !== target.profile.selectorRevision)) fail("binding-changed");
111
+ const beforeBinding = binding();
112
+ if (store.profile.publicationFormat !== PLACEMENT_FORMAT) return inactive(store);
113
+ if (!options.placementAttestation) fail("attestation-unavailable");
114
+ if (!opened.length || opened.length > 2 || !opened.includes(store) || opened.some(target => target.profile.home !== home || !target.profile.sharded
115
+ || !target.profile.generation || !target.profile.shardUser && target.profile.shardKey !== store.profile.shardKey)) fail("binding-changed");
116
+ const markers = readPlacementRuntimeMarkers(home), active = readActivePlacementBinding(home, markers.profile, markers.shards);
117
+ if (!active || active.profile.placementHead !== store.profile.placementHead) fail("binding-changed");
118
+ const targets = opened.map(target => ({ store: target, realm: target.profile.shardUser ? { scope: "user" as const } : { scope: "project" as const, key: target.profile.shardKey! }, generation: target.profile.generation! }));
119
+ const pendingChecks = targets.map(target => captureInspectionPendingPrunes(target.store.dir));
120
+ const port: PlacementCapturePort = { attest: options.placementAttestation, ...(options.policyVersion ? { policyVersion: options.policyVersion } : {}) };
121
+ const capture = captureSyncPlacementContext(home, port, targets.map(target => physical(target.realm)));
122
+ captured = true;
123
+ if (capture.activeHead !== active.profile.placementHead || canonicalJSON(capture.context.writers) !== canonicalJSON(capture.writerInventory?.writers??active.activation.writers)) fail("attestation-unavailable");
124
+ phase = "journal-unavailable"; const retained = captureInspectionJournal(home, active.profile.placementHead);
125
+ const check = () => {
126
+ if (binding() !== beforeBinding) fail("binding-changed"); retained.assertCurrent(); capture.assertUnchanged();
127
+ if (binding() !== beforeBinding) fail("binding-changed");
128
+ };
129
+ phase = "evidence-unavailable";
130
+ const cacheRoot = join(home, "shard-sync-placement"), rootStamp = lstatSync(cacheRoot, { throwIfNoEntry: false }) ? placementCacheRootStamp(home) : undefined;
131
+ const cache = join(cacheRoot, "objects.git"), cacheAbsent = !lstatSync(cache, { throwIfNoEntry: false });
132
+ const objects = cacheAbsent ? undefined : new PlacementInspectionObjects(home, check);
133
+ const histories = await inspectRetainedPublicationHistories(retained, targets, objects);
134
+ const cached = targets.map(target => objects?.ref(target.realm));
135
+ const view = readPublicPruneViewForActivation(home, active.profile.pruneInputSetHash, active.profile.generation);
136
+ const foreignChecks: (() => void)[] = [];
137
+ phase = "input-unavailable";
138
+ const plannedTargets = targets.map((target, index) => {
139
+ const realm = physical(target.realm), id = canonicalJSON(realm), sources = capture.sourceRowsByRealm.get(id);
140
+ if (!sources) fail("binding-changed");
141
+ const prunes = [...capture.specialRows.values()].filter(source => canonicalJSON(source.key.realm) === id && source.row.op === "prune");
142
+ const reduced = reducePruneRows(prunes.map(source => ({ row: source.row, placement: source.sourcePlacement })), { rows: sources, expiries: explicitExpirySnapshot(sources) }, target.generation);
143
+ if (reduced.invalid || reduced.pending.length) fail("input-unavailable");
144
+ const inputSetHash = view.realms[target.realm.scope === "user" ? "user" : `project:${target.realm.key}`];
145
+ let pruned = reduced.targets;
146
+ if (inputSetHash) {
147
+ const replay = () => reducePublicPruneInputs({ dir: target.store.dir, realm: target.realm, receivingGeneration: target.generation, inputSetHash,
148
+ publicationFormat: PLACEMENT_FORMAT, nativeTargets: [...reduced.targets], ordinary: sources.map(source => source.row) });
149
+ const before = replay(); pruned = before.targets;
150
+ const identity = (value: ReturnType<typeof replay>) => hashJSON({ targets: [...value.targets].sort(), rows: [...value.rowHashes].sort(), inputs: value.inputs, commitments: value.commitments });
151
+ const fingerprint = identity(before); foreignChecks.push(() => { if (identity(replay()) !== fingerprint) fail("input-changed"); });
152
+ }
153
+ const localPrunes = new Map(prunes.filter(source => source.sourcePlacement === "synced").map(source => [source.key.id, source.row as PruneRow]));
154
+ return { realm: target.realm, generation: target.generation, remote: histories.tips[index], local: cached[index], fetched: new Set(histories.old[index]?.rows.keys()), localPrunes, pruned };
155
+ });
156
+ const lineages = targets.map((target, index) => {
157
+ const rows = new Map<string, ShardSyncRow>(histories.old[index]?.rows);
158
+ for (const source of [...capture.rows.values(), ...capture.specialRows.values()]) if (canonicalJSON(source.key.realm) === canonicalJSON(physical(target.realm)) && source.row.op !== "vault_link") {
159
+ const prior = rows.get(source.key.id); if (prior && hashJSON(prior) !== source.key.rowHash) fail("input-unavailable"); rows.set(source.key.id, source.row);
160
+ }
161
+ const candidates = [...rows.values()].filter((row): row is PruneRow => row.op === "prune" && row.generation === target.generation);
162
+ const boundary = join(target.store.dir, "local", `publisher-boundary-${target.store.machine.id}.json`);
163
+ const proof = lstatSync(boundary, { throwIfNoEntry: false }) ? inspectPublisherPruneLineages(target.store.profile, candidates) : new Map<string, PublicPruneLineage>();
164
+ return { rows, proof };
165
+ });
166
+ const verified = verifiedPlacementPrunes(targets.map((target, index) => ({ realm: target.realm, generation: target.generation, rows: lineages[index]!.rows,
167
+ fetched: plannedTargets[index]!.fetched, lineageIds: new Set<string>(lineages[index]!.proof.keys()) })));
168
+ const rowCategories = new Map<string, Set<SyncPolicyCategory>>();
169
+ phase = "policy-unavailable"; const evaluate = outboundPolicyEvaluator(home, options.screen);
170
+ phase = "input-unavailable";
171
+ const guardedEvaluate = (value: unknown): ReturnType<typeof evaluate> => {
172
+ check(); let verdict: ReturnType<typeof evaluate>;
173
+ try { verdict = evaluate(structuredClone(value) as Record<string, unknown>); } catch { return fail("policy-unavailable"); }
174
+ check();
175
+ if (!verdict.ok && value && typeof value === "object" && "id" in value && typeof value.id === "string") {
176
+ let known = rowCategories.get(value.id); if (!known) rowCategories.set(value.id, known = new Set()); known.add(verdict.category);
177
+ }
178
+ return verdict;
179
+ };
180
+ const materialized = await materializePlacementRows({ capture, projection: retained.projection, generation: active.profile.generation, targets: plannedTargets,
181
+ old: histories.old, verifiedPrunes: verified, createdUnixSeconds: Math.floor(Date.now() / 1000), evaluate: guardedEvaluate,
182
+ readTree: async (_index, head) => { if (!objects) fail("evidence-unavailable"); return objects.tree(head); },
183
+ publisherLineages: (index, rows) => inspectPublisherPruneLineages(targets[index]!.store.profile, rows) });
184
+ const exposureFor = (key: PlacementKey): SyncPlacementExposure => {
185
+ const known = histories.published.get(placementKeyId(key)), attempts = retained.projection.get(key)?.attempts;
186
+ return known?.length ? { certainty: "verified-published", evidence: known } : { certainty: attempts?.length ? "unknown" : "not-observed" };
187
+ };
188
+ const rows: SyncPlacementInspectionRow[] = materialized.decisions.map(decision => {
189
+ const state = retained.projection.get(decision.key), holdReasons = state?.reasons ?? [], own = new Set<SyncPolicyCategory>(rowCategories.get(decision.key.id));
190
+ for (const value of [decision.reason, ...holdReasons]) if (categories.includes(value as SyncPolicyCategory)) own.add(value as SyncPolicyCategory);
191
+ const disposition: SyncPlacementDisposition = decision.eligible ? "eligible" : holdReasons.includes("person-local") ? "person-local"
192
+ : decision.reason !== "source-local" && own.size ? "policy-local" : "held";
193
+ return { ...decision.key, sourcePlacement: decision.sourcePlacement, disposition, reason: decision.reason,
194
+ categories: [...own].sort(), holdReasons: [...holdReasons].sort(), exposure: exposureFor(decision.key) };
195
+ });
196
+ const outboundKeys = new Set(rows.map(keyId));
197
+ const intrinsicLocal: SyncPlacementIntrinsicLocalRow[] = [];
198
+ for (const source of capture.specialRows.values()) if (!outboundKeys.has(placementKeyId(source.key))) {
199
+ if (source.sourcePlacement !== "local") fail("input-unavailable");
200
+ guardedEvaluate(source.row);
201
+ const holdReasons = retained.projection.get(source.key)?.reasons ?? [], own = new Set<SyncPolicyCategory>(rowCategories.get(source.key.id));
202
+ for (const reason of holdReasons) if (categories.includes(reason as SyncPolicyCategory)) own.add(reason as SyncPolicyCategory);
203
+ intrinsicLocal.push({ ...source.key, sourcePlacement: "local", reason: "intrinsic-local", categories: [...own].sort(), holdReasons: [...holdReasons].sort(), exposure: exposureFor(source.key) });
204
+ }
205
+ intrinsicLocal.sort((a, b) => keyId(a).localeCompare(keyId(b)));
206
+ // The final recapture invokes host callbacks first; every remaining proof is passive filesystem/data validation.
207
+ phase = "input-changed"; capture.assertCurrent(); retained.assertCurrent(); for (const checkPending of pendingChecks) checkPending(); for (const checkForeign of foreignChecks) checkForeign(); objects?.assertRetained();
208
+ if (cacheAbsent && lstatSync(cache, { throwIfNoEntry: false })) fail("input-changed");
209
+ if (rootStamp !== undefined ? placementCacheRootStamp(home) !== rootStamp : lstatSync(cacheRoot, { throwIfNoEntry: false }) !== undefined) fail("input-changed");
210
+ if (binding() !== beforeBinding) fail("binding-changed");
211
+ assertFormatFloor(home, "sync");
212
+ const counts = { selected: rows.length + intrinsicLocal.length, rows: rows.length, intrinsicLocal: intrinsicLocal.length, eligible: 0, policyLocal: 0, personLocal: 0, held: 0, verifiedPublished: 0, notObserved: 0, unknown: 0 };
213
+ for (const row of rows) {
214
+ counts[row.disposition === "policy-local" ? "policyLocal" : row.disposition === "person-local" ? "personLocal" : row.disposition]++;
215
+ }
216
+ for (const row of [...rows, ...intrinsicLocal]) {
217
+ counts[row.exposure.certainty === "verified-published" ? "verifiedPublished" : row.exposure.certainty === "not-observed" ? "notObserved" : "unknown"]++;
218
+ }
219
+ return { complete: true, rows, intrinsicLocal, counts, fingerprint: hashJSON({ rows, intrinsicLocal }) };
220
+ } catch (error) {
221
+ let reason: SyncPlacementInspectionIncompleteReason = phase;
222
+ if (error && typeof error === "object" && !types.isProxy(error)) {
223
+ if(error instanceof SharedWriterRevisionError){const code=Object.getOwnPropertyDescriptor(error,"code")?.value;
224
+ if(code==="staging"||code==="pending")return{complete:false,reason:"journal-unavailable",nextStep:new SharedWriterRevisionError(code).message};
225
+ }
226
+ if (error instanceof Incomplete) reason = error.reason;
227
+ else if (error instanceof FormatFloorError) {
228
+ if (error.code === "format_floor") reason = "reader-upgrade-required";
229
+ else return { complete: false, reason: "input-unavailable", nextStep: "The sync reader-floor declaration is unreadable or unsupported; inspect and restore the person-approved declaration with a compatible runtime, then inspect again." };
230
+ }
231
+ else if (error instanceof SyncPlacementRuntimeError) reason = "binding-changed";
232
+ else if (error instanceof SegmentReadBudgetError) reason = "input-bound";
233
+ else if (error instanceof SyncPlacementJournalError) {
234
+ if (error.code === "bound") reason = "input-bound";
235
+ else if (error.code === "changed") reason = captured ? "input-changed" : "input-unavailable";
236
+ }
237
+ }
238
+ return { complete: false, reason, nextStep: remedies[reason] };
239
+ }
240
+ }