@orkestrel/scaffold 0.0.44 → 0.0.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +10 -10
  2. package/dist/bin/main.js +31 -31
  3. package/dist/bin/main.js.map +1 -1
  4. package/dist/host/AGENTS.md +7 -2
  5. package/dist/host/agents/orchestration.md +232 -56
  6. package/dist/host/agents/skills/enterprise-bootstrap/SKILL.md +7 -7
  7. package/dist/host/agents/skills/enterprise-bootstrap/references/bootstrap-reference.md +11 -9
  8. package/dist/host/agents/skills/enterprise-bootstrap/references/components.md +3 -3
  9. package/dist/host/agents/skills/enterprise-bootstrap/references/frontend-design.md +3 -3
  10. package/dist/host/agents/skills/enterprise-bootstrap/references/utilities.md +1 -1
  11. package/dist/host/agents/skills/orkestrel-align-packages/SKILL.md +1 -1
  12. package/dist/host/agents/skills/orkestrel-build-application/SKILL.md +2 -2
  13. package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +1 -1
  14. package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +2 -2
  15. package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +8 -8
  16. package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +16 -14
  17. package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +3 -3
  18. package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +14 -14
  19. package/dist/host/agents/skills/orkestrel-harden-package/SKILL.md +1 -1
  20. package/dist/host/agents/skills/orkestrel-human-journey/SKILL.md +3 -3
  21. package/dist/host/agents/skills/orkestrel-human-journey/references/captures.md +3 -3
  22. package/dist/host/agents/skills/orkestrel-human-journey/references/layer.md +3 -3
  23. package/dist/host/agents/skills/orkestrel-polish-surface/SKILL.md +3 -3
  24. package/dist/host/claude/agents/analyst.md +2 -2
  25. package/dist/host/claude/agents/checker.md +2 -2
  26. package/dist/host/claude/agents/codex.md +5 -5
  27. package/dist/host/claude/agents/orkestrel.md +9 -8
  28. package/dist/host/claude/agents/planner.md +1 -1
  29. package/dist/host/claude/agents/researcher.md +2 -2
  30. package/dist/host/claude/agents/reviewer.md +1 -1
  31. package/dist/host/claude/agents/scout.md +2 -2
  32. package/dist/host/claude/agents/sol.md +3 -3
  33. package/dist/host/claude/agents/verifier.md +8 -0
  34. package/dist/host/claude/rules/application.md +7 -7
  35. package/dist/host/claude/rules/architecture.md +6 -6
  36. package/dist/host/claude/rules/documentation.md +1 -0
  37. package/dist/host/claude/rules/patterns.md +3 -3
  38. package/dist/host/claude/rules/quality.md +3 -3
  39. package/dist/host/claude/rules/tests.md +9 -2
  40. package/dist/host/claude/rules/workspace.md +7 -7
  41. package/dist/host/claude/rules/writing.md +12 -2
  42. package/dist/host/codex/agents/planner.toml +1 -1
  43. package/dist/host/configs/helpers.ts +21 -1
  44. package/dist/host/cursor/rules/orchestration.mdc +1 -1
  45. package/dist/host/dotfiles/gitignore +4 -1
  46. package/dist/host/guides/scaffold.md +134 -119
  47. package/dist/host/scripts/codex.sh +0 -0
  48. package/dist/host/scripts/cursor.sh +0 -0
  49. package/dist/host/scripts/deps.sh +0 -0
  50. package/dist/host/scripts/ollama.sh +0 -0
  51. package/dist/host/tests/config.test.ts +40 -1
  52. package/dist/host/tests/policy.test.ts +2 -2
  53. package/dist/host/tests/setupPolicy.ts +8 -5
  54. package/dist/src/core/index.cjs +143 -149
  55. package/dist/src/core/index.cjs.map +1 -1
  56. package/dist/src/core/index.d.cts +1624 -1643
  57. package/dist/src/core/index.d.ts +1624 -1643
  58. package/dist/src/core/index.js +144 -149
  59. package/dist/src/core/index.js.map +1 -1
  60. package/dist/src/server/index.cjs +82 -97
  61. package/dist/src/server/index.cjs.map +1 -1
  62. package/dist/src/server/index.d.cts +1844 -1871
  63. package/dist/src/server/index.d.ts +1844 -1871
  64. package/dist/src/server/index.js +83 -96
  65. package/dist/src/server/index.js.map +1 -1
  66. package/package.json +3 -3
@@ -79,7 +79,7 @@ export declare function computeFileDigest(path: string): string | undefined;
79
79
  * detect a membership edit that did not update it. Order is part of the claim
80
80
  * rather than normalized away, because a staged manifest sorts its entries and
81
81
  * roots once and a reordered copy is a different file. Each entry is projected
82
- * to exactly the three declared fields, so a hand-added property cannot ride
82
+ * to exactly the declared fields, so a hand-added property cannot ride
83
83
  * into the digest and cannot change it either.
84
84
  *
85
85
  * @example
@@ -92,1990 +92,1963 @@ export declare function computeFileDigest(path: string): string | undefined;
92
92
  export declare function computeManifestDigest(entries: readonly ManifestEntry[], roots: readonly string[]): string;
93
93
 
94
94
  /**
95
- * Construct a {@link Materializer}.
95
+ * The exact SHA-256 syntax a digest is stated in: sixty-four lowercase hexadecimal digits.
96
96
  *
97
- * @param options - The vendored host root, the initial listeners, and the listener-error handler.
98
- * @returns The materializer, typed as the contract consumers program against.
99
- * @throws {@link ScaffoldError} coded `INVALID` when `options` is present but is
100
- * not an option bag the materializer accepts, and `TARGET` when the host carries
101
- * a manifest that cannot be read or does not match what it stores.
102
- *
103
- * @example
104
- * ```ts
105
- * import { createMaterializer } from '@orkestrel/scaffold/server'
97
+ * @remarks
98
+ * Fixed length, unlike the core byte encoding, because a digest is one value of
99
+ * one algorithm rather than a variable run of bytes. Lowercase only, so separate
100
+ * spellings of one digest can never compare unequal.
101
+ */
102
+ export declare const DIGEST_PATTERN: RegExp;
103
+
104
+ /**
105
+ * The drive prefix a Windows host path may open with.
106
+ *
107
+ * @remarks
108
+ * The one segment allowed to carry a colon. Every other segment is measured by
109
+ * {@link INVALID_SEGMENT_CHARACTER_PATTERN}, which refuses one, so a stream name
110
+ * such as `file.txt:stream` cannot be smuggled through a later segment.
111
+ */
112
+ export declare const DRIVE_PATTERN: RegExp;
113
+
114
+ /**
115
+ * The complete vendored-host inventory.
116
+ *
117
+ * @remarks
118
+ * `roots` is the sorted directory inventory, which is what distinguishes a
119
+ * declared empty directory. `digest` is the SHA-256 of that exact entry and
120
+ * root membership, so a membership edit that did not update the digest is
121
+ * detected. A self-consistent replacement manifest defines its own smaller
122
+ * membership; authenticating omitted membership is outside a checksum's
123
+ * contract.
124
+ */
125
+ export declare interface HostManifest {
126
+ readonly entries: readonly ManifestEntry[];
127
+ readonly roots: readonly string[];
128
+ readonly digest: string;
129
+ }
130
+
131
+ /**
132
+ * Visible characters no host path segment may carry.
133
+ *
134
+ * @remarks
135
+ * Narrower than the core path law by exactly one character: a backslash is a
136
+ * separator on a Windows host rather than a forbidden character, so it is
137
+ * normalized to `/` before the segments are measured instead of refused here.
138
+ */
139
+ export declare const INVALID_SEGMENT_CHARACTER_PATTERN: RegExp;
140
+
141
+ /**
142
+ * Narrow a value to a Git branch the guide endpoint accepts.
143
+ *
144
+ * @remarks
145
+ * A branch reaches the guide URL's path, so the syntax is closed rather than
146
+ * merely bounded and no `..` is admitted anywhere in it.
147
+ *
148
+ * @example
149
+ * ```ts
150
+ * import { isBranch } from '@orkestrel/scaffold/server'
151
+ *
152
+ * isBranch('main') // true
153
+ * isBranch('main/../etc') // false
154
+ * ```
155
+ */
156
+ export declare const isBranch: Guard<string>;
157
+
158
+ /** Narrow a value to a bounded list of fleet catalog rows. */
159
+ export declare const isCatalogEntries: Guard<readonly CatalogEntry[]>;
160
+
161
+ /** Narrow a value to a bounded list of declared runtime dependencies. */
162
+ export declare const isDependencies: Guard<readonly Dependency[]>;
163
+
164
+ /**
165
+ * Narrow a value to a bounded list of `@orkestrel` package names.
166
+ *
167
+ * @remarks
168
+ * Composed from the core collection and dependency-name guards rather than
169
+ * restated, so the scope law that keeps a derived guide mirror inside its
170
+ * directory has exactly one home.
171
+ *
172
+ * @example
173
+ * ```ts
174
+ * import { isDependencyNames } from '@orkestrel/scaffold/server'
175
+ *
176
+ * isDependencyNames(['@orkestrel/router']) // true
177
+ * isDependencyNames(['router']) // false
178
+ * ```
179
+ */
180
+ export declare const isDependencyNames: Guard<readonly string[]>;
181
+
182
+ /**
183
+ * Narrow a value to one exact SHA-256 digest.
184
+ *
185
+ * @remarks
186
+ * The identity a vendored host manifest and a write precondition are both stated
187
+ * in. Fixed at sixty-four lowercase digits, so the value either is a digest of
188
+ * that algorithm or is refused; there is no shorter or longer accepted form.
189
+ *
190
+ * @example
191
+ * ```ts
192
+ * import { isDigest } from '@orkestrel/scaffold/server'
193
+ *
194
+ * isDigest('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855') // true
195
+ * isDigest('E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855') // false
196
+ * ```
197
+ */
198
+ export declare const isDigest: Guard<string>;
199
+
200
+ /**
201
+ * Narrow a value to a bounded upstream endpoint.
202
+ *
203
+ * @remarks
204
+ * Length only. Which schemes and hosts an endpoint may name is the reader's law,
205
+ * because it builds the request and can report why one was refused, where a
206
+ * guard has only `false` to say.
207
+ */
208
+ export declare const isEndpoint: Guard<string>;
209
+
210
+ /**
211
+ * Test whether a path is a physical file with exact on-disk casing.
212
+ *
213
+ * @param path - The host path to inspect segment by segment.
214
+ * @returns `true` only for a physical file whose requested segments exactly
215
+ * match the names each parent directory stores.
216
+ *
217
+ * @remarks
218
+ * A direct file lookup follows the host's case-folding rules on Windows and
219
+ * common macOS filesystems. Reading each parent directory supplies the stored
220
+ * names, so this predicate can enforce the package's exact-case structural
221
+ * contract on every supported host.
222
+ *
223
+ * @example
224
+ * ```ts
225
+ * import { isExactCaseFile } from '@orkestrel/scaffold/server'
226
+ *
227
+ * isExactCaseFile('/tmp/project/src/bin/main.ts') // true only for that exact spelling
228
+ * ```
229
+ */
230
+ export declare function isExactCaseFile(path: string): boolean;
231
+
232
+ /**
233
+ * Narrow a value to a path naming a location on this host.
234
+ *
235
+ * @param value - The candidate host path.
236
+ * @returns `true` for a bounded absolute or relative path whose every segment is
237
+ * portable across the supported filesystems.
238
+ *
239
+ * @remarks
240
+ * The counterpart to the core path law, not a copy of it. A target directory and
241
+ * the vendored host root are locations on the machine rather than paths inside a
242
+ * workspace, so a drive prefix, a UNC share, and a backslash separator are all
243
+ * admitted here and `..` is a legitimate way to name a sibling directory.
244
+ * Containment is still enforced, but by the core law over the artifact paths
245
+ * written beneath the target, not by this one.
246
+ *
247
+ * What it does refuse is a segment no supported filesystem can hold: an empty
248
+ * one, a reserved Windows device name, a trailing dot or space, a wildcard or
249
+ * redirection character, a colon anywhere but the drive prefix, and a name past
250
+ * the byte ceiling. The character ceiling is read first so an oversized string is
251
+ * refused before it is split.
252
+ *
253
+ * The spellings of an empty segment are answered differently. A trailing
254
+ * separator terminates a directory rather than opening a segment, and every
255
+ * supported filesystem and every Node path API reads `project/` and `project` as
256
+ * one location, so it is admitted. A doubled separator is a genuine empty
257
+ * segment, so `project//src` is refused. Nothing normalizes the argument first —
258
+ * every server entry point guards the caller's text and resolves it afterwards —
259
+ * so a directory taken from a shell completion arrives carrying the separator the
260
+ * shell appended and names the directory it appears to name.
261
+ *
262
+ * @example
263
+ * ```ts
264
+ * import { isFilesystemPath } from '@orkestrel/scaffold/server'
265
+ *
266
+ * isFilesystemPath('C:/Users/sample/project') // true
267
+ * isFilesystemPath('../sibling') // true
268
+ * isFilesystemPath('project/') // true
269
+ * isFilesystemPath('project//src') // false
270
+ * isFilesystemPath('project/nul') // false
271
+ * ```
272
+ */
273
+ export declare function isFilesystemPath(value: unknown): value is string;
274
+
275
+ /**
276
+ * Narrow a value to one {@link HostManifest}.
277
+ *
278
+ * @remarks
279
+ * The manifest is read from a directory a caller named, so it is the least
280
+ * trusted value the server face handles and is guarded whole: every entry, every
281
+ * declared root, and the digest that authenticates their membership.
282
+ */
283
+ export declare const isHostManifest: Guard<HostManifest>;
284
+
285
+ /**
286
+ * Narrow a value to a working-tree inventory within the limit one target may report.
287
+ *
288
+ * @param value - The candidate inventory.
289
+ * @returns `true` for an array of no more than `MAX_INVENTORY_PATHS` items.
290
+ *
291
+ * @remarks
292
+ * Compose this ahead of an element guard exactly as the core collection guard is
293
+ * composed, and for the same reason: the item count is settled before anything
294
+ * walks the items, and a hostile `length` accessor answers `false` rather than
295
+ * escaping as a throw. It exists beside that guard rather than reusing it
296
+ * because they bound different things — one bounds what a caller may hand a
297
+ * public method, this one bounds what a checkout may contain.
298
+ *
299
+ * @example
300
+ * ```ts
301
+ * import { isInventory } from '@orkestrel/scaffold/server'
302
+ *
303
+ * isInventory(['AGENTS.md']) // true
304
+ * isInventory('AGENTS.md') // false
305
+ * ```
306
+ */
307
+ export declare function isInventory(value: unknown): value is readonly unknown[];
308
+
309
+ /**
310
+ * Narrow a value to one {@link ManifestEntry}.
311
+ *
312
+ * @remarks
313
+ * Both paths are measured by the core path law, because a vendored host's
314
+ * storage name and the destination it maps to are each a path inside a
315
+ * workspace. That is what stops a hand-edited manifest from mapping a vendored
316
+ * file to a destination outside the target.
317
+ *
318
+ * @example
319
+ * ```ts
320
+ * import { isManifestEntry } from '@orkestrel/scaffold/server'
321
+ *
322
+ * isManifestEntry({ storage: 'AGENTS.md', destination: 'AGENTS.md', executable: false }) // true
323
+ * ```
324
+ */
325
+ export declare const isManifestEntry: Guard<ManifestEntry>;
326
+
327
+ /**
328
+ * Narrow a value to the materializer's initial listener record.
329
+ *
330
+ * @remarks
331
+ * Every event is optional and every declared value is a function. A key outside
332
+ * the materializer's event map is refused, so a listener wired to a misspelled
333
+ * event fails at construction instead of never firing.
334
+ */
335
+ export declare const isMaterializerHooks: Guard<EmitterHooks<MaterializerEventMap>>;
336
+
337
+ /**
338
+ * Narrow a value to {@link MaterializerOptions}.
339
+ *
340
+ * @example
341
+ * ```ts
342
+ * import { isMaterializerOptions } from '@orkestrel/scaffold/server'
343
+ *
344
+ * isMaterializerOptions({}) // true
345
+ * isMaterializerOptions({ host: 'dist/host*' }) // false
346
+ * ```
347
+ */
348
+ export declare const isMaterializerOptions: Guard<MaterializerOptions>;
349
+
350
+ /** Narrow a value to a bounded list of fetched guide mirrors. */
351
+ export declare const isMirrors: Guard<readonly Mirror[]>;
352
+
353
+ /**
354
+ * Test whether a path is a physical directory this package will read or write into.
355
+ *
356
+ * @param path - The resolved host path to inspect, without following links.
357
+ * @returns `true` only for a directory that is not a link.
358
+ *
359
+ * @remarks
360
+ * A junction and a directory symbolic link both report as directories after
361
+ * they are followed, so the inspection deliberately does not follow: a redirected
362
+ * directory is refused here rather than silently accepted as the one the caller
363
+ * named.
364
+ *
365
+ * @example
366
+ * ```ts
367
+ * import { isPhysicalDirectory } from '@orkestrel/scaffold/server'
368
+ *
369
+ * isPhysicalDirectory('/tmp/project') // true for a plain directory
370
+ * ```
371
+ */
372
+ export declare function isPhysicalDirectory(path: string): boolean;
373
+
374
+ /**
375
+ * Test whether a path is a physical file this package will read or replace.
376
+ *
377
+ * @param path - The resolved host path to inspect, without following links.
378
+ * @returns `true` only for a regular file that is neither a link nor hard-linked
379
+ * elsewhere.
380
+ *
381
+ * @remarks
382
+ * The link tests are the point. A symbolic link is a path pointing somewhere
383
+ * else, so writing through one writes outside the target; a hard link means a
384
+ * second name shares the same bytes, so replacing them changes a file nobody
385
+ * asked about. Both are refused rather than followed.
386
+ *
387
+ * @example
388
+ * ```ts
389
+ * import { isPhysicalFile } from '@orkestrel/scaffold/server'
390
+ *
391
+ * isPhysicalFile('/tmp/project/AGENTS.md') // true for a plain file
392
+ * ```
393
+ */
394
+ export declare function isPhysicalFile(path: string): boolean;
395
+
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
+ /**
416
+ * Narrow a value to a per-request timeout in milliseconds.
417
+ *
418
+ * @remarks
419
+ * A whole number of milliseconds, at least one and no more than
420
+ * {@link MAX_UPSTREAM_TIMEOUT}. Zero is refused because a request that may not
421
+ * take any time is a request that cannot succeed.
422
+ */
423
+ export declare const isTimeout: Guard<number>;
424
+
425
+ /**
426
+ * Narrow a value to the upstream reader's initial listener record.
427
+ *
428
+ * @remarks
429
+ * Closed to the reader's own events for the same reason the materializer's
430
+ * record is closed to its own.
431
+ */
432
+ export declare const isUpstreamHooks: Guard<EmitterHooks<UpstreamEventMap>>;
433
+
434
+ /**
435
+ * Narrow a value to {@link UpstreamOptions}.
436
+ *
437
+ * @remarks
438
+ * Each grouped endpoint is closed to its own leaves, so a setting written under
439
+ * the wrong entity is refused rather than ignored. Every numeric leaf is a whole
440
+ * number inside a ceiling: an unbounded concurrency, retry count, response
441
+ * limit, or call budget is a way to exhaust the caller, so the ceiling is stated
442
+ * here rather than left to the reader. The byte ceilings are the core
443
+ * artifact and total-artifact limits, because a fetched guide is an artifact and
444
+ * a whole call retains no more than a whole plan.
445
+ *
446
+ * @example
447
+ * ```ts
448
+ * import { isUpstreamOptions } from '@orkestrel/scaffold/server'
449
+ *
450
+ * isUpstreamOptions({ guides: { branch: 'main' }, concurrency: 4 }) // true
451
+ * isUpstreamOptions({ concurrency: 0 }) // false
452
+ * ```
453
+ */
454
+ export declare const isUpstreamOptions: Guard<UpstreamOptions>;
455
+
456
+ /**
457
+ * Test whether a target is safe to write a fresh workspace into.
458
+ *
459
+ * @param target - The candidate target directory.
460
+ * @returns `true` when the target is absent, empty, or holds nothing but its own
461
+ * `.git` directory.
462
+ *
463
+ * @remarks
464
+ * The green-field law. A checkout of an empty repository is where a new
465
+ * workspace legitimately starts, so that one directory is admitted and nothing
466
+ * else is; anything more means the caller is repairing a workspace rather than
467
+ * creating one. Only the first two entries are read, so the answer costs the
468
+ * same on an empty directory and on a full one.
469
+ *
470
+ * @example
471
+ * ```ts
472
+ * import { isVacant } from '@orkestrel/scaffold/server'
473
+ *
474
+ * isVacant('./packages/router-new') // true when absent, empty, or `.git` only
475
+ * ```
476
+ */
477
+ export declare function isVacant(target: string): boolean;
478
+
479
+ /**
480
+ * List a directory's descendant directories as sorted root-relative paths.
481
+ *
482
+ * @param root - The directory to inventory.
483
+ * @returns Every descendant directory as a `/`-separated root-relative path, in
484
+ * code-unit order, and `[]` when `root` is absent.
485
+ * @throws `ScaffoldError('INVALID', …)` when `root` is not a host path.
486
+ * @throws `ScaffoldError('TARGET', …)` when `root` is present but is not a
487
+ * physical directory, cannot be read, holds a name this package could not plan,
488
+ * or carries more entries or more nesting than one inventory may report.
489
+ *
490
+ * @remarks
491
+ * The sibling of {@link listFiles}, under the same bounds and the same refusals,
492
+ * and it exists because a directory holding no file is invisible to a file walk.
493
+ * That is the half a vendored host's `roots` declares and the half a file
494
+ * inventory cannot check, so a stager needs both walks to state a complete
495
+ * membership.
496
+ *
497
+ * `root` itself is not listed, because the answer is root-relative and the root
498
+ * has no root-relative name. A redirected directory is not listed and is not
499
+ * walked into, so no traversal can leave the root and no cycle can form.
500
+ *
501
+ * @example
502
+ * ```ts
503
+ * import { listDirectories } from '@orkestrel/scaffold/server'
504
+ *
505
+ * listDirectories('./.claude') // ['agents', 'rules', 'skills', …]
506
+ * ```
507
+ */
508
+ export declare function listDirectories(root: string): readonly string[];
509
+
510
+ /**
511
+ * List a directory's files as sorted root-relative paths.
512
+ *
513
+ * @param root - The directory to inventory.
514
+ * @returns Every descendant file as a `/`-separated root-relative path, in
515
+ * code-unit order, and `[]` when `root` is absent.
516
+ * @throws `ScaffoldError('INVALID', …)` when `root` is not a host path.
517
+ * @throws `ScaffoldError('TARGET', …)` when `root` is present but is not a
518
+ * physical directory, cannot be read, holds a name this package could not plan,
519
+ * or carries more entries or more nesting than one inventory may report.
520
+ *
521
+ * @remarks
522
+ * A whole-tree answer throws where a single-path answer returns `undefined`, and
523
+ * the reason is that a partial inventory reads exactly like a complete one. A
524
+ * caller comparing a target against a plan would treat a truncated listing as
525
+ * proof that the missing files are not there.
526
+ *
527
+ * Absence is the one exception: nothing to inventory is a complete answer, so it
528
+ * is the empty list. Links are listed as files rather than followed, so no
529
+ * traversal can leave the root and no cycle can form.
530
+ *
531
+ * @example
532
+ * ```ts
533
+ * import { listFiles } from '@orkestrel/scaffold/server'
534
+ *
535
+ * listFiles('./dist/host') // ['AGENTS.md', 'CLAUDE.md', 'LICENSE', …]
536
+ * ```
537
+ */
538
+ export declare function listFiles(root: string): readonly string[];
539
+
540
+ /**
541
+ * The reserved metadata name a staged vendored host writes at its own root.
542
+ *
543
+ * @remarks
544
+ * The one name a vendored file may never claim, because the staged root holds
545
+ * the manifest under it. The producer refuses a storage name equal to it and the
546
+ * reader looks for exactly this file, so both sides read one constant rather
547
+ * than repeating a literal that only agrees by inspection.
548
+ */
549
+ export declare const MANIFEST_NAME = "manifest.json";
550
+
551
+ /** One file record of the vendored host's manifest. */
552
+ export declare interface ManifestEntry {
553
+ readonly storage: string;
554
+ readonly destination: string;
555
+ readonly executable: boolean;
556
+ }
557
+
558
+ /**
559
+ * Test whether a captured directory is still the same directory.
560
+ *
561
+ * @param anchor - The identity captured earlier.
562
+ * @returns `true` when the path still holds a physical directory of that exact
563
+ * device and inode.
564
+ *
565
+ * @remarks
566
+ * This binds location rather than history. `true` means the path still resolves
567
+ * to the same physical directory on the same device, so the next write lands
568
+ * where the last one did. A path holding nothing, a file, or a symlink
569
+ * answers `false`; a directory swapped in by `rename` also answers `false`
570
+ * because the replacement carries its own inode. A directory deleted and made
571
+ * again under the same name can receive the old inode back and answers `true`,
572
+ * which nothing here detects.
573
+ *
574
+ * @example
575
+ * ```ts
576
+ * import { matchesAnchor, readAnchor } from '@orkestrel/scaffold/server'
577
+ *
578
+ * const anchor = readAnchor('/tmp/project')
579
+ * anchor !== undefined && matchesAnchor(anchor) // true while it is untouched
580
+ * ```
581
+ */
582
+ export declare function matchesAnchor(anchor: WriteAnchor): boolean;
583
+
584
+ /**
585
+ * Test whether a vendored path is one a target receives executable.
586
+ *
587
+ * @param path - The target-relative path to classify; either separator is read.
588
+ * @returns `true` when the path is declared in {@link EXECUTABLE_PATHS}.
589
+ *
590
+ * @remarks
591
+ * The declaration is the whole answer, and deliberately so. Reading the staging
592
+ * host's mode instead makes the manifest depend on where the package was built:
593
+ * Windows carries no executable bit, so a host staged there declares every entry
594
+ * non-executable and every target it later fills receives hooks at `0644`. One
595
+ * checkout stages one manifest on every host because this predicate never
596
+ * consults the filesystem.
597
+ *
598
+ * @example
599
+ * ```ts
600
+ * import { matchesExecutablePath } from '@orkestrel/scaffold/server'
601
+ *
602
+ * matchesExecutablePath('scripts/codex.sh') // true
603
+ * matchesExecutablePath('scripts\\deps.sh') // true
604
+ * matchesExecutablePath('AGENTS.md') // false
605
+ * ```
606
+ */
607
+ export declare function matchesExecutablePath(path: string): boolean;
608
+
609
+ /**
610
+ * Test whether a destination still holds what was captured of it.
611
+ *
612
+ * @param expectation - The state captured earlier.
613
+ * @returns `true` when re-reading the destination produces that same state.
614
+ *
615
+ * @remarks
616
+ * Compared field for field against a fresh {@link readExpectation}, so an
617
+ * expectation recorded without a digest matches only a destination that still
618
+ * has no digest to give. That is what keeps the comparison honest in both
619
+ * directions: nothing is treated as satisfied because it was never measured.
620
+ *
621
+ * @example
622
+ * ```ts
623
+ * import { matchesExpectation, readExpectation } from '@orkestrel/scaffold/server'
624
+ *
625
+ * const expectation = readExpectation('/tmp/project/AGENTS.md')
626
+ * expectation !== undefined && matchesExpectation(expectation) // true while untouched
627
+ * ```
628
+ */
629
+ export declare function matchesExpectation(expectation: WriteExpectation): boolean;
630
+
631
+ /**
632
+ * Test whether a path addresses a target's own repository metadata.
633
+ *
634
+ * @param path - The path to classify; either separator is read.
635
+ * @returns `true` for `.git` and for anything beneath it.
636
+ *
637
+ * @remarks
638
+ * The one home of the `.git` membership rule, read in either direction. A target
639
+ * holding nothing but this directory is still vacant, because a checkout of an
640
+ * empty repository is where a fresh workspace legitimately starts. A path
641
+ * beneath it is never removed and never vendored, because deleting a target's
642
+ * history is not a repair.
643
+ *
644
+ * @example
645
+ * ```ts
646
+ * import { matchesGitPath } from '@orkestrel/scaffold/server'
647
+ *
648
+ * matchesGitPath('.git') // true
649
+ * matchesGitPath('.git/config') // true
650
+ * matchesGitPath('.gitignore') // false
651
+ * ```
652
+ */
653
+ export declare function matchesGitPath(path: string): boolean;
654
+
655
+ /**
656
+ * Test whether a caught filesystem error reports an absent path.
657
+ *
658
+ * @param error - The caught value.
659
+ * @returns `true` only for an `Error` whose `code` is exactly `ENOENT`.
660
+ *
661
+ * @remarks
662
+ * The one place absence is told apart from failure. Every read here answers
663
+ * `undefined` or an empty result for a path that is not there and reports a path
664
+ * that is there but unreadable, so they must never be read from the same
665
+ * caught value by eye. Total for any caught value, including a hostile one.
666
+ *
667
+ * @example
668
+ * ```ts
669
+ * import { matchesMissingPath } from '@orkestrel/scaffold/server'
670
+ *
671
+ * matchesMissingPath(Object.assign(new Error('gone'), { code: 'ENOENT' })) // true
672
+ * matchesMissingPath(new Error('gone')) // false
673
+ * ```
674
+ */
675
+ export declare function matchesMissingPath(error: unknown): boolean;
676
+
677
+ /**
678
+ * Test whether a destination still matches the narrower state a caller observed.
679
+ *
680
+ * @param precondition - The caller-observed state the write is held to.
681
+ * @returns `true` when the destination is absent as stated, or holds a physical
682
+ * file whose bytes digest to the stated value.
683
+ *
684
+ * @remarks
685
+ * Narrower than {@link matchesExpectation} on purpose. A caller observed bytes,
686
+ * not inodes and timestamps, so binding a write to a device identity it never
687
+ * saw would refuse writes that are perfectly safe — a file rewritten to
688
+ * identical bytes by an editor is still the file the caller read. A precondition
689
+ * that states no digest claims presence only.
690
+ *
691
+ * @example
692
+ * ```ts
693
+ * import { matchesPrecondition } from '@orkestrel/scaffold/server'
694
+ *
695
+ * matchesPrecondition({ path: '/tmp/project/new.md', shape: 'absent' }) // true while absent
696
+ * ```
697
+ */
698
+ export declare function matchesPrecondition(precondition: WritePrecondition): boolean;
699
+
700
+ /**
701
+ * Test whether a target-relative path is one no verb may delete.
702
+ *
703
+ * @param path - The target-relative path to classify.
704
+ * @returns `true` when the path must survive every verb this package runs.
705
+ *
706
+ * @remarks
707
+ * The deletion deny-list, stated as a rule over paths rather than as a list of
708
+ * directories. It is the inversion the contract asks for: the candidate set
709
+ * is re-derived from the plan and narrowed by what git tracks, and the audit
710
+ * must agree with that derivation rather than supply the set itself.
711
+ * Repository metadata is protected because losing history is not a repair,
712
+ * and a target's own `src` and `app` trees are protected because a
713
+ * workspace's source is the one thing scaffold never plans and never owns. A
714
+ * plan the compiler emits never maps a protected root, so this guard exists
715
+ * for the caller-authored plan a consumer can still supply.
716
+ *
717
+ * @example
718
+ * ```ts
719
+ * import { matchesProtectedPath } from '@orkestrel/scaffold/server'
720
+ *
721
+ * matchesProtectedPath('src/core/index.ts') // true
722
+ * matchesProtectedPath('.git/config') // true
723
+ * matchesProtectedPath('.claude/agents/rogue.md') // false
724
+ * ```
725
+ */
726
+ export declare function matchesProtectedPath(path: string): boolean;
727
+
728
+ /**
729
+ * Test whether a path names local configuration or a credential.
730
+ *
731
+ * @param path - The path to classify; either separator is read.
732
+ * @returns `true` when the path must never be copied into a vendored host.
733
+ *
734
+ * @remarks
735
+ * The vendoring deny-list. A host root is staged from a real checkout, so the
736
+ * refusal is stated over the path rather than over the file's content: a
737
+ * credential is recognizable by where it sits and what it is called long before
738
+ * anything reads it. Repository metadata is included through
739
+ * {@link matchesGitPath}, so one call answers the whole question and no caller
740
+ * has to remember to ask twice.
741
+ *
742
+ * @example
743
+ * ```ts
744
+ * import { matchesSensitivePath } from '@orkestrel/scaffold/server'
745
+ *
746
+ * matchesSensitivePath('.npmrc') // true
747
+ * matchesSensitivePath('.claude/settings.local.json') // true
748
+ * matchesSensitivePath('.claude/settings.json') // false
749
+ * ```
750
+ */
751
+ export declare function matchesSensitivePath(path: string): boolean;
752
+
753
+ /**
754
+ * The mutation spine: read the vendored host, re-derive the target, stage, swap.
755
+ *
756
+ * @remarks
757
+ * Every verb runs the same steps. It snapshots each caller-supplied value
758
+ * and guards the snapshot, so a property backed by an accessor never reaches a
759
+ * decision. It re-derives what it is about to touch and compares that against
760
+ * the observation the caller handed in, refusing the whole call when anything
761
+ * moved. Only then does it open a {@link WriteTransaction}, which stages every
762
+ * byte in a private sibling directory and swaps them into place.
763
+ *
764
+ * The vendored host is read once, at construction, and cross-checked once. Its
765
+ * manifest authenticates its own membership, which is all a checksum sitting
766
+ * beside the data can do; matching that membership against the files actually
767
+ * stored is this class's job. The comparison is exact text and therefore exact
768
+ * case, so a manifest naming `agents.md` for a stored `AGENTS.md` is refused on
769
+ * a case-insensitive filesystem rather than silently resolved.
770
+ *
771
+ * What a mutation guarantees is exactly what {@link WriteTransaction}
772
+ * guarantees, and no more: a caught failure part way through a commit rolls the
773
+ * whole commit back, no destination ever receives half-written bytes, and a
774
+ * process killed mid-commit leaves a mixed target. This is not a journal and
775
+ * does not claim to be one.
776
+ *
777
+ * Every error is emitted on `error` immediately before it is thrown, so an
778
+ * observer sees a refusal even where the caller catches it.
779
+ *
780
+ * @example
781
+ * ```ts
782
+ * import type { Plan } from '@orkestrel/scaffold'
783
+ * import { Materializer } from '@orkestrel/scaffold/server'
784
+ *
785
+ * declare const plan: Plan
786
+ *
787
+ * const materializer = new Materializer({ host: './dist/host' })
788
+ * materializer.materialize(plan, './packages/router')
789
+ * materializer.destroy()
790
+ * ```
791
+ */
792
+ export declare class Materializer implements MaterializerInterface {
793
+ #private;
794
+ /**
795
+ * Construct a materializer over one vendored host root.
106
796
  *
107
- * const materializer = createMaterializer({ host: './dist/host' })
108
- * materializer.destroy()
109
- * ```
110
- */
111
- export declare function createMaterializer(options?: MaterializerOptions): MaterializerInterface;
112
-
113
- /**
114
- * Construct an {@link Upstream}.
115
- *
116
- * @param options - The two endpoints, the request bounds, the initial listeners,
117
- * and the listener-error handler.
118
- * @returns The reader, typed as the contract consumers program against.
119
- * @throws {@link ScaffoldError} coded `INVALID` when `options` is present but is
120
- * not an option bag the reader accepts, or when either endpoint names a scheme,
121
- * host, or form the reader will not request.
122
- *
123
- * @example
124
- * ```ts
125
- * import { createUpstream } from '@orkestrel/scaffold/server'
797
+ * @param options - The vendored host root, the initial listeners, and the
798
+ * listener-error handler.
799
+ * @throws {@link ScaffoldError} coded `INVALID` when `options` is present but
800
+ * 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.
802
+ *
803
+ * @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
807
+ * artifact maps onto it one to one.
808
+ *
809
+ * The host is read here rather than on first use, so a broken vendored root
810
+ * fails at construction where the caller can still act on it, and so nothing
811
+ * has to carry a second flag recording whether the read has happened yet.
812
+ */
813
+ constructor(options?: MaterializerOptions);
814
+ /** The materializer's observation channel. */
815
+ get emitter(): EmitterInterface<MaterializerEventMap>;
816
+ /**
817
+ * Compare a plan with a target through the vendored host that will repair it.
126
818
  *
127
- * const upstream = createUpstream({ guides: { branch: 'main' } })
128
- * upstream.destroy()
129
- * ```
130
- */
131
- export declare function createUpstream(options?: UpstreamOptions): UpstreamInterface;
132
-
133
- /**
134
- * The exact SHA-256 syntax a digest is stated in: sixty-four lowercase hexadecimal digits.
135
- *
136
- * @remarks
137
- * Fixed length, unlike the core byte encoding, because a digest is one value of
138
- * one algorithm rather than a variable run of bytes. Lowercase only, so two
139
- * spellings of one digest can never compare unequal.
140
- */
141
- export declare const DIGEST_PATTERN: RegExp;
142
-
143
- /**
144
- * The drive prefix a Windows host path may open with.
145
- *
146
- * @remarks
147
- * The one segment allowed to carry a colon. Every other segment is measured by
148
- * {@link INVALID_SEGMENT_CHARACTER_PATTERN}, which refuses one, so a stream name
149
- * such as `file.txt:stream` cannot be smuggled through a later segment.
150
- */
151
- export declare const DRIVE_PATTERN: RegExp;
152
-
153
- /**
154
- * The complete vendored-host inventory.
155
- *
156
- * @remarks
157
- * `roots` is the sorted directory inventory, which is what distinguishes a
158
- * declared empty directory. `digest` is the SHA-256 of that exact entry and
159
- * root membership, so a membership edit that did not update the digest is
160
- * detected. A self-consistent replacement manifest defines its own smaller
161
- * membership; authenticating omitted membership is outside a checksum's
162
- * contract.
163
- */
164
- export declare interface HostManifest {
165
- readonly entries: readonly ManifestEntry[];
166
- readonly roots: readonly string[];
167
- readonly digest: string;
168
- }
169
-
170
- /**
171
- * Visible characters no host path segment may carry.
172
- *
173
- * @remarks
174
- * Narrower than the core path law by exactly one character: a backslash is a
175
- * separator on a Windows host rather than a forbidden character, so it is
176
- * normalized to `/` before the segments are measured instead of refused here.
177
- */
178
- export declare const INVALID_SEGMENT_CHARACTER_PATTERN: RegExp;
179
-
180
- /**
181
- * Narrow a value to a Git branch the guide endpoint accepts.
182
- *
183
- * @remarks
184
- * A branch reaches the guide URL's path, so the syntax is closed rather than
185
- * merely bounded and no `..` is admitted anywhere in it.
186
- *
187
- * @example
188
- * ```ts
189
- * import { isBranch } from '@orkestrel/scaffold/server'
190
- *
191
- * isBranch('main') // true
192
- * isBranch('main/../etc') // false
193
- * ```
194
- */
195
- export declare const isBranch: Guard<string>;
196
-
197
- /** Narrow a value to a bounded list of fleet catalog rows. */
198
- export declare const isCatalogEntries: Guard<readonly CatalogEntry[]>;
199
-
200
- /** Narrow a value to a bounded list of declared runtime dependencies. */
201
- export declare const isDependencies: Guard<readonly Dependency[]>;
202
-
203
- /**
204
- * Narrow a value to a bounded list of `@orkestrel` package names.
205
- *
206
- * @remarks
207
- * Composed from the core collection and dependency-name guards rather than
208
- * restated, so the scope law that keeps a derived guide mirror inside its
209
- * directory has exactly one home.
210
- *
211
- * @example
212
- * ```ts
213
- * import { isDependencyNames } from '@orkestrel/scaffold/server'
214
- *
215
- * isDependencyNames(['@orkestrel/router']) // true
216
- * isDependencyNames(['router']) // false
217
- * ```
218
- */
219
- export declare const isDependencyNames: Guard<readonly string[]>;
220
-
221
- /**
222
- * Narrow a value to one exact SHA-256 digest.
223
- *
224
- * @remarks
225
- * The identity a vendored host manifest and a write precondition are both stated
226
- * in. Fixed at sixty-four lowercase digits, so the value either is a digest of
227
- * that algorithm or is refused; there is no shorter or longer accepted form.
228
- *
229
- * @example
230
- * ```ts
231
- * import { isDigest } from '@orkestrel/scaffold/server'
232
- *
233
- * isDigest('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855') // true
234
- * isDigest('E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855') // false
235
- * ```
236
- */
237
- export declare const isDigest: Guard<string>;
238
-
239
- /**
240
- * Narrow a value to a bounded upstream endpoint.
241
- *
242
- * @remarks
243
- * Length only. Which schemes and hosts an endpoint may name is the reader's law,
244
- * because it builds the request and can report why one was refused, where a
245
- * guard has only `false` to say.
246
- */
247
- export declare const isEndpoint: Guard<string>;
248
-
249
- /**
250
- * Test whether a path is a physical file with exact on-disk casing.
251
- *
252
- * @param path - The host path to inspect segment by segment.
253
- * @returns `true` only for a physical file whose requested segments exactly
254
- * match the names each parent directory stores.
255
- *
256
- * @remarks
257
- * A direct file lookup follows the host's case-folding rules on Windows and
258
- * common macOS filesystems. Reading each parent directory supplies the stored
259
- * names, so this predicate can enforce the package's exact-case structural
260
- * contract on every supported host.
261
- *
262
- * @example
263
- * ```ts
264
- * import { isExactCaseFile } from '@orkestrel/scaffold/server'
265
- *
266
- * isExactCaseFile('/tmp/project/src/bin/main.ts') // true only for that exact spelling
267
- * ```
268
- */
269
- export declare function isExactCaseFile(path: string): boolean;
270
-
271
- /**
272
- * Narrow a value to a path naming a location on this host.
273
- *
274
- * @param value - The candidate host path.
275
- * @returns `true` for a bounded absolute or relative path whose every segment is
276
- * portable across the supported filesystems.
277
- *
278
- * @remarks
279
- * The counterpart to the core path law, not a copy of it. A target directory and
280
- * the vendored host root are locations on the machine rather than paths inside a
281
- * workspace, so a drive prefix, a UNC share, and a backslash separator are all
282
- * admitted here and `..` is a legitimate way to name a sibling directory.
283
- * Containment is still enforced, but by the core law over the artifact paths
284
- * written beneath the target, not by this one.
285
- *
286
- * What it does refuse is a segment no supported filesystem can hold: an empty
287
- * one, a reserved Windows device name, a trailing dot or space, a wildcard or
288
- * redirection character, a colon anywhere but the drive prefix, and a name past
289
- * the byte ceiling. The character ceiling is read first so an oversized string is
290
- * refused before it is split.
291
- *
292
- * The two spellings of an empty segment are answered differently. A trailing
293
- * separator terminates a directory rather than opening a segment, and every
294
- * supported filesystem and every Node path API reads `project/` and `project` as
295
- * one location, so it is admitted. A doubled separator is a genuine empty
296
- * segment, so `project//src` is refused. Nothing normalizes the argument first —
297
- * every server entry point guards the caller's text and resolves it afterwards —
298
- * so a directory taken from a shell completion arrives carrying the separator the
299
- * shell appended and names the directory it appears to name.
300
- *
301
- * @example
302
- * ```ts
303
- * import { isFilesystemPath } from '@orkestrel/scaffold/server'
304
- *
305
- * isFilesystemPath('C:/Users/sample/project') // true
306
- * isFilesystemPath('../sibling') // true
307
- * isFilesystemPath('project/') // true
308
- * isFilesystemPath('project//src') // false
309
- * isFilesystemPath('project/nul') // false
310
- * ```
311
- */
312
- export declare function isFilesystemPath(value: unknown): value is string;
313
-
314
- /**
315
- * Narrow a value to one {@link HostManifest}.
316
- *
317
- * @remarks
318
- * The manifest is read from a directory a caller named, so it is the least
319
- * trusted value the server face handles and is guarded whole: every entry, every
320
- * declared root, and the digest that authenticates their membership.
321
- */
322
- export declare const isHostManifest: Guard<HostManifest>;
323
-
324
- /**
325
- * Narrow a value to a working-tree inventory within the limit one target may report.
326
- *
327
- * @param value - The candidate inventory.
328
- * @returns `true` for an array of no more than `MAX_INVENTORY_PATHS` items.
329
- *
330
- * @remarks
331
- * Compose this ahead of an element guard exactly as the core collection guard is
332
- * composed, and for the same reason: the item count is settled before anything
333
- * walks the items, and a hostile `length` accessor answers `false` rather than
334
- * escaping as a throw. It exists beside that guard rather than reusing it
335
- * because the two bound different things — one bounds what a caller may hand a
336
- * public method, this one bounds what a checkout may contain.
337
- *
338
- * @example
339
- * ```ts
340
- * import { isInventory } from '@orkestrel/scaffold/server'
341
- *
342
- * isInventory(['AGENTS.md']) // true
343
- * isInventory('AGENTS.md') // false
344
- * ```
345
- */
346
- export declare function isInventory(value: unknown): value is readonly unknown[];
347
-
348
- /**
349
- * Narrow a value to one {@link ManifestEntry}.
350
- *
351
- * @remarks
352
- * Both paths are measured by the core path law, because a vendored host's
353
- * storage name and the destination it maps to are each a path inside a
354
- * workspace. That is what stops a hand-edited manifest from mapping a vendored
355
- * file to a destination outside the target.
356
- *
357
- * @example
358
- * ```ts
359
- * import { isManifestEntry } from '@orkestrel/scaffold/server'
360
- *
361
- * isManifestEntry({ storage: 'AGENTS.md', destination: 'AGENTS.md', executable: false }) // true
362
- * ```
363
- */
364
- export declare const isManifestEntry: Guard<ManifestEntry>;
365
-
366
- /**
367
- * Narrow a value to the materializer's initial listener record.
368
- *
369
- * @remarks
370
- * Every event is optional and every declared value is a function. A key outside
371
- * the materializer's event map is refused, so a listener wired to a misspelled
372
- * event fails at construction instead of never firing.
373
- */
374
- export declare const isMaterializerHooks: Guard<EmitterHooks<MaterializerEventMap>>;
375
-
376
- /**
377
- * Narrow a value to {@link MaterializerOptions}.
378
- *
379
- * @example
380
- * ```ts
381
- * import { isMaterializerOptions } from '@orkestrel/scaffold/server'
382
- *
383
- * isMaterializerOptions({}) // true
384
- * isMaterializerOptions({ host: 'dist/host*' }) // false
385
- * ```
386
- */
387
- export declare const isMaterializerOptions: Guard<MaterializerOptions>;
388
-
389
- /** Narrow a value to a bounded list of fetched guide mirrors. */
390
- export declare const isMirrors: Guard<readonly Mirror[]>;
391
-
392
- /**
393
- * Test whether a path is a physical directory this package will read or write into.
394
- *
395
- * @param path - The resolved host path to inspect, without following links.
396
- * @returns `true` only for a directory that is not a link.
397
- *
398
- * @remarks
399
- * A junction and a directory symbolic link both report as directories once
400
- * followed, so the inspection deliberately does not follow: a redirected
401
- * directory is refused here rather than silently accepted as the one the caller
402
- * named.
403
- *
404
- * @example
405
- * ```ts
406
- * import { isPhysicalDirectory } from '@orkestrel/scaffold/server'
407
- *
408
- * isPhysicalDirectory('/tmp/project') // true for a plain directory
409
- * ```
410
- */
411
- export declare function isPhysicalDirectory(path: string): boolean;
412
-
413
- /**
414
- * Test whether a path is a physical file this package will read or replace.
415
- *
416
- * @param path - The resolved host path to inspect, without following links.
417
- * @returns `true` only for a regular file that is neither a link nor hard-linked
418
- * elsewhere.
419
- *
420
- * @remarks
421
- * The link tests are the point. A symbolic link is a path pointing somewhere
422
- * else, so writing through one writes outside the target; a hard link means a
423
- * second name shares the same bytes, so replacing them changes a file nobody
424
- * asked about. Both are refused rather than followed.
425
- *
426
- * @example
427
- * ```ts
428
- * import { isPhysicalFile } from '@orkestrel/scaffold/server'
429
- *
430
- * isPhysicalFile('/tmp/project/AGENTS.md') // true for a plain file
431
- * ```
432
- */
433
- export declare function isPhysicalFile(path: string): boolean;
434
-
435
- /**
436
- * Narrow a value to a {@link Repository}.
437
- *
438
- * @remarks
439
- * Both path lists are target-relative, so both are measured by the core path
440
- * law: a reported path that is not one this package could have planned is not a
441
- * path it will delete. The inventory guard bounds the lists, because a checkout
442
- * is legitimately far larger than any collection a caller hands a method.
443
- *
444
- * @example
445
- * ```ts
446
- * import { isRepository } from '@orkestrel/scaffold/server'
447
- *
448
- * isRepository({ tracked: ['AGENTS.md'], dirty: [] }) // true
449
- * isRepository({ tracked: ['../secrets'], dirty: [] }) // false
450
- * ```
451
- */
452
- export declare const isRepository: Guard<Repository>;
453
-
454
- /**
455
- * Narrow a value to a per-request timeout in milliseconds.
456
- *
457
- * @remarks
458
- * A whole number of milliseconds, at least one and no more than
459
- * {@link MAX_UPSTREAM_TIMEOUT}. Zero is refused because a request that may not
460
- * take any time is a request that cannot succeed.
461
- */
462
- export declare const isTimeout: Guard<number>;
463
-
464
- /**
465
- * Narrow a value to the upstream reader's initial listener record.
466
- *
467
- * @remarks
468
- * Closed to the reader's own four events for the same reason the materializer's
469
- * record is closed to its five.
470
- */
471
- export declare const isUpstreamHooks: Guard<EmitterHooks<UpstreamEventMap>>;
472
-
473
- /**
474
- * Narrow a value to {@link UpstreamOptions}.
475
- *
476
- * @remarks
477
- * Each grouped endpoint is closed to its own leaves, so a setting written under
478
- * the wrong entity is refused rather than ignored. Every numeric leaf is a whole
479
- * number inside a ceiling: an unbounded concurrency, retry count, response
480
- * limit, or call budget is a way to exhaust the caller, so the ceiling is stated
481
- * here rather than left to the reader. The two byte ceilings are the core
482
- * artifact and total-artifact limits, because a fetched guide is an artifact and
483
- * a whole call retains no more than a whole plan.
484
- *
485
- * @example
486
- * ```ts
487
- * import { isUpstreamOptions } from '@orkestrel/scaffold/server'
488
- *
489
- * isUpstreamOptions({ guides: { branch: 'main' }, concurrency: 4 }) // true
490
- * isUpstreamOptions({ concurrency: 0 }) // false
491
- * ```
492
- */
493
- export declare const isUpstreamOptions: Guard<UpstreamOptions>;
494
-
495
- /**
496
- * Test whether a target is safe to write a fresh workspace into.
497
- *
498
- * @param target - The candidate target directory.
499
- * @returns `true` when the target is absent, empty, or holds nothing but its own
500
- * `.git` directory.
501
- *
502
- * @remarks
503
- * The green-field law. A checkout of an empty repository is where a new
504
- * workspace legitimately starts, so that one directory is admitted and nothing
505
- * else is; anything more means the caller is repairing a workspace rather than
506
- * creating one. Only the first two entries are read, so the answer costs the
507
- * same on an empty directory and on a full one.
508
- *
509
- * @example
510
- * ```ts
511
- * import { isVacant } from '@orkestrel/scaffold/server'
512
- *
513
- * isVacant('./packages/router-new') // true when absent, empty, or `.git` only
514
- * ```
515
- */
516
- export declare function isVacant(target: string): boolean;
517
-
518
- /**
519
- * List a directory's descendant directories as sorted root-relative paths.
520
- *
521
- * @param root - The directory to inventory.
522
- * @returns Every descendant directory as a `/`-separated root-relative path, in
523
- * code-unit order, and `[]` when `root` is absent.
524
- * @throws `ScaffoldError('INVALID', …)` when `root` is not a host path.
525
- * @throws `ScaffoldError('TARGET', …)` when `root` is present but is not a
526
- * physical directory, cannot be read, holds a name this package could not plan,
527
- * or carries more entries or more nesting than one inventory may report.
528
- *
529
- * @remarks
530
- * The sibling of {@link listFiles}, under the same bounds and the same refusals,
531
- * and it exists because a directory holding no file is invisible to a file walk.
532
- * That is the half a vendored host's `roots` declares and the half a file
533
- * inventory cannot check, so a stager needs both walks to state a complete
534
- * membership.
535
- *
536
- * `root` itself is not listed, because the answer is root-relative and the root
537
- * has no root-relative name. A redirected directory is not listed and is not
538
- * walked into, so no traversal can leave the root and no cycle can form.
539
- *
540
- * @example
541
- * ```ts
542
- * import { listDirectories } from '@orkestrel/scaffold/server'
543
- *
544
- * listDirectories('./.claude') // ['agents', 'rules', 'skills', …]
545
- * ```
546
- */
547
- export declare function listDirectories(root: string): readonly string[];
548
-
549
- /**
550
- * List a directory's files as sorted root-relative paths.
551
- *
552
- * @param root - The directory to inventory.
553
- * @returns Every descendant file as a `/`-separated root-relative path, in
554
- * code-unit order, and `[]` when `root` is absent.
555
- * @throws `ScaffoldError('INVALID', …)` when `root` is not a host path.
556
- * @throws `ScaffoldError('TARGET', …)` when `root` is present but is not a
557
- * physical directory, cannot be read, holds a name this package could not plan,
558
- * or carries more entries or more nesting than one inventory may report.
559
- *
560
- * @remarks
561
- * A whole-tree answer throws where a single-path answer returns `undefined`, and
562
- * the reason is that a partial inventory reads exactly like a complete one. A
563
- * caller comparing a target against a plan would treat a truncated listing as
564
- * proof that the missing files are not there.
565
- *
566
- * Absence is the one exception: nothing to inventory is a complete answer, so it
567
- * is the empty list. Links are listed as files rather than followed, so no
568
- * traversal can leave the root and no cycle can form.
569
- *
570
- * @example
571
- * ```ts
572
- * import { listFiles } from '@orkestrel/scaffold/server'
573
- *
574
- * listFiles('./dist/host') // ['AGENTS.md', 'CLAUDE.md', 'LICENSE', …]
575
- * ```
576
- */
577
- export declare function listFiles(root: string): readonly string[];
578
-
579
- /**
580
- * The reserved metadata name a staged vendored host writes at its own root.
581
- *
582
- * @remarks
583
- * The one name a vendored file may never claim, because the staged root holds
584
- * the manifest under it. The producer refuses a storage name equal to it and the
585
- * reader looks for exactly this file, so both sides read one constant rather
586
- * than repeating a literal that only agrees by inspection.
587
- */
588
- export declare const MANIFEST_NAME = "manifest.json";
589
-
590
- /** One file record of the vendored host's manifest. */
591
- export declare interface ManifestEntry {
592
- readonly storage: string;
593
- readonly destination: string;
594
- readonly executable: boolean;
595
- }
596
-
597
- /**
598
- * Test whether a captured directory is still the same directory.
599
- *
600
- * @param anchor - The identity captured earlier.
601
- * @returns `true` when the path still holds a physical directory of that exact
602
- * device and inode.
603
- *
604
- * @remarks
605
- * This binds location rather than history. `true` means the path still resolves
606
- * to the same physical directory on the same device, so the next write lands
607
- * where the last one did. A path now holding nothing, a file, or a symlink
608
- * answers `false`; a directory swapped in by `rename` also answers `false`
609
- * because the replacement carries its own inode. A directory deleted and made
610
- * again under the same name can receive the old inode back and answers `true`,
611
- * which nothing here detects.
612
- *
613
- * @example
614
- * ```ts
615
- * import { matchesAnchor, readAnchor } from '@orkestrel/scaffold/server'
616
- *
617
- * const anchor = readAnchor('/tmp/project')
618
- * anchor !== undefined && matchesAnchor(anchor) // true while it is untouched
619
- * ```
620
- */
621
- export declare function matchesAnchor(anchor: WriteAnchor): boolean;
622
-
623
- /**
624
- * Test whether a vendored path is one a target receives executable.
625
- *
626
- * @param path - The target-relative path to classify; either separator is read.
627
- * @returns `true` when the path is declared in {@link EXECUTABLE_PATHS}.
628
- *
629
- * @remarks
630
- * The declaration is the whole answer, and deliberately so. Reading the staging
631
- * host's mode instead makes the manifest depend on where the package was built:
632
- * Windows carries no executable bit, so a host staged there declares every entry
633
- * non-executable and every target it later fills receives hooks at `0644`. One
634
- * checkout stages one manifest on every host because this predicate never
635
- * consults the filesystem.
636
- *
637
- * @example
638
- * ```ts
639
- * import { matchesExecutablePath } from '@orkestrel/scaffold/server'
640
- *
641
- * matchesExecutablePath('scripts/codex.sh') // true
642
- * matchesExecutablePath('scripts\\deps.sh') // true
643
- * matchesExecutablePath('AGENTS.md') // false
644
- * ```
645
- */
646
- export declare function matchesExecutablePath(path: string): boolean;
647
-
648
- /**
649
- * Test whether a destination still holds what was captured of it.
650
- *
651
- * @param expectation - The state captured earlier.
652
- * @returns `true` when re-reading the destination now produces that same state.
653
- *
654
- * @remarks
655
- * Compared field for field against a fresh {@link readExpectation}, so an
656
- * expectation recorded without a digest matches only a destination that still
657
- * has no digest to give. That is what keeps the comparison honest in both
658
- * directions: nothing is treated as satisfied because it was never measured.
659
- *
660
- * @example
661
- * ```ts
662
- * import { matchesExpectation, readExpectation } from '@orkestrel/scaffold/server'
663
- *
664
- * const expectation = readExpectation('/tmp/project/AGENTS.md')
665
- * expectation !== undefined && matchesExpectation(expectation) // true while untouched
666
- * ```
667
- */
668
- export declare function matchesExpectation(expectation: WriteExpectation): boolean;
669
-
670
- /**
671
- * Test whether a path addresses a target's own repository metadata.
672
- *
673
- * @param path - The path to classify; either separator is read.
674
- * @returns `true` for `.git` and for anything beneath it.
675
- *
676
- * @remarks
677
- * The one home of the `.git` membership rule, read from two directions. A target
678
- * holding nothing but this directory is still vacant, because a checkout of an
679
- * empty repository is where a fresh workspace legitimately starts. A path
680
- * beneath it is never removed and never vendored, because deleting a target's
681
- * history is not a repair.
682
- *
683
- * @example
684
- * ```ts
685
- * import { matchesGitPath } from '@orkestrel/scaffold/server'
686
- *
687
- * matchesGitPath('.git') // true
688
- * matchesGitPath('.git/config') // true
689
- * matchesGitPath('.gitignore') // false
690
- * ```
691
- */
692
- export declare function matchesGitPath(path: string): boolean;
693
-
694
- /**
695
- * Test whether a caught filesystem error reports an absent path.
696
- *
697
- * @param error - The caught value.
698
- * @returns `true` only for an `Error` whose `code` is exactly `ENOENT`.
699
- *
700
- * @remarks
701
- * The one place absence is told apart from failure. Every read here answers
702
- * `undefined` or an empty result for a path that is not there and reports a path
703
- * that is there but unreadable, so the two must never be read from the same
704
- * caught value by eye. Total for any caught value, including a hostile one.
705
- *
706
- * @example
707
- * ```ts
708
- * import { matchesMissingPath } from '@orkestrel/scaffold/server'
709
- *
710
- * matchesMissingPath(Object.assign(new Error('gone'), { code: 'ENOENT' })) // true
711
- * matchesMissingPath(new Error('gone')) // false
712
- * ```
713
- */
714
- export declare function matchesMissingPath(error: unknown): boolean;
715
-
716
- /**
717
- * Test whether a destination still matches the narrower state a caller observed.
718
- *
719
- * @param precondition - The caller-observed state the write is held to.
720
- * @returns `true` when the destination is absent as stated, or holds a physical
721
- * file whose bytes digest to the stated value.
722
- *
723
- * @remarks
724
- * Narrower than {@link matchesExpectation} on purpose. A caller observed bytes,
725
- * not inodes and timestamps, so binding a write to a device identity it never
726
- * saw would refuse writes that are perfectly safe — a file rewritten to
727
- * identical bytes by an editor is still the file the caller read. A precondition
728
- * that states no digest claims presence only.
729
- *
730
- * @example
731
- * ```ts
732
- * import { matchesPrecondition } from '@orkestrel/scaffold/server'
733
- *
734
- * matchesPrecondition({ path: '/tmp/project/new.md', shape: 'absent' }) // true while absent
735
- * ```
736
- */
737
- export declare function matchesPrecondition(precondition: WritePrecondition): boolean;
738
-
739
- /**
740
- * Test whether a target-relative path is one no verb may delete.
741
- *
742
- * @param path - The target-relative path to classify.
743
- * @returns `true` when the path must survive every verb this package runs.
744
- *
745
- * @remarks
746
- * The deletion deny-list, stated as a rule over paths rather than as a list of
747
- * directories. It is the inversion the contract asks for: the candidate set
748
- * comes from an audit's foreign findings narrowed by what git tracks, and this
749
- * is what that set is then measured against. Repository metadata is protected
750
- * because losing history is not a repair, and a target's own `src` and `app`
751
- * trees are protected because a workspace's source is the one thing scaffold
752
- * never plans and never owns, whatever an audit reports about it.
753
- *
754
- * @example
755
- * ```ts
756
- * import { matchesProtectedPath } from '@orkestrel/scaffold/server'
757
- *
758
- * matchesProtectedPath('src/core/index.ts') // true
759
- * matchesProtectedPath('.git/config') // true
760
- * matchesProtectedPath('.claude/agents/rogue.md') // false
761
- * ```
762
- */
763
- export declare function matchesProtectedPath(path: string): boolean;
764
-
765
- /**
766
- * Test whether a path names local configuration or a credential.
767
- *
768
- * @param path - The path to classify; either separator is read.
769
- * @returns `true` when the path must never be copied into a vendored host.
770
- *
771
- * @remarks
772
- * The vendoring deny-list. A host root is staged from a real checkout, so the
773
- * refusal is stated over the path rather than over the file's content: a
774
- * credential is recognizable by where it sits and what it is called long before
775
- * anything reads it. Repository metadata is included through
776
- * {@link matchesGitPath}, so one call answers the whole question and no caller
777
- * has to remember to ask twice.
778
- *
779
- * @example
780
- * ```ts
781
- * import { matchesSensitivePath } from '@orkestrel/scaffold/server'
782
- *
783
- * matchesSensitivePath('.npmrc') // true
784
- * matchesSensitivePath('.claude/settings.local.json') // true
785
- * matchesSensitivePath('.claude/settings.json') // false
786
- * ```
787
- */
788
- export declare function matchesSensitivePath(path: string): boolean;
789
-
790
- /**
791
- * The mutation spine: read the vendored host, re-derive the target, stage, swap.
792
- *
793
- * @remarks
794
- * Every verb runs the same three steps. It snapshots each caller-supplied value
795
- * and guards the snapshot, so a property backed by an accessor never reaches a
796
- * decision. It re-derives what it is about to touch and compares that against
797
- * the observation the caller handed in, refusing the whole call when anything
798
- * moved. Only then does it open a {@link WriteTransaction}, which stages every
799
- * byte in a private sibling directory and swaps them into place.
800
- *
801
- * The vendored host is read once, at construction, and cross-checked once. Its
802
- * manifest authenticates its own membership, which is all a checksum sitting
803
- * beside the data can do; matching that membership against the files actually
804
- * stored is this class's job. The comparison is exact text and therefore exact
805
- * case, so a manifest naming `agents.md` for a stored `AGENTS.md` is refused on
806
- * a case-insensitive filesystem rather than silently resolved.
807
- *
808
- * What a mutation guarantees is exactly what {@link WriteTransaction}
809
- * guarantees, and no more: a caught failure part way through a commit rolls the
810
- * whole commit back, no destination ever receives half-written bytes, and a
811
- * process killed mid-commit leaves a mixed target. This is not a journal and
812
- * does not claim to be one.
813
- *
814
- * Every error is emitted on `error` immediately before it is thrown, so an
815
- * observer sees a refusal even where the caller catches it.
816
- *
817
- * @example
818
- * ```ts
819
- * import type { Plan } from '@orkestrel/scaffold'
820
- * import { Materializer } from '@orkestrel/scaffold/server'
821
- *
822
- * declare const plan: Plan
823
- *
824
- * const materializer = new Materializer({ host: './dist/host' })
825
- * materializer.materialize(plan, './packages/router')
826
- * materializer.destroy()
827
- * ```
828
- */
829
- export declare class Materializer implements MaterializerInterface {
830
- #private;
819
+ * @param plan - The compiled plan to compare.
820
+ * @param target - The directory to inspect.
821
+ * @returns One finding per hydrated planned path, plus foreign files beneath owned host roots.
822
+ * @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
823
+ * exact shape, `TARGET` when the host or target cannot be read within its
824
+ * bounds, and `DESTROYED` after teardown.
825
+ *
826
+ * @remarks
827
+ * Host directories expand before the target is read, so this method and
828
+ * {@link repair} compare the same paths with the same ownership. Foreign
829
+ * candidates are files beneath those expanded roots only; a root file never
830
+ * becomes a deletion candidate merely because its group is selected.
831
+ */
832
+ audit(plan: Plan, target: string): Audit;
831
833
  /**
832
- * Construct a materializer over one vendored host root.
834
+ * Write a plan into a vacant target.
833
835
  *
834
- * @param options - The vendored host root, the initial listeners, and the
835
- * listener-error handler.
836
- * @throws {@link ScaffoldError} coded `INVALID` when `options` is present but
837
- * is not an option bag this materializer accepts, and `TARGET` when the host
838
- * carries a manifest that cannot be read or does not match what it stores.
836
+ * @param plan - The compiled plan to write.
837
+ * @param target - The directory to write into; it must hold nothing the plan would collide with.
838
+ * @returns The paths written and skipped.
839
+ * @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
840
+ * exact shape, `TARGET` when the target is not vacant or the host does not
841
+ * carry a planned artifact, `WRITE` when the write cannot be staged or
842
+ * committed, and `DESTROYED` after teardown.
839
843
  *
840
844
  * @remarks
841
- * `host` defaults to this package's own vendored root, resolved from this
842
- * module's own location so it never depends on the caller's working
843
- * directory. A host carrying no manifest is read as a raw checkout and every
844
- * artifact maps onto it one to one.
845
- *
846
- * The host is read here rather than on first use, so a broken vendored root
847
- * fails at construction where the caller can still act on it, and so nothing
848
- * has to carry a second flag recording whether the read has happened yet.
845
+ * The plan's own bytes are not trusted: every host-origin artifact is re-read
846
+ * from the vendored root, so what lands is what this package ships rather than
847
+ * what a caller-built plan claimed it ships. A vendored directory expands into
848
+ * one artifact per file beneath it, and a vendored directory holding no file
849
+ * at all is created as an empty directory.
849
850
  */
850
- constructor(options?: MaterializerOptions);
851
- /** The materializer's observation channel. */
852
- get emitter(): EmitterInterface<MaterializerEventMap>;
851
+ materialize(plan: Plan, target: string): MaterializeResult;
853
852
  /**
854
- * Compare a plan with a target through the vendored host that will repair it.
853
+ * Write a plan into an existing target, guided by an audit of it.
855
854
  *
856
- * @param plan - The compiled plan to compare.
857
- * @param target - The directory to inspect.
858
- * @returns One finding per hydrated planned path, plus foreign files beneath owned host roots.
855
+ * @param plan - The compiled plan to write.
856
+ * @param audit - The preview returned by this materializer's `audit` method.
857
+ * @param target - The directory to write into.
858
+ * @returns The paths written and skipped, each decided by its artifact's ownership.
859
859
  * @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
860
- * exact shape, `TARGET` when the host or target cannot be read within its
861
- * bounds, and `DESTROYED` after teardown.
860
+ * exact shape, `TARGET` when the target moved since its audit, `WRITE` when
861
+ * the write cannot be staged or committed, and `DESTROYED` after teardown.
862
862
  *
863
863
  * @remarks
864
- * Host directories expand before the target is read, so this method and
865
- * {@link repair} compare the same paths with the same ownership. Foreign
866
- * candidates are files beneath those expanded roots only; a root file never
867
- * becomes a deletion candidate merely because its group is selected.
864
+ * The audit is a preview, not an instruction. The plan is hydrated and
865
+ * compared against the target again here, and the verdicts that produces must
866
+ * match the ones the audit carried for every path the plan owns; anything else
867
+ * means the target moved, and the whole call is refused. The audit is checked
868
+ * for agreement rather than for plausibility, so a verdict the comparison could
869
+ * not have produced — a birth-owned path reported stale, which the `Finding`
870
+ * shape admits — disagrees with the derived one and is refused. A missing
871
+ * destination is restored whatever its ownership; a stale one is replaced only
872
+ * where the artifact claims its bytes, which is what leaves a presence-owned
873
+ * file a consumer has edited exactly as it is.
868
874
  */
869
- audit(plan: Plan, target: string): Audit;
875
+ repair(plan: Plan, audit: Audit, target: string): MaterializeResult;
870
876
  /**
871
- * Write a plan into a vacant target.
877
+ * Write fetched dependency guides to their local mirrors.
872
878
  *
873
- * @param plan - The compiled plan to write.
874
- * @param target - The directory to write into; it must hold nothing the plan would collide with.
875
- * @returns The paths written and skipped.
879
+ * @param mirrors - The fetched guides; each carries the local bytes its write is held to.
880
+ * @param target - The directory to write into.
881
+ * @returns The mirror paths written and skipped; a mirror already current is skipped.
876
882
  * @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
877
- * exact shape, `TARGET` when the target is not vacant or the host does not
878
- * carry a planned artifact, `WRITE` when the write cannot be staged or
879
- * committed, and `DESTROYED` after teardown.
883
+ * exact shape, `TARGET` when a mirror moved since it was fetched, `WRITE` when
884
+ * the write cannot be staged or committed, and `DESTROYED` after teardown.
880
885
  *
881
886
  * @remarks
882
- * The plan's own bytes are not trusted: every host-origin artifact is re-read
883
- * from the vendored root, so what lands is what this package ships rather than
884
- * what a caller-built plan claimed it ships. A vendored directory expands into
885
- * one artifact per file beneath it, and a vendored directory holding no file
886
- * at all is created as an empty directory.
887
+ * A verdict carrying no bytes carries a cause instead, so it is skipped rather
888
+ * than written: one unreachable package never costs the caller the rest of the
889
+ * fetch, and it never empties a mirror it could not replace.
887
890
  */
888
- materialize(plan: Plan, target: string): MaterializeResult;
891
+ mirror(mirrors: readonly Mirror[], target: string): MaterializeResult;
889
892
  /**
890
- * Write a plan into an existing target, guided by an audit of it.
893
+ * Rewrite the marker-bounded package table in the target's catalog agent file.
891
894
  *
892
- * @param plan - The compiled plan to write.
893
- * @param audit - The preview returned by this materializer's `audit` method.
895
+ * @param entries - The published packages the table must list.
894
896
  * @param target - The directory to write into.
895
- * @returns The paths written and skipped, each decided by its artifact's ownership.
897
+ * @returns The catalog path, written when the region's bytes moved and skipped otherwise.
896
898
  * @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
897
- * exact shape, `TARGET` when the target moved since its audit, `WRITE` when
898
- * the write cannot be staged or committed, and `DESTROYED` after teardown.
899
+ * exact shape, `TARGET` when the file is unreadable or carries no marked
900
+ * region, `WRITE` when the write cannot be staged or committed, and
901
+ * `DESTROYED` after teardown.
899
902
  *
900
903
  * @remarks
901
- * The audit is a preview, not an instruction. The plan is hydrated and
902
- * compared against the target again here, and the verdicts that produces must
903
- * match the ones the audit carried for every path the plan owns; anything else
904
- * means the target moved, and the whole call is refused. The audit is checked
905
- * for agreement rather than for plausibility, so a verdict the comparison could
906
- * not have produced — a birth-owned path reported stale, which the `Finding`
907
- * shape admits — disagrees with the derived one and is refused. A missing
908
- * destination is restored whatever its ownership; a stale one is replaced only
909
- * where the artifact claims its bytes, which is what leaves a presence-owned
910
- * file a consumer has edited exactly as it is.
904
+ * Only the text between the markers is replaced, so every word a consumer
905
+ * wrote around the table survives the call. A row whose lookup produced no
906
+ * version prints the cause it carries instead, because dropping the row would
907
+ * hide a package the organization publishes behind one failed request.
911
908
  */
912
- repair(plan: Plan, audit: Audit, target: string): MaterializeResult;
909
+ catalog(entries: readonly CatalogEntry[], target: string): MaterializeResult;
913
910
  /**
914
- * Write fetched dependency guides to their local mirrors.
911
+ * Rewrite the `@orkestrel/*` range set in the target's manifest.
915
912
  *
916
- * @param mirrors - The fetched guides; each carries the local bytes its write is held to.
913
+ * @param dependencies - The names and ranges the manifest must declare.
917
914
  * @param target - The directory to write into.
918
- * @returns The mirror paths written and skipped; a mirror already current is skipped.
915
+ * @returns The manifest path, written when a declared range moved and skipped otherwise.
919
916
  * @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
920
- * exact shape, `TARGET` when a mirror moved since it was fetched, `WRITE` when
921
- * the write cannot be staged or committed, and `DESTROYED` after teardown.
917
+ * exact shape or names a package the manifest does not declare, `TARGET` when
918
+ * the manifest is unreadable, `WRITE` when the write cannot be staged or
919
+ * committed, and `DESTROYED` after teardown.
922
920
  *
923
921
  * @remarks
924
- * A verdict carrying no bytes carries a cause instead, so it is skipped rather
925
- * than written: one unreachable package never costs the caller the rest of the
926
- * fetch, and it never empties a mirror it could not replace.
922
+ * No other part of the manifest is read back out or rewritten, so a consumer's
923
+ * own description, keywords, scripts, and formatting survive the call. Only a
924
+ * range already declared is rewritten: inserting a package would mean
925
+ * re-serializing the whole manifest, which is exactly the edit this verb
926
+ * promises not to make, so an undeclared name is refused by name instead.
927
927
  */
928
- mirror(mirrors: readonly Mirror[], target: string): MaterializeResult;
928
+ declare(dependencies: readonly Dependency[], target: string): MaterializeResult;
929
929
  /**
930
- * Rewrite the marker-bounded package table in the target's catalog agent file.
930
+ * Re-derive and delete the tracked files the plan does not own.
931
931
  *
932
- * @param entries - The published packages the table should list.
933
- * @param target - The directory to write into.
934
- * @returns The catalog path, written when the region's bytes moved and skipped otherwise.
932
+ * @param plan - The compiled plan that decides which paths are foreign.
933
+ * @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.
935
+ * @param target - The directory to delete from.
936
+ * @returns The paths removed.
935
937
  * @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
936
- * exact shape, `TARGET` when the file is unreadable or carries no marked
937
- * region, `WRITE` when the write cannot be staged or committed, and
938
- * `DESTROYED` after teardown.
938
+ * exact shape, `TARGET` when the tree carries uncommitted changes or a
939
+ * candidate moved since its audit, `WRITE` when the deletion cannot be staged
940
+ * or committed, and `DESTROYED` after teardown.
939
941
  *
940
942
  * @remarks
941
- * Only the text between the two markers is replaced, so every word a consumer
942
- * wrote around the table survives the call. A row whose lookup produced no
943
- * version prints the cause it carries instead, because dropping the row would
944
- * hide a package the organization publishes behind one failed request.
943
+ * The candidate set is re-derived and compared against the audit before
944
+ * anything moves, and every file is quarantined and re-verified rather than
945
+ * unlinked, so a failure part way through restores what it already took. The
946
+ * package's own source and application trees are never candidates, whatever
947
+ * the audit reports, and neither is anything git does not track: git is the
948
+ * recovery mechanism, so a path it cannot restore is not one this verb takes.
949
+ * A tree carrying uncommitted work is refused whole for the same reason.
950
+ *
951
+ * The whole call refuses when the preview disagrees with the re-derivation on
952
+ * any foreign finding, including one the deletion itself would skip, because a
953
+ * preview stale anywhere is stale evidence.
945
954
  */
946
- catalog(entries: readonly CatalogEntry[], target: string): MaterializeResult;
955
+ remove(plan: Plan, audit: Audit, repository: Repository, target: string): MaterializeResult;
947
956
  /**
948
- * Rewrite the `@orkestrel/*` range set in the target's manifest.
957
+ * Tear the materializer down. Every later call throws, and teardown is idempotent.
949
958
  *
950
- * @param dependencies - The names and ranges the manifest should declare.
951
- * @param target - The directory to write into.
952
- * @returns The manifest path, written when a declared range moved and skipped otherwise.
953
- * @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
954
- * exact shape or names a package the manifest does not declare, `TARGET` when
955
- * the manifest is unreadable, `WRITE` when the write cannot be staged or
956
- * committed, and `DESTROYED` after teardown.
957
- *
958
- * @remarks
959
- * No other part of the manifest is read back out or rewritten, so a consumer's
960
- * own description, keywords, scripts, and formatting survive the call. Only a
961
- * range already declared is rewritten: inserting a package would mean
962
- * re-serializing the whole manifest, which is exactly the edit this verb
963
- * promises not to make, so an undeclared name is refused by name instead.
964
- */
965
- declare(dependencies: readonly Dependency[], target: string): MaterializeResult;
966
- /**
967
- * Delete the files the plan does not own.
968
- *
969
- * @param audit - The preview returned by this materializer's `audit` method; its foreign findings are the candidate set.
970
- * @param repository - The target's git state; only a tracked path is ever deleted.
971
- * @param target - The directory to delete from.
972
- * @returns The paths removed.
973
- * @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
974
- * exact shape, `TARGET` when the tree carries uncommitted changes or a
975
- * candidate moved since its audit, `WRITE` when the deletion cannot be staged
976
- * or committed, and `DESTROYED` after teardown.
977
- *
978
- * @remarks
979
- * The candidate set is re-derived and compared against the audit before
980
- * anything moves, and every file is quarantined and re-verified rather than
981
- * unlinked, so a failure part way through restores what it already took. The
982
- * package's own source and application trees are never candidates, whatever
983
- * the audit reports, and neither is anything git does not track: git is the
984
- * recovery mechanism, so a path it cannot restore is not one this verb takes.
985
- * A tree carrying uncommitted work is refused whole for the same reason.
986
- */
987
- remove(audit: Audit, repository: Repository, target: string): MaterializeResult;
988
- /**
989
- * Tear the materializer down. Every later call throws, and teardown is idempotent.
990
- *
991
- * @returns Nothing.
992
- *
993
- * @example
994
- * ```ts
995
- * import { Materializer } from '@orkestrel/scaffold/server'
996
- *
997
- * const materializer = new Materializer()
998
- * materializer.destroy()
999
- * materializer.emitter.destroyed // true
1000
- * ```
1001
- */
1002
- destroy(): void;
1003
- }
959
+ * @returns Nothing.
960
+ *
961
+ * @example
962
+ * ```ts
963
+ * import { Materializer } from '@orkestrel/scaffold/server'
964
+ *
965
+ * const materializer = new Materializer()
966
+ * materializer.destroy()
967
+ * materializer.emitter.destroyed // true
968
+ * ```
969
+ */
970
+ destroy(): void;
971
+ }
1004
972
 
1005
- /**
1006
- * The outcome of one mutation of a target.
1007
- *
1008
- * @remarks
1009
- * `written` names every path this call created or replaced. `skipped` names
1010
- * every path it considered and left alone, whether because the target already
1011
- * matched or because the artifact's ownership forbade touching it. `removed`
1012
- * names every path it deleted.
1013
- */
1014
- export declare interface MaterializeResult {
1015
- readonly target: string;
1016
- readonly written: readonly string[];
1017
- readonly skipped: readonly string[];
1018
- readonly removed: readonly string[];
1019
- }
1020
-
1021
- /** The materializer's observation channel. */
1022
- export declare type MaterializerEventMap = {
1023
- readonly write: readonly [path: string];
1024
- readonly remove: readonly [path: string];
1025
- readonly finish: readonly [result: MaterializeResult];
1026
- readonly error: readonly [error: unknown];
1027
- readonly destroy: readonly [];
1028
- };
1029
-
1030
- /**
1031
- * The mutation contract: the package's only filesystem writer.
1032
- *
1033
- * @remarks
1034
- * Every method binds to the observation it was given. It re-derives what it is
1035
- * about to touch, compares that against the supplied preview, and refuses the
1036
- * whole call when membership or bytes moved, rather than racing to be the last
1037
- * writer.
1038
- */
1039
- export declare interface MaterializerInterface {
1040
- readonly emitter: EmitterInterface<MaterializerEventMap>;
1041
- /**
1042
- * Compare a plan with a target through the vendored host that will repair it.
1043
- *
1044
- * @param plan - The compiled plan to compare.
1045
- * @param target - The directory to inspect.
1046
- * @returns One finding per hydrated planned path, plus foreign files beneath owned host roots.
1047
- */
1048
- audit(plan: Plan, target: string): Audit;
1049
- /**
1050
- * Write a plan into a vacant target.
1051
- *
1052
- * @param plan - The compiled plan to write.
1053
- * @param target - The directory to write into; it must hold nothing the plan would collide with.
1054
- * @returns The paths written and skipped.
1055
- */
1056
- materialize(plan: Plan, target: string): MaterializeResult;
1057
- /**
1058
- * Write a plan into an existing target, guided by an audit of it.
1059
- *
1060
- * @param plan - The compiled plan to write.
1061
- * @param audit - The preview returned by this materializer's `audit` method.
1062
- * @param target - The directory to write into.
1063
- * @returns The paths written and skipped, each decided by its artifact's ownership.
1064
- *
1065
- * @remarks
1066
- * The audit is checked for agreement rather than for plausibility, so a verdict
1067
- * the comparison could not have produced — a birth-owned path reported stale,
1068
- * which the `Finding` shape admits — disagrees with the derived one and is
1069
- * refused.
1070
- *
1071
- * The whole audit is guarded before any of it is read, so an audit produced by
1072
- * an earlier version of this package is refused at runtime rather than only at
1073
- * compile time. A planned finding carries `ownership`, which findings made
1074
- * before that field existed do not, and the guard refuses the call with
1075
- * `INVALID`. Take a fresh audit from this materializer; a stored one is a record
1076
- * of what a target looked like then, not an argument to a write now. The
1077
- * refusal is deliberate at `0.0.x` and there is no migration.
1078
- */
1079
- repair(plan: Plan, audit: Audit, target: string): MaterializeResult;
1080
- /**
1081
- * Write fetched dependency guides to their local mirrors.
1082
- *
1083
- * @param mirrors - The fetched guides; each carries the local bytes its write is held to.
1084
- * @param target - The directory to write into.
1085
- * @returns The mirror paths written and skipped; a mirror already current is skipped.
1086
- */
1087
- mirror(mirrors: readonly Mirror[], target: string): MaterializeResult;
1088
- /**
1089
- * Rewrite the marker-bounded package table in the target's catalog agent file.
1090
- *
1091
- * @param entries - The published packages the table should list.
1092
- * @param target - The directory to write into.
1093
- * @returns The catalog path, written when the region's bytes moved and skipped otherwise.
1094
- */
1095
- catalog(entries: readonly CatalogEntry[], target: string): MaterializeResult;
1096
- /**
1097
- * Rewrite the `@orkestrel/*` range set in the target's manifest.
1098
- *
1099
- * @param dependencies - The names and ranges the manifest should declare.
1100
- * @param target - The directory to write into.
1101
- * @returns The manifest path, written when a declared range moved and skipped otherwise.
1102
- *
1103
- * @remarks
1104
- * No other part of the manifest is read back out or rewritten, so a
1105
- * consumer's own description, keywords, and scripts survive the call.
1106
- */
1107
- declare(dependencies: readonly Dependency[], target: string): MaterializeResult;
1108
- /**
1109
- * Delete the files the plan does not own.
1110
- *
1111
- * @param audit - The preview returned by this materializer's `audit` method; its foreign findings are the candidate set.
1112
- * @param repository - The target's git state; only a tracked path is ever deleted.
1113
- * @param target - The directory to delete from.
1114
- * @returns The paths removed.
1115
- *
1116
- * @remarks
1117
- * The candidate set is re-derived and compared against the audit before
1118
- * anything moves, and every file is quarantined and re-verified rather than
1119
- * unlinked, so a failure part way through restores what it already took.
1120
- * The package's own source and application trees are never candidates,
1121
- * whatever the audit reports.
1122
- *
1123
- * The whole audit is guarded before any of it is read, so an audit produced by
1124
- * an earlier version of this package is refused at runtime rather than only at
1125
- * compile time. Only foreign findings are candidates and those never carried
1126
- * `ownership`, but the guard reads every finding, so one planned finding made
1127
- * before that field existed refuses the whole call with `INVALID`. Take a fresh
1128
- * audit from this materializer. The refusal is deliberate at `0.0.x` and there
1129
- * is no migration.
1130
- */
1131
- remove(audit: Audit, repository: Repository, target: string): MaterializeResult;
1132
- /**
1133
- * Tear the materializer down. Every later call throws, and teardown is idempotent.
1134
- *
1135
- * @returns Nothing.
1136
- */
1137
- destroy(): void;
1138
- }
973
+ /**
974
+ * The outcome of one mutation of a target.
975
+ *
976
+ * @remarks
977
+ * `written` names every path this call created or replaced. `skipped` names
978
+ * every path it considered and left alone, whether because the target already
979
+ * matched or because the artifact's ownership forbade touching it. `removed`
980
+ * names every path it deleted.
981
+ */
982
+ export declare interface MaterializeResult {
983
+ readonly target: string;
984
+ readonly written: readonly string[];
985
+ readonly skipped: readonly string[];
986
+ readonly removed: readonly string[];
987
+ }
988
+
989
+ /** The materializer's observation channel. */
990
+ export declare type MaterializerEventMap = {
991
+ readonly write: readonly [path: string];
992
+ readonly remove: readonly [path: string];
993
+ readonly finish: readonly [result: MaterializeResult];
994
+ readonly error: readonly [error: unknown];
995
+ readonly destroy: readonly [];
996
+ };
1139
997
 
1140
- /**
1141
- * Options for the materializer.
1142
- *
1143
- * @remarks
1144
- * `host` is the vendored data root host-origin artifacts are copied from. It
1145
- * defaults to this package's own vendored root, resolved from the installed
1146
- * module's location rather than the caller's working directory. A host that
1147
- * carries no manifest beside it maps artifact paths one to one instead of
1148
- * through the manifest.
1149
- */
1150
- export declare interface MaterializerOptions {
1151
- readonly host?: string;
1152
- readonly on?: EmitterHooks<MaterializerEventMap>;
1153
- readonly error?: EmitterErrorHandler;
1154
- }
998
+ /**
999
+ * The mutation contract: the package's only filesystem writer.
1000
+ *
1001
+ * @remarks
1002
+ * Every method binds to the observation it was given. It re-derives what it is
1003
+ * about to touch, compares that against the supplied preview, and refuses the
1004
+ * whole call when membership or bytes moved, rather than racing to be the last
1005
+ * writer.
1006
+ */
1007
+ export declare interface MaterializerInterface {
1008
+ readonly emitter: EmitterInterface<MaterializerEventMap>;
1009
+ /**
1010
+ * Compare a plan with a target through the vendored host that will repair it.
1011
+ *
1012
+ * @param plan - The compiled plan to compare.
1013
+ * @param target - The directory to inspect.
1014
+ * @returns One finding per hydrated planned path, plus foreign files beneath owned host roots.
1015
+ */
1016
+ audit(plan: Plan, target: string): Audit;
1017
+ /**
1018
+ * Write a plan into a vacant target.
1019
+ *
1020
+ * @param plan - The compiled plan to write.
1021
+ * @param target - The directory to write into; it must hold nothing the plan would collide with.
1022
+ * @returns The paths written and skipped.
1023
+ */
1024
+ materialize(plan: Plan, target: string): MaterializeResult;
1025
+ /**
1026
+ * Write a plan into an existing target, guided by an audit of it.
1027
+ *
1028
+ * @param plan - The compiled plan to write.
1029
+ * @param audit - The preview returned by this materializer's `audit` method.
1030
+ * @param target - The directory to write into.
1031
+ * @returns The paths written and skipped, each decided by its artifact's ownership.
1032
+ *
1033
+ * @remarks
1034
+ * The audit is checked for agreement rather than for plausibility, so a verdict
1035
+ * the comparison could not have produced — a birth-owned path reported stale,
1036
+ * which the `Finding` shape admits — disagrees with the derived one and is
1037
+ * refused.
1038
+ *
1039
+ * The whole audit is guarded before any of it is read, so an audit produced by
1040
+ * an earlier version of this package is refused at runtime rather than only at
1041
+ * compile time. A planned finding carries `ownership`, which findings made
1042
+ * before that field existed do not, and the guard refuses the call with
1043
+ * `INVALID`. Take a fresh audit from this materializer; a stored one is a record
1044
+ * of what a target looked like then, not an argument to a fresh write. The
1045
+ * refusal is deliberate at `0.0.x` and there is no migration.
1046
+ */
1047
+ repair(plan: Plan, audit: Audit, target: string): MaterializeResult;
1048
+ /**
1049
+ * Write fetched dependency guides to their local mirrors.
1050
+ *
1051
+ * @param mirrors - The fetched guides; each carries the local bytes its write is held to.
1052
+ * @param target - The directory to write into.
1053
+ * @returns The mirror paths written and skipped; a mirror already current is skipped.
1054
+ */
1055
+ mirror(mirrors: readonly Mirror[], target: string): MaterializeResult;
1056
+ /**
1057
+ * Rewrite the marker-bounded package table in the target's catalog agent file.
1058
+ *
1059
+ * @param entries - The published packages the table must list.
1060
+ * @param target - The directory to write into.
1061
+ * @returns The catalog path, written when the region's bytes moved and skipped otherwise.
1062
+ */
1063
+ catalog(entries: readonly CatalogEntry[], target: string): MaterializeResult;
1064
+ /**
1065
+ * Rewrite the `@orkestrel/*` range set in the target's manifest.
1066
+ *
1067
+ * @param dependencies - The names and ranges the manifest must declare.
1068
+ * @param target - The directory to write into.
1069
+ * @returns The manifest path, written when a declared range moved and skipped otherwise.
1070
+ *
1071
+ * @remarks
1072
+ * No other part of the manifest is read back out or rewritten, so a
1073
+ * consumer's own description, keywords, and scripts survive the call.
1074
+ */
1075
+ declare(dependencies: readonly Dependency[], target: string): MaterializeResult;
1076
+ /**
1077
+ * Re-derive and delete the tracked files the plan does not own.
1078
+ *
1079
+ * @param plan - The compiled plan that decides which paths are foreign.
1080
+ * @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.
1082
+ * @param target - The directory to delete from.
1083
+ * @returns The paths removed.
1084
+ *
1085
+ * @remarks
1086
+ * The candidate set is re-derived and compared against the audit before
1087
+ * anything moves, and every file is quarantined and re-verified rather than
1088
+ * unlinked, so a failure part way through restores what it already took.
1089
+ * The package's own source and application trees are never candidates,
1090
+ * whatever the audit reports.
1091
+ *
1092
+ * The whole audit is guarded before any of it is read, so an audit produced by
1093
+ * an earlier version of this package is refused at runtime rather than only at
1094
+ * compile time. Only foreign findings are candidates and those never carried
1095
+ * `ownership`, but the guard reads every finding, so one planned finding made
1096
+ * before that field existed refuses the whole call with `INVALID`. Take a fresh
1097
+ * audit from this materializer. The refusal is deliberate at `0.0.x` and there
1098
+ * is no migration.
1099
+ *
1100
+ * The whole call refuses when the preview disagrees with the re-derivation on
1101
+ * any foreign finding, including one the deletion itself would skip, because a
1102
+ * preview stale anywhere is stale evidence.
1103
+ */
1104
+ remove(plan: Plan, audit: Audit, repository: Repository, target: string): MaterializeResult;
1105
+ /**
1106
+ * Tear the materializer down. Every later call throws, and teardown is idempotent.
1107
+ *
1108
+ * @returns Nothing.
1109
+ */
1110
+ destroy(): void;
1111
+ }
1155
1112
 
1156
- /** Maximum characters one guide branch may carry. */
1157
- export declare const MAX_BRANCH_LENGTH = 255;
1113
+ /**
1114
+ * Options for the materializer.
1115
+ *
1116
+ * @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.
1122
+ */
1123
+ export declare interface MaterializerOptions {
1124
+ readonly host?: string;
1125
+ readonly on?: EmitterHooks<MaterializerEventMap>;
1126
+ readonly error?: EmitterErrorHandler;
1127
+ }
1158
1128
 
1159
- /** Maximum characters one caller-supplied upstream endpoint may carry. */
1160
- export declare const MAX_ENDPOINT_LENGTH = 2048;
1129
+ /** Maximum characters one guide branch may carry. */
1130
+ export declare const MAX_BRANCH_LENGTH = 255;
1161
1131
 
1162
- /**
1163
- * Maximum paths one target's working-tree inventory may report.
1164
- *
1165
- * @remarks
1166
- * Far above the core collection ceiling, and deliberately so. A tracked or dirty
1167
- * path list is the target repository's own fact rather than an argument a caller
1168
- * authored, so measuring it against the ceiling that bounds a public collection
1169
- * would read a legitimately large checkout as hostile and refuse the deletion
1170
- * verb on it.
1171
- */
1172
- export declare const MAX_INVENTORY_PATHS = 100000;
1132
+ /** Maximum characters one caller-supplied upstream endpoint may carry. */
1133
+ export declare const MAX_ENDPOINT_LENGTH = 2048;
1173
1134
 
1174
- /**
1175
- * Maximum segments one host path may carry.
1176
- *
1177
- * @remarks
1178
- * Bounds the work a path decision costs before any filesystem call is made. With
1179
- * {@link MAX_PATH_SEGMENT_BYTES} it is also the real length ceiling of an
1180
- * accepted path, well inside the core character ceiling.
1181
- */
1182
- export declare const MAX_PATH_DEPTH = 64;
1135
+ /**
1136
+ * Maximum paths one target's working-tree inventory may report.
1137
+ *
1138
+ * @remarks
1139
+ * Far above the core collection ceiling, and deliberately so. A tracked or dirty
1140
+ * path list is the target repository's own fact rather than an argument a caller
1141
+ * authored, so measuring it against the ceiling that bounds a public collection
1142
+ * would read a legitimately large checkout as hostile and refuse the deletion
1143
+ * verb on it.
1144
+ */
1145
+ export declare const MAX_INVENTORY_PATHS = 100000;
1183
1146
 
1184
- /**
1185
- * Maximum UTF-8 bytes one host path segment may encode to.
1186
- *
1187
- * @remarks
1188
- * The limit every supported filesystem shares for a single name. It is a byte
1189
- * count rather than a character count, because that is the unit the filesystem
1190
- * imposes it in: eighty-six three-byte characters are already past it.
1191
- */
1192
- export declare const MAX_PATH_SEGMENT_BYTES = 255;
1147
+ /**
1148
+ * Maximum segments one host path may carry.
1149
+ *
1150
+ * @remarks
1151
+ * Bounds the work a path decision costs before any filesystem call is made. With
1152
+ * {@link MAX_PATH_SEGMENT_BYTES} it is also the real length ceiling of an
1153
+ * accepted path, well inside the core character ceiling.
1154
+ */
1155
+ export declare const MAX_PATH_DEPTH = 64;
1193
1156
 
1194
- /**
1195
- * Maximum simultaneous upstream requests.
1196
- *
1197
- * @remarks
1198
- * A ceiling rather than a default: the reader picks what it opens by, and this
1199
- * is only what a caller may raise it to.
1200
- */
1201
- export declare const MAX_UPSTREAM_CONCURRENCY = 64;
1157
+ /**
1158
+ * Maximum UTF-8 bytes one host path segment may encode to.
1159
+ *
1160
+ * @remarks
1161
+ * The limit every supported filesystem shares for a single name. It is a byte
1162
+ * count rather than a character count, because that is the unit the filesystem
1163
+ * imposes it in: eighty-six three-byte characters are already past it.
1164
+ */
1165
+ export declare const MAX_PATH_SEGMENT_BYTES = 255;
1202
1166
 
1203
- /** Maximum retries one upstream request may be given after a transport fault. */
1204
- export declare const MAX_UPSTREAM_RETRIES = 5;
1167
+ /**
1168
+ * Maximum simultaneous upstream requests.
1169
+ *
1170
+ * @remarks
1171
+ * A ceiling rather than a default: the reader picks what it opens by, and this
1172
+ * is only what a caller may raise it to.
1173
+ */
1174
+ export declare const MAX_UPSTREAM_CONCURRENCY = 64;
1205
1175
 
1206
- /** Maximum timeout one upstream request may be given, in milliseconds. */
1207
- export declare const MAX_UPSTREAM_TIMEOUT = 300000;
1176
+ /** Maximum retries one upstream request may be given after a transport fault. */
1177
+ export declare const MAX_UPSTREAM_RETRIES = 5;
1208
1178
 
1209
- /**
1210
- * Project a target-relative path to the storage name a vendored host holds it under.
1211
- *
1212
- * @param path - The target-relative path the file is written to.
1213
- * @returns The storage name beneath the host root.
1214
- *
1215
- * @remarks
1216
- * A staged host is a plain directory that npm packs, and npm's own ignore rules
1217
- * would drop a leading-dot entry from the tarball. So every dot that opens a
1218
- * segment comes off, and a dotted file at the root moves under `dotfiles/` to
1219
- * keep it from colliding with an undotted sibling of the same name. The mapping
1220
- * is one direction only: a staged host's manifest records the destination each
1221
- * storage name answers for, so the reader never re-derives this.
1222
- *
1223
- * @example
1224
- * ```ts
1225
- * import { pathToStorage } from '@orkestrel/scaffold/server'
1226
- *
1227
- * pathToStorage('.gitignore') // 'dotfiles/gitignore'
1228
- * pathToStorage('.claude/rules/names.md') // 'claude/rules/names.md'
1229
- * pathToStorage('AGENTS.md') // 'AGENTS.md'
1230
- * ```
1231
- */
1232
- export declare function pathToStorage(path: string): string;
1179
+ /** Maximum timeout one upstream request may be given, in milliseconds. */
1180
+ export declare const MAX_UPSTREAM_TIMEOUT = 300000;
1233
1181
 
1234
- /**
1235
- * Capture one directory's physical identity.
1236
- *
1237
- * @param path - The resolved directory path to capture.
1238
- * @returns The anchor, or `undefined` when the path is not a physical directory.
1239
- *
1240
- * @remarks
1241
- * Device and inode rather than the path, because the path is the thing that can
1242
- * be swapped underneath a write. An anchor captured before a mutation and
1243
- * checked again after it proves the directory written into sits where the
1244
- * inspected one sat, not that it is the one that was inspected.
1245
- *
1246
- * @example
1247
- * ```ts
1248
- * import { readAnchor } from '@orkestrel/scaffold/server'
1249
- *
1250
- * readAnchor('/tmp/project') // { path: '/tmp/project', device: 1, inode: 2 }
1251
- * ```
1252
- */
1253
- export declare function readAnchor(path: string): WriteAnchor | undefined;
1182
+ /**
1183
+ * Project a target-relative path to the storage name a vendored host holds it under.
1184
+ *
1185
+ * @param path - The target-relative path the file is written to.
1186
+ * @returns The storage name beneath the host root.
1187
+ *
1188
+ * @remarks
1189
+ * A staged host is a plain directory that npm packs, and npm's own ignore rules
1190
+ * would drop a leading-dot entry from the tarball. So every dot that opens a
1191
+ * segment comes off, and a dotted file at the root moves under `dotfiles/` to
1192
+ * keep it from colliding with an undotted sibling of the same name. The mapping
1193
+ * is one direction only: a staged host's manifest records the destination each
1194
+ * storage name answers for, so the reader never re-derives this.
1195
+ *
1196
+ * @example
1197
+ * ```ts
1198
+ * import { pathToStorage } from '@orkestrel/scaffold/server'
1199
+ *
1200
+ * pathToStorage('.gitignore') // 'dotfiles/gitignore'
1201
+ * pathToStorage('.claude/rules/names.md') // 'claude/rules/names.md'
1202
+ * pathToStorage('AGENTS.md') // 'AGENTS.md'
1203
+ * ```
1204
+ */
1205
+ export declare function pathToStorage(path: string): string;
1254
1206
 
1255
- /**
1256
- * Capture what one destination holds before a write.
1257
- *
1258
- * @param path - The resolved destination path to capture.
1259
- * @returns The expectation, or `undefined` when the destination is a link or a
1260
- * shape this package will not write over.
1261
- *
1262
- * @remarks
1263
- * Absence is a captured state rather than a failure, because most writes expect
1264
- * exactly that. Each shape carries only the facts it supplies: a directory
1265
- * carries its identity, a file carries its identity, size, and bytes, and an
1266
- * absent destination carries nothing at all. A file past the artifact ceiling
1267
- * carries no digest and is bound by its identity, size, and modification time
1268
- * alone, which is the strongest honest claim about bytes nobody read.
1269
- *
1270
- * @example
1271
- * ```ts
1272
- * import { readExpectation } from '@orkestrel/scaffold/server'
1273
- *
1274
- * readExpectation('/tmp/project/absent.md') // { path: …, shape: 'absent' }
1275
- * ```
1276
- */
1277
- export declare function readExpectation(path: string): WriteExpectation | undefined;
1207
+ /**
1208
+ * Capture one directory's physical identity.
1209
+ *
1210
+ * @param path - The resolved directory path to capture.
1211
+ * @returns The anchor, or `undefined` when the path is not a physical directory.
1212
+ *
1213
+ * @remarks
1214
+ * Device and inode rather than the path, because the path is the thing that can
1215
+ * be swapped underneath a write. An anchor captured before a mutation and
1216
+ * checked again after it proves the directory written into sits where the
1217
+ * inspected one sat, not that it is the one that was inspected.
1218
+ *
1219
+ * @example
1220
+ * ```ts
1221
+ * import { readAnchor } from '@orkestrel/scaffold/server'
1222
+ *
1223
+ * readAnchor('/tmp/project') // { path: '/tmp/project', device: 1, inode: 2 }
1224
+ * ```
1225
+ */
1226
+ export declare function readAnchor(path: string): WriteAnchor | undefined;
1278
1227
 
1279
- /**
1280
- * Read one contained file as its exact bytes in lowercase hexadecimal.
1281
- *
1282
- * @param root - The containing host directory.
1283
- * @param path - The portable root-relative file path.
1284
- * @param limit - The most bytes this read accepts; the artifact ceiling by default.
1285
- * @returns The exact bytes as hexadecimal, or `undefined` when the file is
1286
- * absent, is not a physical readable file, is past `limit`, or moved while it
1287
- * was being read.
1288
- * @throws `ScaffoldError('INVALID', …)` when the arguments are off contract or
1289
- * `path` leaves `root`.
1290
- *
1291
- * @remarks
1292
- * Hexadecimal rather than text, because this is what a byte comparison is stated
1293
- * in everywhere in this package: a plan's artifact, an audit finding, and a
1294
- * snapshot all compare as the same digits. The file's identity and size are
1295
- * measured before and after the read, and one extra byte is requested past the
1296
- * declared size, so a file that grew or was replaced mid-read answers
1297
- * `undefined` rather than half of two files.
1298
- *
1299
- * @example
1300
- * ```ts
1301
- * import { readFileHex } from '@orkestrel/scaffold/server'
1302
- *
1303
- * readFileHex('/tmp/project', 'AGENTS.md') // '2320416765…'
1304
- * ```
1305
- */
1306
- export declare function readFileHex(root: string, path: string, limit?: number): string | undefined;
1228
+ /**
1229
+ * Capture what one destination holds before a write.
1230
+ *
1231
+ * @param path - The resolved destination path to capture.
1232
+ * @returns The expectation, or `undefined` when the destination is a link or a
1233
+ * shape this package will not write over.
1234
+ *
1235
+ * @remarks
1236
+ * Absence is a captured state rather than a failure, because most writes expect
1237
+ * exactly that. Each shape carries only the facts it supplies: a directory
1238
+ * carries its identity, a file carries its identity, size, and bytes, and an
1239
+ * absent destination carries nothing at all. A file past the artifact ceiling
1240
+ * carries no digest and is bound by its identity, size, and modification time
1241
+ * alone, which is the strongest honest claim about bytes nobody read.
1242
+ *
1243
+ * @example
1244
+ * ```ts
1245
+ * import { readExpectation } from '@orkestrel/scaffold/server'
1246
+ *
1247
+ * readExpectation('/tmp/project/absent.md') // { path: …, shape: 'absent' }
1248
+ * ```
1249
+ */
1250
+ export declare function readExpectation(path: string): WriteExpectation | undefined;
1307
1251
 
1308
- /**
1309
- * Read one contained file as bounded UTF-8 text.
1310
- *
1311
- * @param root - The containing host directory.
1312
- * @param path - The portable root-relative file path.
1313
- * @param limit - The most bytes this read accepts; the artifact ceiling by default.
1314
- * @returns The decoded text, or `undefined` when {@link readFileHex} answers
1315
- * nothing or the bytes are not valid UTF-8.
1316
- * @throws `ScaffoldError('INVALID', …)` when the arguments are off contract or
1317
- * `path` leaves `root`.
1318
- *
1319
- * @remarks
1320
- * Decoding is strict, so a file carrying an invalid sequence answers `undefined`
1321
- * rather than text carrying replacement characters. That matters because the
1322
- * text is parsed next: a manifest silently repaired into valid JSON by lossy
1323
- * decoding would be trusted.
1324
- *
1325
- * @example
1326
- * ```ts
1327
- * import { readFileText } from '@orkestrel/scaffold/server'
1328
- *
1329
- * readFileText('/tmp/project', 'package.json') // '{ "name": "@orkestrel/router", … }'
1330
- * ```
1331
- */
1332
- export declare function readFileText(root: string, path: string, limit?: number): string | undefined;
1252
+ /**
1253
+ * Read one contained file as its exact bytes in lowercase hexadecimal.
1254
+ *
1255
+ * @param root - The containing host directory.
1256
+ * @param path - The portable root-relative file path.
1257
+ * @param limit - The most bytes this read accepts; the artifact ceiling by default.
1258
+ * @returns The exact bytes as hexadecimal, or `undefined` when the file is
1259
+ * absent, is not a physical readable file, is past `limit`, or moved while it
1260
+ * was being read.
1261
+ * @throws `ScaffoldError('INVALID', …)` when the arguments are off contract or
1262
+ * `path` leaves `root`.
1263
+ *
1264
+ * @remarks
1265
+ * Hexadecimal rather than text, because this is what a byte comparison is stated
1266
+ * in everywhere in this package: a plan's artifact, an audit finding, and a
1267
+ * snapshot all compare as the same digits. The file's identity and size are
1268
+ * measured before and after the read, and one extra byte is requested past the
1269
+ * declared size, so a file that grew or was replaced mid-read answers
1270
+ * `undefined` rather than half of one file and half of another.
1271
+ *
1272
+ * @example
1273
+ * ```ts
1274
+ * import { readFileHex } from '@orkestrel/scaffold/server'
1275
+ *
1276
+ * readFileHex('/tmp/project', 'AGENTS.md') // '2320416765…'
1277
+ * ```
1278
+ */
1279
+ export declare function readFileHex(root: string, path: string, limit?: number): string | undefined;
1333
1280
 
1334
- /**
1335
- * Read a vendored host's manifest, when it carries one.
1336
- *
1337
- * @param host - The vendored host root to read.
1338
- * @returns The manifest, or `undefined` when the host carries none.
1339
- * @throws `ScaffoldError('INVALID', …)` when `host` is not a host path.
1340
- * @throws `ScaffoldError('TARGET', …)` when the manifest is there but cannot be
1341
- * read, is not the declared shape, or does not match its own membership.
1342
- *
1343
- * @remarks
1344
- * The two failures are held apart deliberately. A host with no manifest is a
1345
- * raw checkout, and a caller reads it by mapping each path one to one. A host
1346
- * with a manifest that does not verify is a staged host that has been edited,
1347
- * and answering `undefined` there would degrade it to that same one-to-one
1348
- * mapping — which is how an edited manifest would get a caller to read files it
1349
- * never declared. So absence answers and corruption throws.
1350
- *
1351
- * Verification here is the manifest's own self-consistency: the digest against
1352
- * the exact membership beside it. Whether that membership matches the files
1353
- * actually stored is a separate question, and it belongs to the reader that
1354
- * walks the host.
1355
- *
1356
- * @example
1357
- * ```ts
1358
- * import { readHostManifest } from '@orkestrel/scaffold/server'
1359
- *
1360
- * readHostManifest('./dist/host') // the manifest, or undefined for a raw root
1361
- * ```
1362
- */
1363
- export declare function readHostManifest(host: string): HostManifest | undefined;
1281
+ /**
1282
+ * Read one contained file as bounded UTF-8 text.
1283
+ *
1284
+ * @param root - The containing host directory.
1285
+ * @param path - The portable root-relative file path.
1286
+ * @param limit - The most bytes this read accepts; the artifact ceiling by default.
1287
+ * @returns The decoded text, or `undefined` when {@link readFileHex} answers
1288
+ * nothing or the bytes are not valid UTF-8.
1289
+ * @throws `ScaffoldError('INVALID', …)` when the arguments are off contract or
1290
+ * `path` leaves `root`.
1291
+ *
1292
+ * @remarks
1293
+ * Decoding is strict, so a file carrying an invalid sequence answers `undefined`
1294
+ * rather than text carrying replacement characters. That matters because the
1295
+ * text is parsed next: a manifest silently repaired into valid JSON by lossy
1296
+ * decoding would be trusted.
1297
+ *
1298
+ * @example
1299
+ * ```ts
1300
+ * import { readFileText } from '@orkestrel/scaffold/server'
1301
+ *
1302
+ * readFileText('/tmp/project', 'package.json') // '{ "name": "@orkestrel/router", … }'
1303
+ * ```
1304
+ */
1305
+ export declare function readFileText(root: string, path: string, limit?: number): string | undefined;
1364
1306
 
1365
- /**
1366
- * Derive one vendored-host manifest entry from a file in a checkout.
1367
- *
1368
- * @param destination - The target-relative path the file is written to.
1369
- * @param source - The resolved host path the bytes are read from.
1370
- * @returns The entry, or `undefined` when `source` is not a physical file this
1371
- * package will vendor or carries more bytes than one artifact may.
1372
- *
1373
- * @remarks
1374
- * The one place the three declared fields are decided together, because they are
1375
- * three readings of one path: {@link pathToStorage} decides where it is stored,
1376
- * the destination is the path it answers for, and {@link matchesExecutablePath}
1377
- * decides whether a target receives it executable.
1378
- *
1379
- * The bit is read from that declaration rather than from the source's mode, so
1380
- * the entry does not depend on where the package was staged. A Windows host
1381
- * reports no executable bit at all, and reading the mode there declared every
1382
- * entry non-executable and shipped consumers hooks they could not run.
1383
- *
1384
- * @example
1385
- * ```ts
1386
- * import { readManifestEntry } from '@orkestrel/scaffold/server'
1387
- *
1388
- * readManifestEntry('.gitignore', '/tmp/checkout/.gitignore')
1389
- * // { storage: 'dotfiles/gitignore', destination: '.gitignore', executable: false }
1390
- * ```
1391
- */
1392
- export declare function readManifestEntry(destination: string, source: string): ManifestEntry | undefined;
1307
+ /**
1308
+ * Read a vendored host's manifest, when it carries one.
1309
+ *
1310
+ * @param host - The vendored host root to read.
1311
+ * @returns The manifest, or `undefined` when the host carries none.
1312
+ * @throws `ScaffoldError('INVALID', …)` when `host` is not a host path.
1313
+ * @throws `ScaffoldError('TARGET', …)` when the manifest is there but cannot be
1314
+ * read, is not the declared shape, or does not match its own membership.
1315
+ *
1316
+ * @remarks
1317
+ * The failures are held apart deliberately. A host with no manifest is a
1318
+ * raw checkout, and a caller reads it by mapping each path one to one. A host
1319
+ * with a manifest that does not verify is a staged host that has been edited,
1320
+ * and answering `undefined` there would degrade it to that same one-to-one
1321
+ * mapping which is how an edited manifest would get a caller to read files it
1322
+ * never declared. So absence answers and corruption throws.
1323
+ *
1324
+ * Verification here is the manifest's own self-consistency: the digest against
1325
+ * the exact membership beside it. Whether that membership matches the files
1326
+ * actually stored is a separate question, and it belongs to the reader that
1327
+ * walks the host.
1328
+ *
1329
+ * @example
1330
+ * ```ts
1331
+ * import { readHostManifest } from '@orkestrel/scaffold/server'
1332
+ *
1333
+ * readHostManifest('./dist/host') // the manifest, or undefined for a raw root
1334
+ * ```
1335
+ */
1336
+ export declare function readHostManifest(host: string): HostManifest | undefined;
1393
1337
 
1394
- /**
1395
- * Read a target's current bytes at the paths a plan claims.
1396
- *
1397
- * @param target - The target directory to read.
1398
- * @param paths - The plan-relative paths to probe.
1399
- * @returns One entry per path that is there: a file maps to its exact bytes as
1400
- * hexadecimal and a directory maps to `''`, which records presence with no bytes
1401
- * to compare. An absent path is omitted.
1402
- * @throws `ScaffoldError('INVALID', …)` when `target` is not a host path or
1403
- * `paths` is not a bounded list of plannable paths.
1404
- * @throws `ScaffoldError('TARGET', …)` when a path that is there cannot be read,
1405
- * or when the whole read would retain more bytes than one plan may.
1406
- *
1407
- * @remarks
1408
- * The one door from a real directory into the vocabulary an audit compares in.
1409
- * Absence is omission rather than an empty value, because core reads a missing
1410
- * key as a missing destination and an empty string as a present directory; the
1411
- * two are different verdicts. A path that is there but unreadable throws instead
1412
- * of being omitted, because omission would report it as missing and a repair
1413
- * would then overwrite whatever is actually sitting there.
1414
- *
1415
- * @example
1416
- * ```ts
1417
- * import { readSnapshot } from '@orkestrel/scaffold/server'
1418
- *
1419
- * readSnapshot('./packages/router', ['package.json', 'guides'])
1420
- * // { 'package.json': '7b226e…', guides: '' }
1421
- * ```
1422
- */
1423
- export declare function readSnapshot(target: string, paths: readonly string[]): Snapshot;
1338
+ /**
1339
+ * Derive one vendored-host manifest entry from a file in a checkout.
1340
+ *
1341
+ * @param destination - The target-relative path the file is written to.
1342
+ * @param source - The resolved host path the bytes are read from.
1343
+ * @returns The entry, or `undefined` when `source` is not a physical file this
1344
+ * package will vendor or carries more bytes than one artifact may.
1345
+ *
1346
+ * @remarks
1347
+ * The one place the declared fields are decided together, because they are
1348
+ * readings of one path: {@link pathToStorage} decides where it is stored,
1349
+ * the destination is the path it answers for, and {@link matchesExecutablePath}
1350
+ * decides whether a target receives it executable.
1351
+ *
1352
+ * The bit is read from that declaration rather than from the source's mode, so
1353
+ * the entry does not depend on where the package was staged. A Windows host
1354
+ * reports no executable bit at all, and reading the mode there declared every
1355
+ * entry non-executable and shipped consumers hooks they could not run.
1356
+ *
1357
+ * @example
1358
+ * ```ts
1359
+ * import { readManifestEntry } from '@orkestrel/scaffold/server'
1360
+ *
1361
+ * readManifestEntry('.gitignore', '/tmp/checkout/.gitignore')
1362
+ * // { storage: 'dotfiles/gitignore', destination: '.gitignore', executable: false }
1363
+ * ```
1364
+ */
1365
+ export declare function readManifestEntry(destination: string, source: string): ManifestEntry | undefined;
1424
1366
 
1425
- /**
1426
- * What git reports about a target's working tree.
1427
- *
1428
- * @remarks
1429
- * `tracked` is the only set a deletion may draw from: git does not report the
1430
- * loss of an untracked path and `git diff` cannot restore it, so an ignored
1431
- * path such as an installed dependency tree, a build output, or an editor
1432
- * directory survives every verb. `dirty` is every path carrying an uncommitted
1433
- * change, taken repo-wide rather than over a write set, because deletion makes
1434
- * the write set the whole workspace. A clean tree is an empty `dirty`. A target
1435
- * that is not a git repository yields no `Repository` at all, so the caller
1436
- * decides what to do about that rather than reading it out of an invented
1437
- * empty value.
1438
- */
1439
- export declare interface Repository {
1440
- readonly tracked: readonly string[];
1441
- readonly dirty: readonly string[];
1442
- }
1367
+ /**
1368
+ * Read a target's current bytes at the paths a plan claims.
1369
+ *
1370
+ * @param target - The target directory to read.
1371
+ * @param paths - The plan-relative paths to probe.
1372
+ * @returns One entry per path that is there: a file maps to its exact bytes as
1373
+ * hexadecimal and a directory maps to `''`, which records presence with no bytes
1374
+ * to compare. An absent path is omitted.
1375
+ * @throws `ScaffoldError('INVALID', …)` when `target` is not a host path or
1376
+ * `paths` is not a bounded list of plannable paths.
1377
+ * @throws `ScaffoldError('TARGET', …)` when a path that is there cannot be read,
1378
+ * or when the whole read would retain more bytes than one plan may.
1379
+ *
1380
+ * @remarks
1381
+ * The one door from a real directory into the vocabulary an audit compares in.
1382
+ * Absence is omission rather than an empty value, because core reads a missing
1383
+ * key as a missing destination and an empty string as a present directory; the
1384
+ * they are different verdicts. A path that is there but unreadable throws instead
1385
+ * of being omitted, because omission would report it as missing and a repair
1386
+ * would then overwrite whatever is actually sitting there.
1387
+ *
1388
+ * @example
1389
+ * ```ts
1390
+ * import { readSnapshot } from '@orkestrel/scaffold/server'
1391
+ *
1392
+ * readSnapshot('./packages/router', ['package.json', 'guides'])
1393
+ * // { 'package.json': '7b226e…', guides: '' }
1394
+ * ```
1395
+ */
1396
+ export declare function readSnapshot(target: string, paths: readonly string[]): Snapshot;
1443
1397
 
1444
- /**
1445
- * The Windows device names that stay reserved even when an extension follows.
1446
- *
1447
- * @remarks
1448
- * Refused on every host rather than only on Windows. A generated workspace is
1449
- * checked out on all of them, so a directory this package writes on Linux must
1450
- * still be a name Windows can hold.
1451
- */
1452
- export declare const RESERVED_SEGMENT_PATTERN: RegExp;
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
+ }
1453
1416
 
1454
- /**
1455
- * Resolve a root-relative path and refuse one that leaves its root.
1456
- *
1457
- * @param root - The containing host directory.
1458
- * @param path - The portable root-relative path.
1459
- * @returns The destination as this package will address it, or `undefined` when
1460
- * either argument is off contract or the destination lies outside `root`.
1461
- *
1462
- * @remarks
1463
- * The containment law, and the one door every read in this module goes through.
1464
- * Both sides are resolved through the real filesystem before they are compared.
1465
- * A dangling link is followed only when its raw target contains no parent
1466
- * traversal. The answer is then the lexical join of `root` and `path` — an
1467
- * absolute path under `root`, not a root-relative one — so the caller operates
1468
- * on the path it named rather than on a resolved form the target may not
1469
- * recognize. A `root` written with a parent segment is collapsed by that
1470
- * resolution before anything is read, so containment is measured against the
1471
- * directory the caller's text names.
1472
- *
1473
- * Comparison is exact text, which fails closed on a case-insensitive
1474
- * filesystem: a root and a path spelled with different case resolve to
1475
- * different strings there and are refused, never wrongly admitted.
1476
- *
1477
- * The answer describes the namespace this call read. The contract excludes a
1478
- * concurrent rename or link swap during the call or before the caller finishes
1479
- * using the returned path. This helper returns a string, not a filesystem
1480
- * handle, so it cannot bind its containment check to a later operation. A caller
1481
- * that admits hostile concurrent namespace mutation needs a handle-bound
1482
- * operation instead.
1483
- *
1484
- * @example
1485
- * ```ts
1486
- * import { resolveContainedPath } from '@orkestrel/scaffold/server'
1487
- *
1488
- * resolveContainedPath('/tmp/project', 'guides/router.md')?.endsWith('router.md') // true
1489
- * resolveContainedPath('/tmp/project', '../secrets') // undefined
1490
- * ```
1491
- */
1492
- export declare function resolveContainedPath(root: string, path: string): string | undefined;
1417
+ /**
1418
+ * The Windows device names that stay reserved even when an extension follows.
1419
+ *
1420
+ * @remarks
1421
+ * Refused on every host rather than only on Windows. A generated workspace is
1422
+ * checked out on all of them, so a directory this package writes on Linux must
1423
+ * still be a name Windows can hold.
1424
+ */
1425
+ export declare const RESERVED_SEGMENT_PATTERN: RegExp;
1493
1426
 
1494
- /**
1495
- * Resolve a path through the real filesystem, keeping the part that does not exist yet.
1496
- *
1497
- * @param path - The absolute or relative host path to resolve.
1498
- * @returns The lexical resolution of `path`, with its existing prefix then
1499
- * resolved through every link, or `undefined` when the text is not a host path,
1500
- * no bounded existing ancestor resolves, a link target cannot be read, a link
1501
- * target carries a `..` segment, or an ancestor cannot be read.
1502
- *
1503
- * @remarks
1504
- * A containment decision has to be made about a destination that does not exist
1505
- * yet, and a lexical answer is not enough: a link anywhere in the existing
1506
- * prefix moves the destination somewhere the text never named. So the deepest
1507
- * existing ancestor is resolved and the remaining segments are re-joined onto
1508
- * it. The climb is bounded by the path-depth ceiling, so an adversarial path
1509
- * cannot make it walk indefinitely.
1510
- *
1511
- * The caller's own text is collapsed first, which is what `resolve` does with a
1512
- * `..` the caller wrote: it cancels the segment before it as text, before any
1513
- * link in that segment is read. So `<root>/hop/..` answers `<root>` even where
1514
- * `hop` links elsewhere, rather than the directory holding what `hop` points at.
1515
- * The collapse only ever shortens the caller's path, so nothing reaches outside
1516
- * it by this; the answer is that lexical location resolved through links, not
1517
- * the physical location the links lead to. {@link resolveContainedPath} passes
1518
- * its `root` through here, so a root written with a parent segment is contained
1519
- * against its collapsed spelling.
1520
- *
1521
- * `realpath` answers `ENOENT` both for a name that is not there and for a link
1522
- * whose target is not there. The name is therefore inspected without following
1523
- * it: a dangling link redirects the walk to its target, while a genuinely absent
1524
- * name is retained as one segment of the unresolved suffix. A dangling link
1525
- * target containing a `..` segment is refused. Resolving that target as one
1526
- * lexical string could discard a preceding link before the filesystem gives
1527
- * `..` its physical meaning.
1528
- *
1529
- * That target is split on both separators on every host, which is the reading
1530
- * `isPath` already gives a planned path. A POSIX filename legally containing a
1531
- * backslash is therefore refused with it: `weird\..\name` is one name to the
1532
- * host and three segments here. The package keeps one separator law rather than
1533
- * a host-dependent second one, and this is the conservative side of it.
1534
- *
1535
- * @example
1536
- * ```ts
1537
- * import { resolveRealPath } from '@orkestrel/scaffold/server'
1538
- *
1539
- * resolveRealPath('./packages/new/src') // the real path of `packages`, plus `new/src`
1540
- * ```
1541
- */
1542
- export declare function resolveRealPath(path: string): string | undefined;
1427
+ /**
1428
+ * Resolve a root-relative path and refuse one that leaves its root.
1429
+ *
1430
+ * @param root - The containing host directory.
1431
+ * @param path - The portable root-relative path.
1432
+ * @returns The destination as this package will address it, or `undefined` when
1433
+ * either argument is off contract or the destination lies outside `root`.
1434
+ *
1435
+ * @remarks
1436
+ * The containment law, and the one door every read in this module goes through.
1437
+ * Both sides are resolved through the real filesystem before they are compared.
1438
+ * A dangling link is followed only when its raw target contains no parent
1439
+ * traversal. The answer is then the lexical join of `root` and `path` — an
1440
+ * absolute path under `root`, not a root-relative one so the caller operates
1441
+ * on the path it named rather than on a resolved form the target may not
1442
+ * recognize. A `root` written with a parent segment is collapsed by that
1443
+ * resolution before anything is read, so containment is measured against the
1444
+ * directory the caller's text names.
1445
+ *
1446
+ * Comparison is exact text, which fails closed on a case-insensitive
1447
+ * filesystem: a root and a path spelled with different case resolve to
1448
+ * different strings there and are refused, never wrongly admitted.
1449
+ *
1450
+ * The answer describes the namespace this call read. The contract excludes a
1451
+ * concurrent rename or link swap during the call or before the caller finishes
1452
+ * using the returned path. This helper returns a string, not a filesystem
1453
+ * handle, so it cannot bind its containment check to a later operation. A caller
1454
+ * that admits hostile concurrent namespace mutation needs a handle-bound
1455
+ * operation instead.
1456
+ *
1457
+ * @example
1458
+ * ```ts
1459
+ * import { resolveContainedPath } from '@orkestrel/scaffold/server'
1460
+ *
1461
+ * resolveContainedPath('/tmp/project', 'guides/router.md')?.endsWith('router.md') // true
1462
+ * resolveContainedPath('/tmp/project', '../secrets') // undefined
1463
+ * ```
1464
+ */
1465
+ export declare function resolveContainedPath(root: string, path: string): string | undefined;
1543
1466
 
1544
- /**
1545
- * Stage a vendored host root from a real checkout.
1546
- *
1547
- * @param checkout - The checkout the vendored paths are read from.
1548
- * @param host - The vendored host root to fill; it must be absent or empty.
1549
- * @returns One entry per staged file, sorted by storage name.
1550
- * @throws `ScaffoldError('INVALID', …)` when either argument is not a host path
1551
- * or a vendored path leaves the checkout or the host root.
1552
- * @throws `ScaffoldError('TARGET', …)` when the checkout is not a directory, the
1553
- * host root is not vacant, the checkout does not carry every vendored path, two
1554
- * vendored files claim one storage name, a vendored file is not a plain file
1555
- * within the artifact ceiling, or the staged manifest does not read back.
1556
- * @throws `ScaffoldError('WRITE', …)` when a staged file or the manifest cannot
1557
- * be written.
1558
- *
1559
- * @remarks
1560
- * This is the producer half of the vendored host, and it is not the mutation
1561
- * contract `MaterializerInterface` states. That contract owns **target**
1562
- * writes: it materializes a compiled plan into a consumer's workspace, binds
1563
- * every destination to what the caller observed, and rolls a failed commit back.
1564
- * This reads this package's own checkout at build time and fills its own build
1565
- * output. Different direction, different lifetime, no consumer target involved,
1566
- * so the two do not overlap and neither one belongs inside the other.
1567
- *
1568
- * Staging is plain rather than transactional for the same reason. A
1569
- * `WriteTransaction` exists to hold a directory that already holds work
1570
- * still; a build output holds nothing, is deleted whole before every build, and
1571
- * has no concurrent reader. What replaces it is refusing early and ordering the
1572
- * writes: the whole membership is derived before anything is created, so a
1573
- * checkout this refuses leaves no host root at all, and `manifest.json` is
1574
- * written last, so a stage that failed part way through leaves a root every
1575
- * reader treats as a raw checkout and fails loudly on.
1576
- *
1577
- * A missing vendored path is refused rather than staged around. A partial root
1578
- * is not detectably partial: it fails later, in a consumer's terminal, on
1579
- * whichever path the plan reached first. Refusing here fails the build that
1580
- * produced it, where the maintainer can act, and it names every missing path at
1581
- * once. A directory is the same case declaring an absent directory as an empty
1582
- * root would create an empty directory in every generated workspace.
1583
- *
1584
- * The vendoring deny-list applies to what the walk discovers beneath a vendored
1585
- * directory, where a maintainer's local credential can legitimately sit, and
1586
- * such a path is skipped. A path `HOST_PATHS` names itself is curated data
1587
- * rather than discovery, so it is staged or the stage is refused.
1588
- *
1589
- * @example
1590
- * ```ts
1591
- * import { stageHost } from '@orkestrel/scaffold/server'
1592
- *
1593
- * stageHost(process.cwd(), 'dist/host').length // the files staged
1594
- * ```
1595
- */
1596
- export declare function stageHost(checkout: string, host: string): readonly ManifestEntry[];
1467
+ /**
1468
+ * Resolve a path through the real filesystem, keeping the part that does not exist yet.
1469
+ *
1470
+ * @param path - The absolute or relative host path to resolve.
1471
+ * @returns The lexical resolution of `path`, with its existing prefix then
1472
+ * resolved through every link, or `undefined` when the text is not a host path,
1473
+ * no bounded existing ancestor resolves, a link target cannot be read, a link
1474
+ * target carries a `..` segment, or an ancestor cannot be read.
1475
+ *
1476
+ * @remarks
1477
+ * A containment decision has to be made about a destination that does not exist
1478
+ * yet, and a lexical answer is not enough: a link anywhere in the existing
1479
+ * prefix moves the destination somewhere the text never named. So the deepest
1480
+ * existing ancestor is resolved and the remaining segments are re-joined onto
1481
+ * it. The climb is bounded by the path-depth ceiling, so an adversarial path
1482
+ * cannot make it walk indefinitely.
1483
+ *
1484
+ * The caller's own text is collapsed first, which is what `resolve` does with a
1485
+ * `..` the caller wrote: it cancels the segment before it as text, before any
1486
+ * link in that segment is read. So `<root>/hop/..` answers `<root>` even where
1487
+ * `hop` links elsewhere, rather than the directory holding what `hop` points at.
1488
+ * The collapse only ever shortens the caller's path, so nothing reaches outside
1489
+ * it by this; the answer is that lexical location resolved through links, not
1490
+ * the physical location the links lead to. {@link resolveContainedPath} passes
1491
+ * its `root` through here, so a root written with a parent segment is contained
1492
+ * against its collapsed spelling.
1493
+ *
1494
+ * `realpath` answers `ENOENT` both for a name that is not there and for a link
1495
+ * whose target is not there. The name is therefore inspected without following
1496
+ * it: a dangling link redirects the walk to its target, while a genuinely absent
1497
+ * name is retained as one segment of the unresolved suffix. A dangling link
1498
+ * target containing a `..` segment is refused. Resolving that target as one
1499
+ * lexical string could discard a preceding link before the filesystem gives
1500
+ * `..` its physical meaning.
1501
+ *
1502
+ * That target is split on both separators on every host, which is the reading
1503
+ * `isPath` already gives a planned path. A POSIX filename legally containing a
1504
+ * backslash is therefore refused with it: `weird\..\name` is one name to the
1505
+ * host and three segments here. The package keeps one separator law rather than
1506
+ * a host-dependent second one, and this is the conservative side of it.
1507
+ *
1508
+ * @example
1509
+ * ```ts
1510
+ * import { resolveRealPath } from '@orkestrel/scaffold/server'
1511
+ *
1512
+ * resolveRealPath('./packages/new/src') // the real path of `packages`, plus `new/src`
1513
+ * ```
1514
+ */
1515
+ export declare function resolveRealPath(path: string): string | undefined;
1597
1516
 
1598
- /**
1599
- * The reading spine: one bounded, unauthenticated, redirect-free request per answer.
1600
- *
1601
- * @remarks
1602
- * This is the package's only network reader, and it never writes. Every call
1603
- * opens one byte allowance and spends it across every read the call makes, so a
1604
- * caller is bounded twice over: `limit` refuses one oversized answer and
1605
- * `budget` refuses many small ones. The two are separate exhaustion routes and
1606
- * neither bound covers the other.
1607
- *
1608
- * A per-package failure never escapes as a throw. It is projected into the
1609
- * verdict that package's row already carries `missing` for an upstream `404`,
1610
- * which is a definite answer, and `failed` for a transport fault, which is no
1611
- * answer at all — so one unreachable package never costs the caller the rest of
1612
- * the answer. The organization package list is the one exception: without it
1613
- * there is no fleet to report, so an unreachable or malformed list is a coded
1614
- * `FETCH` failure.
1615
- *
1616
- * Requests are unauthenticated because every fleet repository is public, and
1617
- * they follow no redirect, so a misconfigured or hostile endpoint cannot move a
1618
- * read to another host. Each one is bounded by its endpoint's timeout and by the
1619
- * reader's own abort signal, so {@link Upstream.destroy} cancels what is in
1620
- * flight instead of waiting for it.
1621
- *
1622
- * The allowance is threaded through the private reads as a mutable
1623
- * `{ remaining: number }` carrier rather than held on the instance, because
1624
- * concurrent calls each own their own budget and must not spend each other's.
1625
- *
1626
- * @example
1627
- * ```ts
1628
- * import { Upstream } from '@orkestrel/scaffold/server'
1629
- *
1630
- * const upstream = new Upstream({ registry: { timeout: 5_000 } })
1631
- * const releases = await upstream.lookup([{ name: '@orkestrel/emitter', range: '^0.0.5' }])
1632
- * upstream.destroy()
1633
- * ```
1634
- */
1635
- export declare class Upstream implements UpstreamInterface {
1636
- #private;
1517
+ /**
1518
+ * Stage a vendored host root from a real checkout.
1519
+ *
1520
+ * @param checkout - The checkout the vendored paths are read from.
1521
+ * @param host - The vendored host root to fill; it must be absent or empty.
1522
+ * @returns One entry per staged file, sorted by storage name.
1523
+ * @throws `ScaffoldError('INVALID', …)` when either argument is not a host path
1524
+ * or a vendored path leaves the checkout or the host root.
1525
+ * @throws `ScaffoldError('TARGET', …)` when the checkout is not a directory, the
1526
+ * host root is not vacant, the checkout does not carry every vendored path, two
1527
+ * vendored files claim one storage name, a vendored file is not a plain file
1528
+ * within the artifact ceiling, or the staged manifest does not read back.
1529
+ * @throws `ScaffoldError('WRITE', …)` when a staged file or the manifest cannot
1530
+ * be written.
1531
+ *
1532
+ * @remarks
1533
+ * This is the producer half of the vendored host, and it is not the mutation
1534
+ * contract `MaterializerInterface` states. That contract owns **target**
1535
+ * writes: it materializes a compiled plan into a consumer's workspace, binds
1536
+ * every destination to what the caller observed, and rolls a failed commit back.
1537
+ * This reads this package's own checkout at build time and fills its own build
1538
+ * output. Different direction, different lifetime, no consumer target involved,
1539
+ * so they do not overlap and neither one belongs inside the other.
1540
+ *
1541
+ * Staging is plain rather than transactional for the same reason. A
1542
+ * `WriteTransaction` exists to hold a directory that already holds work
1543
+ * still; a build output holds nothing, is deleted whole before every build, and
1544
+ * has no concurrent reader. What replaces it is refusing early and ordering the
1545
+ * writes: the whole membership is derived before anything is created, so a
1546
+ * checkout this refuses leaves no host root at all, and `manifest.json` is
1547
+ * written last, so a stage that failed part way through leaves a root every
1548
+ * reader treats as a raw checkout and fails loudly on.
1549
+ *
1550
+ * A missing vendored path is refused rather than staged around. A partial root
1551
+ * is not detectably partial: it fails later, in a consumer's terminal, on
1552
+ * whichever path the plan reached first. Refusing here fails the build that
1553
+ * produced it, where the maintainer can act, and it names every missing path at
1554
+ * once. A directory is the same case — declaring an absent directory as an empty
1555
+ * root would create an empty directory in every generated workspace.
1556
+ *
1557
+ * The vendoring deny-list applies to what the walk discovers beneath a vendored
1558
+ * directory, where a maintainer's local credential can legitimately sit, and
1559
+ * such a path is skipped. A path `HOST_PATHS` names itself is curated data
1560
+ * rather than discovery, so it is staged or the stage is refused.
1561
+ *
1562
+ * @example
1563
+ * ```ts
1564
+ * import { stageHost } from '@orkestrel/scaffold/server'
1565
+ *
1566
+ * stageHost(process.cwd(), 'dist/host').length // the files staged
1567
+ * ```
1568
+ */
1569
+ export declare function stageHost(checkout: string, host: string): readonly ManifestEntry[];
1570
+
1571
+ /**
1572
+ * The reading spine: one bounded, unauthenticated, redirect-free request per answer.
1573
+ *
1574
+ * @remarks
1575
+ * This is the package's only network reader, and it never writes. Every call
1576
+ * opens one byte allowance and spends it across every read the call makes, so a
1577
+ * caller is bounded twice over: `limit` refuses one oversized answer and
1578
+ * `budget` refuses many small ones. They are separate exhaustion routes and
1579
+ * neither bound covers the other.
1580
+ *
1581
+ * A per-package failure never escapes as a throw. It is projected into the
1582
+ * verdict that package's row already carries — `missing` for an upstream `404`,
1583
+ * which is a definite answer, and `failed` for a transport fault, which is no
1584
+ * answer at all — so one unreachable package never costs the caller the rest of
1585
+ * the answer. The organization package list is the one exception: without it
1586
+ * there is no fleet to report, so an unreachable or malformed list is a coded
1587
+ * `FETCH` failure.
1588
+ *
1589
+ * Requests are unauthenticated because every fleet repository is public, and
1590
+ * they follow no redirect, so a misconfigured or hostile endpoint cannot move a
1591
+ * read to another host. Each one is bounded by its endpoint's timeout and by the
1592
+ * reader's own abort signal, so {@link Upstream.destroy} cancels what is in
1593
+ * flight instead of waiting for it.
1594
+ *
1595
+ * The allowance is threaded through the private reads as a mutable
1596
+ * `{ remaining: number }` carrier rather than held on the instance, because
1597
+ * concurrent calls each own their own budget and must not spend each other's.
1598
+ *
1599
+ * @example
1600
+ * ```ts
1601
+ * import { Upstream } from '@orkestrel/scaffold/server'
1602
+ *
1603
+ * const upstream = new Upstream({ registry: { timeout: 5_000 } })
1604
+ * const releases = await upstream.lookup([{ name: '@orkestrel/emitter', range: '^0.0.5' }])
1605
+ * upstream.destroy()
1606
+ * ```
1607
+ */
1608
+ export declare class Upstream implements UpstreamInterface {
1609
+ #private;
1610
+ /**
1611
+ * Construct a reader over one guide host and one registry.
1612
+ *
1613
+ * @param options - The endpoints, the request bounds, the initial
1614
+ * listeners, and the listener-error handler.
1615
+ * @throws {@link ScaffoldError} coded `INVALID` when `options` is present but
1616
+ * is not an option bag this reader accepts, or when either endpoint names a
1617
+ * scheme, host, or form this reader will not request.
1618
+ *
1619
+ * @remarks
1620
+ * `isEndpoint` bounds an endpoint's length and nothing else, so the scheme and
1621
+ * host law is settled here, where a refusal can say which endpoint was refused
1622
+ * and why. An endpoint must be HTTPS, or HTTP to a loopback host — the one
1623
+ * place an unencrypted request has no network between its ends. That
1624
+ * refuses `file:`, `data:`, and plain HTTP to a real host, and it is what
1625
+ * keeps a fixture reachable without weakening transport security anywhere a
1626
+ * real request goes. An endpoint carrying credentials, a query, or a fragment
1627
+ * is refused too: this reader authenticates nothing and appends its own path.
1628
+ */
1629
+ constructor(options?: UpstreamOptions);
1630
+ /** The upstream reader's observation channel. */
1631
+ get emitter(): EmitterInterface<UpstreamEventMap>;
1632
+ /**
1633
+ * Look up the registry's latest release for each declared dependency.
1634
+ *
1635
+ * @param dependencies - The declared dependencies to look up.
1636
+ * @returns One release verdict per dependency, in input order.
1637
+ * @throws {@link ScaffoldError} coded `INVALID` when `dependencies` is not a
1638
+ * bounded list of declared dependencies, and `DESTROYED` when the reader is
1639
+ * torn down before or during the call.
1640
+ *
1641
+ * @remarks
1642
+ * Whether the declared range already admits the reported version is not
1643
+ * decided here and is not stored on the verdict: it is a function of the
1644
+ * `range` and `latest` sitting beside each other, and one centralized helper
1645
+ * answers it for every caller.
1646
+ *
1647
+ * @example
1648
+ * ```ts
1649
+ * import { Upstream } from '@orkestrel/scaffold/server'
1650
+ *
1651
+ * const upstream = new Upstream()
1652
+ * await upstream.lookup([{ name: '@orkestrel/router', range: '^0.0.8' }])
1653
+ * upstream.destroy()
1654
+ * ```
1655
+ */
1656
+ lookup(dependencies: readonly Dependency[]): Promise<readonly Release[]>;
1637
1657
  /**
1638
- * Construct a reader over one guide host and one registry.
1658
+ * Fetch each named package's guide, beside the local mirror it answers for.
1639
1659
  *
1640
- * @param options - The two endpoints, the request bounds, the initial
1641
- * listeners, and the listener-error handler.
1642
- * @throws {@link ScaffoldError} coded `INVALID` when `options` is present but
1643
- * is not an option bag this reader accepts, or when either endpoint names a
1644
- * scheme, host, or form this reader will not request.
1660
+ * @param names - The packages to fetch: the target's declared set, or the whole organization.
1661
+ * @param current - The target's local mirrors as exact bytes, keyed by mirror path.
1662
+ * @returns One mirror verdict per name, in input order.
1663
+ * @throws {@link ScaffoldError} coded `INVALID` when `names` is not a bounded
1664
+ * list of fleet package names or `current` is not a snapshot, and `DESTROYED`
1665
+ * when the reader is torn down before or during the call.
1645
1666
  *
1646
1667
  * @remarks
1647
- * `isEndpoint` bounds an endpoint's length and nothing else, so the scheme and
1648
- * host law is settled here, where a refusal can say which endpoint was refused
1649
- * and why. An endpoint must be HTTPS, or HTTP to a loopback host — the one
1650
- * place an unencrypted request has no network between the two ends. That
1651
- * refuses `file:`, `data:`, and plain HTTP to a real host, and it is what
1652
- * keeps a fixture reachable without weakening transport security anywhere a
1653
- * real request goes. An endpoint carrying credentials, a query, or a fragment
1654
- * is refused too: this reader authenticates nothing and appends its own path.
1668
+ * The mirror path is derived from the package name by the same helper the plan
1669
+ * derives it with, and the fetched URL ends in that exact path, so a verdict
1670
+ * always answers for the file it names. `observed` is carried through from
1671
+ * `current` whatever the lookup produced, because it is the precondition the
1672
+ * later write is held to and a failed fetch does not change what the target
1673
+ * holds.
1674
+ *
1675
+ * @example
1676
+ * ```ts
1677
+ * import { Upstream } from '@orkestrel/scaffold/server'
1678
+ *
1679
+ * const upstream = new Upstream()
1680
+ * await upstream.fetch(['@orkestrel/router'], { 'guides/router.md': '2320526f75746572' })
1681
+ * upstream.destroy()
1682
+ * ```
1655
1683
  */
1656
- constructor(options?: UpstreamOptions);
1657
- /** The upstream reader's observation channel. */
1658
- get emitter(): EmitterInterface<UpstreamEventMap>;
1684
+ fetch(names: readonly string[], current: Snapshot): Promise<readonly Mirror[]>;
1659
1685
  /**
1660
- * Look up the registry's latest release for each declared dependency.
1686
+ * Catalog the published fleet from the registry's organization package list.
1661
1687
  *
1662
- * @param dependencies - The declared dependencies to look up.
1663
- * @returns One release verdict per dependency, in input order.
1664
- * @throws {@link ScaffoldError} coded `INVALID` when `dependencies` is not a
1665
- * bounded list of declared dependencies, and `DESTROYED` when the reader is
1666
- * torn down before or during the call.
1688
+ * @returns One row per published package, sorted by name.
1689
+ * @throws {@link ScaffoldError} coded `FETCH` when the organization package
1690
+ * list is unreachable, malformed, empty, or larger than one bounded
1691
+ * collection, and `DESTROYED` when the reader is torn down before or during
1692
+ * the call.
1667
1693
  *
1668
1694
  * @remarks
1669
- * Whether the declared range already admits the reported version is not
1670
- * decided here and is not stored on the verdict: it is a function of the
1671
- * `range` and `latest` sitting beside each other, and one centralized helper
1672
- * answers it for every caller.
1695
+ * The organization list is the exact membership the registry publishes, not a
1696
+ * relevance search, and it is the whole reason this answer can claim to be the
1697
+ * fleet. So it fails hard where a per-package lookup fails soft: an empty or
1698
+ * unreadable list would otherwise be written into a target's catalog table as
1699
+ * a fleet with no packages in it. A package whose own version lookup then
1700
+ * fails still keeps its row, carrying the cause instead of a version, because
1701
+ * the list already proved the package is published.
1673
1702
  *
1674
1703
  * @example
1675
1704
  * ```ts
1676
1705
  * import { Upstream } from '@orkestrel/scaffold/server'
1677
1706
  *
1678
1707
  * const upstream = new Upstream()
1679
- * await upstream.lookup([{ name: '@orkestrel/router', range: '^0.0.8' }])
1708
+ * const entries = await upstream.catalog()
1680
1709
  * upstream.destroy()
1681
1710
  * ```
1682
1711
  */
1683
- lookup(dependencies: readonly Dependency[]): Promise<readonly Release[]>;
1712
+ catalog(): Promise<readonly CatalogEntry[]>;
1684
1713
  /**
1685
- * Fetch each named package's guide, beside the local mirror it answers for.
1714
+ * Tear the reader down, aborting every request in flight. Teardown is idempotent.
1686
1715
  *
1687
- * @param names - The packages to fetch: the target's declared set, or the whole organization.
1688
- * @param current - The target's local mirrors as exact bytes, keyed by mirror path.
1689
- * @returns One mirror verdict per name, in input order.
1690
- * @throws {@link ScaffoldError} coded `INVALID` when `names` is not a bounded
1691
- * list of fleet package names or `current` is not a snapshot, and `DESTROYED`
1692
- * when the reader is torn down before or during the call.
1693
- *
1694
- * @remarks
1695
- * The mirror path is derived from the package name by the same helper the plan
1696
- * derives it with, and the fetched URL ends in that exact path, so a verdict
1697
- * always answers for the file it names. `observed` is carried through from
1698
- * `current` whatever the lookup produced, because it is the precondition the
1699
- * later write is held to and a failed fetch does not change what the target
1700
- * currently holds.
1701
- *
1702
- * @example
1703
- * ```ts
1704
- * import { Upstream } from '@orkestrel/scaffold/server'
1705
- *
1706
- * const upstream = new Upstream()
1707
- * await upstream.fetch(['@orkestrel/router'], { 'guides/router.md': '2320526f75746572' })
1708
- * upstream.destroy()
1709
- * ```
1710
- */
1711
- fetch(names: readonly string[], current: Snapshot): Promise<readonly Mirror[]>;
1712
- /**
1713
- * Catalog the published fleet from the registry's organization package list.
1714
- *
1715
- * @returns One row per published package, sorted by name.
1716
- * @throws {@link ScaffoldError} coded `FETCH` when the organization package
1717
- * list is unreachable, malformed, empty, or larger than one bounded
1718
- * collection, and `DESTROYED` when the reader is torn down before or during
1719
- * the call.
1720
- *
1721
- * @remarks
1722
- * The organization list is the exact membership the registry publishes, not a
1723
- * relevance search, and it is the whole reason this answer can claim to be the
1724
- * fleet. So it fails hard where a per-package lookup fails soft: an empty or
1725
- * unreadable list would otherwise be written into a target's catalog table as
1726
- * a fleet with no packages in it. A package whose own version lookup then
1727
- * fails still keeps its row, carrying the cause instead of a version, because
1728
- * the list already proved the package is published.
1729
- *
1730
- * @example
1731
- * ```ts
1732
- * import { Upstream } from '@orkestrel/scaffold/server'
1733
- *
1734
- * const upstream = new Upstream()
1735
- * const entries = await upstream.catalog()
1736
- * upstream.destroy()
1737
- * ```
1738
- */
1739
- catalog(): Promise<readonly CatalogEntry[]>;
1740
- /**
1741
- * Tear the reader down, aborting every request in flight. Teardown is idempotent.
1742
- *
1743
- * @returns Nothing.
1744
- *
1745
- * @remarks
1746
- * A call still in flight rejects with a `DESTROYED` error rather than
1747
- * resolving to a partial answer, because half a fleet reads exactly like a
1748
- * whole one.
1749
- *
1750
- * @example
1751
- * ```ts
1752
- * import { Upstream } from '@orkestrel/scaffold/server'
1753
- *
1754
- * const upstream = new Upstream()
1755
- * upstream.destroy()
1756
- * upstream.emitter.destroyed // true
1757
- * ```
1758
- */
1759
- destroy(): void;
1760
- }
1716
+ * @returns Nothing.
1717
+ *
1718
+ * @remarks
1719
+ * A call still in flight rejects with a `DESTROYED` error rather than
1720
+ * resolving to a partial answer, because half a fleet reads exactly like a
1721
+ * whole one.
1722
+ *
1723
+ * @example
1724
+ * ```ts
1725
+ * import { Upstream } from '@orkestrel/scaffold/server'
1726
+ *
1727
+ * const upstream = new Upstream()
1728
+ * upstream.destroy()
1729
+ * upstream.emitter.destroyed // true
1730
+ * ```
1731
+ */
1732
+ destroy(): void;
1733
+ }
1761
1734
 
1762
- /**
1763
- * The upstream reader's observation channel.
1764
- *
1765
- * @remarks
1766
- * Each verdict is published whole rather than as a name beside a summary, so a
1767
- * listener reads the same value the call returns and a failed lookup is told
1768
- * apart from a successful one by the verdict's own discriminant rather than by
1769
- * which event carried it.
1770
- */
1771
- export declare type UpstreamEventMap = {
1772
- readonly release: readonly [release: Release];
1773
- readonly mirror: readonly [mirror: Mirror];
1774
- readonly error: readonly [error: unknown];
1775
- readonly destroy: readonly [];
1776
- };
1777
-
1778
- /**
1779
- * The upstream contract: the package's only network reader, and it never writes.
1780
- *
1781
- * @remarks
1782
- * A per-package failure is collected as a verdict carrying its cause, not
1783
- * thrown, so one unreachable package never costs the caller the rest of the
1784
- * answer. The organization list is the exception: without it there is no fleet
1785
- * to report, so an unreachable or malformed list is a coded failure.
1786
- */
1787
- export declare interface UpstreamInterface {
1788
- readonly emitter: EmitterInterface<UpstreamEventMap>;
1789
- /**
1790
- * Look up the registry's latest release for each declared dependency.
1791
- *
1792
- * @param dependencies - The declared dependencies to look up.
1793
- * @returns One release verdict per dependency, in input order.
1794
- */
1795
- lookup(dependencies: readonly Dependency[]): Promise<readonly Release[]>;
1796
- /**
1797
- * Fetch each named package's guide, beside the local mirror it answers for.
1798
- *
1799
- * @param names - The packages to fetch: the target's declared set, or the whole organization.
1800
- * @param current - The target's local mirrors as exact bytes, keyed by mirror path.
1801
- * @returns One mirror verdict per name, in input order.
1802
- */
1803
- fetch(names: readonly string[], current: Snapshot): Promise<readonly Mirror[]>;
1804
- /**
1805
- * Catalog the published fleet from the registry's organization package list.
1806
- *
1807
- * @returns One row per published package, sorted by name.
1808
- */
1809
- catalog(): Promise<readonly CatalogEntry[]>;
1810
- /**
1811
- * Tear the reader down, aborting every request in flight. Teardown is idempotent.
1812
- *
1813
- * @returns Nothing.
1814
- */
1815
- destroy(): void;
1816
- }
1735
+ /**
1736
+ * The upstream reader's observation channel.
1737
+ *
1738
+ * @remarks
1739
+ * Each verdict is published whole rather than as a name beside a summary, so a
1740
+ * listener reads the same value the call returns and a failed lookup is told
1741
+ * apart from a successful one by the verdict's own discriminant rather than by
1742
+ * which event carried it.
1743
+ */
1744
+ export declare type UpstreamEventMap = {
1745
+ readonly release: readonly [release: Release];
1746
+ readonly mirror: readonly [mirror: Mirror];
1747
+ readonly error: readonly [error: unknown];
1748
+ readonly destroy: readonly [];
1749
+ };
1817
1750
 
1818
- /**
1819
- * Options for the upstream reader.
1820
- *
1821
- * @remarks
1822
- * The two endpoints are grouped under the entity each configures: `guides`
1823
- * takes the guide host's `base`, its `branch`, and its `timeout`; `registry`
1824
- * takes the registry's `base` and `timeout`. `concurrency` bounds requests in
1825
- * flight and `retries` opts into per-request retry on a transport fault.
1826
- * `limit` bounds the bytes read from one response body and `budget` bounds the
1827
- * bytes read across a whole call, so neither one oversized answer nor many
1828
- * small ones can exhaust the caller. Every request is unauthenticated and
1829
- * follows no redirect.
1830
- */
1831
- export declare interface UpstreamOptions {
1832
- readonly guides?: {
1833
- readonly base?: string;
1834
- readonly branch?: string;
1835
- readonly timeout?: number;
1836
- };
1837
- readonly registry?: {
1838
- readonly base?: string;
1839
- readonly timeout?: number;
1840
- };
1841
- readonly concurrency?: number;
1842
- readonly retries?: number;
1843
- readonly limit?: number;
1844
- readonly budget?: number;
1845
- readonly on?: EmitterHooks<UpstreamEventMap>;
1846
- readonly error?: EmitterErrorHandler;
1847
- }
1848
-
1849
- /**
1850
- * One physical directory identity captured across a write transaction.
1851
- *
1852
- * @remarks
1853
- * Device and inode locate the directory and do not date it. Two directories
1854
- * occupying one slot at different times share an anchor, so an anchor proves
1855
- * where a write lands and never that the directory was left alone.
1856
- */
1857
- export declare interface WriteAnchor {
1858
- readonly path: string;
1859
- readonly device: number;
1860
- readonly inode: number;
1861
- }
1862
-
1863
- /** The final directory anchor of a write transaction and the subset one call created. */
1864
- export declare interface WriteDirectoryResult {
1865
- readonly anchor: WriteAnchor;
1866
- readonly created: readonly WriteAnchor[];
1867
- }
1868
-
1869
- /**
1870
- * One destination snapshot captured before a write and required to survive it.
1871
- *
1872
- * @remarks
1873
- * `device`, `inode`, `modified`, `size`, and `digest` are present only where
1874
- * the observed shape supplies them.
1875
- */
1876
- export declare interface WriteExpectation {
1877
- readonly path: string;
1878
- readonly shape: 'absent' | 'file' | 'directory';
1879
- readonly device?: number;
1880
- readonly inode?: number;
1881
- readonly modified?: number;
1882
- readonly size?: number;
1883
- readonly digest?: string;
1884
- }
1885
-
1886
- /** The narrower caller-observed destination state a write transaction must still match. */
1887
- export declare interface WritePrecondition {
1888
- readonly path: string;
1889
- readonly shape: 'absent' | 'file';
1890
- readonly digest?: string;
1891
- }
1892
-
1893
- /**
1894
- * One staged, reversible mutation of one target directory.
1895
- *
1896
- * @remarks
1897
- * The transaction owns a private root beside the target — a sibling directory on
1898
- * the same volume, so every promotion is a rename rather than a copy. Staging
1899
- * writes go into that root and nothing else, so a call that fails while staging
1900
- * has not touched the target at all. Commit is the only step that mutates the
1901
- * target, and it is the only step that can need rolling back.
1902
- *
1903
- * Two bindings hold a destination still. A **precondition** is what the caller
1904
- * observed earlier and is checked once, at construction, so a target that moved
1905
- * between the caller's read and this transaction fails before anything is
1906
- * created. An **expectation** is captured here, at construction, and re-checked
1907
- * at commit, so a target that moves while the write is being staged fails before
1908
- * anything is promoted.
1909
- *
1910
- * What this provides, exactly:
1911
- *
1912
- * - **Across destinations, staged-then-swapped with rollback on a caught
1913
- * failure.** A failure part way through commit restores every destination it
1914
- * already promoted, restores every file it already took, and removes every
1915
- * directory it created, then reports what recovery could not undo. This is
1916
- * measured: a promotion is driven to fail after an earlier one landed, and the
1917
- * earlier destination is read back.
1918
- * - **No partly written destination.** Every file is written whole into the
1919
- * private root and digested there before commit, so a destination never
1920
- * receives bytes that were still being produced.
1921
- * - **Containment, not continuity, of the directories it creates.** Every
1922
- * ancestor is re-read between `mkdir` calls and again before the first
1923
- * promotion, so an ancestor that became a file, a symlink, a directory
1924
- * elsewhere, or nothing is refused. An ancestor deleted and recreated under
1925
- * the same name can receive its old inode back and is indistinguishable here
1926
- * from one that never moved.
1927
- * - **No crash atomicity across destinations.** A process killed between two
1928
- * promotions leaves the target holding some new files and some old ones, and
1929
- * leaves the private root behind. Nothing here is a journal, and the private
1930
- * root's name is the only record a later run could read.
1931
- *
1932
- * A destination is preserved by hard link and then replaced by a single
1933
- * `rename`, rather than moved aside and replaced, so the path continues to name
1934
- * the old file right up to the swap. That is a property of `rename` on the host,
1935
- * not one this package's tests measure: telling it apart from move-then-replace
1936
- * needs a reader observing the destination inside the swap, and no test here
1937
- * does that. Read the claim as the mechanism it describes, not as a proven
1938
- * guarantee about a concurrent reader.
1939
- *
1940
- * A path names a file this transaction writes, takes, or establishes as a
1941
- * directory; every path is target-relative and is measured by the portable-path
1942
- * law. An expectation and a precondition both name the resolved destination
1943
- * instead, because that is the path each is re-read at.
1944
- *
1945
- * @example
1946
- * ```ts
1947
- * import { WriteTransaction } from '@orkestrel/scaffold/server'
1948
- *
1949
- * const transaction = new WriteTransaction('./packages/router', ['AGENTS.md'])
1950
- * try {
1951
- * transaction.write('AGENTS.md', '# Agents\n')
1952
- * transaction.commit() // ['AGENTS.md']
1953
- * } finally {
1954
- * transaction.discard()
1955
- * }
1956
- * ```
1957
- */
1958
- export declare class WriteTransaction {
1959
- #private;
1751
+ /**
1752
+ * The upstream contract: the package's only network reader, and it never writes.
1753
+ *
1754
+ * @remarks
1755
+ * A per-package failure is collected as a verdict carrying its cause, not
1756
+ * thrown, so one unreachable package never costs the caller the rest of the
1757
+ * answer. The organization list is the exception: without it there is no fleet
1758
+ * to report, so an unreachable or malformed list is a coded failure.
1759
+ */
1760
+ export declare interface UpstreamInterface {
1761
+ readonly emitter: EmitterInterface<UpstreamEventMap>;
1762
+ /**
1763
+ * Look up the registry's latest release for each declared dependency.
1764
+ *
1765
+ * @param dependencies - The declared dependencies to look up.
1766
+ * @returns One release verdict per dependency, in input order.
1767
+ */
1768
+ lookup(dependencies: readonly Dependency[]): Promise<readonly Release[]>;
1769
+ /**
1770
+ * Fetch each named package's guide, beside the local mirror it answers for.
1771
+ *
1772
+ * @param names - The packages to fetch: the target's declared set, or the whole organization.
1773
+ * @param current - The target's local mirrors as exact bytes, keyed by mirror path.
1774
+ * @returns One mirror verdict per name, in input order.
1775
+ */
1776
+ fetch(names: readonly string[], current: Snapshot): Promise<readonly Mirror[]>;
1777
+ /**
1778
+ * Catalog the published fleet from the registry's organization package list.
1779
+ *
1780
+ * @returns One row per published package, sorted by name.
1781
+ */
1782
+ catalog(): Promise<readonly CatalogEntry[]>;
1783
+ /**
1784
+ * Tear the reader down, aborting every request in flight. Teardown is idempotent.
1785
+ *
1786
+ * @returns Nothing.
1787
+ */
1788
+ destroy(): void;
1789
+ }
1790
+
1791
+ /**
1792
+ * Options for the upstream reader.
1793
+ *
1794
+ * @remarks
1795
+ * The endpoints are grouped under the entity each configures: `guides`
1796
+ * takes the guide host's `base`, its `branch`, and its `timeout`; `registry`
1797
+ * takes the registry's `base` and `timeout`. `concurrency` bounds requests in
1798
+ * flight and `retries` opts into per-request retry on a transport fault.
1799
+ * `limit` bounds the bytes read from one response body and `budget` bounds the
1800
+ * bytes read across a whole call, so neither one oversized answer nor many
1801
+ * small ones can exhaust the caller. Every request is unauthenticated and
1802
+ * follows no redirect.
1803
+ */
1804
+ export declare interface UpstreamOptions {
1805
+ readonly guides?: {
1806
+ readonly base?: string;
1807
+ readonly branch?: string;
1808
+ readonly timeout?: number;
1809
+ };
1810
+ readonly registry?: {
1811
+ readonly base?: string;
1812
+ readonly timeout?: number;
1813
+ };
1814
+ readonly concurrency?: number;
1815
+ readonly retries?: number;
1816
+ readonly limit?: number;
1817
+ readonly budget?: number;
1818
+ readonly on?: EmitterHooks<UpstreamEventMap>;
1819
+ readonly error?: EmitterErrorHandler;
1820
+ }
1821
+
1822
+ /**
1823
+ * One physical directory identity captured across a write transaction.
1824
+ *
1825
+ * @remarks
1826
+ * Device and inode locate the directory and do not date it. Two directories
1827
+ * occupying one slot at different times share an anchor, so an anchor proves
1828
+ * where a write lands and never that the directory was left alone.
1829
+ */
1830
+ export declare interface WriteAnchor {
1831
+ readonly path: string;
1832
+ readonly device: number;
1833
+ readonly inode: number;
1834
+ }
1835
+
1836
+ /** The final directory anchor of a write transaction and the subset one call created. */
1837
+ export declare interface WriteDirectoryResult {
1838
+ readonly anchor: WriteAnchor;
1839
+ readonly created: readonly WriteAnchor[];
1840
+ }
1841
+
1842
+ /**
1843
+ * One destination snapshot captured before a write and required to survive it.
1844
+ *
1845
+ * @remarks
1846
+ * `device`, `inode`, `modified`, `size`, and `digest` are present only where
1847
+ * the observed shape supplies them.
1848
+ */
1849
+ export declare interface WriteExpectation {
1850
+ readonly path: string;
1851
+ readonly shape: 'absent' | 'file' | 'directory';
1852
+ readonly device?: number;
1853
+ readonly inode?: number;
1854
+ readonly modified?: number;
1855
+ readonly size?: number;
1856
+ readonly digest?: string;
1857
+ }
1858
+
1859
+ /** The narrower caller-observed destination state a write transaction must still match. */
1860
+ export declare interface WritePrecondition {
1861
+ readonly path: string;
1862
+ readonly shape: 'absent' | 'file';
1863
+ readonly digest?: string;
1864
+ }
1865
+
1866
+ /**
1867
+ * One staged, reversible mutation of one target directory.
1868
+ *
1869
+ * @remarks
1870
+ * The transaction owns a private root beside the target a sibling directory on
1871
+ * the same volume, so every promotion is a rename rather than a copy. Staging
1872
+ * writes go into that root and nothing else, so a call that fails while staging
1873
+ * has not touched the target at all. Commit is the only step that mutates the
1874
+ * target, and it is the only step that can need rolling back.
1875
+ *
1876
+ * A precondition and an expectation hold a destination still. A **precondition** is what the caller
1877
+ * observed earlier and is checked once, at construction, so a target that moved
1878
+ * between the caller's read and this transaction fails before anything is
1879
+ * created. An **expectation** is captured here, at construction, and re-checked
1880
+ * at commit, so a target that moves while the write is being staged fails before
1881
+ * anything is promoted.
1882
+ *
1883
+ * What this provides, exactly:
1884
+ *
1885
+ * - **Across destinations, staged-then-swapped with rollback on a caught
1886
+ * failure.** A failure part way through commit restores every destination it
1887
+ * already promoted, restores every file it already took, and removes every
1888
+ * directory it created, then reports what recovery could not undo. This is
1889
+ * measured: a promotion is driven to fail after an earlier one landed, and the
1890
+ * earlier destination is read back.
1891
+ * - **No partly written destination.** Every file is written whole into the
1892
+ * private root and digested there before commit, so a destination never
1893
+ * receives bytes that were still being produced.
1894
+ * - **Containment, not continuity, of the directories it creates.** Every
1895
+ * ancestor is re-read between `mkdir` calls and again before the first
1896
+ * promotion, so an ancestor that became a file, a symlink, a directory
1897
+ * elsewhere, or nothing is refused. An ancestor deleted and recreated under
1898
+ * the same name can receive its old inode back and is indistinguishable here
1899
+ * from one that never moved.
1900
+ * - **No crash atomicity across destinations.** A process killed between
1901
+ * promotions leaves the target holding some new files and some old ones, and
1902
+ * leaves the private root behind. Nothing here is a journal, and the private
1903
+ * root's name is the only record a later run could read.
1904
+ *
1905
+ * A destination is preserved by hard link and then replaced by a single
1906
+ * `rename`, rather than moved aside and replaced, so the path continues to name
1907
+ * the old file right up to the swap. That is a property of `rename` on the host,
1908
+ * not one this package's tests measure: telling it apart from move-then-replace
1909
+ * needs a reader observing the destination inside the swap, and no test here
1910
+ * does that. Read the claim as the mechanism it describes, not as a proven
1911
+ * guarantee about a concurrent reader.
1912
+ *
1913
+ * A path names a file this transaction writes, takes, or establishes as a
1914
+ * directory; every path is target-relative and is measured by the portable-path
1915
+ * law. An expectation and a precondition both name the resolved destination
1916
+ * instead, because that is the path each is re-read at.
1917
+ *
1918
+ * @example
1919
+ * ```ts
1920
+ * import { WriteTransaction } from '@orkestrel/scaffold/server'
1921
+ *
1922
+ * const transaction = new WriteTransaction('./packages/router', ['AGENTS.md'])
1923
+ * try {
1924
+ * transaction.write('AGENTS.md', '# Agents\n')
1925
+ * transaction.commit() // ['AGENTS.md']
1926
+ * } finally {
1927
+ * transaction.discard()
1928
+ * }
1929
+ * ```
1930
+ */
1931
+ export declare class WriteTransaction {
1932
+ #private;
1933
+ /**
1934
+ * Open a transaction over one target directory.
1935
+ *
1936
+ * @param target - The directory every path is written beneath.
1937
+ * @param paths - Every target-relative path this transaction may touch.
1938
+ * @param preconditions - The caller-observed destination states the whole
1939
+ * transaction is held to, each naming a resolved destination of `paths`.
1940
+ * @throws {@link ScaffoldError} coded `INVALID` when an argument is off
1941
+ * contract, `TARGET` when a destination is a shape this package will not write
1942
+ * over or no longer matches its precondition, and `WRITE` when the private
1943
+ * root cannot be established.
1944
+ *
1945
+ * @remarks
1946
+ * Nothing is created until every destination has been inspected and every
1947
+ * precondition has held, so a refused transaction leaves no residue at all.
1948
+ * The private root is created last and carries a random name, so two
1949
+ * transactions over one target never collide.
1950
+ */
1951
+ constructor(target: string, paths: readonly string[], preconditions?: readonly WritePrecondition[]);
1952
+ /** The resolved directory every path is written beneath. */
1953
+ get target(): string;
1954
+ /** What each destination held when the transaction opened, in path order. */
1955
+ get expectations(): readonly WriteExpectation[];
1956
+ /** Whether the transaction can still be committed or discarded. */
1957
+ get open(): boolean;
1958
+ /**
1959
+ * Stage one text file.
1960
+ *
1961
+ * @param path - The target-relative path to write.
1962
+ * @param content - The exact UTF-8 text the destination must hold.
1963
+ * @returns Nothing.
1964
+ * @throws {@link ScaffoldError} coded `INVALID` when the path is not one this
1965
+ * transaction opened or is already staged, `TARGET` when the destination holds
1966
+ * a directory, and `WRITE` when the staged file cannot be written or does not
1967
+ * carry the bytes it was given.
1968
+ *
1969
+ * @remarks
1970
+ * The staged file is read back and digested against the text it was given, so
1971
+ * a partial or interrupted write is refused here rather than promoted later.
1972
+ */
1973
+ write(path: string, content: string): void;
1960
1974
  /**
1961
- * Open a transaction over one target directory.
1975
+ * Stage one byte-for-byte copy of a file that already exists on this host.
1962
1976
  *
1963
- * @param target - The directory every path is written beneath.
1964
- * @param paths - Every target-relative path this transaction may touch.
1965
- * @param preconditions - The caller-observed destination states the whole
1966
- * transaction is held to, each naming a resolved destination of `paths`.
1967
- * @throws {@link ScaffoldError} coded `INVALID` when an argument is off
1968
- * contract, `TARGET` when a destination is a shape this package will not write
1969
- * over or no longer matches its precondition, and `WRITE` when the private
1970
- * root cannot be established.
1977
+ * @param path - The target-relative path to write.
1978
+ * @param source - The resolved absolute path to copy the bytes from.
1979
+ * @param executable - If `true`, the destination carries the executable bit; if `false`, it does not.
1980
+ * @returns Nothing.
1981
+ * @throws {@link ScaffoldError} coded `INVALID` when the path is not one this
1982
+ * transaction opened or is already staged, `TARGET` when the destination holds
1983
+ * a directory or the source is not a physical file, and `WRITE` when the copy
1984
+ * cannot be made or does not digest to the source's bytes.
1971
1985
  *
1972
1986
  * @remarks
1973
- * Nothing is created until every destination has been inspected and every
1974
- * precondition has held, so a refused transaction leaves no residue at all.
1975
- * The private root is created last and carries a random name, so two
1976
- * transactions over one target never collide.
1987
+ * The source is digested before the copy and the copy is digested after it, so
1988
+ * a source that changed mid-copy is refused instead of staged.
1977
1989
  */
1978
- constructor(target: string, paths: readonly string[], preconditions?: readonly WritePrecondition[]);
1979
- /** The resolved directory every path is written beneath. */
1980
- get target(): string;
1981
- /** What each destination held when the transaction opened, in path order. */
1982
- get expectations(): readonly WriteExpectation[];
1983
- /** Whether the transaction can still be committed or discarded. */
1984
- get open(): boolean;
1990
+ copy(path: string, source: string, executable: boolean): void;
1985
1991
  /**
1986
- * Stage one text file.
1992
+ * Establish one directory inside the target, one segment at a time.
1987
1993
  *
1988
- * @param path - The target-relative path to write.
1989
- * @param content - The exact UTF-8 text the destination should hold.
1990
- * @returns Nothing.
1994
+ * @param path - The target-relative directory to establish.
1995
+ * @returns The directory's identity and every segment this call created.
1991
1996
  * @throws {@link ScaffoldError} coded `INVALID` when the path is not one this
1992
- * transaction opened or is already staged, `TARGET` when the destination holds
1993
- * a directory, and `WRITE` when the staged file cannot be written or does not
1994
- * carry the bytes it was given.
1997
+ * transaction opened, `TARGET` when the destination holds a file, and `WRITE`
1998
+ * when a segment cannot be created or changed while it was being created.
1995
1999
  *
1996
2000
  * @remarks
1997
- * The staged file is read back and digested against the text it was given, so
1998
- * a partial or interrupted write is refused here rather than promoted later.
2001
+ * A directory is created immediately rather than staged, because creating one
2002
+ * destroys nothing: rollback removes exactly the segments this call created,
2003
+ * innermost first, and leaves every segment that was already there. Each
2004
+ * created segment is captured by device and inode, so a segment swapped
2005
+ * underneath the transaction is detected rather than written into.
1999
2006
  */
2000
- write(path: string, content: string): void;
2007
+ establish(path: string): WriteDirectoryResult;
2001
2008
  /**
2002
- * Stage one byte-for-byte copy of a file that already exists on this host.
2009
+ * Mark one file for deletion at commit.
2003
2010
  *
2004
- * @param path - The target-relative path to write.
2005
- * @param source - The resolved absolute path to copy the bytes from.
2006
- * @param executable - Whether the destination should carry the executable bit.
2011
+ * @param path - The target-relative file to delete.
2007
2012
  * @returns Nothing.
2008
2013
  * @throws {@link ScaffoldError} coded `INVALID` when the path is not one this
2009
- * transaction opened or is already staged, `TARGET` when the destination holds
2010
- * a directory or the source is not a physical file, and `WRITE` when the copy
2011
- * cannot be made or does not digest to the source's bytes.
2014
+ * transaction opened or is already claimed, and `TARGET` when the destination
2015
+ * does not hold a file.
2012
2016
  *
2013
2017
  * @remarks
2014
- * The source is digested before the copy and the copy is digested after it, so
2015
- * a source that changed mid-copy is refused instead of staged.
2018
+ * Nothing moves here. Commit renames the file into the private backup rather
2019
+ * than unlinking it, so a later failure in the same commit puts it back.
2016
2020
  */
2017
- copy(path: string, source: string, executable: boolean): void;
2021
+ remove(path: string): void;
2018
2022
  /**
2019
- * Establish one directory inside the target, one segment at a time.
2023
+ * Promote every staged file and take every marked file, or roll the whole call back.
2020
2024
  *
2021
- * @param path - The target-relative directory to establish.
2022
- * @returns The directory's identity and every segment this call created.
2023
- * @throws {@link ScaffoldError} coded `INVALID` when the path is not one this
2024
- * transaction opened, `TARGET` when the destination holds a file, and `WRITE`
2025
- * when a segment cannot be created or changed while it was being created.
2025
+ * @returns Every target-relative path whose destination changed: the files
2026
+ * promoted, then the directories established, then the files taken.
2027
+ * @throws {@link ScaffoldError} coded `WRITE` when the transaction is closed,
2028
+ * when a destination moved since the transaction opened, or when the commit
2029
+ * failed; a failure reports what rollback could not undo in its context.
2026
2030
  *
2027
2031
  * @remarks
2028
- * A directory is created immediately rather than staged, because creating one
2029
- * destroys nothing: rollback removes exactly the segments this call created,
2030
- * innermost first, and leaves every segment that was already there. Each
2031
- * created segment is captured by device and inode, so a segment swapped
2032
- * underneath the transaction is detected rather than written into.
2032
+ * Every destination and every directory this transaction created is re-checked
2033
+ * before anything moves, so the common failure moves nothing at all. That
2034
+ * check runs inside the same rollback as the promotions, because a transaction
2035
+ * that refuses before it starts still has a private root and created
2036
+ * directories to clear. The transaction is closed either way: a committed one
2037
+ * has nothing left to undo and a failed one has already been rolled back.
2033
2038
  */
2034
- directory(path: string): WriteDirectoryResult;
2039
+ commit(): readonly string[];
2035
2040
  /**
2036
- * Mark one file for deletion at commit.
2041
+ * Abandon the transaction and remove everything it created.
2037
2042
  *
2038
- * @param path - The target-relative file to delete.
2039
2043
  * @returns Nothing.
2040
- * @throws {@link ScaffoldError} coded `INVALID` when the path is not one this
2041
- * transaction opened or is already claimed, and `TARGET` when the destination
2042
- * does not hold a file.
2044
+ * @throws {@link ScaffoldError} coded `WRITE` when residue could not be
2045
+ * removed, naming the private root that still holds it.
2043
2046
  *
2044
2047
  * @remarks
2045
- * Nothing moves here. Commit renames the file into the private backup rather
2046
- * than unlinking it, so a later failure in the same commit puts it back.
2048
+ * Idempotent, and a no-op on a transaction that already committed or already
2049
+ * failed, so a caller can put it in a `finally` beside the work it guards.
2047
2050
  */
2048
- remove(path: string): void;
2049
- /**
2050
- * Promote every staged file and take every marked file, or roll the whole call back.
2051
- *
2052
- * @returns Every target-relative path whose destination changed: the files
2053
- * promoted, then the directories established, then the files taken.
2054
- * @throws {@link ScaffoldError} coded `WRITE` when the transaction is closed,
2055
- * when a destination moved since the transaction opened, or when the commit
2056
- * failed; a failure reports what rollback could not undo in its context.
2057
- *
2058
- * @remarks
2059
- * Every destination and every directory this transaction created is re-checked
2060
- * before anything moves, so the common failure moves nothing at all. That
2061
- * check runs inside the same rollback as the promotions, because a transaction
2062
- * that refuses before it starts still has a private root and created
2063
- * directories to clear. The transaction is closed either way: a committed one
2064
- * has nothing left to undo and a failed one has already been rolled back.
2065
- */
2066
- commit(): readonly string[];
2067
- /**
2068
- * Abandon the transaction and remove everything it created.
2069
- *
2070
- * @returns Nothing.
2071
- * @throws {@link ScaffoldError} coded `WRITE` when residue could not be
2072
- * removed, naming the private root that still holds it.
2073
- *
2074
- * @remarks
2075
- * Idempotent, and a no-op on a transaction that already committed or already
2076
- * failed, so a caller can put it in a `finally` beside the work it guards.
2077
- */
2078
- discard(): void;
2079
- }
2080
-
2081
- export { }
2051
+ discard(): void;
2052
+ }
2053
+
2054
+ export { }