Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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이 운영·인수 준비 증거를 제조하지 못하게 한다.
Expand Down
17 changes: 12 additions & 5 deletions scripts/lib/pilot-readiness.mjs
Original file line number Diff line number Diff line change
@@ -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";
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Comment thread
seonghobae marked this conversation as resolved.
}

function metricValue(entry, name) {
Expand Down Expand Up @@ -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");
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
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");
Expand Down
33 changes: 33 additions & 0 deletions test/pilot-readiness-future-date.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
Loading