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 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,CAypH1G"}
@@ -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 };
@@ -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.setTrackerTaskRun)(projectRoot, task.id, runId);
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) {