@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,293 @@
1
+ import { mkdir, open, rename, rm } from 'node:fs/promises';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { join, relative, sep } from 'node:path';
4
+ import { observePath, readFileBounded, resolveRealpath, sha256Digest, } from '../infra/fs.js';
5
+ import { diagnosticCodes } from '../schemas/errors.js';
6
+ import { isPathInside } from '../schemas/paths.js';
7
+ import { failed, succeeded } from '../schemas/outcome.js';
8
+ import { codeIndexRegistrationSchema, } from '../schemas/code.js';
9
+ import { codeIdentityDigest } from './identity.js';
10
+ const REPO_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
11
+ export function codeRegistrationPath(root, repositoryId) {
12
+ if (!REPO_ID.test(repositoryId) || repositoryId.length > 64) {
13
+ throw new RangeError(`Invalid repository id: ${repositoryId}`);
14
+ }
15
+ return join(root, '.pappra', 'local', 'code', `${repositoryId}.json`);
16
+ }
17
+ export async function ensureCodeMutationBoundary(root, path, excludedRoots = []) {
18
+ if (!isPathInside(root, path)) {
19
+ return failed(diagnosticCodes.code_path_unsafe, `Private code path escapes the selected repository: ${path}`, { nextAction: 'Use a private path in the selected repository.' });
20
+ }
21
+ const realRoot = await resolveRealpath(root);
22
+ if (realRoot === null) {
23
+ return failed(diagnosticCodes.code_path_unsafe, `Cannot resolve selected repository: ${root}`, {
24
+ nextAction: 'Inspect the selected repository; no mutation was performed.',
25
+ });
26
+ }
27
+ const realExcludedRoots = await Promise.all(excludedRoots.map(async (excluded) => (await resolveRealpath(excluded)) ?? excluded));
28
+ const rel = relative(root, path);
29
+ let current = root;
30
+ for (const part of rel.split(sep)) {
31
+ if (part === '')
32
+ continue;
33
+ current = join(current, part);
34
+ const observed = await observePath(current);
35
+ if (observed.kind === 'symlink') {
36
+ return failed(diagnosticCodes.code_path_unsafe, `Mutation path traverses a symlink: ${current}`, {
37
+ nextAction: 'Replace the redirected path; no mutation was performed.',
38
+ });
39
+ }
40
+ if (observed.kind === 'missing')
41
+ break;
42
+ }
43
+ const real = await resolveRealpath(path);
44
+ if (real !== null) {
45
+ if (!isPathInside(realRoot, real)) {
46
+ return failed(diagnosticCodes.code_path_unsafe, `Mutation path resolves outside the selected repository: ${path}`, {
47
+ nextAction: 'Inspect the selected repository; no mutation was performed.',
48
+ });
49
+ }
50
+ if (realExcludedRoots.some((excluded) => isPathInside(excluded, real))) {
51
+ return failed(diagnosticCodes.code_path_unsafe, `Mutation path resolves into another selected repository: ${path}`, {
52
+ nextAction: 'Choose a private path in the selected repository; no mutation was performed.',
53
+ });
54
+ }
55
+ }
56
+ return succeeded(null);
57
+ }
58
+ const PREPARE_LEASES = new Map();
59
+ export async function acquireCodePrepareLock(root, excludedRoots = []) {
60
+ const directory = join(root, '.pappra', 'local', 'code');
61
+ const path = join(directory, 'prepare.lock');
62
+ const boundary = await ensureCodeMutationBoundary(root, path, excludedRoots);
63
+ if (!boundary.ok)
64
+ return boundary;
65
+ try {
66
+ await mkdir(directory, { recursive: true, mode: 0o700 });
67
+ const content = Buffer.from(`${JSON.stringify({ pid: process.pid, token: randomUUID(), acquiredAt: new Date().toISOString() })}\n`);
68
+ let ownership = null;
69
+ const handle = await open(path, 'wx', 0o600);
70
+ try {
71
+ const opened = await handle.stat();
72
+ if (!opened.isFile()) {
73
+ throw new Error(`Prepare lock is not a regular file: ${path}`);
74
+ }
75
+ ownership = {
76
+ dev: opened.dev,
77
+ ino: opened.ino,
78
+ expected: content,
79
+ };
80
+ await handle.writeFile(content);
81
+ await handle.sync();
82
+ }
83
+ finally {
84
+ await handle.close().catch(() => undefined);
85
+ }
86
+ const observed = await observePath(path);
87
+ if (observed.kind !== 'file' ||
88
+ observed.stats === null ||
89
+ ownership === null ||
90
+ observed.stats.dev !== ownership.dev ||
91
+ observed.stats.ino !== ownership.ino) {
92
+ await removeRegistrationTemporaryIfOwned(root, path, excludedRoots, ownership);
93
+ return failed(diagnosticCodes.code_prepare_failed, `Cannot verify owned prepare lock: ${path}`, { nextAction: 'Inspect the private prepare lock; no command was run.' });
94
+ }
95
+ const lockRead = await readFileBounded(path, 4096);
96
+ if (!lockRead.ok || !lockRead.value.bytes.equals(content)) {
97
+ await removeRegistrationTemporaryIfOwned(root, path, excludedRoots, ownership);
98
+ return failed(diagnosticCodes.code_prepare_failed, `Prepare lock content changed before use: ${path}`, { nextAction: 'Inspect the private prepare lock; no command was run.' });
99
+ }
100
+ PREPARE_LEASES.set(path, {
101
+ root,
102
+ path,
103
+ dev: ownership.dev,
104
+ ino: ownership.ino,
105
+ digest: sha256Digest(content),
106
+ excludedRoots,
107
+ });
108
+ return succeeded(path);
109
+ }
110
+ catch (error) {
111
+ const code = typeof error === 'object' &&
112
+ error !== null &&
113
+ 'code' in error &&
114
+ typeof error.code === 'string'
115
+ ? error.code
116
+ : null;
117
+ return failed(code === 'EEXIST'
118
+ ? diagnosticCodes.code_prepare_conflict
119
+ : diagnosticCodes.code_prepare_failed, code === 'EEXIST'
120
+ ? `Another prepare owns ${path}; no command was run.`
121
+ : `Cannot acquire code prepare lock: ${error instanceof Error ? error.message : String(error)}`, {
122
+ nextAction: 'Inspect the private prepare lock; stale locks require explicit manual resolution.',
123
+ });
124
+ }
125
+ }
126
+ export async function releaseCodePrepareLock(path) {
127
+ const lease = PREPARE_LEASES.get(path);
128
+ if (lease === undefined) {
129
+ return failed(diagnosticCodes.code_prepare_conflict, `No owned prepare lease is recorded for ${path}; cleanup was refused.`, {
130
+ nextAction: 'Inspect the private prepare state; do not delete unknown files.',
131
+ });
132
+ }
133
+ const boundary = await ensureCodeMutationBoundary(lease.root, path, lease.excludedRoots);
134
+ if (!boundary.ok)
135
+ return boundary;
136
+ const observed = await observePath(path);
137
+ if (observed.kind === 'missing') {
138
+ PREPARE_LEASES.delete(path);
139
+ return succeeded(null);
140
+ }
141
+ if (observed.kind !== 'file' ||
142
+ observed.stats === null ||
143
+ observed.stats.dev !== lease.dev ||
144
+ observed.stats.ino !== lease.ino) {
145
+ return failed(diagnosticCodes.code_prepare_conflict, `Prepare lock was replaced or redirected; it was preserved: ${path}`, {
146
+ nextAction: 'Inspect the private prepare lock; no user file was deleted.',
147
+ });
148
+ }
149
+ const read = await readFileBounded(path, 4096);
150
+ if (!read.ok || read.value.digest !== lease.digest) {
151
+ return failed(diagnosticCodes.code_prepare_conflict, `Prepare lock content changed; it was preserved: ${path}`, {
152
+ nextAction: 'Inspect the private prepare lock; no user file was deleted.',
153
+ });
154
+ }
155
+ try {
156
+ await rm(path);
157
+ PREPARE_LEASES.delete(path);
158
+ return succeeded(null);
159
+ }
160
+ catch (error) {
161
+ return failed(diagnosticCodes.code_prepare_partial, `Cannot release owned prepare lock: ${error instanceof Error ? error.message : String(error)}`, {
162
+ nextAction: 'Inspect and remove the owned lock after confirming ownership.',
163
+ });
164
+ }
165
+ }
166
+ export async function readCodeRegistration(root, repositoryId) {
167
+ const path = codeRegistrationPath(root, repositoryId);
168
+ const boundary = await ensureCodeMutationBoundary(root, path);
169
+ if (!boundary.ok)
170
+ return boundary;
171
+ const observation = await observePath(path);
172
+ if (observation.kind === 'missing')
173
+ return succeeded(null);
174
+ if (observation.kind !== 'file') {
175
+ return failed(diagnosticCodes.code_registration_invalid, `Code registration is not a regular file: ${path}`, {
176
+ nextAction: 'Inspect the private code registration; no state was accepted.',
177
+ });
178
+ }
179
+ const read = await readFileBounded(path, 256 * 1024);
180
+ if (!read.ok)
181
+ return read;
182
+ let value;
183
+ try {
184
+ value = JSON.parse(read.value.bytes.toString('utf8'));
185
+ }
186
+ catch {
187
+ return failed(diagnosticCodes.code_registration_invalid, `Code registration is not valid JSON: ${path}`, {
188
+ nextAction: 'Reprepare the selected repository; the registration was not trusted.',
189
+ });
190
+ }
191
+ const parsed = codeIndexRegistrationSchema.safeParse(value);
192
+ if (!parsed.success) {
193
+ return failed(diagnosticCodes.code_registration_invalid, `Code registration schema is invalid: ${path}`, {
194
+ nextAction: 'Reprepare the selected repository; the registration was not trusted.',
195
+ });
196
+ }
197
+ return succeeded(parsed.data);
198
+ }
199
+ export function registrationMatches(registration, identity) {
200
+ return (registration.repositoryId === identity.repositoryId &&
201
+ registration.checkoutId === identity.checkoutId &&
202
+ registration.root === identity.root &&
203
+ registration.engineVersion === identity.engine.version &&
204
+ registration.configDigest === identity.engine.configDigest &&
205
+ registration.identityDigest === codeIdentityDigest(identity));
206
+ }
207
+ function noCleanupResidual() {
208
+ return { residualPath: null, reason: null };
209
+ }
210
+ async function removeRegistrationTemporaryIfOwned(root, path, excludedRoots, identity) {
211
+ const observed = await observePath(path);
212
+ if (observed.kind === 'missing')
213
+ return noCleanupResidual();
214
+ if (identity === null) {
215
+ return { residualPath: path, reason: 'unknown_ownership' };
216
+ }
217
+ const boundary = await ensureCodeMutationBoundary(root, path, excludedRoots);
218
+ if (!boundary.ok) {
219
+ return { residualPath: path, reason: 'boundary_refused' };
220
+ }
221
+ if (observed.kind !== 'file' || observed.stats === null) {
222
+ return { residualPath: path, reason: 'not_regular_file' };
223
+ }
224
+ if (observed.stats.dev !== identity.dev ||
225
+ observed.stats.ino !== identity.ino) {
226
+ return { residualPath: path, reason: 'identity_changed' };
227
+ }
228
+ const read = await readFileBounded(path, 512 * 1024);
229
+ if (!read.ok) {
230
+ return { residualPath: path, reason: 'read_failed' };
231
+ }
232
+ const bytes = read.value.bytes;
233
+ const isComplete = identity.expected.equals(bytes);
234
+ const isSafePrefix = bytes.length < identity.expected.length &&
235
+ identity.expected.subarray(0, bytes.length).equals(bytes);
236
+ if (!isComplete && !isSafePrefix) {
237
+ return { residualPath: path, reason: 'content_changed' };
238
+ }
239
+ try {
240
+ await rm(path);
241
+ return noCleanupResidual();
242
+ }
243
+ catch {
244
+ return { residualPath: path, reason: 'remove_failed' };
245
+ }
246
+ }
247
+ export async function writeCodeRegistration(root, registration, excludedRoots = []) {
248
+ const path = codeRegistrationPath(root, registration.repositoryId);
249
+ const boundary = await ensureCodeMutationBoundary(root, path, excludedRoots);
250
+ if (!boundary.ok)
251
+ return boundary;
252
+ const temporary = `${path}.tmp-${randomUUID()}`;
253
+ const content = Buffer.from(`${JSON.stringify(registration, null, 2)}\n`);
254
+ let owned = null;
255
+ try {
256
+ await mkdir(join(root, '.pappra', 'local', 'code'), {
257
+ recursive: true,
258
+ mode: 0o700,
259
+ });
260
+ const handle = await open(temporary, 'wx', 0o600);
261
+ try {
262
+ const opened = await handle.stat();
263
+ if (!opened.isFile()) {
264
+ throw new Error(`Temporary registration is not a regular file: ${temporary}`);
265
+ }
266
+ owned = {
267
+ dev: opened.dev,
268
+ ino: opened.ino,
269
+ expected: content,
270
+ };
271
+ await handle.writeFile(content);
272
+ await handle.sync();
273
+ }
274
+ finally {
275
+ await handle.close().catch(() => undefined);
276
+ }
277
+ await rename(temporary, path);
278
+ return succeeded(path);
279
+ }
280
+ catch (error) {
281
+ const primaryMessage = `Cannot write code registration ${path}: ${error instanceof Error ? error.message : String(error)}`;
282
+ const cleanup = await removeRegistrationTemporaryIfOwned(root, temporary, excludedRoots, owned);
283
+ if (cleanup.residualPath !== null) {
284
+ const reason = cleanup.reason ?? 'unknown_ownership';
285
+ return failed(diagnosticCodes.code_prepare_partial, `${primaryMessage}; registration temporary was preserved at ${cleanup.residualPath} (${reason}); inspect or remove it after confirming it is not user data.`, {
286
+ nextAction: `Inspect preserved registration temporary ${cleanup.residualPath}; no ready registration was claimed and no residual path was deleted.`,
287
+ });
288
+ }
289
+ return failed(diagnosticCodes.code_prepare_partial, primaryMessage, {
290
+ nextAction: 'Inspect permissions; no ready registration was claimed.',
291
+ });
292
+ }
293
+ }
@@ -0,0 +1,53 @@
1
+ import type { Diagnostic } from '../schemas/diagnostics.js';
2
+ import { type CodeIdentity, type CodeIndexHealth, type CodeIndexRegistration, type CodePreparePlan, type CodePrepareResult, type CodeQueryKind, type CodeQueryLimits, type CodeQueryResult } from '../schemas/code.js';
3
+ import { codeIdentityDigest } from './identity.js';
4
+ export interface CodeStatusResult {
5
+ readonly ok: boolean;
6
+ readonly identity: CodeIdentity | null;
7
+ readonly health: CodeIndexHealth;
8
+ readonly registrationPath: string | null;
9
+ readonly registrationPresent: boolean;
10
+ readonly diagnostics: readonly Diagnostic[];
11
+ }
12
+ export interface CodeStatusInput {
13
+ readonly root: string;
14
+ readonly repositoryId: string;
15
+ readonly signal?: AbortSignal;
16
+ }
17
+ export interface CodePrepareInput extends CodeStatusInput {
18
+ readonly apply: boolean;
19
+ readonly rebuild: boolean;
20
+ }
21
+ export interface CodeQueryInput extends CodeStatusInput {
22
+ readonly kind: CodeQueryKind;
23
+ readonly query: string;
24
+ readonly limits?: Partial<CodeQueryLimits>;
25
+ }
26
+ export interface CodeQueryOutput {
27
+ readonly ok: boolean;
28
+ readonly result: CodeQueryResult | null;
29
+ readonly health: CodeIndexHealth;
30
+ readonly diagnostics: readonly Diagnostic[];
31
+ }
32
+ interface ObservedCode {
33
+ readonly identity: CodeIdentity | null;
34
+ readonly rawHealth: CodeIndexHealth;
35
+ readonly health: CodeIndexHealth;
36
+ readonly registration: CodeIndexRegistration | null;
37
+ readonly registrationPath: string | null;
38
+ readonly excludedRoots: readonly string[];
39
+ readonly access: 'read' | 'write';
40
+ readonly diagnostics: readonly Diagnostic[];
41
+ }
42
+ export declare class CodeIntelligenceService {
43
+ private readonly root;
44
+ constructor(root: string);
45
+ status(input?: Partial<Omit<CodeStatusInput, 'root'>>): Promise<CodeStatusResult>;
46
+ planPrepare(input: CodePrepareInput): Promise<{
47
+ readonly observed: ObservedCode;
48
+ readonly plan: CodePreparePlan;
49
+ }>;
50
+ prepare(input: CodePrepareInput): Promise<CodePrepareResult>;
51
+ query(input: CodeQueryInput): Promise<CodeQueryOutput>;
52
+ }
53
+ export { codeIdentityDigest };