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 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
- ## Tech
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;AAkBlK,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,CAqgH1G"}
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"}
@@ -2336,7 +2336,132 @@ function startServer({ data: initialData, port, projectRoot }) {
2336
2336
  }
2337
2337
  return { passes, fails };
2338
2338
  }
2339
- function normalizeK6Summary(raw, exitCode) {
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.setTrackerTaskRun)(projectRoot, task.id, runId);
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 record = JSON.parse(fs.readFileSync(runPath, 'utf-8'));
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) {