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.
- package/README.md +80 -53
- package/bin/chati.js +150 -5
- package/framework/agents/build/dev.md +509 -76
- package/framework/agents/deploy/devops.md +40 -25
- package/framework/agents/discover/brief.md +156 -22
- package/framework/agents/discover/brownfield-wu.md +24 -14
- package/framework/agents/discover/greenfield-wu.md +100 -14
- package/framework/agents/plan/architect-data-engineer.md +6 -6
- package/framework/agents/plan/architect-system.md +46 -12
- package/framework/agents/plan/architect.md +40 -20
- package/framework/agents/plan/detail.md +36 -24
- package/framework/agents/plan/phases.md +36 -26
- package/framework/agents/plan/tasks.md +114 -33
- package/framework/agents/plan/ux-brand-architect.md +240 -8
- package/framework/agents/plan/ux-component-engineer.md +28 -13
- package/framework/agents/plan/ux-researcher.md +7 -6
- package/framework/agents/plan/ux.md +55 -22
- package/framework/agents/quality/qa-implementation.md +143 -74
- package/framework/agents/quality/qa-planning.md +115 -42
- package/framework/agents/quality/qa-visual.md +439 -0
- package/framework/agents/shared/visualizer.md +128 -0
- package/framework/config.yaml +7 -6
- package/framework/constitution.md +127 -44
- package/framework/context/governance.md +12 -7
- package/framework/context/quality.md +6 -5
- package/framework/context/root.md +6 -6
- package/framework/data/entity-registry.yaml +377 -4
- package/framework/data/model-limits.json +19 -0
- package/framework/domains/agents/qa-visual.yaml +74 -0
- package/framework/domains/constitution.yaml +46 -2
- package/framework/domains/workflows/greenfield-fullstack.yaml +2 -2
- package/framework/hooks/advance-trigger.js +131 -0
- package/framework/hooks/brief-validator.js +83 -0
- package/framework/hooks/constitution-guard.js +24 -5
- package/framework/hooks/license-guard.js +62 -27
- package/framework/hooks/mode-governance.js +13 -2
- package/framework/hooks/model-governance.js +1 -0
- package/framework/hooks/post-dev.js +207 -0
- package/framework/hooks/prism-engine.js +274 -105
- package/framework/hooks/reasoning-escalator.js +371 -0
- package/framework/hooks/reference-trigger.js +117 -0
- package/framework/hooks/session-digest.js +50 -1
- package/framework/hooks/settings.json +32 -1
- package/framework/hooks/style-guard.js +25 -6
- package/framework/hooks/team-quality-gate.js +19 -12
- package/framework/hooks/undercover-guard.js +4 -2
- package/framework/i18n/en.yaml +3 -3
- package/framework/i18n/es.yaml +3 -3
- package/framework/i18n/fr.yaml +3 -3
- package/framework/i18n/pt.yaml +3 -3
- package/framework/intelligence/context-engine.md +4 -5
- package/framework/intelligence/decision-engine.md +1 -1
- package/framework/intelligence/hooks-performance.md +3 -3
- package/framework/migrations/v1.0-to-v1.1.yaml +1 -1
- package/framework/migrations/v1.4-to-v2.0.yaml +11 -11
- package/framework/migrations/v4.0-to-v4.1.yaml +2 -2
- package/framework/migrations/v4.2-to-v4.3.yaml +29 -0
- package/framework/orchestrator/chati-router.js +387 -0
- package/framework/orchestrator/chati-update.md +40 -40
- package/framework/orchestrator/chati.md +294 -94
- package/framework/scaffold/motion-premium/README.md +89 -0
- package/framework/scaffold/motion-premium/app/globals.css.template +400 -0
- package/framework/scaffold/motion-premium/app/layout.tsx.template +110 -0
- package/framework/scaffold/motion-premium/components/animation/BackgroundCrossfadeProvider.tsx.template +170 -0
- package/framework/scaffold/motion-premium/components/animation/LenisProvider.tsx.template +49 -0
- package/framework/scaffold/motion-premium/components/animation/PageTransitionWrapper.tsx.template +83 -0
- package/framework/scaffold/motion-premium/components/animation/Preloader.tsx.template +171 -0
- package/framework/scaffold/motion-premium/components/ui/Container.tsx.template +69 -0
- package/framework/scaffold/motion-premium/components/ui/PageSection.tsx.template +74 -0
- package/framework/scaffold/motion-premium/lib/animations/gsap.ts.template +112 -0
- package/framework/scaffold/motion-premium/lib/animations/refreshCoordinator.ts.template +75 -0
- package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +119 -0
- package/framework/scaffold/motion-premium/lib/animations/useGsapContext.ts.template +92 -0
- package/framework/scaffold/motion-premium/lib/animations/useScrollSnapStepper.ts.template +265 -0
- package/framework/scaffold/motion-premium/lib/animations/useSmoothScroll.ts.template +67 -0
- package/framework/scaffold/motion-premium/lib/brand.ts.template +43 -0
- package/framework/scaffold/motion-premium/scaffold.yaml +174 -0
- package/framework/scaffold/motion-premium-3d/README.md +80 -0
- package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoCanvas.tsx.template +81 -0
- package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoClient.tsx.template +75 -0
- package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/page.tsx.template +26 -0
- package/framework/scaffold/motion-premium-3d/components/3d/CameraRig.tsx.template +100 -0
- package/framework/scaffold/motion-premium-3d/components/3d/CanvasProvider.tsx.template +85 -0
- package/framework/scaffold/motion-premium-3d/components/3d/InvalidateOnScroll.tsx.template +51 -0
- package/framework/scaffold/motion-premium-3d/components/3d/MeshCrossfade.tsx.template +79 -0
- package/framework/scaffold/motion-premium-3d/components/3d/ScrollCrossfade.tsx.template +88 -0
- package/framework/scaffold/motion-premium-3d/components/3d/ScrollScene.tsx.template +121 -0
- package/framework/scaffold/motion-premium-3d/components/webgl/SceneFallback.tsx.template +65 -0
- package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +68 -0
- package/framework/scaffold/motion-premium-3d/lib/webgl/detect.ts.template +69 -0
- package/framework/scaffold/motion-premium-3d/scaffold.yaml +133 -0
- package/framework/schemas/session.schema.json +109 -21
- package/framework/scripts/reference-capture.js +430 -0
- package/framework/scripts/visual-qa.js +674 -0
- package/framework/tasks/orchestrator-handoff.md +1 -1
- package/framework/tasks/orchestrator-resume.md +1 -1
- package/framework/tasks/orchestrator-route.md +1 -1
- package/framework/tasks/orchestrator-status.md +3 -3
- package/framework/tasks/qa-planning-gate-define.md +1 -1
- package/framework/templates/brandbook-html-tmpl.md +1 -1
- package/framework/templates/brandbook-tmpl.yaml +1 -1
- package/framework/templates/component-spec-tmpl.yaml +1 -1
- package/framework/templates/design-token-tmpl.yaml +1 -1
- package/framework/templates/icon-system-tmpl.yaml +1 -1
- package/framework/templates/team-planning-tasks.yaml +6 -5
- package/framework/workflows/brownfield-discovery.yaml +2 -2
- package/framework/workflows/brownfield-fullstack.yaml +15 -11
- package/framework/workflows/brownfield-service.yaml +14 -10
- package/framework/workflows/brownfield-ui.yaml +15 -11
- package/framework/workflows/greenfield-fullstack.yaml +16 -13
- package/framework/workflows/quick-flow.yaml +3 -3
- package/framework/workflows/standard-flow.yaml +12 -9
- package/package.json +10 -5
- package/src/autonomy/autonomous-gate.js +1 -0
- package/src/autonomy/build-state.js +1 -2
- package/src/autonomy/progress-reporter.js +1 -1
- package/src/config/agent-customizer.js +11 -3
- package/src/config/claude-settings-generator.js +27 -7
- package/src/config/context-file-generator.js +41 -21
- package/src/config/framework-adapter.js +1 -0
- package/src/config/gemini-hooks-generator.js +19 -7
- package/src/config/mcp-configs.js +1 -0
- package/src/context/layers/l1-global.js +2 -1
- package/src/dashboard/data-reader.js +4 -3
- package/src/dashboard/layout.js +2 -1
- package/src/decision/analyzer.js +6 -30
- package/src/decision/engine.js +4 -28
- package/src/decision/registry-healer.js +3 -2
- package/src/decision/registry-updater.js +23 -14
- package/src/extensions/loader.js +2 -8
- package/src/gates/g1-planning-complete.js +2 -1
- package/src/gates/g2-qa-planning.js +2 -1
- package/src/gates/g3-implementation.js +2 -1
- package/src/gates/g4-qa-implementation.js +3 -2
- package/src/gates/g5-deploy-ready.js +2 -1
- package/src/health/engine.js +4 -3
- package/src/installer/core.js +422 -81
- package/src/installer/preflight.js +131 -0
- package/src/installer/provider-overlay.js +3 -3
- package/src/installer/scaffold-applier.js +358 -0
- package/src/installer/templates.js +46 -29
- package/src/installer/validator.js +17 -12
- package/src/intelligence/registry-manager.js +22 -29
- package/src/intelligence/timeline.js +11 -6
- package/src/license/commands.js +1 -1
- package/src/license/wait.js +102 -0
- package/src/memory/agent-memory.js +81 -0
- package/src/memory/dream.js +32 -1
- package/src/merger/replace-merger.js +28 -15
- package/src/orchestrator/agent-selector.js +2 -1
- package/src/orchestrator/cli.js +1869 -71
- package/src/orchestrator/doctor.js +270 -0
- package/src/orchestrator/handoff-engine.js +4 -3
- package/src/orchestrator/index.js +2 -0
- package/src/orchestrator/pipeline-manager.js +306 -15
- package/src/orchestrator/session-manager.js +331 -6
- package/src/tasks/handoff.js +3 -2
- package/src/telemetry/config.js +4 -3
- package/src/telemetry/schema.js +1 -0
- package/src/terminal/collector.js +3 -2
- package/src/terminal/index.js +1 -2
- package/src/terminal/isolation.js +52 -18
- package/src/terminal/prompt-builder.js +42 -25
- package/src/terminal/run-parallel.js +1 -1
- package/src/terminal/run-team.js +3 -3
- package/src/terminal/team-task-list.js +43 -4
- package/src/upgrade/backup.js +3 -2
- package/src/upgrade/checker.js +3 -2
- package/src/upgrade/migrator.js +65 -7
- package/src/upgrade/tracked-files-detector.js +86 -0
- package/src/upgrade/user-messages.js +94 -0
- package/src/utils/config-parser.js +2 -1
- package/src/utils/feature-flags.js +2 -1
- package/src/utils/flatten-entities.js +69 -0
- package/src/utils/framework-dir.js +16 -0
- package/src/utils/model-id.js +85 -0
- package/src/utils/provider-limits.js +84 -23
- package/src/utils/schema-validator.js +1 -1
- package/src/wizard/i18n.js +5 -4
- package/src/wizard/index.js +14 -0
- package/assets/logo - c/303/263pia.png +0 -0
- package/assets/logo.svg +0 -42
- package/assets/logo2.png +0 -0
- package/assets/social-preview.png +0 -0
- package/scripts/bundle-framework.js +0 -69
- package/scripts/changelog-generator.js +0 -222
- package/scripts/codebase-mapper.js +0 -728
- package/scripts/commit-message-generator.js +0 -167
- package/scripts/coverage-analyzer.js +0 -260
- package/scripts/dependency-analyzer.js +0 -280
- package/scripts/doctor/checks/agents.js +0 -77
- package/scripts/doctor/checks/constitution.js +0 -41
- package/scripts/doctor/checks/domain-alignment.js +0 -58
- package/scripts/doctor/checks/prism-layers.js +0 -84
- package/scripts/doctor/checks/registry.js +0 -55
- package/scripts/doctor/checks/schemas.js +0 -61
- package/scripts/doctor/fixes/reference-fix.js +0 -100
- package/scripts/doctor/fixes/registry-fix.js +0 -56
- package/scripts/doctor/index.js +0 -212
- package/scripts/framework-analyzer.js +0 -308
- package/scripts/generate-constitution-domain.js +0 -253
- package/scripts/generate-signing-key.js +0 -33
- package/scripts/health-check.js +0 -481
- package/scripts/ide-sync.js +0 -326
- package/scripts/performance-analyzer.js +0 -325
- package/scripts/plan-tracker.js +0 -278
- package/scripts/populate-entity-registry.js +0 -481
- package/scripts/pr-review.js +0 -317
- package/scripts/rollback-manager.js +0 -310
- package/scripts/semantic-lint.js +0 -328
- package/scripts/sign-manifest.js +0 -53
- package/scripts/stuck-detector.js +0 -343
- package/scripts/test-quality-assessment.js +0 -257
- package/scripts/validate-agents.js +0 -368
- package/scripts/validate-package.js +0 -505
- package/scripts/validate-tasks.js +0 -465
- package/src/autonomy/worktree-manager.js +0 -250
- package/src/intelligence/decision-engine.js +0 -256
- package/src/intelligence/document-sharder.js +0 -221
- 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
|
-
}
|