@orkestrel/scaffold 0.0.100 → 0.0.102
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 +36 -12
- package/dist/host/configs/helpers.ts +214 -133
- package/dist/host/configs/policy.ts +110 -105
- package/dist/host/dotfiles/gitignore +1 -0
- package/dist/host/dotfiles/oxlintrc.json +10 -0
- package/dist/host/guides/scaffold.md +244 -170
- package/dist/host/manifest.json +37 -31
- package/dist/host/tests/config.test.ts +1305 -619
- package/dist/host/tests/policy.test.ts +19 -0
- package/dist/host/tests/setupPolicy.ts +1710 -995
- 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
|
@@ -40,6 +40,57 @@ export declare interface BytesReadResult {
|
|
|
40
40
|
readonly note: string;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Climbs from an absolute path to its deepest existing ancestor and resolves it through every link.
|
|
45
|
+
*
|
|
46
|
+
* @param current - The absolute host path the climb reads at this depth.
|
|
47
|
+
* @param pending - The absent segments already climbed past, outermost first.
|
|
48
|
+
* @param depth - The steps already taken, from 0 through `MAX_PATH_DEPTH`.
|
|
49
|
+
* @returns The real path of the first existing ancestor joined with `pending` and every segment the
|
|
50
|
+
* climb passes, or `undefined` when `depth` is not a safe integer from 0 through `MAX_PATH_DEPTH`,
|
|
51
|
+
* the climb reaches the root, a link target cannot be read or carries a `..` segment, an existing
|
|
52
|
+
* entry is not a link, or an entry cannot be read.
|
|
53
|
+
*
|
|
54
|
+
* @remarks
|
|
55
|
+
* Each step reads one path and moves the climb to one other: a dangling link redirects it to the
|
|
56
|
+
* link's target with the same suffix, and an absent name moves it to the parent with that name added
|
|
57
|
+
* to the suffix. One successor per step makes the climb a bounded recursion rather than a worklist,
|
|
58
|
+
* and the depth argument is its bound, so an adversarial path or a link cycle answers `undefined`
|
|
59
|
+
* after `MAX_PATH_DEPTH` steps instead of walking indefinitely. {@link resolveRealPath} starts the
|
|
60
|
+
* climb at depth 0 with nothing pending.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* import { resolve } from 'node:path'
|
|
65
|
+
* import { climbRealPath } from '@orkestrel/scaffold/server'
|
|
66
|
+
*
|
|
67
|
+
* climbRealPath(resolve('./packages/new/src'), [], 0) // the real path of packages, plus new/src
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export declare function climbRealPath(current: string, pending: readonly string[], depth: number): string | undefined;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Places each result at the index its entry names, in a fresh array.
|
|
74
|
+
*
|
|
75
|
+
* @param entries - The index and result pairs, in any order; each index a non-negative integer.
|
|
76
|
+
* @returns An array holding each result at its index, whose length is the highest index plus one,
|
|
77
|
+
* and where an index no entry names stays a hole.
|
|
78
|
+
*
|
|
79
|
+
* @remarks
|
|
80
|
+
* A worker pool finishes its items out of order and can skip one, so its answer is merged by
|
|
81
|
+
* position rather than by arrival: each index is written once into a fresh array, which needs no
|
|
82
|
+
* sort and keeps a skipped index absent instead of closing the gap. A repeated index keeps its last
|
|
83
|
+
* result.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* import { collectIndexed } from '@orkestrel/scaffold/server'
|
|
88
|
+
*
|
|
89
|
+
* collectIndexed([[0, 'first'], [2, 'third']]).length // 3
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export declare function collectIndexed<TResult>(entries: ReadonlyArray<readonly [number, TResult]>): readonly TResult[];
|
|
93
|
+
|
|
43
94
|
/**
|
|
44
95
|
* Computes the SHA-256 digest of text.
|
|
45
96
|
*
|
|
@@ -745,6 +796,29 @@ export declare function isVacant(target: string): boolean;
|
|
|
745
796
|
*/
|
|
746
797
|
export declare const isWorktree: Guard<Worktree>;
|
|
747
798
|
|
|
799
|
+
/**
|
|
800
|
+
* Lists a path followed by each directory above it, nearest first.
|
|
801
|
+
*
|
|
802
|
+
* @param path - The host path to climb from.
|
|
803
|
+
* @param limit - The most paths to list; every ancestor by default.
|
|
804
|
+
* @returns The path, then `dirname` of each result in turn, ending at the first path that is its
|
|
805
|
+
* own parent (`.` for a relative path, the filesystem root for an absolute one) or at `limit`.
|
|
806
|
+
*
|
|
807
|
+
* @remarks
|
|
808
|
+
* The climb is derived from the path text alone and reads no filesystem, so a caller bounds a walk
|
|
809
|
+
* by the length it asks for rather than by a counter it reassigns. The final path of an unbounded
|
|
810
|
+
* list is the one `dirname` leaves unchanged.
|
|
811
|
+
*
|
|
812
|
+
* @example
|
|
813
|
+
* ```ts
|
|
814
|
+
* import { listAncestors } from '@orkestrel/scaffold/server'
|
|
815
|
+
*
|
|
816
|
+
* listAncestors('notes/entry/file.md') // ['notes/entry/file.md', 'notes/entry', 'notes', '.']
|
|
817
|
+
* listAncestors('notes/entry/file.md', 2) // ['notes/entry/file.md', 'notes/entry']
|
|
818
|
+
* ```
|
|
819
|
+
*/
|
|
820
|
+
export declare function listAncestors(path: string, limit?: number): readonly string[];
|
|
821
|
+
|
|
748
822
|
/**
|
|
749
823
|
* Lists the canon paths a target holds, filtered to a plan's groups.
|
|
750
824
|
*
|
|
@@ -1627,809 +1701,727 @@ export declare class Materializer implements MaterializerInterface {
|
|
|
1627
1701
|
export declare function pruneEmptiedDirectories(target: string, removed: readonly string[]): readonly string[];
|
|
1628
1702
|
|
|
1629
1703
|
/**
|
|
1630
|
-
* Represents the byte allowance one
|
|
1631
|
-
*
|
|
1632
|
-
* @remarks
|
|
1633
|
-
* `remaining` is deliberately mutable, and it is the one member of this module's
|
|
1634
|
-
* contracts that is. The carrier is what makes `budget` a bound on a call rather
|
|
1635
|
-
* than on a request: each read subtracts what it consumed from the same object,
|
|
1636
|
-
* so many small answers exhaust the call exactly as one oversized answer does.
|
|
1637
|
-
* Each call constructs its own carrier from `budget`, because concurrent calls
|
|
1638
|
-
* own separate budgets and must not spend each other's.
|
|
1639
|
-
*/
|
|
1640
|
-
export declare interface ReadAllowance {
|
|
1641
|
-
remaining: number;
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
/**
|
|
1645
|
-
* Captures one directory's physical identity.
|
|
1646
|
-
*
|
|
1647
|
-
* @param path - The resolved directory path to capture.
|
|
1648
|
-
* @returns The anchor, or `undefined` when the path is not a physical directory.
|
|
1704
|
+
* Represents the byte allowance one upstream call spends across every read it makes.
|
|
1649
1705
|
*
|
|
1650
1706
|
* @remarks
|
|
1651
|
-
*
|
|
1652
|
-
*
|
|
1653
|
-
*
|
|
1654
|
-
*
|
|
1655
|
-
*
|
|
1707
|
+
* Each call constructs its own allowance from the reader's budget, so concurrent
|
|
1708
|
+
* calls never spend each other's bytes. A read that brings the call exactly to
|
|
1709
|
+
* its budget leaves `remaining` at zero and is accepted; the next request is
|
|
1710
|
+
* refused before it is sent, and a body that passes the budget is refused at the
|
|
1711
|
+
* chunk that overshoots.
|
|
1656
1712
|
*
|
|
1657
1713
|
* @example
|
|
1658
1714
|
* ```ts
|
|
1659
|
-
* import {
|
|
1715
|
+
* import { ReadAllowance } from '@orkestrel/scaffold/server'
|
|
1660
1716
|
*
|
|
1661
|
-
* const
|
|
1662
|
-
*
|
|
1717
|
+
* const allowance = new ReadAllowance(1_024)
|
|
1718
|
+
* allowance.spend(1_000)
|
|
1719
|
+
* allowance.remaining // 24
|
|
1663
1720
|
* ```
|
|
1664
1721
|
*/
|
|
1665
|
-
export declare
|
|
1722
|
+
export declare class ReadAllowance implements ReadAllowanceInterface {
|
|
1723
|
+
#private;
|
|
1724
|
+
/**
|
|
1725
|
+
* Constructs an allowance holding the whole budget.
|
|
1726
|
+
*
|
|
1727
|
+
* @param budget - The bytes the call may read in total.
|
|
1728
|
+
* @throws {@link ScaffoldError} coded `INVALID` when `budget` is not a
|
|
1729
|
+
* non-negative safe integer.
|
|
1730
|
+
*/
|
|
1731
|
+
constructor(budget: number);
|
|
1732
|
+
/** The bytes the call has left; negative once a read overshoots the budget. */
|
|
1733
|
+
get remaining(): number;
|
|
1734
|
+
/**
|
|
1735
|
+
* Spends bytes a read consumed from the allowance.
|
|
1736
|
+
*
|
|
1737
|
+
* @param bytes - The decoded bytes the read consumed.
|
|
1738
|
+
* @returns Nothing.
|
|
1739
|
+
* @throws {@link ScaffoldError} coded `INVALID` when `bytes` is not a
|
|
1740
|
+
* non-negative safe integer.
|
|
1741
|
+
*
|
|
1742
|
+
* @example
|
|
1743
|
+
* ```ts
|
|
1744
|
+
* import { ReadAllowance } from '@orkestrel/scaffold/server'
|
|
1745
|
+
*
|
|
1746
|
+
* const allowance = new ReadAllowance(10)
|
|
1747
|
+
* allowance.spend(12)
|
|
1748
|
+
* allowance.remaining // -2
|
|
1749
|
+
* ```
|
|
1750
|
+
*/
|
|
1751
|
+
spend(bytes: number): void;
|
|
1752
|
+
}
|
|
1666
1753
|
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
* readExpectation('/tmp/project/absent.md') // { path: …, shape: 'absent' }
|
|
1687
|
-
* ```
|
|
1688
|
-
*/
|
|
1689
|
-
export declare function readExpectation(path: string): WriteExpectation | undefined;
|
|
1754
|
+
/**
|
|
1755
|
+
* Describes the byte allowance one upstream call spends across every read it makes.
|
|
1756
|
+
*
|
|
1757
|
+
* @remarks
|
|
1758
|
+
* `remaining` is the budget less every byte spent, and it goes negative once a
|
|
1759
|
+
* read overshoots, so a caller tells a call that has nothing left (`remaining`
|
|
1760
|
+
* at or below zero) from one that spent past its budget (`remaining` below zero).
|
|
1761
|
+
*/
|
|
1762
|
+
export declare interface ReadAllowanceInterface {
|
|
1763
|
+
/** The bytes the call has left; negative once a read overshoots the budget. */
|
|
1764
|
+
readonly remaining: number;
|
|
1765
|
+
/**
|
|
1766
|
+
* Spends bytes a read consumed from the allowance.
|
|
1767
|
+
*
|
|
1768
|
+
* @param bytes - The decoded bytes the read consumed.
|
|
1769
|
+
* @returns Nothing.
|
|
1770
|
+
*/
|
|
1771
|
+
spend(bytes: number): void;
|
|
1772
|
+
}
|
|
1690
1773
|
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
* import { readFileHex } from '@orkestrel/scaffold/server'
|
|
1714
|
-
*
|
|
1715
|
-
* readFileHex('/tmp/project', 'AGENTS.md') // '2320416765…'
|
|
1716
|
-
* ```
|
|
1717
|
-
*/
|
|
1718
|
-
export declare function readFileHex(root: string, path: string, limit?: number): string | undefined;
|
|
1774
|
+
/**
|
|
1775
|
+
* Captures one directory's physical identity.
|
|
1776
|
+
*
|
|
1777
|
+
* @param path - The resolved directory path to capture.
|
|
1778
|
+
* @returns The anchor, or `undefined` when the path is not a physical directory.
|
|
1779
|
+
*
|
|
1780
|
+
* @remarks
|
|
1781
|
+
* Native bigint device and inode values preserve the filesystem identity without
|
|
1782
|
+
* numeric rounding. Device and inode rather than the path, because the path can
|
|
1783
|
+
* be swapped underneath a write. An anchor captured before a mutation and
|
|
1784
|
+
* checked again after it proves the directory written into sits where the
|
|
1785
|
+
* inspected one sat, not that it is the one that was inspected.
|
|
1786
|
+
*
|
|
1787
|
+
* @example
|
|
1788
|
+
* ```ts
|
|
1789
|
+
* import { readAnchor } from '@orkestrel/scaffold/server'
|
|
1790
|
+
*
|
|
1791
|
+
* const anchor = readAnchor(process.cwd())
|
|
1792
|
+
* anchor !== undefined && typeof anchor.device === 'bigint' && typeof anchor.inode === 'bigint' // true for a physical directory
|
|
1793
|
+
* ```
|
|
1794
|
+
*/
|
|
1795
|
+
export declare function readAnchor(path: string): WriteAnchor | undefined;
|
|
1719
1796
|
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
*/
|
|
1744
|
-
export declare function readFileText(root: string, path: string, limit?: number): string | undefined;
|
|
1797
|
+
/**
|
|
1798
|
+
* Captures what one destination holds before a write.
|
|
1799
|
+
*
|
|
1800
|
+
* @param path - The resolved destination path to capture.
|
|
1801
|
+
* @returns The expectation, or `undefined` when the destination is a link or a
|
|
1802
|
+
* shape this package will not write over.
|
|
1803
|
+
*
|
|
1804
|
+
* @remarks
|
|
1805
|
+
* Absence is a captured state rather than a failure, because most writes expect
|
|
1806
|
+
* exactly that. Each shape carries only the facts it supplies: a directory
|
|
1807
|
+
* carries its identity, a file carries its identity, size, and bytes, and an
|
|
1808
|
+
* absent destination carries nothing at all. A file past the artifact ceiling
|
|
1809
|
+
* carries no digest and is bound by its identity, size, and modification time
|
|
1810
|
+
* alone, which is the strongest honest claim about bytes nobody read.
|
|
1811
|
+
*
|
|
1812
|
+
* @example
|
|
1813
|
+
* ```ts
|
|
1814
|
+
* import { readExpectation } from '@orkestrel/scaffold/server'
|
|
1815
|
+
*
|
|
1816
|
+
* readExpectation('/tmp/project/absent.md') // { path: …, shape: 'absent' }
|
|
1817
|
+
* ```
|
|
1818
|
+
*/
|
|
1819
|
+
export declare function readExpectation(path: string): WriteExpectation | undefined;
|
|
1745
1820
|
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1821
|
+
/**
|
|
1822
|
+
* Reads one contained file as its exact bytes in lowercase hexadecimal.
|
|
1823
|
+
*
|
|
1824
|
+
* @param root - The containing host directory.
|
|
1825
|
+
* @param path - The portable root-relative file path.
|
|
1826
|
+
* @param limit - The most bytes this read accepts; the artifact ceiling by default.
|
|
1827
|
+
* @returns The exact bytes as hexadecimal, or `undefined` when the file is
|
|
1828
|
+
* absent, is not a physical readable file, is past `limit`, or moved while it
|
|
1829
|
+
* was being read.
|
|
1830
|
+
* @throws `ScaffoldError('INVALID', …)` when the arguments are off contract or
|
|
1831
|
+
* `path` leaves `root`.
|
|
1832
|
+
*
|
|
1833
|
+
* @remarks
|
|
1834
|
+
* Hexadecimal rather than text, because this is what a byte comparison is stated
|
|
1835
|
+
* in everywhere in this package: a plan's artifact, an audit finding, and a
|
|
1836
|
+
* snapshot all compare as the same digits. The file's identity and size are
|
|
1837
|
+
* measured before and after the read, and one extra byte is requested past the
|
|
1838
|
+
* declared size, so a file that grew or was replaced mid-read answers
|
|
1839
|
+
* `undefined` rather than half of one file and half of another.
|
|
1840
|
+
*
|
|
1841
|
+
* @example
|
|
1842
|
+
* ```ts
|
|
1843
|
+
* import { readFileHex } from '@orkestrel/scaffold/server'
|
|
1844
|
+
*
|
|
1845
|
+
* readFileHex('/tmp/project', 'AGENTS.md') // '2320416765…'
|
|
1846
|
+
* ```
|
|
1847
|
+
*/
|
|
1848
|
+
export declare function readFileHex(root: string, path: string, limit?: number): string | undefined;
|
|
1772
1849
|
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
* import { readHostManifest } from '@orkestrel/scaffold/server'
|
|
1799
|
-
*
|
|
1800
|
-
* readHostManifest('./dist/host') // the manifest, or undefined for a raw root
|
|
1801
|
-
* ```
|
|
1802
|
-
*/
|
|
1803
|
-
export declare function readHostManifest(host: string, name?: string): HostManifest | undefined;
|
|
1850
|
+
/**
|
|
1851
|
+
* Reads one contained file as bounded UTF-8 text.
|
|
1852
|
+
*
|
|
1853
|
+
* @param root - The containing host directory.
|
|
1854
|
+
* @param path - The portable root-relative file path.
|
|
1855
|
+
* @param limit - The most bytes this read accepts; the artifact ceiling by default.
|
|
1856
|
+
* @returns The decoded text, or `undefined` when {@link readFileHex} answers
|
|
1857
|
+
* nothing or the bytes are not valid UTF-8.
|
|
1858
|
+
* @throws `ScaffoldError('INVALID', …)` when the arguments are off contract or
|
|
1859
|
+
* `path` leaves `root`.
|
|
1860
|
+
*
|
|
1861
|
+
* @remarks
|
|
1862
|
+
* Decoding is strict, so a file carrying an invalid sequence answers `undefined`
|
|
1863
|
+
* rather than text carrying replacement characters. That matters because the
|
|
1864
|
+
* text is parsed next: a manifest silently repaired into valid JSON by lossy
|
|
1865
|
+
* decoding would be trusted.
|
|
1866
|
+
*
|
|
1867
|
+
* @example
|
|
1868
|
+
* ```ts
|
|
1869
|
+
* import { readFileText } from '@orkestrel/scaffold/server'
|
|
1870
|
+
*
|
|
1871
|
+
* readFileText('/tmp/project', 'package.json') // '{ "name": "@orkestrel/router", … }'
|
|
1872
|
+
* ```
|
|
1873
|
+
*/
|
|
1874
|
+
export declare function readFileText(root: string, path: string, limit?: number): string | undefined;
|
|
1804
1875
|
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
*/
|
|
1832
|
-
export declare function readManifestEntry(destination: string, source: string): ManifestEntry | undefined;
|
|
1876
|
+
/**
|
|
1877
|
+
* Reads the installed vendored host floor as a value.
|
|
1878
|
+
*
|
|
1879
|
+
* @param root - The vendored host root. Default: the installed package's
|
|
1880
|
+
* vendored root, resolved from this module's location.
|
|
1881
|
+
* @returns The verified manifest and the exact bytes of every declared entry.
|
|
1882
|
+
* @throws `ScaffoldError('TARGET', …)` when the root is not a readable physical
|
|
1883
|
+
* directory, its manifest is absent or unreadable, the manifest does not verify,
|
|
1884
|
+
* or a declared file is unreadable or misses its digest.
|
|
1885
|
+
*
|
|
1886
|
+
* @remarks
|
|
1887
|
+
* Reads the same default floor the {@link Materializer} uses. Each declared
|
|
1888
|
+
* file is addressed through the manifest's storage name and retained under its
|
|
1889
|
+
* destination, so the returned value has the same shape as the installed root.
|
|
1890
|
+
* When this module executes from TypeScript source, the committed inventory is
|
|
1891
|
+
* the manifest and each checkout destination supplies its bytes. The emitted
|
|
1892
|
+
* module reads the staged `manifest.json` file and each storage path instead.
|
|
1893
|
+
*
|
|
1894
|
+
* @example
|
|
1895
|
+
* ```ts
|
|
1896
|
+
* import { readHostFloor } from '@orkestrel/scaffold/server'
|
|
1897
|
+
*
|
|
1898
|
+
* readHostFloor().manifest // the installed floor's verified membership
|
|
1899
|
+
* ```
|
|
1900
|
+
*/
|
|
1901
|
+
export declare function readHostFloor(root?: string): Host;
|
|
1833
1902
|
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1903
|
+
/**
|
|
1904
|
+
* Reads a vendored host's manifest, when it carries one.
|
|
1905
|
+
*
|
|
1906
|
+
* @param host - The vendored host root to read.
|
|
1907
|
+
* @param name - The root-relative manifest path. Default: `manifest.json`.
|
|
1908
|
+
* @returns The manifest, or `undefined` when the host carries none.
|
|
1909
|
+
* @throws `ScaffoldError('INVALID', …)` when `host` is not a host path.
|
|
1910
|
+
* @throws `ScaffoldError('TARGET', …)` when the manifest is there but cannot be
|
|
1911
|
+
* read, is not the declared shape, or does not match its own membership.
|
|
1912
|
+
*
|
|
1913
|
+
* @remarks
|
|
1914
|
+
* The failures are held apart deliberately. A host with no manifest is a
|
|
1915
|
+
* raw checkout, and a caller reads it by mapping each path one to one. A host
|
|
1916
|
+
* with a manifest that does not verify is a staged host that has been edited,
|
|
1917
|
+
* and answering `undefined` there would degrade it to that same one-to-one
|
|
1918
|
+
* mapping — which is how an edited manifest would get a caller to read files it
|
|
1919
|
+
* never declared. So absence answers and corruption throws.
|
|
1920
|
+
*
|
|
1921
|
+
* Verification here is the manifest's own self-consistency: the digest against
|
|
1922
|
+
* the exact membership beside it. Whether that membership matches the files
|
|
1923
|
+
* actually stored is a separate question, and it belongs to the reader that
|
|
1924
|
+
* walks the host.
|
|
1925
|
+
*
|
|
1926
|
+
* @example
|
|
1927
|
+
* ```ts
|
|
1928
|
+
* import { readHostManifest } from '@orkestrel/scaffold/server'
|
|
1929
|
+
*
|
|
1930
|
+
* readHostManifest('./dist/host') // the manifest, or undefined for a raw root
|
|
1931
|
+
* ```
|
|
1932
|
+
*/
|
|
1933
|
+
export declare function readHostManifest(host: string, name?: string): HostManifest | undefined;
|
|
1864
1934
|
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1935
|
+
/**
|
|
1936
|
+
* Derives one vendored-host manifest entry from a file in a checkout.
|
|
1937
|
+
*
|
|
1938
|
+
* @param destination - The target-relative path the file is written to.
|
|
1939
|
+
* @param source - The resolved host path the bytes are read from.
|
|
1940
|
+
* @returns The entry, or `undefined` when `source` is not a physical file this
|
|
1941
|
+
* package will vendor or carries more bytes than one artifact may.
|
|
1942
|
+
*
|
|
1943
|
+
* @remarks
|
|
1944
|
+
* The one place the declared fields are decided together, because they are
|
|
1945
|
+
* readings of one path: {@link pathToStorage} decides where it is stored,
|
|
1946
|
+
* the destination is the path it answers for, and {@link matchesExecutablePath}
|
|
1947
|
+
* decides whether a target receives it executable.
|
|
1948
|
+
*
|
|
1949
|
+
* The bit is read from that declaration rather than from the source's mode, so
|
|
1950
|
+
* the entry does not depend on where the package was staged. A Windows host
|
|
1951
|
+
* reports no executable bit at all, and reading the mode there declared every
|
|
1952
|
+
* entry non-executable and shipped consumers hooks they could not run.
|
|
1953
|
+
*
|
|
1954
|
+
* @example
|
|
1955
|
+
* ```ts
|
|
1956
|
+
* import { readManifestEntry } from '@orkestrel/scaffold/server'
|
|
1957
|
+
*
|
|
1958
|
+
* readManifestEntry('.gitignore', '/tmp/checkout/.gitignore')
|
|
1959
|
+
* // { storage: 'dotfiles/gitignore', destination: '.gitignore', executable: false, digest: '...' }
|
|
1960
|
+
* ```
|
|
1961
|
+
*/
|
|
1962
|
+
export declare function readManifestEntry(destination: string, source: string): ManifestEntry | undefined;
|
|
1883
1963
|
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1964
|
+
/**
|
|
1965
|
+
* Reads a target's current bytes at the paths a plan claims.
|
|
1966
|
+
*
|
|
1967
|
+
* @param target - The target directory to read.
|
|
1968
|
+
* @param paths - The plan-relative paths to probe.
|
|
1969
|
+
* @returns One entry per path that is there: a file maps to its exact bytes as
|
|
1970
|
+
* hexadecimal and a directory maps to `''`, which records presence with no bytes
|
|
1971
|
+
* to compare. An absent path is omitted.
|
|
1972
|
+
* @throws `ScaffoldError('INVALID', …)` when `target` is not a host path or
|
|
1973
|
+
* `paths` is not a bounded list of plannable paths.
|
|
1974
|
+
* @throws `ScaffoldError('TARGET', …)` when a path that is there cannot be read,
|
|
1975
|
+
* or when the whole read would retain more bytes than one plan may.
|
|
1976
|
+
*
|
|
1977
|
+
* @remarks
|
|
1978
|
+
* The one door from a real directory into the vocabulary an audit compares in.
|
|
1979
|
+
* Absence is omission rather than an empty value, because core reads a missing
|
|
1980
|
+
* key as a missing destination and an empty string as a present directory;
|
|
1981
|
+
* they are different verdicts. A path that is there but unreadable throws instead
|
|
1982
|
+
* of being omitted, because omission would report it as missing and a repair
|
|
1983
|
+
* would then overwrite whatever is actually sitting there.
|
|
1984
|
+
*
|
|
1985
|
+
* @example
|
|
1986
|
+
* ```ts
|
|
1987
|
+
* import { readSnapshot } from '@orkestrel/scaffold/server'
|
|
1988
|
+
*
|
|
1989
|
+
* readSnapshot('./packages/router', ['package.json', 'guides'])
|
|
1990
|
+
* // { 'package.json': '7b226e…', guides: '' }
|
|
1991
|
+
* ```
|
|
1992
|
+
*/
|
|
1993
|
+
export declare function readSnapshot(target: string, paths: readonly string[]): Snapshot;
|
|
1906
1994
|
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1995
|
+
/**
|
|
1996
|
+
* Reads the Surface collision baseline from a committed inventory.
|
|
1997
|
+
*
|
|
1998
|
+
* @param root - The checkout's host path.
|
|
1999
|
+
* @param name - The checkout-relative inventory path. Default: `host.json`.
|
|
2000
|
+
* @returns The recorded collisions, or `undefined` when the inventory is absent.
|
|
2001
|
+
* @throws `ScaffoldError('INVALID', …)` when `root` is not a host path or `name` leaves it.
|
|
2002
|
+
* @throws `ScaffoldError('TARGET', …)` when an existing inventory is unreadable,
|
|
2003
|
+
* malformed, or inconsistent with its digest.
|
|
2004
|
+
*
|
|
2005
|
+
* @example
|
|
2006
|
+
* ```ts
|
|
2007
|
+
* import { readSurfaceBaseline } from '@orkestrel/scaffold/server'
|
|
2008
|
+
*
|
|
2009
|
+
* readSurfaceBaseline('.') // the recorded collisions, if the inventory exists
|
|
2010
|
+
* ```
|
|
2011
|
+
*/
|
|
2012
|
+
export declare function readSurfaceBaseline(root: string, name?: string): readonly SurfaceCollision[] | undefined;
|
|
1916
2013
|
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
*
|
|
1940
|
-
* The answer describes the namespace this call read. The contract excludes a
|
|
1941
|
-
* concurrent rename or link swap during the call or before the caller finishes
|
|
1942
|
-
* using the returned path. This helper returns a string, not a filesystem
|
|
1943
|
-
* handle, so it cannot bind its containment check to a later operation. A caller
|
|
1944
|
-
* that admits hostile concurrent namespace mutation needs a handle-bound
|
|
1945
|
-
* operation instead.
|
|
1946
|
-
*
|
|
1947
|
-
* @example
|
|
1948
|
-
* ```ts
|
|
1949
|
-
* import { resolveContainedPath } from '@orkestrel/scaffold/server'
|
|
1950
|
-
*
|
|
1951
|
-
* resolveContainedPath('/tmp/project', 'guides/router.md')?.endsWith('router.md') // true
|
|
1952
|
-
* resolveContainedPath('/tmp/project', '../secrets') // undefined
|
|
1953
|
-
* ```
|
|
1954
|
-
*/
|
|
1955
|
-
export declare function resolveContainedPath(root: string, path: string): string | undefined;
|
|
2014
|
+
/**
|
|
2015
|
+
* Reads bare Surface names claimed by distinct package guides.
|
|
2016
|
+
*
|
|
2017
|
+
* @param root - The physical directory containing the package guides.
|
|
2018
|
+
* @returns Colliding names and their distinct owners, sorted by name and owner.
|
|
2019
|
+
* @throws `ScaffoldError('TARGET', …)` when the directory or a guide cannot be read.
|
|
2020
|
+
* @throws `ScaffoldError('TARGET', …)` when `@orkestrel/guide` cannot be loaded.
|
|
2021
|
+
*
|
|
2022
|
+
* @remarks
|
|
2023
|
+
* Reads immediate `.md` files except `README.md` through `createGuide().surface()`.
|
|
2024
|
+
* Requires `@orkestrel/guide` in this module's resolution path; Scaffold declares it
|
|
2025
|
+
* only for development. Loads it only when called, so importing the server entry
|
|
2026
|
+
* requires no guide tooling in a production install.
|
|
2027
|
+
*
|
|
2028
|
+
* @example
|
|
2029
|
+
* ```ts
|
|
2030
|
+
* import { readSurfaceCollisions } from '@orkestrel/scaffold/server'
|
|
2031
|
+
*
|
|
2032
|
+
* readSurfaceCollisions('./guides').get('Shared') // the guides claiming Shared, if it collides
|
|
2033
|
+
* ```
|
|
2034
|
+
*/
|
|
2035
|
+
export declare function readSurfaceCollisions(root: string): ReadonlyMap<string, readonly string[]>;
|
|
1956
2036
|
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
* @remarks
|
|
1967
|
-
* A containment decision has to be made about a destination that does not exist
|
|
1968
|
-
* yet, and a lexical answer is not enough: a link anywhere in the existing
|
|
1969
|
-
* prefix moves the destination somewhere the text never named. So the deepest
|
|
1970
|
-
* existing ancestor is resolved and the remaining segments are re-joined onto
|
|
1971
|
-
* it. The climb is bounded by the path-depth ceiling, so an adversarial path
|
|
1972
|
-
* cannot make it walk indefinitely.
|
|
1973
|
-
*
|
|
1974
|
-
* The caller's own text is collapsed first, which is what `resolve` does with a
|
|
1975
|
-
* `..` the caller wrote: it cancels the segment before it as text, before any
|
|
1976
|
-
* link in that segment is read. So `<root>/hop/..` answers `<root>` even where
|
|
1977
|
-
* `hop` links elsewhere, rather than the directory holding what `hop` points at.
|
|
1978
|
-
* The collapse only ever shortens the caller's path, so nothing reaches outside
|
|
1979
|
-
* it by this; the answer is that lexical location resolved through links, not
|
|
1980
|
-
* the physical location the links lead to. {@link resolveContainedPath} passes
|
|
1981
|
-
* its `root` through here, so a root written with a parent segment is contained
|
|
1982
|
-
* against its collapsed spelling.
|
|
1983
|
-
*
|
|
1984
|
-
* `realpath` answers `ENOENT` both for a name that is not there and for a link
|
|
1985
|
-
* whose target is not there. The name is therefore inspected without following
|
|
1986
|
-
* it: a dangling link redirects the walk to its target, while a genuinely absent
|
|
1987
|
-
* name is retained as one segment of the unresolved suffix. A dangling link
|
|
1988
|
-
* target containing a `..` segment is refused. Resolving that target as one
|
|
1989
|
-
* lexical string could discard a preceding link before the filesystem gives
|
|
1990
|
-
* `..` its physical meaning.
|
|
1991
|
-
*
|
|
1992
|
-
* That target is split on both separators on every host, which is the reading
|
|
1993
|
-
* `isPath` already gives a planned path. A POSIX filename legally containing a
|
|
1994
|
-
* backslash is therefore refused with it: `weird\..\name` is one name to the
|
|
1995
|
-
* host and three segments here. The package keeps one separator law rather than
|
|
1996
|
-
* a host-dependent second one, and this is the conservative side of it.
|
|
1997
|
-
*
|
|
1998
|
-
* @example
|
|
1999
|
-
* ```ts
|
|
2000
|
-
* import { resolveRealPath } from '@orkestrel/scaffold/server'
|
|
2001
|
-
*
|
|
2002
|
-
* resolveRealPath('./packages/new/src') // the real path of `packages`, plus `new/src`
|
|
2003
|
-
* ```
|
|
2004
|
-
*/
|
|
2005
|
-
export declare function resolveRealPath(path: string): string | undefined;
|
|
2037
|
+
/**
|
|
2038
|
+
* Matches the Windows device names that stay reserved even when an extension follows.
|
|
2039
|
+
*
|
|
2040
|
+
* @remarks
|
|
2041
|
+
* Refused on every host rather than only on Windows. A generated workspace is
|
|
2042
|
+
* checked out on all of them, so a directory this package writes on Linux must
|
|
2043
|
+
* still be a name Windows can hold.
|
|
2044
|
+
*/
|
|
2045
|
+
export declare const RESERVED_SEGMENT_PATTERN: RegExp;
|
|
2006
2046
|
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2047
|
+
/**
|
|
2048
|
+
* Resolves a root-relative path and refuses one that leaves its root.
|
|
2049
|
+
*
|
|
2050
|
+
* @param root - The containing host directory.
|
|
2051
|
+
* @param path - The portable root-relative path.
|
|
2052
|
+
* @returns The destination as this package will address it, or `undefined` when
|
|
2053
|
+
* either argument is off contract or the destination lies outside `root`.
|
|
2054
|
+
*
|
|
2055
|
+
* @remarks
|
|
2056
|
+
* The containment law, and the one door every read in this module goes through.
|
|
2057
|
+
* Both sides are resolved through the real filesystem before they are compared.
|
|
2058
|
+
* A dangling link is followed only when its raw target contains no parent
|
|
2059
|
+
* traversal. The answer is then the lexical join of `root` and `path` — an
|
|
2060
|
+
* absolute path under `root`, not a root-relative one — so the caller operates
|
|
2061
|
+
* on the path it named rather than on a resolved form the target may not
|
|
2062
|
+
* recognize. A `root` written with a parent segment is collapsed by that
|
|
2063
|
+
* resolution before anything is read, so containment is measured against the
|
|
2064
|
+
* directory the caller's text names.
|
|
2065
|
+
*
|
|
2066
|
+
* Comparison is exact text, which fails closed on a case-insensitive
|
|
2067
|
+
* filesystem: a root and a path spelled with different case resolve to
|
|
2068
|
+
* different strings there and are refused, never wrongly admitted.
|
|
2069
|
+
*
|
|
2070
|
+
* The answer describes the namespace this call read. The contract excludes a
|
|
2071
|
+
* concurrent rename or link swap during the call or before the caller finishes
|
|
2072
|
+
* using the returned path. This helper returns a string, not a filesystem
|
|
2073
|
+
* handle, so it cannot bind its containment check to a later operation. A caller
|
|
2074
|
+
* that admits hostile concurrent namespace mutation needs a handle-bound
|
|
2075
|
+
* operation instead.
|
|
2076
|
+
*
|
|
2077
|
+
* @example
|
|
2078
|
+
* ```ts
|
|
2079
|
+
* import { resolveContainedPath } from '@orkestrel/scaffold/server'
|
|
2080
|
+
*
|
|
2081
|
+
* resolveContainedPath('/tmp/project', 'guides/router.md')?.endsWith('router.md') // true
|
|
2082
|
+
* resolveContainedPath('/tmp/project', '../secrets') // undefined
|
|
2083
|
+
* ```
|
|
2084
|
+
*/
|
|
2085
|
+
export declare function resolveContainedPath(root: string, path: string): string | undefined;
|
|
2016
2086
|
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2087
|
+
/**
|
|
2088
|
+
* Resolves a path through the real filesystem, keeping the part that does not exist yet.
|
|
2089
|
+
*
|
|
2090
|
+
* @param path - The absolute or relative host path to resolve.
|
|
2091
|
+
* @returns The lexical resolution of `path`, with its existing prefix then
|
|
2092
|
+
* resolved through every link, or `undefined` when the text is not a host path,
|
|
2093
|
+
* no bounded existing ancestor resolves, a link target cannot be read, a link
|
|
2094
|
+
* target carries a `..` segment, or an ancestor cannot be read.
|
|
2095
|
+
*
|
|
2096
|
+
* @remarks
|
|
2097
|
+
* A containment decision has to be made about a destination that does not exist
|
|
2098
|
+
* yet, and a lexical answer is not enough: a link anywhere in the existing
|
|
2099
|
+
* prefix moves the destination somewhere the text never named. So the deepest
|
|
2100
|
+
* existing ancestor is resolved and the remaining segments are re-joined onto
|
|
2101
|
+
* it. {@link climbRealPath} performs the climb and bounds it by the path-depth
|
|
2102
|
+
* ceiling, so an adversarial path cannot make it walk indefinitely.
|
|
2103
|
+
*
|
|
2104
|
+
* The caller's own text is collapsed first, which is what `resolve` does with a
|
|
2105
|
+
* `..` the caller wrote: it cancels the segment before it as text, before any
|
|
2106
|
+
* link in that segment is read. So `<root>/hop/..` answers `<root>` even where
|
|
2107
|
+
* `hop` links elsewhere, rather than the directory holding what `hop` points at.
|
|
2108
|
+
* The collapse only ever shortens the caller's path, so nothing reaches outside
|
|
2109
|
+
* it by this; the answer is that lexical location resolved through links, not
|
|
2110
|
+
* the physical location the links lead to. {@link resolveContainedPath} passes
|
|
2111
|
+
* its `root` through here, so a root written with a parent segment is contained
|
|
2112
|
+
* against its collapsed spelling.
|
|
2113
|
+
*
|
|
2114
|
+
* `realpath` answers `ENOENT` both for a name that is not there and for a link
|
|
2115
|
+
* whose target is not there. The name is therefore inspected without following
|
|
2116
|
+
* it: a dangling link redirects the walk to its target, while a genuinely absent
|
|
2117
|
+
* name is retained as one segment of the unresolved suffix. A dangling link
|
|
2118
|
+
* target containing a `..` segment is refused. Resolving that target as one
|
|
2119
|
+
* lexical string could discard a preceding link before the filesystem gives
|
|
2120
|
+
* `..` its physical meaning.
|
|
2121
|
+
*
|
|
2122
|
+
* That target is split on both separators on every host, which is the reading
|
|
2123
|
+
* `isPath` already gives a planned path. A POSIX filename legally containing a
|
|
2124
|
+
* backslash is therefore refused with it: `weird\..\name` is one name to the
|
|
2125
|
+
* host and three segments here. The package keeps one separator law rather than
|
|
2126
|
+
* a host-dependent second one, and this is the conservative side of it.
|
|
2127
|
+
*
|
|
2128
|
+
* @example
|
|
2129
|
+
* ```ts
|
|
2130
|
+
* import { resolveRealPath } from '@orkestrel/scaffold/server'
|
|
2131
|
+
*
|
|
2132
|
+
* resolveRealPath('./packages/new/src') // the real path of `packages`, plus `new/src`
|
|
2133
|
+
* ```
|
|
2134
|
+
*/
|
|
2135
|
+
export declare function resolveRealPath(path: string): string | undefined;
|
|
2052
2136
|
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
* or a vendored path leaves the checkout or the host root.
|
|
2063
|
-
* @throws `ScaffoldError('TARGET', …)` when the checkout is not a directory, the
|
|
2064
|
-
* host root is not vacant, the checkout does not carry every vendored path, two
|
|
2065
|
-
* vendored files claim one storage name, a vendored file is not a plain file
|
|
2066
|
-
* within the artifact ceiling, published guides are unreadable, a staged
|
|
2067
|
-
* collision's owner set is not a subset of its published owner set, the inventory is absent without
|
|
2068
|
-
* `establish: true`, or the staged manifest does not read back.
|
|
2069
|
-
* @throws `ScaffoldError('WRITE', …)` when a staged file or the manifest cannot
|
|
2070
|
-
* be written.
|
|
2071
|
-
*
|
|
2072
|
-
* @remarks
|
|
2073
|
-
* This is the producer half of the vendored host, and it is not the mutation
|
|
2074
|
-
* contract `MaterializerInterface` states. That contract owns **target**
|
|
2075
|
-
* writes: it materializes a compiled plan into a consumer's workspace, binds
|
|
2076
|
-
* every destination to what the caller observed, and rolls a failed commit back.
|
|
2077
|
-
* This reads this package's own checkout at build time and fills its own build
|
|
2078
|
-
* output. Different direction, different lifetime, no consumer target involved,
|
|
2079
|
-
* so they do not overlap and neither one belongs inside the other.
|
|
2080
|
-
*
|
|
2081
|
-
* Staging is plain rather than transactional for the same reason. A
|
|
2082
|
-
* `WriteTransaction` exists to hold a directory that already holds work
|
|
2083
|
-
* still; a build output holds nothing, is deleted whole before every build, and
|
|
2084
|
-
* has no concurrent reader. What replaces it is refusing early and ordering the
|
|
2085
|
-
* writes: the whole membership is derived before anything is created, so a
|
|
2086
|
-
* membership refusal leaves no host root at all, and `manifest.json` is
|
|
2087
|
-
* written last, so a copy or Surface refusal leaves a root every
|
|
2088
|
-
* reader treats as a raw checkout and fails loudly on.
|
|
2089
|
-
*
|
|
2090
|
-
* A missing vendored path is refused rather than staged around. A partial root
|
|
2091
|
-
* is not detectably partial: it fails later, in a consumer's terminal, on
|
|
2092
|
-
* whichever path the plan reached first. Refusing here fails the build that
|
|
2093
|
-
* produced it, where the maintainer can act, and it names every missing path at
|
|
2094
|
-
* once. A directory is the same case — declaring an absent directory as an empty
|
|
2095
|
-
* root would create an empty directory in every generated workspace. A canon file
|
|
2096
|
-
* member the checkout no longer holds is the one exception: scaffold keeps owning
|
|
2097
|
-
* the path so a target's copy reports foreign, and the stage ships nothing for it.
|
|
2098
|
-
*
|
|
2099
|
-
* The walk covers `HOST_PATHS`, `CANON_PATHS`, and `REFERENCE_PATHS` together.
|
|
2100
|
-
* A plan selects the paths a target receives; the installed package also carries
|
|
2101
|
-
* the canon and reference files for reading. Every catalog package must have a
|
|
2102
|
-
* guide in the discovered membership before the host is written.
|
|
2103
|
-
* Copied guides must carry no collision whose name and exact owner set the
|
|
2104
|
-
* committed inventory lacks. An absent inventory requires `establish: true`;
|
|
2105
|
-
* an inventory without a valid recorded Surface is refused.
|
|
2106
|
-
* Everything downstream — the missing-path refusal, the
|
|
2107
|
-
* storage collision guard, the sort, the digests, and the root inventory — reads
|
|
2108
|
-
* the union, so every staged path follows the same law.
|
|
2109
|
-
*
|
|
2110
|
-
* The vendoring deny-list applies to what the walk discovers beneath a staged
|
|
2111
|
-
* directory, where a maintainer's local credential can legitimately sit, and
|
|
2112
|
-
* such a path is skipped. A path a staging list names itself is curated data
|
|
2113
|
-
* rather than discovery, so it is staged or the stage is refused.
|
|
2114
|
-
*
|
|
2115
|
-
* @example Vendored data root
|
|
2116
|
-
* ```ts
|
|
2117
|
-
* import { stageHost } from '@orkestrel/scaffold/server'
|
|
2118
|
-
*
|
|
2119
|
-
* stageHost(process.cwd(), 'dist/host') // one ManifestEntry per file staged
|
|
2120
|
-
* ```
|
|
2121
|
-
*/
|
|
2122
|
-
export declare function stageHost(checkout: string, host: string, options?: HostStageOptions): readonly ManifestEntry[];
|
|
2137
|
+
/**
|
|
2138
|
+
* Names the repository this package's own vendored files are served from.
|
|
2139
|
+
*
|
|
2140
|
+
* @remarks
|
|
2141
|
+
* This package's bare name, stated rather than derived, because the reader has no
|
|
2142
|
+
* manifest to read it out of and one raw content host serves both the fleet's
|
|
2143
|
+
* guides and these files.
|
|
2144
|
+
*/
|
|
2145
|
+
export declare const SCAFFOLD_REPOSITORY = "scaffold";
|
|
2123
2146
|
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2147
|
+
/**
|
|
2148
|
+
* Stages the named destinations of a value host into a private root.
|
|
2149
|
+
*
|
|
2150
|
+
* @param host - The host whose bytes are written, keyed by destination.
|
|
2151
|
+
* @param root - The private directory to fill; it must already be a directory
|
|
2152
|
+
* this process may write into.
|
|
2153
|
+
* @param destinations - The destinations to stage, each declared by `host`.
|
|
2154
|
+
* @returns The entry staged for each destination, in the order requested.
|
|
2155
|
+
* @throws `ScaffoldError('INVALID', …)` when `root` is not a host path or a
|
|
2156
|
+
* storage name leaves it.
|
|
2157
|
+
* @throws `ScaffoldError('TARGET', …)` when a destination is one the host does
|
|
2158
|
+
* not declare, carries no bytes, or carries bytes that miss its declared digest.
|
|
2159
|
+
* @throws `ScaffoldError('WRITE', …)` when a file cannot be written or does not
|
|
2160
|
+
* read back as the bytes it was given.
|
|
2161
|
+
*
|
|
2162
|
+
* @remarks
|
|
2163
|
+
* Each file lands under the storage name the manifest declares and takes the
|
|
2164
|
+
* executable bit that manifest records, so a root filled from a value is the
|
|
2165
|
+
* same shape as one staged from a checkout and a reader cannot tell them apart.
|
|
2166
|
+
* That is what lets a mutation copy real files with real modes from bytes a
|
|
2167
|
+
* caller supplied, instead of degrading them to plain text writes.
|
|
2168
|
+
*
|
|
2169
|
+
* The bytes are digested before the write and the staged file after it, so a
|
|
2170
|
+
* value that disagrees with its own manifest is told apart from a write that
|
|
2171
|
+
* did not land.
|
|
2172
|
+
*
|
|
2173
|
+
* @example
|
|
2174
|
+
* ```ts
|
|
2175
|
+
* import { stageBytes } from '@orkestrel/scaffold/server'
|
|
2176
|
+
*
|
|
2177
|
+
* stageBytes(host, '/tmp/orkestrel-host-a1b2', ['scripts/codex.sh'])
|
|
2178
|
+
* // [{ storage: 'scripts/codex.sh', destination: 'scripts/codex.sh', executable: true, digest: '…' }]
|
|
2179
|
+
* ```
|
|
2180
|
+
*/
|
|
2181
|
+
export declare function stageBytes(host: Host, root: string, destinations: readonly string[]): readonly ManifestEntry[];
|
|
2150
2182
|
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2183
|
+
/**
|
|
2184
|
+
* Stages a vendored host root from a real checkout.
|
|
2185
|
+
*
|
|
2186
|
+
* @param checkout - The checkout the vendored paths are read from.
|
|
2187
|
+
* @param host - The vendored host root to fill; it must be absent or empty.
|
|
2188
|
+
* @param options - The inventory selection, establishment switch, and reporting callback.
|
|
2189
|
+
* Default: no reporting.
|
|
2190
|
+
* @returns One entry per staged file, sorted by storage name.
|
|
2191
|
+
* @throws `ScaffoldError('INVALID', …)` when either argument is not a host path
|
|
2192
|
+
* or a vendored path leaves the checkout or the host root.
|
|
2193
|
+
* @throws `ScaffoldError('TARGET', …)` when the checkout is not a directory, the
|
|
2194
|
+
* host root is not vacant, the checkout does not carry every vendored path, two
|
|
2195
|
+
* vendored files claim one storage name, a vendored file is not a plain file
|
|
2196
|
+
* within the artifact ceiling, published guides are unreadable, a staged
|
|
2197
|
+
* collision's owner set is not a subset of its published owner set, the inventory is absent without
|
|
2198
|
+
* `establish: true`, or the staged manifest does not read back.
|
|
2199
|
+
* @throws `ScaffoldError('WRITE', …)` when a staged file or the manifest cannot
|
|
2200
|
+
* be written.
|
|
2201
|
+
*
|
|
2202
|
+
* @remarks
|
|
2203
|
+
* This is the producer half of the vendored host, and it is not the mutation
|
|
2204
|
+
* contract `MaterializerInterface` states. That contract owns **target**
|
|
2205
|
+
* writes: it materializes a compiled plan into a consumer's workspace, binds
|
|
2206
|
+
* every destination to what the caller observed, and rolls a failed commit back.
|
|
2207
|
+
* This reads this package's own checkout at build time and fills its own build
|
|
2208
|
+
* output. Different direction, different lifetime, no consumer target involved,
|
|
2209
|
+
* so they do not overlap and neither one belongs inside the other.
|
|
2210
|
+
*
|
|
2211
|
+
* Staging is plain rather than transactional for the same reason. A
|
|
2212
|
+
* `WriteTransaction` exists to hold a directory that already holds work
|
|
2213
|
+
* still; a build output holds nothing, is deleted whole before every build, and
|
|
2214
|
+
* has no concurrent reader. What replaces it is refusing early and ordering the
|
|
2215
|
+
* writes: the whole membership is derived before anything is created, so a
|
|
2216
|
+
* membership refusal leaves no host root at all, and `manifest.json` is
|
|
2217
|
+
* written last, so a copy or Surface refusal leaves a root every
|
|
2218
|
+
* reader treats as a raw checkout and fails loudly on.
|
|
2219
|
+
*
|
|
2220
|
+
* A missing vendored path is refused rather than staged around. A partial root
|
|
2221
|
+
* is not detectably partial: it fails later, in a consumer's terminal, on
|
|
2222
|
+
* whichever path the plan reached first. Refusing here fails the build that
|
|
2223
|
+
* produced it, where the maintainer can act, and it names every missing path at
|
|
2224
|
+
* once. A directory is the same case — declaring an absent directory as an empty
|
|
2225
|
+
* root would create an empty directory in every generated workspace. A canon file
|
|
2226
|
+
* member the checkout no longer holds is the one exception: scaffold keeps owning
|
|
2227
|
+
* the path so a target's copy reports foreign, and the stage ships nothing for it.
|
|
2228
|
+
*
|
|
2229
|
+
* The walk covers `HOST_PATHS`, `CANON_PATHS`, and `REFERENCE_PATHS` together.
|
|
2230
|
+
* A plan selects the paths a target receives; the installed package also carries
|
|
2231
|
+
* the canon and reference files for reading. Every catalog package must have a
|
|
2232
|
+
* guide in the discovered membership before the host is written.
|
|
2233
|
+
* Copied guides must carry no collision whose name and exact owner set the
|
|
2234
|
+
* committed inventory lacks. An absent inventory requires `establish: true`;
|
|
2235
|
+
* an inventory without a valid recorded Surface is refused.
|
|
2236
|
+
* Everything downstream — the missing-path refusal, the
|
|
2237
|
+
* storage collision guard, the sort, the digests, and the root inventory — reads
|
|
2238
|
+
* the union, so every staged path follows the same law.
|
|
2239
|
+
*
|
|
2240
|
+
* The vendoring deny-list applies to what the walk discovers beneath a staged
|
|
2241
|
+
* directory, where a maintainer's local credential can legitimately sit, and
|
|
2242
|
+
* such a path is skipped. A path a staging list names itself is curated data
|
|
2243
|
+
* rather than discovery, so it is staged or the stage is refused.
|
|
2244
|
+
*
|
|
2245
|
+
* @example Vendored data root
|
|
2246
|
+
* ```ts
|
|
2247
|
+
* import { stageHost } from '@orkestrel/scaffold/server'
|
|
2248
|
+
*
|
|
2249
|
+
* stageHost(process.cwd(), 'dist/host') // one ManifestEntry per file staged
|
|
2250
|
+
* ```
|
|
2251
|
+
*/
|
|
2252
|
+
export declare function stageHost(checkout: string, host: string, options?: HostStageOptions): readonly ManifestEntry[];
|
|
2156
2253
|
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2254
|
+
/**
|
|
2255
|
+
* Stages the committed inventory of the files a vendored host carries.
|
|
2256
|
+
*
|
|
2257
|
+
* @param checkout - The checkout whose vendored paths are inventoried.
|
|
2258
|
+
* @param path - The host path where the JSON inventory is written.
|
|
2259
|
+
* @returns The validated manifest written to `path`.
|
|
2260
|
+
* @throws `ScaffoldError('INVALID', …)` when `path` is not a host path.
|
|
2261
|
+
* @throws `ScaffoldError('WRITE', …)` when a temporary host or the inventory
|
|
2262
|
+
* cannot be written or removed.
|
|
2263
|
+
* @throws `ScaffoldError('TARGET', …)` when the staged inventory does not read
|
|
2264
|
+
* back through the manifest validator.
|
|
2265
|
+
*
|
|
2266
|
+
* @remarks
|
|
2267
|
+
* Uses {@link stageHost} as the single vendored-path expansion. The temporary
|
|
2268
|
+
* host supplies the same entries, roots, per-file digests, and membership
|
|
2269
|
+
* digest as the published host while the requested output remains one JSON
|
|
2270
|
+
* file.
|
|
2271
|
+
*
|
|
2272
|
+
* @example
|
|
2273
|
+
* ```ts
|
|
2274
|
+
* import { stageInventory } from '@orkestrel/scaffold/server'
|
|
2275
|
+
*
|
|
2276
|
+
* stageInventory(process.cwd(), 'host.json') // the committed host inventory
|
|
2277
|
+
* ```
|
|
2278
|
+
*/
|
|
2279
|
+
export declare function stageInventory(checkout: string, path?: string): HostManifest;
|
|
2170
2280
|
|
|
2171
|
-
|
|
2172
|
-
|
|
2281
|
+
/** Represents a Surface name claimed by distinct package guides. */
|
|
2282
|
+
export declare interface SurfaceCollision {
|
|
2283
|
+
readonly name: string;
|
|
2284
|
+
readonly owners: readonly string[];
|
|
2285
|
+
}
|
|
2173
2286
|
|
|
2174
|
-
/**
|
|
2175
|
-
* Represents the reading spine: one bounded, unauthenticated, redirect-free request per answer.
|
|
2176
|
-
*
|
|
2177
|
-
* @remarks
|
|
2178
|
-
* This is the package's only network reader, and it never writes. Every call
|
|
2179
|
-
* opens one byte allowance and spends it across every read the call makes, so a
|
|
2180
|
-
* caller is bounded twice over: `limit` refuses one oversized answer and
|
|
2181
|
-
* `budget` refuses many small ones. They are separate exhaustion routes and
|
|
2182
|
-
* neither bound covers the other.
|
|
2183
|
-
*
|
|
2184
|
-
* A per-package failure never escapes as a throw. It is projected into the
|
|
2185
|
-
* verdict that package's row already carries — `missing` for an upstream `404`,
|
|
2186
|
-
* which is a definite answer, and `failed` for a transport fault, which is no
|
|
2187
|
-
* answer at all — so one unreachable package never costs the caller the rest of
|
|
2188
|
-
* the answer. The organization package list is the one exception: without it
|
|
2189
|
-
* there is no fleet to report, so an unreachable or malformed list is a coded
|
|
2190
|
-
* `FETCH` failure.
|
|
2191
|
-
*
|
|
2192
|
-
* The vendored-file inventory is the other read a whole call rests on, and it
|
|
2193
|
-
* fails the other way: it fails every row of its call rather than throwing, so
|
|
2194
|
-
* the caller receives one whole dead answer it can replace with one whole
|
|
2195
|
-
* baseline instead of a mixture it would have to reconcile.
|
|
2196
|
-
*
|
|
2197
|
-
* Requests are unauthenticated because every fleet repository is public, and
|
|
2198
|
-
* they follow no redirect, so a misconfigured or hostile endpoint cannot move a
|
|
2199
|
-
* read to another host. Each one is bounded by its endpoint's timeout and by the
|
|
2200
|
-
* reader's own abort signal, so {@link Upstream.destroy} cancels what is in
|
|
2201
|
-
* flight instead of waiting for it.
|
|
2202
|
-
*
|
|
2203
|
-
* The allowance is threaded through the private reads as a mutable
|
|
2204
|
-
* {@link ReadAllowance} carrier rather than held on the instance, because
|
|
2205
|
-
* concurrent calls each own their own budget and must not spend each other's.
|
|
2206
|
-
*
|
|
2207
|
-
* @example
|
|
2208
|
-
* ```ts
|
|
2209
|
-
* import { Upstream } from '@orkestrel/scaffold/server'
|
|
2210
|
-
*
|
|
2211
|
-
* const upstream = new Upstream({ registry: { timeout: 5_000 } })
|
|
2212
|
-
* const releases = await upstream.lookup([{ name: '@orkestrel/emitter', range: '^0.0.5' }])
|
|
2213
|
-
*
|
|
2214
|
-
* releases.filter((release) => release.lookup === 'found')
|
|
2215
|
-
* upstream.destroy()
|
|
2216
|
-
* ```
|
|
2217
|
-
*/
|
|
2218
|
-
export declare class Upstream implements UpstreamInterface {
|
|
2219
|
-
#private;
|
|
2220
|
-
/**
|
|
2221
|
-
* Constructs a reader over one raw content host and one registry.
|
|
2222
|
-
*
|
|
2223
|
-
* @param options - The endpoints, the request bounds, the initial
|
|
2224
|
-
* listeners, and the listener-error handler.
|
|
2225
|
-
* @throws {@link ScaffoldError} coded `INVALID` when `options` is present but
|
|
2226
|
-
* is not an option bag this reader accepts, or when either endpoint names a
|
|
2227
|
-
* scheme, host, or form this reader will not request.
|
|
2228
|
-
*
|
|
2229
|
-
* @remarks
|
|
2230
|
-
* `isEndpoint` bounds an endpoint's length and nothing else, so the scheme and
|
|
2231
|
-
* host law is settled here, where a refusal can say which endpoint was refused
|
|
2232
|
-
* and why. An endpoint must be HTTPS, or HTTP to a loopback host — the one
|
|
2233
|
-
* place an unencrypted request has no network between its ends. That
|
|
2234
|
-
* refuses `file:`, `data:`, and plain HTTP to a real host, and it is what
|
|
2235
|
-
* keeps a fixture reachable without weakening transport security anywhere a
|
|
2236
|
-
* real request goes. An endpoint carrying credentials, a query, or a fragment
|
|
2237
|
-
* is refused too: this reader authenticates nothing and appends its own path.
|
|
2238
|
-
*/
|
|
2239
|
-
constructor(options?: UpstreamOptions);
|
|
2240
|
-
/** Exposes the upstream reader's observation channel. */
|
|
2241
|
-
get emitter(): EmitterInterface<UpstreamEventMap>;
|
|
2242
2287
|
/**
|
|
2243
|
-
*
|
|
2288
|
+
* Reports the outcome of one bounded read whose body is taken as text.
|
|
2244
2289
|
*
|
|
2245
|
-
* @
|
|
2246
|
-
*
|
|
2247
|
-
*
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2290
|
+
* @remarks
|
|
2291
|
+
* `content` carries the body only when `lookup` is `found`; otherwise it is
|
|
2292
|
+
* empty and `note` states what happened. A read that is not `found` is an answer
|
|
2293
|
+
* rather than a throw, which is what lets one dead row sit beside live ones.
|
|
2294
|
+
*/
|
|
2295
|
+
export declare interface TextReadResult {
|
|
2296
|
+
readonly lookup: Lookup;
|
|
2297
|
+
readonly content: string;
|
|
2298
|
+
readonly note: string;
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
/** Holds the note a release carries when its packument names no readable latest version. */
|
|
2302
|
+
export declare const UNREADABLE_VERSION_NOTE = "the answer carries no readable latest version";
|
|
2303
|
+
|
|
2304
|
+
/**
|
|
2305
|
+
* Represents the reading spine: one bounded, unauthenticated, redirect-free request per answer.
|
|
2306
|
+
*
|
|
2307
|
+
* @remarks
|
|
2308
|
+
* This is the package's only network reader, and it never writes. Every call
|
|
2309
|
+
* opens one byte allowance and spends it across every read the call makes, so a
|
|
2310
|
+
* caller is bounded twice over: `limit` refuses one oversized answer and
|
|
2311
|
+
* `budget` refuses many small ones. They are separate exhaustion routes and
|
|
2312
|
+
* neither bound covers the other.
|
|
2313
|
+
*
|
|
2314
|
+
* A per-package failure never escapes as a throw. It is projected into the
|
|
2315
|
+
* verdict that package's row already carries — `missing` for an upstream `404`,
|
|
2316
|
+
* which is a definite answer, and `failed` for a transport fault, which is no
|
|
2317
|
+
* answer at all — so one unreachable package never costs the caller the rest of
|
|
2318
|
+
* the answer. The organization package list is the one exception: without it
|
|
2319
|
+
* there is no fleet to report, so an unreachable or malformed list is a coded
|
|
2320
|
+
* `FETCH` failure.
|
|
2321
|
+
*
|
|
2322
|
+
* The vendored-file inventory is the other read a whole call rests on, and it
|
|
2323
|
+
* fails the other way: it fails every row of its call rather than throwing, so
|
|
2324
|
+
* the caller receives one whole dead answer it can replace with one whole
|
|
2325
|
+
* baseline instead of a mixture it would have to reconcile.
|
|
2326
|
+
*
|
|
2327
|
+
* Requests are unauthenticated because every fleet repository is public, and
|
|
2328
|
+
* they follow no redirect, so a misconfigured or hostile endpoint cannot move a
|
|
2329
|
+
* read to another host. Each one is bounded by its endpoint's timeout and by the
|
|
2330
|
+
* reader's own abort signal, so {@link Upstream.destroy} cancels what is in
|
|
2331
|
+
* flight instead of waiting for it.
|
|
2332
|
+
*
|
|
2333
|
+
* Each call constructs its own {@link ReadAllowance} from the reader's budget and passes it
|
|
2334
|
+
* through the private reads rather than holding it on the instance, because concurrent calls
|
|
2335
|
+
* each own a budget and must not spend each other's.
|
|
2336
|
+
*
|
|
2337
|
+
* @example
|
|
2338
|
+
* ```ts
|
|
2339
|
+
* import { Upstream } from '@orkestrel/scaffold/server'
|
|
2340
|
+
*
|
|
2341
|
+
* const upstream = new Upstream({ registry: { timeout: 5_000 } })
|
|
2342
|
+
* const releases = await upstream.lookup([{ name: '@orkestrel/emitter', range: '^0.0.5' }])
|
|
2343
|
+
*
|
|
2344
|
+
* releases.filter((release) => release.lookup === 'found')
|
|
2345
|
+
* upstream.destroy()
|
|
2346
|
+
* ```
|
|
2347
|
+
*/
|
|
2348
|
+
export declare class Upstream implements UpstreamInterface {
|
|
2349
|
+
#private;
|
|
2350
|
+
/**
|
|
2351
|
+
* Constructs a reader over one raw content host and one registry.
|
|
2261
2352
|
*
|
|
2262
|
-
*
|
|
2263
|
-
*
|
|
2264
|
-
*
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
*
|
|
2283
|
-
* later write is held to and a failed fetch does not change what the target
|
|
2284
|
-
* holds.
|
|
2285
|
-
*
|
|
2286
|
-
* @example
|
|
2287
|
-
* ```ts
|
|
2288
|
-
* import { Upstream } from '@orkestrel/scaffold/server'
|
|
2353
|
+
* @param options - The endpoints, the request bounds, the initial
|
|
2354
|
+
* listeners, and the listener-error handler.
|
|
2355
|
+
* @throws {@link ScaffoldError} coded `INVALID` when `options` is present but
|
|
2356
|
+
* is not an option bag this reader accepts, or when either endpoint names a
|
|
2357
|
+
* scheme, host, or form this reader will not request.
|
|
2358
|
+
*
|
|
2359
|
+
* @remarks
|
|
2360
|
+
* `isEndpoint` bounds an endpoint's length and nothing else, so the scheme and
|
|
2361
|
+
* host law is settled here, where a refusal can say which endpoint was refused
|
|
2362
|
+
* and why. An endpoint must be HTTPS, or HTTP to a loopback host — the one
|
|
2363
|
+
* place an unencrypted request has no network between its ends. That
|
|
2364
|
+
* refuses `file:`, `data:`, and plain HTTP to a real host, and it is what
|
|
2365
|
+
* keeps a fixture reachable without weakening transport security anywhere a
|
|
2366
|
+
* real request goes. An endpoint carrying credentials, a query, or a fragment
|
|
2367
|
+
* is refused too: this reader authenticates nothing and appends its own path.
|
|
2368
|
+
*/
|
|
2369
|
+
constructor(options?: UpstreamOptions);
|
|
2370
|
+
/** Exposes the upstream reader's observation channel. */
|
|
2371
|
+
get emitter(): EmitterInterface<UpstreamEventMap>;
|
|
2372
|
+
/**
|
|
2373
|
+
* Looks up the newest release each declared range admits.
|
|
2289
2374
|
*
|
|
2290
|
-
*
|
|
2291
|
-
*
|
|
2292
|
-
*
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
*
|
|
2315
|
-
* inventory declares for that path, before any character decoding. Transport
|
|
2316
|
-
* encoding is transparent and does not enter the comparison, so content that
|
|
2317
|
-
* does not hash to the inventory's claim fails its row rather than reaching a
|
|
2318
|
-
* write. That is integrity against a single committed baseline, not
|
|
2319
|
-
* authenticity: it detects truncated, substituted, or stale content, and it
|
|
2320
|
-
* says nothing about who published the inventory.
|
|
2321
|
-
*
|
|
2322
|
-
* A guide mirror is never answered here whatever the caller asks for and
|
|
2323
|
-
* whatever the target holds, because those bytes belong to `fetch` and to the
|
|
2324
|
-
* mirror verb that writes them.
|
|
2325
|
-
*
|
|
2326
|
-
* @example
|
|
2327
|
-
* ```ts
|
|
2328
|
-
* import { Upstream } from '@orkestrel/scaffold/server'
|
|
2375
|
+
* @param dependencies - The declared dependencies to look up.
|
|
2376
|
+
* @returns One release verdict per dependency, in input order.
|
|
2377
|
+
* @throws {@link ScaffoldError} coded `INVALID` when `dependencies` is not a
|
|
2378
|
+
* bounded list of declared dependencies, and `DESTROYED` when the reader is
|
|
2379
|
+
* torn down before or during the call.
|
|
2380
|
+
*
|
|
2381
|
+
* @remarks
|
|
2382
|
+
* The reader selects across the packument's stable version map before any
|
|
2383
|
+
* collection bound can truncate it. It falls back to `dist-tags.latest` only
|
|
2384
|
+
* when that version satisfies the declared range. `*` requests the newest
|
|
2385
|
+
* stable version outright. The verdict's `major` field carries the stable
|
|
2386
|
+
* major from the latest tag when that tag can be read.
|
|
2387
|
+
*
|
|
2388
|
+
* @example
|
|
2389
|
+
* ```ts
|
|
2390
|
+
* import { Upstream } from '@orkestrel/scaffold/server'
|
|
2391
|
+
*
|
|
2392
|
+
* const upstream = new Upstream()
|
|
2393
|
+
* await upstream.lookup([{ name: '@orkestrel/router', range: '^0.0.8' }])
|
|
2394
|
+
* upstream.destroy()
|
|
2395
|
+
* ```
|
|
2396
|
+
*/
|
|
2397
|
+
lookup(dependencies: readonly Dependency[]): Promise<readonly Release[]>;
|
|
2398
|
+
/**
|
|
2399
|
+
* Fetches each named package's guide, beside the local mirror it answers for.
|
|
2329
2400
|
*
|
|
2330
|
-
*
|
|
2331
|
-
*
|
|
2332
|
-
*
|
|
2333
|
-
*
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
* @example
|
|
2355
|
-
* ```ts
|
|
2356
|
-
* import { Upstream } from '@orkestrel/scaffold/server'
|
|
2357
|
-
*
|
|
2358
|
-
* const upstream = new Upstream()
|
|
2359
|
-
* const entries = await upstream.catalog()
|
|
2360
|
-
* upstream.destroy()
|
|
2361
|
-
* ```
|
|
2362
|
-
*/
|
|
2363
|
-
catalog(): Promise<readonly CatalogEntry[]>;
|
|
2364
|
-
/**
|
|
2365
|
-
* Tears the reader down, aborting every request in flight. Teardown is idempotent.
|
|
2366
|
-
*
|
|
2367
|
-
* @returns Nothing.
|
|
2368
|
-
*
|
|
2369
|
-
* @remarks
|
|
2370
|
-
* A call still in flight rejects with a `DESTROYED` error rather than
|
|
2371
|
-
* resolving to a partial answer, because half a fleet reads exactly like a
|
|
2372
|
-
* whole one.
|
|
2373
|
-
*
|
|
2374
|
-
* @example
|
|
2375
|
-
* ```ts
|
|
2376
|
-
* import { Upstream } from '@orkestrel/scaffold/server'
|
|
2377
|
-
*
|
|
2378
|
-
* const upstream = new Upstream()
|
|
2379
|
-
* upstream.destroy()
|
|
2380
|
-
* upstream.emitter.destroyed // true
|
|
2381
|
-
* ```
|
|
2382
|
-
*/
|
|
2383
|
-
destroy(): void;
|
|
2384
|
-
}
|
|
2385
|
-
|
|
2386
|
-
/**
|
|
2387
|
-
* Represents the upstream reader's observation channel.
|
|
2388
|
-
*
|
|
2389
|
-
* @remarks
|
|
2390
|
-
* Each verdict is published whole rather than as a name beside a summary, so a
|
|
2391
|
-
* listener reads the same value the call returns and a failed lookup is told
|
|
2392
|
-
* apart from a successful one by the verdict's own discriminant rather than by
|
|
2393
|
-
* which event carried it.
|
|
2394
|
-
*/
|
|
2395
|
-
export declare type UpstreamEventMap = {
|
|
2396
|
-
readonly release: readonly [release: Release];
|
|
2397
|
-
readonly mirror: readonly [mirror: Mirror];
|
|
2398
|
-
readonly file: readonly [file: HostFile];
|
|
2399
|
-
readonly error: readonly [error: unknown];
|
|
2400
|
-
readonly destroy: readonly [];
|
|
2401
|
-
};
|
|
2402
|
-
|
|
2403
|
-
/**
|
|
2404
|
-
* Describes the upstream contract: the package's only network reader, and it never writes.
|
|
2405
|
-
*
|
|
2406
|
-
* @remarks
|
|
2407
|
-
* A per-package failure is collected as a verdict carrying its cause, not
|
|
2408
|
-
* thrown, so one unreachable package never costs the caller the rest of the
|
|
2409
|
-
* answer. The organization list is the exception: without it there is no fleet
|
|
2410
|
-
* to report, so an unreachable or malformed list is a coded failure.
|
|
2411
|
-
*
|
|
2412
|
-
* The vendored-file inventory is the other whole-answer read, and it fails
|
|
2413
|
-
* softly: an inventory that produces no answer fails every row of that call
|
|
2414
|
-
* rather than throwing, which is what leaves the caller one whole baseline to
|
|
2415
|
-
* fall back to instead of a mixture.
|
|
2416
|
-
*/
|
|
2417
|
-
export declare interface UpstreamInterface {
|
|
2418
|
-
readonly emitter: EmitterInterface<UpstreamEventMap>;
|
|
2419
|
-
/**
|
|
2420
|
-
* Looks up the newest release each declared range admits.
|
|
2421
|
-
*
|
|
2422
|
-
* @param dependencies - The declared dependencies to look up.
|
|
2423
|
-
* @returns One release verdict per dependency, in input order.
|
|
2424
|
-
*/
|
|
2425
|
-
lookup(dependencies: readonly Dependency[]): Promise<readonly Release[]>;
|
|
2426
|
-
/**
|
|
2427
|
-
* Fetches each named package's guide, beside the local mirror it answers for.
|
|
2428
|
-
*
|
|
2429
|
-
* @param names - The packages to fetch: the target's declared set, or the whole organization.
|
|
2430
|
-
* @param current - The target's local mirrors as exact bytes, keyed by mirror path.
|
|
2431
|
-
* @returns One mirror verdict per name, in input order.
|
|
2432
|
-
*/
|
|
2401
|
+
* @param names - The packages to fetch: the target's declared set, or the whole organization.
|
|
2402
|
+
* @param current - The target's local mirrors as exact bytes, keyed by mirror path.
|
|
2403
|
+
* @returns One mirror verdict per name, in input order.
|
|
2404
|
+
* @throws {@link ScaffoldError} coded `INVALID` when `names` is not a bounded
|
|
2405
|
+
* list of fleet package names or `current` is not a snapshot, and `DESTROYED`
|
|
2406
|
+
* when the reader is torn down before or during the call.
|
|
2407
|
+
*
|
|
2408
|
+
* @remarks
|
|
2409
|
+
* The mirror path is derived from the package name by the same helper the plan
|
|
2410
|
+
* derives it with, and the fetched URL ends in that exact path, so a verdict
|
|
2411
|
+
* always answers for the file it names. `observed` is carried through from
|
|
2412
|
+
* `current` whatever the lookup produced, because it is the precondition the
|
|
2413
|
+
* later write is held to and a failed fetch does not change what the target
|
|
2414
|
+
* holds.
|
|
2415
|
+
*
|
|
2416
|
+
* @example
|
|
2417
|
+
* ```ts
|
|
2418
|
+
* import { Upstream } from '@orkestrel/scaffold/server'
|
|
2419
|
+
*
|
|
2420
|
+
* const upstream = new Upstream()
|
|
2421
|
+
* await upstream.fetch(['@orkestrel/router'], { 'guides/router.md': '2320526f75746572' })
|
|
2422
|
+
* upstream.destroy()
|
|
2423
|
+
* ```
|
|
2424
|
+
*/
|
|
2433
2425
|
fetch(names: readonly string[], current: Snapshot): Promise<readonly Mirror[]>;
|
|
2434
2426
|
/**
|
|
2435
2427
|
* Reads each named vendored file from the repository, beside the target bytes it answers for.
|
|
@@ -2437,307 +2429,445 @@ export declare class Materializer implements MaterializerInterface {
|
|
|
2437
2429
|
* @param paths - The target-relative vendored paths to read.
|
|
2438
2430
|
* @param current - The target files as exact bytes, keyed by the same paths.
|
|
2439
2431
|
* @returns One file verdict per path, in input order.
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
* Catalogs the published fleet from the registry's organization package list.
|
|
2444
|
-
*
|
|
2445
|
-
* @returns One row per published package, sorted by name.
|
|
2446
|
-
*/
|
|
2447
|
-
catalog(): Promise<readonly CatalogEntry[]>;
|
|
2448
|
-
/**
|
|
2449
|
-
* Tears the reader down, aborting every request in flight. Teardown is idempotent.
|
|
2450
|
-
*
|
|
2451
|
-
* @returns Nothing.
|
|
2452
|
-
*/
|
|
2453
|
-
destroy(): void;
|
|
2454
|
-
}
|
|
2455
|
-
|
|
2456
|
-
/**
|
|
2457
|
-
* Represents the options for the upstream reader.
|
|
2458
|
-
*
|
|
2459
|
-
* @remarks
|
|
2460
|
-
* The endpoints are grouped under the entity each configures: `repository`
|
|
2461
|
-
* takes the raw content host's `base`, its `branch`, and its `timeout`;
|
|
2462
|
-
* `registry` takes the registry's `base` and `timeout`. One raw content host
|
|
2463
|
-
* serves both the fleet's guides and this package's own vendored files, so one
|
|
2464
|
-
* group configures both. `concurrency` bounds requests in
|
|
2465
|
-
* flight and `retries` opts into per-request retry on a transport fault.
|
|
2466
|
-
* `limit` bounds the bytes read from one response body and `budget` bounds the
|
|
2467
|
-
* bytes read across a whole call, so neither one oversized answer nor many
|
|
2468
|
-
* small ones can exhaust the caller. Every request is unauthenticated and
|
|
2469
|
-
* follows no redirect.
|
|
2470
|
-
*/
|
|
2471
|
-
export declare interface UpstreamOptions {
|
|
2472
|
-
readonly repository?: {
|
|
2473
|
-
readonly base?: string;
|
|
2474
|
-
readonly branch?: string;
|
|
2475
|
-
readonly timeout?: number;
|
|
2476
|
-
};
|
|
2477
|
-
readonly registry?: {
|
|
2478
|
-
readonly base?: string;
|
|
2479
|
-
readonly timeout?: number;
|
|
2480
|
-
};
|
|
2481
|
-
readonly concurrency?: number;
|
|
2482
|
-
readonly retries?: number;
|
|
2483
|
-
readonly limit?: number;
|
|
2484
|
-
readonly budget?: number;
|
|
2485
|
-
readonly on?: EmitterHooks<UpstreamEventMap>;
|
|
2486
|
-
readonly error?: EmitterErrorHandler;
|
|
2487
|
-
}
|
|
2488
|
-
|
|
2489
|
-
/**
|
|
2490
|
-
* Describes what git reports about a target's working tree.
|
|
2491
|
-
*
|
|
2492
|
-
* @remarks
|
|
2493
|
-
* `tracked` is the only set a deletion may draw from: git does not report the
|
|
2494
|
-
* loss of an untracked path and `git diff` cannot restore it, so an ignored
|
|
2495
|
-
* path such as an installed dependency tree, a build output, or an editor
|
|
2496
|
-
* directory survives every verb. `dirty` is every path carrying an uncommitted
|
|
2497
|
-
* change, taken repo-wide rather than over a write set, because deletion makes
|
|
2498
|
-
* the write set the whole workspace. A clean tree is an empty `dirty`. A target
|
|
2499
|
-
* that is not a git repository yields no `Worktree` at all, so the caller
|
|
2500
|
-
* decides what to do about that rather than reading it out of an invented
|
|
2501
|
-
* empty value.
|
|
2502
|
-
*/
|
|
2503
|
-
export declare interface Worktree {
|
|
2504
|
-
readonly tracked: readonly string[];
|
|
2505
|
-
readonly dirty: readonly string[];
|
|
2506
|
-
}
|
|
2507
|
-
|
|
2508
|
-
/**
|
|
2509
|
-
* Represents one physical directory identity captured across a write transaction.
|
|
2510
|
-
*
|
|
2511
|
-
* @remarks
|
|
2512
|
-
* Native bigint device and inode values locate the directory and do not date it.
|
|
2513
|
-
* Two directories occupying one slot at different times share an anchor, so an anchor proves
|
|
2514
|
-
* where a write lands and never that the directory was left alone.
|
|
2515
|
-
*/
|
|
2516
|
-
export declare interface WriteAnchor {
|
|
2517
|
-
readonly path: string;
|
|
2518
|
-
readonly device: bigint;
|
|
2519
|
-
readonly inode: bigint;
|
|
2520
|
-
}
|
|
2521
|
-
|
|
2522
|
-
/** Reports the final directory anchor of a write transaction and the subset one call created. */
|
|
2523
|
-
export declare interface WriteDirectoryResult {
|
|
2524
|
-
readonly anchor: WriteAnchor;
|
|
2525
|
-
readonly created: readonly WriteAnchor[];
|
|
2526
|
-
}
|
|
2527
|
-
|
|
2528
|
-
/**
|
|
2529
|
-
* Represents one destination snapshot captured before a write and required to survive it.
|
|
2530
|
-
*
|
|
2531
|
-
* @remarks
|
|
2532
|
-
* `device`, `inode`, `modified`, `size`, and `digest` are present only where
|
|
2533
|
-
* the observed shape supplies them.
|
|
2534
|
-
*/
|
|
2535
|
-
export declare interface WriteExpectation {
|
|
2536
|
-
readonly path: string;
|
|
2537
|
-
readonly shape: 'absent' | 'file' | 'directory';
|
|
2538
|
-
readonly device?: number;
|
|
2539
|
-
readonly inode?: number;
|
|
2540
|
-
readonly modified?: number;
|
|
2541
|
-
readonly size?: number;
|
|
2542
|
-
readonly digest?: string;
|
|
2543
|
-
}
|
|
2544
|
-
|
|
2545
|
-
/**
|
|
2546
|
-
* Describes the narrower caller-observed destination state a write transaction must
|
|
2547
|
-
* still match.
|
|
2548
|
-
*/
|
|
2549
|
-
export declare interface WritePrecondition {
|
|
2550
|
-
readonly path: string;
|
|
2551
|
-
readonly shape: 'absent' | 'file';
|
|
2552
|
-
readonly digest?: string;
|
|
2553
|
-
}
|
|
2554
|
-
|
|
2555
|
-
/**
|
|
2556
|
-
* Represents one staged, reversible mutation of one target directory.
|
|
2557
|
-
*
|
|
2558
|
-
* @remarks
|
|
2559
|
-
* The transaction owns a private root beside the target — a sibling directory on
|
|
2560
|
-
* the same volume, so every promotion is a rename rather than a copy. Staging
|
|
2561
|
-
* writes go into that root and nothing else, so a call that fails while staging
|
|
2562
|
-
* has not touched the target at all. Commit is the only step that mutates the
|
|
2563
|
-
* target, and it is the only step that can need rolling back.
|
|
2564
|
-
*
|
|
2565
|
-
* A precondition and an expectation hold a destination still. A **precondition** is what the caller
|
|
2566
|
-
* observed earlier and is checked once, at construction, so a target that moved
|
|
2567
|
-
* between the caller's read and this transaction fails before anything is
|
|
2568
|
-
* created. An **expectation** is captured here, at construction, and re-checked
|
|
2569
|
-
* at commit, so a target that moves while the write is being staged fails before
|
|
2570
|
-
* anything is promoted.
|
|
2571
|
-
*
|
|
2572
|
-
* What this provides, exactly:
|
|
2573
|
-
*
|
|
2574
|
-
* - **Across destinations, staged-then-swapped with rollback on a caught
|
|
2575
|
-
* failure.** A failure part way through commit restores every destination it
|
|
2576
|
-
* already promoted, restores every file it already took, and removes every
|
|
2577
|
-
* directory it created, then reports what recovery could not undo. This is
|
|
2578
|
-
* measured: a promotion is driven to fail after an earlier one landed, and the
|
|
2579
|
-
* earlier destination is read back.
|
|
2580
|
-
* - **No partly written destination.** Every file is written whole into the
|
|
2581
|
-
* private root and digested there before commit, so a destination never
|
|
2582
|
-
* receives bytes that were still being produced.
|
|
2583
|
-
* - **Containment, not continuity, of the directories it creates.** Every
|
|
2584
|
-
* ancestor is re-read between `mkdir` calls and again before the first
|
|
2585
|
-
* promotion, so an ancestor that became a file, a symlink, a directory
|
|
2586
|
-
* elsewhere, or nothing is refused. An ancestor deleted and recreated under
|
|
2587
|
-
* the same name can receive its old inode back and is indistinguishable here
|
|
2588
|
-
* from one that never moved.
|
|
2589
|
-
* - **No crash atomicity across destinations.** A process killed between
|
|
2590
|
-
* promotions leaves the target holding some new files and some old ones, and
|
|
2591
|
-
* leaves the private root behind. Nothing here is a journal, and the private
|
|
2592
|
-
* root's name is the only record a later run could read.
|
|
2593
|
-
*
|
|
2594
|
-
* A destination is preserved by hard link and then replaced by a single
|
|
2595
|
-
* `rename`, rather than moved aside and replaced, so the path continues to name
|
|
2596
|
-
* the old file right up to the swap. That is a property of `rename` on the host,
|
|
2597
|
-
* not one this package's tests measure: telling it apart from move-then-replace
|
|
2598
|
-
* needs a reader observing the destination inside the swap, and no test here
|
|
2599
|
-
* does that. Read the claim as the mechanism it describes, not as a proven
|
|
2600
|
-
* guarantee about a concurrent reader.
|
|
2601
|
-
*
|
|
2602
|
-
* A path names a file this transaction writes, takes, or establishes as a
|
|
2603
|
-
* directory; every path is target-relative and is measured by the portable-path
|
|
2604
|
-
* law. An expectation and a precondition both name the resolved destination
|
|
2605
|
-
* instead, because that is the path each is re-read at.
|
|
2606
|
-
*
|
|
2607
|
-
* @example
|
|
2608
|
-
* ```ts
|
|
2609
|
-
* import { WriteTransaction } from '@orkestrel/scaffold/server'
|
|
2610
|
-
*
|
|
2611
|
-
* const transaction = new WriteTransaction('./packages/router', ['AGENTS.md'])
|
|
2612
|
-
* try {
|
|
2613
|
-
* transaction.write('AGENTS.md', '# Agents\n')
|
|
2614
|
-
* transaction.commit() // ['AGENTS.md']
|
|
2615
|
-
* } finally {
|
|
2616
|
-
* transaction.discard()
|
|
2617
|
-
* }
|
|
2618
|
-
* ```
|
|
2619
|
-
*/
|
|
2620
|
-
export declare class WriteTransaction {
|
|
2621
|
-
#private;
|
|
2622
|
-
/**
|
|
2623
|
-
* Opens a transaction over one target directory.
|
|
2624
|
-
*
|
|
2625
|
-
* @param target - The directory every path is written beneath.
|
|
2626
|
-
* @param paths - Every target-relative path this transaction may touch.
|
|
2627
|
-
* @param preconditions - The caller-observed destination states the whole
|
|
2628
|
-
* transaction is held to, each naming a resolved destination of `paths`.
|
|
2629
|
-
* @throws {@link ScaffoldError} coded `INVALID` when an argument is off
|
|
2630
|
-
* contract, `TARGET` when a destination is a shape this package will not write
|
|
2631
|
-
* over or no longer matches its precondition, and `WRITE` when the private
|
|
2632
|
-
* root cannot be established.
|
|
2432
|
+
* @throws {@link ScaffoldError} coded `INVALID` when `paths` is not a bounded
|
|
2433
|
+
* list of target-relative paths or `current` is not a snapshot, and
|
|
2434
|
+
* `DESTROYED` when the reader is torn down before or during the call.
|
|
2633
2435
|
*
|
|
2634
2436
|
* @remarks
|
|
2635
|
-
*
|
|
2636
|
-
*
|
|
2637
|
-
*
|
|
2638
|
-
*
|
|
2437
|
+
* The committed inventory is read once per call and decides every row, so a
|
|
2438
|
+
* path whose declared digest already matches the target's own bytes is `found`
|
|
2439
|
+
* without a request and the call spends nothing on it. An inventory that
|
|
2440
|
+
* produces no answer fails every row of the call rather than leaving some rows
|
|
2441
|
+
* live and some dead, which is what leaves the caller one whole baseline to
|
|
2442
|
+
* fall back to. A path the inventory does not name is `missing`.
|
|
2443
|
+
*
|
|
2444
|
+
* A fetched response's decoded content is verified against the digest the
|
|
2445
|
+
* inventory declares for that path, before any character decoding. Transport
|
|
2446
|
+
* encoding is transparent and does not enter the comparison, so content that
|
|
2447
|
+
* does not hash to the inventory's claim fails its row rather than reaching a
|
|
2448
|
+
* write. That is integrity against a single committed baseline, not
|
|
2449
|
+
* authenticity: it detects truncated, substituted, or stale content, and it
|
|
2450
|
+
* says nothing about who published the inventory.
|
|
2451
|
+
*
|
|
2452
|
+
* A guide mirror is never answered here whatever the caller asks for and
|
|
2453
|
+
* whatever the target holds, because those bytes belong to `fetch` and to the
|
|
2454
|
+
* mirror verb that writes them.
|
|
2455
|
+
*
|
|
2456
|
+
* @example
|
|
2457
|
+
* ```ts
|
|
2458
|
+
* import { Upstream } from '@orkestrel/scaffold/server'
|
|
2459
|
+
*
|
|
2460
|
+
* const upstream = new Upstream()
|
|
2461
|
+
* await upstream.read(['AGENTS.md'], { 'AGENTS.md': '2320416745' })
|
|
2462
|
+
* upstream.destroy()
|
|
2463
|
+
* ```
|
|
2639
2464
|
*/
|
|
2640
|
-
|
|
2641
|
-
/** Names the resolved directory every path is written beneath. */
|
|
2642
|
-
get target(): string;
|
|
2643
|
-
/** Reports what each destination held when the transaction opened, in path order. */
|
|
2644
|
-
get expectations(): readonly WriteExpectation[];
|
|
2645
|
-
/** Reports whether the transaction can still be committed or discarded. */
|
|
2646
|
-
get open(): boolean;
|
|
2465
|
+
read(paths: readonly string[], current: Snapshot): Promise<readonly HostFile[]>;
|
|
2647
2466
|
/**
|
|
2648
|
-
*
|
|
2467
|
+
* Catalogs the published fleet from the registry's organization package list.
|
|
2649
2468
|
*
|
|
2650
|
-
* @
|
|
2651
|
-
* @
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
*
|
|
2655
|
-
* a directory, and `WRITE` when the staged file cannot be written or does not
|
|
2656
|
-
* carry the bytes it was given.
|
|
2469
|
+
* @returns One row per published package, sorted by name.
|
|
2470
|
+
* @throws {@link ScaffoldError} coded `FETCH` when the organization package
|
|
2471
|
+
* list is unreachable, malformed, empty, or larger than one bounded
|
|
2472
|
+
* collection, and `DESTROYED` when the reader is torn down before or during
|
|
2473
|
+
* the call.
|
|
2657
2474
|
*
|
|
2658
2475
|
* @remarks
|
|
2659
|
-
* The
|
|
2660
|
-
*
|
|
2476
|
+
* The organization list is the exact membership the registry publishes, not a
|
|
2477
|
+
* relevance search, and it is the whole reason this answer can claim to be the
|
|
2478
|
+
* fleet. So it fails hard where a per-package lookup fails soft: an empty or
|
|
2479
|
+
* unreadable list would otherwise be written into a target's catalog table as
|
|
2480
|
+
* a fleet with no packages in it. A package whose own version lookup then
|
|
2481
|
+
* fails still keeps its row, carrying the cause instead of a version, because
|
|
2482
|
+
* the list already proved the package is published.
|
|
2483
|
+
*
|
|
2484
|
+
* @example
|
|
2485
|
+
* ```ts
|
|
2486
|
+
* import { Upstream } from '@orkestrel/scaffold/server'
|
|
2487
|
+
*
|
|
2488
|
+
* const upstream = new Upstream()
|
|
2489
|
+
* const entries = await upstream.catalog()
|
|
2490
|
+
* upstream.destroy()
|
|
2491
|
+
* ```
|
|
2661
2492
|
*/
|
|
2662
|
-
|
|
2493
|
+
catalog(): Promise<readonly CatalogEntry[]>;
|
|
2663
2494
|
/**
|
|
2664
|
-
*
|
|
2495
|
+
* Tears the reader down, aborting every request in flight. Teardown is idempotent.
|
|
2665
2496
|
*
|
|
2666
|
-
* @param path - The target-relative path to write.
|
|
2667
|
-
* @param source - The resolved absolute path to copy the bytes from.
|
|
2668
|
-
* @param executable - If `true`, the destination carries the executable bit; if `false`, it does not.
|
|
2669
2497
|
* @returns Nothing.
|
|
2670
|
-
*
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2498
|
+
*
|
|
2499
|
+
* @remarks
|
|
2500
|
+
* A call still in flight rejects with a `DESTROYED` error rather than
|
|
2501
|
+
* resolving to a partial answer, because half a fleet reads exactly like a
|
|
2502
|
+
* whole one.
|
|
2503
|
+
*
|
|
2504
|
+
* @example
|
|
2505
|
+
* ```ts
|
|
2506
|
+
* import { Upstream } from '@orkestrel/scaffold/server'
|
|
2507
|
+
*
|
|
2508
|
+
* const upstream = new Upstream()
|
|
2509
|
+
* upstream.destroy()
|
|
2510
|
+
* upstream.emitter.destroyed // true
|
|
2511
|
+
* ```
|
|
2512
|
+
*/
|
|
2513
|
+
destroy(): void;
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
/**
|
|
2517
|
+
* Represents the upstream reader's observation channel.
|
|
2518
|
+
*
|
|
2519
|
+
* @remarks
|
|
2520
|
+
* Each verdict is published whole rather than as a name beside a summary, so a
|
|
2521
|
+
* listener reads the same value the call returns and a failed lookup is told
|
|
2522
|
+
* apart from a successful one by the verdict's own discriminant rather than by
|
|
2523
|
+
* which event carried it.
|
|
2524
|
+
*/
|
|
2525
|
+
export declare type UpstreamEventMap = {
|
|
2526
|
+
readonly release: readonly [release: Release];
|
|
2527
|
+
readonly mirror: readonly [mirror: Mirror];
|
|
2528
|
+
readonly file: readonly [file: HostFile];
|
|
2529
|
+
readonly error: readonly [error: unknown];
|
|
2530
|
+
readonly destroy: readonly [];
|
|
2531
|
+
};
|
|
2532
|
+
|
|
2533
|
+
/**
|
|
2534
|
+
* Describes the upstream contract: the package's only network reader, and it never writes.
|
|
2535
|
+
*
|
|
2536
|
+
* @remarks
|
|
2537
|
+
* A per-package failure is collected as a verdict carrying its cause, not
|
|
2538
|
+
* thrown, so one unreachable package never costs the caller the rest of the
|
|
2539
|
+
* answer. The organization list is the exception: without it there is no fleet
|
|
2540
|
+
* to report, so an unreachable or malformed list is a coded failure.
|
|
2541
|
+
*
|
|
2542
|
+
* The vendored-file inventory is the other whole-answer read, and it fails
|
|
2543
|
+
* softly: an inventory that produces no answer fails every row of that call
|
|
2544
|
+
* rather than throwing, which is what leaves the caller one whole baseline to
|
|
2545
|
+
* fall back to instead of a mixture.
|
|
2546
|
+
*/
|
|
2547
|
+
export declare interface UpstreamInterface {
|
|
2548
|
+
readonly emitter: EmitterInterface<UpstreamEventMap>;
|
|
2549
|
+
/**
|
|
2550
|
+
* Looks up the newest release each declared range admits.
|
|
2551
|
+
*
|
|
2552
|
+
* @param dependencies - The declared dependencies to look up.
|
|
2553
|
+
* @returns One release verdict per dependency, in input order.
|
|
2554
|
+
*/
|
|
2555
|
+
lookup(dependencies: readonly Dependency[]): Promise<readonly Release[]>;
|
|
2556
|
+
/**
|
|
2557
|
+
* Fetches each named package's guide, beside the local mirror it answers for.
|
|
2558
|
+
*
|
|
2559
|
+
* @param names - The packages to fetch: the target's declared set, or the whole organization.
|
|
2560
|
+
* @param current - The target's local mirrors as exact bytes, keyed by mirror path.
|
|
2561
|
+
* @returns One mirror verdict per name, in input order.
|
|
2562
|
+
*/
|
|
2563
|
+
fetch(names: readonly string[], current: Snapshot): Promise<readonly Mirror[]>;
|
|
2564
|
+
/**
|
|
2565
|
+
* Reads each named vendored file from the repository, beside the target bytes it answers for.
|
|
2566
|
+
*
|
|
2567
|
+
* @param paths - The target-relative vendored paths to read.
|
|
2568
|
+
* @param current - The target files as exact bytes, keyed by the same paths.
|
|
2569
|
+
* @returns One file verdict per path, in input order.
|
|
2570
|
+
*/
|
|
2571
|
+
read(paths: readonly string[], current: Snapshot): Promise<readonly HostFile[]>;
|
|
2572
|
+
/**
|
|
2573
|
+
* Catalogs the published fleet from the registry's organization package list.
|
|
2574
|
+
*
|
|
2575
|
+
* @returns One row per published package, sorted by name.
|
|
2576
|
+
*/
|
|
2577
|
+
catalog(): Promise<readonly CatalogEntry[]>;
|
|
2578
|
+
/**
|
|
2579
|
+
* Tears the reader down, aborting every request in flight. Teardown is idempotent.
|
|
2580
|
+
*
|
|
2581
|
+
* @returns Nothing.
|
|
2582
|
+
*/
|
|
2583
|
+
destroy(): void;
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
/**
|
|
2587
|
+
* Represents the options for the upstream reader.
|
|
2588
|
+
*
|
|
2589
|
+
* @remarks
|
|
2590
|
+
* The endpoints are grouped under the entity each configures: `repository`
|
|
2591
|
+
* takes the raw content host's `base`, its `branch`, and its `timeout`;
|
|
2592
|
+
* `registry` takes the registry's `base` and `timeout`. One raw content host
|
|
2593
|
+
* serves both the fleet's guides and this package's own vendored files, so one
|
|
2594
|
+
* group configures both. `concurrency` bounds requests in
|
|
2595
|
+
* flight and `retries` opts into per-request retry on a transport fault.
|
|
2596
|
+
* `limit` bounds the bytes read from one response body and `budget` bounds the
|
|
2597
|
+
* bytes read across a whole call, so neither one oversized answer nor many
|
|
2598
|
+
* small ones can exhaust the caller. Every request is unauthenticated and
|
|
2599
|
+
* follows no redirect.
|
|
2600
|
+
*/
|
|
2601
|
+
export declare interface UpstreamOptions {
|
|
2602
|
+
readonly repository?: {
|
|
2603
|
+
readonly base?: string;
|
|
2604
|
+
readonly branch?: string;
|
|
2605
|
+
readonly timeout?: number;
|
|
2606
|
+
};
|
|
2607
|
+
readonly registry?: {
|
|
2608
|
+
readonly base?: string;
|
|
2609
|
+
readonly timeout?: number;
|
|
2610
|
+
};
|
|
2611
|
+
readonly concurrency?: number;
|
|
2612
|
+
readonly retries?: number;
|
|
2613
|
+
readonly limit?: number;
|
|
2614
|
+
readonly budget?: number;
|
|
2615
|
+
readonly on?: EmitterHooks<UpstreamEventMap>;
|
|
2616
|
+
readonly error?: EmitterErrorHandler;
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
/**
|
|
2620
|
+
* Describes what git reports about a target's working tree.
|
|
2621
|
+
*
|
|
2622
|
+
* @remarks
|
|
2623
|
+
* `tracked` is the only set a deletion may draw from: git does not report the
|
|
2624
|
+
* loss of an untracked path and `git diff` cannot restore it, so an ignored
|
|
2625
|
+
* path such as an installed dependency tree, a build output, or an editor
|
|
2626
|
+
* directory survives every verb. `dirty` is every path carrying an uncommitted
|
|
2627
|
+
* change, taken repo-wide rather than over a write set, because deletion makes
|
|
2628
|
+
* the write set the whole workspace. A clean tree is an empty `dirty`. A target
|
|
2629
|
+
* that is not a git repository yields no `Worktree` at all, so the caller
|
|
2630
|
+
* decides what to do about that rather than reading it out of an invented
|
|
2631
|
+
* empty value.
|
|
2632
|
+
*/
|
|
2633
|
+
export declare interface Worktree {
|
|
2634
|
+
readonly tracked: readonly string[];
|
|
2635
|
+
readonly dirty: readonly string[];
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
/**
|
|
2639
|
+
* Represents one physical directory identity captured across a write transaction.
|
|
2640
|
+
*
|
|
2641
|
+
* @remarks
|
|
2642
|
+
* Native bigint device and inode values locate the directory and do not date it.
|
|
2643
|
+
* Two directories occupying one slot at different times share an anchor, so an anchor proves
|
|
2644
|
+
* where a write lands and never that the directory was left alone.
|
|
2645
|
+
*/
|
|
2646
|
+
export declare interface WriteAnchor {
|
|
2647
|
+
readonly path: string;
|
|
2648
|
+
readonly device: bigint;
|
|
2649
|
+
readonly inode: bigint;
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
/** Reports the final directory anchor of a write transaction and the subset one call created. */
|
|
2653
|
+
export declare interface WriteDirectoryResult {
|
|
2654
|
+
readonly anchor: WriteAnchor;
|
|
2655
|
+
readonly created: readonly WriteAnchor[];
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
/**
|
|
2659
|
+
* Represents one destination snapshot captured before a write and required to survive it.
|
|
2660
|
+
*
|
|
2661
|
+
* @remarks
|
|
2662
|
+
* `device`, `inode`, `modified`, `size`, and `digest` are present only where
|
|
2663
|
+
* the observed shape supplies them.
|
|
2664
|
+
*/
|
|
2665
|
+
export declare interface WriteExpectation {
|
|
2666
|
+
readonly path: string;
|
|
2667
|
+
readonly shape: 'absent' | 'file' | 'directory';
|
|
2668
|
+
readonly device?: number;
|
|
2669
|
+
readonly inode?: number;
|
|
2670
|
+
readonly modified?: number;
|
|
2671
|
+
readonly size?: number;
|
|
2672
|
+
readonly digest?: string;
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
/**
|
|
2676
|
+
* Describes the narrower caller-observed destination state a write transaction must
|
|
2677
|
+
* still match.
|
|
2678
|
+
*/
|
|
2679
|
+
export declare interface WritePrecondition {
|
|
2680
|
+
readonly path: string;
|
|
2681
|
+
readonly shape: 'absent' | 'file';
|
|
2682
|
+
readonly digest?: string;
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
/**
|
|
2686
|
+
* Represents one staged, reversible mutation of one target directory.
|
|
2687
|
+
*
|
|
2688
|
+
* @remarks
|
|
2689
|
+
* The transaction owns a private root beside the target — a sibling directory on
|
|
2690
|
+
* the same volume, so every promotion is a rename rather than a copy. Staging
|
|
2691
|
+
* writes go into that root and nothing else, so a call that fails while staging
|
|
2692
|
+
* has not touched the target at all. Commit is the only step that mutates the
|
|
2693
|
+
* target, and it is the only step that can need rolling back.
|
|
2694
|
+
*
|
|
2695
|
+
* A precondition and an expectation hold a destination still. A **precondition** is what the caller
|
|
2696
|
+
* observed earlier and is checked once, at construction, so a target that moved
|
|
2697
|
+
* between the caller's read and this transaction fails before anything is
|
|
2698
|
+
* created. An **expectation** is captured here, at construction, and re-checked
|
|
2699
|
+
* at commit, so a target that moves while the write is being staged fails before
|
|
2700
|
+
* anything is promoted.
|
|
2701
|
+
*
|
|
2702
|
+
* What this provides, exactly:
|
|
2703
|
+
*
|
|
2704
|
+
* - **Across destinations, staged-then-swapped with rollback on a caught
|
|
2705
|
+
* failure.** A failure part way through commit restores every destination it
|
|
2706
|
+
* already promoted, restores every file it already took, and removes every
|
|
2707
|
+
* directory it created, then reports what recovery could not undo. This is
|
|
2708
|
+
* measured: a promotion is driven to fail after an earlier one landed, and the
|
|
2709
|
+
* earlier destination is read back.
|
|
2710
|
+
* - **No partly written destination.** Every file is written whole into the
|
|
2711
|
+
* private root and digested there before commit, so a destination never
|
|
2712
|
+
* receives bytes that were still being produced.
|
|
2713
|
+
* - **Containment, not continuity, of the directories it creates.** Every
|
|
2714
|
+
* ancestor is re-read between `mkdir` calls and again before the first
|
|
2715
|
+
* promotion, so an ancestor that became a file, a symlink, a directory
|
|
2716
|
+
* elsewhere, or nothing is refused. An ancestor deleted and recreated under
|
|
2717
|
+
* the same name can receive its old inode back and is indistinguishable here
|
|
2718
|
+
* from one that never moved.
|
|
2719
|
+
* - **No crash atomicity across destinations.** A process killed between
|
|
2720
|
+
* promotions leaves the target holding some new files and some old ones, and
|
|
2721
|
+
* leaves the private root behind. Nothing here is a journal, and the private
|
|
2722
|
+
* root's name is the only record a later run could read.
|
|
2723
|
+
*
|
|
2724
|
+
* A destination is preserved by hard link and then replaced by a single
|
|
2725
|
+
* `rename`, rather than moved aside and replaced, so the path continues to name
|
|
2726
|
+
* the old file right up to the swap. That is a property of `rename` on the host,
|
|
2727
|
+
* not one this package's tests measure: telling it apart from move-then-replace
|
|
2728
|
+
* needs a reader observing the destination inside the swap, and no test here
|
|
2729
|
+
* does that. Read the claim as the mechanism it describes, not as a proven
|
|
2730
|
+
* guarantee about a concurrent reader.
|
|
2731
|
+
*
|
|
2732
|
+
* A path names a file this transaction writes, takes, or establishes as a
|
|
2733
|
+
* directory; every path is target-relative and is measured by the portable-path
|
|
2734
|
+
* law. An expectation and a precondition both name the resolved destination
|
|
2735
|
+
* instead, because that is the path each is re-read at.
|
|
2736
|
+
*
|
|
2737
|
+
* @example
|
|
2738
|
+
* ```ts
|
|
2739
|
+
* import { WriteTransaction } from '@orkestrel/scaffold/server'
|
|
2740
|
+
*
|
|
2741
|
+
* const transaction = new WriteTransaction('./packages/router', ['AGENTS.md'])
|
|
2742
|
+
* try {
|
|
2743
|
+
* transaction.write('AGENTS.md', '# Agents\n')
|
|
2744
|
+
* transaction.commit() // ['AGENTS.md']
|
|
2745
|
+
* } finally {
|
|
2746
|
+
* transaction.discard()
|
|
2747
|
+
* }
|
|
2748
|
+
* ```
|
|
2749
|
+
*/
|
|
2750
|
+
export declare class WriteTransaction {
|
|
2751
|
+
#private;
|
|
2711
2752
|
/**
|
|
2712
|
-
*
|
|
2753
|
+
* Opens a transaction over one target directory.
|
|
2713
2754
|
*
|
|
2714
|
-
* @
|
|
2715
|
-
*
|
|
2716
|
-
* @
|
|
2717
|
-
|
|
2718
|
-
|
|
2755
|
+
* @param target - The directory every path is written beneath.
|
|
2756
|
+
* @param paths - Every target-relative path this transaction may touch.
|
|
2757
|
+
* @param preconditions - The caller-observed destination states the whole
|
|
2758
|
+
* transaction is held to, each naming a resolved destination of `paths`.
|
|
2759
|
+
* @throws {@link ScaffoldError} coded `INVALID` when an argument is off
|
|
2760
|
+
* contract, `TARGET` when a destination is a shape this package will not write
|
|
2761
|
+
* over or no longer matches its precondition, and `WRITE` when the private
|
|
2762
|
+
* root cannot be established.
|
|
2719
2763
|
*
|
|
2720
2764
|
* @remarks
|
|
2721
|
-
*
|
|
2722
|
-
*
|
|
2723
|
-
*
|
|
2724
|
-
*
|
|
2725
|
-
* directories to clear. The transaction is closed either way: a committed one
|
|
2726
|
-
* has nothing left to undo and a failed one has already been rolled back.
|
|
2765
|
+
* Nothing is created until every destination has been inspected and every
|
|
2766
|
+
* precondition has held, so a refused transaction leaves no residue at all.
|
|
2767
|
+
* The private root is created last and carries a random name, so two
|
|
2768
|
+
* transactions over one target never collide.
|
|
2727
2769
|
*/
|
|
2728
|
-
|
|
2770
|
+
constructor(target: string, paths: readonly string[], preconditions?: readonly WritePrecondition[]);
|
|
2771
|
+
/** Names the resolved directory every path is written beneath. */
|
|
2772
|
+
get target(): string;
|
|
2773
|
+
/** Reports what each destination held when the transaction opened, in path order. */
|
|
2774
|
+
get expectations(): readonly WriteExpectation[];
|
|
2775
|
+
/** Reports whether the transaction can still be committed or discarded. */
|
|
2776
|
+
get open(): boolean;
|
|
2729
2777
|
/**
|
|
2730
|
-
*
|
|
2778
|
+
* Stages one text file.
|
|
2731
2779
|
*
|
|
2780
|
+
* @param path - The target-relative path to write.
|
|
2781
|
+
* @param content - The exact UTF-8 text the destination must hold.
|
|
2732
2782
|
* @returns Nothing.
|
|
2733
|
-
* @throws {@link ScaffoldError} coded `
|
|
2734
|
-
*
|
|
2783
|
+
* @throws {@link ScaffoldError} coded `INVALID` when the path is not one this
|
|
2784
|
+
* transaction opened or is already staged, `TARGET` when the destination holds
|
|
2785
|
+
* a directory, and `WRITE` when the staged file cannot be written or does not
|
|
2786
|
+
* carry the bytes it was given.
|
|
2735
2787
|
*
|
|
2736
2788
|
* @remarks
|
|
2737
|
-
*
|
|
2738
|
-
*
|
|
2789
|
+
* The staged file is read back and digested against the text it was given, so
|
|
2790
|
+
* a partial or interrupted write is refused here rather than promoted later.
|
|
2739
2791
|
*/
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2792
|
+
write(path: string, content: string): void;
|
|
2793
|
+
/**
|
|
2794
|
+
* Stages one byte-for-byte copy of a file that already exists on this host.
|
|
2795
|
+
*
|
|
2796
|
+
* @param path - The target-relative path to write.
|
|
2797
|
+
* @param source - The resolved absolute path to copy the bytes from.
|
|
2798
|
+
* @param executable - If `true`, the destination carries the executable bit; if `false`, it does not.
|
|
2799
|
+
* @returns Nothing.
|
|
2800
|
+
* @throws {@link ScaffoldError} coded `INVALID` when the path is not one this
|
|
2801
|
+
* transaction opened or is already staged, `TARGET` when the destination holds
|
|
2802
|
+
* a directory or the source is not a physical file, and `WRITE` when the copy
|
|
2803
|
+
* cannot be made or does not digest to the source's bytes.
|
|
2804
|
+
*
|
|
2805
|
+
* @remarks
|
|
2806
|
+
* The source is digested before the copy and the copy is digested after it, so
|
|
2807
|
+
* a source that changed mid-copy is refused instead of staged.
|
|
2808
|
+
*/
|
|
2809
|
+
copy(path: string, source: string, executable: boolean): void;
|
|
2810
|
+
/**
|
|
2811
|
+
* Establishes one directory inside the target, one segment at a time.
|
|
2812
|
+
*
|
|
2813
|
+
* @param path - The target-relative directory to establish.
|
|
2814
|
+
* @returns The directory's identity and every segment this call created.
|
|
2815
|
+
* @throws {@link ScaffoldError} coded `INVALID` when the path is not one this
|
|
2816
|
+
* transaction opened, `TARGET` when the destination holds a file, and `WRITE`
|
|
2817
|
+
* when a segment cannot be created or changed while it was being created.
|
|
2818
|
+
*
|
|
2819
|
+
* @remarks
|
|
2820
|
+
* A directory is created immediately rather than staged, because creating one
|
|
2821
|
+
* destroys nothing: rollback removes exactly the segments this call created,
|
|
2822
|
+
* innermost first, and leaves every segment that was already there. Each
|
|
2823
|
+
* created segment is captured by device and inode, so a segment swapped
|
|
2824
|
+
* underneath the transaction is detected rather than written into.
|
|
2825
|
+
*/
|
|
2826
|
+
establish(path: string): WriteDirectoryResult;
|
|
2827
|
+
/**
|
|
2828
|
+
* Marks one file for deletion at commit.
|
|
2829
|
+
*
|
|
2830
|
+
* @param path - The target-relative file to delete.
|
|
2831
|
+
* @returns Nothing.
|
|
2832
|
+
* @throws {@link ScaffoldError} coded `INVALID` when the path is not one this
|
|
2833
|
+
* transaction opened or is already claimed, and `TARGET` when the destination
|
|
2834
|
+
* does not hold a file.
|
|
2835
|
+
*
|
|
2836
|
+
* @remarks
|
|
2837
|
+
* Nothing moves here. Commit renames the file into the private backup rather
|
|
2838
|
+
* than unlinking it, so a later failure in the same commit puts it back.
|
|
2839
|
+
*/
|
|
2840
|
+
remove(path: string): void;
|
|
2841
|
+
/**
|
|
2842
|
+
* Promotes every staged file and takes every marked file, or rolls the whole call back.
|
|
2843
|
+
*
|
|
2844
|
+
* @returns Every target-relative path whose destination changed: the files
|
|
2845
|
+
* promoted, then the directories established, then the files taken.
|
|
2846
|
+
* @throws {@link ScaffoldError} coded `WRITE` when the transaction is closed,
|
|
2847
|
+
* when a destination moved since the transaction opened, or when the commit
|
|
2848
|
+
* failed; a failure reports what rollback could not undo in its context.
|
|
2849
|
+
*
|
|
2850
|
+
* @remarks
|
|
2851
|
+
* Every destination and every directory this transaction created is re-checked
|
|
2852
|
+
* before anything moves, so the common failure moves nothing at all. That
|
|
2853
|
+
* check runs inside the same rollback as the promotions, because a transaction
|
|
2854
|
+
* that refuses before it starts still has a private root and created
|
|
2855
|
+
* directories to clear. The transaction is closed either way: a committed one
|
|
2856
|
+
* has nothing left to undo and a failed one has already been rolled back.
|
|
2857
|
+
*/
|
|
2858
|
+
commit(): readonly string[];
|
|
2859
|
+
/**
|
|
2860
|
+
* Abandons the transaction and removes everything it created.
|
|
2861
|
+
*
|
|
2862
|
+
* @returns Nothing.
|
|
2863
|
+
* @throws {@link ScaffoldError} coded `WRITE` when residue could not be
|
|
2864
|
+
* removed, naming the private root that still holds it.
|
|
2865
|
+
*
|
|
2866
|
+
* @remarks
|
|
2867
|
+
* Idempotent, and a no-op on a transaction that already committed or already
|
|
2868
|
+
* failed, so a caller can put it in a `finally` beside the work it guards.
|
|
2869
|
+
*/
|
|
2870
|
+
discard(): void;
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
export { }
|