@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,37 @@
1
+ import type { Outcome } from '../schemas/outcome.js';
2
+ import type { DocumentLocation } from '../schemas/document.js';
3
+ import type { KnowledgeContext } from './context.js';
4
+ import type { DiscoveredRepository } from '../workspace/types.js';
5
+ export interface KnowledgePaths {
6
+ readonly sharedCatalog: string;
7
+ readonly privateCatalog: string;
8
+ readonly localDir: string;
9
+ readonly lockPath: string;
10
+ readonly journalsDir: string;
11
+ }
12
+ export declare function knowledgePaths(root: string): KnowledgePaths;
13
+ /**
14
+ * Rejects a path if its final component or any existing ancestor resolves
15
+ * outside the allowed root. Missing parents are checked at their nearest
16
+ * existing ancestor; creation may add only descendants below that root.
17
+ */
18
+ export declare function ensureKnowledgePathBoundary(root: string, path: string): Promise<Outcome<null>>;
19
+ export interface ResolvedKnowledgeLocation {
20
+ readonly location: DocumentLocation;
21
+ readonly absolutePath: string;
22
+ readonly containmentRoot: string;
23
+ readonly canonicalPath: string;
24
+ readonly repository: DiscoveredRepository | null;
25
+ readonly access: 'read' | 'write';
26
+ readonly normalizedPath: string;
27
+ }
28
+ export interface ResolveLocationOptions {
29
+ readonly forWrite?: boolean;
30
+ readonly forCreate?: boolean;
31
+ }
32
+ export declare function resolveKnowledgeLocation(context: KnowledgeContext, location: DocumentLocation, options?: ResolveLocationOptions): Promise<Outcome<ResolvedKnowledgeLocation>>;
33
+ export declare function readKnowledgeFile(path: string, maxBytes: number): Promise<Outcome<{
34
+ readonly bytes: Buffer;
35
+ readonly digest: string;
36
+ readonly size: number;
37
+ }>>;
@@ -0,0 +1,236 @@
1
+ import { basename, dirname, join, resolve } from 'node:path';
2
+ import { failed, succeeded } from '../schemas/outcome.js';
3
+ import { diagnosticCodes } from '../schemas/errors.js';
4
+ import { isPathInside, normalizePortableRelativePath, resolvePortableRelativePath, samePath, } from '../schemas/paths.js';
5
+ import { observePath, readFileBounded, resolveRealpath } from '../infra/fs.js';
6
+ export function knowledgePaths(root) {
7
+ const localDir = join(root, '.pappra', 'local', 'knowledge');
8
+ return {
9
+ sharedCatalog: join(root, '.pappra', 'knowledge', 'catalog.json'),
10
+ privateCatalog: join(localDir, 'catalog.json'),
11
+ localDir,
12
+ lockPath: join(localDir, 'apply.lock'),
13
+ journalsDir: join(localDir, 'journals'),
14
+ };
15
+ }
16
+ const managedStatePrefixes = [
17
+ '.pappra/local',
18
+ '.pappra/generated',
19
+ '.pappra/knowledge/catalog.json',
20
+ ];
21
+ /**
22
+ * Rejects a path if its final component or any existing ancestor resolves
23
+ * outside the allowed root. Missing parents are checked at their nearest
24
+ * existing ancestor; creation may add only descendants below that root.
25
+ */
26
+ export async function ensureKnowledgePathBoundary(root, path) {
27
+ if (!isPathInside(root, path)) {
28
+ return failed(diagnosticCodes.document_path_unsafe, `Path is outside the allowed knowledge root: ${path}`, {
29
+ nextAction: 'Use a path inside the workspace or an explicit repository.',
30
+ });
31
+ }
32
+ const realRoot = await resolveRealpath(root);
33
+ if (realRoot === null) {
34
+ return failed(diagnosticCodes.document_path_unsafe, `Cannot resolve knowledge root: ${root}`, { nextAction: 'Use an existing readable workspace root.' });
35
+ }
36
+ const candidate = await resolveRealpath(path);
37
+ if (candidate !== null) {
38
+ return isPathInside(realRoot, candidate)
39
+ ? succeeded(null)
40
+ : failed(diagnosticCodes.document_path_unsafe, `Knowledge path resolves outside its root: ${path} -> ${candidate}`, {
41
+ nextAction: 'Keep knowledge paths as regular files inside the workspace.',
42
+ });
43
+ }
44
+ let ancestor = dirname(path);
45
+ for (;;) {
46
+ let observation;
47
+ try {
48
+ observation = await observePath(ancestor);
49
+ }
50
+ catch (error) {
51
+ return failed(diagnosticCodes.fileUnreadable, `Cannot inspect knowledge parent ${ancestor}: ${error instanceof Error ? error.message : String(error)}`, {
52
+ nextAction: 'Check permissions; Pappra did not write through this path.',
53
+ });
54
+ }
55
+ if (observation.kind !== 'missing') {
56
+ if (observation.kind !== 'directory') {
57
+ return failed(diagnosticCodes.document_path_unsafe, `Knowledge parent is not a directory: ${ancestor}`, {
58
+ nextAction: 'Inspect the parent path; Pappra does not write through it.',
59
+ });
60
+ }
61
+ const real = await resolveRealpath(ancestor);
62
+ return real !== null && isPathInside(realRoot, real)
63
+ ? succeeded(null)
64
+ : failed(diagnosticCodes.document_path_unsafe, `Knowledge parent escapes its root: ${ancestor}`, {
65
+ nextAction: 'Inspect or remove the symlinked parent explicitly.',
66
+ });
67
+ }
68
+ const parent = dirname(ancestor);
69
+ if (samePath(parent, ancestor)) {
70
+ return failed(diagnosticCodes.document_path_unsafe, `Cannot find an existing parent for ${path}`, { nextAction: 'Create the parent inside the workspace first.' });
71
+ }
72
+ ancestor = parent;
73
+ }
74
+ }
75
+ async function nestedWorkspaceBoundary(allowedRoot, startDirectory) {
76
+ if (!isPathInside(allowedRoot, startDirectory))
77
+ return null;
78
+ let current = startDirectory;
79
+ while (!samePath(current, allowedRoot)) {
80
+ if ((await observePath(join(current, '.pappra'))).kind !== 'missing') {
81
+ return current;
82
+ }
83
+ const parent = dirname(current);
84
+ if (samePath(parent, current) || !isPathInside(allowedRoot, parent)) {
85
+ return null;
86
+ }
87
+ current = parent;
88
+ }
89
+ return null;
90
+ }
91
+ async function canonicalTargetPath(path) {
92
+ let current = path;
93
+ const missingSegments = [];
94
+ for (;;) {
95
+ const real = await resolveRealpath(current);
96
+ if (real !== null) {
97
+ return missingSegments.length === 0
98
+ ? real
99
+ : resolve(real, ...missingSegments.reverse());
100
+ }
101
+ const parent = dirname(current);
102
+ if (samePath(parent, current))
103
+ return null;
104
+ missingSegments.push(basename(current));
105
+ current = parent;
106
+ }
107
+ }
108
+ function containingRepository(context, target) {
109
+ let match = null;
110
+ for (const repository of context.repositories) {
111
+ if (!isPathInside(repository.root, target))
112
+ continue;
113
+ if (match === null || repository.root.length > match.root.length) {
114
+ match = repository;
115
+ }
116
+ }
117
+ return match;
118
+ }
119
+ export async function resolveKnowledgeLocation(context, location, options = {}) {
120
+ const normalizedPath = normalizePortableRelativePath(location.path);
121
+ if (normalizedPath === '.' || location.path !== normalizedPath) {
122
+ return failed(diagnosticCodes.document_path_unsafe, `Document path must be a normalized non-empty relative path: ${location.path}`, {
123
+ nextAction: 'Use a forward-slash relative path such as docs/architecture.md.',
124
+ });
125
+ }
126
+ let containmentRoot = context.root;
127
+ let repository = null;
128
+ let access = 'write';
129
+ if (location.kind === 'workspace') {
130
+ if (managedStatePrefixes.some((prefix) => normalizedPath === prefix || normalizedPath.startsWith(`${prefix}/`))) {
131
+ return failed(diagnosticCodes.document_path_unsafe, `Document path is Pappra-managed state: ${normalizedPath}`, {
132
+ nextAction: 'Keep knowledge documents outside local/generated state.',
133
+ });
134
+ }
135
+ }
136
+ else {
137
+ repository =
138
+ context.repositories.find((entry) => entry.id === location.sourceId) ??
139
+ null;
140
+ if (repository === null) {
141
+ return failed(diagnosticCodes.document_source_invalid, `Unknown explicit repository source: ${location.sourceId}`, {
142
+ nextAction: 'Declare the repository and its local mapping before registering its documents.',
143
+ });
144
+ }
145
+ containmentRoot = repository.root;
146
+ access = repository.access;
147
+ }
148
+ const absolutePath = location.kind === 'workspace'
149
+ ? resolvePortableRelativePath(context.root, normalizedPath)
150
+ : resolvePortableRelativePath(containmentRoot, normalizedPath);
151
+ if (!isPathInside(containmentRoot, absolutePath)) {
152
+ return failed(diagnosticCodes.document_path_unsafe, `Path is outside the allowed knowledge root: ${absolutePath}`, {
153
+ nextAction: 'Use a path inside the declared workspace/repository root.',
154
+ });
155
+ }
156
+ const canonicalTarget = await canonicalTargetPath(absolutePath);
157
+ if (canonicalTarget !== null) {
158
+ const targetRepository = containingRepository(context, canonicalTarget);
159
+ if (targetRepository !== null) {
160
+ repository = targetRepository;
161
+ access = targetRepository.access;
162
+ }
163
+ }
164
+ if (options.forWrite === true && access !== 'write') {
165
+ return failed(diagnosticCodes.document_access_denied, `Canonical document target is read-only: ${absolutePath}`, {
166
+ nextAction: 'Use a workspace/repository whose canonical checkout permits writes.',
167
+ });
168
+ }
169
+ let boundary;
170
+ try {
171
+ boundary = await nestedWorkspaceBoundary(containmentRoot, dirname(absolutePath));
172
+ }
173
+ catch (error) {
174
+ return failed(diagnosticCodes.fileUnreadable, `Cannot inspect nested workspace boundaries: ${error instanceof Error ? error.message : String(error)}`, { nextAction: 'Check permissions; no document path was written.' });
175
+ }
176
+ if (boundary !== null) {
177
+ return failed(diagnosticCodes.document_nested_workspace, `Document is inside nested workspace ${boundary}: ${absolutePath}`, { nextAction: 'Register the document in the nested workspace instead.' });
178
+ }
179
+ const boundaryCheck = await ensureKnowledgePathBoundary(containmentRoot, absolutePath);
180
+ if (!boundaryCheck.ok)
181
+ return boundaryCheck;
182
+ if (!options.forCreate) {
183
+ let observation;
184
+ try {
185
+ observation = await observePath(absolutePath);
186
+ }
187
+ catch (error) {
188
+ return failed(diagnosticCodes.fileUnreadable, `Cannot inspect document ${absolutePath}: ${error instanceof Error ? error.message : String(error)}`, { nextAction: 'Check permissions; the file was not read or written.' });
189
+ }
190
+ if (observation.kind === 'missing') {
191
+ return failed(diagnosticCodes.document_not_found, `Document file does not exist: ${absolutePath}`, { nextAction: 'Create or register an existing regular file.' });
192
+ }
193
+ if (observation.kind !== 'file') {
194
+ return failed(diagnosticCodes.document_path_unsafe, `Document path is not a regular file: ${absolutePath}`, {
195
+ nextAction: 'Symlinks, directories, FIFOs, and devices are not document files.',
196
+ });
197
+ }
198
+ const real = await resolveRealpath(absolutePath);
199
+ if (real === null || !isPathInside(containmentRoot, real)) {
200
+ return failed(diagnosticCodes.document_path_unsafe, `Document path resolves outside its root: ${absolutePath}`, {
201
+ nextAction: 'Keep documents as regular files inside the allowed root.',
202
+ });
203
+ }
204
+ if (options.forWrite === true && access !== 'write') {
205
+ return failed(diagnosticCodes.document_access_denied, `Document is read-only: ${absolutePath}`, {
206
+ nextAction: 'Write only through a writable workspace or repository.',
207
+ });
208
+ }
209
+ }
210
+ else {
211
+ let observation;
212
+ try {
213
+ observation = await observePath(absolutePath);
214
+ }
215
+ catch (error) {
216
+ return failed(diagnosticCodes.fileUnreadable, `Cannot inspect new document target ${absolutePath}: ${error instanceof Error ? error.message : String(error)}`, { nextAction: 'Check permissions; no document was written.' });
217
+ }
218
+ if (observation.kind !== 'missing') {
219
+ return failed(diagnosticCodes.document_already_registered, `New document target already exists: ${absolutePath}`, {
220
+ nextAction: 'Use proposeWrite to update the existing file; do not overwrite it as a new document.',
221
+ });
222
+ }
223
+ }
224
+ return succeeded({
225
+ location,
226
+ absolutePath,
227
+ containmentRoot,
228
+ canonicalPath: canonicalTarget ?? absolutePath,
229
+ repository,
230
+ access,
231
+ normalizedPath,
232
+ });
233
+ }
234
+ export async function readKnowledgeFile(path, maxBytes) {
235
+ return readFileBounded(path, maxBytes);
236
+ }
@@ -0,0 +1,36 @@
1
+ import type { Diagnostic } from '../schemas/diagnostics.js';
2
+ import type { DocumentRecord, DocumentSource, DocumentVisibility } from '../schemas/document.js';
3
+ import type { KnowledgeContext } from './context.js';
4
+ import type { KnowledgeCatalogs } from './catalog.js';
5
+ import { type ResolvedKnowledgeLocation } from './paths.js';
6
+ export type Freshness = 'current' | 'needs-review' | 'unknown';
7
+ export interface FreshnessReason {
8
+ readonly code: string;
9
+ readonly message: string;
10
+ }
11
+ export interface SourceCheck {
12
+ readonly kind: DocumentSource['kind'];
13
+ readonly ref: string;
14
+ readonly status: 'current' | 'needs-review' | 'unknown';
15
+ readonly message: string;
16
+ }
17
+ export interface FreshnessReport {
18
+ readonly freshness: Freshness;
19
+ readonly reasons: readonly FreshnessReason[];
20
+ readonly sourceChecks: readonly SourceCheck[];
21
+ readonly coverage: {
22
+ readonly sources: 'checked';
23
+ readonly pathReferences: 'unchecked';
24
+ readonly repositoryContent: 'not-scanned';
25
+ };
26
+ }
27
+ export declare function findRecord(catalogs: KnowledgeCatalogs, documentId: string): DocumentRecord | null;
28
+ export declare function findCanonicalAlias(context: KnowledgeContext, catalogs: KnowledgeCatalogs, target: ResolvedKnowledgeLocation, ignoreDocumentId?: string | null): Promise<DocumentRecord | null>;
29
+ export declare function findRecordByLocation(catalogs: KnowledgeCatalogs, key: string): DocumentRecord | null;
30
+ export declare function inspectFreshness(context: KnowledgeContext, catalogs: KnowledgeCatalogs, record: DocumentRecord, maxBytes: number): Promise<FreshnessReport>;
31
+ export interface RegisteredReference {
32
+ readonly documentId: string;
33
+ readonly relation: 'source' | 'superseded-by';
34
+ }
35
+ export declare function findRegisteredReferences(catalogs: KnowledgeCatalogs, targetDocumentId: string): readonly RegisteredReference[];
36
+ export declare function validateSources(context: KnowledgeContext, catalogs: KnowledgeCatalogs, documentId: string, visibility: DocumentVisibility, sources: readonly DocumentSource[]): readonly Diagnostic[];
@@ -0,0 +1,267 @@
1
+ import { createDiagnostic } from '../schemas/diagnostics.js';
2
+ import { diagnosticCodes } from '../schemas/errors.js';
3
+ import { isPathInside, resolvePortableRelativePath, samePath, } from '../schemas/paths.js';
4
+ import { observePath } from '../infra/fs.js';
5
+ import { readKnowledgeFile, resolveKnowledgeLocation, } from './paths.js';
6
+ export function findRecord(catalogs, documentId) {
7
+ if (catalogs.idConflicts.has(documentId))
8
+ return null;
9
+ return catalogs.records.get(documentId) ?? null;
10
+ }
11
+ export async function findCanonicalAlias(context, catalogs, target, ignoreDocumentId = null) {
12
+ for (const record of catalogs.records.values()) {
13
+ if (record.documentId === ignoreDocumentId)
14
+ continue;
15
+ const resolved = await resolveKnowledgeLocation(context, record.location);
16
+ if (resolved.ok &&
17
+ samePath(resolved.value.canonicalPath, target.canonicalPath)) {
18
+ return record;
19
+ }
20
+ }
21
+ return null;
22
+ }
23
+ export function findRecordByLocation(catalogs, key) {
24
+ if (catalogs.locationConflicts.has(key))
25
+ return null;
26
+ for (const record of catalogs.records.values()) {
27
+ const recordKey = record.location.kind === 'workspace'
28
+ ? `workspace:${record.location.path}`
29
+ : `source:${record.location.sourceId}:${record.location.path}`;
30
+ if (recordKey === key)
31
+ return record;
32
+ }
33
+ return null;
34
+ }
35
+ function sourceRef(source) {
36
+ return source.kind === 'document'
37
+ ? `document:${source.documentId}`
38
+ : `code:${source.repositoryId}:${source.path}@${source.version}`;
39
+ }
40
+ async function checkDocumentSource(context, catalogs, source, maxBytes) {
41
+ const target = findRecord(catalogs, source.documentId);
42
+ if (target === null) {
43
+ return {
44
+ kind: 'document',
45
+ ref: sourceRef(source),
46
+ status: 'needs-review',
47
+ message: `Referenced document ${source.documentId} is missing or ambiguous.`,
48
+ };
49
+ }
50
+ const resolved = await resolveKnowledgeLocation(context, target.location);
51
+ if (!resolved.ok) {
52
+ return {
53
+ kind: 'document',
54
+ ref: sourceRef(source),
55
+ status: 'needs-review',
56
+ message: `Referenced document cannot be read: ${resolved.message}`,
57
+ };
58
+ }
59
+ const file = await readKnowledgeFile(resolved.value.absolutePath, maxBytes);
60
+ if (!file.ok) {
61
+ return {
62
+ kind: 'document',
63
+ ref: sourceRef(source),
64
+ status: 'needs-review',
65
+ message: `Referenced document cannot be read: ${file.message}`,
66
+ };
67
+ }
68
+ return file.value.digest === source.digest
69
+ ? {
70
+ kind: 'document',
71
+ ref: sourceRef(source),
72
+ status: 'current',
73
+ message: 'Referenced document digest matches.',
74
+ }
75
+ : {
76
+ kind: 'document',
77
+ ref: sourceRef(source),
78
+ status: 'needs-review',
79
+ message: 'Referenced document digest changed since this source was recorded.',
80
+ };
81
+ }
82
+ async function checkCodeSource(context, source) {
83
+ const repository = context.repositories.find((entry) => entry.id === source.repositoryId);
84
+ if (repository === undefined) {
85
+ return {
86
+ kind: 'code',
87
+ ref: sourceRef(source),
88
+ status: 'needs-review',
89
+ message: `Repository ${source.repositoryId} is not available.`,
90
+ };
91
+ }
92
+ const path = resolvePortableRelativePath(repository.root, source.path);
93
+ if (!isPathInside(repository.root, path)) {
94
+ return {
95
+ kind: 'code',
96
+ ref: sourceRef(source),
97
+ status: 'needs-review',
98
+ message: `Code source path escapes repository ${source.repositoryId}.`,
99
+ };
100
+ }
101
+ const observation = await observePath(path);
102
+ if (observation.kind !== 'file') {
103
+ return {
104
+ kind: 'code',
105
+ ref: sourceRef(source),
106
+ status: 'needs-review',
107
+ message: `Code source is not a regular file: ${source.path}`,
108
+ };
109
+ }
110
+ if (repository.git.head === null) {
111
+ return {
112
+ kind: 'code',
113
+ ref: sourceRef(source),
114
+ status: 'unknown',
115
+ message: `Repository ${source.repositoryId} has no observable HEAD.`,
116
+ };
117
+ }
118
+ return repository.git.head === source.version
119
+ ? {
120
+ kind: 'code',
121
+ ref: sourceRef(source),
122
+ status: 'current',
123
+ message: 'Repository HEAD matches the recorded code version.',
124
+ }
125
+ : {
126
+ kind: 'code',
127
+ ref: sourceRef(source),
128
+ status: 'needs-review',
129
+ message: `Repository HEAD ${repository.git.head} differs from recorded version ${source.version}.`,
130
+ };
131
+ }
132
+ export async function inspectFreshness(context, catalogs, record, maxBytes) {
133
+ const reasons = [];
134
+ const resolved = await resolveKnowledgeLocation(context, record.location);
135
+ let fileCheck = 'unknown';
136
+ if (!resolved.ok) {
137
+ reasons.push({ code: resolved.code, message: resolved.message });
138
+ }
139
+ else {
140
+ const file = await readKnowledgeFile(resolved.value.absolutePath, maxBytes);
141
+ if (!file.ok) {
142
+ reasons.push({ code: file.code, message: file.message });
143
+ }
144
+ else if (file.value.digest !== record.digest) {
145
+ fileCheck = 'needs-review';
146
+ reasons.push({
147
+ code: diagnosticCodes.document_file_changed,
148
+ message: 'Document bytes differ from the registered digest.',
149
+ });
150
+ }
151
+ else {
152
+ fileCheck = 'current';
153
+ }
154
+ }
155
+ const sourceChecks = [];
156
+ for (const source of record.sources) {
157
+ const check = source.kind === 'document'
158
+ ? await checkDocumentSource(context, catalogs, source, maxBytes)
159
+ : await checkCodeSource(context, source);
160
+ sourceChecks.push(check);
161
+ if (check.status === 'needs-review') {
162
+ reasons.push({
163
+ code: diagnosticCodes.document_source_invalid,
164
+ message: check.message,
165
+ });
166
+ }
167
+ }
168
+ const hasUnknown = sourceChecks.some((entry) => entry.status === 'unknown');
169
+ const freshness = reasons.length > 0 || fileCheck === 'needs-review'
170
+ ? 'needs-review'
171
+ : hasUnknown
172
+ ? 'unknown'
173
+ : 'current';
174
+ return {
175
+ freshness,
176
+ reasons,
177
+ sourceChecks,
178
+ coverage: {
179
+ sources: 'checked',
180
+ pathReferences: 'unchecked',
181
+ repositoryContent: 'not-scanned',
182
+ },
183
+ };
184
+ }
185
+ export function findRegisteredReferences(catalogs, targetDocumentId) {
186
+ const references = [];
187
+ for (const record of catalogs.records.values()) {
188
+ if (record.documentId === targetDocumentId)
189
+ continue;
190
+ for (const source of record.sources) {
191
+ if (source.kind === 'document' &&
192
+ source.documentId === targetDocumentId) {
193
+ references.push({
194
+ documentId: record.documentId,
195
+ relation: 'source',
196
+ });
197
+ }
198
+ }
199
+ if (record.supersededBy === targetDocumentId) {
200
+ references.push({
201
+ documentId: record.documentId,
202
+ relation: 'superseded-by',
203
+ });
204
+ }
205
+ }
206
+ return references;
207
+ }
208
+ export function validateSources(context, catalogs, documentId, visibility, sources) {
209
+ const diagnostics = [];
210
+ for (const source of sources) {
211
+ if (source.kind === 'document') {
212
+ if (source.documentId === documentId) {
213
+ diagnostics.push(createDiagnostic({
214
+ code: diagnosticCodes.document_source_invalid,
215
+ severity: 'error',
216
+ message: `Document ${documentId} cannot cite itself as a source.`,
217
+ source: context.configPath,
218
+ hint: 'Use an existing other document or a code version as the source.',
219
+ }));
220
+ }
221
+ else {
222
+ const target = findRecord(catalogs, source.documentId);
223
+ if (target === null) {
224
+ diagnostics.push(createDiagnostic({
225
+ code: diagnosticCodes.document_source_invalid,
226
+ severity: 'error',
227
+ message: `Document source ${source.documentId} does not exist in the catalog.`,
228
+ source: context.configPath,
229
+ hint: 'Register the source document first; Pappra does not invent source history.',
230
+ }));
231
+ }
232
+ else if (visibility === 'shareable' &&
233
+ target.visibility === 'private') {
234
+ diagnostics.push(createDiagnostic({
235
+ code: diagnosticCodes.document_visibility_invalid,
236
+ severity: 'error',
237
+ message: `A shareable document cannot cite private source ${source.documentId}.`,
238
+ source: context.configPath,
239
+ hint: 'Keep private references out of the shared catalog or share the source explicitly.',
240
+ }));
241
+ }
242
+ }
243
+ }
244
+ else {
245
+ const repository = context.repositories.find((entry) => entry.id === source.repositoryId);
246
+ if (repository === undefined) {
247
+ diagnostics.push(createDiagnostic({
248
+ code: diagnosticCodes.document_source_invalid,
249
+ severity: 'error',
250
+ message: `Code source repository ${source.repositoryId} is not declared or mapped.`,
251
+ source: context.configPath,
252
+ hint: 'Declare the repository and map external checkouts before citing code.',
253
+ }));
254
+ }
255
+ else if (!isPathInside(repository.root, resolvePortableRelativePath(repository.root, source.path))) {
256
+ diagnostics.push(createDiagnostic({
257
+ code: diagnosticCodes.document_source_invalid,
258
+ severity: 'error',
259
+ message: `Code source path escapes repository ${source.repositoryId}: ${source.path}`,
260
+ source: context.configPath,
261
+ hint: 'Use a relative path inside the repository checkout.',
262
+ }));
263
+ }
264
+ }
265
+ }
266
+ return diagnostics;
267
+ }
@@ -0,0 +1,21 @@
1
+ import { type FreshnessReport } from './refs.js';
2
+ import type { ApplyWriteInput, ApplyWriteResult, ArchiveDocumentResult, DeleteExplicitInput, DeleteExplicitResult, DocumentCheckResult, ListDocumentsOptions, ListDocumentsResult, PreviewDeleteInput, PreviewDeleteResult, ProposeWriteInput, ProposeWriteResult, ReadDocumentResult, RegisterDocumentInput, RegisterDocumentResult, ShareDocumentResult, SupersedeDocumentInput, SupersedeDocumentResult, KnowledgeServiceOptions } from './types.js';
3
+ export declare class KnowledgeService {
4
+ #private;
5
+ readonly root: string;
6
+ constructor(root: string, options?: KnowledgeServiceOptions);
7
+ register(input: RegisterDocumentInput): Promise<RegisterDocumentResult>;
8
+ read(documentId: string): Promise<ReadDocumentResult>;
9
+ list(options?: ListDocumentsOptions): Promise<ListDocumentsResult>;
10
+ check(options?: {
11
+ readonly includePrivate?: boolean;
12
+ }): Promise<DocumentCheckResult>;
13
+ proposeWrite(input: ProposeWriteInput): Promise<ProposeWriteResult>;
14
+ applyWrite(input: ApplyWriteInput): Promise<ApplyWriteResult>;
15
+ archive(documentId: string): Promise<ArchiveDocumentResult>;
16
+ supersede(input: SupersedeDocumentInput): Promise<SupersedeDocumentResult>;
17
+ share(documentId: string): Promise<ShareDocumentResult>;
18
+ previewDelete(input: PreviewDeleteInput): Promise<PreviewDeleteResult>;
19
+ deleteExplicit(input: DeleteExplicitInput): Promise<DeleteExplicitResult>;
20
+ }
21
+ export type { ApplyWriteResult, ArchiveDocumentResult, DeleteExplicitResult, DocumentCheckResult, ListDocumentsResult, PreviewDeleteResult, ProposeWriteResult, ReadDocumentResult, RegisterDocumentResult, ShareDocumentResult, SupersedeDocumentResult, FreshnessReport, };