create-quiver 0.17.2 → 0.17.3
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/CHANGELOG.md +4 -0
- package/docs/CLI_UX_GUIDE.md +13 -3
- package/docs/TROUBLESHOOTING.md +43 -2
- package/docs/reference/commands.md +31 -9
- package/docs/workflows/existing-project.md +18 -3
- package/package.json +1 -1
- package/specs/quiver-v43-cli-i18n-audit-release-readiness/command-language-mode-matrix.json +4 -0
- package/specs/quiver-v55-analyze-project-doc-apply-ux/EVIDENCE_REPORT.md +209 -0
- package/specs/quiver-v55-analyze-project-doc-apply-ux/EXECUTION_PLAN.md +49 -0
- package/specs/quiver-v55-analyze-project-doc-apply-ux/SPEC.md +276 -0
- package/specs/quiver-v55-analyze-project-doc-apply-ux/STATUS.md +29 -0
- package/specs/quiver-v55-analyze-project-doc-apply-ux/slices/slice-01-cli-proposal-contract/CLOSURE_BRIEF.md +47 -0
- package/specs/quiver-v55-analyze-project-doc-apply-ux/slices/slice-01-cli-proposal-contract/EXECUTION_BRIEF.md +67 -0
- package/specs/quiver-v55-analyze-project-doc-apply-ux/slices/slice-01-cli-proposal-contract/pr.md +129 -0
- package/specs/quiver-v55-analyze-project-doc-apply-ux/slices/slice-01-cli-proposal-contract/slice.json +82 -0
- package/specs/quiver-v55-analyze-project-doc-apply-ux/slices/slice-02-save-proposal-flow/CLOSURE_BRIEF.md +39 -0
- package/specs/quiver-v55-analyze-project-doc-apply-ux/slices/slice-02-save-proposal-flow/EXECUTION_BRIEF.md +61 -0
- package/specs/quiver-v55-analyze-project-doc-apply-ux/slices/slice-02-save-proposal-flow/pr.md +116 -0
- package/specs/quiver-v55-analyze-project-doc-apply-ux/slices/slice-02-save-proposal-flow/slice.json +69 -0
- package/specs/quiver-v55-analyze-project-doc-apply-ux/slices/slice-03-noninteractive-apply-engine/CLOSURE_BRIEF.md +36 -0
- package/specs/quiver-v55-analyze-project-doc-apply-ux/slices/slice-03-noninteractive-apply-engine/EXECUTION_BRIEF.md +62 -0
- package/specs/quiver-v55-analyze-project-doc-apply-ux/slices/slice-03-noninteractive-apply-engine/pr.md +132 -0
- package/specs/quiver-v55-analyze-project-doc-apply-ux/slices/slice-03-noninteractive-apply-engine/slice.json +76 -0
- package/specs/quiver-v55-analyze-project-doc-apply-ux/slices/slice-04-interactive-apply-ux/CLOSURE_BRIEF.md +47 -0
- package/specs/quiver-v55-analyze-project-doc-apply-ux/slices/slice-04-interactive-apply-ux/EXECUTION_BRIEF.md +70 -0
- package/specs/quiver-v55-analyze-project-doc-apply-ux/slices/slice-04-interactive-apply-ux/pr.md +141 -0
- package/specs/quiver-v55-analyze-project-doc-apply-ux/slices/slice-04-interactive-apply-ux/slice.json +72 -0
- package/specs/quiver-v55-analyze-project-doc-apply-ux/slices/slice-05-apply-saved-proposal/CLOSURE_BRIEF.md +42 -0
- package/specs/quiver-v55-analyze-project-doc-apply-ux/slices/slice-05-apply-saved-proposal/EXECUTION_BRIEF.md +61 -0
- package/specs/quiver-v55-analyze-project-doc-apply-ux/slices/slice-05-apply-saved-proposal/pr.md +129 -0
- package/specs/quiver-v55-analyze-project-doc-apply-ux/slices/slice-05-apply-saved-proposal/slice.json +67 -0
- package/specs/quiver-v55-analyze-project-doc-apply-ux/slices/slice-06-i18n-docs-release-smoke/CLOSURE_BRIEF.md +38 -0
- package/specs/quiver-v55-analyze-project-doc-apply-ux/slices/slice-06-i18n-docs-release-smoke/EXECUTION_BRIEF.md +64 -0
- package/specs/quiver-v55-analyze-project-doc-apply-ux/slices/slice-06-i18n-docs-release-smoke/pr.md +123 -0
- package/specs/quiver-v55-analyze-project-doc-apply-ux/slices/slice-06-i18n-docs-release-smoke/slice.json +76 -0
- package/src/create-quiver/commands/ai.js +636 -66
- package/src/create-quiver/index.js +80 -1
- package/src/create-quiver/lib/ai/analyze-project-apply.js +269 -0
- package/src/create-quiver/lib/ai/analyze-project-interactive.js +241 -0
- package/src/create-quiver/lib/ai/analyze-project-proposal.js +582 -0
- package/src/create-quiver/lib/ai/analyze-project-validation.js +10 -8
- package/src/create-quiver/lib/cli/ux-flags.js +6 -0
- package/src/create-quiver/lib/i18n/messages/en.js +28 -0
- package/src/create-quiver/lib/i18n/messages/es.js +29 -0
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
const crypto = require('node:crypto');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
const { z } = require('zod');
|
|
6
|
+
|
|
7
|
+
const { parseAnalyzeProjectDocProposal } = require('./analyze-project-docs');
|
|
8
|
+
const { validateProjectRelativePath } = require('../paths');
|
|
9
|
+
|
|
10
|
+
const ANALYZE_PROJECT_PROPOSAL_MANIFEST_SCHEMA_VERSION = 1;
|
|
11
|
+
const ANALYZE_PROJECT_PROPOSAL_MANIFEST_KIND = 'quiver-analyze-project-doc-proposal';
|
|
12
|
+
const ANALYZE_PROJECT_WRITE_MANIFEST_SCHEMA_VERSION = 1;
|
|
13
|
+
const ANALYZE_PROJECT_WRITE_MANIFEST_KIND = 'quiver-analyze-project-doc-writes';
|
|
14
|
+
|
|
15
|
+
const PROPOSAL_ARTIFACT_FILENAMES = Object.freeze({
|
|
16
|
+
json: 'analyze-project-doc-proposal.json',
|
|
17
|
+
markdown: 'analyze-project-doc-proposal.md',
|
|
18
|
+
diff: 'analyze-project-doc-proposal.diff',
|
|
19
|
+
manifest: 'manifest.json',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function formatError(message) {
|
|
23
|
+
return `create-quiver: ${message}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class AnalyzeProjectProposalContractError extends Error {
|
|
27
|
+
constructor(message, issues = []) {
|
|
28
|
+
super(formatError(message));
|
|
29
|
+
this.name = 'AnalyzeProjectProposalContractError';
|
|
30
|
+
this.code = 'AI_ANALYZE_PROJECT_PROPOSAL_CONTRACT_INVALID';
|
|
31
|
+
this.issues = issues;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeArtifactPath(value, fieldName = 'artifact path') {
|
|
36
|
+
try {
|
|
37
|
+
return validateProjectRelativePath(value, fieldName);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
throw new AnalyzeProjectProposalContractError(`${fieldName} is not a safe project-relative path`, [
|
|
40
|
+
{ path: fieldName, issue: 'invalid-project-relative-path', message: error.message },
|
|
41
|
+
]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function toPosix(filePath) {
|
|
46
|
+
return String(filePath || '').replace(/\\/g, '/');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function toRelativePosix(root, filePath) {
|
|
50
|
+
return toPosix(path.relative(root, filePath));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sha256(contents) {
|
|
54
|
+
return crypto.createHash('sha256').update(String(contents || ''), 'utf8').digest('hex');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeAnalyzeProjectProposalRunId(value) {
|
|
58
|
+
const runId = String(value || '').trim();
|
|
59
|
+
if (!runId) {
|
|
60
|
+
throw new AnalyzeProjectProposalContractError('analyze-project proposal run id is required', [
|
|
61
|
+
{ path: 'run_id', issue: 'missing-run-id', message: 'run_id is required' },
|
|
62
|
+
]);
|
|
63
|
+
}
|
|
64
|
+
if (runId === '.' || runId === '..' || runId.includes('/') || runId.includes('\\') || runId.includes('\0')) {
|
|
65
|
+
throw new AnalyzeProjectProposalContractError('analyze-project proposal run id is not safe', [
|
|
66
|
+
{ path: 'run_id', issue: 'unsafe-run-id', message: 'run_id must be a single path segment' },
|
|
67
|
+
]);
|
|
68
|
+
}
|
|
69
|
+
if (!/^[A-Za-z0-9._-]+$/.test(runId)) {
|
|
70
|
+
throw new AnalyzeProjectProposalContractError('analyze-project proposal run id is not safe', [
|
|
71
|
+
{ path: 'run_id', issue: 'unsafe-run-id', message: 'run_id can contain only letters, numbers, dots, underscores, and hyphens' },
|
|
72
|
+
]);
|
|
73
|
+
}
|
|
74
|
+
return runId;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function writeJsonFile(filePath, value) {
|
|
78
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
79
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function writeTextFile(filePath, value) {
|
|
83
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
84
|
+
fs.writeFileSync(filePath, String(value || ''));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readJsonFile(filePath, description) {
|
|
88
|
+
if (!fs.existsSync(filePath)) {
|
|
89
|
+
throw new AnalyzeProjectProposalContractError(`missing analyze-project ${description}`, [
|
|
90
|
+
{ path: toPosix(filePath), issue: 'missing-artifact', message: `${description} does not exist` },
|
|
91
|
+
]);
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
95
|
+
} catch (error) {
|
|
96
|
+
throw new AnalyzeProjectProposalContractError(`invalid analyze-project ${description}`, [
|
|
97
|
+
{ path: toPosix(filePath), issue: 'malformed-json', message: error.message },
|
|
98
|
+
]);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function artifactPathFromRef(ref) {
|
|
103
|
+
if (typeof ref === 'string') {
|
|
104
|
+
return normalizeArtifactPath(ref, 'artifact ref');
|
|
105
|
+
}
|
|
106
|
+
if (ref && typeof ref.path === 'string') {
|
|
107
|
+
return normalizeArtifactPath(ref.path, 'artifact ref');
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function relativePathSchema(fieldName) {
|
|
113
|
+
return z.string().trim().min(1).transform((value) => normalizeArtifactPath(value, fieldName));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const proposalManifestSchema = z.object({
|
|
117
|
+
schema_version: z.literal(ANALYZE_PROJECT_PROPOSAL_MANIFEST_SCHEMA_VERSION),
|
|
118
|
+
kind: z.literal(ANALYZE_PROJECT_PROPOSAL_MANIFEST_KIND),
|
|
119
|
+
run_id: z.string().trim().min(1),
|
|
120
|
+
created_at: z.string().trim().min(1),
|
|
121
|
+
language: z.enum(['en', 'es']),
|
|
122
|
+
provider: z.string().trim().min(1).nullable(),
|
|
123
|
+
proposal_json: relativePathSchema('proposal_json'),
|
|
124
|
+
proposal_markdown: relativePathSchema('proposal_markdown'),
|
|
125
|
+
proposal_diff: relativePathSchema('proposal_diff'),
|
|
126
|
+
selected_context_manifest: relativePathSchema('selected_context_manifest'),
|
|
127
|
+
repair_manifest: relativePathSchema('repair_manifest').nullable(),
|
|
128
|
+
doc_paths: z.array(relativePathSchema('doc_paths')).default([]),
|
|
129
|
+
doc_before_hashes: z.record(z.string().trim().min(1), z.string().trim().min(1).nullable()).default({}),
|
|
130
|
+
proposal_sha256: z.string().trim().min(1),
|
|
131
|
+
events: z.array(z.record(z.string(), z.unknown())).default([]),
|
|
132
|
+
}).strict();
|
|
133
|
+
|
|
134
|
+
const writeManifestActionSchema = z.object({
|
|
135
|
+
path: relativePathSchema('actions.path'),
|
|
136
|
+
action: z.enum(['create', 'update', 'skip']),
|
|
137
|
+
before_sha256: z.string().trim().min(1).nullable(),
|
|
138
|
+
after_sha256: z.string().trim().min(1).nullable(),
|
|
139
|
+
snapshot_path: relativePathSchema('actions.snapshot_path').nullable(),
|
|
140
|
+
dirty: z.boolean().default(false),
|
|
141
|
+
status: z.enum(['planned', 'written', 'skipped', 'failed']).default('planned'),
|
|
142
|
+
}).strict();
|
|
143
|
+
|
|
144
|
+
const writeManifestSchema = z.object({
|
|
145
|
+
schema_version: z.literal(ANALYZE_PROJECT_WRITE_MANIFEST_SCHEMA_VERSION),
|
|
146
|
+
kind: z.literal(ANALYZE_PROJECT_WRITE_MANIFEST_KIND),
|
|
147
|
+
run_id: z.string().trim().min(1),
|
|
148
|
+
created_at: z.string().trim().min(1),
|
|
149
|
+
proposal_manifest: relativePathSchema('proposal_manifest'),
|
|
150
|
+
snapshot_root: relativePathSchema('snapshot_root').nullable(),
|
|
151
|
+
actions: z.array(writeManifestActionSchema).default([]),
|
|
152
|
+
validation: z.object({
|
|
153
|
+
ok: z.boolean(),
|
|
154
|
+
strict: z.boolean().default(false),
|
|
155
|
+
errors: z.array(z.record(z.string(), z.unknown())).default([]),
|
|
156
|
+
warnings: z.array(z.record(z.string(), z.unknown())).default([]),
|
|
157
|
+
}).strict(),
|
|
158
|
+
partial_write: z.boolean().default(false),
|
|
159
|
+
events: z.array(z.record(z.string(), z.unknown())).default([]),
|
|
160
|
+
}).strict();
|
|
161
|
+
|
|
162
|
+
function mapZodIssues(error) {
|
|
163
|
+
return error.issues.map((issue) => ({
|
|
164
|
+
path: issue.path.length > 0 ? issue.path.join('.') : null,
|
|
165
|
+
issue: issue.code,
|
|
166
|
+
message: issue.message,
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function normalizeAnalyzeProjectProposalManifest(value) {
|
|
171
|
+
const parsed = proposalManifestSchema.safeParse(value);
|
|
172
|
+
if (!parsed.success) {
|
|
173
|
+
throw new AnalyzeProjectProposalContractError('analyze-project proposal manifest does not match the required schema', mapZodIssues(parsed.error));
|
|
174
|
+
}
|
|
175
|
+
return parsed.data;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function normalizeAnalyzeProjectWriteManifest(value) {
|
|
179
|
+
const parsed = writeManifestSchema.safeParse(value);
|
|
180
|
+
if (!parsed.success) {
|
|
181
|
+
throw new AnalyzeProjectProposalContractError('analyze-project write manifest does not match the required schema', mapZodIssues(parsed.error));
|
|
182
|
+
}
|
|
183
|
+
return parsed.data;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function buildAnalyzeProjectProposalArtifactPaths(runId) {
|
|
187
|
+
const safeRunId = normalizeAnalyzeProjectProposalRunId(runId);
|
|
188
|
+
|
|
189
|
+
const root = `.quiver/runs/${safeRunId}/proposal`;
|
|
190
|
+
return {
|
|
191
|
+
root,
|
|
192
|
+
proposal_json: `${root}/${PROPOSAL_ARTIFACT_FILENAMES.json}`,
|
|
193
|
+
proposal_markdown: `${root}/${PROPOSAL_ARTIFACT_FILENAMES.markdown}`,
|
|
194
|
+
proposal_diff: `${root}/${PROPOSAL_ARTIFACT_FILENAMES.diff}`,
|
|
195
|
+
manifest: `${root}/${PROPOSAL_ARTIFACT_FILENAMES.manifest}`,
|
|
196
|
+
write_manifest: `.quiver/runs/${safeRunId}/writes/analyze-project-doc-writes.json`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function assertSavedProposalManifestMatchesRun(manifest, paths, runId) {
|
|
201
|
+
const issues = [];
|
|
202
|
+
if (manifest.run_id !== runId) {
|
|
203
|
+
issues.push({
|
|
204
|
+
path: 'manifest.run_id',
|
|
205
|
+
issue: 'run-id-mismatch',
|
|
206
|
+
message: `manifest run_id must match requested run id (${runId})`,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
for (const [field, expected] of Object.entries({
|
|
210
|
+
proposal_json: paths.proposal_json,
|
|
211
|
+
proposal_markdown: paths.proposal_markdown,
|
|
212
|
+
proposal_diff: paths.proposal_diff,
|
|
213
|
+
})) {
|
|
214
|
+
if (manifest[field] !== expected) {
|
|
215
|
+
issues.push({
|
|
216
|
+
path: `manifest.${field}`,
|
|
217
|
+
issue: 'artifact-path-mismatch',
|
|
218
|
+
message: `${field} must match ${expected}`,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (issues.length > 0) {
|
|
223
|
+
throw new AnalyzeProjectProposalContractError('saved analyze-project proposal manifest does not match the requested run', issues);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function assertSavedProposalArtifactsExist(repoRoot, manifest) {
|
|
228
|
+
const issues = [];
|
|
229
|
+
for (const [field, relativePath] of Object.entries({
|
|
230
|
+
proposal_json: manifest.proposal_json,
|
|
231
|
+
proposal_markdown: manifest.proposal_markdown,
|
|
232
|
+
proposal_diff: manifest.proposal_diff,
|
|
233
|
+
})) {
|
|
234
|
+
if (!fs.existsSync(path.join(repoRoot, relativePath))) {
|
|
235
|
+
issues.push({
|
|
236
|
+
path: field,
|
|
237
|
+
issue: 'missing-artifact',
|
|
238
|
+
message: `${relativePath} does not exist`,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (issues.length > 0) {
|
|
243
|
+
throw new AnalyzeProjectProposalContractError('saved analyze-project proposal is missing required artifacts', issues);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function assertSavedProposalDocPathsMatchManifest(proposal, manifest) {
|
|
248
|
+
const proposalPaths = (proposal.docs || []).map((doc) => doc.path).sort();
|
|
249
|
+
const manifestPaths = [...(manifest.doc_paths || [])].sort();
|
|
250
|
+
if (JSON.stringify(proposalPaths) === JSON.stringify(manifestPaths)) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
throw new AnalyzeProjectProposalContractError('saved analyze-project proposal docs do not match the proposal manifest', [
|
|
254
|
+
{
|
|
255
|
+
path: 'doc_paths',
|
|
256
|
+
issue: 'doc-paths-mismatch',
|
|
257
|
+
message: 'manual proposal edits may change content, but target doc paths must match the saved manifest',
|
|
258
|
+
expected: manifestPaths,
|
|
259
|
+
actual: proposalPaths,
|
|
260
|
+
},
|
|
261
|
+
]);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function readAnalyzeProjectSavedProposal(repoRoot, runId) {
|
|
265
|
+
const resolvedRunId = normalizeAnalyzeProjectProposalRunId(runId);
|
|
266
|
+
const paths = buildAnalyzeProjectProposalArtifactPaths(resolvedRunId);
|
|
267
|
+
const manifestPath = path.join(repoRoot, paths.manifest);
|
|
268
|
+
const manifest = normalizeAnalyzeProjectProposalManifest(readJsonFile(manifestPath, 'proposal manifest'));
|
|
269
|
+
assertSavedProposalManifestMatchesRun(manifest, paths, resolvedRunId);
|
|
270
|
+
assertSavedProposalArtifactsExist(repoRoot, manifest);
|
|
271
|
+
|
|
272
|
+
const proposalPath = path.join(repoRoot, manifest.proposal_json);
|
|
273
|
+
let proposal;
|
|
274
|
+
try {
|
|
275
|
+
proposal = parseAnalyzeProjectDocProposal(fs.readFileSync(proposalPath, 'utf8'));
|
|
276
|
+
} catch (error) {
|
|
277
|
+
throw new AnalyzeProjectProposalContractError('saved analyze-project proposal JSON is invalid', error.issues || [{
|
|
278
|
+
path: manifest.proposal_json,
|
|
279
|
+
issue: error.code || 'invalid-proposal',
|
|
280
|
+
message: error.message,
|
|
281
|
+
}]);
|
|
282
|
+
}
|
|
283
|
+
assertSavedProposalDocPathsMatchManifest(proposal, manifest);
|
|
284
|
+
|
|
285
|
+
const proposalJson = `${JSON.stringify(proposal, null, 2)}\n`;
|
|
286
|
+
const proposalHash = sha256(proposalJson);
|
|
287
|
+
const proposalEdited = proposalHash !== manifest.proposal_sha256;
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
run_id: resolvedRunId,
|
|
291
|
+
proposal,
|
|
292
|
+
proposal_sha256: proposalHash,
|
|
293
|
+
proposal_edited: proposalEdited,
|
|
294
|
+
manifest,
|
|
295
|
+
artifacts: {
|
|
296
|
+
root: paths.root,
|
|
297
|
+
proposal_json: manifest.proposal_json,
|
|
298
|
+
proposal_markdown: manifest.proposal_markdown,
|
|
299
|
+
proposal_diff: manifest.proposal_diff,
|
|
300
|
+
manifest: paths.manifest,
|
|
301
|
+
proposal_sha256: proposalHash,
|
|
302
|
+
doc_paths: manifest.doc_paths,
|
|
303
|
+
changed_docs: manifest.doc_paths,
|
|
304
|
+
manifest_data: manifest,
|
|
305
|
+
files: [
|
|
306
|
+
manifest.proposal_json,
|
|
307
|
+
manifest.proposal_markdown,
|
|
308
|
+
manifest.proposal_diff,
|
|
309
|
+
paths.manifest,
|
|
310
|
+
],
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function findLatestAnalyzeProjectSavedProposalRun(repoRoot) {
|
|
316
|
+
const runsRoot = path.join(repoRoot, '.quiver', 'runs');
|
|
317
|
+
if (!fs.existsSync(runsRoot)) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const candidates = [];
|
|
322
|
+
for (const entry of fs.readdirSync(runsRoot)) {
|
|
323
|
+
let runId;
|
|
324
|
+
try {
|
|
325
|
+
runId = normalizeAnalyzeProjectProposalRunId(entry);
|
|
326
|
+
} catch {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const manifestPath = path.join(runsRoot, runId, 'proposal', PROPOSAL_ARTIFACT_FILENAMES.manifest);
|
|
330
|
+
if (!fs.existsSync(manifestPath)) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
let timestamp = 0;
|
|
334
|
+
try {
|
|
335
|
+
const manifest = normalizeAnalyzeProjectProposalManifest(readJsonFile(manifestPath, 'proposal manifest'));
|
|
336
|
+
timestamp = Date.parse(manifest.created_at) || fs.statSync(manifestPath).mtimeMs;
|
|
337
|
+
} catch {
|
|
338
|
+
timestamp = fs.statSync(manifestPath).mtimeMs;
|
|
339
|
+
}
|
|
340
|
+
candidates.push({ runId, timestamp });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
candidates.sort((a, b) => {
|
|
344
|
+
if (b.timestamp !== a.timestamp) {
|
|
345
|
+
return b.timestamp - a.timestamp;
|
|
346
|
+
}
|
|
347
|
+
return b.runId.localeCompare(a.runId);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
return candidates[0]?.runId || null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function formatAnalyzeProjectFullDiff(writePlan = []) {
|
|
354
|
+
const lines = [];
|
|
355
|
+
for (const item of writePlan) {
|
|
356
|
+
if (item.action === 'skip') {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
lines.push(`--- ${item.path} (current)`);
|
|
360
|
+
lines.push(`+++ ${item.path} (proposed)`);
|
|
361
|
+
const currentLines = String(item.currentContent || '').split('\n');
|
|
362
|
+
const proposedLines = String(item.proposedContent || '').split('\n');
|
|
363
|
+
for (const line of currentLines) {
|
|
364
|
+
lines.push(`- ${line}`);
|
|
365
|
+
}
|
|
366
|
+
for (const line of proposedLines) {
|
|
367
|
+
lines.push(`+ ${line}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return `${(lines.length > 0 ? lines : ['- no changes']).join('\n')}\n`;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function formatAnalyzeProjectProposalSummary({ runId, provider, proposal, writePlan, artifacts } = {}) {
|
|
374
|
+
const changed = (writePlan || []).filter((item) => item.action !== 'skip');
|
|
375
|
+
const dirty = changed.filter((item) => item.dirty);
|
|
376
|
+
const docs = Array.isArray(proposal?.docs) ? proposal.docs : [];
|
|
377
|
+
const lines = [
|
|
378
|
+
'# Analyze Project Documentation Proposal',
|
|
379
|
+
'',
|
|
380
|
+
`Run: ${runId}`,
|
|
381
|
+
`Provider: ${provider || 'unknown'}`,
|
|
382
|
+
`Proposed docs: ${docs.length}`,
|
|
383
|
+
`Docs with changes: ${changed.length}`,
|
|
384
|
+
`Dirty existing docs: ${dirty.length}`,
|
|
385
|
+
'',
|
|
386
|
+
'## Files',
|
|
387
|
+
];
|
|
388
|
+
|
|
389
|
+
for (const item of writePlan || []) {
|
|
390
|
+
lines.push(`- ${item.path}: ${item.action}${item.dirty ? ' (dirty existing doc)' : ''}`);
|
|
391
|
+
}
|
|
392
|
+
if (!Array.isArray(writePlan) || writePlan.length === 0) {
|
|
393
|
+
lines.push('- none');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
lines.push('', '## Artifacts');
|
|
397
|
+
lines.push(`- Proposal JSON: ${artifacts.proposal_json}`);
|
|
398
|
+
lines.push(`- Diff: ${artifacts.proposal_diff}`);
|
|
399
|
+
lines.push(`- Manifest: ${artifacts.manifest}`);
|
|
400
|
+
lines.push('', 'This summary intentionally omits full proposed document contents. Use the JSON or diff artifact for details.');
|
|
401
|
+
|
|
402
|
+
return `${lines.join('\n')}\n`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function writeAnalyzeProjectProposalArtifacts(repoRoot, options = {}) {
|
|
406
|
+
const runId = String(options.runId || '').trim();
|
|
407
|
+
if (!runId) {
|
|
408
|
+
throw new AnalyzeProjectProposalContractError('analyze-project proposal artifacts require a run id', [
|
|
409
|
+
{ path: 'run_id', issue: 'missing-run-id', message: 'run_id is required' },
|
|
410
|
+
]);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const now = options.now instanceof Date ? options.now : new Date(options.now || Date.now());
|
|
414
|
+
const provider = options.provider ? String(options.provider) : null;
|
|
415
|
+
const language = options.language === 'es' ? 'es' : 'en';
|
|
416
|
+
const proposal = options.proposal || { schema_version: 1, kind: 'quiver-analyze-project-doc-proposal', summary: '', docs: [] };
|
|
417
|
+
const writePlan = Array.isArray(options.writePlan) ? options.writePlan : [];
|
|
418
|
+
const paths = buildAnalyzeProjectProposalArtifactPaths(runId);
|
|
419
|
+
const proposalJson = `${JSON.stringify(proposal, null, 2)}\n`;
|
|
420
|
+
const proposalHash = sha256(proposalJson);
|
|
421
|
+
const docBeforeHashes = {};
|
|
422
|
+
|
|
423
|
+
for (const item of writePlan) {
|
|
424
|
+
docBeforeHashes[item.path] = item.before_sha256 || null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const selectedContextPath = artifactPathFromRef(options.selectedContextManifest)
|
|
428
|
+
|| `.quiver/runs/${runId}/context/selected-context.json`;
|
|
429
|
+
const repairPath = artifactPathFromRef(options.repairManifest);
|
|
430
|
+
const manifest = normalizeAnalyzeProjectProposalManifest({
|
|
431
|
+
schema_version: ANALYZE_PROJECT_PROPOSAL_MANIFEST_SCHEMA_VERSION,
|
|
432
|
+
kind: ANALYZE_PROJECT_PROPOSAL_MANIFEST_KIND,
|
|
433
|
+
run_id: runId,
|
|
434
|
+
created_at: now.toISOString(),
|
|
435
|
+
language,
|
|
436
|
+
provider,
|
|
437
|
+
proposal_json: paths.proposal_json,
|
|
438
|
+
proposal_markdown: paths.proposal_markdown,
|
|
439
|
+
proposal_diff: paths.proposal_diff,
|
|
440
|
+
selected_context_manifest: selectedContextPath,
|
|
441
|
+
repair_manifest: repairPath,
|
|
442
|
+
doc_paths: writePlan.map((item) => item.path),
|
|
443
|
+
doc_before_hashes: docBeforeHashes,
|
|
444
|
+
proposal_sha256: proposalHash,
|
|
445
|
+
events: [
|
|
446
|
+
{
|
|
447
|
+
type: 'proposal-saved',
|
|
448
|
+
at: now.toISOString(),
|
|
449
|
+
doc_count: writePlan.length,
|
|
450
|
+
changed_count: writePlan.filter((item) => item.action !== 'skip').length,
|
|
451
|
+
},
|
|
452
|
+
...(Array.isArray(options.events) ? options.events : []),
|
|
453
|
+
],
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
const absolutePaths = {
|
|
457
|
+
proposalJson: path.join(repoRoot, paths.proposal_json),
|
|
458
|
+
proposalMarkdown: path.join(repoRoot, paths.proposal_markdown),
|
|
459
|
+
proposalDiff: path.join(repoRoot, paths.proposal_diff),
|
|
460
|
+
manifest: path.join(repoRoot, paths.manifest),
|
|
461
|
+
};
|
|
462
|
+
const diff = formatAnalyzeProjectFullDiff(writePlan);
|
|
463
|
+
const summary = formatAnalyzeProjectProposalSummary({
|
|
464
|
+
runId,
|
|
465
|
+
provider,
|
|
466
|
+
proposal,
|
|
467
|
+
writePlan,
|
|
468
|
+
artifacts: paths,
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
writeTextFile(absolutePaths.proposalJson, proposalJson);
|
|
472
|
+
writeTextFile(absolutePaths.proposalMarkdown, summary);
|
|
473
|
+
writeTextFile(absolutePaths.proposalDiff, diff);
|
|
474
|
+
writeJsonFile(absolutePaths.manifest, manifest);
|
|
475
|
+
|
|
476
|
+
return {
|
|
477
|
+
root: paths.root,
|
|
478
|
+
proposal_json: paths.proposal_json,
|
|
479
|
+
proposal_markdown: paths.proposal_markdown,
|
|
480
|
+
proposal_diff: paths.proposal_diff,
|
|
481
|
+
manifest: paths.manifest,
|
|
482
|
+
proposal_sha256: proposalHash,
|
|
483
|
+
doc_paths: manifest.doc_paths,
|
|
484
|
+
changed_docs: writePlan.filter((item) => item.action !== 'skip').map((item) => item.path),
|
|
485
|
+
manifest_data: manifest,
|
|
486
|
+
files: [
|
|
487
|
+
paths.proposal_json,
|
|
488
|
+
paths.proposal_markdown,
|
|
489
|
+
paths.proposal_diff,
|
|
490
|
+
paths.manifest,
|
|
491
|
+
],
|
|
492
|
+
absolute_files: Object.values(absolutePaths).map((filePath) => toRelativePosix(repoRoot, filePath)),
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function writeAnalyzeProjectWriteManifest(repoRoot, options = {}) {
|
|
497
|
+
const runId = String(options.runId || '').trim();
|
|
498
|
+
if (!runId) {
|
|
499
|
+
throw new AnalyzeProjectProposalContractError('analyze-project write manifest requires a run id', [
|
|
500
|
+
{ path: 'run_id', issue: 'missing-run-id', message: 'run_id is required' },
|
|
501
|
+
]);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const now = options.now instanceof Date ? options.now : new Date(options.now || Date.now());
|
|
505
|
+
const paths = buildAnalyzeProjectProposalArtifactPaths(runId);
|
|
506
|
+
const writePlan = Array.isArray(options.writePlan) ? options.writePlan : [];
|
|
507
|
+
const writtenDocs = new Set(Array.isArray(options.writtenDocs) ? options.writtenDocs : []);
|
|
508
|
+
const snapshotEntries = new Map(
|
|
509
|
+
(options.snapshot?.entries || []).map((entry) => [entry.path, entry]),
|
|
510
|
+
);
|
|
511
|
+
const actionStatus = options.status || 'planned';
|
|
512
|
+
const actions = writePlan.map((item) => {
|
|
513
|
+
const snapshotEntry = snapshotEntries.get(item.path);
|
|
514
|
+
let status = item.action === 'skip' ? 'skipped' : actionStatus;
|
|
515
|
+
if (actionStatus === 'completed') {
|
|
516
|
+
status = item.action === 'skip' ? 'skipped' : writtenDocs.has(item.path) ? 'written' : 'failed';
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
path: item.path,
|
|
521
|
+
action: item.action,
|
|
522
|
+
before_sha256: item.before_sha256 || null,
|
|
523
|
+
after_sha256: item.after_sha256 || null,
|
|
524
|
+
snapshot_path: snapshotEntry?.snapshot_path || null,
|
|
525
|
+
dirty: item.dirty === true,
|
|
526
|
+
status,
|
|
527
|
+
};
|
|
528
|
+
});
|
|
529
|
+
const validation = options.validation || {
|
|
530
|
+
ok: false,
|
|
531
|
+
strict: options.strict === true,
|
|
532
|
+
errors: [],
|
|
533
|
+
warnings: [],
|
|
534
|
+
};
|
|
535
|
+
const manifest = normalizeAnalyzeProjectWriteManifest({
|
|
536
|
+
schema_version: ANALYZE_PROJECT_WRITE_MANIFEST_SCHEMA_VERSION,
|
|
537
|
+
kind: ANALYZE_PROJECT_WRITE_MANIFEST_KIND,
|
|
538
|
+
run_id: runId,
|
|
539
|
+
created_at: now.toISOString(),
|
|
540
|
+
proposal_manifest: options.proposalManifest || paths.manifest,
|
|
541
|
+
snapshot_root: options.snapshot?.root || null,
|
|
542
|
+
actions,
|
|
543
|
+
validation: {
|
|
544
|
+
ok: validation.ok === true,
|
|
545
|
+
strict: validation.strict === true,
|
|
546
|
+
errors: Array.isArray(validation.errors) ? validation.errors : [],
|
|
547
|
+
warnings: Array.isArray(validation.warnings) ? validation.warnings : [],
|
|
548
|
+
},
|
|
549
|
+
partial_write: options.partialWrite === true,
|
|
550
|
+
events: Array.isArray(options.events) ? options.events : [],
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
const manifestPath = path.join(repoRoot, paths.write_manifest);
|
|
554
|
+
writeJsonFile(manifestPath, manifest);
|
|
555
|
+
|
|
556
|
+
return {
|
|
557
|
+
path: paths.write_manifest,
|
|
558
|
+
root: path.dirname(paths.write_manifest).split(path.sep).join('/'),
|
|
559
|
+
manifest_data: manifest,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
module.exports = {
|
|
564
|
+
ANALYZE_PROJECT_PROPOSAL_MANIFEST_KIND,
|
|
565
|
+
ANALYZE_PROJECT_PROPOSAL_MANIFEST_SCHEMA_VERSION,
|
|
566
|
+
ANALYZE_PROJECT_WRITE_MANIFEST_KIND,
|
|
567
|
+
ANALYZE_PROJECT_WRITE_MANIFEST_SCHEMA_VERSION,
|
|
568
|
+
AnalyzeProjectProposalContractError,
|
|
569
|
+
PROPOSAL_ARTIFACT_FILENAMES,
|
|
570
|
+
buildAnalyzeProjectProposalArtifactPaths,
|
|
571
|
+
findLatestAnalyzeProjectSavedProposalRun,
|
|
572
|
+
formatAnalyzeProjectFullDiff,
|
|
573
|
+
formatAnalyzeProjectProposalSummary,
|
|
574
|
+
normalizeAnalyzeProjectProposalRunId,
|
|
575
|
+
normalizeAnalyzeProjectProposalManifest,
|
|
576
|
+
normalizeAnalyzeProjectWriteManifest,
|
|
577
|
+
proposalManifestSchema,
|
|
578
|
+
readAnalyzeProjectSavedProposal,
|
|
579
|
+
writeAnalyzeProjectProposalArtifacts,
|
|
580
|
+
writeAnalyzeProjectWriteManifest,
|
|
581
|
+
writeManifestSchema,
|
|
582
|
+
};
|
|
@@ -270,14 +270,16 @@ function collectChangedDocPaths(report) {
|
|
|
270
270
|
}
|
|
271
271
|
|
|
272
272
|
function addSchemaValidation(report, errors) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
273
|
+
if (report.apply_run !== true || report.analysis) {
|
|
274
|
+
try {
|
|
275
|
+
normalizeAnalyzeProjectAnalysis(report.analysis, {
|
|
276
|
+
selectedFiles: report.selected_files || [],
|
|
277
|
+
promptFiles: report.prompt?.files || [],
|
|
278
|
+
});
|
|
279
|
+
} catch (error) {
|
|
280
|
+
for (const issue of error.issues || []) {
|
|
281
|
+
errors.push(makeIssue(issue.path, issue.issue || 'analysis-schema-invalid', issue.message || error.message));
|
|
282
|
+
}
|
|
281
283
|
}
|
|
282
284
|
}
|
|
283
285
|
|
|
@@ -17,6 +17,12 @@ const UX_FLAG_MATRIX = Object.freeze({
|
|
|
17
17
|
review: true,
|
|
18
18
|
note: 'planner-assisted docs-only context preparation',
|
|
19
19
|
}),
|
|
20
|
+
'ai analyze-project': Object.freeze({
|
|
21
|
+
withPlanner: false,
|
|
22
|
+
interactive: false,
|
|
23
|
+
review: true,
|
|
24
|
+
note: 'deep project analysis and advanced proposal review',
|
|
25
|
+
}),
|
|
20
26
|
'ai plan': Object.freeze({
|
|
21
27
|
withPlanner: true,
|
|
22
28
|
interactive: true,
|
|
@@ -15,6 +15,10 @@ module.exports = {
|
|
|
15
15
|
commandDescriptions: {},
|
|
16
16
|
optionDescriptions: {
|
|
17
17
|
'Show one human dashboard section': 'Show one human dashboard section (overview, specs, slices, blockers, warnings, agents, approvals, runs, active-slice, next-steps)',
|
|
18
|
+
'For ai analyze-project, apply validated documentation proposals through the safe docs workflow': 'For ai analyze-project, apply validated documentation proposals through the safe docs workflow',
|
|
19
|
+
'For ai analyze-project, save a validated documentation proposal without writing final docs': 'For ai analyze-project, save a validated documentation proposal without writing final docs',
|
|
20
|
+
'For ai analyze-project, show or save the proposed documentation diff': 'For ai analyze-project, show or save the proposed documentation diff',
|
|
21
|
+
'For ai analyze-project, allow dirty target docs checks where the safe apply workflow supports it': 'For ai analyze-project, allow dirty target docs checks where the safe apply workflow supports it',
|
|
18
22
|
},
|
|
19
23
|
},
|
|
20
24
|
'common.command.help': 'Run: npx create-quiver --help',
|
|
@@ -56,6 +60,30 @@ module.exports = {
|
|
|
56
60
|
'ai.actionable.fix': 'Fix',
|
|
57
61
|
'ai.actionable.impact': 'Impact',
|
|
58
62
|
'ai.actionable.next_command': 'Next command',
|
|
63
|
+
'ai.analyze_project.apply.after_diff_question': 'What do you want to do after reviewing the diff?',
|
|
64
|
+
'ai.analyze_project.apply.cancelled': 'AI analyze-project apply canceled',
|
|
65
|
+
'ai.analyze_project.apply.cancelled_writes': 'Writes: none (final docs were not modified)',
|
|
66
|
+
'ai.analyze_project.apply.completed': 'Analysis completed.',
|
|
67
|
+
'ai.analyze_project.apply.context_artifact': 'Selected context: {path}',
|
|
68
|
+
'ai.analyze_project.apply.counts': 'Changes: {changed}; dirty target docs: {dirty}',
|
|
69
|
+
'ai.analyze_project.apply.diff_title': 'Diff preview',
|
|
70
|
+
'ai.analyze_project.apply.diff_truncated': '[diff truncated; full diff saved in the artifact]',
|
|
71
|
+
'ai.analyze_project.apply.full_diff_artifact': 'Full diff artifact: {path}',
|
|
72
|
+
'ai.analyze_project.apply.generated': 'AI generated a documentation proposal from the project source code.',
|
|
73
|
+
'ai.analyze_project.apply.option.apply.hint': 'Writes proposed docs using safe merge and a prior snapshot.',
|
|
74
|
+
'ai.analyze_project.apply.option.apply.label': 'Apply documentation',
|
|
75
|
+
'ai.analyze_project.apply.option.cancel.hint': 'Does not write final documentation.',
|
|
76
|
+
'ai.analyze_project.apply.option.cancel.label': 'Cancel',
|
|
77
|
+
'ai.analyze_project.apply.option.edit.hint': 'Opens the proposal in your editor before applying.',
|
|
78
|
+
'ai.analyze_project.apply.option.edit.label': 'Edit proposal',
|
|
79
|
+
'ai.analyze_project.apply.option.save.hint': 'Saves the proposal under .quiver/runs without final docs writes.',
|
|
80
|
+
'ai.analyze_project.apply.option.save.label': 'Save proposal',
|
|
81
|
+
'ai.analyze_project.apply.option.view_diff.hint': 'Shows a bounded terminal diff before deciding.',
|
|
82
|
+
'ai.analyze_project.apply.option.view_diff.label': 'View diff',
|
|
83
|
+
'ai.analyze_project.apply.proposed_files': 'Proposed files:',
|
|
84
|
+
'ai.analyze_project.apply.question': 'What do you want to do?',
|
|
85
|
+
'ai.analyze_project.apply.recommended': 'recommended',
|
|
86
|
+
'ai.analyze_project.apply.repair': 'Repair manifest: {value}',
|
|
59
87
|
'ai.agent.choice.model_available': 'available',
|
|
60
88
|
'ai.agent.choice.model_custom': 'Custom',
|
|
61
89
|
'ai.agent.choice.model_recommended': 'recommended',
|