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.
- package/README.md +18 -13
- package/bin/chati.js +32 -4
- package/framework/agents/build/dev.md +9 -5
- package/framework/agents/discover/brief.md +2 -0
- package/framework/agents/discover/brownfield-wu.md +2 -0
- package/framework/agents/discover/greenfield-wu.md +18 -1
- package/framework/agents/plan/detail.md +2 -0
- package/framework/agents/plan/tasks.md +31 -0
- package/framework/agents/plan/ux-brand-architect.md +21 -2
- package/framework/agents/plan/ux-component-engineer.md +10 -0
- package/framework/agents/quality/qa-implementation.md +1 -1
- package/framework/agents/quality/qa-planning.md +1 -1
- package/framework/agents/quality/qa-visual.md +33 -21
- package/framework/config.yaml +5 -4
- package/framework/constitution.md +58 -11
- package/framework/context/governance.md +12 -10
- package/framework/context/root.md +2 -2
- package/framework/data/entity-registry.yaml +12 -4
- package/framework/data/qa-rubrics.yaml +335 -0
- package/framework/domains/constitution.yaml +3 -3
- package/framework/executors/json-validate.js +80 -0
- package/framework/executors/npm-script.js +72 -0
- package/framework/executors/path-check.js +67 -0
- package/framework/executors/registry.yaml +36 -0
- package/framework/hooks/advance-trigger.js +47 -3
- package/framework/hooks/constitution-guard.js +9 -4
- package/framework/hooks/git-push-authority.js +113 -0
- package/framework/hooks/license-guard.js +59 -9
- package/framework/hooks/model-governance.js +40 -17
- package/framework/hooks/prism-engine.js +78 -11
- package/framework/hooks/session-digest.js +12 -3
- package/framework/hooks/settings.json +7 -3
- package/framework/hooks/style-guard.js +52 -3
- package/framework/hooks/team-quality-gate.js +43 -5
- 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/confidence.yaml +85 -6
- package/framework/intelligence/context-engine.md +9 -5
- package/framework/intelligence/decision-engine.md +1 -1
- package/framework/orchestrator/chati-router.js +59 -5
- package/framework/orchestrator/chati-update.md +19 -3
- package/framework/orchestrator/chati.md +20 -1
- package/framework/quality-gates/planning-gate.md +2 -0
- package/framework/rules/human-writing-style.md +47 -0
- package/framework/scaffold/motion-premium/README.md +12 -2
- package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +1 -1
- package/framework/scaffold/motion-premium/scaffold.yaml +6 -1
- package/framework/scaffold/motion-premium-3d/README.md +10 -0
- package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +1 -1
- package/framework/scaffold/motion-premium-3d/scaffold.yaml +3 -0
- package/framework/scaffold/saas-dashboard/README.md +58 -0
- package/framework/scaffold/saas-dashboard/app/dashboard/page.tsx.template +546 -0
- package/framework/scaffold/saas-dashboard/app/globals.css.template +746 -0
- package/framework/scaffold/saas-dashboard/app/layout.tsx.template +192 -0
- package/framework/scaffold/saas-dashboard/app/shell.css.template +1070 -0
- package/framework/scaffold/saas-dashboard/components/data/Chart.tsx.template +725 -0
- package/framework/scaffold/saas-dashboard/components/data/DataTable.tsx.template +623 -0
- package/framework/scaffold/saas-dashboard/components/data/DataTable.types.ts.template +104 -0
- package/framework/scaffold/saas-dashboard/components/data/DataTablePagination.tsx.template +234 -0
- package/framework/scaffold/saas-dashboard/components/data/SkeletonBlock.tsx.template +111 -0
- package/framework/scaffold/saas-dashboard/components/data/StatCard.tsx.template +393 -0
- package/framework/scaffold/saas-dashboard/components/data/StatCardGrid.tsx.template +121 -0
- package/framework/scaffold/saas-dashboard/components/feedback/EmptyState.tsx.template +281 -0
- package/framework/scaffold/saas-dashboard/components/interaction/CommandPalette.tsx.template +767 -0
- package/framework/scaffold/saas-dashboard/components/interaction/Field.tsx.template +351 -0
- package/framework/scaffold/saas-dashboard/components/interaction/Form.tsx.template +424 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Badge.tsx.template +193 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Button.tsx.template +261 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Card.tsx.template +287 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Input.tsx.template +310 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Spinner.tsx.template +135 -0
- package/framework/scaffold/saas-dashboard/components/primitives/index.ts.template +60 -0
- package/framework/scaffold/saas-dashboard/components/shell/AppShell.tsx.template +174 -0
- package/framework/scaffold/saas-dashboard/components/shell/Breadcrumb.tsx.template +186 -0
- package/framework/scaffold/saas-dashboard/components/shell/Container.tsx.template +86 -0
- package/framework/scaffold/saas-dashboard/components/shell/NavItem.tsx.template +147 -0
- package/framework/scaffold/saas-dashboard/components/shell/Sidebar.tsx.template +378 -0
- package/framework/scaffold/saas-dashboard/components/shell/SidebarContext.tsx.template +169 -0
- package/framework/scaffold/saas-dashboard/components/shell/ThemeToggle.tsx.template +128 -0
- package/framework/scaffold/saas-dashboard/components/shell/Topbar.tsx.template +301 -0
- package/framework/scaffold/saas-dashboard/lib/brand.ts.template +324 -0
- package/framework/scaffold/saas-dashboard/scaffold.yaml +225 -0
- package/framework/schemas/session.schema.json +5 -0
- package/framework/schemas/task.schema.json +9 -0
- package/framework/scripts/visual-qa.js +101 -3
- package/framework/tasks/orchestrator-health.md +4 -4
- package/framework/tasks/qa-impl-verdict.md +11 -5
- package/framework/templates/brandbook-html-tmpl.md +1 -1
- package/framework/templates/qa-gate-tmpl.yaml +24 -11
- package/package.json +4 -3
- package/src/config/context-file-generator.js +0 -6
- package/src/dashboard/renderer.js +0 -36
- package/src/executors/runner.js +204 -0
- package/src/installer/core.js +13 -3
- package/src/installer/templates.js +5 -6
- package/src/intelligence/registry-manager.js +1 -1
- package/src/license/client.js +9 -3
- package/src/license/commands.js +12 -2
- package/src/license/machine-id.js +42 -1
- package/src/memory/gotchas.js +58 -40
- package/src/memory/magic-docs.js +1 -1
- package/src/memory/session-digest.js +9 -4
- package/src/orchestrator/cli.js +186 -15
- package/src/orchestrator/doctor.js +98 -16
- package/src/orchestrator/pipeline-manager.js +44 -22
- package/src/orchestrator/session-manager.js +64 -8
- package/src/telemetry/sender.js +7 -19
- package/src/terminal/run-team.js +3 -3
- package/src/upgrade/tracked-files-detector.js +34 -16
- package/src/utils/feature-flags.js +1 -1
- package/src/utils/flatten-entities.js +4 -40
- package/src/utils/schema-validator.js +0 -14
- package/src/wizard/i18n.js +3 -3
- package/src/api/index.js +0 -120
- package/src/autonomy/autonomous-gate.js +0 -294
- package/src/autonomy/build-loop.js +0 -281
- package/src/autonomy/build-state.js +0 -286
- package/src/autonomy/cause-analyzer.js +0 -177
- package/src/autonomy/escalation.js +0 -214
- package/src/autonomy/index.js +0 -51
- package/src/autonomy/mode-manager.js +0 -225
- package/src/autonomy/mode-suggester.js +0 -283
- package/src/autonomy/progress-reporter.js +0 -275
- package/src/autonomy/safety-net.js +0 -370
- package/src/config/agent-customizer.js +0 -231
- package/src/decision/analyzer.js +0 -291
- package/src/decision/engine.js +0 -250
- package/src/decision/index.js +0 -38
- package/src/decision/registry-healer.js +0 -468
- package/src/decision/registry-updater.js +0 -339
- package/src/extensions/loader.js +0 -145
- package/src/extensions/registry.js +0 -134
- package/src/gates/circuit-breaker.js +0 -151
- package/src/gates/g1-planning-complete.js +0 -154
- package/src/gates/g2-qa-planning.js +0 -156
- package/src/gates/g3-implementation.js +0 -215
- package/src/gates/g4-qa-implementation.js +0 -240
- package/src/gates/g5-deploy-ready.js +0 -181
- package/src/gates/gate-base.js +0 -185
- package/src/gates/index.js +0 -46
- package/src/health/auto-fix.js +0 -216
- package/src/health/engine.js +0 -246
- package/src/merger/semantic-merger.js +0 -292
- package/src/preview/detector.js +0 -238
- package/src/preview/index.js +0 -20
- package/src/preview/launcher.js +0 -235
- package/src/preview/log-buffer.js +0 -103
- package/src/quality/metrics-collector.js +0 -281
- package/src/quality/test-runner.js +0 -366
- package/src/tasks/executor.js +0 -195
- package/src/tasks/index.js +0 -4
- package/src/tasks/loader.js +0 -210
- package/src/tasks/router.js +0 -182
- package/src/utils/event-bus.js +0 -126
package/src/preview/launcher.js
DELETED
|
@@ -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
|
-
}
|