codeharness 0.21.0 → 0.22.0
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
CHANGED
|
@@ -1941,7 +1941,7 @@ async function scaffoldDocs(opts) {
|
|
|
1941
1941
|
}
|
|
1942
1942
|
|
|
1943
1943
|
// src/modules/infra/init-project.ts
|
|
1944
|
-
var HARNESS_VERSION = true ? "0.
|
|
1944
|
+
var HARNESS_VERSION = true ? "0.22.0" : "0.0.0-dev";
|
|
1945
1945
|
function failResult(opts, error) {
|
|
1946
1946
|
return {
|
|
1947
1947
|
status: "fail",
|
|
@@ -5178,11 +5178,134 @@ import { join as join16 } from "path";
|
|
|
5178
5178
|
// src/modules/observability/coverage.ts
|
|
5179
5179
|
import { readFileSync as readFileSync18, writeFileSync as writeFileSync12, renameSync as renameSync2, existsSync as existsSync21 } from "fs";
|
|
5180
5180
|
import { join as join17 } from "path";
|
|
5181
|
+
var STATE_FILE2 = "sprint-state.json";
|
|
5182
|
+
var DEFAULT_STATIC_TARGET = 80;
|
|
5183
|
+
function defaultCoverageState() {
|
|
5184
|
+
return {
|
|
5185
|
+
static: {
|
|
5186
|
+
coveragePercent: 0,
|
|
5187
|
+
lastScanTimestamp: "",
|
|
5188
|
+
history: []
|
|
5189
|
+
},
|
|
5190
|
+
targets: {
|
|
5191
|
+
staticTarget: DEFAULT_STATIC_TARGET
|
|
5192
|
+
}
|
|
5193
|
+
};
|
|
5194
|
+
}
|
|
5195
|
+
function readStateFile(projectDir) {
|
|
5196
|
+
const fp = join17(projectDir, STATE_FILE2);
|
|
5197
|
+
if (!existsSync21(fp)) {
|
|
5198
|
+
return ok2({});
|
|
5199
|
+
}
|
|
5200
|
+
try {
|
|
5201
|
+
const raw = readFileSync18(fp, "utf-8");
|
|
5202
|
+
const parsed = JSON.parse(raw);
|
|
5203
|
+
return ok2(parsed);
|
|
5204
|
+
} catch (err) {
|
|
5205
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5206
|
+
return fail2(`Failed to read ${STATE_FILE2}: ${msg}`);
|
|
5207
|
+
}
|
|
5208
|
+
}
|
|
5209
|
+
function readCoverageState(projectDir) {
|
|
5210
|
+
if (!projectDir || typeof projectDir !== "string") {
|
|
5211
|
+
return fail2("projectDir is required and must be a non-empty string");
|
|
5212
|
+
}
|
|
5213
|
+
const stateResult = readStateFile(projectDir);
|
|
5214
|
+
if (!stateResult.success) {
|
|
5215
|
+
return fail2(stateResult.error);
|
|
5216
|
+
}
|
|
5217
|
+
return ok2(extractCoverageState(stateResult.data));
|
|
5218
|
+
}
|
|
5219
|
+
function extractCoverageState(state) {
|
|
5220
|
+
const obs = state.observability;
|
|
5221
|
+
if (!obs) {
|
|
5222
|
+
return defaultCoverageState();
|
|
5223
|
+
}
|
|
5224
|
+
const staticSection = obs.static;
|
|
5225
|
+
const targets = obs.targets;
|
|
5226
|
+
const runtimeSection = obs.runtime;
|
|
5227
|
+
const runtime = runtimeSection && typeof runtimeSection.coveragePercent === "number" ? {
|
|
5228
|
+
coveragePercent: runtimeSection.coveragePercent,
|
|
5229
|
+
lastValidationTimestamp: typeof runtimeSection.lastValidationTimestamp === "string" ? runtimeSection.lastValidationTimestamp : "",
|
|
5230
|
+
modulesWithTelemetry: typeof runtimeSection.modulesWithTelemetry === "number" ? runtimeSection.modulesWithTelemetry : 0,
|
|
5231
|
+
totalModules: typeof runtimeSection.totalModules === "number" ? runtimeSection.totalModules : 0,
|
|
5232
|
+
telemetryDetected: typeof runtimeSection.telemetryDetected === "boolean" ? runtimeSection.telemetryDetected : false
|
|
5233
|
+
} : void 0;
|
|
5234
|
+
const parsedGaps = parseGapArray(staticSection?.gaps);
|
|
5235
|
+
const result = {
|
|
5236
|
+
static: {
|
|
5237
|
+
coveragePercent: typeof staticSection?.coveragePercent === "number" ? staticSection.coveragePercent : 0,
|
|
5238
|
+
lastScanTimestamp: typeof staticSection?.lastScanTimestamp === "string" ? staticSection.lastScanTimestamp : "",
|
|
5239
|
+
history: Array.isArray(staticSection?.history) ? staticSection.history.filter(
|
|
5240
|
+
(entry) => typeof entry === "object" && entry !== null && typeof entry.coveragePercent === "number" && typeof entry.timestamp === "string"
|
|
5241
|
+
) : [],
|
|
5242
|
+
...parsedGaps.length > 0 ? { gaps: parsedGaps } : {}
|
|
5243
|
+
},
|
|
5244
|
+
targets: {
|
|
5245
|
+
staticTarget: typeof targets?.staticTarget === "number" ? targets.staticTarget : DEFAULT_STATIC_TARGET,
|
|
5246
|
+
...typeof targets?.runtimeTarget === "number" ? { runtimeTarget: targets.runtimeTarget } : {}
|
|
5247
|
+
},
|
|
5248
|
+
...runtime ? { runtime } : {}
|
|
5249
|
+
};
|
|
5250
|
+
return result;
|
|
5251
|
+
}
|
|
5252
|
+
function parseGapArray(raw) {
|
|
5253
|
+
if (!Array.isArray(raw)) return [];
|
|
5254
|
+
return raw.filter((g) => {
|
|
5255
|
+
if (typeof g !== "object" || g === null) return false;
|
|
5256
|
+
const r = g;
|
|
5257
|
+
return typeof r.file === "string" && typeof r.line === "number" && typeof r.type === "string" && typeof r.description === "string";
|
|
5258
|
+
}).map((g) => ({
|
|
5259
|
+
file: g.file,
|
|
5260
|
+
line: g.line,
|
|
5261
|
+
type: g.type,
|
|
5262
|
+
description: g.description,
|
|
5263
|
+
severity: g.severity === "error" || g.severity === "warning" ? g.severity : "info"
|
|
5264
|
+
}));
|
|
5265
|
+
}
|
|
5181
5266
|
|
|
5182
5267
|
// src/modules/observability/runtime-coverage.ts
|
|
5183
5268
|
import { readFileSync as readFileSync19, writeFileSync as writeFileSync13, renameSync as renameSync3, existsSync as existsSync22 } from "fs";
|
|
5184
5269
|
import { join as join18 } from "path";
|
|
5185
5270
|
|
|
5271
|
+
// src/modules/observability/coverage-gate.ts
|
|
5272
|
+
var DEFAULT_STATIC_TARGET2 = 80;
|
|
5273
|
+
var DEFAULT_RUNTIME_TARGET = 60;
|
|
5274
|
+
function checkObservabilityCoverageGate(projectDir, overrides) {
|
|
5275
|
+
if (!projectDir || typeof projectDir !== "string") {
|
|
5276
|
+
return fail2("projectDir is required and must be a non-empty string");
|
|
5277
|
+
}
|
|
5278
|
+
const stateResult = readCoverageState(projectDir);
|
|
5279
|
+
if (!stateResult.success) {
|
|
5280
|
+
return fail2(stateResult.error);
|
|
5281
|
+
}
|
|
5282
|
+
const state = stateResult.data;
|
|
5283
|
+
const staticTarget = overrides?.staticTarget ?? state.targets.staticTarget ?? DEFAULT_STATIC_TARGET2;
|
|
5284
|
+
const runtimeTarget = overrides?.runtimeTarget ?? state.targets.runtimeTarget ?? DEFAULT_RUNTIME_TARGET;
|
|
5285
|
+
const staticCurrent = state.static.coveragePercent;
|
|
5286
|
+
const staticMet = staticCurrent >= staticTarget;
|
|
5287
|
+
const staticResult = {
|
|
5288
|
+
met: staticMet,
|
|
5289
|
+
current: staticCurrent,
|
|
5290
|
+
target: staticTarget,
|
|
5291
|
+
gap: staticMet ? 0 : staticTarget - staticCurrent
|
|
5292
|
+
};
|
|
5293
|
+
let runtimeResult = null;
|
|
5294
|
+
if (state.runtime) {
|
|
5295
|
+
const runtimeCurrent = state.runtime.coveragePercent;
|
|
5296
|
+
const runtimeMet = runtimeCurrent >= runtimeTarget;
|
|
5297
|
+
runtimeResult = {
|
|
5298
|
+
met: runtimeMet,
|
|
5299
|
+
current: runtimeCurrent,
|
|
5300
|
+
target: runtimeTarget,
|
|
5301
|
+
gap: runtimeMet ? 0 : runtimeTarget - runtimeCurrent
|
|
5302
|
+
};
|
|
5303
|
+
}
|
|
5304
|
+
const passed = staticResult.met && (runtimeResult === null || runtimeResult.met);
|
|
5305
|
+
const gapSummary = state.static.gaps ? [...state.static.gaps] : [];
|
|
5306
|
+
return ok2({ passed, staticResult, runtimeResult, gapSummary });
|
|
5307
|
+
}
|
|
5308
|
+
|
|
5186
5309
|
// src/modules/verify/browser.ts
|
|
5187
5310
|
import { execFileSync as execFileSync9 } from "child_process";
|
|
5188
5311
|
import { existsSync as existsSync23, readFileSync as readFileSync20 } from "fs";
|
|
@@ -10693,8 +10816,106 @@ function registerProgressCommand(program) {
|
|
|
10693
10816
|
});
|
|
10694
10817
|
}
|
|
10695
10818
|
|
|
10819
|
+
// src/commands/observability-gate.ts
|
|
10820
|
+
function registerObservabilityGateCommand(program) {
|
|
10821
|
+
program.command("observability-gate").description("Check observability coverage against targets (commit gate)").option("--json", "Machine-readable JSON output").option("--min-static <percent>", "Override static coverage target").option("--min-runtime <percent>", "Override runtime coverage target").action((opts, cmd) => {
|
|
10822
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
10823
|
+
const isJson = opts.json === true || globalOpts.json === true;
|
|
10824
|
+
const root = process.cwd();
|
|
10825
|
+
const overrides = {};
|
|
10826
|
+
if (opts.minStatic !== void 0) {
|
|
10827
|
+
const parsed = parseInt(opts.minStatic, 10);
|
|
10828
|
+
if (isNaN(parsed) || parsed < 0 || parsed > 100) {
|
|
10829
|
+
if (isJson) {
|
|
10830
|
+
jsonOutput({ status: "error", message: "--min-static must be a number between 0 and 100" });
|
|
10831
|
+
} else {
|
|
10832
|
+
fail("--min-static must be a number between 0 and 100");
|
|
10833
|
+
}
|
|
10834
|
+
process.exitCode = 1;
|
|
10835
|
+
return;
|
|
10836
|
+
}
|
|
10837
|
+
overrides.staticTarget = parsed;
|
|
10838
|
+
}
|
|
10839
|
+
if (opts.minRuntime !== void 0) {
|
|
10840
|
+
const parsed = parseInt(opts.minRuntime, 10);
|
|
10841
|
+
if (isNaN(parsed) || parsed < 0 || parsed > 100) {
|
|
10842
|
+
if (isJson) {
|
|
10843
|
+
jsonOutput({ status: "error", message: "--min-runtime must be a number between 0 and 100" });
|
|
10844
|
+
} else {
|
|
10845
|
+
fail("--min-runtime must be a number between 0 and 100");
|
|
10846
|
+
}
|
|
10847
|
+
process.exitCode = 1;
|
|
10848
|
+
return;
|
|
10849
|
+
}
|
|
10850
|
+
overrides.runtimeTarget = parsed;
|
|
10851
|
+
}
|
|
10852
|
+
const result = checkObservabilityCoverageGate(root, overrides);
|
|
10853
|
+
if (!result.success) {
|
|
10854
|
+
if (isJson) {
|
|
10855
|
+
jsonOutput({ status: "error", message: result.error });
|
|
10856
|
+
} else {
|
|
10857
|
+
fail(`Observability gate error: ${result.error}`);
|
|
10858
|
+
}
|
|
10859
|
+
process.exitCode = 1;
|
|
10860
|
+
return;
|
|
10861
|
+
}
|
|
10862
|
+
const gate = result.data;
|
|
10863
|
+
if (isJson) {
|
|
10864
|
+
jsonOutput({
|
|
10865
|
+
status: gate.passed ? "pass" : "fail",
|
|
10866
|
+
passed: gate.passed,
|
|
10867
|
+
static: {
|
|
10868
|
+
current: gate.staticResult.current,
|
|
10869
|
+
target: gate.staticResult.target,
|
|
10870
|
+
met: gate.staticResult.met,
|
|
10871
|
+
gap: gate.staticResult.gap
|
|
10872
|
+
},
|
|
10873
|
+
runtime: gate.runtimeResult ? {
|
|
10874
|
+
current: gate.runtimeResult.current,
|
|
10875
|
+
target: gate.runtimeResult.target,
|
|
10876
|
+
met: gate.runtimeResult.met,
|
|
10877
|
+
gap: gate.runtimeResult.gap
|
|
10878
|
+
} : null,
|
|
10879
|
+
gaps: gate.gapSummary.map((g) => ({
|
|
10880
|
+
file: g.file,
|
|
10881
|
+
line: g.line,
|
|
10882
|
+
type: g.type,
|
|
10883
|
+
description: g.description
|
|
10884
|
+
}))
|
|
10885
|
+
});
|
|
10886
|
+
} else {
|
|
10887
|
+
const staticLine = `Static: ${gate.staticResult.current}% / ${gate.staticResult.target}% target`;
|
|
10888
|
+
if (gate.passed) {
|
|
10889
|
+
ok(`Observability gate passed. ${staticLine}`);
|
|
10890
|
+
if (gate.runtimeResult) {
|
|
10891
|
+
ok(`Runtime: ${gate.runtimeResult.current}% / ${gate.runtimeResult.target}% target`);
|
|
10892
|
+
}
|
|
10893
|
+
} else {
|
|
10894
|
+
fail(`Observability gate failed. ${staticLine}`);
|
|
10895
|
+
if (gate.runtimeResult && !gate.runtimeResult.met) {
|
|
10896
|
+
fail(`Runtime: ${gate.runtimeResult.current}% / ${gate.runtimeResult.target}% target`);
|
|
10897
|
+
}
|
|
10898
|
+
if (gate.gapSummary.length > 0) {
|
|
10899
|
+
fail("Gaps:");
|
|
10900
|
+
const shown = gate.gapSummary.slice(0, 5);
|
|
10901
|
+
for (const g of shown) {
|
|
10902
|
+
fail(` ${g.file}:${g.line} \u2014 ${g.description}`);
|
|
10903
|
+
}
|
|
10904
|
+
if (gate.gapSummary.length > 5) {
|
|
10905
|
+
fail(` ... and ${gate.gapSummary.length - 5} more.`);
|
|
10906
|
+
}
|
|
10907
|
+
}
|
|
10908
|
+
fail("Add logging to flagged functions. Run: codeharness observability-gate for details.");
|
|
10909
|
+
}
|
|
10910
|
+
}
|
|
10911
|
+
if (!gate.passed) {
|
|
10912
|
+
process.exitCode = 1;
|
|
10913
|
+
}
|
|
10914
|
+
});
|
|
10915
|
+
}
|
|
10916
|
+
|
|
10696
10917
|
// src/index.ts
|
|
10697
|
-
var VERSION = true ? "0.
|
|
10918
|
+
var VERSION = true ? "0.22.0" : "0.0.0-dev";
|
|
10698
10919
|
function createProgram() {
|
|
10699
10920
|
const program = new Command();
|
|
10700
10921
|
program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");
|
|
@@ -10719,6 +10940,7 @@ function createProgram() {
|
|
|
10719
10940
|
registerValidateStateCommand(program);
|
|
10720
10941
|
registerValidateCommand(program);
|
|
10721
10942
|
registerProgressCommand(program);
|
|
10943
|
+
registerObservabilityGateCommand(program);
|
|
10722
10944
|
return program;
|
|
10723
10945
|
}
|
|
10724
10946
|
if (!process.env["VITEST"]) {
|
|
@@ -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 */
|
|
@@ -214,38 +227,15 @@ type Result<T> = Ok<T> | Fail;
|
|
|
214
227
|
*/
|
|
215
228
|
declare function analyze(projectDir: string, config?: AnalyzerConfig): Result<AnalyzerResult>;
|
|
216
229
|
|
|
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
|
-
*/
|
|
230
|
+
/** Coverage state persistence — tracks observability coverage in sprint-state.json. */
|
|
223
231
|
|
|
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
|
-
*/
|
|
232
|
+
/** Save coverage result from an analysis run into sprint-state.json. */
|
|
230
233
|
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
|
-
*/
|
|
234
|
+
/** Read the observability coverage state from sprint-state.json. */
|
|
237
235
|
declare function readCoverageState(projectDir: string): Result<ObservabilityCoverageState>;
|
|
238
|
-
/**
|
|
239
|
-
* Get coverage trend by comparing latest vs previous history entries.
|
|
240
|
-
*/
|
|
236
|
+
/** Get coverage trend by comparing latest vs previous history entries. */
|
|
241
237
|
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
|
-
*/
|
|
238
|
+
/** Check current coverage against a target threshold. */
|
|
249
239
|
declare function checkCoverageTarget(projectDir: string, target?: number): Result<CoverageTargetResult>;
|
|
250
240
|
|
|
251
241
|
/** Per-AC observability gap presence */
|
|
@@ -292,4 +282,28 @@ declare function computeRuntimeCoverage(gapResults: ObservabilityGapResult): Run
|
|
|
292
282
|
*/
|
|
293
283
|
declare function saveRuntimeCoverage(projectDir: string, result: RuntimeCoverageResult): Result<void>;
|
|
294
284
|
|
|
295
|
-
|
|
285
|
+
/**
|
|
286
|
+
* Observability coverage gate — checks if coverage meets targets for commit gating.
|
|
287
|
+
*
|
|
288
|
+
* Implements Story 2.2: reads cached coverage from sprint-state.json and compares
|
|
289
|
+
* against targets. Does NOT re-run Semgrep — uses cached results for performance.
|
|
290
|
+
* Static coverage is always required; runtime is only checked when data exists.
|
|
291
|
+
*/
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Check observability coverage against targets for commit gating.
|
|
295
|
+
*
|
|
296
|
+
* Reads static and runtime coverage from sprint-state.json. Static coverage
|
|
297
|
+
* is always checked. Runtime coverage is only checked when runtime data exists
|
|
298
|
+
* in the state file (i.e., after at least one verification run).
|
|
299
|
+
*
|
|
300
|
+
* @param projectDir - Project root directory
|
|
301
|
+
* @param overrides - Optional target overrides
|
|
302
|
+
* @returns Gate result with pass/fail, per-dimension results, and gap summary
|
|
303
|
+
*/
|
|
304
|
+
declare function checkObservabilityCoverageGate(projectDir: string, overrides?: {
|
|
305
|
+
staticTarget?: number;
|
|
306
|
+
runtimeTarget?: number;
|
|
307
|
+
}): Result<ObservabilityCoverageGateResult>;
|
|
308
|
+
|
|
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 };
|
|
@@ -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,9 +384,48 @@ 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
|
+
}
|
|
358
425
|
export {
|
|
359
426
|
analyze,
|
|
360
427
|
checkCoverageTarget,
|
|
428
|
+
checkObservabilityCoverageGate,
|
|
361
429
|
computeRuntimeCoverage,
|
|
362
430
|
getCoverageTrend,
|
|
363
431
|
readCoverageState,
|
package/package.json
CHANGED
package/ralph/bridge.sh
CHANGED
|
@@ -99,9 +99,9 @@ fi
|
|
|
99
99
|
|
|
100
100
|
# ─── Sprint Status Parsing ────────────────────────────────────────────────
|
|
101
101
|
|
|
102
|
-
# Parse sprint status YAML into
|
|
102
|
+
# Parse sprint status YAML into a newline-delimited key=value store
|
|
103
103
|
# Maps story slug (e.g., "1-1-login-page") to status
|
|
104
|
-
|
|
104
|
+
SPRINT_STATUSES=""
|
|
105
105
|
|
|
106
106
|
parse_sprint_status() {
|
|
107
107
|
local file="$1"
|
|
@@ -133,7 +133,8 @@ parse_sprint_status() {
|
|
|
133
133
|
# Extract story number from slug: "1-1-login-page" -> "1.1"
|
|
134
134
|
if [[ "$key" =~ ^([0-9]+)-([0-9]+) ]]; then
|
|
135
135
|
local story_id="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}"
|
|
136
|
-
SPRINT_STATUSES
|
|
136
|
+
SPRINT_STATUSES="${SPRINT_STATUSES}${story_id}=${value}
|
|
137
|
+
"
|
|
137
138
|
fi
|
|
138
139
|
fi
|
|
139
140
|
fi
|
|
@@ -181,8 +182,10 @@ parse_epics() {
|
|
|
181
182
|
|
|
182
183
|
# Determine status from sprint status or default to pending
|
|
183
184
|
local status="pending"
|
|
184
|
-
|
|
185
|
-
|
|
185
|
+
local _sprint_val
|
|
186
|
+
_sprint_val=$(echo "$SPRINT_STATUSES" | grep "^${current_story_id}=" | head -1 | cut -d= -f2-)
|
|
187
|
+
if [[ -n "$_sprint_val" ]]; then
|
|
188
|
+
status=$(map_status "$_sprint_val")
|
|
186
189
|
fi
|
|
187
190
|
|
|
188
191
|
# Clean up description
|