Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ef02d67
test(kpi): reject corrupt event metric evidence
seonghobae Aug 21, 2026
9835130
fix(kpi): reject corrupt event metric evidence
seonghobae Aug 21, 2026
84c785f
test(kpi): preserve zero latency evidence
seonghobae Aug 21, 2026
e4cbac0
test(kpi): reject impossible HTTP status evidence
seonghobae Aug 21, 2026
e6db2ed
fix(kpi): reject impossible HTTP status evidence
seonghobae Aug 21, 2026
c06675c
test(kpi): reject future and invalid window evidence
seonghobae Aug 21, 2026
a681b68
fix(kpi): fail closed on invalid window authority
seonghobae Aug 21, 2026
6bb224f
test(kpi): reject coercible latency evidence
seonghobae Aug 21, 2026
c5a7ee6
test(kpi): reject coercible status evidence
seonghobae Aug 21, 2026
12a2b80
fix(kpi): require typed status and latency evidence
seonghobae Aug 21, 2026
c39f310
test(kpi): reject out-of-domain threshold authority
seonghobae Aug 21, 2026
16496e9
fix(kpi): validate threshold authority domain
seonghobae Aug 21, 2026
ddafb4d
test(kpi): require canonical event identity
seonghobae Aug 21, 2026
943c43d
fix(kpi): require canonical event identity
seonghobae Aug 21, 2026
e4f578c
test(kpi): cover threshold authority branches
seonghobae Aug 21, 2026
6bb1c04
test(kpi): cover empty event authority
seonghobae Aug 21, 2026
79f2546
test(kpi): require latency for every exchange event
seonghobae Aug 21, 2026
b1eab74
fix(kpi): require complete latency evidence
seonghobae Aug 21, 2026
cbf5248
test(kpi): reject whitespace event authority
seonghobae Aug 21, 2026
ed8bdd4
fix(kpi): reject blank event authority
seonghobae Aug 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 55 additions & 12 deletions scripts/check-kpi.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import { hasDuplicateJsonObjectKeys } from "./normalize-commercial-readiness-evi

const fs = await import("node:fs/promises");
const { existsSync } = await import("node:fs");
const calendarDatePrefixPattern = /^(\d{4}-\d{2}-\d{2})(?:$|[T ])/;

const inputPath = process.argv[2] ?? "exchange-30d.ndjson";
const failureThreshold = Number(process.argv[3] ?? "0.02");
const p95Threshold = Number(process.argv[4] ?? "300");
const requireWindowDays = Number(process.env.NOEMA_KPI_REQUIRE_WINDOW_DAYS);
const requireWindowDaysRaw = process.env.NOEMA_KPI_REQUIRE_WINDOW_DAYS;
const hasWindowRequirement = requireWindowDaysRaw !== undefined;
const requireWindowDays = hasWindowRequirement ? Number(requireWindowDaysRaw) : Number.NaN;

if (!inputPath) {
console.error("Usage: node scripts/check-kpi.mjs [wrangler-tail-ndjson] [failureThreshold] [p95ThresholdMs]");
Expand All @@ -20,13 +23,18 @@ if (!existsSync(inputPath)) {
process.exit(1);
}

if (!Number.isFinite(failureThreshold) || !Number.isFinite(p95Threshold)) {
console.error("Invalid threshold values.");
if (!Number.isFinite(failureThreshold) || failureThreshold < 0 || failureThreshold > 1) {
console.error("KPI failure threshold must be between 0 and 1.");
process.exit(1);
}

if (Number.isFinite(requireWindowDays) && requireWindowDays <= 0) {
console.error("NOEMA_KPI_REQUIRE_WINDOW_DAYS must be a positive number.");
if (!Number.isFinite(p95Threshold) || p95Threshold < 0) {
console.error("KPI p95 threshold must be non-negative and finite.");
process.exit(1);
}

if (hasWindowRequirement && (!Number.isFinite(requireWindowDays) || requireWindowDays <= 0)) {
console.error("NOEMA_KPI_REQUIRE_WINDOW_DAYS must be a positive finite number.");
process.exit(1);
}

Expand Down Expand Up @@ -71,19 +79,47 @@ for (const line of lines) {
}

const route = resolveRoute(record);
const event = record.event || "http_request";
if (route !== "/exchange" || event !== "http_request") continue;
if (route !== "/exchange") continue;
if (typeof record.event !== "string" || record.event.trim().length === 0) {
console.error("KPI exchange record is missing canonical http_request event identity.");
process.exit(1);
}
if (record.event !== "http_request") continue;
Comment on lines +83 to +87

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Behavior change without a CHANGELOG entry

This PR changes the fail-closed exit behavior of the KPI check script but adds no ## Unreleased entry to CHANGELOG.md, which CONTRIBUTING.md and CLAUDE.md require for every behavior change. The diff touches only the script and tests.

Prompt for agents
CONTRIBUTING.md and CLAUDE.md require that CHANGELOG.md's `## Unreleased` section be updated with every behavior change. This PR hardens scripts/check-kpi.mjs to fail closed on corrupt/incomplete exchange evidence and invalid threshold/window configuration, which is a behavior change, but CHANGELOG.md is not modified. Add a short `## Unreleased` bullet (Korean, matching the existing changelog style) describing the new KPI evidence integrity rejections.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


exchanges += 1;

const status = Number(record.status_code || record.status || record.response?.status);
if (Number.isNaN(status) || status >= 400) failures += 1;
const status = record.status_code ?? record.status ?? record.response?.status;
if (typeof status !== "number" || !Number.isInteger(status) || status < 100 || status > 599) {
console.error("Invalid exchange HTTP status in KPI log; expected an integer from 100 through 599.");
process.exit(1);
}
if (status >= 400) failures += 1;

const latency = Number(record.latency_ms || record.latencyMs || record.duration_ms);
if (!Number.isNaN(latency)) latencies.push(latency);
const latency = record.latency_ms ?? record.latencyMs ?? record.duration_ms;
if (latency === undefined || latency === null) {
console.error("KPI exchange latency is required for every canonical http_request event.");
process.exit(1);
}
if (typeof latency !== "number" || !Number.isFinite(latency) || latency < 0) {
console.error("Invalid exchange latency in KPI log; expected a finite non-negative number.");
process.exit(1);
}
Comment on lines +91 to +106

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Missing status/latency now hard-fails the gate

An /exchange http_request record lacking a valid integer status or finite non-negative latency now exits the whole check, where before a missing status counted as a failure and a missing latency was just dropped from the p95 sample. Any legacy log missing these fields would now fail the KPI gate.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

latencies.push(latency);

const ts = resolveTimestampMs(record);
if (Number.isNaN(ts)) {
console.error("Invalid exchange timestamp in KPI log; refusing normalized calendar evidence.");
process.exit(1);
}
if (ts != null) {
if (!Number.isFinite(ts) || ts < 0) {
console.error("Invalid exchange timestamp in KPI log; expected a finite non-negative timestamp.");
process.exit(1);
}
if (ts > Date.now()) {
console.error("Invalid exchange timestamp in KPI log; timestamp cannot be in the future.");
process.exit(1);
}
Comment on lines +119 to +122

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Future-timestamp rejection extends to non-windowed runs

The ts > Date.now() check runs for every exchange record, not only under a window requirement, so the default kpi:check now hard-fails on any timestamp ahead of the checker's clock. Edge-vs-runner clock skew could in principle reject valid evidence, though checks normally run well after collection.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

exchangesWithTimestamp += 1;
minTimestampMs = Math.min(minTimestampMs, ts);
maxTimestampMs = Math.max(maxTimestampMs, ts);
Expand All @@ -99,7 +135,6 @@ latencies.sort((a, b) => a - b);
const p95Index = Math.max(0, Math.ceil((0.95 * latencies.length) - 1));
const p95 = latencies.length ? latencies[p95Index] : null;
const failureRate = failures / exchanges;
const hasWindowRequirement = Number.isFinite(requireWindowDays);
const requiredWindowMs = hasWindowRequirement ? requireWindowDays * 24 * 60 * 60 * 1000 : null;
const exchangeWindowMs = Number.isFinite(minTimestampMs) && Number.isFinite(maxTimestampMs)
? maxTimestampMs - minTimestampMs
Expand Down Expand Up @@ -172,6 +207,14 @@ function resolveTimestampMs(record) {
return candidate;
}
if (typeof candidate === "string") {
const calendarMatch = calendarDatePrefixPattern.exec(candidate);
if (calendarMatch) {
const datePart = calendarMatch[1];
const calendarDate = new Date(`${datePart}T00:00:00.000Z`);
if (Number.isNaN(calendarDate.getTime()) || calendarDate.toISOString().slice(0, 10) !== datePart) {
return Number.NaN;
}
}
const parsed = Date.parse(candidate);
if (!Number.isNaN(parsed)) return parsed;
const numeric = Number(candidate);
Comment on lines 209 to 220

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Some string timestamp formats silently ignored

resolveTimestampMs returns null for string formats that miss the calendar prefix and fail Date.parse/Number (e.g. 2026-02-01Z), silently excluding the record from window evidence rather than rejecting it. Pre-existing and unaffected by this PR.

(Refers to this code)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Expand Down
93 changes: 93 additions & 0 deletions test/check-kpi-event-authority.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { spawnSync } from "node:child_process";
import { describe, expect, it } from "vitest";

function runCheckKpi(logPath: string) {
const env = { ...process.env };
delete env.NOEMA_KPI_REQUIRE_WINDOW_DAYS;
return spawnSync(process.execPath, ["scripts/check-kpi.mjs", logPath, "0.02", "300"], {
cwd: process.cwd(),
encoding: "utf8",
env,
});
}

function expectInvalidEventIdentity(record: Record<string, unknown>) {
const dir = mkdtempSync(join(tmpdir(), "noema-check-kpi-event-"));
try {
const logPath = join(dir, "exchange-30d.ndjson");
writeFileSync(logPath, `${JSON.stringify(record)}\n`);

const result = runCheckKpi(logPath);

expect(result.status).toBe(1);
expect(result.stderr).toContain("exchange record is missing canonical http_request event identity");
expect(result.stdout).toBe("");
} finally {
rmSync(dir, { recursive: true, force: true });
}
}

describe("KPI event authority", () => {
it("rejects an exchange-shaped record that omits the canonical http_request event identity", () => {
expectInvalidEventIdentity({
route: "/exchange",
status_code: 200,
latency_ms: 10,
});
});

it("rejects an empty event identity instead of treating it as a canonical http_request", () => {
expectInvalidEventIdentity({
event: "",
route: "/exchange",
status_code: 200,
latency_ms: 10,
});
});

it("rejects a whitespace-only event identity instead of excluding an exchange observation", () => {
expectInvalidEventIdentity({
event: " \t",
route: "/exchange",
status_code: 500,
latency_ms: 10,
});
});

it("continues to ignore explicitly non-http_request events on the exchange route", () => {
const dir = mkdtempSync(join(tmpdir(), "noema-check-kpi-event-ignore-"));
try {
const logPath = join(dir, "exchange-30d.ndjson");
writeFileSync(logPath, [
JSON.stringify({
event: "workflow_trust",
route: "/exchange",
status_code: 403,
latency_ms: 1,
}),
JSON.stringify({
event: "http_request",
route: "/exchange",
status_code: 200,
latency_ms: 10,
}),
"",
].join("\n"));

const result = runCheckKpi(logPath);

expect(result.status).toBe(0);
expect(result.stderr).toBe("");
expect(JSON.parse(result.stdout)).toMatchObject({
exchange_requests: 1,
exchange_failures: 0,
pass: true,
});
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});
108 changes: 106 additions & 2 deletions test/check-kpi-input-integrity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ import { join } from "node:path";
import { spawnSync } from "node:child_process";
import { describe, expect, it } from "vitest";

function runCheckKpi(logPath: string) {
function runCheckKpi(logPath: string, requireWindowDays?: string) {
const env = { ...process.env };
delete env.NOEMA_KPI_REQUIRE_WINDOW_DAYS;
if (requireWindowDays === undefined) {
delete env.NOEMA_KPI_REQUIRE_WINDOW_DAYS;
} else {
env.NOEMA_KPI_REQUIRE_WINDOW_DAYS = requireWindowDays;
}
return spawnSync(process.execPath, ["scripts/check-kpi.mjs", logPath, "0.02", "300"], {
cwd: process.cwd(),
encoding: "utf8",
Expand Down Expand Up @@ -55,6 +59,106 @@ describe("KPI threshold input integrity", () => {
}
});

it("rejects an impossible calendar timestamp instead of normalizing it into window evidence", () => {
const dir = mkdtempSync(join(tmpdir(), "noema-check-kpi-time-"));
try {
const logPath = join(dir, "exchange-30d.ndjson");
writeFileSync(logPath, [
JSON.stringify({
event: "http_request",
route: "/exchange",
status_code: 200,
latency_ms: 120,
timestamp: "2026-02-01T00:00:00.000Z",
}),
JSON.stringify({
event: "http_request",
route: "/exchange",
status_code: 200,
latency_ms: 140,
timestamp: "2026-02-30",
}),
"",
].join("\n"));

const result = runCheckKpi(logPath, "28");

expect(result.status).toBe(1);
expect(result.stderr).toContain("Invalid exchange timestamp in KPI log");
expect(result.stdout).toBe("");
} finally {
rmSync(dir, { recursive: true, force: true });
}
});

it("rejects negative latency instead of letting corrupt evidence lower p95", () => {
const dir = mkdtempSync(join(tmpdir(), "noema-check-kpi-latency-"));
try {
const logPath = join(dir, "exchange-30d.ndjson");
writeFileSync(logPath, `${JSON.stringify({
event: "http_request",
route: "/exchange",
status_code: 200,
latency_ms: -1,
})}\n`);

const result = runCheckKpi(logPath);

expect(result.status).toBe(1);
expect(result.stderr).toContain("Invalid exchange latency in KPI log");
expect(result.stdout).toBe("");
} finally {
rmSync(dir, { recursive: true, force: true });
}
});

it("rejects string latency instead of coercing untyped evidence into p95", () => {
const dir = mkdtempSync(join(tmpdir(), "noema-check-kpi-latency-type-"));
try {
const logPath = join(dir, "exchange-30d.ndjson");
writeFileSync(logPath, `${JSON.stringify({
event: "http_request",
route: "/exchange",
status_code: 200,
latency_ms: "0",
})}\n`);

const result = runCheckKpi(logPath);

expect(result.status).toBe(1);
expect(result.stderr).toContain("Invalid exchange latency in KPI log");
expect(result.stdout).toBe("");
} finally {
rmSync(dir, { recursive: true, force: true });
}
});

it("preserves a zero-millisecond latency as a real KPI sample", () => {
const dir = mkdtempSync(join(tmpdir(), "noema-check-kpi-zero-latency-"));
try {
const logPath = join(dir, "exchange-30d.ndjson");
writeFileSync(logPath, `${JSON.stringify({
event: "http_request",
route: "/exchange",
status_code: 200,
latency_ms: 0,
})}\n`);

const result = runCheckKpi(logPath);

expect(result.status).toBe(0);
expect(result.stderr).toBe("");
expect(JSON.parse(result.stdout)).toMatchObject({
exchange_requests: 1,
exchange_failures: 0,
exchange_p95_latency_ms: 0,
pass: true,
});
} finally {
rmSync(dir, { recursive: true, force: true });
}
});

it("preserves valid threshold semantics and ignores non-JSON lines", () => {
const dir = mkdtempSync(join(tmpdir(), "noema-check-kpi-"));
try {
Expand Down
46 changes: 46 additions & 0 deletions test/check-kpi-latency-completeness.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { spawnSync } from "node:child_process";
import { describe, expect, it } from "vitest";

function runCheckKpi(logPath: string) {
const env = { ...process.env };
delete env.NOEMA_KPI_REQUIRE_WINDOW_DAYS;
return spawnSync(process.execPath, ["scripts/check-kpi.mjs", logPath, "0.02", "300"], {
cwd: process.cwd(),
encoding: "utf8",
env,
});
}

describe("KPI latency evidence completeness", () => {
it("rejects a canonical exchange event that omits latency instead of shrinking the p95 sample", () => {
const dir = mkdtempSync(join(tmpdir(), "noema-check-kpi-latency-"));
try {
const logPath = join(dir, "exchange-30d.ndjson");
writeFileSync(logPath, [
JSON.stringify({
event: "http_request",
route: "/exchange",
status_code: 200,
latency_ms: 10,
}),
JSON.stringify({
event: "http_request",
route: "/exchange",
status_code: 200,
}),
"",
].join("\n"));

const result = runCheckKpi(logPath);

expect(result.status).toBe(1);
expect(result.stderr).toContain("exchange latency is required");
expect(result.stdout).toBe("");
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});
Loading
Loading