@crewhaus/tool-lifecycle 0.7.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.
@@ -0,0 +1,131 @@
1
+ import { type Redactor, type SharedFragment, type SharedMemory } from "@crewhaus/harness-lifecycle";
2
+ import { type Loaded } from "./result";
3
+ export declare const NO_PII_PASS_REASON: string;
4
+ /**
5
+ * The redactor this build can supply: the package's own credential masking
6
+ * and its strict post-mask rescan, with an identity PII pass.
7
+ *
8
+ * `IDENTITY_REDACTOR` from the package is deliberately NOT used — it never
9
+ * flags anything, so a memory holding an API key would be pushed verbatim.
10
+ */
11
+ export declare function makeRedactor(): Redactor;
12
+ /**
13
+ * The shared store's on-disk layout, as `applyPush` / `readSharedFragments`
14
+ * write and read it. Mirrored once here so the audit, the containment check
15
+ * and the post-push verification all name the same file rather than each
16
+ * spelling the rule out again.
17
+ */
18
+ export declare const SHARED_MEMORIES_RELPATH = "memories.jsonl";
19
+ export declare const SHARED_MANIFEST_RELPATH = "manifest.jsonl";
20
+ export declare function sharedFragmentRelPath(frag: {
21
+ readonly kind: "grader" | "prompt";
22
+ readonly contentHash: string;
23
+ }): string;
24
+ export type Rejected = {
25
+ readonly at: string;
26
+ readonly reason: string;
27
+ };
28
+ export type SharedMemoryAudit = {
29
+ readonly memories: ReadonlyArray<SharedMemory>;
30
+ readonly rejected: ReadonlyArray<Rejected>;
31
+ readonly linesSeen: number;
32
+ };
33
+ /**
34
+ * Read the shared `memories.jsonl` through the package's own validator, and
35
+ * keep the rejects instead of dropping them.
36
+ *
37
+ * A missing file is an empty store and says so; an unreadable one is a
38
+ * failure and says THAT — a shared store that cannot be read is not a shared
39
+ * store with nothing in it.
40
+ */
41
+ export declare function auditSharedMemories(sharedDirAbs: string): Loaded<SharedMemoryAudit>;
42
+ export type SharedFragmentAudit = {
43
+ readonly fragments: ReadonlyArray<SharedFragment>;
44
+ readonly rejected: ReadonlyArray<Rejected>;
45
+ };
46
+ /**
47
+ * Fragments of one kind, with the files the package's validation refused.
48
+ *
49
+ * The valid set comes from `readSharedFragments`, so the accept/reject rule
50
+ * stays in one place; what is derived here is only WHICH candidate files did
51
+ * not survive it. Re-deriving the rule to explain each rejection would be a
52
+ * second copy of it, and the second copy is the one that goes stale.
53
+ */
54
+ export declare function auditSharedFragments(sharedDirAbs: string, kind: "grader" | "prompt"): Loaded<SharedFragmentAudit>;
55
+ /**
56
+ * The tag `applyPull` appends to every memory it lands
57
+ * (`shared:<source harness>`).
58
+ *
59
+ * It matters because the content hash covers text AND tags: a memory pulled
60
+ * from the shared store lands with one more tag than the record it came from,
61
+ * so hashing what is on disk does NOT reproduce the shared record's hash. The
62
+ * CLI computes the harness's hashes without accounting for it
63
+ * (`apps/cli/src/index.ts`, the `knowledge pull` branch), which is why a
64
+ * second pull re-imports everything and `shared.jsonl` grows on every run.
65
+ *
66
+ * `harnessMemoryHashes` below closes that by hashing a landed memory BOTH
67
+ * ways. The hashing rule itself is still the package's `memoryContentHash`;
68
+ * what is mirrored here is only the tag `applyPull` writes, and
69
+ * `index.test.ts` pins it — a second pull must bring in nothing.
70
+ */
71
+ export declare const SHARED_TAG_PREFIX = "shared:";
72
+ /**
73
+ * Every content hash the harness already holds, counting a pulled memory
74
+ * under its shared hash as well as its local one.
75
+ *
76
+ * The undo is EXACTLY `applyPull`'s do: it appends ONE provenance tag to the
77
+ * end of the record's tags, so exactly one trailing tag is taken back off.
78
+ * Stripping every `shared:`-prefixed tag instead — which is what this did —
79
+ * is a different function, and it is wrong for the record that already
80
+ * carried provenance of its own (one harness pulled a memory and pushed it
81
+ * on, so the shared record's own tags include a `shared:` tag). For that
82
+ * record neither form reproduced the shared hash, so the FIRST pull reported
83
+ * `verified: false` with the memory listed as missing although it had landed
84
+ * perfectly, and every later pull re-imported it — the unbounded growth this
85
+ * mirror exists to prevent, reintroduced by the mirror.
86
+ */
87
+ export declare function harnessMemoryHashes(memories: ReadonlyArray<{
88
+ readonly text: string;
89
+ readonly tags: ReadonlyArray<string>;
90
+ }>): Set<string>;
91
+ /**
92
+ * Where `applyPull` lands each artifact inside a harness.
93
+ *
94
+ * Mirrored, not re-derived: the package chooses these paths and does not
95
+ * export them, and a tool that cannot name them can neither contain the
96
+ * writes (a symlink at one of them sends the file out of the workspace) nor
97
+ * check that they arrived. `index.test.ts` pins every one of them against a
98
+ * real pull, so a rename upstream fails a test here rather than silently
99
+ * turning the checks into no-ops.
100
+ */
101
+ export declare const PULLED_MEMORIES_RELPATH = ".crewhaus/memories/shared.jsonl";
102
+ export declare const PULLED_GRADER_PREFIX = "graders.shared-";
103
+ export declare const PULLED_GRADER_SUFFIX = ".yaml";
104
+ export declare function pulledFragmentRelPath(frag: {
105
+ readonly kind: "grader" | "prompt";
106
+ readonly contentHash: string;
107
+ }): string;
108
+ /**
109
+ * Content hashes of the grader fragments a previous pull already landed.
110
+ *
111
+ * `readHarnessGraders` looks only at `graders.yaml` / `.crewhaus/graders.yaml`
112
+ * — by design, those are the harness's OWN grader files — so a fragment
113
+ * `applyPull` wrote to `graders.shared-<hash>.yaml` is invisible to it, and
114
+ * every later pull counted that fragment as new and reported pulling it
115
+ * again. Matched by the filename shape and hashed with the package's own
116
+ * `fragmentContentHash`, so the accept rule stays the package's; only the
117
+ * landing name is mirrored.
118
+ */
119
+ export declare function landedSharedGraderHashes(harnessDirAbs: string): Loaded<Set<string>>;
120
+ export type HarnessMemoryAudit = {
121
+ readonly dirPresent: boolean;
122
+ readonly files: number;
123
+ readonly linesSeen: number;
124
+ };
125
+ /**
126
+ * How many lines the harness's memory files hold, so a store whose lines are
127
+ * mostly unparseable is not reported as a store with few memories. The parsed
128
+ * count comes from the package's `readHarnessMemories`; the difference
129
+ * between the two is the interesting number.
130
+ */
131
+ export declare function auditHarnessMemoryFiles(harnessDirAbs: string): Loaded<HarnessMemoryAudit>;
@@ -0,0 +1,283 @@
1
+ /**
2
+ * `KnowledgeSync`'s half: accounting for what a shared store contains that
3
+ * could not be used, and wiring the redactor this build can honestly offer.
4
+ *
5
+ * The rules all belong to `@crewhaus/harness-lifecycle`: what a valid shared
6
+ * memory is (`validateSharedMemory`, including the re-hash that defeats
7
+ * dedupe-poisoning), what a credential looks like (`maskKnowledgeSecrets` /
8
+ * `looksLikeSecret`), what to push (`planPush`) and what to pull
9
+ * (`planPull`). None of it is restated here.
10
+ *
11
+ * Two things are added.
12
+ *
13
+ * FIRST, SKIPS ARE COUNTED. The package's readers drop an invalid shared
14
+ * record with a warning to stderr and return the rest, which is right for a
15
+ * CLI and wrong for a tool: a caller handed `{"memories": 12}` has no way to
16
+ * know that 40 more were rejected as forged. A shared store is untrusted
17
+ * input — the count of what it offered and could not be used is part of the
18
+ * answer, not noise.
19
+ *
20
+ * SECOND, THE REDACTION GAP IS NAMED. Production wires
21
+ * `buildKnowledgeRedactor` around `@crewhaus/pii-redactor`. That package is
22
+ * not a dependency here, so this build runs the credential half — masking,
23
+ * and the strict post-mask rescan that DROPS anything still secret-shaped —
24
+ * with an identity PII pass. Names, emails and phone numbers are therefore
25
+ * NOT redacted, and a push says so and requires the caller to acknowledge it
26
+ * rather than quietly shipping a weaker guarantee under the same name.
27
+ */
28
+ import { readFileSync, readdirSync } from "node:fs";
29
+ import * as path from "node:path";
30
+ import { buildKnowledgeRedactor, fragmentContentHash, memoryContentHash, readSharedFragments, validateSharedMemory, } from "@crewhaus/harness-lifecycle";
31
+ import { fail } from "./result";
32
+ export const NO_PII_PASS_REASON = "@crewhaus/pii-redactor is not a dependency of @crewhaus/tool-lifecycle, so the PII pass of the " +
33
+ "knowledge redactor is an identity function here: credential-shaped tokens ARE masked and any " +
34
+ "artifact still secret-shaped afterwards is dropped, but names, emails, addresses and phone " +
35
+ "numbers are NOT removed. Pass allowWithoutPiiRedaction to push on those terms.";
36
+ /**
37
+ * The redactor this build can supply: the package's own credential masking
38
+ * and its strict post-mask rescan, with an identity PII pass.
39
+ *
40
+ * `IDENTITY_REDACTOR` from the package is deliberately NOT used — it never
41
+ * flags anything, so a memory holding an API key would be pushed verbatim.
42
+ */
43
+ export function makeRedactor() {
44
+ return buildKnowledgeRedactor(async (text) => text);
45
+ }
46
+ // ---------------------------------------------------------------------------
47
+ // shared store, with the rejects counted
48
+ // ---------------------------------------------------------------------------
49
+ /**
50
+ * The shared store's on-disk layout, as `applyPush` / `readSharedFragments`
51
+ * write and read it. Mirrored once here so the audit, the containment check
52
+ * and the post-push verification all name the same file rather than each
53
+ * spelling the rule out again.
54
+ */
55
+ export const SHARED_MEMORIES_RELPATH = "memories.jsonl";
56
+ export const SHARED_MANIFEST_RELPATH = "manifest.jsonl";
57
+ const SHARED_FRAGMENT_DIR = { grader: "graders", prompt: "prompts" };
58
+ const SHARED_FRAGMENT_EXT = { grader: ".yaml", prompt: ".md" };
59
+ export function sharedFragmentRelPath(frag) {
60
+ return `${SHARED_FRAGMENT_DIR[frag.kind]}/${frag.contentHash}${SHARED_FRAGMENT_EXT[frag.kind]}`;
61
+ }
62
+ /**
63
+ * Read the shared `memories.jsonl` through the package's own validator, and
64
+ * keep the rejects instead of dropping them.
65
+ *
66
+ * A missing file is an empty store and says so; an unreadable one is a
67
+ * failure and says THAT — a shared store that cannot be read is not a shared
68
+ * store with nothing in it.
69
+ */
70
+ export function auditSharedMemories(sharedDirAbs) {
71
+ const file = path.join(sharedDirAbs, SHARED_MEMORIES_RELPATH);
72
+ let raw;
73
+ try {
74
+ raw = readFileSync(file, "utf8");
75
+ }
76
+ catch (err) {
77
+ const code = err.code;
78
+ if (code === "ENOENT")
79
+ return { ok: true, value: { memories: [], rejected: [], linesSeen: 0 } };
80
+ return fail("unreadable", `the shared memories file could not be read (${code ?? "unknown error"})`);
81
+ }
82
+ const memories = [];
83
+ const rejected = [];
84
+ let linesSeen = 0;
85
+ let lineNo = 0;
86
+ for (const line of raw.split("\n")) {
87
+ lineNo += 1;
88
+ if (line.trim() === "")
89
+ continue;
90
+ linesSeen += 1;
91
+ let parsed;
92
+ try {
93
+ parsed = JSON.parse(line);
94
+ }
95
+ catch {
96
+ rejected.push({ at: `memories.jsonl:${lineNo}`, reason: "not valid JSON" });
97
+ continue;
98
+ }
99
+ const verdict = validateSharedMemory(parsed);
100
+ if (verdict.ok)
101
+ memories.push(verdict.memory);
102
+ else
103
+ rejected.push({ at: `memories.jsonl:${lineNo}`, reason: verdict.reason });
104
+ }
105
+ return { ok: true, value: { memories, rejected, linesSeen } };
106
+ }
107
+ /**
108
+ * Fragments of one kind, with the files the package's validation refused.
109
+ *
110
+ * The valid set comes from `readSharedFragments`, so the accept/reject rule
111
+ * stays in one place; what is derived here is only WHICH candidate files did
112
+ * not survive it. Re-deriving the rule to explain each rejection would be a
113
+ * second copy of it, and the second copy is the one that goes stale.
114
+ */
115
+ export function auditSharedFragments(sharedDirAbs, kind) {
116
+ const dir = path.join(sharedDirAbs, SHARED_FRAGMENT_DIR[kind]);
117
+ const ext = SHARED_FRAGMENT_EXT[kind];
118
+ let names;
119
+ try {
120
+ names = readdirSync(dir).sort();
121
+ }
122
+ catch (err) {
123
+ const code = err.code;
124
+ if (code === "ENOENT")
125
+ return { ok: true, value: { fragments: [], rejected: [] } };
126
+ return fail("unreadable", `the shared ${kind} directory could not be listed (${code ?? "unknown error"})`);
127
+ }
128
+ let fragments;
129
+ try {
130
+ fragments = readSharedFragments(sharedDirAbs, kind);
131
+ }
132
+ catch (err) {
133
+ return fail("unreadable", `the shared ${kind} fragments could not be read: ${err.message}`);
134
+ }
135
+ const accepted = new Set(fragments.map((f) => f.filename));
136
+ const rejected = names
137
+ .filter((n) => n.endsWith(ext) && !accepted.has(n))
138
+ .map((n) => ({
139
+ at: `${SHARED_FRAGMENT_DIR[kind]}/${n}`,
140
+ reason: "refused by @crewhaus/harness-lifecycle's fragment validation — the filename's claimed content hash does not match the body's, or the body is over the size cap",
141
+ }));
142
+ return { ok: true, value: { fragments, rejected } };
143
+ }
144
+ // ---------------------------------------------------------------------------
145
+ // harness side
146
+ // ---------------------------------------------------------------------------
147
+ /**
148
+ * The tag `applyPull` appends to every memory it lands
149
+ * (`shared:<source harness>`).
150
+ *
151
+ * It matters because the content hash covers text AND tags: a memory pulled
152
+ * from the shared store lands with one more tag than the record it came from,
153
+ * so hashing what is on disk does NOT reproduce the shared record's hash. The
154
+ * CLI computes the harness's hashes without accounting for it
155
+ * (`apps/cli/src/index.ts`, the `knowledge pull` branch), which is why a
156
+ * second pull re-imports everything and `shared.jsonl` grows on every run.
157
+ *
158
+ * `harnessMemoryHashes` below closes that by hashing a landed memory BOTH
159
+ * ways. The hashing rule itself is still the package's `memoryContentHash`;
160
+ * what is mirrored here is only the tag `applyPull` writes, and
161
+ * `index.test.ts` pins it — a second pull must bring in nothing.
162
+ */
163
+ export const SHARED_TAG_PREFIX = "shared:";
164
+ /**
165
+ * Every content hash the harness already holds, counting a pulled memory
166
+ * under its shared hash as well as its local one.
167
+ *
168
+ * The undo is EXACTLY `applyPull`'s do: it appends ONE provenance tag to the
169
+ * end of the record's tags, so exactly one trailing tag is taken back off.
170
+ * Stripping every `shared:`-prefixed tag instead — which is what this did —
171
+ * is a different function, and it is wrong for the record that already
172
+ * carried provenance of its own (one harness pulled a memory and pushed it
173
+ * on, so the shared record's own tags include a `shared:` tag). For that
174
+ * record neither form reproduced the shared hash, so the FIRST pull reported
175
+ * `verified: false` with the memory listed as missing although it had landed
176
+ * perfectly, and every later pull re-imported it — the unbounded growth this
177
+ * mirror exists to prevent, reintroduced by the mirror.
178
+ */
179
+ export function harnessMemoryHashes(memories) {
180
+ const hashes = new Set();
181
+ for (const memory of memories) {
182
+ hashes.add(memoryContentHash(memory.text, memory.tags));
183
+ const last = memory.tags[memory.tags.length - 1];
184
+ if (last?.startsWith(SHARED_TAG_PREFIX) === true) {
185
+ hashes.add(memoryContentHash(memory.text, memory.tags.slice(0, -1)));
186
+ }
187
+ }
188
+ return hashes;
189
+ }
190
+ /**
191
+ * Where `applyPull` lands each artifact inside a harness.
192
+ *
193
+ * Mirrored, not re-derived: the package chooses these paths and does not
194
+ * export them, and a tool that cannot name them can neither contain the
195
+ * writes (a symlink at one of them sends the file out of the workspace) nor
196
+ * check that they arrived. `index.test.ts` pins every one of them against a
197
+ * real pull, so a rename upstream fails a test here rather than silently
198
+ * turning the checks into no-ops.
199
+ */
200
+ export const PULLED_MEMORIES_RELPATH = ".crewhaus/memories/shared.jsonl";
201
+ const PULLED_PROMPT_RELDIR = ".crewhaus/prompts";
202
+ export const PULLED_GRADER_PREFIX = "graders.shared-";
203
+ export const PULLED_GRADER_SUFFIX = ".yaml";
204
+ export function pulledFragmentRelPath(frag) {
205
+ const short = frag.contentHash.slice(0, 12);
206
+ return frag.kind === "prompt"
207
+ ? `${PULLED_PROMPT_RELDIR}/shared-${short}.md`
208
+ : `${PULLED_GRADER_PREFIX}${short}${PULLED_GRADER_SUFFIX}`;
209
+ }
210
+ /**
211
+ * Content hashes of the grader fragments a previous pull already landed.
212
+ *
213
+ * `readHarnessGraders` looks only at `graders.yaml` / `.crewhaus/graders.yaml`
214
+ * — by design, those are the harness's OWN grader files — so a fragment
215
+ * `applyPull` wrote to `graders.shared-<hash>.yaml` is invisible to it, and
216
+ * every later pull counted that fragment as new and reported pulling it
217
+ * again. Matched by the filename shape and hashed with the package's own
218
+ * `fragmentContentHash`, so the accept rule stays the package's; only the
219
+ * landing name is mirrored.
220
+ */
221
+ export function landedSharedGraderHashes(harnessDirAbs) {
222
+ let names;
223
+ try {
224
+ names = readdirSync(harnessDirAbs).sort();
225
+ }
226
+ catch (err) {
227
+ const code = err.code;
228
+ return fail("unreadable", `the harness directory could not be listed (${code ?? "unknown error"})`);
229
+ }
230
+ const hashes = new Set();
231
+ for (const name of names) {
232
+ if (!name.startsWith(PULLED_GRADER_PREFIX) || !name.endsWith(PULLED_GRADER_SUFFIX))
233
+ continue;
234
+ let contents;
235
+ try {
236
+ contents = readFileSync(path.join(harnessDirAbs, name), "utf8");
237
+ }
238
+ catch (err) {
239
+ // An unreadable landed fragment is not an absent one: reporting it as
240
+ // absent would make the next pull re-import it and call it new.
241
+ return fail("unreadable", `the pulled grader fragment ${name} could not be read (${err.code ?? "unknown error"})`);
242
+ }
243
+ hashes.add(fragmentContentHash(contents));
244
+ }
245
+ return { ok: true, value: hashes };
246
+ }
247
+ /**
248
+ * How many lines the harness's memory files hold, so a store whose lines are
249
+ * mostly unparseable is not reported as a store with few memories. The parsed
250
+ * count comes from the package's `readHarnessMemories`; the difference
251
+ * between the two is the interesting number.
252
+ */
253
+ export function auditHarnessMemoryFiles(harnessDirAbs) {
254
+ const dir = path.join(harnessDirAbs, ".crewhaus", "memories");
255
+ let names;
256
+ try {
257
+ names = readdirSync(dir).sort();
258
+ }
259
+ catch (err) {
260
+ const code = err.code;
261
+ if (code === "ENOENT")
262
+ return { ok: true, value: { dirPresent: false, files: 0, linesSeen: 0 } };
263
+ return fail("unreadable", `the harness memories directory could not be listed (${code ?? "unknown error"})`);
264
+ }
265
+ let files = 0;
266
+ let linesSeen = 0;
267
+ for (const name of names) {
268
+ if (!name.endsWith(".jsonl"))
269
+ continue;
270
+ files += 1;
271
+ let raw;
272
+ try {
273
+ raw = readFileSync(path.join(dir, name), "utf8");
274
+ }
275
+ catch (err) {
276
+ return fail("unreadable", `the harness memory file ${name} could not be read (${err.code ?? "unknown error"})`);
277
+ }
278
+ for (const line of raw.split("\n"))
279
+ if (line.trim() !== "")
280
+ linesSeen += 1;
281
+ }
282
+ return { ok: true, value: { dirPresent: true, files, linesSeen } };
283
+ }
@@ -0,0 +1,106 @@
1
+ import { type Loaded } from "./result";
2
+ /**
3
+ * The two directories under `.crewhaus` that ARE the harness's record store:
4
+ * the pair `runRetentionExport` carries, and the same pair
5
+ * `openHarnessRecordStore` enumerates and a sweep deletes from. Mirrored here
6
+ * because `@crewhaus/harness-lifecycle` keeps the relpaths private.
7
+ */
8
+ export declare const COVERED_STORE_DIRS: readonly ["sessions", "audit"];
9
+ export declare const RECEIPT_FILENAME = "store-migration.json";
10
+ /**
11
+ * The file `runRetentionExport` writes into its output directory on every
12
+ * REAL run, on top of the copies themselves.
13
+ *
14
+ * Mirrored here (the package does not export the name) because it is a
15
+ * destination this tool writes but does not copy: leaving it out of the
16
+ * written set made the conflict check miss it, so a destination that already
17
+ * held a `manifest.json` — a previous `crewhaus retention export`, say — had
18
+ * it overwritten without `overwrite` being asked for and without the preview
19
+ * ever naming it. `index.test.ts` pins the coupling: a real migration must
20
+ * produce this file at this name.
21
+ */
22
+ export declare const EXPORT_MANIFEST_FILENAME = "manifest.json";
23
+ export declare const RECORD_SHAPE_MIGRATION_UNAVAILABLE: string;
24
+ export type StoreVersion = {
25
+ readonly state: "known";
26
+ readonly memoriesSchemaVersion: number;
27
+ }
28
+ /** No `.crewhaus/meta.json`: nothing has stamped a version here yet. */
29
+ | {
30
+ readonly state: "unstamped";
31
+ } | {
32
+ readonly state: "unknown";
33
+ readonly reason: string;
34
+ };
35
+ /**
36
+ * Read `<harnessDir>/.crewhaus/meta.json`, the file `crewhaus migrate
37
+ * memories` stamps (`{ memories: { schemaVersion, migratedAt } }`).
38
+ *
39
+ * An unreadable or malformed meta file is `unknown`, never version 1: a
40
+ * migration that guesses the version it is migrating FROM is a migration that
41
+ * rewrites records twice.
42
+ */
43
+ export declare function readStoreVersion(harnessDirAbs: string): StoreVersion;
44
+ export type Uncovered = {
45
+ readonly entry: string;
46
+ readonly reason: string;
47
+ };
48
+ /**
49
+ * Everything directly under `.crewhaus` that the export path does not copy.
50
+ *
51
+ * Returned so the result can name it. A caller who migrates a store and is
52
+ * told only "42 sessions, 3 audit days" will believe the memories came too.
53
+ */
54
+ export declare function uncoveredStateEntries(harnessDirAbs: string): Loaded<Uncovered[]>;
55
+ export type MigrationReceiptFile = {
56
+ readonly rel: string;
57
+ readonly bytes: number;
58
+ readonly sha256: string;
59
+ };
60
+ export type MigrationReceipt = {
61
+ readonly writtenAt: string;
62
+ readonly sourceRoot: string;
63
+ readonly destinationRoot: string;
64
+ readonly storeVersion: StoreVersion;
65
+ readonly since?: string;
66
+ readonly sessions: ReadonlyArray<string>;
67
+ readonly auditDays: ReadonlyArray<string>;
68
+ readonly chainTailCopied: boolean;
69
+ readonly files: ReadonlyArray<MigrationReceiptFile>;
70
+ readonly verified: boolean;
71
+ };
72
+ export type DestinationState = {
73
+ readonly exists: boolean;
74
+ /** Files already present that a migration would write over. */
75
+ readonly conflicts: ReadonlyArray<string>;
76
+ readonly receipt?: MigrationReceipt;
77
+ /** Set when a receipt is present but could not be trusted. */
78
+ readonly receiptProblem?: string;
79
+ };
80
+ /**
81
+ * What is already at the destination, and whether a previous migration put it
82
+ * there. A receipt naming a DIFFERENT source is the case that matters: two
83
+ * harnesses' stores merged into one directory cannot be told apart
84
+ * afterwards, and the audit chains cannot both be valid.
85
+ */
86
+ export declare function inspectDestination(destAbs: string, plannedRelPaths: ReadonlyArray<string>): Loaded<DestinationState>;
87
+ /** The destination-relative paths a migration of this selection would write. */
88
+ export declare function plannedPaths(opts: {
89
+ readonly sessions: ReadonlyArray<string>;
90
+ readonly auditDays: ReadonlyArray<string>;
91
+ readonly chainTail: boolean;
92
+ /** Session ids that have a sibling event log at the source. */
93
+ readonly withEventLog: ReadonlySet<string>;
94
+ }): string[];
95
+ /**
96
+ * Every destination-relative path a REAL migration writes: the copies, plus
97
+ * the export's own `manifest.json` and this tool's receipt.
98
+ *
99
+ * Distinct from {@link plannedPaths} on purpose. `plannedPaths` is the set
100
+ * that is copied from a source and can therefore be hashed on both sides;
101
+ * this is the set that is WRITTEN, which is what the conflict check and the
102
+ * containment check have to be asked about. Reporting the first as the second
103
+ * is how a destination file nobody copied gets destroyed quietly.
104
+ */
105
+ export declare function writtenPaths(plannedRelPaths: ReadonlyArray<string>): string[];
106
+ export declare function writeReceipt(destAbs: string, receipt: MigrationReceipt): Loaded<string>;