diff --git a/CHANGELOG.md b/CHANGELOG.md index ecea4f7d6..dfad0a6a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## Unreleased +- 판매 가능성 파일럿 완료 증거의 시간 권한을 fail-closed로 강화한다. `운영 전환 승인일`과 `온보딩 완료일`은 실제 존재하는 달력 날짜이면서 검증 시점보다 미래가 아니어야 하며, 아직 발생하지 않은 완료·이관 날짜가 saleable-readiness 증거를 제조하지 못하게 한다. - Actions runner-assignment 운영 증거를 fail-closed로 강화한다. `observed_at`은 exact canonical UTC instant만 허용하고 `Date.parse()`가 정규화하는 비정규·존재하지 않는 시각은 거부하며, audit report는 심볼릭 링크가 포함된 parent 경로를 거부한다. 테스트용 filesystem seam도 parent 검증과 atomic write가 동일한 I/O authority를 사용하도록 결합해 검증 경계와 쓰기 경계가 서로 다른 파일시스템을 보지 않게 한다. - 중앙 `.github` reviewer workflow의 immutable OIDC source commit 이동을 Noema runtime trust에 즉시 반영한다. reviewed `noema-review.yml` blob이 동일하더라도 GitHub `job_workflow_sha`는 source commit에 결합되므로 `ALLOWED_WORKFLOW_SHA`와 executable regression을 현재 중앙 protected source에 정확히 맞춰 stale trust를 실패-폐쇄한다. - 외부 스케줄러 운영 증거의 시간 권한을 fail-closed로 강화한다. canonical UTC `scheduled_at`/`started_at`이 검증 시점보다 미래인 retained evidence는 `scheduler_timestamp_future`로 거부해 아직 실행되지 않은 hourly run이 운영·인수 준비 증거를 제조하지 못하게 한다. diff --git a/scripts/lib/pilot-readiness.mjs b/scripts/lib/pilot-readiness.mjs index 35b3f2a18..0b23cf7b6 100644 --- a/scripts/lib/pilot-readiness.mjs +++ b/scripts/lib/pilot-readiness.mjs @@ -1,10 +1,13 @@ const dateOnlyRegex = /^\d{4}-\d{2}-\d{2}$/; -function hasValidDate(value) { +function dateStatus(value) { const normalized = String(value ?? "").trim(); - if (!dateOnlyRegex.test(normalized)) return false; + if (!dateOnlyRegex.test(normalized)) return "invalid"; const parsed = new Date(`${normalized}T00:00:00.000Z`); - return !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === normalized; + if (Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== normalized) { + return "invalid"; + } + return parsed.getTime() > Date.now() ? "future" : "valid"; } function metricValue(entry, name) { @@ -60,13 +63,17 @@ function evaluatePilotEntry(entry) { const traceId = fieldValue(entry, "trace_id 샘플"); const failureRate = metricValue(entry, "exchange_failure_rate"); const p95 = metricValue(entry, "exchange_p95_latency_ms"); + const onboardingDateStatus = dateStatus(onboardingDate); + const handoverDateStatus = dateStatus(handoverDate); const failures = []; 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"); - if (!hasValidDate(onboardingDate)) failures.push("온보딩 완료일 required"); - if (!hasValidDate(handoverDate)) failures.push("운영 전환 승인일 required"); + if (onboardingDateStatus === "invalid") failures.push("온보딩 완료일 required"); + if (onboardingDateStatus === "future") failures.push("온보딩 완료일 must not be in the future"); + if (handoverDateStatus === "invalid") failures.push("운영 전환 승인일 required"); + if (handoverDateStatus === "future") failures.push("운영 전환 승인일 must not be in the future"); if (!hasCheckedLine(entry, "운영 이관 승인")) failures.push("운영 이관 승인 required"); if (!hasCheckedLine(entry, "(?:p95 <= 300|p95 < 300)")) failures.push("p95 threshold checkbox required"); if (!hasCheckedLine(entry, "실패율 <= 0\\.02")) failures.push("failure-rate threshold checkbox required"); diff --git a/test/pilot-readiness-future-date.test.ts b/test/pilot-readiness-future-date.test.ts new file mode 100644 index 000000000..c7abb026f --- /dev/null +++ b/test/pilot-readiness-future-date.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest"; +import { evaluatePilotReadinessText } from "../scripts/lib/pilot-readiness.mjs"; + +function completedPilotWithDates(handoverDate: string, onboardingDate: string) { + return `# 파일럿 온보딩 진행 기록 + +## 항목 1 +- 고객명: Acme Security +- NOEMA URL: https://noema.acme-security.com/exchange +- 지원 채널 합의: Slack acme-noema-ops +- 증빙 출처: production +- 계약/매출 증빙 경로: contracts/acme-paid-pilot.pdf +- 분석 데이터 경로: artifacts/saleable-readiness/noema-kpi-evidence.json +- exchange_failure_rate: 0 +- exchange_p95_latency_ms: 157 +- [x] 실패율 <= 0.02 +- [x] p95 < 300 +- [x] 운영 이관 승인 +- 운영 전환 승인일: ${handoverDate} +- 온보딩 완료일: ${onboardingDate} +- trace_id 샘플: trace-2f4c9a77-1e8a-4f3b-9b9a-a8c1e6f0b5d1 +`; +} + +describe("pilot readiness completion chronology", () => { + it("rejects future completion dates instead of granting saleable-readiness authority", () => { + const result = evaluatePilotReadinessText(completedPilotWithDates("9999-12-30", "9999-12-31")); + + expect(result.passed).toBe(false); + expect(result.entries[0].failures).toContain("운영 전환 승인일 must not be in the future"); + expect(result.entries[0].failures).toContain("온보딩 완료일 must not be in the future"); + }); +});