chati-dev 4.3.0 → 4.4.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 (156) hide show
  1. package/README.md +18 -13
  2. package/bin/chati.js +32 -4
  3. package/framework/agents/build/dev.md +9 -5
  4. package/framework/agents/discover/brief.md +2 -0
  5. package/framework/agents/discover/brownfield-wu.md +2 -0
  6. package/framework/agents/discover/greenfield-wu.md +18 -1
  7. package/framework/agents/plan/detail.md +2 -0
  8. package/framework/agents/plan/tasks.md +31 -0
  9. package/framework/agents/plan/ux-brand-architect.md +21 -2
  10. package/framework/agents/plan/ux-component-engineer.md +10 -0
  11. package/framework/agents/quality/qa-implementation.md +1 -1
  12. package/framework/agents/quality/qa-planning.md +1 -1
  13. package/framework/agents/quality/qa-visual.md +33 -21
  14. package/framework/config.yaml +5 -4
  15. package/framework/constitution.md +58 -11
  16. package/framework/context/governance.md +12 -10
  17. package/framework/context/root.md +2 -2
  18. package/framework/data/entity-registry.yaml +12 -4
  19. package/framework/data/qa-rubrics.yaml +335 -0
  20. package/framework/domains/constitution.yaml +3 -3
  21. package/framework/executors/json-validate.js +80 -0
  22. package/framework/executors/npm-script.js +72 -0
  23. package/framework/executors/path-check.js +67 -0
  24. package/framework/executors/registry.yaml +36 -0
  25. package/framework/hooks/advance-trigger.js +47 -3
  26. package/framework/hooks/constitution-guard.js +9 -4
  27. package/framework/hooks/git-push-authority.js +113 -0
  28. package/framework/hooks/license-guard.js +59 -9
  29. package/framework/hooks/model-governance.js +40 -17
  30. package/framework/hooks/prism-engine.js +78 -11
  31. package/framework/hooks/session-digest.js +12 -3
  32. package/framework/hooks/settings.json +7 -3
  33. package/framework/hooks/style-guard.js +52 -3
  34. package/framework/hooks/team-quality-gate.js +43 -5
  35. package/framework/i18n/en.yaml +3 -3
  36. package/framework/i18n/es.yaml +3 -3
  37. package/framework/i18n/fr.yaml +3 -3
  38. package/framework/i18n/pt.yaml +3 -3
  39. package/framework/intelligence/confidence.yaml +85 -6
  40. package/framework/intelligence/context-engine.md +9 -5
  41. package/framework/intelligence/decision-engine.md +1 -1
  42. package/framework/orchestrator/chati-router.js +59 -5
  43. package/framework/orchestrator/chati-update.md +19 -3
  44. package/framework/orchestrator/chati.md +20 -1
  45. package/framework/quality-gates/planning-gate.md +2 -0
  46. package/framework/rules/human-writing-style.md +47 -0
  47. package/framework/scaffold/motion-premium/README.md +12 -2
  48. package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +1 -1
  49. package/framework/scaffold/motion-premium/scaffold.yaml +6 -1
  50. package/framework/scaffold/motion-premium-3d/README.md +10 -0
  51. package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +1 -1
  52. package/framework/scaffold/motion-premium-3d/scaffold.yaml +3 -0
  53. package/framework/scaffold/saas-dashboard/README.md +58 -0
  54. package/framework/scaffold/saas-dashboard/app/dashboard/page.tsx.template +546 -0
  55. package/framework/scaffold/saas-dashboard/app/globals.css.template +746 -0
  56. package/framework/scaffold/saas-dashboard/app/layout.tsx.template +192 -0
  57. package/framework/scaffold/saas-dashboard/app/shell.css.template +1070 -0
  58. package/framework/scaffold/saas-dashboard/components/data/Chart.tsx.template +725 -0
  59. package/framework/scaffold/saas-dashboard/components/data/DataTable.tsx.template +623 -0
  60. package/framework/scaffold/saas-dashboard/components/data/DataTable.types.ts.template +104 -0
  61. package/framework/scaffold/saas-dashboard/components/data/DataTablePagination.tsx.template +234 -0
  62. package/framework/scaffold/saas-dashboard/components/data/SkeletonBlock.tsx.template +111 -0
  63. package/framework/scaffold/saas-dashboard/components/data/StatCard.tsx.template +393 -0
  64. package/framework/scaffold/saas-dashboard/components/data/StatCardGrid.tsx.template +121 -0
  65. package/framework/scaffold/saas-dashboard/components/feedback/EmptyState.tsx.template +281 -0
  66. package/framework/scaffold/saas-dashboard/components/interaction/CommandPalette.tsx.template +767 -0
  67. package/framework/scaffold/saas-dashboard/components/interaction/Field.tsx.template +351 -0
  68. package/framework/scaffold/saas-dashboard/components/interaction/Form.tsx.template +424 -0
  69. package/framework/scaffold/saas-dashboard/components/primitives/Badge.tsx.template +193 -0
  70. package/framework/scaffold/saas-dashboard/components/primitives/Button.tsx.template +261 -0
  71. package/framework/scaffold/saas-dashboard/components/primitives/Card.tsx.template +287 -0
  72. package/framework/scaffold/saas-dashboard/components/primitives/Input.tsx.template +310 -0
  73. package/framework/scaffold/saas-dashboard/components/primitives/Spinner.tsx.template +135 -0
  74. package/framework/scaffold/saas-dashboard/components/primitives/index.ts.template +60 -0
  75. package/framework/scaffold/saas-dashboard/components/shell/AppShell.tsx.template +174 -0
  76. package/framework/scaffold/saas-dashboard/components/shell/Breadcrumb.tsx.template +186 -0
  77. package/framework/scaffold/saas-dashboard/components/shell/Container.tsx.template +86 -0
  78. package/framework/scaffold/saas-dashboard/components/shell/NavItem.tsx.template +147 -0
  79. package/framework/scaffold/saas-dashboard/components/shell/Sidebar.tsx.template +378 -0
  80. package/framework/scaffold/saas-dashboard/components/shell/SidebarContext.tsx.template +169 -0
  81. package/framework/scaffold/saas-dashboard/components/shell/ThemeToggle.tsx.template +128 -0
  82. package/framework/scaffold/saas-dashboard/components/shell/Topbar.tsx.template +301 -0
  83. package/framework/scaffold/saas-dashboard/lib/brand.ts.template +324 -0
  84. package/framework/scaffold/saas-dashboard/scaffold.yaml +225 -0
  85. package/framework/schemas/session.schema.json +5 -0
  86. package/framework/schemas/task.schema.json +9 -0
  87. package/framework/scripts/visual-qa.js +101 -3
  88. package/framework/tasks/orchestrator-health.md +4 -4
  89. package/framework/tasks/qa-impl-verdict.md +11 -5
  90. package/framework/templates/brandbook-html-tmpl.md +1 -1
  91. package/framework/templates/qa-gate-tmpl.yaml +24 -11
  92. package/package.json +4 -3
  93. package/src/config/context-file-generator.js +0 -6
  94. package/src/dashboard/renderer.js +0 -36
  95. package/src/executors/runner.js +204 -0
  96. package/src/installer/core.js +13 -3
  97. package/src/installer/templates.js +5 -6
  98. package/src/intelligence/registry-manager.js +1 -1
  99. package/src/license/client.js +9 -3
  100. package/src/license/commands.js +12 -2
  101. package/src/license/machine-id.js +42 -1
  102. package/src/memory/gotchas.js +58 -40
  103. package/src/memory/magic-docs.js +1 -1
  104. package/src/memory/session-digest.js +9 -4
  105. package/src/orchestrator/cli.js +186 -15
  106. package/src/orchestrator/doctor.js +98 -16
  107. package/src/orchestrator/pipeline-manager.js +44 -22
  108. package/src/orchestrator/session-manager.js +64 -8
  109. package/src/telemetry/sender.js +7 -19
  110. package/src/terminal/run-team.js +3 -3
  111. package/src/upgrade/tracked-files-detector.js +34 -16
  112. package/src/utils/feature-flags.js +1 -1
  113. package/src/utils/flatten-entities.js +4 -40
  114. package/src/utils/schema-validator.js +0 -14
  115. package/src/wizard/i18n.js +3 -3
  116. package/src/api/index.js +0 -120
  117. package/src/autonomy/autonomous-gate.js +0 -294
  118. package/src/autonomy/build-loop.js +0 -281
  119. package/src/autonomy/build-state.js +0 -286
  120. package/src/autonomy/cause-analyzer.js +0 -177
  121. package/src/autonomy/escalation.js +0 -214
  122. package/src/autonomy/index.js +0 -51
  123. package/src/autonomy/mode-manager.js +0 -225
  124. package/src/autonomy/mode-suggester.js +0 -283
  125. package/src/autonomy/progress-reporter.js +0 -275
  126. package/src/autonomy/safety-net.js +0 -370
  127. package/src/config/agent-customizer.js +0 -231
  128. package/src/decision/analyzer.js +0 -291
  129. package/src/decision/engine.js +0 -250
  130. package/src/decision/index.js +0 -38
  131. package/src/decision/registry-healer.js +0 -468
  132. package/src/decision/registry-updater.js +0 -339
  133. package/src/extensions/loader.js +0 -145
  134. package/src/extensions/registry.js +0 -134
  135. package/src/gates/circuit-breaker.js +0 -151
  136. package/src/gates/g1-planning-complete.js +0 -154
  137. package/src/gates/g2-qa-planning.js +0 -156
  138. package/src/gates/g3-implementation.js +0 -215
  139. package/src/gates/g4-qa-implementation.js +0 -240
  140. package/src/gates/g5-deploy-ready.js +0 -181
  141. package/src/gates/gate-base.js +0 -185
  142. package/src/gates/index.js +0 -46
  143. package/src/health/auto-fix.js +0 -216
  144. package/src/health/engine.js +0 -246
  145. package/src/merger/semantic-merger.js +0 -292
  146. package/src/preview/detector.js +0 -238
  147. package/src/preview/index.js +0 -20
  148. package/src/preview/launcher.js +0 -235
  149. package/src/preview/log-buffer.js +0 -103
  150. package/src/quality/metrics-collector.js +0 -281
  151. package/src/quality/test-runner.js +0 -366
  152. package/src/tasks/executor.js +0 -195
  153. package/src/tasks/index.js +0 -4
  154. package/src/tasks/loader.js +0 -210
  155. package/src/tasks/router.js +0 -182
  156. package/src/utils/event-bus.js +0 -126
@@ -1,235 +0,0 @@
1
- /**
2
- * @fileoverview Preview server lifecycle management.
3
- * Spawns dev servers, finds available ports, health-checks, and opens browsers.
4
- * NEVER kills existing processes — only manages servers it spawns.
5
- */
6
-
7
- import net from 'net';
8
- import http from 'http';
9
- import { spawn } from 'child_process';
10
- import { platform } from 'os';
11
- import { LogBuffer } from './log-buffer.js';
12
-
13
- /**
14
- * Test if a port is available (not in use by another process).
15
- * Uses net.createServer to probe — never kills existing services.
16
- *
17
- * @param {number} port
18
- * @returns {Promise<boolean>}
19
- */
20
- function isPortAvailable(port) {
21
- return new Promise((resolve) => {
22
- const server = net.createServer();
23
- server.once('error', () => resolve(false));
24
- server.once('listening', () => {
25
- server.close(() => resolve(true));
26
- });
27
- server.listen(port);
28
- });
29
- }
30
-
31
- /**
32
- * Find an available port starting from a given number.
33
- * Scans sequentially — NEVER kills existing processes on occupied ports.
34
- *
35
- * @param {number} [startPort=3000] - Port to start scanning from
36
- * @param {number} [maxAttempts=20] - Maximum ports to try
37
- * @returns {Promise<number>} Available port number
38
- * @throws {Error} If no port is available within range
39
- */
40
- export async function findAvailablePort(startPort = 3000, maxAttempts = 20) {
41
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
42
- const port = startPort + attempt;
43
- const available = await isPortAvailable(port);
44
- if (available) return port;
45
- }
46
-
47
- throw new Error(
48
- `No available port found in range ${startPort}-${startPort + maxAttempts - 1}`,
49
- );
50
- }
51
-
52
- /**
53
- * Wait for an HTTP server to respond on the given URL.
54
- * Polls with exponential backoff until the server is ready.
55
- *
56
- * @param {string} url - URL to check (e.g., "http://localhost:3000")
57
- * @param {number} [timeout=30000] - Max wait time in ms
58
- * @returns {Promise<void>}
59
- * @throws {Error} If server doesn't respond within timeout
60
- */
61
- export function waitForServer(url, timeout = 30000) {
62
- const startTime = Date.now();
63
- let delay = 500;
64
-
65
- return new Promise((resolve, reject) => {
66
- function attempt() {
67
- if (Date.now() - startTime > timeout) {
68
- reject(new Error(`Server at ${url} did not respond within ${timeout}ms`));
69
- return;
70
- }
71
-
72
- const req = http.get(url, (res) => {
73
- // Any response (even 404) means server is up
74
- res.resume();
75
- resolve();
76
- });
77
-
78
- req.on('error', () => {
79
- // Server not ready yet — retry with backoff
80
- delay = Math.min(delay * 1.5, 3000);
81
- setTimeout(attempt, delay);
82
- });
83
-
84
- req.setTimeout(2000, () => {
85
- req.destroy();
86
- setTimeout(attempt, delay);
87
- });
88
- }
89
-
90
- attempt();
91
- });
92
- }
93
-
94
- /**
95
- * Open a URL in the system's default browser.
96
- *
97
- * @param {string} url - URL to open
98
- * @returns {void}
99
- */
100
- export function openBrowser(url) {
101
- const os = platform();
102
- let command;
103
- let args;
104
-
105
- if (os === 'darwin') {
106
- command = 'open';
107
- args = [url];
108
- } else if (os === 'win32') {
109
- command = 'cmd';
110
- args = ['/c', 'start', url];
111
- } else {
112
- command = 'xdg-open';
113
- args = [url];
114
- }
115
-
116
- const proc = spawn(command, args, { stdio: 'ignore', detached: true });
117
- proc.unref();
118
- }
119
-
120
- /**
121
- * Gracefully kill a child process.
122
- * Sends SIGTERM, waits 3s, then SIGKILL if still alive.
123
- *
124
- * @param {import('child_process').ChildProcess} proc
125
- * @returns {Promise<void>}
126
- */
127
- function killProcess(proc) {
128
- return new Promise((resolve) => {
129
- if (!proc || proc.killed || proc.exitCode !== null) {
130
- resolve();
131
- return;
132
- }
133
-
134
- const forceKillTimer = setTimeout(() => {
135
- try {
136
- proc.kill('SIGKILL');
137
- } catch { /* expected: process may already be dead */
138
- // Already dead
139
- }
140
- resolve();
141
- }, 3000);
142
-
143
- proc.once('exit', () => {
144
- clearTimeout(forceKillTimer);
145
- resolve();
146
- });
147
-
148
- try {
149
- proc.kill('SIGTERM');
150
- } catch { /* expected: process may already be dead */
151
- clearTimeout(forceKillTimer);
152
- resolve();
153
- }
154
- });
155
- }
156
-
157
- /**
158
- * Launch a preview server for the project.
159
- *
160
- * @param {string} projectDir - Absolute path to project root
161
- * @param {object} options
162
- * @param {string} options.command - Command to run (e.g., "npm")
163
- * @param {string[]} options.args - Command arguments (e.g., ["run", "dev"])
164
- * @param {number} options.port - Port to use
165
- * @param {string} [options.framework='unknown'] - Detected framework name
166
- * @param {boolean} [options.openBrowser=true] - Open browser after server is ready
167
- * @param {number} [options.timeout=30000] - Health check timeout in ms
168
- * @returns {Promise<{ url: string, port: number, pid: number, framework: string, logs: LogBuffer, kill: () => Promise<void> }>}
169
- */
170
- export async function launchPreview(projectDir, options) {
171
- const {
172
- command,
173
- args,
174
- port,
175
- framework = 'unknown',
176
- openBrowser: shouldOpenBrowser = true,
177
- timeout = 30000,
178
- } = options;
179
-
180
- const logs = new LogBuffer();
181
- const url = `http://localhost:${port}`;
182
-
183
- // Spawn the dev server
184
- const proc = spawn(command, args, {
185
- cwd: projectDir,
186
- env: { ...process.env, PORT: String(port) },
187
- stdio: ['ignore', 'pipe', 'pipe'],
188
- });
189
-
190
- // Capture stdout
191
- if (proc.stdout) {
192
- proc.stdout.on('data', (data) => {
193
- for (const line of data.toString().split('\n')) {
194
- if (line.trim()) logs.append(line, 'stdout');
195
- }
196
- });
197
- }
198
-
199
- // Capture stderr
200
- if (proc.stderr) {
201
- proc.stderr.on('data', (data) => {
202
- for (const line of data.toString().split('\n')) {
203
- if (line.trim()) logs.append(line, 'stderr');
204
- }
205
- });
206
- }
207
-
208
- // Handle spawn errors
209
- const spawnError = await new Promise((resolve) => {
210
- proc.once('error', (err) => resolve(err));
211
- // Give it a moment to fail or succeed
212
- setTimeout(() => resolve(null), 500);
213
- });
214
-
215
- if (spawnError) {
216
- throw new Error(`Failed to start dev server: ${spawnError.message}`);
217
- }
218
-
219
- // Wait for the server to be ready
220
- await waitForServer(url, timeout);
221
-
222
- // Open browser
223
- if (shouldOpenBrowser) {
224
- openBrowser(url);
225
- }
226
-
227
- return {
228
- url,
229
- port,
230
- pid: proc.pid,
231
- framework,
232
- logs,
233
- kill: () => killProcess(proc),
234
- };
235
- }
@@ -1,103 +0,0 @@
1
- /**
2
- * @fileoverview Circular log buffer for capturing server output.
3
- * Used by the preview launcher to collect stdout/stderr from dev servers.
4
- * Provides filtered views (errors, recent) for Dev agent context.
5
- */
6
-
7
- /**
8
- * Circular buffer that captures server logs with metadata.
9
- * Automatically evicts oldest entries when capacity is reached.
10
- */
11
- export class LogBuffer {
12
- #lines = [];
13
- #maxLines;
14
-
15
- /**
16
- * @param {number} [maxLines=200] - Maximum lines to retain
17
- */
18
- constructor(maxLines = 200) {
19
- this.#maxLines = maxLines;
20
- }
21
-
22
- /**
23
- * Append a log line.
24
- * @param {string} line - Raw output line
25
- * @param {'stdout'|'stderr'} [stream='stdout'] - Source stream
26
- */
27
- append(line, stream = 'stdout') {
28
- if (typeof line !== 'string') return;
29
-
30
- this.#lines.push({
31
- line: line.trim(),
32
- stream,
33
- timestamp: Date.now(),
34
- });
35
-
36
- // Evict oldest when over capacity
37
- if (this.#lines.length > this.#maxLines) {
38
- this.#lines.shift();
39
- }
40
- }
41
-
42
- /**
43
- * Get all buffered lines.
44
- * @returns {Array<{ line: string, stream: string, timestamp: number }>}
45
- */
46
- getAll() {
47
- return [...this.#lines];
48
- }
49
-
50
- /**
51
- * Get lines that look like errors or warnings.
52
- * Includes all stderr lines + stdout lines matching error patterns.
53
- * @returns {Array<{ line: string, stream: string, timestamp: number }>}
54
- */
55
- getErrors() {
56
- return this.#lines.filter(
57
- (l) =>
58
- l.stream === 'stderr' ||
59
- /(error|warn(ing)?|exception|failed|fatal|panic|unhandled)/i.test(l.line),
60
- );
61
- }
62
-
63
- /**
64
- * Get the N most recent lines.
65
- * @param {number} [n=50] - Number of lines
66
- * @returns {Array<{ line: string, stream: string, timestamp: number }>}
67
- */
68
- getRecent(n = 50) {
69
- return this.#lines.slice(-n);
70
- }
71
-
72
- /**
73
- * Get total number of buffered lines.
74
- * @returns {number}
75
- */
76
- get size() {
77
- return this.#lines.length;
78
- }
79
-
80
- /**
81
- * Clear all buffered lines.
82
- */
83
- clear() {
84
- this.#lines = [];
85
- }
86
-
87
- /**
88
- * Format logs as structured context for the Dev agent.
89
- * Includes error summary + recent output for debugging.
90
- * @returns {{ totalLines: number, errorCount: number, errors: string[], recentOutput: string[] }}
91
- */
92
- toContext() {
93
- const errors = this.getErrors();
94
- const recent = this.getRecent(50);
95
-
96
- return {
97
- totalLines: this.#lines.length,
98
- errorCount: errors.length,
99
- errors: errors.map((e) => e.line),
100
- recentOutput: recent.map((l) => `[${l.stream}] ${l.line}`),
101
- };
102
- }
103
- }
@@ -1,281 +0,0 @@
1
- /**
2
- * @fileoverview Quality metrics collector and trend analysis.
3
- *
4
- * Persists quality metrics over time and calculates trends
5
- * (improving, stable, declining) via linear regression.
6
- *
7
- * Constitution Article XIV — Framework Registry Governance.
8
- */
9
-
10
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
11
- import { join, dirname } from 'path';
12
-
13
- // ---------------------------------------------------------------------------
14
- // Constants
15
- // ---------------------------------------------------------------------------
16
-
17
- /** Metrics storage filename. */
18
- const METRICS_FILE = 'quality-history.json';
19
-
20
- /** Metrics directory relative to project root. */
21
- const METRICS_DIR = '.chati/metrics';
22
-
23
- /** Maximum metrics entries to keep per project. */
24
- export const MAX_METRICS_ENTRIES = 500;
25
-
26
- /** Valid metric types. */
27
- export const METRIC_TYPES = {
28
- QA_SCORE: 'qa_score',
29
- HEALTH_SCORE: 'health_score',
30
- TEST_COUNT: 'test_count',
31
- COVERAGE: 'coverage',
32
- LINT_ERRORS: 'lint_errors',
33
- BUILD_TIME: 'build_time',
34
- };
35
-
36
- /** Trend direction values. */
37
- export const TREND_DIRECTION = {
38
- IMPROVING: 'improving',
39
- STABLE: 'stable',
40
- DECLINING: 'declining',
41
- };
42
-
43
- // ---------------------------------------------------------------------------
44
- // Helpers
45
- // ---------------------------------------------------------------------------
46
-
47
- /**
48
- * Get the metrics file path for a project.
49
- *
50
- * @param {string} projectDir
51
- * @returns {string}
52
- */
53
- function getMetricsPath(projectDir) {
54
- return join(projectDir, METRICS_DIR, METRICS_FILE);
55
- }
56
-
57
- /**
58
- * Load metrics from disk.
59
- *
60
- * @param {string} projectDir
61
- * @returns {{ metrics: object[] }}
62
- */
63
- function loadMetrics(projectDir) {
64
- const filePath = getMetricsPath(projectDir);
65
- if (!existsSync(filePath)) {
66
- return { metrics: [] };
67
- }
68
-
69
- try {
70
- const raw = readFileSync(filePath, 'utf-8');
71
- const data = JSON.parse(raw);
72
- return { metrics: Array.isArray(data.metrics) ? data.metrics : [] };
73
- } catch { /* expected: file may not exist */
74
- return { metrics: [] };
75
- }
76
- }
77
-
78
- /**
79
- * Save metrics to disk.
80
- *
81
- * @param {string} projectDir
82
- * @param {{ metrics: object[] }} data
83
- */
84
- function saveMetrics(projectDir, data) {
85
- const filePath = getMetricsPath(projectDir);
86
- const dir = dirname(filePath);
87
-
88
- if (!existsSync(dir)) {
89
- mkdirSync(dir, { recursive: true });
90
- }
91
-
92
- // Trim to MAX_METRICS_ENTRIES
93
- if (data.metrics.length > MAX_METRICS_ENTRIES) {
94
- data.metrics = data.metrics.slice(-MAX_METRICS_ENTRIES);
95
- }
96
-
97
- writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
98
- }
99
-
100
- // ---------------------------------------------------------------------------
101
- // Public API
102
- // ---------------------------------------------------------------------------
103
-
104
- /**
105
- * @typedef {object} MetricEntry
106
- * @property {string} type - Metric type (from METRIC_TYPES)
107
- * @property {number} value - Metric value
108
- * @property {string} [agent] - Agent that produced the metric
109
- * @property {string} [sessionId] - Session identifier
110
- * @property {string} timestamp - ISO timestamp
111
- */
112
-
113
- /**
114
- * Record a quality metric.
115
- *
116
- * @param {string} projectDir
117
- * @param {{ type: string, value: number, agent?: string, sessionId?: string }} metric
118
- * @returns {{ recorded: boolean, total: number }}
119
- */
120
- export function recordMetric(projectDir, metric) {
121
- if (!metric || typeof metric.value !== 'number') {
122
- return { recorded: false, total: 0 };
123
- }
124
-
125
- const validTypes = Object.values(METRIC_TYPES);
126
- if (!validTypes.includes(metric.type)) {
127
- return { recorded: false, total: 0 };
128
- }
129
-
130
- const data = loadMetrics(projectDir);
131
-
132
- data.metrics.push({
133
- type: metric.type,
134
- value: metric.value,
135
- agent: metric.agent || null,
136
- sessionId: metric.sessionId || null,
137
- timestamp: new Date().toISOString(),
138
- });
139
-
140
- saveMetrics(projectDir, data);
141
-
142
- return { recorded: true, total: data.metrics.length };
143
- }
144
-
145
- /**
146
- * Get metrics history with optional filtering.
147
- *
148
- * @param {string} projectDir
149
- * @param {{ type?: string, agent?: string, limit?: number, since?: string }} [options={}]
150
- * @returns {{ metrics: MetricEntry[], trend: { direction: string, slope: number, confidence: number } }}
151
- */
152
- export function getMetricsHistory(projectDir, options = {}) {
153
- const data = loadMetrics(projectDir);
154
- let metrics = data.metrics;
155
-
156
- // Filter by type
157
- if (options.type) {
158
- metrics = metrics.filter(m => m.type === options.type);
159
- }
160
-
161
- // Filter by agent
162
- if (options.agent) {
163
- metrics = metrics.filter(m => m.agent === options.agent);
164
- }
165
-
166
- // Filter by time
167
- if (options.since) {
168
- const sinceDate = new Date(options.since).getTime();
169
- metrics = metrics.filter(m => new Date(m.timestamp).getTime() >= sinceDate);
170
- }
171
-
172
- // Apply limit
173
- if (options.limit && options.limit > 0) {
174
- metrics = metrics.slice(-options.limit);
175
- }
176
-
177
- // Calculate trend
178
- const trend = calculateTrend(metrics);
179
-
180
- return { metrics, trend };
181
- }
182
-
183
- /**
184
- * Calculate trend from a series of metrics using linear regression.
185
- *
186
- * @param {MetricEntry[]} metrics
187
- * @returns {{ direction: string, slope: number, confidence: number }}
188
- */
189
- export function calculateTrend(metrics) {
190
- if (!metrics || metrics.length < 3) {
191
- return { direction: TREND_DIRECTION.STABLE, slope: 0, confidence: 0 };
192
- }
193
-
194
- const values = metrics.map(m => m.value);
195
- const n = values.length;
196
-
197
- // Simple linear regression: y = mx + b
198
- let sumX = 0;
199
- let sumY = 0;
200
- let sumXY = 0;
201
- let sumX2 = 0;
202
-
203
- for (let i = 0; i < n; i++) {
204
- sumX += i;
205
- sumY += values[i];
206
- sumXY += i * values[i];
207
- sumX2 += i * i;
208
- }
209
-
210
- const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
211
-
212
- // Calculate R² (coefficient of determination)
213
- const meanY = sumY / n;
214
- let ssRes = 0;
215
- let ssTot = 0;
216
-
217
- for (let i = 0; i < n; i++) {
218
- const predicted = meanY + slope * (i - (n - 1) / 2);
219
- ssRes += (values[i] - predicted) ** 2;
220
- ssTot += (values[i] - meanY) ** 2;
221
- }
222
-
223
- const r2 = ssTot > 0 ? 1 - ssRes / ssTot : 0;
224
- const confidence = Math.max(0, Math.min(1, Math.abs(r2)));
225
-
226
- // Determine direction
227
- // Normalize slope by mean to get relative change
228
- const normalizedSlope = meanY !== 0 ? slope / Math.abs(meanY) : slope;
229
- const threshold = 0.02; // 2% change per step is significant
230
-
231
- let direction;
232
- if (normalizedSlope > threshold && confidence > 0.3) {
233
- direction = TREND_DIRECTION.IMPROVING;
234
- } else if (normalizedSlope < -threshold && confidence > 0.3) {
235
- direction = TREND_DIRECTION.DECLINING;
236
- } else {
237
- direction = TREND_DIRECTION.STABLE;
238
- }
239
-
240
- return {
241
- direction,
242
- slope: Math.round(slope * 1000) / 1000,
243
- confidence: Math.round(confidence * 100) / 100,
244
- };
245
- }
246
-
247
- /**
248
- * Get a quality dashboard summary.
249
- *
250
- * @param {string} projectDir
251
- * @returns {{ latestScores: Record<string, number>, trends: Record<string, object>, totalMetrics: number, lastUpdated: string|null }}
252
- */
253
- export function getQualityDashboard(projectDir) {
254
- const data = loadMetrics(projectDir);
255
- const metrics = data.metrics;
256
-
257
- if (metrics.length === 0) {
258
- return { latestScores: {}, trends: {}, totalMetrics: 0, lastUpdated: null };
259
- }
260
-
261
- // Get latest value for each metric type
262
- const latestScores = {};
263
- const trends = {};
264
-
265
- for (const type of Object.values(METRIC_TYPES)) {
266
- const typeMetrics = metrics.filter(m => m.type === type);
267
- if (typeMetrics.length > 0) {
268
- latestScores[type] = typeMetrics[typeMetrics.length - 1].value;
269
- trends[type] = calculateTrend(typeMetrics);
270
- }
271
- }
272
-
273
- const lastUpdated = metrics[metrics.length - 1]?.timestamp || null;
274
-
275
- return {
276
- latestScores,
277
- trends,
278
- totalMetrics: metrics.length,
279
- lastUpdated,
280
- };
281
- }