@orkestrel/scaffold 0.0.23 → 0.0.25

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.
Files changed (39) hide show
  1. package/README.md +84 -99
  2. package/dist/bin/main.js +1094 -0
  3. package/dist/bin/main.js.map +1 -0
  4. package/dist/host/CLAUDE.md +3 -1
  5. package/dist/host/agents/orchestration.md +61 -4
  6. package/dist/host/agents/skills/orkestrel-align-packages/SKILL.md +1 -1
  7. package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +7 -5
  8. package/dist/host/agents/skills/orkestrel-harden-package/SKILL.md +1 -1
  9. package/dist/host/agents/skills/orkestrel-harden-package/references/contract.md +1 -1
  10. package/dist/host/claude/agents/orkestrel.md +4 -4
  11. package/dist/host/claude/rules/architecture.md +45 -3
  12. package/dist/host/claude/rules/quality.md +4 -0
  13. package/dist/host/claude/rules/tests.md +57 -1
  14. package/dist/host/claude/rules/workspace.md +50 -17
  15. package/dist/host/codex/agents/orkestrel.toml +1 -1
  16. package/dist/host/configs/helpers.ts +762 -0
  17. package/dist/host/dotfiles/oxlintrc.json +2 -1
  18. package/dist/host/guides/scaffold.md +862 -0
  19. package/dist/host/manifest.json +40 -33
  20. package/dist/host/tests/config.test.ts +544 -0
  21. package/dist/host/tests/policy.test.ts +46 -0
  22. package/dist/host/tests/setupPolicy.ts +529 -701
  23. package/dist/src/core/index.cjs +3568 -10576
  24. package/dist/src/core/index.cjs.map +1 -1
  25. package/dist/src/core/index.d.cts +2361 -2800
  26. package/dist/src/core/index.d.ts +2361 -2800
  27. package/dist/src/core/index.js +3512 -10440
  28. package/dist/src/core/index.js.map +1 -1
  29. package/dist/src/server/index.cjs +2855 -3765
  30. package/dist/src/server/index.cjs.map +1 -1
  31. package/dist/src/server/index.d.cts +1915 -1330
  32. package/dist/src/server/index.d.ts +1915 -1330
  33. package/dist/src/server/index.js +2812 -3680
  34. package/dist/src/server/index.js.map +1 -1
  35. package/package.json +16 -23
  36. package/dist/bin/scaffold.js +0 -1896
  37. package/dist/bin/scaffold.js.map +0 -1
  38. package/dist/host/guides/src/scaffold.md +0 -2922
  39. /package/dist/host/guides/{src/guide.md → guide.md} +0 -0
@@ -1,1382 +1,1967 @@
1
1
  import { Audit } from '@orkestrel/scaffold';
2
- import { Blueprint } from '@orkestrel/scaffold';
3
- import { BooleanShape } from '@orkestrel/contract';
4
2
  import { CatalogEntry } from '@orkestrel/scaffold';
5
- import { ContractInterface } from '@orkestrel/contract';
6
3
  import { Dependency } from '@orkestrel/scaffold';
7
4
  import { EmitterErrorHandler } from '@orkestrel/emitter';
8
5
  import { EmitterHooks } from '@orkestrel/emitter';
9
6
  import { EmitterInterface } from '@orkestrel/emitter';
10
- import { GuideSync } from '@orkestrel/scaffold';
11
- import { HostArtifact } from '@orkestrel/scaffold';
12
- import { NumberShape } from '@orkestrel/contract';
13
- import { ObjectShape } from '@orkestrel/contract';
14
- import { OptionalShape } from '@orkestrel/contract';
7
+ import { Guard } from '@orkestrel/contract';
8
+ import { Mirror } from '@orkestrel/scaffold';
15
9
  import { Plan } from '@orkestrel/scaffold';
16
- import { ScaffoldErrorCode } from '@orkestrel/scaffold';
10
+ import { Release } from '@orkestrel/scaffold';
17
11
  import { Snapshot } from '@orkestrel/scaffold';
18
- import { StringShape } from '@orkestrel/contract';
19
- import { SyncReport } from '@orkestrel/scaffold';
20
- import { VersionSync } from '@orkestrel/scaffold';
21
-
22
- /** One mutable aggregate entry allowance shared by fleet catalog roots. */
23
- export declare type CatalogAllowance = Float64Array;
24
12
 
25
13
  /**
26
- * Build the fleet package catalog one `CatalogEntry` per `@orkestrel/*`
27
- * package discovered under each root, its description drawn from its own
28
- * guide's FIRST blockquote.
14
+ * The Git branch syntax the guide endpoint accepts.
29
15
  *
30
- * @param roots - The fleet root directories to scan (each walked via `discoverPackages`).
31
16
  * @remarks
32
- * Per discovered package directory: `name` / `version` come from its own
33
- * `package.json`; `description` is the first paragraph of the guide's opening
34
- * blockquote the flattened text of the FIRST `ParagraphNode` among the
35
- * FIRST `BlockquoteNode` found's (depth-first, pre-order, via `walkNodes`)
36
- * TOP-LEVEL children in its `guides/src/<short>.md` (`<short>` = `name` with
37
- * the `@orkestrel/` prefix stripped), parsed with `@orkestrel/markdown`'s
38
- * `parseDocument` — a multi-paragraph blockquote overview yields only its
39
- * FIRST paragraph, never the whole quote glued together; embedded newlines
40
- * collapse to single spaces, and surrounding whitespace trims. A
41
- * missing/unreadable guide, a guide carrying no blockquote, or a blockquote
42
- * with no top-level paragraph child, yields `description: ''`, never a
43
- * thrown error. Entries merge across `roots` (a later root's entry for a
44
- * repeated `name` wins), then code-unit sort by `name`. An unreadable ROOT
45
- * itself is NOT wrapped here — whatever `discoverPackages` throws for it
46
- * propagates as-is; the bin layer is responsible for coding that failure
47
- * `TARGET`.
48
- * @returns The merged, sorted `CatalogEntry[]`.
49
- *
50
- * @example
51
- * ```ts
52
- * import { catalogPackages } from '@orkestrel/scaffold/server'
53
- *
54
- * catalogPackages(['/repos']) // [{ name: '@orkestrel/contract', version: '0.0.5', description: '…' }, …]
55
- * ```
17
+ * A branch is caller-supplied and reaches a URL path, so it is closed to
18
+ * alphanumerics, dot, underscore, hyphen, and the separator, must open with an
19
+ * alphanumeric, and may carry no `..` anywhere. That last refusal is what stops
20
+ * a branch from walking out of the guide directory it addresses.
56
21
  */
57
- export declare function catalogPackages(roots: readonly string[], limit?: number): readonly CatalogEntry[];
22
+ export declare const BRANCH_PATTERN: RegExp;
58
23
 
59
24
  /**
60
- * Promote a complete staged set and roll every earlier destination back when
61
- * any later promotion fails.
62
- *
63
- * @param transaction - Same-volume sibling staging state.
64
- * @param paths - Portable target-relative files to promote.
65
- * @throws `ScaffoldError('WRITE', ...)` with recovery details on failure.
66
- */
67
- export declare function commitWriteTransaction(transaction: WriteTransaction, paths: readonly string[]): void;
68
-
69
- /** Consume one aggregate fleet-catalog traversal slot. */
70
- export declare function consumeCatalogAllowance(allowance: CatalogAllowance, root: string): void;
71
-
72
- /**
73
- * Create a `MaterializerInterface` (server) — the materialization entity,
74
- * seeded from `MaterializerOptions`.
75
- *
76
- * @param options - Optional `host` root override, emitter hooks, and error handler
77
- * @returns A {@link MaterializerInterface}
25
+ * Compute the SHA-256 digest of text.
78
26
  *
79
- * @example
80
- * ```ts
81
- * import { createMaterializer } from '@orkestrel/scaffold/server'
82
- *
83
- * const materializer = createMaterializer()
84
- * materializer.destroy()
85
- * ```
86
- */
87
- export declare function createMaterializer(options?: MaterializerOptions): MaterializerInterface;
88
-
89
- /**
90
- * Create a `SyncInterface` (server) — the upstream-synchronization entity,
91
- * seeded from `SyncOptions`.
27
+ * @param content - The text to digest.
28
+ * @returns Sixty-four lowercase hexadecimal digits.
92
29
  *
93
- * @param options - Optional endpoint bases/branch, concurrency, retries, strict, emitter hooks, and error handler
94
- * @returns A {@link SyncInterface}
30
+ * @remarks
31
+ * The identity the server face states, and the reason it differs from core's:
32
+ * core settled on a folded 64-bit identity because compilation is synchronous by
33
+ * contract and the only cryptographic digest a host-independent scope reaches is
34
+ * asynchronous. A Node host reaches the real one synchronously, and
35
+ * `HostManifest.digest` is documented as SHA-256, so this is what the server
36
+ * uses everywhere a digest is claimed.
95
37
  *
96
38
  * @example
97
39
  * ```ts
98
- * import { createSync } from '@orkestrel/scaffold/server'
40
+ * import { computeDigest } from '@orkestrel/scaffold/server'
99
41
  *
100
- * const sync = createSync()
101
- * sync.destroy()
42
+ * computeDigest('hi\n') // '98ea6e4f216f2fb4b69fff9b3a44842c38686ca685f3f55dc48c5d3fb1107be4'
102
43
  * ```
103
44
  */
104
- export declare function createSync(options?: SyncOptions): SyncInterface;
45
+ export declare function computeDigest(content: string): string;
105
46
 
106
47
  /**
107
- * Create a physical directory path one segment at a time behind captured identities.
48
+ * Compute the SHA-256 digest of one file's exact bytes.
108
49
  *
109
- * @param path - The absolute directory path to establish.
110
- * @param boundary - The boundary name used in diagnostics.
111
- * @returns The final directory anchor and the subset created by this call.
112
- */
113
- export declare function createWriteDirectory(path: string, boundary: string): WriteDirectoryResult;
114
-
115
- /** Default cumulative response bytes retained by one Sync operation. */
116
- export declare const DEFAULT_SYNC_BUDGET = 16777216;
117
-
118
- /** Default simultaneous upstream requests used by `Sync`. */
119
- export declare const DEFAULT_SYNC_CONCURRENCY = 6;
120
-
121
- /** Default maximum dependencies or catalog entries retained by one Sync operation. */
122
- export declare const DEFAULT_SYNC_ITEMS = 256;
123
-
124
- /** Default per-response byte limit used by `Sync`. */
125
- export declare const DEFAULT_SYNC_LIMIT = 5242880;
126
-
127
- /** Default per-request timeout used by `Sync`, in milliseconds. */
128
- export declare const DEFAULT_SYNC_TIMEOUT = 10000;
129
-
130
- /**
131
- * Reconstruct a `Blueprint` from an EXISTING repo at `target` — the faithful
132
- * inverse `audit` / `repair` / `mirror` need to diff a live package against
133
- * its own would-be scaffold, rather than a fresh, dependency-less stand-in.
50
+ * @param path - The resolved host path to digest.
51
+ * @returns The digest, or `undefined` when the path is not a physical file, is
52
+ * past the artifact ceiling, or moved while it was being read.
134
53
  *
135
- * @param target - The existing package directory to derive a `Blueprint` from.
136
54
  * @remarks
137
- * A scoped package name is stripped to its bounded safe short name. An
138
- * unscoped name is accepted only when the target is app-only, `private: true`,
139
- * and the name satisfies `isWorkspaceName`; every other name is a coded
140
- * `TARGET` failure. `src` is derived from `src/<environment>/` and `app` from
141
- * `app/<environment>/`; a target with no environment on either axis is also a coded
142
- * `TARGET` failure. The `bin` and `integration` facts probe physical
143
- * directories at `src/bin` and `tests/integration`. `services` is the sorted
144
- * set of direct vendor directories under `tests/service` that contain a test
145
- * at any depth, while `global` probes the physical exact-case `tests/setupGlobal.ts` file and `showcase`
146
- * probes the physical exact-case `configs/app/vite.showcase.config.ts` regular file; none is
147
- * inferred from the workspace name. `dependencies` /
148
- * `peers` are the `@orkestrel/`-prefixed entries of `manifest.dependencies` /
149
- * `manifest.peerDependencies` (a peer flagged `peerDependenciesMeta[name]
150
- * .optional === true` carries `optional: true`). `extras` is EVERY entry of
151
- * `manifest.devDependencies` (not only `@orkestrel/`-prefixed ones — an
152
- * external extra like `zod` must round-trip too), EXCLUDING the complete
153
- * generated dependency set `devDependenciesFor` emits for the reconstructed
154
- * blueprint's actual environments and structural axes. A devDependency ALSO present in
155
- * `manifest.peerDependencies` or `manifest.dependencies` (e.g. a peer
156
- * dev-installed for local testing) is likewise excluded from `extras` — it
157
- * already appears as a `peer`/`dependency` above, and double-counting it as
158
- * an `extra` would land it in `peers ∩ extras`, a blocking `validateBlueprint`
159
- * gate. `overrides` is always `[]` — derivation cannot recover a caller's
160
- * artifact-override intent from repository state.
161
- * @returns The reconstructed `Blueprint`.
162
- * @throws `ScaffoldError('TARGET', …)` when `target`'s manifest is unreadable
163
- * (via `readManifest`), is not valid JSON, its name is unsafe for its
164
- * publication mode, or `target` carries no source or application environment.
165
- * @throws `ScaffoldError('INVALID', …)` when service tests use the retired flat
166
- * layout, a vendor directory has no test, or a service companion is missing.
55
+ * Read in bounded chunks rather than loaded whole, so digesting a large file
56
+ * costs one buffer instead of its size. The file's identity and size are
57
+ * measured before and after the read and a mismatch answers `undefined`, so a
58
+ * digest is either of one settled file or is not produced at all.
167
59
  *
168
60
  * @example
169
61
  * ```ts
170
- * import { deriveBlueprint } from '@orkestrel/scaffold/server'
62
+ * import { computeFileDigest } from '@orkestrel/scaffold/server'
171
63
  *
172
- * deriveBlueprint('./packages/router') // { name: 'router', src: ['core', 'server'], … }
64
+ * computeFileDigest('/tmp/project/AGENTS.md') // the file's SHA-256
65
+ * computeFileDigest('/tmp/project/absent.md') // undefined
173
66
  * ```
174
67
  */
175
- export declare function deriveBlueprint(target: string): Blueprint;
176
-
177
- /**
178
- * Compute a bounded-memory SHA-256 digest for one file.
179
- *
180
- * @param path - The file to read.
181
- * @returns Its lowercase SHA-256 digest.
182
- */
183
- export declare function digestFile(path: string): string;
184
-
185
- /**
186
- * Compute SHA-256 from exact lowercase hexadecimal bytes without decoding the whole value at once.
187
- *
188
- * @param hex - Exact hexadecimal bytes.
189
- * @returns Their lowercase SHA-256 digest.
190
- */
191
- export declare function digestHex(hex: string): string;
68
+ export declare function computeFileDigest(path: string): string | undefined;
192
69
 
193
70
  /**
194
- * Digest the exact declared membership of a vendored host manifest.
71
+ * Compute the digest of a vendored host's declared membership.
195
72
  *
196
73
  * @param entries - The ordered file membership declarations.
197
74
  * @param roots - The ordered directory membership declarations.
198
- * @returns A SHA-256 digest independent of the manifest's stored digest field.
199
- */
200
- export declare function digestHostManifest(entries: readonly ManifestEntry[], roots: readonly string[]): string;
201
-
202
- /**
203
- * Compute SHA-256 from UTF-8 text.
204
- *
205
- * @param value - The text to hash.
206
- * @returns Its lowercase SHA-256 digest.
207
- */
208
- export declare function digestText(value: string): string;
209
-
210
- /**
211
- * Remove an uncommitted or already-committed write transaction's private residue.
212
- *
213
- * @param transaction - The transaction to discard.
214
- * @throws `ScaffoldError('WRITE', ...)` when its private root cannot be removed.
215
- */
216
- export declare function discardWriteTransaction(transaction: WriteTransaction): void;
217
-
218
- /**
219
- * List a fleet root's `@orkestrel/*` package directories.
220
- *
221
- * @param root - The fleet root directory to scan.
222
- * @returns Absolute, code-unit-sorted paths of `root`'s immediate child
223
- * directories whose `package.json` parses and whose `name` starts with
224
- * `@orkestrel/`. A child with an unreadable or unparsable `package.json`,
225
- * or a non-`@orkestrel` name, is skipped silently — it simply is not a
226
- * fleet member.
227
- * @throws `ScaffoldError('TARGET', …)` when a child directory name contains
228
- * terminal controls and therefore cannot be safely inspected or reported.
229
- *
230
- * @example
231
- * ```ts
232
- * import { discoverPackages } from '@orkestrel/scaffold/server'
233
- *
234
- * discoverPackages('./packages') // ['/abs/packages/router', '/abs/packages/budget']
235
- * ```
236
- */
237
- export declare function discoverPackages(root: string, allowance?: CatalogAllowance): readonly string[];
238
-
239
- /**
240
- * Render the local pointer written when an upstream dependency guide is not vendored yet.
241
- *
242
- * @param source - The canonical `guides/src/<name>.md` source path.
243
- * @returns The deterministic pointer content.
244
- */
245
- export declare function guideStub(source: string): string;
246
-
247
- /**
248
- * Extract the first paragraph from the first blockquote in a Markdown guide.
249
- *
250
- * @param text - The guide Markdown to traverse.
251
- * @returns A normalized one-line description, or `undefined` when parsing
252
- * fails, no blockquote paragraph exists, or the paragraph is empty.
253
- *
254
- * @example
255
- * ```ts
256
- * import { guideToDescription } from '@orkestrel/scaffold/server'
257
- *
258
- * guideToDescription('> A concise package description.\n>\n> More detail.')
259
- * // 'A concise package description.'
260
- * ```
261
- */
262
- export declare function guideToDescription(text: string): string | undefined;
263
-
264
- /** One validated guide update and its contained destination. */
265
- export declare interface GuideWrite {
266
- readonly guide: GuideSync;
267
- readonly destination: string;
268
- }
269
-
270
- /** Reserved metadata file written at the root of every staged host. */
271
- export declare const HOST_MANIFEST_PATH = "manifest.json";
272
-
273
- /** A complete vendored-host inventory with file entries and declared directory roots. */
274
- export declare interface HostManifest {
275
- readonly entries: readonly ManifestEntry[];
276
- readonly roots: readonly string[];
277
- /** SHA-256 of the exact declared entry/root membership. */
278
- readonly digest: string;
279
- }
280
-
281
- /**
282
- * Locate this MODULE's own installed package root — the nearest ancestor of
283
- * `import.meta.url` holding a `package.json` — and return its vendored
284
- * `dist/host` data root. THE single source of truth for the default
285
- * `Materializer` / `scaffold` bin host: once installed, walking up from the
286
- * module's own file (not `process.cwd()`, which points at whichever project
287
- * happens to be running) resolves to `node_modules/@orkestrel/scaffold`, the
288
- * correct default host — the package ships its vendored data with itself.
289
- * `dist/host` may not exist yet when this resolves from SOURCE under a test
290
- * runner; that is fine — existence is checked at the point of use, not here.
291
- *
292
- * @returns The absolute vendored `dist/host` path.
293
- * @throws `ScaffoldError('TARGET', …)` when no ancestor of this module's own
294
- * location holds a `package.json`.
295
- *
296
- * @example
297
- * ```ts
298
- * import { hostRoot } from '@orkestrel/scaffold/server'
299
- *
300
- * hostRoot() // '/…/node_modules/@orkestrel/scaffold/dist/host'
301
- * ```
302
- */
303
- export declare function hostRoot(): string;
304
-
305
- /**
306
- * Rehydrate a `Plan`'s `host`-origin artifacts with their exact byte hex read
307
- * from `host` — manifest-aware, via `locateHostSource`.
308
- *
309
- * @param plan - The plan to hydrate.
310
- * @param host - The resolved host root to read from.
311
- * @returns A new `Plan` whose file-shaped `host` artifacts carry exact `hex`.
312
- * Directory-shaped host artifacts expand into one byte-aware artifact
313
- * per file, preserving their group and mapping the artifact/source prefixes.
314
- * `template` / `computed` artifacts pass through untouched.
315
- * @throws `ScaffoldError('TARGET', …)` when the host is not a readable
316
- * directory, a present manifest is invalid/incomplete, or a required source
317
- * is absent, escaping, not a file/directory as declared, or unreadable.
318
- *
319
- * @example
320
- * ```ts
321
- * import { hydratePlan } from '@orkestrel/scaffold/server'
322
- *
323
- * const hydrated = hydratePlan(plan, './dist/host')
324
- * ```
325
- */
326
- export declare function hydratePlan(plan: Plan, host: string): Plan;
327
-
328
- /** Narrow one exact aggregate fleet traversal allowance. */
329
- export declare function isCatalogAllowance(value: unknown): value is CatalogAllowance;
330
-
331
- /** Whether a normalized catalog description is bounded and contains no controls. */
332
- export declare function isCatalogDescription(value: unknown): value is string;
333
-
334
- /** Whether one dependency is an exact data-property record safe to snapshot. */
335
- export declare function isDependencyData(value: unknown): value is Dependency;
336
-
337
- /** Whether one host filesystem path is bounded, non-empty, and safe to render in a terminal. */
338
- export declare function isFilesystemPath(value: unknown): value is string;
339
-
340
- /**
341
- * Narrow a value to one exact complete vendored-host manifest.
342
- *
343
- * @param value - The candidate manifest value.
344
- * @returns `true` only for an exact `{ entries, roots, digest }` record with safe paths.
345
- */
346
- export declare function isHostManifest(value: unknown): value is HostManifest;
347
-
348
- /**
349
- * Narrow a value to one exact vendored-host manifest entry.
350
- *
351
- * @param value - The candidate raw manifest entry.
352
- * @returns `true` only for the exact safe entry shape.
353
- */
354
- export declare function isManifestEntry(value: unknown): value is ManifestEntry;
355
-
356
- /** Narrow an exact initial-listener record for `Materializer`. */
357
- export declare function isMaterializerEventHooks(value: unknown): value is EmitterHooks<MaterializerEventMap>;
358
-
359
- /**
360
- * Determine whether a caught filesystem error reports an absent path.
361
- *
362
- * @param value - The caught value.
363
- * @returns `true` only for an `Error` whose `code` is exactly `ENOENT`.
364
- */
365
- export declare function isMissingPathError(value: unknown): boolean;
366
-
367
- /**
368
- * Determine whether a value is a non-empty portable relative POSIX path.
369
- *
370
- * @param value - The candidate path.
371
- * @returns `true` when every path segment is safe and portable.
372
- */
373
- export declare function isPortablePath(value: unknown): value is string;
374
-
375
- /**
376
- * Whether a path is an existing physical directory rather than a file or link.
377
- *
378
- * @param path - The filesystem path to inspect without following links.
379
- * @returns `true` only for a successful `lstat` reporting a directory.
380
- */
381
- export declare function isRealDirectory(path: string): boolean;
382
-
383
- /**
384
- * Whether a path is an existing physical file rather than a directory or link.
385
- *
386
- * @param path - The filesystem path to inspect without following links.
387
- * @returns `true` only for a successful `lstat` reporting a single-link file.
388
- */
389
- export declare function isRealFile(path: string): boolean;
390
-
391
- /** Determine whether a target-relative path addresses preserved repository metadata. */
392
- export declare function isReservedTargetPath(value: string): boolean;
393
-
394
- /**
395
- * Determine whether a host-relative path resembles local configuration or credentials.
396
- *
397
- * @param value - The portable candidate path.
398
- * @returns `true` when the path must be excluded from vendored host output.
399
- */
400
- export declare function isSensitiveHostPath(value: string): boolean;
401
-
402
- /** Narrow an exact initial-listener record for `Sync`. */
403
- export declare function isSyncEventHooks(value: unknown): value is EmitterHooks<SyncEventMap>;
404
-
405
- /** Whether one externally supplied string is safe to render in terminal or JSON diagnostics. */
406
- export declare function isTerminalText(value: unknown): value is string;
407
-
408
- /**
409
- * Whether a target path is absent, empty, or contains nothing but a `.git`
410
- * directory — the green-field target law `Materializer.materialize` enforces.
411
- *
412
- * @param target - The candidate target directory path.
413
- * @returns `true` when `target` is safe to materialize a fresh package into.
414
- *
415
- * @example
416
- * ```ts
417
- * import { isVacant } from '@orkestrel/scaffold/server'
418
- *
419
- * isVacant('./packages/router-new') // true — absent, empty, or only a .git dir
420
- * ```
421
- */
422
- export declare function isVacant(target: string): boolean;
423
-
424
- /** Narrow one exact transaction destination precondition. */
425
- export declare function isWritePrecondition(value: unknown): value is WritePrecondition;
426
-
427
- /**
428
- * Recursively list a directory's descendant directories.
429
- *
430
- * @param root - The directory to list.
431
- * @returns Root-relative POSIX directory paths in code-unit order.
432
- */
433
- export declare function listDirectories(root: string): readonly string[];
434
-
435
- /**
436
- * Recursively list a directory's files as root-relative paths.
437
- *
438
- * @param root - The directory to list.
439
- * @returns Root-relative file paths (posix-style `/` separators), code-unit
440
- * sorted, or `[]` when `root` is absent.
441
- *
442
- * @example
443
- * ```ts
444
- * import { listFiles } from '@orkestrel/scaffold/server'
445
- *
446
- * listFiles('./dist/host/.claude/agents') // ['scout.md', 'builder.md', …]
447
- * ```
448
- */
449
- export declare function listFiles(root: string): readonly string[];
450
-
451
- /**
452
- * Resolve the absolute host-storage path for a host-origin artifact's
453
- * `source`, manifest-aware.
454
- *
455
- * @param manifest - The parsed complete host manifest, or `undefined`
456
- * when the host carries none (raw-repo-root fallback).
457
- * @param source - The artifact's `source` (or `path`) to resolve.
458
- * @param host - The resolved host root the path is joined against.
459
- * @returns `join(host, source)` when `manifest` is `undefined` (no vendored
460
- * staging indirection); when `manifest` is present, `join(host,
461
- * entries[0].storage)` for the SINGLE manifest entry whose `destination`
462
- * equals `source`, or `undefined` when zero or more than one entry matches
463
- * (`source` names a directory, or the manifest is ambiguous — no single
464
- * storage file to point at).
465
- *
466
- * @example
467
- * ```ts
468
- * import { locateHostSource } from '@orkestrel/scaffold/server'
469
- *
470
- * locateHostSource(undefined, 'package.json', './dist/host') // './dist/host/package.json'
471
- * locateHostSource(
472
- * {
473
- * entries: [{ storage: 'pkg.tmpl', destination: 'package.json', executable: false }],
474
- * roots: [],
475
- * digest: '3b52f19450237ea4cfb39bc7a77b1e5094b52a8ff1fc9c02fd68b13ad088aa0f',
476
- * },
477
- * 'package.json',
478
- * './dist/host',
479
- * )
480
- * // './dist/host/pkg.tmpl'
481
- * ```
482
- */
483
- export declare function locateHostSource(manifest: HostManifest | undefined, source: string, host: string): string | undefined;
484
-
485
- /** One entry of the vendored host's `manifest.json` (server). */
486
- export declare interface ManifestEntry {
487
- readonly storage: string;
488
- readonly destination: string;
489
- readonly executable: boolean;
490
- }
491
-
492
- /**
493
- * The materialization entity (server) — the only impure environment in the
494
- * package, writing a `Plan` to `node:fs` behind an explicit call.
495
- *
496
- * @remarks
497
- * `materialize` is green-field: it refuses any target `isVacant` rejects
498
- * (`ScaffoldError('TARGET', …)`), then byte-copies each `host` artifact from
499
- * the `host` root and writes each `template` / `computed` artifact's rendered
500
- * `content`, failing fast on any write error (`ScaffoldError('WRITE', …)`).
501
- * `repair` is into-existing: it skips the vacancy check and writes `missing`
502
- * artifacts an `Audit` names. `stale` artifacts are report-only unless its
503
- * `replace` switch is true, because byte replacement discards local changes.
504
- * Hydrated directory-shaped host entries are expanded into
505
- * file-shaped artifacts, so canonical skills and agent configuration are
506
- * audited and repaired file by file. `prune` deletes stale files under
507
- * `target/.claude/agents/`, `target/.codex/agents/`, and `target/scripts/`
508
- * that the vendored `host` does not name. After `destroy()`
509
- * every method throws `DESTROYED`; teardown is idempotent, emitter last.
510
- * Before the vacancy check or any write, `materialize` and `repair` require a
511
- * structural `isPlan` match and a valid `validatePlan` result. Contextual
512
- * overrides that are missing from the artifact set, host-owned, or target
513
- * `package.json` therefore fail closed instead of being silently ignored.
514
- *
515
- * @remarks
516
- * `host`-origin copies are MANIFEST-AWARE: when the resolved `host` root
517
- * carries a `manifest.json` (this package's own vendored `dist/host`), each
518
- * artifact's `source` (a destination-relative path) is looked up in the
519
- * manifest to find its un-dotted STORAGE path plus an `executable` bit
520
- * (applied via `chmodSync` after the copy) — the vendored-package shape,
521
- * where storage names avoid leading dots npm would otherwise mangle. When
522
- * `host` carries no `manifest.json` (a caller-supplied raw repo root, e.g. a
523
- * sibling checkout or a test fixture), `source` maps to `host` 1:1, exactly
524
- * as before.
525
- *
526
- * @remarks
527
- * A manifest-present `source` with ZERO matching entries degrades to a stub
528
- * ONLY when that `source` is a dependency-guide pointer — one that starts
529
- * with `guides/src/` and ends with `.md` (the `guides/src/<dep>.md` pointer
530
- * `Compiler` emits for any dependency outside this package's vendored set).
531
- * That is the ONLY zero-match case that is legitimate: every `HOST_PATHS`
532
- * source is always staged by `stageHost`, so a non-guide zero-match means a
533
- * corrupted or truncated `manifest.json`, not an intentionally-unvendored
534
- * artifact. For a guide pointer, a short stub file is written at the
535
- * destination and reported exactly like a successful copy. The READ path
536
- * leaves that intentional pointer without canonical bytes, so `diffPlan`
537
- * treats it as presence-owned rather than inventing a byte match.
538
- * For every OTHER zero-match, the
539
- * fail-closed `ScaffoldError('TARGET', …)` is thrown — degrading an
540
- * unscoped zero-match would otherwise let a corrupted manifest silently stub
541
- * an unrecoverable artifact (e.g. `AGENTS.md` — `pull` only ever fetches
542
- * dependency guides) or write a FILE named `.claude` over what should be a
543
- * directory artifact. The raw-root fallback (`manifest === undefined`, a
544
- * caller-supplied `--from`) keeps its own throw regardless: an EXPLICITLY
545
- * named source failing to resolve is a different, caller-error failure
546
- * class, not a "not vendored" degrade.
547
- *
548
- * @remarks
549
- * Defense in depth at the filesystem trust boundary: EVERY resolved
550
- * destination (`materialize` and `repair`, both origins) is asserted to stay
551
- * within `resolve(target)` before any write, and every `host`-origin copy
552
- * source is asserted to stay within `resolve(host)` before any read — a
553
- * traversal segment (`../`) in an artifact's `path` or `source` cannot escape
554
- * either root, even if a gate upstream (e.g. an ungated `Plan` built by hand)
555
- * let it through. A destination violation throws `ScaffoldError('WRITE', …)`;
556
- * a source violation throws `ScaffoldError('TARGET', …)`.
557
- *
558
- * @remarks
559
- * The containment check is REAL-PATH aware, not merely lexical: both the
560
- * root (`target` / `host`) and the candidate destination/source are resolved
561
- * through `realpathSync` on their DEEPEST EXISTING ancestor before the prefix
562
- * comparison, so a symlinked subdirectory planted inside an otherwise
563
- * legitimate root cannot smuggle a write (or read) outside it — `repair`,
564
- * which has no `isVacant` gate, is covered exactly like `materialize`. A path
565
- * segment that does not yet exist on disk (the still-to-be-created file/dir
566
- * a write is about to create) is rejoined onto the resolved existing
567
- * ancestor rather than realpath'd itself.
568
- *
569
- * @example
570
- * ```ts
571
- * import { blueprint, blueprintToPlan } from '@orkestrel/scaffold'
572
- * import { createMaterializer } from '@orkestrel/scaffold/server'
573
- *
574
- * const plan = blueprintToPlan(blueprint('budget', { src: ['core'] }))
575
- * const materializer = createMaterializer()
576
- * materializer.materialize(plan, './packages/budget-new')
577
- * materializer.destroy()
578
- * ```
579
- */
580
- export declare class Materializer implements MaterializerInterface {
581
- #private;
582
- constructor(options?: MaterializerOptions);
583
- get emitter(): EmitterInterface<MaterializerEventMap>;
584
- materialize(plan: Plan, target: string): MaterializeResult;
585
- repair(plan: Plan, audit: Audit, target: string, replace?: boolean): MaterializeResult;
586
- prune(target: string, expected: Readonly<Record<string, string>>): MaterializeResult;
587
- destroy(): void;
588
- }
589
-
590
- /** The outcome of one materialization (server). */
591
- export declare interface MaterializeResult {
592
- readonly target: string;
593
- readonly written: readonly string[];
594
- readonly copied: readonly string[];
595
- readonly skipped: readonly string[];
596
- readonly removed: readonly string[];
597
- }
598
-
599
- /** `Materializer`'s push observation channel (AGENTS §13, server). */
600
- export declare type MaterializerEventMap = {
601
- readonly copy: readonly [path: string];
602
- readonly write: readonly [path: string];
603
- readonly remove: readonly [path: string];
604
- readonly done: readonly [result: MaterializeResult];
605
- readonly error: readonly [error: unknown];
606
- readonly destroy: readonly [];
607
- };
608
-
609
- /** The materialization contract (server) — the only impure entity in the package. */
610
- export declare interface MaterializerInterface {
611
- readonly emitter: EmitterInterface<MaterializerEventMap>;
612
- materialize(plan: Plan, target: string): MaterializeResult;
613
- repair(plan: Plan, audit: Audit, target: string, replace?: boolean): MaterializeResult;
614
- prune(target: string, expected: Snapshot): MaterializeResult;
615
- destroy(): void;
616
- }
617
-
618
- /**
619
- * Options for `createMaterializer` / the `Materializer` constructor (server).
620
- *
621
- * @remarks
622
- * `host` is the vendored-data root `host`-origin artifacts are copied FROM;
623
- * defaults to THIS PACKAGE'S OWN vendored data root (`dist/host`, resolved
624
- * from the installed module's own location) — the package vendors its host
625
- * data and ships it with itself, so the default host is never the caller's
626
- * working directory. A caller-supplied `host` pointing at a raw repo root
627
- * (no `manifest.json` alongside it) maps artifact paths 1:1 instead of
628
- * through the manifest — the sibling-repo / test-fixture shape.
629
- */
630
- export declare interface MaterializerOptions {
631
- readonly host?: string;
632
- readonly on?: EmitterHooks<MaterializerEventMap>;
633
- readonly error?: EmitterErrorHandler;
634
- }
635
-
636
- /** The compiled, closed data-only `MaterializerOptions` contract. */
637
- export declare const materializerOptionsContract: ContractInterface<Readonly<{} & {
638
- host?: string;
639
- }>>;
640
-
641
- /** Build the closed data-only `MaterializerOptions` shape. */
642
- export declare function materializerOptionsShape(): ObjectShape<{
643
- host: OptionalShape<StringShape>;
644
- }, false>;
645
-
646
- /** Maximum normalized package-catalog description length. */
647
- export declare const MAX_CATALOG_DESCRIPTION_LENGTH = 500;
648
-
649
- /** Maximum caller-controlled filesystem path depth accepted before filesystem work. */
650
- export declare const MAX_FILESYSTEM_DEPTH = 64;
651
-
652
- /** Maximum UTF-8 bytes parsed from one local package guide. */
653
- export declare const MAX_GUIDE_BYTES = 5242880;
654
-
655
- /** Maximum directory nesting accepted by one vendored-host traversal. */
656
- export declare const MAX_HOST_DEPTH = 32;
657
-
658
- /** Maximum filesystem entries accepted by one vendored-host traversal. */
659
- export declare const MAX_HOST_ENTRIES = 4096;
660
-
661
- /** Maximum UTF-8 bytes accepted in one portable filesystem path segment. */
662
- export declare const MAX_PATH_SEGMENT_BYTES = 255;
663
-
664
- /** Maximum caller-supplied endpoint characters inspected before URL allocation. */
665
- export declare const MAX_SYNC_BASE_LENGTH = 2048;
666
-
667
- /** Maximum portable Git branch characters accepted by the guide endpoint. */
668
- export declare const MAX_SYNC_BRANCH_LENGTH = 255;
669
-
670
- /** Maximum configurable cumulative response bytes retained by one Sync operation. */
671
- export declare const MAX_SYNC_BUDGET = 104857600;
672
-
673
- /** Maximum simultaneous upstream requests accepted by `Sync`. */
674
- export declare const MAX_SYNC_CONCURRENCY = 64;
675
-
676
- /** Maximum configurable dependencies or catalog entries for one Sync operation. */
677
- export declare const MAX_SYNC_ITEMS = 1000;
678
-
679
- /** Maximum response body accepted by `Sync`, in bytes. */
680
- export declare const MAX_SYNC_LIMIT = 5242880;
681
-
682
- /** Maximum retry count accepted by `Sync`. */
683
- export declare const MAX_SYNC_RETRIES = 5;
684
-
685
- /** Maximum per-request timeout accepted by `Sync`, in milliseconds. */
686
- export declare const MAX_SYNC_TIMEOUT = 300000;
687
-
688
- /**
689
- * Remove the canonical Orkestrel scope from a dependency name.
690
- *
691
- * @param name - A dependency name.
692
- * @returns Its unscoped member when canonical, otherwise the original name.
693
- */
694
- export declare function packageShortName(name: string): string;
695
-
696
- /** Parse a bounded dense array of host filesystem paths without invoking caller methods. */
697
- export declare function parseFilesystemPaths(value: unknown, limit: number): readonly string[] | undefined;
698
-
699
- /** Parse exact materializer options before allocating its emitter. */
700
- export declare function parseMaterializerOptions(value: unknown): MaterializerOptions;
701
-
702
- /** Parse a bounded dense array of portable paths without invoking caller methods. */
703
- export declare function parsePortablePaths(value: unknown, limit: number): readonly string[] | undefined;
704
-
705
- /** Parse and normalize one upstream HTTP(S) endpoint base. */
706
- export declare function parseSyncBase(value: unknown): SyncBase;
707
-
708
- /** Parse a safe Git branch path for the canonical raw-guide URL. */
709
- export declare function parseSyncBranch(value: unknown): SyncBranch;
710
-
711
- /**
712
- * Snapshot only declared guide references while enforcing per-file and cumulative byte limits.
713
- *
714
- * @param value - The caller-supplied guide content map.
715
- * @param names - Exact dependency names eligible for selection.
716
- * @param budget - Maximum cumulative UTF-8 bytes.
717
- * @returns A frozen selected map, or `undefined` when no map was supplied.
718
- */
719
- export declare function parseSyncCurrent(value: Readonly<Record<string, string>> | undefined, names: readonly string[], budget: number): Readonly<Record<string, string>> | undefined;
720
-
721
- /** Parse and semantically validate dependency data before Sync performs network I/O. */
722
- export declare function parseSyncDependencies(value: unknown, external: boolean): readonly Dependency[];
723
-
724
- /**
725
- * Parse and semantically validate bare registry package names before Sync performs network I/O.
726
- *
727
- * @param value - Untrusted package-name collection.
728
- * @returns A frozen owned snapshot of valid unique npm package names.
729
- */
730
- export declare function parseSyncNames(value: unknown): readonly string[];
731
-
732
- /**
733
- * Parse the exact bounded `SyncOptions` boundary before allocating resources.
734
- *
735
- * @param value - Caller-supplied options.
736
- * @returns A fresh, validated options record.
737
- */
738
- export declare function parseSyncOptions(value: unknown): SyncOptions;
739
-
740
- /** Parse bounded exact transaction preconditions without invoking caller methods. */
741
- export declare function parseWritePreconditions(value: unknown, limit: number): readonly WritePrecondition[] | undefined;
742
-
743
- /**
744
- * The prune-owned directories. Files outside these roots are never candidates
745
- * for removal, including project-owned skills under `.agents/skills` and
746
- * `.claude/skills`.
747
- *
748
- * @example
749
- * ```ts
750
- * import { PRUNE_DIRECTORIES } from '@orkestrel/scaffold/server'
751
- *
752
- * PRUNE_DIRECTORIES // ['.claude/agents', '.codex/agents', 'scripts']
753
- * ```
754
- */
755
- export declare const PRUNE_DIRECTORIES: readonly string[];
756
-
757
- /**
758
- * List the repo-relative POSIX paths under `target`'s prune directories
759
- * (`.claude/agents`, `.codex/agents`, `scripts`) that the vendored `host`
760
- * allowlist does NOT declare — THE single source of truth for prune drift, consumed by both
761
- * `Materializer.prune` (which deletes exactly these paths) and the bin's
762
- * audit/preview UX (which now shows them honestly instead of a
763
- * structurally-always-zero `audit.foreign`).
764
- *
765
- * @param target - The target directory to scan for unexpected files.
766
- * @param host - The vendored host root the allowlist is derived from.
767
- * @returns The unexpected relative paths (e.g. `.claude/agents/rogue.md`); `[]`
768
- * when a prune directory is absent under `target`, or when none of its
769
- * files are unexpected. Pure read — never deletes anything.
770
- * @throws `ScaffoldError('TARGET', …)` when `host` cannot positively
771
- * establish a vendored allowlist for a prune directory that DOES exist
772
- * under `target` (see `vendoredPruneSet`'s fail-closed remarks).
773
- *
774
- * @example
775
- * ```ts
776
- * import { pruneTargets } from '@orkestrel/scaffold/server'
777
- *
778
- * pruneTargets('./packages/router', hostRoot()) // ['.claude/agents/rogue.md']
779
- * ```
780
- */
781
- export declare function pruneTargets(target: string, host: string): readonly string[];
782
-
783
- /**
784
- * Read one contained file as exact lowercase hexadecimal bytes.
785
- *
786
- * @param root - The declared containing root.
787
- * @param path - The root-relative file path.
788
- * @param code - The coded failure for containment or reading.
789
- * @param boundary - The boundary name used in diagnostics.
790
- * @returns The exact file bytes encoded as lowercase hexadecimal.
791
- */
792
- export declare function readFileHex(root: string, path: string, code: ScaffoldErrorCode, boundary: string, limit?: number): string;
793
-
794
- /**
795
- * Read one contained physical file as bounded UTF-8 text.
796
- *
797
- * @param root - The declared containing root.
798
- * @param path - The root-relative file path.
799
- * @param code - The coded failure for containment or reading.
800
- * @param boundary - The boundary name used in diagnostics.
801
- * @returns The exact file bytes decoded as UTF-8 text.
802
- */
803
- export declare function readFileText(root: string, path: string, code: ScaffoldErrorCode, boundary: string, limit?: number): string;
804
-
805
- /**
806
- * Read bounded physical local guide mirrors for package names.
807
- *
808
- * @param target - The package root.
809
- * @param names - The package names whose mirrors are eligible.
810
- * @returns Existing guide content keyed by package name.
811
- */
812
- export declare function readGuideReferences(target: string, names: readonly string[]): Readonly<Record<string, string>>;
813
-
814
- /**
815
- * Read and validate a vendored host root's `manifest.json`, when present.
816
- *
817
- * @param host - The host root to probe.
818
- * @returns The parsed complete manifest, or `undefined` when `host` has no
819
- * `manifest.json` — the raw-repo-root fallback (`Materializer` then maps
820
- * an artifact's `source` to `host` 1:1, no vendored staging indirection).
821
- * @throws `ScaffoldError('TARGET', …)` when `manifest.json` exists but is
822
- * unreadable, malformed, membership-corrupted, collision-prone,
823
- * root-incomplete, or does not map bijectively and case-exactly onto real
824
- * contained storage files.
825
- *
826
- * @example
827
- * ```ts
828
- * import { readHostManifest } from '@orkestrel/scaffold/server'
829
- *
830
- * readHostManifest('./dist/host') // HostManifest | undefined
831
- * ```
832
- */
833
- export declare function readHostManifest(host: string): HostManifest | undefined;
834
-
835
- /**
836
- * Read `target/package.json` text — the read that feeds `manifestToDependencies`.
837
- *
838
- * @param target - The target directory to read the manifest from.
839
- * @returns The manifest file's raw text.
840
- * @throws `ScaffoldError('TARGET', …)` when the manifest is absent or
841
- * unreadable (e.g. `EACCES` / `EPERM`) — carries the resolved `full` path
842
- * in `context`.
843
- *
844
- * @example
845
- * ```ts
846
- * import { readManifest } from '@orkestrel/scaffold/server'
847
- *
848
- * readManifest('./packages/router') // '{ "name": "@orkestrel/router", … }'
849
- * ```
850
- */
851
- export declare function readManifest(target: string): string;
852
-
853
- /**
854
- * Read a target's current bytes at a set of relative paths into a
855
- * byte-exact hexadecimal {@link Snapshot} — the I/O that feeds `diffPlan`.
856
- *
857
- * @param target - The target directory to read from.
858
- * @param paths - The plan-relative artifact paths to probe.
859
- * @returns A snapshot keyed by path; each file maps to its exact lowercase
860
- * hexadecimal bytes and a directly requested directory maps to `''`
861
- * (presence only). An absent path is omitted entirely.
862
- * @throws `ScaffoldError('TARGET', …)` when an EXISTING path fails to read
863
- * (e.g. `EACCES` / `EPERM`) — carries the offending relative `path` (and
864
- * the resolved `full` path) in `context`. An absent path is never an
865
- * error — it is simply omitted, per the return contract above.
866
- *
867
- * @example
868
- * ```ts
869
- * import { readTarget } from '@orkestrel/scaffold/server'
870
- *
871
- * readTarget('./packages/router', ['package.json', 'src/core/index.ts'])
872
- * // { 'package.json': '7b226e616d65223a…', 'src/core/index.ts': '6578706f7274…' }
873
- * ```
874
- */
875
- export declare function readTarget(target: string, paths: readonly string[]): Snapshot;
876
-
877
- /**
878
- * Map a manifest destination from an artifact's source prefix to its target prefix.
879
- *
880
- * @param artifact - The host artifact carrying the target path and optional source.
881
- * @param destination - The matched manifest destination.
882
- * @returns The exact target-relative path for the matched manifest file.
883
- * @throws `ScaffoldError('INVALID', …)` when `destination` is outside the source prefix.
884
- */
885
- export declare function remapArtifactPath(artifact: HostArtifact, destination: string): string;
886
-
887
- /**
888
- * Atomically promote a staged sibling directory while preserving recoverable state.
889
- *
890
- * @param staging - The completed staging directory.
891
- * @param target - The destination directory to replace.
892
- * @param backup - The sibling path reserved for the prior target.
893
- * @throws `ScaffoldError('WRITE', …)` with explicit `committed` and recovery paths.
894
- */
895
- export declare function replaceDirectory(staging: string, target: string, backup: string): void;
896
-
897
- /** Windows device names that remain reserved even when followed by an extension. */
898
- export declare const RESERVED_PATH_SEGMENT_PATTERN: RegExp;
899
-
900
- /** Existing repository metadata that a materialization target must never own. */
901
- export declare const RESERVED_TARGET_PATH_PATTERN: RegExp;
902
-
903
- /**
904
- * Resolve a path beneath a declared root and reject lexical or symlink escape.
905
- *
906
- * @param root - The containing filesystem root.
907
- * @param path - The candidate path, relative or absolute.
908
- * @param code - The coded error to raise on escape.
909
- * @param boundary - The boundary name used in diagnostics.
910
- * @returns The lexically resolved candidate after realpath-aware validation.
911
- */
912
- export declare function resolveContainedPath(root: string, path: string, code: ScaffoldErrorCode, boundary: string): string;
913
-
914
- /**
915
- * Resolve and completely preflight the canonical guide destinations a sync may write.
916
- *
917
- * @param guides - Structurally validated behind guide results.
918
- * @param target - The repository root that owns `guides/src`.
919
- * @returns Each guide paired with its contained destination.
920
- * @throws `ScaffoldError('WRITE', ...)` before mutation for ownership, collision,
921
- * containment, or existing filesystem-shape violations.
922
- */
923
- export declare function resolveGuideWrites(guides: readonly GuideSync[], target: string): readonly GuideWrite[];
924
-
925
- /**
926
- * Resolve a contained path whose existing ancestor chain contains no links.
927
- *
928
- * @param root - The trusted lexical and physical root.
929
- * @param path - The portable root-relative path.
930
- * @param code - The coded error to raise on failure.
931
- * @param boundary - The boundary name used in diagnostics.
932
- * @returns The contained destination.
933
- * @throws `ScaffoldError` when containment fails or an ancestor is not a real directory.
934
- */
935
- export declare function resolvePhysicalPath(root: string, path: string, code: ScaffoldErrorCode, boundary: string): string;
936
-
937
- /**
938
- * Resolve the deepest existing ancestor of a path through the real filesystem.
939
- *
940
- * @param path - The absolute or relative path to resolve.
941
- * @returns A path whose existing prefix has been resolved through symlinks.
942
- */
943
- export declare function resolveRealPath(path: string): string;
944
-
945
- /**
946
- * Restore quarantined files to their original target-relative paths.
947
- *
948
- * @param transaction - The nominal transaction that owns the quarantine and target.
949
- * @param paths - The relative paths to restore, in their original move order.
950
- * @throws `ScaffoldError('WRITE', …)` after attempting every reverse-order
951
- * restoration when one or more files could not be restored.
952
- */
953
- export declare function restoreFiles(transaction: WriteTransaction, paths: readonly string[]): void;
954
-
955
- /**
956
- * Filter a manifest record's entries down to `@orkestrel/`-prefixed keys with
957
- * string values — the shared `dependencies` / `peerDependencies` /
958
- * `devDependencies` reader `deriveBlueprint` uses for every dependency-shaped
959
- * field.
960
- *
961
- * @param value - The candidate manifest field value (e.g. `parsed.dependencies`).
962
- * @returns The `@orkestrel/`-prefixed `[name, range]` entries; `[]` when
963
- * `value` is not a plain object (per `isRecord`).
964
- *
965
- * @example
966
- * ```ts
967
- * import { selectOrkestrelEntries } from '@orkestrel/scaffold/server'
968
- *
969
- * selectOrkestrelEntries({ '@orkestrel/core': '^1.0.0', lodash: '^4.0.0' })
970
- * // [['@orkestrel/core', '^1.0.0']]
971
- * ```
972
- */
973
- export declare function selectOrkestrelEntries(value: unknown): readonly (readonly [string, string])[];
974
-
975
- /** Local configuration and credential-like paths that must never enter a vendored host. */
976
- export declare const SENSITIVE_HOST_PATH_PATTERN: RegExp;
977
-
978
- /**
979
- * Stage the vendored host set (byte-preserved copies + `manifest.json`) from
980
- * a repo root into an output directory — the BUILD-time primitive the
981
- * `build:host` npm script now calls directly (replacing a standalone build
982
- * script); `Materializer.materialize` is the RUNTIME reader of what this
983
- * writes (via `hostRoot` / `readHostManifest`).
75
+ * @returns The SHA-256 of that exact membership, in that exact order.
984
76
  *
985
- * @param root - The repo root every `paths` entry resolves against.
986
- * @param out - The output directory to replace after staging completes.
987
- * @param paths - The repo-relative file/directory entries to stage; defaults
988
- * to the package's own vendored set (`HOST_PATHS`) — a caller passes an
989
- * explicit list only to stage an arbitrary/test set.
990
77
  * @remarks
991
- * Every source, path collision, and root/output relationship is preflighted
992
- * before output mutation. Files are copied into a temporary sibling; the
993
- * completed staging tree atomically replaces `out`, with rollback when the
994
- * swap fails. The manifest records both sorted file `entries` and the
995
- * complete sorted directory `roots` inventory so destructive consumers can
996
- * distinguish a declared-empty root from a truncated manifest.
997
- * @returns The written manifest's entries (`{ storage, destination, executable }`).
998
- * @throws `ScaffoldError('TARGET', …)` for an invalid/escaping source or path
999
- * collision, and `ScaffoldError('WRITE', …)` for staging/swap failures.
1000
- *
1001
- * @example
1002
- * ```ts
1003
- * import { stageHost } from '@orkestrel/scaffold/server'
1004
- *
1005
- * const entries = stageHost(process.cwd(), 'dist/host')
1006
- * entries.length // number of files staged
1007
- * ```
1008
- */
1009
- export declare function stageHost(root: string, out: string, paths?: readonly string[]): readonly ManifestEntry[];
1010
-
1011
- /**
1012
- * Map a repo-relative path to its vendored-host STAGING path, per the
1013
- * dotfile-mapping rule `stageHost` writes into `manifest.json`.
1014
- *
1015
- * @param path - The repo-relative source path (e.g. `.claude/agents/reviewer.md`).
1016
- * @returns The mapped storage path: a leading-dot TOP-LEVEL FILE maps to
1017
- * `dotfiles/<name-without-dot>`; a leading-dot DIRECTORY segment loses its
1018
- * dot wherever it appears; an undotted path is unchanged.
78
+ * Independent of the manifest's own `digest` field, which is what lets a reader
79
+ * detect a membership edit that did not update it. Order is part of the claim
80
+ * rather than normalized away, because a staged manifest sorts its entries and
81
+ * roots once and a reordered copy is a different file. Each entry is projected
82
+ * to exactly the three declared fields, so a hand-added property cannot ride
83
+ * into the digest and cannot change it either.
1019
84
  *
1020
85
  * @example
1021
86
  * ```ts
1022
- * import { storagePath } from '@orkestrel/scaffold/server'
87
+ * import { computeManifestDigest } from '@orkestrel/scaffold/server'
1023
88
  *
1024
- * storagePath('.gitignore') // 'dotfiles/gitignore'
1025
- * storagePath('.claude/agents/reviewer.md') // 'claude/agents/reviewer.md'
1026
- * storagePath('.github/workflows/ci.yml') // 'github/workflows/ci.yml'
1027
- * storagePath('AGENTS.md') // 'AGENTS.md'
89
+ * computeManifestDigest([], []) // the digest of the empty membership
1028
90
  * ```
1029
91
  */
1030
- export declare function storagePath(path: string): string;
92
+ export declare function computeManifestDigest(entries: readonly ManifestEntry[], roots: readonly string[]): string;
1031
93
 
1032
94
  /**
1033
- * The upstream-synchronization entity (server) — the impure FETCH sibling of
1034
- * `Materializer`, Promise-based and network-only.
1035
- *
1036
- * @remarks
1037
- * Every method reads upstream over HTTPS with a 10-second per-request
1038
- * `AbortSignal.timeout` and bounded `concurrency` (default 6, never an
1039
- * unbounded `Promise.all`). The default COLLECT posture captures each
1040
- * dependency's `freshness` (`404` → `missing`, transport / other non-2xx →
1041
- * `failed`) into the result; `strict` mode instead throws
1042
- * `ScaffoldError('FETCH', …)` naming the failing URL. `guides`'s optional
1043
- * `current` parameter is a caller-supplied local-mirror content map keyed by
1044
- * dependency NAME (the `diffPlan` caller-supplied-reference pattern): WITH the
1045
- * map, a fetched guide byte-equal to its entry verdicts `current`, anything
1046
- * differing or absent from the map verdicts `behind`; WITHOUT the map, every
1047
- * successful fetch verdicts `behind` (no reference means it needs syncing).
1048
- * `pull` builds that map itself from the TARGET's declared dependency mirrors;
1049
- * `mirror` builds it from the registry's exact organization package list,
1050
- * excluding the target's own guide and issuing no registry version reads. Both
1051
- * verdicts are target-relative; `write` commits only the
1052
- * `behind` guides (never `current`, `missing`, or `failed`, which carries no
1053
- * trustworthy content) under the same realpath-anchored containment law
1054
- * `Materializer` enforces. After `destroy()` every method throws `DESTROYED`;
1055
- * teardown is idempotent, emitter last.
1056
- *
1057
- * @example
1058
- * ```ts
1059
- * import { createSync } from '@orkestrel/scaffold/server'
95
+ * Construct a {@link Materializer}.
1060
96
  *
1061
- * const sync = createSync()
1062
- * const report = await sync.pull('.')
1063
- * if (report.failed === 0) await sync.write(report, '.')
1064
- * sync.destroy()
1065
- * ```
1066
- */
1067
- export declare class Sync implements SyncInterface {
1068
- #private;
1069
- constructor(options?: SyncOptions);
1070
- get emitter(): EmitterInterface<SyncEventMap>;
1071
- lookup(names: readonly string[]): Promise<readonly VersionLookup[]>;
1072
- guides(deps: readonly Dependency[], current?: Readonly<Record<string, string>>): Promise<readonly GuideSync[]>;
1073
- versions(deps: readonly Dependency[]): Promise<readonly VersionSync[]>;
1074
- /**
1075
- * The fleet package catalog, sourced from the npm registry — the
1076
- * AUTHORITATIVE enumeration (never a caller-supplied root).
97
+ * @param options - The vendored host root, the initial listeners, and the listener-error handler.
98
+ * @returns The materializer, typed as the contract consumers program against.
99
+ * @throws {@link ScaffoldError} coded `INVALID` when `options` is present but is
100
+ * not an option bag the materializer accepts, and `TARGET` when the host carries
101
+ * a manifest that cannot be read or does not match what it stores.
1077
102
  *
1078
- * @returns One {@link CatalogEntry} per `@orkestrel/*` package the registry
1079
- * lists, code-unit sorted by `name`.
1080
- * @remarks
1081
- * Three fetches build each entry: (1) the org's exact package-list
1082
- * (`-/org/orkestrel/package`) enumerates every published name an
1083
- * unreachable or malformed response throws a coded `ScaffoldError('FETCH')`
1084
- * UNCONDITIONALLY (never gated by `strict`), since without it there is no
1085
- * catalog to build; (2) each name's own registry packument supplies
1086
- * `version` (`dist-tags.latest`) and a registry-path `description`
1087
- * fallback — a failed/malformed packument keeps the entry (degraded:
1088
- * `version: ''`) rather than dropping it, since the org list already
1089
- * proved the package exists; (3) each name's own guide
1090
- * (`guides/src/<short>.md`, same canonical URL as `guides()`, fetched
1091
- * unauthenticated — every fleet repo is public) supplies the PREFERRED
1092
- * `description` — its first blockquote's first paragraph — falling back
1093
- * to the packument description when the guide 404s, faults, or carries no
1094
- * blockquote; a 404 STAYS LISTED (it is a reachability signal, not an
1095
- * absence) with a note reading `guide unreachable (HTTP 404 — repo
1096
- * private or guide missing?)`. Emits `package` once per entry with a
1097
- * combined human-readable `note` (empty when both fetches succeeded)
1098
- * alongside the existing `error` events for each degraded sub-fetch.
103
+ * @example
104
+ * ```ts
105
+ * import { createMaterializer } from '@orkestrel/scaffold/server'
106
+ *
107
+ * const materializer = createMaterializer({ host: './dist/host' })
108
+ * materializer.destroy()
109
+ * ```
1099
110
  */
1100
- catalog(): Promise<readonly CatalogEntry[]>;
1101
- pull(target: string, dependencies?: readonly Dependency[]): Promise<SyncReport>;
1102
- mirror(target: string): Promise<SyncReport>;
1103
- write(report: SyncReport, target: string): Promise<readonly string[]>;
1104
- destroy(): void;
1105
- }
1106
-
1107
- /** Safe ASCII branch alphabet accepted before Git ref-component validation. */
1108
- export declare const SYNC_BRANCH_PATTERN: RegExp;
1109
-
1110
- /** Mutable one-cell byte allowance shared by concurrent Sync readers. */
1111
- export declare type SyncAllowance = Float64Array;
1112
-
1113
- /** One normalized upstream HTTP(S) endpoint base accepted by `Sync`. */
1114
- export declare type SyncBase = string;
1115
-
1116
- /** One bounded Git-compatible branch path accepted by the guide endpoint. */
1117
- export declare type SyncBranch = string;
1118
-
1119
- /**
1120
- * `Sync`'s push observation channel (AGENTS §13, server).
1121
- *
1122
- * @remarks
1123
- * `package` fires once per `catalog()` entry processed; its `note` is the
1124
- * empty string when both the registry packument and the guide fetch
1125
- * succeeded, else a human-readable explanation of which one degraded (and
1126
- * why) the same `''`-means-nothing convention `GuideSync.note` /
1127
- * `VersionSync.note` use, just non-optional here since every `catalog()`
1128
- * entry emits exactly once regardless of outcome.
1129
- */
1130
- export declare type SyncEventMap = {
1131
- readonly guide: readonly [name: string];
1132
- readonly version: readonly [name: string];
1133
- readonly package: readonly [name: string, note: string];
1134
- readonly write: readonly [path: string];
1135
- readonly done: readonly [report: SyncReport];
1136
- readonly error: readonly [error: unknown];
1137
- readonly destroy: readonly [];
1138
- };
1139
-
1140
- /** Build the closed upstream-guide endpoint options shape. */
1141
- export declare function syncGuideOptionsShape(): ObjectShape<{
1142
- base: OptionalShape<StringShape>;
1143
- branch: OptionalShape<StringShape>;
1144
- timeout: OptionalShape<NumberShape>;
1145
- }, false>;
1146
-
1147
- /**
1148
- * The upstream-synchronization contract (server) the impure FETCH sibling
1149
- * of `MaterializerInterface`.
1150
- */
1151
- export declare interface SyncInterface {
1152
- readonly emitter: EmitterInterface<SyncEventMap>;
1153
- lookup(names: readonly string[]): Promise<readonly VersionLookup[]>;
1154
- guides(deps: readonly Dependency[], current?: Readonly<Record<string, string>>): Promise<readonly GuideSync[]>;
1155
- versions(deps: readonly Dependency[]): Promise<readonly VersionSync[]>;
1156
- catalog(): Promise<readonly CatalogEntry[]>;
1157
- pull(target: string, dependencies?: readonly Dependency[]): Promise<SyncReport>;
1158
- mirror(target: string): Promise<SyncReport>;
1159
- write(report: SyncReport, target: string): Promise<readonly string[]>;
1160
- destroy(): void;
1161
- }
1162
-
1163
- /**
1164
- * Options for `createSync` / the `Sync` constructor (server).
1165
- *
1166
- * @remarks
1167
- * The endpoint bases + branch are INJECTABLE — `guides.base` defaults to
1168
- * `raw.githubusercontent.com`, `guides.branch` to `main`, `registry.base` to
1169
- * `registry.npmjs.org`, and `guides.timeout` / `registry.timeout` to 10
1170
- * seconds each. `concurrency` bounds in-flight requests (default 6, never an
1171
- * unbounded `Promise.all`); `retries` opts into per-request retry on a
1172
- * transport fault (default 0); `strict` flips a collect-mode failure into a
1173
- * thrown `ScaffoldError('FETCH', …)` (default `false`). `limit` bounds the
1174
- * bytes read from a single response body (declared `Content-Length` or
1175
- * streamed total, whichever trips first) — default 5,242,880 (5 MiB); a body
1176
- * that would exceed it is a transport fault, handled exactly like any other
1177
- * (retry-eligible per `retries`, then `failed` / strict `FETCH`). Every fetch
1178
- * is unauthenticated no token, no `Authorization` header, anywhere; every
1179
- * fleet repo is public, so plain reachability is the only signal (a guide
1180
- * `404` degrades gracefully rather than needing credentials).
1181
- * `registry.base` also anchors `catalog()`'s org package-list lookup
1182
- * (`<registry.base>/-/org/orkestrel/package`) and its per-package packument
1183
- * fetches (`<registry.base>/<name>`) — the same base every other registry
1184
- * read already uses.
1185
- */
1186
- export declare interface SyncOptions {
1187
- readonly guides?: {
1188
- readonly base?: SyncBase;
1189
- readonly branch?: SyncBranch;
1190
- readonly timeout?: number;
1191
- };
1192
- readonly registry?: {
1193
- readonly base?: SyncBase;
1194
- readonly timeout?: number;
1195
- };
1196
- readonly concurrency?: number;
1197
- readonly retries?: number;
1198
- readonly strict?: boolean;
1199
- readonly limit?: number;
1200
- readonly items?: number;
1201
- readonly budget?: number;
1202
- readonly on?: EmitterHooks<SyncEventMap>;
1203
- readonly error?: EmitterErrorHandler;
1204
- }
1205
-
1206
- /** The compiled, closed data-only `SyncOptions` contract. */
1207
- export declare const syncOptionsContract: ContractInterface<Readonly<{} & {
1208
- guides?: Readonly<{} & {
1209
- base?: string;
1210
- branch?: string;
1211
- timeout?: number;
1212
- }>;
1213
- registry?: Readonly<{} & {
1214
- base?: string;
1215
- timeout?: number;
1216
- }>;
1217
- concurrency?: number;
1218
- retries?: number;
1219
- strict?: boolean;
1220
- limit?: number;
1221
- items?: number;
1222
- budget?: number;
1223
- }>>;
1224
-
1225
- /** Build the closed data-only `SyncOptions` shape. */
1226
- export declare function syncOptionsShape(): ObjectShape<{
1227
- guides: OptionalShape<ObjectShape<{
1228
- base: OptionalShape<StringShape>;
1229
- branch: OptionalShape<StringShape>;
1230
- timeout: OptionalShape<NumberShape>;
1231
- }, false>>;
1232
- registry: OptionalShape<ObjectShape<{
1233
- base: OptionalShape<StringShape>;
1234
- timeout: OptionalShape<NumberShape>;
1235
- }, false>>;
1236
- concurrency: OptionalShape<NumberShape>;
1237
- retries: OptionalShape<NumberShape>;
1238
- strict: OptionalShape<BooleanShape>;
1239
- limit: OptionalShape<NumberShape>;
1240
- items: OptionalShape<NumberShape>;
1241
- budget: OptionalShape<NumberShape>;
1242
- }, false>;
1243
-
1244
- /** Build the closed registry endpoint options shape. */
1245
- export declare function syncRegistryOptionsShape(): ObjectShape<{
1246
- base: OptionalShape<StringShape>;
1247
- timeout: OptionalShape<NumberShape>;
1248
- }, false>;
1249
-
1250
- /**
1251
- * Assemble one synchronization report from already ordered guide and version outcomes.
1252
- *
1253
- * @param target - The target package root.
1254
- * @param guides - Guide outcomes.
1255
- * @param versions - Version outcomes.
1256
- * @returns The derived whole report.
1257
- */
1258
- export declare function syncReportOf(target: string, guides: readonly GuideSync[], versions: readonly VersionSync[]): SyncReport;
1259
-
1260
- /**
1261
- * Revalidate one captured physical-directory identity without following links.
1262
- *
1263
- * @param anchor - The captured path, device, and inode.
1264
- * @param boundary - The boundary name used in diagnostics.
1265
- * @throws `ScaffoldError('WRITE', ...)` when the directory is missing or changed.
1266
- */
1267
- export declare function validateWriteAnchor(anchor: WriteAnchor, boundary: string): void;
1268
-
1269
- /**
1270
- * Revalidate every private directory owned by a write transaction.
1271
- *
1272
- * @param transaction - The nominal transaction state.
1273
- * @throws `ScaffoldError('WRITE', ...)` when any private directory changed.
1274
- */
1275
- export declare function validateWriteDirectories(transaction: WriteTransaction): void;
1276
-
1277
- /**
1278
- * Revalidate the transaction target against its original or transaction-owned identity.
1279
- *
1280
- * @param transaction - The nominal transaction state.
1281
- * @param owned - A target directory created by the current commit.
1282
- * @throws `ScaffoldError('WRITE', ...)` when the target identity changed.
1283
- */
1284
- export declare function validateWriteTarget(transaction: WriteTransaction, owned: WriteAnchor | undefined): void;
1285
-
1286
- /**
1287
- * The vendored set of destination-relative paths under `directory` (one of
1288
- * `PRUNE_DIRECTORIES`) that `pruneTargets` must NOT report — read from the
1289
- * manifest's `destination`s when `host` has one, else listed straight off
1290
- * `host/<directory>`.
1291
- *
1292
- * @param host - The vendored host root to establish the allowlist from.
1293
- * @param directory - The prune directory (one of `PRUNE_DIRECTORIES`) to scope the allowlist to.
1294
- * @remarks
1295
- * FAIL CLOSED: before returning any allowlist (even an empty one), the
1296
- * vendored source must be POSITIVELY established, or a caller would treat an
1297
- * unresolved host as "vendors nothing" and report every file under
1298
- * `target/<directory>` as unexpected. A missing `host` root, or (no
1299
- * `manifest.json` AND no `host/<directory>`), is a coded `TARGET` failure —
1300
- * the distinction this guards is missing-host vs genuinely-empty-vendor: a
1301
- * `host` that EXISTS and vendors zero files in `directory` (an existing empty
1302
- * dir, or a manifest with zero entries for it) remains a valid empty allowlist.
1303
- * @returns The allowed destination-relative paths under `directory`.
1304
- * @throws `ScaffoldError('TARGET', …)` when `host` does not exist, or when
1305
- * `host` has no `manifest.json` and no `host/<directory>` either.
1306
- *
1307
- * @example
1308
- * ```ts
1309
- * import { vendoredPruneSet } from '@orkestrel/scaffold/server'
1310
- *
1311
- * vendoredPruneSet('./dist/host', '.claude/agents') // Set { '.claude/agents/reviewer.md', }
1312
- * ```
1313
- */
1314
- export declare function vendoredPruneSet(host: string, directory: string): ReadonlySet<string>;
1315
-
1316
- /** One bare-name registry lookup, without a caller-invented declaration range. */
1317
- export declare type VersionLookup = {
1318
- readonly name: string;
1319
- readonly latest: string;
1320
- readonly freshness: 'behind';
1321
- } | {
1322
- readonly name: string;
1323
- readonly freshness: 'missing' | 'failed';
1324
- readonly note: string;
1325
- };
1326
-
1327
- /** Exact SHA-256 digest accepted by write preconditions. */
1328
- export declare const WRITE_DIGEST_PATTERN: RegExp;
1329
-
1330
- /** A physical directory identity captured across a write transaction. */
1331
- export declare interface WriteAnchor {
1332
- readonly path: string;
1333
- readonly device: number;
1334
- readonly inode: number;
1335
- }
1336
-
1337
- /** Anchored result of creating a physical directory path one segment at a time. */
1338
- export declare interface WriteDirectoryResult {
1339
- readonly anchor: WriteAnchor;
1340
- readonly created: readonly WriteAnchor[];
1341
- }
1342
-
1343
- /** One destination snapshot required to remain stable through a write commit. */
1344
- export declare interface WriteExpectation {
1345
- readonly path: string;
1346
- readonly shape: 'absent' | 'file' | 'directory';
1347
- readonly device?: number;
1348
- readonly inode?: number;
1349
- readonly modified?: number;
1350
- readonly size?: number;
1351
- readonly digest?: string;
1352
- }
1353
-
1354
- /** Caller-observed destination state that a write transaction must still match. */
1355
- export declare interface WritePrecondition {
1356
- readonly path: string;
1357
- readonly shape: 'absent' | 'file';
1358
- readonly digest?: string;
1359
- }
1360
-
1361
- /**
1362
- * Nominal, same-volume write-transaction state. Construction derives every
1363
- * filesystem path from a target and portable relative paths; callers cannot
1364
- * supply a deletion root or mutate captured arrays.
1365
- */
1366
- export declare class WriteTransaction {
1367
- #private;
1368
- constructor(token: unknown, target: string, root: string, stage: string, backup: string, expectations: readonly WriteExpectation[], parents: readonly WriteAnchor[], directories: readonly WriteAnchor[], anchor: WriteAnchor, existing: WriteAnchor | undefined);
1369
- /** Create private staging state and snapshot every destination before mutation. */
1370
- static create(target: string, paths: readonly string[], preconditions?: readonly WritePrecondition[]): WriteTransaction;
1371
- get target(): string;
1372
- get root(): string;
1373
- get stage(): string;
1374
- get backup(): string;
1375
- get expectations(): readonly WriteExpectation[];
1376
- get parents(): readonly WriteAnchor[];
1377
- get directories(): readonly WriteAnchor[];
1378
- get anchor(): WriteAnchor;
1379
- get existing(): WriteAnchor | undefined;
1380
- }
1381
-
1382
- export { }
111
+ export declare function createMaterializer(options?: MaterializerOptions): MaterializerInterface;
112
+
113
+ /**
114
+ * Construct an {@link Upstream}.
115
+ *
116
+ * @param options - The two endpoints, the request bounds, the initial listeners,
117
+ * and the listener-error handler.
118
+ * @returns The reader, typed as the contract consumers program against.
119
+ * @throws {@link ScaffoldError} coded `INVALID` when `options` is present but is
120
+ * not an option bag the reader accepts, or when either endpoint names a scheme,
121
+ * host, or form the reader will not request.
122
+ *
123
+ * @example
124
+ * ```ts
125
+ * import { createUpstream } from '@orkestrel/scaffold/server'
126
+ *
127
+ * const upstream = createUpstream({ guides: { branch: 'main' } })
128
+ * upstream.destroy()
129
+ * ```
130
+ */
131
+ export declare function createUpstream(options?: UpstreamOptions): UpstreamInterface;
132
+
133
+ /**
134
+ * The exact SHA-256 syntax a digest is stated in: sixty-four lowercase hexadecimal digits.
135
+ *
136
+ * @remarks
137
+ * Fixed length, unlike the core byte encoding, because a digest is one value of
138
+ * one algorithm rather than a variable run of bytes. Lowercase only, so two
139
+ * spellings of one digest can never compare unequal.
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
+ * @example
293
+ * ```ts
294
+ * import { isFilesystemPath } from '@orkestrel/scaffold/server'
295
+ *
296
+ * isFilesystemPath('C:/Users/sample/project') // true
297
+ * isFilesystemPath('../sibling') // true
298
+ * isFilesystemPath('project/nul') // false
299
+ * ```
300
+ */
301
+ export declare function isFilesystemPath(value: unknown): value is string;
302
+
303
+ /**
304
+ * Narrow a value to one {@link HostManifest}.
305
+ *
306
+ * @remarks
307
+ * The manifest is read from a directory a caller named, so it is the least
308
+ * trusted value the server face handles and is guarded whole: every entry, every
309
+ * declared root, and the digest that authenticates their membership.
310
+ */
311
+ export declare const isHostManifest: Guard<HostManifest>;
312
+
313
+ /**
314
+ * Narrow a value to a working-tree inventory within the limit one target may report.
315
+ *
316
+ * @param value - The candidate inventory.
317
+ * @returns `true` for an array of no more than `MAX_INVENTORY_PATHS` items.
318
+ *
319
+ * @remarks
320
+ * Compose this ahead of an element guard exactly as the core collection guard is
321
+ * composed, and for the same reason: the item count is settled before anything
322
+ * walks the items, and a hostile `length` accessor answers `false` rather than
323
+ * escaping as a throw. It exists beside that guard rather than reusing it
324
+ * because the two bound different things — one bounds what a caller may hand a
325
+ * public method, this one bounds what a checkout may contain.
326
+ *
327
+ * @example
328
+ * ```ts
329
+ * import { isInventory } from '@orkestrel/scaffold/server'
330
+ *
331
+ * isInventory(['AGENTS.md']) // true
332
+ * isInventory('AGENTS.md') // false
333
+ * ```
334
+ */
335
+ export declare function isInventory(value: unknown): value is readonly unknown[];
336
+
337
+ /**
338
+ * Narrow a value to one {@link ManifestEntry}.
339
+ *
340
+ * @remarks
341
+ * Both paths are measured by the core path law, because a vendored host's
342
+ * storage name and the destination it maps to are each a path inside a
343
+ * workspace. That is what stops a hand-edited manifest from mapping a vendored
344
+ * file to a destination outside the target.
345
+ *
346
+ * @example
347
+ * ```ts
348
+ * import { isManifestEntry } from '@orkestrel/scaffold/server'
349
+ *
350
+ * isManifestEntry({ storage: 'AGENTS.md', destination: 'AGENTS.md', executable: false }) // true
351
+ * ```
352
+ */
353
+ export declare const isManifestEntry: Guard<ManifestEntry>;
354
+
355
+ /**
356
+ * Narrow a value to the materializer's initial listener record.
357
+ *
358
+ * @remarks
359
+ * Every event is optional and every declared value is a function. A key outside
360
+ * the materializer's event map is refused, so a listener wired to a misspelled
361
+ * event fails at construction instead of never firing.
362
+ */
363
+ export declare const isMaterializerHooks: Guard<EmitterHooks<MaterializerEventMap>>;
364
+
365
+ /**
366
+ * Narrow a value to {@link MaterializerOptions}.
367
+ *
368
+ * @example
369
+ * ```ts
370
+ * import { isMaterializerOptions } from '@orkestrel/scaffold/server'
371
+ *
372
+ * isMaterializerOptions({}) // true
373
+ * isMaterializerOptions({ host: 'dist/host*' }) // false
374
+ * ```
375
+ */
376
+ export declare const isMaterializerOptions: Guard<MaterializerOptions>;
377
+
378
+ /** Narrow a value to a bounded list of fetched guide mirrors. */
379
+ export declare const isMirrors: Guard<readonly Mirror[]>;
380
+
381
+ /**
382
+ * Test whether a path is a physical directory this package will read or write into.
383
+ *
384
+ * @param path - The resolved host path to inspect, without following links.
385
+ * @returns `true` only for a directory that is not a link.
386
+ *
387
+ * @remarks
388
+ * A junction and a directory symbolic link both report as directories once
389
+ * followed, so the inspection deliberately does not follow: a redirected
390
+ * directory is refused here rather than silently accepted as the one the caller
391
+ * named.
392
+ *
393
+ * @example
394
+ * ```ts
395
+ * import { isPhysicalDirectory } from '@orkestrel/scaffold/server'
396
+ *
397
+ * isPhysicalDirectory('/tmp/project') // true for a plain directory
398
+ * ```
399
+ */
400
+ export declare function isPhysicalDirectory(path: string): boolean;
401
+
402
+ /**
403
+ * Test whether a path is a physical file this package will read or replace.
404
+ *
405
+ * @param path - The resolved host path to inspect, without following links.
406
+ * @returns `true` only for a regular file that is neither a link nor hard-linked
407
+ * elsewhere.
408
+ *
409
+ * @remarks
410
+ * The link tests are the point. A symbolic link is a path pointing somewhere
411
+ * else, so writing through one writes outside the target; a hard link means a
412
+ * second name shares the same bytes, so replacing them changes a file nobody
413
+ * asked about. Both are refused rather than followed.
414
+ *
415
+ * @example
416
+ * ```ts
417
+ * import { isPhysicalFile } from '@orkestrel/scaffold/server'
418
+ *
419
+ * isPhysicalFile('/tmp/project/AGENTS.md') // true for a plain file
420
+ * ```
421
+ */
422
+ export declare function isPhysicalFile(path: string): boolean;
423
+
424
+ /**
425
+ * Narrow a value to a {@link Repository}.
426
+ *
427
+ * @remarks
428
+ * Both path lists are target-relative, so both are measured by the core path
429
+ * law: a reported path that is not one this package could have planned is not a
430
+ * path it will delete. The inventory guard bounds the lists, because a checkout
431
+ * is legitimately far larger than any collection a caller hands a method.
432
+ *
433
+ * @example
434
+ * ```ts
435
+ * import { isRepository } from '@orkestrel/scaffold/server'
436
+ *
437
+ * isRepository({ tracked: ['AGENTS.md'], dirty: [] }) // true
438
+ * isRepository({ tracked: ['../secrets'], dirty: [] }) // false
439
+ * ```
440
+ */
441
+ export declare const isRepository: Guard<Repository>;
442
+
443
+ /**
444
+ * Narrow a value to a per-request timeout in milliseconds.
445
+ *
446
+ * @remarks
447
+ * A whole number of milliseconds, at least one and no more than
448
+ * {@link MAX_UPSTREAM_TIMEOUT}. Zero is refused because a request that may not
449
+ * take any time is a request that cannot succeed.
450
+ */
451
+ export declare const isTimeout: Guard<number>;
452
+
453
+ /**
454
+ * Narrow a value to the upstream reader's initial listener record.
455
+ *
456
+ * @remarks
457
+ * Closed to the reader's own four events for the same reason the materializer's
458
+ * record is closed to its five.
459
+ */
460
+ export declare const isUpstreamHooks: Guard<EmitterHooks<UpstreamEventMap>>;
461
+
462
+ /**
463
+ * Narrow a value to {@link UpstreamOptions}.
464
+ *
465
+ * @remarks
466
+ * Each grouped endpoint is closed to its own leaves, so a setting written under
467
+ * the wrong entity is refused rather than ignored. Every numeric leaf is a whole
468
+ * number inside a ceiling: an unbounded concurrency, retry count, response
469
+ * limit, or call budget is a way to exhaust the caller, so the ceiling is stated
470
+ * here rather than left to the reader. The two byte ceilings are the core
471
+ * artifact and total-artifact limits, because a fetched guide is an artifact and
472
+ * a whole call retains no more than a whole plan.
473
+ *
474
+ * @example
475
+ * ```ts
476
+ * import { isUpstreamOptions } from '@orkestrel/scaffold/server'
477
+ *
478
+ * isUpstreamOptions({ guides: { branch: 'main' }, concurrency: 4 }) // true
479
+ * isUpstreamOptions({ concurrency: 0 }) // false
480
+ * ```
481
+ */
482
+ export declare const isUpstreamOptions: Guard<UpstreamOptions>;
483
+
484
+ /**
485
+ * Test whether a target is safe to write a fresh workspace into.
486
+ *
487
+ * @param target - The candidate target directory.
488
+ * @returns `true` when the target is absent, empty, or holds nothing but its own
489
+ * `.git` directory.
490
+ *
491
+ * @remarks
492
+ * The green-field law. A checkout of an empty repository is where a new
493
+ * workspace legitimately starts, so that one directory is admitted and nothing
494
+ * else is; anything more means the caller is repairing a workspace rather than
495
+ * creating one. Only the first two entries are read, so the answer costs the
496
+ * same on an empty directory and on a full one.
497
+ *
498
+ * @example
499
+ * ```ts
500
+ * import { isVacant } from '@orkestrel/scaffold/server'
501
+ *
502
+ * isVacant('./packages/router-new') // true when absent, empty, or `.git` only
503
+ * ```
504
+ */
505
+ export declare function isVacant(target: string): boolean;
506
+
507
+ /**
508
+ * List a directory's descendant directories as sorted root-relative paths.
509
+ *
510
+ * @param root - The directory to inventory.
511
+ * @returns Every descendant directory as a `/`-separated root-relative path, in
512
+ * code-unit order, and `[]` when `root` is absent.
513
+ * @throws `ScaffoldError('INVALID', …)` when `root` is not a host path.
514
+ * @throws `ScaffoldError('TARGET', …)` when `root` is present but is not a
515
+ * physical directory, cannot be read, holds a name this package could not plan,
516
+ * or carries more entries or more nesting than one inventory may report.
517
+ *
518
+ * @remarks
519
+ * The sibling of {@link listFiles}, under the same bounds and the same refusals,
520
+ * and it exists because a directory holding no file is invisible to a file walk.
521
+ * That is the half a vendored host's `roots` declares and the half a file
522
+ * inventory cannot check, so a stager needs both walks to state a complete
523
+ * membership.
524
+ *
525
+ * `root` itself is not listed, because the answer is root-relative and the root
526
+ * has no root-relative name. A redirected directory is not listed and is not
527
+ * walked into, so no traversal can leave the root and no cycle can form.
528
+ *
529
+ * @example
530
+ * ```ts
531
+ * import { listDirectories } from '@orkestrel/scaffold/server'
532
+ *
533
+ * listDirectories('./.claude') // ['agents', 'rules', 'skills', …]
534
+ * ```
535
+ */
536
+ export declare function listDirectories(root: string): readonly string[];
537
+
538
+ /**
539
+ * List a directory's files as sorted root-relative paths.
540
+ *
541
+ * @param root - The directory to inventory.
542
+ * @returns Every descendant file as a `/`-separated root-relative path, in
543
+ * code-unit order, and `[]` when `root` is absent.
544
+ * @throws `ScaffoldError('INVALID', …)` when `root` is not a host path.
545
+ * @throws `ScaffoldError('TARGET', …)` when `root` is present but is not a
546
+ * physical directory, cannot be read, holds a name this package could not plan,
547
+ * or carries more entries or more nesting than one inventory may report.
548
+ *
549
+ * @remarks
550
+ * A whole-tree answer throws where a single-path answer returns `undefined`, and
551
+ * the reason is that a partial inventory reads exactly like a complete one. A
552
+ * caller comparing a target against a plan would treat a truncated listing as
553
+ * proof that the missing files are not there.
554
+ *
555
+ * Absence is the one exception: nothing to inventory is a complete answer, so it
556
+ * is the empty list. Links are listed as files rather than followed, so no
557
+ * traversal can leave the root and no cycle can form.
558
+ *
559
+ * @example
560
+ * ```ts
561
+ * import { listFiles } from '@orkestrel/scaffold/server'
562
+ *
563
+ * listFiles('./dist/host') // ['AGENTS.md', 'claude/rules/names.md', …]
564
+ * ```
565
+ */
566
+ export declare function listFiles(root: string): readonly string[];
567
+
568
+ /**
569
+ * The reserved metadata name a staged vendored host writes at its own root.
570
+ *
571
+ * @remarks
572
+ * The one name a vendored file may never claim, because the staged root holds
573
+ * the manifest under it. The producer refuses a storage name equal to it and the
574
+ * reader looks for exactly this file, so both sides read one constant rather
575
+ * than repeating a literal that only agrees by inspection.
576
+ */
577
+ export declare const MANIFEST_NAME = "manifest.json";
578
+
579
+ /** One file record of the vendored host's manifest. */
580
+ export declare interface ManifestEntry {
581
+ readonly storage: string;
582
+ readonly destination: string;
583
+ readonly executable: boolean;
584
+ }
585
+
586
+ /**
587
+ * Test whether a captured directory is still the same directory.
588
+ *
589
+ * @param anchor - The identity captured earlier.
590
+ * @returns `true` when the path still holds a physical directory of that exact
591
+ * device and inode.
592
+ *
593
+ * @remarks
594
+ * The check a write repeats between steps. A directory replaced by another
595
+ * directory of the same name answers `false` here, which is the case a path
596
+ * comparison alone cannot see.
597
+ *
598
+ * @example
599
+ * ```ts
600
+ * import { matchesAnchor, readAnchor } from '@orkestrel/scaffold/server'
601
+ *
602
+ * const anchor = readAnchor('/tmp/project')
603
+ * anchor !== undefined && matchesAnchor(anchor) // true while it is untouched
604
+ * ```
605
+ */
606
+ export declare function matchesAnchor(anchor: WriteAnchor): boolean;
607
+
608
+ /**
609
+ * Test whether a destination still holds what was captured of it.
610
+ *
611
+ * @param expectation - The state captured earlier.
612
+ * @returns `true` when re-reading the destination now produces that same state.
613
+ *
614
+ * @remarks
615
+ * Compared field for field against a fresh {@link readExpectation}, so an
616
+ * expectation recorded without a digest matches only a destination that still
617
+ * has no digest to give. That is what keeps the comparison honest in both
618
+ * directions: nothing is treated as satisfied because it was never measured.
619
+ *
620
+ * @example
621
+ * ```ts
622
+ * import { matchesExpectation, readExpectation } from '@orkestrel/scaffold/server'
623
+ *
624
+ * const expectation = readExpectation('/tmp/project/AGENTS.md')
625
+ * expectation !== undefined && matchesExpectation(expectation) // true while untouched
626
+ * ```
627
+ */
628
+ export declare function matchesExpectation(expectation: WriteExpectation): boolean;
629
+
630
+ /**
631
+ * Test whether a path addresses a target's own repository metadata.
632
+ *
633
+ * @param path - The path to classify; either separator is read.
634
+ * @returns `true` for `.git` and for anything beneath it.
635
+ *
636
+ * @remarks
637
+ * The one home of the `.git` membership rule, read from two directions. A target
638
+ * holding nothing but this directory is still vacant, because a checkout of an
639
+ * empty repository is where a fresh workspace legitimately starts. A path
640
+ * beneath it is never removed and never vendored, because deleting a target's
641
+ * history is not a repair.
642
+ *
643
+ * @example
644
+ * ```ts
645
+ * import { matchesGitPath } from '@orkestrel/scaffold/server'
646
+ *
647
+ * matchesGitPath('.git') // true
648
+ * matchesGitPath('.git/config') // true
649
+ * matchesGitPath('.gitignore') // false
650
+ * ```
651
+ */
652
+ export declare function matchesGitPath(path: string): boolean;
653
+
654
+ /**
655
+ * Test whether a caught filesystem error reports an absent path.
656
+ *
657
+ * @param error - The caught value.
658
+ * @returns `true` only for an `Error` whose `code` is exactly `ENOENT`.
659
+ *
660
+ * @remarks
661
+ * The one place absence is told apart from failure. Every read here answers
662
+ * `undefined` or an empty result for a path that is not there and reports a path
663
+ * that is there but unreadable, so the two must never be read from the same
664
+ * caught value by eye. Total for any caught value, including a hostile one.
665
+ *
666
+ * @example
667
+ * ```ts
668
+ * import { matchesMissingPath } from '@orkestrel/scaffold/server'
669
+ *
670
+ * matchesMissingPath(Object.assign(new Error('gone'), { code: 'ENOENT' })) // true
671
+ * matchesMissingPath(new Error('gone')) // false
672
+ * ```
673
+ */
674
+ export declare function matchesMissingPath(error: unknown): boolean;
675
+
676
+ /**
677
+ * Test whether a destination still matches the narrower state a caller observed.
678
+ *
679
+ * @param precondition - The caller-observed state the write is held to.
680
+ * @returns `true` when the destination is absent as stated, or holds a physical
681
+ * file whose bytes digest to the stated value.
682
+ *
683
+ * @remarks
684
+ * Narrower than {@link matchesExpectation} on purpose. A caller observed bytes,
685
+ * not inodes and timestamps, so binding a write to a device identity it never
686
+ * saw would refuse writes that are perfectly safe — a file rewritten to
687
+ * identical bytes by an editor is still the file the caller read. A precondition
688
+ * that states no digest claims presence only.
689
+ *
690
+ * @example
691
+ * ```ts
692
+ * import { matchesPrecondition } from '@orkestrel/scaffold/server'
693
+ *
694
+ * matchesPrecondition({ path: '/tmp/project/new.md', shape: 'absent' }) // true while absent
695
+ * ```
696
+ */
697
+ export declare function matchesPrecondition(precondition: WritePrecondition): boolean;
698
+
699
+ /**
700
+ * Test whether a target-relative path is one no verb may delete.
701
+ *
702
+ * @param path - The target-relative path to classify.
703
+ * @returns `true` when the path must survive every verb this package runs.
704
+ *
705
+ * @remarks
706
+ * The deletion deny-list, stated as a rule over paths rather than as a list of
707
+ * directories. It is the inversion the contract asks for: the candidate set
708
+ * comes from an audit's foreign findings narrowed by what git tracks, and this
709
+ * is what that set is then measured against. Repository metadata is protected
710
+ * because losing history is not a repair, and a target's own `src` and `app`
711
+ * trees are protected because a workspace's source is the one thing scaffold
712
+ * never plans and never owns, whatever an audit reports about it.
713
+ *
714
+ * @example
715
+ * ```ts
716
+ * import { matchesProtectedPath } from '@orkestrel/scaffold/server'
717
+ *
718
+ * matchesProtectedPath('src/core/index.ts') // true
719
+ * matchesProtectedPath('.git/config') // true
720
+ * matchesProtectedPath('.claude/agents/rogue.md') // false
721
+ * ```
722
+ */
723
+ export declare function matchesProtectedPath(path: string): boolean;
724
+
725
+ /**
726
+ * Test whether a path names local configuration or a credential.
727
+ *
728
+ * @param path - The path to classify; either separator is read.
729
+ * @returns `true` when the path must never be copied into a vendored host.
730
+ *
731
+ * @remarks
732
+ * The vendoring deny-list. A host root is staged from a real checkout, so the
733
+ * refusal is stated over the path rather than over the file's content: a
734
+ * credential is recognizable by where it sits and what it is called long before
735
+ * anything reads it. Repository metadata is included through
736
+ * {@link matchesGitPath}, so one call answers the whole question and no caller
737
+ * has to remember to ask twice.
738
+ *
739
+ * @example
740
+ * ```ts
741
+ * import { matchesSensitivePath } from '@orkestrel/scaffold/server'
742
+ *
743
+ * matchesSensitivePath('.npmrc') // true
744
+ * matchesSensitivePath('.claude/settings.local.json') // true
745
+ * matchesSensitivePath('.claude/settings.json') // false
746
+ * ```
747
+ */
748
+ export declare function matchesSensitivePath(path: string): boolean;
749
+
750
+ /**
751
+ * The mutation spine: read the vendored host, re-derive the target, stage, swap.
752
+ *
753
+ * @remarks
754
+ * Every verb runs the same three steps. It snapshots each caller-supplied value
755
+ * and guards the snapshot, so a property backed by an accessor never reaches a
756
+ * decision. It re-derives what it is about to touch and compares that against
757
+ * the observation the caller handed in, refusing the whole call when anything
758
+ * moved. Only then does it open a {@link WriteTransaction}, which stages every
759
+ * byte in a private sibling directory and swaps them into place.
760
+ *
761
+ * The vendored host is read once, at construction, and cross-checked once. Its
762
+ * manifest authenticates its own membership, which is all a checksum sitting
763
+ * beside the data can do; matching that membership against the files actually
764
+ * stored is this class's job. The comparison is exact text and therefore exact
765
+ * case, so a manifest naming `agents.md` for a stored `AGENTS.md` is refused on
766
+ * a case-insensitive filesystem rather than silently resolved.
767
+ *
768
+ * What a mutation guarantees is exactly what {@link WriteTransaction}
769
+ * guarantees, and no more: a caught failure part way through a commit rolls the
770
+ * whole commit back, no destination ever receives half-written bytes, and a
771
+ * process killed mid-commit leaves a mixed target. This is not a journal and
772
+ * does not claim to be one.
773
+ *
774
+ * Every error is emitted on `error` immediately before it is thrown, so an
775
+ * observer sees a refusal even where the caller catches it.
776
+ *
777
+ * @example
778
+ * ```ts
779
+ * import type { Plan } from '@orkestrel/scaffold'
780
+ * import { Materializer } from '@orkestrel/scaffold/server'
781
+ *
782
+ * declare const plan: Plan
783
+ *
784
+ * const materializer = new Materializer({ host: './dist/host' })
785
+ * materializer.materialize(plan, './packages/router')
786
+ * materializer.destroy()
787
+ * ```
788
+ */
789
+ export declare class Materializer implements MaterializerInterface {
790
+ #private;
791
+ /**
792
+ * Construct a materializer over one vendored host root.
793
+ *
794
+ * @param options - The vendored host root, the initial listeners, and the
795
+ * listener-error handler.
796
+ * @throws {@link ScaffoldError} coded `INVALID` when `options` is present but
797
+ * is not an option bag this materializer accepts, and `TARGET` when the host
798
+ * carries a manifest that cannot be read or does not match what it stores.
799
+ *
800
+ * @remarks
801
+ * `host` defaults to this package's own vendored root, resolved from this
802
+ * module's own location so it never depends on the caller's working
803
+ * directory. A host carrying no manifest is read as a raw checkout and every
804
+ * artifact maps onto it one to one.
805
+ *
806
+ * The host is read here rather than on first use, so a broken vendored root
807
+ * fails at construction where the caller can still act on it, and so nothing
808
+ * has to carry a second flag recording whether the read has happened yet.
809
+ */
810
+ constructor(options?: MaterializerOptions);
811
+ /** The materializer's observation channel. */
812
+ get emitter(): EmitterInterface<MaterializerEventMap>;
813
+ /**
814
+ * Compare a plan with a target through the vendored host that will repair it.
815
+ *
816
+ * @param plan - The compiled plan to compare.
817
+ * @param target - The directory to inspect.
818
+ * @returns One finding per hydrated planned path, plus foreign files beneath owned host roots.
819
+ * @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
820
+ * exact shape, `TARGET` when the host or target cannot be read within its
821
+ * bounds, and `DESTROYED` after teardown.
822
+ *
823
+ * @remarks
824
+ * Host directories expand before the target is read, so this method and
825
+ * {@link repair} compare the same paths with the same ownership. Foreign
826
+ * candidates are files beneath those expanded roots only; a root file never
827
+ * becomes a deletion candidate merely because its group is selected.
828
+ */
829
+ audit(plan: Plan, target: string): Audit;
830
+ /**
831
+ * Write a plan into a vacant target.
832
+ *
833
+ * @param plan - The compiled plan to write.
834
+ * @param target - The directory to write into; it must hold nothing the plan would collide with.
835
+ * @returns The paths written and skipped.
836
+ * @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
837
+ * exact shape, `TARGET` when the target is not vacant or the host does not
838
+ * carry a planned artifact, `WRITE` when the write cannot be staged or
839
+ * committed, and `DESTROYED` after teardown.
840
+ *
841
+ * @remarks
842
+ * The plan's own bytes are not trusted: every host-origin artifact is re-read
843
+ * from the vendored root, so what lands is what this package ships rather than
844
+ * what a caller-built plan claimed it ships. A vendored directory expands into
845
+ * one artifact per file beneath it, and a vendored directory holding no file
846
+ * at all is created as an empty directory.
847
+ */
848
+ materialize(plan: Plan, target: string): MaterializeResult;
849
+ /**
850
+ * Write a plan into an existing target, guided by an audit of it.
851
+ *
852
+ * @param plan - The compiled plan to write.
853
+ * @param audit - The preview returned by this materializer's `audit` method.
854
+ * @param target - The directory to write into.
855
+ * @returns The paths written and skipped, each decided by its artifact's ownership.
856
+ * @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
857
+ * exact shape, `TARGET` when the target moved since its audit, `WRITE` when
858
+ * the write cannot be staged or committed, and `DESTROYED` after teardown.
859
+ *
860
+ * @remarks
861
+ * The audit is a preview, not an instruction. The plan is hydrated and
862
+ * compared against the target again here, and the verdicts that produces must
863
+ * match the ones the audit carried for every path the plan owns; anything else
864
+ * means the target moved, and the whole call is refused. A missing destination
865
+ * is restored whatever its ownership; a stale one is replaced only where the
866
+ * artifact claims its bytes, which is what leaves a presence-owned file a
867
+ * consumer has edited exactly as it is.
868
+ */
869
+ repair(plan: Plan, audit: Audit, target: string): MaterializeResult;
870
+ /**
871
+ * Write fetched dependency guides to their local mirrors.
872
+ *
873
+ * @param mirrors - The fetched guides; each carries the local bytes its write is held to.
874
+ * @param target - The directory to write into.
875
+ * @returns The mirror paths written and skipped; a mirror already current is skipped.
876
+ * @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
877
+ * exact shape, `TARGET` when a mirror moved since it was fetched, `WRITE` when
878
+ * the write cannot be staged or committed, and `DESTROYED` after teardown.
879
+ *
880
+ * @remarks
881
+ * A verdict carrying no bytes carries a cause instead, so it is skipped rather
882
+ * than written: one unreachable package never costs the caller the rest of the
883
+ * fetch, and it never empties a mirror it could not replace.
884
+ */
885
+ mirror(mirrors: readonly Mirror[], target: string): MaterializeResult;
886
+ /**
887
+ * Rewrite the marker-bounded package table in the target's catalog agent file.
888
+ *
889
+ * @param entries - The published packages the table should list.
890
+ * @param target - The directory to write into.
891
+ * @returns The catalog path, written when the region's bytes moved and skipped otherwise.
892
+ * @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
893
+ * exact shape, `TARGET` when the file is unreadable or carries no marked
894
+ * region, `WRITE` when the write cannot be staged or committed, and
895
+ * `DESTROYED` after teardown.
896
+ *
897
+ * @remarks
898
+ * Only the text between the two markers is replaced, so every word a consumer
899
+ * wrote around the table survives the call. A row whose lookup produced no
900
+ * version prints the cause it carries instead, because dropping the row would
901
+ * hide a package the organization publishes behind one failed request.
902
+ */
903
+ catalog(entries: readonly CatalogEntry[], target: string): MaterializeResult;
904
+ /**
905
+ * Rewrite the `@orkestrel/*` range set in the target's manifest.
906
+ *
907
+ * @param dependencies - The names and ranges the manifest should declare.
908
+ * @param target - The directory to write into.
909
+ * @returns The manifest path, written when a declared range moved and skipped otherwise.
910
+ * @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
911
+ * exact shape or names a package the manifest does not declare, `TARGET` when
912
+ * the manifest is unreadable, `WRITE` when the write cannot be staged or
913
+ * committed, and `DESTROYED` after teardown.
914
+ *
915
+ * @remarks
916
+ * No other part of the manifest is read back out or rewritten, so a consumer's
917
+ * own description, keywords, scripts, and formatting survive the call. Only a
918
+ * range already declared is rewritten: inserting a package would mean
919
+ * re-serializing the whole manifest, which is exactly the edit this verb
920
+ * promises not to make, so an undeclared name is refused by name instead.
921
+ */
922
+ declare(dependencies: readonly Dependency[], target: string): MaterializeResult;
923
+ /**
924
+ * Delete the files the plan does not own.
925
+ *
926
+ * @param audit - The preview returned by this materializer's `audit` method; its foreign findings are the candidate set.
927
+ * @param repository - The target's git state; only a tracked path is ever deleted.
928
+ * @param target - The directory to delete from.
929
+ * @returns The paths removed.
930
+ * @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
931
+ * exact shape, `TARGET` when the tree carries uncommitted changes or a
932
+ * candidate moved since its audit, `WRITE` when the deletion cannot be staged
933
+ * or committed, and `DESTROYED` after teardown.
934
+ *
935
+ * @remarks
936
+ * The candidate set is re-derived and compared against the audit before
937
+ * anything moves, and every file is quarantined and re-verified rather than
938
+ * unlinked, so a failure part way through restores what it already took. The
939
+ * package's own source and application trees are never candidates, whatever
940
+ * the audit reports, and neither is anything git does not track: git is the
941
+ * recovery mechanism, so a path it cannot restore is not one this verb takes.
942
+ * A tree carrying uncommitted work is refused whole for the same reason.
943
+ */
944
+ remove(audit: Audit, repository: Repository, target: string): MaterializeResult;
945
+ /**
946
+ * Tear the materializer down. Every later call throws, and teardown is idempotent.
947
+ *
948
+ * @returns Nothing.
949
+ *
950
+ * @example
951
+ * ```ts
952
+ * import { Materializer } from '@orkestrel/scaffold/server'
953
+ *
954
+ * const materializer = new Materializer()
955
+ * materializer.destroy()
956
+ * materializer.emitter.destroyed // true
957
+ * ```
958
+ */
959
+ destroy(): void;
960
+ }
961
+
962
+ /**
963
+ * The outcome of one mutation of a target.
964
+ *
965
+ * @remarks
966
+ * `written` names every path this call created or replaced. `skipped` names
967
+ * every path it considered and left alone, whether because the target already
968
+ * matched or because the artifact's ownership forbade touching it. `removed`
969
+ * names every path it deleted.
970
+ */
971
+ export declare interface MaterializeResult {
972
+ readonly target: string;
973
+ readonly written: readonly string[];
974
+ readonly skipped: readonly string[];
975
+ readonly removed: readonly string[];
976
+ }
977
+
978
+ /** The materializer's observation channel. */
979
+ export declare type MaterializerEventMap = {
980
+ readonly write: readonly [path: string];
981
+ readonly remove: readonly [path: string];
982
+ readonly finish: readonly [result: MaterializeResult];
983
+ readonly error: readonly [error: unknown];
984
+ readonly destroy: readonly [];
985
+ };
986
+
987
+ /**
988
+ * The mutation contract: the package's only filesystem writer.
989
+ *
990
+ * @remarks
991
+ * Every method binds to the observation it was given. It re-derives what it is
992
+ * about to touch, compares that against the supplied preview, and refuses the
993
+ * whole call when membership or bytes moved, rather than racing to be the last
994
+ * writer.
995
+ */
996
+ export declare interface MaterializerInterface {
997
+ readonly emitter: EmitterInterface<MaterializerEventMap>;
998
+ /**
999
+ * Compare a plan with a target through the vendored host that will repair it.
1000
+ *
1001
+ * @param plan - The compiled plan to compare.
1002
+ * @param target - The directory to inspect.
1003
+ * @returns One finding per hydrated planned path, plus foreign files beneath owned host roots.
1004
+ */
1005
+ audit(plan: Plan, target: string): Audit;
1006
+ /**
1007
+ * Write a plan into a vacant target.
1008
+ *
1009
+ * @param plan - The compiled plan to write.
1010
+ * @param target - The directory to write into; it must hold nothing the plan would collide with.
1011
+ * @returns The paths written and skipped.
1012
+ */
1013
+ materialize(plan: Plan, target: string): MaterializeResult;
1014
+ /**
1015
+ * Write a plan into an existing target, guided by an audit of it.
1016
+ *
1017
+ * @param plan - The compiled plan to write.
1018
+ * @param audit - The preview returned by this materializer's `audit` method.
1019
+ * @param target - The directory to write into.
1020
+ * @returns The paths written and skipped, each decided by its artifact's ownership.
1021
+ */
1022
+ repair(plan: Plan, audit: Audit, target: string): MaterializeResult;
1023
+ /**
1024
+ * Write fetched dependency guides to their local mirrors.
1025
+ *
1026
+ * @param mirrors - The fetched guides; each carries the local bytes its write is held to.
1027
+ * @param target - The directory to write into.
1028
+ * @returns The mirror paths written and skipped; a mirror already current is skipped.
1029
+ */
1030
+ mirror(mirrors: readonly Mirror[], target: string): MaterializeResult;
1031
+ /**
1032
+ * Rewrite the marker-bounded package table in the target's catalog agent file.
1033
+ *
1034
+ * @param entries - The published packages the table should list.
1035
+ * @param target - The directory to write into.
1036
+ * @returns The catalog path, written when the region's bytes moved and skipped otherwise.
1037
+ */
1038
+ catalog(entries: readonly CatalogEntry[], target: string): MaterializeResult;
1039
+ /**
1040
+ * Rewrite the `@orkestrel/*` range set in the target's manifest.
1041
+ *
1042
+ * @param dependencies - The names and ranges the manifest should declare.
1043
+ * @param target - The directory to write into.
1044
+ * @returns The manifest path, written when a declared range moved and skipped otherwise.
1045
+ *
1046
+ * @remarks
1047
+ * No other part of the manifest is read back out or rewritten, so a
1048
+ * consumer's own description, keywords, and scripts survive the call.
1049
+ */
1050
+ declare(dependencies: readonly Dependency[], target: string): MaterializeResult;
1051
+ /**
1052
+ * Delete the files the plan does not own.
1053
+ *
1054
+ * @param audit - The preview returned by this materializer's `audit` method; its foreign findings are the candidate set.
1055
+ * @param repository - The target's git state; only a tracked path is ever deleted.
1056
+ * @param target - The directory to delete from.
1057
+ * @returns The paths removed.
1058
+ *
1059
+ * @remarks
1060
+ * The candidate set is re-derived and compared against the audit before
1061
+ * anything moves, and every file is quarantined and re-verified rather than
1062
+ * unlinked, so a failure part way through restores what it already took.
1063
+ * The package's own source and application trees are never candidates,
1064
+ * whatever the audit reports.
1065
+ */
1066
+ remove(audit: Audit, repository: Repository, target: string): MaterializeResult;
1067
+ /**
1068
+ * Tear the materializer down. Every later call throws, and teardown is idempotent.
1069
+ *
1070
+ * @returns Nothing.
1071
+ */
1072
+ destroy(): void;
1073
+ }
1074
+
1075
+ /**
1076
+ * Options for the materializer.
1077
+ *
1078
+ * @remarks
1079
+ * `host` is the vendored data root host-origin artifacts are copied from. It
1080
+ * defaults to this package's own vendored root, resolved from the installed
1081
+ * module's location rather than the caller's working directory. A host that
1082
+ * carries no manifest beside it maps artifact paths one to one instead of
1083
+ * through the manifest.
1084
+ */
1085
+ export declare interface MaterializerOptions {
1086
+ readonly host?: string;
1087
+ readonly on?: EmitterHooks<MaterializerEventMap>;
1088
+ readonly error?: EmitterErrorHandler;
1089
+ }
1090
+
1091
+ /** Maximum characters one guide branch may carry. */
1092
+ export declare const MAX_BRANCH_LENGTH = 255;
1093
+
1094
+ /** Maximum characters one caller-supplied upstream endpoint may carry. */
1095
+ export declare const MAX_ENDPOINT_LENGTH = 2048;
1096
+
1097
+ /**
1098
+ * Maximum paths one target's working-tree inventory may report.
1099
+ *
1100
+ * @remarks
1101
+ * Far above the core collection ceiling, and deliberately so. A tracked or dirty
1102
+ * path list is the target repository's own fact rather than an argument a caller
1103
+ * authored, so measuring it against the ceiling that bounds a public collection
1104
+ * would read a legitimately large checkout as hostile and refuse the deletion
1105
+ * verb on it.
1106
+ */
1107
+ export declare const MAX_INVENTORY_PATHS = 100000;
1108
+
1109
+ /**
1110
+ * Maximum segments one host path may carry.
1111
+ *
1112
+ * @remarks
1113
+ * Bounds the work a path decision costs before any filesystem call is made. With
1114
+ * {@link MAX_PATH_SEGMENT_BYTES} it is also the real length ceiling of an
1115
+ * accepted path, well inside the core character ceiling.
1116
+ */
1117
+ export declare const MAX_PATH_DEPTH = 64;
1118
+
1119
+ /**
1120
+ * Maximum UTF-8 bytes one host path segment may encode to.
1121
+ *
1122
+ * @remarks
1123
+ * The limit every supported filesystem shares for a single name. It is a byte
1124
+ * count rather than a character count, because that is the unit the filesystem
1125
+ * imposes it in: eighty-six three-byte characters are already past it.
1126
+ */
1127
+ export declare const MAX_PATH_SEGMENT_BYTES = 255;
1128
+
1129
+ /**
1130
+ * Maximum simultaneous upstream requests.
1131
+ *
1132
+ * @remarks
1133
+ * A ceiling rather than a default: the reader picks what it opens by, and this
1134
+ * is only what a caller may raise it to.
1135
+ */
1136
+ export declare const MAX_UPSTREAM_CONCURRENCY = 64;
1137
+
1138
+ /** Maximum retries one upstream request may be given after a transport fault. */
1139
+ export declare const MAX_UPSTREAM_RETRIES = 5;
1140
+
1141
+ /** Maximum timeout one upstream request may be given, in milliseconds. */
1142
+ export declare const MAX_UPSTREAM_TIMEOUT = 300000;
1143
+
1144
+ /**
1145
+ * Project a target-relative path to the storage name a vendored host holds it under.
1146
+ *
1147
+ * @param path - The target-relative path the file is written to.
1148
+ * @returns The storage name beneath the host root.
1149
+ *
1150
+ * @remarks
1151
+ * A staged host is a plain directory that npm packs, and npm's own ignore rules
1152
+ * would drop a leading-dot entry from the tarball. So every dot that opens a
1153
+ * segment comes off, and a dotted file at the root moves under `dotfiles/` to
1154
+ * keep it from colliding with an undotted sibling of the same name. The mapping
1155
+ * is one direction only: a staged host's manifest records the destination each
1156
+ * storage name answers for, so the reader never re-derives this.
1157
+ *
1158
+ * @example
1159
+ * ```ts
1160
+ * import { pathToStorage } from '@orkestrel/scaffold/server'
1161
+ *
1162
+ * pathToStorage('.gitignore') // 'dotfiles/gitignore'
1163
+ * pathToStorage('.claude/rules/names.md') // 'claude/rules/names.md'
1164
+ * pathToStorage('AGENTS.md') // 'AGENTS.md'
1165
+ * ```
1166
+ */
1167
+ export declare function pathToStorage(path: string): string;
1168
+
1169
+ /**
1170
+ * Capture one directory's physical identity.
1171
+ *
1172
+ * @param path - The resolved directory path to capture.
1173
+ * @returns The anchor, or `undefined` when the path is not a physical directory.
1174
+ *
1175
+ * @remarks
1176
+ * Device and inode rather than the path, because the path is the thing that can
1177
+ * be swapped underneath a write. An anchor captured before a mutation and
1178
+ * checked again after it is what proves the directory written into is the
1179
+ * directory that was inspected.
1180
+ *
1181
+ * @example
1182
+ * ```ts
1183
+ * import { readAnchor } from '@orkestrel/scaffold/server'
1184
+ *
1185
+ * readAnchor('/tmp/project') // { path: '/tmp/project', device: 1, inode: 2 }
1186
+ * ```
1187
+ */
1188
+ export declare function readAnchor(path: string): WriteAnchor | undefined;
1189
+
1190
+ /**
1191
+ * Capture what one destination holds before a write.
1192
+ *
1193
+ * @param path - The resolved destination path to capture.
1194
+ * @returns The expectation, or `undefined` when the destination is a link or a
1195
+ * shape this package will not write over.
1196
+ *
1197
+ * @remarks
1198
+ * Absence is a captured state rather than a failure, because most writes expect
1199
+ * exactly that. Each shape carries only the facts it supplies: a directory
1200
+ * carries its identity, a file carries its identity, size, and bytes, and an
1201
+ * absent destination carries nothing at all. A file past the artifact ceiling
1202
+ * carries no digest and is bound by its identity, size, and modification time
1203
+ * alone, which is the strongest honest claim about bytes nobody read.
1204
+ *
1205
+ * @example
1206
+ * ```ts
1207
+ * import { readExpectation } from '@orkestrel/scaffold/server'
1208
+ *
1209
+ * readExpectation('/tmp/project/absent.md') // { path: …, shape: 'absent' }
1210
+ * ```
1211
+ */
1212
+ export declare function readExpectation(path: string): WriteExpectation | undefined;
1213
+
1214
+ /**
1215
+ * Read one contained file as its exact bytes in lowercase hexadecimal.
1216
+ *
1217
+ * @param root - The containing host directory.
1218
+ * @param path - The portable root-relative file path.
1219
+ * @param limit - The most bytes this read accepts; the artifact ceiling by default.
1220
+ * @returns The exact bytes as hexadecimal, or `undefined` when the file is
1221
+ * absent, is not a physical readable file, is past `limit`, or moved while it
1222
+ * was being read.
1223
+ * @throws `ScaffoldError('INVALID', …)` when the arguments are off contract or
1224
+ * `path` leaves `root`.
1225
+ *
1226
+ * @remarks
1227
+ * Hexadecimal rather than text, because this is what a byte comparison is stated
1228
+ * in everywhere in this package: a plan's artifact, an audit finding, and a
1229
+ * snapshot all compare as the same digits. The file's identity and size are
1230
+ * measured before and after the read, and one extra byte is requested past the
1231
+ * declared size, so a file that grew or was replaced mid-read answers
1232
+ * `undefined` rather than half of two files.
1233
+ *
1234
+ * @example
1235
+ * ```ts
1236
+ * import { readFileHex } from '@orkestrel/scaffold/server'
1237
+ *
1238
+ * readFileHex('/tmp/project', 'AGENTS.md') // '2320416765…'
1239
+ * ```
1240
+ */
1241
+ export declare function readFileHex(root: string, path: string, limit?: number): string | undefined;
1242
+
1243
+ /**
1244
+ * Read one contained file as bounded UTF-8 text.
1245
+ *
1246
+ * @param root - The containing host directory.
1247
+ * @param path - The portable root-relative file path.
1248
+ * @param limit - The most bytes this read accepts; the artifact ceiling by default.
1249
+ * @returns The decoded text, or `undefined` when {@link readFileHex} answers
1250
+ * nothing or the bytes are not valid UTF-8.
1251
+ * @throws `ScaffoldError('INVALID', …)` when the arguments are off contract or
1252
+ * `path` leaves `root`.
1253
+ *
1254
+ * @remarks
1255
+ * Decoding is strict, so a file carrying an invalid sequence answers `undefined`
1256
+ * rather than text carrying replacement characters. That matters because the
1257
+ * text is parsed next: a manifest silently repaired into valid JSON by lossy
1258
+ * decoding would be trusted.
1259
+ *
1260
+ * @example
1261
+ * ```ts
1262
+ * import { readFileText } from '@orkestrel/scaffold/server'
1263
+ *
1264
+ * readFileText('/tmp/project', 'package.json') // '{ "name": "@orkestrel/router", … }'
1265
+ * ```
1266
+ */
1267
+ export declare function readFileText(root: string, path: string, limit?: number): string | undefined;
1268
+
1269
+ /**
1270
+ * Read a vendored host's manifest, when it carries one.
1271
+ *
1272
+ * @param host - The vendored host root to read.
1273
+ * @returns The manifest, or `undefined` when the host carries none.
1274
+ * @throws `ScaffoldError('INVALID', …)` when `host` is not a host path.
1275
+ * @throws `ScaffoldError('TARGET', …)` when the manifest is there but cannot be
1276
+ * read, is not the declared shape, or does not match its own membership.
1277
+ *
1278
+ * @remarks
1279
+ * The two failures are held apart deliberately. A host with no manifest is a
1280
+ * raw checkout, and a caller reads it by mapping each path one to one. A host
1281
+ * with a manifest that does not verify is a staged host that has been edited,
1282
+ * and answering `undefined` there would degrade it to that same one-to-one
1283
+ * mapping — which is how an edited manifest would get a caller to read files it
1284
+ * never declared. So absence answers and corruption throws.
1285
+ *
1286
+ * Verification here is the manifest's own self-consistency: the digest against
1287
+ * the exact membership beside it. Whether that membership matches the files
1288
+ * actually stored is a separate question, and it belongs to the reader that
1289
+ * walks the host.
1290
+ *
1291
+ * @example
1292
+ * ```ts
1293
+ * import { readHostManifest } from '@orkestrel/scaffold/server'
1294
+ *
1295
+ * readHostManifest('./dist/host') // the manifest, or undefined for a raw root
1296
+ * ```
1297
+ */
1298
+ export declare function readHostManifest(host: string): HostManifest | undefined;
1299
+
1300
+ /**
1301
+ * Derive one vendored-host manifest entry from a file in a checkout.
1302
+ *
1303
+ * @param destination - The target-relative path the file is written to.
1304
+ * @param source - The resolved host path the bytes are read from.
1305
+ * @returns The entry, or `undefined` when `source` is not a physical file this
1306
+ * package will vendor or carries more bytes than one artifact may.
1307
+ *
1308
+ * @remarks
1309
+ * The one place the three declared fields are decided together, because they are
1310
+ * three readings of one file: {@link pathToStorage} decides where it is stored,
1311
+ * the destination is the path it answers for, and the executable bit is read
1312
+ * from the source's own mode.
1313
+ *
1314
+ * That mode is the honest limit of this reading. A Windows host reports no
1315
+ * executable bit at all, so a host staged there declares every entry
1316
+ * non-executable and a consumer receives scripts without it. Staging on a POSIX
1317
+ * host is what carries the bit through.
1318
+ *
1319
+ * @example
1320
+ * ```ts
1321
+ * import { readManifestEntry } from '@orkestrel/scaffold/server'
1322
+ *
1323
+ * readManifestEntry('.gitignore', '/tmp/checkout/.gitignore')
1324
+ * // { storage: 'dotfiles/gitignore', destination: '.gitignore', executable: false }
1325
+ * ```
1326
+ */
1327
+ export declare function readManifestEntry(destination: string, source: string): ManifestEntry | undefined;
1328
+
1329
+ /**
1330
+ * Read a target's current bytes at the paths a plan claims.
1331
+ *
1332
+ * @param target - The target directory to read.
1333
+ * @param paths - The plan-relative paths to probe.
1334
+ * @returns One entry per path that is there: a file maps to its exact bytes as
1335
+ * hexadecimal and a directory maps to `''`, which records presence with no bytes
1336
+ * to compare. An absent path is omitted.
1337
+ * @throws `ScaffoldError('INVALID', …)` when `target` is not a host path or
1338
+ * `paths` is not a bounded list of plannable paths.
1339
+ * @throws `ScaffoldError('TARGET', …)` when a path that is there cannot be read,
1340
+ * or when the whole read would retain more bytes than one plan may.
1341
+ *
1342
+ * @remarks
1343
+ * The one door from a real directory into the vocabulary an audit compares in.
1344
+ * Absence is omission rather than an empty value, because core reads a missing
1345
+ * key as a missing destination and an empty string as a present directory; the
1346
+ * two are different verdicts. A path that is there but unreadable throws instead
1347
+ * of being omitted, because omission would report it as missing and a repair
1348
+ * would then overwrite whatever is actually sitting there.
1349
+ *
1350
+ * @example
1351
+ * ```ts
1352
+ * import { readSnapshot } from '@orkestrel/scaffold/server'
1353
+ *
1354
+ * readSnapshot('./packages/router', ['package.json', 'guides'])
1355
+ * // { 'package.json': '7b226e…', guides: '' }
1356
+ * ```
1357
+ */
1358
+ export declare function readSnapshot(target: string, paths: readonly string[]): Snapshot;
1359
+
1360
+ /**
1361
+ * What git reports about a target's working tree.
1362
+ *
1363
+ * @remarks
1364
+ * `tracked` is the only set a deletion may draw from: git does not report the
1365
+ * loss of an untracked path and `git diff` cannot restore it, so an ignored
1366
+ * path such as an installed dependency tree, a build output, or an editor
1367
+ * directory survives every verb. `dirty` is every path carrying an uncommitted
1368
+ * change, taken repo-wide rather than over a write set, because deletion makes
1369
+ * the write set the whole workspace. A clean tree is an empty `dirty`. A target
1370
+ * that is not a git repository yields no `Repository` at all, so the caller
1371
+ * decides what to do about that rather than reading it out of an invented
1372
+ * empty value.
1373
+ */
1374
+ export declare interface Repository {
1375
+ readonly tracked: readonly string[];
1376
+ readonly dirty: readonly string[];
1377
+ }
1378
+
1379
+ /**
1380
+ * The Windows device names that stay reserved even when an extension follows.
1381
+ *
1382
+ * @remarks
1383
+ * Refused on every host rather than only on Windows. A generated workspace is
1384
+ * checked out on all of them, so a directory this package writes on Linux must
1385
+ * still be a name Windows can hold.
1386
+ */
1387
+ export declare const RESERVED_SEGMENT_PATTERN: RegExp;
1388
+
1389
+ /**
1390
+ * Resolve a root-relative path and refuse one that leaves its root.
1391
+ *
1392
+ * @param root - The containing host directory.
1393
+ * @param path - The portable root-relative path.
1394
+ * @returns The destination as this package will address it, or `undefined` when
1395
+ * either argument is off contract or the destination lies outside `root`.
1396
+ *
1397
+ * @remarks
1398
+ * The containment law, and the one door every read in this module goes through.
1399
+ * Both sides are resolved through the real filesystem before they are compared,
1400
+ * so a link planted inside the root cannot smuggle a destination out of it; the
1401
+ * answer is then the lexical join, so the caller operates on the path it named
1402
+ * rather than on a resolved form the target may not recognize.
1403
+ *
1404
+ * Comparison is exact text, which fails closed on a case-insensitive
1405
+ * filesystem: a root and a path spelled with different case resolve to
1406
+ * different strings there and are refused, never wrongly admitted.
1407
+ *
1408
+ * @example
1409
+ * ```ts
1410
+ * import { resolveContainedPath } from '@orkestrel/scaffold/server'
1411
+ *
1412
+ * resolveContainedPath('/tmp/project', 'guides/router.md') // '/tmp/project/guides/router.md'
1413
+ * resolveContainedPath('/tmp/project', '../secrets') // undefined
1414
+ * ```
1415
+ */
1416
+ export declare function resolveContainedPath(root: string, path: string): string | undefined;
1417
+
1418
+ /**
1419
+ * Resolve a path through the real filesystem, keeping the part that does not exist yet.
1420
+ *
1421
+ * @param path - The absolute or relative host path to resolve.
1422
+ * @returns The path with its existing prefix resolved through every link, or
1423
+ * `undefined` when the text is not a host path, no bounded existing ancestor
1424
+ * resolves, or an ancestor cannot be read.
1425
+ *
1426
+ * @remarks
1427
+ * A containment decision has to be made about a destination that does not exist
1428
+ * yet, and a lexical answer is not enough: a link anywhere in the existing
1429
+ * prefix moves the destination somewhere the text never named. So the deepest
1430
+ * existing ancestor is resolved and the remaining segments are re-joined onto
1431
+ * it. The climb is bounded by the path-depth ceiling, so an adversarial path
1432
+ * cannot make it walk indefinitely.
1433
+ *
1434
+ * @example
1435
+ * ```ts
1436
+ * import { resolveRealPath } from '@orkestrel/scaffold/server'
1437
+ *
1438
+ * resolveRealPath('./packages/new/src') // the real path of `packages`, plus `new/src`
1439
+ * ```
1440
+ */
1441
+ export declare function resolveRealPath(path: string): string | undefined;
1442
+
1443
+ /**
1444
+ * Stage a vendored host root from a real checkout.
1445
+ *
1446
+ * @param checkout - The checkout the vendored paths are read from.
1447
+ * @param host - The vendored host root to fill; it must be absent or empty.
1448
+ * @returns One entry per staged file, sorted by storage name.
1449
+ * @throws `ScaffoldError('INVALID', …)` when either argument is not a host path
1450
+ * or a vendored path leaves the checkout or the host root.
1451
+ * @throws `ScaffoldError('TARGET', …)` when the checkout is not a directory, the
1452
+ * host root is not vacant, the checkout does not carry every vendored path, two
1453
+ * vendored files claim one storage name, a vendored file is not a plain file
1454
+ * within the artifact ceiling, or the staged manifest does not read back.
1455
+ * @throws `ScaffoldError('WRITE', …)` when a staged file or the manifest cannot
1456
+ * be written.
1457
+ *
1458
+ * @remarks
1459
+ * This is the producer half of the vendored host, and it is not the mutation
1460
+ * contract `MaterializerInterface` states. That contract owns **target**
1461
+ * writes: it materializes a compiled plan into a consumer's workspace, binds
1462
+ * every destination to what the caller observed, and rolls a failed commit back.
1463
+ * This reads this package's own checkout at build time and fills its own build
1464
+ * output. Different direction, different lifetime, no consumer target involved,
1465
+ * so the two do not overlap and neither one belongs inside the other.
1466
+ *
1467
+ * Staging is plain rather than transactional for the same reason. A
1468
+ * `WriteTransaction` exists to hold a directory that already holds work
1469
+ * still; a build output holds nothing, is deleted whole before every build, and
1470
+ * has no concurrent reader. What replaces it is refusing early and ordering the
1471
+ * writes: the whole membership is derived before anything is created, so a
1472
+ * checkout this refuses leaves no host root at all, and `manifest.json` is
1473
+ * written last, so a stage that failed part way through leaves a root every
1474
+ * reader treats as a raw checkout and fails loudly on.
1475
+ *
1476
+ * A missing vendored path is refused rather than staged around. A partial root
1477
+ * is not detectably partial: it fails later, in a consumer's terminal, on
1478
+ * whichever path the plan reached first. Refusing here fails the build that
1479
+ * produced it, where the maintainer can act, and it names every missing path at
1480
+ * once. A directory is the same case — declaring an absent directory as an empty
1481
+ * root would create an empty directory in every generated workspace.
1482
+ *
1483
+ * The vendoring deny-list applies to what the walk discovers beneath a vendored
1484
+ * directory, where a maintainer's local credential can legitimately sit, and
1485
+ * such a path is skipped. A path `HOST_PATHS` names itself is curated data
1486
+ * rather than discovery, so it is staged or the stage is refused.
1487
+ *
1488
+ * @example
1489
+ * ```ts
1490
+ * import { stageHost } from '@orkestrel/scaffold/server'
1491
+ *
1492
+ * stageHost(process.cwd(), 'dist/host').length // the files staged
1493
+ * ```
1494
+ */
1495
+ export declare function stageHost(checkout: string, host: string): readonly ManifestEntry[];
1496
+
1497
+ /**
1498
+ * The reading spine: one bounded, unauthenticated, redirect-free request per answer.
1499
+ *
1500
+ * @remarks
1501
+ * This is the package's only network reader, and it never writes. Every call
1502
+ * opens one byte allowance and spends it across every read the call makes, so a
1503
+ * caller is bounded twice over: `limit` refuses one oversized answer and
1504
+ * `budget` refuses many small ones. The two are separate exhaustion routes and
1505
+ * neither bound covers the other.
1506
+ *
1507
+ * A per-package failure never escapes as a throw. It is projected into the
1508
+ * verdict that package's row already carries — `missing` for an upstream `404`,
1509
+ * which is a definite answer, and `failed` for a transport fault, which is no
1510
+ * answer at all — so one unreachable package never costs the caller the rest of
1511
+ * the answer. The organization package list is the one exception: without it
1512
+ * there is no fleet to report, so an unreachable or malformed list is a coded
1513
+ * `FETCH` failure.
1514
+ *
1515
+ * Requests are unauthenticated because every fleet repository is public, and
1516
+ * they follow no redirect, so a misconfigured or hostile endpoint cannot move a
1517
+ * read to another host. Each one is bounded by its endpoint's timeout and by the
1518
+ * reader's own abort signal, so {@link Upstream.destroy} cancels what is in
1519
+ * flight instead of waiting for it.
1520
+ *
1521
+ * The allowance is threaded through the private reads as a mutable
1522
+ * `{ remaining: number }` carrier rather than held on the instance, because
1523
+ * concurrent calls each own their own budget and must not spend each other's.
1524
+ *
1525
+ * @example
1526
+ * ```ts
1527
+ * import { Upstream } from '@orkestrel/scaffold/server'
1528
+ *
1529
+ * const upstream = new Upstream({ registry: { timeout: 5_000 } })
1530
+ * const releases = await upstream.lookup([{ name: '@orkestrel/emitter', range: '^0.0.5' }])
1531
+ * upstream.destroy()
1532
+ * ```
1533
+ */
1534
+ export declare class Upstream implements UpstreamInterface {
1535
+ #private;
1536
+ /**
1537
+ * Construct a reader over one guide host and one registry.
1538
+ *
1539
+ * @param options - The two endpoints, the request bounds, the initial
1540
+ * listeners, and the listener-error handler.
1541
+ * @throws {@link ScaffoldError} coded `INVALID` when `options` is present but
1542
+ * is not an option bag this reader accepts, or when either endpoint names a
1543
+ * scheme, host, or form this reader will not request.
1544
+ *
1545
+ * @remarks
1546
+ * `isEndpoint` bounds an endpoint's length and nothing else, so the scheme and
1547
+ * host law is settled here, where a refusal can say which endpoint was refused
1548
+ * and why. An endpoint must be HTTPS, or HTTP to a loopback host — the one
1549
+ * place an unencrypted request has no network between the two ends. That
1550
+ * refuses `file:`, `data:`, and plain HTTP to a real host, and it is what
1551
+ * keeps a fixture reachable without weakening transport security anywhere a
1552
+ * real request goes. An endpoint carrying credentials, a query, or a fragment
1553
+ * is refused too: this reader authenticates nothing and appends its own path.
1554
+ */
1555
+ constructor(options?: UpstreamOptions);
1556
+ /** The upstream reader's observation channel. */
1557
+ get emitter(): EmitterInterface<UpstreamEventMap>;
1558
+ /**
1559
+ * Look up the registry's latest release for each declared dependency.
1560
+ *
1561
+ * @param dependencies - The declared dependencies to look up.
1562
+ * @returns One release verdict per dependency, in input order.
1563
+ * @throws {@link ScaffoldError} coded `INVALID` when `dependencies` is not a
1564
+ * bounded list of declared dependencies, and `DESTROYED` when the reader is
1565
+ * torn down before or during the call.
1566
+ *
1567
+ * @remarks
1568
+ * Whether the declared range already admits the reported version is not
1569
+ * decided here and is not stored on the verdict: it is a function of the
1570
+ * `range` and `latest` sitting beside each other, and one centralized helper
1571
+ * answers it for every caller.
1572
+ *
1573
+ * @example
1574
+ * ```ts
1575
+ * import { Upstream } from '@orkestrel/scaffold/server'
1576
+ *
1577
+ * const upstream = new Upstream()
1578
+ * await upstream.lookup([{ name: '@orkestrel/router', range: '^0.0.8' }])
1579
+ * upstream.destroy()
1580
+ * ```
1581
+ */
1582
+ lookup(dependencies: readonly Dependency[]): Promise<readonly Release[]>;
1583
+ /**
1584
+ * Fetch each named package's guide, beside the local mirror it answers for.
1585
+ *
1586
+ * @param names - The packages to fetch: the target's declared set, or the whole organization.
1587
+ * @param current - The target's local mirrors as exact bytes, keyed by mirror path.
1588
+ * @returns One mirror verdict per name, in input order.
1589
+ * @throws {@link ScaffoldError} coded `INVALID` when `names` is not a bounded
1590
+ * list of fleet package names or `current` is not a snapshot, and `DESTROYED`
1591
+ * when the reader is torn down before or during the call.
1592
+ *
1593
+ * @remarks
1594
+ * The mirror path is derived from the package name by the same helper the plan
1595
+ * derives it with, and the fetched URL ends in that exact path, so a verdict
1596
+ * always answers for the file it names. `observed` is carried through from
1597
+ * `current` whatever the lookup produced, because it is the precondition the
1598
+ * later write is held to and a failed fetch does not change what the target
1599
+ * currently holds.
1600
+ *
1601
+ * @example
1602
+ * ```ts
1603
+ * import { Upstream } from '@orkestrel/scaffold/server'
1604
+ *
1605
+ * const upstream = new Upstream()
1606
+ * await upstream.fetch(['@orkestrel/router'], { 'guides/router.md': '2320526f75746572' })
1607
+ * upstream.destroy()
1608
+ * ```
1609
+ */
1610
+ fetch(names: readonly string[], current: Snapshot): Promise<readonly Mirror[]>;
1611
+ /**
1612
+ * Catalog the published fleet from the registry's organization package list.
1613
+ *
1614
+ * @returns One row per published package, sorted by name.
1615
+ * @throws {@link ScaffoldError} coded `FETCH` when the organization package
1616
+ * list is unreachable, malformed, empty, or larger than one bounded
1617
+ * collection, and `DESTROYED` when the reader is torn down before or during
1618
+ * the call.
1619
+ *
1620
+ * @remarks
1621
+ * The organization list is the exact membership the registry publishes, not a
1622
+ * relevance search, and it is the whole reason this answer can claim to be the
1623
+ * fleet. So it fails hard where a per-package lookup fails soft: an empty or
1624
+ * unreadable list would otherwise be written into a target's catalog table as
1625
+ * a fleet with no packages in it. A package whose own version lookup then
1626
+ * fails still keeps its row, carrying the cause instead of a version, because
1627
+ * the list already proved the package is published.
1628
+ *
1629
+ * @example
1630
+ * ```ts
1631
+ * import { Upstream } from '@orkestrel/scaffold/server'
1632
+ *
1633
+ * const upstream = new Upstream()
1634
+ * const entries = await upstream.catalog()
1635
+ * upstream.destroy()
1636
+ * ```
1637
+ */
1638
+ catalog(): Promise<readonly CatalogEntry[]>;
1639
+ /**
1640
+ * Tear the reader down, aborting every request in flight. Teardown is idempotent.
1641
+ *
1642
+ * @returns Nothing.
1643
+ *
1644
+ * @remarks
1645
+ * A call still in flight rejects with a `DESTROYED` error rather than
1646
+ * resolving to a partial answer, because half a fleet reads exactly like a
1647
+ * whole one.
1648
+ *
1649
+ * @example
1650
+ * ```ts
1651
+ * import { Upstream } from '@orkestrel/scaffold/server'
1652
+ *
1653
+ * const upstream = new Upstream()
1654
+ * upstream.destroy()
1655
+ * upstream.emitter.destroyed // true
1656
+ * ```
1657
+ */
1658
+ destroy(): void;
1659
+ }
1660
+
1661
+ /**
1662
+ * The upstream reader's observation channel.
1663
+ *
1664
+ * @remarks
1665
+ * Each verdict is published whole rather than as a name beside a summary, so a
1666
+ * listener reads the same value the call returns and a failed lookup is told
1667
+ * apart from a successful one by the verdict's own discriminant rather than by
1668
+ * which event carried it.
1669
+ */
1670
+ export declare type UpstreamEventMap = {
1671
+ readonly release: readonly [release: Release];
1672
+ readonly mirror: readonly [mirror: Mirror];
1673
+ readonly error: readonly [error: unknown];
1674
+ readonly destroy: readonly [];
1675
+ };
1676
+
1677
+ /**
1678
+ * The upstream contract: the package's only network reader, and it never writes.
1679
+ *
1680
+ * @remarks
1681
+ * A per-package failure is collected as a verdict carrying its cause, not
1682
+ * thrown, so one unreachable package never costs the caller the rest of the
1683
+ * answer. The organization list is the exception: without it there is no fleet
1684
+ * to report, so an unreachable or malformed list is a coded failure.
1685
+ */
1686
+ export declare interface UpstreamInterface {
1687
+ readonly emitter: EmitterInterface<UpstreamEventMap>;
1688
+ /**
1689
+ * Look up the registry's latest release for each declared dependency.
1690
+ *
1691
+ * @param dependencies - The declared dependencies to look up.
1692
+ * @returns One release verdict per dependency, in input order.
1693
+ */
1694
+ lookup(dependencies: readonly Dependency[]): Promise<readonly Release[]>;
1695
+ /**
1696
+ * Fetch each named package's guide, beside the local mirror it answers for.
1697
+ *
1698
+ * @param names - The packages to fetch: the target's declared set, or the whole organization.
1699
+ * @param current - The target's local mirrors as exact bytes, keyed by mirror path.
1700
+ * @returns One mirror verdict per name, in input order.
1701
+ */
1702
+ fetch(names: readonly string[], current: Snapshot): Promise<readonly Mirror[]>;
1703
+ /**
1704
+ * Catalog the published fleet from the registry's organization package list.
1705
+ *
1706
+ * @returns One row per published package, sorted by name.
1707
+ */
1708
+ catalog(): Promise<readonly CatalogEntry[]>;
1709
+ /**
1710
+ * Tear the reader down, aborting every request in flight. Teardown is idempotent.
1711
+ *
1712
+ * @returns Nothing.
1713
+ */
1714
+ destroy(): void;
1715
+ }
1716
+
1717
+ /**
1718
+ * Options for the upstream reader.
1719
+ *
1720
+ * @remarks
1721
+ * The two endpoints are grouped under the entity each configures: `guides`
1722
+ * takes the guide host's `base`, its `branch`, and its `timeout`; `registry`
1723
+ * takes the registry's `base` and `timeout`. `concurrency` bounds requests in
1724
+ * flight and `retries` opts into per-request retry on a transport fault.
1725
+ * `limit` bounds the bytes read from one response body and `budget` bounds the
1726
+ * bytes read across a whole call, so neither one oversized answer nor many
1727
+ * small ones can exhaust the caller. Every request is unauthenticated and
1728
+ * follows no redirect.
1729
+ */
1730
+ export declare interface UpstreamOptions {
1731
+ readonly guides?: {
1732
+ readonly base?: string;
1733
+ readonly branch?: string;
1734
+ readonly timeout?: number;
1735
+ };
1736
+ readonly registry?: {
1737
+ readonly base?: string;
1738
+ readonly timeout?: number;
1739
+ };
1740
+ readonly concurrency?: number;
1741
+ readonly retries?: number;
1742
+ readonly limit?: number;
1743
+ readonly budget?: number;
1744
+ readonly on?: EmitterHooks<UpstreamEventMap>;
1745
+ readonly error?: EmitterErrorHandler;
1746
+ }
1747
+
1748
+ /** One physical directory identity captured across a write transaction. */
1749
+ export declare interface WriteAnchor {
1750
+ readonly path: string;
1751
+ readonly device: number;
1752
+ readonly inode: number;
1753
+ }
1754
+
1755
+ /** The final directory anchor of a write transaction and the subset one call created. */
1756
+ export declare interface WriteDirectoryResult {
1757
+ readonly anchor: WriteAnchor;
1758
+ readonly created: readonly WriteAnchor[];
1759
+ }
1760
+
1761
+ /**
1762
+ * One destination snapshot captured before a write and required to survive it.
1763
+ *
1764
+ * @remarks
1765
+ * `device`, `inode`, `modified`, `size`, and `digest` are present only where
1766
+ * the observed shape supplies them.
1767
+ */
1768
+ export declare interface WriteExpectation {
1769
+ readonly path: string;
1770
+ readonly shape: 'absent' | 'file' | 'directory';
1771
+ readonly device?: number;
1772
+ readonly inode?: number;
1773
+ readonly modified?: number;
1774
+ readonly size?: number;
1775
+ readonly digest?: string;
1776
+ }
1777
+
1778
+ /** The narrower caller-observed destination state a write transaction must still match. */
1779
+ export declare interface WritePrecondition {
1780
+ readonly path: string;
1781
+ readonly shape: 'absent' | 'file';
1782
+ readonly digest?: string;
1783
+ }
1784
+
1785
+ /**
1786
+ * One staged, reversible mutation of one target directory.
1787
+ *
1788
+ * @remarks
1789
+ * The transaction owns a private root beside the target — a sibling directory on
1790
+ * the same volume, so every promotion is a rename rather than a copy. Staging
1791
+ * writes go into that root and nothing else, so a call that fails while staging
1792
+ * has not touched the target at all. Commit is the only step that mutates the
1793
+ * target, and it is the only step that can need rolling back.
1794
+ *
1795
+ * Two bindings hold a destination still. A **precondition** is what the caller
1796
+ * observed earlier and is checked once, at construction, so a target that moved
1797
+ * between the caller's read and this transaction fails before anything is
1798
+ * created. An **expectation** is captured here, at construction, and re-checked
1799
+ * at commit, so a target that moves while the write is being staged fails before
1800
+ * anything is promoted.
1801
+ *
1802
+ * What this provides, exactly:
1803
+ *
1804
+ * - **Across destinations, staged-then-swapped with rollback on a caught
1805
+ * failure.** A failure part way through commit restores every destination it
1806
+ * already promoted, restores every file it already took, and removes every
1807
+ * directory it created, then reports what recovery could not undo. This is
1808
+ * measured: a promotion is driven to fail after an earlier one landed, and the
1809
+ * earlier destination is read back.
1810
+ * - **No partly written destination.** Every file is written whole into the
1811
+ * private root and digested there before commit, so a destination never
1812
+ * receives bytes that were still being produced.
1813
+ * - **No crash atomicity across destinations.** A process killed between two
1814
+ * promotions leaves the target holding some new files and some old ones, and
1815
+ * leaves the private root behind. Nothing here is a journal, and the private
1816
+ * root's name is the only record a later run could read.
1817
+ *
1818
+ * A destination is preserved by hard link and then replaced by a single
1819
+ * `rename`, rather than moved aside and replaced, so the path continues to name
1820
+ * the old file right up to the swap. That is a property of `rename` on the host,
1821
+ * not one this package's tests measure: telling it apart from move-then-replace
1822
+ * needs a reader observing the destination inside the swap, and no test here
1823
+ * does that. Read the claim as the mechanism it describes, not as a proven
1824
+ * guarantee about a concurrent reader.
1825
+ *
1826
+ * A path names a file this transaction writes, takes, or establishes as a
1827
+ * directory; every path is target-relative and is measured by the portable-path
1828
+ * law. An expectation and a precondition both name the resolved destination
1829
+ * instead, because that is the path each is re-read at.
1830
+ *
1831
+ * @example
1832
+ * ```ts
1833
+ * import { WriteTransaction } from '@orkestrel/scaffold/server'
1834
+ *
1835
+ * const transaction = new WriteTransaction('./packages/router', ['AGENTS.md'])
1836
+ * try {
1837
+ * transaction.write('AGENTS.md', '# Agents\n')
1838
+ * transaction.commit() // ['AGENTS.md']
1839
+ * } finally {
1840
+ * transaction.discard()
1841
+ * }
1842
+ * ```
1843
+ */
1844
+ export declare class WriteTransaction {
1845
+ #private;
1846
+ /**
1847
+ * Open a transaction over one target directory.
1848
+ *
1849
+ * @param target - The directory every path is written beneath.
1850
+ * @param paths - Every target-relative path this transaction may touch.
1851
+ * @param preconditions - The caller-observed destination states the whole
1852
+ * transaction is held to, each naming a resolved destination of `paths`.
1853
+ * @throws {@link ScaffoldError} coded `INVALID` when an argument is off
1854
+ * contract, `TARGET` when a destination is a shape this package will not write
1855
+ * over or no longer matches its precondition, and `WRITE` when the private
1856
+ * root cannot be established.
1857
+ *
1858
+ * @remarks
1859
+ * Nothing is created until every destination has been inspected and every
1860
+ * precondition has held, so a refused transaction leaves no residue at all.
1861
+ * The private root is created last and carries a random name, so two
1862
+ * transactions over one target never collide.
1863
+ */
1864
+ constructor(target: string, paths: readonly string[], preconditions?: readonly WritePrecondition[]);
1865
+ /** The resolved directory every path is written beneath. */
1866
+ get target(): string;
1867
+ /** What each destination held when the transaction opened, in path order. */
1868
+ get expectations(): readonly WriteExpectation[];
1869
+ /** Whether the transaction can still be committed or discarded. */
1870
+ get open(): boolean;
1871
+ /**
1872
+ * Stage one text file.
1873
+ *
1874
+ * @param path - The target-relative path to write.
1875
+ * @param content - The exact UTF-8 text the destination should hold.
1876
+ * @returns Nothing.
1877
+ * @throws {@link ScaffoldError} coded `INVALID` when the path is not one this
1878
+ * transaction opened or is already staged, `TARGET` when the destination holds
1879
+ * a directory, and `WRITE` when the staged file cannot be written or does not
1880
+ * carry the bytes it was given.
1881
+ *
1882
+ * @remarks
1883
+ * The staged file is read back and digested against the text it was given, so
1884
+ * a partial or interrupted write is refused here rather than promoted later.
1885
+ */
1886
+ write(path: string, content: string): void;
1887
+ /**
1888
+ * Stage one byte-for-byte copy of a file that already exists on this host.
1889
+ *
1890
+ * @param path - The target-relative path to write.
1891
+ * @param source - The resolved absolute path to copy the bytes from.
1892
+ * @param executable - Whether the destination should carry the executable bit.
1893
+ * @returns Nothing.
1894
+ * @throws {@link ScaffoldError} coded `INVALID` when the path is not one this
1895
+ * transaction opened or is already staged, `TARGET` when the destination holds
1896
+ * a directory or the source is not a physical file, and `WRITE` when the copy
1897
+ * cannot be made or does not digest to the source's bytes.
1898
+ *
1899
+ * @remarks
1900
+ * The source is digested before the copy and the copy is digested after it, so
1901
+ * a source that changed mid-copy is refused instead of staged.
1902
+ */
1903
+ copy(path: string, source: string, executable: boolean): void;
1904
+ /**
1905
+ * Establish one directory inside the target, one segment at a time.
1906
+ *
1907
+ * @param path - The target-relative directory to establish.
1908
+ * @returns The directory's identity and every segment this call created.
1909
+ * @throws {@link ScaffoldError} coded `INVALID` when the path is not one this
1910
+ * transaction opened, `TARGET` when the destination holds a file, and `WRITE`
1911
+ * when a segment cannot be created or changed while it was being created.
1912
+ *
1913
+ * @remarks
1914
+ * A directory is created immediately rather than staged, because creating one
1915
+ * destroys nothing: rollback removes exactly the segments this call created,
1916
+ * innermost first, and leaves every segment that was already there. Each
1917
+ * created segment is captured by device and inode, so a segment swapped
1918
+ * underneath the transaction is detected rather than written into.
1919
+ */
1920
+ directory(path: string): WriteDirectoryResult;
1921
+ /**
1922
+ * Mark one file for deletion at commit.
1923
+ *
1924
+ * @param path - The target-relative file to delete.
1925
+ * @returns Nothing.
1926
+ * @throws {@link ScaffoldError} coded `INVALID` when the path is not one this
1927
+ * transaction opened or is already claimed, and `TARGET` when the destination
1928
+ * does not hold a file.
1929
+ *
1930
+ * @remarks
1931
+ * Nothing moves here. Commit renames the file into the private backup rather
1932
+ * than unlinking it, so a later failure in the same commit puts it back.
1933
+ */
1934
+ remove(path: string): void;
1935
+ /**
1936
+ * Promote every staged file and take every marked file, or roll the whole call back.
1937
+ *
1938
+ * @returns Every target-relative path whose destination changed: the files
1939
+ * promoted, then the directories established, then the files taken.
1940
+ * @throws {@link ScaffoldError} coded `WRITE` when the transaction is closed,
1941
+ * when a destination moved since the transaction opened, or when the commit
1942
+ * failed; a failure reports what rollback could not undo in its context.
1943
+ *
1944
+ * @remarks
1945
+ * Every destination and every directory this transaction created is re-checked
1946
+ * before anything moves, so the common failure moves nothing at all. That
1947
+ * check runs inside the same rollback as the promotions, because a transaction
1948
+ * that refuses before it starts still has a private root and created
1949
+ * directories to clear. The transaction is closed either way: a committed one
1950
+ * has nothing left to undo and a failed one has already been rolled back.
1951
+ */
1952
+ commit(): readonly string[];
1953
+ /**
1954
+ * Abandon the transaction and remove everything it created.
1955
+ *
1956
+ * @returns Nothing.
1957
+ * @throws {@link ScaffoldError} coded `WRITE` when residue could not be
1958
+ * removed, naming the private root that still holds it.
1959
+ *
1960
+ * @remarks
1961
+ * Idempotent, and a no-op on a transaction that already committed or already
1962
+ * failed, so a caller can put it in a `finally` beside the work it guards.
1963
+ */
1964
+ discard(): void;
1965
+ }
1966
+
1967
+ export { }