@orkestrel/scaffold 0.0.101 → 0.0.103
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.
- package/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +6 -14
- package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +41 -28
- package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +5 -12
- package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +25 -13
- package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +18 -25
- package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +11 -9
- package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +233 -232
- package/dist/agents/skills/orkestrel-publish/scripts/compare.js +95 -89
- package/dist/agents/skills/orkestrel-publish/scripts/pins.js +5 -7
- package/dist/agents/skills/orkestrel-publish/scripts/swap.js +1273 -0
- package/dist/agents/skills/orkestrel-publish/scripts/wave.js +99 -89
- package/dist/agents/skills/orkestrel-publish/scripts/window.js +54 -52
- package/dist/agents/skills/orkestrel-scout/scripts/map.js +160 -161
- package/dist/bin/main.js +483 -444
- package/dist/bin/main.js.map +1 -1
- package/dist/host/AGENTS.md +1 -0
- package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +10 -10
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +8 -11
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +58 -28
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +5 -10
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +31 -12
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +28 -23
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +12 -10
- package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +318 -237
- package/dist/host/agents/skills/orkestrel-journey/references/statechart.md +2 -5
- package/dist/host/agents/skills/orkestrel-publish/SKILL.md +7 -6
- package/dist/host/agents/skills/orkestrel-publish/references/release.md +12 -10
- package/dist/host/agents/skills/orkestrel-publish/references/wave.md +12 -5
- package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +124 -82
- package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +5 -7
- package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1655 -0
- package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +128 -97
- package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +70 -55
- package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +215 -155
- package/dist/host/claude/rules/architecture.md +2 -2
- package/dist/host/claude/rules/tests.md +8 -2
- package/dist/host/claude/rules/typescript.md +130 -0
- package/dist/host/claude/rules/workspace.md +33 -18
- package/dist/host/configs/helpers.ts +209 -148
- package/dist/host/configs/policy.ts +110 -105
- package/dist/host/dotfiles/oxlintrc.json +1 -11
- package/dist/host/guides/scaffold.md +244 -170
- package/dist/host/manifest.json +36 -30
- package/dist/host/tests/config.test.ts +1263 -687
- package/dist/host/tests/policy.test.ts +19 -0
- package/dist/host/tests/setupPolicy.ts +1699 -1026
- package/dist/src/core/index.cjs +1595 -1686
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +202 -18
- package/dist/src/core/index.d.ts +202 -18
- package/dist/src/core/index.js +1590 -1687
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +448 -392
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +1182 -1052
- package/dist/src/server/index.d.ts +1182 -1052
- package/dist/src/server/index.js +447 -395
- package/dist/src/server/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -482,18 +482,61 @@ function isExactCaseFile(path) {
|
|
|
482
482
|
if (!isPhysicalFile(full)) return false;
|
|
483
483
|
const root = (0, node_path.parse)(full).root;
|
|
484
484
|
const segments = (0, node_path.relative)(root, full).split(node_path.sep);
|
|
485
|
-
|
|
486
|
-
|
|
485
|
+
return segments.every((segment, index) => {
|
|
486
|
+
const parent = (0, node_path.join)(root, ...segments.slice(0, index));
|
|
487
487
|
const entries = (0, _orkestrel_contract.attempt)(() => (0, node_fs.readdirSync)(parent));
|
|
488
|
-
if (entries.success)
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
488
|
+
if (entries.success) return entries.value.includes(segment);
|
|
489
|
+
const actual = (0, _orkestrel_contract.attempt)(() => node_fs.realpathSync.native((0, node_path.join)(parent, segment)));
|
|
490
|
+
return actual.success && (0, node_path.basename)(actual.value) === segment;
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Places each result at the index its entry names, in a fresh array.
|
|
495
|
+
*
|
|
496
|
+
* @param entries - The index and result pairs, in any order; each index a non-negative integer.
|
|
497
|
+
* @returns An array holding each result at its index, whose length is the highest index plus one,
|
|
498
|
+
* and where an index no entry names stays a hole.
|
|
499
|
+
*
|
|
500
|
+
* @remarks
|
|
501
|
+
* A worker pool finishes its items out of order and can skip one, so its answer is merged by
|
|
502
|
+
* position rather than by arrival: each index is written once into a fresh array, which needs no
|
|
503
|
+
* sort and keeps a skipped index absent instead of closing the gap. A repeated index keeps its last
|
|
504
|
+
* result.
|
|
505
|
+
*
|
|
506
|
+
* @example
|
|
507
|
+
* ```ts
|
|
508
|
+
* import { collectIndexed } from '@orkestrel/scaffold/server'
|
|
509
|
+
*
|
|
510
|
+
* collectIndexed([[0, 'first'], [2, 'third']]).length // 3
|
|
511
|
+
* ```
|
|
512
|
+
*/
|
|
513
|
+
function collectIndexed(entries) {
|
|
514
|
+
return Object.assign([], Object.fromEntries(entries));
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Lists a path followed by each directory above it, nearest first.
|
|
518
|
+
*
|
|
519
|
+
* @param path - The host path to climb from.
|
|
520
|
+
* @param limit - The most paths to list; every ancestor by default.
|
|
521
|
+
* @returns The path, then `dirname` of each result in turn, ending at the first path that is its
|
|
522
|
+
* own parent (`.` for a relative path, the filesystem root for an absolute one) or at `limit`.
|
|
523
|
+
*
|
|
524
|
+
* @remarks
|
|
525
|
+
* The climb is derived from the path text alone and reads no filesystem, so a caller bounds a walk
|
|
526
|
+
* by the length it asks for rather than by a counter it reassigns. The final path of an unbounded
|
|
527
|
+
* list is the one `dirname` leaves unchanged.
|
|
528
|
+
*
|
|
529
|
+
* @example
|
|
530
|
+
* ```ts
|
|
531
|
+
* import { listAncestors } from '@orkestrel/scaffold/server'
|
|
532
|
+
*
|
|
533
|
+
* listAncestors('notes/entry/file.md') // ['notes/entry/file.md', 'notes/entry', 'notes', '.']
|
|
534
|
+
* listAncestors('notes/entry/file.md', 2) // ['notes/entry/file.md', 'notes/entry']
|
|
535
|
+
* ```
|
|
536
|
+
*/
|
|
537
|
+
function listAncestors(path, limit = Number.POSITIVE_INFINITY) {
|
|
538
|
+
const parent = (0, node_path.dirname)(path);
|
|
539
|
+
return parent === path || limit <= 1 ? [path] : [path, ...listAncestors(parent, limit - 1)];
|
|
497
540
|
}
|
|
498
541
|
/**
|
|
499
542
|
* Tests whether a path is a physical directory this package will read or write into.
|
|
@@ -549,14 +592,15 @@ function computeFileDigest(path) {
|
|
|
549
592
|
if (!before.isFile() || before.nlink !== 1 || before.size > _src_core.MAX_ARTIFACT_BYTES) return void 0;
|
|
550
593
|
const hash = (0, node_crypto.createHash)("sha256");
|
|
551
594
|
const buffer = Buffer.alloc(65536);
|
|
552
|
-
|
|
595
|
+
const lengths = [];
|
|
553
596
|
for (;;) {
|
|
554
597
|
const length = (0, node_fs.readSync)(handle, buffer, 0, buffer.byteLength, null);
|
|
555
598
|
if (length === 0) break;
|
|
556
|
-
|
|
557
|
-
if (
|
|
599
|
+
lengths.push(length);
|
|
600
|
+
if ((0, _src_core.computeTotal)(lengths) > _src_core.MAX_ARTIFACT_BYTES) return void 0;
|
|
558
601
|
hash.update(buffer.subarray(0, length));
|
|
559
602
|
}
|
|
603
|
+
const size = (0, _src_core.computeTotal)(lengths);
|
|
560
604
|
const after = (0, node_fs.fstatSync)(handle);
|
|
561
605
|
if (size !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs) return;
|
|
562
606
|
return hash.digest("hex");
|
|
@@ -578,8 +622,8 @@ function computeFileDigest(path) {
|
|
|
578
622
|
* yet, and a lexical answer is not enough: a link anywhere in the existing
|
|
579
623
|
* prefix moves the destination somewhere the text never named. So the deepest
|
|
580
624
|
* existing ancestor is resolved and the remaining segments are re-joined onto
|
|
581
|
-
* it.
|
|
582
|
-
* cannot make it walk indefinitely.
|
|
625
|
+
* it. {@link climbRealPath} performs the climb and bounds it by the path-depth
|
|
626
|
+
* ceiling, so an adversarial path cannot make it walk indefinitely.
|
|
583
627
|
*
|
|
584
628
|
* The caller's own text is collapsed first, which is what `resolve` does with a
|
|
585
629
|
* `..` the caller wrote: it cancels the segment before it as text, before any
|
|
@@ -613,32 +657,50 @@ function computeFileDigest(path) {
|
|
|
613
657
|
* ```
|
|
614
658
|
*/
|
|
615
659
|
function resolveRealPath(path) {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
660
|
+
return isFilesystemPath(path) ? climbRealPath((0, node_path.resolve)(path), [], 0) : void 0;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Climbs from an absolute path to its deepest existing ancestor and resolves it through every link.
|
|
664
|
+
*
|
|
665
|
+
* @param current - The absolute host path the climb reads at this depth.
|
|
666
|
+
* @param pending - The absent segments already climbed past, outermost first.
|
|
667
|
+
* @param depth - The steps already taken, from 0 through `MAX_PATH_DEPTH`.
|
|
668
|
+
* @returns The real path of the first existing ancestor joined with `pending` and every segment the
|
|
669
|
+
* climb passes, or `undefined` when `depth` is not a safe integer from 0 through `MAX_PATH_DEPTH`,
|
|
670
|
+
* the climb reaches the root, a link target cannot be read or carries a `..` segment, an existing
|
|
671
|
+
* entry is not a link, or an entry cannot be read.
|
|
672
|
+
*
|
|
673
|
+
* @remarks
|
|
674
|
+
* Each step reads one path and moves the climb to one other: a dangling link redirects it to the
|
|
675
|
+
* link's target with the same suffix, and an absent name moves it to the parent with that name added
|
|
676
|
+
* to the suffix. One successor per step makes the climb a bounded recursion rather than a worklist,
|
|
677
|
+
* and the depth argument is its bound, so an adversarial path or a link cycle answers `undefined`
|
|
678
|
+
* after `MAX_PATH_DEPTH` steps instead of walking indefinitely. {@link resolveRealPath} starts the
|
|
679
|
+
* climb at depth 0 with nothing pending.
|
|
680
|
+
*
|
|
681
|
+
* @example
|
|
682
|
+
* ```ts
|
|
683
|
+
* import { resolve } from 'node:path'
|
|
684
|
+
* import { climbRealPath } from '@orkestrel/scaffold/server'
|
|
685
|
+
*
|
|
686
|
+
* climbRealPath(resolve('./packages/new/src'), [], 0) // the real path of packages, plus new/src
|
|
687
|
+
* ```
|
|
688
|
+
*/
|
|
689
|
+
function climbRealPath(current, pending, depth) {
|
|
690
|
+
if (!Number.isSafeInteger(depth) || depth < 0 || depth > 64) return void 0;
|
|
691
|
+
const real = (0, _orkestrel_contract.attempt)(() => (0, node_fs.realpathSync)(current));
|
|
692
|
+
if (real.success) return pending.length === 0 ? real.value : (0, node_path.join)(real.value, ...pending);
|
|
693
|
+
if (!matchesMissingPath(real.error)) return void 0;
|
|
694
|
+
const status = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(current));
|
|
695
|
+
if (status.success) {
|
|
696
|
+
if (!status.value.isSymbolicLink()) return void 0;
|
|
697
|
+
const target = (0, _orkestrel_contract.attempt)(() => (0, node_fs.readlinkSync)(current));
|
|
698
|
+
if (!target.success || target.value.split(/[\\/]/u).includes("..")) return void 0;
|
|
699
|
+
return climbRealPath((0, node_path.resolve)((0, node_path.dirname)(current), target.value), pending, depth + 1);
|
|
641
700
|
}
|
|
701
|
+
if (!matchesMissingPath(status.error)) return void 0;
|
|
702
|
+
const parent = (0, node_path.dirname)(current);
|
|
703
|
+
return parent === current ? void 0 : climbRealPath(parent, [(0, node_path.relative)(parent, current), ...pending], depth + 1);
|
|
642
704
|
}
|
|
643
705
|
/**
|
|
644
706
|
* Resolves a root-relative path and refuses one that leaves its root.
|
|
@@ -765,13 +827,12 @@ function listFiles(root) {
|
|
|
765
827
|
});
|
|
766
828
|
}
|
|
767
829
|
if (!status.value.isDirectory() || status.value.isSymbolicLink()) throw new _src_core.ScaffoldError("TARGET", `Listing root is not a physical directory at ${root}`, { root });
|
|
768
|
-
const files = [];
|
|
769
830
|
const pending = [{
|
|
770
831
|
full: root,
|
|
771
832
|
path: "",
|
|
772
833
|
depth: 0
|
|
773
834
|
}];
|
|
774
|
-
|
|
835
|
+
const entries = [];
|
|
775
836
|
while (pending.length > 0) {
|
|
776
837
|
const current = pending.pop();
|
|
777
838
|
if (current === void 0) break;
|
|
@@ -786,20 +847,21 @@ function listFiles(root) {
|
|
|
786
847
|
for (;;) {
|
|
787
848
|
const entry = handle.readSync();
|
|
788
849
|
if (entry === null) break;
|
|
789
|
-
|
|
790
|
-
|
|
850
|
+
const path = current.path === "" ? entry.name : `${current.path}/${entry.name}`;
|
|
851
|
+
const file = !entry.isDirectory() || entry.isSymbolicLink();
|
|
852
|
+
entries.push({
|
|
853
|
+
path,
|
|
854
|
+
file
|
|
855
|
+
});
|
|
856
|
+
if (entries.length > 1e5) throw new _src_core.ScaffoldError("TARGET", "Listing exceeds the paths one inventory may report", {
|
|
791
857
|
root,
|
|
792
858
|
limit: MAX_INVENTORY_PATHS
|
|
793
859
|
});
|
|
794
|
-
const path = current.path === "" ? entry.name : `${current.path}/${entry.name}`;
|
|
795
860
|
if (!(0, _src_core.isPath)(path)) throw new _src_core.ScaffoldError("TARGET", `Listing found a path this package cannot plan at ${path}`, {
|
|
796
861
|
root,
|
|
797
862
|
path
|
|
798
863
|
});
|
|
799
|
-
if (
|
|
800
|
-
files.push(path);
|
|
801
|
-
continue;
|
|
802
|
-
}
|
|
864
|
+
if (file) continue;
|
|
803
865
|
const depth = current.depth + 1;
|
|
804
866
|
if (depth >= 64) throw new _src_core.ScaffoldError("TARGET", `Listing exceeds the depth one path may carry at ${path}`, {
|
|
805
867
|
root,
|
|
@@ -816,7 +878,7 @@ function listFiles(root) {
|
|
|
816
878
|
(0, _orkestrel_contract.attempt)(() => handle.closeSync());
|
|
817
879
|
if (!walked.success) throw walked.error;
|
|
818
880
|
}
|
|
819
|
-
return
|
|
881
|
+
return entries.filter(({ file }) => file).map(({ path }) => path).sort();
|
|
820
882
|
}
|
|
821
883
|
/**
|
|
822
884
|
* Lists a directory's descendant directories as sorted root-relative paths.
|
|
@@ -858,13 +920,12 @@ function listDirectories(root) {
|
|
|
858
920
|
});
|
|
859
921
|
}
|
|
860
922
|
if (!status.value.isDirectory() || status.value.isSymbolicLink()) throw new _src_core.ScaffoldError("TARGET", `Listing root is not a physical directory at ${root}`, { root });
|
|
861
|
-
const directories = [];
|
|
862
923
|
const pending = [{
|
|
863
924
|
full: root,
|
|
864
925
|
path: "",
|
|
865
926
|
depth: 0
|
|
866
927
|
}];
|
|
867
|
-
|
|
928
|
+
const entries = [];
|
|
868
929
|
while (pending.length > 0) {
|
|
869
930
|
const current = pending.pop();
|
|
870
931
|
if (current === void 0) break;
|
|
@@ -879,24 +940,27 @@ function listDirectories(root) {
|
|
|
879
940
|
for (;;) {
|
|
880
941
|
const entry = handle.readSync();
|
|
881
942
|
if (entry === null) break;
|
|
882
|
-
|
|
883
|
-
|
|
943
|
+
const path = current.path === "" ? entry.name : `${current.path}/${entry.name}`;
|
|
944
|
+
const directory = entry.isDirectory() && !entry.isSymbolicLink();
|
|
945
|
+
entries.push({
|
|
946
|
+
path,
|
|
947
|
+
directory
|
|
948
|
+
});
|
|
949
|
+
if (entries.length > 1e5) throw new _src_core.ScaffoldError("TARGET", "Listing exceeds the paths one inventory may report", {
|
|
884
950
|
root,
|
|
885
951
|
limit: MAX_INVENTORY_PATHS
|
|
886
952
|
});
|
|
887
|
-
const path = current.path === "" ? entry.name : `${current.path}/${entry.name}`;
|
|
888
953
|
if (!(0, _src_core.isPath)(path)) throw new _src_core.ScaffoldError("TARGET", `Listing found a path this package cannot plan at ${path}`, {
|
|
889
954
|
root,
|
|
890
955
|
path
|
|
891
956
|
});
|
|
892
|
-
if (!
|
|
957
|
+
if (!directory) continue;
|
|
893
958
|
const depth = current.depth + 1;
|
|
894
959
|
if (depth >= 64) throw new _src_core.ScaffoldError("TARGET", `Listing exceeds the depth one path may carry at ${path}`, {
|
|
895
960
|
root,
|
|
896
961
|
path,
|
|
897
962
|
limit: 64
|
|
898
963
|
});
|
|
899
|
-
directories.push(path);
|
|
900
964
|
pending.push({
|
|
901
965
|
full: (0, node_path.join)(current.full, entry.name),
|
|
902
966
|
path,
|
|
@@ -907,7 +971,7 @@ function listDirectories(root) {
|
|
|
907
971
|
(0, _orkestrel_contract.attempt)(() => handle.closeSync());
|
|
908
972
|
if (!walked.success) throw walked.error;
|
|
909
973
|
}
|
|
910
|
-
return
|
|
974
|
+
return entries.filter(({ directory }) => directory).map(({ path }) => path).sort();
|
|
911
975
|
}
|
|
912
976
|
/**
|
|
913
977
|
* Lists the canon paths a target holds, filtered to a plan's groups.
|
|
@@ -941,14 +1005,12 @@ function listDirectories(root) {
|
|
|
941
1005
|
* ```
|
|
942
1006
|
*/
|
|
943
1007
|
function listCanonPaths(target, groups) {
|
|
944
|
-
|
|
945
|
-
for (const member of _src_core.CANON_PATHS) {
|
|
1008
|
+
return _src_core.CANON_PATHS.flatMap((member) => {
|
|
946
1009
|
const full = resolveContainedPath(target, member);
|
|
947
|
-
if (full === void 0)
|
|
948
|
-
if (isPhysicalFile(full))
|
|
949
|
-
|
|
950
|
-
}
|
|
951
|
-
return held.filter((path) => groups.includes((0, _src_core.inferGroup)(path)));
|
|
1010
|
+
if (full === void 0) return [];
|
|
1011
|
+
if (isPhysicalFile(full)) return [member];
|
|
1012
|
+
return isPhysicalDirectory(full) ? listFiles(full).map((name) => `${member}/${name}`) : [];
|
|
1013
|
+
}).filter((path) => groups.includes((0, _src_core.inferGroup)(path)));
|
|
952
1014
|
}
|
|
953
1015
|
/**
|
|
954
1016
|
* Removes every directory one set of deletions emptied.
|
|
@@ -981,24 +1043,13 @@ function listCanonPaths(target, groups) {
|
|
|
981
1043
|
* ```
|
|
982
1044
|
*/
|
|
983
1045
|
function pruneEmptiedDirectories(target, removed) {
|
|
984
|
-
|
|
985
|
-
for (const path of removed) {
|
|
986
|
-
let parent = (0, node_path.dirname)(path);
|
|
987
|
-
while (parent !== "." && parent !== (0, node_path.dirname)(parent) && !candidates.has(parent)) {
|
|
988
|
-
candidates.add(parent);
|
|
989
|
-
parent = (0, node_path.dirname)(parent);
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
const ordered = [...candidates].sort((left, right) => (0, _orkestrel_contract.compareValues)(right.split("/").length, left.split("/").length) || (0, _orkestrel_contract.compareValues)(left, right));
|
|
993
|
-
const pruned = [];
|
|
994
|
-
for (const candidate of ordered) {
|
|
1046
|
+
return [...new Set(Iterator.from(removed).flatMap((path) => listAncestors(path).slice(1, -1)).toArray())].sort((left, right) => (0, _orkestrel_contract.compareValues)(right.split("/").length, left.split("/").length) || (0, _orkestrel_contract.compareValues)(left, right)).flatMap((candidate) => {
|
|
995
1047
|
const directory = resolveContainedPath(target, candidate);
|
|
996
|
-
if (directory === void 0 || !isPhysicalDirectory(directory))
|
|
1048
|
+
if (directory === void 0 || !isPhysicalDirectory(directory)) return [];
|
|
997
1049
|
const entries = (0, _orkestrel_contract.attempt)(() => (0, node_fs.readdirSync)(directory));
|
|
998
|
-
if (!entries.success || entries.value.length > 0)
|
|
999
|
-
|
|
1000
|
-
}
|
|
1001
|
-
return pruned;
|
|
1050
|
+
if (!entries.success || entries.value.length > 0) return [];
|
|
1051
|
+
return (0, _orkestrel_contract.attempt)(() => (0, node_fs.rmdirSync)(directory)).success ? [candidate] : [];
|
|
1052
|
+
});
|
|
1002
1053
|
}
|
|
1003
1054
|
/**
|
|
1004
1055
|
* Reads one contained file as its exact bytes in lowercase hexadecimal.
|
|
@@ -1046,12 +1097,15 @@ function readFileHex(root, path, limit = _src_core.MAX_ARTIFACT_BYTES) {
|
|
|
1046
1097
|
const before = (0, node_fs.fstatSync)(handle);
|
|
1047
1098
|
if (!before.isFile() || before.nlink !== 1 || before.size > limit) return void 0;
|
|
1048
1099
|
const bytes = Buffer.alloc(before.size);
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
const
|
|
1100
|
+
const reads = [];
|
|
1101
|
+
for (;;) {
|
|
1102
|
+
const position = (0, _src_core.computeTotal)(reads);
|
|
1103
|
+
if (position >= bytes.byteLength) break;
|
|
1104
|
+
const length = (0, node_fs.readSync)(handle, bytes, position, bytes.byteLength - position, position);
|
|
1052
1105
|
if (length === 0) break;
|
|
1053
|
-
|
|
1106
|
+
reads.push(length);
|
|
1054
1107
|
}
|
|
1108
|
+
const offset = (0, _src_core.computeTotal)(reads);
|
|
1055
1109
|
const overflow = (0, node_fs.readSync)(handle, Buffer.alloc(1), 0, 1, offset);
|
|
1056
1110
|
const after = (0, node_fs.fstatSync)(handle);
|
|
1057
1111
|
if (offset !== bytes.byteLength || overflow !== 0 || after.size !== before.size || after.mtimeMs !== before.mtimeMs) return;
|
|
@@ -1123,9 +1177,7 @@ function readSnapshot(target, paths) {
|
|
|
1123
1177
|
target,
|
|
1124
1178
|
limit: _src_core.MAX_COLLECTION_ITEMS
|
|
1125
1179
|
});
|
|
1126
|
-
|
|
1127
|
-
const snapshot = {};
|
|
1128
|
-
for (const path of paths) {
|
|
1180
|
+
const read = Iterator.from(paths).reduce((state, path) => {
|
|
1129
1181
|
const full = resolveContainedPath(target, path);
|
|
1130
1182
|
if (full === void 0) throw new _src_core.ScaffoldError("INVALID", `Snapshot path leaves its target at ${path}`, {
|
|
1131
1183
|
target,
|
|
@@ -1133,27 +1185,32 @@ function readSnapshot(target, paths) {
|
|
|
1133
1185
|
});
|
|
1134
1186
|
const status = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(full));
|
|
1135
1187
|
if (!status.success) {
|
|
1136
|
-
if (matchesMissingPath(status.error))
|
|
1188
|
+
if (matchesMissingPath(status.error)) return state;
|
|
1137
1189
|
throw new _src_core.ScaffoldError("TARGET", `Snapshot path cannot be inspected at ${path}`, {
|
|
1138
1190
|
target,
|
|
1139
1191
|
path,
|
|
1140
1192
|
error: status.error
|
|
1141
1193
|
});
|
|
1142
1194
|
}
|
|
1143
|
-
if (isPhysicalDirectory(full)) {
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
}
|
|
1147
|
-
const hex = readFileHex(target, path, Math.min(_src_core.MAX_ARTIFACT_BYTES,
|
|
1195
|
+
if (isPhysicalDirectory(full)) return {
|
|
1196
|
+
...state,
|
|
1197
|
+
entries: [...state.entries, [path, ""]]
|
|
1198
|
+
};
|
|
1199
|
+
const hex = readFileHex(target, path, Math.min(_src_core.MAX_ARTIFACT_BYTES, _src_core.MAX_TOTAL_ARTIFACT_BYTES - (0, _src_core.computeTotal)(state.spent)));
|
|
1148
1200
|
if (hex === void 0) throw new _src_core.ScaffoldError("TARGET", `Snapshot path is not a readable file at ${path}`, {
|
|
1149
1201
|
target,
|
|
1150
1202
|
path,
|
|
1151
1203
|
limit: _src_core.MAX_TOTAL_ARTIFACT_BYTES
|
|
1152
1204
|
});
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1205
|
+
return {
|
|
1206
|
+
entries: [...state.entries, [path, hex]],
|
|
1207
|
+
spent: [...state.spent, hex.length / 2]
|
|
1208
|
+
};
|
|
1209
|
+
}, {
|
|
1210
|
+
entries: [],
|
|
1211
|
+
spent: []
|
|
1212
|
+
});
|
|
1213
|
+
return Object.fromEntries(read.entries);
|
|
1157
1214
|
}
|
|
1158
1215
|
/**
|
|
1159
1216
|
* Reads a vendored host's manifest, when it carries one.
|
|
@@ -1237,20 +1294,18 @@ function readHostFloor(root) {
|
|
|
1237
1294
|
if (!isPhysicalDirectory(host)) throw new _src_core.ScaffoldError("TARGET", `The vendored host root is not readable at ${host}`, { host });
|
|
1238
1295
|
const manifest = readHostManifest(host, source ? _src_core.HOST_INVENTORY_PATH : MANIFEST_NAME);
|
|
1239
1296
|
if (manifest === void 0) throw new _src_core.ScaffoldError("TARGET", `The vendored host carries no manifest at ${host}`, { host });
|
|
1240
|
-
const bytes = {};
|
|
1241
|
-
for (const entry of manifest.entries) {
|
|
1242
|
-
const path = source ? entry.destination : entry.storage;
|
|
1243
|
-
const hex = readFileHex(host, path);
|
|
1244
|
-
if (hex === void 0 || hexToDigest(hex) !== entry.digest) throw new _src_core.ScaffoldError("TARGET", `The vendored host cannot read the declared file at ${path}`, {
|
|
1245
|
-
host,
|
|
1246
|
-
path,
|
|
1247
|
-
destination: entry.destination
|
|
1248
|
-
});
|
|
1249
|
-
bytes[entry.destination] = hex;
|
|
1250
|
-
}
|
|
1251
1297
|
return {
|
|
1252
1298
|
manifest,
|
|
1253
|
-
bytes
|
|
1299
|
+
bytes: Object.fromEntries(manifest.entries.map((entry) => {
|
|
1300
|
+
const path = source ? entry.destination : entry.storage;
|
|
1301
|
+
const hex = readFileHex(host, path);
|
|
1302
|
+
if (hex === void 0 || hexToDigest(hex) !== entry.digest) throw new _src_core.ScaffoldError("TARGET", `The vendored host cannot read the declared file at ${path}`, {
|
|
1303
|
+
host,
|
|
1304
|
+
path,
|
|
1305
|
+
destination: entry.destination
|
|
1306
|
+
});
|
|
1307
|
+
return [entry.destination, hex];
|
|
1308
|
+
}))
|
|
1254
1309
|
};
|
|
1255
1310
|
}
|
|
1256
1311
|
/**
|
|
@@ -1330,24 +1385,21 @@ function readManifestEntry(destination, source) {
|
|
|
1330
1385
|
*/
|
|
1331
1386
|
function filesToHost(files, floor) {
|
|
1332
1387
|
const declared = new Set(floor.manifest.entries.map((entry) => entry.destination));
|
|
1333
|
-
const
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
}
|
|
1338
|
-
const entries = [];
|
|
1339
|
-
const bytes = {};
|
|
1340
|
-
for (const entry of floor.manifest.entries) {
|
|
1388
|
+
const rows = [...(0, _src_core.iterateAnswers)(files, (file) => file.lookup !== "found" || !declared.has(file.path) ? void 0 : (0, _src_core.isFloorPath)(file.path) ? [] : [[file.path, file.hex]])];
|
|
1389
|
+
if (!rows.every(_orkestrel_contract.isDefined)) return void 0;
|
|
1390
|
+
const held = new Map(rows.flat());
|
|
1391
|
+
const filled = [...(0, _src_core.iterateAnswers)(floor.manifest.entries, (entry) => {
|
|
1341
1392
|
const hex = (0, _src_core.isFloorPath)(entry.destination) ? floor.bytes[entry.destination] : held.get(entry.destination);
|
|
1342
|
-
|
|
1343
|
-
entries.push({
|
|
1393
|
+
return hex === void 0 ? void 0 : [{
|
|
1344
1394
|
storage: entry.storage,
|
|
1345
1395
|
destination: entry.destination,
|
|
1346
1396
|
executable: entry.executable,
|
|
1347
1397
|
digest: hexToDigest(hex)
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1398
|
+
}, hex];
|
|
1399
|
+
})];
|
|
1400
|
+
if (!filled.every(_orkestrel_contract.isDefined)) return void 0;
|
|
1401
|
+
const entries = filled.map(([entry]) => entry);
|
|
1402
|
+
const bytes = Object.fromEntries(filled.map(([entry, hex]) => [entry.destination, hex]));
|
|
1351
1403
|
return {
|
|
1352
1404
|
manifest: {
|
|
1353
1405
|
entries,
|
|
@@ -1395,8 +1447,7 @@ function filesToHost(files, floor) {
|
|
|
1395
1447
|
function stageBytes(host, root, destinations) {
|
|
1396
1448
|
if (!isFilesystemPath(root)) throw new _src_core.ScaffoldError("INVALID", "Staging host root is not a host path", { host: root });
|
|
1397
1449
|
const declared = new Map(host.manifest.entries.map((entry) => [entry.destination, entry]));
|
|
1398
|
-
|
|
1399
|
-
for (const destination of destinations) {
|
|
1450
|
+
return Iterator.from(destinations).map((destination) => {
|
|
1400
1451
|
const entry = declared.get(destination);
|
|
1401
1452
|
const hex = host.bytes[destination];
|
|
1402
1453
|
if (entry === void 0 || hex === void 0) throw new _src_core.ScaffoldError("TARGET", `The host carries no bytes for ${destination}`, {
|
|
@@ -1426,9 +1477,8 @@ function stageBytes(host, root, destinations) {
|
|
|
1426
1477
|
host: root,
|
|
1427
1478
|
storage: entry.storage
|
|
1428
1479
|
});
|
|
1429
|
-
|
|
1430
|
-
}
|
|
1431
|
-
return staged;
|
|
1480
|
+
return entry;
|
|
1481
|
+
}).toArray();
|
|
1432
1482
|
}
|
|
1433
1483
|
/**
|
|
1434
1484
|
* Stages a vendored host root from a real checkout.
|
|
@@ -1505,37 +1555,36 @@ function stageHost(checkout, host, options) {
|
|
|
1505
1555
|
const source = (0, node_path.resolve)(checkout);
|
|
1506
1556
|
if (!isPhysicalDirectory(source)) throw new _src_core.ScaffoldError("TARGET", `Staging checkout is not a physical directory at ${source}`, { checkout: source });
|
|
1507
1557
|
if (!isVacant(host)) throw new _src_core.ScaffoldError("TARGET", `Staging host root is not vacant at ${host}`, { host });
|
|
1508
|
-
const
|
|
1509
|
-
const roots = [];
|
|
1510
|
-
const missing = [];
|
|
1511
|
-
for (const path of [
|
|
1558
|
+
const reads = [
|
|
1512
1559
|
..._src_core.HOST_PATHS,
|
|
1513
1560
|
..._src_core.CANON_PATHS,
|
|
1514
1561
|
..._src_core.REFERENCE_PATHS
|
|
1515
|
-
]) {
|
|
1562
|
+
].map((path) => {
|
|
1516
1563
|
const full = resolveContainedPath(source, path);
|
|
1517
1564
|
if (full === void 0) throw new _src_core.ScaffoldError("INVALID", `Vendored path leaves its checkout at ${path}`, {
|
|
1518
1565
|
checkout: source,
|
|
1519
1566
|
path
|
|
1520
1567
|
});
|
|
1521
|
-
if (isPhysicalFile(full)) {
|
|
1522
|
-
vendored
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1568
|
+
if (isPhysicalFile(full)) return {
|
|
1569
|
+
vendored: [path],
|
|
1570
|
+
roots: [],
|
|
1571
|
+
missing: []
|
|
1572
|
+
};
|
|
1573
|
+
if (!isPhysicalDirectory(full)) return {
|
|
1574
|
+
vendored: [],
|
|
1575
|
+
roots: [],
|
|
1576
|
+
missing: _src_core.CANON_PATHS.includes(path) ? [] : [path]
|
|
1577
|
+
};
|
|
1578
|
+
const nested = listDirectories(full);
|
|
1579
|
+
return {
|
|
1580
|
+
vendored: listFiles(full).map((name) => `${path}/${name}`).filter((destination) => !matchesSensitivePath(destination)),
|
|
1581
|
+
roots: [path, ...nested.map((name) => `${path}/${name}`).filter((rooted) => !matchesSensitivePath(rooted))],
|
|
1582
|
+
missing: []
|
|
1583
|
+
};
|
|
1584
|
+
});
|
|
1585
|
+
const vendored = reads.flatMap((read) => read.vendored);
|
|
1586
|
+
const roots = reads.flatMap((read) => read.roots).sort();
|
|
1587
|
+
const missing = reads.flatMap((read) => read.missing);
|
|
1539
1588
|
if (missing.length > 0) throw new _src_core.ScaffoldError("TARGET", "The checkout does not carry every vendored path", {
|
|
1540
1589
|
checkout: source,
|
|
1541
1590
|
missing
|
|
@@ -1555,9 +1604,8 @@ function stageHost(checkout, host, options) {
|
|
|
1555
1604
|
path
|
|
1556
1605
|
});
|
|
1557
1606
|
}
|
|
1558
|
-
const
|
|
1559
|
-
const candidates =
|
|
1560
|
-
for (const destination of vendored) {
|
|
1607
|
+
const storages = vendored.map((destination) => pathToStorage(destination));
|
|
1608
|
+
const candidates = vendored.map((destination, index) => {
|
|
1561
1609
|
const full = resolveContainedPath(source, destination);
|
|
1562
1610
|
if (full === void 0) throw new _src_core.ScaffoldError("INVALID", `Vendored path leaves its checkout at ${destination}`, {
|
|
1563
1611
|
checkout: source,
|
|
@@ -1569,16 +1617,13 @@ function stageHost(checkout, host, options) {
|
|
|
1569
1617
|
path: destination,
|
|
1570
1618
|
limit: _src_core.MAX_ARTIFACT_BYTES
|
|
1571
1619
|
});
|
|
1572
|
-
if (
|
|
1620
|
+
if (entry.storage === "manifest.json" || storages.indexOf(entry.storage) < index) throw new _src_core.ScaffoldError("TARGET", `Two vendored paths claim the storage name ${entry.storage}`, {
|
|
1573
1621
|
checkout: source,
|
|
1574
1622
|
path: destination,
|
|
1575
1623
|
storage: entry.storage
|
|
1576
1624
|
});
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
}
|
|
1580
|
-
candidates.sort((first, second) => first.storage < second.storage ? -1 : 1);
|
|
1581
|
-
roots.sort();
|
|
1625
|
+
return entry;
|
|
1626
|
+
}).sort((first, second) => first.storage < second.storage ? -1 : 1);
|
|
1582
1627
|
const baseline = resolveContainedPath(source, options?.inventory ?? _src_core.HOST_INVENTORY_PATH);
|
|
1583
1628
|
if (baseline === void 0) throw new _src_core.ScaffoldError("INVALID", "Surface inventory leaves its checkout", {
|
|
1584
1629
|
checkout: source,
|
|
@@ -1598,8 +1643,7 @@ function stageHost(checkout, host, options) {
|
|
|
1598
1643
|
host: root,
|
|
1599
1644
|
...established.success ? {} : { error: established.error }
|
|
1600
1645
|
});
|
|
1601
|
-
const entries =
|
|
1602
|
-
for (const entry of candidates) {
|
|
1646
|
+
const entries = candidates.map((entry) => {
|
|
1603
1647
|
const origin = resolveContainedPath(source, entry.destination);
|
|
1604
1648
|
const destination = resolveContainedPath(root, entry.storage);
|
|
1605
1649
|
if (origin === void 0 || destination === void 0) throw new _src_core.ScaffoldError("INVALID", `Vendored path leaves its root at ${entry.destination}`, {
|
|
@@ -1623,17 +1667,16 @@ function stageHost(checkout, host, options) {
|
|
|
1623
1667
|
host: root,
|
|
1624
1668
|
storage: entry.storage
|
|
1625
1669
|
});
|
|
1626
|
-
|
|
1670
|
+
return {
|
|
1627
1671
|
...entry,
|
|
1628
1672
|
digest
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1673
|
+
};
|
|
1674
|
+
});
|
|
1631
1675
|
const collisions = readSurfaceCollisions((0, node_path.join)(root, "guides"));
|
|
1632
|
-
const growth = []
|
|
1633
|
-
if (published !== void 0) for (const [name, owners] of collisions) {
|
|
1676
|
+
const growth = published === void 0 ? [] : [...collisions].flatMap(([name, owners]) => {
|
|
1634
1677
|
const record = published.get(name);
|
|
1635
|
-
|
|
1636
|
-
}
|
|
1678
|
+
return record !== void 0 && owners.every((owner) => record.includes(owner)) ? [] : [`${name} staged (${owners.join(", ")}), recorded (${record?.join(", ") ?? "absent"})`];
|
|
1679
|
+
});
|
|
1637
1680
|
if (growth.length > 0) throw new _src_core.ScaffoldError("TARGET", `Staged Surface collisions differ from the inventory: ${growth.join("; ")}`, {
|
|
1638
1681
|
checkout: source,
|
|
1639
1682
|
host: root,
|
|
@@ -1693,26 +1736,19 @@ function readSurfaceCollisions(root) {
|
|
|
1693
1736
|
error: loaded.error
|
|
1694
1737
|
});
|
|
1695
1738
|
const guide = loaded.value;
|
|
1696
|
-
const
|
|
1697
|
-
for (const path of listFiles(root)) {
|
|
1698
|
-
if (path.includes("/") || (0, node_path.extname)(path) !== ".md" || path === "README.md") continue;
|
|
1739
|
+
const claims = listFiles(root).filter((path) => !path.includes("/") && (0, node_path.extname)(path) === ".md" && path !== "README.md").flatMap((path) => {
|
|
1699
1740
|
const text = readFileText(root, path);
|
|
1700
1741
|
if (text === void 0) throw new _src_core.ScaffoldError("TARGET", `Surface guide cannot be read at ${path}`, {
|
|
1701
1742
|
root,
|
|
1702
1743
|
path
|
|
1703
1744
|
});
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
for (const name of [...owners.keys()].sort()) {
|
|
1712
|
-
const claimed = owners.get(name);
|
|
1713
|
-
if (claimed !== void 0 && claimed.size > 1) collisions.set(name, [...claimed].sort());
|
|
1714
|
-
}
|
|
1715
|
-
return collisions;
|
|
1745
|
+
return guide.createGuide(text).surface().map((symbol) => [symbol.name, (0, node_path.basename)(path, ".md")]);
|
|
1746
|
+
});
|
|
1747
|
+
const grouped = Map.groupBy(claims, ([name]) => name);
|
|
1748
|
+
return new Map([...grouped.keys()].sort().flatMap((name) => {
|
|
1749
|
+
const owners = [...new Set((grouped.get(name) ?? []).map(([, owner]) => owner))];
|
|
1750
|
+
return owners.length > 1 ? [[name, owners.sort()]] : [];
|
|
1751
|
+
}));
|
|
1716
1752
|
}
|
|
1717
1753
|
/**
|
|
1718
1754
|
* Reads the Surface collision baseline from a committed inventory.
|
|
@@ -2462,17 +2498,15 @@ var WriteTransaction = class {
|
|
|
2462
2498
|
paths
|
|
2463
2499
|
});
|
|
2464
2500
|
this.#target = (0, node_path.resolve)(target);
|
|
2465
|
-
|
|
2466
|
-
for (const path of paths) {
|
|
2501
|
+
this.#expectations = new Map(Array.from(paths, (path) => {
|
|
2467
2502
|
const expectation = readExpectation(this.#resolve(this.#target, path));
|
|
2468
2503
|
if (expectation === void 0) throw new _src_core.ScaffoldError("TARGET", `The destination at ${path} is a shape this package will not write over.`, {
|
|
2469
2504
|
target: this.#target,
|
|
2470
2505
|
path
|
|
2471
2506
|
});
|
|
2472
|
-
|
|
2473
|
-
}
|
|
2474
|
-
this.#expectations
|
|
2475
|
-
const destinations = new Set([...expectations.values()].map((expectation) => expectation.path));
|
|
2507
|
+
return [path, expectation];
|
|
2508
|
+
}));
|
|
2509
|
+
const destinations = new Set([...this.#expectations.values()].map((expectation) => expectation.path));
|
|
2476
2510
|
for (const precondition of preconditions ?? []) {
|
|
2477
2511
|
if (!destinations.has(precondition.path)) throw new _src_core.ScaffoldError("INVALID", `The precondition at ${precondition.path} names no destination of this transaction.`, {
|
|
2478
2512
|
target: this.#target,
|
|
@@ -2661,8 +2695,14 @@ var WriteTransaction = class {
|
|
|
2661
2695
|
const taken = [];
|
|
2662
2696
|
const applied = (0, _orkestrel_contract.attempt)(() => {
|
|
2663
2697
|
this.#preflight();
|
|
2664
|
-
for (const path of this.#staged)
|
|
2665
|
-
|
|
2698
|
+
for (const path of this.#staged) {
|
|
2699
|
+
this.#promote(path);
|
|
2700
|
+
promoted.push(path);
|
|
2701
|
+
}
|
|
2702
|
+
for (const path of this.#taken) {
|
|
2703
|
+
this.#take(path);
|
|
2704
|
+
taken.push(path);
|
|
2705
|
+
}
|
|
2666
2706
|
});
|
|
2667
2707
|
this.#open = false;
|
|
2668
2708
|
if (!applied.success) {
|
|
@@ -2719,8 +2759,7 @@ var WriteTransaction = class {
|
|
|
2719
2759
|
path
|
|
2720
2760
|
});
|
|
2721
2761
|
const base = (0, node_path.resolve)(root);
|
|
2722
|
-
|
|
2723
|
-
for (let depth = 0; depth <= 64; depth += 1) {
|
|
2762
|
+
for (const ancestor of listAncestors(destination, 66).slice(1)) {
|
|
2724
2763
|
const state = readExpectation(ancestor);
|
|
2725
2764
|
if (state === void 0 || state.shape === "file") throw new _src_core.ScaffoldError("WRITE", `The path ${path} passes through a link or a file.`, {
|
|
2726
2765
|
root,
|
|
@@ -2728,9 +2767,6 @@ var WriteTransaction = class {
|
|
|
2728
2767
|
ancestor
|
|
2729
2768
|
});
|
|
2730
2769
|
if (ancestor === base) return destination;
|
|
2731
|
-
const parent = (0, node_path.dirname)(ancestor);
|
|
2732
|
-
if (parent === ancestor) break;
|
|
2733
|
-
ancestor = parent;
|
|
2734
2770
|
}
|
|
2735
2771
|
throw new _src_core.ScaffoldError("INVALID", `The path ${path} does not resolve beneath its root.`, {
|
|
2736
2772
|
root,
|
|
@@ -2739,41 +2775,44 @@ var WriteTransaction = class {
|
|
|
2739
2775
|
}
|
|
2740
2776
|
#establish(path) {
|
|
2741
2777
|
const destination = (0, node_path.resolve)(path);
|
|
2742
|
-
const missing =
|
|
2743
|
-
let current = destination;
|
|
2744
|
-
let anchor = readAnchor(current);
|
|
2745
|
-
for (let depth = 0; anchor === void 0 && depth <= 64; depth += 1) {
|
|
2746
|
-
const state = readExpectation(current);
|
|
2747
|
-
if (state === void 0 || state.shape !== "absent") throw new _src_core.ScaffoldError("WRITE", `The directory at ${current} is not a physical directory.`, { path: destination });
|
|
2748
|
-
missing.push(current);
|
|
2749
|
-
const parent = (0, node_path.dirname)(current);
|
|
2750
|
-
if (parent === current) throw new _src_core.ScaffoldError("WRITE", `The directory at ${destination} has no existing ancestor.`, { path: destination });
|
|
2751
|
-
current = parent;
|
|
2752
|
-
anchor = readAnchor(current);
|
|
2753
|
-
}
|
|
2754
|
-
if (anchor === void 0) throw new _src_core.ScaffoldError("WRITE", `The directory at ${destination} is nested past the ceiling.`, {
|
|
2755
|
-
path: destination,
|
|
2756
|
-
limit: 64
|
|
2757
|
-
});
|
|
2778
|
+
const { anchor: located, missing } = this.#locate(destination);
|
|
2758
2779
|
const created = [];
|
|
2759
2780
|
for (const segment of [...missing].reverse()) {
|
|
2781
|
+
const anchor = created.at(-1) ?? located;
|
|
2760
2782
|
if (!matchesAnchor(anchor)) throw new _src_core.ScaffoldError("WRITE", `The directory at ${anchor.path} changed while writing.`, {
|
|
2761
2783
|
path: destination,
|
|
2762
2784
|
ancestor: anchor.path
|
|
2763
2785
|
});
|
|
2764
2786
|
(0, node_fs.mkdirSync)(segment);
|
|
2765
|
-
this.#created.push(segment);
|
|
2766
2787
|
const established = readAnchor(segment);
|
|
2788
|
+
this.#created.push(established ?? segment);
|
|
2767
2789
|
if (established === void 0) throw new _src_core.ScaffoldError("WRITE", `The directory at ${segment} changed while writing.`, { path: destination });
|
|
2768
|
-
anchor = established;
|
|
2769
|
-
this.#created[this.#created.length - 1] = established;
|
|
2770
2790
|
created.push(established);
|
|
2771
2791
|
}
|
|
2772
2792
|
return {
|
|
2773
|
-
anchor,
|
|
2793
|
+
anchor: created.at(-1) ?? located,
|
|
2774
2794
|
created
|
|
2775
2795
|
};
|
|
2776
2796
|
}
|
|
2797
|
+
#locate(destination) {
|
|
2798
|
+
const missing = [];
|
|
2799
|
+
for (const [depth, current] of listAncestors(destination, 66).entries()) {
|
|
2800
|
+
const anchor = readAnchor(current);
|
|
2801
|
+
if (anchor !== void 0) return {
|
|
2802
|
+
anchor,
|
|
2803
|
+
missing
|
|
2804
|
+
};
|
|
2805
|
+
if (depth > 64) break;
|
|
2806
|
+
const state = readExpectation(current);
|
|
2807
|
+
if (state === void 0 || state.shape !== "absent") throw new _src_core.ScaffoldError("WRITE", `The directory at ${current} is not a physical directory.`, { path: destination });
|
|
2808
|
+
missing.push(current);
|
|
2809
|
+
if ((0, node_path.dirname)(current) === current) throw new _src_core.ScaffoldError("WRITE", `The directory at ${destination} has no existing ancestor.`, { path: destination });
|
|
2810
|
+
}
|
|
2811
|
+
throw new _src_core.ScaffoldError("WRITE", `The directory at ${destination} is nested past the ceiling.`, {
|
|
2812
|
+
path: destination,
|
|
2813
|
+
limit: 64
|
|
2814
|
+
});
|
|
2815
|
+
}
|
|
2777
2816
|
#preflight() {
|
|
2778
2817
|
for (const anchor of this.#created) {
|
|
2779
2818
|
if ((0, _orkestrel_contract.isString)(anchor)) throw new _src_core.ScaffoldError("WRITE", `The directory at ${anchor} changed while writing.`, {
|
|
@@ -2794,7 +2833,7 @@ var WriteTransaction = class {
|
|
|
2794
2833
|
path
|
|
2795
2834
|
});
|
|
2796
2835
|
}
|
|
2797
|
-
#promote(path
|
|
2836
|
+
#promote(path) {
|
|
2798
2837
|
const expectation = this.#expectation(path);
|
|
2799
2838
|
if (!matchesExpectation(expectation)) throw new _src_core.ScaffoldError("WRITE", `The destination at ${path} moved while writing.`, { path });
|
|
2800
2839
|
const staged = this.#resolve(this.#stage, path);
|
|
@@ -2805,23 +2844,19 @@ var WriteTransaction = class {
|
|
|
2805
2844
|
(0, node_fs.linkSync)(expectation.path, backup);
|
|
2806
2845
|
}
|
|
2807
2846
|
(0, node_fs.renameSync)(staged, expectation.path);
|
|
2808
|
-
promoted.push(path);
|
|
2809
2847
|
}
|
|
2810
|
-
#take(path
|
|
2848
|
+
#take(path) {
|
|
2811
2849
|
const expectation = this.#expectation(path);
|
|
2812
2850
|
if (!matchesExpectation(expectation)) throw new _src_core.ScaffoldError("WRITE", `The destination at ${path} moved while writing.`, { path });
|
|
2813
2851
|
const backup = this.#resolve(this.#backup, path);
|
|
2814
2852
|
(0, node_fs.mkdirSync)((0, node_path.dirname)(backup), { recursive: true });
|
|
2815
2853
|
(0, node_fs.renameSync)(expectation.path, backup);
|
|
2816
|
-
taken.push(path);
|
|
2817
2854
|
}
|
|
2818
2855
|
#recover(promoted, taken) {
|
|
2819
|
-
const
|
|
2820
|
-
for (const path of [...taken].reverse()) {
|
|
2856
|
+
const restoring = [...[...taken].reverse().flatMap((path) => {
|
|
2821
2857
|
const restored = (0, _orkestrel_contract.attempt)(() => (0, node_fs.renameSync)(this.#resolve(this.#backup, path), this.#expectation(path).path));
|
|
2822
|
-
|
|
2823
|
-
}
|
|
2824
|
-
for (const path of [...promoted].reverse()) {
|
|
2858
|
+
return restored.success ? [] : [restored.error];
|
|
2859
|
+
}), ...[...promoted].reverse().flatMap((path) => {
|
|
2825
2860
|
const expectation = this.#expectation(path);
|
|
2826
2861
|
const restored = (0, _orkestrel_contract.attempt)(() => {
|
|
2827
2862
|
if (expectation.shape === "absent") {
|
|
@@ -2830,21 +2865,23 @@ var WriteTransaction = class {
|
|
|
2830
2865
|
}
|
|
2831
2866
|
(0, node_fs.renameSync)(this.#resolve(this.#backup, path), expectation.path);
|
|
2832
2867
|
});
|
|
2833
|
-
|
|
2834
|
-
}
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
}
|
|
2842
|
-
for (const created of [...this.#created].reverse()) {
|
|
2868
|
+
return restored.success ? [] : [restored.error];
|
|
2869
|
+
})];
|
|
2870
|
+
const cleared = restoring.length === 0 ? (0, _orkestrel_contract.attempt)(() => (0, node_fs.rmSync)(this.#root, {
|
|
2871
|
+
recursive: true,
|
|
2872
|
+
force: true
|
|
2873
|
+
})) : void 0;
|
|
2874
|
+
const clearing = cleared === void 0 || cleared.success ? [] : [cleared.error];
|
|
2875
|
+
const removing = [...this.#created].reverse().flatMap((created) => {
|
|
2843
2876
|
const path = (0, _orkestrel_contract.isString)(created) ? created : created.path;
|
|
2844
2877
|
const removed = (0, _orkestrel_contract.attempt)(() => (0, node_fs.rmdirSync)(path));
|
|
2845
|
-
|
|
2846
|
-
}
|
|
2847
|
-
return
|
|
2878
|
+
return !removed.success && !matchesMissingPath(removed.error) ? [removed.error] : [];
|
|
2879
|
+
});
|
|
2880
|
+
return [
|
|
2881
|
+
...restoring,
|
|
2882
|
+
...clearing,
|
|
2883
|
+
...removing
|
|
2884
|
+
];
|
|
2848
2885
|
}
|
|
2849
2886
|
#stagePath(path) {
|
|
2850
2887
|
const staged = this.#resolve(this.#stage, path);
|
|
@@ -3071,18 +3108,17 @@ var Materializer = class {
|
|
|
3071
3108
|
const derived = this.#derive(accepted, directory, hydrated);
|
|
3072
3109
|
this.#reconfirm(derived.findings, preview.findings, directory);
|
|
3073
3110
|
const drifted = new Map(derived.findings.map((finding) => [finding.path, finding.drift]));
|
|
3074
|
-
const
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
}
|
|
3111
|
+
const judged = hydrated.artifacts.map((artifact) => ({
|
|
3112
|
+
artifact,
|
|
3113
|
+
drift: drifted.get(artifact.path)
|
|
3114
|
+
}));
|
|
3115
|
+
const skipped = judged.flatMap(({ artifact, drift }) => drift === "missing" || drift === "stale" ? [] : [artifact.path]);
|
|
3116
|
+
const selected = judged.flatMap(({ artifact, drift }) => drift === "missing" || drift === "stale" ? [{
|
|
3117
|
+
artifact,
|
|
3118
|
+
missing: drift === "missing"
|
|
3119
|
+
}] : []);
|
|
3120
|
+
const preconditions = selected.map(({ artifact, missing }) => this.#bind(directory, artifact.path, missing));
|
|
3121
|
+
const writes = selected.map(({ artifact }) => artifact);
|
|
3086
3122
|
return this.#apply(directory, writes, [], skipped, preconditions);
|
|
3087
3123
|
}
|
|
3088
3124
|
/**
|
|
@@ -3107,15 +3143,15 @@ var Materializer = class {
|
|
|
3107
3143
|
const writes = [];
|
|
3108
3144
|
const skipped = [];
|
|
3109
3145
|
const preconditions = [];
|
|
3110
|
-
|
|
3146
|
+
const spent = [];
|
|
3111
3147
|
for (const fetched of accepted) {
|
|
3112
|
-
const hex = fetched.lookup === "found" ? (0, _src_core.contentToHex)(fetched.content) : fetched.observed === void 0 ? this.#reference(fetched.path,
|
|
3148
|
+
const hex = fetched.lookup === "found" ? (0, _src_core.contentToHex)(fetched.content) : fetched.observed === void 0 ? this.#reference(fetched.path, _src_core.MAX_TOTAL_ARTIFACT_BYTES - (0, _src_core.computeTotal)(spent)) : void 0;
|
|
3113
3149
|
if (hex === void 0 || fetched.observed === hex) {
|
|
3114
3150
|
skipped.push(fetched.path);
|
|
3115
3151
|
continue;
|
|
3116
3152
|
}
|
|
3117
|
-
|
|
3118
|
-
if (
|
|
3153
|
+
spent.push(hex.length / 2);
|
|
3154
|
+
if ((0, _src_core.computeTotal)(spent) > _src_core.MAX_TOTAL_ARTIFACT_BYTES) throw this.#error("TARGET", "The guide mirrors exceed the total artifact byte limit.");
|
|
3119
3155
|
const current = readFileHex(directory, fetched.path);
|
|
3120
3156
|
if (current !== fetched.observed) throw this.#error("TARGET", `The mirror at ${fetched.path} moved since it was fetched.`, {
|
|
3121
3157
|
target: directory,
|
|
@@ -3248,18 +3284,10 @@ var Materializer = class {
|
|
|
3248
3284
|
const derived = this.#derive(accepted, directory);
|
|
3249
3285
|
this.#reconfirmCandidates(derived.findings, preview.findings, directory);
|
|
3250
3286
|
const tracked = new Set(state.tracked);
|
|
3251
|
-
const
|
|
3252
|
-
const skipped =
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
if (!tracked.has(finding.path) || matchesProtectedPath(finding.path)) {
|
|
3256
|
-
skipped.push(finding.path);
|
|
3257
|
-
continue;
|
|
3258
|
-
}
|
|
3259
|
-
removals.push(finding.path);
|
|
3260
|
-
}
|
|
3261
|
-
const preconditions = [];
|
|
3262
|
-
for (const path of removals) preconditions.push(this.#bind(directory, path, false));
|
|
3287
|
+
const foreign = derived.findings.filter((finding) => finding.drift === "foreign").map((finding) => finding.path);
|
|
3288
|
+
const skipped = foreign.filter((path) => !tracked.has(path) || matchesProtectedPath(path));
|
|
3289
|
+
const removals = foreign.filter((path) => tracked.has(path) && !matchesProtectedPath(path));
|
|
3290
|
+
const preconditions = removals.map((path) => this.#bind(directory, path, false));
|
|
3263
3291
|
return this.#purge(directory, removals, skipped, preconditions);
|
|
3264
3292
|
}
|
|
3265
3293
|
/**
|
|
@@ -3319,49 +3347,51 @@ var Materializer = class {
|
|
|
3319
3347
|
if (this.#entries.size !== manifest.entries.length) throw this.#error("TARGET", "The vendored host manifest maps two files to one destination.", { host: root });
|
|
3320
3348
|
}
|
|
3321
3349
|
#hydrate(plan) {
|
|
3322
|
-
const
|
|
3323
|
-
|
|
3350
|
+
const groups = [];
|
|
3351
|
+
const spent = [];
|
|
3324
3352
|
for (const artifact of plan.artifacts) {
|
|
3325
3353
|
if (artifact.origin !== "host") {
|
|
3326
|
-
|
|
3327
|
-
|
|
3354
|
+
spent.push((0, _src_core.computeBytes)(artifact.content));
|
|
3355
|
+
groups.push([artifact]);
|
|
3328
3356
|
continue;
|
|
3329
3357
|
}
|
|
3330
|
-
const expanded = this.#expand(artifact,
|
|
3331
|
-
|
|
3332
|
-
|
|
3358
|
+
const expanded = this.#expand(artifact, _src_core.MAX_TOTAL_ARTIFACT_BYTES - (0, _src_core.computeTotal)(spent));
|
|
3359
|
+
spent.push((0, _src_core.computeTotal)(expanded.map((one) => one.origin === "host" ? (one.hex?.length ?? 0) / 2 : (0, _src_core.computeBytes)(one.content))));
|
|
3360
|
+
groups.push(expanded);
|
|
3333
3361
|
}
|
|
3334
|
-
if (
|
|
3362
|
+
if ((0, _src_core.computeTotal)(spent) > _src_core.MAX_TOTAL_ARTIFACT_BYTES) throw this.#error("TARGET", "The hydrated plan retains more bytes than one plan may.", {
|
|
3335
3363
|
host: this.#root,
|
|
3336
3364
|
limit: _src_core.MAX_TOTAL_ARTIFACT_BYTES
|
|
3337
3365
|
});
|
|
3338
3366
|
return {
|
|
3339
3367
|
blueprint: plan.blueprint,
|
|
3340
3368
|
groups: plan.groups,
|
|
3341
|
-
artifacts
|
|
3369
|
+
artifacts: groups.flat()
|
|
3342
3370
|
};
|
|
3343
3371
|
}
|
|
3344
3372
|
#derive(plan, target, hydrated = this.#hydrate(plan)) {
|
|
3345
|
-
const paths = new Set(
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
target
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3373
|
+
const paths = /* @__PURE__ */ new Set([
|
|
3374
|
+
...hydrated.artifacts.map((artifact) => artifact.path),
|
|
3375
|
+
...this.#roots(plan).flatMap((root) => {
|
|
3376
|
+
const directory = resolveContainedPath(target, root);
|
|
3377
|
+
if (directory === void 0) throw this.#error("TARGET", `The owned root at ${root} leaves its target.`, {
|
|
3378
|
+
target,
|
|
3379
|
+
path: root
|
|
3380
|
+
});
|
|
3381
|
+
return listFiles(directory).map((name) => `${root}/${name}`);
|
|
3382
|
+
}),
|
|
3383
|
+
...listCanonPaths(target, plan.groups)
|
|
3384
|
+
]);
|
|
3355
3385
|
const questions = [];
|
|
3356
3386
|
if (plan.groups.includes("guides")) {
|
|
3357
3387
|
const directory = resolveContainedPath(target, "guides");
|
|
3358
|
-
|
|
3388
|
+
const spent = [];
|
|
3359
3389
|
for (const file of directory === void 0 ? [] : listFiles(directory)) {
|
|
3360
3390
|
const path = `guides/${file}`;
|
|
3361
3391
|
if (file.includes("/") || file === "README.md" || !file.endsWith(".md") || path === (0, _src_core.nameToGuide)(plan.blueprint.name)) continue;
|
|
3362
|
-
const hosted = this.#reference(path,
|
|
3392
|
+
const hosted = this.#reference(path, _src_core.MAX_TOTAL_ARTIFACT_BYTES - (0, _src_core.computeTotal)(spent));
|
|
3363
3393
|
if (hosted === void 0) continue;
|
|
3364
|
-
|
|
3394
|
+
spent.push(hosted.length / 2);
|
|
3365
3395
|
const present = readFileHex(target, path);
|
|
3366
3396
|
if (present !== void 0 && present !== hosted) questions.push({
|
|
3367
3397
|
field: "guides",
|
|
@@ -3386,20 +3416,15 @@ var Materializer = class {
|
|
|
3386
3416
|
return readFileHex(this.#root, path, Math.max(0, Math.min(_src_core.MAX_ARTIFACT_BYTES, budget)));
|
|
3387
3417
|
}
|
|
3388
3418
|
#roots(plan) {
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
if (artifact.origin !== "host") continue;
|
|
3419
|
+
return [...new Set(plan.artifacts.flatMap((artifact) => {
|
|
3420
|
+
if (artifact.origin !== "host") return [];
|
|
3392
3421
|
const source = artifact.source ?? artifact.path;
|
|
3393
|
-
if (this.#manifest?.roots.includes(source) === true)
|
|
3394
|
-
|
|
3395
|
-
continue;
|
|
3396
|
-
}
|
|
3397
|
-
if (this.#manifest !== void 0) continue;
|
|
3422
|
+
if (this.#manifest?.roots.includes(source) === true) return [artifact.path];
|
|
3423
|
+
if (this.#manifest !== void 0) return [];
|
|
3398
3424
|
const root = this.#root;
|
|
3399
3425
|
const directory = root === void 0 ? void 0 : resolveContainedPath(root, source);
|
|
3400
|
-
|
|
3401
|
-
}
|
|
3402
|
-
return [...roots];
|
|
3426
|
+
return directory !== void 0 && isPhysicalDirectory(directory) ? [artifact.path] : [];
|
|
3427
|
+
}))];
|
|
3403
3428
|
}
|
|
3404
3429
|
#expand(artifact, remaining) {
|
|
3405
3430
|
const manifest = this.#manifest;
|
|
@@ -3412,15 +3437,15 @@ var Materializer = class {
|
|
|
3412
3437
|
source
|
|
3413
3438
|
});
|
|
3414
3439
|
const expanded = [];
|
|
3415
|
-
|
|
3440
|
+
const spent = [];
|
|
3416
3441
|
for (const entry of matched) {
|
|
3417
3442
|
const path = this.#remap(artifact, entry.destination);
|
|
3418
3443
|
if ((0, _src_core.isRetainedPath)(path)) {
|
|
3419
3444
|
expanded.push(this.#presence(artifact, path, entry.destination));
|
|
3420
3445
|
continue;
|
|
3421
3446
|
}
|
|
3422
|
-
const hex = this.#read(entry,
|
|
3423
|
-
|
|
3447
|
+
const hex = this.#read(entry, remaining - (0, _src_core.computeTotal)(spent));
|
|
3448
|
+
spent.push(hex.length / 2);
|
|
3424
3449
|
expanded.push(this.#hydrated(artifact, path, entry.destination, hex));
|
|
3425
3450
|
}
|
|
3426
3451
|
return expanded;
|
|
@@ -3441,7 +3466,7 @@ var Materializer = class {
|
|
|
3441
3466
|
source
|
|
3442
3467
|
});
|
|
3443
3468
|
const expanded = [];
|
|
3444
|
-
|
|
3469
|
+
const spent = [];
|
|
3445
3470
|
for (const name of listFiles(full)) {
|
|
3446
3471
|
const path = `${artifact.path}/${name}`;
|
|
3447
3472
|
const destination = `${source}/${name}`;
|
|
@@ -3449,8 +3474,8 @@ var Materializer = class {
|
|
|
3449
3474
|
expanded.push(this.#presence(artifact, path, destination));
|
|
3450
3475
|
continue;
|
|
3451
3476
|
}
|
|
3452
|
-
const hex = this.#readRoot(destination,
|
|
3453
|
-
|
|
3477
|
+
const hex = this.#readRoot(destination, remaining - (0, _src_core.computeTotal)(spent));
|
|
3478
|
+
spent.push(hex.length / 2);
|
|
3454
3479
|
expanded.push(this.#hydrated(artifact, path, destination, hex));
|
|
3455
3480
|
}
|
|
3456
3481
|
return expanded;
|
|
@@ -3458,16 +3483,15 @@ var Materializer = class {
|
|
|
3458
3483
|
#empty(plan) {
|
|
3459
3484
|
const manifest = this.#manifest;
|
|
3460
3485
|
if (manifest === void 0) return [];
|
|
3461
|
-
const directories =
|
|
3462
|
-
|
|
3463
|
-
if (artifact.origin !== "host") continue;
|
|
3486
|
+
const directories = plan.artifacts.flatMap((artifact) => {
|
|
3487
|
+
if (artifact.origin !== "host") return [];
|
|
3464
3488
|
const source = artifact.source ?? artifact.path;
|
|
3465
|
-
|
|
3466
|
-
if (root !== source && !root.startsWith(`${source}/`))
|
|
3467
|
-
|
|
3468
|
-
}
|
|
3469
|
-
}
|
|
3470
|
-
return [...directories].sort();
|
|
3489
|
+
return manifest.roots.flatMap((root) => {
|
|
3490
|
+
if (root !== source && !root.startsWith(`${source}/`)) return [];
|
|
3491
|
+
return manifest.entries.some((entry) => entry.destination === root || entry.destination.startsWith(`${root}/`)) ? [] : [this.#remap(artifact, root)];
|
|
3492
|
+
});
|
|
3493
|
+
});
|
|
3494
|
+
return [...new Set(directories)].sort();
|
|
3471
3495
|
}
|
|
3472
3496
|
#remap(artifact, destination) {
|
|
3473
3497
|
const source = artifact.source ?? artifact.path;
|
|
@@ -3664,11 +3688,7 @@ var Materializer = class {
|
|
|
3664
3688
|
#fill(writes) {
|
|
3665
3689
|
const value = this.#value;
|
|
3666
3690
|
if (value === void 0) return void 0;
|
|
3667
|
-
const destinations =
|
|
3668
|
-
for (const artifact of writes) {
|
|
3669
|
-
if (artifact.origin !== "host") continue;
|
|
3670
|
-
destinations.add(artifact.source ?? artifact.path);
|
|
3671
|
-
}
|
|
3691
|
+
const destinations = new Set(writes.flatMap((artifact) => artifact.origin === "host" ? [artifact.source ?? artifact.path] : []));
|
|
3672
3692
|
if (destinations.size === 0) return void 0;
|
|
3673
3693
|
const opened = (0, _orkestrel_contract.attempt)(() => (0, node_fs.mkdtempSync)((0, node_path.join)((0, node_os.tmpdir)(), "orkestrel-scaffold-fill-")));
|
|
3674
3694
|
if (!opened.success) throw this.#error("WRITE", "The supplied host could not be filled into a private root.", { error: opened.error });
|
|
@@ -3738,8 +3758,7 @@ var Materializer = class {
|
|
|
3738
3758
|
}
|
|
3739
3759
|
#recatalog(entries) {
|
|
3740
3760
|
const layers = (0, _src_core.catalogToLayers)(entries);
|
|
3741
|
-
const placed =
|
|
3742
|
-
for (const [index, layer] of layers.entries()) for (const name of layer) placed.set(name, index);
|
|
3761
|
+
const placed = new Map(layers.flatMap((layer, index) => layer.map((name) => [name, index])));
|
|
3743
3762
|
const rows = entries.map((entry) => {
|
|
3744
3763
|
if (entry.lookup !== "found") return [
|
|
3745
3764
|
`\`${entry.name}\``,
|
|
@@ -3766,11 +3785,7 @@ var Materializer = class {
|
|
|
3766
3785
|
"Runtime dependencies",
|
|
3767
3786
|
"Peer dependencies"
|
|
3768
3787
|
];
|
|
3769
|
-
const widths = headers.map((header, column) =>
|
|
3770
|
-
let width = Math.max(3, header.length);
|
|
3771
|
-
for (const row of rows) width = Math.max(width, row[column]?.length ?? 0);
|
|
3772
|
-
return width;
|
|
3773
|
-
});
|
|
3788
|
+
const widths = headers.map((header, column) => Math.max(3, header.length, ...rows.map((row) => row[column]?.length ?? 0)));
|
|
3774
3789
|
const table = [
|
|
3775
3790
|
headers,
|
|
3776
3791
|
widths.map((width) => "-".repeat(width)),
|
|
@@ -3826,6 +3841,66 @@ var Materializer = class {
|
|
|
3826
3841
|
}
|
|
3827
3842
|
};
|
|
3828
3843
|
//#endregion
|
|
3844
|
+
//#region src/server/ReadAllowance.ts
|
|
3845
|
+
/**
|
|
3846
|
+
* Represents the byte allowance one upstream call spends across every read it makes.
|
|
3847
|
+
*
|
|
3848
|
+
* @remarks
|
|
3849
|
+
* Each call constructs its own allowance from the reader's budget, so concurrent
|
|
3850
|
+
* calls never spend each other's bytes. A read that brings the call exactly to
|
|
3851
|
+
* its budget leaves `remaining` at zero and is accepted; the next request is
|
|
3852
|
+
* refused before it is sent, and a body that passes the budget is refused at the
|
|
3853
|
+
* chunk that overshoots.
|
|
3854
|
+
*
|
|
3855
|
+
* @example
|
|
3856
|
+
* ```ts
|
|
3857
|
+
* import { ReadAllowance } from '@orkestrel/scaffold/server'
|
|
3858
|
+
*
|
|
3859
|
+
* const allowance = new ReadAllowance(1_024)
|
|
3860
|
+
* allowance.spend(1_000)
|
|
3861
|
+
* allowance.remaining // 24
|
|
3862
|
+
* ```
|
|
3863
|
+
*/
|
|
3864
|
+
var ReadAllowance = class {
|
|
3865
|
+
#remaining;
|
|
3866
|
+
/**
|
|
3867
|
+
* Constructs an allowance holding the whole budget.
|
|
3868
|
+
*
|
|
3869
|
+
* @param budget - The bytes the call may read in total.
|
|
3870
|
+
* @throws {@link ScaffoldError} coded `INVALID` when `budget` is not a
|
|
3871
|
+
* non-negative safe integer.
|
|
3872
|
+
*/
|
|
3873
|
+
constructor(budget) {
|
|
3874
|
+
if (!(0, _orkestrel_contract.isNonNegativeInteger)(budget) || !Number.isSafeInteger(budget)) throw new _src_core.ScaffoldError("INVALID", "The read budget is not a non-negative safe integer.", { field: "budget" });
|
|
3875
|
+
this.#remaining = budget;
|
|
3876
|
+
}
|
|
3877
|
+
/** The bytes the call has left; negative once a read overshoots the budget. */
|
|
3878
|
+
get remaining() {
|
|
3879
|
+
return this.#remaining;
|
|
3880
|
+
}
|
|
3881
|
+
/**
|
|
3882
|
+
* Spends bytes a read consumed from the allowance.
|
|
3883
|
+
*
|
|
3884
|
+
* @param bytes - The decoded bytes the read consumed.
|
|
3885
|
+
* @returns Nothing.
|
|
3886
|
+
* @throws {@link ScaffoldError} coded `INVALID` when `bytes` is not a
|
|
3887
|
+
* non-negative safe integer.
|
|
3888
|
+
*
|
|
3889
|
+
* @example
|
|
3890
|
+
* ```ts
|
|
3891
|
+
* import { ReadAllowance } from '@orkestrel/scaffold/server'
|
|
3892
|
+
*
|
|
3893
|
+
* const allowance = new ReadAllowance(10)
|
|
3894
|
+
* allowance.spend(12)
|
|
3895
|
+
* allowance.remaining // -2
|
|
3896
|
+
* ```
|
|
3897
|
+
*/
|
|
3898
|
+
spend(bytes) {
|
|
3899
|
+
if (!(0, _orkestrel_contract.isNonNegativeInteger)(bytes) || !Number.isSafeInteger(bytes)) throw new _src_core.ScaffoldError("INVALID", "The spent bytes are not a non-negative safe integer.", { field: "bytes" });
|
|
3900
|
+
this.#remaining -= bytes;
|
|
3901
|
+
}
|
|
3902
|
+
};
|
|
3903
|
+
//#endregion
|
|
3829
3904
|
//#region src/server/Upstream.ts
|
|
3830
3905
|
/**
|
|
3831
3906
|
* Represents the reading spine: one bounded, unauthenticated, redirect-free request per answer.
|
|
@@ -3856,9 +3931,9 @@ var Materializer = class {
|
|
|
3856
3931
|
* reader's own abort signal, so {@link Upstream.destroy} cancels what is in
|
|
3857
3932
|
* flight instead of waiting for it.
|
|
3858
3933
|
*
|
|
3859
|
-
*
|
|
3860
|
-
*
|
|
3861
|
-
*
|
|
3934
|
+
* Each call constructs its own {@link ReadAllowance} from the reader's budget and passes it
|
|
3935
|
+
* through the private reads rather than holding it on the instance, because concurrent calls
|
|
3936
|
+
* each own a budget and must not spend each other's.
|
|
3862
3937
|
*
|
|
3863
3938
|
* @example
|
|
3864
3939
|
* ```ts
|
|
@@ -3951,7 +4026,7 @@ var Upstream = class {
|
|
|
3951
4026
|
async lookup(dependencies) {
|
|
3952
4027
|
this.#assertAlive();
|
|
3953
4028
|
const accepted = this.#accept(dependencies, isDependencies, "dependencies");
|
|
3954
|
-
const allowance =
|
|
4029
|
+
const allowance = new ReadAllowance(this.#budget);
|
|
3955
4030
|
return this.#gather(accepted, (dependency) => this.#release(dependency, allowance));
|
|
3956
4031
|
}
|
|
3957
4032
|
/**
|
|
@@ -3985,7 +4060,7 @@ var Upstream = class {
|
|
|
3985
4060
|
this.#assertAlive();
|
|
3986
4061
|
const accepted = this.#accept(names, isDependencyNames, "names");
|
|
3987
4062
|
const observed = this.#accept(current, _src_core.isSnapshot, "current");
|
|
3988
|
-
const allowance =
|
|
4063
|
+
const allowance = new ReadAllowance(this.#budget);
|
|
3989
4064
|
return this.#gather(accepted, (name) => this.#mirror(name, observed, allowance));
|
|
3990
4065
|
}
|
|
3991
4066
|
/**
|
|
@@ -4032,7 +4107,7 @@ var Upstream = class {
|
|
|
4032
4107
|
const accepted = this.#accept(paths, isPaths, "paths");
|
|
4033
4108
|
const observed = this.#accept(current, _src_core.isSnapshot, "current");
|
|
4034
4109
|
if (accepted.length === 0) return [];
|
|
4035
|
-
const allowance =
|
|
4110
|
+
const allowance = new ReadAllowance(this.#budget);
|
|
4036
4111
|
const inventory = await this.#inventory(allowance);
|
|
4037
4112
|
return this.#gather(accepted, (path) => this.#file(path, inventory, observed, allowance));
|
|
4038
4113
|
}
|
|
@@ -4065,7 +4140,7 @@ var Upstream = class {
|
|
|
4065
4140
|
*/
|
|
4066
4141
|
async catalog() {
|
|
4067
4142
|
this.#assertAlive();
|
|
4068
|
-
const allowance =
|
|
4143
|
+
const allowance = new ReadAllowance(this.#budget);
|
|
4069
4144
|
const names = await this.#packages(allowance);
|
|
4070
4145
|
return this.#gather(names, (name) => this.#entry(name, allowance));
|
|
4071
4146
|
}
|
|
@@ -4234,16 +4309,10 @@ var Upstream = class {
|
|
|
4234
4309
|
lookup: "failed",
|
|
4235
4310
|
note: `the vendored inventory at ${url} does not match its own membership digest`
|
|
4236
4311
|
};
|
|
4237
|
-
const digests = /* @__PURE__ */ new Map();
|
|
4238
|
-
const duplicates = /* @__PURE__ */ new Set();
|
|
4239
|
-
for (const entry of manifest.entries) {
|
|
4240
|
-
if (digests.has(entry.destination)) duplicates.add(entry.destination);
|
|
4241
|
-
digests.set(entry.destination, entry.digest);
|
|
4242
|
-
}
|
|
4243
4312
|
return {
|
|
4244
4313
|
lookup: "found",
|
|
4245
|
-
digests,
|
|
4246
|
-
duplicates,
|
|
4314
|
+
digests: new Map(manifest.entries.map((entry) => [entry.destination, entry.digest])),
|
|
4315
|
+
duplicates: new Set(manifest.entries.map((entry) => entry.destination).filter((destination, index, destinations) => destinations.indexOf(destination) !== index)),
|
|
4247
4316
|
note: ""
|
|
4248
4317
|
};
|
|
4249
4318
|
}
|
|
@@ -4272,18 +4341,10 @@ var Upstream = class {
|
|
|
4272
4341
|
if (!(0, _orkestrel_contract.isRecord)(manifest)) return [];
|
|
4273
4342
|
const declared = manifest[section];
|
|
4274
4343
|
if (!(0, _orkestrel_contract.isRecord)(declared)) return [];
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
if (_src_core.CONTROL_CHARACTER_PATTERN.test(name) || _src_core.CONTROL_CHARACTER_PATTERN.test(range)) continue;
|
|
4280
|
-
if (name.length === 0 || name.length > _src_core.MAX_DEPENDENCY_NAME_LENGTH) continue;
|
|
4281
|
-
edges.push({
|
|
4282
|
-
name,
|
|
4283
|
-
range
|
|
4284
|
-
});
|
|
4285
|
-
}
|
|
4286
|
-
return edges;
|
|
4344
|
+
return Object.entries(declared).flatMap(([name, range]) => !(0, _orkestrel_contract.isString)(range) || range.length === 0 || range.length > _src_core.MAX_RANGE_LENGTH || _src_core.CONTROL_CHARACTER_PATTERN.test(name) || _src_core.CONTROL_CHARACTER_PATTERN.test(range) || name.length === 0 || name.length > _src_core.MAX_DEPENDENCY_NAME_LENGTH ? [] : [{
|
|
4345
|
+
name,
|
|
4346
|
+
range
|
|
4347
|
+
}]).slice(0, _src_core.MAX_COLLECTION_ITEMS);
|
|
4287
4348
|
}
|
|
4288
4349
|
async #packages(allowance) {
|
|
4289
4350
|
const url = `${this.#registryBase}/-/org/${ORKESTREL_SCOPE}/package`;
|
|
@@ -4312,11 +4373,7 @@ var Upstream = class {
|
|
|
4312
4373
|
const parsed = (0, _orkestrel_contract.parseJSON)(content);
|
|
4313
4374
|
if (!(0, _orkestrel_contract.isRecord)(parsed)) return void 0;
|
|
4314
4375
|
const versions = parsed.versions;
|
|
4315
|
-
|
|
4316
|
-
if ((0, _orkestrel_contract.isRecord)(versions)) for (const version of Object.keys(versions)) {
|
|
4317
|
-
if (!this.#admits(range, version)) continue;
|
|
4318
|
-
if (selected === void 0 || (0, _src_core.compareVersions)(version, selected) > 0) selected = version;
|
|
4319
|
-
}
|
|
4376
|
+
const selected = (0, _orkestrel_contract.isRecord)(versions) ? Object.keys(versions).filter((version) => this.#admits(range, version)).reduce((highest, version) => highest === void 0 || (0, _src_core.compareVersions)(version, highest) > 0 ? version : highest, void 0) : void 0;
|
|
4320
4377
|
if (selected !== void 0) return selected;
|
|
4321
4378
|
const latest = this.#latest(content);
|
|
4322
4379
|
return latest !== void 0 && this.#admits(range, latest) ? latest : void 0;
|
|
@@ -4344,14 +4401,12 @@ var Upstream = class {
|
|
|
4344
4401
|
#encode(path) {
|
|
4345
4402
|
return path.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
4346
4403
|
}
|
|
4347
|
-
async #readWithRetries(url, timeout, allowance, accept, binary = false) {
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
note = outcome.note;
|
|
4354
|
-
}
|
|
4404
|
+
async #readWithRetries(url, timeout, allowance, accept, binary = false, attempt = 0) {
|
|
4405
|
+
this.#assertAlive();
|
|
4406
|
+
const outcome = binary ? await this.#request(url, timeout, allowance, accept, true) : await this.#request(url, timeout, allowance, accept);
|
|
4407
|
+
if (outcome.lookup !== "failed") return outcome;
|
|
4408
|
+
if (attempt < this.#retries) return binary ? this.#readWithRetries(url, timeout, allowance, accept, true, attempt + 1) : this.#readWithRetries(url, timeout, allowance, accept, false, attempt + 1);
|
|
4409
|
+
const note = outcome.note;
|
|
4355
4410
|
this.#error("FETCH", `The upstream read at ${url} produced no answer.`, {
|
|
4356
4411
|
url,
|
|
4357
4412
|
note
|
|
@@ -4455,14 +4510,14 @@ var Upstream = class {
|
|
|
4455
4510
|
const reader = body.getReader();
|
|
4456
4511
|
const decoder = binary ? void 0 : new TextDecoder("utf-8", { fatal: true });
|
|
4457
4512
|
const chunks = [];
|
|
4458
|
-
|
|
4513
|
+
const sizes = [];
|
|
4459
4514
|
try {
|
|
4460
4515
|
for (;;) {
|
|
4461
4516
|
const chunk = await reader.read();
|
|
4462
4517
|
if (chunk.done) break;
|
|
4463
|
-
|
|
4464
|
-
allowance.
|
|
4465
|
-
if (
|
|
4518
|
+
sizes.push(chunk.value.byteLength);
|
|
4519
|
+
allowance.spend(chunk.value.byteLength);
|
|
4520
|
+
if ((0, _src_core.computeTotal)(sizes) > this.#limit) {
|
|
4466
4521
|
await reader.cancel();
|
|
4467
4522
|
const note = `the response passed the ${String(this.#limit)}-byte response limit`;
|
|
4468
4523
|
return binary ? {
|
|
@@ -4515,31 +4570,28 @@ var Upstream = class {
|
|
|
4515
4570
|
return code === void 0 ? message : `${message} (${code})`;
|
|
4516
4571
|
}
|
|
4517
4572
|
async #gather(items, run) {
|
|
4518
|
-
const
|
|
4519
|
-
const
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
return
|
|
4573
|
+
const entries = items.entries();
|
|
4574
|
+
const failed = new AbortController();
|
|
4575
|
+
const workers = Array.from({ length: Math.min(this.#concurrency, items.length) }, () => this.#drain(entries, run, failed));
|
|
4576
|
+
const settled = await Promise.allSettled(workers);
|
|
4577
|
+
if (failed.signal.aborted) {
|
|
4578
|
+
const reason = failed.signal.reason;
|
|
4579
|
+
throw (0, _orkestrel_contract.isRecord)(reason) ? reason.error : reason;
|
|
4580
|
+
}
|
|
4581
|
+
return collectIndexed(settled.flatMap((worker) => worker.status === "fulfilled" ? worker.value : []));
|
|
4527
4582
|
}
|
|
4528
|
-
async #drain(
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
state.cursor += 1;
|
|
4533
|
-
if (index >= items.length) return;
|
|
4534
|
-
const item = items[index];
|
|
4535
|
-
if (item === void 0) return;
|
|
4583
|
+
async #drain(entries, run, failed) {
|
|
4584
|
+
const produced = [];
|
|
4585
|
+
for (const [index, item] of entries) {
|
|
4586
|
+
if (failed.signal.aborted || item === void 0) break;
|
|
4536
4587
|
try {
|
|
4537
|
-
|
|
4588
|
+
produced.push([index, await run(item)]);
|
|
4538
4589
|
} catch (error) {
|
|
4539
|
-
|
|
4540
|
-
|
|
4590
|
+
failed.abort(Object.freeze({ error }));
|
|
4591
|
+
break;
|
|
4541
4592
|
}
|
|
4542
4593
|
}
|
|
4594
|
+
return produced;
|
|
4543
4595
|
}
|
|
4544
4596
|
#accept(value, guard, field) {
|
|
4545
4597
|
const snapshot = (0, _src_core.cloneValue)(value);
|
|
@@ -4579,10 +4631,13 @@ exports.Materializer = Materializer;
|
|
|
4579
4631
|
exports.ORKESTREL_SCOPE = ORKESTREL_SCOPE;
|
|
4580
4632
|
exports.PACKUMENT_MEDIA_TYPE = PACKUMENT_MEDIA_TYPE;
|
|
4581
4633
|
exports.RESERVED_SEGMENT_PATTERN = RESERVED_SEGMENT_PATTERN;
|
|
4634
|
+
exports.ReadAllowance = ReadAllowance;
|
|
4582
4635
|
exports.SCAFFOLD_REPOSITORY = SCAFFOLD_REPOSITORY;
|
|
4583
4636
|
exports.UNREADABLE_VERSION_NOTE = UNREADABLE_VERSION_NOTE;
|
|
4584
4637
|
exports.Upstream = Upstream;
|
|
4585
4638
|
exports.WriteTransaction = WriteTransaction;
|
|
4639
|
+
exports.climbRealPath = climbRealPath;
|
|
4640
|
+
exports.collectIndexed = collectIndexed;
|
|
4586
4641
|
exports.computeDigest = computeDigest;
|
|
4587
4642
|
exports.computeFileDigest = computeFileDigest;
|
|
4588
4643
|
exports.computeManifestDigest = computeManifestDigest;
|
|
@@ -4613,6 +4668,7 @@ exports.isUpstreamHooks = isUpstreamHooks;
|
|
|
4613
4668
|
exports.isUpstreamOptions = isUpstreamOptions;
|
|
4614
4669
|
exports.isVacant = isVacant;
|
|
4615
4670
|
exports.isWorktree = isWorktree;
|
|
4671
|
+
exports.listAncestors = listAncestors;
|
|
4616
4672
|
exports.listCanonPaths = listCanonPaths;
|
|
4617
4673
|
exports.listDirectories = listDirectories;
|
|
4618
4674
|
exports.listFiles = listFiles;
|