chati-dev 4.2.2 → 4.3.0

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 (220) hide show
  1. package/README.md +80 -53
  2. package/bin/chati.js +150 -5
  3. package/framework/agents/build/dev.md +509 -76
  4. package/framework/agents/deploy/devops.md +40 -25
  5. package/framework/agents/discover/brief.md +156 -22
  6. package/framework/agents/discover/brownfield-wu.md +24 -14
  7. package/framework/agents/discover/greenfield-wu.md +100 -14
  8. package/framework/agents/plan/architect-data-engineer.md +6 -6
  9. package/framework/agents/plan/architect-system.md +46 -12
  10. package/framework/agents/plan/architect.md +40 -20
  11. package/framework/agents/plan/detail.md +36 -24
  12. package/framework/agents/plan/phases.md +36 -26
  13. package/framework/agents/plan/tasks.md +114 -33
  14. package/framework/agents/plan/ux-brand-architect.md +240 -8
  15. package/framework/agents/plan/ux-component-engineer.md +28 -13
  16. package/framework/agents/plan/ux-researcher.md +7 -6
  17. package/framework/agents/plan/ux.md +55 -22
  18. package/framework/agents/quality/qa-implementation.md +143 -74
  19. package/framework/agents/quality/qa-planning.md +115 -42
  20. package/framework/agents/quality/qa-visual.md +439 -0
  21. package/framework/agents/shared/visualizer.md +128 -0
  22. package/framework/config.yaml +7 -6
  23. package/framework/constitution.md +127 -44
  24. package/framework/context/governance.md +12 -7
  25. package/framework/context/quality.md +6 -5
  26. package/framework/context/root.md +6 -6
  27. package/framework/data/entity-registry.yaml +377 -4
  28. package/framework/data/model-limits.json +19 -0
  29. package/framework/domains/agents/qa-visual.yaml +74 -0
  30. package/framework/domains/constitution.yaml +46 -2
  31. package/framework/domains/workflows/greenfield-fullstack.yaml +2 -2
  32. package/framework/hooks/advance-trigger.js +131 -0
  33. package/framework/hooks/brief-validator.js +83 -0
  34. package/framework/hooks/constitution-guard.js +24 -5
  35. package/framework/hooks/license-guard.js +62 -27
  36. package/framework/hooks/mode-governance.js +13 -2
  37. package/framework/hooks/model-governance.js +1 -0
  38. package/framework/hooks/post-dev.js +207 -0
  39. package/framework/hooks/prism-engine.js +274 -105
  40. package/framework/hooks/reasoning-escalator.js +371 -0
  41. package/framework/hooks/reference-trigger.js +117 -0
  42. package/framework/hooks/session-digest.js +50 -1
  43. package/framework/hooks/settings.json +32 -1
  44. package/framework/hooks/style-guard.js +25 -6
  45. package/framework/hooks/team-quality-gate.js +19 -12
  46. package/framework/hooks/undercover-guard.js +4 -2
  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 +4 -5
  52. package/framework/intelligence/decision-engine.md +1 -1
  53. package/framework/intelligence/hooks-performance.md +3 -3
  54. package/framework/migrations/v1.0-to-v1.1.yaml +1 -1
  55. package/framework/migrations/v1.4-to-v2.0.yaml +11 -11
  56. package/framework/migrations/v4.0-to-v4.1.yaml +2 -2
  57. package/framework/migrations/v4.2-to-v4.3.yaml +29 -0
  58. package/framework/orchestrator/chati-router.js +387 -0
  59. package/framework/orchestrator/chati-update.md +40 -40
  60. package/framework/orchestrator/chati.md +294 -94
  61. package/framework/scaffold/motion-premium/README.md +89 -0
  62. package/framework/scaffold/motion-premium/app/globals.css.template +400 -0
  63. package/framework/scaffold/motion-premium/app/layout.tsx.template +110 -0
  64. package/framework/scaffold/motion-premium/components/animation/BackgroundCrossfadeProvider.tsx.template +170 -0
  65. package/framework/scaffold/motion-premium/components/animation/LenisProvider.tsx.template +49 -0
  66. package/framework/scaffold/motion-premium/components/animation/PageTransitionWrapper.tsx.template +83 -0
  67. package/framework/scaffold/motion-premium/components/animation/Preloader.tsx.template +171 -0
  68. package/framework/scaffold/motion-premium/components/ui/Container.tsx.template +69 -0
  69. package/framework/scaffold/motion-premium/components/ui/PageSection.tsx.template +74 -0
  70. package/framework/scaffold/motion-premium/lib/animations/gsap.ts.template +112 -0
  71. package/framework/scaffold/motion-premium/lib/animations/refreshCoordinator.ts.template +75 -0
  72. package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +119 -0
  73. package/framework/scaffold/motion-premium/lib/animations/useGsapContext.ts.template +92 -0
  74. package/framework/scaffold/motion-premium/lib/animations/useScrollSnapStepper.ts.template +265 -0
  75. package/framework/scaffold/motion-premium/lib/animations/useSmoothScroll.ts.template +67 -0
  76. package/framework/scaffold/motion-premium/lib/brand.ts.template +43 -0
  77. package/framework/scaffold/motion-premium/scaffold.yaml +174 -0
  78. package/framework/scaffold/motion-premium-3d/README.md +80 -0
  79. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoCanvas.tsx.template +81 -0
  80. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoClient.tsx.template +75 -0
  81. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/page.tsx.template +26 -0
  82. package/framework/scaffold/motion-premium-3d/components/3d/CameraRig.tsx.template +100 -0
  83. package/framework/scaffold/motion-premium-3d/components/3d/CanvasProvider.tsx.template +85 -0
  84. package/framework/scaffold/motion-premium-3d/components/3d/InvalidateOnScroll.tsx.template +51 -0
  85. package/framework/scaffold/motion-premium-3d/components/3d/MeshCrossfade.tsx.template +79 -0
  86. package/framework/scaffold/motion-premium-3d/components/3d/ScrollCrossfade.tsx.template +88 -0
  87. package/framework/scaffold/motion-premium-3d/components/3d/ScrollScene.tsx.template +121 -0
  88. package/framework/scaffold/motion-premium-3d/components/webgl/SceneFallback.tsx.template +65 -0
  89. package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +68 -0
  90. package/framework/scaffold/motion-premium-3d/lib/webgl/detect.ts.template +69 -0
  91. package/framework/scaffold/motion-premium-3d/scaffold.yaml +133 -0
  92. package/framework/schemas/session.schema.json +109 -21
  93. package/framework/scripts/reference-capture.js +430 -0
  94. package/framework/scripts/visual-qa.js +674 -0
  95. package/framework/tasks/orchestrator-handoff.md +1 -1
  96. package/framework/tasks/orchestrator-resume.md +1 -1
  97. package/framework/tasks/orchestrator-route.md +1 -1
  98. package/framework/tasks/orchestrator-status.md +3 -3
  99. package/framework/tasks/qa-planning-gate-define.md +1 -1
  100. package/framework/templates/brandbook-html-tmpl.md +1 -1
  101. package/framework/templates/brandbook-tmpl.yaml +1 -1
  102. package/framework/templates/component-spec-tmpl.yaml +1 -1
  103. package/framework/templates/design-token-tmpl.yaml +1 -1
  104. package/framework/templates/icon-system-tmpl.yaml +1 -1
  105. package/framework/templates/team-planning-tasks.yaml +6 -5
  106. package/framework/workflows/brownfield-discovery.yaml +2 -2
  107. package/framework/workflows/brownfield-fullstack.yaml +15 -11
  108. package/framework/workflows/brownfield-service.yaml +14 -10
  109. package/framework/workflows/brownfield-ui.yaml +15 -11
  110. package/framework/workflows/greenfield-fullstack.yaml +16 -13
  111. package/framework/workflows/quick-flow.yaml +3 -3
  112. package/framework/workflows/standard-flow.yaml +12 -9
  113. package/package.json +10 -5
  114. package/src/autonomy/autonomous-gate.js +1 -0
  115. package/src/autonomy/build-state.js +1 -2
  116. package/src/autonomy/progress-reporter.js +1 -1
  117. package/src/config/agent-customizer.js +11 -3
  118. package/src/config/claude-settings-generator.js +27 -7
  119. package/src/config/context-file-generator.js +41 -21
  120. package/src/config/framework-adapter.js +1 -0
  121. package/src/config/gemini-hooks-generator.js +19 -7
  122. package/src/config/mcp-configs.js +1 -0
  123. package/src/context/layers/l1-global.js +2 -1
  124. package/src/dashboard/data-reader.js +4 -3
  125. package/src/dashboard/layout.js +2 -1
  126. package/src/decision/analyzer.js +6 -30
  127. package/src/decision/engine.js +4 -28
  128. package/src/decision/registry-healer.js +3 -2
  129. package/src/decision/registry-updater.js +23 -14
  130. package/src/extensions/loader.js +2 -8
  131. package/src/gates/g1-planning-complete.js +2 -1
  132. package/src/gates/g2-qa-planning.js +2 -1
  133. package/src/gates/g3-implementation.js +2 -1
  134. package/src/gates/g4-qa-implementation.js +3 -2
  135. package/src/gates/g5-deploy-ready.js +2 -1
  136. package/src/health/engine.js +4 -3
  137. package/src/installer/core.js +422 -81
  138. package/src/installer/preflight.js +131 -0
  139. package/src/installer/provider-overlay.js +3 -3
  140. package/src/installer/scaffold-applier.js +358 -0
  141. package/src/installer/templates.js +46 -29
  142. package/src/installer/validator.js +17 -12
  143. package/src/intelligence/registry-manager.js +22 -29
  144. package/src/intelligence/timeline.js +11 -6
  145. package/src/license/commands.js +1 -1
  146. package/src/license/wait.js +102 -0
  147. package/src/memory/agent-memory.js +81 -0
  148. package/src/memory/dream.js +32 -1
  149. package/src/merger/replace-merger.js +28 -15
  150. package/src/orchestrator/agent-selector.js +2 -1
  151. package/src/orchestrator/cli.js +1869 -71
  152. package/src/orchestrator/doctor.js +270 -0
  153. package/src/orchestrator/handoff-engine.js +4 -3
  154. package/src/orchestrator/index.js +2 -0
  155. package/src/orchestrator/pipeline-manager.js +306 -15
  156. package/src/orchestrator/session-manager.js +331 -6
  157. package/src/tasks/handoff.js +3 -2
  158. package/src/telemetry/config.js +4 -3
  159. package/src/telemetry/schema.js +1 -0
  160. package/src/terminal/collector.js +3 -2
  161. package/src/terminal/index.js +1 -2
  162. package/src/terminal/isolation.js +52 -18
  163. package/src/terminal/prompt-builder.js +42 -25
  164. package/src/terminal/run-parallel.js +1 -1
  165. package/src/terminal/run-team.js +3 -3
  166. package/src/terminal/team-task-list.js +43 -4
  167. package/src/upgrade/backup.js +3 -2
  168. package/src/upgrade/checker.js +3 -2
  169. package/src/upgrade/migrator.js +65 -7
  170. package/src/upgrade/tracked-files-detector.js +86 -0
  171. package/src/upgrade/user-messages.js +94 -0
  172. package/src/utils/config-parser.js +2 -1
  173. package/src/utils/feature-flags.js +2 -1
  174. package/src/utils/flatten-entities.js +69 -0
  175. package/src/utils/framework-dir.js +16 -0
  176. package/src/utils/model-id.js +85 -0
  177. package/src/utils/provider-limits.js +84 -23
  178. package/src/utils/schema-validator.js +1 -1
  179. package/src/wizard/i18n.js +5 -4
  180. package/src/wizard/index.js +14 -0
  181. package/assets/logo - c/303/263pia.png +0 -0
  182. package/assets/logo.svg +0 -42
  183. package/assets/logo2.png +0 -0
  184. package/assets/social-preview.png +0 -0
  185. package/scripts/bundle-framework.js +0 -69
  186. package/scripts/changelog-generator.js +0 -222
  187. package/scripts/codebase-mapper.js +0 -728
  188. package/scripts/commit-message-generator.js +0 -167
  189. package/scripts/coverage-analyzer.js +0 -260
  190. package/scripts/dependency-analyzer.js +0 -280
  191. package/scripts/doctor/checks/agents.js +0 -77
  192. package/scripts/doctor/checks/constitution.js +0 -41
  193. package/scripts/doctor/checks/domain-alignment.js +0 -58
  194. package/scripts/doctor/checks/prism-layers.js +0 -84
  195. package/scripts/doctor/checks/registry.js +0 -55
  196. package/scripts/doctor/checks/schemas.js +0 -61
  197. package/scripts/doctor/fixes/reference-fix.js +0 -100
  198. package/scripts/doctor/fixes/registry-fix.js +0 -56
  199. package/scripts/doctor/index.js +0 -212
  200. package/scripts/framework-analyzer.js +0 -308
  201. package/scripts/generate-constitution-domain.js +0 -253
  202. package/scripts/generate-signing-key.js +0 -33
  203. package/scripts/health-check.js +0 -481
  204. package/scripts/ide-sync.js +0 -326
  205. package/scripts/performance-analyzer.js +0 -325
  206. package/scripts/plan-tracker.js +0 -278
  207. package/scripts/populate-entity-registry.js +0 -481
  208. package/scripts/pr-review.js +0 -317
  209. package/scripts/rollback-manager.js +0 -310
  210. package/scripts/semantic-lint.js +0 -328
  211. package/scripts/sign-manifest.js +0 -53
  212. package/scripts/stuck-detector.js +0 -343
  213. package/scripts/test-quality-assessment.js +0 -257
  214. package/scripts/validate-agents.js +0 -368
  215. package/scripts/validate-package.js +0 -505
  216. package/scripts/validate-tasks.js +0 -465
  217. package/src/autonomy/worktree-manager.js +0 -250
  218. package/src/intelligence/decision-engine.js +0 -256
  219. package/src/intelligence/document-sharder.js +0 -221
  220. package/src/intelligence/elicitation.js +0 -265
@@ -1,343 +0,0 @@
1
- /**
2
- * StuckDetector — Detects stalled execution patterns.
3
- *
4
- * Identifies consecutive failures, circular approaches, and stall timeouts
5
- * to trigger escalation before wasting further resources.
6
- *
7
- * @module scripts/stuck-detector
8
- */
9
-
10
- /**
11
- * @typedef {Object} Attempt
12
- * @property {string} taskId
13
- * @property {string} agent
14
- * @property {string} approach
15
- * @property {boolean} success
16
- * @property {number} timestamp
17
- * @property {string} [error]
18
- */
19
-
20
- /**
21
- * @typedef {Object} StuckResult
22
- * @property {boolean} stuck
23
- * @property {string} reason
24
- * @property {Object} evidence
25
- */
26
-
27
- /**
28
- * @typedef {Object} CircularResult
29
- * @property {boolean} circular
30
- * @property {string[]} pattern
31
- */
32
-
33
- /**
34
- * @typedef {Object} EscalationContext
35
- * @property {Attempt[]} history
36
- * @property {number} failureCount
37
- * @property {string[]} approaches
38
- * @property {string} suggestion
39
- */
40
-
41
- /**
42
- * @typedef {Object} Stats
43
- * @property {number} totalAttempts
44
- * @property {number} failures
45
- * @property {number} successRate
46
- * @property {number} averageDuration
47
- */
48
-
49
- export class StuckDetector {
50
- /**
51
- * @param {Object} [options]
52
- * @param {number} [options.maxConsecutiveFailures=3]
53
- * @param {number} [options.circularWindow=10]
54
- * @param {number} [options.stallTimeout=300000]
55
- */
56
- constructor(options = {}) {
57
- this.maxConsecutiveFailures = options.maxConsecutiveFailures ?? 3;
58
- this.circularWindow = options.circularWindow ?? 10;
59
- this.stallTimeout = options.stallTimeout ?? 300000; // 5 minutes
60
- /** @type {Attempt[]} */
61
- this.attempts = [];
62
- }
63
-
64
- /**
65
- * Records an execution attempt.
66
- * @param {Attempt} attempt
67
- */
68
- recordAttempt(attempt) {
69
- if (!attempt || typeof attempt !== 'object') {
70
- throw new Error('Attempt must be a non-null object');
71
- }
72
- if (!attempt.taskId || typeof attempt.taskId !== 'string') {
73
- throw new Error('Attempt must have a string taskId');
74
- }
75
- if (!attempt.agent || typeof attempt.agent !== 'string') {
76
- throw new Error('Attempt must have a string agent');
77
- }
78
- if (!attempt.approach || typeof attempt.approach !== 'string') {
79
- throw new Error('Attempt must have a string approach');
80
- }
81
- if (typeof attempt.success !== 'boolean') {
82
- throw new Error('Attempt must have a boolean success');
83
- }
84
-
85
- this.attempts.push({
86
- taskId: attempt.taskId,
87
- agent: attempt.agent,
88
- approach: attempt.approach,
89
- success: attempt.success,
90
- timestamp: attempt.timestamp ?? Date.now(),
91
- error: attempt.error ?? null,
92
- });
93
- }
94
-
95
- /**
96
- * Checks whether execution is stuck.
97
- * Evaluates: consecutive failures, circular approaches, stall timeout.
98
- * @returns {StuckResult}
99
- */
100
- isStuck() {
101
- if (this.attempts.length === 0) {
102
- return { stuck: false, reason: 'No attempts recorded', evidence: {} };
103
- }
104
-
105
- // --- Check consecutive failures ---
106
- const consecutiveFailures = this._countConsecutiveFailures();
107
- if (consecutiveFailures >= this.maxConsecutiveFailures) {
108
- const failedAttempts = this.attempts.slice(-consecutiveFailures);
109
- return {
110
- stuck: true,
111
- reason: `${consecutiveFailures} consecutive failures detected`,
112
- evidence: {
113
- type: 'consecutive_failures',
114
- count: consecutiveFailures,
115
- attempts: failedAttempts.map((a) => ({
116
- taskId: a.taskId,
117
- agent: a.agent,
118
- approach: a.approach,
119
- error: a.error,
120
- })),
121
- },
122
- };
123
- }
124
-
125
- // --- Check circular approaches ---
126
- const recentAttempts = this.attempts.slice(-this.circularWindow);
127
- const circular = this.detectCircularApproach(recentAttempts);
128
- if (circular.circular) {
129
- return {
130
- stuck: true,
131
- reason: `Circular approach pattern detected: ${circular.pattern.join(' -> ')}`,
132
- evidence: {
133
- type: 'circular_approach',
134
- pattern: circular.pattern,
135
- window: recentAttempts.length,
136
- },
137
- };
138
- }
139
-
140
- // --- Check stall timeout ---
141
- const stallResult = this._checkStallTimeout();
142
- if (stallResult.stalled) {
143
- return {
144
- stuck: true,
145
- reason: `Execution stalled for ${Math.round(stallResult.duration / 1000)}s without success`,
146
- evidence: {
147
- type: 'stall_timeout',
148
- duration: stallResult.duration,
149
- threshold: this.stallTimeout,
150
- lastSuccess: stallResult.lastSuccessTimestamp,
151
- },
152
- };
153
- }
154
-
155
- return { stuck: false, reason: 'Execution proceeding normally', evidence: {} };
156
- }
157
-
158
- /**
159
- * Detects if the same approaches are being tried repeatedly (A->B->A->B).
160
- * @param {Attempt[]} attempts
161
- * @returns {CircularResult}
162
- */
163
- detectCircularApproach(attempts) {
164
- if (!attempts || attempts.length < 4) {
165
- return { circular: false, pattern: [] };
166
- }
167
-
168
- const approaches = attempts.map((a) => a.approach);
169
-
170
- // Try pattern lengths from 2 to half the window
171
- const maxPatternLen = Math.floor(approaches.length / 2);
172
- for (let patternLen = 2; patternLen <= maxPatternLen; patternLen++) {
173
- const candidate = approaches.slice(0, patternLen);
174
- let repeats = 0;
175
- let matched = true;
176
-
177
- for (let i = 0; i < approaches.length; i++) {
178
- if (approaches[i] !== candidate[i % patternLen]) {
179
- matched = false;
180
- break;
181
- }
182
- if (i > 0 && i % patternLen === 0) {
183
- repeats++;
184
- }
185
- }
186
-
187
- // Need at least 2 full repetitions to count as circular
188
- if (matched && repeats >= 2) {
189
- return { circular: true, pattern: candidate };
190
- }
191
- }
192
-
193
- // Also check for trailing repetition pattern (recent attempts form a cycle)
194
- for (let patternLen = 2; patternLen <= maxPatternLen; patternLen++) {
195
- const tail = approaches.slice(-patternLen * 2);
196
- if (tail.length < patternLen * 2) continue;
197
-
198
- const firstHalf = tail.slice(0, patternLen);
199
- const secondHalf = tail.slice(patternLen);
200
- const isRepeat = firstHalf.every((val, idx) => val === secondHalf[idx]);
201
-
202
- if (isRepeat) {
203
- return { circular: true, pattern: firstHalf };
204
- }
205
- }
206
-
207
- return { circular: false, pattern: [] };
208
- }
209
-
210
- /**
211
- * Builds context for escalation to a human operator.
212
- * @returns {EscalationContext}
213
- */
214
- getEscalationContext() {
215
- const failures = this.attempts.filter((a) => !a.success);
216
- const uniqueApproaches = [...new Set(this.attempts.map((a) => a.approach))];
217
-
218
- let suggestion = 'Review recent failures and consider an alternative strategy.';
219
-
220
- if (failures.length > 0) {
221
- const errorMessages = failures
222
- .filter((f) => f.error)
223
- .map((f) => f.error);
224
- const uniqueErrors = [...new Set(errorMessages)];
225
-
226
- if (uniqueErrors.length === 1) {
227
- suggestion = `All failures share the same error: "${uniqueErrors[0]}". Investigate this specific root cause.`;
228
- } else if (uniqueApproaches.length <= 2 && failures.length >= 3) {
229
- suggestion =
230
- 'Only 1-2 approaches have been tried. Consider a fundamentally different approach.';
231
- } else if (failures.length === this.attempts.length) {
232
- suggestion =
233
- 'No successful attempts at all. The task may need re-scoping or prerequisite work.';
234
- }
235
- }
236
-
237
- return {
238
- history: [...this.attempts],
239
- failureCount: failures.length,
240
- approaches: uniqueApproaches,
241
- suggestion,
242
- };
243
- }
244
-
245
- /**
246
- * Clears all recorded attempts.
247
- */
248
- reset() {
249
- this.attempts = [];
250
- }
251
-
252
- /**
253
- * Returns aggregate statistics about recorded attempts.
254
- * @returns {Stats}
255
- */
256
- getStats() {
257
- const total = this.attempts.length;
258
- if (total === 0) {
259
- return { totalAttempts: 0, failures: 0, successRate: 0, averageDuration: 0 };
260
- }
261
-
262
- const failures = this.attempts.filter((a) => !a.success).length;
263
- const successRate = ((total - failures) / total) * 100;
264
-
265
- // Calculate average duration between consecutive attempts
266
- let totalDuration = 0;
267
- let durationCount = 0;
268
- for (let i = 1; i < this.attempts.length; i++) {
269
- const diff = this.attempts[i].timestamp - this.attempts[i - 1].timestamp;
270
- if (diff > 0) {
271
- totalDuration += diff;
272
- durationCount++;
273
- }
274
- }
275
- const averageDuration = durationCount > 0 ? totalDuration / durationCount : 0;
276
-
277
- return {
278
- totalAttempts: total,
279
- failures,
280
- successRate: Math.round(successRate * 100) / 100,
281
- averageDuration: Math.round(averageDuration),
282
- };
283
- }
284
-
285
- // ── Private helpers ──────────────────────────────────────────
286
-
287
- /**
288
- * Counts consecutive failures from the most recent attempt backward.
289
- * @returns {number}
290
- * @private
291
- */
292
- _countConsecutiveFailures() {
293
- let count = 0;
294
- for (let i = this.attempts.length - 1; i >= 0; i--) {
295
- if (!this.attempts[i].success) {
296
- count++;
297
- } else {
298
- break;
299
- }
300
- }
301
- return count;
302
- }
303
-
304
- /**
305
- * Checks if execution has stalled (no success within stallTimeout).
306
- * @returns {{ stalled: boolean, duration: number, lastSuccessTimestamp: number|null }}
307
- * @private
308
- */
309
- _checkStallTimeout() {
310
- const now = Date.now();
311
- const successes = this.attempts.filter((a) => a.success);
312
- const lastSuccessTimestamp =
313
- successes.length > 0 ? successes[successes.length - 1].timestamp : null;
314
-
315
- // If we have never succeeded, measure from first attempt
316
- const referenceTime = lastSuccessTimestamp ?? this.attempts[0]?.timestamp ?? now;
317
- const duration = now - referenceTime;
318
-
319
- // Only consider stalled if there are recent failures after reference
320
- const attemptsAfterRef = this.attempts.filter((a) => a.timestamp > referenceTime);
321
- const hasRecentFailures = attemptsAfterRef.some((a) => !a.success);
322
-
323
- return {
324
- stalled: duration >= this.stallTimeout && (hasRecentFailures || !lastSuccessTimestamp),
325
- duration,
326
- lastSuccessTimestamp,
327
- };
328
- }
329
- }
330
-
331
- /**
332
- * Convenience: creates a StuckDetector and evaluates a batch of attempts.
333
- * @param {Attempt[]} attempts
334
- * @param {Object} [options]
335
- * @returns {StuckResult}
336
- */
337
- export function evaluateAttempts(attempts, options = {}) {
338
- const detector = new StuckDetector(options);
339
- for (const attempt of attempts) {
340
- detector.recordAttempt(attempt);
341
- }
342
- return detector.isStuck();
343
- }
@@ -1,257 +0,0 @@
1
- /**
2
- * Test Quality Assessment — Evaluates test file quality through static analysis.
3
- *
4
- * Counts test structures, detects anti-patterns, and calculates quality scores
5
- * to help maintain high testing standards.
6
- *
7
- * @module scripts/test-quality-assessment
8
- */
9
-
10
- import { readFileSync, readdirSync, statSync } from 'node:fs';
11
- import { join } from 'node:path';
12
-
13
- /**
14
- * @typedef {Object} FileReport
15
- * @property {string} filePath
16
- * @property {number} describeCount
17
- * @property {number} itCount
18
- * @property {number} assertionCount
19
- * @property {number} assertionsPerTest
20
- * @property {boolean} hasEdgeCases
21
- * @property {boolean} hasErrorHandling
22
- * @property {boolean} hasSetupTeardown
23
- * @property {string[]} antiPatterns
24
- * @property {number} lineCount
25
- */
26
-
27
- /**
28
- * @typedef {Object} QualityReport
29
- * @property {FileReport[]} files
30
- * @property {Object} overall
31
- * @property {number} overall.score
32
- * @property {string} overall.grade
33
- * @property {string[]} overall.issues
34
- */
35
-
36
- // Patterns for detecting various test constructs
37
- const PATTERNS = {
38
- describe: /\bdescribe\s*\(/g,
39
- it: /\b(?:it|test)\s*\(/g,
40
- assertion: /\b(?:expect|assert|should)\s*[.(]/g,
41
- edgeCase: /\b(?:edge|boundary|limit|overflow|underflow|empty|null|undefined|NaN|zero|negative|max|min)\b/gi,
42
- errorHandling: /\b(?:throw|reject|error|catch|fail|invalid|malformed|corrupt)\b/gi,
43
- beforeEach: /\b(?:beforeEach|beforeAll|before)\s*\(/g,
44
- afterEach: /\b(?:afterEach|afterAll|after)\s*\(/g,
45
- emptyTest: /\b(?:it|test)\s*\(\s*(['"`]).*?\1\s*,\s*(?:(?:async\s*)?\(\s*\)\s*=>\s*\{\s*\}|function\s*\(\s*\)\s*\{\s*\})\s*\)/g,
46
- todoTest: /\b(?:it|test)\s*\.\s*(?:skip|todo)\s*\(/g,
47
- };
48
-
49
- /**
50
- * Finds all *.test.js and *.spec.js files recursively.
51
- * @param {string} dir
52
- * @returns {string[]}
53
- */
54
- export function findTestFiles(dir) {
55
- const results = [];
56
-
57
- function walk(currentDir) {
58
- let entries;
59
- try {
60
- entries = readdirSync(currentDir);
61
- } catch {
62
- return;
63
- }
64
- for (const entry of entries) {
65
- if (entry === 'node_modules' || entry === '.git') continue;
66
- const fullPath = join(currentDir, entry);
67
- let stat;
68
- try {
69
- stat = statSync(fullPath);
70
- } catch {
71
- continue;
72
- }
73
- if (stat.isDirectory()) {
74
- walk(fullPath);
75
- } else if (
76
- stat.isFile() &&
77
- (entry.endsWith('.test.js') || entry.endsWith('.spec.js'))
78
- ) {
79
- results.push(fullPath);
80
- }
81
- }
82
- }
83
-
84
- walk(dir);
85
- return results.sort();
86
- }
87
-
88
- /**
89
- * Assesses quality of a single test file.
90
- * @param {string} filePath
91
- * @returns {FileReport}
92
- */
93
- export function assessSingleFile(filePath) {
94
- let content;
95
- try {
96
- content = readFileSync(filePath, 'utf-8');
97
- } catch (err) {
98
- return {
99
- filePath,
100
- describeCount: 0,
101
- itCount: 0,
102
- assertionCount: 0,
103
- assertionsPerTest: 0,
104
- hasEdgeCases: false,
105
- hasErrorHandling: false,
106
- hasSetupTeardown: false,
107
- antiPatterns: [`Unable to read file: ${err.message}`],
108
- lineCount: 0,
109
- };
110
- }
111
-
112
- const lines = content.split('\n');
113
- const lineCount = lines.length;
114
-
115
- const describeCount = (content.match(PATTERNS.describe) || []).length;
116
- const itCount = (content.match(PATTERNS.it) || []).length;
117
- const assertionCount = (content.match(PATTERNS.assertion) || []).length;
118
- const assertionsPerTest = itCount > 0 ? Math.round((assertionCount / itCount) * 100) / 100 : 0;
119
-
120
- const hasEdgeCases = PATTERNS.edgeCase.test(content);
121
- PATTERNS.edgeCase.lastIndex = 0; // reset regex state
122
-
123
- const hasErrorHandling = PATTERNS.errorHandling.test(content);
124
- PATTERNS.errorHandling.lastIndex = 0;
125
-
126
- const hasBeforeAfter =
127
- PATTERNS.beforeEach.test(content) || PATTERNS.afterEach.test(content);
128
- PATTERNS.beforeEach.lastIndex = 0;
129
- PATTERNS.afterEach.lastIndex = 0;
130
-
131
- // Detect anti-patterns
132
- const antiPatterns = [];
133
-
134
- if (itCount > 0 && assertionCount === 0) {
135
- antiPatterns.push('Tests have no assertions');
136
- }
137
-
138
- const emptyTests = (content.match(PATTERNS.emptyTest) || []).length;
139
- if (emptyTests > 0) {
140
- antiPatterns.push(`${emptyTests} empty test(s) found`);
141
- }
142
-
143
- const todoTests = (content.match(PATTERNS.todoTest) || []).length;
144
- if (todoTests > 0) {
145
- antiPatterns.push(`${todoTests} skipped/todo test(s)`);
146
- }
147
-
148
- // Detect duplicate test names
149
- const testNames = [];
150
- const testNameRegex = /\b(?:it|test)\s*\(\s*(['"`])(.*?)\1/g;
151
- let match;
152
- while ((match = testNameRegex.exec(content)) !== null) {
153
- testNames.push(match[2]);
154
- }
155
- const duplicates = testNames.filter((name, idx) => testNames.indexOf(name) !== idx);
156
- if (duplicates.length > 0) {
157
- const unique = [...new Set(duplicates)];
158
- antiPatterns.push(`Duplicate test names: ${unique.join(', ')}`);
159
- }
160
-
161
- if (assertionsPerTest > 0 && assertionsPerTest < 1) {
162
- antiPatterns.push('Low assertion density (some tests may lack assertions)');
163
- }
164
-
165
- return {
166
- filePath,
167
- describeCount,
168
- itCount,
169
- assertionCount,
170
- assertionsPerTest,
171
- hasEdgeCases,
172
- hasErrorHandling,
173
- hasSetupTeardown: hasBeforeAfter,
174
- antiPatterns,
175
- lineCount,
176
- };
177
- }
178
-
179
- /**
180
- * Calculates an overall quality score from multiple file reports.
181
- * @param {FileReport[]} reports
182
- * @returns {{ score: number, grade: string, issues: string[] }}
183
- */
184
- export function calculateOverallScore(reports) {
185
- if (reports.length === 0) {
186
- return { score: 0, grade: 'F', issues: ['No test files found'] };
187
- }
188
-
189
- const issues = [];
190
- let score = 100;
191
-
192
- // Aggregate stats
193
- const totalTests = reports.reduce((s, r) => s + r.itCount, 0);
194
- const totalAssertions = reports.reduce((s, r) => s + r.assertionCount, 0);
195
- const filesWithEdgeCases = reports.filter((r) => r.hasEdgeCases).length;
196
- const filesWithErrorHandling = reports.filter((r) => r.hasErrorHandling).length;
197
- const filesWithSetup = reports.filter((r) => r.hasSetupTeardown).length;
198
- const allAntiPatterns = reports.flatMap((r) => r.antiPatterns);
199
-
200
- // Deductions
201
- if (totalTests === 0) {
202
- score -= 50;
203
- issues.push('No test cases found');
204
- }
205
-
206
- if (totalTests > 0 && totalAssertions / totalTests < 1) {
207
- score -= 15;
208
- issues.push('Low assertion density across test suite');
209
- }
210
-
211
- const edgeCaseRatio = filesWithEdgeCases / reports.length;
212
- if (edgeCaseRatio < 0.3) {
213
- score -= 10;
214
- issues.push(`Only ${Math.round(edgeCaseRatio * 100)}% of files test edge cases`);
215
- }
216
-
217
- const errorHandlingRatio = filesWithErrorHandling / reports.length;
218
- if (errorHandlingRatio < 0.3) {
219
- score -= 10;
220
- issues.push(`Only ${Math.round(errorHandlingRatio * 100)}% of files test error handling`);
221
- }
222
-
223
- const setupRatio = filesWithSetup / reports.length;
224
- if (setupRatio < 0.2) {
225
- score -= 5;
226
- issues.push('Few files use setup/teardown (beforeEach/afterEach)');
227
- }
228
-
229
- // Anti-pattern deductions (capped)
230
- const antiPatternPenalty = Math.min(allAntiPatterns.length * 3, 20);
231
- if (antiPatternPenalty > 0) {
232
- score -= antiPatternPenalty;
233
- issues.push(`${allAntiPatterns.length} anti-pattern(s) detected across files`);
234
- }
235
-
236
- score = Math.max(0, Math.min(100, score));
237
-
238
- let grade;
239
- if (score >= 90) grade = 'A';
240
- else if (score >= 80) grade = 'B';
241
- else if (score >= 70) grade = 'C';
242
- else if (score >= 60) grade = 'D';
243
- else grade = 'F';
244
-
245
- return { score, grade, issues };
246
- }
247
-
248
- /**
249
- * Assesses quality of multiple test files.
250
- * @param {string[]} testFilePaths
251
- * @returns {QualityReport}
252
- */
253
- export function assessTestQuality(testFilePaths) {
254
- const files = testFilePaths.map((fp) => assessSingleFile(fp));
255
- const overall = calculateOverallScore(files);
256
- return { files, overall };
257
- }