@orkestrel/scaffold 0.0.48 → 0.0.50

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.
@@ -1,8 +1,9 @@
1
- import { andOf, arrayOf, attempt, boundsOf, holds, isArray, isBoolean, isError, isFunction, isInteger, isRecord, isString, parseJSON, parseJSONAs, parseStringField, recordOf, stringOf } from "@orkestrel/contract";
2
- import { CATALOG_AGENT_PATH, CONTROL_CHARACTER_PATTERN, EXECUTABLE_PATHS, HOST_PATHS, MAX_ARTIFACT_BYTES, MAX_COLLECTION_ITEMS, MAX_DEPENDENCY_NAME_LENGTH, MAX_MANIFEST_BYTES, MAX_PATH_LENGTH, MAX_RANGE_LENGTH, MAX_REGISTRY_BYTES, MAX_TOTAL_ARTIFACT_BYTES, MAX_TOTAL_REGISTRY_BYTES, ScaffoldError, WORKSPACE_OWNED_PATHS, bytesToHex, catalogToLayers, cloneValue, compareVersions, computeBytes, contentToHex, extractRangeMajor, extractVersion, inferGroup, isAudit, isCatalogEntry, isCollection, isDependency, isDependencyName, isMirror, isPath, isPlan, isSnapshot, matchesDriftReachability, matchesRange, nameToGuide, planToFindings, replaceManifestRanges } from "../core/index.js";
1
+ import { andOf, arrayOf, attempt, boundsOf, holds, isArray, isBoolean, isError, isFunction, isInteger, isRecord, isString, parseJSON, parseJSONAs, parseStringField, recordOf, stringOf, unionOf } from "@orkestrel/contract";
2
+ import { CATALOG_AGENT_PATH, CONTROL_CHARACTER_PATTERN, EXECUTABLE_PATHS, HOST_INVENTORY_PATH, HOST_PATHS, MAX_ARTIFACT_BYTES, MAX_COLLECTION_ITEMS, MAX_DEPENDENCY_NAME_LENGTH, MAX_MANIFEST_BYTES, MAX_PATH_LENGTH, MAX_RANGE_LENGTH, MAX_REGISTRY_BYTES, MAX_TOTAL_ARTIFACT_BYTES, MAX_TOTAL_REGISTRY_BYTES, ScaffoldError, WORKSPACE_OWNED_PATHS, bytesToHex, catalogToLayers, cloneValue, compareVersions, computeBytes, contentToHex, extractRangeMajor, extractVersion, inferGroup, isAudit, isCatalogEntry, isCollection, isDeferredPath, isDependency, isDependencyName, isHex, isManifestScript, isMirror, isPath, isPlan, isSnapshot, matchesDriftReachability, matchesRange, nameToGuide, planToFindings, replaceManifestRanges, replaceManifestScripts } from "../core/index.js";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
- import { chmodSync, closeSync, constants, copyFileSync, fstatSync, linkSync, lstatSync, mkdirSync, openSync, opendirSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, writeFileSync } from "node:fs";
5
- import { basename, dirname, join, parse, relative, resolve, sep } from "node:path";
4
+ import { chmodSync, closeSync, constants, copyFileSync, fstatSync, linkSync, lstatSync, mkdirSync, mkdtempSync, openSync, opendirSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, writeFileSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { basename, dirname, extname, join, parse, relative, resolve, sep } from "node:path";
6
7
  import { fileURLToPath } from "node:url";
7
8
  import { Emitter } from "@orkestrel/emitter";
8
9
  //#region src/server/constants.ts
@@ -43,13 +44,13 @@ var RESERVED_SEGMENT_PATTERN = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9]|conin\$|co
43
44
  */
44
45
  var DIGEST_PATTERN = /^[0-9a-f]{64}$/;
45
46
  /**
46
- * The Git branch syntax the guide endpoint accepts.
47
+ * The Git branch syntax the repository endpoint accepts.
47
48
  *
48
49
  * @remarks
49
50
  * A branch is caller-supplied and reaches a URL path, so it is closed to
50
51
  * alphanumerics, dot, underscore, hyphen, and the separator, must open with an
51
52
  * alphanumeric, and may carry no `..` anywhere. That last refusal is what stops
52
- * a branch from walking out of the guide directory it addresses.
53
+ * a branch from walking out of the repository path it addresses.
53
54
  */
54
55
  var BRANCH_PATTERN = /^(?!.*\.\.)[A-Za-z0-9][A-Za-z0-9._/-]*$/;
55
56
  /**
@@ -83,7 +84,7 @@ var MAX_PATH_DEPTH = 64;
83
84
  var MAX_INVENTORY_PATHS = 1e5;
84
85
  /** Maximum characters one caller-supplied upstream endpoint may carry. */
85
86
  var MAX_ENDPOINT_LENGTH = 2048;
86
- /** Maximum characters one guide branch may carry. */
87
+ /** Maximum characters one repository branch may carry. */
87
88
  var MAX_BRANCH_LENGTH = 255;
88
89
  /**
89
90
  * Maximum simultaneous upstream requests.
@@ -225,10 +226,10 @@ var isEndpoint = stringOf({
225
226
  max: MAX_ENDPOINT_LENGTH
226
227
  });
227
228
  /**
228
- * Narrow a value to a Git branch the guide endpoint accepts.
229
+ * Narrow a value to a Git branch the repository endpoint accepts.
229
230
  *
230
231
  * @remarks
231
- * A branch reaches the guide URL's path, so the syntax is closed rather than
232
+ * A branch reaches the repository URL's path, so the syntax is closed rather than
232
233
  * merely bounded and no `..` is admitted anywhere in it.
233
234
  *
234
235
  * @example
@@ -270,8 +271,49 @@ var isTimeout = andOf(isInteger, boundsOf(1, MAX_UPSTREAM_TIMEOUT));
270
271
  * ```
271
272
  */
272
273
  var isDependencyNames = andOf(isCollection, arrayOf(isDependencyName));
274
+ /**
275
+ * Narrow a value to a bounded list of target-relative paths.
276
+ *
277
+ * @remarks
278
+ * Composed from the core collection and path guards rather than restated, so
279
+ * the containment law that keeps a caller-supplied path inside its target has
280
+ * exactly one home. It bounds what a caller may hand a public method, which is
281
+ * why it is not {@link isInventory}: that one bounds what a checkout may hold.
282
+ *
283
+ * @example
284
+ * ```ts
285
+ * import { isPaths } from '@orkestrel/scaffold/server'
286
+ *
287
+ * isPaths(['AGENTS.md']) // true
288
+ * isPaths(['../secrets']) // false
289
+ * ```
290
+ */
291
+ var isPaths = andOf(isCollection, arrayOf(isPath));
273
292
  /** Narrow a value to a bounded list of declared runtime dependencies. */
274
293
  var isDependencies = andOf(isCollection, arrayOf(isDependency));
294
+ /**
295
+ * Narrow a value to one {@link ManifestRegionSet}.
296
+ *
297
+ * @remarks
298
+ * The whole closed record a manifest-writing method accepts, so a caller
299
+ * naming a region the writer does not carry is refused before any byte moves.
300
+ * Each region is bounded by the same collection law its own list guard applies.
301
+ *
302
+ * @example
303
+ * ```ts
304
+ * import { isManifestRegionSet } from '@orkestrel/scaffold/server'
305
+ *
306
+ * isManifestRegionSet({ pins: { runtime: [], development: [] }, scripts: [] }) // true
307
+ * isManifestRegionSet({ pins: { runtime: [], development: [] } }) // false
308
+ * ```
309
+ */
310
+ var isManifestRegionSet = recordOf({
311
+ pins: recordOf({
312
+ runtime: isDependencies,
313
+ development: isDependencies
314
+ }),
315
+ scripts: andOf(isCollection, arrayOf(isManifestScript))
316
+ });
275
317
  /** Narrow a value to a bounded list of fetched guide mirrors. */
276
318
  var isMirrors = andOf(isCollection, arrayOf(isMirror));
277
319
  /** Narrow a value to a bounded list of fleet catalog rows. */
@@ -289,13 +331,19 @@ var isCatalogEntries = andOf(isCollection, arrayOf(isCatalogEntry));
289
331
  * ```ts
290
332
  * import { isManifestEntry } from '@orkestrel/scaffold/server'
291
333
  *
292
- * isManifestEntry({ storage: 'AGENTS.md', destination: 'AGENTS.md', executable: false }) // true
334
+ * isManifestEntry({
335
+ * storage: 'AGENTS.md',
336
+ * destination: 'AGENTS.md',
337
+ * executable: false,
338
+ * digest: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
339
+ * }) // true
293
340
  * ```
294
341
  */
295
342
  var isManifestEntry = recordOf({
296
343
  storage: isPath,
297
344
  destination: isPath,
298
- executable: isBoolean
345
+ executable: isBoolean,
346
+ digest: isDigest
299
347
  });
300
348
  /**
301
349
  * Narrow a value to one {@link HostManifest}.
@@ -311,7 +359,33 @@ var isHostManifest = recordOf({
311
359
  digest: isDigest
312
360
  });
313
361
  /**
314
- * Narrow a value to a {@link Repository}.
362
+ * Narrow a value to one {@link Host}.
363
+ *
364
+ * @remarks
365
+ * A whole vendored host handed in as a value is as untrusted as one read from a
366
+ * directory a caller named, so both halves are guarded: the manifest by the same
367
+ * membership law a read root is held to, and the bytes by the core snapshot law,
368
+ * which bounds the fill and reads every key as a path and every value as exact
369
+ * lowercase hexadecimal. Whether those halves agree with each other is the
370
+ * reader's question rather than this one's, because a guard has only `false` to
371
+ * say and a mismatch has a path to name.
372
+ *
373
+ * @example
374
+ * ```ts
375
+ * import { isHost } from '@orkestrel/scaffold/server'
376
+ *
377
+ * const digest = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
378
+ *
379
+ * isHost({ manifest: { entries: [], roots: [], digest }, bytes: {} }) // true
380
+ * isHost({ manifest: { entries: [], roots: [], digest } }) // false
381
+ * ```
382
+ */
383
+ var isHost = recordOf({
384
+ manifest: isHostManifest,
385
+ bytes: isSnapshot
386
+ });
387
+ /**
388
+ * Narrow a value to a {@link Worktree}.
315
389
  *
316
390
  * @remarks
317
391
  * Both path lists are target-relative, so both are measured by the core path
@@ -321,13 +395,13 @@ var isHostManifest = recordOf({
321
395
  *
322
396
  * @example
323
397
  * ```ts
324
- * import { isRepository } from '@orkestrel/scaffold/server'
398
+ * import { isWorktree } from '@orkestrel/scaffold/server'
325
399
  *
326
- * isRepository({ tracked: ['AGENTS.md'], dirty: [] }) // true
327
- * isRepository({ tracked: ['../secrets'], dirty: [] }) // false
400
+ * isWorktree({ tracked: ['AGENTS.md'], dirty: [] }) // true
401
+ * isWorktree({ tracked: ['../secrets'], dirty: [] }) // false
328
402
  * ```
329
403
  */
330
- var isRepository = recordOf({
404
+ var isWorktree = recordOf({
331
405
  tracked: andOf(isInventory, arrayOf(isPath)),
332
406
  dirty: andOf(isInventory, arrayOf(isPath))
333
407
  });
@@ -349,6 +423,12 @@ var isMaterializerHooks = recordOf({
349
423
  /**
350
424
  * Narrow a value to {@link MaterializerOptions}.
351
425
  *
426
+ * @remarks
427
+ * `host` admits both representations of one vendored root: a directory path and
428
+ * a whole {@link Host} value. They share a key because they are one setting
429
+ * stated two ways rather than two settings, so nothing downstream has to
430
+ * reconcile a pair that could disagree.
431
+ *
352
432
  * @example
353
433
  * ```ts
354
434
  * import { isMaterializerOptions } from '@orkestrel/scaffold/server'
@@ -358,7 +438,7 @@ var isMaterializerHooks = recordOf({
358
438
  * ```
359
439
  */
360
440
  var isMaterializerOptions = recordOf({
361
- host: isFilesystemPath,
441
+ host: unionOf(isFilesystemPath, isHost),
362
442
  on: isMaterializerHooks,
363
443
  error: isFunction
364
444
  }, true);
@@ -372,6 +452,7 @@ var isMaterializerOptions = recordOf({
372
452
  var isUpstreamHooks = recordOf({
373
453
  release: isFunction,
374
454
  mirror: isFunction,
455
+ file: isFunction,
375
456
  error: isFunction,
376
457
  destroy: isFunction
377
458
  }, true);
@@ -391,12 +472,12 @@ var isUpstreamHooks = recordOf({
391
472
  * ```ts
392
473
  * import { isUpstreamOptions } from '@orkestrel/scaffold/server'
393
474
  *
394
- * isUpstreamOptions({ guides: { branch: 'main' }, concurrency: 4 }) // true
475
+ * isUpstreamOptions({ repository: { branch: 'main' }, concurrency: 4 }) // true
395
476
  * isUpstreamOptions({ concurrency: 0 }) // false
396
477
  * ```
397
478
  */
398
479
  var isUpstreamOptions = recordOf({
399
- guides: recordOf({
480
+ repository: recordOf({
400
481
  base: isEndpoint,
401
482
  branch: isBranch,
402
483
  timeout: isTimeout
@@ -473,7 +554,7 @@ function matchesGitPath(path) {
473
554
  * directories. It is the inversion the contract asks for: the candidate set
474
555
  * is re-derived from the plan and narrowed by what git tracks, and the audit
475
556
  * must agree with that derivation rather than supply the set itself.
476
- * Repository metadata is protected because losing history is not a repair,
557
+ * Git metadata is protected because losing history is not a repair,
477
558
  * and a target's own `src` and `app` trees are protected because a
478
559
  * workspace's source is the one thing scaffold never plans and never owns. A
479
560
  * plan the compiler emits never maps a protected root, so this guard exists
@@ -504,7 +585,7 @@ function matchesProtectedPath(path) {
504
585
  * The vendoring deny-list. A host root is staged from a real checkout, so the
505
586
  * refusal is stated over the path rather than over the file's content: a
506
587
  * credential is recognizable by where it sits and what it is called long before
507
- * anything reads it. Repository metadata is included through
588
+ * anything reads it. Git metadata is included through
508
589
  * {@link matchesGitPath}, so one call answers the whole question and no caller
509
590
  * has to remember to ask twice.
510
591
  *
@@ -601,6 +682,25 @@ function computeDigest(content) {
601
682
  return createHash("sha256").update(content, "utf8").digest("hex");
602
683
  }
603
684
  /**
685
+ * Projects exact bytes stated in hexadecimal to their SHA-256 digest.
686
+ *
687
+ * @param hex - The exact lowercase hexadecimal bytes to digest.
688
+ * @returns Sixty-four lowercase hexadecimal digits.
689
+ * @throws `ScaffoldError('INVALID', …)` when `hex` is not exact bounded
690
+ * lowercase hexadecimal text.
691
+ *
692
+ * @example
693
+ * ```ts
694
+ * import { hexToDigest } from '@orkestrel/scaffold/server'
695
+ *
696
+ * hexToDigest('68690a') // '98ea6e4f216f2fb4b69fff9b3a44842c38686ca685f3f55dc48c5d3fb1107be4'
697
+ * ```
698
+ */
699
+ function hexToDigest(hex) {
700
+ if (!isHex(hex)) throw new ScaffoldError("INVALID", "Digest input is not exact hexadecimal bytes", { hex });
701
+ return createHash("sha256").update(Buffer.from(hex, "hex")).digest("hex");
702
+ }
703
+ /**
604
704
  * Compute the digest of a vendored host's declared membership.
605
705
  *
606
706
  * @param entries - The ordered file membership declarations.
@@ -627,7 +727,8 @@ function computeManifestDigest(entries, roots) {
627
727
  entries: entries.map((entry) => ({
628
728
  storage: entry.storage,
629
729
  destination: entry.destination,
630
- executable: entry.executable
730
+ executable: entry.executable,
731
+ digest: entry.digest
631
732
  })),
632
733
  roots: [...roots]
633
734
  }));
@@ -1269,6 +1370,7 @@ function readSnapshot(target, paths) {
1269
1370
  * Read a vendored host's manifest, when it carries one.
1270
1371
  *
1271
1372
  * @param host - The vendored host root to read.
1373
+ * @param name - The root-relative manifest path. Default: `manifest.json`.
1272
1374
  * @returns The manifest, or `undefined` when the host carries none.
1273
1375
  * @throws `ScaffoldError('INVALID', …)` when `host` is not a host path.
1274
1376
  * @throws `ScaffoldError('TARGET', …)` when the manifest is there but cannot be
@@ -1294,9 +1396,8 @@ function readSnapshot(target, paths) {
1294
1396
  * readHostManifest('./dist/host') // the manifest, or undefined for a raw root
1295
1397
  * ```
1296
1398
  */
1297
- function readHostManifest(host) {
1399
+ function readHostManifest(host, name = MANIFEST_NAME) {
1298
1400
  if (!isFilesystemPath(host)) throw new ScaffoldError("INVALID", "Host root is not a host path", { host });
1299
- const name = MANIFEST_NAME;
1300
1401
  const full = resolveContainedPath(host, name);
1301
1402
  if (full === void 0) throw new ScaffoldError("INVALID", `Host manifest leaves its root at ${host}`, { host });
1302
1403
  const status = attempt(() => lstatSync(full));
@@ -1315,6 +1416,55 @@ function readHostManifest(host) {
1315
1416
  return manifest;
1316
1417
  }
1317
1418
  /**
1419
+ * Reads the installed vendored host floor as a value.
1420
+ *
1421
+ * @param root - The vendored host root. Default: the installed package's
1422
+ * vendored root, resolved from this module's location.
1423
+ * @returns The verified manifest and the exact bytes of every declared entry.
1424
+ * @throws `ScaffoldError('TARGET', …)` when the root is not a readable physical
1425
+ * directory, its manifest is absent or unreadable, the manifest does not verify,
1426
+ * or a declared file is unreadable or misses its digest.
1427
+ *
1428
+ * @remarks
1429
+ * Reads the same default floor the {@link Materializer} uses. Each declared
1430
+ * file is addressed through the manifest's storage name and retained under its
1431
+ * destination, so the returned value has the same shape as the installed root.
1432
+ * When this module executes from TypeScript source, the committed inventory is
1433
+ * the manifest and each checkout destination supplies its bytes. The emitted
1434
+ * module reads the staged `manifest.json` file and each storage path instead.
1435
+ *
1436
+ * @example
1437
+ * ```ts
1438
+ * import { readHostFloor } from '@orkestrel/scaffold/server'
1439
+ *
1440
+ * readHostFloor().manifest // the installed floor's verified membership
1441
+ * ```
1442
+ */
1443
+ function readHostFloor(root) {
1444
+ const location = fileURLToPath(import.meta.url);
1445
+ const module = dirname(location);
1446
+ const source = root === void 0 && extname(location) === ".ts";
1447
+ const host = root ?? resolve(module, source ? "../.." : "../../host");
1448
+ if (!isPhysicalDirectory(host)) throw new ScaffoldError("TARGET", `The vendored host root is not readable at ${host}`, { host });
1449
+ const manifest = readHostManifest(host, source ? HOST_INVENTORY_PATH : MANIFEST_NAME);
1450
+ if (manifest === void 0) throw new ScaffoldError("TARGET", `The vendored host carries no manifest at ${host}`, { host });
1451
+ const bytes = {};
1452
+ for (const entry of manifest.entries) {
1453
+ const path = source ? entry.destination : entry.storage;
1454
+ const hex = readFileHex(host, path);
1455
+ if (hex === void 0 || hexToDigest(hex) !== entry.digest) throw new ScaffoldError("TARGET", `The vendored host cannot read the declared file at ${path}`, {
1456
+ host,
1457
+ path,
1458
+ destination: entry.destination
1459
+ });
1460
+ bytes[entry.destination] = hex;
1461
+ }
1462
+ return {
1463
+ manifest,
1464
+ bytes
1465
+ };
1466
+ }
1467
+ /**
1318
1468
  * Derive one vendored-host manifest entry from a file in a checkout.
1319
1469
  *
1320
1470
  * @param destination - The target-relative path the file is written to.
@@ -1338,20 +1488,155 @@ function readHostManifest(host) {
1338
1488
  * import { readManifestEntry } from '@orkestrel/scaffold/server'
1339
1489
  *
1340
1490
  * readManifestEntry('.gitignore', '/tmp/checkout/.gitignore')
1341
- * // { storage: 'dotfiles/gitignore', destination: '.gitignore', executable: false }
1491
+ * // { storage: 'dotfiles/gitignore', destination: '.gitignore', executable: false, digest: '...' }
1342
1492
  * ```
1343
1493
  */
1344
1494
  function readManifestEntry(destination, source) {
1345
- if (!isPhysicalFile(source)) return void 0;
1346
- const status = attempt(() => lstatSync(source));
1347
- if (!status.success || status.value.size > MAX_ARTIFACT_BYTES) return void 0;
1495
+ const digest = computeFileDigest(source);
1496
+ if (digest === void 0) return void 0;
1348
1497
  return {
1349
1498
  storage: pathToStorage(destination),
1350
1499
  destination,
1351
- executable: matchesExecutablePath(destination)
1500
+ executable: matchesExecutablePath(destination),
1501
+ digest
1352
1502
  };
1353
1503
  }
1354
1504
  /**
1505
+ * Assemble a whole vendored host from live files and the installed floor.
1506
+ *
1507
+ * @param files - The host-owned vendored files read from the repository, one
1508
+ * row per path.
1509
+ * @param floor - The installed host floor, which fixes the membership a fill
1510
+ * may draw from and supplies the bytes owned by another surface.
1511
+ * @returns The assembled host, or `undefined` when any row produced no answer or
1512
+ * names a path the floor does not declare, or when a host-owned path is absent.
1513
+ *
1514
+ * @remarks
1515
+ * The one place the host-owned all-or-nothing rule is decided, so no verb
1516
+ * restates it. The host surface contributes one baseline: a fill carries live
1517
+ * bytes for every path that surface writes, or it is nothing. A row that failed,
1518
+ * went missing, names an undeclared path, or leaves a host-owned path absent
1519
+ * answers `undefined`. Deferred paths are presence-only and retain the installed
1520
+ * floor bytes that their catalog or mirror surface owns; repair never writes
1521
+ * those floor bytes. One `Host` can therefore carry live host bytes beside floor
1522
+ * bytes without mixing baselines within a surface.
1523
+ *
1524
+ * The emitted entries keep the release's own order and its storage and
1525
+ * executable declarations, and carry digests recomputed over the bytes the fill
1526
+ * actually holds. That is what lets a reader verify the value against itself,
1527
+ * and it is why an undeclared path is refused rather than added: membership
1528
+ * moves with a release, never with a fetch.
1529
+ *
1530
+ * @example
1531
+ * ```ts
1532
+ * import { filesToHost } from '@orkestrel/scaffold/server'
1533
+ *
1534
+ * filesToHost([{ path: 'AGENTS.md', lookup: 'found', hex: '23204167656e74730a' }], floor)
1535
+ * // { manifest: { entries: [ … ], roots: [ … ], digest: '…' }, bytes: { 'AGENTS.md': '…' } }
1536
+ * ```
1537
+ */
1538
+ function filesToHost(files, floor) {
1539
+ const declared = new Set(floor.manifest.entries.map((entry) => entry.destination));
1540
+ const held = /* @__PURE__ */ new Map();
1541
+ for (const file of files) {
1542
+ if (file.lookup !== "found" || !declared.has(file.path)) return void 0;
1543
+ if (!isDeferredPath(file.path)) held.set(file.path, file.hex);
1544
+ }
1545
+ const entries = [];
1546
+ const bytes = {};
1547
+ for (const entry of floor.manifest.entries) {
1548
+ const hex = isDeferredPath(entry.destination) ? floor.bytes[entry.destination] : held.get(entry.destination);
1549
+ if (hex === void 0) return void 0;
1550
+ entries.push({
1551
+ storage: entry.storage,
1552
+ destination: entry.destination,
1553
+ executable: entry.executable,
1554
+ digest: hexToDigest(hex)
1555
+ });
1556
+ bytes[entry.destination] = hex;
1557
+ }
1558
+ return {
1559
+ manifest: {
1560
+ entries,
1561
+ roots: floor.manifest.roots,
1562
+ digest: computeManifestDigest(entries, floor.manifest.roots)
1563
+ },
1564
+ bytes
1565
+ };
1566
+ }
1567
+ /**
1568
+ * Stage the named destinations of a value host into a private root.
1569
+ *
1570
+ * @param host - The host whose bytes are written, keyed by destination.
1571
+ * @param root - The private directory to fill; it must already be a directory
1572
+ * this process may write into.
1573
+ * @param destinations - The destinations to stage, each declared by `host`.
1574
+ * @returns The entry staged for each destination, in the order requested.
1575
+ * @throws `ScaffoldError('INVALID', …)` when `root` is not a host path or a
1576
+ * storage name leaves it.
1577
+ * @throws `ScaffoldError('TARGET', …)` when a destination is one the host does
1578
+ * not declare, carries no bytes, or carries bytes that miss its declared digest.
1579
+ * @throws `ScaffoldError('WRITE', …)` when a file cannot be written or does not
1580
+ * read back as the bytes it was given.
1581
+ *
1582
+ * @remarks
1583
+ * Each file lands under the storage name the manifest declares and takes the
1584
+ * executable bit that manifest records, so a root filled from a value is the
1585
+ * same shape as one staged from a checkout and a reader cannot tell them apart.
1586
+ * That is what lets a mutation copy real files with real modes from bytes a
1587
+ * caller supplied, instead of degrading them to plain text writes.
1588
+ *
1589
+ * The bytes are digested before the write and the staged file after it, so a
1590
+ * value that disagrees with its own manifest is told apart from a write that
1591
+ * did not land.
1592
+ *
1593
+ * @example
1594
+ * ```ts
1595
+ * import { stageBytes } from '@orkestrel/scaffold/server'
1596
+ *
1597
+ * stageBytes(host, '/tmp/orkestrel-host-a1b2', ['scripts/codex.sh'])
1598
+ * // [{ storage: 'scripts/codex.sh', destination: 'scripts/codex.sh', executable: true, digest: '…' }]
1599
+ * ```
1600
+ */
1601
+ function stageBytes(host, root, destinations) {
1602
+ if (!isFilesystemPath(root)) throw new ScaffoldError("INVALID", "Staging host root is not a host path", { host: root });
1603
+ const declared = new Map(host.manifest.entries.map((entry) => [entry.destination, entry]));
1604
+ const staged = [];
1605
+ for (const destination of destinations) {
1606
+ const entry = declared.get(destination);
1607
+ const hex = host.bytes[destination];
1608
+ if (entry === void 0 || hex === void 0) throw new ScaffoldError("TARGET", `The host carries no bytes for ${destination}`, {
1609
+ host: root,
1610
+ destination
1611
+ });
1612
+ if (hexToDigest(hex) !== entry.digest) throw new ScaffoldError("TARGET", `The host bytes for ${destination} miss its digest`, {
1613
+ host: root,
1614
+ destination
1615
+ });
1616
+ const full = resolveContainedPath(root, entry.storage);
1617
+ if (full === void 0) throw new ScaffoldError("INVALID", `Host storage leaves its root at ${entry.storage}`, {
1618
+ host: root,
1619
+ storage: entry.storage
1620
+ });
1621
+ const written = attempt(() => {
1622
+ mkdirSync(dirname(full), { recursive: true });
1623
+ writeFileSync(full, Buffer.from(hex, "hex"), { flag: "wx" });
1624
+ if (entry.executable) chmodSync(full, 493);
1625
+ });
1626
+ if (!written.success) throw new ScaffoldError("WRITE", `Host bytes could not be staged at ${entry.storage}`, {
1627
+ host: root,
1628
+ storage: entry.storage,
1629
+ error: written.error
1630
+ });
1631
+ if (computeFileDigest(full) !== entry.digest) throw new ScaffoldError("WRITE", `Staged host bytes at ${entry.storage} did not read back`, {
1632
+ host: root,
1633
+ storage: entry.storage
1634
+ });
1635
+ staged.push(entry);
1636
+ }
1637
+ return staged;
1638
+ }
1639
+ /**
1355
1640
  * Stage a vendored host root from a real checkout.
1356
1641
  *
1357
1642
  * @param checkout - The checkout the vendored paths are read from.
@@ -1441,7 +1726,7 @@ function stageHost(checkout, host) {
1441
1726
  missing
1442
1727
  });
1443
1728
  const stored = /* @__PURE__ */ new Set([MANIFEST_NAME]);
1444
- const entries = [];
1729
+ const candidates = [];
1445
1730
  for (const destination of vendored) {
1446
1731
  const full = resolveContainedPath(source, destination);
1447
1732
  if (full === void 0) throw new ScaffoldError("INVALID", `Vendored path leaves its checkout at ${destination}`, {
@@ -1460,9 +1745,9 @@ function stageHost(checkout, host) {
1460
1745
  storage: entry.storage
1461
1746
  });
1462
1747
  stored.add(entry.storage);
1463
- entries.push(entry);
1748
+ candidates.push(entry);
1464
1749
  }
1465
- entries.sort((first, second) => first.storage < second.storage ? -1 : 1);
1750
+ candidates.sort((first, second) => first.storage < second.storage ? -1 : 1);
1466
1751
  roots.sort();
1467
1752
  const root = resolve(host);
1468
1753
  const established = attempt(() => mkdirSync(root, { recursive: true }));
@@ -1470,7 +1755,8 @@ function stageHost(checkout, host) {
1470
1755
  host: root,
1471
1756
  ...established.success ? {} : { error: established.error }
1472
1757
  });
1473
- for (const entry of entries) {
1758
+ const entries = [];
1759
+ for (const entry of candidates) {
1474
1760
  const origin = resolveContainedPath(source, entry.destination);
1475
1761
  const destination = resolveContainedPath(root, entry.storage);
1476
1762
  if (origin === void 0 || destination === void 0) throw new ScaffoldError("INVALID", `Vendored path leaves its root at ${entry.destination}`, {
@@ -1489,6 +1775,15 @@ function stageHost(checkout, host) {
1489
1775
  storage: entry.storage,
1490
1776
  error: copied.error
1491
1777
  });
1778
+ const digest = computeFileDigest(destination);
1779
+ if (digest === void 0) throw new ScaffoldError("WRITE", `Vendored file could not be verified at ${entry.storage}`, {
1780
+ host: root,
1781
+ storage: entry.storage
1782
+ });
1783
+ entries.push({
1784
+ ...entry,
1785
+ digest
1786
+ });
1492
1787
  }
1493
1788
  const manifest = {
1494
1789
  entries,
@@ -1510,6 +1805,67 @@ function stageHost(checkout, host) {
1510
1805
  return entries;
1511
1806
  }
1512
1807
  /**
1808
+ * Stages the committed inventory of the files a vendored host carries.
1809
+ *
1810
+ * @param checkout - The checkout whose vendored paths are inventoried.
1811
+ * @param path - The host path where the JSON inventory is written.
1812
+ * @returns The validated manifest written to `path`.
1813
+ * @throws `ScaffoldError('INVALID', …)` when `path` is not a host path.
1814
+ * @throws `ScaffoldError('WRITE', …)` when a temporary host or the inventory
1815
+ * cannot be written or removed.
1816
+ * @throws `ScaffoldError('TARGET', …)` when the staged inventory does not read
1817
+ * back through the manifest validator.
1818
+ *
1819
+ * @remarks
1820
+ * Uses {@link stageHost} as the single vendored-path expansion. The temporary
1821
+ * host supplies the same entries, roots, per-file digests, and membership
1822
+ * digest as the published host while the requested output remains one JSON
1823
+ * file.
1824
+ *
1825
+ * @example
1826
+ * ```ts
1827
+ * import { stageInventory } from '@orkestrel/scaffold/server'
1828
+ *
1829
+ * stageInventory(process.cwd(), 'host.json') // the committed host inventory
1830
+ * ```
1831
+ */
1832
+ function stageInventory(checkout, path) {
1833
+ if (!isFilesystemPath(path)) throw new ScaffoldError("INVALID", "Inventory destination is not a host path", { path });
1834
+ const temporary = attempt(() => mkdtempSync(join(tmpdir(), "orkestrel-scaffold-host-")));
1835
+ if (!temporary.success) throw new ScaffoldError("WRITE", "Inventory staging root could not be established", {
1836
+ path,
1837
+ error: temporary.error
1838
+ });
1839
+ const staged = attempt(() => {
1840
+ stageHost(checkout, temporary.value);
1841
+ const manifest = readHostManifest(temporary.value);
1842
+ if (manifest === void 0) throw new ScaffoldError("TARGET", "The staged inventory carries no manifest", { path });
1843
+ const target = resolve(path);
1844
+ const published = attempt(() => {
1845
+ mkdirSync(dirname(target), { recursive: true });
1846
+ writeFileSync(target, `${JSON.stringify(manifest, null, " ")}\n`, "utf8");
1847
+ });
1848
+ if (!published.success) throw new ScaffoldError("WRITE", `Inventory could not be written at ${target}`, {
1849
+ path: target,
1850
+ error: published.error
1851
+ });
1852
+ const text = readFileText(dirname(target), basename(target), MAX_MANIFEST_BYTES);
1853
+ const verified = text === void 0 ? void 0 : parseJSONAs(text, isHostManifest);
1854
+ if (verified === void 0 || verified.digest !== computeManifestDigest(verified.entries, verified.roots)) throw new ScaffoldError("TARGET", `Inventory does not read back at ${target}`, { path: target });
1855
+ return verified;
1856
+ });
1857
+ const removed = attempt(() => rmSync(temporary.value, {
1858
+ recursive: true,
1859
+ force: true
1860
+ }));
1861
+ if (!removed.success) throw new ScaffoldError("WRITE", `Inventory staging root could not be removed`, {
1862
+ path: temporary.value,
1863
+ error: removed.error
1864
+ });
1865
+ if (!staged.success) throw staged.error;
1866
+ return staged.value;
1867
+ }
1868
+ /**
1513
1869
  * Capture one directory's physical identity.
1514
1870
  *
1515
1871
  * @param path - The resolved directory path to capture.
@@ -2195,6 +2551,12 @@ var WriteTransaction = class {
2195
2551
  * case, so a manifest naming `agents.md` for a stored `AGENTS.md` is refused on
2196
2552
  * a case-insensitive filesystem rather than silently resolved.
2197
2553
  *
2554
+ * That host arrives as a directory path or as a whole {@link Host} value, and
2555
+ * every verb reads one immutable host either way. A value is owned, verified
2556
+ * against its own membership and digests, and read in memory; a write fills it
2557
+ * into a private root and copies from there, so the executable declarations the
2558
+ * release fixed reach the target from either representation.
2559
+ *
2198
2560
  * What a mutation guarantees is exactly what {@link WriteTransaction}
2199
2561
  * guarantees, and no more: a caught failure part way through a commit rolls the
2200
2562
  * whole commit back, no destination ever receives half-written bytes, and a
@@ -2220,25 +2582,34 @@ var Materializer = class Materializer {
2220
2582
  static #opening = "<!-- orkestrel:catalog -->";
2221
2583
  static #closing = "<!-- /orkestrel:catalog -->";
2222
2584
  #emitter;
2223
- #host;
2585
+ #root;
2586
+ #value;
2224
2587
  #manifest;
2225
2588
  #entries;
2226
2589
  #destroyed = false;
2227
2590
  /**
2228
2591
  * Construct a materializer over one vendored host root.
2229
2592
  *
2230
- * @param options - The vendored host root, the initial listeners, and the
2231
- * listener-error handler.
2593
+ * @param options - The vendored host, in either representation, the initial
2594
+ * listeners, and the listener-error handler.
2232
2595
  * @throws {@link ScaffoldError} coded `INVALID` when `options` is present but
2233
2596
  * is not an option bag this materializer accepts, and `TARGET` when the host
2234
- * carries a manifest that cannot be read or does not match what it stores.
2597
+ * carries a manifest that cannot be read, does not match what it stores, or
2598
+ * is a value that does not agree with the bytes beside it.
2235
2599
  *
2236
2600
  * @remarks
2237
- * `host` defaults to this package's own vendored root, resolved from this
2238
- * module's own location so it never depends on the caller's working
2239
- * directory. A host carrying no manifest is read as a raw checkout and every
2601
+ * A `host` path defaults to this package's own vendored root, resolved from
2602
+ * this module's own location so it never depends on the caller's working
2603
+ * directory. A root carrying no manifest is read as a raw checkout and every
2240
2604
  * artifact maps onto it one to one.
2241
2605
  *
2606
+ * A `host` value is owned before it is read and then held immutable, so the
2607
+ * bytes this check measured are the bytes every later read returns. It is
2608
+ * verified the way a root is, against the same membership law: the manifest
2609
+ * digest must cover the membership beside it, no two entries may claim one
2610
+ * destination, and the fill must carry exactly one hashing byte string per
2611
+ * declared entry.
2612
+ *
2242
2613
  * The host is read here rather than on first use, so a broken vendored root
2243
2614
  * fails at construction where the caller can still act on it, and so nothing
2244
2615
  * has to carry a second flag recording whether the read has happened yet.
@@ -2249,15 +2620,27 @@ var Materializer = class Materializer {
2249
2620
  ...options?.on === void 0 ? {} : { on: options.on },
2250
2621
  ...options?.error === void 0 ? {} : { error: options.error }
2251
2622
  });
2252
- this.#host = options?.host ?? resolve(dirname(fileURLToPath(import.meta.url)), "../../host");
2253
- const read = attempt(() => readHostManifest(this.#host));
2254
- if (!read.success) throw this.#error("TARGET", "The vendored host carries a manifest that cannot be read.", {
2255
- host: this.#host,
2256
- error: read.error
2257
- });
2258
- this.#manifest = read.value;
2259
- this.#entries = new Map((read.value?.entries ?? []).map((entry) => [entry.destination, entry]));
2260
- if (read.value !== void 0) this.#reconcile(read.value);
2623
+ const supplied = options?.host ?? readHostFloor();
2624
+ if (supplied !== void 0 && !isFilesystemPath(supplied)) {
2625
+ const value = this.#own(supplied);
2626
+ this.#value = value;
2627
+ this.#root = void 0;
2628
+ this.#manifest = value.manifest;
2629
+ this.#entries = new Map(value.manifest.entries.map((entry) => [entry.destination, entry]));
2630
+ this.#verify(value);
2631
+ } else {
2632
+ const root = supplied;
2633
+ this.#value = void 0;
2634
+ this.#root = root;
2635
+ const read = attempt(() => readHostManifest(root));
2636
+ if (!read.success) throw this.#error("TARGET", "The vendored host carries a manifest that cannot be read.", {
2637
+ host: root,
2638
+ error: read.error
2639
+ });
2640
+ this.#manifest = read.value;
2641
+ this.#entries = new Map((read.value?.entries ?? []).map((entry) => [entry.destination, entry]));
2642
+ if (read.value !== void 0) this.#reconcile(read.value, root);
2643
+ }
2261
2644
  }
2262
2645
  /** The materializer's observation channel. */
2263
2646
  get emitter() {
@@ -2425,26 +2808,32 @@ var Materializer = class Materializer {
2425
2808
  return this.#rewrite(directory, CATALOG_AGENT_PATH, MAX_ARTIFACT_BYTES, this.#recatalog(accepted));
2426
2809
  }
2427
2810
  /**
2428
- * Rewrite the declared dependency ranges the caller names in the target's manifest.
2811
+ * Rewrite the manifest regions the caller names in the target's manifest.
2429
2812
  *
2430
- * @param dependencies - The names and ranges the manifest must declare.
2813
+ * @param regions - The dependency ranges and script values the manifest must declare.
2431
2814
  * @param target - The directory to write into.
2432
- * @returns The manifest path, written when a declared range moved and skipped otherwise.
2815
+ * @returns The manifest path, written when a named region moved and skipped otherwise.
2433
2816
  * @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
2434
2817
  * exact shape or names a package the manifest does not declare, `TARGET` when
2435
2818
  * the manifest is unreadable, `WRITE` when the write cannot be staged or
2436
2819
  * committed, and `DESTROYED` after teardown.
2437
2820
  *
2438
2821
  * @remarks
2439
- * No other part of the manifest is read back out or rewritten, so a consumer's
2440
- * own description, keywords, scripts, and formatting survive the call. Only a
2441
- * range already declared is rewritten: inserting a package would mean
2442
- * re-serializing the whole manifest, which is exactly the edit this verb
2443
- * promises not to make, so an undeclared name is refused by name instead.
2822
+ * No other part of the manifest is read back out or rewritten. The method
2823
+ * never reads or writes `peerDependencies` or `peerDependenciesMeta`. Only a
2824
+ * range already declared in its named writable section is rewritten, so an
2825
+ * undeclared name is refused instead of inserted.
2826
+ *
2827
+ * The regions refuse differently because their targets differ. A range
2828
+ * the manifest does not declare is the caller's mistake and throws. A script
2829
+ * holding a value the region does not accept is the workspace author's own
2830
+ * chain, so the script region is skipped without a byte moving and the range
2831
+ * region is still written. The advisory channel reports what the maintainer
2832
+ * must paste.
2444
2833
  */
2445
- declare(dependencies, target) {
2834
+ declare(regions, target) {
2446
2835
  this.#assertAlive();
2447
- const accepted = this.#accept(dependencies, isDependencies, "dependencies");
2836
+ const accepted = this.#accept(regions, isManifestRegionSet, "regions");
2448
2837
  const directory = this.#accept(target, isFilesystemPath, "target");
2449
2838
  return this.#rewrite(directory, "package.json", MAX_MANIFEST_BYTES, this.#redeclare(accepted));
2450
2839
  }
@@ -2453,7 +2842,7 @@ var Materializer = class Materializer {
2453
2842
  *
2454
2843
  * @param plan - The compiled plan that decides which paths are foreign.
2455
2844
  * @param audit - The preview returned by this materializer's `audit` method; it must agree with the candidate set this call re-derives.
2456
- * @param repository - The target's git state; only a tracked path is ever deleted.
2845
+ * @param worktree - The target's git state; only a tracked path is ever deleted.
2457
2846
  * @param target - The directory to delete from.
2458
2847
  * @returns The paths removed.
2459
2848
  * @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
@@ -2474,11 +2863,11 @@ var Materializer = class Materializer {
2474
2863
  * any foreign finding, including one the deletion itself would skip, because a
2475
2864
  * preview stale anywhere is stale evidence.
2476
2865
  */
2477
- remove(plan, audit, repository, target) {
2866
+ remove(plan, audit, worktree, target) {
2478
2867
  this.#assertAlive();
2479
2868
  const accepted = this.#accept(plan, isPlan, "plan");
2480
2869
  const preview = this.#accept(audit, isAudit, "audit");
2481
- const state = this.#accept(repository, isRepository, "repository");
2870
+ const state = this.#accept(worktree, isWorktree, "worktree");
2482
2871
  const directory = this.#accept(target, isFilesystemPath, "target");
2483
2872
  if (state.dirty.length > 0) throw this.#error("TARGET", `The target at ${directory} carries uncommitted changes.`, {
2484
2873
  target: directory,
@@ -2521,20 +2910,41 @@ var Materializer = class Materializer {
2521
2910
  this.#emitter.emit("destroy");
2522
2911
  this.#emitter.destroy();
2523
2912
  }
2524
- #reconcile(manifest) {
2525
- const walked = attempt(() => listFiles(this.#host));
2913
+ #own(host) {
2914
+ const owned = cloneValue(host);
2915
+ if (isHost(owned)) return owned;
2916
+ throw this.#error("INVALID", "The host argument is not the exact shape this materializer accepts.", { field: "host" });
2917
+ }
2918
+ #verify(host) {
2919
+ const { entries, roots } = host.manifest;
2920
+ if (host.manifest.digest !== computeManifestDigest(entries, roots)) throw this.#error("TARGET", "The vendored host manifest does not cover the membership beside it.");
2921
+ if (this.#entries.size !== entries.length) throw this.#error("TARGET", "The vendored host manifest maps two files to one destination.");
2922
+ if (new Set(entries.map((entry) => entry.storage)).size !== entries.length) throw this.#error("TARGET", "The vendored host manifest maps two destinations to one file.");
2923
+ const held = Object.keys(host.bytes);
2924
+ if (held.length !== entries.length) throw this.#error("TARGET", "The vendored host does not carry what its manifest declares.", {
2925
+ held: held.length,
2926
+ declared: entries.length
2927
+ });
2928
+ for (const entry of entries) {
2929
+ const hex = host.bytes[entry.destination];
2930
+ if (hex === void 0) throw this.#error("TARGET", `The vendored host carries no bytes for ${entry.destination}.`, { destination: entry.destination });
2931
+ if (hexToDigest(hex) !== entry.digest) throw this.#error("TARGET", `The vendored host carries bytes for ${entry.destination} that miss its digest.`, { destination: entry.destination });
2932
+ }
2933
+ }
2934
+ #reconcile(manifest, root) {
2935
+ const walked = attempt(() => listFiles(root));
2526
2936
  if (!walked.success) throw this.#error("TARGET", "The vendored host cannot be inventoried.", {
2527
- host: this.#host,
2937
+ host: root,
2528
2938
  error: walked.error
2529
2939
  });
2530
2940
  const declared = [...manifest.entries.map((entry) => entry.storage), "manifest.json"].sort();
2531
2941
  const stored = walked.value;
2532
2942
  if (stored.length !== declared.length || stored.some((name, index) => name !== declared[index])) throw this.#error("TARGET", "The vendored host does not store what its manifest declares.", {
2533
- host: this.#host,
2943
+ host: root,
2534
2944
  stored: stored.length,
2535
2945
  declared: declared.length
2536
2946
  });
2537
- if (this.#entries.size !== manifest.entries.length) throw this.#error("TARGET", "The vendored host manifest maps two files to one destination.", { host: this.#host });
2947
+ if (this.#entries.size !== manifest.entries.length) throw this.#error("TARGET", "The vendored host manifest maps two files to one destination.", { host: root });
2538
2948
  }
2539
2949
  #hydrate(plan) {
2540
2950
  const artifacts = [];
@@ -2550,7 +2960,7 @@ var Materializer = class Materializer {
2550
2960
  artifacts.push(...expanded);
2551
2961
  }
2552
2962
  if (remaining < 0) throw this.#error("TARGET", "The hydrated plan retains more bytes than one plan may.", {
2553
- host: this.#host,
2963
+ host: this.#root,
2554
2964
  limit: MAX_TOTAL_ARTIFACT_BYTES
2555
2965
  });
2556
2966
  return {
@@ -2584,7 +2994,8 @@ var Materializer = class Materializer {
2584
2994
  continue;
2585
2995
  }
2586
2996
  if (this.#manifest !== void 0) continue;
2587
- const directory = resolveContainedPath(this.#host, source);
2997
+ const root = this.#root;
2998
+ const directory = root === void 0 ? void 0 : resolveContainedPath(root, source);
2588
2999
  if (directory !== void 0 && isPhysicalDirectory(directory)) roots.add(artifact.path);
2589
3000
  }
2590
3001
  return [...roots];
@@ -2596,7 +3007,7 @@ var Materializer = class Materializer {
2596
3007
  const matched = manifest.entries.filter((entry) => entry.destination === source || entry.destination.startsWith(`${source}/`));
2597
3008
  const rooted = manifest.roots.some((root) => root === source || root.startsWith(`${source}/`));
2598
3009
  if (matched.length === 0 && !rooted) throw this.#error("TARGET", `The vendored host does not carry ${source}.`, {
2599
- host: this.#host,
3010
+ host: this.#root,
2600
3011
  source
2601
3012
  });
2602
3013
  const expanded = [];
@@ -2607,29 +3018,30 @@ var Materializer = class Materializer {
2607
3018
  expanded.push(this.#presence(artifact, path, entry.destination));
2608
3019
  continue;
2609
3020
  }
2610
- if (this.#deferred(path)) {
3021
+ if (isDeferredPath(path)) {
2611
3022
  expanded.push(this.#presence(artifact, path, entry.destination));
2612
3023
  continue;
2613
3024
  }
2614
- const hex = this.#read(entry.storage, budget);
3025
+ const hex = this.#read(entry, budget);
2615
3026
  budget -= hex.length / 2;
2616
3027
  expanded.push(this.#hydrated(artifact, path, entry.destination, hex));
2617
3028
  }
2618
3029
  return expanded;
2619
3030
  }
2620
3031
  #expandRaw(artifact, source, remaining) {
2621
- const full = resolveContainedPath(this.#host, source);
3032
+ const root = this.#root;
3033
+ const full = root === void 0 ? void 0 : resolveContainedPath(root, source);
2622
3034
  if (full === void 0) throw this.#error("TARGET", `The host source at ${source} leaves its root.`, {
2623
- host: this.#host,
3035
+ host: this.#root,
2624
3036
  source
2625
3037
  });
2626
3038
  if (isPhysicalFile(full)) {
2627
3039
  if (WORKSPACE_OWNED_PATHS.includes(artifact.path)) return [this.#presence(artifact, artifact.path, source)];
2628
- if (this.#deferred(artifact.path)) return [this.#presence(artifact, artifact.path, source)];
2629
- return [this.#hydrated(artifact, artifact.path, source, this.#read(source, remaining))];
3040
+ if (isDeferredPath(artifact.path)) return [this.#presence(artifact, artifact.path, source)];
3041
+ return [this.#hydrated(artifact, artifact.path, source, this.#readRoot(source, remaining))];
2630
3042
  }
2631
3043
  if (!isPhysicalDirectory(full)) throw this.#error("TARGET", `The host source at ${source} is not a readable file.`, {
2632
- host: this.#host,
3044
+ host: this.#root,
2633
3045
  source
2634
3046
  });
2635
3047
  const expanded = [];
@@ -2641,11 +3053,11 @@ var Materializer = class Materializer {
2641
3053
  expanded.push(this.#presence(artifact, path, destination));
2642
3054
  continue;
2643
3055
  }
2644
- if (this.#deferred(path)) {
3056
+ if (isDeferredPath(path)) {
2645
3057
  expanded.push(this.#presence(artifact, path, destination));
2646
3058
  continue;
2647
3059
  }
2648
- const hex = this.#read(destination, budget);
3060
+ const hex = this.#readRoot(destination, budget);
2649
3061
  budget -= hex.length / 2;
2650
3062
  expanded.push(this.#hydrated(artifact, path, destination, hex));
2651
3063
  }
@@ -2669,15 +3081,12 @@ var Materializer = class Materializer {
2669
3081
  const source = artifact.source ?? artifact.path;
2670
3082
  if (destination === source) return artifact.path;
2671
3083
  if (!destination.startsWith(`${source}/`)) throw this.#error("TARGET", `The vendored destination ${destination} is outside ${source}.`, {
2672
- host: this.#host,
3084
+ host: this.#root,
2673
3085
  source,
2674
3086
  destination
2675
3087
  });
2676
3088
  return `${artifact.path}/${destination.slice(source.length + 1)}`;
2677
3089
  }
2678
- #deferred(path) {
2679
- return path === CATALOG_AGENT_PATH || path.startsWith("guides/") && path.endsWith(".md");
2680
- }
2681
3090
  #presence(artifact, path, destination) {
2682
3091
  return {
2683
3092
  path,
@@ -2699,10 +3108,20 @@ var Materializer = class Materializer {
2699
3108
  hex
2700
3109
  };
2701
3110
  }
2702
- #read(storage, budget) {
2703
- const hex = readFileHex(this.#host, storage, Math.max(0, Math.min(MAX_ARTIFACT_BYTES, budget)));
3111
+ #read(entry, budget) {
3112
+ const held = this.#value?.bytes[entry.destination];
3113
+ if (held === void 0) return this.#readRoot(entry.storage, budget);
3114
+ if (held.length / 2 > Math.max(0, Math.min(MAX_ARTIFACT_BYTES, budget))) throw this.#error("TARGET", `The vendored host cannot be read at ${entry.destination}.`, {
3115
+ destination: entry.destination,
3116
+ limit: MAX_ARTIFACT_BYTES
3117
+ });
3118
+ return held;
3119
+ }
3120
+ #readRoot(storage, budget) {
3121
+ const root = this.#root;
3122
+ const hex = root === void 0 ? void 0 : readFileHex(root, storage, Math.max(0, Math.min(MAX_ARTIFACT_BYTES, budget)));
2704
3123
  if (hex === void 0) throw this.#error("TARGET", `The vendored host cannot be read at ${storage}.`, {
2705
- host: this.#host,
3124
+ host: this.#root,
2706
3125
  storage
2707
3126
  });
2708
3127
  return hex;
@@ -2808,20 +3227,49 @@ var Materializer = class Materializer {
2808
3227
  skipped,
2809
3228
  removed: []
2810
3229
  });
2811
- const transaction = this.#open(target, paths, preconditions);
2812
- const staged = attempt(() => {
2813
- for (const artifact of writes) if (artifact.origin === "host") this.#copy(transaction, artifact);
2814
- else transaction.write(artifact.path, artifact.content);
2815
- for (const path of directories) transaction.establish(path);
2816
- });
2817
- const written = this.#close(transaction, staged, target);
2818
- for (const path of written) this.#emitter.emit("write", path);
2819
- return this.#finish({
2820
- target,
2821
- written,
2822
- skipped,
2823
- removed: []
3230
+ const filled = this.#fill(writes);
3231
+ try {
3232
+ const transaction = this.#open(target, paths, preconditions);
3233
+ const staged = attempt(() => {
3234
+ for (const artifact of writes) if (artifact.origin === "host") this.#copy(transaction, artifact, filled ?? this.#root);
3235
+ else transaction.write(artifact.path, artifact.content);
3236
+ for (const path of directories) transaction.establish(path);
3237
+ });
3238
+ const written = this.#close(transaction, staged, target);
3239
+ for (const path of written) this.#emitter.emit("write", path);
3240
+ return this.#finish({
3241
+ target,
3242
+ written,
3243
+ skipped,
3244
+ removed: []
3245
+ });
3246
+ } finally {
3247
+ if (filled !== void 0) rmSync(filled, {
3248
+ recursive: true,
3249
+ force: true
3250
+ });
3251
+ }
3252
+ }
3253
+ #fill(writes) {
3254
+ const value = this.#value;
3255
+ if (value === void 0) return void 0;
3256
+ const destinations = /* @__PURE__ */ new Set();
3257
+ for (const artifact of writes) {
3258
+ if (artifact.origin !== "host") continue;
3259
+ destinations.add(artifact.source ?? artifact.path);
3260
+ }
3261
+ if (destinations.size === 0) return void 0;
3262
+ const opened = attempt(() => mkdtempSync(join(tmpdir(), "orkestrel-scaffold-fill-")));
3263
+ if (!opened.success) throw this.#error("WRITE", "The supplied host could not be filled into a private root.", { error: opened.error });
3264
+ const root = opened.value;
3265
+ const stored = attempt(() => stageBytes(value, root, [...destinations]));
3266
+ if (stored.success) return root;
3267
+ rmSync(root, {
3268
+ recursive: true,
3269
+ force: true
2824
3270
  });
3271
+ this.#emitter.emit("error", stored.error);
3272
+ throw stored.error;
2825
3273
  }
2826
3274
  #purge(target, removals, skipped, preconditions) {
2827
3275
  if (removals.length === 0) return this.#finish({
@@ -2865,13 +3313,13 @@ var Materializer = class Materializer {
2865
3313
  this.#emitter.emit("error", committed.error);
2866
3314
  throw committed.error;
2867
3315
  }
2868
- #copy(transaction, artifact) {
3316
+ #copy(transaction, artifact, root) {
2869
3317
  const destination = artifact.source ?? artifact.path;
2870
3318
  const entry = this.#entries.get(destination);
2871
3319
  const storage = entry === void 0 ? destination : entry.storage;
2872
- const source = resolveContainedPath(this.#host, storage);
3320
+ const source = root === void 0 ? void 0 : resolveContainedPath(root, storage);
2873
3321
  if (source === void 0) throw this.#error("TARGET", `The vendored source at ${storage} leaves its root.`, {
2874
- host: this.#host,
3322
+ host: root,
2875
3323
  storage
2876
3324
  });
2877
3325
  transaction.copy(artifact.path, source, entry?.executable === true);
@@ -2922,12 +3370,14 @@ var Materializer = class Materializer {
2922
3370
  #cell(note) {
2923
3371
  return note.replaceAll("|", "\\|").replaceAll(/\s+/gu, " ").trim();
2924
3372
  }
2925
- #redeclare(dependencies) {
3373
+ #redeclare(regions) {
2926
3374
  return (text) => {
2927
- const manifest = replaceManifestRanges(text, dependencies);
2928
- if (manifest !== void 0) return manifest;
2929
- const missing = dependencies.find((dependency) => !text.includes(JSON.stringify(dependency.name)));
2930
- throw this.#error("INVALID", `The manifest does not declare ${missing?.name ?? "a requested package"}, so its range cannot be rewritten.`, missing === void 0 ? void 0 : { name: missing.name });
3375
+ const manifest = replaceManifestRanges(text, regions.pins);
3376
+ if (manifest === void 0) {
3377
+ const missing = [...regions.pins.runtime, ...regions.pins.development].find((dependency) => !text.includes(JSON.stringify(dependency.name)));
3378
+ throw this.#error("INVALID", `The manifest does not declare ${missing?.name ?? "a requested package"}, so its range cannot be rewritten.`, missing === void 0 ? void 0 : { name: missing.name });
3379
+ }
3380
+ return replaceManifestScripts(manifest, regions.scripts) ?? manifest;
2931
3381
  };
2932
3382
  }
2933
3383
  #finish(result) {
@@ -2968,6 +3418,11 @@ var Materializer = class Materializer {
2968
3418
  * there is no fleet to report, so an unreachable or malformed list is a coded
2969
3419
  * `FETCH` failure.
2970
3420
  *
3421
+ * The vendored-file inventory is the other read a whole call rests on, and it
3422
+ * fails the other way: it fails every row of its call rather than throwing, so
3423
+ * the caller receives one whole dead answer it can replace with one whole
3424
+ * baseline instead of a mixture it would have to reconcile.
3425
+ *
2971
3426
  * Requests are unauthenticated because every fleet repository is public, and
2972
3427
  * they follow no redirect, so a misconfigured or hostile endpoint cannot move a
2973
3428
  * read to another host. Each one is bounded by its endpoint's timeout and by the
@@ -2988,19 +3443,20 @@ var Materializer = class Materializer {
2988
3443
  * ```
2989
3444
  */
2990
3445
  var Upstream = class Upstream {
2991
- static #defaultGuide = "https://raw.githubusercontent.com";
3446
+ static #defaultRepository = "https://raw.githubusercontent.com";
2992
3447
  static #defaultRegistry = "https://registry.npmjs.org";
2993
3448
  static #defaultBranch = "main";
2994
3449
  static #defaultTimeout = 1e4;
2995
3450
  static #defaultConcurrency = 6;
2996
3451
  static #defaultRetries = 0;
2997
3452
  static #scope = "orkestrel";
3453
+ static #vendor = "scaffold";
2998
3454
  static #unreadable = "the answer carries no readable latest version";
2999
3455
  static #packument = "application/vnd.npm.install-v1+json";
3000
3456
  #emitter;
3001
- #guideBase;
3002
- #guideBranch;
3003
- #guideTimeout;
3457
+ #repositoryBase;
3458
+ #repositoryBranch;
3459
+ #repositoryTimeout;
3004
3460
  #registryBase;
3005
3461
  #registryTimeout;
3006
3462
  #concurrency;
@@ -3010,7 +3466,7 @@ var Upstream = class Upstream {
3010
3466
  #controller = new AbortController();
3011
3467
  #destroyed = false;
3012
3468
  /**
3013
- * Construct a reader over one guide host and one registry.
3469
+ * Construct a reader over one raw content host and one registry.
3014
3470
  *
3015
3471
  * @param options - The endpoints, the request bounds, the initial
3016
3472
  * listeners, and the listener-error handler.
@@ -3034,9 +3490,9 @@ var Upstream = class Upstream {
3034
3490
  ...options?.on === void 0 ? {} : { on: options.on },
3035
3491
  ...options?.error === void 0 ? {} : { error: options.error }
3036
3492
  });
3037
- this.#guideBase = this.#endpoint(options?.guides?.base ?? Upstream.#defaultGuide, "guides");
3038
- this.#guideBranch = options?.guides?.branch ?? Upstream.#defaultBranch;
3039
- this.#guideTimeout = options?.guides?.timeout ?? Upstream.#defaultTimeout;
3493
+ this.#repositoryBase = this.#endpoint(options?.repository?.base ?? Upstream.#defaultRepository, "repository");
3494
+ this.#repositoryBranch = options?.repository?.branch ?? Upstream.#defaultBranch;
3495
+ this.#repositoryTimeout = options?.repository?.timeout ?? Upstream.#defaultTimeout;
3040
3496
  this.#registryBase = this.#endpoint(options?.registry?.base ?? Upstream.#defaultRegistry, "registry");
3041
3497
  this.#registryTimeout = options?.registry?.timeout ?? Upstream.#defaultTimeout;
3042
3498
  this.#concurrency = options?.concurrency ?? Upstream.#defaultConcurrency;
@@ -3114,6 +3570,54 @@ var Upstream = class Upstream {
3114
3570
  return this.#gather(accepted, (name) => this.#mirror(name, observed, allowance));
3115
3571
  }
3116
3572
  /**
3573
+ * Read each named vendored file from the repository, beside the target bytes it answers for.
3574
+ *
3575
+ * @param paths - The target-relative vendored paths to read.
3576
+ * @param current - The target files as exact bytes, keyed by the same paths.
3577
+ * @returns One file verdict per path, in input order.
3578
+ * @throws {@link ScaffoldError} coded `INVALID` when `paths` is not a bounded
3579
+ * list of target-relative paths or `current` is not a snapshot, and
3580
+ * `DESTROYED` when the reader is torn down before or during the call.
3581
+ *
3582
+ * @remarks
3583
+ * The committed inventory is read once per call and decides every row, so a
3584
+ * path whose declared digest already matches the target's own bytes is `found`
3585
+ * without a request and the call spends nothing on it. An inventory that
3586
+ * produces no answer fails every row of the call rather than leaving some rows
3587
+ * live and some dead, which is what leaves the caller one whole baseline to
3588
+ * fall back to. A path the inventory does not name is `missing`.
3589
+ *
3590
+ * A fetched response's decoded content is verified against the digest the
3591
+ * inventory declares for that path, before any character decoding. Transport
3592
+ * encoding is transparent and does not enter the comparison, so content that
3593
+ * does not hash to the inventory's claim fails its row rather than reaching a
3594
+ * write. That is integrity against a single committed baseline, not
3595
+ * authenticity: it detects truncated, substituted, or stale content, and it
3596
+ * says nothing about who published the inventory.
3597
+ *
3598
+ * A guide mirror is never answered here whatever the caller asks for and
3599
+ * whatever the target holds, because those bytes belong to `fetch` and to the
3600
+ * mirror verb that writes them.
3601
+ *
3602
+ * @example
3603
+ * ```ts
3604
+ * import { Upstream } from '@orkestrel/scaffold/server'
3605
+ *
3606
+ * const upstream = new Upstream()
3607
+ * await upstream.read(['AGENTS.md'], { 'AGENTS.md': '2320416745' })
3608
+ * upstream.destroy()
3609
+ * ```
3610
+ */
3611
+ async read(paths, current) {
3612
+ this.#assertAlive();
3613
+ const accepted = this.#accept(paths, isPaths, "paths");
3614
+ const observed = this.#accept(current, isSnapshot, "current");
3615
+ if (accepted.length === 0) return [];
3616
+ const allowance = { remaining: this.#budget };
3617
+ const inventory = await this.#inventory(allowance);
3618
+ return this.#gather(accepted, (path) => this.#file(path, inventory, observed, allowance));
3619
+ }
3620
+ /**
3117
3621
  * Catalog the published fleet from the registry's organization package list.
3118
3622
  *
3119
3623
  * @returns One row per published package, sorted by name.
@@ -3190,14 +3694,14 @@ var Upstream = class Upstream {
3190
3694
  return parsed.href.replace(/\/+$/u, "");
3191
3695
  }
3192
3696
  async #release(dependency, allowance) {
3193
- const outcome = await this.#read(this.#registryURL(dependency.name), this.#registryTimeout, allowance, Upstream.#packument);
3697
+ const outcome = await this.#readWithRetries(this.#registryURL(dependency.name), this.#registryTimeout, allowance, Upstream.#packument);
3194
3698
  const latest = outcome.lookup === "found" ? this.#releaseVersion(outcome.content, dependency.range) : void 0;
3195
3699
  const tagged = outcome.lookup === "found" ? this.#latest(outcome.content) : void 0;
3196
3700
  const major = tagged === void 0 ? void 0 : extractVersion(tagged)?.[0];
3197
3701
  const release = latest === void 0 ? {
3198
3702
  name: dependency.name,
3199
3703
  range: dependency.range,
3200
- lookup: outcome.lookup === "missing" ? "missing" : "failed",
3704
+ lookup: outcome.lookup === "found" ? "unmatched" : outcome.lookup,
3201
3705
  note: outcome.lookup === "found" ? Upstream.#unreadable : outcome.note,
3202
3706
  ...major === void 0 ? {} : { major }
3203
3707
  } : {
@@ -3212,7 +3716,7 @@ var Upstream = class Upstream {
3212
3716
  }
3213
3717
  async #mirror(name, current, allowance) {
3214
3718
  const path = nameToGuide(name);
3215
- const outcome = await this.#read(this.#guideURL(name), this.#guideTimeout, allowance);
3719
+ const outcome = await this.#readWithRetries(this.#guideURL(name), this.#repositoryTimeout, allowance);
3216
3720
  const observed = current[path];
3217
3721
  const mirror = outcome.lookup === "found" ? {
3218
3722
  name,
@@ -3230,8 +3734,102 @@ var Upstream = class Upstream {
3230
3734
  this.#emitter.emit("mirror", mirror);
3231
3735
  return mirror;
3232
3736
  }
3737
+ async #file(path, inventory, current, allowance) {
3738
+ const file = await this.#answer(path, inventory, current[path], allowance);
3739
+ this.#emitter.emit("file", file);
3740
+ return file;
3741
+ }
3742
+ async #answer(path, inventory, observed, allowance) {
3743
+ const carried = observed === void 0 ? {} : { observed };
3744
+ if (inventory.lookup !== "found") return {
3745
+ path,
3746
+ lookup: inventory.lookup,
3747
+ note: inventory.note,
3748
+ ...carried
3749
+ };
3750
+ if (inferGroup(path) === "guides") return {
3751
+ path,
3752
+ lookup: "missing",
3753
+ note: `${path} is a guide mirror the fleet serves`,
3754
+ ...carried
3755
+ };
3756
+ if (inventory.duplicates.has(path)) return {
3757
+ path,
3758
+ lookup: "failed",
3759
+ note: `the inventory names ${path} more than once`,
3760
+ ...carried
3761
+ };
3762
+ const digest = inventory.digests.get(path);
3763
+ if (digest === void 0) return {
3764
+ path,
3765
+ lookup: "missing",
3766
+ note: `the inventory does not name ${path}`,
3767
+ ...carried
3768
+ };
3769
+ if (observed !== void 0 && hexToDigest(observed) === digest) return {
3770
+ path,
3771
+ lookup: "found",
3772
+ hex: observed,
3773
+ ...carried
3774
+ };
3775
+ const outcome = await this.#readWithRetries(this.#vendorURL(path), this.#repositoryTimeout, allowance, void 0, true);
3776
+ if (outcome.lookup !== "found") return {
3777
+ path,
3778
+ lookup: outcome.lookup,
3779
+ note: outcome.note,
3780
+ ...carried
3781
+ };
3782
+ if (hexToDigest(outcome.hex) !== digest) return {
3783
+ path,
3784
+ lookup: "failed",
3785
+ note: `the bytes served for ${path} do not match the digest the inventory declares`,
3786
+ ...carried
3787
+ };
3788
+ return {
3789
+ path,
3790
+ lookup: "found",
3791
+ hex: outcome.hex,
3792
+ ...carried
3793
+ };
3794
+ }
3795
+ async #inventory(allowance) {
3796
+ const url = this.#vendorURL(HOST_INVENTORY_PATH);
3797
+ const empty = {
3798
+ digests: /* @__PURE__ */ new Map(),
3799
+ duplicates: /* @__PURE__ */ new Set()
3800
+ };
3801
+ const outcome = await this.#readWithRetries(url, this.#repositoryTimeout, allowance);
3802
+ if (outcome.lookup !== "found") return {
3803
+ ...empty,
3804
+ lookup: outcome.lookup,
3805
+ note: outcome.lookup === "missing" ? `the vendored inventory at ${url} is not published there` : `the vendored inventory at ${url} produced no answer: ${outcome.note}`
3806
+ };
3807
+ const manifest = parseJSONAs(outcome.content, isHostManifest);
3808
+ if (manifest === void 0) return {
3809
+ ...empty,
3810
+ lookup: "failed",
3811
+ note: `the vendored inventory at ${url} is not a readable manifest`
3812
+ };
3813
+ if (manifest.digest !== computeManifestDigest(manifest.entries, manifest.roots)) return {
3814
+ ...empty,
3815
+ lookup: "failed",
3816
+ note: `the vendored inventory at ${url} does not match its own membership digest`
3817
+ };
3818
+ const digests = /* @__PURE__ */ new Map();
3819
+ const duplicates = /* @__PURE__ */ new Set();
3820
+ for (const entry of manifest.entries) {
3821
+ if (digests.has(entry.destination)) duplicates.add(entry.destination);
3822
+ digests.set(entry.destination, entry.digest);
3823
+ }
3824
+ return {
3825
+ lookup: "found",
3826
+ digests,
3827
+ duplicates,
3828
+ note: ""
3829
+ };
3830
+ }
3233
3831
  async #entry(name, allowance) {
3234
- const outcome = await this.#read(this.#registryURL(name), this.#registryTimeout, allowance, Upstream.#packument);
3832
+ const outcome = await this.#readWithRetries(this.#registryURL(name), this.#registryTimeout, allowance, Upstream.#packument);
3235
3833
  const version = outcome.lookup === "found" ? this.#latest(outcome.content) : void 0;
3236
3834
  if (version !== void 0) return {
3237
3835
  name,
@@ -3241,7 +3839,7 @@ var Upstream = class Upstream {
3241
3839
  };
3242
3840
  return {
3243
3841
  name,
3244
- lookup: outcome.lookup === "missing" ? "missing" : "failed",
3842
+ lookup: outcome.lookup === "found" ? "unmatched" : outcome.lookup,
3245
3843
  note: outcome.lookup === "found" ? Upstream.#unreadable : outcome.note
3246
3844
  };
3247
3845
  }
@@ -3269,7 +3867,7 @@ var Upstream = class Upstream {
3269
3867
  }
3270
3868
  async #packages(allowance) {
3271
3869
  const url = `${this.#registryBase}/-/org/${Upstream.#scope}/package`;
3272
- const outcome = await this.#read(url, this.#registryTimeout, allowance);
3870
+ const outcome = await this.#readWithRetries(url, this.#registryTimeout, allowance);
3273
3871
  if (outcome.lookup !== "found") throw this.#error("FETCH", `The organization package list at ${url} produced no answer.`, {
3274
3872
  url,
3275
3873
  note: outcome.note
@@ -3315,15 +3913,22 @@ var Upstream = class Upstream {
3315
3913
  return `${this.#registryBase}/${encodeURIComponent(name).replaceAll("%40", "@")}`;
3316
3914
  }
3317
3915
  #guideURL(name) {
3318
- const branch = this.#guideBranch.split("/").map((segment) => encodeURIComponent(segment)).join("/");
3916
+ const branch = this.#encode(this.#repositoryBranch);
3319
3917
  const repository = encodeURIComponent(name.slice(name.lastIndexOf("/") + 1));
3320
- return `${this.#guideBase}/${Upstream.#scope}/${repository}/refs/heads/${branch}/${nameToGuide(name)}`;
3918
+ return `${this.#repositoryBase}/${Upstream.#scope}/${repository}/refs/heads/${branch}/${nameToGuide(name)}`;
3321
3919
  }
3322
- async #read(url, timeout, allowance, accept) {
3920
+ #vendorURL(path) {
3921
+ const branch = this.#encode(this.#repositoryBranch);
3922
+ return `${this.#repositoryBase}/${Upstream.#scope}/${Upstream.#vendor}/refs/heads/${branch}/${this.#encode(path)}`;
3923
+ }
3924
+ #encode(path) {
3925
+ return path.split("/").map((segment) => encodeURIComponent(segment)).join("/");
3926
+ }
3927
+ async #readWithRetries(url, timeout, allowance, accept, binary = false) {
3323
3928
  let note = "";
3324
- for (let attempt = 0; attempt <= this.#retries; attempt += 1) {
3929
+ for (let round = 0; round <= this.#retries; round += 1) {
3325
3930
  this.#assertAlive();
3326
- const outcome = await this.#request(url, timeout, allowance, accept);
3931
+ const outcome = binary ? await this.#request(url, timeout, allowance, accept, true) : await this.#request(url, timeout, allowance, accept);
3327
3932
  if (outcome.lookup !== "failed") return outcome;
3328
3933
  note = outcome.note;
3329
3934
  }
@@ -3331,18 +3936,29 @@ var Upstream = class Upstream {
3331
3936
  url,
3332
3937
  note
3333
3938
  });
3334
- return {
3939
+ return binary ? {
3335
3940
  lookup: "failed",
3336
- content: "",
3941
+ hex: "",
3337
3942
  note
3338
- };
3339
- }
3340
- async #request(url, timeout, allowance, accept) {
3341
- if (allowance.remaining <= 0) return {
3943
+ } : {
3342
3944
  lookup: "failed",
3343
3945
  content: "",
3344
- note: `the call spent its ${String(this.#budget)}-byte allowance`
3946
+ note
3345
3947
  };
3948
+ }
3949
+ async #request(url, timeout, allowance, accept, binary = false) {
3950
+ if (allowance.remaining <= 0) {
3951
+ const note = `the call spent its ${String(this.#budget)}-byte allowance`;
3952
+ return binary ? {
3953
+ lookup: "failed",
3954
+ hex: "",
3955
+ note
3956
+ } : {
3957
+ lookup: "failed",
3958
+ content: "",
3959
+ note
3960
+ };
3961
+ }
3346
3962
  try {
3347
3963
  const response = await fetch(url, {
3348
3964
  signal: AbortSignal.any([this.#controller.signal, AbortSignal.timeout(timeout)]),
@@ -3351,7 +3967,11 @@ var Upstream = class Upstream {
3351
3967
  });
3352
3968
  if (response.status === 404) {
3353
3969
  await response.body?.cancel();
3354
- return {
3970
+ return binary ? {
3971
+ lookup: "missing",
3972
+ hex: "",
3973
+ note: "HTTP 404"
3974
+ } : {
3355
3975
  lookup: "missing",
3356
3976
  content: "",
3357
3977
  note: "HTTP 404"
@@ -3359,39 +3979,61 @@ var Upstream = class Upstream {
3359
3979
  }
3360
3980
  if (response.status >= 300 && response.status < 400) {
3361
3981
  await response.body?.cancel();
3362
- return {
3982
+ const note = `HTTP ${String(response.status)}, and a redirect is never followed`;
3983
+ return binary ? {
3984
+ lookup: "failed",
3985
+ hex: "",
3986
+ note
3987
+ } : {
3363
3988
  lookup: "failed",
3364
3989
  content: "",
3365
- note: `HTTP ${String(response.status)}, and a redirect is never followed`
3990
+ note
3366
3991
  };
3367
3992
  }
3368
3993
  if (!response.ok) {
3369
3994
  await response.body?.cancel();
3370
- return {
3995
+ const note = `HTTP ${String(response.status)}`;
3996
+ return binary ? {
3997
+ lookup: "failed",
3998
+ hex: "",
3999
+ note
4000
+ } : {
3371
4001
  lookup: "failed",
3372
4002
  content: "",
3373
- note: `HTTP ${String(response.status)}`
4003
+ note
3374
4004
  };
3375
4005
  }
3376
- return await this.#body(response, allowance);
4006
+ return binary ? await this.#body(response, allowance, true) : await this.#body(response, allowance);
3377
4007
  } catch (error) {
3378
4008
  this.#assertAlive();
3379
- return {
4009
+ const note = this.#note(error);
4010
+ return binary ? {
4011
+ lookup: "failed",
4012
+ hex: "",
4013
+ note
4014
+ } : {
3380
4015
  lookup: "failed",
3381
4016
  content: "",
3382
- note: this.#note(error)
4017
+ note
3383
4018
  };
3384
4019
  }
3385
4020
  }
3386
- async #body(response, allowance) {
4021
+ async #body(response, allowance, binary = false) {
3387
4022
  const body = response.body;
3388
- if (body === null) return {
3389
- lookup: "failed",
3390
- content: "",
3391
- note: `HTTP ${String(response.status)}, and the answer carries no body`
3392
- };
4023
+ if (body === null) {
4024
+ const note = `HTTP ${String(response.status)}, and the answer carries no body`;
4025
+ return binary ? {
4026
+ lookup: "failed",
4027
+ hex: "",
4028
+ note
4029
+ } : {
4030
+ lookup: "failed",
4031
+ content: "",
4032
+ note
4033
+ };
4034
+ }
3393
4035
  const reader = body.getReader();
3394
- const decoder = new TextDecoder("utf-8", { fatal: true });
4036
+ const decoder = binary ? void 0 : new TextDecoder("utf-8", { fatal: true });
3395
4037
  const chunks = [];
3396
4038
  let total = 0;
3397
4039
  try {
@@ -3402,30 +4044,44 @@ var Upstream = class Upstream {
3402
4044
  allowance.remaining -= chunk.value.byteLength;
3403
4045
  if (total > this.#limit) {
3404
4046
  await reader.cancel();
3405
- return {
4047
+ const note = `the response passed the ${String(this.#limit)}-byte response limit`;
4048
+ return binary ? {
4049
+ lookup: "failed",
4050
+ hex: "",
4051
+ note
4052
+ } : {
3406
4053
  lookup: "failed",
3407
4054
  content: "",
3408
- note: `the response passed the ${String(this.#limit)}-byte response limit`
4055
+ note
3409
4056
  };
3410
4057
  }
3411
4058
  if (allowance.remaining < 0) {
3412
4059
  await reader.cancel();
3413
- return {
4060
+ const note = `the call spent its ${String(this.#budget)}-byte allowance`;
4061
+ return binary ? {
4062
+ lookup: "failed",
4063
+ hex: "",
4064
+ note
4065
+ } : {
3414
4066
  lookup: "failed",
3415
4067
  content: "",
3416
- note: `the call spent its ${String(this.#budget)}-byte allowance`
4068
+ note
3417
4069
  };
3418
4070
  }
3419
- chunks.push(decoder.decode(chunk.value, { stream: true }));
4071
+ chunks.push(decoder === void 0 ? Buffer.from(chunk.value).toString("hex") : decoder.decode(chunk.value, { stream: true }));
3420
4072
  }
3421
- chunks.push(decoder.decode());
4073
+ if (decoder !== void 0) chunks.push(decoder.decode());
3422
4074
  } catch (error) {
3423
4075
  await reader.cancel().catch(() => void 0);
3424
4076
  throw error;
3425
4077
  } finally {
3426
4078
  reader.releaseLock();
3427
4079
  }
3428
- return {
4080
+ return binary ? {
4081
+ lookup: "found",
4082
+ hex: chunks.join(""),
4083
+ note: ""
4084
+ } : {
3429
4085
  lookup: "found",
3430
4086
  content: chunks.join(""),
3431
4087
  note: ""
@@ -3480,6 +4136,6 @@ var Upstream = class Upstream {
3480
4136
  }
3481
4137
  };
3482
4138
  //#endregion
3483
- export { BRANCH_PATTERN, DIGEST_PATTERN, DRIVE_PATTERN, INVALID_SEGMENT_CHARACTER_PATTERN, MANIFEST_NAME, MAX_BRANCH_LENGTH, MAX_ENDPOINT_LENGTH, MAX_INVENTORY_PATHS, MAX_PATH_DEPTH, MAX_PATH_SEGMENT_BYTES, MAX_UPSTREAM_CONCURRENCY, MAX_UPSTREAM_RETRIES, MAX_UPSTREAM_TIMEOUT, Materializer, RESERVED_SEGMENT_PATTERN, Upstream, WriteTransaction, computeDigest, computeFileDigest, computeManifestDigest, isBranch, isCatalogEntries, isDependencies, isDependencyNames, isDigest, isEndpoint, isExactCaseFile, isFilesystemPath, isHostManifest, isInventory, isManifestEntry, isMaterializerHooks, isMaterializerOptions, isMirrors, isPhysicalDirectory, isPhysicalFile, isRepository, isTimeout, isUpstreamHooks, isUpstreamOptions, isVacant, listDirectories, listFiles, matchesAnchor, matchesExecutablePath, matchesExpectation, matchesGitPath, matchesMissingPath, matchesPrecondition, matchesProtectedPath, matchesSensitivePath, pathToStorage, readAnchor, readExpectation, readFileHex, readFileText, readHostManifest, readManifestEntry, readSnapshot, resolveContainedPath, resolveRealPath, stageHost };
4139
+ export { BRANCH_PATTERN, DIGEST_PATTERN, DRIVE_PATTERN, INVALID_SEGMENT_CHARACTER_PATTERN, MANIFEST_NAME, MAX_BRANCH_LENGTH, MAX_ENDPOINT_LENGTH, MAX_INVENTORY_PATHS, MAX_PATH_DEPTH, MAX_PATH_SEGMENT_BYTES, MAX_UPSTREAM_CONCURRENCY, MAX_UPSTREAM_RETRIES, MAX_UPSTREAM_TIMEOUT, Materializer, RESERVED_SEGMENT_PATTERN, Upstream, WriteTransaction, computeDigest, computeFileDigest, computeManifestDigest, filesToHost, hexToDigest, isBranch, isCatalogEntries, isDependencies, isDependencyNames, isDigest, isEndpoint, isExactCaseFile, isFilesystemPath, isHost, isHostManifest, isInventory, isManifestEntry, isManifestRegionSet, isMaterializerHooks, isMaterializerOptions, isMirrors, isPaths, isPhysicalDirectory, isPhysicalFile, isTimeout, isUpstreamHooks, isUpstreamOptions, isVacant, isWorktree, listDirectories, listFiles, matchesAnchor, matchesExecutablePath, matchesExpectation, matchesGitPath, matchesMissingPath, matchesPrecondition, matchesProtectedPath, matchesSensitivePath, pathToStorage, readAnchor, readExpectation, readFileHex, readFileText, readHostFloor, readHostManifest, readManifestEntry, readSnapshot, resolveContainedPath, resolveRealPath, stageBytes, stageHost, stageInventory };
3484
4140
 
3485
4141
  //# sourceMappingURL=index.js.map