@orkestrel/scaffold 0.0.2 → 0.0.3

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