@orkestrel/scaffold 0.0.24 → 0.0.26

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.
@@ -289,12 +289,23 @@ export declare function computeManifestDigest(entries: readonly ManifestEntry[],
289
289
  * the byte ceiling. The character ceiling is read first so an oversized string is
290
290
  * refused before it is split.
291
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
+ *
292
301
  * @example
293
302
  * ```ts
294
303
  * import { isFilesystemPath } from '@orkestrel/scaffold/server'
295
304
  *
296
305
  * isFilesystemPath('C:/Users/sample/project') // true
297
306
  * isFilesystemPath('../sibling') // true
307
+ * isFilesystemPath('project/') // true
308
+ * isFilesystemPath('project//src') // false
298
309
  * isFilesystemPath('project/nul') // false
299
310
  * ```
300
311
  */
@@ -560,7 +571,7 @@ export declare function computeManifestDigest(entries: readonly ManifestEntry[],
560
571
  * ```ts
561
572
  * import { listFiles } from '@orkestrel/scaffold/server'
562
573
  *
563
- * listFiles('./dist/host') // ['AGENTS.md', 'claude/rules/names.md', …]
574
+ * listFiles('./dist/host') // ['AGENTS.md', 'CLAUDE.md', 'LICENSE', …]
564
575
  * ```
565
576
  */
566
577
  export declare function listFiles(root: string): readonly string[];
@@ -591,9 +602,13 @@ export declare function computeManifestDigest(entries: readonly ManifestEntry[],
591
602
  * device and inode.
592
603
  *
593
604
  * @remarks
594
- * The check a write repeats between steps. A directory replaced by another
595
- * directory of the same name answers `false` here, which is the case a path
596
- * comparison alone cannot see.
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.
597
612
  *
598
613
  * @example
599
614
  * ```ts
@@ -605,6 +620,31 @@ export declare function computeManifestDigest(entries: readonly ManifestEntry[],
605
620
  */
606
621
  export declare function matchesAnchor(anchor: WriteAnchor): boolean;
607
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
+
608
648
  /**
609
649
  * Test whether a destination still holds what was captured of it.
610
650
  *
@@ -861,10 +901,13 @@ export declare function computeManifestDigest(entries: readonly ManifestEntry[],
861
901
  * The audit is a preview, not an instruction. The plan is hydrated and
862
902
  * compared against the target again here, and the verdicts that produces must
863
903
  * match the ones the audit carried for every path the plan owns; anything else
864
- * means the target moved, and the whole call is refused. A missing destination
865
- * is restored whatever its ownership; a stale one is replaced only where the
866
- * artifact claims its bytes, which is what leaves a presence-owned file a
867
- * consumer has edited exactly as it is.
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.
868
911
  */
869
912
  repair(plan: Plan, audit: Audit, target: string): MaterializeResult;
870
913
  /**
@@ -1018,6 +1061,20 @@ export declare function computeManifestDigest(entries: readonly ManifestEntry[],
1018
1061
  * @param audit - The preview returned by this materializer's `audit` method.
1019
1062
  * @param target - The directory to write into.
1020
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.
1021
1078
  */
1022
1079
  repair(plan: Plan, audit: Audit, target: string): MaterializeResult;
1023
1080
  /**
@@ -1062,6 +1119,14 @@ export declare function computeManifestDigest(entries: readonly ManifestEntry[],
1062
1119
  * unlinked, so a failure part way through restores what it already took.
1063
1120
  * The package's own source and application trees are never candidates,
1064
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.
1065
1130
  */
1066
1131
  remove(audit: Audit, repository: Repository, target: string): MaterializeResult;
1067
1132
  /**
@@ -1175,8 +1240,8 @@ export declare function computeManifestDigest(entries: readonly ManifestEntry[],
1175
1240
  * @remarks
1176
1241
  * Device and inode rather than the path, because the path is the thing that can
1177
1242
  * be swapped underneath a write. An anchor captured before a mutation and
1178
- * checked again after it is what proves the directory written into is the
1179
- * directory that was inspected.
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.
1180
1245
  *
1181
1246
  * @example
1182
1247
  * ```ts
@@ -1307,14 +1372,14 @@ export declare function computeManifestDigest(entries: readonly ManifestEntry[],
1307
1372
  *
1308
1373
  * @remarks
1309
1374
  * The one place the three declared fields are decided together, because they are
1310
- * three readings of one file: {@link pathToStorage} decides where it is stored,
1311
- * the destination is the path it answers for, and the executable bit is read
1312
- * from the source's own mode.
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.
1313
1378
  *
1314
- * That mode is the honest limit of this reading. A Windows host reports no
1315
- * executable bit at all, so a host staged there declares every entry
1316
- * non-executable and a consumer receives scripts without it. Staging on a POSIX
1317
- * host is what carries the bit through.
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.
1318
1383
  *
1319
1384
  * @example
1320
1385
  * ```ts
@@ -1396,20 +1461,31 @@ export declare function computeManifestDigest(entries: readonly ManifestEntry[],
1396
1461
  *
1397
1462
  * @remarks
1398
1463
  * The containment law, and the one door every read in this module goes through.
1399
- * Both sides are resolved through the real filesystem before they are compared,
1400
- * so a link planted inside the root cannot smuggle a destination out of it; the
1401
- * answer is then the lexical join, so the caller operates on the path it named
1402
- * rather than on a resolved form the target may not recognize.
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.
1403
1472
  *
1404
1473
  * Comparison is exact text, which fails closed on a case-insensitive
1405
1474
  * filesystem: a root and a path spelled with different case resolve to
1406
1475
  * different strings there and are refused, never wrongly admitted.
1407
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
+ *
1408
1484
  * @example
1409
1485
  * ```ts
1410
1486
  * import { resolveContainedPath } from '@orkestrel/scaffold/server'
1411
1487
  *
1412
- * resolveContainedPath('/tmp/project', 'guides/router.md') // '/tmp/project/guides/router.md'
1488
+ * resolveContainedPath('/tmp/project', 'guides/router.md')?.endsWith('router.md') // true
1413
1489
  * resolveContainedPath('/tmp/project', '../secrets') // undefined
1414
1490
  * ```
1415
1491
  */
@@ -1419,9 +1495,10 @@ export declare function computeManifestDigest(entries: readonly ManifestEntry[],
1419
1495
  * Resolve a path through the real filesystem, keeping the part that does not exist yet.
1420
1496
  *
1421
1497
  * @param path - The absolute or relative host path to resolve.
1422
- * @returns The path with its existing prefix resolved through every link, or
1423
- * `undefined` when the text is not a host path, no bounded existing ancestor
1424
- * resolves, or an ancestor cannot be read.
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.
1425
1502
  *
1426
1503
  * @remarks
1427
1504
  * A containment decision has to be made about a destination that does not exist
@@ -1431,6 +1508,30 @@ export declare function computeManifestDigest(entries: readonly ManifestEntry[],
1431
1508
  * it. The climb is bounded by the path-depth ceiling, so an adversarial path
1432
1509
  * cannot make it walk indefinitely.
1433
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
+ *
1434
1535
  * @example
1435
1536
  * ```ts
1436
1537
  * import { resolveRealPath } from '@orkestrel/scaffold/server'
@@ -1745,7 +1846,14 @@ export declare function computeManifestDigest(entries: readonly ManifestEntry[],
1745
1846
  readonly error?: EmitterErrorHandler;
1746
1847
  }
1747
1848
 
1748
- /** One physical directory identity captured across a write transaction. */
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
+ */
1749
1857
  export declare interface WriteAnchor {
1750
1858
  readonly path: string;
1751
1859
  readonly device: number;
@@ -1810,6 +1918,12 @@ export declare function computeManifestDigest(entries: readonly ManifestEntry[],
1810
1918
  * - **No partly written destination.** Every file is written whole into the
1811
1919
  * private root and digested there before commit, so a destination never
1812
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.
1813
1927
  * - **No crash atomicity across destinations.** A process killed between two
1814
1928
  * promotions leaves the target holding some new files and some old ones, and
1815
1929
  * leaves the private root behind. Nothing here is a journal, and the private
@@ -289,12 +289,23 @@ export declare function computeManifestDigest(entries: readonly ManifestEntry[],
289
289
  * the byte ceiling. The character ceiling is read first so an oversized string is
290
290
  * refused before it is split.
291
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
+ *
292
301
  * @example
293
302
  * ```ts
294
303
  * import { isFilesystemPath } from '@orkestrel/scaffold/server'
295
304
  *
296
305
  * isFilesystemPath('C:/Users/sample/project') // true
297
306
  * isFilesystemPath('../sibling') // true
307
+ * isFilesystemPath('project/') // true
308
+ * isFilesystemPath('project//src') // false
298
309
  * isFilesystemPath('project/nul') // false
299
310
  * ```
300
311
  */
@@ -560,7 +571,7 @@ export declare function computeManifestDigest(entries: readonly ManifestEntry[],
560
571
  * ```ts
561
572
  * import { listFiles } from '@orkestrel/scaffold/server'
562
573
  *
563
- * listFiles('./dist/host') // ['AGENTS.md', 'claude/rules/names.md', …]
574
+ * listFiles('./dist/host') // ['AGENTS.md', 'CLAUDE.md', 'LICENSE', …]
564
575
  * ```
565
576
  */
566
577
  export declare function listFiles(root: string): readonly string[];
@@ -591,9 +602,13 @@ export declare function computeManifestDigest(entries: readonly ManifestEntry[],
591
602
  * device and inode.
592
603
  *
593
604
  * @remarks
594
- * The check a write repeats between steps. A directory replaced by another
595
- * directory of the same name answers `false` here, which is the case a path
596
- * comparison alone cannot see.
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.
597
612
  *
598
613
  * @example
599
614
  * ```ts
@@ -605,6 +620,31 @@ export declare function computeManifestDigest(entries: readonly ManifestEntry[],
605
620
  */
606
621
  export declare function matchesAnchor(anchor: WriteAnchor): boolean;
607
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
+
608
648
  /**
609
649
  * Test whether a destination still holds what was captured of it.
610
650
  *
@@ -861,10 +901,13 @@ export declare function computeManifestDigest(entries: readonly ManifestEntry[],
861
901
  * The audit is a preview, not an instruction. The plan is hydrated and
862
902
  * compared against the target again here, and the verdicts that produces must
863
903
  * match the ones the audit carried for every path the plan owns; anything else
864
- * means the target moved, and the whole call is refused. A missing destination
865
- * is restored whatever its ownership; a stale one is replaced only where the
866
- * artifact claims its bytes, which is what leaves a presence-owned file a
867
- * consumer has edited exactly as it is.
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.
868
911
  */
869
912
  repair(plan: Plan, audit: Audit, target: string): MaterializeResult;
870
913
  /**
@@ -1018,6 +1061,20 @@ export declare function computeManifestDigest(entries: readonly ManifestEntry[],
1018
1061
  * @param audit - The preview returned by this materializer's `audit` method.
1019
1062
  * @param target - The directory to write into.
1020
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.
1021
1078
  */
1022
1079
  repair(plan: Plan, audit: Audit, target: string): MaterializeResult;
1023
1080
  /**
@@ -1062,6 +1119,14 @@ export declare function computeManifestDigest(entries: readonly ManifestEntry[],
1062
1119
  * unlinked, so a failure part way through restores what it already took.
1063
1120
  * The package's own source and application trees are never candidates,
1064
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.
1065
1130
  */
1066
1131
  remove(audit: Audit, repository: Repository, target: string): MaterializeResult;
1067
1132
  /**
@@ -1175,8 +1240,8 @@ export declare function computeManifestDigest(entries: readonly ManifestEntry[],
1175
1240
  * @remarks
1176
1241
  * Device and inode rather than the path, because the path is the thing that can
1177
1242
  * be swapped underneath a write. An anchor captured before a mutation and
1178
- * checked again after it is what proves the directory written into is the
1179
- * directory that was inspected.
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.
1180
1245
  *
1181
1246
  * @example
1182
1247
  * ```ts
@@ -1307,14 +1372,14 @@ export declare function computeManifestDigest(entries: readonly ManifestEntry[],
1307
1372
  *
1308
1373
  * @remarks
1309
1374
  * The one place the three declared fields are decided together, because they are
1310
- * three readings of one file: {@link pathToStorage} decides where it is stored,
1311
- * the destination is the path it answers for, and the executable bit is read
1312
- * from the source's own mode.
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.
1313
1378
  *
1314
- * That mode is the honest limit of this reading. A Windows host reports no
1315
- * executable bit at all, so a host staged there declares every entry
1316
- * non-executable and a consumer receives scripts without it. Staging on a POSIX
1317
- * host is what carries the bit through.
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.
1318
1383
  *
1319
1384
  * @example
1320
1385
  * ```ts
@@ -1396,20 +1461,31 @@ export declare function computeManifestDigest(entries: readonly ManifestEntry[],
1396
1461
  *
1397
1462
  * @remarks
1398
1463
  * The containment law, and the one door every read in this module goes through.
1399
- * Both sides are resolved through the real filesystem before they are compared,
1400
- * so a link planted inside the root cannot smuggle a destination out of it; the
1401
- * answer is then the lexical join, so the caller operates on the path it named
1402
- * rather than on a resolved form the target may not recognize.
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.
1403
1472
  *
1404
1473
  * Comparison is exact text, which fails closed on a case-insensitive
1405
1474
  * filesystem: a root and a path spelled with different case resolve to
1406
1475
  * different strings there and are refused, never wrongly admitted.
1407
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
+ *
1408
1484
  * @example
1409
1485
  * ```ts
1410
1486
  * import { resolveContainedPath } from '@orkestrel/scaffold/server'
1411
1487
  *
1412
- * resolveContainedPath('/tmp/project', 'guides/router.md') // '/tmp/project/guides/router.md'
1488
+ * resolveContainedPath('/tmp/project', 'guides/router.md')?.endsWith('router.md') // true
1413
1489
  * resolveContainedPath('/tmp/project', '../secrets') // undefined
1414
1490
  * ```
1415
1491
  */
@@ -1419,9 +1495,10 @@ export declare function computeManifestDigest(entries: readonly ManifestEntry[],
1419
1495
  * Resolve a path through the real filesystem, keeping the part that does not exist yet.
1420
1496
  *
1421
1497
  * @param path - The absolute or relative host path to resolve.
1422
- * @returns The path with its existing prefix resolved through every link, or
1423
- * `undefined` when the text is not a host path, no bounded existing ancestor
1424
- * resolves, or an ancestor cannot be read.
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.
1425
1502
  *
1426
1503
  * @remarks
1427
1504
  * A containment decision has to be made about a destination that does not exist
@@ -1431,6 +1508,30 @@ export declare function computeManifestDigest(entries: readonly ManifestEntry[],
1431
1508
  * it. The climb is bounded by the path-depth ceiling, so an adversarial path
1432
1509
  * cannot make it walk indefinitely.
1433
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
+ *
1434
1535
  * @example
1435
1536
  * ```ts
1436
1537
  * import { resolveRealPath } from '@orkestrel/scaffold/server'
@@ -1745,7 +1846,14 @@ export declare function computeManifestDigest(entries: readonly ManifestEntry[],
1745
1846
  readonly error?: EmitterErrorHandler;
1746
1847
  }
1747
1848
 
1748
- /** One physical directory identity captured across a write transaction. */
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
+ */
1749
1857
  export declare interface WriteAnchor {
1750
1858
  readonly path: string;
1751
1859
  readonly device: number;
@@ -1810,6 +1918,12 @@ export declare function computeManifestDigest(entries: readonly ManifestEntry[],
1810
1918
  * - **No partly written destination.** Every file is written whole into the
1811
1919
  * private root and digested there before commit, so a destination never
1812
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.
1813
1927
  * - **No crash atomicity across destinations.** A process killed between two
1814
1928
  * promotions leaves the target holding some new files and some old ones, and
1815
1929
  * leaves the private root behind. Nothing here is a journal, and the private