@phyzess/pappra 0.0.0-stage → 0.1.0-beta.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 (221) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -2
  3. package/THIRD_PARTY_NOTICES.md +16 -0
  4. package/assets/integrations/skills/pappra/SKILL.md +68 -0
  5. package/assets/integrations/skills/pappra/references/hosts.md +30 -0
  6. package/assets/integrations/skills/pappra/references/lifecycle.md +37 -0
  7. package/assets/integrations/skills/pappra/references/operations.md +14 -0
  8. package/assets/manifest.json +4 -0
  9. package/dist/app/collaboration.d.ts +170 -0
  10. package/dist/app/collaboration.js +1650 -0
  11. package/dist/app/doctor.d.ts +9 -0
  12. package/dist/app/doctor.js +1030 -0
  13. package/dist/app/init.d.ts +143 -0
  14. package/dist/app/init.js +1111 -0
  15. package/dist/cli/commands/code.d.ts +16 -0
  16. package/dist/cli/commands/code.js +127 -0
  17. package/dist/cli/commands/collaboration.d.ts +13 -0
  18. package/dist/cli/commands/collaboration.js +186 -0
  19. package/dist/cli/commands/continuity.d.ts +29 -0
  20. package/dist/cli/commands/continuity.js +133 -0
  21. package/dist/cli/commands/docs.d.ts +11 -0
  22. package/dist/cli/commands/docs.js +133 -0
  23. package/dist/cli/commands/doctor.d.ts +14 -0
  24. package/dist/cli/commands/doctor.js +57 -0
  25. package/dist/cli/commands/host.d.ts +13 -0
  26. package/dist/cli/commands/host.js +125 -0
  27. package/dist/cli/commands/init.d.ts +51 -0
  28. package/dist/cli/commands/init.js +668 -0
  29. package/dist/cli/main.d.ts +2 -0
  30. package/dist/cli/main.js +10 -0
  31. package/dist/cli/output.d.ts +4 -0
  32. package/dist/cli/output.js +1 -0
  33. package/dist/cli/run.d.ts +2 -0
  34. package/dist/cli/run.js +597 -0
  35. package/dist/code/budget.d.ts +14 -0
  36. package/dist/code/budget.js +39 -0
  37. package/dist/code/exec.d.ts +23 -0
  38. package/dist/code/exec.js +133 -0
  39. package/dist/code/identity.d.ts +13 -0
  40. package/dist/code/identity.js +214 -0
  41. package/dist/code/index.d.ts +6 -0
  42. package/dist/code/index.js +3 -0
  43. package/dist/code/providers/codegraph.d.ts +59 -0
  44. package/dist/code/providers/codegraph.js +567 -0
  45. package/dist/code/registration.d.ts +9 -0
  46. package/dist/code/registration.js +293 -0
  47. package/dist/code/service.d.ts +53 -0
  48. package/dist/code/service.js +633 -0
  49. package/dist/code/text.d.ts +3 -0
  50. package/dist/code/text.js +202 -0
  51. package/dist/collab/admission.d.ts +450 -0
  52. package/dist/collab/admission.js +797 -0
  53. package/dist/collab/canonical.d.ts +26 -0
  54. package/dist/collab/canonical.js +136 -0
  55. package/dist/collab/contracts.d.ts +73 -0
  56. package/dist/collab/contracts.js +76 -0
  57. package/dist/collab/envelope.d.ts +52 -0
  58. package/dist/collab/envelope.js +241 -0
  59. package/dist/collab/fingerprint.d.ts +7 -0
  60. package/dist/collab/fingerprint.js +58 -0
  61. package/dist/collab/handoff.d.ts +350 -0
  62. package/dist/collab/handoff.js +513 -0
  63. package/dist/collab/index.d.ts +9 -0
  64. package/dist/collab/index.js +8 -0
  65. package/dist/collab/lifecycle.d.ts +449 -0
  66. package/dist/collab/lifecycle.js +534 -0
  67. package/dist/collab/messages.d.ts +148 -0
  68. package/dist/collab/messages.js +583 -0
  69. package/dist/collab/notify.d.ts +47 -0
  70. package/dist/collab/notify.js +140 -0
  71. package/dist/collab/preflight.d.ts +31 -0
  72. package/dist/collab/preflight.js +271 -0
  73. package/dist/collab/resources.d.ts +119 -0
  74. package/dist/collab/resources.js +451 -0
  75. package/dist/collab/store.d.ts +143 -0
  76. package/dist/collab/store.js +606 -0
  77. package/dist/collab/types.d.ts +354 -0
  78. package/dist/collab/types.js +1 -0
  79. package/dist/collab/validation.d.ts +10 -0
  80. package/dist/collab/validation.js +303 -0
  81. package/dist/continuity/context.d.ts +24 -0
  82. package/dist/continuity/context.js +179 -0
  83. package/dist/continuity/index.d.ts +3 -0
  84. package/dist/continuity/index.js +1 -0
  85. package/dist/continuity/paths.d.ts +37 -0
  86. package/dist/continuity/paths.js +258 -0
  87. package/dist/continuity/service.d.ts +59 -0
  88. package/dist/continuity/service.js +1258 -0
  89. package/dist/hosts/claude/native.d.ts +79 -0
  90. package/dist/hosts/claude/native.js +853 -0
  91. package/dist/hosts/codex/native.d.ts +345 -0
  92. package/dist/hosts/codex/native.js +1528 -0
  93. package/dist/hosts/codex/protocol.d.ts +51 -0
  94. package/dist/hosts/codex/protocol.js +42 -0
  95. package/dist/hosts/detect.d.ts +28 -0
  96. package/dist/hosts/detect.js +73 -0
  97. package/dist/hosts/index.d.ts +10 -0
  98. package/dist/hosts/index.js +10 -0
  99. package/dist/hosts/integration/bundle.d.ts +16 -0
  100. package/dist/hosts/integration/bundle.js +81 -0
  101. package/dist/hosts/integration/hook-entry.d.ts +1 -0
  102. package/dist/hosts/integration/hook-entry.js +68 -0
  103. package/dist/hosts/integration/hook.d.ts +32 -0
  104. package/dist/hosts/integration/hook.js +781 -0
  105. package/dist/hosts/integration/pi-extension-runtime.d.ts +18 -0
  106. package/dist/hosts/integration/pi-extension-runtime.js +89 -0
  107. package/dist/hosts/integration/pi-template.d.ts +3 -0
  108. package/dist/hosts/integration/pi-template.js +5 -0
  109. package/dist/hosts/integration/service.d.ts +41 -0
  110. package/dist/hosts/integration/service.js +1381 -0
  111. package/dist/hosts/integration/support.d.ts +36 -0
  112. package/dist/hosts/integration/support.js +41 -0
  113. package/dist/hosts/integration/templates.d.ts +22 -0
  114. package/dist/hosts/integration/templates.js +70 -0
  115. package/dist/hosts/pi/protocol.d.ts +22 -0
  116. package/dist/hosts/pi/protocol.js +28 -0
  117. package/dist/hosts/pi/query-result.d.ts +22 -0
  118. package/dist/hosts/pi/query-result.js +710 -0
  119. package/dist/hosts/pi/rpc.d.ts +937 -0
  120. package/dist/hosts/pi/rpc.js +1621 -0
  121. package/dist/hosts/pilot.d.ts +485 -0
  122. package/dist/hosts/pilot.js +593 -0
  123. package/dist/index.d.ts +5 -0
  124. package/dist/index.js +4 -0
  125. package/dist/infra/fs.d.ts +27 -0
  126. package/dist/infra/fs.js +130 -0
  127. package/dist/infra/git.d.ts +60 -0
  128. package/dist/infra/git.js +358 -0
  129. package/dist/infra/owned-process-group.d.ts +173 -0
  130. package/dist/infra/owned-process-group.js +571 -0
  131. package/dist/install/apply.d.ts +65 -0
  132. package/dist/install/apply.js +663 -0
  133. package/dist/install/index.d.ts +5 -0
  134. package/dist/install/index.js +5 -0
  135. package/dist/install/ownership.d.ts +85 -0
  136. package/dist/install/ownership.js +430 -0
  137. package/dist/install/plan.d.ts +39 -0
  138. package/dist/install/plan.js +97 -0
  139. package/dist/install/rollback.d.ts +18 -0
  140. package/dist/install/rollback.js +544 -0
  141. package/dist/install/runtime.d.ts +76 -0
  142. package/dist/install/runtime.js +1145 -0
  143. package/dist/knowledge/catalog.d.ts +47 -0
  144. package/dist/knowledge/catalog.js +308 -0
  145. package/dist/knowledge/context.d.ts +28 -0
  146. package/dist/knowledge/context.js +61 -0
  147. package/dist/knowledge/index.d.ts +3 -0
  148. package/dist/knowledge/index.js +1 -0
  149. package/dist/knowledge/journal.d.ts +16 -0
  150. package/dist/knowledge/journal.js +109 -0
  151. package/dist/knowledge/lifecycle.d.ts +8 -0
  152. package/dist/knowledge/lifecycle.js +1001 -0
  153. package/dist/knowledge/paths.d.ts +37 -0
  154. package/dist/knowledge/paths.js +236 -0
  155. package/dist/knowledge/refs.d.ts +36 -0
  156. package/dist/knowledge/refs.js +267 -0
  157. package/dist/knowledge/service.d.ts +21 -0
  158. package/dist/knowledge/service.js +393 -0
  159. package/dist/knowledge/types.d.ts +171 -0
  160. package/dist/knowledge/types.js +1 -0
  161. package/dist/knowledge/write.d.ts +8 -0
  162. package/dist/knowledge/write.js +1223 -0
  163. package/dist/package-info.d.ts +6 -0
  164. package/dist/package-info.js +19 -0
  165. package/dist/schemas/code.d.ts +377 -0
  166. package/dist/schemas/code.js +145 -0
  167. package/dist/schemas/collaboration.d.ts +115 -0
  168. package/dist/schemas/collaboration.js +106 -0
  169. package/dist/schemas/continuity.d.ts +315 -0
  170. package/dist/schemas/continuity.js +199 -0
  171. package/dist/schemas/diagnostics.d.ts +28 -0
  172. package/dist/schemas/diagnostics.js +27 -0
  173. package/dist/schemas/doctor.d.ts +157 -0
  174. package/dist/schemas/doctor.js +75 -0
  175. package/dist/schemas/document.d.ts +644 -0
  176. package/dist/schemas/document.js +296 -0
  177. package/dist/schemas/errors.d.ts +177 -0
  178. package/dist/schemas/errors.js +176 -0
  179. package/dist/schemas/evidence.d.ts +24 -0
  180. package/dist/schemas/evidence.js +16 -0
  181. package/dist/schemas/host.d.ts +436 -0
  182. package/dist/schemas/host.js +201 -0
  183. package/dist/schemas/index.d.ts +15 -0
  184. package/dist/schemas/index.js +15 -0
  185. package/dist/schemas/init.d.ts +121 -0
  186. package/dist/schemas/init.js +94 -0
  187. package/dist/schemas/install.d.ts +213 -0
  188. package/dist/schemas/install.js +139 -0
  189. package/dist/schemas/machine.d.ts +43 -0
  190. package/dist/schemas/machine.js +96 -0
  191. package/dist/schemas/outcome.d.ts +23 -0
  192. package/dist/schemas/outcome.js +17 -0
  193. package/dist/schemas/paths.d.ts +19 -0
  194. package/dist/schemas/paths.js +63 -0
  195. package/dist/schemas/workspace.d.ts +120 -0
  196. package/dist/schemas/workspace.js +201 -0
  197. package/dist/workspace/config-file.d.ts +36 -0
  198. package/dist/workspace/config-file.js +237 -0
  199. package/dist/workspace/config.d.ts +28 -0
  200. package/dist/workspace/config.js +79 -0
  201. package/dist/workspace/discover.d.ts +34 -0
  202. package/dist/workspace/discover.js +195 -0
  203. package/dist/workspace/documents.d.ts +21 -0
  204. package/dist/workspace/documents.js +395 -0
  205. package/dist/workspace/ignore.d.ts +10 -0
  206. package/dist/workspace/ignore.js +48 -0
  207. package/dist/workspace/index.d.ts +7 -0
  208. package/dist/workspace/index.js +3 -0
  209. package/dist/workspace/repositories.d.ts +24 -0
  210. package/dist/workspace/repositories.js +411 -0
  211. package/dist/workspace/resolve.d.ts +24 -0
  212. package/dist/workspace/resolve.js +268 -0
  213. package/dist/workspace/types.d.ts +71 -0
  214. package/dist/workspace/types.js +1 -0
  215. package/dist/workspace/validation.d.ts +18 -0
  216. package/dist/workspace/validation.js +197 -0
  217. package/package.json +105 -4
  218. package/skills/pappra/SKILL.md +68 -0
  219. package/skills/pappra/references/hosts.md +30 -0
  220. package/skills/pappra/references/lifecycle.md +37 -0
  221. package/skills/pappra/references/operations.md +14 -0
@@ -0,0 +1,663 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ import { createDiagnostic } from '../schemas/diagnostics.js';
5
+ import { diagnosticCodes } from '../schemas/errors.js';
6
+ import { managedFileEntrySchema, } from '../schemas/install.js';
7
+ import { machineConfigSchema } from '../schemas/machine.js';
8
+ import { acquireInstallLock, backupPath, createManifest, ensureStatePathBoundary, ensureWorkspaceStateBoundary, inspectOwnedFile, journalPath, readInstallJournal, readWorkspaceManifest, releaseInstallLock, rootIdentityOf, scanJournalDirectory, workspaceInstallPaths, writeInstallJournal, writeJsonAtomic, writeWorkspaceManifest, } from './ownership.js';
9
+ import { observePath, sha256Digest } from '../infra/fs.js';
10
+ import { errorMessage } from '../schemas/outcome.js';
11
+ function diagnostic(code, message, hint, source) {
12
+ return createDiagnostic({
13
+ code,
14
+ severity: 'error',
15
+ message,
16
+ source,
17
+ hint,
18
+ });
19
+ }
20
+ function now() {
21
+ return new Date().toISOString();
22
+ }
23
+ function appendStep(journal, name, detail, failed = false) {
24
+ return {
25
+ ...journal,
26
+ updatedAt: now(),
27
+ steps: [
28
+ ...journal.steps,
29
+ { name, status: failed ? 'failed' : 'ok', at: now(), detail },
30
+ ],
31
+ };
32
+ }
33
+ function manifestEntry(installId, path, digest, identity) {
34
+ return {
35
+ path,
36
+ installId,
37
+ operation: 'create',
38
+ beforeDigest: null,
39
+ afterDigest: digest,
40
+ backupRef: null,
41
+ backupDigest: null,
42
+ identity,
43
+ owner: 'pappra',
44
+ verifiedAt: now(),
45
+ };
46
+ }
47
+ export async function beginManagedApply(root, plan, writes, rootIdentity) {
48
+ const stateBoundary = await ensureWorkspaceStateBoundary(root);
49
+ if (!stateBoundary.ok) {
50
+ return {
51
+ ok: false,
52
+ status: 'invalid',
53
+ diagnostics: [
54
+ diagnostic(diagnosticCodes.initTargetUnsafe, stateBoundary.message, stateBoundary.nextAction ??
55
+ 'Inspect symlinked Pappra state paths; no metadata was written.', workspaceInstallPaths(root).localDir),
56
+ ],
57
+ residualPaths: [],
58
+ };
59
+ }
60
+ const currentIdentity = await rootIdentityOf(root);
61
+ if (currentIdentity === null ||
62
+ currentIdentity.dev !== rootIdentity.dev ||
63
+ currentIdentity.ino !== rootIdentity.ino) {
64
+ return {
65
+ ok: false,
66
+ status: 'invalid',
67
+ diagnostics: [
68
+ diagnostic(diagnosticCodes.initPlanStale, 'Workspace root identity changed before managed apply.', 'Retry init from the current workspace root; no files were written.', root),
69
+ ],
70
+ residualPaths: [],
71
+ };
72
+ }
73
+ if (writes.length !== plan.files.length ||
74
+ plan.files.some((file) => file.operation !== 'create')) {
75
+ return {
76
+ ok: false,
77
+ status: 'invalid',
78
+ diagnostics: [
79
+ diagnostic(diagnosticCodes.initApplyUnsupported, 'P04 managed apply currently supports create-only plans.', 'Regenerate the plan; managed replacement is not silently emulated.', root),
80
+ ],
81
+ residualPaths: [],
82
+ };
83
+ }
84
+ const manifestRead = await readWorkspaceManifest(root);
85
+ if (manifestRead.diagnostics.length > 0) {
86
+ return {
87
+ ok: false,
88
+ status: 'invalid',
89
+ diagnostics: manifestRead.diagnostics,
90
+ residualPaths: [],
91
+ };
92
+ }
93
+ const manifest = manifestRead.value ?? createManifest(root, rootIdentity);
94
+ if (manifest.workspaceRoot !== root ||
95
+ manifest.rootIdentity.dev !== rootIdentity.dev ||
96
+ manifest.rootIdentity.ino !== rootIdentity.ino) {
97
+ return {
98
+ ok: false,
99
+ status: 'invalid',
100
+ diagnostics: [
101
+ diagnostic(diagnosticCodes.initPlanStale, 'Existing installation manifest belongs to another root identity.', 'Inspect .pappra/local/install-manifest.json; Pappra does not adopt it.', workspaceInstallPaths(root).manifestPath),
102
+ ],
103
+ residualPaths: [],
104
+ };
105
+ }
106
+ const existingPaths = new Set(manifest.entries.map((entry) => entry.path));
107
+ for (const file of plan.files) {
108
+ if (existingPaths.has(file.path)) {
109
+ return {
110
+ ok: false,
111
+ status: 'invalid',
112
+ 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
+ ],
115
+ residualPaths: [],
116
+ };
117
+ }
118
+ }
119
+ const lock = await acquireInstallLock(workspaceInstallPaths(root).lockPath, 'apply-workspace-files', root, root);
120
+ if (!lock.ok || lock.lock === null) {
121
+ return {
122
+ ok: false,
123
+ status: lock.status === 'locked' ? 'locked' : 'failed',
124
+ diagnostics: [
125
+ diagnostic(diagnosticCodes.initApplyFailed, lock.message ?? 'Workspace apply lock is held.', 'Another Pappra apply owns this workspace; locks are not reclaimed by age.', lock.path),
126
+ ],
127
+ residualPaths: [],
128
+ };
129
+ }
130
+ const installId = randomUUID();
131
+ const journalFile = journalPath(root, installId);
132
+ let journal = {
133
+ schema: 'pappra/install-journal/1',
134
+ installId,
135
+ operation: 'apply-workspace-files',
136
+ target: plan.planId,
137
+ workspaceRoot: root,
138
+ runtimeVersion: null,
139
+ status: 'prepared',
140
+ createdAt: now(),
141
+ updatedAt: now(),
142
+ steps: [
143
+ { name: 'lock-acquired', status: 'ok', at: now(), detail: lock.path },
144
+ ...writes.map((write) => ({
145
+ name: `pending:${write.path}`,
146
+ status: 'ok',
147
+ at: now(),
148
+ detail: write.digest,
149
+ })),
150
+ ],
151
+ entries: writes.map((write) => ({
152
+ path: write.path,
153
+ status: 'pending',
154
+ digest: write.digest,
155
+ identity: null,
156
+ backupRef: null,
157
+ backupDigest: null,
158
+ detail: null,
159
+ })),
160
+ residuals: [],
161
+ manifestRef: workspaceInstallPaths(root).manifestPath,
162
+ };
163
+ const journalWritten = await writeInstallJournal(journalFile, journal, root);
164
+ if (!journalWritten.ok) {
165
+ await releaseInstallLock(lock.path, lock.lock.token);
166
+ return {
167
+ ok: false,
168
+ status: 'failed',
169
+ diagnostics: [
170
+ diagnostic(diagnosticCodes.initApplyFailed, `Cannot write apply journal: ${journalWritten.message}`, 'Inspect .pappra/local permissions and retry; no files were written.', journalFile),
171
+ ],
172
+ residualPaths: [],
173
+ };
174
+ }
175
+ journal = appendStep({ ...journal, status: 'applying', updatedAt: now() }, 'prepared', null);
176
+ await writeInstallJournal(journalFile, journal, root);
177
+ return {
178
+ ok: true,
179
+ context: {
180
+ root,
181
+ installId,
182
+ planId: plan.planId,
183
+ lockPath: lock.path,
184
+ lockToken: lock.lock.token,
185
+ journalFile,
186
+ manifest,
187
+ journal,
188
+ entries: [],
189
+ plannedPaths: plan.files.map((file) => file.path),
190
+ },
191
+ };
192
+ }
193
+ export async function recordManagedFile(context, path, digest, identity) {
194
+ if (!context.plannedPaths.includes(path)) {
195
+ return {
196
+ ok: false,
197
+ diagnostics: [
198
+ diagnostic(diagnosticCodes.initPlanInvalid, `Managed apply recorded an unplanned path: ${path}`, 'Regenerate the plan; the journal was not advanced.', context.journalFile),
199
+ ],
200
+ };
201
+ }
202
+ const entries = context.journal.entries.map((entry) => entry.path === path
203
+ ? {
204
+ ...entry,
205
+ status: 'created',
206
+ identity,
207
+ detail: null,
208
+ }
209
+ : entry);
210
+ const journal = appendStep({
211
+ ...context.journal,
212
+ entries,
213
+ updatedAt: now(),
214
+ }, `created:${path}`, digest);
215
+ const written = await writeInstallJournal(context.journalFile, journal, context.root);
216
+ if (!written.ok) {
217
+ return {
218
+ ok: false,
219
+ diagnostics: [
220
+ diagnostic(diagnosticCodes.initApplyFailed, `Cannot persist created file in journal: ${written.message}`, 'Inspect .pappra/local/journals; the file may need manual recovery.', context.journalFile),
221
+ ],
222
+ };
223
+ }
224
+ context.journal = journal;
225
+ context.entries.push(manifestEntry(context.installId, path, digest, identity));
226
+ return { ok: true };
227
+ }
228
+ export async function completeManagedApply(context) {
229
+ const appliedJournal = appendStep({ ...context.journal, status: 'applied', updatedAt: now() }, 'files-created', context.entries.map((entry) => entry.path).join(', '));
230
+ await writeInstallJournal(context.journalFile, appliedJournal, context.root);
231
+ context.journal = appliedJournal;
232
+ const entries = [...context.manifest.entries, ...context.entries];
233
+ const manifest = {
234
+ ...context.manifest,
235
+ entries,
236
+ updatedAt: now(),
237
+ };
238
+ const manifestWritten = await writeWorkspaceManifest(context.root, manifest);
239
+ if (!manifestWritten.ok) {
240
+ return {
241
+ ok: false,
242
+ manifest: null,
243
+ journal: context.journal,
244
+ diagnostics: [
245
+ diagnostic(diagnosticCodes.initApplyFailed, `Cannot write installation manifest: ${manifestWritten.message}`, 'Files may remain; inspect .pappra/local/install-manifest.json and the journal.', workspaceInstallPaths(context.root).manifestPath),
246
+ ],
247
+ residualPaths: [],
248
+ };
249
+ }
250
+ const verifiedJournal = appendStep({
251
+ ...context.journal,
252
+ status: 'verified',
253
+ entries: context.journal.entries.map((entry) => ({
254
+ ...entry,
255
+ status: 'verified',
256
+ })),
257
+ manifestRef: workspaceInstallPaths(context.root).manifestPath,
258
+ updatedAt: now(),
259
+ }, 'manifest-written', manifestWritten.value.digest);
260
+ const journalWritten = await writeInstallJournal(context.journalFile, verifiedJournal, context.root);
261
+ const lockRelease = await releaseInstallLock(context.lockPath, context.lockToken);
262
+ const residualPaths = [];
263
+ if (!journalWritten.ok)
264
+ residualPaths.push(context.journalFile);
265
+ if (!lockRelease.ok)
266
+ residualPaths.push(context.lockPath);
267
+ return {
268
+ ok: true,
269
+ manifest,
270
+ journal: verifiedJournal,
271
+ diagnostics: residualPaths.map((path) => createDiagnostic({
272
+ code: diagnosticCodes.initApplyResidual,
273
+ severity: 'warning',
274
+ message: `Managed apply completed but could not clean state: ${path}`,
275
+ source: path,
276
+ hint: 'Files and manifest are present; inspect this residual state before retrying.',
277
+ })),
278
+ residualPaths,
279
+ };
280
+ }
281
+ export async function failManagedApply(context, message, residuals) {
282
+ const journal = appendStep({
283
+ ...context.journal,
284
+ status: residuals.length === 0 ? 'rolled-back' : 'rollback-required',
285
+ residuals: [...residuals],
286
+ entries: context.journal.entries.map((entry) => residuals.includes(entry.path)
287
+ ? { ...entry, status: 'residual' }
288
+ : entry.status === 'pending' || entry.status === 'created'
289
+ ? { ...entry, status: 'removed' }
290
+ : entry),
291
+ updatedAt: now(),
292
+ }, 'failed', message, true);
293
+ const journalWritten = await writeInstallJournal(context.journalFile, journal, context.root).catch(() => ({ ok: false }));
294
+ const lockRelease = await releaseInstallLock(context.lockPath, context.lockToken);
295
+ const residualPaths = [...residuals];
296
+ if (!journalWritten.ok)
297
+ residualPaths.push(context.journalFile);
298
+ if (!lockRelease.ok)
299
+ residualPaths.push(context.lockPath);
300
+ return {
301
+ ok: false,
302
+ manifest: null,
303
+ journal,
304
+ diagnostics: [
305
+ createDiagnostic({
306
+ code: diagnosticCodes.initApplyFailed,
307
+ severity: 'error',
308
+ message: `${message}${residualPaths.length === 0 ? ' No planned file remains.' : ` Residual paths: ${residualPaths.join(', ')}`}`,
309
+ source: context.journalFile,
310
+ hint: residualPaths.length === 0
311
+ ? 'Inspect the journal; runtime/provider actions were not executed.'
312
+ : 'Inspect residual paths manually; Pappra does not delete files with uncertain ownership.',
313
+ }),
314
+ ...residualPaths.map((path) => createDiagnostic({
315
+ code: diagnosticCodes.initApplyResidual,
316
+ severity: 'error',
317
+ message: `Residual path requires manual review: ${path}`,
318
+ source: path,
319
+ hint: 'Pappra did not remove this path because ownership or content could not be proven.',
320
+ })),
321
+ ],
322
+ residualPaths,
323
+ };
324
+ }
325
+ async function findIncompleteWorkspaceJournal(root) {
326
+ const directory = workspaceInstallPaths(root).journalsDir;
327
+ const listing = await scanJournalDirectory(directory);
328
+ if (listing.kind === 'missing') {
329
+ return { incomplete: null, unreadable: [] };
330
+ }
331
+ if (listing.kind === 'unreadable') {
332
+ return { incomplete: null, unreadable: [directory] };
333
+ }
334
+ const unreadable = [...listing.nonRegular];
335
+ let incomplete = null;
336
+ for (const path of listing.files) {
337
+ const read = await readInstallJournal(path);
338
+ if (read.value === null) {
339
+ unreadable.push(path);
340
+ continue;
341
+ }
342
+ if (incomplete === null &&
343
+ (read.value.status === 'prepared' ||
344
+ read.value.status === 'applying' ||
345
+ read.value.status === 'applied' ||
346
+ read.value.status === 'rollback-required')) {
347
+ incomplete = path;
348
+ }
349
+ }
350
+ return { incomplete, unreadable };
351
+ }
352
+ /** Updates the managed machine.json runtime location after a verified install. */
353
+ export async function updateMachineRuntime(root, runtime, options = {}) {
354
+ const stateBoundary = await ensureWorkspaceStateBoundary(root);
355
+ if (!stateBoundary.ok) {
356
+ return {
357
+ ok: false,
358
+ status: 'failed',
359
+ diagnostics: [
360
+ diagnostic(diagnosticCodes.initTargetUnsafe, stateBoundary.message, stateBoundary.nextAction ?? 'Inspect symlinked Pappra state paths.', workspaceInstallPaths(root).localDir),
361
+ ],
362
+ residualPaths: [],
363
+ };
364
+ }
365
+ const existingLock = await observePath(workspaceInstallPaths(root).lockPath);
366
+ if (existingLock.kind !== 'missing') {
367
+ return {
368
+ ok: false,
369
+ status: 'locked',
370
+ diagnostics: [
371
+ diagnostic(diagnosticCodes.initApplyResidual, 'Workspace apply lock exists; machine update is not attempted.', 'Inspect the lock owner; Pappra does not bypass it with an unchanged shortcut.', workspaceInstallPaths(root).lockPath),
372
+ ],
373
+ residualPaths: [workspaceInstallPaths(root).lockPath],
374
+ };
375
+ }
376
+ const journalScan = await findIncompleteWorkspaceJournal(root);
377
+ if (journalScan.unreadable.length > 0) {
378
+ return {
379
+ ok: false,
380
+ status: 'partial',
381
+ diagnostics: [
382
+ diagnostic(diagnosticCodes.initApplyResidual, `Workspace journal state is unreadable or incompatible: ${journalScan.unreadable.join(', ')}`, 'Inspect the listed journals; Pappra does not bypass or repair an unresolved state.', journalScan.unreadable[0] ?? workspaceInstallPaths(root).journalsDir),
383
+ ],
384
+ residualPaths: journalScan.unreadable,
385
+ };
386
+ }
387
+ if (journalScan.incomplete !== null) {
388
+ return {
389
+ ok: false,
390
+ status: 'partial',
391
+ diagnostics: [
392
+ diagnostic(diagnosticCodes.initApplyResidual, `A previous managed apply/update is incomplete: ${journalScan.incomplete}`, 'Inspect the journal; Pappra does not bypass an unresolved state by updating again.', journalScan.incomplete),
393
+ ],
394
+ residualPaths: [journalScan.incomplete],
395
+ };
396
+ }
397
+ const manifestRead = await readWorkspaceManifest(root);
398
+ if (manifestRead.value === null) {
399
+ return {
400
+ ok: false,
401
+ status: 'unmanaged',
402
+ diagnostics: [
403
+ diagnostic(diagnosticCodes.initApplyUnsupported, 'machine.json is not owned by a P04 installation manifest.', 'Use a managed init/apply or configure flow; Pappra does not adopt unowned files.', workspaceInstallPaths(root).manifestPath),
404
+ ],
405
+ residualPaths: [],
406
+ };
407
+ }
408
+ const manifest = manifestRead.value;
409
+ const currentIdentity = await rootIdentityOf(root);
410
+ if (currentIdentity === null ||
411
+ currentIdentity.dev !== manifest.rootIdentity.dev ||
412
+ currentIdentity.ino !== manifest.rootIdentity.ino) {
413
+ return {
414
+ ok: false,
415
+ status: 'failed',
416
+ diagnostics: [
417
+ diagnostic(diagnosticCodes.initPlanStale, 'Workspace root identity does not match the managed manifest.', 'Inspect the workspace; Pappra does not update another root.', root),
418
+ ],
419
+ residualPaths: [],
420
+ };
421
+ }
422
+ const machinePath = '.pappra/local/machine.json';
423
+ const index = manifest.entries.findIndex((entry) => entry.path === machinePath);
424
+ if (index < 0) {
425
+ return {
426
+ ok: false,
427
+ status: 'unmanaged',
428
+ diagnostics: [
429
+ diagnostic(diagnosticCodes.initApplyUnsupported, 'machine.json has no managed ownership entry.', 'Create it through a P04 managed apply before installing a runtime.', workspaceInstallPaths(root).manifestPath),
430
+ ],
431
+ residualPaths: [],
432
+ };
433
+ }
434
+ const entry = manifest.entries[index];
435
+ const ownership = await inspectOwnedFile(root, entry);
436
+ if (ownership.status !== 'match') {
437
+ return {
438
+ ok: false,
439
+ status: 'conflict',
440
+ diagnostics: [
441
+ diagnostic(diagnosticCodes.initTargetExists, `machine.json no longer matches its managed digest: ${ownership.status}`, 'Preserve the user-modified file; adjust ownership only after review.', join(root, machinePath)),
442
+ ],
443
+ residualPaths: [machinePath],
444
+ };
445
+ }
446
+ const currentMachineBytes = await readFile(join(root, machinePath)).catch(() => null);
447
+ if (currentMachineBytes !== null) {
448
+ try {
449
+ const currentMachine = JSON.parse(currentMachineBytes.toString('utf8'));
450
+ if (currentMachine.runtime?.version === runtime.runtimeVersion &&
451
+ currentMachine.runtime.executable === runtime.executable) {
452
+ return {
453
+ ok: true,
454
+ status: 'unchanged',
455
+ diagnostics: [],
456
+ residualPaths: [],
457
+ };
458
+ }
459
+ }
460
+ catch {
461
+ // The schema-validated update path below reports malformed machine state.
462
+ }
463
+ }
464
+ const lock = await acquireInstallLock(workspaceInstallPaths(root).lockPath, 'update-machine-runtime', root, root);
465
+ if (!lock.ok || lock.lock === null) {
466
+ return {
467
+ ok: false,
468
+ status: lock.status === 'locked' ? 'locked' : 'failed',
469
+ diagnostics: [
470
+ diagnostic(diagnosticCodes.initApplyFailed, lock.message ?? 'Workspace lock is held.', 'Another managed operation owns this workspace.', lock.path),
471
+ ],
472
+ residualPaths: [],
473
+ };
474
+ }
475
+ const updateInstallId = randomUUID();
476
+ const updateJournalFile = journalPath(root, updateInstallId);
477
+ let updateJournal = {
478
+ schema: 'pappra/install-journal/1',
479
+ installId: updateInstallId,
480
+ operation: 'update-machine-runtime',
481
+ target: machinePath,
482
+ workspaceRoot: root,
483
+ runtimeVersion: runtime.runtimeVersion,
484
+ status: 'prepared',
485
+ createdAt: now(),
486
+ updatedAt: now(),
487
+ steps: [
488
+ { name: 'lock-acquired', status: 'ok', at: now(), detail: lock.path },
489
+ ],
490
+ entries: [
491
+ {
492
+ path: machinePath,
493
+ status: 'pending',
494
+ digest: runtime.packageDigest,
495
+ identity: null,
496
+ backupRef: null,
497
+ backupDigest: null,
498
+ detail: null,
499
+ },
500
+ ],
501
+ residuals: [],
502
+ manifestRef: workspaceInstallPaths(root).manifestPath,
503
+ };
504
+ const initialJournal = await writeInstallJournal(updateJournalFile, updateJournal, root);
505
+ if (!initialJournal.ok) {
506
+ await releaseInstallLock(lock.path, lock.lock.token);
507
+ return {
508
+ ok: false,
509
+ status: 'failed',
510
+ diagnostics: [
511
+ diagnostic(diagnosticCodes.initApplyFailed, `Cannot write machine update journal: ${initialJournal.message}`, 'Inspect .pappra/local/journals; runtime remains installed but pending.', updateJournalFile),
512
+ ],
513
+ residualPaths: [],
514
+ };
515
+ }
516
+ updateJournal = appendStep({ ...updateJournal, status: 'applying', updatedAt: now() }, 'prepared', null);
517
+ await writeInstallJournal(updateJournalFile, updateJournal, root);
518
+ try {
519
+ const machineFile = join(root, machinePath);
520
+ const oldBytes = await readFile(machineFile);
521
+ let raw;
522
+ try {
523
+ raw = JSON.parse(oldBytes.toString('utf8'));
524
+ }
525
+ catch {
526
+ throw new Error('machine.json is not valid JSON.');
527
+ }
528
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
529
+ throw new Error('machine.json is not an object.');
530
+ }
531
+ const next = {
532
+ ...raw,
533
+ runtime: {
534
+ version: runtime.runtimeVersion,
535
+ executable: runtime.executable,
536
+ verifiedAt: runtime.verifiedAt,
537
+ },
538
+ };
539
+ const parsedMachine = machineConfigSchema.safeParse(next);
540
+ if (!parsedMachine.success) {
541
+ throw new Error(`Updated machine.json is invalid: ${parsedMachine.error.issues[0]?.message ?? 'unknown issue'}`);
542
+ }
543
+ const updatedAt = now();
544
+ const nextBytes = Buffer.from(`${JSON.stringify(next, null, 2)}\n`);
545
+ const backup = backupPath(root, updateInstallId, machinePath);
546
+ const backupBoundary = await ensureStatePathBoundary(root, backup);
547
+ if (!backupBoundary.ok) {
548
+ throw new Error(backupBoundary.message);
549
+ }
550
+ await mkdir(dirname(backup), { recursive: true });
551
+ await writeFile(backup, oldBytes, { mode: 0o600, flag: 'wx' });
552
+ await options.beforeWrite?.();
553
+ const machineWrite = await writeJsonAtomic(machineFile, next, root);
554
+ if (!machineWrite.ok) {
555
+ throw new Error(`Cannot write machine.json: ${machineWrite.message}`);
556
+ }
557
+ const observation = await observePath(machineFile);
558
+ if (observation.kind !== 'file' || observation.stats === null) {
559
+ throw new Error('machine.json disappeared after update.');
560
+ }
561
+ const actualBytes = await readFile(machineFile);
562
+ const afterDigest = sha256Digest(nextBytes);
563
+ if (sha256Digest(actualBytes) !== afterDigest) {
564
+ throw new Error('machine.json bytes do not match the intended update.');
565
+ }
566
+ const backupRef = backup
567
+ .slice(root.length + 1)
568
+ .split('\\')
569
+ .join('/');
570
+ const backupDigest = sha256Digest(oldBytes);
571
+ const updatedEntry = {
572
+ ...entry,
573
+ afterDigest,
574
+ backupRef,
575
+ backupDigest,
576
+ identity: { dev: observation.stats.dev, ino: observation.stats.ino },
577
+ verifiedAt: updatedAt,
578
+ };
579
+ const updatedEntries = [...manifest.entries];
580
+ updatedEntries[index] = managedFileEntrySchema.parse(updatedEntry);
581
+ const nextManifest = {
582
+ ...manifest,
583
+ entries: updatedEntries,
584
+ updatedAt,
585
+ };
586
+ const manifestWritten = await writeWorkspaceManifest(root, nextManifest);
587
+ if (!manifestWritten.ok) {
588
+ throw new Error(`Cannot update installation manifest: ${manifestWritten.message}`);
589
+ }
590
+ updateJournal = appendStep({
591
+ ...updateJournal,
592
+ status: 'verified',
593
+ entries: updateJournal.entries.map((entry) => ({
594
+ ...entry,
595
+ status: 'verified',
596
+ digest: afterDigest,
597
+ identity: updatedEntry.identity,
598
+ backupRef,
599
+ backupDigest,
600
+ })),
601
+ updatedAt: now(),
602
+ }, 'verified', afterDigest);
603
+ await options.beforeFinalJournalPersist?.();
604
+ const journalWritten = await writeInstallJournal(updateJournalFile, updateJournal, root);
605
+ const lockRelease = await releaseInstallLock(lock.path, lock.lock.token);
606
+ const residualPaths = [];
607
+ if (!journalWritten.ok)
608
+ residualPaths.push(updateJournalFile);
609
+ if (!lockRelease.ok)
610
+ residualPaths.push(lock.path);
611
+ if (residualPaths.length > 0) {
612
+ return {
613
+ ok: false,
614
+ status: 'partial',
615
+ updateInstallId,
616
+ journalPath: updateJournalFile,
617
+ diagnostics: residualPaths.map((path) => createDiagnostic({
618
+ code: diagnosticCodes.initApplyResidual,
619
+ severity: 'error',
620
+ message: `machine.json changed but managed state is not final: ${path}`,
621
+ source: path,
622
+ hint: 'Inspect the journal/lock; do not treat initialization as complete.',
623
+ })),
624
+ residualPaths,
625
+ };
626
+ }
627
+ return {
628
+ ok: true,
629
+ status: 'updated',
630
+ updateInstallId,
631
+ journalPath: updateJournalFile,
632
+ diagnostics: [],
633
+ residualPaths: [],
634
+ };
635
+ }
636
+ catch (error) {
637
+ updateJournal = appendStep({
638
+ ...updateJournal,
639
+ status: 'rollback-required',
640
+ residuals: [machinePath],
641
+ updatedAt: now(),
642
+ }, 'failed', errorMessage(error), true);
643
+ const failedJournal = await writeInstallJournal(updateJournalFile, updateJournal, root).catch(() => ({ ok: false }));
644
+ const lockRelease = await releaseInstallLock(lock.path, lock.lock.token);
645
+ const residual = [machinePath];
646
+ if (!failedJournal.ok)
647
+ residual.push(updateJournalFile);
648
+ if (!lockRelease.ok)
649
+ residual.push(lock.path);
650
+ return {
651
+ ok: false,
652
+ status: 'failed',
653
+ diagnostics: [
654
+ diagnostic(diagnosticCodes.initApplyFailed, `Cannot update machine runtime location: ${errorMessage(error)}`, 'Inspect .pappra/local/backups and install-manifest.json; runtime remains installed but pending.', machinePath),
655
+ ],
656
+ residualPaths: residual,
657
+ };
658
+ }
659
+ }
660
+ export async function inspectManagedWorkspaceState(root) {
661
+ const manifest = await readWorkspaceManifest(root);
662
+ return { manifest: manifest.value, diagnostics: manifest.diagnostics };
663
+ }
@@ -0,0 +1,5 @@
1
+ export * from './apply.js';
2
+ export * from './ownership.js';
3
+ export * from './plan.js';
4
+ export * from './rollback.js';
5
+ export * from './runtime.js';
@@ -0,0 +1,5 @@
1
+ export * from './apply.js';
2
+ export * from './ownership.js';
3
+ export * from './plan.js';
4
+ export * from './rollback.js';
5
+ export * from './runtime.js';