viberadar 0.3.223 → 0.3.224
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 +158 -5
- 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,CAypH1G"}
|
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 };
|
|
@@ -2757,10 +2888,27 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
2757
2888
|
/** Actually spawn the agent process for a queue item */
|
|
2758
2889
|
async function executeAgentItem(item) {
|
|
2759
2890
|
const { runId, task, featureKey, filePath, selectedFilePaths, title, agent, savedErrors, savedFailedFiles, savedTestType, autoFixAttempt = 0, autoFixSourceTask, } = item;
|
|
2891
|
+
const trackerTaskId = typeof item.meta?.taskId === 'string' ? item.meta.taskId : null;
|
|
2760
2892
|
const normalizeRelPath = (p) => p.replace(/\\/g, '/');
|
|
2761
2893
|
const emitOutput = (line, isError = false, isDim = false) => {
|
|
2762
2894
|
broadcast('agent-output', { runId, line, isError, isDim });
|
|
2763
2895
|
};
|
|
2896
|
+
const updateTrackedTaskFromRun = (phase) => {
|
|
2897
|
+
if (!trackerTaskId)
|
|
2898
|
+
return;
|
|
2899
|
+
try {
|
|
2900
|
+
const patch = phase === 'completed'
|
|
2901
|
+
? { status: 'done', lastRunId: runId }
|
|
2902
|
+
: phase === 'failed'
|
|
2903
|
+
? { status: 'review', lastRunId: runId }
|
|
2904
|
+
: { lastRunId: runId };
|
|
2905
|
+
(0, tracker_1.updateTrackerTask)(projectRoot, trackerTaskId, patch);
|
|
2906
|
+
broadcast('tasks-updated', { filePath: tracker_1.TRACKER_FILE_NAME, taskId: trackerTaskId, runId, phase });
|
|
2907
|
+
}
|
|
2908
|
+
catch (err) {
|
|
2909
|
+
emitOutput(`⚠️ Не удалось обновить задачу трекера ${trackerTaskId}: ${err.message || err}`, true, true);
|
|
2910
|
+
}
|
|
2911
|
+
};
|
|
2764
2912
|
const targetSourcePaths = (() => {
|
|
2765
2913
|
if (task === 'write-tests-file' && filePath) {
|
|
2766
2914
|
return [normalizeRelPath(filePath)];
|
|
@@ -2783,6 +2931,7 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
2783
2931
|
});
|
|
2784
2932
|
function failBeforeStart(message) {
|
|
2785
2933
|
setRunPhase(runId, 'failed', { error: message, targetSourcePaths });
|
|
2934
|
+
updateTrackedTaskFromRun('failed');
|
|
2786
2935
|
broadcast('agent-error', { runId, message });
|
|
2787
2936
|
agentRunning = false;
|
|
2788
2937
|
processNextInQueue();
|
|
@@ -3480,12 +3629,14 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
3480
3629
|
validationStats: finalValidationStats,
|
|
3481
3630
|
error: finalError,
|
|
3482
3631
|
});
|
|
3632
|
+
updateTrackedTaskFromRun(finalPhase);
|
|
3483
3633
|
processNextInQueue(true);
|
|
3484
3634
|
}
|
|
3485
3635
|
else if (code === 255) {
|
|
3486
3636
|
process.stdout.write(` ❌ Agent auth error (exit code 255)\n`);
|
|
3487
3637
|
const message = `${agent === 'claude' ? 'Claude Code' : 'Codex'} не авторизован. Нажми 🔑 Перелогиниться в меню агента.`;
|
|
3488
3638
|
setRunPhase(runId, 'failed', { targetSourcePaths, error: message });
|
|
3639
|
+
updateTrackedTaskFromRun('failed');
|
|
3489
3640
|
broadcast('agent-error', {
|
|
3490
3641
|
runId,
|
|
3491
3642
|
message,
|
|
@@ -3498,6 +3649,7 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
3498
3649
|
process.stdout.write(` ❌ Agent failed (exit code ${code})\n`);
|
|
3499
3650
|
const message = `Агент завершился с кодом ${code}`;
|
|
3500
3651
|
setRunPhase(runId, 'failed', { targetSourcePaths, error: message });
|
|
3652
|
+
updateTrackedTaskFromRun('failed');
|
|
3501
3653
|
broadcast('agent-error', { runId, message });
|
|
3502
3654
|
if (queueBlockSignal === 403 || queueBlockSignal === 429) {
|
|
3503
3655
|
stopQueuedTasks(`пойман ${queueBlockSignal} от ${agent === 'claude' ? 'Claude Code' : 'Codex'}`);
|
|
@@ -3519,6 +3671,7 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
3519
3671
|
: `Не удалось запустить ${agent}: ${err.message}`;
|
|
3520
3672
|
process.stdout.write(' ❌ Agent spawn error: ' + err.message + '\n');
|
|
3521
3673
|
setRunPhase(runId, 'failed', { targetSourcePaths, error: msg });
|
|
3674
|
+
updateTrackedTaskFromRun('failed');
|
|
3522
3675
|
broadcast('agent-error', { runId, message: msg, notInstalled: isNotFound, agent });
|
|
3523
3676
|
processNextInQueue(true);
|
|
3524
3677
|
});
|
|
@@ -4001,7 +4154,7 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
4001
4154
|
sendJson(res, 409, { ok: false, error: 'Не удалось поставить задачу в очередь агента' });
|
|
4002
4155
|
return;
|
|
4003
4156
|
}
|
|
4004
|
-
const updatedTask = (0, tracker_1.
|
|
4157
|
+
const updatedTask = (0, tracker_1.updateTrackerTask)(projectRoot, task.id, { status: 'in-progress', lastRunId: runId });
|
|
4005
4158
|
broadcast('tasks-updated', { filePath: tracker_1.TRACKER_FILE_NAME, taskId: task.id, runId });
|
|
4006
4159
|
sendJson(res, 200, { ok: true, runId, task: updatedTask });
|
|
4007
4160
|
}
|
|
@@ -5613,7 +5766,7 @@ a{color:var(--blue)}
|
|
|
5613
5766
|
loadState.status = code === 0 ? 'done' : 'error';
|
|
5614
5767
|
}
|
|
5615
5768
|
loadState.endTime = Date.now();
|
|
5616
|
-
loadState.summary = readK6Summary(summaryPath, code);
|
|
5769
|
+
loadState.summary = readK6Summary(summaryPath, code, jsonOutPath);
|
|
5617
5770
|
if (loadState.summary?.totalRequests != null)
|
|
5618
5771
|
loadState.totalRequests = loadState.summary.totalRequests;
|
|
5619
5772
|
if (loadState.summary?.errorPct != null && loadState.summary.totalRequests != null) {
|