@orkestrel/scaffold 0.0.2 → 0.0.4

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 (82) hide show
  1. package/README.md +29 -15
  2. package/dist/bin/scaffold.js +1162 -1200
  3. package/dist/bin/scaffold.js.map +1 -1
  4. package/dist/host/AGENTS.md +124 -925
  5. package/dist/host/CLAUDE.md +335 -495
  6. package/dist/host/agents/skills/orkestrel-align-packages/SKILL.md +53 -0
  7. package/dist/host/agents/skills/orkestrel-align-packages/agents/openai.yaml +4 -0
  8. package/dist/host/agents/skills/orkestrel-align-packages/references/fleet.md +50 -0
  9. package/dist/host/agents/skills/orkestrel-align-packages/references/integration.md +56 -0
  10. package/dist/host/agents/skills/orkestrel-build-application/SKILL.md +64 -0
  11. package/dist/host/agents/skills/orkestrel-build-application/agents/openai.yaml +4 -0
  12. package/dist/host/agents/skills/orkestrel-build-application/references/application.md +129 -0
  13. package/dist/host/agents/skills/orkestrel-harden-package/SKILL.md +64 -0
  14. package/dist/host/agents/skills/orkestrel-harden-package/agents/openai.yaml +4 -0
  15. package/dist/host/agents/skills/orkestrel-harden-package/references/centralization.md +85 -0
  16. package/dist/host/agents/skills/orkestrel-harden-package/references/contract.md +57 -0
  17. package/dist/host/agents/skills/orkestrel-harden-package/references/hardening.md +93 -0
  18. package/dist/host/agents/skills/orkestrel-harden-package/references/research.md +61 -0
  19. package/dist/host/claude/agents/application.md +30 -0
  20. package/dist/host/claude/agents/builder.md +10 -7
  21. package/dist/host/claude/agents/checker.md +14 -8
  22. package/dist/host/claude/agents/codex.md +94 -0
  23. package/dist/host/claude/agents/grok.md +37 -34
  24. package/dist/host/claude/agents/implementer.md +40 -0
  25. package/dist/host/claude/agents/orkestrel.md +88 -223
  26. package/dist/host/claude/agents/planner.md +13 -34
  27. package/dist/host/claude/agents/reviewer.md +38 -24
  28. package/dist/host/claude/agents/verifier.md +14 -10
  29. package/dist/host/claude/rules/application.md +57 -0
  30. package/dist/host/claude/rules/architecture.md +180 -0
  31. package/dist/host/claude/rules/browser.md +28 -0
  32. package/dist/host/claude/rules/documentation.md +64 -0
  33. package/dist/host/claude/rules/names.md +209 -0
  34. package/dist/host/claude/rules/patterns.md +130 -0
  35. package/dist/host/claude/rules/quality.md +45 -0
  36. package/dist/host/claude/rules/styles.md +64 -0
  37. package/dist/host/claude/rules/tests.md +123 -0
  38. package/dist/host/claude/rules/typescript.md +78 -0
  39. package/dist/host/claude/rules/workspace.md +180 -0
  40. package/dist/host/claude/settings.json +112 -0
  41. package/dist/host/claude/skills/orkestrel-align-packages/SKILL.md +10 -0
  42. package/dist/host/claude/skills/orkestrel-build-application/SKILL.md +12 -0
  43. package/dist/host/claude/skills/orkestrel-harden-package/SKILL.md +10 -0
  44. package/dist/host/codex/agents/analyst.toml +15 -0
  45. package/dist/host/codex/agents/application.toml +25 -0
  46. package/dist/host/codex/agents/builder.toml +24 -0
  47. package/dist/host/codex/agents/checker.toml +15 -0
  48. package/dist/host/codex/agents/grok.toml +17 -0
  49. package/dist/host/codex/agents/implementer.toml +17 -0
  50. package/dist/host/codex/agents/opus.toml +25 -0
  51. package/dist/host/codex/agents/orkestrel.toml +16 -0
  52. package/dist/host/codex/agents/planner.toml +20 -0
  53. package/dist/host/codex/agents/reviewer.toml +18 -0
  54. package/dist/host/codex/agents/verifier.toml +14 -0
  55. package/dist/host/codex/config.toml +66 -0
  56. package/dist/host/cursor/mcp.json +12 -0
  57. package/dist/host/dotfiles/mcp.json +8 -0
  58. package/dist/host/dotfiles/oxlintrc.json +307 -1
  59. package/dist/host/guides/src/scaffold.md +2059 -2007
  60. package/dist/host/manifest.json +376 -137
  61. package/dist/host/scripts/codex.sh +49 -0
  62. package/dist/host/scripts/cursor.sh +33 -47
  63. package/dist/host/scripts/deps.sh +34 -16
  64. package/dist/host/scripts/ollama.sh +6 -149
  65. package/dist/host/tests/setupPolicy.ts +390 -0
  66. package/dist/src/core/index.cjs +6603 -1200
  67. package/dist/src/core/index.cjs.map +1 -1
  68. package/dist/src/core/index.d.cts +802 -267
  69. package/dist/src/core/index.d.ts +802 -267
  70. package/dist/src/core/index.js +6521 -1192
  71. package/dist/src/core/index.js.map +1 -1
  72. package/dist/src/server/index.cjs +3132 -593
  73. package/dist/src/server/index.cjs.map +1 -1
  74. package/dist/src/server/index.d.cts +643 -95
  75. package/dist/src/server/index.d.ts +643 -95
  76. package/dist/src/server/index.js +3054 -594
  77. package/dist/src/server/index.js.map +1 -1
  78. package/package.json +16 -12
  79. package/dist/host/claude/agents/composer.md +0 -64
  80. package/dist/host/claude/agents/researcher.md +0 -38
  81. package/dist/host/claude/agents/scout.md +0 -35
  82. package/dist/host/github/workflows/ci.yml +0 -64
@@ -1,40 +1,1695 @@
1
- import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
- import { dirname, join, relative, resolve, sep } from "node:path";
1
+ import { CONTROL_CHARACTER_PATTERN, DEFAULT_ENGINES, DEFAULT_VERSION, DEPENDENCY_NAME_PATTERN, ENVIRONMENTS, HEX_PATTERN, HOST_PATHS, INVALID_PATH_CHARACTER_PATTERN, MAX_ARTIFACT_BYTES, MAX_ARTIFACT_HEX_LENGTH, MAX_COLLECTION_ITEMS, MAX_MANIFEST_BYTES, MAX_PATH_LENGTH, MAX_TOTAL_ARTIFACT_BYTES, SYNC_BASELINE_PATTERN, ScaffoldError, VERSION_PATTERN, blueprint, bytesToHex, contentByteLength, devDependenciesFor, diffPlan, findFileConflict, findPathConflict, hasOnlyDataProperties, hasValidAuditBytes, hasValidSnapshotBytes, isDenseDataArray, isDependency, isEmitterErrorHandler, isPlan, isSyncReport, isWorkspaceName, manifestToDependencies, ownDataValue, parsePlan, parseSyncReport, rangeToFreshness, snapshotPlan, validateDependencyArray, validatePlan } from "../core/index.js";
2
+ import { attempt, booleanShape, createContract, integerShape, isError, isFunction, isRecord, isString, objectShape, optionalShape, parseJSON, parseJSONAs, stringShape } from "@orkestrel/contract";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import { chmodSync, closeSync, constants, copyFileSync, existsSync, fstatSync, linkSync, lstatSync, mkdirSync, openSync, opendirSync, readSync, realpathSync, renameSync, rmSync, rmdirSync, writeFileSync } from "node:fs";
5
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
3
6
  import { fileURLToPath } from "node:url";
4
7
  import { flattenText, isBlockquoteNode, isParagraphNode, parseDocument, walkNodes } from "@orkestrel/markdown";
5
- import { DEFAULT_ENGINES, DEFAULT_VERSION, HOST_PATHS, ScaffoldError, blueprint, devDependenciesFor, manifestToDependencies, rangeToFreshness } from "../core/index.js";
6
8
  import { Emitter } from "@orkestrel/emitter";
9
+ //#region src/server/constants.ts
10
+ /**
11
+ * The prune-owned directories. Files outside these roots are never candidates
12
+ * for removal, including project-owned skills under `.agents/skills` and
13
+ * `.claude/skills`.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { PRUNE_DIRECTORIES } from '@orkestrel/scaffold/server'
18
+ *
19
+ * PRUNE_DIRECTORIES // ['.claude/agents', '.codex/agents', 'scripts']
20
+ * ```
21
+ */
22
+ var PRUNE_DIRECTORIES = Object.freeze([
23
+ ".claude/agents",
24
+ ".codex/agents",
25
+ "scripts"
26
+ ]);
27
+ /** Reserved metadata file written at the root of every staged host. */
28
+ var HOST_MANIFEST_PATH = "manifest.json";
29
+ /** Local configuration and credential-like paths that must never enter a vendored host. */
30
+ var SENSITIVE_HOST_PATH_PATTERN = /(?:^|\/)(?:(?:\.git|\.ssh|\.aws|\.azure|\.docker|\.kube|\.gnupg|\.env(?:\.[^/]*)?)(?:\/|$)|(?:\.npmrc|\.pypirc|\.netrc|\.git-credentials|settings\.local\.json|auth\.json|credentials(?:\.json)?|application_default_credentials\.json|id_rsa|id_ed25519|kubeconfig)(?:\/|$)|\.config\/(?:gh|gcloud)(?:\/|$)|\.local\/share\/keyrings(?:\/|$)|[^/]*service-account[^/]*\.json$|[^/]*\.(?:jks|key|p12|pem|pfx|pkcs12)$)/i;
31
+ /** Existing repository metadata that a materialization target must never own. */
32
+ var RESERVED_TARGET_PATH_PATTERN = /(?:^|\/)\.git(?:\/|$)/i;
33
+ /** Maximum normalized package-catalog description length. */
34
+ var MAX_CATALOG_DESCRIPTION_LENGTH = 500;
35
+ /** Maximum UTF-8 bytes parsed from one local package guide. */
36
+ var MAX_GUIDE_BYTES = MAX_ARTIFACT_BYTES;
37
+ /** Maximum filesystem entries accepted by one vendored-host traversal. */
38
+ var MAX_HOST_ENTRIES = 4096;
39
+ /** Maximum directory nesting accepted by one vendored-host traversal. */
40
+ var MAX_HOST_DEPTH = 32;
41
+ /** Maximum caller-controlled filesystem path depth accepted before filesystem work. */
42
+ var MAX_FILESYSTEM_DEPTH = 64;
43
+ /** Maximum UTF-8 bytes accepted in one portable filesystem path segment. */
44
+ var MAX_PATH_SEGMENT_BYTES = 255;
45
+ /** Windows device names that remain reserved even when followed by an extension. */
46
+ var RESERVED_PATH_SEGMENT_PATTERN = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9]|conin\$|conout\$)(?:\..*)?$/i;
47
+ /** Maximum simultaneous upstream requests accepted by `Sync`. */
48
+ var MAX_SYNC_CONCURRENCY = 64;
49
+ /** Default simultaneous upstream requests used by `Sync`. */
50
+ var DEFAULT_SYNC_CONCURRENCY = 6;
51
+ /** Maximum retry count accepted by `Sync`. */
52
+ var MAX_SYNC_RETRIES = 5;
53
+ /** Maximum per-request timeout accepted by `Sync`, in milliseconds. */
54
+ var MAX_SYNC_TIMEOUT = 3e5;
55
+ /** Default per-request timeout used by `Sync`, in milliseconds. */
56
+ var DEFAULT_SYNC_TIMEOUT = 1e4;
57
+ /** Maximum response body accepted by `Sync`, in bytes. */
58
+ var MAX_SYNC_LIMIT = MAX_ARTIFACT_BYTES;
59
+ /** Default per-response byte limit used by `Sync`. */
60
+ var DEFAULT_SYNC_LIMIT = MAX_ARTIFACT_BYTES;
61
+ /** Default maximum dependencies or catalog entries retained by one Sync operation. */
62
+ var DEFAULT_SYNC_ITEMS = 256;
63
+ /** Maximum configurable dependencies or catalog entries for one Sync operation. */
64
+ var MAX_SYNC_ITEMS = 1e3;
65
+ /** Default cumulative response bytes retained by one Sync operation. */
66
+ var DEFAULT_SYNC_BUDGET = 16777216;
67
+ /** Maximum configurable cumulative response bytes retained by one Sync operation. */
68
+ var MAX_SYNC_BUDGET = 104857600;
69
+ /** Maximum caller-supplied endpoint characters inspected before URL allocation. */
70
+ var MAX_SYNC_BASE_LENGTH = 2048;
71
+ /** Maximum portable Git branch characters accepted by the guide endpoint. */
72
+ var MAX_SYNC_BRANCH_LENGTH = 255;
73
+ /** Exact SHA-256 digest accepted by write preconditions. */
74
+ var WRITE_DIGEST_PATTERN = /^[0-9a-f]{64}$/;
75
+ /** Safe ASCII branch alphabet accepted before Git ref-component validation. */
76
+ var SYNC_BRANCH_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
77
+ //#endregion
78
+ //#region src/server/shapers.ts
79
+ /** Build the closed upstream-guide endpoint options shape. */
80
+ function syncGuideOptionsShape() {
81
+ return objectShape({
82
+ base: optionalShape(stringShape({
83
+ min: 1,
84
+ max: MAX_SYNC_BASE_LENGTH
85
+ })),
86
+ branch: optionalShape(stringShape({
87
+ min: 1,
88
+ max: 255
89
+ })),
90
+ timeout: optionalShape(integerShape({
91
+ min: 1,
92
+ max: MAX_SYNC_TIMEOUT
93
+ }))
94
+ });
95
+ }
96
+ /** Build the closed registry endpoint options shape. */
97
+ function syncRegistryOptionsShape() {
98
+ return objectShape({
99
+ base: optionalShape(stringShape({
100
+ min: 1,
101
+ max: MAX_SYNC_BASE_LENGTH
102
+ })),
103
+ timeout: optionalShape(integerShape({
104
+ min: 1,
105
+ max: MAX_SYNC_TIMEOUT
106
+ }))
107
+ });
108
+ }
109
+ /** Build the closed data-only `SyncOptions` shape. */
110
+ function syncOptionsShape() {
111
+ return objectShape({
112
+ guides: optionalShape(syncGuideOptionsShape()),
113
+ registry: optionalShape(syncRegistryOptionsShape()),
114
+ concurrency: optionalShape(integerShape({
115
+ min: 1,
116
+ max: 64
117
+ })),
118
+ retries: optionalShape(integerShape({
119
+ min: 0,
120
+ max: 5
121
+ })),
122
+ strict: optionalShape(booleanShape()),
123
+ limit: optionalShape(integerShape({
124
+ min: 1,
125
+ max: MAX_SYNC_LIMIT
126
+ })),
127
+ items: optionalShape(integerShape({
128
+ min: 1,
129
+ max: MAX_SYNC_ITEMS
130
+ })),
131
+ budget: optionalShape(integerShape({
132
+ min: 1,
133
+ max: MAX_SYNC_BUDGET
134
+ }))
135
+ });
136
+ }
137
+ /** Build the closed data-only `MaterializerOptions` shape. */
138
+ function materializerOptionsShape() {
139
+ return objectShape({ host: optionalShape(stringShape({
140
+ min: 1,
141
+ max: MAX_PATH_LENGTH
142
+ })) });
143
+ }
144
+ //#endregion
145
+ //#region src/server/contracts.ts
146
+ /** The compiled, closed data-only `SyncOptions` contract. */
147
+ var syncOptionsContract = createContract(syncOptionsShape());
148
+ /** The compiled, closed data-only `MaterializerOptions` contract. */
149
+ var materializerOptionsContract = createContract(materializerOptionsShape());
150
+ //#endregion
151
+ //#region src/server/validators.ts
152
+ /** Narrow one exact aggregate fleet traversal allowance. */
153
+ function isCatalogAllowance(value) {
154
+ const result = attempt(() => {
155
+ const bufferGetter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Float64Array.prototype), "buffer")?.get;
156
+ if (bufferGetter === void 0 || !ArrayBuffer.isView(value) || !(value instanceof Float64Array) || !(Reflect.apply(bufferGetter, value, []) instanceof ArrayBuffer) || value.length !== 1) return false;
157
+ const remaining = value[0];
158
+ return remaining !== void 0 && Number.isSafeInteger(remaining) && remaining >= 0 && remaining <= 4096;
159
+ });
160
+ return result.success && result.value;
161
+ }
162
+ /**
163
+ * Determine whether a value is a non-empty portable relative POSIX path.
164
+ *
165
+ * @param value - The candidate path.
166
+ * @returns `true` when every path segment is safe and portable.
167
+ */
168
+ function isPortablePath(value) {
169
+ if (!isString(value) || value.length === 0 || value.length > MAX_PATH_LENGTH) return false;
170
+ if (INVALID_PATH_CHARACTER_PATTERN.test(value) || CONTROL_CHARACTER_PATTERN.test(value)) return false;
171
+ const segments = value.split("/");
172
+ return segments.length <= 64 && !segments.some((segment) => segment === "" || segment === "." || segment === ".." || segment.endsWith(".") || segment.endsWith(" ") || contentByteLength(segment) > 255 || RESERVED_PATH_SEGMENT_PATTERN.test(segment));
173
+ }
174
+ /** Whether one host filesystem path is bounded, non-empty, and safe to render in a terminal. */
175
+ function isFilesystemPath(value) {
176
+ if (!isString(value) || value.length === 0 || value.length > MAX_PATH_LENGTH || CONTROL_CHARACTER_PATTERN.test(value)) return false;
177
+ const normalized = value.replaceAll("\\", "/");
178
+ const withoutRoot = normalized.startsWith("//") ? normalized.slice(2) : normalized.startsWith("/") ? normalized.slice(1) : normalized;
179
+ if (withoutRoot.includes("//")) return false;
180
+ const segments = withoutRoot.split("/").filter((segment) => segment.length > 0);
181
+ if (segments.length > 64) return false;
182
+ return !segments.some((segment, index) => {
183
+ if (segment === "." || segment === "..") return false;
184
+ if (index === 0 && /^[A-Za-z]:$/u.test(segment)) return false;
185
+ return /[<>:"|?*]/u.test(segment) || segment.endsWith(".") || segment.endsWith(" ") || contentByteLength(segment) > 255 || RESERVED_PATH_SEGMENT_PATTERN.test(segment);
186
+ });
187
+ }
188
+ /** Whether one externally supplied string is safe to render in terminal or JSON diagnostics. */
189
+ function isTerminalText(value) {
190
+ return isString(value) && !CONTROL_CHARACTER_PATTERN.test(value);
191
+ }
192
+ /** Whether one dependency is an exact data-property record safe to snapshot. */
193
+ function isDependencyData(value) {
194
+ return hasOnlyDataProperties(value) && isDependency(value);
195
+ }
196
+ /**
197
+ * Determine whether a host-relative path resembles local configuration or credentials.
198
+ *
199
+ * @param value - The portable candidate path.
200
+ * @returns `true` when the path must be excluded from vendored host output.
201
+ */
202
+ function isSensitiveHostPath(value) {
203
+ return SENSITIVE_HOST_PATH_PATTERN.test(value.replaceAll("\\", "/"));
204
+ }
205
+ /** Determine whether a target-relative path addresses preserved repository metadata. */
206
+ function isReservedTargetPath(value) {
207
+ return RESERVED_TARGET_PATH_PATTERN.test(value.replaceAll("\\", "/"));
208
+ }
209
+ /** Whether a normalized catalog description is bounded and contains no controls. */
210
+ function isCatalogDescription(value) {
211
+ return isString(value) && value.length <= 500 && !CONTROL_CHARACTER_PATTERN.test(value);
212
+ }
213
+ /**
214
+ * Determine whether a caught filesystem error reports an absent path.
215
+ *
216
+ * @param value - The caught value.
217
+ * @returns `true` only for an `Error` whose `code` is exactly `ENOENT`.
218
+ */
219
+ function isMissingPathError(value) {
220
+ const result = attempt(() => isError(value) && Reflect.get(value, "code") === "ENOENT");
221
+ return result.success && result.value;
222
+ }
223
+ /** Narrow one exact transaction destination precondition. */
224
+ function isWritePrecondition(value) {
225
+ const result = attempt(() => {
226
+ if (!isRecord(value)) return false;
227
+ const keys = Reflect.ownKeys(value);
228
+ if (keys.some((key) => key !== "path" && key !== "shape" && key !== "digest") || keys.length < 2 || keys.length > 3) return false;
229
+ const path = Reflect.getOwnPropertyDescriptor(value, "path");
230
+ const shape = Reflect.getOwnPropertyDescriptor(value, "shape");
231
+ const digest = Reflect.getOwnPropertyDescriptor(value, "digest");
232
+ if (path === void 0 || !Reflect.has(path, "value") || shape === void 0 || !Reflect.has(shape, "value") || digest !== void 0 && !Reflect.has(digest, "value") || !isPortablePath(path.value)) return false;
233
+ return shape.value === "absent" ? digest === void 0 : shape.value === "file" && digest !== void 0 && typeof digest.value === "string" && WRITE_DIGEST_PATTERN.test(digest.value);
234
+ });
235
+ return result.success && result.value;
236
+ }
237
+ /**
238
+ * Narrow a value to one exact vendored-host manifest entry.
239
+ *
240
+ * @param value - The candidate raw manifest entry.
241
+ * @returns `true` only for the exact safe entry shape.
242
+ */
243
+ function isManifestEntry(value) {
244
+ const result = attempt(() => {
245
+ if (!isRecord(value)) return false;
246
+ const keys = Reflect.ownKeys(value);
247
+ const storage = Reflect.getOwnPropertyDescriptor(value, "storage");
248
+ const destination = Reflect.getOwnPropertyDescriptor(value, "destination");
249
+ const executable = Reflect.getOwnPropertyDescriptor(value, "executable");
250
+ return keys.length === 3 && keys.every((key) => key === "storage" || key === "destination" || key === "executable") && storage !== void 0 && Reflect.has(storage, "value") && destination !== void 0 && Reflect.has(destination, "value") && executable !== void 0 && Reflect.has(executable, "value") && isPortablePath(storage.value) && isPortablePath(destination.value) && typeof executable.value === "boolean";
251
+ });
252
+ return result.success && result.value;
253
+ }
254
+ /**
255
+ * Narrow a value to one exact complete vendored-host manifest.
256
+ *
257
+ * @param value - The candidate manifest value.
258
+ * @returns `true` only for an exact `{ entries, roots }` record with safe paths.
259
+ */
260
+ function isHostManifest(value) {
261
+ const result = attempt(() => {
262
+ if (!isRecord(value)) return false;
263
+ const keys = Reflect.ownKeys(value);
264
+ const entries = Reflect.getOwnPropertyDescriptor(value, "entries");
265
+ const roots = Reflect.getOwnPropertyDescriptor(value, "roots");
266
+ return keys.length === 2 && keys.every((key) => key === "entries" || key === "roots") && entries !== void 0 && Reflect.has(entries, "value") && roots !== void 0 && Reflect.has(roots, "value") && isDenseDataArray(entries.value, 4096, isManifestEntry) && isDenseDataArray(roots.value, 4096, isPortablePath);
267
+ });
268
+ return result.success && result.value;
269
+ }
270
+ /** Narrow an exact initial-listener record for `Sync`. */
271
+ function isSyncEventHooks(value) {
272
+ const result = attempt(() => {
273
+ if (!isRecord(value)) return false;
274
+ const keys = Reflect.ownKeys(value);
275
+ if (keys.some((key) => key !== "guide" && key !== "version" && key !== "package" && key !== "write" && key !== "done" && key !== "error" && key !== "destroy")) return false;
276
+ return keys.every((key) => {
277
+ const descriptor = Reflect.getOwnPropertyDescriptor(value, key);
278
+ return descriptor !== void 0 && Reflect.has(descriptor, "value") && isFunction(descriptor.value);
279
+ });
280
+ });
281
+ return result.success && result.value;
282
+ }
283
+ /** Narrow an exact initial-listener record for `Materializer`. */
284
+ function isMaterializerEventHooks(value) {
285
+ const result = attempt(() => {
286
+ if (!isRecord(value)) return false;
287
+ const keys = Reflect.ownKeys(value);
288
+ if (keys.some((key) => key !== "copy" && key !== "write" && key !== "remove" && key !== "done" && key !== "error" && key !== "destroy")) return false;
289
+ return keys.every((key) => {
290
+ const descriptor = Reflect.getOwnPropertyDescriptor(value, key);
291
+ return descriptor !== void 0 && Reflect.has(descriptor, "value") && isFunction(descriptor.value);
292
+ });
293
+ });
294
+ return result.success && result.value;
295
+ }
296
+ //#endregion
297
+ //#region src/server/parsers.ts
298
+ /** Parse and semantically validate dependency data before Sync performs network I/O. */
299
+ function parseSyncDependencies(value, external) {
300
+ const owned = attempt(() => {
301
+ if (!isDenseDataArray(value, 1e3, isDependencyData)) throw new Error("dependencies are malformed");
302
+ const length = Reflect.getOwnPropertyDescriptor(value, "length")?.value;
303
+ if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0 || length > 1e3) throw new Error("dependency length is malformed");
304
+ const dependencies = [];
305
+ for (let index = 0; index < length; index += 1) {
306
+ const descriptor = Reflect.getOwnPropertyDescriptor(value, String(index));
307
+ if (descriptor === void 0 || !Reflect.has(descriptor, "value") || !isDependencyData(descriptor.value)) throw new Error("dependency is malformed");
308
+ const name = Reflect.getOwnPropertyDescriptor(descriptor.value, "name")?.value;
309
+ const range = Reflect.getOwnPropertyDescriptor(descriptor.value, "range")?.value;
310
+ const optional = Reflect.getOwnPropertyDescriptor(descriptor.value, "optional")?.value;
311
+ if (typeof name !== "string" || typeof range !== "string" || optional !== void 0 && typeof optional !== "boolean") throw new Error("dependency is malformed");
312
+ dependencies.push(Object.freeze({
313
+ name,
314
+ range,
315
+ ...optional === void 0 ? {} : { optional }
316
+ }));
317
+ }
318
+ return Object.freeze(dependencies);
319
+ });
320
+ if (!owned.success) throw new ScaffoldError("INVALID", "Sync dependencies are malformed", { error: owned.error });
321
+ const snapshot = owned.value;
322
+ const validation = validateDependencyArray(external ? "extras" : "dependencies", snapshot);
323
+ if (validation.questions.length > 0) throw new ScaffoldError("INVALID", "Sync dependencies are invalid", { questions: validation.questions });
324
+ return snapshot;
325
+ }
326
+ /** Parse a bounded dense array of host filesystem paths without invoking caller methods. */
327
+ function parseFilesystemPaths(value, limit) {
328
+ const parsed = attempt(() => {
329
+ if (!Number.isSafeInteger(limit) || limit < 0 || !isDenseDataArray(value, limit, isFilesystemPath)) return;
330
+ const length = Reflect.getOwnPropertyDescriptor(value, "length")?.value;
331
+ if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0 || length > limit) return;
332
+ const paths = [];
333
+ for (let index = 0; index < length; index += 1) {
334
+ const descriptor = Reflect.getOwnPropertyDescriptor(value, String(index));
335
+ if (descriptor === void 0 || !Reflect.has(descriptor, "value") || !isFilesystemPath(descriptor.value)) return;
336
+ paths.push(descriptor.value);
337
+ }
338
+ return Object.freeze(paths);
339
+ });
340
+ return parsed.success ? parsed.value : void 0;
341
+ }
342
+ /** Parse a bounded dense array of portable paths without invoking caller methods. */
343
+ function parsePortablePaths(value, limit) {
344
+ const parsed = attempt(() => {
345
+ if (!Number.isSafeInteger(limit) || limit < 0 || !isDenseDataArray(value, limit, isPortablePath)) return;
346
+ const length = Reflect.getOwnPropertyDescriptor(value, "length")?.value;
347
+ if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0 || length > limit) return;
348
+ const paths = [];
349
+ for (let index = 0; index < length; index += 1) {
350
+ const descriptor = Reflect.getOwnPropertyDescriptor(value, String(index));
351
+ if (descriptor === void 0 || !Reflect.has(descriptor, "value") || !isPortablePath(descriptor.value)) return;
352
+ paths.push(descriptor.value);
353
+ }
354
+ return Object.freeze(paths);
355
+ });
356
+ return parsed.success ? parsed.value : void 0;
357
+ }
358
+ /** Parse bounded exact transaction preconditions without invoking caller methods. */
359
+ function parseWritePreconditions(value, limit) {
360
+ const parsed = attempt(() => {
361
+ if (!Number.isSafeInteger(limit) || limit < 0 || !isDenseDataArray(value, limit, isWritePrecondition)) return;
362
+ const length = Reflect.getOwnPropertyDescriptor(value, "length")?.value;
363
+ if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0 || length > limit) return;
364
+ const preconditions = [];
365
+ for (let index = 0; index < length; index += 1) {
366
+ const descriptor = Reflect.getOwnPropertyDescriptor(value, String(index));
367
+ if (descriptor === void 0 || !Reflect.has(descriptor, "value") || !isWritePrecondition(descriptor.value)) return;
368
+ const precondition = descriptor.value;
369
+ const path = Reflect.getOwnPropertyDescriptor(precondition, "path")?.value;
370
+ const shape = Reflect.getOwnPropertyDescriptor(precondition, "shape")?.value;
371
+ const digest = Reflect.getOwnPropertyDescriptor(precondition, "digest")?.value;
372
+ if (typeof path !== "string") return void 0;
373
+ if (shape === "absent" && digest === void 0) {
374
+ const copy = {
375
+ path,
376
+ shape
377
+ };
378
+ if (!isWritePrecondition(copy)) return void 0;
379
+ preconditions.push(Object.freeze(copy));
380
+ continue;
381
+ }
382
+ if (shape !== "file" || typeof digest !== "string") return void 0;
383
+ const copy = {
384
+ path,
385
+ shape,
386
+ digest
387
+ };
388
+ if (!isWritePrecondition(copy)) return void 0;
389
+ preconditions.push(Object.freeze(copy));
390
+ }
391
+ return Object.freeze(preconditions);
392
+ });
393
+ return parsed.success ? parsed.value : void 0;
394
+ }
395
+ /** Parse and normalize one upstream HTTP(S) endpoint base. */
396
+ function parseSyncBase(value) {
397
+ if (typeof value !== "string" || value.length === 0 || value.length > 2048 || value !== value.trim() || /^[A-Za-z][A-Za-z0-9+.-]*:\/\//i.test(value) && !/^https?:\/\//i.test(value)) throw new ScaffoldError("INVALID", "Invalid Sync endpoint base", { value });
398
+ const normalized = /^https?:\/\//i.test(value) ? value : `https://${value}`;
399
+ const parsed = attempt(() => new URL(normalized));
400
+ if (!parsed.success || parsed.value.protocol !== "http:" && parsed.value.protocol !== "https:" || parsed.value.username.length > 0 || parsed.value.password.length > 0 || parsed.value.search.length > 0 || parsed.value.hash.length > 0 || parsed.value.protocol === "http:" && ![
401
+ "127.0.0.1",
402
+ "[::1]",
403
+ "localhost"
404
+ ].includes(parsed.value.hostname.toLowerCase())) throw new ScaffoldError("INVALID", "Invalid Sync endpoint base", { value });
405
+ const base = parsed.value.href.replace(/\/+$/, "");
406
+ if (base.length > 2048) throw new ScaffoldError("INVALID", "Invalid Sync endpoint base", { value });
407
+ return base;
408
+ }
409
+ /**
410
+ * Snapshot only declared guide references while enforcing per-file and cumulative byte limits.
411
+ *
412
+ * @param value - The caller-supplied guide content map.
413
+ * @param names - Exact dependency names eligible for selection.
414
+ * @param budget - Maximum cumulative UTF-8 bytes.
415
+ * @returns A frozen selected map, or `undefined` when no map was supplied.
416
+ */
417
+ function parseSyncCurrent(value, names, budget) {
418
+ if (value === void 0) return void 0;
419
+ const snapshot = attempt(() => {
420
+ if (!isRecord(value)) throw new Error("current must be a plain record");
421
+ const selected = {};
422
+ let consumed = 0;
423
+ for (const name of names) {
424
+ const descriptor = Reflect.getOwnPropertyDescriptor(value, name);
425
+ if (descriptor === void 0) continue;
426
+ if (!Reflect.has(descriptor, "value") || typeof descriptor.value !== "string") throw new Error("current values must be string data properties");
427
+ const bytes = contentByteLength(descriptor.value);
428
+ consumed += bytes;
429
+ if (bytes > MAX_ARTIFACT_BYTES || consumed > budget) throw new Error("current guide content exceeds its byte allowance");
430
+ selected[name] = descriptor.value;
431
+ }
432
+ return Object.freeze(selected);
433
+ });
434
+ if (!snapshot.success) throw new ScaffoldError("INVALID", "Sync current guides are malformed", { error: snapshot.error });
435
+ return snapshot.value;
436
+ }
437
+ /** Parse a safe Git branch path for the canonical raw-guide URL. */
438
+ function parseSyncBranch(value) {
439
+ const segments = typeof value === "string" ? value.split("/") : [];
440
+ if (typeof value !== "string" || value.length === 0 || value.length > 255 || !SYNC_BRANCH_PATTERN.test(value) || value.includes("..") || value.includes("@{") || value === "@" || segments.some((segment) => segment === "" || segment.startsWith(".") || segment.endsWith(".") || /\.lock$/iu.test(segment))) throw new ScaffoldError("INVALID", "Invalid Sync guide branch", { value });
441
+ return value;
442
+ }
443
+ /** Parse exact materializer options before allocating its emitter. */
444
+ function parseMaterializerOptions(value) {
445
+ if (value === void 0) return {};
446
+ const read = attempt(() => {
447
+ if (!isRecord(value)) throw new Error("options must be a plain record");
448
+ const keys = Reflect.ownKeys(value);
449
+ if (keys.some((key) => key !== "host" && key !== "on" && key !== "error")) throw new Error("options contain an unknown key");
450
+ if (keys.map((key) => Reflect.getOwnPropertyDescriptor(value, key)).some((descriptor) => descriptor === void 0 || !Reflect.has(descriptor, "value"))) throw new Error("options must use data properties");
451
+ return {
452
+ host: Reflect.getOwnPropertyDescriptor(value, "host")?.value,
453
+ on: Reflect.getOwnPropertyDescriptor(value, "on")?.value,
454
+ error: Reflect.getOwnPropertyDescriptor(value, "error")?.value
455
+ };
456
+ });
457
+ if (!read.success) throw new ScaffoldError("INVALID", "Materializer options are malformed", { error: read.error });
458
+ const parsed = materializerOptionsContract.parse({ ...read.value.host === void 0 ? {} : { host: read.value.host } });
459
+ if (parsed === void 0 || parsed.host !== void 0 && !isFilesystemPath(parsed.host)) throw new ScaffoldError("INVALID", "Materializer host is malformed");
460
+ if (read.value.on !== void 0 && !isMaterializerEventHooks(read.value.on)) throw new ScaffoldError("INVALID", "Materializer event hooks are malformed");
461
+ const ownedHooks = attempt(() => {
462
+ const sourceHooks = read.value.on;
463
+ if (sourceHooks === void 0) return void 0;
464
+ if (typeof sourceHooks !== "object" || sourceHooks === null) throw new Error("event hooks are malformed");
465
+ const copy = Reflect.getOwnPropertyDescriptor(sourceHooks, "copy")?.value;
466
+ const write = Reflect.getOwnPropertyDescriptor(sourceHooks, "write")?.value;
467
+ const remove = Reflect.getOwnPropertyDescriptor(sourceHooks, "remove")?.value;
468
+ const done = Reflect.getOwnPropertyDescriptor(sourceHooks, "done")?.value;
469
+ const error = Reflect.getOwnPropertyDescriptor(sourceHooks, "error")?.value;
470
+ const destroy = Reflect.getOwnPropertyDescriptor(sourceHooks, "destroy")?.value;
471
+ if (copy !== void 0 && !isFunction(copy) || write !== void 0 && !isFunction(write) || remove !== void 0 && !isFunction(remove) || done !== void 0 && !isFunction(done) || error !== void 0 && !isFunction(error) || destroy !== void 0 && !isFunction(destroy)) throw new Error("event hooks are malformed");
472
+ return Object.freeze({
473
+ ...copy === void 0 ? {} : { copy },
474
+ ...write === void 0 ? {} : { write },
475
+ ...remove === void 0 ? {} : { remove },
476
+ ...done === void 0 ? {} : { done },
477
+ ...error === void 0 ? {} : { error },
478
+ ...destroy === void 0 ? {} : { destroy }
479
+ });
480
+ });
481
+ if (!ownedHooks.success) throw new ScaffoldError("INVALID", "Materializer event hooks are malformed", { error: ownedHooks.error });
482
+ const on = ownedHooks.value;
483
+ if (read.value.error !== void 0 && !isEmitterErrorHandler(read.value.error)) throw new ScaffoldError("INVALID", "Materializer error handler is malformed");
484
+ return {
485
+ ...parsed.host === void 0 ? {} : { host: parsed.host },
486
+ ...on === void 0 ? {} : { on },
487
+ ...read.value.error === void 0 ? {} : { error: read.value.error }
488
+ };
489
+ }
490
+ /**
491
+ * Parse the exact bounded `SyncOptions` boundary before allocating resources.
492
+ *
493
+ * @param value - Caller-supplied options.
494
+ * @returns A fresh, validated options record.
495
+ */
496
+ function parseSyncOptions(value) {
497
+ if (value === void 0) return {};
498
+ const read = attempt(() => {
499
+ if (!isRecord(value)) throw new Error("options must be a plain record");
500
+ const keys = Reflect.ownKeys(value);
501
+ if (keys.some((key) => key !== "guides" && key !== "registry" && key !== "concurrency" && key !== "retries" && key !== "strict" && key !== "limit" && key !== "items" && key !== "budget" && key !== "on" && key !== "error")) throw new Error("options contain an unknown key");
502
+ if (keys.map((key) => Reflect.getOwnPropertyDescriptor(value, key)).some((descriptor) => descriptor === void 0 || !Reflect.has(descriptor, "value"))) throw new Error("options must use data properties");
503
+ return {
504
+ guides: Reflect.getOwnPropertyDescriptor(value, "guides")?.value,
505
+ registry: Reflect.getOwnPropertyDescriptor(value, "registry")?.value,
506
+ concurrency: Reflect.getOwnPropertyDescriptor(value, "concurrency")?.value,
507
+ retries: Reflect.getOwnPropertyDescriptor(value, "retries")?.value,
508
+ strict: Reflect.getOwnPropertyDescriptor(value, "strict")?.value,
509
+ limit: Reflect.getOwnPropertyDescriptor(value, "limit")?.value,
510
+ items: Reflect.getOwnPropertyDescriptor(value, "items")?.value,
511
+ budget: Reflect.getOwnPropertyDescriptor(value, "budget")?.value,
512
+ on: Reflect.getOwnPropertyDescriptor(value, "on")?.value,
513
+ error: Reflect.getOwnPropertyDescriptor(value, "error")?.value
514
+ };
515
+ });
516
+ if (!read.success) throw new ScaffoldError("INVALID", "Sync options are malformed", { error: read.error });
517
+ const nested = attempt(() => {
518
+ let guides;
519
+ if (read.value.guides !== void 0) {
520
+ const source = read.value.guides;
521
+ if (!isRecord(source)) throw new Error("guides must be a plain record");
522
+ const keys = Reflect.ownKeys(source);
523
+ if (keys.some((key) => key !== "base" && key !== "branch" && key !== "timeout")) throw new Error("guides contain an unknown key");
524
+ if (keys.map((key) => Reflect.getOwnPropertyDescriptor(source, key)).some((descriptor) => descriptor === void 0 || !Reflect.has(descriptor, "value"))) throw new Error("guides must use data properties");
525
+ guides = {
526
+ base: Reflect.getOwnPropertyDescriptor(source, "base")?.value,
527
+ branch: Reflect.getOwnPropertyDescriptor(source, "branch")?.value,
528
+ timeout: Reflect.getOwnPropertyDescriptor(source, "timeout")?.value
529
+ };
530
+ }
531
+ let registry;
532
+ if (read.value.registry !== void 0) {
533
+ const source = read.value.registry;
534
+ if (!isRecord(source)) throw new Error("registry must be a plain record");
535
+ const keys = Reflect.ownKeys(source);
536
+ if (keys.some((key) => key !== "base" && key !== "timeout")) throw new Error("registry contains an unknown key");
537
+ if (keys.map((key) => Reflect.getOwnPropertyDescriptor(source, key)).some((descriptor) => descriptor === void 0 || !Reflect.has(descriptor, "value"))) throw new Error("registry must use data properties");
538
+ registry = {
539
+ base: Reflect.getOwnPropertyDescriptor(source, "base")?.value,
540
+ timeout: Reflect.getOwnPropertyDescriptor(source, "timeout")?.value
541
+ };
542
+ }
543
+ return {
544
+ guides,
545
+ registry
546
+ };
547
+ });
548
+ if (!nested.success) throw new ScaffoldError("INVALID", "Sync endpoint options are malformed", { error: nested.error });
549
+ const configuration = {
550
+ ...nested.value.guides === void 0 ? {} : { guides: nested.value.guides },
551
+ ...nested.value.registry === void 0 ? {} : { registry: nested.value.registry },
552
+ ...read.value.concurrency === void 0 ? {} : { concurrency: read.value.concurrency },
553
+ ...read.value.retries === void 0 ? {} : { retries: read.value.retries },
554
+ ...read.value.strict === void 0 ? {} : { strict: read.value.strict },
555
+ ...read.value.limit === void 0 ? {} : { limit: read.value.limit },
556
+ ...read.value.items === void 0 ? {} : { items: read.value.items },
557
+ ...read.value.budget === void 0 ? {} : { budget: read.value.budget }
558
+ };
559
+ const parsed = syncOptionsContract.parse(configuration);
560
+ if (parsed === void 0) throw new ScaffoldError("INVALID", "Sync options are malformed");
561
+ if (read.value.on !== void 0 && !isSyncEventHooks(read.value.on)) throw new ScaffoldError("INVALID", "Sync event hooks are malformed");
562
+ const ownedHooks = attempt(() => {
563
+ const sourceHooks = read.value.on;
564
+ if (sourceHooks === void 0) return void 0;
565
+ if (typeof sourceHooks !== "object" || sourceHooks === null) throw new Error("event hooks are malformed");
566
+ const guide = Reflect.getOwnPropertyDescriptor(sourceHooks, "guide")?.value;
567
+ const version = Reflect.getOwnPropertyDescriptor(sourceHooks, "version")?.value;
568
+ const packageListener = Reflect.getOwnPropertyDescriptor(sourceHooks, "package")?.value;
569
+ const write = Reflect.getOwnPropertyDescriptor(sourceHooks, "write")?.value;
570
+ const done = Reflect.getOwnPropertyDescriptor(sourceHooks, "done")?.value;
571
+ const error = Reflect.getOwnPropertyDescriptor(sourceHooks, "error")?.value;
572
+ const destroy = Reflect.getOwnPropertyDescriptor(sourceHooks, "destroy")?.value;
573
+ if (guide !== void 0 && !isFunction(guide) || version !== void 0 && !isFunction(version) || packageListener !== void 0 && !isFunction(packageListener) || write !== void 0 && !isFunction(write) || done !== void 0 && !isFunction(done) || error !== void 0 && !isFunction(error) || destroy !== void 0 && !isFunction(destroy)) throw new Error("event hooks are malformed");
574
+ return Object.freeze({
575
+ ...guide === void 0 ? {} : { guide },
576
+ ...version === void 0 ? {} : { version },
577
+ ...packageListener === void 0 ? {} : { package: packageListener },
578
+ ...write === void 0 ? {} : { write },
579
+ ...done === void 0 ? {} : { done },
580
+ ...error === void 0 ? {} : { error },
581
+ ...destroy === void 0 ? {} : { destroy }
582
+ });
583
+ });
584
+ if (!ownedHooks.success) throw new ScaffoldError("INVALID", "Sync event hooks are malformed", { error: ownedHooks.error });
585
+ const on = ownedHooks.value;
586
+ if (read.value.error !== void 0 && !isEmitterErrorHandler(read.value.error)) throw new ScaffoldError("INVALID", "Sync error handler is malformed");
587
+ const guides = parsed.guides === void 0 ? void 0 : {
588
+ ...parsed.guides.base === void 0 ? {} : { base: parseSyncBase(parsed.guides.base) },
589
+ ...parsed.guides.branch === void 0 ? {} : { branch: parseSyncBranch(parsed.guides.branch) },
590
+ ...parsed.guides.timeout === void 0 ? {} : { timeout: parsed.guides.timeout }
591
+ };
592
+ const registry = parsed.registry === void 0 ? void 0 : {
593
+ ...parsed.registry.base === void 0 ? {} : { base: parseSyncBase(parsed.registry.base) },
594
+ ...parsed.registry.timeout === void 0 ? {} : { timeout: parsed.registry.timeout }
595
+ };
596
+ return {
597
+ ...guides === void 0 ? {} : { guides },
598
+ ...registry === void 0 ? {} : { registry },
599
+ ...parsed.concurrency === void 0 ? {} : { concurrency: parsed.concurrency },
600
+ ...parsed.retries === void 0 ? {} : { retries: parsed.retries },
601
+ ...parsed.strict === void 0 ? {} : { strict: parsed.strict },
602
+ ...parsed.limit === void 0 ? {} : { limit: parsed.limit },
603
+ ...parsed.items === void 0 ? {} : { items: parsed.items },
604
+ ...parsed.budget === void 0 ? {} : { budget: parsed.budget },
605
+ ...on === void 0 ? {} : { on },
606
+ ...read.value.error === void 0 ? {} : { error: read.value.error }
607
+ };
608
+ }
609
+ //#endregion
610
+ //#region src/server/WriteTransaction.ts
611
+ /**
612
+ * Nominal, same-volume write-transaction state. Construction derives every
613
+ * filesystem path from a target and portable relative paths; callers cannot
614
+ * supply a deletion root or mutate captured arrays.
615
+ */
616
+ var WriteTransaction = class WriteTransaction {
617
+ static #token = Object.freeze({});
618
+ #target;
619
+ #root;
620
+ #stage;
621
+ #backup;
622
+ #expectations;
623
+ #parents;
624
+ #directories;
625
+ #anchor;
626
+ #existing;
627
+ constructor(token, target, root, stage, backup, expectations, parents, directories, anchor, existing) {
628
+ if (token !== WriteTransaction.#token) throw new ScaffoldError("WRITE", "WriteTransaction must be created through create()");
629
+ this.#target = target;
630
+ this.#root = root;
631
+ this.#stage = stage;
632
+ this.#backup = backup;
633
+ this.#expectations = Object.freeze([...expectations]);
634
+ this.#parents = Object.freeze(parents.map((parent) => Object.freeze({ ...parent })));
635
+ this.#directories = Object.freeze(directories.map((directory) => Object.freeze({ ...directory })));
636
+ this.#anchor = Object.freeze({ ...anchor });
637
+ this.#existing = existing === void 0 ? void 0 : Object.freeze({ ...existing });
638
+ }
639
+ /** Create private staging state and snapshot every destination before mutation. */
640
+ static create(target, paths, preconditions) {
641
+ if (!isFilesystemPath(target)) throw new ScaffoldError("WRITE", "Write transaction target is malformed", { target });
642
+ const resolvedTarget = resolve(target);
643
+ const requested = parsePortablePaths(paths, MAX_COLLECTION_ITEMS);
644
+ if (requested === void 0) throw new ScaffoldError("WRITE", "Write transaction paths are malformed", { target: resolvedTarget });
645
+ const conditions = preconditions === void 0 ? void 0 : parseWritePreconditions(preconditions, MAX_COLLECTION_ITEMS);
646
+ if (preconditions !== void 0 && conditions === void 0) throw new ScaffoldError("WRITE", "Write transaction preconditions are malformed", { target: resolvedTarget });
647
+ const conflict = findFileConflict(requested);
648
+ if (conflict !== void 0) throw new ScaffoldError("WRITE", `Write transaction collision between "${conflict[0]}" and "${conflict[1]}"`, {
649
+ paths: conflict,
650
+ committed: false
651
+ });
652
+ if (conditions !== void 0 && (conditions.length !== requested.length || requested.some((path) => !conditions.some((condition) => condition.path === path)) || conditions.some((condition) => !isPortablePath(condition.path) || condition.shape === "file" && (condition.digest === void 0 || !WRITE_DIGEST_PATTERN.test(condition.digest)) || condition.shape === "absent" && condition.digest !== void 0))) throw new ScaffoldError("WRITE", "Write transaction preconditions are malformed", { paths: requested });
653
+ const parent = dirname(resolvedTarget);
654
+ const missingParents = [];
655
+ let ancestor = parent;
656
+ let anchor;
657
+ while (anchor === void 0) {
658
+ const status = attempt(() => lstatSync(ancestor));
659
+ if (status.success) {
660
+ if (!status.value.isDirectory() || status.value.isSymbolicLink()) throw new ScaffoldError("WRITE", "Write transaction parent is not a real directory", {
661
+ target: resolvedTarget,
662
+ parent: ancestor
663
+ });
664
+ anchor = Object.freeze({
665
+ path: ancestor,
666
+ device: status.value.dev,
667
+ inode: status.value.ino
668
+ });
669
+ break;
670
+ }
671
+ if (!isMissingPathError(status.error)) throw new ScaffoldError("WRITE", "Failed to inspect write transaction parent", {
672
+ target: resolvedTarget,
673
+ parent: ancestor,
674
+ error: status.error
675
+ });
676
+ missingParents.push(ancestor);
677
+ const next = dirname(ancestor);
678
+ if (next === ancestor) throw new ScaffoldError("WRITE", "Write transaction has no existing parent", { target: resolvedTarget });
679
+ ancestor = next;
680
+ }
681
+ const targetStatus = attempt(() => lstatSync(resolvedTarget));
682
+ const existing = targetStatus.success && targetStatus.value.isDirectory() && !targetStatus.value.isSymbolicLink() ? Object.freeze({
683
+ path: resolvedTarget,
684
+ device: targetStatus.value.dev,
685
+ inode: targetStatus.value.ino
686
+ }) : void 0;
687
+ if (!targetStatus.success && !isMissingPathError(targetStatus.error)) throw new ScaffoldError("WRITE", "Failed to inspect write transaction target", {
688
+ target: resolvedTarget,
689
+ error: targetStatus.error
690
+ });
691
+ if (targetStatus.success && existing === void 0) throw new ScaffoldError("WRITE", "Write transaction target is not a real directory", { target: resolvedTarget });
692
+ const expectations = [];
693
+ for (const path of requested) {
694
+ if (!isPortablePath(path)) throw new ScaffoldError("WRITE", `Invalid transaction path at ${path}`, { path });
695
+ const precondition = conditions?.find((condition) => condition.path === path);
696
+ const destination = resolvePhysicalPath(resolvedTarget, path, "WRITE", "target");
697
+ const current = attempt(() => lstatSync(destination));
698
+ if (!current.success) {
699
+ if (!isMissingPathError(current.error)) throw new ScaffoldError("WRITE", `Failed to inspect transaction destination at ${path}`, {
700
+ path,
701
+ error: current.error
702
+ });
703
+ if (precondition?.shape === "file") throw new ScaffoldError("WRITE", `Write precondition changed at ${path}`, { path });
704
+ expectations.push(Object.freeze({
705
+ path,
706
+ shape: "absent"
707
+ }));
708
+ continue;
709
+ }
710
+ if (current.value.isSymbolicLink() || current.value.isFile() && current.value.nlink !== 1 || !current.value.isFile() && !current.value.isDirectory()) throw new ScaffoldError("WRITE", `Unsafe transaction destination at ${path}`, { path });
711
+ if (current.value.isDirectory()) {
712
+ if (precondition !== void 0) throw new ScaffoldError("WRITE", `Write precondition changed at ${path}`, { path });
713
+ expectations.push(Object.freeze({
714
+ path,
715
+ shape: "directory",
716
+ device: current.value.dev,
717
+ inode: current.value.ino,
718
+ modified: current.value.mtimeMs,
719
+ size: current.value.size
720
+ }));
721
+ continue;
722
+ }
723
+ const content = attempt(() => digestFile(destination));
724
+ if (!content.success) throw new ScaffoldError("WRITE", `Failed to snapshot transaction destination at ${path}`, {
725
+ path,
726
+ error: content.error
727
+ });
728
+ if (precondition?.shape === "absent" || precondition?.shape === "file" && precondition.digest !== content.value) throw new ScaffoldError("WRITE", `Write precondition changed at ${path}`, { path });
729
+ expectations.push(Object.freeze({
730
+ path,
731
+ shape: "file",
732
+ device: current.value.dev,
733
+ inode: current.value.ino,
734
+ modified: current.value.mtimeMs,
735
+ size: current.value.size,
736
+ digest: content.value
737
+ }));
738
+ }
739
+ const token = randomUUID();
740
+ const root = join(parent, `.${basename(resolvedTarget)}.write-${token}`);
741
+ const stage = join(root, "stage");
742
+ const backup = join(root, "backup");
743
+ const parents = [];
744
+ const directories = [];
745
+ const created = attempt(() => {
746
+ const anchored = lstatSync(anchor.path);
747
+ if (!anchored.isDirectory() || anchored.isSymbolicLink() || anchored.dev !== anchor.device || anchored.ino !== anchor.inode) throw new Error("write transaction anchor changed");
748
+ for (const directory of [...missingParents].reverse()) {
749
+ mkdirSync(directory);
750
+ const status = lstatSync(directory);
751
+ if (!status.isDirectory() || status.isSymbolicLink()) throw new Error(`write transaction parent changed at ${directory}`);
752
+ parents.push(Object.freeze({
753
+ path: directory,
754
+ device: status.dev,
755
+ inode: status.ino
756
+ }));
757
+ }
758
+ mkdirSync(root, { mode: 448 });
759
+ const rootStatus = lstatSync(root);
760
+ if (!rootStatus.isDirectory() || rootStatus.isSymbolicLink()) throw new Error("write transaction root is not a real directory");
761
+ directories.push(Object.freeze({
762
+ path: root,
763
+ device: rootStatus.dev,
764
+ inode: rootStatus.ino
765
+ }));
766
+ mkdirSync(stage, { mode: 448 });
767
+ const stageStatus = lstatSync(stage);
768
+ if (!stageStatus.isDirectory() || stageStatus.isSymbolicLink()) throw new Error("write transaction stage is not a real directory");
769
+ directories.push(Object.freeze({
770
+ path: stage,
771
+ device: stageStatus.dev,
772
+ inode: stageStatus.ino
773
+ }));
774
+ mkdirSync(backup, { mode: 448 });
775
+ const backupStatus = lstatSync(backup);
776
+ if (!backupStatus.isDirectory() || backupStatus.isSymbolicLink()) throw new Error("write transaction backup is not a real directory");
777
+ directories.push(Object.freeze({
778
+ path: backup,
779
+ device: backupStatus.dev,
780
+ inode: backupStatus.ino
781
+ }));
782
+ for (const directory of [...parents, ...directories]) {
783
+ const status = lstatSync(directory.path);
784
+ if (!status.isDirectory() || status.isSymbolicLink() || status.dev !== directory.device || status.ino !== directory.inode) throw new Error(`write transaction directory changed at ${directory.path}`);
785
+ }
786
+ });
787
+ if (!created.success) {
788
+ if (attempt(() => {
789
+ for (const directory of directories) {
790
+ const status = lstatSync(directory.path);
791
+ if (!status.isDirectory() || status.isSymbolicLink() || status.dev !== directory.device || status.ino !== directory.inode) throw new Error(`write transaction directory changed at ${directory.path}`);
792
+ }
793
+ if (directories.length > 0) rmSync(root, { recursive: true });
794
+ }).success) for (const directory of [...parents].reverse()) attempt(() => {
795
+ const status = lstatSync(directory.path);
796
+ if (!status.isDirectory() || status.isSymbolicLink() || status.dev !== directory.device || status.ino !== directory.inode) throw new Error(`write transaction parent changed at ${directory.path}`);
797
+ rmdirSync(directory.path);
798
+ });
799
+ throw new ScaffoldError("WRITE", "Failed to create write transaction", {
800
+ target: resolvedTarget,
801
+ root,
802
+ error: created.error
803
+ });
804
+ }
805
+ return new WriteTransaction(WriteTransaction.#token, resolvedTarget, root, stage, backup, expectations, parents, directories, anchor, existing);
806
+ }
807
+ get target() {
808
+ return this.#target;
809
+ }
810
+ get root() {
811
+ return this.#root;
812
+ }
813
+ get stage() {
814
+ return this.#stage;
815
+ }
816
+ get backup() {
817
+ return this.#backup;
818
+ }
819
+ get expectations() {
820
+ return this.#expectations;
821
+ }
822
+ get parents() {
823
+ return this.#parents;
824
+ }
825
+ get directories() {
826
+ return this.#directories;
827
+ }
828
+ get anchor() {
829
+ return this.#anchor;
830
+ }
831
+ get existing() {
832
+ return this.#existing;
833
+ }
834
+ };
835
+ //#endregion
7
836
  //#region src/server/helpers.ts
8
837
  /**
9
- * Locate this MODULE's own installed package root the nearest ancestor of
10
- * `import.meta.url` holding a `package.json` — and return its vendored
11
- * `dist/host` data root. THE single source of truth for the default
12
- * `Materializer` / `scaffold` bin host: once installed, walking up from the
13
- * module's own file (not `process.cwd()`, which points at whichever project
14
- * happens to be running) resolves to `node_modules/@orkestrel/scaffold`, the
15
- * correct default host the package ships its vendored data with itself.
16
- * `dist/host` may not exist yet when this resolves from SOURCE under a test
17
- * runner; that is fine — existence is checked at the point of use, not here.
838
+ * Whether a path is an existing physical directory rather than a file or link.
839
+ *
840
+ * @param path - The filesystem path to inspect without following links.
841
+ * @returns `true` only for a successful `lstat` reporting a directory.
842
+ */
843
+ function isRealDirectory(path) {
844
+ const status = attempt(() => lstatSync(path).isDirectory());
845
+ return status.success && status.value;
846
+ }
847
+ /**
848
+ * Compute a bounded-memory SHA-256 digest for one file.
849
+ *
850
+ * @param path - The file to read.
851
+ * @returns Its lowercase SHA-256 digest.
852
+ */
853
+ function digestFile(path) {
854
+ const initial = lstatSync(path);
855
+ if (!initial.isFile() || initial.isSymbolicLink() || initial.nlink !== 1) throw new Error(`Digest source is not a physical file at ${path}`);
856
+ if (initial.size > MAX_ARTIFACT_BYTES) throw new Error(`Digest source exceeds ${MAX_ARTIFACT_BYTES} bytes at ${path}`);
857
+ const handle = openSync(path, "r");
858
+ const hash = createHash("sha256");
859
+ const buffer = Buffer.allocUnsafe(65536);
860
+ const read = attempt(() => {
861
+ const current = fstatSync(handle);
862
+ if (!current.isFile() || current.nlink !== 1 || current.dev !== initial.dev || current.ino !== initial.ino) throw new Error(`Digest source changed before reading at ${path}`);
863
+ let size = 0;
864
+ for (;;) {
865
+ const length = readSync(handle, buffer, 0, buffer.byteLength, null);
866
+ if (length === 0) break;
867
+ size += length;
868
+ if (size > MAX_ARTIFACT_BYTES) throw new Error(`Digest source exceeds ${MAX_ARTIFACT_BYTES} bytes at ${path}`);
869
+ hash.update(buffer.subarray(0, length));
870
+ }
871
+ const after = fstatSync(handle);
872
+ const pathStatus = lstatSync(path);
873
+ if (size !== current.size || after.dev !== current.dev || after.ino !== current.ino || after.mtimeMs !== current.mtimeMs || after.size !== current.size || !pathStatus.isFile() || pathStatus.isSymbolicLink() || pathStatus.nlink !== 1 || pathStatus.dev !== current.dev || pathStatus.ino !== current.ino) throw new Error(`Digest source changed while reading at ${path}`);
874
+ return hash.digest("hex");
875
+ });
876
+ const closed = attempt(() => closeSync(handle));
877
+ if (!read.success) throw read.error;
878
+ if (!closed.success) throw closed.error;
879
+ return read.value;
880
+ }
881
+ /**
882
+ * Compute SHA-256 from exact lowercase hexadecimal bytes without decoding the whole value at once.
883
+ *
884
+ * @param hex - Exact hexadecimal bytes.
885
+ * @returns Their lowercase SHA-256 digest.
886
+ */
887
+ function digestHex(hex) {
888
+ if (hex.length > MAX_ARTIFACT_HEX_LENGTH || !HEX_PATTERN.test(hex)) throw new Error("Digest input is not bounded exact hexadecimal bytes");
889
+ const hash = createHash("sha256");
890
+ for (let offset = 0; offset < hex.length; offset += 131072) hash.update(Buffer.from(hex.slice(offset, offset + 131072), "hex"));
891
+ return hash.digest("hex");
892
+ }
893
+ /**
894
+ * Compute SHA-256 from UTF-8 text.
895
+ *
896
+ * @param value - The text to hash.
897
+ * @returns Its lowercase SHA-256 digest.
898
+ */
899
+ function digestText(value) {
900
+ return createHash("sha256").update(value, "utf8").digest("hex");
901
+ }
902
+ /**
903
+ * Render the local pointer written when an upstream dependency guide is not vendored yet.
904
+ *
905
+ * @param source - The canonical `guides/src/<name>.md` source path.
906
+ * @returns The deterministic pointer content.
907
+ */
908
+ function guideStub(source) {
909
+ return `> Vendored guide for @orkestrel/${source.slice(11, source.length - 3)} — run \`scaffold pull\` to fetch it.\n`;
910
+ }
911
+ /**
912
+ * Remove the canonical Orkestrel scope from a dependency name.
913
+ *
914
+ * @param name - A dependency name.
915
+ * @returns Its unscoped member when canonical, otherwise the original name.
916
+ */
917
+ function packageShortName(name) {
918
+ return name.startsWith("@orkestrel/") ? name.slice(11) : name;
919
+ }
920
+ /**
921
+ * Read bounded physical local guide mirrors for declared dependencies.
922
+ *
923
+ * @param target - The package root.
924
+ * @param dependencies - The declared dependencies whose mirrors are eligible.
925
+ * @returns Existing guide content keyed by dependency name.
926
+ */
927
+ function readGuideReferences(target, dependencies) {
928
+ const current = {};
929
+ for (const dependency of dependencies) {
930
+ const path = `guides/src/${packageShortName(dependency.name)}.md`;
931
+ const full = resolvePhysicalPath(target, path, "TARGET", "target");
932
+ const status = attempt(() => lstatSync(full));
933
+ if (!status.success) {
934
+ if (isMissingPathError(status.error)) continue;
935
+ throw new ScaffoldError("TARGET", `Failed to inspect local guide at ${path}`, {
936
+ path,
937
+ error: status.error
938
+ });
939
+ }
940
+ if (!status.value.isFile() || status.value.isSymbolicLink() || status.value.nlink !== 1) throw new ScaffoldError("TARGET", `Local guide is not a physical file at ${path}`, { path });
941
+ current[dependency.name] = readFileText(target, path, "TARGET", "target");
942
+ }
943
+ return Object.freeze(current);
944
+ }
945
+ /**
946
+ * Assemble one synchronization report from already ordered guide and version outcomes.
947
+ *
948
+ * @param target - The target package root.
949
+ * @param guides - Guide outcomes.
950
+ * @param versions - Version outcomes.
951
+ * @returns The derived whole report.
952
+ */
953
+ function syncReportOf(target, guides, versions) {
954
+ const failed = [...guides, ...versions].filter((entry) => entry.freshness === "missing" || entry.freshness === "failed").length;
955
+ return {
956
+ target,
957
+ guides,
958
+ versions,
959
+ clean: failed === 0 && guides.every((guide) => guide.freshness === "current") && versions.every((version) => version.freshness === "current"),
960
+ failed
961
+ };
962
+ }
963
+ /**
964
+ * Locate this MODULE's own installed package root — the nearest ancestor of
965
+ * `import.meta.url` holding a `package.json` — and return its vendored
966
+ * `dist/host` data root. THE single source of truth for the default
967
+ * `Materializer` / `scaffold` bin host: once installed, walking up from the
968
+ * module's own file (not `process.cwd()`, which points at whichever project
969
+ * happens to be running) resolves to `node_modules/@orkestrel/scaffold`, the
970
+ * correct default host — the package ships its vendored data with itself.
971
+ * `dist/host` may not exist yet when this resolves from SOURCE under a test
972
+ * runner; that is fine — existence is checked at the point of use, not here.
973
+ *
974
+ * @returns The absolute vendored `dist/host` path.
975
+ * @throws `ScaffoldError('TARGET', …)` when no ancestor of this module's own
976
+ * location holds a `package.json`.
977
+ *
978
+ * @example
979
+ * ```ts
980
+ * import { hostRoot } from '@orkestrel/scaffold/server'
981
+ *
982
+ * hostRoot() // '/…/node_modules/@orkestrel/scaffold/dist/host'
983
+ * ```
984
+ */
985
+ function hostRoot() {
986
+ let dir = dirname(fileURLToPath(import.meta.url));
987
+ for (;;) {
988
+ if (existsSync(join(dir, "package.json"))) return join(dir, "dist", "host");
989
+ const parent = dirname(dir);
990
+ if (parent === dir) throw new ScaffoldError("TARGET", "No package root found above the module location", { module: import.meta.url });
991
+ dir = parent;
992
+ }
993
+ }
994
+ /**
995
+ * Resolve the deepest existing ancestor of a path through the real filesystem.
996
+ *
997
+ * @param path - The absolute or relative path to resolve.
998
+ * @returns A path whose existing prefix has been resolved through symlinks.
999
+ */
1000
+ function resolveRealPath(path) {
1001
+ if (!isFilesystemPath(path)) throw new ScaffoldError("TARGET", "Filesystem path is malformed or exceeds its bounds");
1002
+ let current = resolve(path);
1003
+ const unresolved = [];
1004
+ while (!existsSync(current)) {
1005
+ const parent = dirname(current);
1006
+ if (parent === current || unresolved.length >= 64) throw new ScaffoldError("TARGET", "Filesystem path has no bounded existing ancestor", {
1007
+ path,
1008
+ limit: 64
1009
+ });
1010
+ unresolved.unshift(relative(parent, current));
1011
+ current = parent;
1012
+ }
1013
+ let physical = realpathSync(current);
1014
+ for (const segment of unresolved) physical = join(physical, segment);
1015
+ return physical;
1016
+ }
1017
+ /**
1018
+ * Resolve a path beneath a declared root and reject lexical or symlink escape.
1019
+ *
1020
+ * @param root - The containing filesystem root.
1021
+ * @param path - The candidate path, relative or absolute.
1022
+ * @param code - The coded error to raise on escape.
1023
+ * @param boundary - The boundary name used in diagnostics.
1024
+ * @returns The lexically resolved candidate after realpath-aware validation.
1025
+ */
1026
+ function resolveContainedPath(root, path, code, boundary) {
1027
+ if (!isFilesystemPath(root) || !isFilesystemPath(path)) throw new ScaffoldError(code, `${boundary} path is malformed or exceeds its bounds`, {
1028
+ path,
1029
+ root
1030
+ });
1031
+ const resolvedRoot = resolveRealPath(resolve(root));
1032
+ const resolvedCandidate = resolveRealPath(resolve(root, path));
1033
+ if (resolvedCandidate !== resolvedRoot && !resolvedCandidate.startsWith(resolvedRoot + sep)) throw new ScaffoldError(code, `Path "${path}" escapes the ${boundary} root`, {
1034
+ path,
1035
+ root
1036
+ });
1037
+ return resolve(root, path);
1038
+ }
1039
+ /**
1040
+ * Resolve a contained path whose existing ancestor chain contains no links.
1041
+ *
1042
+ * @param root - The trusted lexical and physical root.
1043
+ * @param path - The portable root-relative path.
1044
+ * @param code - The coded error to raise on failure.
1045
+ * @param boundary - The boundary name used in diagnostics.
1046
+ * @returns The contained destination.
1047
+ * @throws `ScaffoldError` when containment fails or an ancestor is not a real directory.
1048
+ */
1049
+ function resolvePhysicalPath(root, path, code, boundary) {
1050
+ const resolvedRoot = resolve(root);
1051
+ const destination = resolveContainedPath(resolvedRoot, path, code, boundary);
1052
+ if (destination === resolvedRoot) {
1053
+ const status = attempt(() => lstatSync(resolvedRoot));
1054
+ if (!status.success || !status.value.isDirectory() || status.value.isSymbolicLink()) throw new ScaffoldError(code, `${boundary} root is not a real directory`, {
1055
+ path,
1056
+ root,
1057
+ ...status.success ? {} : { error: status.error }
1058
+ });
1059
+ return destination;
1060
+ }
1061
+ let parent = dirname(destination);
1062
+ for (;;) {
1063
+ if (existsSync(parent)) {
1064
+ const status = attempt(() => lstatSync(parent));
1065
+ if (!status.success || !status.value.isDirectory() || status.value.isSymbolicLink()) throw new ScaffoldError(code, `${boundary} parent is not a real directory`, {
1066
+ path,
1067
+ parent,
1068
+ ...status.success ? {} : { error: status.error }
1069
+ });
1070
+ }
1071
+ if (parent === resolvedRoot) break;
1072
+ const next = dirname(parent);
1073
+ if (next === parent) throw new ScaffoldError(code, `${boundary} parent escapes its root`, {
1074
+ path,
1075
+ root
1076
+ });
1077
+ parent = next;
1078
+ }
1079
+ return destination;
1080
+ }
1081
+ /**
1082
+ * Revalidate one captured physical-directory identity without following links.
1083
+ *
1084
+ * @param anchor - The captured path, device, and inode.
1085
+ * @param boundary - The boundary name used in diagnostics.
1086
+ * @throws `ScaffoldError('WRITE', ...)` when the directory is missing or changed.
1087
+ */
1088
+ function validateWriteAnchor(anchor, boundary) {
1089
+ const status = attempt(() => lstatSync(anchor.path));
1090
+ if (!status.success || !status.value.isDirectory() || status.value.isSymbolicLink() || status.value.dev !== anchor.device || status.value.ino !== anchor.inode) throw new ScaffoldError("WRITE", `Write transaction ${boundary} changed`, {
1091
+ path: anchor.path,
1092
+ ...status.success ? {} : { error: status.error }
1093
+ });
1094
+ }
1095
+ /**
1096
+ * Create a physical directory path one segment at a time behind captured identities.
1097
+ *
1098
+ * @param path - The absolute directory path to establish.
1099
+ * @param boundary - The boundary name used in diagnostics.
1100
+ * @returns The final directory anchor and the subset created by this call.
1101
+ */
1102
+ function createWriteDirectory(path, boundary) {
1103
+ const destination = resolve(path);
1104
+ const missing = [];
1105
+ let current = destination;
1106
+ let existing;
1107
+ for (;;) {
1108
+ const status = attempt(() => lstatSync(current));
1109
+ if (status.success) {
1110
+ if (!status.value.isDirectory() || status.value.isSymbolicLink()) throw new ScaffoldError("WRITE", `${boundary} is not a physical directory`, { path: current });
1111
+ existing = Object.freeze({
1112
+ path: current,
1113
+ device: status.value.dev,
1114
+ inode: status.value.ino
1115
+ });
1116
+ break;
1117
+ }
1118
+ if (!isMissingPathError(status.error)) throw new ScaffoldError("WRITE", `Failed to inspect ${boundary}`, {
1119
+ path: current,
1120
+ error: status.error
1121
+ });
1122
+ missing.push(current);
1123
+ const parent = dirname(current);
1124
+ if (parent === current) throw new ScaffoldError("WRITE", `Cannot establish ${boundary}`, { path: destination });
1125
+ current = parent;
1126
+ }
1127
+ const created = [];
1128
+ let anchor = existing;
1129
+ for (const directory of [...missing].reverse()) {
1130
+ validateWriteAnchor(anchor, boundary);
1131
+ mkdirSync(directory);
1132
+ validateWriteAnchor(anchor, boundary);
1133
+ const status = lstatSync(directory);
1134
+ if (!status.isDirectory() || status.isSymbolicLink()) throw new ScaffoldError("WRITE", `${boundary} changed while creating it`, { path: directory });
1135
+ anchor = Object.freeze({
1136
+ path: directory,
1137
+ device: status.dev,
1138
+ inode: status.ino
1139
+ });
1140
+ created.push(anchor);
1141
+ }
1142
+ return {
1143
+ anchor,
1144
+ created
1145
+ };
1146
+ }
1147
+ /**
1148
+ * Revalidate every private directory owned by a write transaction.
1149
+ *
1150
+ * @param transaction - The nominal transaction state.
1151
+ * @throws `ScaffoldError('WRITE', ...)` when any private directory changed.
1152
+ */
1153
+ function validateWriteDirectories(transaction) {
1154
+ validateWriteAnchor(transaction.anchor, "anchor");
1155
+ for (const parent of transaction.parents) validateWriteAnchor(parent, "parent");
1156
+ for (const directory of transaction.directories) validateWriteAnchor(directory, "private directory");
1157
+ }
1158
+ /**
1159
+ * Revalidate the transaction target against its original or transaction-owned identity.
1160
+ *
1161
+ * @param transaction - The nominal transaction state.
1162
+ * @param owned - A target directory created by the current commit.
1163
+ * @throws `ScaffoldError('WRITE', ...)` when the target identity changed.
1164
+ */
1165
+ function validateWriteTarget(transaction, owned) {
1166
+ validateWriteDirectories(transaction);
1167
+ const expected = transaction.existing ?? owned;
1168
+ const status = attempt(() => lstatSync(transaction.target));
1169
+ if (expected === void 0) {
1170
+ if (status.success || !isMissingPathError(status.error)) throw new ScaffoldError("WRITE", "Write transaction target changed", {
1171
+ target: transaction.target,
1172
+ ...status.success ? {} : { error: status.error }
1173
+ });
1174
+ return;
1175
+ }
1176
+ if (!status.success || !status.value.isDirectory() || status.value.isSymbolicLink() || status.value.dev !== expected.device || status.value.ino !== expected.inode) throw new ScaffoldError("WRITE", "Write transaction target changed", {
1177
+ target: transaction.target,
1178
+ ...status.success ? {} : { error: status.error }
1179
+ });
1180
+ }
1181
+ /**
1182
+ * Remove an uncommitted or already-committed write transaction's private residue.
1183
+ *
1184
+ * @param transaction - The transaction to discard.
1185
+ * @throws `ScaffoldError('WRITE', ...)` when its private root cannot be removed.
1186
+ */
1187
+ function discardWriteTransaction(transaction) {
1188
+ if (!(transaction instanceof WriteTransaction)) throw new ScaffoldError("WRITE", "Write transaction identity is invalid");
1189
+ validateWriteDirectories(transaction);
1190
+ const discarded = attempt(() => rmSync(transaction.root, { recursive: true }));
1191
+ if (!discarded.success) throw new ScaffoldError("WRITE", "Failed to discard write transaction", {
1192
+ target: transaction.target,
1193
+ root: transaction.root,
1194
+ error: discarded.error
1195
+ });
1196
+ for (const parent of [...transaction.parents].reverse()) {
1197
+ const removed = attempt(() => {
1198
+ const status = lstatSync(parent.path);
1199
+ if (!status.isDirectory() || status.isSymbolicLink() || status.dev !== parent.device || status.ino !== parent.inode) throw new Error(`write transaction parent changed at ${parent.path}`);
1200
+ rmdirSync(parent.path);
1201
+ });
1202
+ if (!removed.success && !isMissingPathError(removed.error)) throw new ScaffoldError("WRITE", "Failed to discard write transaction parent", {
1203
+ target: transaction.target,
1204
+ root: transaction.root,
1205
+ parent: parent.path,
1206
+ error: removed.error
1207
+ });
1208
+ }
1209
+ }
1210
+ /**
1211
+ * Promote a complete staged set and roll every earlier destination back when
1212
+ * any later promotion fails.
1213
+ *
1214
+ * @param transaction - Same-volume sibling staging state.
1215
+ * @param paths - Portable target-relative files to promote.
1216
+ * @throws `ScaffoldError('WRITE', ...)` with recovery details on failure.
1217
+ */
1218
+ function commitWriteTransaction(transaction, paths) {
1219
+ if (!(transaction instanceof WriteTransaction)) throw new ScaffoldError("WRITE", "Write transaction identity is invalid");
1220
+ const requested = parsePortablePaths(paths, MAX_COLLECTION_ITEMS);
1221
+ const conflict = requested === void 0 ? void 0 : findFileConflict(requested);
1222
+ if (requested === void 0 || conflict !== void 0 || requested.length !== transaction.expectations.length || requested.some((path) => !transaction.expectations.some((expectation) => expectation.path === path)) || transaction.expectations.some((expectation) => !requested.some((path) => path === expectation.path))) {
1223
+ discardWriteTransaction(transaction);
1224
+ throw new ScaffoldError("WRITE", requested !== void 0 && conflict === void 0 ? "Write transaction paths do not match its expectations" : requested !== void 0 ? `Write transaction collision between "${conflict?.[0]}" and "${conflict?.[1]}"` : "Write transaction paths are malformed", {
1225
+ paths: conflict ?? requested ?? [],
1226
+ committed: false
1227
+ });
1228
+ }
1229
+ const preflight = attempt(() => {
1230
+ validateWriteTarget(transaction, void 0);
1231
+ validateWriteDirectories(transaction);
1232
+ for (const path of requested) {
1233
+ if (!isPortablePath(path)) throw new ScaffoldError("WRITE", `Invalid transaction path at ${path}`, { path });
1234
+ const expectation = transaction.expectations.find((candidate) => candidate.path === path);
1235
+ if (expectation === void 0) throw new ScaffoldError("WRITE", `Missing transaction expectation at ${path}`, { path });
1236
+ const staged = resolvePhysicalPath(transaction.stage, path, "WRITE", "staging");
1237
+ const stagedStatus = attempt(() => lstatSync(staged));
1238
+ if (!stagedStatus.success || stagedStatus.value.isSymbolicLink() || !stagedStatus.value.isFile() || stagedStatus.value.nlink !== 1) throw new ScaffoldError("WRITE", `Invalid staged transaction entry at ${path}`, {
1239
+ path,
1240
+ error: stagedStatus.success ? void 0 : stagedStatus.error
1241
+ });
1242
+ const destination = resolvePhysicalPath(transaction.target, path, "WRITE", "target");
1243
+ const current = attempt(() => lstatSync(destination));
1244
+ if (expectation.shape === "absent") {
1245
+ if (current.success || !isMissingPathError(current.error)) throw new ScaffoldError("WRITE", `Transaction destination changed at ${path}`, {
1246
+ path,
1247
+ error: current.success ? void 0 : current.error
1248
+ });
1249
+ continue;
1250
+ }
1251
+ if (!current.success) throw new ScaffoldError("WRITE", `Transaction destination changed at ${path}`, {
1252
+ path,
1253
+ error: current.error
1254
+ });
1255
+ const shape = current.value.isDirectory() ? "directory" : "file";
1256
+ const content = shape === "file" ? attempt(() => digestFile(destination)) : void 0;
1257
+ if (current.value.isSymbolicLink() || current.value.isFile() && current.value.nlink !== 1 || current.value.isDirectory() !== stagedStatus.value.isDirectory() || current.value.isDirectory() || shape !== expectation.shape || current.value.dev !== expectation.device || current.value.ino !== expectation.inode || current.value.mtimeMs !== expectation.modified || current.value.size !== expectation.size || content !== void 0 && (!content.success || content.value !== expectation.digest)) throw new ScaffoldError("WRITE", `Transaction destination changed at ${path}`, {
1258
+ path,
1259
+ ...content?.success === false ? { error: content.error } : {}
1260
+ });
1261
+ }
1262
+ });
1263
+ if (!preflight.success) {
1264
+ const cleanup = attempt(() => discardWriteTransaction(transaction));
1265
+ throw new ScaffoldError("WRITE", "Write transaction failed preflight", {
1266
+ target: transaction.target,
1267
+ error: preflight.error,
1268
+ committed: false,
1269
+ ...cleanup.success ? {} : { cleanup: cleanup.error }
1270
+ });
1271
+ }
1272
+ const preserved = [];
1273
+ const promoted = [];
1274
+ const createdParents = [];
1275
+ let ownedTarget;
1276
+ const committed = attempt(() => {
1277
+ for (const path of requested) {
1278
+ validateWriteTarget(transaction, ownedTarget);
1279
+ validateWriteDirectories(transaction);
1280
+ if (!isPortablePath(path)) throw new ScaffoldError("WRITE", `Invalid transaction path at ${path}`, { path });
1281
+ const staged = resolvePhysicalPath(transaction.stage, path, "WRITE", "staging");
1282
+ const stagedStatus = attempt(() => lstatSync(staged));
1283
+ if (!stagedStatus.success || stagedStatus.value.isSymbolicLink() || !stagedStatus.value.isFile() || stagedStatus.value.nlink !== 1) throw new ScaffoldError("WRITE", `Invalid staged transaction entry at ${path}`, {
1284
+ path,
1285
+ error: stagedStatus.success ? void 0 : stagedStatus.error
1286
+ });
1287
+ const destination = resolvePhysicalPath(transaction.target, path, "WRITE", "target");
1288
+ const backup = resolvePhysicalPath(transaction.backup, path, "WRITE", "backup");
1289
+ const expectation = transaction.expectations.find((candidate) => candidate.path === path);
1290
+ if (expectation === void 0) throw new ScaffoldError("WRITE", `Missing transaction expectation at ${path}`, { path });
1291
+ const current = attempt(() => lstatSync(destination));
1292
+ if (expectation.shape === "absent") {
1293
+ if (current.success || !isMissingPathError(current.error)) throw new ScaffoldError("WRITE", `Transaction destination changed at ${path}`, {
1294
+ path,
1295
+ error: current.success ? void 0 : current.error
1296
+ });
1297
+ } else if (current.success) {
1298
+ const shape = current.value.isDirectory() ? "directory" : "file";
1299
+ const content = shape === "file" ? attempt(() => digestFile(destination)) : void 0;
1300
+ if (current.value.isSymbolicLink() || current.value.isFile() && current.value.nlink !== 1 || current.value.isDirectory() !== stagedStatus.value.isDirectory() || current.value.isDirectory() || shape !== expectation.shape || current.value.dev !== expectation.device || current.value.ino !== expectation.inode || current.value.mtimeMs !== expectation.modified || current.value.size !== expectation.size || content !== void 0 && (!content.success || content.value !== expectation.digest)) throw new ScaffoldError("WRITE", `Transaction destination changed at ${path}`, {
1301
+ path,
1302
+ ...content?.success === false ? { error: content.error } : {}
1303
+ });
1304
+ mkdirSync(dirname(backup), { recursive: true });
1305
+ renameSync(destination, backup);
1306
+ preserved.push(path);
1307
+ const capturedStatus = lstatSync(backup);
1308
+ const capturedContent = digestFile(backup);
1309
+ if (!capturedStatus.isFile() || capturedStatus.isSymbolicLink() || capturedStatus.nlink !== 1 || capturedStatus.dev !== expectation.device || capturedStatus.ino !== expectation.inode || capturedStatus.mtimeMs !== expectation.modified || capturedStatus.size !== expectation.size || capturedContent !== expectation.digest) throw new ScaffoldError("WRITE", `Transaction destination changed during preservation at ${path}`, { path });
1310
+ } else throw new ScaffoldError("WRITE", `Transaction destination changed at ${path}`, {
1311
+ path,
1312
+ error: current.error
1313
+ });
1314
+ const missingParents = [];
1315
+ let parent = dirname(destination);
1316
+ while (parent !== dirname(transaction.target)) {
1317
+ const status = attempt(() => lstatSync(parent));
1318
+ if (status.success) {
1319
+ if (transaction.existing === void 0 && ownedTarget === void 0 && parent === transaction.target) throw new ScaffoldError("WRITE", "Write transaction target changed", { target: transaction.target });
1320
+ break;
1321
+ }
1322
+ if (!isMissingPathError(status.error)) throw status.error;
1323
+ missingParents.push(parent);
1324
+ parent = dirname(parent);
1325
+ }
1326
+ for (const createdParent of [...missingParents].reverse()) {
1327
+ mkdirSync(createdParent);
1328
+ const status = lstatSync(createdParent);
1329
+ if (!status.isDirectory() || status.isSymbolicLink()) throw new ScaffoldError("WRITE", `Write transaction parent changed at ${createdParent}`, {
1330
+ target: transaction.target,
1331
+ parent: createdParent
1332
+ });
1333
+ if (!createdParents.some((candidate) => candidate.path === createdParent)) {
1334
+ const captured = Object.freeze({
1335
+ path: createdParent,
1336
+ device: status.dev,
1337
+ inode: status.ino
1338
+ });
1339
+ createdParents.push(captured);
1340
+ if (createdParent === transaction.target) ownedTarget = captured;
1341
+ }
1342
+ }
1343
+ if (transaction.existing === void 0 && ownedTarget === void 0) throw new ScaffoldError("WRITE", "Write transaction target was not created safely", { target: transaction.target });
1344
+ linkSync(staged, destination);
1345
+ const linkedStatus = lstatSync(destination);
1346
+ const stagedAfterLink = lstatSync(staged);
1347
+ if (!linkedStatus.isFile() || linkedStatus.isSymbolicLink() || linkedStatus.dev !== stagedStatus.value.dev || linkedStatus.ino !== stagedStatus.value.ino || stagedAfterLink.dev !== linkedStatus.dev || stagedAfterLink.ino !== linkedStatus.ino) throw new ScaffoldError("WRITE", `Staged promotion changed at ${path}`, { path });
1348
+ const unlinked = attempt(() => rmSync(staged));
1349
+ if (!unlinked.success) {
1350
+ const cleanup = attempt(() => {
1351
+ const destinationStatus = lstatSync(destination);
1352
+ if (destinationStatus.dev !== linkedStatus.dev || destinationStatus.ino !== linkedStatus.ino) throw new Error(`promoted file changed at ${path}`);
1353
+ rmSync(destination);
1354
+ });
1355
+ throw new ScaffoldError("WRITE", `Failed to release staged link at ${path}`, {
1356
+ path,
1357
+ error: unlinked.error,
1358
+ cleanup: cleanup.success ? void 0 : cleanup.error
1359
+ });
1360
+ }
1361
+ const promotedStatus = lstatSync(destination);
1362
+ promoted.push(Object.freeze({
1363
+ path,
1364
+ shape: "file",
1365
+ device: promotedStatus.dev,
1366
+ inode: promotedStatus.ino,
1367
+ modified: promotedStatus.mtimeMs,
1368
+ size: promotedStatus.size,
1369
+ digest: digestFile(destination)
1370
+ }));
1371
+ }
1372
+ });
1373
+ if (!committed.success) {
1374
+ const recoveryErrors = [];
1375
+ let preserveResidue = false;
1376
+ for (const promotion of [...promoted].reverse()) {
1377
+ const removed = attempt(() => {
1378
+ validateWriteTarget(transaction, ownedTarget);
1379
+ const destination = resolvePhysicalPath(transaction.target, promotion.path, "WRITE", "target");
1380
+ const status = lstatSync(destination);
1381
+ const shape = status.isDirectory() ? "directory" : "file";
1382
+ const digest = shape === "file" ? attempt(() => digestFile(destination)) : void 0;
1383
+ if (status.isSymbolicLink() || shape !== promotion.shape || status.dev !== promotion.device || status.ino !== promotion.inode || digest !== void 0 && (!digest.success || status.mtimeMs !== promotion.modified || status.size !== promotion.size || digest.value !== promotion.digest)) throw new ScaffoldError("WRITE", `Promoted transaction destination changed at ${promotion.path}`, {
1384
+ path: promotion.path,
1385
+ ...digest?.success === false ? { error: digest.error } : {}
1386
+ });
1387
+ if (shape === "directory") {
1388
+ const recovery = resolvePhysicalPath(transaction.stage, `.rollback/${promotion.path}`, "WRITE", "rollback");
1389
+ mkdirSync(dirname(recovery), { recursive: true });
1390
+ validateWriteTarget(transaction, ownedTarget);
1391
+ renameSync(destination, recovery);
1392
+ preserveResidue = true;
1393
+ } else rmSync(destination);
1394
+ });
1395
+ if (!removed.success) recoveryErrors.push(removed.error);
1396
+ }
1397
+ for (const path of [...preserved].reverse()) {
1398
+ const restored = attempt(() => {
1399
+ validateWriteTarget(transaction, ownedTarget);
1400
+ validateWriteDirectories(transaction);
1401
+ const destination = resolvePhysicalPath(transaction.target, path, "WRITE", "target");
1402
+ const backup = resolvePhysicalPath(transaction.backup, path, "WRITE", "backup");
1403
+ mkdirSync(dirname(destination), { recursive: true });
1404
+ linkSync(backup, destination);
1405
+ rmSync(backup);
1406
+ });
1407
+ if (!restored.success) recoveryErrors.push(restored.error);
1408
+ }
1409
+ for (const parent of [...createdParents].reverse()) {
1410
+ const removed = attempt(() => {
1411
+ validateWriteTarget(transaction, ownedTarget);
1412
+ validateWriteDirectories(transaction);
1413
+ validateWriteAnchor(parent, "created parent");
1414
+ rmdirSync(parent.path);
1415
+ });
1416
+ if (!removed.success && !isMissingPathError(removed.error)) recoveryErrors.push(removed.error);
1417
+ }
1418
+ const cleanup = recoveryErrors.length === 0 && !preserveResidue ? attempt(() => discardWriteTransaction(transaction)) : void 0;
1419
+ throw new ScaffoldError("WRITE", "Write transaction failed before commit", {
1420
+ target: transaction.target,
1421
+ root: transaction.root,
1422
+ error: committed.error,
1423
+ committed: false,
1424
+ recovery: recoveryErrors,
1425
+ residue: preserveResidue ? transaction.root : void 0,
1426
+ cleanup: cleanup?.success === false ? cleanup.error : void 0
1427
+ });
1428
+ }
1429
+ validateWriteDirectories(transaction);
1430
+ const cleanup = attempt(() => rmSync(transaction.root, { recursive: true }));
1431
+ if (!cleanup.success) throw new ScaffoldError("WRITE", "Write transaction committed with cleanup residue", {
1432
+ target: transaction.target,
1433
+ root: transaction.root,
1434
+ error: cleanup.error,
1435
+ committed: true
1436
+ });
1437
+ }
1438
+ /**
1439
+ * Resolve and completely preflight the canonical guide destinations a sync may write.
18
1440
  *
19
- * @returns The absolute vendored `dist/host` path.
20
- * @throws `ScaffoldError('TARGET', …)` when no ancestor of this module's own
21
- * location holds a `package.json`.
22
- *
23
- * @example
24
- * ```ts
25
- * import { hostRoot } from '@orkestrel/scaffold/server'
1441
+ * @param guides - Structurally validated behind guide results.
1442
+ * @param target - The repository root that owns `guides/src`.
1443
+ * @returns Each guide paired with its contained destination.
1444
+ * @throws `ScaffoldError('WRITE', ...)` before mutation for ownership, collision,
1445
+ * containment, or existing filesystem-shape violations.
1446
+ */
1447
+ function resolveGuideWrites(guides, target) {
1448
+ const copied = attempt(() => guides.map((guide) => {
1449
+ const name = guide.name;
1450
+ const path = guide.path;
1451
+ const content = guide.content;
1452
+ const freshness = guide.freshness;
1453
+ const note = guide.note;
1454
+ const baseline = guide.baseline;
1455
+ return {
1456
+ name,
1457
+ path,
1458
+ content,
1459
+ freshness,
1460
+ ...note === void 0 ? {} : { note },
1461
+ ...baseline === void 0 ? {} : { baseline }
1462
+ };
1463
+ }));
1464
+ if (!copied.success) throw new ScaffoldError("WRITE", "Sync guides could not be read safely", { error: copied.error });
1465
+ const behind = copied.value.filter((guide) => guide.freshness === "behind");
1466
+ const paths = [];
1467
+ for (const guide of behind) {
1468
+ if (!DEPENDENCY_NAME_PATTERN.test(guide.name)) throw new ScaffoldError("WRITE", `Invalid guide dependency name at ${guide.name}`, { name: guide.name });
1469
+ const expected = `guides/src/${packageShortName(guide.name)}.md`;
1470
+ if (guide.path !== expected) throw new ScaffoldError("WRITE", `Guide path "${guide.path}" does not match dependency ${guide.name}`, {
1471
+ path: guide.path,
1472
+ expected
1473
+ });
1474
+ paths.push(guide.path);
1475
+ }
1476
+ const conflict = findFileConflict(paths);
1477
+ if (conflict !== void 0) throw new ScaffoldError("WRITE", `Guide destination collision between "${conflict[0]}" and "${conflict[1]}"`, { paths: conflict });
1478
+ const resolvedTarget = resolve(target);
1479
+ const destinations = behind.map((guide) => ({
1480
+ guide,
1481
+ destination: resolvePhysicalPath(target, guide.path, "WRITE", "target")
1482
+ }));
1483
+ for (const { guide, destination } of destinations) {
1484
+ if (existsSync(destination)) {
1485
+ const status = attempt(() => lstatSync(destination));
1486
+ if (!status.success || !status.value.isFile() || status.value.isSymbolicLink() || status.value.nlink !== 1) throw new ScaffoldError("WRITE", `Guide destination is not a file at ${guide.path}`, {
1487
+ path: guide.path,
1488
+ ...status.success ? {} : { error: status.error }
1489
+ });
1490
+ }
1491
+ if (!destination.startsWith(resolvedTarget + sep)) throw new ScaffoldError("WRITE", `Guide destination escapes the target for ${guide.path}`, {
1492
+ path: guide.path,
1493
+ target
1494
+ });
1495
+ }
1496
+ return destinations;
1497
+ }
1498
+ /**
1499
+ * Restore quarantined files to their original target-relative paths.
26
1500
  *
27
- * hostRoot() // '/…/node_modules/@orkestrel/scaffold/dist/host'
28
- * ```
1501
+ * @param transaction - The nominal transaction that owns the quarantine and target.
1502
+ * @param paths - The relative paths to restore, in their original move order.
1503
+ * @throws `ScaffoldError('WRITE', …)` after attempting every reverse-order
1504
+ * restoration when one or more files could not be restored.
29
1505
  */
30
- function hostRoot() {
31
- let dir = dirname(fileURLToPath(import.meta.url));
32
- for (;;) {
33
- if (existsSync(join(dir, "package.json"))) return join(dir, "dist", "host");
34
- const parent = dirname(dir);
35
- if (parent === dir) throw new ScaffoldError("TARGET", "No package root found above the module location", { module: import.meta.url });
36
- dir = parent;
1506
+ function restoreFiles(transaction, paths) {
1507
+ const requested = parsePortablePaths(paths, MAX_COLLECTION_ITEMS);
1508
+ if (requested === void 0) throw new ScaffoldError("WRITE", "Quarantine restore paths are malformed", { limit: MAX_COLLECTION_ITEMS });
1509
+ const failed = [];
1510
+ const errors = [];
1511
+ for (const path of [...requested].reverse()) {
1512
+ const restored = attempt(() => {
1513
+ validateWriteTarget(transaction, void 0);
1514
+ const from = resolvePhysicalPath(transaction.backup, path, "WRITE", "quarantine");
1515
+ const to = resolvePhysicalPath(transaction.target, path, "WRITE", "target");
1516
+ const sourceStatus = lstatSync(from);
1517
+ if (!sourceStatus.isFile() || sourceStatus.isSymbolicLink() || sourceStatus.nlink !== 1) throw new ScaffoldError("WRITE", `Invalid quarantined file at ${path}`, { path });
1518
+ mkdirSync(dirname(to), { recursive: true });
1519
+ validateWriteTarget(transaction, void 0);
1520
+ linkSync(from, to);
1521
+ validateWriteTarget(transaction, void 0);
1522
+ rmSync(from);
1523
+ });
1524
+ if (!restored.success) {
1525
+ failed.push(path);
1526
+ errors.push(restored.error);
1527
+ }
37
1528
  }
1529
+ if (failed.length > 0) throw new ScaffoldError("WRITE", "Failed to restore quarantined files", {
1530
+ source: transaction.backup,
1531
+ target: transaction.target,
1532
+ paths: failed,
1533
+ errors
1534
+ });
1535
+ }
1536
+ /**
1537
+ * Atomically promote a staged sibling directory while preserving recoverable state.
1538
+ *
1539
+ * @param staging - The completed staging directory.
1540
+ * @param target - The destination directory to replace.
1541
+ * @param backup - The sibling path reserved for the prior target.
1542
+ * @throws `ScaffoldError('WRITE', …)` with explicit `committed` and recovery paths.
1543
+ */
1544
+ function replaceDirectory(staging, target, backup) {
1545
+ const resolvedStaging = resolve(staging);
1546
+ const resolvedTarget = resolve(target);
1547
+ const resolvedBackup = resolve(backup);
1548
+ const parent = dirname(resolvedTarget);
1549
+ const paths = [
1550
+ resolvedStaging,
1551
+ resolvedTarget,
1552
+ resolvedBackup
1553
+ ];
1554
+ if (dirname(resolvedStaging) !== parent || dirname(resolvedBackup) !== parent) throw new ScaffoldError("WRITE", "Directory replacement paths must share one parent", {
1555
+ staging: resolvedStaging,
1556
+ target: resolvedTarget,
1557
+ backup: resolvedBackup,
1558
+ committed: false
1559
+ });
1560
+ if (new Set(paths.map((path) => path.toLowerCase())).size !== paths.length) throw new ScaffoldError("WRITE", "Directory replacement paths must be distinct", {
1561
+ staging: resolvedStaging,
1562
+ target: resolvedTarget,
1563
+ backup: resolvedBackup,
1564
+ committed: false
1565
+ });
1566
+ const parentStatus = attempt(() => lstatSync(parent));
1567
+ if (!parentStatus.success || !parentStatus.value.isDirectory() || parentStatus.value.isSymbolicLink()) throw new ScaffoldError("WRITE", "Directory replacement parent must be a real directory", {
1568
+ staging: resolvedStaging,
1569
+ target: resolvedTarget,
1570
+ backup: resolvedBackup,
1571
+ committed: false,
1572
+ ...parentStatus.success ? {} : { error: parentStatus.error }
1573
+ });
1574
+ const parentAnchor = Object.freeze({
1575
+ path: parent,
1576
+ device: parentStatus.value.dev,
1577
+ inode: parentStatus.value.ino
1578
+ });
1579
+ const reserved = attempt(() => lstatSync(resolvedBackup));
1580
+ if (reserved.success) throw new ScaffoldError("WRITE", "Directory replacement backup already exists", {
1581
+ staging: resolvedStaging,
1582
+ target: resolvedTarget,
1583
+ backup: resolvedBackup,
1584
+ committed: false
1585
+ });
1586
+ if (!isMissingPathError(reserved.error)) throw new ScaffoldError("WRITE", "Failed to inspect directory replacement backup", {
1587
+ staging: resolvedStaging,
1588
+ target: resolvedTarget,
1589
+ backup: resolvedBackup,
1590
+ committed: false,
1591
+ error: reserved.error
1592
+ });
1593
+ const staged = attempt(() => lstatSync(resolvedStaging));
1594
+ if (!staged.success || !staged.value.isDirectory() || staged.value.isSymbolicLink()) throw new ScaffoldError("WRITE", "Directory replacement staging must be a real directory", {
1595
+ staging: resolvedStaging,
1596
+ target: resolvedTarget,
1597
+ backup: resolvedBackup,
1598
+ committed: false,
1599
+ error: staged.success ? void 0 : staged.error
1600
+ });
1601
+ const stagingAnchor = Object.freeze({
1602
+ path: resolvedStaging,
1603
+ device: staged.value.dev,
1604
+ inode: staged.value.ino
1605
+ });
1606
+ const current = attempt(() => lstatSync(resolvedTarget));
1607
+ if (!current.success && !isMissingPathError(current.error)) throw new ScaffoldError("WRITE", "Failed to inspect directory replacement target", {
1608
+ staging: resolvedStaging,
1609
+ target: resolvedTarget,
1610
+ backup: resolvedBackup,
1611
+ committed: false,
1612
+ error: current.error
1613
+ });
1614
+ if (current.success && (!current.value.isDirectory() || current.value.isSymbolicLink())) throw new ScaffoldError("WRITE", "Directory replacement target must be a real directory", {
1615
+ staging: resolvedStaging,
1616
+ target: resolvedTarget,
1617
+ backup: resolvedBackup,
1618
+ committed: false
1619
+ });
1620
+ const existing = current.success;
1621
+ const targetAnchor = current.success ? Object.freeze({
1622
+ path: resolvedTarget,
1623
+ device: current.value.dev,
1624
+ inode: current.value.ino
1625
+ }) : void 0;
1626
+ let preserved = false;
1627
+ let promoted = false;
1628
+ const replacement = attempt(() => {
1629
+ validateWriteAnchor(parentAnchor, "directory replacement parent");
1630
+ validateWriteAnchor(stagingAnchor, "directory replacement staging");
1631
+ if (existing) {
1632
+ if (targetAnchor === void 0) throw new Error("directory replacement target identity is missing");
1633
+ validateWriteAnchor(targetAnchor, "directory replacement target");
1634
+ renameSync(resolvedTarget, resolvedBackup);
1635
+ preserved = true;
1636
+ validateWriteAnchor(parentAnchor, "directory replacement parent");
1637
+ validateWriteAnchor(Object.freeze({
1638
+ ...targetAnchor,
1639
+ path: resolvedBackup
1640
+ }), "directory replacement backup");
1641
+ const vacated = attempt(() => lstatSync(resolvedTarget));
1642
+ if (vacated.success || !isMissingPathError(vacated.error)) throw new Error("directory replacement target was not vacated");
1643
+ }
1644
+ validateWriteAnchor(parentAnchor, "directory replacement parent");
1645
+ validateWriteAnchor(stagingAnchor, "directory replacement staging");
1646
+ renameSync(resolvedStaging, resolvedTarget);
1647
+ promoted = true;
1648
+ validateWriteAnchor(parentAnchor, "directory replacement parent");
1649
+ validateWriteAnchor(Object.freeze({
1650
+ ...stagingAnchor,
1651
+ path: resolvedTarget
1652
+ }), "directory replacement target");
1653
+ if (existing) {
1654
+ if (targetAnchor === void 0) throw new Error("directory replacement backup identity is missing");
1655
+ validateWriteAnchor(Object.freeze({
1656
+ ...targetAnchor,
1657
+ path: resolvedBackup
1658
+ }), "directory replacement backup");
1659
+ rmSync(resolvedBackup, {
1660
+ recursive: true,
1661
+ force: true
1662
+ });
1663
+ preserved = false;
1664
+ }
1665
+ validateWriteAnchor(parentAnchor, "directory replacement parent");
1666
+ validateWriteAnchor(Object.freeze({
1667
+ ...stagingAnchor,
1668
+ path: resolvedTarget
1669
+ }), "directory replacement target");
1670
+ });
1671
+ if (replacement.success) return;
1672
+ const restored = existing && preserved && !promoted ? attempt(() => {
1673
+ if (targetAnchor === void 0) throw new Error("directory replacement backup identity is missing");
1674
+ validateWriteAnchor(parentAnchor, "directory replacement parent");
1675
+ validateWriteAnchor(Object.freeze({
1676
+ ...targetAnchor,
1677
+ path: resolvedBackup
1678
+ }), "directory replacement backup");
1679
+ const destination = attempt(() => lstatSync(resolvedTarget));
1680
+ if (destination.success || !isMissingPathError(destination.error)) throw new Error("directory replacement target is not vacant");
1681
+ renameSync(resolvedBackup, resolvedTarget);
1682
+ validateWriteAnchor(targetAnchor, "directory replacement target");
1683
+ preserved = false;
1684
+ }) : void 0;
1685
+ throw new ScaffoldError("WRITE", "Directory replacement failed", {
1686
+ staging: resolvedStaging,
1687
+ target: resolvedTarget,
1688
+ backup: preserved ? resolvedBackup : void 0,
1689
+ committed: promoted,
1690
+ error: replacement.error,
1691
+ restore: restored?.success === false ? restored.error : void 0
1692
+ });
38
1693
  }
39
1694
  /**
40
1695
  * Filter a manifest record's entries down to `@orkestrel/`-prefixed keys with
@@ -56,7 +1711,15 @@ function hostRoot() {
56
1711
  */
57
1712
  function selectOrkestrelEntries(value) {
58
1713
  if (!isRecord(value)) return [];
59
- return Object.entries(value).filter((entry) => typeof entry[1] === "string" && entry[0].startsWith("@orkestrel/"));
1714
+ const selected = attempt(() => {
1715
+ const entries = [];
1716
+ for (const [name, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(value))) {
1717
+ if (!Reflect.has(descriptor, "value")) return [];
1718
+ if (typeof descriptor.value === "string" && DEPENDENCY_NAME_PATTERN.test(name)) entries.push([name, descriptor.value]);
1719
+ }
1720
+ return entries;
1721
+ });
1722
+ return selected.success ? selected.value : [];
60
1723
  }
61
1724
  /**
62
1725
  * Reconstruct a `Blueprint` from an EXISTING repo at `target` — the faithful
@@ -65,12 +1728,12 @@ function selectOrkestrelEntries(value) {
65
1728
  *
66
1729
  * @param target - The existing package directory to derive a `Blueprint` from.
67
1730
  * @remarks
68
- * `name` strips the `@orkestrel/` prefix off `manifest.name` a non-`@orkestrel`
69
- * name is a coded `TARGET` failure, since this tool derives only `@orkestrel`
70
- * packages. `surfaces` is read off the LIVE line: every surface the package
71
- * carries has a `src/<surface>/` directory, so each of `'core' | 'browser' |
72
- * 'server'` is included iff that directory exists at `target`; a target with
73
- * NONE of the three is also a coded `TARGET` failure. `dependencies` /
1731
+ * A scoped package name is stripped to its bounded safe short name. An
1732
+ * unscoped name is accepted only when the target is app-only, `private: true`,
1733
+ * and the name satisfies `isWorkspaceName`; every other name is a coded
1734
+ * `TARGET` failure. `src` is derived from `src/<environment>/` and `app` from
1735
+ * `app/<environment>/`; a target with no environment on either axis is also a coded
1736
+ * `TARGET` failure. `dependencies` /
74
1737
  * `peers` are the `@orkestrel/`-prefixed entries of `manifest.dependencies` /
75
1738
  * `manifest.peerDependencies` (a peer flagged `peerDependenciesMeta[name]
76
1739
  * .optional === true` carries `optional: true`). `extras` is EVERY entry of
@@ -82,63 +1745,66 @@ function selectOrkestrelEntries(value) {
82
1745
  * ALSO present in
83
1746
  * `manifest.peerDependencies` or `manifest.dependencies` (e.g. a peer
84
1747
  * dev-installed for local testing) is likewise excluded from `extras` — it
85
- * already surfaces as a `peer`/`dependency` above, and double-counting it as
1748
+ * already appears as a `peer`/`dependency` above, and double-counting it as
86
1749
  * an `extra` would land it in `peers ∩ extras`, a blocking `validateBlueprint`
87
1750
  * gate. `overrides` is always `[]` — derivation cannot know a caller's
88
1751
  * template-override intent.
89
1752
  * @returns The reconstructed `Blueprint`.
90
1753
  * @throws `ScaffoldError('TARGET', …)` when `target`'s manifest is unreadable
91
- * (via `readManifest`), is not valid JSON, its `name` is not `@orkestrel`-
92
- * prefixed, or `target` carries none of the three surface directories.
1754
+ * (via `readManifest`), is not valid JSON, its name is unsafe for its
1755
+ * publication mode, or `target` carries no source or application environment.
93
1756
  *
94
1757
  * @example
95
1758
  * ```ts
96
1759
  * import { deriveBlueprint } from '@orkestrel/scaffold/server'
97
1760
  *
98
- * deriveBlueprint('./packages/router') // { name: 'router', surfaces: ['core', 'server'], … }
1761
+ * deriveBlueprint('./packages/router') // { name: 'router', src: ['core', 'server'], … }
99
1762
  * ```
100
1763
  */
101
1764
  function deriveBlueprint(target) {
102
- const BASELINE_EXTRAS = /* @__PURE__ */ new Set([
1765
+ const baselineExtras = /* @__PURE__ */ new Set([
103
1766
  ...Object.keys(devDependenciesFor([])),
104
1767
  "@orkestrel/guide",
105
1768
  "@orkestrel/scaffold"
106
1769
  ]);
107
- const text = readManifest(target);
108
- let parsed;
109
- try {
110
- parsed = JSON.parse(text);
111
- } catch (error) {
112
- throw new ScaffoldError("TARGET", `Manifest at ${target} is not valid JSON`, {
113
- target,
114
- error
115
- });
116
- }
117
- if (!isRecord(parsed)) throw new ScaffoldError("TARGET", `Manifest at ${target} is not a JSON object`, { target });
118
- const rawName = parsed.name;
119
- if (typeof rawName !== "string" || !rawName.startsWith("@orkestrel/")) throw new ScaffoldError("TARGET", `Manifest name "${String(rawName)}" is not an @orkestrel package`, {
1770
+ const parsed = parseJSON(readManifest(target));
1771
+ if (!isRecord(parsed)) throw new ScaffoldError("TARGET", `Manifest at ${target} is not a valid JSON object`, { target });
1772
+ const src = ENVIRONMENTS.filter((environment) => isRealDirectory(join(target, "src", environment)));
1773
+ const app = ENVIRONMENTS.filter((environment) => isRealDirectory(join(target, "app", environment)));
1774
+ if (src.length === 0 && app.length === 0) throw new ScaffoldError("TARGET", `No source or application environment directory found under ${target}`, { target });
1775
+ const rawName = ownDataValue(parsed, "name");
1776
+ if (typeof rawName !== "string") throw new ScaffoldError("TARGET", "Manifest name must be a string", {
1777
+ target,
1778
+ name: rawName
1779
+ });
1780
+ const scopedValue = rawName.startsWith("@orkestrel/") ? rawName.slice(11) : void 0;
1781
+ const scopedName = isWorkspaceName(scopedValue);
1782
+ const privateAppName = src.length === 0 && ownDataValue(parsed, "private") === true && isWorkspaceName(rawName);
1783
+ if (!scopedName && !privateAppName) throw new ScaffoldError("TARGET", `Manifest name "${String(rawName)}" is neither an @orkestrel package nor a private application`, {
120
1784
  target,
121
1785
  name: rawName
122
1786
  });
123
- const name = rawName.slice(11);
124
- const description = typeof parsed.description === "string" ? parsed.description : void 0;
125
- const keywords = Array.isArray(parsed.keywords) && parsed.keywords.every((word) => typeof word === "string") ? parsed.keywords : [];
126
- const version = typeof parsed.version === "string" ? parsed.version : DEFAULT_VERSION;
127
- const engines = isRecord(parsed.engines) && typeof parsed.engines.node === "string" ? parsed.engines.node : DEFAULT_ENGINES;
128
- const surfaces = [
129
- "core",
130
- "browser",
131
- "server"
132
- ].filter((surface) => existsSync(join(target, "src", surface)));
133
- if (surfaces.length === 0) throw new ScaffoldError("TARGET", `No surface directory found under ${target}/src`, { target });
134
- const dependencies = selectOrkestrelEntries(parsed.dependencies).map(([depName, range]) => ({
1787
+ const name = scopedName ? scopedValue : rawName;
1788
+ const rawDescription = ownDataValue(parsed, "description");
1789
+ const description = typeof rawDescription === "string" ? rawDescription : void 0;
1790
+ const rawKeywords = ownDataValue(parsed, "keywords");
1791
+ const keywords = Array.isArray(rawKeywords) && rawKeywords.every((word) => typeof word === "string") ? rawKeywords : [];
1792
+ const rawVersion = ownDataValue(parsed, "version");
1793
+ const version = typeof rawVersion === "string" ? rawVersion : DEFAULT_VERSION;
1794
+ const rawNode = ownDataValue(ownDataValue(parsed, "engines"), "node");
1795
+ const engines = typeof rawNode === "string" ? rawNode : DEFAULT_ENGINES;
1796
+ const engine = isRealDirectory(join(target, "src", "bin"));
1797
+ const rawDependencies = ownDataValue(parsed, "dependencies");
1798
+ const rawPeerDependencies = ownDataValue(parsed, "peerDependencies");
1799
+ const dependencies = selectOrkestrelEntries(rawDependencies).map(([depName, range]) => ({
135
1800
  name: depName,
136
1801
  range
137
1802
  }));
138
- const peersMeta = isRecord(parsed.peerDependenciesMeta) ? parsed.peerDependenciesMeta : void 0;
139
- const peers = selectOrkestrelEntries(parsed.peerDependencies).map(([depName, range]) => {
140
- const meta = peersMeta !== void 0 ? peersMeta[depName] : void 0;
141
- return isRecord(meta) && meta.optional === true ? {
1803
+ const rawPeersMeta = ownDataValue(parsed, "peerDependenciesMeta");
1804
+ const peersMeta = isRecord(rawPeersMeta) ? rawPeersMeta : void 0;
1805
+ const peers = selectOrkestrelEntries(rawPeerDependencies).map(([depName, range]) => {
1806
+ const meta = ownDataValue(peersMeta, depName);
1807
+ return isRecord(meta) && ownDataValue(meta, "optional") === true ? {
142
1808
  name: depName,
143
1809
  range,
144
1810
  optional: true
@@ -147,21 +1813,25 @@ function deriveBlueprint(target) {
147
1813
  range
148
1814
  };
149
1815
  });
150
- const peerAndDependencyNames = /* @__PURE__ */ new Set([...selectOrkestrelEntries(parsed.peerDependencies).map(([depName]) => depName), ...selectOrkestrelEntries(parsed.dependencies).map(([depName]) => depName)]);
151
- const devDependencies = isRecord(parsed.devDependencies) ? parsed.devDependencies : {};
1816
+ const peerAndDependencyNames = /* @__PURE__ */ new Set([...selectOrkestrelEntries(rawPeerDependencies).map(([depName]) => depName), ...selectOrkestrelEntries(rawDependencies).map(([depName]) => depName)]);
1817
+ const rawDevDependencies = ownDataValue(parsed, "devDependencies");
1818
+ const devDependencies = isRecord(rawDevDependencies) ? rawDevDependencies : {};
1819
+ const extras = Object.entries(devDependencies).filter((entry) => typeof entry[1] === "string").filter(([depName]) => !baselineExtras.has(depName) && !peerAndDependencyNames.has(depName)).map(([depName, range]) => ({
1820
+ name: depName,
1821
+ range
1822
+ }));
152
1823
  return blueprint(name, {
153
- description,
1824
+ ...description === void 0 ? {} : { description },
154
1825
  keywords,
155
- surfaces,
1826
+ src,
1827
+ app,
156
1828
  dependencies,
157
1829
  peers,
158
- extras: Object.entries(devDependencies).filter((entry) => typeof entry[1] === "string").filter(([depName]) => !BASELINE_EXTRAS.has(depName) && !peerAndDependencyNames.has(depName)).map(([depName, range]) => ({
159
- name: depName,
160
- range
161
- })),
1830
+ extras,
162
1831
  version,
163
1832
  engines,
164
- overrides: []
1833
+ overrides: [],
1834
+ engine
165
1835
  });
166
1836
  }
167
1837
  /**
@@ -179,21 +1849,33 @@ function deriveBlueprint(target) {
179
1849
  * ```
180
1850
  */
181
1851
  function isVacant(target) {
182
- if (!existsSync(target)) return true;
183
- if (!statSync(target).isDirectory()) return false;
184
- const entries = readdirSync(target);
185
- return entries.length === 0 || entries.length === 1 && entries[0] === ".git";
1852
+ if (!isFilesystemPath(target)) return false;
1853
+ const status = attempt(() => lstatSync(target));
1854
+ if (!status.success) return isMissingPathError(status.error);
1855
+ if (!status.value.isDirectory() || status.value.isSymbolicLink()) return false;
1856
+ const handle = opendirSync(target);
1857
+ let first;
1858
+ let second = false;
1859
+ try {
1860
+ first = handle.readSync()?.name;
1861
+ second = first === void 0 ? false : handle.readSync() !== null;
1862
+ } finally {
1863
+ handle.closeSync();
1864
+ }
1865
+ if (first === void 0) return true;
1866
+ if (second || first !== ".git") return false;
1867
+ const metadata = lstatSync(join(target, ".git"));
1868
+ return metadata.isDirectory() && !metadata.isSymbolicLink();
186
1869
  }
187
1870
  /**
188
- * Read a target's current content at a set of relative paths into a
189
- * `Record<string, string>` — the I/O that feeds the pure `diffPlan`.
1871
+ * Read a target's current bytes at a set of relative paths into a
1872
+ * byte-exact hexadecimal {@link Snapshot} — the I/O that feeds `diffPlan`.
190
1873
  *
191
1874
  * @param target - The target directory to read from.
192
1875
  * @param paths - The plan-relative artifact paths to probe.
193
- * @returns A record keyed by path; a directory entry maps to `''` (presence
194
- * only a `host`-origin directory artifact is audited by presence, never
195
- * content), an absent path is OMITTED entirely (never an empty-string
196
- * placeholder for a missing file, so `diffPlan` reports it `missing`).
1876
+ * @returns A snapshot keyed by path; each file maps to its exact lowercase
1877
+ * hexadecimal bytes and a directly requested directory maps to `''`
1878
+ * (presence only). An absent path is omitted entirely.
197
1879
  * @throws `ScaffoldError('TARGET', …)` when an EXISTING path fails to read
198
1880
  * (e.g. `EACCES` / `EPERM`) — carries the offending relative `path` (and
199
1881
  * the resolved `full` path) in `context`. An absent path is never an
@@ -204,25 +1886,37 @@ function isVacant(target) {
204
1886
  * import { readTarget } from '@orkestrel/scaffold/server'
205
1887
  *
206
1888
  * readTarget('./packages/router', ['package.json', 'src/core/index.ts'])
207
- * // { 'package.json': '{ "name": }', 'src/core/index.ts': '…' }
1889
+ * // { 'package.json': '7b226e616d65223a…', 'src/core/index.ts': '6578706f7274…' }
208
1890
  * ```
209
1891
  */
210
1892
  function readTarget(target, paths) {
211
- const current = {};
212
- for (const path of paths) {
213
- const full = join(target, path);
1893
+ if (!isFilesystemPath(target)) throw new ScaffoldError("TARGET", "Target path is malformed or exceeds its bounds", { target });
1894
+ const requested = parsePortablePaths(paths, MAX_COLLECTION_ITEMS);
1895
+ if (requested === void 0) throw new ScaffoldError("TARGET", "Target snapshot paths are malformed", {
1896
+ target,
1897
+ limit: MAX_COLLECTION_ITEMS
1898
+ });
1899
+ let remaining = MAX_TOTAL_ARTIFACT_BYTES;
1900
+ const entries = [];
1901
+ for (const path of requested) {
1902
+ const full = resolvePhysicalPath(target, path, "TARGET", "target");
214
1903
  if (!existsSync(full)) continue;
215
- try {
216
- current[path] = statSync(full).isDirectory() ? "" : readFileSync(full, "utf8");
217
- } catch (error) {
218
- throw new ScaffoldError("TARGET", `Failed to read target file at ${path}`, {
219
- path,
220
- full,
221
- error
222
- });
1904
+ const status = attempt(() => lstatSync(full));
1905
+ if (!status.success || status.value.isSymbolicLink() || !status.value.isDirectory() && (!status.value.isFile() || status.value.nlink !== 1 || status.value.size > remaining)) throw new ScaffoldError("TARGET", `Failed to read target file at ${path}`, {
1906
+ path,
1907
+ full,
1908
+ limit: MAX_TOTAL_ARTIFACT_BYTES,
1909
+ ...status.success ? {} : { error: status.error }
1910
+ });
1911
+ if (status.value.isDirectory()) {
1912
+ entries.push([path, ""]);
1913
+ continue;
223
1914
  }
1915
+ const hex = readFileHex(target, path, "TARGET", "target", Math.min(MAX_ARTIFACT_BYTES, remaining));
1916
+ remaining -= hex.length / 2;
1917
+ entries.push([path, hex]);
224
1918
  }
225
- return current;
1919
+ return Object.fromEntries(entries);
226
1920
  }
227
1921
  /**
228
1922
  * Read `target/package.json` text — the read that feeds `manifestToDependencies`.
@@ -241,105 +1935,230 @@ function readTarget(target, paths) {
241
1935
  * ```
242
1936
  */
243
1937
  function readManifest(target) {
1938
+ if (!isFilesystemPath(target)) throw new ScaffoldError("TARGET", "Manifest target is malformed or exceeds its bounds", { target });
244
1939
  const full = join(target, "package.json");
245
- try {
246
- return readFileSync(full, "utf8");
247
- } catch (error) {
248
- throw new ScaffoldError("TARGET", `Failed to read manifest at ${full}`, {
249
- target,
250
- full,
251
- error
252
- });
253
- }
254
- }
255
- /**
256
- * Whether `value` is a plain object (not `null`, not an array).
257
- *
258
- * @param value - The candidate value.
259
- * @returns `true` when `value` narrows to `Record<string, unknown>`.
260
- *
261
- * @example
262
- * ```ts
263
- * import { isRecord } from '@orkestrel/scaffold/server'
264
- *
265
- * isRecord({ a: 1 }) // true
266
- * isRecord(null) // false
267
- * ```
268
- */
269
- function isRecord(value) {
270
- return typeof value === "object" && value !== null && !Array.isArray(value);
271
- }
272
- /**
273
- * Whether `value` is a well-formed `host/manifest.json` entry — a string
274
- * `storage`, a string `destination`, and a boolean `executable`.
275
- *
276
- * @param value - The candidate raw manifest entry.
277
- * @returns `true` when `value` narrows to `ManifestEntry`.
278
- *
279
- * @example
280
- * ```ts
281
- * import { isManifestEntry } from '@orkestrel/scaffold/server'
282
- *
283
- * isManifestEntry({ storage: 'a', destination: 'b', executable: false }) // true
284
- * isManifestEntry({ storage: 'a', destination: 'b' }) // false — missing `executable`
285
- * ```
286
- */
287
- function isManifestEntry(value) {
288
- if (!isRecord(value)) return false;
289
- return typeof value.storage === "string" && typeof value.destination === "string" && typeof value.executable === "boolean";
1940
+ const result = attempt(() => readFileText(target, "package.json", "TARGET", "target", MAX_MANIFEST_BYTES));
1941
+ if (!result.success) throw new ScaffoldError("TARGET", `Failed to read manifest at ${full}`, {
1942
+ target,
1943
+ full,
1944
+ error: result.error
1945
+ });
1946
+ return result.value;
290
1947
  }
291
1948
  /**
292
1949
  * Read and validate a vendored host root's `manifest.json`, when present.
293
1950
  *
294
1951
  * @param host - The host root to probe.
295
- * @returns The parsed entries, or `undefined` when `host` has no
1952
+ * @returns The parsed complete manifest, or `undefined` when `host` has no
296
1953
  * `manifest.json` — the raw-repo-root fallback (`Materializer` then maps
297
1954
  * an artifact's `source` to `host` 1:1, no vendored staging indirection).
298
1955
  * @throws `ScaffoldError('TARGET', …)` when `manifest.json` exists but is
299
- * unreadable, is not valid JSON, or is not an array of `ManifestEntry`.
1956
+ * unreadable, malformed, collision-prone, root-incomplete, or does not map
1957
+ * bijectively and case-exactly onto real contained storage files.
300
1958
  *
301
1959
  * @example
302
1960
  * ```ts
303
1961
  * import { readHostManifest } from '@orkestrel/scaffold/server'
304
1962
  *
305
- * readHostManifest('./dist/host') // readonly ManifestEntry[] | undefined
1963
+ * readHostManifest('./dist/host') // HostManifest | undefined
306
1964
  * ```
307
1965
  */
308
1966
  function readHostManifest(host) {
309
- const full = join(host, "manifest.json");
1967
+ if (!isFilesystemPath(host)) throw new ScaffoldError("TARGET", "Host path is malformed or exceeds its bounds", { host });
1968
+ const full = resolvePhysicalPath(host, HOST_MANIFEST_PATH, "TARGET", "host");
310
1969
  if (!existsSync(full)) return void 0;
311
- let text;
312
- try {
313
- text = readFileSync(full, "utf8");
314
- } catch (error) {
315
- throw new ScaffoldError("TARGET", `Failed to read host manifest at ${full}`, {
1970
+ const text = attempt(() => readFileText(host, HOST_MANIFEST_PATH, "TARGET", "host", MAX_MANIFEST_BYTES));
1971
+ if (!text.success) throw new ScaffoldError("TARGET", `Failed to read host manifest at ${full}`, {
1972
+ host,
1973
+ full,
1974
+ error: text.error
1975
+ });
1976
+ const manifest = parseJSONAs(text.value, isHostManifest);
1977
+ if (manifest === void 0) throw new ScaffoldError("TARGET", `Host manifest at ${full} is malformed`, {
1978
+ host,
1979
+ full
1980
+ });
1981
+ const destinationConflict = findFileConflict(manifest.entries.map((entry) => entry.destination));
1982
+ if (destinationConflict !== void 0) throw new ScaffoldError("TARGET", `Host manifest destination collision between "${destinationConflict[0]}" and "${destinationConflict[1]}"`, {
1983
+ host,
1984
+ full,
1985
+ field: "destination",
1986
+ paths: destinationConflict
1987
+ });
1988
+ const storageConflict = findFileConflict([HOST_MANIFEST_PATH, ...manifest.entries.map((entry) => entry.storage)]);
1989
+ if (storageConflict !== void 0) throw new ScaffoldError("TARGET", `Host manifest storage collision between "${storageConflict[0]}" and "${storageConflict[1]}"`, {
1990
+ host,
1991
+ full,
1992
+ field: "storage",
1993
+ paths: storageConflict
1994
+ });
1995
+ const rootConflict = findPathConflict(manifest.roots);
1996
+ if (rootConflict !== void 0) throw new ScaffoldError("TARGET", `Host manifest root collision between "${rootConflict[0]}" and "${rootConflict[1]}"`, {
1997
+ host,
1998
+ full,
1999
+ field: "root",
2000
+ paths: rootConflict
2001
+ });
2002
+ const declaredRoots = new Set(manifest.roots);
2003
+ for (const entry of manifest.entries) {
2004
+ const segments = entry.destination.split("/");
2005
+ for (let index = 1; index < segments.length; index += 1) {
2006
+ const root = segments.slice(0, index).join("/");
2007
+ if (!declaredRoots.has(root)) throw new ScaffoldError("TARGET", `Host manifest does not declare destination root "${root}"`, {
2008
+ host,
2009
+ full,
2010
+ root,
2011
+ destination: entry.destination
2012
+ });
2013
+ }
2014
+ }
2015
+ for (const root of manifest.roots) {
2016
+ const foldedRoot = root.toLowerCase();
2017
+ const file = manifest.entries.find((entry) => {
2018
+ const destination = entry.destination.toLowerCase();
2019
+ return foldedRoot === destination || foldedRoot.startsWith(`${destination}/`);
2020
+ });
2021
+ if (file !== void 0) throw new ScaffoldError("TARGET", `Host manifest directory "${root}" conflicts with file "${file.destination}"`, {
316
2022
  host,
317
2023
  full,
318
- error
2024
+ root,
2025
+ destination: file.destination
319
2026
  });
320
2027
  }
321
- let parsed;
322
- try {
323
- parsed = JSON.parse(text);
324
- } catch (error) {
325
- throw new ScaffoldError("TARGET", `Host manifest at ${full} is not valid JSON`, {
2028
+ const listed = attempt(() => listFiles(host));
2029
+ if (!listed.success) throw new ScaffoldError("TARGET", `Failed to inventory host storage at ${host}`, {
2030
+ host,
2031
+ full,
2032
+ error: listed.error
2033
+ });
2034
+ const stored = listed.value.filter((path) => path !== HOST_MANIFEST_PATH);
2035
+ const storedConflict = findPathConflict(stored);
2036
+ if (storedConflict !== void 0) throw new ScaffoldError("TARGET", `Host storage collision between "${storedConflict[0]}" and "${storedConflict[1]}"`, {
2037
+ host,
2038
+ full,
2039
+ paths: storedConflict
2040
+ });
2041
+ const storedByPath = new Map(stored.map((path) => [path.toLowerCase(), path]));
2042
+ const declaredStorage = new Set(manifest.entries.map((entry) => entry.storage.toLowerCase()));
2043
+ for (const path of stored) if (!declaredStorage.has(path.toLowerCase())) throw new ScaffoldError("TARGET", `Host storage file "${path}" is not declared`, {
2044
+ host,
2045
+ full,
2046
+ path
2047
+ });
2048
+ for (const entry of manifest.entries) {
2049
+ const storedPath = storedByPath.get(entry.storage.toLowerCase());
2050
+ if (storedPath === void 0 || storedPath !== entry.storage) throw new ScaffoldError("TARGET", `Host storage file "${entry.storage}" is missing or case-mismatched`, {
2051
+ host,
2052
+ full,
2053
+ storage: entry.storage,
2054
+ actual: storedPath
2055
+ });
2056
+ const storage = resolvePhysicalPath(host, entry.storage, "TARGET", "host");
2057
+ const status = attempt(() => lstatSync(storage));
2058
+ if (!status.success || !status.value.isFile() || status.value.isSymbolicLink() || status.value.nlink !== 1) throw new ScaffoldError("TARGET", `Host storage at "${entry.storage}" is not a file`, {
326
2059
  host,
327
2060
  full,
328
- error
2061
+ storage: entry.storage,
2062
+ error: status.success ? void 0 : status.error
329
2063
  });
330
2064
  }
331
- if (!Array.isArray(parsed) || !parsed.every(isManifestEntry)) throw new ScaffoldError("TARGET", `Host manifest at ${full} is not an array of manifest entries`, {
332
- host,
333
- full
2065
+ return manifest;
2066
+ }
2067
+ /**
2068
+ * Read one contained file as exact lowercase hexadecimal bytes.
2069
+ *
2070
+ * @param root - The declared containing root.
2071
+ * @param path - The root-relative file path.
2072
+ * @param code - The coded failure for containment or reading.
2073
+ * @param boundary - The boundary name used in diagnostics.
2074
+ * @returns The exact file bytes encoded as lowercase hexadecimal.
2075
+ */
2076
+ function readFileHex(root, path, code, boundary, limit = MAX_ARTIFACT_BYTES) {
2077
+ if (!Number.isSafeInteger(limit) || limit < 0 || limit > MAX_ARTIFACT_BYTES) throw new ScaffoldError(code, `Invalid file byte limit at ${path}`, {
2078
+ path,
2079
+ root,
2080
+ limit
2081
+ });
2082
+ if (!isPortablePath(path)) throw new ScaffoldError(code, `Unsafe file path at ${path}`, {
2083
+ path,
2084
+ root
2085
+ });
2086
+ const full = resolvePhysicalPath(root, path, code, boundary);
2087
+ const status = attempt(() => lstatSync(full));
2088
+ if (!status.success || !status.value.isFile() || status.value.isSymbolicLink() || status.value.nlink !== 1) throw new ScaffoldError(code, `File is not a physical readable file at ${path}`, {
2089
+ path,
2090
+ full,
2091
+ ...status.success ? {} : { error: status.error }
2092
+ });
2093
+ const opened = attempt(() => openSync(full, "r"));
2094
+ if (!opened.success) throw new ScaffoldError(code, `Failed to read file at ${path}`, {
2095
+ path,
2096
+ full,
2097
+ error: opened.error
334
2098
  });
335
- return parsed;
2099
+ const handle = opened.value;
2100
+ const result = attempt(() => {
2101
+ const current = fstatSync(handle);
2102
+ if (!current.isFile() || current.nlink !== 1 || current.dev !== status.value.dev || current.ino !== status.value.ino) throw new ScaffoldError(code, `File changed before reading at ${path}`, {
2103
+ path,
2104
+ full
2105
+ });
2106
+ if (current.size > limit) throw new ScaffoldError(code, `File exceeds the artifact limit at ${path}`, {
2107
+ path,
2108
+ full,
2109
+ limit,
2110
+ size: current.size
2111
+ });
2112
+ const bytes = Buffer.alloc(current.size);
2113
+ let offset = 0;
2114
+ while (offset < bytes.byteLength) {
2115
+ const length = readSync(handle, bytes, offset, bytes.byteLength - offset, offset);
2116
+ if (length === 0) break;
2117
+ offset += length;
2118
+ }
2119
+ const overflow = Buffer.alloc(1);
2120
+ const extra = readSync(handle, overflow, 0, overflow.byteLength, offset);
2121
+ const after = fstatSync(handle);
2122
+ const pathStatus = lstatSync(full);
2123
+ if (offset !== bytes.byteLength || extra !== 0 || after.dev !== current.dev || after.ino !== current.ino || after.mtimeMs !== current.mtimeMs || after.size !== current.size || !pathStatus.isFile() || pathStatus.isSymbolicLink() || pathStatus.nlink !== 1 || pathStatus.dev !== current.dev || pathStatus.ino !== current.ino) throw new ScaffoldError(code, `File changed while reading at ${path}`, {
2124
+ path,
2125
+ full
2126
+ });
2127
+ return bytesToHex(bytes);
2128
+ });
2129
+ const closed = attempt(() => closeSync(handle));
2130
+ if (!result.success) throw result.error;
2131
+ if (!closed.success) throw new ScaffoldError(code, `Failed to close file at ${path}`, {
2132
+ path,
2133
+ full,
2134
+ error: closed.error
2135
+ });
2136
+ return result.value;
2137
+ }
2138
+ /**
2139
+ * Read one contained physical file as bounded UTF-8 text.
2140
+ *
2141
+ * @param root - The declared containing root.
2142
+ * @param path - The root-relative file path.
2143
+ * @param code - The coded failure for containment or reading.
2144
+ * @param boundary - The boundary name used in diagnostics.
2145
+ * @returns The exact file bytes decoded as UTF-8 text.
2146
+ */
2147
+ function readFileText(root, path, code, boundary, limit = MAX_ARTIFACT_BYTES) {
2148
+ const decoded = attempt(() => new TextDecoder("utf-8", { fatal: true }).decode(Buffer.from(readFileHex(root, path, code, boundary, limit), "hex")));
2149
+ if (!decoded.success) throw new ScaffoldError(code, `File is not valid UTF-8 at ${path}`, {
2150
+ path,
2151
+ root,
2152
+ error: decoded.error
2153
+ });
2154
+ return decoded.value;
336
2155
  }
337
2156
  /**
338
2157
  * Recursively list a directory's files as root-relative paths.
339
2158
  *
340
2159
  * @param root - The directory to list.
341
- * @returns Root-relative file paths (posix-style `/` separators), or `[]`
342
- * when `root` is absent.
2160
+ * @returns Root-relative file paths (posix-style `/` separators), code-unit
2161
+ * sorted, or `[]` when `root` is absent.
343
2162
  *
344
2163
  * @example
345
2164
  * ```ts
@@ -349,20 +2168,122 @@ function readHostManifest(host) {
349
2168
  * ```
350
2169
  */
351
2170
  function listFiles(root) {
352
- if (!existsSync(root)) return [];
2171
+ if (!isFilesystemPath(root)) throw new ScaffoldError("TARGET", "Listing root is malformed or exceeds its bounds", { root });
2172
+ const rootStatus = attempt(() => lstatSync(root));
2173
+ if (!rootStatus.success) {
2174
+ if (isMissingPathError(rootStatus.error)) return [];
2175
+ throw new ScaffoldError("TARGET", "Failed to inspect listing root", {
2176
+ root,
2177
+ error: rootStatus.error
2178
+ });
2179
+ }
2180
+ if (!rootStatus.value.isDirectory() || rootStatus.value.isSymbolicLink()) throw new ScaffoldError("TARGET", "Listing root is not a physical directory", { root });
353
2181
  const files = [];
354
- for (const entry of readdirSync(root, { withFileTypes: true })) {
355
- const full = join(root, entry.name);
356
- if (entry.isDirectory()) for (const nested of listFiles(full)) files.push(`${entry.name}/${nested}`);
357
- else files.push(entry.name);
2182
+ const pending = [{
2183
+ full: root,
2184
+ path: "",
2185
+ depth: 0
2186
+ }];
2187
+ let visited = 0;
2188
+ while (pending.length > 0) {
2189
+ const current = pending.pop();
2190
+ if (current === void 0) break;
2191
+ const handle = opendirSync(current.full);
2192
+ try {
2193
+ for (;;) {
2194
+ const entry = handle.readSync();
2195
+ if (entry === null) break;
2196
+ visited += 1;
2197
+ if (visited > 4096) throw new ScaffoldError("TARGET", `Host traversal exceeds ${MAX_HOST_ENTRIES} entries`, {
2198
+ root,
2199
+ limit: MAX_HOST_ENTRIES
2200
+ });
2201
+ const path = current.path === "" ? entry.name : `${current.path}/${entry.name}`;
2202
+ if (!isPortablePath(path)) throw new ScaffoldError("TARGET", "Filesystem traversal found a non-portable path");
2203
+ if (entry.isDirectory() && !entry.isSymbolicLink()) {
2204
+ const depth = current.depth + 1;
2205
+ if (depth > 32) throw new ScaffoldError("TARGET", `Host traversal exceeds depth 32`, {
2206
+ root,
2207
+ path,
2208
+ limit: 32
2209
+ });
2210
+ pending.push({
2211
+ full: join(current.full, entry.name),
2212
+ path,
2213
+ depth
2214
+ });
2215
+ } else files.push(path);
2216
+ }
2217
+ } finally {
2218
+ handle.closeSync();
2219
+ }
2220
+ }
2221
+ return files.sort();
2222
+ }
2223
+ /**
2224
+ * Recursively list a directory's descendant directories.
2225
+ *
2226
+ * @param root - The directory to list.
2227
+ * @returns Root-relative POSIX directory paths in code-unit order.
2228
+ */
2229
+ function listDirectories(root) {
2230
+ if (!isFilesystemPath(root)) throw new ScaffoldError("TARGET", "Listing root is malformed or exceeds its bounds", { root });
2231
+ const rootStatus = attempt(() => lstatSync(root));
2232
+ if (!rootStatus.success) {
2233
+ if (isMissingPathError(rootStatus.error)) return [];
2234
+ throw new ScaffoldError("TARGET", "Failed to inspect listing root", {
2235
+ root,
2236
+ error: rootStatus.error
2237
+ });
2238
+ }
2239
+ if (!rootStatus.value.isDirectory() || rootStatus.value.isSymbolicLink()) throw new ScaffoldError("TARGET", "Listing root is not a physical directory", { root });
2240
+ const directories = [];
2241
+ const pending = [{
2242
+ full: root,
2243
+ path: "",
2244
+ depth: 0
2245
+ }];
2246
+ let visited = 0;
2247
+ while (pending.length > 0) {
2248
+ const current = pending.pop();
2249
+ if (current === void 0) break;
2250
+ const handle = opendirSync(current.full);
2251
+ try {
2252
+ for (;;) {
2253
+ const entry = handle.readSync();
2254
+ if (entry === null) break;
2255
+ visited += 1;
2256
+ if (visited > 4096) throw new ScaffoldError("TARGET", `Host traversal exceeds ${MAX_HOST_ENTRIES} entries`, {
2257
+ root,
2258
+ limit: MAX_HOST_ENTRIES
2259
+ });
2260
+ if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
2261
+ const path = current.path === "" ? entry.name : `${current.path}/${entry.name}`;
2262
+ if (!isPortablePath(path)) throw new ScaffoldError("TARGET", "Filesystem traversal found a non-portable path");
2263
+ const depth = current.depth + 1;
2264
+ if (depth > 32) throw new ScaffoldError("TARGET", `Host traversal exceeds depth 32`, {
2265
+ root,
2266
+ path,
2267
+ limit: 32
2268
+ });
2269
+ directories.push(path);
2270
+ pending.push({
2271
+ full: join(current.full, entry.name),
2272
+ path,
2273
+ depth
2274
+ });
2275
+ }
2276
+ } finally {
2277
+ handle.closeSync();
2278
+ }
358
2279
  }
359
- return files;
2280
+ return directories.sort();
360
2281
  }
361
2282
  /**
362
2283
  * Map a repo-relative path to its vendored-host STAGING path, per the
363
2284
  * dotfile-mapping rule `stageHost` writes into `manifest.json`.
364
2285
  *
365
- * @param path - The repo-relative source path (e.g. `.claude/agents/scout.md`).
2286
+ * @param path - The repo-relative source path (e.g. `.claude/agents/reviewer.md`).
366
2287
  * @returns The mapped storage path: a leading-dot TOP-LEVEL FILE maps to
367
2288
  * `dotfiles/<name-without-dot>`; a leading-dot DIRECTORY segment loses its
368
2289
  * dot wherever it appears; an undotted path is unchanged.
@@ -372,7 +2293,7 @@ function listFiles(root) {
372
2293
  * import { storagePath } from '@orkestrel/scaffold/server'
373
2294
  *
374
2295
  * storagePath('.gitignore') // 'dotfiles/gitignore'
375
- * storagePath('.claude/agents/scout.md') // 'claude/agents/scout.md'
2296
+ * storagePath('.claude/agents/reviewer.md') // 'claude/agents/reviewer.md'
376
2297
  * storagePath('.github/workflows/ci.yml') // 'github/workflows/ci.yml'
377
2298
  * storagePath('AGENTS.md') // 'AGENTS.md'
378
2299
  * ```
@@ -381,6 +2302,7 @@ function storagePath(path) {
381
2302
  const segments = path.split("/");
382
2303
  if (segments.length === 1) {
383
2304
  const name = segments[0];
2305
+ if (name === void 0) return path;
384
2306
  return name.startsWith(".") ? `dotfiles/${name.slice(1)}` : name;
385
2307
  }
386
2308
  return segments.map((segment) => segment.startsWith(".") ? segment.slice(1) : segment).join("/");
@@ -393,26 +2315,20 @@ function storagePath(path) {
393
2315
  * writes (via `hostRoot` / `readHostManifest`).
394
2316
  *
395
2317
  * @param root - The repo root every `paths` entry resolves against.
396
- * @param out - The output directory to wipe and stage into (typically `dist/host`).
2318
+ * @param out - The output directory to replace after staging completes.
397
2319
  * @param paths - The repo-relative file/directory entries to stage; defaults
398
2320
  * to the package's own vendored set (`HOST_PATHS`) — a caller passes an
399
2321
  * explicit list only to stage an arbitrary/test set.
400
2322
  * @remarks
401
- * `out` is wiped (`rmSync(out, { recursive: true, force: true })`) BEFORE
402
- * staging, so a stale file left over from a prior run never lingers. Each
403
- * `paths` entry is walked to its per-file leaves (a directory recursively,
404
- * via `listFiles`; a file, itself) and copied byte-for-byte
405
- * (`copyFileSync`) to `<out>/<storagePath(path)>`. The `executable` flag
406
- * `entries` reports is derived from the `.sh` suffix on `destination` —
407
- * deterministic on every build platform (Windows `stat` carries no execute
408
- * bit); all vendored executables are shell scripts by construction.
409
- * `manifest.json` is written LAST, as `entries` code-unit sorted by
410
- * `destination`, tab-indented JSON with a trailing newline.
2323
+ * Every source, path collision, and root/output relationship is preflighted
2324
+ * before output mutation. Files are copied into a temporary sibling; the
2325
+ * completed staging tree atomically replaces `out`, with rollback when the
2326
+ * swap fails. The manifest records both sorted file `entries` and the
2327
+ * complete sorted directory `roots` inventory so destructive consumers can
2328
+ * distinguish a declared-empty root from a truncated manifest.
411
2329
  * @returns The written manifest's entries (`{ storage, destination, executable }`).
412
- * @throws `ScaffoldError('TARGET', …)` naming the offending path when a
413
- * `paths` entry has no source under `root`, or naming BOTH colliding
414
- * destinations when two entries map to the same `storagePath` (the guard
415
- * run BEFORE `manifest.json` is written).
2330
+ * @throws `ScaffoldError('TARGET', …)` for an invalid/escaping source or path
2331
+ * collision, and `ScaffoldError('WRITE', …)` for staging/swap failures.
416
2332
  *
417
2333
  * @example
418
2334
  * ```ts
@@ -423,53 +2339,200 @@ function storagePath(path) {
423
2339
  * ```
424
2340
  */
425
2341
  function stageHost(root, out, paths = HOST_PATHS) {
2342
+ if (!isFilesystemPath(root) || !isFilesystemPath(out)) throw new ScaffoldError("TARGET", "Host root or output is malformed or exceeds its bounds", {
2343
+ root,
2344
+ out
2345
+ });
2346
+ const copiedPaths = parsePortablePaths(paths, MAX_HOST_ENTRIES);
2347
+ if (copiedPaths === void 0) throw new ScaffoldError("TARGET", "Host source paths are malformed", {
2348
+ root,
2349
+ out,
2350
+ limit: MAX_HOST_ENTRIES
2351
+ });
426
2352
  const destinations = [];
427
- for (const path of paths) {
428
- const absolute = join(root, path);
2353
+ const roots = [];
2354
+ const resolvedRoot = resolveRealPath(resolve(root));
2355
+ const resolvedOut = resolveRealPath(resolve(out));
2356
+ if (resolvedRoot === resolvedOut || resolvedRoot.startsWith(resolvedOut + sep)) throw new ScaffoldError("TARGET", `Host output at ${out} contains the source root`, {
2357
+ root,
2358
+ out
2359
+ });
2360
+ for (const path of copiedPaths) {
2361
+ if (!isPortablePath(path) || isSensitiveHostPath(path)) throw new ScaffoldError("TARGET", `Invalid host source path at ${path}`, {
2362
+ path,
2363
+ root
2364
+ });
2365
+ const absolute = resolvePhysicalPath(root, path, "TARGET", "host");
429
2366
  if (!existsSync(absolute)) throw new ScaffoldError("TARGET", `Missing host source at ${path}`, {
430
2367
  path,
431
2368
  root
432
2369
  });
433
- if (statSync(absolute).isDirectory()) for (const nested of listFiles(absolute)) destinations.push(`${path}/${nested}`);
434
- else destinations.push(path);
2370
+ const status = attempt(() => lstatSync(absolute));
2371
+ if (!status.success) throw new ScaffoldError("TARGET", `Failed to inspect host source at ${path}`, {
2372
+ path,
2373
+ root,
2374
+ error: status.error
2375
+ });
2376
+ if (status.value.isSymbolicLink()) throw new ScaffoldError("TARGET", `Host source is linked at ${path}`, {
2377
+ path,
2378
+ root
2379
+ });
2380
+ if (status.value.isDirectory()) {
2381
+ const resolvedSource = resolveRealPath(absolute);
2382
+ if (resolvedOut === resolvedSource || resolvedOut.startsWith(resolvedSource + sep)) throw new ScaffoldError("TARGET", `Host output at ${out} is inside source ${path}`, {
2383
+ root,
2384
+ out,
2385
+ path
2386
+ });
2387
+ roots.push(path);
2388
+ for (const nested of listDirectories(absolute)) roots.push(`${path}/${nested}`);
2389
+ for (const nested of listFiles(absolute)) destinations.push(`${path}/${nested}`);
2390
+ } else destinations.push(path);
435
2391
  }
436
- rmSync(out, {
437
- recursive: true,
438
- force: true
2392
+ if (destinations.length > 4096 || roots.length > 4096) throw new ScaffoldError("TARGET", "Host source inventory exceeds the entry limit", {
2393
+ root,
2394
+ limit: MAX_HOST_ENTRIES
439
2395
  });
440
2396
  const entries = [];
2397
+ const expectations = [];
2398
+ let totalBytes = 0;
441
2399
  for (const destination of destinations) {
2400
+ if (isSensitiveHostPath(destination)) throw new ScaffoldError("TARGET", `Sensitive host source is not vendorable at ${destination}`, {
2401
+ root,
2402
+ destination
2403
+ });
2404
+ const source = resolvePhysicalPath(root, destination, "TARGET", "host");
2405
+ const status = attempt(() => lstatSync(source));
2406
+ if (!status.success || !status.value.isFile() || status.value.isSymbolicLink() || status.value.nlink !== 1) throw new ScaffoldError("TARGET", `Host source is not a readable file at ${destination}`, {
2407
+ root,
2408
+ destination,
2409
+ error: status.success ? void 0 : status.error
2410
+ });
2411
+ totalBytes += status.value.size;
2412
+ if (totalBytes > MAX_TOTAL_ARTIFACT_BYTES) throw new ScaffoldError("TARGET", "Host source inventory exceeds the aggregate byte limit", {
2413
+ root,
2414
+ limit: MAX_TOTAL_ARTIFACT_BYTES
2415
+ });
2416
+ expectations.push(Object.freeze({
2417
+ path: destination,
2418
+ shape: "file",
2419
+ device: status.value.dev,
2420
+ inode: status.value.ino,
2421
+ modified: status.value.mtimeMs,
2422
+ size: status.value.size,
2423
+ digest: digestFile(source)
2424
+ }));
442
2425
  const storage = storagePath(destination);
443
- const sourceAbsolute = join(root, destination);
444
- const destinationAbsolute = join(out, storage);
445
- mkdirSync(dirname(destinationAbsolute), { recursive: true });
446
- copyFileSync(sourceAbsolute, destinationAbsolute);
447
2426
  const executable = destination.endsWith(".sh");
448
2427
  entries.push({
449
2428
  storage,
450
2429
  destination,
451
2430
  executable
452
2431
  });
453
- }
454
- entries.sort((a, b) => a.destination < b.destination ? -1 : a.destination > b.destination ? 1 : 0);
455
- const byStorage = /* @__PURE__ */ new Map();
456
- for (const entry of entries) {
457
- const existing = byStorage.get(entry.storage);
458
- if (existing !== void 0) throw new ScaffoldError("TARGET", `Storage path collision at "${entry.storage}" destinations "${existing}" and "${entry.destination}" both map to it`, {
459
- storage: entry.storage,
460
- destinations: [existing, entry.destination]
2432
+ const segments = destination.split("/");
2433
+ for (let index = 1; index < segments.length; index += 1) roots.push(segments.slice(0, index).join("/"));
2434
+ }
2435
+ entries.sort((a, b) => a.destination < b.destination ? -1 : a.destination > b.destination ? 1 : 0);
2436
+ const destinationConflict = findFileConflict(entries.map((entry) => entry.destination));
2437
+ if (destinationConflict !== void 0) throw new ScaffoldError("TARGET", `Destination collision between "${destinationConflict[0]}" and "${destinationConflict[1]}"`, { paths: destinationConflict });
2438
+ const storageConflict = findFileConflict(entries.map((entry) => entry.storage));
2439
+ if (storageConflict !== void 0) {
2440
+ const storageDestinations = entries.filter((entry) => entry.storage.toLowerCase() === storageConflict[0].toLowerCase()).map((entry) => entry.destination);
2441
+ throw new ScaffoldError("TARGET", `Storage collision at "${storageConflict[0]}" between destinations "${storageDestinations[0] ?? ""}" and "${storageDestinations[1] ?? ""}"`, {
2442
+ storage: storageConflict[0],
2443
+ destinations: storageDestinations
2444
+ });
2445
+ }
2446
+ const manifestConflict = findFileConflict([HOST_MANIFEST_PATH, ...entries.map((entry) => entry.storage)]);
2447
+ if (manifestConflict !== void 0) throw new ScaffoldError("TARGET", `Storage path "${manifestConflict[1]}" collides with reserved manifest.json`, { paths: manifestConflict });
2448
+ const uniqueRoots = [...new Set(roots)].sort();
2449
+ if (uniqueRoots.length > 4096) throw new ScaffoldError("TARGET", "Host source inventory exceeds the entry limit", {
2450
+ root,
2451
+ limit: MAX_HOST_ENTRIES
2452
+ });
2453
+ const rootConflict = findPathConflict(uniqueRoots);
2454
+ if (rootConflict !== void 0) throw new ScaffoldError("TARGET", `Directory collision between "${rootConflict[0]}" and "${rootConflict[1]}"`, { paths: rootConflict });
2455
+ const manifest = {
2456
+ entries,
2457
+ roots: uniqueRoots
2458
+ };
2459
+ const parent = dirname(resolve(out));
2460
+ const name = basename(resolve(out));
2461
+ const token = randomUUID();
2462
+ const staging = resolveContainedPath(parent, `.${name}.stage-${token}`, "WRITE", "output");
2463
+ const backup = resolveContainedPath(parent, `.${name}.backup-${token}`, "WRITE", "output");
2464
+ let stagingAnchor;
2465
+ let parentDirectory;
2466
+ const staged = attempt(() => {
2467
+ parentDirectory = createWriteDirectory(parent, "host output parent");
2468
+ validateWriteAnchor(parentDirectory.anchor, "host output parent");
2469
+ mkdirSync(staging, { recursive: false });
2470
+ validateWriteAnchor(parentDirectory.anchor, "host output parent");
2471
+ const stagingStatus = lstatSync(staging);
2472
+ if (!stagingStatus.isDirectory() || stagingStatus.isSymbolicLink()) throw new ScaffoldError("WRITE", "Host staging root is not a physical directory", { staging });
2473
+ stagingAnchor = Object.freeze({
2474
+ path: staging,
2475
+ device: stagingStatus.dev,
2476
+ inode: stagingStatus.ino
2477
+ });
2478
+ for (const entry of entries) {
2479
+ validateWriteAnchor(parentDirectory.anchor, "host output parent");
2480
+ validateWriteAnchor(stagingAnchor, "host staging root");
2481
+ const source = resolvePhysicalPath(root, entry.destination, "TARGET", "host");
2482
+ const expectation = expectations.find((candidate) => candidate.path === entry.destination);
2483
+ const sourceStatus = lstatSync(source);
2484
+ const sourceDigest = digestFile(source);
2485
+ if (expectation === void 0 || !sourceStatus.isFile() || sourceStatus.isSymbolicLink() || sourceStatus.nlink !== 1 || sourceStatus.dev !== expectation.device || sourceStatus.ino !== expectation.inode || sourceStatus.mtimeMs !== expectation.modified || sourceStatus.size !== expectation.size || sourceDigest !== expectation.digest) throw new ScaffoldError("TARGET", `Host source changed at ${entry.destination}`, {
2486
+ root,
2487
+ destination: entry.destination
2488
+ });
2489
+ mkdirSync(dirname(resolvePhysicalPath(staging, entry.storage, "WRITE", "staging")), { recursive: true });
2490
+ validateWriteAnchor(stagingAnchor, "host staging root");
2491
+ const containedDestination = resolvePhysicalPath(staging, entry.storage, "WRITE", "staging");
2492
+ copyFileSync(source, containedDestination, constants.COPYFILE_EXCL);
2493
+ const copiedStatus = lstatSync(containedDestination);
2494
+ if (!copiedStatus.isFile() || copiedStatus.isSymbolicLink() || copiedStatus.nlink !== 1 || digestFile(containedDestination) !== expectation.digest) throw new ScaffoldError("WRITE", `Host staging copy changed at ${entry.destination}`, {
2495
+ staging,
2496
+ destination: entry.destination
2497
+ });
2498
+ }
2499
+ writeFileSync(join(staging, HOST_MANIFEST_PATH), `${JSON.stringify(manifest, null, " ")}\n`, {
2500
+ encoding: "utf8",
2501
+ flag: "wx"
2502
+ });
2503
+ validateWriteAnchor(parentDirectory.anchor, "host output parent");
2504
+ if (readHostManifest(staging) === void 0) throw new ScaffoldError("WRITE", "Staged host manifest could not be verified", { staging });
2505
+ });
2506
+ if (!staged.success) {
2507
+ const identity = stagingAnchor;
2508
+ const cleanup = identity === void 0 ? void 0 : attempt(() => {
2509
+ validateWriteAnchor(identity, "host staging root");
2510
+ rmSync(staging, { recursive: true });
2511
+ });
2512
+ const parentCleanup = attempt(() => {
2513
+ for (const created of [...parentDirectory?.created ?? []].reverse()) {
2514
+ validateWriteAnchor(created, "host output parent");
2515
+ rmdirSync(created.path);
2516
+ }
2517
+ });
2518
+ throw new ScaffoldError("WRITE", `Failed to stage host output at ${out}`, {
2519
+ root,
2520
+ out,
2521
+ error: staged.error,
2522
+ cleanup: cleanup?.success === false ? cleanup.error : void 0,
2523
+ parentCleanup: parentCleanup.success ? void 0 : parentCleanup.error
461
2524
  });
462
- byStorage.set(entry.storage, entry.destination);
463
2525
  }
464
- mkdirSync(out, { recursive: true });
465
- writeFileSync(join(out, "manifest.json"), `${JSON.stringify(entries, null, " ")}\n`);
2526
+ if (parentDirectory === void 0) throw new ScaffoldError("WRITE", "Host output parent was not established", { out });
2527
+ validateWriteAnchor(parentDirectory.anchor, "host output parent");
2528
+ replaceDirectory(staging, out, backup);
466
2529
  return entries;
467
2530
  }
468
2531
  /**
469
2532
  * Resolve the absolute host-storage path for a host-origin artifact's
470
2533
  * `source`, manifest-aware.
471
2534
  *
472
- * @param manifest - The host's parsed `manifest.json` entries, or `undefined`
2535
+ * @param manifest - The parsed complete host manifest, or `undefined`
473
2536
  * when the host carries none (raw-repo-root fallback).
474
2537
  * @param source - The artifact's `source` (or `path`) to resolve.
475
2538
  * @param host - The resolved host root the path is joined against.
@@ -485,27 +2548,54 @@ function stageHost(root, out, paths = HOST_PATHS) {
485
2548
  * import { locateHostSource } from '@orkestrel/scaffold/server'
486
2549
  *
487
2550
  * locateHostSource(undefined, 'package.json', './dist/host') // './dist/host/package.json'
488
- * locateHostSource([{ storage: 'pkg.tmpl', destination: 'package.json', executable: false }], 'package.json', './dist/host')
2551
+ * locateHostSource(
2552
+ * {
2553
+ * entries: [{ storage: 'pkg.tmpl', destination: 'package.json', executable: false }],
2554
+ * roots: [],
2555
+ * },
2556
+ * 'package.json',
2557
+ * './dist/host',
2558
+ * )
489
2559
  * // './dist/host/pkg.tmpl'
490
2560
  * ```
491
2561
  */
492
2562
  function locateHostSource(manifest, source, host) {
493
2563
  if (manifest === void 0) return join(host, source);
494
- const entries = manifest.filter((entry) => entry.destination === source);
2564
+ const entries = manifest.entries.filter((entry) => entry.destination === source);
495
2565
  if (entries.length !== 1) return void 0;
496
- return join(host, entries[0].storage);
2566
+ const [entry] = entries;
2567
+ return entry === void 0 ? void 0 : join(host, entry.storage);
2568
+ }
2569
+ /**
2570
+ * Map a manifest destination from an artifact's source prefix to its target prefix.
2571
+ *
2572
+ * @param artifact - The host artifact carrying the target path and optional source.
2573
+ * @param destination - The matched manifest destination.
2574
+ * @returns The exact target-relative path for the matched manifest file.
2575
+ * @throws `ScaffoldError('INVALID', …)` when `destination` is outside the source prefix.
2576
+ */
2577
+ function remapArtifactPath(artifact, destination) {
2578
+ const source = artifact.source ?? artifact.path;
2579
+ if (destination === source) return artifact.path;
2580
+ if (!destination.startsWith(`${source}/`)) throw new ScaffoldError("INVALID", `Manifest destination does not match ${source}`, {
2581
+ source,
2582
+ destination
2583
+ });
2584
+ return `${artifact.path}/${destination.slice(source.length + 1)}`;
497
2585
  }
498
2586
  /**
499
- * Rehydrate a `Plan`'s `host`-origin artifacts with their real byte content
500
- * read from `host` — manifest-aware, via `locateHostSource`.
2587
+ * Rehydrate a `Plan`'s `host`-origin artifacts with their exact byte hex read
2588
+ * from `host` — manifest-aware, via `locateHostSource`.
501
2589
  *
502
2590
  * @param plan - The plan to hydrate.
503
2591
  * @param host - The resolved host root to read from.
504
- * @returns A new `Plan` whose file-shaped `host` artifacts carry `content`;
505
- * `template` / `computed` artifacts and directory-shaped `host` artifacts
506
- * (no single storage file to read) pass through untouched.
507
- * @throws `ScaffoldError('TARGET', …)` when a resolved, existing host source
508
- * file fails to read.
2592
+ * @returns A new `Plan` whose file-shaped `host` artifacts carry exact `hex`.
2593
+ * Directory-shaped host artifacts expand into one byte-aware artifact
2594
+ * per file, preserving their group and mapping the artifact/source prefixes.
2595
+ * `template` / `computed` artifacts pass through untouched.
2596
+ * @throws `ScaffoldError('TARGET', …)` when the host is not a readable
2597
+ * directory, a present manifest is invalid/incomplete, or a required source
2598
+ * is absent, escaping, not a file/directory as declared, or unreadable.
509
2599
  *
510
2600
  * @example
511
2601
  * ```ts
@@ -515,43 +2605,136 @@ function locateHostSource(manifest, source, host) {
515
2605
  * ```
516
2606
  */
517
2607
  function hydratePlan(plan, host) {
2608
+ if (!isFilesystemPath(host)) throw new ScaffoldError("TARGET", "Host path is malformed or exceeds its bounds", { host });
2609
+ const owned = snapshotPlan(plan);
2610
+ const status = attempt(() => lstatSync(host));
2611
+ if (!status.success || !status.value.isDirectory() || status.value.isSymbolicLink()) throw new ScaffoldError("TARGET", `Host root is not a readable directory at ${host}`, {
2612
+ host,
2613
+ error: status.success ? void 0 : status.error
2614
+ });
518
2615
  const manifest = readHostManifest(host);
519
- const artifacts = plan.artifacts.map((artifact) => {
520
- if (artifact.origin !== "host") return artifact;
2616
+ const artifacts = [];
2617
+ let remaining = MAX_TOTAL_ARTIFACT_BYTES;
2618
+ for (const artifact of owned.artifacts) if (artifact.origin !== "host") remaining -= Buffer.byteLength(artifact.content, "utf8");
2619
+ for (const artifact of owned.artifacts) {
2620
+ if (artifact.origin !== "host") {
2621
+ artifacts.push(artifact);
2622
+ continue;
2623
+ }
521
2624
  const source = artifact.source ?? artifact.path;
2625
+ if (!isPortablePath(source) || isSensitiveHostPath(source)) throw new ScaffoldError("TARGET", `Unsafe host artifact source at ${source}`, {
2626
+ host,
2627
+ source
2628
+ });
522
2629
  const full = locateHostSource(manifest, source, host);
523
- if (full === void 0 || !existsSync(full) || statSync(full).isDirectory()) return artifact;
524
- try {
525
- return {
2630
+ const relative$1 = full === void 0 ? void 0 : relative(host, full).split(sep).join("/");
2631
+ if (relative$1 !== void 0) {
2632
+ const contained = resolvePhysicalPath(host, relative$1, "TARGET", "host");
2633
+ if (existsSync(contained)) {
2634
+ const exact = attempt(() => lstatSync(contained));
2635
+ if (!exact.success) throw new ScaffoldError("TARGET", `Failed to inspect host artifact source at ${source}`, {
2636
+ host,
2637
+ source,
2638
+ error: exact.error
2639
+ });
2640
+ if (exact.value.isFile() && !exact.value.isSymbolicLink() && exact.value.nlink === 1) {
2641
+ const hex = readFileHex(host, relative$1, "TARGET", "host", Math.min(MAX_ARTIFACT_BYTES, remaining));
2642
+ remaining -= hex.length / 2;
2643
+ artifacts.push({
2644
+ ...artifact,
2645
+ hex
2646
+ });
2647
+ continue;
2648
+ }
2649
+ if (manifest !== void 0) throw new ScaffoldError("TARGET", `Host artifact source is not a file at ${source}`, {
2650
+ host,
2651
+ source
2652
+ });
2653
+ }
2654
+ if (manifest !== void 0) throw new ScaffoldError("TARGET", `Host artifact source is missing at ${source}`, {
2655
+ host,
2656
+ source
2657
+ });
2658
+ }
2659
+ if (manifest !== void 0) {
2660
+ const entries = manifest.entries.filter((entry) => entry.destination.startsWith(`${source}/`));
2661
+ if (artifacts.length + entries.length > MAX_COLLECTION_ITEMS) throw new ScaffoldError("TARGET", "Hydrated plan exceeds the artifact count limit", {
2662
+ host,
2663
+ limit: MAX_COLLECTION_ITEMS
2664
+ });
2665
+ if (entries.length === 0) {
2666
+ if (source.startsWith("guides/src/") && source.endsWith(".md")) {
2667
+ artifacts.push(artifact);
2668
+ continue;
2669
+ }
2670
+ throw new ScaffoldError("TARGET", `Host manifest does not declare ${source}`, {
2671
+ host,
2672
+ source
2673
+ });
2674
+ }
2675
+ for (const entry of entries) {
2676
+ if (isSensitiveHostPath(entry.destination)) throw new ScaffoldError("TARGET", `Sensitive host artifact source at ${entry.destination}`, {
2677
+ host,
2678
+ source: entry.destination
2679
+ });
2680
+ const nestedPath = entry.destination.slice(source.length + 1);
2681
+ const hex = readFileHex(host, entry.storage, "TARGET", "host", Math.min(MAX_ARTIFACT_BYTES, remaining));
2682
+ remaining -= hex.length / 2;
2683
+ artifacts.push({
2684
+ ...artifact,
2685
+ path: `${artifact.path}/${nestedPath}`,
2686
+ source: entry.destination,
2687
+ hex
2688
+ });
2689
+ }
2690
+ continue;
2691
+ }
2692
+ const directory = resolvePhysicalPath(host, source, "TARGET", "host");
2693
+ if (!existsSync(directory)) throw new ScaffoldError("TARGET", `Host artifact source is missing at ${source}`, {
2694
+ host,
2695
+ source
2696
+ });
2697
+ const directoryStatus = attempt(() => lstatSync(directory));
2698
+ if (!directoryStatus.success || !directoryStatus.value.isDirectory() || directoryStatus.value.isSymbolicLink()) throw new ScaffoldError("TARGET", `Host artifact source is not a directory at ${source}`, {
2699
+ host,
2700
+ source,
2701
+ error: directoryStatus.success ? void 0 : directoryStatus.error
2702
+ });
2703
+ const relatives = listFiles(directory);
2704
+ if (artifacts.length + relatives.length > MAX_COLLECTION_ITEMS) throw new ScaffoldError("TARGET", "Hydrated plan exceeds the artifact count limit", {
2705
+ host,
2706
+ limit: MAX_COLLECTION_ITEMS
2707
+ });
2708
+ if (relatives.length === 0) {
2709
+ artifacts.push(artifact);
2710
+ continue;
2711
+ }
2712
+ for (const nestedPath of relatives) {
2713
+ const nestedSource = `${source}/${nestedPath}`;
2714
+ if (isSensitiveHostPath(nestedSource)) throw new ScaffoldError("TARGET", `Sensitive host artifact source at ${nestedSource}`, {
2715
+ host,
2716
+ source: nestedSource
2717
+ });
2718
+ const hex = readFileHex(host, nestedSource, "TARGET", "host", Math.min(MAX_ARTIFACT_BYTES, remaining));
2719
+ remaining -= hex.length / 2;
2720
+ artifacts.push({
526
2721
  ...artifact,
527
- content: readFileSync(full, "utf8")
528
- };
529
- } catch (error) {
530
- throw new ScaffoldError("TARGET", `Failed to read host artifact at ${source}`, {
531
- source,
532
- full,
533
- error
2722
+ path: `${artifact.path}/${nestedPath}`,
2723
+ source: nestedSource,
2724
+ hex
534
2725
  });
535
2726
  }
536
- });
537
- return {
538
- ...plan,
2727
+ }
2728
+ const conflict = findFileConflict(artifacts.map((artifact) => artifact.path));
2729
+ if (conflict !== void 0) throw new ScaffoldError("INVALID", `Hydrated artifact collision between "${conflict[0]}" and "${conflict[1]}"`, { paths: conflict });
2730
+ const hydrated = {
2731
+ ...owned,
539
2732
  artifacts
540
2733
  };
2734
+ if (!isPlan(hydrated)) throw new ScaffoldError("INVALID", "Hydrated plan violates the bounded Plan contract");
2735
+ return hydrated;
541
2736
  }
542
2737
  /**
543
- * The `prune`-owned directories — a hard allowlist; `pruneTargets` (and the
544
- * `Materializer.prune` that consumes it) never scans anything outside these two.
545
- *
546
- * @example
547
- * ```ts
548
- * import { PRUNE_DIRECTORIES } from '@orkestrel/scaffold/server'
549
- *
550
- * PRUNE_DIRECTORIES // ['.claude/agents', 'scripts']
551
- * ```
552
- */
553
- var PRUNE_DIRECTORIES = [".claude/agents", "scripts"];
554
- /**
555
2738
  * The vendored set of destination-relative paths under `directory` (one of
556
2739
  * `PRUNE_DIRECTORIES`) that `pruneTargets` must NOT report — read from the
557
2740
  * manifest's `destination`s when `host` has one, else listed straight off
@@ -576,7 +2759,7 @@ var PRUNE_DIRECTORIES = [".claude/agents", "scripts"];
576
2759
  * ```ts
577
2760
  * import { vendoredPruneSet } from '@orkestrel/scaffold/server'
578
2761
  *
579
- * vendoredPruneSet('./dist/host', '.claude/agents') // Set { '.claude/agents/scout.md', … }
2762
+ * vendoredPruneSet('./dist/host', '.claude/agents') // Set { '.claude/agents/reviewer.md', … }
580
2763
  * ```
581
2764
  */
582
2765
  function vendoredPruneSet(host, directory) {
@@ -585,18 +2768,30 @@ function vendoredPruneSet(host, directory) {
585
2768
  directory
586
2769
  });
587
2770
  const manifest = readHostManifest(host);
588
- if (manifest !== void 0) return new Set(manifest.filter((entry) => entry.destination.startsWith(`${directory}/`)).map((entry) => entry.destination));
589
- const hostDirectory = join(host, directory);
2771
+ if (manifest !== void 0) {
2772
+ if (!manifest.roots.includes(directory)) throw new ScaffoldError("TARGET", `Cannot establish vendored source for prune: manifest does not declare ${directory}`, {
2773
+ host,
2774
+ directory
2775
+ });
2776
+ return new Set(manifest.entries.filter((entry) => entry.destination.startsWith(`${directory}/`)).map((entry) => entry.destination));
2777
+ }
2778
+ const hostDirectory = resolvePhysicalPath(host, directory, "TARGET", "host");
590
2779
  if (!existsSync(hostDirectory)) throw new ScaffoldError("TARGET", `Cannot establish vendored source for prune: no manifest.json and no host directory at ${hostDirectory}`, {
591
2780
  host,
592
2781
  directory
593
2782
  });
2783
+ const hostStatus = attempt(() => lstatSync(hostDirectory));
2784
+ if (!hostStatus.success || !hostStatus.value.isDirectory() || hostStatus.value.isSymbolicLink()) throw new ScaffoldError("TARGET", `Cannot establish vendored source for prune: host directory is not physical at ${hostDirectory}`, {
2785
+ host,
2786
+ directory,
2787
+ ...hostStatus.success ? {} : { error: hostStatus.error }
2788
+ });
594
2789
  return new Set(listFiles(hostDirectory).map((relative) => `${directory}/${relative}`));
595
2790
  }
596
2791
  /**
597
2792
  * List the repo-relative POSIX paths under `target`'s prune directories
598
- * (`.claude/agents`, `scripts`) that the vendored `host` allowlist does NOT
599
- * declare — THE single source of truth for prune drift, consumed by both
2793
+ * (`.claude/agents`, `.codex/agents`, `scripts`) that the vendored `host`
2794
+ * allowlist does NOT declare — THE single source of truth for prune drift, consumed by both
600
2795
  * `Materializer.prune` (which deletes exactly these paths) and the bin's
601
2796
  * audit/preview UX (which now shows them honestly instead of a
602
2797
  * structurally-always-zero `audit.foreign`).
@@ -618,10 +2813,20 @@ function vendoredPruneSet(host, directory) {
618
2813
  * ```
619
2814
  */
620
2815
  function pruneTargets(target, host) {
2816
+ if (!isFilesystemPath(target) || !isFilesystemPath(host)) throw new ScaffoldError("TARGET", "Prune root is malformed or exceeds its bounds", {
2817
+ target,
2818
+ host
2819
+ });
621
2820
  const paths = [];
622
2821
  for (const directory of PRUNE_DIRECTORIES) {
623
- const root = join(target, directory);
2822
+ const root = resolvePhysicalPath(target, directory, "TARGET", "target");
624
2823
  if (!existsSync(root)) continue;
2824
+ const status = attempt(() => lstatSync(root));
2825
+ if (!status.success || !status.value.isDirectory() || status.value.isSymbolicLink()) throw new ScaffoldError("TARGET", `Prune root is not a physical directory at ${root}`, {
2826
+ target,
2827
+ directory,
2828
+ ...status.success ? {} : { error: status.error }
2829
+ });
625
2830
  const allowed = vendoredPruneSet(host, directory);
626
2831
  for (const relative of listFiles(root)) {
627
2832
  const path = `${directory}/${relative}`;
@@ -630,6 +2835,22 @@ function pruneTargets(target, host) {
630
2835
  }
631
2836
  return paths;
632
2837
  }
2838
+ /** Consume one aggregate fleet-catalog traversal slot. */
2839
+ function consumeCatalogAllowance(allowance, root) {
2840
+ const consumed = attempt(() => {
2841
+ if (!isCatalogAllowance(allowance)) throw new Error("allowance must be one Float64 cell");
2842
+ const remaining = allowance[0];
2843
+ if (remaining === void 0 || !Number.isSafeInteger(remaining) || remaining < 1 || remaining > 4096) throw new Error("allowance is outside its bounds");
2844
+ const next = remaining - 1;
2845
+ allowance[0] = next;
2846
+ if (allowance[0] !== next) throw new Error("allowance decrement was not retained");
2847
+ });
2848
+ if (!consumed.success) throw new ScaffoldError("TARGET", "Fleet catalog exceeds its aggregate entry limit", {
2849
+ root,
2850
+ limit: MAX_HOST_ENTRIES,
2851
+ error: consumed.error
2852
+ });
2853
+ }
633
2854
  /**
634
2855
  * List a fleet root's `@orkestrel/*` package directories.
635
2856
  *
@@ -647,26 +2868,71 @@ function pruneTargets(target, host) {
647
2868
  * discoverPackages('./packages') // ['/abs/packages/router', '/abs/packages/budget']
648
2869
  * ```
649
2870
  */
650
- function discoverPackages(root) {
2871
+ function discoverPackages(root, allowance = new Float64Array([MAX_HOST_ENTRIES])) {
2872
+ if (!isCatalogAllowance(allowance)) throw new ScaffoldError("TARGET", "Fleet catalog allowance is malformed", {
2873
+ root,
2874
+ limit: MAX_HOST_ENTRIES
2875
+ });
2876
+ if (!isFilesystemPath(root)) throw new ScaffoldError("TARGET", "Fleet root is malformed or exceeds its bounds", { root });
2877
+ const rootStatus = attempt(() => lstatSync(root));
2878
+ if (!rootStatus.success || !rootStatus.value.isDirectory() || rootStatus.value.isSymbolicLink()) throw new ScaffoldError("TARGET", `Fleet root is not a physical directory at ${root}`, {
2879
+ root,
2880
+ ...rootStatus.success ? {} : { error: rootStatus.error }
2881
+ });
651
2882
  const packages = [];
652
- for (const entry of readdirSync(root, { withFileTypes: true })) {
653
- if (!entry.isDirectory()) continue;
654
- const directory = join(root, entry.name);
655
- const manifestPath = join(directory, "package.json");
656
- if (!existsSync(manifestPath)) continue;
657
- let parsed;
658
- try {
659
- parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
660
- } catch {
661
- continue;
2883
+ const handle = opendirSync(root);
2884
+ try {
2885
+ for (;;) {
2886
+ const entry = handle.readSync();
2887
+ if (entry === null) break;
2888
+ consumeCatalogAllowance(allowance, root);
2889
+ if (!entry.isDirectory()) continue;
2890
+ const directory = join(root, entry.name);
2891
+ const text = attempt(() => readFileText(directory, "package.json", "TARGET", "package", MAX_MANIFEST_BYTES));
2892
+ if (!text.success) continue;
2893
+ const parsed = parseJSON(text.value);
2894
+ if (!isRecord(parsed)) continue;
2895
+ const name = ownDataValue(parsed, "name");
2896
+ if (typeof name !== "string" || !DEPENDENCY_NAME_PATTERN.test(name)) continue;
2897
+ if (!isPortablePath(entry.name)) throw new ScaffoldError("TARGET", "Fleet discovery found a non-portable directory");
2898
+ packages.push(directory);
662
2899
  }
663
- if (!isRecord(parsed)) continue;
664
- const name = parsed.name;
665
- if (typeof name === "string" && name.startsWith("@orkestrel/")) packages.push(directory);
2900
+ } finally {
2901
+ handle.closeSync();
666
2902
  }
667
2903
  return packages.sort();
668
2904
  }
669
2905
  /**
2906
+ * Extract the first paragraph from the first blockquote in a Markdown guide.
2907
+ *
2908
+ * @param text - The guide Markdown to traverse.
2909
+ * @returns A normalized one-line description, or `undefined` when parsing
2910
+ * fails, no blockquote paragraph exists, or the paragraph is empty.
2911
+ *
2912
+ * @example
2913
+ * ```ts
2914
+ * import { guideToDescription } from '@orkestrel/scaffold/server'
2915
+ *
2916
+ * guideToDescription('> A concise package description.\n>\n> More detail.')
2917
+ * // 'A concise package description.'
2918
+ * ```
2919
+ */
2920
+ function guideToDescription(text) {
2921
+ if (text.length > MAX_GUIDE_BYTES || contentByteLength(text) > MAX_GUIDE_BYTES) return;
2922
+ const parsed = attempt(() => parseDocument(text));
2923
+ if (!parsed.success) return void 0;
2924
+ let quote;
2925
+ for (const node of walkNodes(parsed.value)) if (isBlockquoteNode(node)) {
2926
+ quote = node;
2927
+ break;
2928
+ }
2929
+ if (quote === void 0) return void 0;
2930
+ const paragraph = quote.children.find((child) => isParagraphNode(child));
2931
+ if (paragraph === void 0) return void 0;
2932
+ const description = flattenText(paragraph).replace(/\s+/g, " ").trim();
2933
+ return description.length > 0 && isCatalogDescription(description) ? description : void 0;
2934
+ }
2935
+ /**
670
2936
  * Build the fleet package catalog — one `CatalogEntry` per `@orkestrel/*`
671
2937
  * package discovered under each root, its description drawn from its own
672
2938
  * guide's FIRST blockquote.
@@ -698,47 +2964,39 @@ function discoverPackages(root) {
698
2964
  * catalogPackages(['/repos']) // [{ name: '@orkestrel/contract', version: '0.0.5', description: '…' }, …]
699
2965
  * ```
700
2966
  */
701
- function catalogPackages(roots) {
2967
+ function catalogPackages(roots, limit = MAX_HOST_ENTRIES) {
2968
+ const requested = parseFilesystemPaths(roots, MAX_HOST_ENTRIES);
2969
+ if (requested === void 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 4096) throw new ScaffoldError("TARGET", "Catalog roots are malformed", { limit: MAX_HOST_ENTRIES });
702
2970
  const merged = /* @__PURE__ */ new Map();
703
- for (const root of roots) for (const directory of discoverPackages(root)) {
704
- let parsed;
705
- try {
706
- parsed = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
707
- } catch {
708
- continue;
709
- }
710
- if (!isRecord(parsed)) continue;
711
- const name = parsed.name;
712
- if (typeof name !== "string" || !name.startsWith("@orkestrel/")) continue;
713
- const version = typeof parsed.version === "string" ? parsed.version : DEFAULT_VERSION;
714
- const guidePath = join(directory, "guides", "src", `${name.slice(11)}.md`);
715
- let description = "";
716
- if (existsSync(guidePath)) try {
717
- const document = parseDocument(readFileSync(guidePath, "utf8"));
718
- let quote;
719
- for (const node of walkNodes(document)) if (isBlockquoteNode(node)) {
720
- quote = node;
721
- break;
722
- }
723
- if (quote !== void 0) {
724
- const paragraph = quote.children.find((child) => isParagraphNode(child));
725
- if (paragraph !== void 0) description = flattenText(paragraph).replace(/\s+/g, " ").trim();
726
- }
727
- } catch {
728
- description = "";
2971
+ const allowance = new Float64Array([limit]);
2972
+ for (const root of requested) {
2973
+ consumeCatalogAllowance(allowance, root);
2974
+ for (const directory of discoverPackages(root, allowance)) {
2975
+ const text = attempt(() => readFileText(directory, "package.json", "TARGET", "package", MAX_MANIFEST_BYTES));
2976
+ if (!text.success) continue;
2977
+ const parsed = parseJSON(text.value);
2978
+ if (!isRecord(parsed)) continue;
2979
+ const name = ownDataValue(parsed, "name");
2980
+ if (typeof name !== "string" || !DEPENDENCY_NAME_PATTERN.test(name)) continue;
2981
+ const version = ownDataValue(parsed, "version");
2982
+ if (typeof version !== "string" || !VERSION_PATTERN.test(version)) continue;
2983
+ const short = packageShortName(name);
2984
+ let description = "";
2985
+ const guide = attempt(() => readFileText(directory, `guides/src/${short}.md`, "TARGET", "package", MAX_GUIDE_BYTES));
2986
+ if (guide.success) description = guideToDescription(guide.value) ?? "";
2987
+ merged.set(name, {
2988
+ name,
2989
+ version,
2990
+ description
2991
+ });
729
2992
  }
730
- merged.set(name, {
731
- name,
732
- version,
733
- description
734
- });
735
2993
  }
736
2994
  return [...merged.values()].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
737
2995
  }
738
2996
  //#endregion
739
2997
  //#region src/server/Materializer.ts
740
2998
  /**
741
- * The materialization entity (server) — the only impure surface in the
2999
+ * The materialization entity (server) — the only impure environment in the
742
3000
  * package, writing a `Plan` to `node:fs` behind an explicit call.
743
3001
  *
744
3002
  * @remarks
@@ -748,10 +3006,16 @@ function catalogPackages(roots) {
748
3006
  * `content`, failing fast on any write error (`ScaffoldError('WRITE', …)`).
749
3007
  * `repair` is into-existing: it skips the vacancy check and writes ONLY the
750
3008
  * `missing` / `stale` artifacts an `Audit` names, leaving `aligned` ones
751
- * untouched. `prune` deletes stale files under `target/.claude/agents/` and
752
- * `target/scripts/` that the vendored `host` no longer names — the retired
753
- * `mirror.sh`/`scaffold.sh` cleanup step, now a method. After `destroy()`
3009
+ * untouched. Hydrated directory-shaped host entries are expanded into
3010
+ * file-shaped artifacts, so canonical skills and agent configuration are
3011
+ * audited and repaired file by file. `prune` deletes stale files under
3012
+ * `target/.claude/agents/`, `target/.codex/agents/`, and `target/scripts/`
3013
+ * that the vendored `host` does not name. After `destroy()`
754
3014
  * every method throws `DESTROYED`; teardown is idempotent, emitter last.
3015
+ * Before the vacancy check or any write, `materialize` and `repair` require a
3016
+ * structural `isPlan` match and a valid `validatePlan` result. Contextual
3017
+ * overrides that are missing from the artifact set, host-owned, or target
3018
+ * `package.json` therefore fail closed instead of being silently ignored.
755
3019
  *
756
3020
  * @remarks
757
3021
  * `host`-origin copies are MANIFEST-AWARE: when the resolved `host` root
@@ -811,40 +3075,34 @@ function catalogPackages(roots) {
811
3075
  * import { blueprint, blueprintToPlan } from '@orkestrel/scaffold'
812
3076
  * import { createMaterializer } from '@orkestrel/scaffold/server'
813
3077
  *
814
- * const plan = blueprintToPlan(blueprint('budget', { surfaces: ['core'] }))
3078
+ * const plan = blueprintToPlan(blueprint('budget', { src: ['core'] }))
815
3079
  * const materializer = createMaterializer()
816
3080
  * materializer.materialize(plan, './packages/budget-new')
817
3081
  * materializer.destroy()
818
3082
  * ```
819
3083
  */
820
- var Materializer = class Materializer {
3084
+ var Materializer = class {
821
3085
  #emitter;
822
3086
  #host;
823
3087
  #manifestLoaded = false;
824
- #manifestEntries;
3088
+ #manifestValue;
825
3089
  #destroyed = false;
826
3090
  constructor(options) {
3091
+ const parsed = parseMaterializerOptions(options);
827
3092
  this.#emitter = new Emitter({
828
- on: options?.on,
829
- error: options?.error
3093
+ ...parsed.on === void 0 ? {} : { on: parsed.on },
3094
+ ...parsed.error === void 0 ? {} : { error: parsed.error }
830
3095
  });
831
- this.#host = options?.host ?? hostRoot();
3096
+ this.#host = parsed.host ?? hostRoot();
832
3097
  }
833
3098
  get emitter() {
834
3099
  return this.#emitter;
835
3100
  }
836
3101
  materialize(plan, target) {
837
3102
  this.#ensureAlive();
3103
+ const prepared = this.#prepare(plan, target);
838
3104
  if (!isVacant(target)) throw new ScaffoldError("TARGET", "materialize requires a vacant target", { target });
839
- const written = [];
840
- const copied = [];
841
- for (const artifact of plan.artifacts) if (artifact.origin === "host") {
842
- this.#copy(artifact, target);
843
- copied.push(artifact.path);
844
- } else {
845
- this.#write(artifact, target);
846
- written.push(artifact.path);
847
- }
3105
+ const { written, copied } = this.#apply(prepared, target);
848
3106
  const result = {
849
3107
  target,
850
3108
  written,
@@ -857,24 +3115,39 @@ var Materializer = class Materializer {
857
3115
  }
858
3116
  repair(plan, audit, target) {
859
3117
  this.#ensureAlive();
860
- const drifted = new Map(audit.findings.map((finding) => [finding.path, finding.drift]));
861
- const written = [];
862
- const copied = [];
3118
+ const context = this.#prepare(plan, target, plan, false);
3119
+ if (!hasOnlyDataProperties(audit) || !hasValidAuditBytes(audit)) throw new ScaffoldError("INVALID", "repair requires a bounded audit snapshot");
3120
+ const preview = attempt(() => structuredClone(audit));
3121
+ if (!preview.success || !Array.isArray(preview.value.findings)) throw new ScaffoldError("INVALID", "repair requires a structurally valid audit", { ...preview.success ? {} : { error: preview.error } });
3122
+ const drifted = new Set(preview.value.findings.filter((finding) => finding.drift === "missing" || finding.drift === "stale").map((finding) => finding.path));
3123
+ const selected = {
3124
+ ...context,
3125
+ artifacts: context.artifacts.filter((artifact) => drifted.has(artifact.path))
3126
+ };
3127
+ const prepared = this.#prepare(selected, target, context);
3128
+ const preparedByPath = new Map(prepared.artifacts.map((artifact) => [artifact.path, artifact]));
3129
+ const comparison = {
3130
+ ...context,
3131
+ artifacts: context.artifacts.map((artifact) => preparedByPath.get(artifact.path) ?? artifact)
3132
+ };
3133
+ const current = diffPlan(comparison, readTarget(target, comparison.artifacts.map((artifact) => artifact.path)));
3134
+ if (JSON.stringify(current.findings) !== JSON.stringify(preview.value.findings)) throw new ScaffoldError("TARGET", "Repair target changed after its audit preview", { target });
3135
+ const preconditions = prepared.artifacts.map((artifact) => {
3136
+ const finding = preview.value.findings.find((candidate) => candidate.path === artifact.path);
3137
+ if (finding?.drift === "missing") return {
3138
+ path: artifact.path,
3139
+ shape: "absent"
3140
+ };
3141
+ if (finding?.drift === "stale" && finding.observed !== void 0) return {
3142
+ path: artifact.path,
3143
+ shape: "file",
3144
+ digest: digestHex(finding.observed)
3145
+ };
3146
+ throw new ScaffoldError("INVALID", `Repair precondition is missing at ${artifact.path}`, { path: artifact.path });
3147
+ });
863
3148
  const skipped = [];
864
- for (const artifact of plan.artifacts) {
865
- const drift = drifted.get(artifact.path);
866
- if (drift !== "missing" && drift !== "stale") {
867
- skipped.push(artifact.path);
868
- continue;
869
- }
870
- if (artifact.origin === "host") {
871
- this.#copy(artifact, target);
872
- copied.push(artifact.path);
873
- } else {
874
- this.#write(artifact, target);
875
- written.push(artifact.path);
876
- }
877
- }
3149
+ for (const artifact of context.artifacts) if (!drifted.has(artifact.path)) skipped.push(artifact.path);
3150
+ const { written, copied } = this.#apply(prepared, target, preconditions);
878
3151
  const result = {
879
3152
  target,
880
3153
  written,
@@ -885,14 +3158,75 @@ var Materializer = class Materializer {
885
3158
  this.#emitter.emit("done", result);
886
3159
  return result;
887
3160
  }
888
- prune(target) {
3161
+ prune(target, expected) {
889
3162
  this.#ensureAlive();
890
- const removed = [];
891
- for (const path of pruneTargets(target, this.#host)) {
892
- unlinkSync(Materializer.#assertContained(target, path, "WRITE", path));
893
- removed.push(path);
894
- this.#emitter.emit("remove", path);
3163
+ if (!isFilesystemPath(target)) throw new ScaffoldError("TARGET", "Prune target is malformed or exceeds its bounds", { target });
3164
+ const paths = pruneTargets(target, this.#host);
3165
+ if (!hasOnlyDataProperties(expected) || !hasValidSnapshotBytes(expected)) throw new ScaffoldError("TARGET", "Prune preview is malformed or unbounded", { target });
3166
+ const preview = attempt(() => structuredClone(expected));
3167
+ const expectedPaths = preview.success ? Object.keys(preview.value).sort() : [];
3168
+ if (!preview.success || JSON.stringify(expectedPaths) !== JSON.stringify([...paths].sort()) || JSON.stringify(readTarget(target, paths)) !== JSON.stringify(preview.value)) throw new ScaffoldError("TARGET", "Prune targets changed after their preview", {
3169
+ target,
3170
+ ...preview.success ? {} : { error: preview.error }
3171
+ });
3172
+ if (paths.length === 0) {
3173
+ const result = {
3174
+ target,
3175
+ written: [],
3176
+ copied: [],
3177
+ skipped: [],
3178
+ removed: []
3179
+ };
3180
+ this.#emitter.emit("done", result);
3181
+ return result;
3182
+ }
3183
+ const transaction = WriteTransaction.create(target, []);
3184
+ const files = paths.map((path) => {
3185
+ resolvePhysicalPath(target, path, "WRITE", "target");
3186
+ return { path };
3187
+ });
3188
+ const moved = [];
3189
+ const staged = attempt(() => {
3190
+ for (const file of files) {
3191
+ validateWriteTarget(transaction, void 0);
3192
+ const source = resolvePhysicalPath(target, file.path, "WRITE", "target");
3193
+ const sourceStatus = lstatSync(source);
3194
+ if (!sourceStatus.isFile() || sourceStatus.isSymbolicLink() || sourceStatus.nlink !== 1) throw new ScaffoldError("WRITE", `Prune target is not a physical file at ${file.path}`, { path: file.path });
3195
+ const sourceDigest = digestFile(source);
3196
+ const expectedHex = preview.value[file.path];
3197
+ if (expectedHex === void 0 || sourceDigest !== digestHex(expectedHex)) throw new ScaffoldError("WRITE", `Prune target changed at ${file.path}`, { path: file.path });
3198
+ mkdirSync(dirname(resolvePhysicalPath(transaction.backup, file.path, "WRITE", "quarantine")), { recursive: true });
3199
+ validateWriteTarget(transaction, void 0);
3200
+ const containedDestination = resolvePhysicalPath(transaction.backup, file.path, "WRITE", "quarantine");
3201
+ renameSync(source, containedDestination);
3202
+ moved.push(file);
3203
+ const capturedStatus = lstatSync(containedDestination);
3204
+ const capturedDigest = digestFile(containedDestination);
3205
+ if (!capturedStatus.isFile() || capturedStatus.isSymbolicLink() || capturedStatus.nlink !== 1 || capturedStatus.dev !== sourceStatus.dev || capturedStatus.ino !== sourceStatus.ino || capturedStatus.mtimeMs !== sourceStatus.mtimeMs || capturedStatus.size !== sourceStatus.size || capturedDigest !== sourceDigest) throw new ScaffoldError("WRITE", `Prune target changed at ${file.path}`, { path: file.path });
3206
+ }
3207
+ });
3208
+ if (!staged.success) {
3209
+ const recovery = attempt(() => restoreFiles(transaction, moved.map((file) => file.path)));
3210
+ const cleanup = recovery.success ? attempt(() => discardWriteTransaction(transaction)) : void 0;
3211
+ throw new ScaffoldError("WRITE", "Failed to stage prune targets", {
3212
+ target,
3213
+ error: staged.error,
3214
+ quarantine: transaction.backup,
3215
+ committed: false,
3216
+ recovery: recovery.success ? void 0 : recovery.error,
3217
+ cleanup: cleanup?.success === false ? cleanup.error : void 0
3218
+ });
895
3219
  }
3220
+ const removed = files.map((file) => file.path);
3221
+ const cleared = attempt(() => discardWriteTransaction(transaction));
3222
+ if (!cleared.success) throw new ScaffoldError("WRITE", "Prune committed with cleanup residue", {
3223
+ target,
3224
+ error: cleared.error,
3225
+ quarantine: transaction.backup,
3226
+ committed: true,
3227
+ removed
3228
+ });
3229
+ for (const path of removed) this.#emitter.emit("remove", path);
896
3230
  const result = {
897
3231
  target,
898
3232
  written: [],
@@ -909,100 +3243,183 @@ var Materializer = class Materializer {
909
3243
  this.#emitter.emit("destroy");
910
3244
  this.#emitter.destroy();
911
3245
  }
912
- #copy(artifact, target) {
913
- const source = artifact.source ?? artifact.path;
914
- const manifest = this.#manifest();
915
- if (manifest === void 0) {
916
- const from = Materializer.#assertContained(this.#host, source, "TARGET", artifact.path);
917
- const to = Materializer.#assertContained(target, artifact.path, "WRITE", artifact.path);
918
- try {
919
- mkdirSync(dirname(to), { recursive: true });
920
- cpSync(from, to, { recursive: true });
921
- } catch (error) {
922
- this.#emitter.emit("error", error);
923
- throw new ScaffoldError("WRITE", `Failed to copy host artifact at ${artifact.path}`, {
924
- path: artifact.path,
925
- error
926
- });
3246
+ #apply(plan, target, preconditions) {
3247
+ if (plan.artifacts.length === 0) return {
3248
+ written: [],
3249
+ copied: []
3250
+ };
3251
+ const paths = plan.artifacts.map((artifact) => artifact.path);
3252
+ const transaction = WriteTransaction.create(target, paths, preconditions);
3253
+ const staged = attempt(() => {
3254
+ for (const artifact of plan.artifacts) {
3255
+ validateWriteDirectories(transaction);
3256
+ if (artifact.origin === "host") this.#copy(artifact, transaction);
3257
+ else this.#write(artifact, transaction);
927
3258
  }
928
- this.#emitter.emit("copy", artifact.path);
929
- return;
3259
+ });
3260
+ if (!staged.success) {
3261
+ const cleanup = attempt(() => discardWriteTransaction(transaction));
3262
+ this.#emitter.emit("error", staged.error);
3263
+ throw new ScaffoldError("WRITE", "Failed to stage materialization", {
3264
+ target,
3265
+ error: staged.error,
3266
+ cleanup: cleanup.success ? void 0 : cleanup.error
3267
+ });
3268
+ }
3269
+ const committed = attempt(() => commitWriteTransaction(transaction, paths));
3270
+ if (!committed.success) {
3271
+ this.#emitter.emit("error", committed.error);
3272
+ throw committed.error;
930
3273
  }
931
- const entries = manifest.filter((entry) => entry.destination === source || entry.destination.startsWith(`${source}/`));
932
- if (entries.length === 0) {
3274
+ const written = plan.artifacts.filter((artifact) => artifact.origin !== "host").map((artifact) => artifact.path);
3275
+ const copied = plan.artifacts.filter((artifact) => artifact.origin === "host").map((artifact) => artifact.path);
3276
+ for (const path of written) this.#emitter.emit("write", path);
3277
+ for (const path of copied) this.#emitter.emit("copy", path);
3278
+ return {
3279
+ written,
3280
+ copied
3281
+ };
3282
+ }
3283
+ #copy(artifact, transaction) {
3284
+ const source = artifact.source ?? artifact.path;
3285
+ const manifest = this.#manifest();
3286
+ const entries = manifest?.entries.filter((entry) => entry.destination === source || entry.destination.startsWith(`${source}/`)) ?? [];
3287
+ if (manifest !== void 0 && entries.length === 0) {
933
3288
  if (!source.startsWith("guides/src/") || !source.endsWith(".md")) throw new ScaffoldError("TARGET", `Manifest entry for "${source}" is missing — the vendored manifest may be corrupted or truncated`, {
934
- target,
3289
+ target: transaction.stage,
935
3290
  source
936
3291
  });
937
- const to = Materializer.#assertContained(target, artifact.path, "WRITE", artifact.path);
3292
+ const to = resolvePhysicalPath(transaction.stage, artifact.path, "WRITE", "staging");
938
3293
  try {
939
3294
  mkdirSync(dirname(to), { recursive: true });
940
- writeFileSync(to, Materializer.#stub(source), "utf8");
3295
+ validateWriteDirectories(transaction);
3296
+ const destination = resolvePhysicalPath(transaction.stage, artifact.path, "WRITE", "staging");
3297
+ const content = guideStub(source);
3298
+ writeFileSync(destination, content, {
3299
+ encoding: "utf8",
3300
+ flag: "wx"
3301
+ });
3302
+ const status = lstatSync(destination);
3303
+ if (!status.isFile() || status.isSymbolicLink() || status.nlink !== 1 || digestFile(destination) !== digestText(content)) throw new Error(`staged guide pointer changed at ${artifact.path}`);
3304
+ validateWriteDirectories(transaction);
941
3305
  } catch (error) {
942
- this.#emitter.emit("error", error);
943
3306
  throw new ScaffoldError("WRITE", `Failed to write host artifact stub at ${artifact.path}`, {
944
3307
  path: artifact.path,
945
3308
  error
946
3309
  });
947
3310
  }
948
- this.#emitter.emit("copy", artifact.path);
949
3311
  return;
950
3312
  }
951
- for (const entry of entries) {
952
- const from = Materializer.#assertContained(this.#host, entry.storage, "TARGET", artifact.path);
953
- const to = Materializer.#assertContained(target, entry.destination, "WRITE", artifact.path);
954
- try {
955
- mkdirSync(dirname(to), { recursive: true });
956
- cpSync(from, to);
957
- if (entry.executable) chmodSync(to, 493);
958
- } catch (error) {
959
- this.#emitter.emit("error", error);
960
- throw new ScaffoldError("WRITE", `Failed to copy host artifact at ${artifact.path}`, {
961
- path: artifact.path,
962
- error
3313
+ if (manifest === void 0 && artifact.hex === void 0) {
3314
+ const status = lstatSync(resolvePhysicalPath(this.#host, source, "TARGET", "host"));
3315
+ if (!status.isDirectory() || status.isSymbolicLink()) throw new ScaffoldError("TARGET", `Host artifact is not a physical directory at ${source}`, { source });
3316
+ const destination = resolvePhysicalPath(transaction.stage, artifact.path, "WRITE", "staging");
3317
+ mkdirSync(destination, { recursive: true });
3318
+ validateWriteDirectories(transaction);
3319
+ const staged = lstatSync(destination);
3320
+ if (!staged.isDirectory() || staged.isSymbolicLink()) throw new ScaffoldError("WRITE", `Staged host directory changed at ${artifact.path}`, { path: artifact.path });
3321
+ return;
3322
+ }
3323
+ const entry = entries[0];
3324
+ const sourcePath = entry === void 0 ? source : entry.storage;
3325
+ const destinationPath = entry === void 0 ? artifact.path : remapArtifactPath(artifact, entry.destination);
3326
+ const expected = artifact.hex;
3327
+ if (expected === void 0) throw new ScaffoldError("TARGET", `Host artifact has no byte snapshot at ${source}`, { source });
3328
+ const from = resolvePhysicalPath(this.#host, sourcePath, "TARGET", "host");
3329
+ const sourceStatus = lstatSync(from);
3330
+ if (!sourceStatus.isFile() || sourceStatus.isSymbolicLink() || sourceStatus.nlink !== 1 || digestFile(from) !== digestHex(expected)) throw new ScaffoldError("TARGET", `Host artifact changed at ${source}`, { source });
3331
+ const to = resolvePhysicalPath(transaction.stage, destinationPath, "WRITE", "staging");
3332
+ try {
3333
+ mkdirSync(dirname(to), { recursive: true });
3334
+ validateWriteDirectories(transaction);
3335
+ const destination = resolvePhysicalPath(transaction.stage, destinationPath, "WRITE", "staging");
3336
+ copyFileSync(from, destination, constants.COPYFILE_EXCL);
3337
+ if (entry?.executable === true) chmodSync(destination, 493);
3338
+ const copied = lstatSync(destination);
3339
+ if (!copied.isFile() || copied.isSymbolicLink() || copied.nlink !== 1 || digestFile(destination) !== digestHex(expected)) throw new Error(`staged host artifact changed at ${artifact.path}`);
3340
+ validateWriteDirectories(transaction);
3341
+ } catch (error) {
3342
+ throw new ScaffoldError("WRITE", `Failed to copy host artifact at ${artifact.path}`, {
3343
+ path: artifact.path,
3344
+ error
3345
+ });
3346
+ }
3347
+ }
3348
+ #prepare(plan, target, context = plan, hydrate = true) {
3349
+ if (!isFilesystemPath(target)) throw new ScaffoldError("TARGET", "Materializer target is malformed or exceeds its bounds", { target });
3350
+ if (!hasOnlyDataProperties(plan)) throw new ScaffoldError("INVALID", "Materializer requires a data-only Plan");
3351
+ const snapshot = attempt(() => structuredClone(plan));
3352
+ const parsed = snapshot.success ? parsePlan(snapshot.value) : void 0;
3353
+ if (parsed === void 0) throw new ScaffoldError("INVALID", "Materializer requires a valid Plan", { ...snapshot.success ? {} : { error: snapshot.error } });
3354
+ const contextSnapshot = context === plan ? snapshot : hasOnlyDataProperties(context) ? attempt(() => structuredClone(context)) : void 0;
3355
+ const parsedContext = context === plan ? parsed : contextSnapshot?.success === true ? parsePlan(contextSnapshot.value) : void 0;
3356
+ if (parsedContext === void 0) throw new ScaffoldError("INVALID", "Materializer requires a valid contextual Plan", { ...contextSnapshot?.success === false ? { error: contextSnapshot.error } : {} });
3357
+ const validation = validatePlan(parsedContext);
3358
+ if (!validation.valid) throw new ScaffoldError("INVALID", "Materializer requires a semantically and contextually valid Plan", { questions: validation.questions });
3359
+ for (const artifact of parsed.artifacts) {
3360
+ if (!isPortablePath(artifact.path)) throw new ScaffoldError("WRITE", `Invalid artifact path at ${artifact.path}`, { path: artifact.path });
3361
+ if (isReservedTargetPath(artifact.path)) throw new ScaffoldError("WRITE", `Artifact path targets reserved repository metadata at ${artifact.path}`, { path: artifact.path });
3362
+ if (artifact.source !== void 0 && !isPortablePath(artifact.source)) throw new ScaffoldError("TARGET", `Invalid artifact source at ${artifact.source}`, { source: artifact.source });
3363
+ }
3364
+ const hosted = parsed.artifacts.some((artifact) => artifact.origin === "host");
3365
+ const prepared = hydrate && hosted ? hydratePlan(parsed, this.#host) : parsed;
3366
+ const conflict = findFileConflict(prepared.artifacts.map((artifact) => artifact.path));
3367
+ if (conflict !== void 0) throw new ScaffoldError("INVALID", `Plan artifact collision between "${conflict[0]}" and "${conflict[1]}"`, { paths: conflict });
3368
+ if (!hydrate) return prepared;
3369
+ const manifest = hosted ? this.#manifest() : void 0;
3370
+ for (const artifact of prepared.artifacts) {
3371
+ const destination = resolvePhysicalPath(target, artifact.path, "WRITE", "target");
3372
+ if (!existsSync(destination)) continue;
3373
+ const destinationStatus = attempt(() => lstatSync(destination));
3374
+ if (!destinationStatus.success) throw new ScaffoldError("TARGET", `Failed to inspect artifact target at ${artifact.path}`, {
3375
+ path: artifact.path,
3376
+ error: destinationStatus.error
3377
+ });
3378
+ if (destinationStatus.value.isSymbolicLink() || destinationStatus.value.isFile() && destinationStatus.value.nlink !== 1) throw new ScaffoldError("TARGET", `Artifact target is linked at ${artifact.path}`, { path: artifact.path });
3379
+ let directory = false;
3380
+ if (artifact.origin === "host" && manifest === void 0 && artifact.hex === void 0) {
3381
+ const source = artifact.source ?? artifact.path;
3382
+ const sourcePath = resolvePhysicalPath(this.#host, source, "TARGET", "host");
3383
+ const sourceStatus = attempt(() => lstatSync(sourcePath));
3384
+ if (!sourceStatus.success) throw new ScaffoldError("TARGET", `Failed to inspect host artifact at ${source}`, {
3385
+ source,
3386
+ error: sourceStatus.error
963
3387
  });
3388
+ directory = sourceStatus.value.isDirectory() && !sourceStatus.value.isSymbolicLink();
964
3389
  }
3390
+ if (destinationStatus.value.isDirectory() !== directory) throw new ScaffoldError("TARGET", `Artifact target has an incompatible shape at ${artifact.path}`, {
3391
+ path: artifact.path,
3392
+ directory
3393
+ });
965
3394
  }
966
- this.#emitter.emit("copy", artifact.path);
3395
+ return prepared;
967
3396
  }
968
3397
  #manifest() {
969
3398
  if (!this.#manifestLoaded) {
970
- this.#manifestEntries = readHostManifest(this.#host);
3399
+ this.#manifestValue = readHostManifest(this.#host);
971
3400
  this.#manifestLoaded = true;
972
3401
  }
973
- return this.#manifestEntries;
3402
+ return this.#manifestValue;
974
3403
  }
975
- #write(artifact, target) {
976
- const to = Materializer.#assertContained(target, artifact.path, "WRITE", artifact.path);
3404
+ #write(artifact, transaction) {
3405
+ const to = resolvePhysicalPath(transaction.stage, artifact.path, "WRITE", "staging");
977
3406
  try {
978
3407
  mkdirSync(dirname(to), { recursive: true });
979
- writeFileSync(to, artifact.content ?? "", "utf8");
3408
+ validateWriteDirectories(transaction);
3409
+ const destination = resolvePhysicalPath(transaction.stage, artifact.path, "WRITE", "staging");
3410
+ writeFileSync(destination, artifact.content, {
3411
+ encoding: "utf8",
3412
+ flag: "wx"
3413
+ });
3414
+ const status = lstatSync(destination);
3415
+ if (!status.isFile() || status.isSymbolicLink() || status.nlink !== 1 || digestFile(destination) !== digestText(artifact.content)) throw new Error(`staged artifact changed at ${artifact.path}`);
3416
+ validateWriteDirectories(transaction);
980
3417
  } catch (error) {
981
- this.#emitter.emit("error", error);
982
3418
  throw new ScaffoldError("WRITE", `Failed to write artifact at ${artifact.path}`, {
983
3419
  path: artifact.path,
984
3420
  error
985
3421
  });
986
3422
  }
987
- this.#emitter.emit("write", artifact.path);
988
- }
989
- static #assertContained(root, relative, code, path) {
990
- const resolvedRoot = Materializer.#resolveReal(resolve(root));
991
- const resolvedCandidate = Materializer.#resolveReal(resolve(root, relative));
992
- if (resolvedCandidate !== resolvedRoot && !resolvedCandidate.startsWith(resolvedRoot + sep)) throw new ScaffoldError(code, `Artifact path "${path}" escapes the ${code === "WRITE" ? "target" : "host"} root`, {
993
- path,
994
- root
995
- });
996
- return resolve(root, relative);
997
- }
998
- static #stub(source) {
999
- return `> Vendored guide for @orkestrel/${source.slice(11, source.length - 3)} — run \`scaffold pull\` to fetch it.\n`;
1000
- }
1001
- static #resolveReal(path) {
1002
- if (existsSync(path)) return realpathSync(path);
1003
- const parent = dirname(path);
1004
- if (parent === path) return path;
1005
- return join(Materializer.#resolveReal(parent), relative(parent, path));
1006
3423
  }
1007
3424
  #ensureAlive() {
1008
3425
  if (this.#destroyed) throw new ScaffoldError("DESTROYED", "Materializer has been destroyed");
@@ -1044,9 +3461,6 @@ var Materializer = class Materializer {
1044
3461
  * ```
1045
3462
  */
1046
3463
  var Sync = class Sync {
1047
- static #DEFAULT_TIMEOUT = 1e4;
1048
- static #DEFAULT_CONCURRENCY = 6;
1049
- static #DEFAULT_LIMIT = 5242880;
1050
3464
  #emitter;
1051
3465
  #guidesBase;
1052
3466
  #branch;
@@ -1057,32 +3471,51 @@ var Sync = class Sync {
1057
3471
  #retries;
1058
3472
  #strict;
1059
3473
  #limit;
3474
+ #items;
3475
+ #budget;
3476
+ #controller = new AbortController();
1060
3477
  #destroyed = false;
1061
3478
  constructor(options) {
3479
+ const parsed = parseSyncOptions(options);
1062
3480
  this.#emitter = new Emitter({
1063
- on: options?.on,
1064
- error: options?.error
1065
- });
1066
- this.#guidesBase = options?.guides?.base ?? "raw.githubusercontent.com";
1067
- this.#branch = options?.guides?.branch ?? "main";
1068
- this.#guidesTimeout = options?.guides?.timeout ?? Sync.#DEFAULT_TIMEOUT;
1069
- this.#registryBase = options?.registry?.base ?? "registry.npmjs.org";
1070
- this.#registryTimeout = options?.registry?.timeout ?? Sync.#DEFAULT_TIMEOUT;
1071
- this.#concurrency = options?.concurrency ?? Sync.#DEFAULT_CONCURRENCY;
1072
- this.#retries = options?.retries ?? 0;
1073
- this.#strict = options?.strict ?? false;
1074
- this.#limit = options?.limit ?? Sync.#DEFAULT_LIMIT;
3481
+ ...parsed.on === void 0 ? {} : { on: parsed.on },
3482
+ ...parsed.error === void 0 ? {} : { error: parsed.error }
3483
+ });
3484
+ this.#guidesBase = parsed.guides?.base ?? "https://raw.githubusercontent.com";
3485
+ this.#branch = parsed.guides?.branch ?? "main";
3486
+ this.#guidesTimeout = parsed.guides?.timeout ?? 1e4;
3487
+ this.#registryBase = parsed.registry?.base ?? "https://registry.npmjs.org";
3488
+ this.#registryTimeout = parsed.registry?.timeout ?? 1e4;
3489
+ this.#concurrency = parsed.concurrency ?? 6;
3490
+ this.#retries = parsed.retries ?? 0;
3491
+ this.#strict = parsed.strict ?? false;
3492
+ this.#limit = parsed.limit ?? DEFAULT_SYNC_LIMIT;
3493
+ this.#items = parsed.items ?? 256;
3494
+ this.#budget = parsed.budget ?? 16777216;
1075
3495
  }
1076
3496
  get emitter() {
1077
3497
  return this.#emitter;
1078
3498
  }
1079
3499
  async guides(deps, current) {
1080
3500
  this.#ensureAlive();
1081
- return Sync.#runPool(deps, this.#concurrency, async (dep) => {
1082
- const short = Sync.#shortName(dep.name);
3501
+ return this.#guides(deps, current, Float64Array.of(this.#budget));
3502
+ }
3503
+ async versions(deps) {
3504
+ this.#ensureAlive();
3505
+ return this.#versions(deps, Float64Array.of(this.#budget));
3506
+ }
3507
+ #guides(deps, current, allowance) {
3508
+ const parsed = parseSyncDependencies(deps, false);
3509
+ if (parsed.length > this.#items) throw new ScaffoldError("INVALID", "Sync dependency count exceeds its item limit", {
3510
+ items: parsed.length,
3511
+ limit: this.#items
3512
+ });
3513
+ const reference = parseSyncCurrent(current, parsed.map((dependency) => dependency.name), this.#budget);
3514
+ return Sync.#runPool(parsed, this.#concurrency, async (dep) => {
3515
+ const short = packageShortName(dep.name);
1083
3516
  const url = this.#guideUrl(short);
1084
- const outcome = await Sync.#fetchText(url, this.#guidesTimeout, this.#retries, this.#limit);
1085
- const guide = Sync.#toGuideSync(dep.name, short, outcome, current);
3517
+ const outcome = await Sync.#fetchText(url, this.#guidesTimeout, this.#retries, this.#limit, allowance, this.#controller.signal);
3518
+ const guide = Sync.#toGuideSync(dep.name, short, outcome, reference);
1086
3519
  if (outcome.kind === "failed") this.#emitter.emit("error", outcome.error);
1087
3520
  this.#emitter.emit("guide", dep.name);
1088
3521
  if (this.#strict && (guide.freshness === "missing" || guide.freshness === "failed")) throw new ScaffoldError("FETCH", `Failed to fetch guide at ${url}`, {
@@ -1092,11 +3525,15 @@ var Sync = class Sync {
1092
3525
  return guide;
1093
3526
  });
1094
3527
  }
1095
- async versions(deps) {
1096
- this.#ensureAlive();
1097
- return Sync.#runPool(deps, this.#concurrency, async (dep) => {
3528
+ #versions(deps, allowance) {
3529
+ const parsed = parseSyncDependencies(deps, true);
3530
+ if (parsed.length > this.#items) throw new ScaffoldError("INVALID", "Sync dependency count exceeds its item limit", {
3531
+ items: parsed.length,
3532
+ limit: this.#items
3533
+ });
3534
+ return Sync.#runPool(parsed, this.#concurrency, async (dep) => {
1098
3535
  const url = this.#registryUrl(dep.name);
1099
- const outcome = await Sync.#fetchText(url, this.#registryTimeout, this.#retries, this.#limit);
3536
+ const outcome = await Sync.#fetchText(url, this.#registryTimeout, this.#retries, this.#limit, allowance, this.#controller.signal);
1100
3537
  const version = Sync.#toVersionSync(dep, outcome);
1101
3538
  if (outcome.kind === "failed") this.#emitter.emit("error", outcome.error);
1102
3539
  this.#emitter.emit("version", dep.name);
@@ -1136,13 +3573,19 @@ var Sync = class Sync {
1136
3573
  async catalog() {
1137
3574
  this.#ensureAlive();
1138
3575
  const orgUrl = this.#orgUrl();
1139
- const orgOutcome = await Sync.#fetchText(orgUrl, this.#registryTimeout, this.#retries, this.#limit);
3576
+ const allowance = Float64Array.of(this.#budget);
3577
+ const orgOutcome = await Sync.#fetchText(orgUrl, this.#registryTimeout, this.#retries, this.#limit, allowance, this.#controller.signal);
1140
3578
  const names = Sync.#toOrgPackages(orgOutcome, orgUrl);
3579
+ if (names.length > this.#items) throw new ScaffoldError("FETCH", `Package list at ${orgUrl} exceeds the item limit`, {
3580
+ url: orgUrl,
3581
+ items: names.length,
3582
+ limit: this.#items
3583
+ });
1141
3584
  return [...await Sync.#runPool(names, this.#concurrency, async (name) => {
1142
- const packumentOutcome = await Sync.#fetchText(this.#registryUrl(name), this.#registryTimeout, this.#retries, this.#limit);
3585
+ const packumentOutcome = await Sync.#fetchText(this.#registryUrl(name), this.#registryTimeout, this.#retries, this.#limit, allowance, this.#controller.signal);
1143
3586
  const packument = Sync.#toPackument(packumentOutcome);
1144
- const short = Sync.#shortName(name);
1145
- const guideOutcome = await Sync.#fetchText(this.#guideUrl(short), this.#guidesTimeout, this.#retries, this.#limit);
3587
+ const short = packageShortName(name);
3588
+ const guideOutcome = await Sync.#fetchText(this.#guideUrl(short), this.#guidesTimeout, this.#retries, this.#limit, allowance, this.#controller.signal);
1146
3589
  const guide = Sync.#toGuideDescription(guideOutcome);
1147
3590
  if (packumentOutcome.kind === "failed") this.#emitter.emit("error", packumentOutcome.error);
1148
3591
  if (guideOutcome.kind === "failed") this.#emitter.emit("error", guideOutcome.error);
@@ -1156,74 +3599,104 @@ var Sync = class Sync {
1156
3599
  };
1157
3600
  })].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
1158
3601
  }
1159
- async pull(target) {
3602
+ async pull(target, dependencies) {
1160
3603
  this.#ensureAlive();
1161
- const deps = manifestToDependencies(readManifest(target));
1162
- const current = {};
1163
- for (const dep of deps) {
1164
- const full = join(target, "guides", "src", `${Sync.#shortName(dep.name)}.md`);
1165
- if (!existsSync(full)) continue;
1166
- try {
1167
- current[dep.name] = readFileSync(full, "utf8");
1168
- } catch {}
1169
- }
1170
- const guides = await this.guides(deps, current);
1171
- const versions = await this.versions(deps);
1172
- const failed = [...guides, ...versions].filter((entry) => entry.freshness === "missing" || entry.freshness === "failed").length;
1173
- const report = {
1174
- target,
1175
- guides,
1176
- versions,
1177
- clean: failed === 0 && guides.every((guide) => guide.freshness === "current") && versions.every((version) => version.freshness === "current"),
1178
- failed
1179
- };
3604
+ const declared = manifestToDependencies(readManifest(target));
3605
+ const deps = dependencies === void 0 ? declared : parseSyncDependencies(dependencies, false);
3606
+ if (dependencies !== void 0 && deps.some((dependency) => !declared.some((candidate) => candidate.name === dependency.name && candidate.range === dependency.range))) throw new ScaffoldError("INVALID", "Sync pull selection is not declared by the target");
3607
+ const current = readGuideReferences(target, deps);
3608
+ const allowance = Float64Array.of(this.#budget);
3609
+ const report = syncReportOf(target, await this.#guides(deps, current, allowance), await this.#versions(deps, allowance));
1180
3610
  this.#emitter.emit("done", report);
1181
3611
  return report;
1182
3612
  }
1183
3613
  async write(report, target) {
1184
3614
  this.#ensureAlive();
1185
- const written = [];
1186
- for (const guide of report.guides) {
1187
- if (guide.freshness !== "behind") continue;
1188
- const to = Sync.#assertContained(target, guide.path);
1189
- try {
1190
- mkdirSync(dirname(to), { recursive: true });
1191
- writeFileSync(to, guide.content, "utf8");
1192
- } catch (error) {
1193
- this.#emitter.emit("error", error);
1194
- throw new ScaffoldError("WRITE", `Failed to write guide at ${guide.path}`, {
3615
+ if (!isFilesystemPath(target)) throw new ScaffoldError("WRITE", "Sync write target is malformed or exceeds its bounds", { target });
3616
+ if (!hasOnlyDataProperties(report) || !isSyncReport(report)) throw new ScaffoldError("WRITE", "Sync report is malformed");
3617
+ const snapshot = attempt(() => structuredClone(report));
3618
+ const parsed = snapshot.success ? parseSyncReport(snapshot.value) : void 0;
3619
+ if (parsed === void 0) throw new ScaffoldError("WRITE", "Sync report is malformed", { ...snapshot.success ? {} : { error: snapshot.error } });
3620
+ const destinations = resolveGuideWrites(parsed.guides, target);
3621
+ const written = destinations.map(({ guide }) => guide.path);
3622
+ if (written.length === 0) return written;
3623
+ const preconditions = [];
3624
+ for (const { guide, destination } of destinations) {
3625
+ if (guide.baseline === void 0 || !SYNC_BASELINE_PATTERN.test(guide.baseline)) throw new ScaffoldError("WRITE", `Guide baseline is missing or malformed at ${guide.path}`, { path: guide.path });
3626
+ const status = attempt(() => lstatSync(destination));
3627
+ if (guide.baseline === "absent") {
3628
+ if (status.success || !isMissingPathError(status.error)) throw new ScaffoldError("WRITE", `Guide destination changed at ${guide.path}`, {
1195
3629
  path: guide.path,
1196
- error
3630
+ ...status.success ? {} : { error: status.error }
3631
+ });
3632
+ preconditions.push({
3633
+ path: guide.path,
3634
+ shape: "absent"
3635
+ });
3636
+ } else if (!status.success || !status.value.isFile() || status.value.isSymbolicLink() || status.value.nlink !== 1 || digestFile(destination) !== guide.baseline) throw new ScaffoldError("WRITE", `Guide destination changed at ${guide.path}`, {
3637
+ path: guide.path,
3638
+ ...status.success ? {} : { error: status.error }
3639
+ });
3640
+ else preconditions.push({
3641
+ path: guide.path,
3642
+ shape: "file",
3643
+ digest: guide.baseline
3644
+ });
3645
+ }
3646
+ const transaction = WriteTransaction.create(target, written, preconditions);
3647
+ const staged = attempt(() => {
3648
+ for (const { guide } of destinations) {
3649
+ if (Buffer.byteLength(guide.content, "utf8") > MAX_ARTIFACT_BYTES) throw new ScaffoldError("WRITE", `Guide exceeds the artifact limit at ${guide.path}`, {
3650
+ path: guide.path,
3651
+ limit: MAX_ARTIFACT_BYTES
1197
3652
  });
3653
+ validateWriteDirectories(transaction);
3654
+ mkdirSync(dirname(resolvePhysicalPath(transaction.stage, guide.path, "WRITE", "staging")), { recursive: true });
3655
+ validateWriteDirectories(transaction);
3656
+ const destination = resolvePhysicalPath(transaction.stage, guide.path, "WRITE", "staging");
3657
+ writeFileSync(destination, guide.content, {
3658
+ encoding: "utf8",
3659
+ flag: "wx"
3660
+ });
3661
+ const status = lstatSync(destination);
3662
+ if (!status.isFile() || status.isSymbolicLink() || status.nlink !== 1 || digestFile(destination) !== digestText(guide.content)) throw new ScaffoldError("WRITE", `Staged guide changed at ${guide.path}`, { path: guide.path });
3663
+ validateWriteDirectories(transaction);
1198
3664
  }
1199
- written.push(guide.path);
1200
- this.#emitter.emit("write", guide.path);
3665
+ });
3666
+ if (!staged.success) {
3667
+ const cleanup = attempt(() => discardWriteTransaction(transaction));
3668
+ this.#emitter.emit("error", staged.error);
3669
+ throw new ScaffoldError("WRITE", "Failed to stage guide synchronization", {
3670
+ target,
3671
+ error: staged.error,
3672
+ cleanup: cleanup.success ? void 0 : cleanup.error
3673
+ });
1201
3674
  }
3675
+ const committed = attempt(() => commitWriteTransaction(transaction, written));
3676
+ if (!committed.success) {
3677
+ this.#emitter.emit("error", committed.error);
3678
+ throw committed.error;
3679
+ }
3680
+ for (const path of written) this.#emitter.emit("write", path);
1202
3681
  return written;
1203
3682
  }
1204
3683
  destroy() {
1205
3684
  if (this.#destroyed) return;
1206
3685
  this.#destroyed = true;
3686
+ this.#controller.abort();
1207
3687
  this.#emitter.emit("destroy");
1208
3688
  this.#emitter.destroy();
1209
3689
  }
1210
3690
  #guideUrl(short) {
1211
- return `${Sync.#normalizeBase(this.#guidesBase)}/orkestrel/${short}/refs/heads/${this.#branch}/guides/src/${short}.md`;
3691
+ const branch = this.#branch.split("/").map((segment) => encodeURIComponent(segment)).join("/");
3692
+ const encoded = encodeURIComponent(short);
3693
+ return `${this.#guidesBase}/orkestrel/${encoded}/refs/heads/${branch}/guides/src/${encoded}.md`;
1212
3694
  }
1213
3695
  #registryUrl(name) {
1214
- return `${Sync.#normalizeBase(this.#registryBase)}/${name.replace("/", "%2F")}`;
3696
+ return `${this.#registryBase}/${name.replace("/", "%2F")}`;
1215
3697
  }
1216
3698
  #orgUrl() {
1217
- return `${Sync.#normalizeBase(this.#registryBase)}/-/org/orkestrel/package`;
1218
- }
1219
- static #normalizeBase(base) {
1220
- return /^https?:\/\//.test(base) ? base : `https://${base}`;
1221
- }
1222
- static #shortName(name) {
1223
- return name.startsWith("@orkestrel/") ? name.slice(11) : name;
1224
- }
1225
- static #isRecord(value) {
1226
- return typeof value === "object" && value !== null && !Array.isArray(value);
3699
+ return `${this.#registryBase}/-/org/orkestrel/package`;
1227
3700
  }
1228
3701
  static async #runPool(items, concurrency, worker) {
1229
3702
  const results = new Array(items.length);
@@ -1248,8 +3721,10 @@ var Sync = class Sync {
1248
3721
  const index = pool.state.cursor;
1249
3722
  pool.state.cursor += 1;
1250
3723
  if (index >= pool.items.length) return;
3724
+ const item = pool.items[index];
3725
+ if (item === void 0) return;
1251
3726
  try {
1252
- pool.results[index] = await pool.worker(pool.items[index]);
3727
+ pool.results[index] = await pool.worker(item);
1253
3728
  } catch (error) {
1254
3729
  if (pool.state.firstError === void 0) pool.state.firstError = { error };
1255
3730
  pool.state.stopped = true;
@@ -1257,38 +3732,47 @@ var Sync = class Sync {
1257
3732
  }
1258
3733
  }
1259
3734
  }
1260
- static async #fetchText(url, timeout, retries, limit) {
3735
+ static async #fetchText(url, timeout, retries, limit, allowance, signal) {
1261
3736
  let lastError;
1262
3737
  let lastNote = "";
1263
- for (let attempt = 0; attempt <= retries; attempt += 1) try {
1264
- const response = await fetch(url, {
1265
- signal: AbortSignal.timeout(timeout),
1266
- redirect: "manual"
1267
- });
1268
- if (response.status === 404) return { kind: "missing" };
1269
- if (response.type === "opaqueredirect" || response.status >= 300 && response.status < 400) {
1270
- lastNote = "redirected (redirect following is disabled)";
1271
- lastError = /* @__PURE__ */ new Error(`Redirect blocked for ${url}`);
1272
- continue;
1273
- }
1274
- if (!response.ok) {
1275
- lastNote = `HTTP ${String(response.status)}`;
1276
- lastError = /* @__PURE__ */ new Error(`Unexpected HTTP status ${response.status} for ${url}`);
1277
- continue;
1278
- }
1279
- const read = await Sync.#readBounded(response, url, limit);
1280
- if (!read.ok) {
1281
- lastError = read.error;
1282
- lastNote = read.note;
1283
- continue;
3738
+ for (let retry = 0; retry <= retries; retry += 1) {
3739
+ if (signal.aborted) throw new ScaffoldError("DESTROYED", "Sync has been destroyed");
3740
+ try {
3741
+ const response = await fetch(url, {
3742
+ signal: AbortSignal.any([signal, AbortSignal.timeout(timeout)]),
3743
+ redirect: "manual"
3744
+ });
3745
+ if (response.status === 404) {
3746
+ await Sync.#cancel(response);
3747
+ return { kind: "missing" };
3748
+ }
3749
+ if (response.type === "opaqueredirect" || response.status >= 300 && response.status < 400) {
3750
+ lastNote = "redirected (redirect following is disabled)";
3751
+ lastError = /* @__PURE__ */ new Error(`Redirect blocked for ${url}`);
3752
+ await Sync.#cancel(response);
3753
+ continue;
3754
+ }
3755
+ if (!response.ok) {
3756
+ lastNote = `HTTP ${String(response.status)}`;
3757
+ lastError = /* @__PURE__ */ new Error(`Unexpected HTTP status ${response.status} for ${url}`);
3758
+ await Sync.#cancel(response);
3759
+ continue;
3760
+ }
3761
+ const read = await Sync.#readBounded(response, url, limit, allowance);
3762
+ if (!read.ok) {
3763
+ lastError = read.error;
3764
+ lastNote = read.note;
3765
+ continue;
3766
+ }
3767
+ return {
3768
+ kind: "ok",
3769
+ text: read.text
3770
+ };
3771
+ } catch (error) {
3772
+ if (signal.aborted) throw new ScaffoldError("DESTROYED", "Sync has been destroyed", { error });
3773
+ lastError = error;
3774
+ lastNote = Sync.#transportNote(error);
1284
3775
  }
1285
- return {
1286
- kind: "ok",
1287
- text: read.text
1288
- };
1289
- } catch (error) {
1290
- lastError = error;
1291
- lastNote = Sync.#transportNote(error);
1292
3776
  }
1293
3777
  return {
1294
3778
  kind: "failed",
@@ -1296,6 +3780,9 @@ var Sync = class Sync {
1296
3780
  note: lastNote
1297
3781
  };
1298
3782
  }
3783
+ static async #cancel(response) {
3784
+ await response.body?.cancel();
3785
+ }
1299
3786
  static #transportNote(error) {
1300
3787
  if (!(error instanceof Error)) return String(error);
1301
3788
  const code = Sync.#causeCode(error);
@@ -1307,15 +3794,19 @@ var Sync = class Sync {
1307
3794
  if (!("code" in cause)) return void 0;
1308
3795
  return typeof cause.code === "string" ? cause.code : void 0;
1309
3796
  }
1310
- static async #readBounded(response, url, limit) {
3797
+ static async #readBounded(response, url, limit, allowance) {
1311
3798
  const declared = response.headers.get("content-length");
1312
3799
  if (declared !== null) {
1313
3800
  const declaredBytes = Number(declared);
1314
- if (Number.isFinite(declaredBytes) && declaredBytes > limit) return {
1315
- ok: false,
1316
- error: /* @__PURE__ */ new Error(`Response body for ${url} declares ${String(declaredBytes)} bytes, exceeding the ${String(limit)}-byte limit`),
1317
- note: `response exceeded limit (${String(limit)} bytes)`
1318
- };
3801
+ if (Number.isFinite(declaredBytes) && (declaredBytes > limit || declaredBytes > (allowance[0] ?? 0))) {
3802
+ await response.body?.cancel();
3803
+ const threshold = Math.min(limit, allowance[0] ?? 0);
3804
+ return {
3805
+ ok: false,
3806
+ error: /* @__PURE__ */ new Error(`Response body for ${url} declares ${String(declaredBytes)} bytes, exceeding the ${String(threshold)}-byte allowance`),
3807
+ note: `response exceeded allowance (${String(threshold)} bytes)`
3808
+ };
3809
+ }
1319
3810
  }
1320
3811
  const body = response.body;
1321
3812
  if (body === null) return {
@@ -1323,7 +3814,7 @@ var Sync = class Sync {
1323
3814
  text: ""
1324
3815
  };
1325
3816
  const reader = body.getReader();
1326
- const decoder = new TextDecoder();
3817
+ const decoder = new TextDecoder("utf-8", { fatal: true });
1327
3818
  const chunks = [];
1328
3819
  let total = 0;
1329
3820
  try {
@@ -1331,14 +3822,23 @@ var Sync = class Sync {
1331
3822
  const { done, value } = await reader.read();
1332
3823
  if (done) break;
1333
3824
  total += value.byteLength;
1334
- if (total > limit) return {
1335
- ok: false,
1336
- error: /* @__PURE__ */ new Error(`Response body for ${url} exceeded the ${String(limit)}-byte limit`),
1337
- note: `response exceeded limit (${String(limit)} bytes)`
1338
- };
3825
+ if (total > limit || value.byteLength > (allowance[0] ?? 0)) {
3826
+ await reader.cancel();
3827
+ const threshold = Math.min(limit, total + (allowance[0] ?? 0) - value.byteLength);
3828
+ allowance[0] = Math.max(0, (allowance[0] ?? 0) - value.byteLength);
3829
+ return {
3830
+ ok: false,
3831
+ error: /* @__PURE__ */ new Error(`Response body for ${url} exceeded the ${String(threshold)}-byte allowance`),
3832
+ note: `response exceeded allowance (${String(threshold)} bytes)`
3833
+ };
3834
+ }
3835
+ allowance[0] = (allowance[0] ?? 0) - value.byteLength;
1339
3836
  chunks.push(decoder.decode(value, { stream: true }));
1340
3837
  }
1341
3838
  chunks.push(decoder.decode());
3839
+ } catch (error) {
3840
+ await reader.cancel(error).catch(() => void 0);
3841
+ throw error;
1342
3842
  } finally {
1343
3843
  reader.releaseLock();
1344
3844
  }
@@ -1349,26 +3849,31 @@ var Sync = class Sync {
1349
3849
  }
1350
3850
  static #toGuideSync(name, short, outcome, current) {
1351
3851
  const path = `guides/src/${short}.md`;
3852
+ const local = current?.[name];
3853
+ const baseline = current === void 0 ? void 0 : local === void 0 ? "absent" : digestText(local);
1352
3854
  if (outcome.kind === "missing") return {
1353
3855
  name,
1354
3856
  path,
1355
3857
  content: "",
1356
3858
  freshness: "missing",
1357
- note: "HTTP 404"
3859
+ note: "HTTP 404",
3860
+ ...baseline === void 0 ? {} : { baseline }
1358
3861
  };
1359
3862
  if (outcome.kind === "failed") return {
1360
3863
  name,
1361
3864
  path,
1362
3865
  content: "",
1363
3866
  freshness: "failed",
1364
- note: outcome.note
3867
+ note: outcome.note,
3868
+ ...baseline === void 0 ? {} : { baseline }
1365
3869
  };
1366
- const freshness = current?.[name] === outcome.text ? "current" : "behind";
3870
+ const freshness = current !== void 0 && local !== void 0 && local === outcome.text ? "current" : "behind";
1367
3871
  return {
1368
3872
  name,
1369
3873
  path,
1370
3874
  content: outcome.text,
1371
- freshness
3875
+ freshness,
3876
+ ...baseline === void 0 ? {} : { baseline }
1372
3877
  };
1373
3878
  }
1374
3879
  static #toVersionSync(dep, outcome) {
@@ -1409,15 +3914,10 @@ var Sync = class Sync {
1409
3914
  note
1410
3915
  });
1411
3916
  }
1412
- let parsed;
1413
- try {
1414
- parsed = JSON.parse(outcome.text);
1415
- } catch {
1416
- throw new ScaffoldError("FETCH", `Malformed package list response at ${url}`, { url });
1417
- }
1418
- if (!Sync.#isRecord(parsed)) throw new ScaffoldError("FETCH", `Malformed package list response at ${url}`, { url });
1419
- const names = Object.keys(parsed).filter((name) => name.startsWith("@orkestrel/"));
1420
- if (names.length === 0) throw new ScaffoldError("FETCH", `Malformed package list response at ${url}`, { url });
3917
+ const parsed = parseJSON(outcome.text);
3918
+ if (!isRecord(parsed)) throw new ScaffoldError("FETCH", `Malformed package list response at ${url}`, { url });
3919
+ const names = Object.keys(parsed);
3920
+ if (names.length === 0 || names.some((name) => !DEPENDENCY_NAME_PATTERN.test(name))) throw new ScaffoldError("FETCH", `Malformed package list response at ${url}`, { url });
1421
3921
  return names;
1422
3922
  }
1423
3923
  static #toPackument(outcome) {
@@ -1431,26 +3931,24 @@ var Sync = class Sync {
1431
3931
  description: "",
1432
3932
  note: outcome.note
1433
3933
  };
1434
- let parsed;
1435
- try {
1436
- parsed = JSON.parse(outcome.text);
1437
- } catch {
1438
- return {
1439
- version: "",
1440
- description: "",
1441
- note: "malformed registry response (invalid JSON)"
1442
- };
1443
- }
1444
- if (!Sync.#isRecord(parsed)) return {
3934
+ const parsed = parseJSON(outcome.text);
3935
+ if (parsed === void 0) return {
3936
+ version: "",
3937
+ description: "",
3938
+ note: "malformed registry response (invalid JSON)"
3939
+ };
3940
+ if (!isRecord(parsed)) return {
1445
3941
  version: "",
1446
3942
  description: "",
1447
3943
  note: "malformed registry response (not an object)"
1448
3944
  };
1449
- const distTags = parsed["dist-tags"];
1450
- const version = Sync.#isRecord(distTags) && typeof distTags.latest === "string" ? distTags.latest : "";
3945
+ const distTags = ownDataValue(parsed, "dist-tags");
3946
+ const latest = ownDataValue(distTags, "latest");
3947
+ const version = isRecord(distTags) && typeof latest === "string" && VERSION_PATTERN.test(latest) ? latest : "";
3948
+ const rawDescription = ownDataValue(parsed, "description");
1451
3949
  return {
1452
3950
  version,
1453
- description: typeof parsed.description === "string" ? parsed.description : "",
3951
+ description: isCatalogDescription(rawDescription) ? rawDescription : "",
1454
3952
  note: version === "" ? "malformed registry response (missing dist-tags.latest)" : ""
1455
3953
  };
1456
3954
  }
@@ -1463,7 +3961,7 @@ var Sync = class Sync {
1463
3961
  description: void 0,
1464
3962
  note: outcome.note
1465
3963
  };
1466
- const description = Sync.#firstBlockquoteParagraph(outcome.text);
3964
+ const description = guideToDescription(outcome.text);
1467
3965
  return description !== void 0 ? {
1468
3966
  description,
1469
3967
  note: ""
@@ -1472,24 +3970,6 @@ var Sync = class Sync {
1472
3970
  note: "guide has no blockquote description"
1473
3971
  };
1474
3972
  }
1475
- static #firstBlockquoteParagraph(text) {
1476
- let document;
1477
- try {
1478
- document = parseDocument(text);
1479
- } catch {
1480
- return;
1481
- }
1482
- let quote;
1483
- for (const node of walkNodes(document)) if (isBlockquoteNode(node)) {
1484
- quote = node;
1485
- break;
1486
- }
1487
- if (quote === void 0) return void 0;
1488
- const paragraph = quote.children.find((child) => isParagraphNode(child));
1489
- if (paragraph === void 0) return void 0;
1490
- const flattened = flattenText(paragraph).replace(/\s+/g, " ").trim();
1491
- return flattened.length > 0 ? flattened : void 0;
1492
- }
1493
3973
  static #combineNote(guideNote, packumentNote) {
1494
3974
  const parts = [];
1495
3975
  if (packumentNote !== "") parts.push(`version unavailable — ${packumentNote}`);
@@ -1497,32 +3977,12 @@ var Sync = class Sync {
1497
3977
  return parts.join("; ");
1498
3978
  }
1499
3979
  static #parseLatest(text) {
1500
- let parsed;
1501
- try {
1502
- parsed = JSON.parse(text);
1503
- } catch {
1504
- return;
1505
- }
1506
- if (!Sync.#isRecord(parsed)) return void 0;
1507
- const distTags = parsed["dist-tags"];
1508
- if (!Sync.#isRecord(distTags)) return void 0;
1509
- const latest = distTags.latest;
1510
- return typeof latest === "string" ? latest : void 0;
1511
- }
1512
- static #assertContained(target, relative) {
1513
- const resolvedRoot = Sync.#resolveReal(resolve(target));
1514
- const resolvedCandidate = Sync.#resolveReal(resolve(target, relative));
1515
- if (resolvedCandidate !== resolvedRoot && !resolvedCandidate.startsWith(resolvedRoot + sep)) throw new ScaffoldError("WRITE", `Guide path "${relative}" escapes the target root`, {
1516
- path: relative,
1517
- target
1518
- });
1519
- return resolve(target, relative);
1520
- }
1521
- static #resolveReal(path) {
1522
- if (existsSync(path)) return realpathSync(path);
1523
- const parent = dirname(path);
1524
- if (parent === path) return path;
1525
- return join(Sync.#resolveReal(parent), relative(parent, path));
3980
+ const parsed = parseJSON(text);
3981
+ if (!isRecord(parsed)) return void 0;
3982
+ const distTags = ownDataValue(parsed, "dist-tags");
3983
+ if (!isRecord(distTags)) return void 0;
3984
+ const latest = ownDataValue(distTags, "latest");
3985
+ return typeof latest === "string" && VERSION_PATTERN.test(latest) ? latest : void 0;
1526
3986
  }
1527
3987
  #ensureAlive() {
1528
3988
  if (this.#destroyed) throw new ScaffoldError("DESTROYED", "Sync has been destroyed");
@@ -1567,6 +4027,6 @@ function createSync(options) {
1567
4027
  return new Sync(options);
1568
4028
  }
1569
4029
  //#endregion
1570
- export { Materializer, PRUNE_DIRECTORIES, Sync, catalogPackages, createMaterializer, createSync, deriveBlueprint, discoverPackages, hostRoot, hydratePlan, isManifestEntry, isRecord, isVacant, listFiles, locateHostSource, pruneTargets, readHostManifest, readManifest, readTarget, selectOrkestrelEntries, stageHost, storagePath, vendoredPruneSet };
4030
+ export { DEFAULT_SYNC_BUDGET, DEFAULT_SYNC_CONCURRENCY, DEFAULT_SYNC_ITEMS, DEFAULT_SYNC_LIMIT, DEFAULT_SYNC_TIMEOUT, HOST_MANIFEST_PATH, MAX_CATALOG_DESCRIPTION_LENGTH, MAX_FILESYSTEM_DEPTH, MAX_GUIDE_BYTES, MAX_HOST_DEPTH, MAX_HOST_ENTRIES, MAX_PATH_SEGMENT_BYTES, MAX_SYNC_BASE_LENGTH, MAX_SYNC_BRANCH_LENGTH, MAX_SYNC_BUDGET, MAX_SYNC_CONCURRENCY, MAX_SYNC_ITEMS, MAX_SYNC_LIMIT, MAX_SYNC_RETRIES, MAX_SYNC_TIMEOUT, Materializer, PRUNE_DIRECTORIES, RESERVED_PATH_SEGMENT_PATTERN, RESERVED_TARGET_PATH_PATTERN, SENSITIVE_HOST_PATH_PATTERN, SYNC_BRANCH_PATTERN, Sync, WRITE_DIGEST_PATTERN, WriteTransaction, catalogPackages, commitWriteTransaction, consumeCatalogAllowance, createMaterializer, createSync, createWriteDirectory, deriveBlueprint, digestFile, digestHex, digestText, discardWriteTransaction, discoverPackages, guideStub, guideToDescription, hostRoot, hydratePlan, isCatalogAllowance, isCatalogDescription, isDependencyData, isFilesystemPath, isHostManifest, isManifestEntry, isMaterializerEventHooks, isMissingPathError, isPortablePath, isRealDirectory, isReservedTargetPath, isSensitiveHostPath, isSyncEventHooks, isTerminalText, isVacant, isWritePrecondition, listDirectories, listFiles, locateHostSource, materializerOptionsContract, materializerOptionsShape, packageShortName, parseFilesystemPaths, parseMaterializerOptions, parsePortablePaths, parseSyncBase, parseSyncBranch, parseSyncCurrent, parseSyncDependencies, parseSyncOptions, parseWritePreconditions, pruneTargets, readFileHex, readFileText, readGuideReferences, readHostManifest, readManifest, readTarget, remapArtifactPath, replaceDirectory, resolveContainedPath, resolveGuideWrites, resolvePhysicalPath, resolveRealPath, restoreFiles, selectOrkestrelEntries, stageHost, storagePath, syncGuideOptionsShape, syncOptionsContract, syncOptionsShape, syncRegistryOptionsShape, syncReportOf, validateWriteAnchor, validateWriteDirectories, validateWriteTarget, vendoredPruneSet };
1571
4031
 
1572
4032
  //# sourceMappingURL=index.js.map