codeharness 0.21.1 → 0.22.1
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/dist/index.js +1231 -124
- package/dist/modules/observability/index.d.ts +101 -29
- package/dist/modules/observability/index.js +240 -7
- package/package.json +1 -1
- package/ralph/ralph.sh +6 -1
|
@@ -96,6 +96,8 @@ interface StaticCoverageState {
|
|
|
96
96
|
readonly lastScanTimestamp: string;
|
|
97
97
|
/** Historical coverage entries */
|
|
98
98
|
readonly history: readonly CoverageHistoryEntry[];
|
|
99
|
+
/** Cached observability gaps from last analysis run (persisted by saveCoverageResult) */
|
|
100
|
+
readonly gaps?: readonly ObservabilityGap[];
|
|
99
101
|
}
|
|
100
102
|
/** Coverage targets configuration */
|
|
101
103
|
interface CoverageTargets {
|
|
@@ -153,6 +155,17 @@ interface RuntimeCoverageState {
|
|
|
153
155
|
/** Whether telemetry was detected at all */
|
|
154
156
|
readonly telemetryDetected: boolean;
|
|
155
157
|
}
|
|
158
|
+
/** Result of the observability coverage gate check */
|
|
159
|
+
interface ObservabilityCoverageGateResult {
|
|
160
|
+
/** Whether the gate passed (all required checks met their targets) */
|
|
161
|
+
readonly passed: boolean;
|
|
162
|
+
/** Static coverage check result */
|
|
163
|
+
readonly staticResult: CoverageTargetResult;
|
|
164
|
+
/** Runtime coverage check result, or null if no runtime data exists */
|
|
165
|
+
readonly runtimeResult: CoverageTargetResult | null;
|
|
166
|
+
/** List of observability gaps for actionable feedback */
|
|
167
|
+
readonly gapSummary: ObservabilityGap[];
|
|
168
|
+
}
|
|
156
169
|
/** Trend comparison between latest and previous coverage entries */
|
|
157
170
|
interface CoverageTrend {
|
|
158
171
|
/** Latest coverage percentage */
|
|
@@ -177,6 +190,50 @@ interface CoverageTargetResult {
|
|
|
177
190
|
/** Gap: target - current (0 if met) */
|
|
178
191
|
readonly gap: number;
|
|
179
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
|
+
}
|
|
180
237
|
|
|
181
238
|
/**
|
|
182
239
|
* Discriminated union Result type for consistent error handling.
|
|
@@ -214,38 +271,15 @@ type Result<T> = Ok<T> | Fail;
|
|
|
214
271
|
*/
|
|
215
272
|
declare function analyze(projectDir: string, config?: AnalyzerConfig): Result<AnalyzerResult>;
|
|
216
273
|
|
|
217
|
-
/**
|
|
218
|
-
* Coverage state persistence — tracks observability coverage in sprint-state.json.
|
|
219
|
-
*
|
|
220
|
-
* Implements Story 1.3: read/write/trend/target comparison for static analysis coverage.
|
|
221
|
-
* Only the `static` section is managed here; `runtime` is deferred to Epic 2.
|
|
222
|
-
*/
|
|
274
|
+
/** Coverage state persistence — tracks observability coverage in sprint-state.json. */
|
|
223
275
|
|
|
224
|
-
/**
|
|
225
|
-
* Save coverage result from an analysis run into sprint-state.json.
|
|
226
|
-
*
|
|
227
|
-
* Updates `observability.static.coveragePercent`, `observability.static.lastScanTimestamp`,
|
|
228
|
-
* and appends to `observability.static.history`. Preserves all other state fields.
|
|
229
|
-
*/
|
|
276
|
+
/** Save coverage result from an analysis run into sprint-state.json. */
|
|
230
277
|
declare function saveCoverageResult(projectDir: string, result: AnalyzerResult): Result<void>;
|
|
231
|
-
/**
|
|
232
|
-
* Read the observability coverage state from sprint-state.json.
|
|
233
|
-
*
|
|
234
|
-
* Returns the typed `ObservabilityCoverageState` from the `observability` key.
|
|
235
|
-
* If no observability section exists, returns default state.
|
|
236
|
-
*/
|
|
278
|
+
/** Read the observability coverage state from sprint-state.json. */
|
|
237
279
|
declare function readCoverageState(projectDir: string): Result<ObservabilityCoverageState>;
|
|
238
|
-
/**
|
|
239
|
-
* Get coverage trend by comparing latest vs previous history entries.
|
|
240
|
-
*/
|
|
280
|
+
/** Get coverage trend by comparing latest vs previous history entries. */
|
|
241
281
|
declare function getCoverageTrend(projectDir: string): Result<CoverageTrend>;
|
|
242
|
-
/**
|
|
243
|
-
* Check current coverage against a target threshold.
|
|
244
|
-
*
|
|
245
|
-
* @param projectDir - Project root directory
|
|
246
|
-
* @param target - Target percentage (default 80%)
|
|
247
|
-
* @returns Structured result indicating whether coverage meets the target
|
|
248
|
-
*/
|
|
282
|
+
/** Check current coverage against a target threshold. */
|
|
249
283
|
declare function checkCoverageTarget(projectDir: string, target?: number): Result<CoverageTargetResult>;
|
|
250
284
|
|
|
251
285
|
/** Per-AC observability gap presence */
|
|
@@ -292,4 +326,42 @@ declare function computeRuntimeCoverage(gapResults: ObservabilityGapResult): Run
|
|
|
292
326
|
*/
|
|
293
327
|
declare function saveRuntimeCoverage(projectDir: string, result: RuntimeCoverageResult): Result<void>;
|
|
294
328
|
|
|
295
|
-
|
|
329
|
+
/**
|
|
330
|
+
* Observability coverage gate — checks if coverage meets targets for commit gating.
|
|
331
|
+
*
|
|
332
|
+
* Implements Story 2.2: reads cached coverage from sprint-state.json and compares
|
|
333
|
+
* against targets. Does NOT re-run Semgrep — uses cached results for performance.
|
|
334
|
+
* Static coverage is always required; runtime is only checked when data exists.
|
|
335
|
+
*/
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Check observability coverage against targets for commit gating.
|
|
339
|
+
*
|
|
340
|
+
* Reads static and runtime coverage from sprint-state.json. Static coverage
|
|
341
|
+
* is always checked. Runtime coverage is only checked when runtime data exists
|
|
342
|
+
* in the state file (i.e., after at least one verification run).
|
|
343
|
+
*
|
|
344
|
+
* @param projectDir - Project root directory
|
|
345
|
+
* @param overrides - Optional target overrides
|
|
346
|
+
* @returns Gate result with pass/fail, per-dimension results, and gap summary
|
|
347
|
+
*/
|
|
348
|
+
declare function checkObservabilityCoverageGate(projectDir: string, overrides?: {
|
|
349
|
+
staticTarget?: number;
|
|
350
|
+
runtimeTarget?: number;
|
|
351
|
+
}): Result<ObservabilityCoverageGateResult>;
|
|
352
|
+
|
|
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 };
|
|
@@ -195,11 +195,13 @@ function saveCoverageResult(projectDir, result) {
|
|
|
195
195
|
const updatedStatic = {
|
|
196
196
|
coveragePercent: result.summary.coveragePercent,
|
|
197
197
|
lastScanTimestamp: timestamp,
|
|
198
|
-
history: trimmedHistory
|
|
198
|
+
history: trimmedHistory,
|
|
199
|
+
gaps: result.gaps.map(({ file, line, type, description, severity }) => ({ file, line, type, description, severity }))
|
|
199
200
|
};
|
|
200
201
|
const updatedObservability = {
|
|
201
202
|
static: updatedStatic,
|
|
202
|
-
targets: existing.targets
|
|
203
|
+
targets: existing.targets,
|
|
204
|
+
...existing.runtime ? { runtime: existing.runtime } : {}
|
|
203
205
|
};
|
|
204
206
|
const updatedState = {
|
|
205
207
|
...state,
|
|
@@ -275,18 +277,45 @@ function extractCoverageState(state) {
|
|
|
275
277
|
}
|
|
276
278
|
const staticSection = obs.static;
|
|
277
279
|
const targets = obs.targets;
|
|
278
|
-
|
|
280
|
+
const runtimeSection = obs.runtime;
|
|
281
|
+
const runtime = runtimeSection && typeof runtimeSection.coveragePercent === "number" ? {
|
|
282
|
+
coveragePercent: runtimeSection.coveragePercent,
|
|
283
|
+
lastValidationTimestamp: typeof runtimeSection.lastValidationTimestamp === "string" ? runtimeSection.lastValidationTimestamp : "",
|
|
284
|
+
modulesWithTelemetry: typeof runtimeSection.modulesWithTelemetry === "number" ? runtimeSection.modulesWithTelemetry : 0,
|
|
285
|
+
totalModules: typeof runtimeSection.totalModules === "number" ? runtimeSection.totalModules : 0,
|
|
286
|
+
telemetryDetected: typeof runtimeSection.telemetryDetected === "boolean" ? runtimeSection.telemetryDetected : false
|
|
287
|
+
} : void 0;
|
|
288
|
+
const parsedGaps = parseGapArray(staticSection?.gaps);
|
|
289
|
+
const result = {
|
|
279
290
|
static: {
|
|
280
291
|
coveragePercent: typeof staticSection?.coveragePercent === "number" ? staticSection.coveragePercent : 0,
|
|
281
292
|
lastScanTimestamp: typeof staticSection?.lastScanTimestamp === "string" ? staticSection.lastScanTimestamp : "",
|
|
282
293
|
history: Array.isArray(staticSection?.history) ? staticSection.history.filter(
|
|
283
294
|
(entry) => typeof entry === "object" && entry !== null && typeof entry.coveragePercent === "number" && typeof entry.timestamp === "string"
|
|
284
|
-
) : []
|
|
295
|
+
) : [],
|
|
296
|
+
...parsedGaps.length > 0 ? { gaps: parsedGaps } : {}
|
|
285
297
|
},
|
|
286
298
|
targets: {
|
|
287
|
-
staticTarget: typeof targets?.staticTarget === "number" ? targets.staticTarget : DEFAULT_STATIC_TARGET
|
|
288
|
-
|
|
299
|
+
staticTarget: typeof targets?.staticTarget === "number" ? targets.staticTarget : DEFAULT_STATIC_TARGET,
|
|
300
|
+
...typeof targets?.runtimeTarget === "number" ? { runtimeTarget: targets.runtimeTarget } : {}
|
|
301
|
+
},
|
|
302
|
+
...runtime ? { runtime } : {}
|
|
289
303
|
};
|
|
304
|
+
return result;
|
|
305
|
+
}
|
|
306
|
+
function parseGapArray(raw) {
|
|
307
|
+
if (!Array.isArray(raw)) return [];
|
|
308
|
+
return raw.filter((g) => {
|
|
309
|
+
if (typeof g !== "object" || g === null) return false;
|
|
310
|
+
const r = g;
|
|
311
|
+
return typeof r.file === "string" && typeof r.line === "number" && typeof r.type === "string" && typeof r.description === "string";
|
|
312
|
+
}).map((g) => ({
|
|
313
|
+
file: g.file,
|
|
314
|
+
line: g.line,
|
|
315
|
+
type: g.type,
|
|
316
|
+
description: g.description,
|
|
317
|
+
severity: g.severity === "error" || g.severity === "warning" ? g.severity : "info"
|
|
318
|
+
}));
|
|
290
319
|
}
|
|
291
320
|
|
|
292
321
|
// src/modules/observability/runtime-coverage.ts
|
|
@@ -355,12 +384,216 @@ function saveRuntimeCoverage(projectDir, result) {
|
|
|
355
384
|
return fail(`Failed to write ${STATE_FILE2}: ${msg}`);
|
|
356
385
|
}
|
|
357
386
|
}
|
|
387
|
+
|
|
388
|
+
// src/modules/observability/coverage-gate.ts
|
|
389
|
+
var DEFAULT_STATIC_TARGET2 = 80;
|
|
390
|
+
var DEFAULT_RUNTIME_TARGET = 60;
|
|
391
|
+
function checkObservabilityCoverageGate(projectDir, overrides) {
|
|
392
|
+
if (!projectDir || typeof projectDir !== "string") {
|
|
393
|
+
return fail("projectDir is required and must be a non-empty string");
|
|
394
|
+
}
|
|
395
|
+
const stateResult = readCoverageState(projectDir);
|
|
396
|
+
if (!stateResult.success) {
|
|
397
|
+
return fail(stateResult.error);
|
|
398
|
+
}
|
|
399
|
+
const state = stateResult.data;
|
|
400
|
+
const staticTarget = overrides?.staticTarget ?? state.targets.staticTarget ?? DEFAULT_STATIC_TARGET2;
|
|
401
|
+
const runtimeTarget = overrides?.runtimeTarget ?? state.targets.runtimeTarget ?? DEFAULT_RUNTIME_TARGET;
|
|
402
|
+
const staticCurrent = state.static.coveragePercent;
|
|
403
|
+
const staticMet = staticCurrent >= staticTarget;
|
|
404
|
+
const staticResult = {
|
|
405
|
+
met: staticMet,
|
|
406
|
+
current: staticCurrent,
|
|
407
|
+
target: staticTarget,
|
|
408
|
+
gap: staticMet ? 0 : staticTarget - staticCurrent
|
|
409
|
+
};
|
|
410
|
+
let runtimeResult = null;
|
|
411
|
+
if (state.runtime) {
|
|
412
|
+
const runtimeCurrent = state.runtime.coveragePercent;
|
|
413
|
+
const runtimeMet = runtimeCurrent >= runtimeTarget;
|
|
414
|
+
runtimeResult = {
|
|
415
|
+
met: runtimeMet,
|
|
416
|
+
current: runtimeCurrent,
|
|
417
|
+
target: runtimeTarget,
|
|
418
|
+
gap: runtimeMet ? 0 : runtimeTarget - runtimeCurrent
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
const passed = staticResult.met && (runtimeResult === null || runtimeResult.met);
|
|
422
|
+
const gapSummary = state.static.gaps ? [...state.static.gaps] : [];
|
|
423
|
+
return ok({ passed, staticResult, runtimeResult, gapSummary });
|
|
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
|
+
}
|
|
358
586
|
export {
|
|
359
587
|
analyze,
|
|
588
|
+
checkBackendHealth,
|
|
360
589
|
checkCoverageTarget,
|
|
590
|
+
checkObservabilityCoverageGate,
|
|
361
591
|
computeRuntimeCoverage,
|
|
362
592
|
getCoverageTrend,
|
|
593
|
+
mapEventsToModules,
|
|
594
|
+
queryTelemetryEvents,
|
|
363
595
|
readCoverageState,
|
|
364
596
|
saveCoverageResult,
|
|
365
|
-
saveRuntimeCoverage
|
|
597
|
+
saveRuntimeCoverage,
|
|
598
|
+
validateRuntime
|
|
366
599
|
};
|
package/package.json
CHANGED
package/ralph/ralph.sh
CHANGED
|
@@ -782,7 +782,12 @@ execute_iteration() {
|
|
|
782
782
|
fi
|
|
783
783
|
|
|
784
784
|
local has_errors="false"
|
|
785
|
-
|
|
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"
|