viberadar 0.3.223 → 0.3.225
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 +16 -10
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +202 -6
- package/dist/server/index.js.map +1 -1
- package/dist/tracker.d.ts +1 -0
- package/dist/tracker.d.ts.map +1 -1
- package/dist/tracker.js +6 -0
- package/dist/tracker.js.map +1 -1
- package/dist/ui/dashboard.html +114 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -56,16 +56,22 @@ npx jest --coverage
|
|
|
56
56
|
npx playwright test
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
## Logging standard
|
|
60
|
-
|
|
61
|
-
Repository includes an observability baseline for structured logs:
|
|
62
|
-
|
|
63
|
-
- Standard document: `docs/observability/logging-standard.md`
|
|
64
|
-
- Error code dictionary: `config/logging-error-codes.json`
|
|
65
|
-
- CI/lint command: `npm run lint:logs`
|
|
66
|
-
- Lint self-check tests: `npm run test:lint-logs`
|
|
67
|
-
|
|
68
|
-
##
|
|
59
|
+
## Logging standard
|
|
60
|
+
|
|
61
|
+
Repository includes an observability baseline for structured logs:
|
|
62
|
+
|
|
63
|
+
- Standard document: `docs/observability/logging-standard.md`
|
|
64
|
+
- Error code dictionary: `config/logging-error-codes.json`
|
|
65
|
+
- CI/lint command: `npm run lint:logs`
|
|
66
|
+
- Lint self-check tests: `npm run test:lint-logs`
|
|
67
|
+
|
|
68
|
+
## Task tracker standard
|
|
69
|
+
|
|
70
|
+
Task Tracker imports project tasks from `viberadar.tasks.json`.
|
|
71
|
+
Authoring rules for human reviewers and Codex skills are documented in
|
|
72
|
+
`docs/task-tracker/task-authoring-standard.md`.
|
|
73
|
+
|
|
74
|
+
## Tech
|
|
69
75
|
|
|
70
76
|
- CLI: TypeScript + Node.js
|
|
71
77
|
- Dashboard: vanilla HTML/JS (zero dependencies in browser)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAO7B,OAAO,EAAE,UAAU,EAA4H,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAO7B,OAAO,EAAE,UAAU,EAA4H,MAAM,YAAY,CAAC;AAiBlK,UAAU,aAAa;IACrB,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC;CACrB;AA0vED,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAgsH1G"}
|
package/dist/server/index.js
CHANGED
|
@@ -2336,7 +2336,132 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
2336
2336
|
}
|
|
2337
2337
|
return { passes, fails };
|
|
2338
2338
|
}
|
|
2339
|
-
function
|
|
2339
|
+
function parseK6ThresholdMs(thresholds, percentile) {
|
|
2340
|
+
if (!thresholds || typeof thresholds !== 'object')
|
|
2341
|
+
return undefined;
|
|
2342
|
+
for (const expression of Object.keys(thresholds)) {
|
|
2343
|
+
const escaped = percentile.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2344
|
+
const match = expression.match(new RegExp(`p\\(${escaped}\\)\\s*<\\s*(\\d+(?:\\.\\d+)?)`));
|
|
2345
|
+
if (match) {
|
|
2346
|
+
const value = Number(match[1]);
|
|
2347
|
+
if (Number.isFinite(value))
|
|
2348
|
+
return value;
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
return undefined;
|
|
2352
|
+
}
|
|
2353
|
+
function normalizeK6EndpointTag(tags) {
|
|
2354
|
+
if (!tags || typeof tags !== 'object')
|
|
2355
|
+
return null;
|
|
2356
|
+
const record = tags;
|
|
2357
|
+
const candidates = [record.endpoint, record.name, record.url];
|
|
2358
|
+
for (const candidate of candidates) {
|
|
2359
|
+
if (typeof candidate !== 'string')
|
|
2360
|
+
continue;
|
|
2361
|
+
const value = candidate.trim();
|
|
2362
|
+
if (!value)
|
|
2363
|
+
continue;
|
|
2364
|
+
return value.replace(/^https?:\/\/[^/]+/i, '').slice(0, 180);
|
|
2365
|
+
}
|
|
2366
|
+
return null;
|
|
2367
|
+
}
|
|
2368
|
+
function percentile(values, p) {
|
|
2369
|
+
if (values.length === 0)
|
|
2370
|
+
return undefined;
|
|
2371
|
+
const sorted = values.slice().sort((a, b) => a - b);
|
|
2372
|
+
const rank = (sorted.length - 1) * p;
|
|
2373
|
+
const lower = Math.floor(rank);
|
|
2374
|
+
const upper = Math.ceil(rank);
|
|
2375
|
+
if (lower === upper)
|
|
2376
|
+
return sorted[lower];
|
|
2377
|
+
const weight = rank - lower;
|
|
2378
|
+
return sorted[lower] * (1 - weight) + sorted[upper] * weight;
|
|
2379
|
+
}
|
|
2380
|
+
function parseK6EndpointMetrics(metricsPath) {
|
|
2381
|
+
if (!fs.existsSync(metricsPath))
|
|
2382
|
+
return [];
|
|
2383
|
+
const acc = new Map();
|
|
2384
|
+
const getBucket = (endpoint) => {
|
|
2385
|
+
let bucket = acc.get(endpoint);
|
|
2386
|
+
if (!bucket) {
|
|
2387
|
+
bucket = { requests: 0, failures: 0, durations: [], durationSum: 0 };
|
|
2388
|
+
acc.set(endpoint, bucket);
|
|
2389
|
+
}
|
|
2390
|
+
return bucket;
|
|
2391
|
+
};
|
|
2392
|
+
const fd = fs.openSync(metricsPath, 'r');
|
|
2393
|
+
const buffer = Buffer.alloc(1024 * 1024);
|
|
2394
|
+
let remainder = '';
|
|
2395
|
+
try {
|
|
2396
|
+
while (true) {
|
|
2397
|
+
const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, null);
|
|
2398
|
+
if (bytesRead <= 0)
|
|
2399
|
+
break;
|
|
2400
|
+
const text = remainder + buffer.subarray(0, bytesRead).toString('utf-8');
|
|
2401
|
+
const lines = text.split(/\r?\n/);
|
|
2402
|
+
remainder = lines.pop() || '';
|
|
2403
|
+
for (const line of lines)
|
|
2404
|
+
processK6MetricLine(line);
|
|
2405
|
+
}
|
|
2406
|
+
if (remainder.trim())
|
|
2407
|
+
processK6MetricLine(remainder);
|
|
2408
|
+
}
|
|
2409
|
+
finally {
|
|
2410
|
+
fs.closeSync(fd);
|
|
2411
|
+
}
|
|
2412
|
+
function processK6MetricLine(line) {
|
|
2413
|
+
if (!line.trim())
|
|
2414
|
+
return;
|
|
2415
|
+
let point;
|
|
2416
|
+
try {
|
|
2417
|
+
point = JSON.parse(line);
|
|
2418
|
+
}
|
|
2419
|
+
catch {
|
|
2420
|
+
return;
|
|
2421
|
+
}
|
|
2422
|
+
if (point?.type !== 'Point')
|
|
2423
|
+
return;
|
|
2424
|
+
const data = point.data;
|
|
2425
|
+
if (!data || typeof data.value !== 'number')
|
|
2426
|
+
return;
|
|
2427
|
+
const endpoint = normalizeK6EndpointTag(data.tags);
|
|
2428
|
+
if (!endpoint)
|
|
2429
|
+
return;
|
|
2430
|
+
const bucket = getBucket(endpoint);
|
|
2431
|
+
if (point.metric === 'http_reqs') {
|
|
2432
|
+
bucket.requests += data.value;
|
|
2433
|
+
}
|
|
2434
|
+
else if (point.metric === 'http_req_failed') {
|
|
2435
|
+
if (data.value > 0)
|
|
2436
|
+
bucket.failures += data.value;
|
|
2437
|
+
}
|
|
2438
|
+
else if (point.metric === 'http_req_duration') {
|
|
2439
|
+
bucket.durations.push(data.value);
|
|
2440
|
+
bucket.durationSum += data.value;
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
return Array.from(acc.entries())
|
|
2444
|
+
.map(([endpoint, bucket]) => {
|
|
2445
|
+
const requests = Math.round(bucket.requests || bucket.durations.length);
|
|
2446
|
+
const failures = Math.round(bucket.failures);
|
|
2447
|
+
const avgDuration = bucket.durations.length > 0 ? bucket.durationSum / bucket.durations.length : undefined;
|
|
2448
|
+
return {
|
|
2449
|
+
endpoint,
|
|
2450
|
+
requests,
|
|
2451
|
+
failures,
|
|
2452
|
+
errorPct: requests > 0 ? (failures / requests) * 100 : undefined,
|
|
2453
|
+
avgDuration,
|
|
2454
|
+
p90Duration: percentile(bucket.durations, 0.90),
|
|
2455
|
+
p95Duration: percentile(bucket.durations, 0.95),
|
|
2456
|
+
p99Duration: percentile(bucket.durations, 0.99),
|
|
2457
|
+
maxDuration: bucket.durations.length ? bucket.durations.reduce((max, value) => Math.max(max, value), 0) : undefined,
|
|
2458
|
+
};
|
|
2459
|
+
})
|
|
2460
|
+
.filter((endpoint) => endpoint.requests > 0 || endpoint.avgDuration != null)
|
|
2461
|
+
.sort((a, b) => (b.p95Duration || 0) - (a.p95Duration || 0) || b.requests - a.requests)
|
|
2462
|
+
.slice(0, 100);
|
|
2463
|
+
}
|
|
2464
|
+
function normalizeK6Summary(raw, exitCode, metricsPath) {
|
|
2340
2465
|
const metrics = raw?.metrics || {};
|
|
2341
2466
|
const duration = metrics.http_req_duration?.values || {};
|
|
2342
2467
|
const reqs = metrics.http_reqs?.values || {};
|
|
@@ -2352,6 +2477,9 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
2352
2477
|
thresholdsPassed++;
|
|
2353
2478
|
}
|
|
2354
2479
|
}
|
|
2480
|
+
const endpoints = metricsPath ? parseK6EndpointMetrics(metricsPath) : [];
|
|
2481
|
+
const p95ThresholdMs = parseK6ThresholdMs(metrics.http_req_duration?.thresholds, '95');
|
|
2482
|
+
const p99ThresholdMs = parseK6ThresholdMs(metrics.http_req_duration?.thresholds, '99');
|
|
2355
2483
|
return {
|
|
2356
2484
|
totalRequests: typeof reqs.count === 'number' ? reqs.count : undefined,
|
|
2357
2485
|
rps: typeof reqs.rate === 'number' ? reqs.rate : undefined,
|
|
@@ -2365,14 +2493,17 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
2365
2493
|
checksFailed: checks.fails,
|
|
2366
2494
|
thresholdsPassed,
|
|
2367
2495
|
thresholdsFailed,
|
|
2496
|
+
p95ThresholdMs,
|
|
2497
|
+
p99ThresholdMs,
|
|
2498
|
+
endpoints,
|
|
2368
2499
|
exitCode,
|
|
2369
2500
|
};
|
|
2370
2501
|
}
|
|
2371
|
-
function readK6Summary(summaryPath, exitCode) {
|
|
2502
|
+
function readK6Summary(summaryPath, exitCode, metricsPath) {
|
|
2372
2503
|
try {
|
|
2373
2504
|
if (!fs.existsSync(summaryPath))
|
|
2374
2505
|
return { exitCode };
|
|
2375
|
-
return normalizeK6Summary(JSON.parse(fs.readFileSync(summaryPath, 'utf-8')), exitCode);
|
|
2506
|
+
return normalizeK6Summary(JSON.parse(fs.readFileSync(summaryPath, 'utf-8')), exitCode, metricsPath);
|
|
2376
2507
|
}
|
|
2377
2508
|
catch {
|
|
2378
2509
|
return { exitCode };
|
|
@@ -2426,6 +2557,31 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
2426
2557
|
catch { }
|
|
2427
2558
|
}
|
|
2428
2559
|
}
|
|
2560
|
+
function enrichLoadRunRecord(record) {
|
|
2561
|
+
if (Array.isArray(record.summary?.endpoints) && record.summary.endpoints.length > 0) {
|
|
2562
|
+
return { record, changed: false };
|
|
2563
|
+
}
|
|
2564
|
+
const resultPath = record.config?.resultPath;
|
|
2565
|
+
if (!resultPath || typeof resultPath !== 'string') {
|
|
2566
|
+
return { record, changed: false };
|
|
2567
|
+
}
|
|
2568
|
+
const summaryPath = path.join(resultPath, 'summary.json');
|
|
2569
|
+
const metricsPath = path.join(resultPath, 'metrics.ndjson');
|
|
2570
|
+
const summary = readK6Summary(summaryPath, record.summary?.exitCode ?? null, metricsPath);
|
|
2571
|
+
if (!summary || !Array.isArray(summary.endpoints) || summary.endpoints.length === 0) {
|
|
2572
|
+
return { record, changed: false };
|
|
2573
|
+
}
|
|
2574
|
+
const next = {
|
|
2575
|
+
...record,
|
|
2576
|
+
summary: { ...(record.summary || {}), ...summary },
|
|
2577
|
+
};
|
|
2578
|
+
if (summary.totalRequests != null)
|
|
2579
|
+
next.totalRequests = summary.totalRequests;
|
|
2580
|
+
if (summary.errorPct != null && summary.totalRequests != null) {
|
|
2581
|
+
next.totalErrors = Math.round(summary.totalRequests * (summary.errorPct / 100));
|
|
2582
|
+
}
|
|
2583
|
+
return { record: next, changed: true };
|
|
2584
|
+
}
|
|
2429
2585
|
function loadLastRunIntoState() {
|
|
2430
2586
|
const latest = readLoadRunIndex()[0];
|
|
2431
2587
|
if (!latest)
|
|
@@ -2757,10 +2913,27 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
2757
2913
|
/** Actually spawn the agent process for a queue item */
|
|
2758
2914
|
async function executeAgentItem(item) {
|
|
2759
2915
|
const { runId, task, featureKey, filePath, selectedFilePaths, title, agent, savedErrors, savedFailedFiles, savedTestType, autoFixAttempt = 0, autoFixSourceTask, } = item;
|
|
2916
|
+
const trackerTaskId = typeof item.meta?.taskId === 'string' ? item.meta.taskId : null;
|
|
2760
2917
|
const normalizeRelPath = (p) => p.replace(/\\/g, '/');
|
|
2761
2918
|
const emitOutput = (line, isError = false, isDim = false) => {
|
|
2762
2919
|
broadcast('agent-output', { runId, line, isError, isDim });
|
|
2763
2920
|
};
|
|
2921
|
+
const updateTrackedTaskFromRun = (phase) => {
|
|
2922
|
+
if (!trackerTaskId)
|
|
2923
|
+
return;
|
|
2924
|
+
try {
|
|
2925
|
+
const patch = phase === 'completed'
|
|
2926
|
+
? { status: 'done', lastRunId: runId }
|
|
2927
|
+
: phase === 'failed'
|
|
2928
|
+
? { status: 'review', lastRunId: runId }
|
|
2929
|
+
: { lastRunId: runId };
|
|
2930
|
+
(0, tracker_1.updateTrackerTask)(projectRoot, trackerTaskId, patch);
|
|
2931
|
+
broadcast('tasks-updated', { filePath: tracker_1.TRACKER_FILE_NAME, taskId: trackerTaskId, runId, phase });
|
|
2932
|
+
}
|
|
2933
|
+
catch (err) {
|
|
2934
|
+
emitOutput(`⚠️ Не удалось обновить задачу трекера ${trackerTaskId}: ${err.message || err}`, true, true);
|
|
2935
|
+
}
|
|
2936
|
+
};
|
|
2764
2937
|
const targetSourcePaths = (() => {
|
|
2765
2938
|
if (task === 'write-tests-file' && filePath) {
|
|
2766
2939
|
return [normalizeRelPath(filePath)];
|
|
@@ -2783,6 +2956,7 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
2783
2956
|
});
|
|
2784
2957
|
function failBeforeStart(message) {
|
|
2785
2958
|
setRunPhase(runId, 'failed', { error: message, targetSourcePaths });
|
|
2959
|
+
updateTrackedTaskFromRun('failed');
|
|
2786
2960
|
broadcast('agent-error', { runId, message });
|
|
2787
2961
|
agentRunning = false;
|
|
2788
2962
|
processNextInQueue();
|
|
@@ -3480,12 +3654,14 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
3480
3654
|
validationStats: finalValidationStats,
|
|
3481
3655
|
error: finalError,
|
|
3482
3656
|
});
|
|
3657
|
+
updateTrackedTaskFromRun(finalPhase);
|
|
3483
3658
|
processNextInQueue(true);
|
|
3484
3659
|
}
|
|
3485
3660
|
else if (code === 255) {
|
|
3486
3661
|
process.stdout.write(` ❌ Agent auth error (exit code 255)\n`);
|
|
3487
3662
|
const message = `${agent === 'claude' ? 'Claude Code' : 'Codex'} не авторизован. Нажми 🔑 Перелогиниться в меню агента.`;
|
|
3488
3663
|
setRunPhase(runId, 'failed', { targetSourcePaths, error: message });
|
|
3664
|
+
updateTrackedTaskFromRun('failed');
|
|
3489
3665
|
broadcast('agent-error', {
|
|
3490
3666
|
runId,
|
|
3491
3667
|
message,
|
|
@@ -3498,6 +3674,7 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
3498
3674
|
process.stdout.write(` ❌ Agent failed (exit code ${code})\n`);
|
|
3499
3675
|
const message = `Агент завершился с кодом ${code}`;
|
|
3500
3676
|
setRunPhase(runId, 'failed', { targetSourcePaths, error: message });
|
|
3677
|
+
updateTrackedTaskFromRun('failed');
|
|
3501
3678
|
broadcast('agent-error', { runId, message });
|
|
3502
3679
|
if (queueBlockSignal === 403 || queueBlockSignal === 429) {
|
|
3503
3680
|
stopQueuedTasks(`пойман ${queueBlockSignal} от ${agent === 'claude' ? 'Claude Code' : 'Codex'}`);
|
|
@@ -3519,6 +3696,7 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
3519
3696
|
: `Не удалось запустить ${agent}: ${err.message}`;
|
|
3520
3697
|
process.stdout.write(' ❌ Agent spawn error: ' + err.message + '\n');
|
|
3521
3698
|
setRunPhase(runId, 'failed', { targetSourcePaths, error: msg });
|
|
3699
|
+
updateTrackedTaskFromRun('failed');
|
|
3522
3700
|
broadcast('agent-error', { runId, message: msg, notInstalled: isNotFound, agent });
|
|
3523
3701
|
processNextInQueue(true);
|
|
3524
3702
|
});
|
|
@@ -4001,7 +4179,7 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
4001
4179
|
sendJson(res, 409, { ok: false, error: 'Не удалось поставить задачу в очередь агента' });
|
|
4002
4180
|
return;
|
|
4003
4181
|
}
|
|
4004
|
-
const updatedTask = (0, tracker_1.
|
|
4182
|
+
const updatedTask = (0, tracker_1.updateTrackerTask)(projectRoot, task.id, { status: 'in-progress', lastRunId: runId });
|
|
4005
4183
|
broadcast('tasks-updated', { filePath: tracker_1.TRACKER_FILE_NAME, taskId: task.id, runId });
|
|
4006
4184
|
sendJson(res, 200, { ok: true, runId, task: updatedTask });
|
|
4007
4185
|
}
|
|
@@ -5393,7 +5571,25 @@ a{color:var(--blue)}
|
|
|
5393
5571
|
return;
|
|
5394
5572
|
}
|
|
5395
5573
|
const runPath = path.join(loadRunsDir, `${runId}.json`);
|
|
5396
|
-
const
|
|
5574
|
+
const parsed = JSON.parse(fs.readFileSync(runPath, 'utf-8'));
|
|
5575
|
+
const { record, changed } = enrichLoadRunRecord(parsed);
|
|
5576
|
+
if (changed) {
|
|
5577
|
+
try {
|
|
5578
|
+
fs.writeFileSync(runPath, JSON.stringify(record, null, 2), 'utf-8');
|
|
5579
|
+
}
|
|
5580
|
+
catch { }
|
|
5581
|
+
try {
|
|
5582
|
+
const index = readLoadRunIndex();
|
|
5583
|
+
const item = index.find((i) => i.runId === runId);
|
|
5584
|
+
if (item) {
|
|
5585
|
+
item.summary = record.summary;
|
|
5586
|
+
item.status = record.status;
|
|
5587
|
+
item.endTime = record.endTime;
|
|
5588
|
+
writeLoadRunIndex(index);
|
|
5589
|
+
}
|
|
5590
|
+
}
|
|
5591
|
+
catch { }
|
|
5592
|
+
}
|
|
5397
5593
|
res.writeHead(200, jsonH);
|
|
5398
5594
|
res.end(JSON.stringify(record));
|
|
5399
5595
|
}
|
|
@@ -5613,7 +5809,7 @@ a{color:var(--blue)}
|
|
|
5613
5809
|
loadState.status = code === 0 ? 'done' : 'error';
|
|
5614
5810
|
}
|
|
5615
5811
|
loadState.endTime = Date.now();
|
|
5616
|
-
loadState.summary = readK6Summary(summaryPath, code);
|
|
5812
|
+
loadState.summary = readK6Summary(summaryPath, code, jsonOutPath);
|
|
5617
5813
|
if (loadState.summary?.totalRequests != null)
|
|
5618
5814
|
loadState.totalRequests = loadState.summary.totalRequests;
|
|
5619
5815
|
if (loadState.summary?.errorPct != null && loadState.summary.totalRequests != null) {
|