@danielblomma/cortex-mcp 2.4.0 → 2.4.2

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 (214) hide show
  1. package/CHANGELOG.md +226 -0
  2. package/README.md +24 -3
  3. package/bin/cli/arguments.mjs +60 -0
  4. package/bin/cli/context-passthrough.mjs +129 -0
  5. package/bin/cli/daemon.mjs +157 -0
  6. package/bin/cli/enterprise.mjs +516 -0
  7. package/bin/cli/help.mjs +88 -0
  8. package/bin/cli/hooks.mjs +199 -0
  9. package/bin/cli/mcp-command.mjs +87 -0
  10. package/bin/cli/paths.mjs +14 -0
  11. package/bin/cli/process.mjs +49 -0
  12. package/bin/cli/project-commands.mjs +237 -0
  13. package/bin/cli/project-runtime.mjs +34 -0
  14. package/bin/cli/query-command.mjs +18 -0
  15. package/bin/cli/router.mjs +66 -0
  16. package/bin/cli/run-command.mjs +44 -0
  17. package/bin/cli/scaffold-ownership.mjs +936 -0
  18. package/bin/cli/scaffold.mjs +687 -0
  19. package/bin/cli/stage-command.mjs +9 -0
  20. package/bin/cli/telemetry-command.mjs +18 -0
  21. package/bin/cli/trusted-runtime.mjs +18 -0
  22. package/bin/cortex.mjs +12 -1607
  23. package/bin/daemon-control.mjs +162 -0
  24. package/mcp-registry-submission.json +1 -1
  25. package/package.json +8 -3
  26. package/scaffold/mcp/dist/.cortex-build-hash +1 -0
  27. package/scaffold/mcp/dist/cli/enterprise-setup.js +134 -0
  28. package/scaffold/mcp/dist/cli/govern.js +937 -0
  29. package/scaffold/mcp/dist/cli/query.js +409 -0
  30. package/scaffold/mcp/dist/cli/run.js +295 -0
  31. package/scaffold/mcp/dist/cli/stage.js +308 -0
  32. package/scaffold/mcp/dist/cli/telemetry-test.js +141 -0
  33. package/scaffold/mcp/dist/cli/ungoverned-detector.js +133 -0
  34. package/scaffold/mcp/dist/contextEntities.js +282 -0
  35. package/scaffold/mcp/dist/core/audit/query.js +72 -0
  36. package/scaffold/mcp/dist/core/audit/writer.js +28 -0
  37. package/scaffold/mcp/dist/core/config.js +235 -0
  38. package/scaffold/mcp/dist/core/enterprise-host-identity.js +193 -0
  39. package/scaffold/mcp/dist/core/enterprise-identity.js +18 -0
  40. package/scaffold/mcp/dist/core/enterprise-rotation.js +158 -0
  41. package/scaffold/mcp/dist/core/govern-paths.js +21 -0
  42. package/scaffold/mcp/dist/core/index.js +14 -0
  43. package/scaffold/mcp/dist/core/license.js +278 -0
  44. package/scaffold/mcp/dist/core/policy/enforce.js +66 -0
  45. package/scaffold/mcp/dist/core/policy/injection.js +172 -0
  46. package/scaffold/mcp/dist/core/policy/store.js +179 -0
  47. package/scaffold/mcp/dist/core/rbac/check.js +30 -0
  48. package/scaffold/mcp/dist/core/secure-endpoint.js +25 -0
  49. package/scaffold/mcp/dist/core/telemetry/collector.js +285 -0
  50. package/scaffold/mcp/dist/core/telemetry/state-dir.js +31 -0
  51. package/scaffold/mcp/dist/core/validators/builtins.js +632 -0
  52. package/scaffold/mcp/dist/core/validators/config.js +44 -0
  53. package/scaffold/mcp/dist/core/validators/engine.js +120 -0
  54. package/scaffold/mcp/dist/core/validators/evaluators/code_comments.js +236 -0
  55. package/scaffold/mcp/dist/core/validators/evaluators/regex.js +116 -0
  56. package/scaffold/mcp/dist/core/workflow/artifact-io.js +103 -0
  57. package/scaffold/mcp/dist/core/workflow/capabilities.js +88 -0
  58. package/scaffold/mcp/dist/core/workflow/default-workflows.js +102 -0
  59. package/scaffold/mcp/dist/core/workflow/enforcement.js +164 -0
  60. package/scaffold/mcp/dist/core/workflow/envelope.js +132 -0
  61. package/scaffold/mcp/dist/core/workflow/index.js +11 -0
  62. package/scaffold/mcp/dist/core/workflow/mcp-tools.js +175 -0
  63. package/scaffold/mcp/dist/core/workflow/resolution.js +69 -0
  64. package/scaffold/mcp/dist/core/workflow/run-lifecycle.js +123 -0
  65. package/scaffold/mcp/dist/core/workflow/schemas.js +141 -0
  66. package/scaffold/mcp/dist/core/workflow/synced-capability-registry.js +60 -0
  67. package/scaffold/mcp/dist/core/workflow/synced-registry.js +60 -0
  68. package/scaffold/mcp/dist/daemon/capability-sync-checker.js +242 -0
  69. package/scaffold/mcp/dist/daemon/client.js +130 -0
  70. package/scaffold/mcp/dist/daemon/egress-proxy.js +288 -0
  71. package/scaffold/mcp/dist/daemon/global-host-events.js +222 -0
  72. package/scaffold/mcp/dist/daemon/heartbeat-pusher.js +116 -0
  73. package/scaffold/mcp/dist/daemon/heartbeat-tracker.js +165 -0
  74. package/scaffold/mcp/dist/daemon/host-events-pusher.js +249 -0
  75. package/scaffold/mcp/dist/daemon/main.js +449 -0
  76. package/scaffold/mcp/dist/daemon/paths.js +36 -0
  77. package/scaffold/mcp/dist/daemon/project-service-registry.js +78 -0
  78. package/scaffold/mcp/dist/daemon/protocol.js +8 -0
  79. package/scaffold/mcp/dist/daemon/server.js +180 -0
  80. package/scaffold/mcp/dist/daemon/skill-sync-checker.js +557 -0
  81. package/scaffold/mcp/dist/daemon/sync-checker.js +165 -0
  82. package/scaffold/mcp/dist/daemon/ungoverned-scanner.js +136 -0
  83. package/scaffold/mcp/dist/daemon/workflow-sync-checker.js +249 -0
  84. package/scaffold/mcp/dist/defaults.js +6 -0
  85. package/scaffold/mcp/dist/embed.js +627 -0
  86. package/scaffold/mcp/dist/embedScheduler.js +479 -0
  87. package/scaffold/mcp/dist/embeddings.js +167 -0
  88. package/scaffold/mcp/dist/enterprise/audit/push.js +66 -0
  89. package/scaffold/mcp/dist/enterprise/index.js +327 -0
  90. package/scaffold/mcp/dist/enterprise/model/deploy.js +27 -0
  91. package/scaffold/mcp/dist/enterprise/policy/sync.js +129 -0
  92. package/scaffold/mcp/dist/enterprise/privacy/boundary.js +184 -0
  93. package/scaffold/mcp/dist/enterprise/reviews/changed-files.js +33 -0
  94. package/scaffold/mcp/dist/enterprise/reviews/pattern-context.js +202 -0
  95. package/scaffold/mcp/dist/enterprise/reviews/policy-selection.js +46 -0
  96. package/scaffold/mcp/dist/enterprise/reviews/push.js +102 -0
  97. package/scaffold/mcp/dist/enterprise/reviews/trust-state.js +186 -0
  98. package/scaffold/mcp/dist/enterprise/telemetry/sync.js +57 -0
  99. package/scaffold/mcp/dist/enterprise/tools/enterprise.js +826 -0
  100. package/scaffold/mcp/dist/enterprise/tools/harness.js +40 -0
  101. package/scaffold/mcp/dist/enterprise/tools/walk.js +73 -0
  102. package/scaffold/mcp/dist/enterprise/violations/push.js +77 -0
  103. package/scaffold/mcp/dist/enterprise/workflow/push.js +44 -0
  104. package/scaffold/mcp/dist/enterprise/workflow/state.js +329 -0
  105. package/scaffold/mcp/dist/frontmatter.js +33 -0
  106. package/scaffold/mcp/dist/graph.js +769 -0
  107. package/scaffold/mcp/dist/graphCsv.js +55 -0
  108. package/scaffold/mcp/dist/graphMetrics.js +8 -0
  109. package/scaffold/mcp/dist/hooks/permission-request.js +89 -0
  110. package/scaffold/mcp/dist/hooks/post-tool-use.js +105 -0
  111. package/scaffold/mcp/dist/hooks/pre-compact.js +29 -0
  112. package/scaffold/mcp/dist/hooks/pre-tool-use.js +78 -0
  113. package/scaffold/mcp/dist/hooks/session-end.js +43 -0
  114. package/scaffold/mcp/dist/hooks/session-start.js +41 -0
  115. package/scaffold/mcp/dist/hooks/shared.js +194 -0
  116. package/scaffold/mcp/dist/hooks/stop.js +28 -0
  117. package/scaffold/mcp/dist/hooks/user-prompt-submit.js +33 -0
  118. package/scaffold/mcp/dist/impactPresentation.js +137 -0
  119. package/scaffold/mcp/dist/impactRanking.js +191 -0
  120. package/scaffold/mcp/dist/impactResponse.js +30 -0
  121. package/scaffold/mcp/dist/impactResults.js +105 -0
  122. package/scaffold/mcp/dist/impactSeed.js +20 -0
  123. package/scaffold/mcp/dist/impactTraversal.js +64 -0
  124. package/scaffold/mcp/dist/jsonl.js +77 -0
  125. package/scaffold/mcp/dist/loadGraph.js +759 -0
  126. package/scaffold/mcp/dist/lruCache.js +38 -0
  127. package/scaffold/mcp/dist/paths.js +97 -0
  128. package/scaffold/mcp/dist/patternEvidence.js +272 -0
  129. package/scaffold/mcp/dist/plugin.js +81 -0
  130. package/scaffold/mcp/dist/presets.js +78 -0
  131. package/scaffold/mcp/dist/relatedResponse.js +18 -0
  132. package/scaffold/mcp/dist/relatedTraversal.js +78 -0
  133. package/scaffold/mcp/dist/rules.js +23 -0
  134. package/scaffold/mcp/dist/search.js +212 -0
  135. package/scaffold/mcp/dist/searchCore.js +457 -0
  136. package/scaffold/mcp/dist/searchResults.js +230 -0
  137. package/scaffold/mcp/dist/server.js +317 -0
  138. package/scaffold/mcp/dist/types.js +1 -0
  139. package/scaffold/mcp/package-lock.json +321 -200
  140. package/scaffold/mcp/package.json +15 -6
  141. package/scaffold/mcp/src/cli/enterprise-setup.ts +82 -8
  142. package/scaffold/mcp/src/cli/govern.ts +137 -52
  143. package/scaffold/mcp/src/cli/run.ts +53 -19
  144. package/scaffold/mcp/src/cli/stage.ts +8 -3
  145. package/scaffold/mcp/src/core/config.ts +9 -2
  146. package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
  147. package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
  148. package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
  149. package/scaffold/mcp/src/core/govern-paths.ts +30 -0
  150. package/scaffold/mcp/src/core/license.ts +186 -17
  151. package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
  152. package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
  153. package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
  154. package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
  155. package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
  156. package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
  157. package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
  158. package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
  159. package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
  160. package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
  161. package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
  162. package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
  163. package/scaffold/mcp/src/daemon/main.ts +99 -25
  164. package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
  165. package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
  166. package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
  167. package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
  168. package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
  169. package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
  170. package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
  171. package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
  172. package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
  173. package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
  174. package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
  175. package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
  176. package/scaffold/mcp/src/plugin.ts +20 -0
  177. package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
  178. package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
  179. package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
  180. package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
  181. package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
  182. package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
  183. package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
  184. package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
  185. package/scaffold/mcp/tests/license.test.mjs +367 -0
  186. package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
  187. package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
  188. package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
  189. package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
  190. package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
  191. package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
  192. package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
  193. package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +40 -1
  194. package/scaffold/ownership/baseline-v2.4.1.json +22 -0
  195. package/scaffold/ownership/current.json +4 -0
  196. package/scaffold/ownership/v1.json +456 -0
  197. package/scaffold/scripts/ingest-parsers.mjs +1 -387
  198. package/scaffold/scripts/ingest-worker.mjs +4 -1
  199. package/scaffold/scripts/ingest.mjs +5 -3899
  200. package/scaffold/scripts/lib/ingest/arguments.mjs +78 -0
  201. package/scaffold/scripts/lib/ingest/chunks.mjs +212 -0
  202. package/scaffold/scripts/lib/ingest/config.mjs +120 -0
  203. package/scaffold/scripts/lib/ingest/constants.mjs +222 -0
  204. package/scaffold/scripts/lib/ingest/files.mjs +387 -0
  205. package/scaffold/scripts/lib/ingest/incremental-state.mjs +131 -0
  206. package/scaffold/scripts/lib/ingest/io.mjs +78 -0
  207. package/scaffold/scripts/lib/ingest/main.mjs +76 -0
  208. package/scaffold/scripts/lib/ingest/parser-composition.mjs +140 -0
  209. package/scaffold/scripts/lib/ingest/parser-registry.mjs +387 -0
  210. package/scaffold/scripts/lib/ingest/pipeline-stages.mjs +1625 -0
  211. package/scaffold/scripts/lib/ingest/projects.mjs +272 -0
  212. package/scaffold/scripts/lib/ingest/relations.mjs +860 -0
  213. package/scaffold/scripts/lib/ingest/runtime-paths.mjs +11 -0
  214. package/scaffold/scripts/lib/ingest/workers.mjs +264 -0
@@ -0,0 +1,936 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ const OWNERSHIP_SCHEMA_VERSION = 1;
6
+ const OWNERSHIP_DIR_REL = path.join("scaffold", "ownership");
7
+ const CURRENT_MANIFEST_REL = path.join(OWNERSHIP_DIR_REL, "current.json");
8
+ const INSTALLED_STATE_REL = ".context/scaffold-state.json";
9
+ const SHA256_PATTERN = /^[a-f0-9]{64}$/;
10
+ const BASELINE_ID_PATTERN = /^[A-Za-z0-9._-]+$/;
11
+ const REQUIRED_PROTECTED_FILES = [
12
+ ".context/config.yaml",
13
+ ".context/enterprise.yaml",
14
+ ".context/enterprise.yml",
15
+ ".context/ontology.cypher",
16
+ ".context/rules.yaml",
17
+ "AGENTS.md",
18
+ "CLAUDE.md",
19
+ ];
20
+
21
+ function readJsonFile(filePath, label) {
22
+ let contents;
23
+ try {
24
+ contents = fs.readFileSync(filePath, "utf8");
25
+ } catch (error) {
26
+ throw new Error(`Unable to read ${label} at ${filePath}: ${error.message}`);
27
+ }
28
+ try {
29
+ return JSON.parse(contents);
30
+ } catch (error) {
31
+ throw new Error(`Invalid JSON in ${label} at ${filePath}: ${error.message}`);
32
+ }
33
+ }
34
+
35
+ function assertPlainObject(value, label) {
36
+ if (
37
+ value === null ||
38
+ typeof value !== "object" ||
39
+ Array.isArray(value)
40
+ ) {
41
+ throw new Error(`${label} must be an object`);
42
+ }
43
+ }
44
+
45
+ function normalizePortableRelative(value, label) {
46
+ if (typeof value !== "string" || value.length === 0) {
47
+ throw new Error(`${label} must be a non-empty relative path`);
48
+ }
49
+ if (
50
+ value.includes("\\") ||
51
+ value.includes("\0") ||
52
+ path.posix.isAbsolute(value) ||
53
+ path.win32.isAbsolute(value) ||
54
+ /^[A-Za-z]:/.test(value)
55
+ ) {
56
+ throw new Error(`${label} must be a portable relative path: ${value}`);
57
+ }
58
+ const normalized = path.posix.normalize(value);
59
+ if (
60
+ normalized !== value ||
61
+ normalized === "." ||
62
+ normalized === ".." ||
63
+ normalized.startsWith("../") ||
64
+ normalized.includes("/../")
65
+ ) {
66
+ throw new Error(`${label} escapes or broadens its managed root: ${value}`);
67
+ }
68
+ return normalized;
69
+ }
70
+
71
+ function joinPortable(...parts) {
72
+ return parts.filter((part) => part !== ".").join("/");
73
+ }
74
+
75
+ function normalizeManagedFile(file, label) {
76
+ if (typeof file === "string") {
77
+ const normalized = normalizePortableRelative(file, label);
78
+ return { source: normalized, target: normalized, optional: false };
79
+ }
80
+ assertPlainObject(file, label);
81
+ const source = normalizePortableRelative(file.source, `${label}.source`);
82
+ const target = normalizePortableRelative(file.target, `${label}.target`);
83
+ if (file.optional !== undefined && typeof file.optional !== "boolean") {
84
+ throw new Error(`${label}.optional must be a boolean`);
85
+ }
86
+ return { source, target, optional: file.optional === true };
87
+ }
88
+
89
+ export function validateOwnershipManifest(rawManifest, expectedVersion) {
90
+ assertPlainObject(rawManifest, "Scaffold ownership manifest");
91
+ if (rawManifest.schemaVersion !== OWNERSHIP_SCHEMA_VERSION) {
92
+ throw new Error(
93
+ `Unsupported scaffold ownership schema: ${rawManifest.schemaVersion}`,
94
+ );
95
+ }
96
+ if (
97
+ !Number.isSafeInteger(rawManifest.manifestVersion) ||
98
+ rawManifest.manifestVersion < 1
99
+ ) {
100
+ throw new Error("Scaffold manifestVersion must be a positive integer");
101
+ }
102
+ if (
103
+ expectedVersion !== undefined &&
104
+ rawManifest.manifestVersion !== expectedVersion
105
+ ) {
106
+ throw new Error(
107
+ `Scaffold ownership manifest version mismatch: expected ${expectedVersion}, got ${rawManifest.manifestVersion}`,
108
+ );
109
+ }
110
+ if (
111
+ !Array.isArray(rawManifest.managedRoots) ||
112
+ rawManifest.managedRoots.length === 0
113
+ ) {
114
+ throw new Error("Scaffold ownership manifest must define managedRoots");
115
+ }
116
+ if (!Array.isArray(rawManifest.protectedFiles)) {
117
+ throw new Error("Scaffold ownership manifest must define protectedFiles");
118
+ }
119
+ if (!Array.isArray(rawManifest.preservedFiles)) {
120
+ throw new Error("Scaffold ownership manifest must define preservedFiles");
121
+ }
122
+ if (
123
+ rawManifest.preStateBaselines !== undefined &&
124
+ !Array.isArray(rawManifest.preStateBaselines)
125
+ ) {
126
+ throw new Error(
127
+ "Scaffold ownership manifest preStateBaselines must be an array",
128
+ );
129
+ }
130
+
131
+ const protectedFiles = rawManifest.protectedFiles.map((file, index) =>
132
+ normalizePortableRelative(file, `protectedFiles[${index}]`),
133
+ );
134
+ if (new Set(protectedFiles).size !== protectedFiles.length) {
135
+ throw new Error("Scaffold ownership manifest has duplicate protected files");
136
+ }
137
+ const protectedSet = new Set(protectedFiles);
138
+ for (const requiredPath of REQUIRED_PROTECTED_FILES) {
139
+ if (!protectedSet.has(requiredPath)) {
140
+ throw new Error(
141
+ `Scaffold ownership manifest must protect ${requiredPath}`,
142
+ );
143
+ }
144
+ }
145
+ const targetSet = new Set();
146
+ const managedRoots = rawManifest.managedRoots.map((root, rootIndex) => {
147
+ const label = `managedRoots[${rootIndex}]`;
148
+ assertPlainObject(root, label);
149
+ const source = normalizePortableRelative(root.source, `${label}.source`);
150
+ const target = normalizePortableRelative(root.target, `${label}.target`);
151
+ if (
152
+ root.optionalPrefixes !== undefined &&
153
+ !Array.isArray(root.optionalPrefixes)
154
+ ) {
155
+ throw new Error(`${label}.optionalPrefixes must be an array`);
156
+ }
157
+ const optionalPrefixes =
158
+ root.optionalPrefixes === undefined
159
+ ? []
160
+ : root.optionalPrefixes.map((prefix, prefixIndex) =>
161
+ normalizePortableRelative(
162
+ prefix,
163
+ `${label}.optionalPrefixes[${prefixIndex}]`,
164
+ ),
165
+ );
166
+ if (!Array.isArray(root.files) || root.files.length === 0) {
167
+ throw new Error(`${label}.files must be a non-empty array`);
168
+ }
169
+ const files = root.files.map((file, fileIndex) => {
170
+ const normalized = normalizeManagedFile(
171
+ file,
172
+ `${label}.files[${fileIndex}]`,
173
+ );
174
+ const targetPath = joinPortable(target, normalized.target);
175
+ if (targetPath === INSTALLED_STATE_REL) {
176
+ throw new Error(
177
+ `Installed scaffold state cannot be a managed source: ${targetPath}`,
178
+ );
179
+ }
180
+ if (protectedSet.has(targetPath)) {
181
+ throw new Error(
182
+ `Managed scaffold target is protected and cannot be owned: ${targetPath}`,
183
+ );
184
+ }
185
+ if (targetSet.has(targetPath)) {
186
+ throw new Error(`Duplicate managed scaffold target: ${targetPath}`);
187
+ }
188
+ targetSet.add(targetPath);
189
+ return {
190
+ ...normalized,
191
+ optional:
192
+ normalized.optional ||
193
+ optionalPrefixes.some(
194
+ (prefix) =>
195
+ normalized.source === prefix ||
196
+ normalized.source.startsWith(`${prefix}/`),
197
+ ),
198
+ };
199
+ });
200
+ return { source, target, optionalPrefixes, files };
201
+ });
202
+
203
+ if (
204
+ rawManifest.legacyRoots !== undefined &&
205
+ !Array.isArray(rawManifest.legacyRoots)
206
+ ) {
207
+ throw new Error("Scaffold ownership manifest legacyRoots must be an array");
208
+ }
209
+ const managedRootTargets = new Set(
210
+ managedRoots.map((root) => root.target),
211
+ );
212
+ const legacyTargetSet = new Set();
213
+ const legacyRoots = (rawManifest.legacyRoots ?? []).map(
214
+ (root, rootIndex) => {
215
+ const label = `legacyRoots[${rootIndex}]`;
216
+ assertPlainObject(root, label);
217
+ const sourceManagedRoot = normalizePortableRelative(
218
+ root.sourceManagedRoot,
219
+ `${label}.sourceManagedRoot`,
220
+ );
221
+ const target = normalizePortableRelative(root.target, `${label}.target`);
222
+ if (!managedRootTargets.has(sourceManagedRoot)) {
223
+ throw new Error(
224
+ `${label}.sourceManagedRoot must reference a managed root`,
225
+ );
226
+ }
227
+ if (legacyTargetSet.has(target)) {
228
+ throw new Error(`Duplicate legacy scaffold target root: ${target}`);
229
+ }
230
+ legacyTargetSet.add(target);
231
+ return { sourceManagedRoot, target };
232
+ },
233
+ );
234
+ const expandedLegacyTargetSet = new Set();
235
+ const managedRootsByTarget = new Map(
236
+ managedRoots.map((root) => [root.target, root]),
237
+ );
238
+ for (const legacyRoot of legacyRoots) {
239
+ const sourceRoot = managedRootsByTarget.get(
240
+ legacyRoot.sourceManagedRoot,
241
+ );
242
+ for (const file of sourceRoot.files) {
243
+ const legacyTarget = joinPortable(legacyRoot.target, file.target);
244
+ if (
245
+ targetSet.has(legacyTarget) ||
246
+ protectedSet.has(legacyTarget) ||
247
+ legacyTarget === INSTALLED_STATE_REL
248
+ ) {
249
+ throw new Error(
250
+ `Legacy scaffold target collides with a live target: ${legacyTarget}`,
251
+ );
252
+ }
253
+ if (expandedLegacyTargetSet.has(legacyTarget)) {
254
+ throw new Error(`Duplicate legacy scaffold target: ${legacyTarget}`);
255
+ }
256
+ expandedLegacyTargetSet.add(legacyTarget);
257
+ }
258
+ }
259
+
260
+ const preStateBaselines = (rawManifest.preStateBaselines ?? []).map(
261
+ (baselineId, index) => {
262
+ if (
263
+ typeof baselineId !== "string" ||
264
+ !BASELINE_ID_PATTERN.test(baselineId)
265
+ ) {
266
+ throw new Error(
267
+ `preStateBaselines[${index}] must be a portable baseline identifier`,
268
+ );
269
+ }
270
+ return baselineId;
271
+ },
272
+ );
273
+ if (new Set(preStateBaselines).size !== preStateBaselines.length) {
274
+ throw new Error("Scaffold ownership manifest has duplicate baselines");
275
+ }
276
+
277
+ const preservedTargetSet = new Set();
278
+ const preservedFiles = rawManifest.preservedFiles.map((file, index) => {
279
+ const label = `preservedFiles[${index}]`;
280
+ assertPlainObject(file, label);
281
+ const source = normalizePortableRelative(file.source, `${label}.source`);
282
+ const target = normalizePortableRelative(file.target, `${label}.target`);
283
+ if (target === INSTALLED_STATE_REL) {
284
+ throw new Error(
285
+ `Installed scaffold state cannot be a preserved source: ${target}`,
286
+ );
287
+ }
288
+ if (!protectedSet.has(target)) {
289
+ throw new Error(`Preserved scaffold target must be protected: ${target}`);
290
+ }
291
+ if (preservedTargetSet.has(target)) {
292
+ throw new Error(`Duplicate preserved scaffold target: ${target}`);
293
+ }
294
+ preservedTargetSet.add(target);
295
+ return { source, target };
296
+ });
297
+
298
+ return {
299
+ schemaVersion: OWNERSHIP_SCHEMA_VERSION,
300
+ manifestVersion: rawManifest.manifestVersion,
301
+ managedRoots,
302
+ legacyRoots,
303
+ preStateBaselines,
304
+ protectedFiles,
305
+ preservedFiles,
306
+ };
307
+ }
308
+
309
+ function validateCurrentPointer(rawPointer) {
310
+ assertPlainObject(rawPointer, "Current scaffold ownership pointer");
311
+ if (rawPointer.schemaVersion !== OWNERSHIP_SCHEMA_VERSION) {
312
+ throw new Error(
313
+ `Unsupported scaffold ownership pointer schema: ${rawPointer.schemaVersion}`,
314
+ );
315
+ }
316
+ if (
317
+ !Number.isSafeInteger(rawPointer.manifestVersion) ||
318
+ rawPointer.manifestVersion < 1
319
+ ) {
320
+ throw new Error(
321
+ "Current scaffold ownership pointer must contain a positive manifestVersion",
322
+ );
323
+ }
324
+ return rawPointer.manifestVersion;
325
+ }
326
+
327
+ function ownershipManifestPath(packageRoot, manifestVersion) {
328
+ return path.join(
329
+ packageRoot,
330
+ OWNERSHIP_DIR_REL,
331
+ `v${manifestVersion}.json`,
332
+ );
333
+ }
334
+
335
+ export function loadOwnershipManifest(packageRoot, manifestVersion) {
336
+ const manifestPath = ownershipManifestPath(packageRoot, manifestVersion);
337
+ return validateOwnershipManifest(
338
+ readJsonFile(manifestPath, "scaffold ownership manifest"),
339
+ manifestVersion,
340
+ );
341
+ }
342
+
343
+ export function loadCurrentOwnershipManifest(packageRoot) {
344
+ const pointerPath = path.join(packageRoot, CURRENT_MANIFEST_REL);
345
+ const manifestVersion = validateCurrentPointer(
346
+ readJsonFile(pointerPath, "current scaffold ownership pointer"),
347
+ );
348
+ return loadOwnershipManifest(packageRoot, manifestVersion);
349
+ }
350
+
351
+ export function expandManagedFiles(manifest) {
352
+ return manifest.managedRoots.flatMap((root) =>
353
+ root.files.map((file) => ({
354
+ source: joinPortable(root.source, file.source),
355
+ target: joinPortable(root.target, file.target),
356
+ managedRoot: root.target,
357
+ optional: file.optional,
358
+ })),
359
+ );
360
+ }
361
+
362
+ export function expandLegacyFiles(manifest) {
363
+ const rootsByTarget = new Map(
364
+ manifest.managedRoots.map((root) => [root.target, root]),
365
+ );
366
+ return manifest.legacyRoots.flatMap((legacyRoot) => {
367
+ const sourceRoot = rootsByTarget.get(legacyRoot.sourceManagedRoot);
368
+ return sourceRoot.files.map((file) => ({
369
+ source: joinPortable(sourceRoot.source, file.source),
370
+ target: joinPortable(legacyRoot.target, file.target),
371
+ optional: file.optional,
372
+ }));
373
+ });
374
+ }
375
+
376
+ function validatePreStateBaseline(rawBaseline, expectedId) {
377
+ assertPlainObject(rawBaseline, "Pre-state scaffold ownership baseline");
378
+ if (rawBaseline.schemaVersion !== OWNERSHIP_SCHEMA_VERSION) {
379
+ throw new Error(
380
+ `Unsupported pre-state ownership schema: ${rawBaseline.schemaVersion}`,
381
+ );
382
+ }
383
+ if (
384
+ rawBaseline.baselineId !== expectedId ||
385
+ !BASELINE_ID_PATTERN.test(rawBaseline.baselineId)
386
+ ) {
387
+ throw new Error(
388
+ `Pre-state ownership baseline identifier mismatch: ${rawBaseline.baselineId}`,
389
+ );
390
+ }
391
+ if (
392
+ typeof rawBaseline.sourceCommit !== "string" ||
393
+ !/^[a-f0-9]{40}$/.test(rawBaseline.sourceCommit)
394
+ ) {
395
+ throw new Error(
396
+ "Pre-state ownership baseline must identify a full source commit",
397
+ );
398
+ }
399
+ if (!Array.isArray(rawBaseline.files) || rawBaseline.files.length === 0) {
400
+ throw new Error("Pre-state ownership baseline must define files");
401
+ }
402
+ const targetSet = new Set();
403
+ return {
404
+ schemaVersion: OWNERSHIP_SCHEMA_VERSION,
405
+ baselineId: expectedId,
406
+ sourceCommit: rawBaseline.sourceCommit,
407
+ files: rawBaseline.files.map((file, index) => {
408
+ const label = `Pre-state baseline files[${index}]`;
409
+ assertPlainObject(file, label);
410
+ const target = normalizePortableRelative(
411
+ file.target,
412
+ `${label}.target`,
413
+ );
414
+ if (!SHA256_PATTERN.test(file.sha256)) {
415
+ throw new Error(`${label}.sha256 must be a SHA-256 fingerprint`);
416
+ }
417
+ const legacyTarget =
418
+ file.legacyTarget === undefined
419
+ ? null
420
+ : normalizePortableRelative(
421
+ file.legacyTarget,
422
+ `${label}.legacyTarget`,
423
+ );
424
+ if (targetSet.has(target)) {
425
+ throw new Error(`Duplicate pre-state baseline target: ${target}`);
426
+ }
427
+ targetSet.add(target);
428
+ return { target, sha256: file.sha256, legacyTarget };
429
+ }),
430
+ };
431
+ }
432
+
433
+ function addBaselineHash(hashMap, target, hash) {
434
+ const hashes = hashMap.get(target) ?? new Set();
435
+ hashes.add(hash);
436
+ hashMap.set(target, hashes);
437
+ }
438
+
439
+ export function loadPreStateOwnershipBaselines(packageRoot, manifest) {
440
+ const managedTargets = new Set(
441
+ expandManagedFiles(manifest).map((entry) => entry.target),
442
+ );
443
+ const legacyTargets = new Set(
444
+ expandLegacyFiles(manifest).map((entry) => entry.target),
445
+ );
446
+ const managedHashes = new Map();
447
+ const legacyHashes = new Map();
448
+ for (const baselineId of manifest.preStateBaselines) {
449
+ const baselinePath = path.join(
450
+ packageRoot,
451
+ OWNERSHIP_DIR_REL,
452
+ `baseline-${baselineId}.json`,
453
+ );
454
+ const baseline = validatePreStateBaseline(
455
+ readJsonFile(baselinePath, "pre-state scaffold ownership baseline"),
456
+ baselineId,
457
+ );
458
+ for (const file of baseline.files) {
459
+ if (!managedTargets.has(file.target)) {
460
+ throw new Error(
461
+ `Pre-state baseline target is not currently managed: ${file.target}`,
462
+ );
463
+ }
464
+ addBaselineHash(managedHashes, file.target, file.sha256);
465
+ if (file.legacyTarget !== null) {
466
+ if (!legacyTargets.has(file.legacyTarget)) {
467
+ throw new Error(
468
+ `Pre-state legacy target is not declared for migration: ${file.legacyTarget}`,
469
+ );
470
+ }
471
+ addBaselineHash(legacyHashes, file.legacyTarget, file.sha256);
472
+ }
473
+ }
474
+ }
475
+ return { managedHashes, legacyHashes };
476
+ }
477
+
478
+ function assertContained(rootPath, candidatePath, label) {
479
+ const relative = path.relative(rootPath, candidatePath);
480
+ if (
481
+ relative.length === 0 ||
482
+ relative === ".." ||
483
+ relative.startsWith(`..${path.sep}`) ||
484
+ path.isAbsolute(relative)
485
+ ) {
486
+ throw new Error(`${label} is outside its expected root: ${candidatePath}`);
487
+ }
488
+ }
489
+
490
+ function lstatWithoutSymlinks(rootPath, relativePath, label) {
491
+ const portable = normalizePortableRelative(relativePath, label);
492
+ const candidatePath = path.resolve(
493
+ rootPath,
494
+ ...portable.split("/"),
495
+ );
496
+ assertContained(rootPath, candidatePath, label);
497
+ let currentPath = rootPath;
498
+ const segments = portable.split("/");
499
+ for (let index = 0; index < segments.length; index += 1) {
500
+ currentPath = path.join(currentPath, segments[index]);
501
+ let stat;
502
+ try {
503
+ stat = fs.lstatSync(currentPath);
504
+ } catch (error) {
505
+ if (error && typeof error === "object" && error.code === "ENOENT") {
506
+ return { path: candidatePath, stat: null };
507
+ }
508
+ throw error;
509
+ }
510
+ if (stat.isSymbolicLink()) {
511
+ throw new Error(`Refusing symlinked ${label}: ${currentPath}`);
512
+ }
513
+ if (index < segments.length - 1 && !stat.isDirectory()) {
514
+ throw new Error(`Non-directory ancestor for ${label}: ${currentPath}`);
515
+ }
516
+ if (index === segments.length - 1) {
517
+ return { path: candidatePath, stat };
518
+ }
519
+ }
520
+ throw new Error(`Unable to resolve ${label}: ${relativePath}`);
521
+ }
522
+
523
+ function requireRegularSource(scaffoldRoot, entry) {
524
+ const source = lstatWithoutSymlinks(
525
+ scaffoldRoot,
526
+ entry.source,
527
+ `scaffold source ${entry.source}`,
528
+ );
529
+ if (source.stat === null) {
530
+ if (entry.optional) return null;
531
+ throw new Error(`Required scaffold source is missing: ${source.path}`);
532
+ }
533
+ if (!source.stat.isFile()) {
534
+ throw new Error(`Scaffold source is not a regular file: ${source.path}`);
535
+ }
536
+ return source;
537
+ }
538
+
539
+ function preflightTarget(targetRoot, relativePath, label) {
540
+ const target = lstatWithoutSymlinks(targetRoot, relativePath, label);
541
+ if (target.stat !== null && !target.stat.isFile()) {
542
+ throw new Error(`${label} is not a regular file: ${target.path}`);
543
+ }
544
+ return target;
545
+ }
546
+
547
+ function sha256File(filePath) {
548
+ return crypto
549
+ .createHash("sha256")
550
+ .update(fs.readFileSync(filePath))
551
+ .digest("hex");
552
+ }
553
+
554
+ function validateInstalledState(rawState) {
555
+ assertPlainObject(rawState, "Installed scaffold state");
556
+ if (rawState.schemaVersion !== OWNERSHIP_SCHEMA_VERSION) {
557
+ throw new Error(
558
+ `Unsupported installed scaffold state schema: ${rawState.schemaVersion}`,
559
+ );
560
+ }
561
+ if (
562
+ !Number.isSafeInteger(rawState.manifestVersion) ||
563
+ rawState.manifestVersion < 1
564
+ ) {
565
+ throw new Error(
566
+ "Installed scaffold state must contain a positive manifestVersion",
567
+ );
568
+ }
569
+ assertPlainObject(rawState.fileHashes, "Installed scaffold fileHashes");
570
+ const fileHashes = Object.create(null);
571
+ for (const [relativePath, hash] of Object.entries(rawState.fileHashes)) {
572
+ const normalized = normalizePortableRelative(
573
+ relativePath,
574
+ `Installed scaffold hash path ${relativePath}`,
575
+ );
576
+ if (normalized !== relativePath || !SHA256_PATTERN.test(hash)) {
577
+ throw new Error(
578
+ `Invalid installed scaffold fingerprint for ${relativePath}`,
579
+ );
580
+ }
581
+ fileHashes[normalized] = hash;
582
+ }
583
+ return {
584
+ schemaVersion: OWNERSHIP_SCHEMA_VERSION,
585
+ manifestVersion: rawState.manifestVersion,
586
+ fileHashes,
587
+ };
588
+ }
589
+
590
+ function readInstalledState(targetRoot) {
591
+ const state = lstatWithoutSymlinks(
592
+ targetRoot,
593
+ INSTALLED_STATE_REL,
594
+ "installed scaffold state",
595
+ );
596
+ if (state.stat === null) return null;
597
+ if (!state.stat.isFile()) {
598
+ throw new Error(`Installed scaffold state is not a file: ${state.path}`);
599
+ }
600
+ return validateInstalledState(
601
+ readJsonFile(state.path, "installed scaffold state"),
602
+ );
603
+ }
604
+
605
+ function prepareObsoleteCleanup(
606
+ packageRoot,
607
+ targetRoot,
608
+ currentManifest,
609
+ installedState,
610
+ ) {
611
+ if (
612
+ installedState === null ||
613
+ installedState.manifestVersion === currentManifest.manifestVersion
614
+ ) {
615
+ return [];
616
+ }
617
+ const priorManifest = loadOwnershipManifest(
618
+ packageRoot,
619
+ installedState.manifestVersion,
620
+ );
621
+ const currentTargets = new Set(
622
+ expandManagedFiles(currentManifest).map((entry) => entry.target),
623
+ );
624
+ const protectedTargets = new Set(currentManifest.protectedFiles);
625
+ const obsolete = expandManagedFiles(priorManifest)
626
+ .filter((entry) => !currentTargets.has(entry.target))
627
+ .sort((left, right) => left.target.localeCompare(right.target));
628
+ const prepared = [];
629
+ for (const entry of obsolete) {
630
+ if (protectedTargets.has(entry.target)) {
631
+ throw new Error(
632
+ `Refusing to remove protected obsolete scaffold path: ${entry.target}`,
633
+ );
634
+ }
635
+ const candidate = preflightTarget(
636
+ targetRoot,
637
+ entry.target,
638
+ `obsolete scaffold file ${entry.target}`,
639
+ );
640
+ if (candidate.stat === null) continue;
641
+ const installedHash = installedState.fileHashes[entry.target];
642
+ if (!installedHash) {
643
+ throw new Error(
644
+ `Refusing to remove obsolete scaffold file without an installed fingerprint: ${entry.target}`,
645
+ );
646
+ }
647
+ if (sha256File(candidate.path) !== installedHash) {
648
+ throw new Error(
649
+ `Refusing to remove locally modified obsolete scaffold file: ${entry.target}`,
650
+ );
651
+ }
652
+ prepared.push({
653
+ ...entry,
654
+ path: candidate.path,
655
+ installedHash,
656
+ dev: candidate.stat.dev,
657
+ ino: candidate.stat.ino,
658
+ });
659
+ }
660
+ return prepared;
661
+ }
662
+
663
+ function copyRegularFile(sourcePath, targetPath, beforeRename) {
664
+ const targetDirectory = path.dirname(targetPath);
665
+ fs.mkdirSync(targetDirectory, { recursive: true });
666
+ const temporaryPath = path.join(
667
+ targetDirectory,
668
+ `.${path.basename(targetPath)}.tmp-${process.pid}-${crypto.randomUUID()}`,
669
+ );
670
+ try {
671
+ fs.copyFileSync(sourcePath, temporaryPath, fs.constants.COPYFILE_EXCL);
672
+ fs.chmodSync(temporaryPath, fs.statSync(sourcePath).mode);
673
+ beforeRename();
674
+ fs.renameSync(temporaryPath, targetPath);
675
+ } catch (error) {
676
+ try {
677
+ fs.unlinkSync(temporaryPath);
678
+ } catch {
679
+ // Best effort cleanup for an incomplete atomic replacement.
680
+ }
681
+ throw error;
682
+ }
683
+ }
684
+
685
+ function priorOwnedTargets(packageRoot, installedState) {
686
+ if (installedState === null) return new Set();
687
+ const priorManifest = loadOwnershipManifest(
688
+ packageRoot,
689
+ installedState.manifestVersion,
690
+ );
691
+ return new Set(
692
+ expandManagedFiles(priorManifest)
693
+ .filter((entry) => installedState.fileHashes[entry.target])
694
+ .map((entry) => entry.target),
695
+ );
696
+ }
697
+
698
+ function assertTargetUnchanged(targetRoot, entry, initialTarget) {
699
+ const currentTarget = preflightTarget(
700
+ targetRoot,
701
+ entry.target,
702
+ `managed scaffold target ${entry.target}`,
703
+ );
704
+ if (initialTarget.stat === null) {
705
+ if (currentTarget.stat !== null) {
706
+ throw new Error(
707
+ `Managed scaffold target appeared during install: ${entry.target}`,
708
+ );
709
+ }
710
+ return currentTarget;
711
+ }
712
+ if (
713
+ currentTarget.stat === null ||
714
+ currentTarget.stat.dev !== initialTarget.stat.dev ||
715
+ currentTarget.stat.ino !== initialTarget.stat.ino
716
+ ) {
717
+ throw new Error(
718
+ `Managed scaffold target changed during install: ${entry.target}`,
719
+ );
720
+ }
721
+ return currentTarget;
722
+ }
723
+
724
+ function removePreparedObsolete(targetRoot, entry) {
725
+ const candidate = preflightTarget(
726
+ targetRoot,
727
+ entry.target,
728
+ `obsolete scaffold file ${entry.target}`,
729
+ );
730
+ if (candidate.stat === null) return false;
731
+ if (
732
+ candidate.stat.dev !== entry.dev ||
733
+ candidate.stat.ino !== entry.ino ||
734
+ sha256File(candidate.path) !== entry.installedHash
735
+ ) {
736
+ throw new Error(
737
+ `Refusing to remove obsolete scaffold file changed during cleanup: ${entry.target}`,
738
+ );
739
+ }
740
+ fs.unlinkSync(candidate.path);
741
+ return true;
742
+ }
743
+
744
+ function writeInstalledState(targetRoot, manifest, installedHashes) {
745
+ const fileHashes = {};
746
+ for (const entry of expandManagedFiles(manifest).sort((left, right) =>
747
+ left.target.localeCompare(right.target),
748
+ )) {
749
+ const installedHash = installedHashes.get(entry.target);
750
+ if (!installedHash) continue;
751
+ const target = preflightTarget(
752
+ targetRoot,
753
+ entry.target,
754
+ `installed scaffold file ${entry.target}`,
755
+ );
756
+ if (target.stat === null) {
757
+ throw new Error(
758
+ `Installed scaffold file disappeared before state persistence: ${entry.target}`,
759
+ );
760
+ }
761
+ fileHashes[entry.target] =
762
+ installedHash === true ? sha256File(target.path) : installedHash;
763
+ }
764
+ const statePath = path.join(
765
+ targetRoot,
766
+ ...INSTALLED_STATE_REL.split("/"),
767
+ );
768
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
769
+ preflightTarget(targetRoot, INSTALLED_STATE_REL, "installed scaffold state");
770
+ const temporaryPath = `${statePath}.tmp-${process.pid}-${crypto.randomUUID()}`;
771
+ try {
772
+ fs.writeFileSync(
773
+ temporaryPath,
774
+ `${JSON.stringify(
775
+ {
776
+ schemaVersion: OWNERSHIP_SCHEMA_VERSION,
777
+ manifestVersion: manifest.manifestVersion,
778
+ fileHashes,
779
+ },
780
+ null,
781
+ 2,
782
+ )}\n`,
783
+ { encoding: "utf8", flag: "wx", mode: 0o644 },
784
+ );
785
+ fs.renameSync(temporaryPath, statePath);
786
+ } catch (error) {
787
+ try {
788
+ fs.unlinkSync(temporaryPath);
789
+ } catch {
790
+ // Best effort cleanup for an incomplete state write.
791
+ }
792
+ throw error;
793
+ }
794
+ }
795
+
796
+ export function installManagedScaffold(
797
+ packageRoot,
798
+ targetDir,
799
+ { force, manifestVersion } = {},
800
+ ) {
801
+ const packageRootReal = fs.realpathSync(packageRoot);
802
+ const scaffoldRoot = fs.realpathSync(path.join(packageRootReal, "scaffold"));
803
+ const targetRoot = fs.realpathSync(targetDir);
804
+ const currentManifest =
805
+ manifestVersion === undefined
806
+ ? loadCurrentOwnershipManifest(packageRootReal)
807
+ : loadOwnershipManifest(packageRootReal, manifestVersion);
808
+ const managedFiles = expandManagedFiles(currentManifest);
809
+ const sources = new Map();
810
+ const initialTargets = new Map();
811
+ const initialPreservedTargets = new Map();
812
+
813
+ for (const entry of managedFiles) {
814
+ sources.set(entry.target, requireRegularSource(scaffoldRoot, entry));
815
+ initialTargets.set(
816
+ entry.target,
817
+ preflightTarget(
818
+ targetRoot,
819
+ entry.target,
820
+ `managed scaffold target ${entry.target}`,
821
+ ),
822
+ );
823
+ }
824
+ for (const entry of currentManifest.preservedFiles) {
825
+ const source = requireRegularSource(scaffoldRoot, {
826
+ source: entry.source,
827
+ optional: false,
828
+ });
829
+ sources.set(entry.target, source);
830
+ initialPreservedTargets.set(
831
+ entry.target,
832
+ preflightTarget(
833
+ targetRoot,
834
+ entry.target,
835
+ `preserved scaffold target ${entry.target}`,
836
+ ),
837
+ );
838
+ }
839
+
840
+ const installedState = readInstalledState(targetRoot);
841
+ const ownedTargets = priorOwnedTargets(packageRootReal, installedState);
842
+ const preStateOwnership = loadPreStateOwnershipBaselines(
843
+ packageRootReal,
844
+ currentManifest,
845
+ );
846
+ if (force === true) {
847
+ for (const entry of managedFiles) {
848
+ const source = sources.get(entry.target);
849
+ const target = initialTargets.get(entry.target);
850
+ if (source === null || target.stat === null) continue;
851
+ if (installedState !== null) {
852
+ if (!ownedTargets.has(entry.target)) {
853
+ throw new Error(
854
+ `Refusing to overwrite unowned scaffold collision: ${entry.target}`,
855
+ );
856
+ }
857
+ } else {
858
+ const targetHash = sha256File(target.path);
859
+ const matchesCurrentSource = targetHash === sha256File(source.path);
860
+ const matchesBaseline =
861
+ preStateOwnership.managedHashes
862
+ .get(entry.target)
863
+ ?.has(targetHash) === true;
864
+ if (!matchesCurrentSource && !matchesBaseline) {
865
+ throw new Error(
866
+ `Refusing to overwrite unowned scaffold collision: ${entry.target}`,
867
+ );
868
+ }
869
+ }
870
+ }
871
+ }
872
+ const obsolete =
873
+ force === true
874
+ ? prepareObsoleteCleanup(
875
+ packageRootReal,
876
+ targetRoot,
877
+ currentManifest,
878
+ installedState,
879
+ )
880
+ : [];
881
+
882
+ const removed = [];
883
+ for (const entry of obsolete) {
884
+ if (removePreparedObsolete(targetRoot, entry)) {
885
+ removed.push(entry.target);
886
+ }
887
+ }
888
+
889
+ let copied = 0;
890
+ const installedHashes = new Map();
891
+ for (const entry of managedFiles) {
892
+ const source = sources.get(entry.target);
893
+ const targetPath = path.join(targetRoot, ...entry.target.split("/"));
894
+ if (source === null) {
895
+ const priorHash = installedState?.fileHashes[entry.target];
896
+ if (priorHash && fs.existsSync(targetPath)) {
897
+ installedHashes.set(entry.target, priorHash);
898
+ }
899
+ continue;
900
+ }
901
+ const initialTarget = initialTargets.get(entry.target);
902
+ if (force !== true && initialTarget.stat !== null) {
903
+ const priorHash = installedState?.fileHashes[entry.target];
904
+ if (priorHash && ownedTargets.has(entry.target)) {
905
+ installedHashes.set(entry.target, priorHash);
906
+ }
907
+ continue;
908
+ }
909
+ copyRegularFile(source.path, targetPath, () =>
910
+ assertTargetUnchanged(targetRoot, entry, initialTarget),
911
+ );
912
+ installedHashes.set(entry.target, true);
913
+ copied += 1;
914
+ }
915
+ for (const entry of currentManifest.preservedFiles) {
916
+ const targetPath = path.join(targetRoot, ...entry.target.split("/"));
917
+ const initialTarget = initialPreservedTargets.get(entry.target);
918
+ if (initialTarget.stat !== null) continue;
919
+ copyRegularFile(sources.get(entry.target).path, targetPath, () =>
920
+ assertTargetUnchanged(targetRoot, entry, initialTarget),
921
+ );
922
+ copied += 1;
923
+ }
924
+
925
+ writeInstalledState(targetRoot, currentManifest, installedHashes);
926
+ return {
927
+ manifest: currentManifest,
928
+ copied,
929
+ removed,
930
+ };
931
+ }
932
+
933
+ export const scaffoldOwnershipConstants = Object.freeze({
934
+ schemaVersion: OWNERSHIP_SCHEMA_VERSION,
935
+ installedStateRelativePath: INSTALLED_STATE_REL,
936
+ });