chati-dev 1.3.3 → 2.0.1
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/README.md +7 -6
- package/framework/agents/build/dev.md +343 -0
- package/framework/agents/clarity/architect.md +113 -0
- package/framework/agents/clarity/brief.md +183 -0
- package/framework/agents/clarity/brownfield-wu.md +182 -0
- package/framework/agents/clarity/detail.md +111 -0
- package/framework/agents/clarity/greenfield-wu.md +154 -0
- package/framework/agents/clarity/phases.md +1 -0
- package/framework/agents/clarity/tasks.md +1 -0
- package/framework/agents/clarity/ux.md +113 -0
- package/framework/agents/deploy/devops.md +1 -0
- package/framework/agents/quality/qa-implementation.md +1 -0
- package/framework/agents/quality/qa-planning.md +1 -0
- package/framework/config.yaml +3 -3
- package/framework/constitution.md +58 -1
- package/framework/context/governance.md +37 -0
- package/framework/context/protocols.md +34 -0
- package/framework/context/quality.md +27 -0
- package/framework/context/root.md +24 -0
- package/framework/data/entity-registry.yaml +1 -1
- package/framework/domains/agents/architect.yaml +51 -0
- package/framework/domains/agents/brief.yaml +47 -0
- package/framework/domains/agents/brownfield-wu.yaml +49 -0
- package/framework/domains/agents/detail.yaml +47 -0
- package/framework/domains/agents/dev.yaml +49 -0
- package/framework/domains/agents/devops.yaml +43 -0
- package/framework/domains/agents/greenfield-wu.yaml +47 -0
- package/framework/domains/agents/orchestrator.yaml +49 -0
- package/framework/domains/agents/phases.yaml +47 -0
- package/framework/domains/agents/qa-implementation.yaml +43 -0
- package/framework/domains/agents/qa-planning.yaml +44 -0
- package/framework/domains/agents/tasks.yaml +48 -0
- package/framework/domains/agents/ux.yaml +50 -0
- package/framework/domains/constitution.yaml +77 -0
- package/framework/domains/global.yaml +64 -0
- package/framework/domains/workflows/brownfield-discovery.yaml +16 -0
- package/framework/domains/workflows/brownfield-fullstack.yaml +26 -0
- package/framework/domains/workflows/brownfield-service.yaml +22 -0
- package/framework/domains/workflows/brownfield-ui.yaml +22 -0
- package/framework/domains/workflows/greenfield-fullstack.yaml +26 -0
- package/framework/hooks/constitution-guard.js +101 -0
- package/framework/hooks/mode-governance.js +92 -0
- package/framework/hooks/model-governance.js +76 -0
- package/framework/hooks/prism-engine.js +89 -0
- package/framework/hooks/session-digest.js +60 -0
- package/framework/hooks/settings.json +44 -0
- package/framework/i18n/en.yaml +3 -3
- package/framework/i18n/es.yaml +3 -3
- package/framework/i18n/fr.yaml +3 -3
- package/framework/i18n/pt.yaml +3 -3
- package/framework/intelligence/context-engine.md +2 -2
- package/framework/intelligence/decision-engine.md +1 -1
- package/framework/migrations/v1.4-to-v2.0.yaml +167 -0
- package/framework/migrations/v2.0-to-v2.0.1.yaml +132 -0
- package/framework/orchestrator/chati.md +350 -7
- package/framework/schemas/session.schema.json +15 -0
- package/framework/tasks/architect-api-design.md +63 -0
- package/framework/tasks/architect-consolidate.md +47 -0
- package/framework/tasks/architect-db-design.md +73 -0
- package/framework/tasks/architect-design.md +95 -0
- package/framework/tasks/architect-security-review.md +62 -0
- package/framework/tasks/architect-stack-selection.md +53 -0
- package/framework/tasks/brief-consolidate.md +249 -0
- package/framework/tasks/brief-constraint-identify.md +277 -0
- package/framework/tasks/brief-extract-requirements.md +339 -0
- package/framework/tasks/brief-stakeholder-map.md +176 -0
- package/framework/tasks/brief-validate-completeness.md +121 -0
- package/framework/tasks/brownfield-wu-architecture-map.md +394 -0
- package/framework/tasks/brownfield-wu-deep-discovery.md +312 -0
- package/framework/tasks/brownfield-wu-dependency-scan.md +359 -0
- package/framework/tasks/brownfield-wu-migration-plan.md +483 -0
- package/framework/tasks/brownfield-wu-report.md +325 -0
- package/framework/tasks/brownfield-wu-risk-assess.md +424 -0
- package/framework/tasks/detail-acceptance-criteria.md +372 -0
- package/framework/tasks/detail-consolidate.md +138 -0
- package/framework/tasks/detail-edge-case-analysis.md +300 -0
- package/framework/tasks/detail-expand-prd.md +389 -0
- package/framework/tasks/detail-nfr-extraction.md +223 -0
- package/framework/tasks/dev-code-review.md +404 -0
- package/framework/tasks/dev-consolidate.md +543 -0
- package/framework/tasks/dev-debug.md +322 -0
- package/framework/tasks/dev-implement.md +252 -0
- package/framework/tasks/dev-iterate.md +411 -0
- package/framework/tasks/dev-pr-prepare.md +497 -0
- package/framework/tasks/dev-refactor.md +342 -0
- package/framework/tasks/dev-test-write.md +306 -0
- package/framework/tasks/devops-ci-setup.md +412 -0
- package/framework/tasks/devops-consolidate.md +712 -0
- package/framework/tasks/devops-deploy-config.md +598 -0
- package/framework/tasks/devops-monitoring-setup.md +658 -0
- package/framework/tasks/devops-release-prepare.md +673 -0
- package/framework/tasks/greenfield-wu-analyze-empty.md +169 -0
- package/framework/tasks/greenfield-wu-report.md +266 -0
- package/framework/tasks/greenfield-wu-scaffold-detection.md +203 -0
- package/framework/tasks/greenfield-wu-tech-stack-assess.md +255 -0
- package/framework/tasks/orchestrator-deviation.md +260 -0
- package/framework/tasks/orchestrator-escalate.md +276 -0
- package/framework/tasks/orchestrator-handoff.md +243 -0
- package/framework/tasks/orchestrator-health.md +372 -0
- package/framework/tasks/orchestrator-mode-switch.md +262 -0
- package/framework/tasks/orchestrator-resume.md +189 -0
- package/framework/tasks/orchestrator-route.md +169 -0
- package/framework/tasks/orchestrator-spawn-terminal.md +358 -0
- package/framework/tasks/orchestrator-status.md +260 -0
- package/framework/tasks/orchestrator-suggest-mode.md +372 -0
- package/framework/tasks/phases-breakdown.md +91 -0
- package/framework/tasks/phases-dependency-mapping.md +67 -0
- package/framework/tasks/phases-mvp-scoping.md +94 -0
- package/framework/tasks/qa-impl-consolidate.md +522 -0
- package/framework/tasks/qa-impl-performance-test.md +487 -0
- package/framework/tasks/qa-impl-regression-check.md +413 -0
- package/framework/tasks/qa-impl-sast-scan.md +402 -0
- package/framework/tasks/qa-impl-test-execute.md +344 -0
- package/framework/tasks/qa-impl-verdict.md +339 -0
- package/framework/tasks/qa-planning-consolidate.md +309 -0
- package/framework/tasks/qa-planning-coverage-plan.md +338 -0
- package/framework/tasks/qa-planning-gate-define.md +339 -0
- package/framework/tasks/qa-planning-risk-matrix.md +631 -0
- package/framework/tasks/qa-planning-test-strategy.md +217 -0
- package/framework/tasks/tasks-acceptance-write.md +75 -0
- package/framework/tasks/tasks-consolidate.md +57 -0
- package/framework/tasks/tasks-decompose.md +80 -0
- package/framework/tasks/tasks-estimate.md +66 -0
- package/framework/tasks/ux-a11y-check.md +49 -0
- package/framework/tasks/ux-component-map.md +55 -0
- package/framework/tasks/ux-consolidate.md +46 -0
- package/framework/tasks/ux-user-flow.md +46 -0
- package/framework/tasks/ux-wireframe.md +76 -0
- package/package.json +1 -1
- package/scripts/bundle-framework.js +2 -0
- package/scripts/changelog-generator.js +222 -0
- package/scripts/codebase-mapper.js +728 -0
- package/scripts/commit-message-generator.js +167 -0
- package/scripts/coverage-analyzer.js +260 -0
- package/scripts/dependency-analyzer.js +280 -0
- package/scripts/framework-analyzer.js +308 -0
- package/scripts/generate-constitution-domain.js +253 -0
- package/scripts/health-check.js +481 -0
- package/scripts/ide-sync.js +327 -0
- package/scripts/performance-analyzer.js +325 -0
- package/scripts/plan-tracker.js +278 -0
- package/scripts/populate-entity-registry.js +481 -0
- package/scripts/pr-review.js +317 -0
- package/scripts/rollback-manager.js +310 -0
- package/scripts/stuck-detector.js +343 -0
- package/scripts/test-quality-assessment.js +257 -0
- package/scripts/validate-agents.js +367 -0
- package/scripts/validate-tasks.js +465 -0
- package/src/autonomy/autonomous-gate.js +293 -0
- package/src/autonomy/index.js +51 -0
- package/src/autonomy/mode-manager.js +225 -0
- package/src/autonomy/mode-suggester.js +283 -0
- package/src/autonomy/progress-reporter.js +268 -0
- package/src/autonomy/safety-net.js +320 -0
- package/src/context/bracket-tracker.js +79 -0
- package/src/context/domain-loader.js +107 -0
- package/src/context/engine.js +144 -0
- package/src/context/formatter.js +184 -0
- package/src/context/index.js +4 -0
- package/src/context/layers/l0-constitution.js +28 -0
- package/src/context/layers/l1-global.js +37 -0
- package/src/context/layers/l2-agent.js +39 -0
- package/src/context/layers/l3-workflow.js +42 -0
- package/src/context/layers/l4-task.js +24 -0
- package/src/decision/analyzer.js +167 -0
- package/src/decision/engine.js +270 -0
- package/src/decision/index.js +38 -0
- package/src/decision/registry-healer.js +450 -0
- package/src/decision/registry-updater.js +330 -0
- package/src/gates/circuit-breaker.js +119 -0
- package/src/gates/g1-planning-complete.js +153 -0
- package/src/gates/g2-qa-planning.js +153 -0
- package/src/gates/g3-implementation.js +188 -0
- package/src/gates/g4-qa-implementation.js +207 -0
- package/src/gates/g5-deploy-ready.js +180 -0
- package/src/gates/gate-base.js +144 -0
- package/src/gates/index.js +46 -0
- package/src/installer/brownfield-upgrader.js +249 -0
- package/src/installer/core.js +55 -3
- package/src/installer/file-hasher.js +51 -0
- package/src/installer/manifest.js +117 -0
- package/src/installer/templates.js +17 -15
- package/src/installer/transaction.js +229 -0
- package/src/installer/validator.js +18 -1
- package/src/intelligence/registry-manager.js +2 -2
- package/src/memory/agent-memory.js +255 -0
- package/src/memory/gotchas-injector.js +72 -0
- package/src/memory/gotchas.js +361 -0
- package/src/memory/index.js +35 -0
- package/src/memory/search.js +233 -0
- package/src/memory/session-digest.js +239 -0
- package/src/merger/env-merger.js +112 -0
- package/src/merger/index.js +56 -0
- package/src/merger/replace-merger.js +51 -0
- package/src/merger/yaml-merger.js +127 -0
- package/src/orchestrator/agent-selector.js +285 -0
- package/src/orchestrator/deviation-handler.js +350 -0
- package/src/orchestrator/handoff-engine.js +271 -0
- package/src/orchestrator/index.js +67 -0
- package/src/orchestrator/intent-classifier.js +264 -0
- package/src/orchestrator/pipeline-manager.js +492 -0
- package/src/orchestrator/pipeline-state.js +223 -0
- package/src/orchestrator/session-manager.js +409 -0
- package/src/tasks/executor.js +195 -0
- package/src/tasks/handoff.js +226 -0
- package/src/tasks/index.js +4 -0
- package/src/tasks/loader.js +210 -0
- package/src/tasks/router.js +182 -0
- package/src/terminal/collector.js +216 -0
- package/src/terminal/index.js +30 -0
- package/src/terminal/isolation.js +129 -0
- package/src/terminal/monitor.js +277 -0
- package/src/terminal/spawner.js +269 -0
- package/src/upgrade/checker.js +1 -1
- package/src/wizard/i18n.js +3 -3
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PR Review — Automated pull request review helper.
|
|
3
|
+
*
|
|
4
|
+
* Uses git diff to analyze changes between branches, assess risk levels,
|
|
5
|
+
* and generate formatted review reports suitable for PR comments.
|
|
6
|
+
*
|
|
7
|
+
* @module scripts/pr-review
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execSync } from 'node:child_process';
|
|
11
|
+
import { extname, dirname } from 'node:path';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {Object} ChangedFiles
|
|
15
|
+
* @property {string[]} added
|
|
16
|
+
* @property {string[]} modified
|
|
17
|
+
* @property {string[]} deleted
|
|
18
|
+
* @property {string[]} renamed
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} ChangeAnalysis
|
|
23
|
+
* @property {string} file
|
|
24
|
+
* @property {string} changeType — 'added' | 'modified' | 'deleted' | 'renamed'
|
|
25
|
+
* @property {string} risk — 'low' | 'medium' | 'high'
|
|
26
|
+
* @property {string[]} suggestions
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {Object} ReviewReport
|
|
31
|
+
* @property {ChangedFiles} changedFiles
|
|
32
|
+
* @property {ChangeAnalysis[]} analyses
|
|
33
|
+
* @property {{ high: number, medium: number, low: number }} riskSummary
|
|
34
|
+
* @property {number} totalFiles
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns paths that should receive extra scrutiny during review.
|
|
39
|
+
* @returns {string[]}
|
|
40
|
+
*/
|
|
41
|
+
export function getSensitivePaths() {
|
|
42
|
+
return [
|
|
43
|
+
'auth/',
|
|
44
|
+
'security/',
|
|
45
|
+
'database/',
|
|
46
|
+
'migrations/',
|
|
47
|
+
'middleware/',
|
|
48
|
+
'.env',
|
|
49
|
+
'.env.',
|
|
50
|
+
'package.json',
|
|
51
|
+
'package-lock.json',
|
|
52
|
+
'yarn.lock',
|
|
53
|
+
'pnpm-lock.yaml',
|
|
54
|
+
'Dockerfile',
|
|
55
|
+
'docker-compose',
|
|
56
|
+
'.github/workflows/',
|
|
57
|
+
'tsconfig.json',
|
|
58
|
+
'config/',
|
|
59
|
+
'secrets/',
|
|
60
|
+
'credentials',
|
|
61
|
+
'prisma/schema',
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Executes a git command and returns the output.
|
|
67
|
+
* @param {string} cmd
|
|
68
|
+
* @param {string} cwd
|
|
69
|
+
* @returns {string}
|
|
70
|
+
*/
|
|
71
|
+
function git(cmd, cwd) {
|
|
72
|
+
try {
|
|
73
|
+
return execSync(`git ${cmd}`, {
|
|
74
|
+
cwd,
|
|
75
|
+
encoding: 'utf-8',
|
|
76
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
77
|
+
}).trim();
|
|
78
|
+
} catch {
|
|
79
|
+
return '';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Gets the list of changed files between two branches.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} base — base branch name
|
|
87
|
+
* @param {string} head — head branch name
|
|
88
|
+
* @param {string} [cwd=process.cwd()]
|
|
89
|
+
* @returns {ChangedFiles}
|
|
90
|
+
*/
|
|
91
|
+
export function getChangedFiles(base, head, cwd = process.cwd()) {
|
|
92
|
+
const output = git(`diff --name-status ${base}...${head}`, cwd);
|
|
93
|
+
if (!output) {
|
|
94
|
+
return { added: [], modified: [], deleted: [], renamed: [] };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const added = [];
|
|
98
|
+
const modified = [];
|
|
99
|
+
const deleted = [];
|
|
100
|
+
const renamed = [];
|
|
101
|
+
|
|
102
|
+
for (const line of output.split('\n')) {
|
|
103
|
+
if (!line.trim()) continue;
|
|
104
|
+
const parts = line.split('\t');
|
|
105
|
+
const status = parts[0].trim();
|
|
106
|
+
const file = parts[parts.length - 1].trim();
|
|
107
|
+
|
|
108
|
+
if (status === 'A') added.push(file);
|
|
109
|
+
else if (status === 'M') modified.push(file);
|
|
110
|
+
else if (status === 'D') deleted.push(file);
|
|
111
|
+
else if (status.startsWith('R')) renamed.push(file);
|
|
112
|
+
else modified.push(file); // default to modified for unknown statuses
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { added, modified, deleted, renamed };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Determines if a file path touches a sensitive area.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} filePath
|
|
122
|
+
* @returns {boolean}
|
|
123
|
+
*/
|
|
124
|
+
function isSensitivePath(filePath) {
|
|
125
|
+
const sensitive = getSensitivePaths();
|
|
126
|
+
const lower = filePath.toLowerCase();
|
|
127
|
+
return sensitive.some((s) => lower.includes(s.toLowerCase()));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Analyzes a single file change and assesses risk.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} filePath
|
|
134
|
+
* @param {string} changeType — 'added' | 'modified' | 'deleted' | 'renamed'
|
|
135
|
+
* @returns {ChangeAnalysis}
|
|
136
|
+
*/
|
|
137
|
+
export function analyzeChange(filePath, changeType) {
|
|
138
|
+
const suggestions = [];
|
|
139
|
+
let risk = 'low';
|
|
140
|
+
|
|
141
|
+
const ext = extname(filePath);
|
|
142
|
+
const dir = dirname(filePath);
|
|
143
|
+
const sensitive = isSensitivePath(filePath);
|
|
144
|
+
|
|
145
|
+
// Risk assessment based on change type
|
|
146
|
+
if (changeType === 'deleted') {
|
|
147
|
+
risk = 'high';
|
|
148
|
+
suggestions.push('Verify this file is no longer referenced anywhere');
|
|
149
|
+
suggestions.push('Check for imports or requires that depend on this file');
|
|
150
|
+
} else if (changeType === 'modified') {
|
|
151
|
+
risk = 'medium';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Sensitive path escalation
|
|
155
|
+
if (sensitive) {
|
|
156
|
+
risk = 'high';
|
|
157
|
+
suggestions.push('This file is in a sensitive area — review carefully');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Test-only changes are low risk
|
|
161
|
+
if (filePath.includes('.test.') || filePath.includes('.spec.') ||
|
|
162
|
+
filePath.includes('__tests__') || dir.includes('test')) {
|
|
163
|
+
if (!sensitive) {
|
|
164
|
+
risk = 'low';
|
|
165
|
+
}
|
|
166
|
+
suggestions.push('Verify tests pass after this change');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Config file changes
|
|
170
|
+
if (['.json', '.yaml', '.yml', '.toml', '.env'].includes(ext) ||
|
|
171
|
+
filePath.includes('config')) {
|
|
172
|
+
if (risk !== 'high') risk = 'medium';
|
|
173
|
+
suggestions.push('Configuration change — verify all environments');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Lock file changes
|
|
177
|
+
if (filePath.includes('lock') || filePath === 'package-lock.json' ||
|
|
178
|
+
filePath === 'yarn.lock' || filePath === 'pnpm-lock.yaml') {
|
|
179
|
+
risk = 'medium';
|
|
180
|
+
suggestions.push('Lock file changed — ensure dependencies are intentional');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Migration files
|
|
184
|
+
if (filePath.includes('migration')) {
|
|
185
|
+
risk = 'high';
|
|
186
|
+
suggestions.push('Database migration — verify rollback strategy');
|
|
187
|
+
suggestions.push('Test migration on a staging database first');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Large file additions (just flag it)
|
|
191
|
+
if (changeType === 'added') {
|
|
192
|
+
if (['.js', '.ts', '.jsx', '.tsx'].includes(ext)) {
|
|
193
|
+
suggestions.push('New file — ensure it follows project conventions');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { file: filePath, changeType, risk, suggestions };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Performs a full review of changes between two branches.
|
|
202
|
+
*
|
|
203
|
+
* @param {Object} options
|
|
204
|
+
* @param {string} options.baseBranch
|
|
205
|
+
* @param {string} options.headBranch
|
|
206
|
+
* @param {string} [options.targetDir]
|
|
207
|
+
* @returns {ReviewReport}
|
|
208
|
+
*/
|
|
209
|
+
export function reviewPR(options) {
|
|
210
|
+
const { baseBranch, headBranch, targetDir = process.cwd() } = options;
|
|
211
|
+
|
|
212
|
+
const changedFiles = getChangedFiles(baseBranch, headBranch, targetDir);
|
|
213
|
+
const analyses = [];
|
|
214
|
+
|
|
215
|
+
for (const file of changedFiles.added) {
|
|
216
|
+
analyses.push(analyzeChange(file, 'added'));
|
|
217
|
+
}
|
|
218
|
+
for (const file of changedFiles.modified) {
|
|
219
|
+
analyses.push(analyzeChange(file, 'modified'));
|
|
220
|
+
}
|
|
221
|
+
for (const file of changedFiles.deleted) {
|
|
222
|
+
analyses.push(analyzeChange(file, 'deleted'));
|
|
223
|
+
}
|
|
224
|
+
for (const file of changedFiles.renamed) {
|
|
225
|
+
analyses.push(analyzeChange(file, 'renamed'));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const riskSummary = { high: 0, medium: 0, low: 0 };
|
|
229
|
+
for (const a of analyses) {
|
|
230
|
+
riskSummary[a.risk]++;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
changedFiles,
|
|
235
|
+
analyses,
|
|
236
|
+
riskSummary,
|
|
237
|
+
totalFiles:
|
|
238
|
+
changedFiles.added.length +
|
|
239
|
+
changedFiles.modified.length +
|
|
240
|
+
changedFiles.deleted.length +
|
|
241
|
+
changedFiles.renamed.length,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Formats a review report as markdown suitable for a PR comment.
|
|
247
|
+
*
|
|
248
|
+
* @param {ReviewReport} report
|
|
249
|
+
* @returns {string}
|
|
250
|
+
*/
|
|
251
|
+
export function formatReviewReport(report) {
|
|
252
|
+
const lines = [];
|
|
253
|
+
|
|
254
|
+
lines.push('## PR Review Summary');
|
|
255
|
+
lines.push('');
|
|
256
|
+
lines.push(`**Files changed:** ${report.totalFiles}`);
|
|
257
|
+
lines.push(`**Risk breakdown:** ${report.riskSummary.high} high, ${report.riskSummary.medium} medium, ${report.riskSummary.low} low`);
|
|
258
|
+
lines.push('');
|
|
259
|
+
|
|
260
|
+
// File change overview
|
|
261
|
+
const { changedFiles } = report;
|
|
262
|
+
if (changedFiles.added.length > 0) {
|
|
263
|
+
lines.push(`**Added (${changedFiles.added.length}):** ${changedFiles.added.join(', ')}`);
|
|
264
|
+
}
|
|
265
|
+
if (changedFiles.modified.length > 0) {
|
|
266
|
+
lines.push(`**Modified (${changedFiles.modified.length}):** ${changedFiles.modified.join(', ')}`);
|
|
267
|
+
}
|
|
268
|
+
if (changedFiles.deleted.length > 0) {
|
|
269
|
+
lines.push(`**Deleted (${changedFiles.deleted.length}):** ${changedFiles.deleted.join(', ')}`);
|
|
270
|
+
}
|
|
271
|
+
if (changedFiles.renamed.length > 0) {
|
|
272
|
+
lines.push(`**Renamed (${changedFiles.renamed.length}):** ${changedFiles.renamed.join(', ')}`);
|
|
273
|
+
}
|
|
274
|
+
lines.push('');
|
|
275
|
+
|
|
276
|
+
// High-risk items first
|
|
277
|
+
const highRisk = report.analyses.filter((a) => a.risk === 'high');
|
|
278
|
+
if (highRisk.length > 0) {
|
|
279
|
+
lines.push('### High Risk');
|
|
280
|
+
lines.push('');
|
|
281
|
+
for (const item of highRisk) {
|
|
282
|
+
lines.push(`- **${item.file}** (${item.changeType})`);
|
|
283
|
+
for (const suggestion of item.suggestions) {
|
|
284
|
+
lines.push(` - ${suggestion}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
lines.push('');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Medium-risk items
|
|
291
|
+
const mediumRisk = report.analyses.filter((a) => a.risk === 'medium');
|
|
292
|
+
if (mediumRisk.length > 0) {
|
|
293
|
+
lines.push('### Medium Risk');
|
|
294
|
+
lines.push('');
|
|
295
|
+
for (const item of mediumRisk) {
|
|
296
|
+
lines.push(`- **${item.file}** (${item.changeType})`);
|
|
297
|
+
for (const suggestion of item.suggestions) {
|
|
298
|
+
lines.push(` - ${suggestion}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
lines.push('');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Low-risk summary
|
|
305
|
+
const lowRisk = report.analyses.filter((a) => a.risk === 'low');
|
|
306
|
+
if (lowRisk.length > 0) {
|
|
307
|
+
lines.push('### Low Risk');
|
|
308
|
+
lines.push('');
|
|
309
|
+
for (const item of lowRisk) {
|
|
310
|
+
const sugText = item.suggestions.length > 0 ? ` — ${item.suggestions[0]}` : '';
|
|
311
|
+
lines.push(`- ${item.file} (${item.changeType})${sugText}`);
|
|
312
|
+
}
|
|
313
|
+
lines.push('');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return lines.join('\n');
|
|
317
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rollback Manager — Creates and restores file-level checkpoints.
|
|
3
|
+
*
|
|
4
|
+
* Snapshots the current state of project files, stores checkpoint metadata
|
|
5
|
+
* in .chati/checkpoints/, and supports rollback to any saved state.
|
|
6
|
+
*
|
|
7
|
+
* @module scripts/rollback-manager
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
11
|
+
import {
|
|
12
|
+
readFileSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
readdirSync,
|
|
15
|
+
statSync,
|
|
16
|
+
mkdirSync,
|
|
17
|
+
unlinkSync,
|
|
18
|
+
rmdirSync,
|
|
19
|
+
existsSync,
|
|
20
|
+
copyFileSync,
|
|
21
|
+
} from 'node:fs';
|
|
22
|
+
import { join, relative, dirname } from 'node:path';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {Object} FileSnapshot
|
|
26
|
+
* @property {string} path — relative path from targetDir
|
|
27
|
+
* @property {string} hash — sha256 hex of file content
|
|
28
|
+
* @property {boolean} exists — whether file exists at checkpoint time
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {Object} Checkpoint
|
|
33
|
+
* @property {string} id
|
|
34
|
+
* @property {string} label
|
|
35
|
+
* @property {number} timestamp
|
|
36
|
+
* @property {FileSnapshot[]} files
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @typedef {Object} RollbackResult
|
|
41
|
+
* @property {number} restored
|
|
42
|
+
* @property {number} skipped
|
|
43
|
+
* @property {string[]} errors
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Computes sha256 hex hash of a file.
|
|
48
|
+
* @param {string} filePath
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
export function hashFile(filePath) {
|
|
52
|
+
const content = readFileSync(filePath);
|
|
53
|
+
return createHash('sha256').update(content).digest('hex');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Collects all files recursively from a directory (excludes node_modules, .git, .chati).
|
|
58
|
+
* @param {string} dir
|
|
59
|
+
* @param {string} baseDir
|
|
60
|
+
* @returns {string[]} — relative paths
|
|
61
|
+
*/
|
|
62
|
+
function collectAllFiles(dir, baseDir) {
|
|
63
|
+
const results = [];
|
|
64
|
+
const skipDirs = new Set(['node_modules', '.git', '.chati', 'dist', 'build']);
|
|
65
|
+
|
|
66
|
+
function walk(currentDir) {
|
|
67
|
+
let entries;
|
|
68
|
+
try {
|
|
69
|
+
entries = readdirSync(currentDir);
|
|
70
|
+
} catch {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
if (skipDirs.has(entry)) continue;
|
|
75
|
+
const fullPath = join(currentDir, entry);
|
|
76
|
+
let stat;
|
|
77
|
+
try {
|
|
78
|
+
stat = statSync(fullPath);
|
|
79
|
+
} catch {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (stat.isDirectory()) {
|
|
83
|
+
walk(fullPath);
|
|
84
|
+
} else if (stat.isFile()) {
|
|
85
|
+
results.push(relative(baseDir, fullPath));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
walk(dir);
|
|
91
|
+
return results.sort();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export class RollbackManager {
|
|
95
|
+
/**
|
|
96
|
+
* @param {string} targetDir — root project directory
|
|
97
|
+
*/
|
|
98
|
+
constructor(targetDir) {
|
|
99
|
+
this.targetDir = targetDir;
|
|
100
|
+
this.checkpointDir = join(targetDir, '.chati', 'checkpoints');
|
|
101
|
+
this.backupDir = join(targetDir, '.chati', 'checkpoints', '_backups');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Ensures checkpoint directories exist.
|
|
106
|
+
* @private
|
|
107
|
+
*/
|
|
108
|
+
_ensureDirs() {
|
|
109
|
+
if (!existsSync(this.checkpointDir)) {
|
|
110
|
+
mkdirSync(this.checkpointDir, { recursive: true });
|
|
111
|
+
}
|
|
112
|
+
if (!existsSync(this.backupDir)) {
|
|
113
|
+
mkdirSync(this.backupDir, { recursive: true });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Creates a checkpoint of the current file state.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} label — human-readable label
|
|
121
|
+
* @returns {Checkpoint}
|
|
122
|
+
*/
|
|
123
|
+
createCheckpoint(label) {
|
|
124
|
+
this._ensureDirs();
|
|
125
|
+
|
|
126
|
+
const id = randomUUID().split('-')[0]; // short id
|
|
127
|
+
const timestamp = Date.now();
|
|
128
|
+
const relativePaths = collectAllFiles(this.targetDir, this.targetDir);
|
|
129
|
+
|
|
130
|
+
const files = relativePaths.map((relPath) => {
|
|
131
|
+
const fullPath = join(this.targetDir, relPath);
|
|
132
|
+
let fileHash;
|
|
133
|
+
try {
|
|
134
|
+
fileHash = hashFile(fullPath);
|
|
135
|
+
} catch {
|
|
136
|
+
fileHash = '';
|
|
137
|
+
}
|
|
138
|
+
return { path: relPath, hash: fileHash, exists: true };
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Save file backups
|
|
142
|
+
const checkpointBackupDir = join(this.backupDir, id);
|
|
143
|
+
mkdirSync(checkpointBackupDir, { recursive: true });
|
|
144
|
+
|
|
145
|
+
for (const fileSnap of files) {
|
|
146
|
+
const srcPath = join(this.targetDir, fileSnap.path);
|
|
147
|
+
const destPath = join(checkpointBackupDir, fileSnap.path);
|
|
148
|
+
const destDir = dirname(destPath);
|
|
149
|
+
if (!existsSync(destDir)) {
|
|
150
|
+
mkdirSync(destDir, { recursive: true });
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
copyFileSync(srcPath, destPath);
|
|
154
|
+
} catch {
|
|
155
|
+
// Skip files that can't be copied
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const checkpoint = { id, label, timestamp, files };
|
|
160
|
+
const checkpointFile = join(this.checkpointDir, `${id}.json`);
|
|
161
|
+
writeFileSync(checkpointFile, JSON.stringify(checkpoint, null, 2), 'utf-8');
|
|
162
|
+
|
|
163
|
+
return checkpoint;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Restores files to a checkpoint state.
|
|
168
|
+
* Files added after the checkpoint are NOT deleted (safety measure).
|
|
169
|
+
*
|
|
170
|
+
* @param {string} checkpointId
|
|
171
|
+
* @returns {RollbackResult}
|
|
172
|
+
*/
|
|
173
|
+
rollback(checkpointId) {
|
|
174
|
+
const checkpointFile = join(this.checkpointDir, `${checkpointId}.json`);
|
|
175
|
+
if (!existsSync(checkpointFile)) {
|
|
176
|
+
return { restored: 0, skipped: 0, errors: [`Checkpoint "${checkpointId}" not found`] };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let checkpoint;
|
|
180
|
+
try {
|
|
181
|
+
checkpoint = JSON.parse(readFileSync(checkpointFile, 'utf-8'));
|
|
182
|
+
} catch (err) {
|
|
183
|
+
return { restored: 0, skipped: 0, errors: [`Failed to parse checkpoint: ${err.message}`] };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const backupDir = join(this.backupDir, checkpointId);
|
|
187
|
+
let restored = 0;
|
|
188
|
+
let skipped = 0;
|
|
189
|
+
const errors = [];
|
|
190
|
+
|
|
191
|
+
for (const fileSnap of checkpoint.files) {
|
|
192
|
+
const targetPath = join(this.targetDir, fileSnap.path);
|
|
193
|
+
const backupPath = join(backupDir, fileSnap.path);
|
|
194
|
+
|
|
195
|
+
if (!existsSync(backupPath)) {
|
|
196
|
+
skipped++;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check if file has actually changed
|
|
201
|
+
if (existsSync(targetPath)) {
|
|
202
|
+
try {
|
|
203
|
+
const currentHash = hashFile(targetPath);
|
|
204
|
+
if (currentHash === fileSnap.hash) {
|
|
205
|
+
skipped++;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
} catch {
|
|
209
|
+
// proceed with restore
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const destDir = dirname(targetPath);
|
|
215
|
+
if (!existsSync(destDir)) {
|
|
216
|
+
mkdirSync(destDir, { recursive: true });
|
|
217
|
+
}
|
|
218
|
+
copyFileSync(backupPath, targetPath);
|
|
219
|
+
restored++;
|
|
220
|
+
} catch (err) {
|
|
221
|
+
errors.push(`Failed to restore "${fileSnap.path}": ${err.message}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return { restored, skipped, errors };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Lists all saved checkpoints, sorted by timestamp descending.
|
|
230
|
+
* @returns {Checkpoint[]}
|
|
231
|
+
*/
|
|
232
|
+
listCheckpoints() {
|
|
233
|
+
if (!existsSync(this.checkpointDir)) return [];
|
|
234
|
+
|
|
235
|
+
const entries = readdirSync(this.checkpointDir).filter((f) => f.endsWith('.json'));
|
|
236
|
+
const checkpoints = [];
|
|
237
|
+
|
|
238
|
+
for (const entry of entries) {
|
|
239
|
+
try {
|
|
240
|
+
const data = JSON.parse(readFileSync(join(this.checkpointDir, entry), 'utf-8'));
|
|
241
|
+
checkpoints.push(data);
|
|
242
|
+
} catch {
|
|
243
|
+
// Skip corrupt files
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return checkpoints.sort((a, b) => b.timestamp - a.timestamp);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Deletes a checkpoint and its backup files.
|
|
252
|
+
*
|
|
253
|
+
* @param {string} checkpointId
|
|
254
|
+
* @returns {boolean}
|
|
255
|
+
*/
|
|
256
|
+
deleteCheckpoint(checkpointId) {
|
|
257
|
+
const checkpointFile = join(this.checkpointDir, `${checkpointId}.json`);
|
|
258
|
+
if (!existsSync(checkpointFile)) return false;
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
unlinkSync(checkpointFile);
|
|
262
|
+
} catch {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Clean up backup directory
|
|
267
|
+
const backupDir = join(this.backupDir, checkpointId);
|
|
268
|
+
if (existsSync(backupDir)) {
|
|
269
|
+
this._removeDirRecursive(backupDir);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Returns the most recent checkpoint or null.
|
|
277
|
+
* @returns {Checkpoint|null}
|
|
278
|
+
*/
|
|
279
|
+
getLatestCheckpoint() {
|
|
280
|
+
const checkpoints = this.listCheckpoints();
|
|
281
|
+
return checkpoints.length > 0 ? checkpoints[0] : null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Recursively removes a directory.
|
|
286
|
+
* @param {string} dir
|
|
287
|
+
* @private
|
|
288
|
+
*/
|
|
289
|
+
_removeDirRecursive(dir) {
|
|
290
|
+
if (!existsSync(dir)) return;
|
|
291
|
+
|
|
292
|
+
const entries = readdirSync(dir);
|
|
293
|
+
for (const entry of entries) {
|
|
294
|
+
const fullPath = join(dir, entry);
|
|
295
|
+
const stat = statSync(fullPath);
|
|
296
|
+
if (stat.isDirectory()) {
|
|
297
|
+
this._removeDirRecursive(fullPath);
|
|
298
|
+
} else {
|
|
299
|
+
unlinkSync(fullPath);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Remove the now-empty directory
|
|
304
|
+
try {
|
|
305
|
+
rmdirSync(dir);
|
|
306
|
+
} catch {
|
|
307
|
+
// directory may already be gone
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|