@orkestrel/scaffold 0.0.47 → 0.0.49

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.
@@ -5,19 +5,20 @@ import { EmitterErrorHandler } from '@orkestrel/emitter';
5
5
  import { EmitterHooks } from '@orkestrel/emitter';
6
6
  import { EmitterInterface } from '@orkestrel/emitter';
7
7
  import { Guard } from '@orkestrel/contract';
8
+ import { HostFile } from '@orkestrel/scaffold';
8
9
  import { Mirror } from '@orkestrel/scaffold';
9
10
  import { Plan } from '@orkestrel/scaffold';
10
11
  import { Release } from '@orkestrel/scaffold';
11
12
  import { Snapshot } from '@orkestrel/scaffold';
12
13
 
13
14
  /**
14
- * The Git branch syntax the guide endpoint accepts.
15
+ * The Git branch syntax the repository endpoint accepts.
15
16
  *
16
17
  * @remarks
17
18
  * A branch is caller-supplied and reaches a URL path, so it is closed to
18
19
  * alphanumerics, dot, underscore, hyphen, and the separator, must open with an
19
20
  * alphanumeric, and may carry no `..` anywhere. That last refusal is what stops
20
- * a branch from walking out of the guide directory it addresses.
21
+ * a branch from walking out of the repository path it addresses.
21
22
  */
22
23
  export declare const BRANCH_PATTERN: RegExp;
23
24
 
@@ -111,6 +112,80 @@ export declare const DIGEST_PATTERN: RegExp;
111
112
  */
112
113
  export declare const DRIVE_PATTERN: RegExp;
113
114
 
115
+ /**
116
+ * Assemble a whole vendored host from live files and the installed floor.
117
+ *
118
+ * @param files - The host-owned vendored files read from the repository, one
119
+ * row per path.
120
+ * @param floor - The installed host floor, which fixes the membership a fill
121
+ * may draw from and supplies the bytes owned by another surface.
122
+ * @returns The assembled host, or `undefined` when any row produced no answer or
123
+ * names a path the floor does not declare, or when a host-owned path is absent.
124
+ *
125
+ * @remarks
126
+ * The one place the host-owned all-or-nothing rule is decided, so no verb
127
+ * restates it. The host surface contributes one baseline: a fill carries live
128
+ * bytes for every path that surface writes, or it is nothing. A row that failed,
129
+ * went missing, names an undeclared path, or leaves a host-owned path absent
130
+ * answers `undefined`. Deferred paths are presence-only and retain the installed
131
+ * floor bytes that their catalog or mirror surface owns; repair never writes
132
+ * those floor bytes. One `Host` can therefore carry live host bytes beside floor
133
+ * bytes without mixing baselines within a surface.
134
+ *
135
+ * The emitted entries keep the release's own order and its storage and
136
+ * executable declarations, and carry digests recomputed over the bytes the fill
137
+ * actually holds. That is what lets a reader verify the value against itself,
138
+ * and it is why an undeclared path is refused rather than added: membership
139
+ * moves with a release, never with a fetch.
140
+ *
141
+ * @example
142
+ * ```ts
143
+ * import { filesToHost } from '@orkestrel/scaffold/server'
144
+ *
145
+ * filesToHost([{ path: 'AGENTS.md', lookup: 'found', hex: '23204167656e74730a' }], floor)
146
+ * // { manifest: { entries: [ … ], roots: [ … ], digest: '…' }, bytes: { 'AGENTS.md': '…' } }
147
+ * ```
148
+ */
149
+ export declare function filesToHost(files: readonly HostFile[], floor: Host): Host | undefined;
150
+
151
+ /**
152
+ * Projects exact bytes stated in hexadecimal to their SHA-256 digest.
153
+ *
154
+ * @param hex - The exact lowercase hexadecimal bytes to digest.
155
+ * @returns Sixty-four lowercase hexadecimal digits.
156
+ * @throws `ScaffoldError('INVALID', …)` when `hex` is not exact bounded
157
+ * lowercase hexadecimal text.
158
+ *
159
+ * @example
160
+ * ```ts
161
+ * import { hexToDigest } from '@orkestrel/scaffold/server'
162
+ *
163
+ * hexToDigest('68690a') // '98ea6e4f216f2fb4b69fff9b3a44842c38686ca685f3f55dc48c5d3fb1107be4'
164
+ * ```
165
+ */
166
+ export declare function hexToDigest(hex: string): string;
167
+
168
+ /**
169
+ * A whole vendored host supplied as a value: the membership beside the bytes filling it.
170
+ *
171
+ * @remarks
172
+ * `manifest` carries the membership, the roots, and the executable declarations
173
+ * the installed release fixed, and each entry's `digest` is the digest of the
174
+ * bytes this value actually holds. `bytes` is keyed by each entry's
175
+ * `destination` and covers exactly the entries the fill holds, so an entry with
176
+ * no bytes and a byte string under no entry are each a refusal rather than a
177
+ * gap a reader has to work around.
178
+ *
179
+ * Membership never moves without a release. A fill therefore states which of
180
+ * the release's paths it carries and never introduces one the release did not
181
+ * declare, which is what keeps a value host's reach identical to the installed
182
+ * root's however the bytes were obtained.
183
+ */
184
+ export declare interface Host {
185
+ readonly manifest: HostManifest;
186
+ readonly bytes: Snapshot;
187
+ }
188
+
114
189
  /**
115
190
  * The complete vendored-host inventory.
116
191
  *
@@ -139,10 +214,10 @@ export declare interface HostManifest {
139
214
  export declare const INVALID_SEGMENT_CHARACTER_PATTERN: RegExp;
140
215
 
141
216
  /**
142
- * Narrow a value to a Git branch the guide endpoint accepts.
217
+ * Narrow a value to a Git branch the repository endpoint accepts.
143
218
  *
144
219
  * @remarks
145
- * A branch reaches the guide URL's path, so the syntax is closed rather than
220
+ * A branch reaches the repository URL's path, so the syntax is closed rather than
146
221
  * merely bounded and no `..` is admitted anywhere in it.
147
222
  *
148
223
  * @example
@@ -272,6 +347,30 @@ export declare function isExactCaseFile(path: string): boolean;
272
347
  */
273
348
  export declare function isFilesystemPath(value: unknown): value is string;
274
349
 
350
+ /**
351
+ * Narrow a value to one {@link Host}.
352
+ *
353
+ * @remarks
354
+ * A whole vendored host handed in as a value is as untrusted as one read from a
355
+ * directory a caller named, so both halves are guarded: the manifest by the same
356
+ * membership law a read root is held to, and the bytes by the core snapshot law,
357
+ * which bounds the fill and reads every key as a path and every value as exact
358
+ * lowercase hexadecimal. Whether those halves agree with each other is the
359
+ * reader's question rather than this one's, because a guard has only `false` to
360
+ * say and a mismatch has a path to name.
361
+ *
362
+ * @example
363
+ * ```ts
364
+ * import { isHost } from '@orkestrel/scaffold/server'
365
+ *
366
+ * const digest = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
367
+ *
368
+ * isHost({ manifest: { entries: [], roots: [], digest }, bytes: {} }) // true
369
+ * isHost({ manifest: { entries: [], roots: [], digest } }) // false
370
+ * ```
371
+ */
372
+ export declare const isHost: Guard<Host>;
373
+
275
374
  /**
276
375
  * Narrow a value to one {@link HostManifest}.
277
376
  *
@@ -319,7 +418,12 @@ export declare function isInventory(value: unknown): value is readonly unknown[]
319
418
  * ```ts
320
419
  * import { isManifestEntry } from '@orkestrel/scaffold/server'
321
420
  *
322
- * isManifestEntry({ storage: 'AGENTS.md', destination: 'AGENTS.md', executable: false }) // true
421
+ * isManifestEntry({
422
+ * storage: 'AGENTS.md',
423
+ * destination: 'AGENTS.md',
424
+ * executable: false,
425
+ * digest: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
426
+ * }) // true
323
427
  * ```
324
428
  */
325
429
  export declare const isManifestEntry: Guard<ManifestEntry>;
@@ -337,6 +441,12 @@ export declare const isMaterializerHooks: Guard<EmitterHooks<MaterializerEventMa
337
441
  /**
338
442
  * Narrow a value to {@link MaterializerOptions}.
339
443
  *
444
+ * @remarks
445
+ * `host` admits both representations of one vendored root: a directory path and
446
+ * a whole {@link Host} value. They share a key because they are one setting
447
+ * stated two ways rather than two settings, so nothing downstream has to
448
+ * reconcile a pair that could disagree.
449
+ *
340
450
  * @example
341
451
  * ```ts
342
452
  * import { isMaterializerOptions } from '@orkestrel/scaffold/server'
@@ -350,6 +460,25 @@ export declare const isMaterializerOptions: Guard<MaterializerOptions>;
350
460
  /** Narrow a value to a bounded list of fetched guide mirrors. */
351
461
  export declare const isMirrors: Guard<readonly Mirror[]>;
352
462
 
463
+ /**
464
+ * Narrow a value to a bounded list of target-relative paths.
465
+ *
466
+ * @remarks
467
+ * Composed from the core collection and path guards rather than restated, so
468
+ * the containment law that keeps a caller-supplied path inside its target has
469
+ * exactly one home. It bounds what a caller may hand a public method, which is
470
+ * why it is not {@link isInventory}: that one bounds what a checkout may hold.
471
+ *
472
+ * @example
473
+ * ```ts
474
+ * import { isPaths } from '@orkestrel/scaffold/server'
475
+ *
476
+ * isPaths(['AGENTS.md']) // true
477
+ * isPaths(['../secrets']) // false
478
+ * ```
479
+ */
480
+ export declare const isPaths: Guard<readonly string[]>;
481
+
353
482
  /**
354
483
  * Test whether a path is a physical directory this package will read or write into.
355
484
  *
@@ -393,25 +522,6 @@ export declare function isPhysicalDirectory(path: string): boolean;
393
522
  */
394
523
  export declare function isPhysicalFile(path: string): boolean;
395
524
 
396
- /**
397
- * Narrow a value to a {@link Repository}.
398
- *
399
- * @remarks
400
- * Both path lists are target-relative, so both are measured by the core path
401
- * law: a reported path that is not one this package could have planned is not a
402
- * path it will delete. The inventory guard bounds the lists, because a checkout
403
- * is legitimately far larger than any collection a caller hands a method.
404
- *
405
- * @example
406
- * ```ts
407
- * import { isRepository } from '@orkestrel/scaffold/server'
408
- *
409
- * isRepository({ tracked: ['AGENTS.md'], dirty: [] }) // true
410
- * isRepository({ tracked: ['../secrets'], dirty: [] }) // false
411
- * ```
412
- */
413
- export declare const isRepository: Guard<Repository>;
414
-
415
525
  /**
416
526
  * Narrow a value to a per-request timeout in milliseconds.
417
527
  *
@@ -447,7 +557,7 @@ export declare const isUpstreamHooks: Guard<EmitterHooks<UpstreamEventMap>>;
447
557
  * ```ts
448
558
  * import { isUpstreamOptions } from '@orkestrel/scaffold/server'
449
559
  *
450
- * isUpstreamOptions({ guides: { branch: 'main' }, concurrency: 4 }) // true
560
+ * isUpstreamOptions({ repository: { branch: 'main' }, concurrency: 4 }) // true
451
561
  * isUpstreamOptions({ concurrency: 0 }) // false
452
562
  * ```
453
563
  */
@@ -476,6 +586,25 @@ export declare const isUpstreamOptions: Guard<UpstreamOptions>;
476
586
  */
477
587
  export declare function isVacant(target: string): boolean;
478
588
 
589
+ /**
590
+ * Narrow a value to a {@link Worktree}.
591
+ *
592
+ * @remarks
593
+ * Both path lists are target-relative, so both are measured by the core path
594
+ * law: a reported path that is not one this package could have planned is not a
595
+ * path it will delete. The inventory guard bounds the lists, because a checkout
596
+ * is legitimately far larger than any collection a caller hands a method.
597
+ *
598
+ * @example
599
+ * ```ts
600
+ * import { isWorktree } from '@orkestrel/scaffold/server'
601
+ *
602
+ * isWorktree({ tracked: ['AGENTS.md'], dirty: [] }) // true
603
+ * isWorktree({ tracked: ['../secrets'], dirty: [] }) // false
604
+ * ```
605
+ */
606
+ export declare const isWorktree: Guard<Worktree>;
607
+
479
608
  /**
480
609
  * List a directory's descendant directories as sorted root-relative paths.
481
610
  *
@@ -548,11 +677,19 @@ export declare function listFiles(root: string): readonly string[];
548
677
  */
549
678
  export declare const MANIFEST_NAME = "manifest.json";
550
679
 
551
- /** One file record of the vendored host's manifest. */
680
+ /**
681
+ * One file record of the vendored host's manifest.
682
+ *
683
+ * @remarks
684
+ * `digest` is the SHA-256 of the file's exact bytes. A live read compares it
685
+ * against the target's copy, and verifies host-supplied bytes against it before
686
+ * treating them as that entry.
687
+ */
552
688
  export declare interface ManifestEntry {
553
689
  readonly storage: string;
554
690
  readonly destination: string;
555
691
  readonly executable: boolean;
692
+ readonly digest: string;
556
693
  }
557
694
 
558
695
  /**
@@ -708,7 +845,7 @@ export declare function matchesPrecondition(precondition: WritePrecondition): bo
708
845
  * directories. It is the inversion the contract asks for: the candidate set
709
846
  * is re-derived from the plan and narrowed by what git tracks, and the audit
710
847
  * must agree with that derivation rather than supply the set itself.
711
- * Repository metadata is protected because losing history is not a repair,
848
+ * Git metadata is protected because losing history is not a repair,
712
849
  * and a target's own `src` and `app` trees are protected because a
713
850
  * workspace's source is the one thing scaffold never plans and never owns. A
714
851
  * plan the compiler emits never maps a protected root, so this guard exists
@@ -735,7 +872,7 @@ export declare function matchesProtectedPath(path: string): boolean;
735
872
  * The vendoring deny-list. A host root is staged from a real checkout, so the
736
873
  * refusal is stated over the path rather than over the file's content: a
737
874
  * credential is recognizable by where it sits and what it is called long before
738
- * anything reads it. Repository metadata is included through
875
+ * anything reads it. Git metadata is included through
739
876
  * {@link matchesGitPath}, so one call answers the whole question and no caller
740
877
  * has to remember to ask twice.
741
878
  *
@@ -768,6 +905,12 @@ export declare function matchesSensitivePath(path: string): boolean;
768
905
  * case, so a manifest naming `agents.md` for a stored `AGENTS.md` is refused on
769
906
  * a case-insensitive filesystem rather than silently resolved.
770
907
  *
908
+ * That host arrives as a directory path or as a whole {@link Host} value, and
909
+ * every verb reads one immutable host either way. A value is owned, verified
910
+ * against its own membership and digests, and read in memory; a write fills it
911
+ * into a private root and copies from there, so the executable declarations the
912
+ * release fixed reach the target from either representation.
913
+ *
771
914
  * What a mutation guarantees is exactly what {@link WriteTransaction}
772
915
  * guarantees, and no more: a caught failure part way through a commit rolls the
773
916
  * whole commit back, no destination ever receives half-written bytes, and a
@@ -794,18 +937,26 @@ export declare class Materializer implements MaterializerInterface {
794
937
  /**
795
938
  * Construct a materializer over one vendored host root.
796
939
  *
797
- * @param options - The vendored host root, the initial listeners, and the
798
- * listener-error handler.
940
+ * @param options - The vendored host, in either representation, the initial
941
+ * listeners, and the listener-error handler.
799
942
  * @throws {@link ScaffoldError} coded `INVALID` when `options` is present but
800
943
  * is not an option bag this materializer accepts, and `TARGET` when the host
801
- * carries a manifest that cannot be read or does not match what it stores.
944
+ * carries a manifest that cannot be read, does not match what it stores, or
945
+ * is a value that does not agree with the bytes beside it.
802
946
  *
803
947
  * @remarks
804
- * `host` defaults to this package's own vendored root, resolved from this
805
- * module's own location so it never depends on the caller's working
806
- * directory. A host carrying no manifest is read as a raw checkout and every
948
+ * A `host` path defaults to this package's own vendored root, resolved from
949
+ * this module's own location so it never depends on the caller's working
950
+ * directory. A root carrying no manifest is read as a raw checkout and every
807
951
  * artifact maps onto it one to one.
808
952
  *
953
+ * A `host` value is owned before it is read and then held immutable, so the
954
+ * bytes this check measured are the bytes every later read returns. It is
955
+ * verified the way a root is, against the same membership law: the manifest
956
+ * digest must cover the membership beside it, no two entries may claim one
957
+ * destination, and the fill must carry exactly one hashing byte string per
958
+ * declared entry.
959
+ *
809
960
  * The host is read here rather than on first use, so a broken vendored root
810
961
  * fails at construction where the caller can still act on it, and so nothing
811
962
  * has to carry a second flag recording whether the read has happened yet.
@@ -931,7 +1082,7 @@ export declare class Materializer implements MaterializerInterface {
931
1082
  *
932
1083
  * @param plan - The compiled plan that decides which paths are foreign.
933
1084
  * @param audit - The preview returned by this materializer's `audit` method; it must agree with the candidate set this call re-derives.
934
- * @param repository - The target's git state; only a tracked path is ever deleted.
1085
+ * @param worktree - The target's git state; only a tracked path is ever deleted.
935
1086
  * @param target - The directory to delete from.
936
1087
  * @returns The paths removed.
937
1088
  * @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
@@ -952,7 +1103,7 @@ export declare class Materializer implements MaterializerInterface {
952
1103
  * any foreign finding, including one the deletion itself would skip, because a
953
1104
  * preview stale anywhere is stale evidence.
954
1105
  */
955
- remove(plan: Plan, audit: Audit, repository: Repository, target: string): MaterializeResult;
1106
+ remove(plan: Plan, audit: Audit, worktree: Worktree, target: string): MaterializeResult;
956
1107
  /**
957
1108
  * Tear the materializer down. Every later call throws, and teardown is idempotent.
958
1109
  *
@@ -1078,7 +1229,7 @@ export declare class Materializer implements MaterializerInterface {
1078
1229
  *
1079
1230
  * @param plan - The compiled plan that decides which paths are foreign.
1080
1231
  * @param audit - The preview returned by this materializer's `audit` method; it must agree with the candidate set this call re-derives.
1081
- * @param repository - The target's git state; only a tracked path is ever deleted.
1232
+ * @param worktree - The target's git state; only a tracked path is ever deleted.
1082
1233
  * @param target - The directory to delete from.
1083
1234
  * @returns The paths removed.
1084
1235
  *
@@ -1101,7 +1252,7 @@ export declare class Materializer implements MaterializerInterface {
1101
1252
  * any foreign finding, including one the deletion itself would skip, because a
1102
1253
  * preview stale anywhere is stale evidence.
1103
1254
  */
1104
- remove(plan: Plan, audit: Audit, repository: Repository, target: string): MaterializeResult;
1255
+ remove(plan: Plan, audit: Audit, worktree: Worktree, target: string): MaterializeResult;
1105
1256
  /**
1106
1257
  * Tear the materializer down. Every later call throws, and teardown is idempotent.
1107
1258
  *
@@ -1114,19 +1265,21 @@ export declare class Materializer implements MaterializerInterface {
1114
1265
  * Options for the materializer.
1115
1266
  *
1116
1267
  * @remarks
1117
- * `host` is the vendored data root host-origin artifacts are copied from. It
1118
- * defaults to this package's own vendored root, resolved from the installed
1119
- * module's location rather than the caller's working directory. A host that
1120
- * carries no manifest beside it maps artifact paths one to one instead of
1121
- * through the manifest.
1268
+ * `host` is the vendored data root host-origin artifacts are copied from, in
1269
+ * either representation. A string is a directory: it defaults to this package's
1270
+ * own vendored root, resolved from the installed module's location rather than
1271
+ * the caller's working directory, and a directory that carries no manifest
1272
+ * beside it maps artifact paths one to one instead of through the manifest. A
1273
+ * {@link Host} is the same root supplied as a value, so a caller that already
1274
+ * holds the bytes never stages them to a directory to be read back.
1122
1275
  */
1123
1276
  export declare interface MaterializerOptions {
1124
- readonly host?: string;
1277
+ readonly host?: string | Host;
1125
1278
  readonly on?: EmitterHooks<MaterializerEventMap>;
1126
1279
  readonly error?: EmitterErrorHandler;
1127
1280
  }
1128
1281
 
1129
- /** Maximum characters one guide branch may carry. */
1282
+ /** Maximum characters one repository branch may carry. */
1130
1283
  export declare const MAX_BRANCH_LENGTH = 255;
1131
1284
 
1132
1285
  /** Maximum characters one caller-supplied upstream endpoint may carry. */
@@ -1304,10 +1457,38 @@ export declare class Materializer implements MaterializerInterface {
1304
1457
  */
1305
1458
  export declare function readFileText(root: string, path: string, limit?: number): string | undefined;
1306
1459
 
1460
+ /**
1461
+ * Reads the installed vendored host floor as a value.
1462
+ *
1463
+ * @param root - The vendored host root. Default: the installed package's
1464
+ * vendored root, resolved from this module's location.
1465
+ * @returns The verified manifest and the exact bytes of every declared entry.
1466
+ * @throws `ScaffoldError('TARGET', …)` when the root is not a readable physical
1467
+ * directory, its manifest is absent or unreadable, the manifest does not verify,
1468
+ * or a declared file is unreadable or misses its digest.
1469
+ *
1470
+ * @remarks
1471
+ * Reads the same default floor the {@link Materializer} uses. Each declared
1472
+ * file is addressed through the manifest's storage name and retained under its
1473
+ * destination, so the returned value has the same shape as the installed root.
1474
+ * When this module executes from TypeScript source, the committed inventory is
1475
+ * the manifest and each checkout destination supplies its bytes. The emitted
1476
+ * module reads the staged `manifest.json` file and each storage path instead.
1477
+ *
1478
+ * @example
1479
+ * ```ts
1480
+ * import { readHostFloor } from '@orkestrel/scaffold/server'
1481
+ *
1482
+ * readHostFloor().manifest // the installed floor's verified membership
1483
+ * ```
1484
+ */
1485
+ export declare function readHostFloor(root?: string): Host;
1486
+
1307
1487
  /**
1308
1488
  * Read a vendored host's manifest, when it carries one.
1309
1489
  *
1310
1490
  * @param host - The vendored host root to read.
1491
+ * @param name - The root-relative manifest path. Default: `manifest.json`.
1311
1492
  * @returns The manifest, or `undefined` when the host carries none.
1312
1493
  * @throws `ScaffoldError('INVALID', …)` when `host` is not a host path.
1313
1494
  * @throws `ScaffoldError('TARGET', …)` when the manifest is there but cannot be
@@ -1333,7 +1514,7 @@ export declare class Materializer implements MaterializerInterface {
1333
1514
  * readHostManifest('./dist/host') // the manifest, or undefined for a raw root
1334
1515
  * ```
1335
1516
  */
1336
- export declare function readHostManifest(host: string): HostManifest | undefined;
1517
+ export declare function readHostManifest(host: string, name?: string): HostManifest | undefined;
1337
1518
 
1338
1519
  /**
1339
1520
  * Derive one vendored-host manifest entry from a file in a checkout.
@@ -1359,7 +1540,7 @@ export declare class Materializer implements MaterializerInterface {
1359
1540
  * import { readManifestEntry } from '@orkestrel/scaffold/server'
1360
1541
  *
1361
1542
  * readManifestEntry('.gitignore', '/tmp/checkout/.gitignore')
1362
- * // { storage: 'dotfiles/gitignore', destination: '.gitignore', executable: false }
1543
+ * // { storage: 'dotfiles/gitignore', destination: '.gitignore', executable: false, digest: '...' }
1363
1544
  * ```
1364
1545
  */
1365
1546
  export declare function readManifestEntry(destination: string, source: string): ManifestEntry | undefined;
@@ -1395,25 +1576,6 @@ export declare class Materializer implements MaterializerInterface {
1395
1576
  */
1396
1577
  export declare function readSnapshot(target: string, paths: readonly string[]): Snapshot;
1397
1578
 
1398
- /**
1399
- * What git reports about a target's working tree.
1400
- *
1401
- * @remarks
1402
- * `tracked` is the only set a deletion may draw from: git does not report the
1403
- * loss of an untracked path and `git diff` cannot restore it, so an ignored
1404
- * path such as an installed dependency tree, a build output, or an editor
1405
- * directory survives every verb. `dirty` is every path carrying an uncommitted
1406
- * change, taken repo-wide rather than over a write set, because deletion makes
1407
- * the write set the whole workspace. A clean tree is an empty `dirty`. A target
1408
- * that is not a git repository yields no `Repository` at all, so the caller
1409
- * decides what to do about that rather than reading it out of an invented
1410
- * empty value.
1411
- */
1412
- export declare interface Repository {
1413
- readonly tracked: readonly string[];
1414
- readonly dirty: readonly string[];
1415
- }
1416
-
1417
1579
  /**
1418
1580
  * The Windows device names that stay reserved even when an extension follows.
1419
1581
  *
@@ -1514,6 +1676,42 @@ export declare class Materializer implements MaterializerInterface {
1514
1676
  */
1515
1677
  export declare function resolveRealPath(path: string): string | undefined;
1516
1678
 
1679
+ /**
1680
+ * Stage the named destinations of a value host into a private root.
1681
+ *
1682
+ * @param host - The host whose bytes are written, keyed by destination.
1683
+ * @param root - The private directory to fill; it must already be a directory
1684
+ * this process may write into.
1685
+ * @param destinations - The destinations to stage, each declared by `host`.
1686
+ * @returns The entry staged for each destination, in the order requested.
1687
+ * @throws `ScaffoldError('INVALID', …)` when `root` is not a host path or a
1688
+ * storage name leaves it.
1689
+ * @throws `ScaffoldError('TARGET', …)` when a destination is one the host does
1690
+ * not declare, carries no bytes, or carries bytes that miss its declared digest.
1691
+ * @throws `ScaffoldError('WRITE', …)` when a file cannot be written or does not
1692
+ * read back as the bytes it was given.
1693
+ *
1694
+ * @remarks
1695
+ * Each file lands under the storage name the manifest declares and takes the
1696
+ * executable bit that manifest records, so a root filled from a value is the
1697
+ * same shape as one staged from a checkout and a reader cannot tell them apart.
1698
+ * That is what lets a mutation copy real files with real modes from bytes a
1699
+ * caller supplied, instead of degrading them to plain text writes.
1700
+ *
1701
+ * The bytes are digested before the write and the staged file after it, so a
1702
+ * value that disagrees with its own manifest is told apart from a write that
1703
+ * did not land.
1704
+ *
1705
+ * @example
1706
+ * ```ts
1707
+ * import { stageBytes } from '@orkestrel/scaffold/server'
1708
+ *
1709
+ * stageBytes(host, '/tmp/orkestrel-host-a1b2', ['scripts/codex.sh'])
1710
+ * // [{ storage: 'scripts/codex.sh', destination: 'scripts/codex.sh', executable: true, digest: '…' }]
1711
+ * ```
1712
+ */
1713
+ export declare function stageBytes(host: Host, root: string, destinations: readonly string[]): readonly ManifestEntry[];
1714
+
1517
1715
  /**
1518
1716
  * Stage a vendored host root from a real checkout.
1519
1717
  *
@@ -1568,6 +1766,33 @@ export declare class Materializer implements MaterializerInterface {
1568
1766
  */
1569
1767
  export declare function stageHost(checkout: string, host: string): readonly ManifestEntry[];
1570
1768
 
1769
+ /**
1770
+ * Stages the committed inventory of the files a vendored host carries.
1771
+ *
1772
+ * @param checkout - The checkout whose vendored paths are inventoried.
1773
+ * @param path - The host path where the JSON inventory is written.
1774
+ * @returns The validated manifest written to `path`.
1775
+ * @throws `ScaffoldError('INVALID', …)` when `path` is not a host path.
1776
+ * @throws `ScaffoldError('WRITE', …)` when a temporary host or the inventory
1777
+ * cannot be written or removed.
1778
+ * @throws `ScaffoldError('TARGET', …)` when the staged inventory does not read
1779
+ * back through the manifest validator.
1780
+ *
1781
+ * @remarks
1782
+ * Uses {@link stageHost} as the single vendored-path expansion. The temporary
1783
+ * host supplies the same entries, roots, per-file digests, and membership
1784
+ * digest as the published host while the requested output remains one JSON
1785
+ * file.
1786
+ *
1787
+ * @example
1788
+ * ```ts
1789
+ * import { stageInventory } from '@orkestrel/scaffold/server'
1790
+ *
1791
+ * stageInventory(process.cwd(), 'host.json') // the committed host inventory
1792
+ * ```
1793
+ */
1794
+ export declare function stageInventory(checkout: string, path: string): HostManifest;
1795
+
1571
1796
  /**
1572
1797
  * The reading spine: one bounded, unauthenticated, redirect-free request per answer.
1573
1798
  *
@@ -1586,6 +1811,11 @@ export declare class Materializer implements MaterializerInterface {
1586
1811
  * there is no fleet to report, so an unreachable or malformed list is a coded
1587
1812
  * `FETCH` failure.
1588
1813
  *
1814
+ * The vendored-file inventory is the other read a whole call rests on, and it
1815
+ * fails the other way: it fails every row of its call rather than throwing, so
1816
+ * the caller receives one whole dead answer it can replace with one whole
1817
+ * baseline instead of a mixture it would have to reconcile.
1818
+ *
1589
1819
  * Requests are unauthenticated because every fleet repository is public, and
1590
1820
  * they follow no redirect, so a misconfigured or hostile endpoint cannot move a
1591
1821
  * read to another host. Each one is bounded by its endpoint's timeout and by the
@@ -1608,7 +1838,7 @@ export declare class Materializer implements MaterializerInterface {
1608
1838
  export declare class Upstream implements UpstreamInterface {
1609
1839
  #private;
1610
1840
  /**
1611
- * Construct a reader over one guide host and one registry.
1841
+ * Construct a reader over one raw content host and one registry.
1612
1842
  *
1613
1843
  * @param options - The endpoints, the request bounds, the initial
1614
1844
  * listeners, and the listener-error handler.
@@ -1684,372 +1914,447 @@ export declare class Materializer implements MaterializerInterface {
1684
1914
  */
1685
1915
  fetch(names: readonly string[], current: Snapshot): Promise<readonly Mirror[]>;
1686
1916
  /**
1687
- * Catalog the published fleet from the registry's organization package list.
1917
+ * Read each named vendored file from the repository, beside the target bytes it answers for.
1688
1918
  *
1689
- * @returns One row per published package, sorted by name.
1690
- * @throws {@link ScaffoldError} coded `FETCH` when the organization package
1691
- * list is unreachable, malformed, empty, or larger than one bounded
1692
- * collection, and `DESTROYED` when the reader is torn down before or during
1693
- * the call.
1919
+ * @param paths - The target-relative vendored paths to read.
1920
+ * @param current - The target files as exact bytes, keyed by the same paths.
1921
+ * @returns One file verdict per path, in input order.
1922
+ * @throws {@link ScaffoldError} coded `INVALID` when `paths` is not a bounded
1923
+ * list of target-relative paths or `current` is not a snapshot, and
1924
+ * `DESTROYED` when the reader is torn down before or during the call.
1694
1925
  *
1695
1926
  * @remarks
1696
- * The organization list is the exact membership the registry publishes, not a
1697
- * relevance search, and it is the whole reason this answer can claim to be the
1698
- * fleet. So it fails hard where a per-package lookup fails soft: an empty or
1699
- * unreadable list would otherwise be written into a target's catalog table as
1700
- * a fleet with no packages in it. A package whose own version lookup then
1701
- * fails still keeps its row, carrying the cause instead of a version, because
1702
- * the list already proved the package is published.
1927
+ * The committed inventory is read once per call and decides every row, so a
1928
+ * path whose declared digest already matches the target's own bytes is `found`
1929
+ * without a request and the call spends nothing on it. An inventory that
1930
+ * produces no answer fails every row of the call rather than leaving some rows
1931
+ * live and some dead, which is what leaves the caller one whole baseline to
1932
+ * fall back to. A path the inventory does not name is `missing`.
1933
+ *
1934
+ * A fetched response's decoded content is verified against the digest the
1935
+ * inventory declares for that path, before any character decoding. Transport
1936
+ * encoding is transparent and does not enter the comparison, so content that
1937
+ * does not hash to the inventory's claim fails its row rather than reaching a
1938
+ * write. That is integrity against a single committed baseline, not
1939
+ * authenticity: it detects truncated, substituted, or stale content, and it
1940
+ * says nothing about who published the inventory.
1941
+ *
1942
+ * A guide mirror is never answered here whatever the caller asks for and
1943
+ * whatever the target holds, because those bytes belong to `fetch` and to the
1944
+ * mirror verb that writes them.
1703
1945
  *
1704
1946
  * @example
1705
1947
  * ```ts
1706
1948
  * import { Upstream } from '@orkestrel/scaffold/server'
1707
1949
  *
1708
1950
  * const upstream = new Upstream()
1709
- * const entries = await upstream.catalog()
1951
+ * await upstream.read(['AGENTS.md'], { 'AGENTS.md': '2320416745' })
1710
1952
  * upstream.destroy()
1711
1953
  * ```
1712
1954
  */
1713
- catalog(): Promise<readonly CatalogEntry[]>;
1955
+ read(paths: readonly string[], current: Snapshot): Promise<readonly HostFile[]>;
1714
1956
  /**
1715
- * Tear the reader down, aborting every request in flight. Teardown is idempotent.
1957
+ * Catalog the published fleet from the registry's organization package list.
1716
1958
  *
1717
- * @returns Nothing.
1959
+ * @returns One row per published package, sorted by name.
1960
+ * @throws {@link ScaffoldError} coded `FETCH` when the organization package
1961
+ * list is unreachable, malformed, empty, or larger than one bounded
1962
+ * collection, and `DESTROYED` when the reader is torn down before or during
1963
+ * the call.
1964
+ *
1965
+ * @remarks
1966
+ * The organization list is the exact membership the registry publishes, not a
1967
+ * relevance search, and it is the whole reason this answer can claim to be the
1968
+ * fleet. So it fails hard where a per-package lookup fails soft: an empty or
1969
+ * unreadable list would otherwise be written into a target's catalog table as
1970
+ * a fleet with no packages in it. A package whose own version lookup then
1971
+ * fails still keeps its row, carrying the cause instead of a version, because
1972
+ * the list already proved the package is published.
1973
+ *
1974
+ * @example
1975
+ * ```ts
1976
+ * import { Upstream } from '@orkestrel/scaffold/server'
1977
+ *
1978
+ * const upstream = new Upstream()
1979
+ * const entries = await upstream.catalog()
1980
+ * upstream.destroy()
1981
+ * ```
1982
+ */
1983
+ catalog(): Promise<readonly CatalogEntry[]>;
1984
+ /**
1985
+ * Tear the reader down, aborting every request in flight. Teardown is idempotent.
1986
+ *
1987
+ * @returns Nothing.
1988
+ *
1989
+ * @remarks
1990
+ * A call still in flight rejects with a `DESTROYED` error rather than
1991
+ * resolving to a partial answer, because half a fleet reads exactly like a
1992
+ * whole one.
1993
+ *
1994
+ * @example
1995
+ * ```ts
1996
+ * import { Upstream } from '@orkestrel/scaffold/server'
1997
+ *
1998
+ * const upstream = new Upstream()
1999
+ * upstream.destroy()
2000
+ * upstream.emitter.destroyed // true
2001
+ * ```
2002
+ */
2003
+ destroy(): void;
2004
+ }
2005
+
2006
+ /**
2007
+ * The upstream reader's observation channel.
1718
2008
  *
1719
2009
  * @remarks
1720
- * A call still in flight rejects with a `DESTROYED` error rather than
1721
- * resolving to a partial answer, because half a fleet reads exactly like a
1722
- * whole one.
2010
+ * Each verdict is published whole rather than as a name beside a summary, so a
2011
+ * listener reads the same value the call returns and a failed lookup is told
2012
+ * apart from a successful one by the verdict's own discriminant rather than by
2013
+ * which event carried it.
2014
+ */
2015
+ export declare type UpstreamEventMap = {
2016
+ readonly release: readonly [release: Release];
2017
+ readonly mirror: readonly [mirror: Mirror];
2018
+ readonly file: readonly [file: HostFile];
2019
+ readonly error: readonly [error: unknown];
2020
+ readonly destroy: readonly [];
2021
+ };
2022
+
2023
+ /**
2024
+ * The upstream contract: the package's only network reader, and it never writes.
1723
2025
  *
1724
- * @example
1725
- * ```ts
1726
- * import { Upstream } from '@orkestrel/scaffold/server'
2026
+ * @remarks
2027
+ * A per-package failure is collected as a verdict carrying its cause, not
2028
+ * thrown, so one unreachable package never costs the caller the rest of the
2029
+ * answer. The organization list is the exception: without it there is no fleet
2030
+ * to report, so an unreachable or malformed list is a coded failure.
1727
2031
  *
1728
- * const upstream = new Upstream()
1729
- * upstream.destroy()
1730
- * upstream.emitter.destroyed // true
1731
- * ```
2032
+ * The vendored-file inventory is the other whole-answer read, and it fails
2033
+ * softly: an inventory that produces no answer fails every row of that call
2034
+ * rather than throwing, which is what leaves the caller one whole baseline to
2035
+ * fall back to instead of a mixture.
1732
2036
  */
1733
- destroy(): void;
1734
- }
1735
-
1736
- /**
1737
- * The upstream reader's observation channel.
1738
- *
1739
- * @remarks
1740
- * Each verdict is published whole rather than as a name beside a summary, so a
1741
- * listener reads the same value the call returns and a failed lookup is told
1742
- * apart from a successful one by the verdict's own discriminant rather than by
1743
- * which event carried it.
1744
- */
1745
- export declare type UpstreamEventMap = {
1746
- readonly release: readonly [release: Release];
1747
- readonly mirror: readonly [mirror: Mirror];
1748
- readonly error: readonly [error: unknown];
1749
- readonly destroy: readonly [];
1750
- };
2037
+ export declare interface UpstreamInterface {
2038
+ readonly emitter: EmitterInterface<UpstreamEventMap>;
2039
+ /**
2040
+ * Look up the newest release each declared range admits.
2041
+ *
2042
+ * @param dependencies - The declared dependencies to look up.
2043
+ * @returns One release verdict per dependency, in input order.
2044
+ */
2045
+ lookup(dependencies: readonly Dependency[]): Promise<readonly Release[]>;
2046
+ /**
2047
+ * Fetch each named package's guide, beside the local mirror it answers for.
2048
+ *
2049
+ * @param names - The packages to fetch: the target's declared set, or the whole organization.
2050
+ * @param current - The target's local mirrors as exact bytes, keyed by mirror path.
2051
+ * @returns One mirror verdict per name, in input order.
2052
+ */
2053
+ fetch(names: readonly string[], current: Snapshot): Promise<readonly Mirror[]>;
2054
+ /**
2055
+ * Read each named vendored file from the repository, beside the target bytes it answers for.
2056
+ *
2057
+ * @param paths - The target-relative vendored paths to read.
2058
+ * @param current - The target files as exact bytes, keyed by the same paths.
2059
+ * @returns One file verdict per path, in input order.
2060
+ */
2061
+ read(paths: readonly string[], current: Snapshot): Promise<readonly HostFile[]>;
2062
+ /**
2063
+ * Catalog the published fleet from the registry's organization package list.
2064
+ *
2065
+ * @returns One row per published package, sorted by name.
2066
+ */
2067
+ catalog(): Promise<readonly CatalogEntry[]>;
2068
+ /**
2069
+ * Tear the reader down, aborting every request in flight. Teardown is idempotent.
2070
+ *
2071
+ * @returns Nothing.
2072
+ */
2073
+ destroy(): void;
2074
+ }
1751
2075
 
1752
- /**
1753
- * The upstream contract: the package's only network reader, and it never writes.
1754
- *
1755
- * @remarks
1756
- * A per-package failure is collected as a verdict carrying its cause, not
1757
- * thrown, so one unreachable package never costs the caller the rest of the
1758
- * answer. The organization list is the exception: without it there is no fleet
1759
- * to report, so an unreachable or malformed list is a coded failure.
1760
- */
1761
- export declare interface UpstreamInterface {
1762
- readonly emitter: EmitterInterface<UpstreamEventMap>;
1763
- /**
1764
- * Look up the newest release each declared range admits.
1765
- *
1766
- * @param dependencies - The declared dependencies to look up.
1767
- * @returns One release verdict per dependency, in input order.
1768
- */
1769
- lookup(dependencies: readonly Dependency[]): Promise<readonly Release[]>;
1770
- /**
1771
- * Fetch each named package's guide, beside the local mirror it answers for.
1772
- *
1773
- * @param names - The packages to fetch: the target's declared set, or the whole organization.
1774
- * @param current - The target's local mirrors as exact bytes, keyed by mirror path.
1775
- * @returns One mirror verdict per name, in input order.
1776
- */
1777
- fetch(names: readonly string[], current: Snapshot): Promise<readonly Mirror[]>;
1778
- /**
1779
- * Catalog the published fleet from the registry's organization package list.
1780
- *
1781
- * @returns One row per published package, sorted by name.
1782
- */
1783
- catalog(): Promise<readonly CatalogEntry[]>;
1784
- /**
1785
- * Tear the reader down, aborting every request in flight. Teardown is idempotent.
1786
- *
1787
- * @returns Nothing.
1788
- */
1789
- destroy(): void;
1790
- }
2076
+ /**
2077
+ * Options for the upstream reader.
2078
+ *
2079
+ * @remarks
2080
+ * The endpoints are grouped under the entity each configures: `repository`
2081
+ * takes the raw content host's `base`, its `branch`, and its `timeout`;
2082
+ * `registry` takes the registry's `base` and `timeout`. One raw content host
2083
+ * serves both the fleet's guides and this package's own vendored files, so one
2084
+ * group configures both. `concurrency` bounds requests in
2085
+ * flight and `retries` opts into per-request retry on a transport fault.
2086
+ * `limit` bounds the bytes read from one response body and `budget` bounds the
2087
+ * bytes read across a whole call, so neither one oversized answer nor many
2088
+ * small ones can exhaust the caller. Every request is unauthenticated and
2089
+ * follows no redirect.
2090
+ */
2091
+ export declare interface UpstreamOptions {
2092
+ readonly repository?: {
2093
+ readonly base?: string;
2094
+ readonly branch?: string;
2095
+ readonly timeout?: number;
2096
+ };
2097
+ readonly registry?: {
2098
+ readonly base?: string;
2099
+ readonly timeout?: number;
2100
+ };
2101
+ readonly concurrency?: number;
2102
+ readonly retries?: number;
2103
+ readonly limit?: number;
2104
+ readonly budget?: number;
2105
+ readonly on?: EmitterHooks<UpstreamEventMap>;
2106
+ readonly error?: EmitterErrorHandler;
2107
+ }
1791
2108
 
1792
- /**
1793
- * Options for the upstream reader.
1794
- *
1795
- * @remarks
1796
- * The endpoints are grouped under the entity each configures: `guides`
1797
- * takes the guide host's `base`, its `branch`, and its `timeout`; `registry`
1798
- * takes the registry's `base` and `timeout`. `concurrency` bounds requests in
1799
- * flight and `retries` opts into per-request retry on a transport fault.
1800
- * `limit` bounds the bytes read from one response body and `budget` bounds the
1801
- * bytes read across a whole call, so neither one oversized answer nor many
1802
- * small ones can exhaust the caller. Every request is unauthenticated and
1803
- * follows no redirect.
1804
- */
1805
- export declare interface UpstreamOptions {
1806
- readonly guides?: {
1807
- readonly base?: string;
1808
- readonly branch?: string;
1809
- readonly timeout?: number;
1810
- };
1811
- readonly registry?: {
1812
- readonly base?: string;
1813
- readonly timeout?: number;
1814
- };
1815
- readonly concurrency?: number;
1816
- readonly retries?: number;
1817
- readonly limit?: number;
1818
- readonly budget?: number;
1819
- readonly on?: EmitterHooks<UpstreamEventMap>;
1820
- readonly error?: EmitterErrorHandler;
1821
- }
2109
+ /**
2110
+ * What git reports about a target's working tree.
2111
+ *
2112
+ * @remarks
2113
+ * `tracked` is the only set a deletion may draw from: git does not report the
2114
+ * loss of an untracked path and `git diff` cannot restore it, so an ignored
2115
+ * path such as an installed dependency tree, a build output, or an editor
2116
+ * directory survives every verb. `dirty` is every path carrying an uncommitted
2117
+ * change, taken repo-wide rather than over a write set, because deletion makes
2118
+ * the write set the whole workspace. A clean tree is an empty `dirty`. A target
2119
+ * that is not a git repository yields no `Worktree` at all, so the caller
2120
+ * decides what to do about that rather than reading it out of an invented
2121
+ * empty value.
2122
+ */
2123
+ export declare interface Worktree {
2124
+ readonly tracked: readonly string[];
2125
+ readonly dirty: readonly string[];
2126
+ }
1822
2127
 
1823
- /**
1824
- * One physical directory identity captured across a write transaction.
1825
- *
1826
- * @remarks
1827
- * Device and inode locate the directory and do not date it. Two directories
1828
- * occupying one slot at different times share an anchor, so an anchor proves
1829
- * where a write lands and never that the directory was left alone.
1830
- */
1831
- export declare interface WriteAnchor {
1832
- readonly path: string;
1833
- readonly device: number;
1834
- readonly inode: number;
1835
- }
1836
-
1837
- /** The final directory anchor of a write transaction and the subset one call created. */
1838
- export declare interface WriteDirectoryResult {
1839
- readonly anchor: WriteAnchor;
1840
- readonly created: readonly WriteAnchor[];
1841
- }
2128
+ /**
2129
+ * One physical directory identity captured across a write transaction.
2130
+ *
2131
+ * @remarks
2132
+ * Device and inode locate the directory and do not date it. Two directories
2133
+ * occupying one slot at different times share an anchor, so an anchor proves
2134
+ * where a write lands and never that the directory was left alone.
2135
+ */
2136
+ export declare interface WriteAnchor {
2137
+ readonly path: string;
2138
+ readonly device: number;
2139
+ readonly inode: number;
2140
+ }
2141
+
2142
+ /** The final directory anchor of a write transaction and the subset one call created. */
2143
+ export declare interface WriteDirectoryResult {
2144
+ readonly anchor: WriteAnchor;
2145
+ readonly created: readonly WriteAnchor[];
2146
+ }
1842
2147
 
1843
- /**
1844
- * One destination snapshot captured before a write and required to survive it.
1845
- *
1846
- * @remarks
1847
- * `device`, `inode`, `modified`, `size`, and `digest` are present only where
1848
- * the observed shape supplies them.
1849
- */
1850
- export declare interface WriteExpectation {
1851
- readonly path: string;
1852
- readonly shape: 'absent' | 'file' | 'directory';
1853
- readonly device?: number;
1854
- readonly inode?: number;
1855
- readonly modified?: number;
1856
- readonly size?: number;
1857
- readonly digest?: string;
1858
- }
1859
-
1860
- /** The narrower caller-observed destination state a write transaction must still match. */
1861
- export declare interface WritePrecondition {
1862
- readonly path: string;
1863
- readonly shape: 'absent' | 'file';
1864
- readonly digest?: string;
1865
- }
2148
+ /**
2149
+ * One destination snapshot captured before a write and required to survive it.
2150
+ *
2151
+ * @remarks
2152
+ * `device`, `inode`, `modified`, `size`, and `digest` are present only where
2153
+ * the observed shape supplies them.
2154
+ */
2155
+ export declare interface WriteExpectation {
2156
+ readonly path: string;
2157
+ readonly shape: 'absent' | 'file' | 'directory';
2158
+ readonly device?: number;
2159
+ readonly inode?: number;
2160
+ readonly modified?: number;
2161
+ readonly size?: number;
2162
+ readonly digest?: string;
2163
+ }
2164
+
2165
+ /** The narrower caller-observed destination state a write transaction must still match. */
2166
+ export declare interface WritePrecondition {
2167
+ readonly path: string;
2168
+ readonly shape: 'absent' | 'file';
2169
+ readonly digest?: string;
2170
+ }
1866
2171
 
1867
- /**
1868
- * One staged, reversible mutation of one target directory.
1869
- *
1870
- * @remarks
1871
- * The transaction owns a private root beside the target — a sibling directory on
1872
- * the same volume, so every promotion is a rename rather than a copy. Staging
1873
- * writes go into that root and nothing else, so a call that fails while staging
1874
- * has not touched the target at all. Commit is the only step that mutates the
1875
- * target, and it is the only step that can need rolling back.
1876
- *
1877
- * A precondition and an expectation hold a destination still. A **precondition** is what the caller
1878
- * observed earlier and is checked once, at construction, so a target that moved
1879
- * between the caller's read and this transaction fails before anything is
1880
- * created. An **expectation** is captured here, at construction, and re-checked
1881
- * at commit, so a target that moves while the write is being staged fails before
1882
- * anything is promoted.
1883
- *
1884
- * What this provides, exactly:
1885
- *
1886
- * - **Across destinations, staged-then-swapped with rollback on a caught
1887
- * failure.** A failure part way through commit restores every destination it
1888
- * already promoted, restores every file it already took, and removes every
1889
- * directory it created, then reports what recovery could not undo. This is
1890
- * measured: a promotion is driven to fail after an earlier one landed, and the
1891
- * earlier destination is read back.
1892
- * - **No partly written destination.** Every file is written whole into the
1893
- * private root and digested there before commit, so a destination never
1894
- * receives bytes that were still being produced.
1895
- * - **Containment, not continuity, of the directories it creates.** Every
1896
- * ancestor is re-read between `mkdir` calls and again before the first
1897
- * promotion, so an ancestor that became a file, a symlink, a directory
1898
- * elsewhere, or nothing is refused. An ancestor deleted and recreated under
1899
- * the same name can receive its old inode back and is indistinguishable here
1900
- * from one that never moved.
1901
- * - **No crash atomicity across destinations.** A process killed between
1902
- * promotions leaves the target holding some new files and some old ones, and
1903
- * leaves the private root behind. Nothing here is a journal, and the private
1904
- * root's name is the only record a later run could read.
1905
- *
1906
- * A destination is preserved by hard link and then replaced by a single
1907
- * `rename`, rather than moved aside and replaced, so the path continues to name
1908
- * the old file right up to the swap. That is a property of `rename` on the host,
1909
- * not one this package's tests measure: telling it apart from move-then-replace
1910
- * needs a reader observing the destination inside the swap, and no test here
1911
- * does that. Read the claim as the mechanism it describes, not as a proven
1912
- * guarantee about a concurrent reader.
1913
- *
1914
- * A path names a file this transaction writes, takes, or establishes as a
1915
- * directory; every path is target-relative and is measured by the portable-path
1916
- * law. An expectation and a precondition both name the resolved destination
1917
- * instead, because that is the path each is re-read at.
1918
- *
1919
- * @example
1920
- * ```ts
1921
- * import { WriteTransaction } from '@orkestrel/scaffold/server'
1922
- *
1923
- * const transaction = new WriteTransaction('./packages/router', ['AGENTS.md'])
1924
- * try {
1925
- * transaction.write('AGENTS.md', '# Agents\n')
1926
- * transaction.commit() // ['AGENTS.md']
1927
- * } finally {
1928
- * transaction.discard()
1929
- * }
1930
- * ```
1931
- */
1932
- export declare class WriteTransaction {
1933
- #private;
1934
- /**
1935
- * Open a transaction over one target directory.
1936
- *
1937
- * @param target - The directory every path is written beneath.
1938
- * @param paths - Every target-relative path this transaction may touch.
1939
- * @param preconditions - The caller-observed destination states the whole
1940
- * transaction is held to, each naming a resolved destination of `paths`.
1941
- * @throws {@link ScaffoldError} coded `INVALID` when an argument is off
1942
- * contract, `TARGET` when a destination is a shape this package will not write
1943
- * over or no longer matches its precondition, and `WRITE` when the private
1944
- * root cannot be established.
1945
- *
1946
- * @remarks
1947
- * Nothing is created until every destination has been inspected and every
1948
- * precondition has held, so a refused transaction leaves no residue at all.
1949
- * The private root is created last and carries a random name, so two
1950
- * transactions over one target never collide.
1951
- */
1952
- constructor(target: string, paths: readonly string[], preconditions?: readonly WritePrecondition[]);
1953
- /** The resolved directory every path is written beneath. */
1954
- get target(): string;
1955
- /** What each destination held when the transaction opened, in path order. */
1956
- get expectations(): readonly WriteExpectation[];
1957
- /** Whether the transaction can still be committed or discarded. */
1958
- get open(): boolean;
2172
+ /**
2173
+ * One staged, reversible mutation of one target directory.
2174
+ *
2175
+ * @remarks
2176
+ * The transaction owns a private root beside the target — a sibling directory on
2177
+ * the same volume, so every promotion is a rename rather than a copy. Staging
2178
+ * writes go into that root and nothing else, so a call that fails while staging
2179
+ * has not touched the target at all. Commit is the only step that mutates the
2180
+ * target, and it is the only step that can need rolling back.
2181
+ *
2182
+ * A precondition and an expectation hold a destination still. A **precondition** is what the caller
2183
+ * observed earlier and is checked once, at construction, so a target that moved
2184
+ * between the caller's read and this transaction fails before anything is
2185
+ * created. An **expectation** is captured here, at construction, and re-checked
2186
+ * at commit, so a target that moves while the write is being staged fails before
2187
+ * anything is promoted.
2188
+ *
2189
+ * What this provides, exactly:
2190
+ *
2191
+ * - **Across destinations, staged-then-swapped with rollback on a caught
2192
+ * failure.** A failure part way through commit restores every destination it
2193
+ * already promoted, restores every file it already took, and removes every
2194
+ * directory it created, then reports what recovery could not undo. This is
2195
+ * measured: a promotion is driven to fail after an earlier one landed, and the
2196
+ * earlier destination is read back.
2197
+ * - **No partly written destination.** Every file is written whole into the
2198
+ * private root and digested there before commit, so a destination never
2199
+ * receives bytes that were still being produced.
2200
+ * - **Containment, not continuity, of the directories it creates.** Every
2201
+ * ancestor is re-read between `mkdir` calls and again before the first
2202
+ * promotion, so an ancestor that became a file, a symlink, a directory
2203
+ * elsewhere, or nothing is refused. An ancestor deleted and recreated under
2204
+ * the same name can receive its old inode back and is indistinguishable here
2205
+ * from one that never moved.
2206
+ * - **No crash atomicity across destinations.** A process killed between
2207
+ * promotions leaves the target holding some new files and some old ones, and
2208
+ * leaves the private root behind. Nothing here is a journal, and the private
2209
+ * root's name is the only record a later run could read.
2210
+ *
2211
+ * A destination is preserved by hard link and then replaced by a single
2212
+ * `rename`, rather than moved aside and replaced, so the path continues to name
2213
+ * the old file right up to the swap. That is a property of `rename` on the host,
2214
+ * not one this package's tests measure: telling it apart from move-then-replace
2215
+ * needs a reader observing the destination inside the swap, and no test here
2216
+ * does that. Read the claim as the mechanism it describes, not as a proven
2217
+ * guarantee about a concurrent reader.
2218
+ *
2219
+ * A path names a file this transaction writes, takes, or establishes as a
2220
+ * directory; every path is target-relative and is measured by the portable-path
2221
+ * law. An expectation and a precondition both name the resolved destination
2222
+ * instead, because that is the path each is re-read at.
2223
+ *
2224
+ * @example
2225
+ * ```ts
2226
+ * import { WriteTransaction } from '@orkestrel/scaffold/server'
2227
+ *
2228
+ * const transaction = new WriteTransaction('./packages/router', ['AGENTS.md'])
2229
+ * try {
2230
+ * transaction.write('AGENTS.md', '# Agents\n')
2231
+ * transaction.commit() // ['AGENTS.md']
2232
+ * } finally {
2233
+ * transaction.discard()
2234
+ * }
2235
+ * ```
2236
+ */
2237
+ export declare class WriteTransaction {
2238
+ #private;
1959
2239
  /**
1960
- * Stage one text file.
2240
+ * Open a transaction over one target directory.
1961
2241
  *
1962
- * @param path - The target-relative path to write.
1963
- * @param content - The exact UTF-8 text the destination must hold.
1964
- * @returns Nothing.
1965
- * @throws {@link ScaffoldError} coded `INVALID` when the path is not one this
1966
- * transaction opened or is already staged, `TARGET` when the destination holds
1967
- * a directory, and `WRITE` when the staged file cannot be written or does not
1968
- * carry the bytes it was given.
2242
+ * @param target - The directory every path is written beneath.
2243
+ * @param paths - Every target-relative path this transaction may touch.
2244
+ * @param preconditions - The caller-observed destination states the whole
2245
+ * transaction is held to, each naming a resolved destination of `paths`.
2246
+ * @throws {@link ScaffoldError} coded `INVALID` when an argument is off
2247
+ * contract, `TARGET` when a destination is a shape this package will not write
2248
+ * over or no longer matches its precondition, and `WRITE` when the private
2249
+ * root cannot be established.
1969
2250
  *
1970
2251
  * @remarks
1971
- * The staged file is read back and digested against the text it was given, so
1972
- * a partial or interrupted write is refused here rather than promoted later.
2252
+ * Nothing is created until every destination has been inspected and every
2253
+ * precondition has held, so a refused transaction leaves no residue at all.
2254
+ * The private root is created last and carries a random name, so two
2255
+ * transactions over one target never collide.
1973
2256
  */
1974
- write(path: string, content: string): void;
2257
+ constructor(target: string, paths: readonly string[], preconditions?: readonly WritePrecondition[]);
2258
+ /** The resolved directory every path is written beneath. */
2259
+ get target(): string;
2260
+ /** What each destination held when the transaction opened, in path order. */
2261
+ get expectations(): readonly WriteExpectation[];
2262
+ /** Whether the transaction can still be committed or discarded. */
2263
+ get open(): boolean;
1975
2264
  /**
1976
- * Stage one byte-for-byte copy of a file that already exists on this host.
2265
+ * Stage one text file.
1977
2266
  *
1978
2267
  * @param path - The target-relative path to write.
1979
- * @param source - The resolved absolute path to copy the bytes from.
1980
- * @param executable - If `true`, the destination carries the executable bit; if `false`, it does not.
2268
+ * @param content - The exact UTF-8 text the destination must hold.
1981
2269
  * @returns Nothing.
1982
2270
  * @throws {@link ScaffoldError} coded `INVALID` when the path is not one this
1983
2271
  * transaction opened or is already staged, `TARGET` when the destination holds
1984
- * a directory or the source is not a physical file, and `WRITE` when the copy
1985
- * cannot be made or does not digest to the source's bytes.
2272
+ * a directory, and `WRITE` when the staged file cannot be written or does not
2273
+ * carry the bytes it was given.
1986
2274
  *
1987
2275
  * @remarks
1988
- * The source is digested before the copy and the copy is digested after it, so
1989
- * a source that changed mid-copy is refused instead of staged.
2276
+ * The staged file is read back and digested against the text it was given, so
2277
+ * a partial or interrupted write is refused here rather than promoted later.
1990
2278
  */
1991
- copy(path: string, source: string, executable: boolean): void;
2279
+ write(path: string, content: string): void;
1992
2280
  /**
1993
- * Establish one directory inside the target, one segment at a time.
2281
+ * Stage one byte-for-byte copy of a file that already exists on this host.
1994
2282
  *
1995
- * @param path - The target-relative directory to establish.
1996
- * @returns The directory's identity and every segment this call created.
2283
+ * @param path - The target-relative path to write.
2284
+ * @param source - The resolved absolute path to copy the bytes from.
2285
+ * @param executable - If `true`, the destination carries the executable bit; if `false`, it does not.
2286
+ * @returns Nothing.
1997
2287
  * @throws {@link ScaffoldError} coded `INVALID` when the path is not one this
1998
- * transaction opened, `TARGET` when the destination holds a file, and `WRITE`
1999
- * when a segment cannot be created or changed while it was being created.
2288
+ * transaction opened or is already staged, `TARGET` when the destination holds
2289
+ * a directory or the source is not a physical file, and `WRITE` when the copy
2290
+ * cannot be made or does not digest to the source's bytes.
2000
2291
  *
2001
2292
  * @remarks
2002
- * A directory is created immediately rather than staged, because creating one
2003
- * destroys nothing: rollback removes exactly the segments this call created,
2004
- * innermost first, and leaves every segment that was already there. Each
2005
- * created segment is captured by device and inode, so a segment swapped
2006
- * underneath the transaction is detected rather than written into.
2293
+ * The source is digested before the copy and the copy is digested after it, so
2294
+ * a source that changed mid-copy is refused instead of staged.
2007
2295
  */
2008
- establish(path: string): WriteDirectoryResult;
2296
+ copy(path: string, source: string, executable: boolean): void;
2009
2297
  /**
2010
- * Mark one file for deletion at commit.
2298
+ * Establish one directory inside the target, one segment at a time.
2011
2299
  *
2012
- * @param path - The target-relative file to delete.
2013
- * @returns Nothing.
2300
+ * @param path - The target-relative directory to establish.
2301
+ * @returns The directory's identity and every segment this call created.
2014
2302
  * @throws {@link ScaffoldError} coded `INVALID` when the path is not one this
2015
- * transaction opened or is already claimed, and `TARGET` when the destination
2016
- * does not hold a file.
2303
+ * transaction opened, `TARGET` when the destination holds a file, and `WRITE`
2304
+ * when a segment cannot be created or changed while it was being created.
2017
2305
  *
2018
2306
  * @remarks
2019
- * Nothing moves here. Commit renames the file into the private backup rather
2020
- * than unlinking it, so a later failure in the same commit puts it back.
2307
+ * A directory is created immediately rather than staged, because creating one
2308
+ * destroys nothing: rollback removes exactly the segments this call created,
2309
+ * innermost first, and leaves every segment that was already there. Each
2310
+ * created segment is captured by device and inode, so a segment swapped
2311
+ * underneath the transaction is detected rather than written into.
2021
2312
  */
2022
- remove(path: string): void;
2313
+ establish(path: string): WriteDirectoryResult;
2023
2314
  /**
2024
- * Promote every staged file and take every marked file, or roll the whole call back.
2315
+ * Mark one file for deletion at commit.
2025
2316
  *
2026
- * @returns Every target-relative path whose destination changed: the files
2027
- * promoted, then the directories established, then the files taken.
2028
- * @throws {@link ScaffoldError} coded `WRITE` when the transaction is closed,
2029
- * when a destination moved since the transaction opened, or when the commit
2030
- * failed; a failure reports what rollback could not undo in its context.
2317
+ * @param path - The target-relative file to delete.
2318
+ * @returns Nothing.
2319
+ * @throws {@link ScaffoldError} coded `INVALID` when the path is not one this
2320
+ * transaction opened or is already claimed, and `TARGET` when the destination
2321
+ * does not hold a file.
2031
2322
  *
2032
2323
  * @remarks
2033
- * Every destination and every directory this transaction created is re-checked
2034
- * before anything moves, so the common failure moves nothing at all. That
2035
- * check runs inside the same rollback as the promotions, because a transaction
2036
- * that refuses before it starts still has a private root and created
2037
- * directories to clear. The transaction is closed either way: a committed one
2038
- * has nothing left to undo and a failed one has already been rolled back.
2324
+ * Nothing moves here. Commit renames the file into the private backup rather
2325
+ * than unlinking it, so a later failure in the same commit puts it back.
2039
2326
  */
2040
- commit(): readonly string[];
2327
+ remove(path: string): void;
2041
2328
  /**
2042
- * Abandon the transaction and remove everything it created.
2329
+ * Promote every staged file and take every marked file, or roll the whole call back.
2043
2330
  *
2044
- * @returns Nothing.
2045
- * @throws {@link ScaffoldError} coded `WRITE` when residue could not be
2046
- * removed, naming the private root that still holds it.
2331
+ * @returns Every target-relative path whose destination changed: the files
2332
+ * promoted, then the directories established, then the files taken.
2333
+ * @throws {@link ScaffoldError} coded `WRITE` when the transaction is closed,
2334
+ * when a destination moved since the transaction opened, or when the commit
2335
+ * failed; a failure reports what rollback could not undo in its context.
2047
2336
  *
2048
2337
  * @remarks
2049
- * Idempotent, and a no-op on a transaction that already committed or already
2050
- * failed, so a caller can put it in a `finally` beside the work it guards.
2338
+ * Every destination and every directory this transaction created is re-checked
2339
+ * before anything moves, so the common failure moves nothing at all. That
2340
+ * check runs inside the same rollback as the promotions, because a transaction
2341
+ * that refuses before it starts still has a private root and created
2342
+ * directories to clear. The transaction is closed either way: a committed one
2343
+ * has nothing left to undo and a failed one has already been rolled back.
2051
2344
  */
2052
- discard(): void;
2053
- }
2054
-
2055
- export { }
2345
+ commit(): readonly string[];
2346
+ /**
2347
+ * Abandon the transaction and remove everything it created.
2348
+ *
2349
+ * @returns Nothing.
2350
+ * @throws {@link ScaffoldError} coded `WRITE` when residue could not be
2351
+ * removed, naming the private root that still holds it.
2352
+ *
2353
+ * @remarks
2354
+ * Idempotent, and a no-op on a transaction that already committed or already
2355
+ * failed, so a caller can put it in a `finally` beside the work it guards.
2356
+ */
2357
+ discard(): void;
2358
+ }
2359
+
2360
+ export { }