@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,395 @@
|
|
|
1
|
+
import { basename, dirname, join, relative, sep } from 'node:path';
|
|
2
|
+
import { readdir } from 'node:fs/promises';
|
|
3
|
+
import { createDiagnostic, pointer } from '../schemas/diagnostics.js';
|
|
4
|
+
import { diagnosticCodes } from '../schemas/errors.js';
|
|
5
|
+
import { isPathInside, resolvePortableRelativePath, samePath, toWorkspaceRelativePath, } from '../schemas/paths.js';
|
|
6
|
+
import { normalizedExcludes, } from '../schemas/workspace.js';
|
|
7
|
+
import { observePath, resolveRealpath } from '../infra/fs.js';
|
|
8
|
+
import { filterGitIgnored } from '../infra/git.js';
|
|
9
|
+
import { isExcludedPath, isHiddenSegment } from './ignore.js';
|
|
10
|
+
const ruleFileNames = new Set([
|
|
11
|
+
'agents.md',
|
|
12
|
+
'claude.md',
|
|
13
|
+
'.cursorrules',
|
|
14
|
+
'copilot-instructions.md',
|
|
15
|
+
]);
|
|
16
|
+
const documentationFileNames = new Set([
|
|
17
|
+
'readme.md',
|
|
18
|
+
'contributing.md',
|
|
19
|
+
'architecture.md',
|
|
20
|
+
'security.md',
|
|
21
|
+
'context.md',
|
|
22
|
+
]);
|
|
23
|
+
function classifyDocument(path, root) {
|
|
24
|
+
const name = basename(path).toLowerCase();
|
|
25
|
+
if (ruleFileNames.has(name))
|
|
26
|
+
return 'rule';
|
|
27
|
+
const relativePath = toWorkspaceRelativePath(root, path);
|
|
28
|
+
const normalized = (relativePath ?? relative(root, path).split(sep).join('/')).toLowerCase();
|
|
29
|
+
if (normalized.split('/').includes('adr') ||
|
|
30
|
+
normalized.includes('/decisions/') ||
|
|
31
|
+
normalized.includes('/decision-records/')) {
|
|
32
|
+
return 'decision-record';
|
|
33
|
+
}
|
|
34
|
+
return 'documentation';
|
|
35
|
+
}
|
|
36
|
+
async function hasWorkspaceMarker(directory) {
|
|
37
|
+
return (await observePath(join(directory, '.pappra'))).kind !== 'missing';
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Finds the nearest nested workspace on the canonical path from an allowed
|
|
41
|
+
* root to a target directory. The allowed root itself is exempt, so an
|
|
42
|
+
* external checkout or workspace root may own a `.pappra` marker.
|
|
43
|
+
*/
|
|
44
|
+
async function nestedWorkspaceBoundary(allowedRoot, startDirectory) {
|
|
45
|
+
if (!isPathInside(allowedRoot, startDirectory))
|
|
46
|
+
return null;
|
|
47
|
+
let current = startDirectory;
|
|
48
|
+
while (!samePath(current, allowedRoot)) {
|
|
49
|
+
if (await hasWorkspaceMarker(current))
|
|
50
|
+
return current;
|
|
51
|
+
const parent = dirname(current);
|
|
52
|
+
if (samePath(parent, current) || !isPathInside(allowedRoot, parent)) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
current = parent;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
function isMarkdown(path) {
|
|
60
|
+
const lower = path.toLowerCase();
|
|
61
|
+
return (lower.endsWith('.md') || lower.endsWith('.mdx') || lower.endsWith('.mdc'));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Finds existing rules and documents in place. Content is never read, moved,
|
|
65
|
+
* or rewritten; only path, kind, provenance and size are reported.
|
|
66
|
+
*/
|
|
67
|
+
export async function discoverDocuments(input) {
|
|
68
|
+
const { root, config, configPath, repositories, options } = input;
|
|
69
|
+
const diagnostics = [];
|
|
70
|
+
const documents = new Map();
|
|
71
|
+
const boundaries = new Set();
|
|
72
|
+
const excluded = config === null ? [] : normalizedExcludes(config);
|
|
73
|
+
let truncated = false;
|
|
74
|
+
let scannedDirectories = 0;
|
|
75
|
+
const enclosingRepository = (path) => {
|
|
76
|
+
let match = null;
|
|
77
|
+
for (const repository of repositories) {
|
|
78
|
+
if (!isPathInside(repository.root, path))
|
|
79
|
+
continue;
|
|
80
|
+
if (match === null || repository.root.length > match.root.length) {
|
|
81
|
+
match = repository;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return match;
|
|
85
|
+
};
|
|
86
|
+
const addDocument = async (path, origin, declaredBy, containmentRoot, forcedKind = null) => {
|
|
87
|
+
if (documents.size >= options.maxDocuments) {
|
|
88
|
+
if (!truncated) {
|
|
89
|
+
diagnostics.push(createDiagnostic({
|
|
90
|
+
code: diagnosticCodes.discoveryLimitReached,
|
|
91
|
+
severity: 'warning',
|
|
92
|
+
message: `Document discovery stopped after ${options.maxDocuments} entries.`,
|
|
93
|
+
source: root,
|
|
94
|
+
hint: 'Reduce knowledge.sources or raise the bound explicitly.',
|
|
95
|
+
}));
|
|
96
|
+
truncated = true;
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const observation = await observePath(path);
|
|
101
|
+
if (observation.kind === 'missing' || observation.kind === 'other')
|
|
102
|
+
return;
|
|
103
|
+
if (observation.kind === 'symlink') {
|
|
104
|
+
const target = await resolveRealpath(path);
|
|
105
|
+
diagnostics.push(createDiagnostic({
|
|
106
|
+
code: diagnosticCodes.discoverySymlinkSkipped,
|
|
107
|
+
severity: 'info',
|
|
108
|
+
message: `Document symlink was not followed: ${path}${target === null ? '' : ` -> ${target}`}`,
|
|
109
|
+
source: declaredBy,
|
|
110
|
+
hint: 'Keep documentation as regular files; discovery does not follow symlinks.',
|
|
111
|
+
}));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (observation.kind !== 'file')
|
|
115
|
+
return;
|
|
116
|
+
const real = await resolveRealpath(path);
|
|
117
|
+
if (real === null)
|
|
118
|
+
return;
|
|
119
|
+
if (!isPathInside(containmentRoot, real)) {
|
|
120
|
+
diagnostics.push(createDiagnostic({
|
|
121
|
+
code: diagnosticCodes.documentPathEscapesRoot,
|
|
122
|
+
severity: 'warning',
|
|
123
|
+
message: `Document resolves outside its allowed root: ${path} -> ${real}`,
|
|
124
|
+
source: declaredBy,
|
|
125
|
+
hint: 'Keep workspace documents inside the workspace; an explicit external repository may only contribute documents inside that checkout.',
|
|
126
|
+
}));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const boundary = await nestedWorkspaceBoundary(containmentRoot, dirname(real));
|
|
130
|
+
if (boundary !== null) {
|
|
131
|
+
boundaries.add(boundary);
|
|
132
|
+
diagnostics.push(createDiagnostic({
|
|
133
|
+
code: diagnosticCodes.discoveryNestedWorkspace,
|
|
134
|
+
severity: 'info',
|
|
135
|
+
message: `Document is inside nested workspace ${boundary}: ${real}`,
|
|
136
|
+
source: declaredBy,
|
|
137
|
+
hint: 'The outer discovery does not enter or absorb a nested workspace.',
|
|
138
|
+
}));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (documents.has(real))
|
|
142
|
+
return;
|
|
143
|
+
const repository = enclosingRepository(real);
|
|
144
|
+
documents.set(real, {
|
|
145
|
+
path: real,
|
|
146
|
+
workspaceRelativePath: toWorkspaceRelativePath(root, real),
|
|
147
|
+
repositoryId: repository?.id ?? null,
|
|
148
|
+
kind: forcedKind ?? classifyDocument(real, root),
|
|
149
|
+
origin,
|
|
150
|
+
declaredBy,
|
|
151
|
+
sizeBytes: observation.stats?.size ?? 0,
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
const scanDirectory = async (directory, depthLimit, origin, declaredBy, containmentRoot, forcedKind) => {
|
|
155
|
+
const visit = async (current, depth) => {
|
|
156
|
+
if (scannedDirectories >= options.maxDirectories) {
|
|
157
|
+
if (!truncated) {
|
|
158
|
+
diagnostics.push(createDiagnostic({
|
|
159
|
+
code: diagnosticCodes.discoveryLimitReached,
|
|
160
|
+
severity: 'warning',
|
|
161
|
+
message: `Document scan stopped after ${options.maxDirectories} directories.`,
|
|
162
|
+
source: current,
|
|
163
|
+
hint: 'Narrow knowledge.sources or the workspace scope.',
|
|
164
|
+
}));
|
|
165
|
+
truncated = true;
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
scannedDirectories += 1;
|
|
170
|
+
const directoryReal = await resolveRealpath(current);
|
|
171
|
+
if (directoryReal === null) {
|
|
172
|
+
diagnostics.push(createDiagnostic({
|
|
173
|
+
code: diagnosticCodes.fileUnreadable,
|
|
174
|
+
severity: 'warning',
|
|
175
|
+
message: `Cannot resolve document directory: ${current}`,
|
|
176
|
+
source: current,
|
|
177
|
+
hint: 'Check permissions; discovery skips unreadable directories.',
|
|
178
|
+
}));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (!isPathInside(containmentRoot, directoryReal)) {
|
|
182
|
+
diagnostics.push(createDiagnostic({
|
|
183
|
+
code: diagnosticCodes.documentPathEscapesRoot,
|
|
184
|
+
severity: 'warning',
|
|
185
|
+
message: `Document directory resolves outside its allowed root: ${current} -> ${directoryReal}`,
|
|
186
|
+
source: current,
|
|
187
|
+
hint: 'Keep workspace documents inside the workspace; external repository documents must stay inside that checkout.',
|
|
188
|
+
}));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const boundary = await nestedWorkspaceBoundary(containmentRoot, directoryReal);
|
|
192
|
+
if (boundary !== null) {
|
|
193
|
+
boundaries.add(boundary);
|
|
194
|
+
diagnostics.push(createDiagnostic({
|
|
195
|
+
code: diagnosticCodes.discoveryNestedWorkspace,
|
|
196
|
+
severity: 'info',
|
|
197
|
+
message: `Document directory is inside nested workspace ${boundary}: ${directoryReal}`,
|
|
198
|
+
source: current,
|
|
199
|
+
hint: 'The outer discovery does not enter or absorb a nested workspace.',
|
|
200
|
+
}));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
let entries;
|
|
204
|
+
try {
|
|
205
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
diagnostics.push(createDiagnostic({
|
|
209
|
+
code: diagnosticCodes.fileUnreadable,
|
|
210
|
+
severity: 'warning',
|
|
211
|
+
message: `Cannot read document directory ${current}: ${error instanceof Error ? error.message : String(error)}`,
|
|
212
|
+
source: current,
|
|
213
|
+
hint: 'Check permissions; discovery skips unreadable directories.',
|
|
214
|
+
}));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
218
|
+
const limited = entries.slice(0, options.maxEntriesPerDirectory);
|
|
219
|
+
if (entries.length > limited.length) {
|
|
220
|
+
diagnostics.push(createDiagnostic({
|
|
221
|
+
code: diagnosticCodes.discoveryLimitReached,
|
|
222
|
+
severity: 'warning',
|
|
223
|
+
message: `Document directory has ${entries.length} entries; only the first ${limited.length} were inspected.`,
|
|
224
|
+
source: current,
|
|
225
|
+
hint: 'Reduce the directory scope or raise the bound explicitly.',
|
|
226
|
+
}));
|
|
227
|
+
truncated = true;
|
|
228
|
+
}
|
|
229
|
+
for (const entry of limited) {
|
|
230
|
+
if (isHiddenSegment(entry.name))
|
|
231
|
+
continue;
|
|
232
|
+
const child = join(current, entry.name);
|
|
233
|
+
const workspaceRelative = toWorkspaceRelativePath(root, child);
|
|
234
|
+
const localRelative = workspaceRelative ?? relative(directory, child).split(sep).join('/');
|
|
235
|
+
if (isExcludedPath(workspaceRelative ?? localRelative, workspaceRelative === null ? [] : excluded)) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (entry.isSymbolicLink()) {
|
|
239
|
+
const target = await resolveRealpath(child);
|
|
240
|
+
const outside = target === null || !isPathInside(directory, target);
|
|
241
|
+
diagnostics.push(createDiagnostic({
|
|
242
|
+
code: diagnosticCodes.discoverySymlinkSkipped,
|
|
243
|
+
severity: outside ? 'warning' : 'info',
|
|
244
|
+
message: `Document symlink was not followed: ${child}${target === null ? '' : ` -> ${target}`}`,
|
|
245
|
+
source: current,
|
|
246
|
+
hint: 'Keep documentation inside the workspace; external symlinks are never followed.',
|
|
247
|
+
}));
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (entry.isDirectory()) {
|
|
251
|
+
if (depth < depthLimit)
|
|
252
|
+
await visit(child, depth + 1);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (!entry.isFile() || !isMarkdown(child))
|
|
256
|
+
continue;
|
|
257
|
+
await addDocument(child, origin, declaredBy, containmentRoot, forcedKind);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
await visit(directory, 0);
|
|
261
|
+
};
|
|
262
|
+
const wellKnownRoots = new Map();
|
|
263
|
+
wellKnownRoots.set(root, enclosingRepository(root));
|
|
264
|
+
for (const repository of repositories) {
|
|
265
|
+
wellKnownRoots.set(repository.root, repository);
|
|
266
|
+
}
|
|
267
|
+
const addWellKnown = async (directory, repository) => {
|
|
268
|
+
const origin = repository === null ? 'well-known' : 'repository-scan';
|
|
269
|
+
const declaredPrefix = repository === null ? 'well-known' : 'repository-scan';
|
|
270
|
+
const containmentRoot = repository?.root ?? root;
|
|
271
|
+
for (const name of [
|
|
272
|
+
'AGENTS.md',
|
|
273
|
+
'CLAUDE.md',
|
|
274
|
+
'.cursorrules',
|
|
275
|
+
'README.md',
|
|
276
|
+
'CONTRIBUTING.md',
|
|
277
|
+
'ARCHITECTURE.md',
|
|
278
|
+
'SECURITY.md',
|
|
279
|
+
'CONTEXT.md',
|
|
280
|
+
]) {
|
|
281
|
+
const path = join(directory, name);
|
|
282
|
+
await addDocument(path, origin, `${declaredPrefix}:${name}`, containmentRoot, ruleFileNames.has(name.toLowerCase())
|
|
283
|
+
? 'rule'
|
|
284
|
+
: documentationFileNames.has(name.toLowerCase())
|
|
285
|
+
? 'documentation'
|
|
286
|
+
: null);
|
|
287
|
+
}
|
|
288
|
+
await addDocument(join(directory, '.github', 'copilot-instructions.md'), origin, `${declaredPrefix}:.github/copilot-instructions.md`, containmentRoot, 'rule');
|
|
289
|
+
const cursorRules = join(directory, '.cursor', 'rules');
|
|
290
|
+
const cursorObservation = await observePath(cursorRules);
|
|
291
|
+
if (cursorObservation.kind === 'directory') {
|
|
292
|
+
await scanDirectory(cursorRules, 2, origin, `${declaredPrefix}:.cursor/rules`, containmentRoot, 'rule');
|
|
293
|
+
}
|
|
294
|
+
const docs = join(directory, 'docs');
|
|
295
|
+
const docsObservation = await observePath(docs);
|
|
296
|
+
if (docsObservation.kind === 'directory') {
|
|
297
|
+
await scanDirectory(docs, 3, repository === null ? 'directory-scan' : 'repository-scan', `${declaredPrefix}:docs`, containmentRoot, null);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
if (config !== null) {
|
|
301
|
+
for (const [index, source] of config.knowledge.sources.entries()) {
|
|
302
|
+
const declaredBy = `${configPath}#${pointer('knowledge', 'sources', index)}`;
|
|
303
|
+
const candidate = resolvePortableRelativePath(root, source);
|
|
304
|
+
const observation = await observePath(candidate);
|
|
305
|
+
if (observation.kind === 'missing') {
|
|
306
|
+
diagnostics.push(createDiagnostic({
|
|
307
|
+
code: diagnosticCodes.knowledgeSourceMissing,
|
|
308
|
+
severity: 'error',
|
|
309
|
+
message: `Configured knowledge source does not exist: ${candidate}`,
|
|
310
|
+
source: configPath,
|
|
311
|
+
pointer: pointer('knowledge', 'sources', index),
|
|
312
|
+
hint: 'Correct or remove the source; Pappra does not create or migrate documentation.',
|
|
313
|
+
}));
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const real = await resolveRealpath(candidate);
|
|
317
|
+
if (real === null) {
|
|
318
|
+
diagnostics.push(createDiagnostic({
|
|
319
|
+
code: diagnosticCodes.knowledgeSourceNotReadable,
|
|
320
|
+
severity: 'error',
|
|
321
|
+
message: `Cannot resolve configured knowledge source: ${candidate}`,
|
|
322
|
+
source: configPath,
|
|
323
|
+
pointer: pointer('knowledge', 'sources', index),
|
|
324
|
+
hint: 'Check permissions and the path.',
|
|
325
|
+
}));
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const isExternalSource = !isPathInside(root, real);
|
|
329
|
+
if (isExternalSource) {
|
|
330
|
+
diagnostics.push(createDiagnostic({
|
|
331
|
+
code: diagnosticCodes.knowledgeSourceSymlinkEscape,
|
|
332
|
+
severity: 'error',
|
|
333
|
+
message: `Configured knowledge source resolves outside the workspace: ${real}`,
|
|
334
|
+
source: configPath,
|
|
335
|
+
pointer: pointer('knowledge', 'sources', index),
|
|
336
|
+
hint: 'Keep knowledge inside the workspace; use an external repository mapping for external code.',
|
|
337
|
+
}));
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const resolvedObservation = await observePath(real);
|
|
341
|
+
if (resolvedObservation.kind === 'file') {
|
|
342
|
+
await addDocument(real, 'workspace-config', declaredBy, root);
|
|
343
|
+
}
|
|
344
|
+
else if (resolvedObservation.kind === 'directory') {
|
|
345
|
+
await scanDirectory(real, 3, 'workspace-config', declaredBy, root, null);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
for (const [directory, repository] of wellKnownRoots) {
|
|
350
|
+
await addWellKnown(directory, repository);
|
|
351
|
+
}
|
|
352
|
+
const grouped = new Map();
|
|
353
|
+
for (const document of documents.values()) {
|
|
354
|
+
const repository = enclosingRepository(document.path);
|
|
355
|
+
if (repository === null)
|
|
356
|
+
continue;
|
|
357
|
+
const group = grouped.get(repository) ?? [];
|
|
358
|
+
group.push(document.path);
|
|
359
|
+
grouped.set(repository, group);
|
|
360
|
+
}
|
|
361
|
+
for (const [repository, paths] of grouped) {
|
|
362
|
+
const ignored = await filterGitIgnored(repository.root, paths, {
|
|
363
|
+
timeoutMs: options.gitTimeoutMs,
|
|
364
|
+
});
|
|
365
|
+
if (!ignored.ok) {
|
|
366
|
+
diagnostics.push(createDiagnostic({
|
|
367
|
+
code: diagnosticCodes.ignoreFilterUnavailable,
|
|
368
|
+
severity: 'warning',
|
|
369
|
+
message: `Cannot evaluate .gitignore for ${repository.root}: ${ignored.message}`,
|
|
370
|
+
source: repository.root,
|
|
371
|
+
hint: 'Discovery keeps the candidates and reports that ignore filtering was unavailable.',
|
|
372
|
+
}));
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
for (const path of ignored.value) {
|
|
376
|
+
const document = documents.get(path);
|
|
377
|
+
if (document === undefined)
|
|
378
|
+
continue;
|
|
379
|
+
documents.delete(path);
|
|
380
|
+
diagnostics.push(createDiagnostic({
|
|
381
|
+
code: diagnosticCodes.documentIgnored,
|
|
382
|
+
severity: 'info',
|
|
383
|
+
message: `Document is ignored by git: ${path}`,
|
|
384
|
+
source: path,
|
|
385
|
+
hint: 'Ignored documentation remains in place; discovery did not read or move it.',
|
|
386
|
+
}));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
documents: [...documents.values()],
|
|
391
|
+
boundaries: [...boundaries],
|
|
392
|
+
diagnostics,
|
|
393
|
+
truncated,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Directory names that discovery never descends into. These are conservative
|
|
3
|
+
* defaults; explicit workspace.json repositories are still bounds-checked but
|
|
4
|
+
* are not silently dropped by this scan filter.
|
|
5
|
+
*/
|
|
6
|
+
export declare const builtInExcludedSegments: ReadonlySet<string>;
|
|
7
|
+
export declare function relativeSegments(relativePath: string): readonly string[];
|
|
8
|
+
/** Applies built-in segment exclusions and literal workspace-relative prefixes. */
|
|
9
|
+
export declare function isExcludedPath(relativePath: string, configExcludes: readonly string[]): boolean;
|
|
10
|
+
export declare function isHiddenSegment(name: string): boolean;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { normalizePortableRelativePath } from '../schemas/paths.js';
|
|
2
|
+
/**
|
|
3
|
+
* Directory names that discovery never descends into. These are conservative
|
|
4
|
+
* defaults; explicit workspace.json repositories are still bounds-checked but
|
|
5
|
+
* are not silently dropped by this scan filter.
|
|
6
|
+
*/
|
|
7
|
+
export const builtInExcludedSegments = new Set([
|
|
8
|
+
'.git',
|
|
9
|
+
'.pappra',
|
|
10
|
+
'node_modules',
|
|
11
|
+
'dist',
|
|
12
|
+
'build',
|
|
13
|
+
'.build',
|
|
14
|
+
'coverage',
|
|
15
|
+
'target',
|
|
16
|
+
'.next',
|
|
17
|
+
'.nuxt',
|
|
18
|
+
'.svelte-kit',
|
|
19
|
+
'.turbo',
|
|
20
|
+
'.parcel-cache',
|
|
21
|
+
'.cache',
|
|
22
|
+
'.venv',
|
|
23
|
+
'venv',
|
|
24
|
+
'__pycache__',
|
|
25
|
+
'.codegraph',
|
|
26
|
+
'.serena',
|
|
27
|
+
'.idea',
|
|
28
|
+
'.vscode',
|
|
29
|
+
]);
|
|
30
|
+
export function relativeSegments(relativePath) {
|
|
31
|
+
if (relativePath === '.' || relativePath === '')
|
|
32
|
+
return [];
|
|
33
|
+
return relativePath.split('/');
|
|
34
|
+
}
|
|
35
|
+
/** Applies built-in segment exclusions and literal workspace-relative prefixes. */
|
|
36
|
+
export function isExcludedPath(relativePath, configExcludes) {
|
|
37
|
+
if (relativePath === '.' || relativePath === '')
|
|
38
|
+
return false;
|
|
39
|
+
const segments = relativeSegments(relativePath);
|
|
40
|
+
if (segments.some((segment) => builtInExcludedSegments.has(segment))) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
const normalized = normalizePortableRelativePath(relativePath);
|
|
44
|
+
return configExcludes.some((exclude) => normalized === exclude || normalized.startsWith(`${exclude}/`));
|
|
45
|
+
}
|
|
46
|
+
export function isHiddenSegment(name) {
|
|
47
|
+
return name.startsWith('.') && name !== '.' && name !== '..';
|
|
48
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { inspectMachineConfig, inspectWorkspaceConfig, loadMachineConfig, loadWorkspaceConfig, } from './config.js';
|
|
2
|
+
export type { InspectWorkspaceConfigOptions, MachineConfigInspection, WorkspaceConfigInspection, } from './config.js';
|
|
3
|
+
export { discoverWorkspace } from './discover.js';
|
|
4
|
+
export type { WorkspaceDiscovery } from './discover.js';
|
|
5
|
+
export { resolveWorkspaceRoot } from './resolve.js';
|
|
6
|
+
export type { ResolveWorkspaceRootOptions, WorkspaceRootResolution, WorkspaceRootState, } from './resolve.js';
|
|
7
|
+
export type { DiscoveredDocument, DiscoveredRepository, DiscoveryOptions, DocumentKind, DocumentOrigin, MachineConfigState, UnresolvedRepository, ValidationCandidate, WorkspaceConfigState, } from './types.js';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Diagnostic } from '../schemas/diagnostics.js';
|
|
2
|
+
import { type WorkspaceConfig } from '../schemas/workspace.js';
|
|
3
|
+
import type { MachineConfig } from '../schemas/machine.js';
|
|
4
|
+
import type { DiscoveredRepository, NormalizedDiscoveryOptions, UnresolvedRepository } from './types.js';
|
|
5
|
+
export interface RepositoryDiscoveryInput {
|
|
6
|
+
readonly root: string;
|
|
7
|
+
readonly config: WorkspaceConfig | null;
|
|
8
|
+
readonly machine: MachineConfig | null;
|
|
9
|
+
readonly configPath: string;
|
|
10
|
+
readonly options: NormalizedDiscoveryOptions;
|
|
11
|
+
}
|
|
12
|
+
export interface RepositoryDiscoveryOutput {
|
|
13
|
+
readonly repositories: readonly DiscoveredRepository[];
|
|
14
|
+
readonly unresolved: readonly UnresolvedRepository[];
|
|
15
|
+
readonly boundaries: readonly string[];
|
|
16
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
17
|
+
readonly truncated: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Resolves explicit repository declarations first, then performs a bounded
|
|
21
|
+
* scan when the workspace root is not itself a repository. A scan never
|
|
22
|
+
* descends into a repository, so source files are not enumerated.
|
|
23
|
+
*/
|
|
24
|
+
export declare function discoverRepositories(input: RepositoryDiscoveryInput): Promise<RepositoryDiscoveryOutput>;
|