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