@phyzess/pappra 0.1.0-beta.13 → 0.1.0-beta.18

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 (357) hide show
  1. package/README.md +134 -15
  2. package/assets/integrations/skills/pappra/SKILL.md +28 -7
  3. package/assets/integrations/skills/pappra/references/collaboration.md +633 -0
  4. package/assets/integrations/skills/pappra/references/environment.md +360 -0
  5. package/assets/integrations/skills/pappra/references/hosts.md +20 -0
  6. package/assets/integrations/skills/pappra/references/operations.md +36 -5
  7. package/dist/adapters/pi/recovery-extension.d.ts +65 -0
  8. package/dist/adapters/pi/recovery-extension.js +238 -0
  9. package/dist/adapters/pi/session-transition-extension.d.ts +30 -0
  10. package/dist/adapters/pi/session-transition-extension.js +265 -0
  11. package/dist/app/automatic-handoff-events.d.ts +26 -0
  12. package/dist/app/automatic-handoff-events.js +134 -0
  13. package/dist/app/bounded-file.d.ts +2 -0
  14. package/dist/app/bounded-file.js +2 -0
  15. package/dist/app/capability-evidence/claude.d.ts +37 -0
  16. package/dist/app/capability-evidence/claude.js +206 -0
  17. package/dist/app/capability-evidence/codex.d.ts +35 -0
  18. package/dist/app/capability-evidence/codex.js +193 -0
  19. package/dist/app/capability-evidence/index.d.ts +33 -0
  20. package/dist/app/capability-evidence/index.js +117 -0
  21. package/dist/app/capability-evidence/pi.d.ts +73 -0
  22. package/dist/app/capability-evidence/pi.js +669 -0
  23. package/dist/app/capability-evidence/shared.d.ts +69 -0
  24. package/dist/app/capability-evidence/shared.js +195 -0
  25. package/dist/app/collab-auto-handoff.d.ts +55 -0
  26. package/dist/app/collab-auto-handoff.js +384 -0
  27. package/dist/app/collab-bindings.d.ts +57 -0
  28. package/dist/app/collab-bindings.js +1006 -0
  29. package/dist/app/collab-bootstrap-transport.d.ts +163 -0
  30. package/dist/app/collab-bootstrap-transport.js +769 -0
  31. package/dist/app/collab-bootstrap.d.ts +137 -0
  32. package/dist/app/collab-bootstrap.js +3455 -0
  33. package/dist/app/collab-handoff.d.ts +12 -0
  34. package/dist/app/collab-handoff.js +511 -0
  35. package/dist/app/collab-messages.d.ts +2 -0
  36. package/dist/app/collab-messages.js +2 -0
  37. package/dist/app/collab-notify.d.ts +156 -0
  38. package/dist/app/collab-notify.js +1208 -0
  39. package/dist/app/collab-preflight.d.ts +34 -0
  40. package/dist/app/collab-preflight.js +96 -0
  41. package/dist/app/collab-recovery-executor.d.ts +161 -0
  42. package/dist/app/collab-recovery-executor.js +1153 -0
  43. package/dist/app/collab-recovery-projection.d.ts +103 -0
  44. package/dist/app/collab-recovery-projection.js +659 -0
  45. package/dist/app/collab-recovery.d.ts +25 -0
  46. package/dist/app/collab-recovery.js +429 -0
  47. package/dist/app/collab-resources.d.ts +16 -0
  48. package/dist/app/collab-resources.js +278 -0
  49. package/dist/app/collab-role-events.d.ts +22 -0
  50. package/dist/app/collab-role-events.js +329 -0
  51. package/dist/app/collab-role-orchestration.d.ts +53 -0
  52. package/dist/app/collab-role-orchestration.js +601 -0
  53. package/dist/app/collab-role-plan.d.ts +81 -0
  54. package/dist/app/collab-role-plan.js +504 -0
  55. package/dist/app/collab-role-repair.d.ts +109 -0
  56. package/dist/app/collab-role-repair.js +821 -0
  57. package/dist/app/collab-rollover.d.ts +263 -0
  58. package/dist/app/collab-rollover.js +1112 -0
  59. package/dist/app/collab-route.d.ts +64 -0
  60. package/dist/app/collab-route.js +240 -0
  61. package/dist/app/collab-session-transition.d.ts +221 -0
  62. package/dist/app/collab-session-transition.js +3334 -0
  63. package/dist/app/collab-state.d.ts +7 -0
  64. package/dist/app/collab-state.js +162 -0
  65. package/dist/app/collab-task-store.d.ts +22 -0
  66. package/dist/app/collab-task-store.js +204 -0
  67. package/dist/app/collab-work-execution.d.ts +127 -0
  68. package/dist/app/collab-work-execution.js +1027 -0
  69. package/dist/app/collab-work-plan.d.ts +87 -0
  70. package/dist/app/collab-work-plan.js +437 -0
  71. package/dist/app/collaboration.d.ts +8 -2
  72. package/dist/app/collaboration.js +101 -12
  73. package/dist/app/doctor.js +108 -5
  74. package/dist/app/host-mcp.js +34 -8
  75. package/dist/app/host-probe.js +4 -4
  76. package/dist/app/init.d.ts +58 -4
  77. package/dist/app/init.js +768 -155
  78. package/dist/app/managed-pi-bootstrap-admission.d.ts +14 -0
  79. package/dist/app/managed-pi-bootstrap-admission.js +104 -0
  80. package/dist/app/managed-pi-bootstrap-refresh.d.ts +10 -0
  81. package/dist/app/managed-pi-bootstrap-refresh.js +78 -0
  82. package/dist/app/managed-pi-delivery.d.ts +8 -0
  83. package/dist/app/managed-pi-delivery.js +110 -0
  84. package/dist/app/managed-pi-handoff-receiver.d.ts +15 -0
  85. package/dist/app/managed-pi-handoff-receiver.js +263 -0
  86. package/dist/app/managed-pi-handoff-reference.d.ts +3 -0
  87. package/dist/app/managed-pi-handoff-reference.js +60 -0
  88. package/dist/app/managed-pi-herdr.d.ts +21 -0
  89. package/dist/app/managed-pi-herdr.js +185 -0
  90. package/dist/app/managed-pi-initial-admission.d.ts +28 -0
  91. package/dist/app/managed-pi-initial-admission.js +416 -0
  92. package/dist/app/managed-pi-initial-launch.d.ts +5 -0
  93. package/dist/app/managed-pi-initial-launch.js +61 -0
  94. package/dist/app/managed-pi-native-observation.d.ts +36 -0
  95. package/dist/app/managed-pi-native-observation.js +191 -0
  96. package/dist/app/managed-pi-readiness.d.ts +29 -0
  97. package/dist/app/managed-pi-readiness.js +288 -0
  98. package/dist/app/managed-pi-repair-stop.d.ts +21 -0
  99. package/dist/app/managed-pi-repair-stop.js +371 -0
  100. package/dist/app/managed-pi-resume-admission.d.ts +32 -0
  101. package/dist/app/managed-pi-resume-admission.js +618 -0
  102. package/dist/app/managed-pi-retirement.d.ts +28 -0
  103. package/dist/app/managed-pi-retirement.js +112 -0
  104. package/dist/app/managed-pi-sdk-control.d.ts +7 -0
  105. package/dist/app/managed-pi-sdk-control.js +169 -0
  106. package/dist/app/managed-pi-sdk-runtime.d.ts +3 -0
  107. package/dist/app/managed-pi-sdk-runtime.js +483 -0
  108. package/dist/app/managed-pi-source-admission.d.ts +14 -0
  109. package/dist/app/managed-pi-source-admission.js +323 -0
  110. package/dist/app/managed-pi-transition-host.d.ts +36 -0
  111. package/dist/app/managed-pi-transition-host.js +292 -0
  112. package/dist/app/managed-pi.d.ts +54 -0
  113. package/dist/app/managed-pi.js +248 -0
  114. package/dist/app/shell-readiness.d.ts +66 -0
  115. package/dist/app/shell-readiness.js +368 -0
  116. package/dist/app/task-pin.d.ts +2 -23
  117. package/dist/app/task-pin.js +2 -113
  118. package/dist/cli/commands/collaboration-binding.d.ts +13 -0
  119. package/dist/cli/commands/collaboration-binding.js +118 -0
  120. package/dist/cli/commands/collaboration-bootstrap.d.ts +10 -0
  121. package/dist/cli/commands/collaboration-bootstrap.js +84 -0
  122. package/dist/cli/commands/collaboration-capabilities.d.ts +13 -0
  123. package/dist/cli/commands/collaboration-capabilities.js +127 -0
  124. package/dist/cli/commands/collaboration-handoff.d.ts +13 -0
  125. package/dist/cli/commands/collaboration-handoff.js +102 -0
  126. package/dist/cli/commands/collaboration-message.d.ts +17 -0
  127. package/dist/cli/commands/collaboration-message.js +177 -0
  128. package/dist/cli/commands/collaboration-plan.d.ts +8 -0
  129. package/dist/cli/commands/collaboration-plan.js +108 -0
  130. package/dist/cli/commands/collaboration-preflight.d.ts +10 -0
  131. package/dist/cli/commands/collaboration-preflight.js +167 -0
  132. package/dist/cli/commands/collaboration-recovery-run.d.ts +46 -0
  133. package/dist/cli/commands/collaboration-recovery-run.js +224 -0
  134. package/dist/cli/commands/collaboration-recovery.d.ts +11 -0
  135. package/dist/cli/commands/collaboration-recovery.js +104 -0
  136. package/dist/cli/commands/collaboration-resources.d.ts +11 -0
  137. package/dist/cli/commands/collaboration-resources.js +96 -0
  138. package/dist/cli/commands/collaboration-role-plan.d.ts +9 -0
  139. package/dist/cli/commands/collaboration-role-plan.js +69 -0
  140. package/dist/cli/commands/collaboration-role-run.d.ts +11 -0
  141. package/dist/cli/commands/collaboration-role-run.js +128 -0
  142. package/dist/cli/commands/collaboration-rollover.d.ts +10 -0
  143. package/dist/cli/commands/collaboration-rollover.js +103 -0
  144. package/dist/cli/commands/collaboration-state.d.ts +10 -0
  145. package/dist/cli/commands/collaboration-state.js +96 -0
  146. package/dist/cli/commands/collaboration-transition.d.ts +10 -0
  147. package/dist/cli/commands/collaboration-transition.js +75 -0
  148. package/dist/cli/commands/collaboration-work-run.d.ts +34 -0
  149. package/dist/cli/commands/collaboration-work-run.js +178 -0
  150. package/dist/cli/commands/collaboration.d.ts +4 -0
  151. package/dist/cli/commands/collaboration.js +39 -4
  152. package/dist/cli/commands/continuity.js +2 -6
  153. package/dist/cli/commands/docs.d.ts +2 -0
  154. package/dist/cli/commands/docs.js +89 -15
  155. package/dist/cli/commands/host.d.ts +7 -1
  156. package/dist/cli/commands/host.js +122 -7
  157. package/dist/cli/commands/init.d.ts +20 -3
  158. package/dist/cli/commands/init.js +897 -232
  159. package/dist/cli/commands/mcp.d.ts +1 -0
  160. package/dist/cli/commands/mcp.js +11 -5
  161. package/dist/cli/commands/repair.d.ts +12 -0
  162. package/dist/cli/commands/repair.js +41 -0
  163. package/dist/cli/commands/runtime.d.ts +1 -1
  164. package/dist/cli/commands/runtime.js +139 -38
  165. package/dist/cli/commands/session.d.ts +7 -0
  166. package/dist/cli/commands/session.js +82 -0
  167. package/dist/cli/commands/task.js +19 -5
  168. package/dist/cli/run.js +866 -24
  169. package/dist/code/identity.d.ts +7 -0
  170. package/dist/code/identity.js +7 -3
  171. package/dist/code/registration.js +7 -0
  172. package/dist/code/service.d.ts +9 -0
  173. package/dist/code/service.js +16 -3
  174. package/dist/code/text.js +19 -7
  175. package/dist/collab/admission.d.ts +91 -66
  176. package/dist/collab/admission.js +21 -6
  177. package/dist/collab/canonical.d.ts +1 -1
  178. package/dist/collab/canonical.js +44 -3
  179. package/dist/collab/envelope.d.ts +2 -2
  180. package/dist/collab/envelope.js +12 -12
  181. package/dist/collab/first-work.d.ts +3 -0
  182. package/dist/collab/first-work.js +15 -0
  183. package/dist/collab/handoff.d.ts +16 -0
  184. package/dist/collab/handoff.js +39 -5
  185. package/dist/collab/index.d.ts +97 -1
  186. package/dist/collab/index.js +9 -1
  187. package/dist/collab/messages.js +126 -3
  188. package/dist/collab/persisted-messages.d.ts +123 -0
  189. package/dist/collab/persisted-messages.js +496 -0
  190. package/dist/collab/preflight.d.ts +1 -1
  191. package/dist/collab/resources.d.ts +13 -3
  192. package/dist/collab/resources.js +37 -6
  193. package/dist/collab/rollover.d.ts +311 -0
  194. package/dist/collab/rollover.js +1024 -0
  195. package/dist/collab/store.d.ts +1 -0
  196. package/dist/collab/store.js +1 -4
  197. package/dist/collab/types.d.ts +2 -354
  198. package/dist/collab/validation.d.ts +2 -10
  199. package/dist/collab/validation.js +2 -303
  200. package/dist/collab/work-plan.d.ts +105 -0
  201. package/dist/collab/work-plan.js +843 -0
  202. package/dist/continuity/context.d.ts +11 -2
  203. package/dist/continuity/context.js +100 -13
  204. package/dist/continuity/handoff-bundle.d.ts +296 -0
  205. package/dist/continuity/handoff-bundle.js +477 -0
  206. package/dist/continuity/index.d.ts +2 -0
  207. package/dist/continuity/index.js +1 -0
  208. package/dist/continuity/paths.js +10 -2
  209. package/dist/continuity/service.js +91 -15
  210. package/dist/continuity/task-pin.d.ts +24 -0
  211. package/dist/continuity/task-pin.js +590 -0
  212. package/dist/doctor.d.ts +1 -0
  213. package/dist/doctor.js +6 -0
  214. package/dist/experimental/index.d.ts +13 -0
  215. package/dist/experimental/index.js +25 -0
  216. package/dist/experimental/pi.d.ts +4 -0
  217. package/dist/experimental/pi.js +12 -0
  218. package/dist/hosts/claude/native.js +1 -1
  219. package/dist/hosts/codex/native.js +1 -1
  220. package/dist/hosts/codex/protocol.js +1 -1
  221. package/dist/hosts/integration/bootstrap-config.d.ts +58 -0
  222. package/dist/hosts/integration/bootstrap-config.js +254 -0
  223. package/dist/hosts/integration/bundle-export.d.ts +44 -0
  224. package/dist/hosts/integration/bundle-export.js +366 -3
  225. package/dist/hosts/integration/bundle.js +3 -0
  226. package/dist/hosts/integration/hook.js +36 -9
  227. package/dist/hosts/integration/pappra-bootstrap-extension.mjs +80 -0
  228. package/dist/hosts/integration/pappra-extension-runtime.mjs +59 -24631
  229. package/dist/hosts/integration/pappra-hook.mjs +61 -24749
  230. package/dist/hosts/integration/pappra-recovery-extension.mjs +259 -0
  231. package/dist/hosts/integration/pappra-session-transition-extension.mjs +276 -0
  232. package/dist/hosts/integration/pi-bootstrap-extension.d.ts +36 -0
  233. package/dist/hosts/integration/pi-bootstrap-extension.js +101 -0
  234. package/dist/hosts/integration/pi-bootstrap-guard.d.ts +125 -0
  235. package/dist/hosts/integration/pi-bootstrap-guard.js +438 -0
  236. package/dist/hosts/integration/pi-bootstrap-runtime.d.ts +63 -0
  237. package/dist/hosts/integration/pi-bootstrap-runtime.js +223 -0
  238. package/dist/hosts/integration/pi-native-events.d.ts +112 -0
  239. package/dist/hosts/integration/pi-native-events.js +348 -0
  240. package/dist/hosts/integration/pi-rollover-extension.d.ts +166 -0
  241. package/dist/hosts/integration/pi-rollover-extension.js +431 -0
  242. package/dist/hosts/integration/recovery-config.d.ts +48 -0
  243. package/dist/hosts/integration/recovery-config.js +117 -0
  244. package/dist/hosts/integration/service.d.ts +6 -1
  245. package/dist/hosts/integration/service.js +485 -35
  246. package/dist/hosts/integration/support.d.ts +7 -0
  247. package/dist/hosts/integration/support.js +9 -1
  248. package/dist/hosts/integration/templates.d.ts +3 -0
  249. package/dist/hosts/integration/templates.js +26 -3
  250. package/dist/hosts/pi/managed-host-identity.d.ts +2 -0
  251. package/dist/hosts/pi/managed-host-identity.js +40 -0
  252. package/dist/hosts/pi/managed-provider-diagnostics.d.ts +6 -0
  253. package/dist/hosts/pi/managed-provider-diagnostics.js +184 -0
  254. package/dist/hosts/pi/managed-sdk-session.d.ts +335 -0
  255. package/dist/hosts/pi/managed-sdk-session.js +1639 -0
  256. package/dist/hosts/pi/managed-session-extension.d.ts +20 -0
  257. package/dist/hosts/pi/managed-session-extension.js +84 -0
  258. package/dist/hosts/pi/managed-session.d.ts +177 -0
  259. package/dist/hosts/pi/managed-session.js +1557 -0
  260. package/dist/hosts/pi/protocol.js +1 -1
  261. package/dist/hosts/pi/query-result.js +1 -1
  262. package/dist/hosts/pi/resume-capability.d.ts +41 -0
  263. package/dist/hosts/pi/resume-capability.js +33 -0
  264. package/dist/hosts/pi/rpc.d.ts +182 -134
  265. package/dist/hosts/pi/rpc.js +230 -37
  266. package/dist/hosts/pilot.d.ts +97 -74
  267. package/dist/hosts/pilot.js +5 -1
  268. package/dist/hosts/version.d.ts +3 -0
  269. package/dist/hosts/version.js +18 -0
  270. package/dist/index.d.ts +0 -1
  271. package/dist/index.js +0 -1
  272. package/dist/infra/bounded-file.d.ts +19 -0
  273. package/dist/infra/bounded-file.js +66 -0
  274. package/dist/infra/fs.js +10 -2
  275. package/dist/infra/git.d.ts +29 -0
  276. package/dist/infra/git.js +45 -9
  277. package/dist/infra/json-property.d.ts +2 -0
  278. package/dist/infra/json-property.js +69 -0
  279. package/dist/infra/json.d.ts +4 -0
  280. package/dist/infra/json.js +24 -0
  281. package/dist/infra/owned-process-group.js +1 -1
  282. package/dist/infra/private-lock.d.ts +16 -0
  283. package/dist/infra/private-lock.js +148 -0
  284. package/dist/infra/private-state.d.ts +7 -0
  285. package/dist/infra/private-state.js +146 -0
  286. package/dist/infra/process-identity.d.ts +12 -0
  287. package/dist/infra/process-identity.js +142 -0
  288. package/dist/infra/validation.d.ts +5 -0
  289. package/dist/infra/validation.js +15 -0
  290. package/dist/init.d.ts +1 -0
  291. package/dist/init.js +6 -0
  292. package/dist/install/apply.d.ts +9 -1
  293. package/dist/install/apply.js +176 -12
  294. package/dist/install/dependencies.d.ts +38 -0
  295. package/dist/install/dependencies.js +282 -0
  296. package/dist/install/ownership.js +30 -1
  297. package/dist/install/references.d.ts +31 -0
  298. package/dist/install/references.js +208 -0
  299. package/dist/install/repair.d.ts +37 -0
  300. package/dist/install/repair.js +798 -0
  301. package/dist/install/reset-local.d.ts +16 -0
  302. package/dist/install/reset-local.js +304 -0
  303. package/dist/install/rollback.d.ts +3 -0
  304. package/dist/install/rollback.js +9 -3
  305. package/dist/install/runtime.d.ts +10 -6
  306. package/dist/install/runtime.js +299 -59
  307. package/dist/install/source-fingerprint.d.ts +2 -0
  308. package/dist/install/source-fingerprint.js +103 -0
  309. package/dist/knowledge/catalog.d.ts +1 -0
  310. package/dist/knowledge/catalog.js +143 -10
  311. package/dist/knowledge/journal.js +17 -7
  312. package/dist/knowledge/paths.js +2 -1
  313. package/dist/knowledge/refs.js +4 -0
  314. package/dist/knowledge/register-batch.d.ts +4 -0
  315. package/dist/knowledge/register-batch.js +225 -0
  316. package/dist/knowledge/relocate.d.ts +5 -0
  317. package/dist/knowledge/relocate.js +92 -0
  318. package/dist/knowledge/scan.js +103 -47
  319. package/dist/knowledge/service.d.ts +3 -0
  320. package/dist/knowledge/service.js +33 -1
  321. package/dist/knowledge/write.d.ts +4 -1
  322. package/dist/knowledge/write.js +1 -1
  323. package/dist/mcp/server.d.ts +4 -2
  324. package/dist/mcp/server.js +87 -136
  325. package/dist/schemas/collaboration-state-validation.d.ts +6 -0
  326. package/dist/schemas/collaboration-state-validation.js +342 -0
  327. package/dist/schemas/collaboration-state.d.ts +436 -0
  328. package/dist/schemas/collaboration-state.js +1 -0
  329. package/dist/schemas/collaboration.d.ts +335 -0
  330. package/dist/schemas/collaboration.js +272 -0
  331. package/dist/schemas/continuity.d.ts +8 -0
  332. package/dist/schemas/continuity.js +13 -3
  333. package/dist/schemas/doctor.d.ts +1 -0
  334. package/dist/schemas/doctor.js +1 -0
  335. package/dist/schemas/document.d.ts +61 -0
  336. package/dist/schemas/document.js +9 -0
  337. package/dist/schemas/errors.d.ts +1 -0
  338. package/dist/schemas/errors.js +1 -0
  339. package/dist/schemas/host.d.ts +24 -0
  340. package/dist/schemas/host.js +19 -0
  341. package/dist/schemas/install.d.ts +8 -2
  342. package/dist/schemas/install.js +11 -1
  343. package/dist/schemas/managed-pi-execution.d.ts +16 -0
  344. package/dist/schemas/managed-pi-execution.js +2 -0
  345. package/dist/schemas/outcome.d.ts +9 -5
  346. package/dist/workspace/config-file.js +1 -3
  347. package/dist/workspace/documents.js +12 -2
  348. package/dist/workspace/repositories.d.ts +7 -0
  349. package/dist/workspace/repositories.js +14 -5
  350. package/package.json +35 -35
  351. package/skills/pappra/SKILL.md +28 -7
  352. package/skills/pappra/references/collaboration.md +633 -0
  353. package/skills/pappra/references/environment.md +360 -0
  354. package/skills/pappra/references/hosts.md +20 -0
  355. package/skills/pappra/references/operations.md +36 -5
  356. package/dist/app/collab-from.d.ts +0 -35
  357. package/dist/app/collab-from.js +0 -84
@@ -0,0 +1,15 @@
1
+ /** Decode external JSON as unknown; domain validators establish usable shapes. */
2
+ export function object(value) {
3
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
4
+ }
5
+ export function text(value) {
6
+ return typeof value === 'string' && value.trim().length > 0;
7
+ }
8
+ export function errorCode(error) {
9
+ return object(error) && typeof error['code'] === 'string'
10
+ ? error['code']
11
+ : undefined;
12
+ }
13
+ export function parseJson(text) {
14
+ return JSON.parse(text);
15
+ }
package/dist/init.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './app/init.js';
package/dist/init.js ADDED
@@ -0,0 +1,6 @@
1
+ // Core entrypoint for workspace initialization.
2
+ //
3
+ // The implementation lives in `src/app/init.ts`; this barrel keeps the
4
+ // published `@phyzess/pappra/init` path independent of the internal `app/`
5
+ // layout.
6
+ export * from './app/init.js';
@@ -2,6 +2,7 @@ import type { Diagnostic } from '../schemas/diagnostics.js';
2
2
  import { type InstallJournal, type ManagedFileEntry, type RuntimeManifest, type WorkspaceInstallManifest } from '../schemas/install.js';
3
3
  import type { InitPlan } from '../schemas/init.js';
4
4
  import type { PlannedFileContent } from './plan.js';
5
+ import { type Outcome } from '../schemas/outcome.js';
5
6
  export interface FileIdentity {
6
7
  readonly dev: number;
7
8
  readonly ino: number;
@@ -28,7 +29,10 @@ export type ManagedApplyBegin = {
28
29
  readonly residualPaths: readonly string[];
29
30
  };
30
31
  export declare function beginManagedApply(root: string, plan: InitPlan, writes: readonly PlannedFileContent[], rootIdentity: FileIdentity): Promise<ManagedApplyBegin>;
31
- export declare function recordManagedFile(context: ManagedApplyContext, path: string, digest: string, identity: FileIdentity): Promise<{
32
+ export declare function recordManagedFile(context: ManagedApplyContext, path: string, digest: string, identity: FileIdentity, replacement?: {
33
+ readonly backupRef: string;
34
+ readonly backupDigest: string;
35
+ }): Promise<{
32
36
  readonly ok: true;
33
37
  } | {
34
38
  readonly ok: false;
@@ -56,6 +60,10 @@ export interface ManagedMachineUpdateResult {
56
60
  readonly updateInstallId?: string;
57
61
  readonly journalPath?: string;
58
62
  }
63
+ /** Read-only gate shared by initial planning and every apply, including no-op plans. */
64
+ export declare function inspectWorkspaceInstallReadiness(root: string, ownLock?: boolean): Promise<Outcome<null>>;
65
+ /** Serialize a zero-file apply with installation and recovery, without adding an empty journal. */
66
+ export declare function verifyWorkspaceInstallReady(root: string): Promise<Outcome<null>>;
59
67
  /** Updates the managed machine.json runtime location after a verified install. */
60
68
  export declare function updateMachineRuntime(root: string, runtime: RuntimeManifest, options?: UpdateMachineRuntimeOptions): Promise<ManagedMachineUpdateResult>;
61
69
  export interface ManagedWorkspaceState {
@@ -1,13 +1,14 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
- import { dirname, join } from 'node:path';
3
+ import { dirname, join, relative, sep } from 'node:path';
4
4
  import { createDiagnostic } from '../schemas/diagnostics.js';
5
5
  import { diagnosticCodes } from '../schemas/errors.js';
6
6
  import { managedFileEntrySchema, } from '../schemas/install.js';
7
7
  import { machineConfigSchema } from '../schemas/machine.js';
8
8
  import { acquireInstallLock, backupPath, createManifest, ensureStatePathBoundary, ensureWorkspaceStateBoundary, inspectOwnedFile, journalPath, readInstallJournal, readWorkspaceManifest, releaseInstallLock, rootIdentityOf, scanJournalDirectory, workspaceInstallPaths, writeInstallJournal, writeJsonAtomic, writeWorkspaceManifest, } from './ownership.js';
9
9
  import { observePath, sha256Digest } from '../infra/fs.js';
10
- import { errorMessage } from '../schemas/outcome.js';
10
+ import { beginRuntimeReference } from './references.js';
11
+ import { errorMessage, failed, succeeded, } from '../schemas/outcome.js';
11
12
  function diagnostic(code, message, hint, source) {
12
13
  return createDiagnostic({
13
14
  code,
@@ -71,12 +72,12 @@ export async function beginManagedApply(root, plan, writes, rootIdentity) {
71
72
  };
72
73
  }
73
74
  if (writes.length !== plan.files.length ||
74
- plan.files.some((file) => file.operation !== 'create')) {
75
+ plan.files.some((file) => !['create', 'replace-managed'].includes(file.operation))) {
75
76
  return {
76
77
  ok: false,
77
78
  status: 'invalid',
78
79
  diagnostics: [
79
- diagnostic(diagnosticCodes.initApplyUnsupported, 'P04 managed apply currently supports create-only plans.', 'Regenerate the plan; managed replacement is not silently emulated.', root),
80
+ diagnostic(diagnosticCodes.initApplyUnsupported, 'Managed apply supports only create and managed replacement plans.', 'Regenerate the plan with supported file operations.', root),
80
81
  ],
81
82
  residualPaths: [],
82
83
  };
@@ -105,12 +106,12 @@ export async function beginManagedApply(root, plan, writes, rootIdentity) {
105
106
  }
106
107
  const existingPaths = new Set(manifest.entries.map((entry) => entry.path));
107
108
  for (const file of plan.files) {
108
- if (existingPaths.has(file.path)) {
109
+ if ((file.operation === 'create') === existingPaths.has(file.path)) {
109
110
  return {
110
111
  ok: false,
111
112
  status: 'invalid',
112
113
  diagnostics: [
113
- diagnostic(diagnosticCodes.initTargetExists, `A managed file entry already exists for ${file.path}.`, 'Inspect ownership state; Pappra does not overwrite managed files as create.', workspaceInstallPaths(root).manifestPath),
114
+ diagnostic(diagnosticCodes.initTargetExists, `Managed ownership does not match the planned operation for ${file.path}.`, 'Inspect ownership state; replacements require an existing managed entry.', workspaceInstallPaths(root).manifestPath),
114
115
  ],
115
116
  residualPaths: [],
116
117
  };
@@ -127,8 +128,59 @@ export async function beginManagedApply(root, plan, writes, rootIdentity) {
127
128
  residualPaths: [],
128
129
  };
129
130
  }
131
+ const currentManifest = await readWorkspaceManifest(root);
132
+ const priorJournal = await findIncompleteWorkspaceJournal(root);
133
+ if (currentManifest.digest !== manifestRead.digest ||
134
+ priorJournal.incomplete !== null ||
135
+ priorJournal.unreadable.length > 0) {
136
+ await releaseInstallLock(lock.path, lock.lock.token);
137
+ return {
138
+ ok: false,
139
+ status: 'invalid',
140
+ residualPaths: [],
141
+ diagnostics: [
142
+ diagnostic(diagnosticCodes.initPlanStale, 'Managed ownership changed or an incomplete journal requires recovery.', 'Run pappra repair to inspect interrupted installs, then regenerate the plan.', priorJournal.incomplete ?? workspaceInstallPaths(root).manifestPath),
143
+ ],
144
+ };
145
+ }
130
146
  const installId = randomUUID();
131
147
  const journalFile = journalPath(root, installId);
148
+ const manifestBackupSteps = [];
149
+ if (plan.files.some((file) => file.operation === 'replace-managed')) {
150
+ const backup = backupPath(root, installId, 'install-manifest.json');
151
+ try {
152
+ const boundary = await ensureStatePathBoundary(root, backup);
153
+ if (!boundary.ok)
154
+ throw new Error(boundary.message);
155
+ const bytes = await readFile(workspaceInstallPaths(root).manifestPath);
156
+ if (sha256Digest(bytes) !== manifestRead.digest)
157
+ throw new Error('Ownership changed before backup.');
158
+ await mkdir(dirname(backup), { recursive: true });
159
+ await writeFile(backup, bytes, { flag: 'wx', mode: 0o600 });
160
+ manifestBackupSteps.push({
161
+ name: 'manifest-backup',
162
+ status: 'ok',
163
+ at: now(),
164
+ detail: relative(root, backup).split(sep).join('/'),
165
+ }, {
166
+ name: 'manifest-backup-digest',
167
+ status: 'ok',
168
+ at: now(),
169
+ detail: sha256Digest(bytes),
170
+ });
171
+ }
172
+ catch (error) {
173
+ await releaseInstallLock(lock.path, lock.lock.token);
174
+ return {
175
+ ok: false,
176
+ status: 'failed',
177
+ residualPaths: [],
178
+ diagnostics: [
179
+ diagnostic(diagnosticCodes.initApplyFailed, `Cannot back up managed ownership: ${errorMessage(error)}`, 'Inspect the workspace state; no target was changed.', journalFile),
180
+ ],
181
+ };
182
+ }
183
+ }
132
184
  let journal = {
133
185
  schema: 'pappra/install-journal/1',
134
186
  installId,
@@ -141,6 +193,7 @@ export async function beginManagedApply(root, plan, writes, rootIdentity) {
141
193
  updatedAt: now(),
142
194
  steps: [
143
195
  { name: 'lock-acquired', status: 'ok', at: now(), detail: lock.path },
196
+ ...manifestBackupSteps,
144
197
  ...writes.map((write) => ({
145
198
  name: `pending:${write.path}`,
146
199
  status: 'ok',
@@ -148,13 +201,19 @@ export async function beginManagedApply(root, plan, writes, rootIdentity) {
148
201
  detail: write.digest,
149
202
  })),
150
203
  ],
151
- entries: writes.map((write) => ({
204
+ entries: writes.map((write, index) => ({
152
205
  path: write.path,
153
206
  status: 'pending',
154
207
  digest: write.digest,
155
208
  identity: null,
156
- backupRef: null,
157
- backupDigest: null,
209
+ backupRef: plan.files[index]?.operation === 'replace-managed'
210
+ ? relative(root, backupPath(root, installId, write.path))
211
+ .split(sep)
212
+ .join('/')
213
+ : null,
214
+ backupDigest: plan.files[index]?.operation === 'replace-managed'
215
+ ? plan.files[index].expectedBeforeDigest
216
+ : null,
158
217
  detail: null,
159
218
  })),
160
219
  residuals: [],
@@ -190,7 +249,7 @@ export async function beginManagedApply(root, plan, writes, rootIdentity) {
190
249
  },
191
250
  };
192
251
  }
193
- export async function recordManagedFile(context, path, digest, identity) {
252
+ export async function recordManagedFile(context, path, digest, identity, replacement) {
194
253
  if (!context.plannedPaths.includes(path)) {
195
254
  return {
196
255
  ok: false,
@@ -204,6 +263,8 @@ export async function recordManagedFile(context, path, digest, identity) {
204
263
  ...entry,
205
264
  status: 'created',
206
265
  identity,
266
+ backupRef: replacement?.backupRef ?? null,
267
+ backupDigest: replacement?.backupDigest ?? null,
207
268
  detail: null,
208
269
  }
209
270
  : entry);
@@ -222,14 +283,28 @@ export async function recordManagedFile(context, path, digest, identity) {
222
283
  };
223
284
  }
224
285
  context.journal = journal;
225
- context.entries.push(manifestEntry(context.installId, path, digest, identity));
286
+ context.entries.push({
287
+ ...manifestEntry(context.installId, path, digest, identity),
288
+ ...(replacement === undefined
289
+ ? {}
290
+ : {
291
+ operation: 'replace-managed',
292
+ beforeDigest: replacement.backupDigest,
293
+ backupRef: replacement.backupRef,
294
+ backupDigest: replacement.backupDigest,
295
+ }),
296
+ });
226
297
  return { ok: true };
227
298
  }
228
299
  export async function completeManagedApply(context) {
229
300
  const appliedJournal = appendStep({ ...context.journal, status: 'applied', updatedAt: now() }, 'files-created', context.entries.map((entry) => entry.path).join(', '));
230
301
  await writeInstallJournal(context.journalFile, appliedJournal, context.root);
231
302
  context.journal = appliedJournal;
232
- const entries = [...context.manifest.entries, ...context.entries];
303
+ const changedPaths = new Set(context.entries.map((entry) => entry.path));
304
+ const entries = [
305
+ ...context.manifest.entries.filter((entry) => !changedPaths.has(entry.path)),
306
+ ...context.entries,
307
+ ];
233
308
  const manifest = {
234
309
  ...context.manifest,
235
310
  entries,
@@ -349,8 +424,97 @@ async function findIncompleteWorkspaceJournal(root) {
349
424
  }
350
425
  return { incomplete, unreadable };
351
426
  }
427
+ /** Read-only gate shared by initial planning and every apply, including no-op plans. */
428
+ export async function inspectWorkspaceInstallReadiness(root, ownLock = false) {
429
+ const paths = workspaceInstallPaths(root);
430
+ const boundary = await ensureStatePathBoundary(root, paths.lockPath);
431
+ if (!boundary.ok)
432
+ return boundary;
433
+ if (!ownLock && (await observePath(paths.lockPath)).kind !== 'missing') {
434
+ return failed(diagnosticCodes.initApplyFailed, 'Workspace installation is locked.', {
435
+ nextAction: 'Wait for the active init, or run pappra repair to inspect an interrupted install.',
436
+ });
437
+ }
438
+ const scan = await findIncompleteWorkspaceJournal(root);
439
+ if (scan.incomplete !== null || scan.unreadable.length > 0) {
440
+ return failed(diagnosticCodes.initPlanStale, `Workspace installation requires recovery: ${scan.incomplete ?? scan.unreadable[0]}`, {
441
+ nextAction: 'Run pappra repair for this workspace, then regenerate the init plan.',
442
+ });
443
+ }
444
+ const manifest = await readWorkspaceManifest(root);
445
+ if (manifest.diagnostics.length > 0)
446
+ return failed(diagnosticCodes.initPlanStale, manifest.diagnostics[0].message, { nextAction: 'Inspect install-manifest.json before retrying init.' });
447
+ if (manifest.value !== null) {
448
+ const identity = await rootIdentityOf(root);
449
+ if (manifest.value.workspaceRoot !== root ||
450
+ identity === null ||
451
+ manifest.value.rootIdentity.dev !== identity.dev ||
452
+ manifest.value.rootIdentity.ino !== identity.ino) {
453
+ return failed(diagnosticCodes.initPlanStale, 'Workspace installation belongs to another root identity.', {
454
+ nextAction: 'Inspect the local install manifest; it was not adopted or rewritten.',
455
+ });
456
+ }
457
+ }
458
+ return succeeded(null);
459
+ }
460
+ /** Serialize a zero-file apply with installation and recovery, without adding an empty journal. */
461
+ export async function verifyWorkspaceInstallReady(root) {
462
+ const paths = workspaceInstallPaths(root);
463
+ const lock = await acquireInstallLock(paths.lockPath, 'apply-workspace-files', root, root);
464
+ if (!lock.ok || lock.lock === null)
465
+ return failed(diagnosticCodes.initApplyFailed, lock.message ?? 'Workspace installation is locked.', { nextAction: 'Run pappra repair to inspect the install lock.' });
466
+ let result;
467
+ try {
468
+ result = await inspectWorkspaceInstallReadiness(root, true);
469
+ }
470
+ finally {
471
+ const released = await releaseInstallLock(lock.path, lock.lock.token);
472
+ if (!released.ok)
473
+ result = released;
474
+ }
475
+ return result;
476
+ }
352
477
  /** Updates the managed machine.json runtime location after a verified install. */
353
478
  export async function updateMachineRuntime(root, runtime, options = {}) {
479
+ const lease = await beginRuntimeReference(root, runtime);
480
+ if (!lease.ok)
481
+ return {
482
+ ok: false,
483
+ status: 'failed',
484
+ residualPaths: [],
485
+ diagnostics: [
486
+ diagnostic(lease.code, lease.message, lease.nextAction ?? 'Inspect the runtime reference state.', runtime.runtimeRoot),
487
+ ],
488
+ };
489
+ let result;
490
+ try {
491
+ result = await updateMachineRuntimeAt(root, runtime, options);
492
+ }
493
+ catch (error) {
494
+ result = {
495
+ ok: false,
496
+ status: 'failed',
497
+ residualPaths: [],
498
+ diagnostics: [
499
+ diagnostic(diagnosticCodes.initApplyFailed, errorMessage(error), 'Inspect the workspace binding journal before retrying.', root),
500
+ ],
501
+ };
502
+ }
503
+ const released = await lease.value.release();
504
+ return released.ok
505
+ ? result
506
+ : {
507
+ ...result,
508
+ ok: false,
509
+ status: result.ok ? 'partial' : result.status,
510
+ residualPaths: [...result.residualPaths, lease.value.lockPath],
511
+ diagnostics: [
512
+ ...result.diagnostics,
513
+ diagnostic(released.code, released.message, 'Inspect the retained runtime lock before retrying.', lease.value.lockPath),
514
+ ],
515
+ };
516
+ }
517
+ async function updateMachineRuntimeAt(root, runtime, options) {
354
518
  const stateBoundary = await ensureWorkspaceStateBoundary(root);
355
519
  if (!stateBoundary.ok) {
356
520
  return {
@@ -0,0 +1,38 @@
1
+ export interface PackageDependencyManifest {
2
+ readonly required: readonly string[];
3
+ readonly optional: readonly string[];
4
+ }
5
+ /** Reads the declared dependency names of one package without guessing ranges. */
6
+ export declare function readPackageDependencyManifest(packageRoot: string): Promise<PackageDependencyManifest | null>;
7
+ /**
8
+ * Resolves the exact installed copy that Node would load for `name` from
9
+ * `packageRoot`, walking the same ancestor chain Node resolves through. Every
10
+ * candidate is realpath-resolved and name-checked so a symlinked store entry or
11
+ * a mismatched directory is never mistaken for the requested dependency.
12
+ */
13
+ export declare function resolveInstalledDependency(packageRoot: string, name: string): Promise<string | null>;
14
+ export interface DependencyMaterialization {
15
+ readonly ok: boolean;
16
+ /** Destination directories created by this run, in copy order. */
17
+ readonly copied: readonly string[];
18
+ /** Destination slots satisfied by an already-materialized exact copy. */
19
+ readonly reused: readonly string[];
20
+ /** Optional dependencies that are absent from the installed tree. */
21
+ readonly skippedOptional: readonly string[];
22
+ /** Problems that make the materialized tree unsafe to publish. */
23
+ readonly problems: readonly string[];
24
+ }
25
+ export interface MaterializeDependenciesOptions {
26
+ readonly sourceRoot: string;
27
+ readonly targetRoot: string;
28
+ /** Upper bound on visited packages; guards against pathological trees. */
29
+ readonly maxPackages?: number;
30
+ }
31
+ /**
32
+ * Materializes the installed dependency closure of `sourceRoot` under
33
+ * `targetRoot/node_modules`, reusing exact resolved copies at the highest
34
+ * non-shadowed ancestor. Required dependencies must resolve; absent optional
35
+ * dependencies are skipped. The result is a plain report: callers decide whether
36
+ * an incomplete closure is fatal or a reason to fall back to a package manager.
37
+ */
38
+ export declare function materializeInstalledDependencies(options: MaterializeDependenciesOptions): Promise<DependencyMaterialization>;
@@ -0,0 +1,282 @@
1
+ import { cp, mkdir } from 'node:fs/promises';
2
+ import { basename, dirname, join, relative, resolve, sep } from 'node:path';
3
+ import { observePath, readTextFileBounded, resolveRealpath, } from '../infra/fs.js';
4
+ import { isPathInside, samePath } from '../schemas/paths.js';
5
+ const NODE_MODULES = 'node_modules';
6
+ const DEFAULT_MAX_PACKAGES = 512;
7
+ function packageDependencyNames(value) {
8
+ if (value === undefined)
9
+ return [];
10
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
11
+ return null;
12
+ }
13
+ const entries = Object.entries(value);
14
+ if (entries.some(([name, range]) => !/^(?:@[A-Za-z0-9._-]+\/)?[A-Za-z0-9._-]+$/.test(name) ||
15
+ typeof range !== 'string')) {
16
+ return null;
17
+ }
18
+ return entries.map(([name]) => name);
19
+ }
20
+ /** Reads the declared dependency names of one package without guessing ranges. */
21
+ export async function readPackageDependencyManifest(packageRoot) {
22
+ const manifestRead = await readTextFileBounded(join(packageRoot, 'package.json'));
23
+ if (!manifestRead.ok)
24
+ return null;
25
+ let decoded;
26
+ try {
27
+ decoded = JSON.parse(manifestRead.value.text);
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ if (typeof decoded !== 'object' || decoded === null)
33
+ return null;
34
+ const manifest = decoded;
35
+ const required = packageDependencyNames(manifest.dependencies);
36
+ const optional = packageDependencyNames(manifest.optionalDependencies);
37
+ if (required === null || optional === null)
38
+ return null;
39
+ return { required, optional };
40
+ }
41
+ /**
42
+ * Resolves the exact installed copy that Node would load for `name` from
43
+ * `packageRoot`, walking the same ancestor chain Node resolves through. Every
44
+ * candidate is realpath-resolved and name-checked so a symlinked store entry or
45
+ * a mismatched directory is never mistaken for the requested dependency.
46
+ */
47
+ export async function resolveInstalledDependency(packageRoot, name) {
48
+ let current = packageRoot;
49
+ while (true) {
50
+ const candidate = join(current, NODE_MODULES, ...name.split('/'));
51
+ const realPath = await resolveRealpath(candidate);
52
+ if (realPath !== null &&
53
+ (await observePath(realPath)).kind === 'directory') {
54
+ const manifestRead = await readTextFileBounded(join(realPath, 'package.json'));
55
+ if (manifestRead.ok) {
56
+ try {
57
+ const decoded = JSON.parse(manifestRead.value.text);
58
+ if (typeof decoded === 'object' &&
59
+ decoded !== null &&
60
+ 'name' in decoded &&
61
+ decoded.name === name) {
62
+ return realPath;
63
+ }
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ }
69
+ }
70
+ const parent = dirname(current);
71
+ if (parent === current)
72
+ return null;
73
+ current = parent;
74
+ }
75
+ }
76
+ /**
77
+ * Chooses where one exact dependency belongs inside the destination tree.
78
+ *
79
+ * The slot must sit at the highest ancestor that is not shadowed by a different
80
+ * exact source: Node resolves `node_modules/<name>` from the nearest ancestor
81
+ * outwards, so a nearer conflicting placement forces a nested copy, while a free
82
+ * chain up to the target root allows one shared copy at the highest ancestor.
83
+ */
84
+ function choosePlacement(input) {
85
+ let candidate = null;
86
+ for (const slot of resolutionSlots(input.destination, input.targetRoot, input.name)) {
87
+ const reserved = input.reservations.get(slot);
88
+ if (reserved &&
89
+ [...reserved].some((source) => source !== input.dependencyRoot))
90
+ break;
91
+ const placed = input.placements.get(slot);
92
+ if (placed === input.dependencyRoot)
93
+ return { kind: 'reused', slot };
94
+ if (placed !== undefined)
95
+ break;
96
+ candidate = slot;
97
+ }
98
+ return candidate === null
99
+ ? { kind: 'conflict' }
100
+ : { kind: 'placed', slot: candidate };
101
+ }
102
+ function resolutionSlots(destination, targetRoot, name) {
103
+ const slots = [];
104
+ let directory = destination;
105
+ while (true) {
106
+ // Node never appends `node_modules` to a directory that already is one, so
107
+ // such an ancestor is walked through but never offered as a placement slot.
108
+ if (basename(directory) !== NODE_MODULES)
109
+ slots.push(join(directory, NODE_MODULES, ...name.split('/')));
110
+ if (samePath(directory, targetRoot))
111
+ break;
112
+ const parent = dirname(directory);
113
+ if (parent === directory || !isPathInside(targetRoot, parent))
114
+ break;
115
+ directory = parent;
116
+ }
117
+ return slots;
118
+ }
119
+ /** Empty nearer slots must not later shadow a consumer's already chosen copy. */
120
+ function reserveResolution(reservations, slots, source, resolvedSlot) {
121
+ for (const slot of slots) {
122
+ const expected = reservations.get(slot) ?? new Set();
123
+ expected.add(source);
124
+ reservations.set(slot, expected);
125
+ if (slot === resolvedSlot)
126
+ break;
127
+ }
128
+ }
129
+ /**
130
+ * Payload paths never include a nested `node_modules`: that subtree belongs to
131
+ * another resolver and is materialized explicitly from its own exact source.
132
+ */
133
+ function isPayloadPath(sourceRoot, candidate) {
134
+ const path = relative(sourceRoot, candidate);
135
+ if (path === '')
136
+ return true;
137
+ return !path.split(sep).includes(NODE_MODULES);
138
+ }
139
+ /**
140
+ * Copies a regular package payload without nested dependency trees. Published
141
+ * runtime manifests forbid links, so reject them before publication rather than
142
+ * copying a store/absolute target or silently producing an unusable runtime.
143
+ */
144
+ async function copyPackagePayload(sourceRoot, destination) {
145
+ const existing = await observePath(destination);
146
+ if (existing.kind !== 'missing')
147
+ return false;
148
+ try {
149
+ await cp(sourceRoot, destination, {
150
+ recursive: true,
151
+ dereference: false,
152
+ verbatimSymlinks: true,
153
+ force: false,
154
+ errorOnExist: false,
155
+ filter: async (candidate) => {
156
+ if (!isPayloadPath(sourceRoot, candidate))
157
+ return false;
158
+ const entry = await observePath(candidate);
159
+ if (entry.kind !== 'file' && entry.kind !== 'directory')
160
+ throw new Error('Dependency payload contains a link or special file');
161
+ return true;
162
+ },
163
+ });
164
+ }
165
+ catch {
166
+ return false;
167
+ }
168
+ return true;
169
+ }
170
+ /**
171
+ * Materializes the installed dependency closure of `sourceRoot` under
172
+ * `targetRoot/node_modules`, reusing exact resolved copies at the highest
173
+ * non-shadowed ancestor. Required dependencies must resolve; absent optional
174
+ * dependencies are skipped. The result is a plain report: callers decide whether
175
+ * an incomplete closure is fatal or a reason to fall back to a package manager.
176
+ */
177
+ export async function materializeInstalledDependencies(options) {
178
+ const copied = [];
179
+ const reused = [];
180
+ const skippedOptional = [];
181
+ const problems = [];
182
+ const placements = new Map();
183
+ const reservations = new Map();
184
+ const sourceRoot = (await resolveRealpath(options.sourceRoot)) ?? resolve(options.sourceRoot);
185
+ const targetRoot = resolve(options.targetRoot);
186
+ const limit = options.maxPackages ?? DEFAULT_MAX_PACKAGES;
187
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 4096)
188
+ return {
189
+ ok: false,
190
+ copied,
191
+ reused,
192
+ skippedOptional,
193
+ problems: ['invalid_package_limit'],
194
+ };
195
+ const pending = [
196
+ { packageRoot: sourceRoot, destination: targetRoot },
197
+ ];
198
+ let index = 0;
199
+ while (index < pending.length) {
200
+ if (index >= limit) {
201
+ problems.push(`package_limit:${String(limit)}`);
202
+ break;
203
+ }
204
+ const current = pending[index];
205
+ index += 1;
206
+ if (current === undefined)
207
+ break;
208
+ const manifest = await readPackageDependencyManifest(current.packageRoot);
209
+ if (manifest === null) {
210
+ problems.push(`manifest_unreadable:${current.packageRoot}`);
211
+ continue;
212
+ }
213
+ const declared = [];
214
+ for (const name of manifest.required) {
215
+ if (declared.some((entry) => entry.name === name))
216
+ continue;
217
+ declared.push({ name, optional: false });
218
+ }
219
+ for (const name of manifest.optional) {
220
+ if (declared.some((entry) => entry.name === name))
221
+ continue;
222
+ declared.push({ name, optional: true });
223
+ }
224
+ for (const entry of declared) {
225
+ const name = entry.name;
226
+ const optional = entry.optional;
227
+ const dependencyRoot = await resolveInstalledDependency(current.packageRoot, name);
228
+ if (dependencyRoot === null) {
229
+ const slots = resolutionSlots(current.destination, targetRoot, name);
230
+ if (slots.some((slot) => placements.has(slot)))
231
+ problems.push(`missing_dependency_shadowed:${name}`);
232
+ reserveResolution(reservations, slots, null);
233
+ if (optional) {
234
+ skippedOptional.push(name);
235
+ continue;
236
+ }
237
+ problems.push(`dependency_missing:${name}`);
238
+ continue;
239
+ }
240
+ const placement = choosePlacement({
241
+ placements,
242
+ reservations,
243
+ targetRoot,
244
+ destination: current.destination,
245
+ name,
246
+ dependencyRoot,
247
+ });
248
+ if (placement.kind === 'reused') {
249
+ reserveResolution(reservations, resolutionSlots(current.destination, targetRoot, name), dependencyRoot, placement.slot);
250
+ reused.push(placement.slot);
251
+ continue;
252
+ }
253
+ if (placement.kind === 'conflict') {
254
+ problems.push(`dependency_slot_conflict:${name}`);
255
+ continue;
256
+ }
257
+ if (!isPathInside(targetRoot, placement.slot)) {
258
+ problems.push(`dependency_placement_escape:${name}`);
259
+ continue;
260
+ }
261
+ await mkdir(dirname(placement.slot), { recursive: true });
262
+ if (!(await copyPackagePayload(dependencyRoot, placement.slot))) {
263
+ problems.push(`dependency_copy_failed:${name}`);
264
+ continue;
265
+ }
266
+ placements.set(placement.slot, dependencyRoot);
267
+ reserveResolution(reservations, resolutionSlots(current.destination, targetRoot, name), dependencyRoot, placement.slot);
268
+ copied.push(placement.slot);
269
+ pending.push({
270
+ packageRoot: dependencyRoot,
271
+ destination: placement.slot,
272
+ });
273
+ }
274
+ }
275
+ return {
276
+ ok: problems.length === 0,
277
+ copied,
278
+ reused,
279
+ skippedOptional,
280
+ problems,
281
+ };
282
+ }