create-quiver 0.17.5 → 0.17.6
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/docs/TROUBLESHOOTING.md +52 -0
- package/docs/reference/commands.md +3 -0
- package/docs/workflows/existing-project-ai-quiver-setup.md +0 -1
- package/docs/workflows/existing-project.md +1 -0
- package/package.json +1 -1
- package/specs/quiver-v43-cli-i18n-audit-release-readiness/command-language-mode-matrix.json +1 -0
- package/specs/quiver-v57-evidence-budget-recovery-ux/EVIDENCE_REPORT.md +27 -0
- package/specs/quiver-v57-evidence-budget-recovery-ux/EXECUTION_PLAN.md +41 -0
- package/specs/quiver-v57-evidence-budget-recovery-ux/SPEC.md +139 -0
- package/specs/quiver-v57-evidence-budget-recovery-ux/STATUS.md +20 -0
- package/specs/quiver-v57-evidence-budget-recovery-ux/pr.md +124 -0
- package/specs/quiver-v57-evidence-budget-recovery-ux/slices/slice-01-recovery-contract-security-classifier/CLOSURE_BRIEF.md +22 -0
- package/specs/quiver-v57-evidence-budget-recovery-ux/slices/slice-01-recovery-contract-security-classifier/EXECUTION_BRIEF.md +58 -0
- package/specs/quiver-v57-evidence-budget-recovery-ux/slices/slice-01-recovery-contract-security-classifier/pr.md +12 -0
- package/specs/quiver-v57-evidence-budget-recovery-ux/slices/slice-01-recovery-contract-security-classifier/slice.json +60 -0
- package/specs/quiver-v57-evidence-budget-recovery-ux/slices/slice-02-budget-command-recommendation/CLOSURE_BRIEF.md +9 -0
- package/specs/quiver-v57-evidence-budget-recovery-ux/slices/slice-02-budget-command-recommendation/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v57-evidence-budget-recovery-ux/slices/slice-02-budget-command-recommendation/pr.md +12 -0
- package/specs/quiver-v57-evidence-budget-recovery-ux/slices/slice-02-budget-command-recommendation/slice.json +60 -0
- package/specs/quiver-v57-evidence-budget-recovery-ux/slices/slice-03-cli-json-i18n-output/CLOSURE_BRIEF.md +10 -0
- package/specs/quiver-v57-evidence-budget-recovery-ux/slices/slice-03-cli-json-i18n-output/EXECUTION_BRIEF.md +58 -0
- package/specs/quiver-v57-evidence-budget-recovery-ux/slices/slice-03-cli-json-i18n-output/pr.md +13 -0
- package/specs/quiver-v57-evidence-budget-recovery-ux/slices/slice-03-cli-json-i18n-output/slice.json +68 -0
- package/specs/quiver-v57-evidence-budget-recovery-ux/slices/slice-04-integration-fixtures-docs-release-smoke/CLOSURE_BRIEF.md +14 -0
- package/specs/quiver-v57-evidence-budget-recovery-ux/slices/slice-04-integration-fixtures-docs-release-smoke/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v57-evidence-budget-recovery-ux/slices/slice-04-integration-fixtures-docs-release-smoke/pr.md +14 -0
- package/specs/quiver-v57-evidence-budget-recovery-ux/slices/slice-04-integration-fixtures-docs-release-smoke/slice.json +65 -0
- package/src/create-quiver/commands/ai.js +96 -1
- package/src/create-quiver/lib/ai/analyze-project-recovery.js +736 -0
- package/src/create-quiver/lib/ai/analyze-project-validation.js +1 -0
- package/src/create-quiver/lib/i18n/messages/en.js +6 -0
- package/src/create-quiver/lib/i18n/messages/es.js +6 -0
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
getContextPathExclusionReason,
|
|
6
|
+
normalizeContextPath,
|
|
7
|
+
} = require('./safety');
|
|
8
|
+
const { getProjectRelativePathIssue } = require('../paths');
|
|
9
|
+
|
|
10
|
+
const RECOVERY_SCHEMA_VERSION = 1;
|
|
11
|
+
const DEFAULT_RECOVERY_MAX_FILES_CAP = 300;
|
|
12
|
+
const DEFAULT_RECOVERY_MAX_BYTES_CAP = 1_500_000;
|
|
13
|
+
const RECOVERY_BYTE_ROUNDING = 50_000;
|
|
14
|
+
const RECOVERY_MIN_BYTE_MARGIN = 50_000;
|
|
15
|
+
const RECOVERY_FILE_MARGIN = 20;
|
|
16
|
+
|
|
17
|
+
const RECOVERY_CLASSIFICATION = Object.freeze({
|
|
18
|
+
GENERATED_OR_DEPENDENCY: 'generated_or_dependency',
|
|
19
|
+
METADATA_ONLY: 'metadata_only',
|
|
20
|
+
MISSING_FILE: 'missing_file',
|
|
21
|
+
OUTSIDE_SCOPE: 'outside_scope',
|
|
22
|
+
SAFE_TO_INCLUDE: 'safe_to_include',
|
|
23
|
+
SECURITY_EXCLUDED: 'security_excluded',
|
|
24
|
+
UNKNOWN: 'unknown',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const RECOVERY_REASON = Object.freeze({
|
|
28
|
+
BUDGET_LIMITED: 'budget_limited',
|
|
29
|
+
GENERATED_OR_DEPENDENCY: 'generated_or_dependency',
|
|
30
|
+
METADATA_ONLY: 'metadata_only',
|
|
31
|
+
MISSING_FILE: 'missing_file',
|
|
32
|
+
NOT_DISCOVERED: 'not_discovered',
|
|
33
|
+
OUTSIDE_SCOPE: 'outside_scope',
|
|
34
|
+
SECURITY_EXCLUDED: 'security_excluded',
|
|
35
|
+
UNKNOWN: 'unknown',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const BINARY_EXTENSIONS = new Set([
|
|
39
|
+
'.7z',
|
|
40
|
+
'.avif',
|
|
41
|
+
'.bmp',
|
|
42
|
+
'.class',
|
|
43
|
+
'.dll',
|
|
44
|
+
'.dmg',
|
|
45
|
+
'.doc',
|
|
46
|
+
'.docx',
|
|
47
|
+
'.eot',
|
|
48
|
+
'.exe',
|
|
49
|
+
'.gif',
|
|
50
|
+
'.gz',
|
|
51
|
+
'.ico',
|
|
52
|
+
'.jar',
|
|
53
|
+
'.jpeg',
|
|
54
|
+
'.jpg',
|
|
55
|
+
'.lockb',
|
|
56
|
+
'.mov',
|
|
57
|
+
'.mp3',
|
|
58
|
+
'.mp4',
|
|
59
|
+
'.otf',
|
|
60
|
+
'.pdf',
|
|
61
|
+
'.png',
|
|
62
|
+
'.so',
|
|
63
|
+
'.tar',
|
|
64
|
+
'.ttf',
|
|
65
|
+
'.wasm',
|
|
66
|
+
'.webm',
|
|
67
|
+
'.webp',
|
|
68
|
+
'.woff',
|
|
69
|
+
'.woff2',
|
|
70
|
+
'.zip',
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
const GENERATED_OR_DEPENDENCY_REASONS = new Set([
|
|
74
|
+
'unsafe-segment:.cache',
|
|
75
|
+
'unsafe-segment:.next',
|
|
76
|
+
'unsafe-segment:.nuxt',
|
|
77
|
+
'unsafe-segment:.npm',
|
|
78
|
+
'unsafe-segment:.parcel-cache',
|
|
79
|
+
'unsafe-segment:.pnpm-store',
|
|
80
|
+
'unsafe-segment:.turbo',
|
|
81
|
+
'unsafe-segment:.yarn',
|
|
82
|
+
'unsafe-segment:artifacts',
|
|
83
|
+
'unsafe-segment:build',
|
|
84
|
+
'unsafe-segment:coverage',
|
|
85
|
+
'unsafe-segment:dist',
|
|
86
|
+
'unsafe-segment:gen',
|
|
87
|
+
'unsafe-segment:generated',
|
|
88
|
+
'unsafe-segment:node_modules',
|
|
89
|
+
'unsafe-segment:out',
|
|
90
|
+
'unsafe-segment:reports',
|
|
91
|
+
'unsafe-segment:target',
|
|
92
|
+
'unsafe-segment:vendor',
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
const METADATA_ONLY_BASENAME_PATTERNS = [
|
|
96
|
+
/^\.env\.example$/i,
|
|
97
|
+
/^\.env\.sample$/i,
|
|
98
|
+
/^\.env\.template$/i,
|
|
99
|
+
/^\.env\.dist$/i,
|
|
100
|
+
/^\.env\.defaults?$/i,
|
|
101
|
+
/^\.env\.[a-z0-9_-]+\.example$/i,
|
|
102
|
+
/^\.env\.[a-z0-9_-]+\.sample$/i,
|
|
103
|
+
/^\.env\.[a-z0-9_-]+\.template$/i,
|
|
104
|
+
/^\.env\.[a-z0-9_-]+\.dist$/i,
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
function toPosix(filePath) {
|
|
108
|
+
return String(filePath || '').replace(/\\/g, '/');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function normalizeRelativeEvidencePath(evidencePath) {
|
|
112
|
+
const raw = String(evidencePath || '');
|
|
113
|
+
const pathIssue = getProjectRelativePathIssue(raw);
|
|
114
|
+
if (pathIssue) {
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
input_path: raw,
|
|
118
|
+
path: normalizeContextPath(raw),
|
|
119
|
+
classification: pathIssue === 'empty-path' ? RECOVERY_CLASSIFICATION.UNKNOWN : RECOVERY_CLASSIFICATION.OUTSIDE_SCOPE,
|
|
120
|
+
reason: pathIssue === 'empty-path' ? 'empty-path' : RECOVERY_REASON.OUTSIDE_SCOPE,
|
|
121
|
+
path_issue: pathIssue,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const normalized = normalizeContextPath(raw);
|
|
126
|
+
if (!normalized) {
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
input_path: raw,
|
|
130
|
+
path: '',
|
|
131
|
+
classification: RECOVERY_CLASSIFICATION.UNKNOWN,
|
|
132
|
+
reason: 'empty-path',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (normalized.includes('\0')) {
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
input_path: raw,
|
|
139
|
+
path: normalized,
|
|
140
|
+
classification: RECOVERY_CLASSIFICATION.SECURITY_EXCLUDED,
|
|
141
|
+
reason: 'invalid-null-byte',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (path.posix.isAbsolute(normalized) || /^[A-Za-z]:\//.test(normalized)) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
input_path: raw,
|
|
148
|
+
path: normalized,
|
|
149
|
+
classification: RECOVERY_CLASSIFICATION.OUTSIDE_SCOPE,
|
|
150
|
+
reason: RECOVERY_REASON.OUTSIDE_SCOPE,
|
|
151
|
+
path_issue: 'absolute-path',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const clean = path.posix.normalize(normalized);
|
|
156
|
+
if (clean === '.' || clean === '..' || clean.startsWith('../')) {
|
|
157
|
+
return {
|
|
158
|
+
ok: false,
|
|
159
|
+
input_path: raw,
|
|
160
|
+
path: clean,
|
|
161
|
+
classification: RECOVERY_CLASSIFICATION.OUTSIDE_SCOPE,
|
|
162
|
+
reason: RECOVERY_REASON.OUTSIDE_SCOPE,
|
|
163
|
+
path_issue: 'path-traversal',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
ok: true,
|
|
169
|
+
input_path: raw,
|
|
170
|
+
path: clean,
|
|
171
|
+
path_issue: null,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function isInsideRepo(repoRoot, relativePath) {
|
|
176
|
+
const repo = path.resolve(repoRoot);
|
|
177
|
+
const absolutePath = path.resolve(repo, relativePath);
|
|
178
|
+
const relative = path.relative(repo, absolutePath);
|
|
179
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function basename(filePath) {
|
|
183
|
+
return path.posix.basename(toPosix(filePath));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isMetadataOnlyPath(filePath) {
|
|
187
|
+
const base = basename(filePath);
|
|
188
|
+
return METADATA_ONLY_BASENAME_PATTERNS.some((pattern) => pattern.test(base));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function mapSafetyReasonToClassification(reason, filePath) {
|
|
192
|
+
if (isMetadataOnlyPath(filePath)) {
|
|
193
|
+
return {
|
|
194
|
+
classification: RECOVERY_CLASSIFICATION.METADATA_ONLY,
|
|
195
|
+
reason: RECOVERY_REASON.METADATA_ONLY,
|
|
196
|
+
safe_to_include: false,
|
|
197
|
+
content_allowed: false,
|
|
198
|
+
metadata_only: true,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (GENERATED_OR_DEPENDENCY_REASONS.has(reason)) {
|
|
203
|
+
return {
|
|
204
|
+
classification: RECOVERY_CLASSIFICATION.GENERATED_OR_DEPENDENCY,
|
|
205
|
+
reason: RECOVERY_REASON.GENERATED_OR_DEPENDENCY,
|
|
206
|
+
safe_to_include: false,
|
|
207
|
+
content_allowed: false,
|
|
208
|
+
metadata_only: false,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
classification: RECOVERY_CLASSIFICATION.SECURITY_EXCLUDED,
|
|
214
|
+
reason: RECOVERY_REASON.SECURITY_EXCLUDED,
|
|
215
|
+
safe_to_include: false,
|
|
216
|
+
content_allowed: false,
|
|
217
|
+
metadata_only: false,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function hasBinaryExtension(filePath) {
|
|
222
|
+
return BINARY_EXTENSIONS.has(path.posix.extname(toPosix(filePath)).toLowerCase());
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function appearsBinaryFile(absolutePath) {
|
|
226
|
+
if (hasBinaryExtension(absolutePath)) {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let descriptor;
|
|
231
|
+
try {
|
|
232
|
+
descriptor = fs.openSync(absolutePath, 'r');
|
|
233
|
+
const buffer = Buffer.alloc(1024);
|
|
234
|
+
const bytesRead = fs.readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
235
|
+
for (let index = 0; index < bytesRead; index += 1) {
|
|
236
|
+
if (buffer[index] === 0) {
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return false;
|
|
241
|
+
} catch (_) {
|
|
242
|
+
return true;
|
|
243
|
+
} finally {
|
|
244
|
+
if (typeof descriptor === 'number') {
|
|
245
|
+
fs.closeSync(descriptor);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function makeFileMap(items) {
|
|
251
|
+
const map = new Map();
|
|
252
|
+
for (const item of Array.isArray(items) ? items : []) {
|
|
253
|
+
const normalized = toPosix(item.path || '');
|
|
254
|
+
if (!normalized) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
map.set(normalized, item);
|
|
258
|
+
}
|
|
259
|
+
return map;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function omittedReasonToRecoveryReason(reason) {
|
|
263
|
+
const value = String(reason || '');
|
|
264
|
+
if (value.startsWith('budget:')) {
|
|
265
|
+
return RECOVERY_REASON.BUDGET_LIMITED;
|
|
266
|
+
}
|
|
267
|
+
if (value === 'sampling:lockfile-metadata') {
|
|
268
|
+
return RECOVERY_REASON.METADATA_ONLY;
|
|
269
|
+
}
|
|
270
|
+
return value || RECOVERY_REASON.UNKNOWN;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function classifyKnownOmission(omitted) {
|
|
274
|
+
const recoveryReason = omittedReasonToRecoveryReason(omitted.reason);
|
|
275
|
+
if (omitted.reason === 'binary-file') {
|
|
276
|
+
return {
|
|
277
|
+
classification: RECOVERY_CLASSIFICATION.GENERATED_OR_DEPENDENCY,
|
|
278
|
+
reason: RECOVERY_REASON.GENERATED_OR_DEPENDENCY,
|
|
279
|
+
safe_to_include: false,
|
|
280
|
+
content_allowed: false,
|
|
281
|
+
metadata_only: false,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (omitted.reason === 'symlink' || omitted.reason === 'not-regular-file') {
|
|
285
|
+
return {
|
|
286
|
+
classification: RECOVERY_CLASSIFICATION.SECURITY_EXCLUDED,
|
|
287
|
+
reason: RECOVERY_REASON.SECURITY_EXCLUDED,
|
|
288
|
+
safe_to_include: false,
|
|
289
|
+
content_allowed: false,
|
|
290
|
+
metadata_only: false,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
if (recoveryReason === RECOVERY_REASON.METADATA_ONLY) {
|
|
294
|
+
return {
|
|
295
|
+
classification: RECOVERY_CLASSIFICATION.METADATA_ONLY,
|
|
296
|
+
reason: RECOVERY_REASON.METADATA_ONLY,
|
|
297
|
+
safe_to_include: false,
|
|
298
|
+
content_allowed: false,
|
|
299
|
+
metadata_only: true,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
classification: RECOVERY_CLASSIFICATION.SAFE_TO_INCLUDE,
|
|
305
|
+
reason: recoveryReason,
|
|
306
|
+
safe_to_include: true,
|
|
307
|
+
content_allowed: true,
|
|
308
|
+
metadata_only: false,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function classifySkippedFile(skipped) {
|
|
313
|
+
if (skipped.reason === 'binary-file') {
|
|
314
|
+
return {
|
|
315
|
+
classification: RECOVERY_CLASSIFICATION.GENERATED_OR_DEPENDENCY,
|
|
316
|
+
reason: RECOVERY_REASON.GENERATED_OR_DEPENDENCY,
|
|
317
|
+
safe_to_include: false,
|
|
318
|
+
content_allowed: false,
|
|
319
|
+
metadata_only: false,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
classification: RECOVERY_CLASSIFICATION.SECURITY_EXCLUDED,
|
|
325
|
+
reason: RECOVERY_REASON.SECURITY_EXCLUDED,
|
|
326
|
+
safe_to_include: false,
|
|
327
|
+
content_allowed: false,
|
|
328
|
+
metadata_only: false,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function baseRecoveryEntry(evidencePath, options = {}) {
|
|
333
|
+
return {
|
|
334
|
+
path: evidencePath.path,
|
|
335
|
+
input_path: evidencePath.input_path,
|
|
336
|
+
issue_paths: Array.isArray(options.issuePaths) ? [...new Set(options.issuePaths)].sort() : [],
|
|
337
|
+
classification: RECOVERY_CLASSIFICATION.UNKNOWN,
|
|
338
|
+
reason: RECOVERY_REASON.UNKNOWN,
|
|
339
|
+
safe_to_include: false,
|
|
340
|
+
content_allowed: false,
|
|
341
|
+
metadata_only: false,
|
|
342
|
+
bytes: 0,
|
|
343
|
+
effective_prompt_bytes: 0,
|
|
344
|
+
selected: false,
|
|
345
|
+
omitted_reason: null,
|
|
346
|
+
source: 'unknown',
|
|
347
|
+
safety_reason: null,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function classifyEvidencePath(repoRoot, evidencePath, options = {}) {
|
|
352
|
+
const normalized = normalizeRelativeEvidencePath(evidencePath);
|
|
353
|
+
if (!normalized.ok) {
|
|
354
|
+
return {
|
|
355
|
+
...baseRecoveryEntry(normalized, options),
|
|
356
|
+
classification: normalized.classification,
|
|
357
|
+
reason: normalized.reason,
|
|
358
|
+
source: 'path-validation',
|
|
359
|
+
path_issue: normalized.path_issue || null,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const entry = baseRecoveryEntry(normalized, options);
|
|
364
|
+
const repo = path.resolve(repoRoot || process.cwd());
|
|
365
|
+
if (!isInsideRepo(repo, normalized.path)) {
|
|
366
|
+
return {
|
|
367
|
+
...entry,
|
|
368
|
+
classification: RECOVERY_CLASSIFICATION.OUTSIDE_SCOPE,
|
|
369
|
+
reason: RECOVERY_REASON.OUTSIDE_SCOPE,
|
|
370
|
+
source: 'path-validation',
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const selectedMap = makeFileMap(options.selectedFiles);
|
|
375
|
+
const omittedMap = makeFileMap(options.omittedFiles);
|
|
376
|
+
const skippedMap = makeFileMap(options.skippedFiles);
|
|
377
|
+
const safetyMap = makeFileMap(options.safetyExclusions);
|
|
378
|
+
const selected = selectedMap.get(normalized.path);
|
|
379
|
+
const omitted = omittedMap.get(normalized.path);
|
|
380
|
+
const skipped = skippedMap.get(normalized.path);
|
|
381
|
+
const safety = safetyMap.get(normalized.path);
|
|
382
|
+
|
|
383
|
+
if (selected) {
|
|
384
|
+
return {
|
|
385
|
+
...entry,
|
|
386
|
+
classification: RECOVERY_CLASSIFICATION.SAFE_TO_INCLUDE,
|
|
387
|
+
reason: 'already_selected',
|
|
388
|
+
safe_to_include: true,
|
|
389
|
+
content_allowed: true,
|
|
390
|
+
bytes: selected.bytes || 0,
|
|
391
|
+
effective_prompt_bytes: selected.bytes || 0,
|
|
392
|
+
selected: true,
|
|
393
|
+
source: 'selected-files',
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (safety) {
|
|
398
|
+
return {
|
|
399
|
+
...entry,
|
|
400
|
+
...mapSafetyReasonToClassification(safety.reason, normalized.path),
|
|
401
|
+
safety_reason: safety.reason || null,
|
|
402
|
+
source: 'safety-exclusions',
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const exclusionReason = getContextPathExclusionReason(normalized.path);
|
|
407
|
+
if (exclusionReason) {
|
|
408
|
+
return {
|
|
409
|
+
...entry,
|
|
410
|
+
...mapSafetyReasonToClassification(exclusionReason, normalized.path),
|
|
411
|
+
safety_reason: exclusionReason,
|
|
412
|
+
source: 'safety-policy',
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (omitted) {
|
|
417
|
+
const known = classifyKnownOmission(omitted);
|
|
418
|
+
return {
|
|
419
|
+
...entry,
|
|
420
|
+
...known,
|
|
421
|
+
bytes: omitted.bytes || 0,
|
|
422
|
+
effective_prompt_bytes: known.safe_to_include ? (omitted.bytes || 0) : 0,
|
|
423
|
+
omitted_reason: omitted.reason || null,
|
|
424
|
+
source: 'omitted-files',
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (skipped) {
|
|
429
|
+
return {
|
|
430
|
+
...entry,
|
|
431
|
+
...classifySkippedFile(skipped),
|
|
432
|
+
source: 'skipped-files',
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const absolutePath = path.resolve(repo, normalized.path);
|
|
437
|
+
if (!fs.existsSync(absolutePath)) {
|
|
438
|
+
return {
|
|
439
|
+
...entry,
|
|
440
|
+
classification: RECOVERY_CLASSIFICATION.MISSING_FILE,
|
|
441
|
+
reason: RECOVERY_REASON.MISSING_FILE,
|
|
442
|
+
source: 'filesystem',
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
let stat;
|
|
447
|
+
try {
|
|
448
|
+
stat = fs.statSync(absolutePath);
|
|
449
|
+
} catch (_) {
|
|
450
|
+
return {
|
|
451
|
+
...entry,
|
|
452
|
+
classification: RECOVERY_CLASSIFICATION.UNKNOWN,
|
|
453
|
+
reason: RECOVERY_REASON.UNKNOWN,
|
|
454
|
+
source: 'filesystem',
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (!stat.isFile()) {
|
|
459
|
+
return {
|
|
460
|
+
...entry,
|
|
461
|
+
classification: RECOVERY_CLASSIFICATION.OUTSIDE_SCOPE,
|
|
462
|
+
reason: RECOVERY_REASON.OUTSIDE_SCOPE,
|
|
463
|
+
source: 'filesystem',
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (appearsBinaryFile(absolutePath)) {
|
|
468
|
+
return {
|
|
469
|
+
...entry,
|
|
470
|
+
classification: RECOVERY_CLASSIFICATION.GENERATED_OR_DEPENDENCY,
|
|
471
|
+
reason: RECOVERY_REASON.GENERATED_OR_DEPENDENCY,
|
|
472
|
+
bytes: stat.size,
|
|
473
|
+
source: 'filesystem',
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
...entry,
|
|
479
|
+
classification: RECOVERY_CLASSIFICATION.SAFE_TO_INCLUDE,
|
|
480
|
+
reason: RECOVERY_REASON.NOT_DISCOVERED,
|
|
481
|
+
safe_to_include: true,
|
|
482
|
+
content_allowed: true,
|
|
483
|
+
bytes: stat.size,
|
|
484
|
+
effective_prompt_bytes: stat.size,
|
|
485
|
+
source: 'filesystem',
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function extractEvidencePathFromIssue(issue) {
|
|
490
|
+
if (!issue || typeof issue !== 'object') {
|
|
491
|
+
return '';
|
|
492
|
+
}
|
|
493
|
+
if (typeof issue.evidence_path === 'string') {
|
|
494
|
+
return issue.evidence_path;
|
|
495
|
+
}
|
|
496
|
+
const message = String(issue.message || '');
|
|
497
|
+
const match = message.match(/evidence path is not in the selected sample:\s*(.+)$/i);
|
|
498
|
+
return match ? match[1].trim() : '';
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function summarizeClassifications(classifications) {
|
|
502
|
+
const summary = {};
|
|
503
|
+
for (const item of classifications) {
|
|
504
|
+
summary[item.classification] = (summary[item.classification] || 0) + 1;
|
|
505
|
+
}
|
|
506
|
+
return Object.keys(summary)
|
|
507
|
+
.sort()
|
|
508
|
+
.reduce((acc, key) => {
|
|
509
|
+
acc[key] = summary[key];
|
|
510
|
+
return acc;
|
|
511
|
+
}, {});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function normalizePositiveInteger(value, fallback = 0) {
|
|
515
|
+
const parsed = Number.parseInt(value, 10);
|
|
516
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function roundUpToBlock(value, blockSize) {
|
|
520
|
+
const normalizedValue = Math.max(0, Number(value) || 0);
|
|
521
|
+
const normalizedBlock = Math.max(1, Number(blockSize) || 1);
|
|
522
|
+
return Math.ceil(normalizedValue / normalizedBlock) * normalizedBlock;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function classifyRecommendedFlag(item, options = {}) {
|
|
526
|
+
const omittedReason = String(item.omitted_reason || '');
|
|
527
|
+
if (omittedReason === 'option:tests-disabled' && options.includeTests !== true) {
|
|
528
|
+
return '--include-tests';
|
|
529
|
+
}
|
|
530
|
+
if (omittedReason === 'option:db-disabled' && options.includeDb !== true) {
|
|
531
|
+
return '--include-db';
|
|
532
|
+
}
|
|
533
|
+
if (omittedReason === 'option:source-disabled' && options.includeSource !== true && options.deep !== true) {
|
|
534
|
+
return '--include-source';
|
|
535
|
+
}
|
|
536
|
+
return '';
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function uniqueSorted(values) {
|
|
540
|
+
return [...new Set(values.filter(Boolean))].sort();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function calculateRecoveryBudget(classificationResult, options = {}) {
|
|
544
|
+
const classifications = Array.isArray(classificationResult?.classifications)
|
|
545
|
+
? classificationResult.classifications
|
|
546
|
+
: Array.isArray(classificationResult)
|
|
547
|
+
? classificationResult
|
|
548
|
+
: [];
|
|
549
|
+
const budgets = options.budgets || {};
|
|
550
|
+
const caps = {
|
|
551
|
+
maxFiles: normalizePositiveInteger(options.caps?.maxFiles, DEFAULT_RECOVERY_MAX_FILES_CAP),
|
|
552
|
+
maxBytes: normalizePositiveInteger(options.caps?.maxBytes, DEFAULT_RECOVERY_MAX_BYTES_CAP),
|
|
553
|
+
};
|
|
554
|
+
const currentMaxFiles = normalizePositiveInteger(
|
|
555
|
+
budgets.max_files ?? budgets.maxFiles ?? options.maxFiles,
|
|
556
|
+
0,
|
|
557
|
+
);
|
|
558
|
+
const currentMaxBytes = normalizePositiveInteger(
|
|
559
|
+
budgets.max_bytes ?? budgets.maxBytes ?? options.maxBytes,
|
|
560
|
+
0,
|
|
561
|
+
);
|
|
562
|
+
const currentSelectedFiles = normalizePositiveInteger(
|
|
563
|
+
budgets.selected_files ?? budgets.selectedFiles,
|
|
564
|
+
0,
|
|
565
|
+
);
|
|
566
|
+
const currentSelectedBytes = normalizePositiveInteger(
|
|
567
|
+
budgets.selected_bytes ?? budgets.selectedBytes,
|
|
568
|
+
0,
|
|
569
|
+
);
|
|
570
|
+
const safeMissing = classifications.filter((item) => item.safe_to_include === true && item.selected !== true);
|
|
571
|
+
const safeMissingBytes = safeMissing.reduce((sum, item) => (
|
|
572
|
+
sum + normalizePositiveInteger(item.effective_prompt_bytes || item.bytes, 0)
|
|
573
|
+
), 0);
|
|
574
|
+
const safeMissingFiles = safeMissing.length;
|
|
575
|
+
const safetyMarginBytes = safeMissingBytes > 0
|
|
576
|
+
? Math.max(RECOVERY_MIN_BYTE_MARGIN, Math.ceil(safeMissingBytes * 0.25))
|
|
577
|
+
: 0;
|
|
578
|
+
const calculatedMaxBytes = safeMissingBytes > 0
|
|
579
|
+
? roundUpToBlock(currentSelectedBytes + safeMissingBytes + safetyMarginBytes, RECOVERY_BYTE_ROUNDING)
|
|
580
|
+
: currentMaxBytes;
|
|
581
|
+
const calculatedMaxFiles = safeMissingFiles > 0
|
|
582
|
+
? currentSelectedFiles + safeMissingFiles + RECOVERY_FILE_MARGIN
|
|
583
|
+
: currentMaxFiles;
|
|
584
|
+
const recommendedMaxFiles = Math.max(currentMaxFiles, calculatedMaxFiles);
|
|
585
|
+
const recommendedMaxBytes = Math.max(currentMaxBytes, calculatedMaxBytes);
|
|
586
|
+
const recommendedFlags = uniqueSorted(safeMissing.map((item) => classifyRecommendedFlag(item, options)));
|
|
587
|
+
const exceedsCaps = recommendedMaxFiles > caps.maxFiles || recommendedMaxBytes > caps.maxBytes;
|
|
588
|
+
|
|
589
|
+
return {
|
|
590
|
+
current_max_files: currentMaxFiles,
|
|
591
|
+
current_max_bytes: currentMaxBytes,
|
|
592
|
+
current_selected_files: currentSelectedFiles,
|
|
593
|
+
current_selected_bytes: currentSelectedBytes,
|
|
594
|
+
safe_missing_files: safeMissingFiles,
|
|
595
|
+
safe_missing_bytes: safeMissingBytes,
|
|
596
|
+
safety_margin_bytes: safetyMarginBytes,
|
|
597
|
+
recommended_max_files: recommendedMaxFiles,
|
|
598
|
+
recommended_max_bytes: recommendedMaxBytes,
|
|
599
|
+
recommended_flags: recommendedFlags,
|
|
600
|
+
caps,
|
|
601
|
+
exceeds_caps: exceedsCaps,
|
|
602
|
+
recommendation_type: exceedsCaps
|
|
603
|
+
? 'scope_required'
|
|
604
|
+
: safeMissingFiles > 0 || recommendedFlags.length > 0
|
|
605
|
+
? 'increase_budget'
|
|
606
|
+
: 'inspect_context',
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function shellToken(value) {
|
|
611
|
+
const raw = String(value || '');
|
|
612
|
+
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(raw)) {
|
|
613
|
+
return raw;
|
|
614
|
+
}
|
|
615
|
+
return `'${raw.replace(/'/g, "'\\''")}'`;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function appendBooleanFlag(tokens, flag, enabled) {
|
|
619
|
+
if (enabled === true) {
|
|
620
|
+
tokens.push(flag);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function appendValueFlag(tokens, flag, value) {
|
|
625
|
+
if (value !== undefined && value !== null && String(value).trim() !== '') {
|
|
626
|
+
tokens.push(flag, shellToken(value));
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function buildAnalyzeProjectRecoveryCommand(recommendation, options = {}) {
|
|
631
|
+
if (
|
|
632
|
+
!recommendation ||
|
|
633
|
+
recommendation.recommendation_type === 'scope_required' ||
|
|
634
|
+
recommendation.recommendation_type === 'inspect_context'
|
|
635
|
+
) {
|
|
636
|
+
return '';
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const commandPrefix = Array.isArray(options.commandPrefix) && options.commandPrefix.length > 0
|
|
640
|
+
? options.commandPrefix
|
|
641
|
+
: ['npx', '--yes', 'create-quiver@latest'];
|
|
642
|
+
const tokens = [...commandPrefix.map(shellToken), 'ai', 'analyze-project'];
|
|
643
|
+
appendBooleanFlag(tokens, '--deep', options.deep === true);
|
|
644
|
+
appendValueFlag(tokens, '--max-files', recommendation.recommended_max_files);
|
|
645
|
+
appendValueFlag(tokens, '--max-bytes', recommendation.recommended_max_bytes);
|
|
646
|
+
|
|
647
|
+
for (const flag of recommendation.recommended_flags || []) {
|
|
648
|
+
tokens.push(flag);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
appendBooleanFlag(tokens, '--include-tests', options.includeTests === true && !(recommendation.recommended_flags || []).includes('--include-tests'));
|
|
652
|
+
appendBooleanFlag(tokens, '--include-db', options.includeDb === true && !(recommendation.recommended_flags || []).includes('--include-db'));
|
|
653
|
+
appendBooleanFlag(tokens, '--include-source', options.includeSource === true && options.deep !== true && !(recommendation.recommended_flags || []).includes('--include-source'));
|
|
654
|
+
appendValueFlag(tokens, '--scope', options.scope);
|
|
655
|
+
appendValueFlag(tokens, '--provider', options.provider);
|
|
656
|
+
appendValueFlag(tokens, '--model', options.model);
|
|
657
|
+
appendValueFlag(tokens, '--lang', options.lang);
|
|
658
|
+
appendBooleanFlag(tokens, '--strict', options.strict === true);
|
|
659
|
+
|
|
660
|
+
return tokens.join(' ');
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function buildEvidenceRecoveryPayload(classificationResult, options = {}) {
|
|
664
|
+
const budget = calculateRecoveryBudget(classificationResult, options);
|
|
665
|
+
const command = buildAnalyzeProjectRecoveryCommand(budget, options);
|
|
666
|
+
const available = budget.recommendation_type !== 'inspect_context' && budget.exceeds_caps !== true && Boolean(command);
|
|
667
|
+
return {
|
|
668
|
+
schema_version: RECOVERY_SCHEMA_VERSION,
|
|
669
|
+
available,
|
|
670
|
+
reason: 'evidence_not_selected',
|
|
671
|
+
recommendation_type: budget.recommendation_type,
|
|
672
|
+
command,
|
|
673
|
+
budget,
|
|
674
|
+
evidence_summary: classificationResult?.summary || summarizeClassifications(classificationResult?.classifications || []),
|
|
675
|
+
warnings: budget.exceeds_caps
|
|
676
|
+
? ['recommended context budget exceeds configured recovery caps; reduce scope before rerunning']
|
|
677
|
+
: [],
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function classifyEvidenceRecoveryIssues(repoRoot, issues = [], options = {}) {
|
|
682
|
+
const byEvidencePath = new Map();
|
|
683
|
+
for (const issue of Array.isArray(issues) ? issues : []) {
|
|
684
|
+
if ((issue.issue || issue.code) !== 'evidence-not-selected') {
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
const evidencePath = extractEvidencePathFromIssue(issue);
|
|
688
|
+
if (!evidencePath) {
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
const normalized = normalizeRelativeEvidencePath(evidencePath);
|
|
692
|
+
const key = normalized.path || evidencePath;
|
|
693
|
+
if (!byEvidencePath.has(key)) {
|
|
694
|
+
byEvidencePath.set(key, {
|
|
695
|
+
evidencePath,
|
|
696
|
+
issuePaths: [],
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
const current = byEvidencePath.get(key);
|
|
700
|
+
if (issue.path) {
|
|
701
|
+
current.issuePaths.push(issue.path);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const classifications = Array.from(byEvidencePath.values())
|
|
706
|
+
.sort((a, b) => a.evidencePath.localeCompare(b.evidencePath))
|
|
707
|
+
.map((item) => classifyEvidencePath(repoRoot, item.evidencePath, {
|
|
708
|
+
...options,
|
|
709
|
+
issuePaths: item.issuePaths,
|
|
710
|
+
}));
|
|
711
|
+
|
|
712
|
+
return {
|
|
713
|
+
schema_version: RECOVERY_SCHEMA_VERSION,
|
|
714
|
+
kind: 'quiver-analyze-project-evidence-recovery-classification',
|
|
715
|
+
reason: 'evidence_not_selected',
|
|
716
|
+
classifications,
|
|
717
|
+
summary: summarizeClassifications(classifications),
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
module.exports = {
|
|
722
|
+
DEFAULT_RECOVERY_MAX_BYTES_CAP,
|
|
723
|
+
DEFAULT_RECOVERY_MAX_FILES_CAP,
|
|
724
|
+
RECOVERY_CLASSIFICATION,
|
|
725
|
+
RECOVERY_REASON,
|
|
726
|
+
RECOVERY_SCHEMA_VERSION,
|
|
727
|
+
buildAnalyzeProjectRecoveryCommand,
|
|
728
|
+
buildEvidenceRecoveryPayload,
|
|
729
|
+
calculateRecoveryBudget,
|
|
730
|
+
classifyEvidencePath,
|
|
731
|
+
classifyEvidenceRecoveryIssues,
|
|
732
|
+
extractEvidencePathFromIssue,
|
|
733
|
+
isMetadataOnlyPath,
|
|
734
|
+
normalizeRelativeEvidencePath,
|
|
735
|
+
summarizeClassifications,
|
|
736
|
+
};
|