@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.
- package/LICENSE +21 -0
- package/README.md +80 -2
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/assets/integrations/skills/pappra/SKILL.md +68 -0
- package/assets/integrations/skills/pappra/references/hosts.md +30 -0
- package/assets/integrations/skills/pappra/references/lifecycle.md +37 -0
- package/assets/integrations/skills/pappra/references/operations.md +14 -0
- package/assets/manifest.json +4 -0
- package/dist/app/collaboration.d.ts +170 -0
- package/dist/app/collaboration.js +1650 -0
- package/dist/app/doctor.d.ts +9 -0
- package/dist/app/doctor.js +1030 -0
- package/dist/app/init.d.ts +143 -0
- package/dist/app/init.js +1111 -0
- package/dist/cli/commands/code.d.ts +16 -0
- package/dist/cli/commands/code.js +127 -0
- package/dist/cli/commands/collaboration.d.ts +13 -0
- package/dist/cli/commands/collaboration.js +186 -0
- package/dist/cli/commands/continuity.d.ts +29 -0
- package/dist/cli/commands/continuity.js +133 -0
- package/dist/cli/commands/docs.d.ts +11 -0
- package/dist/cli/commands/docs.js +133 -0
- package/dist/cli/commands/doctor.d.ts +14 -0
- package/dist/cli/commands/doctor.js +57 -0
- package/dist/cli/commands/host.d.ts +13 -0
- package/dist/cli/commands/host.js +125 -0
- package/dist/cli/commands/init.d.ts +51 -0
- package/dist/cli/commands/init.js +668 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +10 -0
- package/dist/cli/output.d.ts +4 -0
- package/dist/cli/output.js +1 -0
- package/dist/cli/run.d.ts +2 -0
- package/dist/cli/run.js +597 -0
- package/dist/code/budget.d.ts +14 -0
- package/dist/code/budget.js +39 -0
- package/dist/code/exec.d.ts +23 -0
- package/dist/code/exec.js +133 -0
- package/dist/code/identity.d.ts +13 -0
- package/dist/code/identity.js +214 -0
- package/dist/code/index.d.ts +6 -0
- package/dist/code/index.js +3 -0
- package/dist/code/providers/codegraph.d.ts +59 -0
- package/dist/code/providers/codegraph.js +567 -0
- package/dist/code/registration.d.ts +9 -0
- package/dist/code/registration.js +293 -0
- package/dist/code/service.d.ts +53 -0
- package/dist/code/service.js +633 -0
- package/dist/code/text.d.ts +3 -0
- package/dist/code/text.js +202 -0
- package/dist/collab/admission.d.ts +450 -0
- package/dist/collab/admission.js +797 -0
- package/dist/collab/canonical.d.ts +26 -0
- package/dist/collab/canonical.js +136 -0
- package/dist/collab/contracts.d.ts +73 -0
- package/dist/collab/contracts.js +76 -0
- package/dist/collab/envelope.d.ts +52 -0
- package/dist/collab/envelope.js +241 -0
- package/dist/collab/fingerprint.d.ts +7 -0
- package/dist/collab/fingerprint.js +58 -0
- package/dist/collab/handoff.d.ts +350 -0
- package/dist/collab/handoff.js +513 -0
- package/dist/collab/index.d.ts +9 -0
- package/dist/collab/index.js +8 -0
- package/dist/collab/lifecycle.d.ts +449 -0
- package/dist/collab/lifecycle.js +534 -0
- package/dist/collab/messages.d.ts +148 -0
- package/dist/collab/messages.js +583 -0
- package/dist/collab/notify.d.ts +47 -0
- package/dist/collab/notify.js +140 -0
- package/dist/collab/preflight.d.ts +31 -0
- package/dist/collab/preflight.js +271 -0
- package/dist/collab/resources.d.ts +119 -0
- package/dist/collab/resources.js +451 -0
- package/dist/collab/store.d.ts +143 -0
- package/dist/collab/store.js +606 -0
- package/dist/collab/types.d.ts +354 -0
- package/dist/collab/types.js +1 -0
- package/dist/collab/validation.d.ts +10 -0
- package/dist/collab/validation.js +303 -0
- package/dist/continuity/context.d.ts +24 -0
- package/dist/continuity/context.js +179 -0
- package/dist/continuity/index.d.ts +3 -0
- package/dist/continuity/index.js +1 -0
- package/dist/continuity/paths.d.ts +37 -0
- package/dist/continuity/paths.js +258 -0
- package/dist/continuity/service.d.ts +59 -0
- package/dist/continuity/service.js +1258 -0
- package/dist/hosts/claude/native.d.ts +79 -0
- package/dist/hosts/claude/native.js +853 -0
- package/dist/hosts/codex/native.d.ts +345 -0
- package/dist/hosts/codex/native.js +1528 -0
- package/dist/hosts/codex/protocol.d.ts +51 -0
- package/dist/hosts/codex/protocol.js +42 -0
- package/dist/hosts/detect.d.ts +28 -0
- package/dist/hosts/detect.js +73 -0
- package/dist/hosts/index.d.ts +10 -0
- package/dist/hosts/index.js +10 -0
- package/dist/hosts/integration/bundle.d.ts +16 -0
- package/dist/hosts/integration/bundle.js +81 -0
- package/dist/hosts/integration/hook-entry.d.ts +1 -0
- package/dist/hosts/integration/hook-entry.js +68 -0
- package/dist/hosts/integration/hook.d.ts +32 -0
- package/dist/hosts/integration/hook.js +781 -0
- package/dist/hosts/integration/pi-extension-runtime.d.ts +18 -0
- package/dist/hosts/integration/pi-extension-runtime.js +89 -0
- package/dist/hosts/integration/pi-template.d.ts +3 -0
- package/dist/hosts/integration/pi-template.js +5 -0
- package/dist/hosts/integration/service.d.ts +41 -0
- package/dist/hosts/integration/service.js +1381 -0
- package/dist/hosts/integration/support.d.ts +36 -0
- package/dist/hosts/integration/support.js +41 -0
- package/dist/hosts/integration/templates.d.ts +22 -0
- package/dist/hosts/integration/templates.js +70 -0
- package/dist/hosts/pi/protocol.d.ts +22 -0
- package/dist/hosts/pi/protocol.js +28 -0
- package/dist/hosts/pi/query-result.d.ts +22 -0
- package/dist/hosts/pi/query-result.js +710 -0
- package/dist/hosts/pi/rpc.d.ts +937 -0
- package/dist/hosts/pi/rpc.js +1621 -0
- package/dist/hosts/pilot.d.ts +485 -0
- package/dist/hosts/pilot.js +593 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/infra/fs.d.ts +27 -0
- package/dist/infra/fs.js +130 -0
- package/dist/infra/git.d.ts +60 -0
- package/dist/infra/git.js +358 -0
- package/dist/infra/owned-process-group.d.ts +173 -0
- package/dist/infra/owned-process-group.js +571 -0
- package/dist/install/apply.d.ts +65 -0
- package/dist/install/apply.js +663 -0
- package/dist/install/index.d.ts +5 -0
- package/dist/install/index.js +5 -0
- package/dist/install/ownership.d.ts +85 -0
- package/dist/install/ownership.js +430 -0
- package/dist/install/plan.d.ts +39 -0
- package/dist/install/plan.js +97 -0
- package/dist/install/rollback.d.ts +18 -0
- package/dist/install/rollback.js +544 -0
- package/dist/install/runtime.d.ts +76 -0
- package/dist/install/runtime.js +1145 -0
- package/dist/knowledge/catalog.d.ts +47 -0
- package/dist/knowledge/catalog.js +308 -0
- package/dist/knowledge/context.d.ts +28 -0
- package/dist/knowledge/context.js +61 -0
- package/dist/knowledge/index.d.ts +3 -0
- package/dist/knowledge/index.js +1 -0
- package/dist/knowledge/journal.d.ts +16 -0
- package/dist/knowledge/journal.js +109 -0
- package/dist/knowledge/lifecycle.d.ts +8 -0
- package/dist/knowledge/lifecycle.js +1001 -0
- package/dist/knowledge/paths.d.ts +37 -0
- package/dist/knowledge/paths.js +236 -0
- package/dist/knowledge/refs.d.ts +36 -0
- package/dist/knowledge/refs.js +267 -0
- package/dist/knowledge/service.d.ts +21 -0
- package/dist/knowledge/service.js +393 -0
- package/dist/knowledge/types.d.ts +171 -0
- package/dist/knowledge/types.js +1 -0
- package/dist/knowledge/write.d.ts +8 -0
- package/dist/knowledge/write.js +1223 -0
- package/dist/package-info.d.ts +6 -0
- package/dist/package-info.js +19 -0
- package/dist/schemas/code.d.ts +377 -0
- package/dist/schemas/code.js +145 -0
- package/dist/schemas/collaboration.d.ts +115 -0
- package/dist/schemas/collaboration.js +106 -0
- package/dist/schemas/continuity.d.ts +315 -0
- package/dist/schemas/continuity.js +199 -0
- package/dist/schemas/diagnostics.d.ts +28 -0
- package/dist/schemas/diagnostics.js +27 -0
- package/dist/schemas/doctor.d.ts +157 -0
- package/dist/schemas/doctor.js +75 -0
- package/dist/schemas/document.d.ts +644 -0
- package/dist/schemas/document.js +296 -0
- package/dist/schemas/errors.d.ts +177 -0
- package/dist/schemas/errors.js +176 -0
- package/dist/schemas/evidence.d.ts +24 -0
- package/dist/schemas/evidence.js +16 -0
- package/dist/schemas/host.d.ts +436 -0
- package/dist/schemas/host.js +201 -0
- package/dist/schemas/index.d.ts +15 -0
- package/dist/schemas/index.js +15 -0
- package/dist/schemas/init.d.ts +121 -0
- package/dist/schemas/init.js +94 -0
- package/dist/schemas/install.d.ts +213 -0
- package/dist/schemas/install.js +139 -0
- package/dist/schemas/machine.d.ts +43 -0
- package/dist/schemas/machine.js +96 -0
- package/dist/schemas/outcome.d.ts +23 -0
- package/dist/schemas/outcome.js +17 -0
- package/dist/schemas/paths.d.ts +19 -0
- package/dist/schemas/paths.js +63 -0
- package/dist/schemas/workspace.d.ts +120 -0
- package/dist/schemas/workspace.js +201 -0
- package/dist/workspace/config-file.d.ts +36 -0
- package/dist/workspace/config-file.js +237 -0
- package/dist/workspace/config.d.ts +28 -0
- package/dist/workspace/config.js +79 -0
- package/dist/workspace/discover.d.ts +34 -0
- package/dist/workspace/discover.js +195 -0
- package/dist/workspace/documents.d.ts +21 -0
- package/dist/workspace/documents.js +395 -0
- package/dist/workspace/ignore.d.ts +10 -0
- package/dist/workspace/ignore.js +48 -0
- package/dist/workspace/index.d.ts +7 -0
- package/dist/workspace/index.js +3 -0
- package/dist/workspace/repositories.d.ts +24 -0
- package/dist/workspace/repositories.js +411 -0
- package/dist/workspace/resolve.d.ts +24 -0
- package/dist/workspace/resolve.js +268 -0
- package/dist/workspace/types.d.ts +71 -0
- package/dist/workspace/types.js +1 -0
- package/dist/workspace/validation.d.ts +18 -0
- package/dist/workspace/validation.js +197 -0
- package/package.json +105 -4
- package/skills/pappra/SKILL.md +68 -0
- package/skills/pappra/references/hosts.md +30 -0
- package/skills/pappra/references/lifecycle.md +37 -0
- package/skills/pappra/references/operations.md +14 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { join, resolve } from 'node:path';
|
|
2
|
+
import { createDiagnostic, hasErrors, pointer, } from '../schemas/diagnostics.js';
|
|
3
|
+
import { diagnosticCodes } from '../schemas/errors.js';
|
|
4
|
+
import { errorMessage } from '../schemas/outcome.js';
|
|
5
|
+
import { isPathInside } from '../schemas/paths.js';
|
|
6
|
+
import { DEFAULT_CONFIG_MAX_BYTES, observePath, readTextFileBounded, resolveRealpath, } from '../infra/fs.js';
|
|
7
|
+
function issuePointer(path) {
|
|
8
|
+
return pointer(...path.map((segment) => String(segment)));
|
|
9
|
+
}
|
|
10
|
+
function issueDiagnostics(issues, source) {
|
|
11
|
+
const diagnostics = [];
|
|
12
|
+
for (const issue of issues) {
|
|
13
|
+
if (issue.code === 'unrecognized_keys') {
|
|
14
|
+
const base = issuePointer(issue.path);
|
|
15
|
+
for (const key of issue.keys) {
|
|
16
|
+
diagnostics.push(createDiagnostic({
|
|
17
|
+
code: diagnosticCodes.configUnknownKey,
|
|
18
|
+
severity: 'error',
|
|
19
|
+
message: `Unknown key "${key}".`,
|
|
20
|
+
source,
|
|
21
|
+
pointer: `${base}/${key.replaceAll('~', '~0').replaceAll('/', '~1')}`,
|
|
22
|
+
hint: 'Remove the key or declare intentional extension data under the existing "extensions" field. A new top-level field requires a schema version change.',
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
diagnostics.push(createDiagnostic({
|
|
28
|
+
code: diagnosticCodes.configSchemaInvalid,
|
|
29
|
+
severity: 'error',
|
|
30
|
+
message: issue.message,
|
|
31
|
+
source,
|
|
32
|
+
pointer: issue.path.length === 0 ? '' : issuePointer(issue.path),
|
|
33
|
+
hint: 'Correct the value; Pappra does not coerce or silently ignore configuration.',
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
return diagnostics;
|
|
37
|
+
}
|
|
38
|
+
function isRecord(value) {
|
|
39
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Reads, decodes, validates, and semantically checks one strict JSON
|
|
43
|
+
* configuration file. Missing configuration is reported instead of defaulted.
|
|
44
|
+
*/
|
|
45
|
+
export async function inspectConfigFile(options) {
|
|
46
|
+
const source = options.path;
|
|
47
|
+
let observation;
|
|
48
|
+
try {
|
|
49
|
+
observation = await observePath(options.path);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return {
|
|
53
|
+
path: options.path,
|
|
54
|
+
exists: true,
|
|
55
|
+
digest: null,
|
|
56
|
+
size: null,
|
|
57
|
+
value: null,
|
|
58
|
+
diagnostics: [
|
|
59
|
+
createDiagnostic({
|
|
60
|
+
code: diagnosticCodes.fileUnreadable,
|
|
61
|
+
severity: 'error',
|
|
62
|
+
message: `Cannot inspect ${options.path}: ${errorMessage(error)}`,
|
|
63
|
+
source,
|
|
64
|
+
hint: 'Check file permissions and the path.',
|
|
65
|
+
}),
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (observation.kind === 'missing') {
|
|
70
|
+
return {
|
|
71
|
+
path: options.path,
|
|
72
|
+
exists: false,
|
|
73
|
+
digest: null,
|
|
74
|
+
size: null,
|
|
75
|
+
value: null,
|
|
76
|
+
diagnostics: [
|
|
77
|
+
createDiagnostic({
|
|
78
|
+
code: diagnosticCodes.configMissing,
|
|
79
|
+
severity: options.missing.severity,
|
|
80
|
+
message: options.missing.message,
|
|
81
|
+
source,
|
|
82
|
+
hint: options.missing.hint,
|
|
83
|
+
}),
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const realPath = await resolveRealpath(options.path);
|
|
88
|
+
if (realPath !== null &&
|
|
89
|
+
options.containmentRoot !== undefined &&
|
|
90
|
+
!isPathInside(options.containmentRoot, realPath)) {
|
|
91
|
+
return {
|
|
92
|
+
path: options.path,
|
|
93
|
+
exists: true,
|
|
94
|
+
digest: null,
|
|
95
|
+
size: null,
|
|
96
|
+
value: null,
|
|
97
|
+
diagnostics: [
|
|
98
|
+
createDiagnostic({
|
|
99
|
+
code: diagnosticCodes.configSymlinkEscape,
|
|
100
|
+
severity: 'error',
|
|
101
|
+
message: `Configuration file resolves outside the workspace: ${realPath}`,
|
|
102
|
+
source,
|
|
103
|
+
hint: 'Keep the configuration inside .pappra/; external symlinks are not followed.',
|
|
104
|
+
}),
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const read = await readTextFileBounded(options.path, options.maxBytes ?? DEFAULT_CONFIG_MAX_BYTES);
|
|
109
|
+
if (!read.ok) {
|
|
110
|
+
return {
|
|
111
|
+
path: options.path,
|
|
112
|
+
exists: true,
|
|
113
|
+
digest: null,
|
|
114
|
+
size: null,
|
|
115
|
+
value: null,
|
|
116
|
+
diagnostics: [
|
|
117
|
+
createDiagnostic({
|
|
118
|
+
code: read.code,
|
|
119
|
+
severity: 'error',
|
|
120
|
+
message: read.message,
|
|
121
|
+
source,
|
|
122
|
+
hint: read.nextAction,
|
|
123
|
+
}),
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const { digest, size, text } = read.value;
|
|
128
|
+
if (text.startsWith('\uFEFF')) {
|
|
129
|
+
return {
|
|
130
|
+
path: options.path,
|
|
131
|
+
exists: true,
|
|
132
|
+
digest,
|
|
133
|
+
size,
|
|
134
|
+
value: null,
|
|
135
|
+
diagnostics: [
|
|
136
|
+
createDiagnostic({
|
|
137
|
+
code: diagnosticCodes.configInvalidJson,
|
|
138
|
+
severity: 'error',
|
|
139
|
+
message: 'Configuration starts with a UTF-8 BOM.',
|
|
140
|
+
source,
|
|
141
|
+
hint: 'Remove the BOM; Pappra does not rewrite configuration during inspection.',
|
|
142
|
+
}),
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
let raw;
|
|
147
|
+
try {
|
|
148
|
+
raw = JSON.parse(text);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
return {
|
|
152
|
+
path: options.path,
|
|
153
|
+
exists: true,
|
|
154
|
+
digest,
|
|
155
|
+
size,
|
|
156
|
+
value: null,
|
|
157
|
+
diagnostics: [
|
|
158
|
+
createDiagnostic({
|
|
159
|
+
code: diagnosticCodes.configInvalidJson,
|
|
160
|
+
severity: 'error',
|
|
161
|
+
message: `Invalid JSON: ${errorMessage(error)}`,
|
|
162
|
+
source,
|
|
163
|
+
hint: 'Fix the JSON syntax; the original file was not modified.',
|
|
164
|
+
}),
|
|
165
|
+
],
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (isRecord(raw) &&
|
|
169
|
+
typeof raw['schema'] === 'string' &&
|
|
170
|
+
raw['schema'] !== options.schemaName) {
|
|
171
|
+
return {
|
|
172
|
+
path: options.path,
|
|
173
|
+
exists: true,
|
|
174
|
+
digest,
|
|
175
|
+
size,
|
|
176
|
+
value: null,
|
|
177
|
+
diagnostics: [
|
|
178
|
+
createDiagnostic({
|
|
179
|
+
code: diagnosticCodes.configUnsupportedSchema,
|
|
180
|
+
severity: 'error',
|
|
181
|
+
message: `Unsupported schema "${raw['schema']}"; this version reads only "${options.schemaName}".`,
|
|
182
|
+
source,
|
|
183
|
+
pointer: pointer('schema'),
|
|
184
|
+
hint: 'Pappra will not migrate, overwrite, or partially interpret this file. Keep it and use a compatible Pappra version.',
|
|
185
|
+
}),
|
|
186
|
+
],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const parsed = options.schema.safeParse(raw);
|
|
190
|
+
if (!parsed.success) {
|
|
191
|
+
return {
|
|
192
|
+
path: options.path,
|
|
193
|
+
exists: true,
|
|
194
|
+
digest,
|
|
195
|
+
size,
|
|
196
|
+
value: null,
|
|
197
|
+
diagnostics: issueDiagnostics(parsed.error.issues, source),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const diagnostics = options.semantic?.(parsed.data) ?? [];
|
|
201
|
+
return {
|
|
202
|
+
path: options.path,
|
|
203
|
+
exists: true,
|
|
204
|
+
digest,
|
|
205
|
+
size,
|
|
206
|
+
value: hasErrors(diagnostics) ? null : parsed.data,
|
|
207
|
+
diagnostics,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/** Converts an inspection into a convenience result for callers that fail closed. */
|
|
211
|
+
export function requireConfig(inspection) {
|
|
212
|
+
if (inspection.value !== null) {
|
|
213
|
+
return { ok: true, value: inspection.value };
|
|
214
|
+
}
|
|
215
|
+
const firstError = inspection.diagnostics.find((entry) => entry.severity === 'error') ??
|
|
216
|
+
inspection.diagnostics[0];
|
|
217
|
+
if (firstError === undefined) {
|
|
218
|
+
return {
|
|
219
|
+
ok: false,
|
|
220
|
+
code: diagnosticCodes.configSchemaInvalid,
|
|
221
|
+
message: `Configuration is unavailable: ${inspection.path}`,
|
|
222
|
+
retryable: false,
|
|
223
|
+
nextAction: 'Run pappra doctor after correcting the configuration.',
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
ok: false,
|
|
228
|
+
code: firstError.code,
|
|
229
|
+
message: firstError.message,
|
|
230
|
+
retryable: false,
|
|
231
|
+
nextAction: firstError.hint,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/** Joins a workspace-relative configuration path without normalizing above root. */
|
|
235
|
+
export function workspaceConfigPath(root) {
|
|
236
|
+
return join(resolve(root), '.pappra', 'workspace.json');
|
|
237
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type MachineConfig } from '../schemas/machine.js';
|
|
2
|
+
import type { Outcome } from '../schemas/outcome.js';
|
|
3
|
+
import { type WorkspaceConfig } from '../schemas/workspace.js';
|
|
4
|
+
import { type ConfigFileInspection } from './config-file.js';
|
|
5
|
+
export interface WorkspaceConfigInspection extends ConfigFileInspection<WorkspaceConfig> {
|
|
6
|
+
readonly root: string;
|
|
7
|
+
}
|
|
8
|
+
export interface MachineConfigInspection extends ConfigFileInspection<MachineConfig> {
|
|
9
|
+
readonly root: string;
|
|
10
|
+
}
|
|
11
|
+
export interface InspectWorkspaceConfigOptions {
|
|
12
|
+
/** Missing workspace configuration is fatal by default; discovery may expect it. */
|
|
13
|
+
readonly missing?: 'error' | 'info';
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Inspects `.pappra/workspace.json` without changing it. Invalid or unsupported
|
|
17
|
+
* configuration yields `value: null` and structured diagnostics.
|
|
18
|
+
*/
|
|
19
|
+
export declare function inspectWorkspaceConfig(root: string, options?: InspectWorkspaceConfigOptions): Promise<WorkspaceConfigInspection>;
|
|
20
|
+
/**
|
|
21
|
+
* Inspects `.pappra/local/machine.json`. The local file is allowed to be absent
|
|
22
|
+
* on a fresh clone, but a present invalid or mismatched file is never used.
|
|
23
|
+
*/
|
|
24
|
+
export declare function inspectMachineConfig(root: string): Promise<MachineConfigInspection>;
|
|
25
|
+
/** Fails closed when configuration is missing or invalid. */
|
|
26
|
+
export declare function loadWorkspaceConfig(root: string, options?: InspectWorkspaceConfigOptions): Promise<Outcome<WorkspaceConfig>>;
|
|
27
|
+
/** Fails closed when an external mapping or runtime location is required. */
|
|
28
|
+
export declare function loadMachineConfig(root: string): Promise<Outcome<MachineConfig>>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { join, resolve } from 'node:path';
|
|
2
|
+
import { createDiagnostic, pointer } from '../schemas/diagnostics.js';
|
|
3
|
+
import { diagnosticCodes } from '../schemas/errors.js';
|
|
4
|
+
import { MACHINE_SCHEMA, machineConfigSchema, validateMachineConfigSemantics, } from '../schemas/machine.js';
|
|
5
|
+
import { samePath } from '../schemas/paths.js';
|
|
6
|
+
import { WORKSPACE_SCHEMA, validateWorkspaceConfigSemantics, workspaceConfigSchema, } from '../schemas/workspace.js';
|
|
7
|
+
import { resolveRealpath } from '../infra/fs.js';
|
|
8
|
+
import { inspectConfigFile, requireConfig, } from './config-file.js';
|
|
9
|
+
async function canonicalRoot(root) {
|
|
10
|
+
const absolute = resolve(root);
|
|
11
|
+
return (await resolveRealpath(absolute)) ?? absolute;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Inspects `.pappra/workspace.json` without changing it. Invalid or unsupported
|
|
15
|
+
* configuration yields `value: null` and structured diagnostics.
|
|
16
|
+
*/
|
|
17
|
+
export async function inspectWorkspaceConfig(root, options = {}) {
|
|
18
|
+
const canonical = await canonicalRoot(root);
|
|
19
|
+
const configPath = join(canonical, '.pappra', 'workspace.json');
|
|
20
|
+
const inspection = await inspectConfigFile({
|
|
21
|
+
path: configPath,
|
|
22
|
+
schemaName: WORKSPACE_SCHEMA,
|
|
23
|
+
schema: workspaceConfigSchema,
|
|
24
|
+
containmentRoot: canonical,
|
|
25
|
+
missing: {
|
|
26
|
+
severity: options.missing ?? 'error',
|
|
27
|
+
message: `Workspace configuration not found: ${configPath}`,
|
|
28
|
+
hint: 'Run pappra init in the intended workspace root; Pappra does not guess configuration.',
|
|
29
|
+
},
|
|
30
|
+
semantic: (config) => validateWorkspaceConfigSemantics(config, { configPath }),
|
|
31
|
+
});
|
|
32
|
+
return { root: canonical, ...inspection };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Inspects `.pappra/local/machine.json`. The local file is allowed to be absent
|
|
36
|
+
* on a fresh clone, but a present invalid or mismatched file is never used.
|
|
37
|
+
*/
|
|
38
|
+
export async function inspectMachineConfig(root) {
|
|
39
|
+
const canonical = await canonicalRoot(root);
|
|
40
|
+
const configPath = join(canonical, '.pappra', 'local', 'machine.json');
|
|
41
|
+
const inspection = await inspectConfigFile({
|
|
42
|
+
path: configPath,
|
|
43
|
+
schemaName: MACHINE_SCHEMA,
|
|
44
|
+
schema: machineConfigSchema,
|
|
45
|
+
containmentRoot: canonical,
|
|
46
|
+
missing: {
|
|
47
|
+
severity: 'info',
|
|
48
|
+
message: `Local machine configuration not found: ${configPath}`,
|
|
49
|
+
hint: 'Pappra init can create machine.json on this checkout; shared workspace.json does not contain absolute paths.',
|
|
50
|
+
},
|
|
51
|
+
semantic: (config) => validateMachineConfigSemantics(config, { configPath }),
|
|
52
|
+
});
|
|
53
|
+
const diagnostics = [...inspection.diagnostics];
|
|
54
|
+
let value = inspection.value;
|
|
55
|
+
if (value !== null) {
|
|
56
|
+
const recorded = (await resolveRealpath(value.workspaceRealpath)) ??
|
|
57
|
+
resolve(value.workspaceRealpath);
|
|
58
|
+
if (!samePath(recorded, canonical)) {
|
|
59
|
+
diagnostics.push(createDiagnostic({
|
|
60
|
+
code: diagnosticCodes.machineWorkspaceMismatch,
|
|
61
|
+
severity: 'error',
|
|
62
|
+
message: `machine.json belongs to ${value.workspaceRealpath}, not ${canonical}.`,
|
|
63
|
+
source: configPath,
|
|
64
|
+
pointer: pointer('workspaceRealpath'),
|
|
65
|
+
hint: 'This checkout was moved or copied. Rebind the local mapping explicitly instead of reusing the old identity.',
|
|
66
|
+
}));
|
|
67
|
+
value = null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { root: canonical, ...inspection, value, diagnostics };
|
|
71
|
+
}
|
|
72
|
+
/** Fails closed when configuration is missing or invalid. */
|
|
73
|
+
export async function loadWorkspaceConfig(root, options = {}) {
|
|
74
|
+
return requireConfig(await inspectWorkspaceConfig(root, options));
|
|
75
|
+
}
|
|
76
|
+
/** Fails closed when an external mapping or runtime location is required. */
|
|
77
|
+
export async function loadMachineConfig(root) {
|
|
78
|
+
return requireConfig(await inspectMachineConfig(root));
|
|
79
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Diagnostic } from '../schemas/diagnostics.js';
|
|
2
|
+
import type { MachineConfig } from '../schemas/machine.js';
|
|
3
|
+
import type { WorkspaceConfig } from '../schemas/workspace.js';
|
|
4
|
+
import type { DiscoveredDocument, DiscoveredRepository, DiscoveryOptions, MachineConfigState, UnresolvedRepository, ValidationCandidate, WorkspaceConfigState } from './types.js';
|
|
5
|
+
export interface WorkspaceDiscovery {
|
|
6
|
+
readonly requestedRoot: string;
|
|
7
|
+
readonly root: string;
|
|
8
|
+
readonly rootState: 'directory' | 'unavailable';
|
|
9
|
+
readonly configPath: string;
|
|
10
|
+
readonly configState: WorkspaceConfigState;
|
|
11
|
+
readonly config: WorkspaceConfig | null;
|
|
12
|
+
readonly machinePath: string;
|
|
13
|
+
readonly machineState: MachineConfigState;
|
|
14
|
+
readonly machine: MachineConfig | null;
|
|
15
|
+
readonly repositories: readonly DiscoveredRepository[];
|
|
16
|
+
readonly unresolvedRepositories: readonly UnresolvedRepository[];
|
|
17
|
+
/** Nested workspace roots observed and deliberately not entered. */
|
|
18
|
+
readonly boundaries: readonly string[];
|
|
19
|
+
readonly documents: readonly DiscoveredDocument[];
|
|
20
|
+
readonly validationCandidates: readonly ValidationCandidate[];
|
|
21
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
22
|
+
readonly truncated: boolean;
|
|
23
|
+
readonly observedAt: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Discovers repositories, rules, documents, and validation candidates under an
|
|
27
|
+
* explicit workspace root.
|
|
28
|
+
*
|
|
29
|
+
* This function is read-only: it never executes package scripts, hooks, git
|
|
30
|
+
* fetch, or model calls, and never writes configuration. Missing configuration
|
|
31
|
+
* is reported and discovery continues only with capability-truthful scan data;
|
|
32
|
+
* invalid configuration is never partially applied.
|
|
33
|
+
*/
|
|
34
|
+
export declare function discoverWorkspace(workspaceRoot: string, options?: DiscoveryOptions): Promise<WorkspaceDiscovery>;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { join, resolve } from 'node:path';
|
|
2
|
+
import { createDiagnostic } from '../schemas/diagnostics.js';
|
|
3
|
+
import { diagnosticCodes } from '../schemas/errors.js';
|
|
4
|
+
import { observePath, resolveRealpath } from '../infra/fs.js';
|
|
5
|
+
import { inspectMachineConfig, inspectWorkspaceConfig } from './config.js';
|
|
6
|
+
import { discoverDocuments } from './documents.js';
|
|
7
|
+
import { discoverRepositories } from './repositories.js';
|
|
8
|
+
import { discoverValidationCandidates } from './validation.js';
|
|
9
|
+
function boundedInteger(name, value, fallback, minimum) {
|
|
10
|
+
if (value === undefined)
|
|
11
|
+
return fallback;
|
|
12
|
+
if (!Number.isInteger(value) || value < minimum) {
|
|
13
|
+
throw new RangeError(`${name} must be an integer >= ${minimum}; received ${String(value)}.`);
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
function normalizeOptions(options) {
|
|
18
|
+
return {
|
|
19
|
+
scanDepth: boundedInteger('scanDepth', options.scanDepth, 2, 0),
|
|
20
|
+
maxDirectories: boundedInteger('maxDirectories', options.maxDirectories, 400, 1),
|
|
21
|
+
maxDocuments: boundedInteger('maxDocuments', options.maxDocuments, 300, 1),
|
|
22
|
+
maxEntriesPerDirectory: boundedInteger('maxEntriesPerDirectory', options.maxEntriesPerDirectory, 200, 1),
|
|
23
|
+
gitTimeoutMs: boundedInteger('gitTimeoutMs', options.gitTimeoutMs, 5_000, 1),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function configState(value, exists) {
|
|
27
|
+
if (value !== null)
|
|
28
|
+
return 'valid';
|
|
29
|
+
return exists ? 'invalid' : 'missing';
|
|
30
|
+
}
|
|
31
|
+
function machineState(value, exists) {
|
|
32
|
+
if (value !== null)
|
|
33
|
+
return 'valid';
|
|
34
|
+
return exists ? 'invalid' : 'missing';
|
|
35
|
+
}
|
|
36
|
+
function unavailableDiscovery(requestedRoot, canonicalRoot, diagnostic) {
|
|
37
|
+
return {
|
|
38
|
+
requestedRoot,
|
|
39
|
+
root: canonicalRoot,
|
|
40
|
+
rootState: 'unavailable',
|
|
41
|
+
configPath: join(canonicalRoot, '.pappra', 'workspace.json'),
|
|
42
|
+
configState: 'missing',
|
|
43
|
+
config: null,
|
|
44
|
+
machinePath: join(canonicalRoot, '.pappra', 'local', 'machine.json'),
|
|
45
|
+
machineState: 'missing',
|
|
46
|
+
machine: null,
|
|
47
|
+
repositories: [],
|
|
48
|
+
unresolvedRepositories: [],
|
|
49
|
+
boundaries: [],
|
|
50
|
+
documents: [],
|
|
51
|
+
validationCandidates: [],
|
|
52
|
+
diagnostics: [diagnostic],
|
|
53
|
+
truncated: false,
|
|
54
|
+
observedAt: new Date().toISOString(),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Discovers repositories, rules, documents, and validation candidates under an
|
|
59
|
+
* explicit workspace root.
|
|
60
|
+
*
|
|
61
|
+
* This function is read-only: it never executes package scripts, hooks, git
|
|
62
|
+
* fetch, or model calls, and never writes configuration. Missing configuration
|
|
63
|
+
* is reported and discovery continues only with capability-truthful scan data;
|
|
64
|
+
* invalid configuration is never partially applied.
|
|
65
|
+
*/
|
|
66
|
+
export async function discoverWorkspace(workspaceRoot, options = {}) {
|
|
67
|
+
const normalized = normalizeOptions(options);
|
|
68
|
+
const requestedRoot = resolve(workspaceRoot);
|
|
69
|
+
let observation;
|
|
70
|
+
try {
|
|
71
|
+
observation = await observePath(requestedRoot);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
return unavailableDiscovery(requestedRoot, requestedRoot, createDiagnostic({
|
|
75
|
+
code: diagnosticCodes.workspaceRootInvalid,
|
|
76
|
+
severity: 'error',
|
|
77
|
+
message: `Cannot inspect workspace root: ${requestedRoot} (${error instanceof Error ? error.message : String(error)})`,
|
|
78
|
+
source: requestedRoot,
|
|
79
|
+
hint: 'Pass an existing readable directory.',
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
if (observation.kind === 'missing') {
|
|
83
|
+
return unavailableDiscovery(requestedRoot, requestedRoot, createDiagnostic({
|
|
84
|
+
code: diagnosticCodes.workspaceRootInvalid,
|
|
85
|
+
severity: 'error',
|
|
86
|
+
message: `Workspace root does not exist: ${requestedRoot}`,
|
|
87
|
+
source: requestedRoot,
|
|
88
|
+
hint: 'Pass an existing directory; discovery does not create it.',
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
const canonicalRoot = await resolveRealpath(requestedRoot);
|
|
92
|
+
if (canonicalRoot === null) {
|
|
93
|
+
return unavailableDiscovery(requestedRoot, requestedRoot, createDiagnostic({
|
|
94
|
+
code: diagnosticCodes.workspaceRootInvalid,
|
|
95
|
+
severity: 'error',
|
|
96
|
+
message: `Cannot resolve workspace root: ${requestedRoot}`,
|
|
97
|
+
source: requestedRoot,
|
|
98
|
+
hint: 'Check permissions and the path.',
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
const rootObservation = await observePath(canonicalRoot);
|
|
102
|
+
if (rootObservation.kind !== 'directory') {
|
|
103
|
+
return unavailableDiscovery(requestedRoot, canonicalRoot, createDiagnostic({
|
|
104
|
+
code: diagnosticCodes.workspaceRootInvalid,
|
|
105
|
+
severity: 'error',
|
|
106
|
+
message: `Workspace root is not a directory: ${canonicalRoot}`,
|
|
107
|
+
source: canonicalRoot,
|
|
108
|
+
hint: 'Pass a directory path.',
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
const [workspaceInspection, machineInspection] = await Promise.all([
|
|
112
|
+
inspectWorkspaceConfig(canonicalRoot, { missing: 'info' }),
|
|
113
|
+
inspectMachineConfig(canonicalRoot),
|
|
114
|
+
]);
|
|
115
|
+
const config = workspaceInspection.value;
|
|
116
|
+
const machine = machineInspection.value;
|
|
117
|
+
const diagnostics = [
|
|
118
|
+
...workspaceInspection.diagnostics,
|
|
119
|
+
...machineInspection.diagnostics,
|
|
120
|
+
];
|
|
121
|
+
if (config !== null && machine !== null && machine.runtime !== null) {
|
|
122
|
+
const runtime = machine.runtime;
|
|
123
|
+
const executableObservation = await observePath(runtime.executable);
|
|
124
|
+
if (executableObservation.kind !== 'file' &&
|
|
125
|
+
executableObservation.kind !== 'symlink') {
|
|
126
|
+
diagnostics.push(createDiagnostic({
|
|
127
|
+
code: diagnosticCodes.runtimeMissing,
|
|
128
|
+
severity: 'warning',
|
|
129
|
+
message: `Recorded runtime does not exist: ${runtime.executable}`,
|
|
130
|
+
source: machineInspection.path,
|
|
131
|
+
hint: 'Reinstall or repair the fixed runtime explicitly; discovery never executes it.',
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
if (runtime.version !== config.runtimeVersion) {
|
|
135
|
+
diagnostics.push(createDiagnostic({
|
|
136
|
+
code: diagnosticCodes.runtimeVersionMismatch,
|
|
137
|
+
severity: 'warning',
|
|
138
|
+
message: `Workspace targets ${config.runtimeVersion}, but the verified local runtime is ${runtime.version}.`,
|
|
139
|
+
source: machineInspection.path,
|
|
140
|
+
hint: 'Run an explicit update or repair flow; discovery does not change runtime selection.',
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const repositoryDiscovery = await discoverRepositories({
|
|
145
|
+
root: canonicalRoot,
|
|
146
|
+
config,
|
|
147
|
+
machine,
|
|
148
|
+
configPath: workspaceInspection.path,
|
|
149
|
+
options: normalized,
|
|
150
|
+
});
|
|
151
|
+
const documentDiscovery = await discoverDocuments({
|
|
152
|
+
root: canonicalRoot,
|
|
153
|
+
config,
|
|
154
|
+
configPath: workspaceInspection.path,
|
|
155
|
+
repositories: repositoryDiscovery.repositories,
|
|
156
|
+
options: normalized,
|
|
157
|
+
});
|
|
158
|
+
const validationDiscovery = await discoverValidationCandidates({
|
|
159
|
+
root: canonicalRoot,
|
|
160
|
+
config,
|
|
161
|
+
configPath: workspaceInspection.path,
|
|
162
|
+
repositories: repositoryDiscovery.repositories,
|
|
163
|
+
});
|
|
164
|
+
const documents = [...documentDiscovery.documents].sort((left, right) => (left.workspaceRelativePath ?? left.path).localeCompare(right.workspaceRelativePath ?? right.path));
|
|
165
|
+
const boundaries = [
|
|
166
|
+
...new Set([
|
|
167
|
+
...repositoryDiscovery.boundaries,
|
|
168
|
+
...documentDiscovery.boundaries,
|
|
169
|
+
]),
|
|
170
|
+
];
|
|
171
|
+
return {
|
|
172
|
+
requestedRoot,
|
|
173
|
+
root: canonicalRoot,
|
|
174
|
+
rootState: 'directory',
|
|
175
|
+
configPath: workspaceInspection.path,
|
|
176
|
+
configState: configState(config, workspaceInspection.exists),
|
|
177
|
+
config,
|
|
178
|
+
machinePath: machineInspection.path,
|
|
179
|
+
machineState: machineState(machine, machineInspection.exists),
|
|
180
|
+
machine,
|
|
181
|
+
repositories: repositoryDiscovery.repositories,
|
|
182
|
+
unresolvedRepositories: repositoryDiscovery.unresolved,
|
|
183
|
+
boundaries,
|
|
184
|
+
documents,
|
|
185
|
+
validationCandidates: validationDiscovery.candidates,
|
|
186
|
+
diagnostics: [
|
|
187
|
+
...diagnostics,
|
|
188
|
+
...repositoryDiscovery.diagnostics,
|
|
189
|
+
...documentDiscovery.diagnostics,
|
|
190
|
+
...validationDiscovery.diagnostics,
|
|
191
|
+
],
|
|
192
|
+
truncated: repositoryDiscovery.truncated || documentDiscovery.truncated,
|
|
193
|
+
observedAt: new Date().toISOString(),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Diagnostic } from '../schemas/diagnostics.js';
|
|
2
|
+
import { type WorkspaceConfig } from '../schemas/workspace.js';
|
|
3
|
+
import type { DiscoveredDocument, DiscoveredRepository, NormalizedDiscoveryOptions } from './types.js';
|
|
4
|
+
export interface DocumentDiscoveryInput {
|
|
5
|
+
readonly root: string;
|
|
6
|
+
readonly config: WorkspaceConfig | null;
|
|
7
|
+
readonly configPath: string;
|
|
8
|
+
readonly repositories: readonly DiscoveredRepository[];
|
|
9
|
+
readonly options: NormalizedDiscoveryOptions;
|
|
10
|
+
}
|
|
11
|
+
export interface DocumentDiscoveryOutput {
|
|
12
|
+
readonly documents: readonly DiscoveredDocument[];
|
|
13
|
+
readonly boundaries: readonly string[];
|
|
14
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
15
|
+
readonly truncated: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Finds existing rules and documents in place. Content is never read, moved,
|
|
19
|
+
* or rewritten; only path, kind, provenance and size are reported.
|
|
20
|
+
*/
|
|
21
|
+
export declare function discoverDocuments(input: DocumentDiscoveryInput): Promise<DocumentDiscoveryOutput>;
|