codeharness 0.22.0 → 0.22.2

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.
@@ -190,6 +190,50 @@ interface CoverageTargetResult {
190
190
  /** Gap: target - current (0 if met) */
191
191
  readonly gap: number;
192
192
  }
193
+ /** Configuration for standalone runtime validation (audit mode) */
194
+ interface RuntimeValidationConfig {
195
+ /** Test command to run (default: 'npm test') */
196
+ readonly testCommand: string;
197
+ /** OTLP endpoint for telemetry export */
198
+ readonly otlpEndpoint: string;
199
+ /** Query endpoint for the observability backend (VictoriaLogs) */
200
+ readonly queryEndpoint: string;
201
+ /** Timeout for the entire validation process in milliseconds (default: 120000) */
202
+ readonly timeoutMs: number;
203
+ }
204
+ /** A single module's telemetry status from runtime validation */
205
+ interface ModuleTelemetryEntry {
206
+ /** Module name (derived from source directory) */
207
+ readonly moduleName: string;
208
+ /** Whether telemetry was detected for this module */
209
+ readonly telemetryDetected: boolean;
210
+ /** Number of telemetry events emitted by this module */
211
+ readonly eventCount: number;
212
+ }
213
+ /** Result of a standalone runtime validation run */
214
+ interface RuntimeValidationResult {
215
+ /** Per-module telemetry entries */
216
+ readonly entries: readonly ModuleTelemetryEntry[];
217
+ /** Total number of modules in the project */
218
+ readonly totalModules: number;
219
+ /** Number of modules that emitted telemetry */
220
+ readonly modulesWithTelemetry: number;
221
+ /** Runtime coverage percentage: modulesWithTelemetry / totalModules * 100 */
222
+ readonly coveragePercent: number;
223
+ /** Whether validation was skipped (e.g., backend unreachable) */
224
+ readonly skipped: boolean;
225
+ /** Reason validation was skipped, if applicable */
226
+ readonly skipReason?: string;
227
+ }
228
+ /** A telemetry event returned from the observability backend query */
229
+ interface TelemetryEvent {
230
+ /** ISO 8601 timestamp of the event */
231
+ readonly timestamp: string;
232
+ /** Log message or event description */
233
+ readonly message: string;
234
+ /** Source file or service attribute */
235
+ readonly source: string;
236
+ }
193
237
 
194
238
  /**
195
239
  * Discriminated union Result type for consistent error handling.
@@ -306,4 +350,18 @@ declare function checkObservabilityCoverageGate(projectDir: string, overrides?:
306
350
  runtimeTarget?: number;
307
351
  }): Result<ObservabilityCoverageGateResult>;
308
352
 
309
- export { type AnalyzerConfig, type AnalyzerResult, type AnalyzerSummary, type CoverageHistoryEntry, type CoverageTargetResult, type CoverageTargets, type CoverageTrend, type GapSeverity, type ObservabilityCoverageGateResult, type ObservabilityCoverageState, type ObservabilityGap, type RuntimeCoverageEntry, type RuntimeCoverageResult, type RuntimeCoverageState, type StaticCoverageState, analyze, checkCoverageTarget, checkObservabilityCoverageGate, computeRuntimeCoverage, getCoverageTrend, readCoverageState, saveCoverageResult, saveRuntimeCoverage };
353
+ /**
354
+ * Standalone runtime validator — validates telemetry outside Docker verification.
355
+ * Implements Story 2.3: run tests with OTLP, query backend, report module coverage.
356
+ */
357
+
358
+ /** Run tests with OTLP enabled, query backend for events, compute module coverage. */
359
+ declare function validateRuntime(projectDir: string, config?: Partial<RuntimeValidationConfig>): Promise<Result<RuntimeValidationResult>>;
360
+ /** Check if the observability backend is reachable (2xx within 3s). */
361
+ declare function checkBackendHealth(queryEndpoint: string): Promise<boolean>;
362
+ /** Query VictoriaLogs for telemetry events within a time window. */
363
+ declare function queryTelemetryEvents(queryEndpoint: string, startTime: string, endTime: string): Promise<Result<TelemetryEvent[]>>;
364
+ /** Map telemetry events to project modules by matching source paths. */
365
+ declare function mapEventsToModules(events: TelemetryEvent[], projectDir: string, modules?: string[]): ModuleTelemetryEntry[];
366
+
367
+ export { type AnalyzerConfig, type AnalyzerResult, type AnalyzerSummary, type CoverageHistoryEntry, type CoverageTargetResult, type CoverageTargets, type CoverageTrend, type GapSeverity, type ModuleTelemetryEntry, type ObservabilityCoverageGateResult, type ObservabilityCoverageState, type ObservabilityGap, type RuntimeCoverageEntry, type RuntimeCoverageResult, type RuntimeCoverageState, type RuntimeValidationConfig, type RuntimeValidationResult, type StaticCoverageState, type TelemetryEvent, analyze, checkBackendHealth, checkCoverageTarget, checkObservabilityCoverageGate, computeRuntimeCoverage, getCoverageTrend, mapEventsToModules, queryTelemetryEvents, readCoverageState, saveCoverageResult, saveRuntimeCoverage, validateRuntime };
@@ -422,13 +422,178 @@ function checkObservabilityCoverageGate(projectDir, overrides) {
422
422
  const gapSummary = state.static.gaps ? [...state.static.gaps] : [];
423
423
  return ok({ passed, staticResult, runtimeResult, gapSummary });
424
424
  }
425
+
426
+ // src/modules/observability/runtime-validator.ts
427
+ import { execSync } from "child_process";
428
+ import { readdirSync, statSync } from "fs";
429
+ import { join as join4 } from "path";
430
+ var DEFAULT_CONFIG = {
431
+ testCommand: "npm test",
432
+ otlpEndpoint: "http://localhost:4318",
433
+ queryEndpoint: "http://localhost:9428",
434
+ timeoutMs: 12e4
435
+ };
436
+ var HEALTH_TIMEOUT_MS = 3e3;
437
+ var QUERY_TIMEOUT_MS = 3e4;
438
+ async function validateRuntime(projectDir, config) {
439
+ if (!projectDir || typeof projectDir !== "string") {
440
+ return fail("projectDir is required and must be a non-empty string");
441
+ }
442
+ const cfg = { ...DEFAULT_CONFIG, ...config };
443
+ if (/[;&|`$(){}!#]/.test(cfg.testCommand)) {
444
+ return fail(`testCommand contains disallowed shell metacharacters: ${cfg.testCommand}`);
445
+ }
446
+ const healthy = await checkBackendHealth(cfg.queryEndpoint);
447
+ if (!healthy) {
448
+ const modules2 = discoverModules(projectDir);
449
+ const entries2 = modules2.map((m) => ({
450
+ moduleName: m,
451
+ telemetryDetected: false,
452
+ eventCount: 0
453
+ }));
454
+ return ok({
455
+ entries: entries2,
456
+ totalModules: modules2.length,
457
+ modulesWithTelemetry: 0,
458
+ coveragePercent: 0,
459
+ skipped: true,
460
+ skipReason: "runtime validation skipped -- observability stack not available"
461
+ });
462
+ }
463
+ const startTime = (/* @__PURE__ */ new Date()).toISOString();
464
+ try {
465
+ execSync(cfg.testCommand, {
466
+ cwd: projectDir,
467
+ timeout: cfg.timeoutMs,
468
+ env: {
469
+ ...process.env,
470
+ OTEL_EXPORTER_OTLP_ENDPOINT: cfg.otlpEndpoint
471
+ },
472
+ stdio: "pipe"
473
+ });
474
+ } catch (err) {
475
+ const msg = err instanceof Error ? err.message : String(err);
476
+ return fail(`Test command failed: ${msg}`);
477
+ }
478
+ const endTime = (/* @__PURE__ */ new Date()).toISOString();
479
+ const eventsResult = await queryTelemetryEvents(cfg.queryEndpoint, startTime, endTime);
480
+ if (!eventsResult.success) {
481
+ return fail(eventsResult.error);
482
+ }
483
+ const modules = discoverModules(projectDir);
484
+ const entries = mapEventsToModules(eventsResult.data, projectDir, modules);
485
+ const modulesWithTelemetry = entries.filter((e) => e.telemetryDetected).length;
486
+ const totalModules = entries.length;
487
+ const coveragePercent = totalModules === 0 ? 0 : modulesWithTelemetry / totalModules * 100;
488
+ return ok({
489
+ entries,
490
+ totalModules,
491
+ modulesWithTelemetry,
492
+ coveragePercent,
493
+ skipped: false
494
+ });
495
+ }
496
+ async function checkBackendHealth(queryEndpoint) {
497
+ try {
498
+ const response = await fetch(`${queryEndpoint}/health`, {
499
+ signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS)
500
+ });
501
+ return response.ok;
502
+ } catch {
503
+ return false;
504
+ }
505
+ }
506
+ async function queryTelemetryEvents(queryEndpoint, startTime, endTime) {
507
+ let url;
508
+ try {
509
+ url = new URL("/select/logsql/query", queryEndpoint);
510
+ } catch {
511
+ return fail(`Invalid queryEndpoint URL: ${queryEndpoint}`);
512
+ }
513
+ url.searchParams.set("query", "*");
514
+ url.searchParams.set("start", startTime);
515
+ url.searchParams.set("end", endTime);
516
+ url.searchParams.set("limit", "1000");
517
+ try {
518
+ const response = await fetch(url.toString(), {
519
+ signal: AbortSignal.timeout(QUERY_TIMEOUT_MS)
520
+ });
521
+ if (!response.ok) {
522
+ return fail(`VictoriaLogs returned ${response.status}`);
523
+ }
524
+ const text = await response.text();
525
+ const events = parseLogEvents(text);
526
+ return ok(events);
527
+ } catch (err) {
528
+ const msg = err instanceof Error ? err.message : String(err);
529
+ return fail(`Failed to query telemetry events: ${msg}`);
530
+ }
531
+ }
532
+ function mapEventsToModules(events, projectDir, modules) {
533
+ void projectDir;
534
+ const moduleList = modules ?? [];
535
+ const moduleCounts = /* @__PURE__ */ new Map();
536
+ for (const mod of moduleList) {
537
+ moduleCounts.set(mod, 0);
538
+ }
539
+ for (const event of events) {
540
+ for (const mod of moduleList) {
541
+ if (event.source.includes(mod) || event.message.includes(mod)) {
542
+ moduleCounts.set(mod, (moduleCounts.get(mod) ?? 0) + 1);
543
+ }
544
+ }
545
+ }
546
+ return moduleList.map((mod) => {
547
+ const count = moduleCounts.get(mod) ?? 0;
548
+ return {
549
+ moduleName: mod,
550
+ telemetryDetected: count > 0,
551
+ eventCount: count
552
+ };
553
+ });
554
+ }
555
+ function discoverModules(projectDir) {
556
+ const srcDir = join4(projectDir, "src");
557
+ try {
558
+ return readdirSync(srcDir).filter((name) => {
559
+ try {
560
+ return statSync(join4(srcDir, name)).isDirectory();
561
+ } catch {
562
+ return false;
563
+ }
564
+ });
565
+ } catch {
566
+ return [];
567
+ }
568
+ }
569
+ function parseLogEvents(text) {
570
+ if (!text.trim()) return [];
571
+ const lines = text.trim().split("\n");
572
+ const events = [];
573
+ for (const line of lines) {
574
+ try {
575
+ const raw = JSON.parse(line);
576
+ events.push({
577
+ timestamp: String(raw._time ?? raw.timestamp ?? ""),
578
+ message: String(raw._msg ?? raw.message ?? ""),
579
+ source: String(raw.source ?? raw._source ?? raw.service ?? "")
580
+ });
581
+ } catch {
582
+ }
583
+ }
584
+ return events;
585
+ }
425
586
  export {
426
587
  analyze,
588
+ checkBackendHealth,
427
589
  checkCoverageTarget,
428
590
  checkObservabilityCoverageGate,
429
591
  computeRuntimeCoverage,
430
592
  getCoverageTrend,
593
+ mapEventsToModules,
594
+ queryTelemetryEvents,
431
595
  readCoverageState,
432
596
  saveCoverageResult,
433
- saveRuntimeCoverage
597
+ saveRuntimeCoverage,
598
+ validateRuntime
434
599
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeharness",
3
- "version": "0.22.0",
3
+ "version": "0.22.2",
4
4
  "type": "module",
5
5
  "description": "CLI for codeharness — makes autonomous coding agents produce software that actually works",
6
6
  "bin": {
package/ralph/ralph.sh CHANGED
@@ -782,7 +782,12 @@ execute_iteration() {
782
782
  fi
783
783
 
784
784
  local has_errors="false"
785
- if grep -v '"[^"]*error[^"]*":' "$output_file" 2>/dev/null | \
785
+ # Only check non-JSON lines for errors. Stream-json output is NDJSON
786
+ # (one JSON object per line), so any line starting with '{' is Claude
787
+ # content — which naturally contains words like "error" and "Exception"
788
+ # in code reviews, test output, and discussion. Grepping those produces
789
+ # false positives that trip the circuit breaker.
790
+ if grep -v '^[[:space:]]*{' "$output_file" 2>/dev/null | \
786
791
  grep -qE '(^Error:|^ERROR:|^error:|\]: error|Error occurred|failed with error|[Ee]xception|Fatal|FATAL)'; then
787
792
  has_errors="true"
788
793
  log_status "WARN" "Errors detected in output"