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.
Files changed (215) hide show
  1. package/README.md +7 -6
  2. package/framework/agents/build/dev.md +343 -0
  3. package/framework/agents/clarity/architect.md +113 -0
  4. package/framework/agents/clarity/brief.md +183 -0
  5. package/framework/agents/clarity/brownfield-wu.md +182 -0
  6. package/framework/agents/clarity/detail.md +111 -0
  7. package/framework/agents/clarity/greenfield-wu.md +154 -0
  8. package/framework/agents/clarity/phases.md +1 -0
  9. package/framework/agents/clarity/tasks.md +1 -0
  10. package/framework/agents/clarity/ux.md +113 -0
  11. package/framework/agents/deploy/devops.md +1 -0
  12. package/framework/agents/quality/qa-implementation.md +1 -0
  13. package/framework/agents/quality/qa-planning.md +1 -0
  14. package/framework/config.yaml +3 -3
  15. package/framework/constitution.md +58 -1
  16. package/framework/context/governance.md +37 -0
  17. package/framework/context/protocols.md +34 -0
  18. package/framework/context/quality.md +27 -0
  19. package/framework/context/root.md +24 -0
  20. package/framework/data/entity-registry.yaml +1 -1
  21. package/framework/domains/agents/architect.yaml +51 -0
  22. package/framework/domains/agents/brief.yaml +47 -0
  23. package/framework/domains/agents/brownfield-wu.yaml +49 -0
  24. package/framework/domains/agents/detail.yaml +47 -0
  25. package/framework/domains/agents/dev.yaml +49 -0
  26. package/framework/domains/agents/devops.yaml +43 -0
  27. package/framework/domains/agents/greenfield-wu.yaml +47 -0
  28. package/framework/domains/agents/orchestrator.yaml +49 -0
  29. package/framework/domains/agents/phases.yaml +47 -0
  30. package/framework/domains/agents/qa-implementation.yaml +43 -0
  31. package/framework/domains/agents/qa-planning.yaml +44 -0
  32. package/framework/domains/agents/tasks.yaml +48 -0
  33. package/framework/domains/agents/ux.yaml +50 -0
  34. package/framework/domains/constitution.yaml +77 -0
  35. package/framework/domains/global.yaml +64 -0
  36. package/framework/domains/workflows/brownfield-discovery.yaml +16 -0
  37. package/framework/domains/workflows/brownfield-fullstack.yaml +26 -0
  38. package/framework/domains/workflows/brownfield-service.yaml +22 -0
  39. package/framework/domains/workflows/brownfield-ui.yaml +22 -0
  40. package/framework/domains/workflows/greenfield-fullstack.yaml +26 -0
  41. package/framework/hooks/constitution-guard.js +101 -0
  42. package/framework/hooks/mode-governance.js +92 -0
  43. package/framework/hooks/model-governance.js +76 -0
  44. package/framework/hooks/prism-engine.js +89 -0
  45. package/framework/hooks/session-digest.js +60 -0
  46. package/framework/hooks/settings.json +44 -0
  47. package/framework/i18n/en.yaml +3 -3
  48. package/framework/i18n/es.yaml +3 -3
  49. package/framework/i18n/fr.yaml +3 -3
  50. package/framework/i18n/pt.yaml +3 -3
  51. package/framework/intelligence/context-engine.md +2 -2
  52. package/framework/intelligence/decision-engine.md +1 -1
  53. package/framework/migrations/v1.4-to-v2.0.yaml +167 -0
  54. package/framework/migrations/v2.0-to-v2.0.1.yaml +132 -0
  55. package/framework/orchestrator/chati.md +350 -7
  56. package/framework/schemas/session.schema.json +15 -0
  57. package/framework/tasks/architect-api-design.md +63 -0
  58. package/framework/tasks/architect-consolidate.md +47 -0
  59. package/framework/tasks/architect-db-design.md +73 -0
  60. package/framework/tasks/architect-design.md +95 -0
  61. package/framework/tasks/architect-security-review.md +62 -0
  62. package/framework/tasks/architect-stack-selection.md +53 -0
  63. package/framework/tasks/brief-consolidate.md +249 -0
  64. package/framework/tasks/brief-constraint-identify.md +277 -0
  65. package/framework/tasks/brief-extract-requirements.md +339 -0
  66. package/framework/tasks/brief-stakeholder-map.md +176 -0
  67. package/framework/tasks/brief-validate-completeness.md +121 -0
  68. package/framework/tasks/brownfield-wu-architecture-map.md +394 -0
  69. package/framework/tasks/brownfield-wu-deep-discovery.md +312 -0
  70. package/framework/tasks/brownfield-wu-dependency-scan.md +359 -0
  71. package/framework/tasks/brownfield-wu-migration-plan.md +483 -0
  72. package/framework/tasks/brownfield-wu-report.md +325 -0
  73. package/framework/tasks/brownfield-wu-risk-assess.md +424 -0
  74. package/framework/tasks/detail-acceptance-criteria.md +372 -0
  75. package/framework/tasks/detail-consolidate.md +138 -0
  76. package/framework/tasks/detail-edge-case-analysis.md +300 -0
  77. package/framework/tasks/detail-expand-prd.md +389 -0
  78. package/framework/tasks/detail-nfr-extraction.md +223 -0
  79. package/framework/tasks/dev-code-review.md +404 -0
  80. package/framework/tasks/dev-consolidate.md +543 -0
  81. package/framework/tasks/dev-debug.md +322 -0
  82. package/framework/tasks/dev-implement.md +252 -0
  83. package/framework/tasks/dev-iterate.md +411 -0
  84. package/framework/tasks/dev-pr-prepare.md +497 -0
  85. package/framework/tasks/dev-refactor.md +342 -0
  86. package/framework/tasks/dev-test-write.md +306 -0
  87. package/framework/tasks/devops-ci-setup.md +412 -0
  88. package/framework/tasks/devops-consolidate.md +712 -0
  89. package/framework/tasks/devops-deploy-config.md +598 -0
  90. package/framework/tasks/devops-monitoring-setup.md +658 -0
  91. package/framework/tasks/devops-release-prepare.md +673 -0
  92. package/framework/tasks/greenfield-wu-analyze-empty.md +169 -0
  93. package/framework/tasks/greenfield-wu-report.md +266 -0
  94. package/framework/tasks/greenfield-wu-scaffold-detection.md +203 -0
  95. package/framework/tasks/greenfield-wu-tech-stack-assess.md +255 -0
  96. package/framework/tasks/orchestrator-deviation.md +260 -0
  97. package/framework/tasks/orchestrator-escalate.md +276 -0
  98. package/framework/tasks/orchestrator-handoff.md +243 -0
  99. package/framework/tasks/orchestrator-health.md +372 -0
  100. package/framework/tasks/orchestrator-mode-switch.md +262 -0
  101. package/framework/tasks/orchestrator-resume.md +189 -0
  102. package/framework/tasks/orchestrator-route.md +169 -0
  103. package/framework/tasks/orchestrator-spawn-terminal.md +358 -0
  104. package/framework/tasks/orchestrator-status.md +260 -0
  105. package/framework/tasks/orchestrator-suggest-mode.md +372 -0
  106. package/framework/tasks/phases-breakdown.md +91 -0
  107. package/framework/tasks/phases-dependency-mapping.md +67 -0
  108. package/framework/tasks/phases-mvp-scoping.md +94 -0
  109. package/framework/tasks/qa-impl-consolidate.md +522 -0
  110. package/framework/tasks/qa-impl-performance-test.md +487 -0
  111. package/framework/tasks/qa-impl-regression-check.md +413 -0
  112. package/framework/tasks/qa-impl-sast-scan.md +402 -0
  113. package/framework/tasks/qa-impl-test-execute.md +344 -0
  114. package/framework/tasks/qa-impl-verdict.md +339 -0
  115. package/framework/tasks/qa-planning-consolidate.md +309 -0
  116. package/framework/tasks/qa-planning-coverage-plan.md +338 -0
  117. package/framework/tasks/qa-planning-gate-define.md +339 -0
  118. package/framework/tasks/qa-planning-risk-matrix.md +631 -0
  119. package/framework/tasks/qa-planning-test-strategy.md +217 -0
  120. package/framework/tasks/tasks-acceptance-write.md +75 -0
  121. package/framework/tasks/tasks-consolidate.md +57 -0
  122. package/framework/tasks/tasks-decompose.md +80 -0
  123. package/framework/tasks/tasks-estimate.md +66 -0
  124. package/framework/tasks/ux-a11y-check.md +49 -0
  125. package/framework/tasks/ux-component-map.md +55 -0
  126. package/framework/tasks/ux-consolidate.md +46 -0
  127. package/framework/tasks/ux-user-flow.md +46 -0
  128. package/framework/tasks/ux-wireframe.md +76 -0
  129. package/package.json +1 -1
  130. package/scripts/bundle-framework.js +2 -0
  131. package/scripts/changelog-generator.js +222 -0
  132. package/scripts/codebase-mapper.js +728 -0
  133. package/scripts/commit-message-generator.js +167 -0
  134. package/scripts/coverage-analyzer.js +260 -0
  135. package/scripts/dependency-analyzer.js +280 -0
  136. package/scripts/framework-analyzer.js +308 -0
  137. package/scripts/generate-constitution-domain.js +253 -0
  138. package/scripts/health-check.js +481 -0
  139. package/scripts/ide-sync.js +327 -0
  140. package/scripts/performance-analyzer.js +325 -0
  141. package/scripts/plan-tracker.js +278 -0
  142. package/scripts/populate-entity-registry.js +481 -0
  143. package/scripts/pr-review.js +317 -0
  144. package/scripts/rollback-manager.js +310 -0
  145. package/scripts/stuck-detector.js +343 -0
  146. package/scripts/test-quality-assessment.js +257 -0
  147. package/scripts/validate-agents.js +367 -0
  148. package/scripts/validate-tasks.js +465 -0
  149. package/src/autonomy/autonomous-gate.js +293 -0
  150. package/src/autonomy/index.js +51 -0
  151. package/src/autonomy/mode-manager.js +225 -0
  152. package/src/autonomy/mode-suggester.js +283 -0
  153. package/src/autonomy/progress-reporter.js +268 -0
  154. package/src/autonomy/safety-net.js +320 -0
  155. package/src/context/bracket-tracker.js +79 -0
  156. package/src/context/domain-loader.js +107 -0
  157. package/src/context/engine.js +144 -0
  158. package/src/context/formatter.js +184 -0
  159. package/src/context/index.js +4 -0
  160. package/src/context/layers/l0-constitution.js +28 -0
  161. package/src/context/layers/l1-global.js +37 -0
  162. package/src/context/layers/l2-agent.js +39 -0
  163. package/src/context/layers/l3-workflow.js +42 -0
  164. package/src/context/layers/l4-task.js +24 -0
  165. package/src/decision/analyzer.js +167 -0
  166. package/src/decision/engine.js +270 -0
  167. package/src/decision/index.js +38 -0
  168. package/src/decision/registry-healer.js +450 -0
  169. package/src/decision/registry-updater.js +330 -0
  170. package/src/gates/circuit-breaker.js +119 -0
  171. package/src/gates/g1-planning-complete.js +153 -0
  172. package/src/gates/g2-qa-planning.js +153 -0
  173. package/src/gates/g3-implementation.js +188 -0
  174. package/src/gates/g4-qa-implementation.js +207 -0
  175. package/src/gates/g5-deploy-ready.js +180 -0
  176. package/src/gates/gate-base.js +144 -0
  177. package/src/gates/index.js +46 -0
  178. package/src/installer/brownfield-upgrader.js +249 -0
  179. package/src/installer/core.js +55 -3
  180. package/src/installer/file-hasher.js +51 -0
  181. package/src/installer/manifest.js +117 -0
  182. package/src/installer/templates.js +17 -15
  183. package/src/installer/transaction.js +229 -0
  184. package/src/installer/validator.js +18 -1
  185. package/src/intelligence/registry-manager.js +2 -2
  186. package/src/memory/agent-memory.js +255 -0
  187. package/src/memory/gotchas-injector.js +72 -0
  188. package/src/memory/gotchas.js +361 -0
  189. package/src/memory/index.js +35 -0
  190. package/src/memory/search.js +233 -0
  191. package/src/memory/session-digest.js +239 -0
  192. package/src/merger/env-merger.js +112 -0
  193. package/src/merger/index.js +56 -0
  194. package/src/merger/replace-merger.js +51 -0
  195. package/src/merger/yaml-merger.js +127 -0
  196. package/src/orchestrator/agent-selector.js +285 -0
  197. package/src/orchestrator/deviation-handler.js +350 -0
  198. package/src/orchestrator/handoff-engine.js +271 -0
  199. package/src/orchestrator/index.js +67 -0
  200. package/src/orchestrator/intent-classifier.js +264 -0
  201. package/src/orchestrator/pipeline-manager.js +492 -0
  202. package/src/orchestrator/pipeline-state.js +223 -0
  203. package/src/orchestrator/session-manager.js +409 -0
  204. package/src/tasks/executor.js +195 -0
  205. package/src/tasks/handoff.js +226 -0
  206. package/src/tasks/index.js +4 -0
  207. package/src/tasks/loader.js +210 -0
  208. package/src/tasks/router.js +182 -0
  209. package/src/terminal/collector.js +216 -0
  210. package/src/terminal/index.js +30 -0
  211. package/src/terminal/isolation.js +129 -0
  212. package/src/terminal/monitor.js +277 -0
  213. package/src/terminal/spawner.js +269 -0
  214. package/src/upgrade/checker.js +1 -1
  215. 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
+ }