create-quiver 0.17.2 → 0.17.4
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 +42 -3
- package/docs/reference/commands.md +33 -12
- 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 +648 -65
- 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-docs.js +16 -1
- 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
|
@@ -230,7 +230,7 @@ const COMMAND_HELP_GROUPS = [
|
|
|
230
230
|
['ai active-slice status|reconcile', 'Inspect or dry-run reconcile local active-slice state from every supported source.'],
|
|
231
231
|
['ai status', 'Show current AI lifecycle phase, approved versions, blockers, and next command.'],
|
|
232
232
|
['ai resume', 'Resume guidance from the last valid lifecycle phase without chat memory.'],
|
|
233
|
-
['ai analyze-project', '
|
|
233
|
+
['ai analyze-project', 'Analyze a bounded project sample and apply validated documentation updates.'],
|
|
234
234
|
['ai onboard', 'Run or print the planner onboarding prompt with a token-aware context pack.'],
|
|
235
235
|
['ai prepare-context', 'Preview or write docs-only AI context updates with assumptions and risks.'],
|
|
236
236
|
['ai agent set|list|show|doctor|repair', 'Manage, diagnose, and dry-run repair planner, executor, reviewer, and doctor provider profiles without secrets.'],
|
|
@@ -330,6 +330,7 @@ function printUsage(language = DEFAULT_LANGUAGE) {
|
|
|
330
330
|
npx create-quiver ai status [options]
|
|
331
331
|
npx create-quiver ai resume [options]
|
|
332
332
|
npx create-quiver ai analyze-project [--deep] [--dry-run] [--json]
|
|
333
|
+
npx create-quiver ai analyze-project apply --run <run-id>
|
|
333
334
|
npx create-quiver ai inspect [options]
|
|
334
335
|
npx create-quiver ai export [--format json|markdown]
|
|
335
336
|
npx create-quiver ai specs list [--json]
|
|
@@ -396,6 +397,10 @@ ${helpText(help, 'headings', 'options', 'Options:')}
|
|
|
396
397
|
--include-source ${optionDescription(help, 'For ai analyze-project, include source files without requiring --deep')}
|
|
397
398
|
--include-tests ${optionDescription(help, 'For ai analyze-project, include test files in the sample')}
|
|
398
399
|
--include-db ${optionDescription(help, 'For ai analyze-project, include schema, migration, and DB files in the sample')}
|
|
400
|
+
--apply-docs ${optionDescription(help, 'For ai analyze-project, apply validated documentation proposals through the safe docs workflow')}
|
|
401
|
+
--save-proposal ${optionDescription(help, 'For ai analyze-project, save a validated documentation proposal without writing final docs')}
|
|
402
|
+
--diff ${optionDescription(help, 'For ai analyze-project, show or save the proposed documentation diff')}
|
|
403
|
+
--allow-dirty-docs ${optionDescription(help, 'For ai analyze-project, allow dirty target docs checks where the safe apply workflow supports it')}
|
|
399
404
|
--scope <path|name> ${optionDescription(help, 'For ai analyze-project, restrict discovery to a repo path or workspace name')}
|
|
400
405
|
--print-prompt ${optionDescription(help, 'Print the exact AI prompt and exit without executing provider CLIs')}
|
|
401
406
|
--with-planner ${optionDescription(help, 'Enable planner-assisted behavior on commands that explicitly support it')}
|
|
@@ -579,6 +584,10 @@ function parseArgs(argv, options = {}) {
|
|
|
579
584
|
aiAnalyzeIncludeDb: false,
|
|
580
585
|
aiAnalyzeIncludeSource: false,
|
|
581
586
|
aiAnalyzeIncludeTests: false,
|
|
587
|
+
aiAnalyzeApplyDocs: false,
|
|
588
|
+
aiAnalyzeSaveProposal: false,
|
|
589
|
+
aiAnalyzeDiff: false,
|
|
590
|
+
aiAnalyzeAllowDirtyDocs: false,
|
|
582
591
|
aiAnalyzeMaxBytes: null,
|
|
583
592
|
aiAnalyzeMaxFiles: null,
|
|
584
593
|
aiAnalyzeScope: '',
|
|
@@ -763,6 +772,30 @@ function parseArgs(argv, options = {}) {
|
|
|
763
772
|
continue;
|
|
764
773
|
}
|
|
765
774
|
|
|
775
|
+
if (arg === '--apply-docs') {
|
|
776
|
+
result.aiAnalyzeApplyDocs = true;
|
|
777
|
+
result.aiAnalyzeOptionUsed = true;
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (arg === '--save-proposal') {
|
|
782
|
+
result.aiAnalyzeSaveProposal = true;
|
|
783
|
+
result.aiAnalyzeOptionUsed = true;
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (arg === '--diff') {
|
|
788
|
+
result.aiAnalyzeDiff = true;
|
|
789
|
+
result.aiAnalyzeOptionUsed = true;
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
if (arg === '--allow-dirty-docs') {
|
|
794
|
+
result.aiAnalyzeAllowDirtyDocs = true;
|
|
795
|
+
result.aiAnalyzeOptionUsed = true;
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
|
|
766
799
|
if (arg === '--max-files') {
|
|
767
800
|
const value = args[++index];
|
|
768
801
|
if (typeof value === 'undefined') {
|
|
@@ -1332,6 +1365,15 @@ function parseArgs(argv, options = {}) {
|
|
|
1332
1365
|
if (result.aiCommand === 'run' && !result.aiRunCommand && positional.length > 0) {
|
|
1333
1366
|
result.aiRunCommand = positional.shift();
|
|
1334
1367
|
}
|
|
1368
|
+
if (result.aiCommand === 'analyze-project' && !result.aiSecondaryCommand && positional.length > 0) {
|
|
1369
|
+
result.aiSecondaryCommand = positional.shift();
|
|
1370
|
+
}
|
|
1371
|
+
if (result.aiCommand === 'analyze-project' && result.aiSecondaryCommand && result.aiSecondaryCommand !== 'apply') {
|
|
1372
|
+
throw new Error(formatError(`unsupported ai analyze-project subcommand: ${result.aiSecondaryCommand}. Supported tasks: apply`));
|
|
1373
|
+
}
|
|
1374
|
+
if (result.aiCommand === 'analyze-project' && result.aiSecondaryCommand === 'apply' && !result.aiRunId) {
|
|
1375
|
+
throw new Error(formatError('ai analyze-project apply requires --run <run-id>'));
|
|
1376
|
+
}
|
|
1335
1377
|
if ((result.aiCommand === 'specs' || result.aiCommand === 'slices' || result.aiCommand === 'models' || result.aiCommand === 'trace' || result.aiCommand === 'active-slice') && !result.aiSecondaryCommand && positional.length > 0) {
|
|
1336
1378
|
result.aiSecondaryCommand = positional.shift();
|
|
1337
1379
|
}
|
|
@@ -1470,6 +1512,33 @@ function parseArgs(argv, options = {}) {
|
|
|
1470
1512
|
throw new Error(formatError('--global is only supported by config language set. Use: npx create-quiver config language set <en|es> --global'));
|
|
1471
1513
|
}
|
|
1472
1514
|
|
|
1515
|
+
if (result.mode === 'ai' && result.aiCommand === 'analyze-project') {
|
|
1516
|
+
if (result.review && result.aiAnalyzeApplyDocs) {
|
|
1517
|
+
throw new Error(formatError('ai analyze-project --apply-docs cannot be combined with --review'));
|
|
1518
|
+
}
|
|
1519
|
+
if (result.dryRun && result.aiAnalyzeApplyDocs) {
|
|
1520
|
+
throw new Error(formatError('ai analyze-project --dry-run cannot be combined with --apply-docs'));
|
|
1521
|
+
}
|
|
1522
|
+
if (result.dryRun && result.aiAnalyzeSaveProposal) {
|
|
1523
|
+
throw new Error(formatError('ai analyze-project --dry-run cannot be combined with --save-proposal'));
|
|
1524
|
+
}
|
|
1525
|
+
if (result.dryRun && result.aiSecondaryCommand === 'apply') {
|
|
1526
|
+
throw new Error(formatError('ai analyze-project apply --run cannot be combined with --dry-run'));
|
|
1527
|
+
}
|
|
1528
|
+
if (result.aiSecondaryCommand === 'apply' && (result.aiAnalyzeApplyDocs || result.aiAnalyzeSaveProposal || result.review)) {
|
|
1529
|
+
throw new Error(formatError('ai analyze-project apply --run cannot be combined with --apply-docs, --save-proposal, or --review'));
|
|
1530
|
+
}
|
|
1531
|
+
if (result.aiSecondaryCommand === 'apply' && result.aiRunId === 'latest' && result.force) {
|
|
1532
|
+
throw new Error(formatError('ai analyze-project apply --run latest cannot be combined with --yes'));
|
|
1533
|
+
}
|
|
1534
|
+
if (result.aiSecondaryCommand === 'apply' && result.aiRunId === 'latest' && result.json) {
|
|
1535
|
+
throw new Error(formatError('ai analyze-project apply --run latest cannot be combined with --json'));
|
|
1536
|
+
}
|
|
1537
|
+
if (result.json && result.aiAnalyzeApplyDocs && !result.force) {
|
|
1538
|
+
throw new Error(formatError('ai analyze-project --json with --apply-docs requires --yes'));
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1473
1542
|
return result;
|
|
1474
1543
|
}
|
|
1475
1544
|
|
|
@@ -3519,6 +3588,10 @@ async function run(argv) {
|
|
|
3519
3588
|
await runAiAnalyzeProject(process.cwd(), {
|
|
3520
3589
|
deep: args.aiAnalyzeDeep,
|
|
3521
3590
|
dryRun: args.dryRun,
|
|
3591
|
+
allowDirtyDocs: args.aiAnalyzeAllowDirtyDocs,
|
|
3592
|
+
applyDocs: args.aiAnalyzeApplyDocs,
|
|
3593
|
+
applyRun: args.aiSecondaryCommand === 'apply',
|
|
3594
|
+
diff: args.aiAnalyzeDiff,
|
|
3522
3595
|
force: args.force,
|
|
3523
3596
|
includeDb: args.aiAnalyzeIncludeDb,
|
|
3524
3597
|
includeSource: args.aiAnalyzeIncludeSource,
|
|
@@ -3528,10 +3601,16 @@ async function run(argv) {
|
|
|
3528
3601
|
language: args.language,
|
|
3529
3602
|
maxBytes: args.aiAnalyzeMaxBytes || undefined,
|
|
3530
3603
|
maxFiles: args.aiAnalyzeMaxFiles || undefined,
|
|
3604
|
+
model: args.aiModel || undefined,
|
|
3531
3605
|
printPrompt: args.aiPrintPrompt,
|
|
3606
|
+
provider: args.aiProvider || undefined,
|
|
3607
|
+
providerExplicit: args.aiProviderExplicit,
|
|
3532
3608
|
review: args.review,
|
|
3609
|
+
runId: args.aiRunId || undefined,
|
|
3610
|
+
saveProposal: args.aiAnalyzeSaveProposal,
|
|
3533
3611
|
scope: args.aiAnalyzeScope || undefined,
|
|
3534
3612
|
strict: args.strict,
|
|
3613
|
+
timeout: args.aiTimeout || undefined,
|
|
3535
3614
|
});
|
|
3536
3615
|
return;
|
|
3537
3616
|
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
const crypto = require('node:crypto');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
buildAnalyzeProjectWritePlan,
|
|
6
|
+
createAnalyzeProjectSnapshot,
|
|
7
|
+
writeAnalyzeProjectDocs,
|
|
8
|
+
} = require('./analyze-project-docs');
|
|
9
|
+
const {
|
|
10
|
+
buildAnalyzeProjectProposalArtifactPaths,
|
|
11
|
+
normalizeAnalyzeProjectProposalManifest,
|
|
12
|
+
writeAnalyzeProjectProposalArtifacts,
|
|
13
|
+
writeAnalyzeProjectWriteManifest,
|
|
14
|
+
} = require('./analyze-project-proposal');
|
|
15
|
+
const { validateAnalyzeProjectPostWrite } = require('./analyze-project-validation');
|
|
16
|
+
|
|
17
|
+
function formatError(message) {
|
|
18
|
+
return `create-quiver: ${message}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class AnalyzeProjectApplyError extends Error {
|
|
22
|
+
constructor(message, issues = []) {
|
|
23
|
+
super(formatError(message));
|
|
24
|
+
this.name = 'AnalyzeProjectApplyError';
|
|
25
|
+
this.code = 'AI_ANALYZE_PROJECT_APPLY_BLOCKED';
|
|
26
|
+
this.issues = issues;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sha256(contents) {
|
|
31
|
+
return crypto.createHash('sha256').update(String(contents || ''), 'utf8').digest('hex');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readCurrentHash(item) {
|
|
35
|
+
if (!fs.existsSync(item.destinationPath)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return sha256(fs.readFileSync(item.destinationPath, 'utf8'));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function collectWrittenDocs(writePlan) {
|
|
42
|
+
const written = [];
|
|
43
|
+
for (const item of writePlan) {
|
|
44
|
+
if (item.action === 'skip') {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (readCurrentHash(item) === item.after_sha256) {
|
|
48
|
+
written.push(item.path);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return written;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function assertAnalyzeProjectApplyPreflight(writePlan, proposalManifest, options = {}) {
|
|
55
|
+
const issues = [];
|
|
56
|
+
const beforeHashes = proposalManifest?.doc_before_hashes || {};
|
|
57
|
+
|
|
58
|
+
for (const item of writePlan) {
|
|
59
|
+
if (item.action === 'skip') {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (item.dirty === true && options.allowDirtyDocs !== true) {
|
|
63
|
+
issues.push({
|
|
64
|
+
path: item.path,
|
|
65
|
+
issue: 'dirty-target-doc',
|
|
66
|
+
message: 'target documentation already has human content; --yes requires --allow-dirty-docs or interactive review',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!Object.prototype.hasOwnProperty.call(beforeHashes, item.path)) {
|
|
71
|
+
issues.push({
|
|
72
|
+
path: item.path,
|
|
73
|
+
issue: 'missing-before-hash',
|
|
74
|
+
message: 'proposal manifest is missing the current doc hash for this target path',
|
|
75
|
+
});
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const currentHash = readCurrentHash(item);
|
|
80
|
+
const expectedHash = beforeHashes[item.path] || null;
|
|
81
|
+
if (currentHash !== expectedHash) {
|
|
82
|
+
issues.push({
|
|
83
|
+
path: item.path,
|
|
84
|
+
issue: 'stale-target-doc',
|
|
85
|
+
message: 'target documentation changed after the proposal was built',
|
|
86
|
+
expected_sha256: expectedHash,
|
|
87
|
+
current_sha256: currentHash,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (issues.length > 0) {
|
|
93
|
+
throw new AnalyzeProjectApplyError(
|
|
94
|
+
'ai analyze-project --apply-docs --yes blocked before writing final docs',
|
|
95
|
+
issues,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { ok: true, issues: [] };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function validationFromWriteError(error, options = {}) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
strict: options.strict === true,
|
|
106
|
+
errors: [{
|
|
107
|
+
path: null,
|
|
108
|
+
issue: 'write-failed',
|
|
109
|
+
message: error?.message || 'analyze-project doc write failed',
|
|
110
|
+
}],
|
|
111
|
+
warnings: [],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizeLoadedProposalArtifacts(runId, proposalArtifacts = {}, proposalManifest = null) {
|
|
116
|
+
const paths = buildAnalyzeProjectProposalArtifactPaths(runId);
|
|
117
|
+
const manifestData = normalizeAnalyzeProjectProposalManifest(proposalManifest || proposalArtifacts.manifest_data || {});
|
|
118
|
+
return {
|
|
119
|
+
root: proposalArtifacts.root || paths.root,
|
|
120
|
+
proposal_json: proposalArtifacts.proposal_json || manifestData.proposal_json,
|
|
121
|
+
proposal_markdown: proposalArtifacts.proposal_markdown || manifestData.proposal_markdown,
|
|
122
|
+
proposal_diff: proposalArtifacts.proposal_diff || manifestData.proposal_diff,
|
|
123
|
+
manifest: proposalArtifacts.manifest || paths.manifest,
|
|
124
|
+
proposal_sha256: proposalArtifacts.proposal_sha256 || manifestData.proposal_sha256,
|
|
125
|
+
doc_paths: proposalArtifacts.doc_paths || manifestData.doc_paths || [],
|
|
126
|
+
changed_docs: proposalArtifacts.changed_docs || manifestData.doc_paths || [],
|
|
127
|
+
manifest_data: manifestData,
|
|
128
|
+
files: Array.isArray(proposalArtifacts.files)
|
|
129
|
+
? proposalArtifacts.files
|
|
130
|
+
: [
|
|
131
|
+
manifestData.proposal_json,
|
|
132
|
+
manifestData.proposal_markdown,
|
|
133
|
+
manifestData.proposal_diff,
|
|
134
|
+
paths.manifest,
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function applyAnalyzeProjectDocProposal(repoRoot, options = {}) {
|
|
140
|
+
const runId = String(options.runId || '').trim();
|
|
141
|
+
const proposal = options.proposal;
|
|
142
|
+
const now = options.now instanceof Date ? options.now : new Date(options.now || Date.now());
|
|
143
|
+
const writePlan = buildAnalyzeProjectWritePlan(repoRoot, proposal);
|
|
144
|
+
const applyEvents = Array.isArray(options.events) ? options.events : [];
|
|
145
|
+
const proposalArtifacts = options.proposalArtifacts
|
|
146
|
+
? normalizeLoadedProposalArtifacts(runId, options.proposalArtifacts, options.proposalManifest)
|
|
147
|
+
: writeAnalyzeProjectProposalArtifacts(repoRoot, {
|
|
148
|
+
runId,
|
|
149
|
+
now,
|
|
150
|
+
provider: options.provider,
|
|
151
|
+
language: options.language,
|
|
152
|
+
proposal,
|
|
153
|
+
writePlan,
|
|
154
|
+
selectedContextManifest: options.selectedContextManifest,
|
|
155
|
+
repairManifest: options.repairManifest,
|
|
156
|
+
events: [{ type: 'proposal-saved-before-apply', at: now.toISOString() }],
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
assertAnalyzeProjectApplyPreflight(writePlan, proposalArtifacts.manifest_data, {
|
|
161
|
+
allowDirtyDocs: options.allowDirtyDocs === true,
|
|
162
|
+
});
|
|
163
|
+
} catch (error) {
|
|
164
|
+
error.proposal_artifacts = proposalArtifacts;
|
|
165
|
+
error.write_plan = writePlan;
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const snapshot = createAnalyzeProjectSnapshot(repoRoot, { run_id: runId }, writePlan, {
|
|
170
|
+
providerArtifact: options.providerArtifact,
|
|
171
|
+
proposal,
|
|
172
|
+
now,
|
|
173
|
+
});
|
|
174
|
+
const plannedManifest = writeAnalyzeProjectWriteManifest(repoRoot, {
|
|
175
|
+
runId,
|
|
176
|
+
now,
|
|
177
|
+
proposalManifest: proposalArtifacts.manifest,
|
|
178
|
+
snapshot,
|
|
179
|
+
writePlan,
|
|
180
|
+
strict: options.strict === true,
|
|
181
|
+
status: 'planned',
|
|
182
|
+
events: [
|
|
183
|
+
...applyEvents,
|
|
184
|
+
{ type: 'write-planned', at: now.toISOString() },
|
|
185
|
+
],
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
let writtenDocs = [];
|
|
189
|
+
try {
|
|
190
|
+
writtenDocs = writeAnalyzeProjectDocs(writePlan);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
const partialDocs = collectWrittenDocs(writePlan);
|
|
193
|
+
const failedManifest = writeAnalyzeProjectWriteManifest(repoRoot, {
|
|
194
|
+
runId,
|
|
195
|
+
now: new Date(),
|
|
196
|
+
proposalManifest: proposalArtifacts.manifest,
|
|
197
|
+
snapshot,
|
|
198
|
+
writePlan,
|
|
199
|
+
writtenDocs: partialDocs,
|
|
200
|
+
validation: validationFromWriteError(error, { strict: options.strict === true }),
|
|
201
|
+
partialWrite: partialDocs.length > 0,
|
|
202
|
+
status: 'completed',
|
|
203
|
+
events: [
|
|
204
|
+
...applyEvents,
|
|
205
|
+
{ type: 'write-planned', at: now.toISOString() },
|
|
206
|
+
{ type: 'write-failed', at: new Date().toISOString(), message: error?.message || 'write failed' },
|
|
207
|
+
],
|
|
208
|
+
});
|
|
209
|
+
error.write_manifest = failedManifest;
|
|
210
|
+
error.snapshot = snapshot;
|
|
211
|
+
error.written_docs = partialDocs;
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const writeReport = {
|
|
216
|
+
...options.report,
|
|
217
|
+
apply_docs: true,
|
|
218
|
+
writes: writtenDocs,
|
|
219
|
+
doc_proposal: proposal,
|
|
220
|
+
write_plan: options.summarizeWritePlan
|
|
221
|
+
? options.summarizeWritePlan(writePlan)
|
|
222
|
+
: writePlan,
|
|
223
|
+
snapshot,
|
|
224
|
+
written_docs: writtenDocs,
|
|
225
|
+
proposal_artifacts: {
|
|
226
|
+
root: proposalArtifacts.root,
|
|
227
|
+
proposal_json: proposalArtifacts.proposal_json,
|
|
228
|
+
proposal_markdown: proposalArtifacts.proposal_markdown,
|
|
229
|
+
proposal_diff: proposalArtifacts.proposal_diff,
|
|
230
|
+
manifest: proposalArtifacts.manifest,
|
|
231
|
+
proposal_sha256: proposalArtifacts.proposal_sha256,
|
|
232
|
+
proposal_edited: options.proposalEdited === true,
|
|
233
|
+
},
|
|
234
|
+
write_manifest: {
|
|
235
|
+
path: plannedManifest.path,
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
const postWriteValidation = validateAnalyzeProjectPostWrite(repoRoot, writeReport, {
|
|
239
|
+
strict: options.strict === true,
|
|
240
|
+
});
|
|
241
|
+
const finalManifest = writeAnalyzeProjectWriteManifest(repoRoot, {
|
|
242
|
+
runId,
|
|
243
|
+
now: new Date(),
|
|
244
|
+
proposalManifest: proposalArtifacts.manifest,
|
|
245
|
+
snapshot,
|
|
246
|
+
writePlan,
|
|
247
|
+
writtenDocs,
|
|
248
|
+
validation: postWriteValidation,
|
|
249
|
+
partialWrite: false,
|
|
250
|
+
status: 'completed',
|
|
251
|
+
events: [
|
|
252
|
+
...applyEvents,
|
|
253
|
+
{ type: 'write-planned', at: now.toISOString() },
|
|
254
|
+
{ type: 'write-completed', at: new Date().toISOString(), written_docs: writtenDocs },
|
|
255
|
+
],
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
...writeReport,
|
|
260
|
+
post_write_validation: postWriteValidation,
|
|
261
|
+
write_manifest: finalManifest,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
module.exports = {
|
|
266
|
+
AnalyzeProjectApplyError,
|
|
267
|
+
applyAnalyzeProjectDocProposal,
|
|
268
|
+
assertAnalyzeProjectApplyPreflight,
|
|
269
|
+
};
|
|
@@ -93,6 +93,21 @@ function mergeManagedBlock(currentContent, proposedContent) {
|
|
|
93
93
|
return `${current.replace(/\s+$/g, '')}\n\n${block}`;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
function redactSnapshotValue(value, repoRoot) {
|
|
97
|
+
if (typeof value === 'string') {
|
|
98
|
+
return redactSensitiveLocalValues(value, { projectRoot: repoRoot });
|
|
99
|
+
}
|
|
100
|
+
if (Array.isArray(value)) {
|
|
101
|
+
return value.map((item) => redactSnapshotValue(item, repoRoot));
|
|
102
|
+
}
|
|
103
|
+
if (value && typeof value === 'object') {
|
|
104
|
+
return Object.fromEntries(
|
|
105
|
+
Object.entries(value).map(([key, item]) => [key, redactSnapshotValue(item, repoRoot)]),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
|
|
96
111
|
function validateDocPath(docPath) {
|
|
97
112
|
let normalized;
|
|
98
113
|
try {
|
|
@@ -303,7 +318,7 @@ function createAnalyzeProjectSnapshot(repoRoot, run, writePlan, options = {}) {
|
|
|
303
318
|
|
|
304
319
|
const providerArtifactPath = path.join(rawRoot, 'analyze-project-provider-artifact.json');
|
|
305
320
|
const providerArtifact = options.providerArtifact
|
|
306
|
-
?
|
|
321
|
+
? redactSnapshotValue(options.providerArtifact, repoRoot)
|
|
307
322
|
: null;
|
|
308
323
|
fs.writeFileSync(providerArtifactPath, `${JSON.stringify(providerArtifact, null, 2)}\n`);
|
|
309
324
|
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
const { selectOption } = require('../cli/selectors');
|
|
2
|
+
const { createUx, resolveUxMode } = require('../cli/ux');
|
|
3
|
+
const { createTranslator } = require('../i18n/catalog');
|
|
4
|
+
const { formatAnalyzeProjectDiffPreview } = require('./analyze-project-docs');
|
|
5
|
+
|
|
6
|
+
const APPLY_ACTION = 'apply';
|
|
7
|
+
const VIEW_DIFF_ACTION = 'view-diff';
|
|
8
|
+
const SAVE_ACTION = 'save-proposal';
|
|
9
|
+
const EDIT_ACTION = 'edit-proposal';
|
|
10
|
+
const CANCEL_ACTION = 'cancel';
|
|
11
|
+
|
|
12
|
+
function canUseAnalyzeProjectInteractiveSelector(options = {}) {
|
|
13
|
+
const mode = resolveUxMode({
|
|
14
|
+
interactive: true,
|
|
15
|
+
json: options.json,
|
|
16
|
+
noColor: options.noColor,
|
|
17
|
+
stdinIsTTY: options.stdinIsTTY,
|
|
18
|
+
stdoutIsTTY: options.stdoutIsTTY,
|
|
19
|
+
stderrIsTTY: options.stderrIsTTY,
|
|
20
|
+
}, options.env || process.env, {
|
|
21
|
+
input: options.inputStream || process.stdin,
|
|
22
|
+
output: options.outputStream || process.stdout,
|
|
23
|
+
error: options.errorStream || process.stderr,
|
|
24
|
+
});
|
|
25
|
+
return mode.usePrompts === true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function changedItems(writePlan = []) {
|
|
29
|
+
return (Array.isArray(writePlan) ? writePlan : []).filter((item) => item.action && item.action !== 'skip');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function selectRecommendedAction(writePlan = []) {
|
|
33
|
+
const changed = changedItems(writePlan);
|
|
34
|
+
const higherRisk = changed.some((item) => item.dirty === true || item.action === 'update');
|
|
35
|
+
return higherRisk ? VIEW_DIFF_ACTION : APPLY_ACTION;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function withRecommendation(label, recommended, translator) {
|
|
39
|
+
return recommended ? `${label} (${translator.t('ai.analyze_project.apply.recommended')})` : label;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildAnalyzeProjectApplyChoices(translator, recommendedAction, options = {}) {
|
|
43
|
+
const includeDiff = options.includeDiff !== false;
|
|
44
|
+
const applyChoice = {
|
|
45
|
+
label: withRecommendation(translator.t('ai.analyze_project.apply.option.apply.label'), recommendedAction === APPLY_ACTION, translator),
|
|
46
|
+
value: APPLY_ACTION,
|
|
47
|
+
hint: translator.t('ai.analyze_project.apply.option.apply.hint'),
|
|
48
|
+
default: recommendedAction === APPLY_ACTION,
|
|
49
|
+
};
|
|
50
|
+
const diffChoice = {
|
|
51
|
+
label: withRecommendation(translator.t('ai.analyze_project.apply.option.view_diff.label'), recommendedAction === VIEW_DIFF_ACTION, translator),
|
|
52
|
+
value: VIEW_DIFF_ACTION,
|
|
53
|
+
hint: translator.t('ai.analyze_project.apply.option.view_diff.hint'),
|
|
54
|
+
default: recommendedAction === VIEW_DIFF_ACTION,
|
|
55
|
+
};
|
|
56
|
+
const choices = [];
|
|
57
|
+
|
|
58
|
+
if (includeDiff && recommendedAction === VIEW_DIFF_ACTION) {
|
|
59
|
+
choices.push(diffChoice, applyChoice);
|
|
60
|
+
} else {
|
|
61
|
+
choices.push(applyChoice);
|
|
62
|
+
if (includeDiff) {
|
|
63
|
+
choices.push(diffChoice);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
choices.push(
|
|
68
|
+
{
|
|
69
|
+
label: translator.t('ai.analyze_project.apply.option.save.label'),
|
|
70
|
+
value: SAVE_ACTION,
|
|
71
|
+
hint: translator.t('ai.analyze_project.apply.option.save.hint'),
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
label: translator.t('ai.analyze_project.apply.option.edit.label'),
|
|
75
|
+
value: EDIT_ACTION,
|
|
76
|
+
hint: translator.t('ai.analyze_project.apply.option.edit.hint'),
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
label: translator.t('ai.analyze_project.apply.option.cancel.label'),
|
|
80
|
+
value: CANCEL_ACTION,
|
|
81
|
+
hint: translator.t('ai.analyze_project.apply.option.cancel.hint'),
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
return choices;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function formatAnalyzeProjectApplySummary(report = {}, proposal = {}, writePlan = [], options = {}) {
|
|
89
|
+
const translator = createTranslator(options.language);
|
|
90
|
+
const changed = changedItems(writePlan);
|
|
91
|
+
const dirty = changed.filter((item) => item.dirty === true);
|
|
92
|
+
const docs = Array.isArray(proposal?.docs) ? proposal.docs : [];
|
|
93
|
+
const lines = [
|
|
94
|
+
translator.t('ai.analyze_project.apply.completed'),
|
|
95
|
+
translator.t('ai.analyze_project.apply.generated'),
|
|
96
|
+
'',
|
|
97
|
+
translator.t('ai.analyze_project.apply.proposed_files'),
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
if (docs.length === 0) {
|
|
101
|
+
lines.push(`- ${translator.t('common.none')}`);
|
|
102
|
+
} else {
|
|
103
|
+
for (const doc of docs) {
|
|
104
|
+
const plan = writePlan.find((item) => item.path === doc.path);
|
|
105
|
+
lines.push(`- ${doc.path}${plan?.action ? ` (${plan.action})` : ''}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
lines.push(
|
|
110
|
+
'',
|
|
111
|
+
translator.t('ai.analyze_project.apply.counts', {
|
|
112
|
+
changed: changed.length,
|
|
113
|
+
dirty: dirty.length,
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (report.analysis_validation?.repaired === true || report.analysis_validation?.repair_manifest) {
|
|
118
|
+
lines.push(translator.t('ai.analyze_project.apply.repair', {
|
|
119
|
+
value: report.analysis_validation.repair_manifest?.path || report.analysis_validation.repair_manifest || translator.t('common.yes'),
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
if (report.selected_context_manifest) {
|
|
123
|
+
lines.push(translator.t('ai.analyze_project.apply.context_artifact', {
|
|
124
|
+
path: report.selected_context_manifest.path || report.selected_context_manifest,
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
lines.push('');
|
|
129
|
+
return `${lines.join('\n')}\n`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function formatBoundedAnalyzeProjectDiff(writePlan = [], options = {}) {
|
|
133
|
+
const translator = createTranslator(options.language);
|
|
134
|
+
const maxLines = Number.isInteger(options.maxDiffLines) ? Math.max(1, options.maxDiffLines) : 80;
|
|
135
|
+
const maxChars = Number.isInteger(options.maxDiffChars) ? Math.max(500, options.maxDiffChars) : 8_000;
|
|
136
|
+
const rawLines = formatAnalyzeProjectDiffPreview(writePlan);
|
|
137
|
+
const lines = rawLines.slice(0, maxLines);
|
|
138
|
+
let text = lines.join('\n');
|
|
139
|
+
let truncated = rawLines.length > lines.length;
|
|
140
|
+
|
|
141
|
+
if (text.length > maxChars) {
|
|
142
|
+
text = text.slice(0, maxChars).replace(/\s+$/g, '');
|
|
143
|
+
truncated = true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (truncated) {
|
|
147
|
+
text = `${text}\n${translator.t('ai.analyze_project.apply.diff_truncated')}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return `${translator.t('ai.analyze_project.apply.diff_title')}\n${text}\n`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function chooseAnalyzeProjectApplyAction(message, choices, options = {}) {
|
|
154
|
+
return selectOption(message, choices, {
|
|
155
|
+
name: 'ai analyze-project apply action',
|
|
156
|
+
flag: '--apply-docs',
|
|
157
|
+
interactive: true,
|
|
158
|
+
stdinIsTTY: options.stdinIsTTY,
|
|
159
|
+
stdoutIsTTY: options.stdoutIsTTY,
|
|
160
|
+
stderrIsTTY: options.stderrIsTTY,
|
|
161
|
+
noColor: options.noColor,
|
|
162
|
+
env: options.env,
|
|
163
|
+
prompts: options.prompts,
|
|
164
|
+
promptSelect: options.promptSelect,
|
|
165
|
+
input: options.inputStream,
|
|
166
|
+
output: options.outputStream,
|
|
167
|
+
error: options.errorStream,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function runAnalyzeProjectInteractiveApplySelector(context = {}) {
|
|
172
|
+
const options = context.options || {};
|
|
173
|
+
const translator = createTranslator(options.language);
|
|
174
|
+
const ux = createUx({ ...options, interactive: true });
|
|
175
|
+
|
|
176
|
+
if (!canUseAnalyzeProjectInteractiveSelector(options)) {
|
|
177
|
+
throw new Error([
|
|
178
|
+
'create-quiver: ai analyze-project --apply-docs requires an interactive TTY unless --yes is passed.',
|
|
179
|
+
'Next safe step: use `npx create-quiver ai analyze-project --deep --apply-docs --yes --provider <provider> --model <model>` for automation, or rerun from an interactive terminal.',
|
|
180
|
+
].join('\n'));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const recommendedAction = selectRecommendedAction(context.writePlan);
|
|
184
|
+
process.stdout.write(formatAnalyzeProjectApplySummary(context.report, context.proposal, context.writePlan, options));
|
|
185
|
+
|
|
186
|
+
const first = await chooseAnalyzeProjectApplyAction(
|
|
187
|
+
translator.t('ai.analyze_project.apply.question'),
|
|
188
|
+
buildAnalyzeProjectApplyChoices(translator, recommendedAction),
|
|
189
|
+
options,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
if (first.value === APPLY_ACTION) {
|
|
193
|
+
return context.actions.apply({ allowDirtyDocs: true, interactiveAction: APPLY_ACTION });
|
|
194
|
+
}
|
|
195
|
+
if (first.value === SAVE_ACTION) {
|
|
196
|
+
return context.actions.save({ interactiveAction: SAVE_ACTION });
|
|
197
|
+
}
|
|
198
|
+
if (first.value === EDIT_ACTION) {
|
|
199
|
+
return context.actions.edit({ interactiveAction: EDIT_ACTION });
|
|
200
|
+
}
|
|
201
|
+
if (first.value === CANCEL_ACTION) {
|
|
202
|
+
return context.actions.cancel({ interactiveAction: CANCEL_ACTION });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const savedReport = await context.actions.save({ interactiveAction: VIEW_DIFF_ACTION, silent: true });
|
|
206
|
+
ux.section(translator.t('ai.analyze_project.apply.full_diff_artifact', {
|
|
207
|
+
path: savedReport.proposal_artifacts?.proposal_diff || translator.t('common.none'),
|
|
208
|
+
}));
|
|
209
|
+
process.stdout.write(formatBoundedAnalyzeProjectDiff(context.writePlan, options));
|
|
210
|
+
|
|
211
|
+
const second = await chooseAnalyzeProjectApplyAction(
|
|
212
|
+
translator.t('ai.analyze_project.apply.after_diff_question'),
|
|
213
|
+
buildAnalyzeProjectApplyChoices(translator, APPLY_ACTION, { includeDiff: false }),
|
|
214
|
+
options,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
if (second.value === APPLY_ACTION) {
|
|
218
|
+
return context.actions.apply({ allowDirtyDocs: true, interactiveAction: APPLY_ACTION });
|
|
219
|
+
}
|
|
220
|
+
if (second.value === SAVE_ACTION) {
|
|
221
|
+
return context.actions.save({ interactiveAction: SAVE_ACTION, savedReport });
|
|
222
|
+
}
|
|
223
|
+
if (second.value === EDIT_ACTION) {
|
|
224
|
+
return context.actions.edit({ interactiveAction: EDIT_ACTION });
|
|
225
|
+
}
|
|
226
|
+
return context.actions.cancel({ interactiveAction: CANCEL_ACTION, savedReport });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = {
|
|
230
|
+
APPLY_ACTION,
|
|
231
|
+
CANCEL_ACTION,
|
|
232
|
+
EDIT_ACTION,
|
|
233
|
+
SAVE_ACTION,
|
|
234
|
+
VIEW_DIFF_ACTION,
|
|
235
|
+
buildAnalyzeProjectApplyChoices,
|
|
236
|
+
canUseAnalyzeProjectInteractiveSelector,
|
|
237
|
+
formatAnalyzeProjectApplySummary,
|
|
238
|
+
formatBoundedAnalyzeProjectDiff,
|
|
239
|
+
runAnalyzeProjectInteractiveApplySelector,
|
|
240
|
+
selectRecommendedAction,
|
|
241
|
+
};
|