chati-dev 4.2.1 → 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 (222) 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 +8 -7
  23. package/framework/constitution.md +127 -43
  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 +91 -36
  35. package/framework/hooks/license-guard.js +129 -190
  36. package/framework/hooks/mode-governance.js +68 -16
  37. package/framework/hooks/model-governance.js +19 -8
  38. package/framework/hooks/package.json +3 -0
  39. package/framework/hooks/post-dev.js +207 -0
  40. package/framework/hooks/prism-engine.js +288 -105
  41. package/framework/hooks/read-protection.js +37 -9
  42. package/framework/hooks/reasoning-escalator.js +371 -0
  43. package/framework/hooks/reference-trigger.js +117 -0
  44. package/framework/hooks/session-digest.js +94 -20
  45. package/framework/hooks/settings.json +32 -1
  46. package/framework/hooks/style-guard.js +52 -13
  47. package/framework/hooks/team-quality-gate.js +58 -25
  48. package/framework/hooks/undercover-guard.js +34 -13
  49. package/framework/i18n/en.yaml +3 -3
  50. package/framework/i18n/es.yaml +3 -3
  51. package/framework/i18n/fr.yaml +3 -3
  52. package/framework/i18n/pt.yaml +3 -3
  53. package/framework/intelligence/context-engine.md +4 -5
  54. package/framework/intelligence/decision-engine.md +1 -1
  55. package/framework/intelligence/hooks-performance.md +3 -3
  56. package/framework/migrations/v1.0-to-v1.1.yaml +1 -1
  57. package/framework/migrations/v1.4-to-v2.0.yaml +11 -11
  58. package/framework/migrations/v4.0-to-v4.1.yaml +2 -2
  59. package/framework/migrations/v4.2-to-v4.3.yaml +29 -0
  60. package/framework/orchestrator/chati-router.js +387 -0
  61. package/framework/orchestrator/chati-update.md +40 -40
  62. package/framework/orchestrator/chati.md +318 -115
  63. package/framework/scaffold/motion-premium/README.md +89 -0
  64. package/framework/scaffold/motion-premium/app/globals.css.template +400 -0
  65. package/framework/scaffold/motion-premium/app/layout.tsx.template +110 -0
  66. package/framework/scaffold/motion-premium/components/animation/BackgroundCrossfadeProvider.tsx.template +170 -0
  67. package/framework/scaffold/motion-premium/components/animation/LenisProvider.tsx.template +49 -0
  68. package/framework/scaffold/motion-premium/components/animation/PageTransitionWrapper.tsx.template +83 -0
  69. package/framework/scaffold/motion-premium/components/animation/Preloader.tsx.template +171 -0
  70. package/framework/scaffold/motion-premium/components/ui/Container.tsx.template +69 -0
  71. package/framework/scaffold/motion-premium/components/ui/PageSection.tsx.template +74 -0
  72. package/framework/scaffold/motion-premium/lib/animations/gsap.ts.template +112 -0
  73. package/framework/scaffold/motion-premium/lib/animations/refreshCoordinator.ts.template +75 -0
  74. package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +119 -0
  75. package/framework/scaffold/motion-premium/lib/animations/useGsapContext.ts.template +92 -0
  76. package/framework/scaffold/motion-premium/lib/animations/useScrollSnapStepper.ts.template +265 -0
  77. package/framework/scaffold/motion-premium/lib/animations/useSmoothScroll.ts.template +67 -0
  78. package/framework/scaffold/motion-premium/lib/brand.ts.template +43 -0
  79. package/framework/scaffold/motion-premium/scaffold.yaml +174 -0
  80. package/framework/scaffold/motion-premium-3d/README.md +80 -0
  81. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoCanvas.tsx.template +81 -0
  82. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoClient.tsx.template +75 -0
  83. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/page.tsx.template +26 -0
  84. package/framework/scaffold/motion-premium-3d/components/3d/CameraRig.tsx.template +100 -0
  85. package/framework/scaffold/motion-premium-3d/components/3d/CanvasProvider.tsx.template +85 -0
  86. package/framework/scaffold/motion-premium-3d/components/3d/InvalidateOnScroll.tsx.template +51 -0
  87. package/framework/scaffold/motion-premium-3d/components/3d/MeshCrossfade.tsx.template +79 -0
  88. package/framework/scaffold/motion-premium-3d/components/3d/ScrollCrossfade.tsx.template +88 -0
  89. package/framework/scaffold/motion-premium-3d/components/3d/ScrollScene.tsx.template +121 -0
  90. package/framework/scaffold/motion-premium-3d/components/webgl/SceneFallback.tsx.template +65 -0
  91. package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +68 -0
  92. package/framework/scaffold/motion-premium-3d/lib/webgl/detect.ts.template +69 -0
  93. package/framework/scaffold/motion-premium-3d/scaffold.yaml +133 -0
  94. package/framework/schemas/session.schema.json +109 -21
  95. package/framework/scripts/reference-capture.js +430 -0
  96. package/framework/scripts/visual-qa.js +674 -0
  97. package/framework/tasks/orchestrator-handoff.md +1 -1
  98. package/framework/tasks/orchestrator-resume.md +1 -1
  99. package/framework/tasks/orchestrator-route.md +1 -1
  100. package/framework/tasks/orchestrator-status.md +3 -3
  101. package/framework/tasks/qa-planning-gate-define.md +1 -1
  102. package/framework/templates/brandbook-html-tmpl.md +1 -1
  103. package/framework/templates/brandbook-tmpl.yaml +1 -1
  104. package/framework/templates/component-spec-tmpl.yaml +1 -1
  105. package/framework/templates/design-token-tmpl.yaml +1 -1
  106. package/framework/templates/icon-system-tmpl.yaml +1 -1
  107. package/framework/templates/team-planning-tasks.yaml +6 -5
  108. package/framework/workflows/brownfield-discovery.yaml +2 -2
  109. package/framework/workflows/brownfield-fullstack.yaml +15 -11
  110. package/framework/workflows/brownfield-service.yaml +14 -10
  111. package/framework/workflows/brownfield-ui.yaml +15 -11
  112. package/framework/workflows/greenfield-fullstack.yaml +16 -13
  113. package/framework/workflows/quick-flow.yaml +3 -3
  114. package/framework/workflows/standard-flow.yaml +12 -9
  115. package/package.json +10 -5
  116. package/src/autonomy/autonomous-gate.js +1 -0
  117. package/src/autonomy/build-state.js +1 -2
  118. package/src/autonomy/progress-reporter.js +1 -1
  119. package/src/config/agent-customizer.js +11 -3
  120. package/src/config/claude-settings-generator.js +226 -0
  121. package/src/config/context-file-generator.js +41 -21
  122. package/src/config/framework-adapter.js +1 -0
  123. package/src/config/gemini-hooks-generator.js +76 -6
  124. package/src/config/mcp-configs.js +1 -0
  125. package/src/context/layers/l1-global.js +2 -1
  126. package/src/dashboard/data-reader.js +4 -3
  127. package/src/dashboard/layout.js +2 -1
  128. package/src/decision/analyzer.js +6 -30
  129. package/src/decision/engine.js +4 -28
  130. package/src/decision/registry-healer.js +3 -2
  131. package/src/decision/registry-updater.js +23 -14
  132. package/src/extensions/loader.js +2 -8
  133. package/src/gates/g1-planning-complete.js +2 -1
  134. package/src/gates/g2-qa-planning.js +2 -1
  135. package/src/gates/g3-implementation.js +2 -1
  136. package/src/gates/g4-qa-implementation.js +3 -2
  137. package/src/gates/g5-deploy-ready.js +2 -1
  138. package/src/health/engine.js +4 -3
  139. package/src/installer/core.js +557 -81
  140. package/src/installer/preflight.js +131 -0
  141. package/src/installer/provider-overlay.js +3 -3
  142. package/src/installer/scaffold-applier.js +358 -0
  143. package/src/installer/templates.js +52 -31
  144. package/src/installer/validator.js +17 -12
  145. package/src/intelligence/registry-manager.js +22 -29
  146. package/src/intelligence/timeline.js +11 -6
  147. package/src/license/commands.js +1 -1
  148. package/src/license/wait.js +102 -0
  149. package/src/memory/agent-memory.js +81 -0
  150. package/src/memory/dream.js +32 -1
  151. package/src/merger/replace-merger.js +28 -15
  152. package/src/orchestrator/agent-selector.js +2 -1
  153. package/src/orchestrator/cli.js +1908 -80
  154. package/src/orchestrator/doctor.js +270 -0
  155. package/src/orchestrator/handoff-engine.js +4 -3
  156. package/src/orchestrator/index.js +2 -0
  157. package/src/orchestrator/pipeline-manager.js +306 -15
  158. package/src/orchestrator/session-manager.js +331 -6
  159. package/src/tasks/handoff.js +3 -2
  160. package/src/telemetry/config.js +4 -3
  161. package/src/telemetry/schema.js +1 -0
  162. package/src/terminal/collector.js +3 -2
  163. package/src/terminal/index.js +1 -2
  164. package/src/terminal/isolation.js +52 -18
  165. package/src/terminal/prompt-builder.js +42 -25
  166. package/src/terminal/run-parallel.js +1 -1
  167. package/src/terminal/run-team.js +3 -3
  168. package/src/terminal/team-task-list.js +43 -4
  169. package/src/upgrade/backup.js +3 -2
  170. package/src/upgrade/checker.js +3 -2
  171. package/src/upgrade/migrator.js +65 -7
  172. package/src/upgrade/tracked-files-detector.js +86 -0
  173. package/src/upgrade/user-messages.js +94 -0
  174. package/src/utils/config-parser.js +2 -1
  175. package/src/utils/feature-flags.js +2 -1
  176. package/src/utils/flatten-entities.js +69 -0
  177. package/src/utils/framework-dir.js +16 -0
  178. package/src/utils/model-id.js +85 -0
  179. package/src/utils/provider-limits.js +84 -23
  180. package/src/utils/schema-validator.js +1 -1
  181. package/src/wizard/i18n.js +5 -4
  182. package/src/wizard/index.js +14 -0
  183. package/assets/logo - c/303/263pia.png +0 -0
  184. package/assets/logo.svg +0 -42
  185. package/assets/logo2.png +0 -0
  186. package/assets/social-preview.png +0 -0
  187. package/scripts/bundle-framework.js +0 -69
  188. package/scripts/changelog-generator.js +0 -222
  189. package/scripts/codebase-mapper.js +0 -728
  190. package/scripts/commit-message-generator.js +0 -167
  191. package/scripts/coverage-analyzer.js +0 -260
  192. package/scripts/dependency-analyzer.js +0 -280
  193. package/scripts/doctor/checks/agents.js +0 -77
  194. package/scripts/doctor/checks/constitution.js +0 -41
  195. package/scripts/doctor/checks/domain-alignment.js +0 -58
  196. package/scripts/doctor/checks/prism-layers.js +0 -84
  197. package/scripts/doctor/checks/registry.js +0 -55
  198. package/scripts/doctor/checks/schemas.js +0 -61
  199. package/scripts/doctor/fixes/reference-fix.js +0 -100
  200. package/scripts/doctor/fixes/registry-fix.js +0 -56
  201. package/scripts/doctor/index.js +0 -212
  202. package/scripts/framework-analyzer.js +0 -308
  203. package/scripts/generate-constitution-domain.js +0 -253
  204. package/scripts/generate-signing-key.js +0 -33
  205. package/scripts/health-check.js +0 -481
  206. package/scripts/ide-sync.js +0 -326
  207. package/scripts/performance-analyzer.js +0 -325
  208. package/scripts/plan-tracker.js +0 -278
  209. package/scripts/populate-entity-registry.js +0 -481
  210. package/scripts/pr-review.js +0 -317
  211. package/scripts/rollback-manager.js +0 -310
  212. package/scripts/semantic-lint.js +0 -328
  213. package/scripts/sign-manifest.js +0 -53
  214. package/scripts/stuck-detector.js +0 -343
  215. package/scripts/test-quality-assessment.js +0 -257
  216. package/scripts/validate-agents.js +0 -368
  217. package/scripts/validate-package.js +0 -367
  218. package/scripts/validate-tasks.js +0 -465
  219. package/src/autonomy/worktree-manager.js +0 -250
  220. package/src/intelligence/decision-engine.js +0 -256
  221. package/src/intelligence/document-sharder.js +0 -221
  222. 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
- }