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
b78df2f
test(pilot): reject placeholder evidence references
seonghobae Aug 23, 2026
8c70d91
fix(pilot): reject placeholder evidence references
seonghobae Aug 23, 2026
8333069
test(pilot): cover every documented sample marker
seonghobae Aug 23, 2026
81de5d3
test(pilot): reject placeholder trace evidence
seonghobae Aug 23, 2026
26fc9a5
fix(pilot): reject placeholder trace evidence
seonghobae Aug 23, 2026
e0265ba
test(pilot): reject ambiguous duplicate evidence authority
seonghobae Aug 23, 2026
2d604b1
fix(pilot): reject duplicate readiness authority
seonghobae Aug 23, 2026
1c6ed10
test(pilot): reject malformed duplicate authority labels
seonghobae Aug 23, 2026
4ba47cd
fix(pilot): count malformed duplicate authority labels
seonghobae Aug 23, 2026
262a2fb
test(pilot): reject loopback production URLs
seonghobae Aug 23, 2026
a186033
fix(pilot): reject loopback production URLs
seonghobae Aug 23, 2026
b007f96
test(pilot): reject absolute local DNS names
seonghobae Aug 23, 2026
ea09394
fix(pilot): normalize absolute local DNS hosts
seonghobae Aug 23, 2026
e620161
test(pilot): reject IPv4-mapped loopback URL
seonghobae Aug 23, 2026
bcf4c2f
fix(pilot): reject IPv4-mapped loopback URL
seonghobae Aug 23, 2026
ddaa550
test(pilot): reject local support-channel evidence
seonghobae Aug 23, 2026
9bcaba7
fix(pilot): reject local support-channel evidence
seonghobae Aug 23, 2026
61a5263
test(pilot): reject local-only production listener identities
seonghobae Aug 23, 2026
dae9b00
fix(pilot): reject local-only production listener identities
seonghobae Aug 23, 2026
cec399a
merge main into pilot readiness hardening
seonghobae Aug 23, 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
56 changes: 51 additions & 5 deletions scripts/lib/pilot-readiness.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,49 @@ function metricValue(entry, name) {
return match ? Number(match[1]) : null;
}

function metricCount(entry, name) {
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return [...entry.matchAll(new RegExp(`^-\\s*\`?${escaped}\`?\\s*:`, "gm"))].length;
}

function fieldValue(entry, label) {
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const match = entry.match(new RegExp(`^-\\s*${escaped}:\\s*(.+)\\s*$`, "m"));
return match ? match[1].trim() : "";
}

function fieldCount(entry, label) {
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return [...entry.matchAll(new RegExp(`^-\\s*${escaped}\\s*:`, "gm"))].length;
}
Comment on lines 24 to +33

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: fieldValue and fieldCount treat pre-colon whitespace differently

fieldValue requires the colon to immediately follow the label, while fieldCount (pilot-readiness.mjs) allows whitespace before it. A line like - 고객명 : Acme is counted but read as empty. This only fails closed and cannot forge a duplicate, so it is not a bug, but the two regexes drifting apart invites future confusion.

Open in Devin Review

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


function hasCheckedLine(entry, labelPattern) {
return new RegExp(`^-\\s*\\[x\\]\\s*${labelPattern}\\s*$`, "m").test(entry);
}

function isLocalOnlyHostname(host) {
const normalized = host.startsWith("[") && host.endsWith("]")
? host.slice(1, -1)
: host;
if (normalized === "::" || normalized === "::1" || normalized === "0.0.0.0") return true;
if (/^::ffff:7f[0-9a-f]{2}:[0-9a-f]{1,4}$/i.test(normalized)) return true;
return /^127(?:\.\d{1,3}){3}$/.test(normalized);
}
Comment on lines +39 to +46

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: IPv4-mapped loopback match depends on URL hex serialization

The IPv4-mapped loopback regex matches the bracket-stripped host, relying on the URL parser serializing [::ffff:127.0.0.2] to hex ::ffff:7f00:2 rather than dotted-quad. WHATWG URL does emit hex pieces, so the tested cases pass and the regex covers all of 127.0.0.0/8. Correct, but the coverage rests on serializer behavior.

Open in Devin Review

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


function isUsableProductionUrl(value) {
if (!value) return false;
try {
const url = new URL(value.replace(/`/g, ""));
const host = url.hostname.toLowerCase();
const canonicalHost = host.endsWith(".") ? host.slice(0, -1) : host;
return url.protocol === "https:"
&& url.username === ""
&& url.password === ""
&& host !== "localhost"
&& host !== "127.0.0.1"
&& !host.endsWith(".local")
&& !host.includes("example");
&& canonicalHost !== "localhost"
&& !canonicalHost.endsWith(".localhost")
&& !isLocalOnlyHostname(canonicalHost)
&& !canonicalHost.endsWith(".local")
&& !canonicalHost.includes("example");
} catch {
return false;
}
Expand All @@ -46,11 +67,19 @@ function isUsableProductionUrl(value) {
function isUsableSupportChannel(value) {
const normalized = value.toLowerCase();
return normalized.length > 0
&& !normalized.includes("@noema.local")
&& !normalized.includes(".local")
&& !normalized.includes("example")
&& !normalized.includes("localhost");
}

function isUsableEvidenceReference(value) {
const normalized = value.toLowerCase();
return normalized.length > 0
&& !normalized.includes("example")
&& !normalized.includes("localhost")
&& !normalized.includes(".local");
}
Comment on lines 67 to +81

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: Coarse substring matching on sample markers

isUsableEvidenceReference and isUsableSupportChannel reject any value containing .local, example, or localhost as a substring. Real values like a path with .locale or a host like exampletech.com are rejected. This fail-closed broadening matches the documented buyer boundary, but the substring test is coarse and can reject genuine evidence in rare cases.

Open in Devin Review

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


function evaluatePilotEntry(entry) {
const customerName = fieldValue(entry, "고객명");
const noemaUrl = fieldValue(entry, "NOEMA URL");
Expand All @@ -65,8 +94,22 @@ function evaluatePilotEntry(entry) {
const p95 = metricValue(entry, "exchange_p95_latency_ms");
const onboardingDateStatus = dateStatus(onboardingDate);
const handoverDateStatus = dateStatus(handoverDate);
const duplicateAuthorities = [
["고객명", fieldCount(entry, "고객명")],
["NOEMA URL", fieldCount(entry, "NOEMA URL")],
["지원 채널 합의", fieldCount(entry, "지원 채널 합의")],
["온보딩 완료일", fieldCount(entry, "온보딩 완료일")],
["운영 전환 승인일", fieldCount(entry, "운영 전환 승인일")],
["증빙 출처", fieldCount(entry, "증빙 출처") + fieldCount(entry, "evidence_source_kind")],
["계약/매출 증빙 경로", fieldCount(entry, "계약/매출 증빙 경로")],
["분석 데이터 경로", fieldCount(entry, "분석 데이터 경로")],
["trace_id 샘플", fieldCount(entry, "trace_id 샘플")],
["exchange_failure_rate", metricCount(entry, "exchange_failure_rate")],
["exchange_p95_latency_ms", metricCount(entry, "exchange_p95_latency_ms")],
].filter(([, count]) => count > 1);

const failures = [];
for (const [label] of duplicateAuthorities) failures.push(`${label} must appear exactly once`);
Comment on lines 111 to +112

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

These new fail-closed readiness rejection rules change behavior, but no ## Unreleased bullet was added to CHANGELOG.md. The contributor rule requires a changelog entry for every behavior change, and the existing top pilot bullet covers only date/time authority, not placeholder, local, duplicate, or URL evidence.

Prompt for agents
CONTRIBUTING.md and CLAUDE.md require that CHANGELOG.md's `## Unreleased` section be updated with every behavior change. This PR adds new pilot-readiness validation behavior in scripts/lib/pilot-readiness.mjs (rejecting placeholder/example/localhost/.local evidence references, loopback / unspecified-listener / .localhost production URLs, broadened .local support-channel rejection, and duplicate authoritative fields) but adds no corresponding CHANGELOG entry. Add a Korean `## Unreleased` bullet describing this fail-closed hardening, consistent with the surrounding entries.
Open in Devin Review

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

if (!customerName) failures.push("고객명 required");
if (!isUsableProductionUrl(noemaUrl)) failures.push("NOEMA URL must be a non-example HTTPS production URL");
if (!isUsableSupportChannel(supportChannel)) failures.push("지원 채널 합의 must be a real non-local channel");
Expand All @@ -80,9 +123,12 @@ function evaluatePilotEntry(entry) {
if (failureRate === null || failureRate > 0.02) failures.push("exchange_failure_rate must be <= 0.02");
if (p95 === null || p95 >= 300) failures.push("exchange_p95_latency_ms must be < 300");
if (!evidencePath) failures.push("분석 데이터 경로 required");
else if (!isUsableEvidenceReference(evidencePath)) failures.push("분석 데이터 경로 must be a non-example evidence reference");
if (!traceId) failures.push("trace_id 샘플 required");
else if (!isUsableEvidenceReference(traceId)) failures.push("trace_id 샘플 must be a non-example evidence reference");
if (evidenceSourceKind !== "production") failures.push("증빙 출처 must be production");
if (!contractEvidencePath) failures.push("계약/매출 증빙 경로 required");
else if (!isUsableEvidenceReference(contractEvidencePath)) failures.push("계약/매출 증빙 경로 must be a non-example evidence reference");

return {
customerName,
Expand Down
150 changes: 150 additions & 0 deletions test/pilot-readiness-placeholder-evidence.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { describe, expect, it } from "vitest";
import { evaluatePilotReadinessText } from "../scripts/lib/pilot-readiness.mjs";

function completedPilot(contractEvidencePath: string, evidencePath: string, traceId = "trace-2f4c9a77-1e8a-4f3b-9b9a-a8c1e6f0b5d1") {
return `# 파일럿 온보딩 진행 기록

## 항목 1
- 고객명: Acme Security
- NOEMA URL: https://noema.acme-security.com/exchange
- 지원 채널 합의: Slack acme-noema-ops
- 증빙 출처: production
- 계약/매출 증빙 경로: ${contractEvidencePath}
- 분석 데이터 경로: ${evidencePath}
- exchange_failure_rate: 0
- exchange_p95_latency_ms: 157
- [x] 실패율 <= 0.02
- [x] p95 < 300
- [x] 운영 이관 승인
- 운영 전환 승인일: 2026-06-30
- 온보딩 완료일: 2026-07-01
- trace_id 샘플: ${traceId}
`;
}

describe("pilot readiness evidence references", () => {
it.each([
["example/contracts/demo-paid-pilot.pdf", "artifacts/example/noema-kpi-evidence.json"],
["localhost/contracts/demo-paid-pilot.pdf", "artifacts/localhost/noema-kpi-evidence.json"],
["fixtures.local/contracts/demo-paid-pilot.pdf", "artifacts/fixtures.local/noema-kpi-evidence.json"],
])("rejects documented sample markers in commercial evidence references", (contractEvidencePath, evidencePath) => {
const result = evaluatePilotReadinessText(completedPilot(contractEvidencePath, evidencePath));

expect(result.passed).toBe(false);
expect(result.entries[0].failures).toContain("계약/매출 증빙 경로 must be a non-example evidence reference");
expect(result.entries[0].failures).toContain("분석 데이터 경로 must be a non-example evidence reference");
});

it("rejects an example trace key as completion evidence", () => {
const result = evaluatePilotReadinessText(completedPilot(
"contracts/acme-paid-pilot.pdf",
"artifacts/saleable-readiness/noema-kpi-evidence.json",
"example-trace-id",
));

expect(result.passed).toBe(false);
expect(result.entries[0].failures).toContain("trace_id 샘플 must be a non-example evidence reference");
});

it.each([
"https://127.0.0.2/exchange",
"https://[::1]/exchange",
"https://[::ffff:127.0.0.2]/exchange",
"https://tenant.localhost/exchange",
"https://0.0.0.0/exchange",
"https://[::]/exchange",
])("rejects local-only or non-routable listener identities as production URLs (%s)", (url) => {
const text = completedPilot(
"contracts/acme-paid-pilot.pdf",
"artifacts/saleable-readiness/noema-kpi-evidence.json",
).replace(
"- NOEMA URL: https://noema.acme-security.com/exchange",
`- NOEMA URL: ${url}`,
);

const result = evaluatePilotReadinessText(text);

expect(result.passed).toBe(false);
expect(result.entries[0].failures).toContain("NOEMA URL must be a non-example HTTPS production URL");
});

it.each([
"https://localhost./exchange",
"https://fixtures.local./exchange",
])("rejects absolute local DNS names as production URLs (%s)", (url) => {
const text = completedPilot(
"contracts/acme-paid-pilot.pdf",
"artifacts/saleable-readiness/noema-kpi-evidence.json",
).replace(
"- NOEMA URL: https://noema.acme-security.com/exchange",
`- NOEMA URL: ${url}`,
);

const result = evaluatePilotReadinessText(text);

expect(result.passed).toBe(false);
expect(result.entries[0].failures).toContain("NOEMA URL must be a non-example HTTPS production URL");
});

it("rejects a .local support channel as sample evidence", () => {
const text = completedPilot(
"contracts/acme-paid-pilot.pdf",
"artifacts/saleable-readiness/noema-kpi-evidence.json",
).replace(
"- 지원 채널 합의: Slack acme-noema-ops",
"- 지원 채널 합의: support@acme.local",
);

const result = evaluatePilotReadinessText(text);

expect(result.passed).toBe(false);
expect(result.entries[0].failures).toContain("지원 채널 합의 must be a real non-local channel");
});

it("rejects duplicate authoritative evidence instead of trusting the first matching line", () => {
const text = completedPilot(
"contracts/acme-paid-pilot.pdf",
"artifacts/saleable-readiness/noema-kpi-evidence.json",
)
.replace(
"- 증빙 출처: production",
"- 증빙 출처: production\n- evidence_source_kind: fixture",
)
.replace(
"- 계약/매출 증빙 경로: contracts/acme-paid-pilot.pdf",
"- 계약/매출 증빙 경로: contracts/acme-paid-pilot.pdf\n- 계약/매출 증빙 경로: example/contracts/forged.pdf",
)
.replace(
"- exchange_failure_rate: 0",
"- exchange_failure_rate: 0\n- exchange_failure_rate: 0.9",
);

const result = evaluatePilotReadinessText(text);

expect(result.passed).toBe(false);
expect(result.entries[0].failures).toContain("증빙 출처 must appear exactly once");
expect(result.entries[0].failures).toContain("계약/매출 증빙 경로 must appear exactly once");
expect(result.entries[0].failures).toContain("exchange_failure_rate must appear exactly once");
});

it("rejects malformed or blank duplicate authority instead of ignoring the second label", () => {
const text = completedPilot(
"contracts/acme-paid-pilot.pdf",
"artifacts/saleable-readiness/noema-kpi-evidence.json",
)
.replace(
"- 계약/매출 증빙 경로: contracts/acme-paid-pilot.pdf",
"- 계약/매출 증빙 경로: contracts/acme-paid-pilot.pdf\n- 계약/매출 증빙 경로:",
)
.replace(
"- exchange_failure_rate: 0",
"- exchange_failure_rate: 0\n- exchange_failure_rate: forged",
);

const result = evaluatePilotReadinessText(text);

expect(result.passed).toBe(false);
expect(result.entries[0].failures).toContain("계약/매출 증빙 경로 must appear exactly once");
expect(result.entries[0].failures).toContain("exchange_failure_rate must appear exactly once");
});
});
Loading