G0~G8 성과·동맹 측정 OS 작업 일괄 고정

8월 7일까지 워킹트리에만 남아 있던 미커밋 작업을 커밋한다. 여러 사본
폴더(worktree·clone)에 흩어져 있던 중간 스냅샷을 정리하기 전에 원본을
git 이력으로 고정하는 것이 목적이다.

- contracts/routes/services: measurement, outcome_trajectory, rupture_repair,
  deliberate_practice, calibration_transfer, supervision_research,
  multimodal_alliance, continuous_improvement 계열 신규 모듈과 테스트
- infra/db/init: 07~16 마이그레이션(측정 기반~calibration transfer 실행)
- apps/web: 세션 리뷰 카드·관리 화면·E2E 스펙 추가
- docs/ops: G0~G8 라이브 통합·배포·롤백 증거 문서와 evidence JSON/PNG
- scripts: smoke·ledger·릴리스 에이전트·NAS 프리뷰 운영 스크립트

engine.public 로그 .bak과 apps/web/test-results 산출물은 커밋에서 제외했다.
This commit is contained in:
Yun Chan 2026-08-08 01:30:53 +09:00
parent 93dd8f82d7
commit 16e791e044
390 changed files with 243188 additions and 499 deletions

View file

@ -20,7 +20,17 @@ from typing import Any
REPO_ROOT = Path(__file__).resolve().parents[1]
REQUIRED_ENV_KEYS = (
INTERNAL_TOKEN_ENV_KEYS = (
"VIGNETTE_RUPTURE_INTERNAL_TOKEN",
"VIGNETTE_PRACTICE_INTERNAL_TOKEN",
"VIGNETTE_CALIBRATION_TRANSFER_INTERNAL_TOKEN",
"VIGNETTE_SUPERVISION_RESEARCH_INTERNAL_TOKEN",
"VIGNETTE_MULTIMODAL_ALLIANCE_INTERNAL_TOKEN",
"VIGNETTE_CONTINUOUS_IMPROVEMENT_INTERNAL_TOKEN",
)
MIN_INTERNAL_TOKEN_LENGTH = 32
ENGINE_GATEWAY_SECRET_ENV_KEY = "ENGINE_GATEWAY_SHARED_SECRET"
COMPOSE_REQUIRED_ENV_KEYS = (
"ENVIRONMENT",
"POSTGRES_PASSWORD",
"APP_DB_PASSWORD",
@ -28,7 +38,15 @@ REQUIRED_ENV_KEYS = (
"SESSION_SECRET",
"OAUTH_GOOGLE_CLIENT_ID",
"OAUTH_GOOGLE_CLIENT_SECRET",
)
) + INTERNAL_TOKEN_ENV_KEYS
DIRECT_RUNTIME_REQUIRED_ENV_KEYS = (
"ENVIRONMENT",
"DATABASE_URL",
"OPENAI_API_KEY",
"SESSION_SECRET",
"OAUTH_GOOGLE_CLIENT_ID",
"OAUTH_GOOGLE_CLIENT_SECRET",
) + INTERNAL_TOKEN_ENV_KEYS
PLACEHOLDER_VALUES = {
"",
"change-me",
@ -78,6 +96,7 @@ def check_pinned_requirements() -> Check:
paths = [
REPO_ROOT / "apps" / "api" / "requirements.txt",
REPO_ROOT / "apps" / "api" / "requirements-rag.txt",
REPO_ROOT / "apps" / "api" / "engine_gateway" / "requirements.txt",
]
offenders: list[str] = []
for path in paths:
@ -162,17 +181,59 @@ def _parse_env_file(path: Path) -> dict[str, str]:
return values
def check_env_file(path: Path, *, allow_placeholder_secrets: bool) -> Check:
def check_env_file(
path: Path,
*,
allow_placeholder_secrets: bool,
deployment_profile: str = "compose",
) -> Check:
if not path.exists():
return Check("env_file", False, f"{_rel(path)} missing")
values = _parse_env_file(path)
errors: list[str] = []
for key in REQUIRED_ENV_KEYS:
required_keys = (
DIRECT_RUNTIME_REQUIRED_ENV_KEYS
if deployment_profile == "direct-runtime"
else COMPOSE_REQUIRED_ENV_KEYS
)
for key in required_keys:
value = values.get(key, "")
if key not in values:
errors.append(f"{key} missing")
elif not allow_placeholder_secrets and value in PLACEHOLDER_VALUES:
errors.append(f"{key} is placeholder or empty")
if not allow_placeholder_secrets:
gateway_secret = values.get(ENGINE_GATEWAY_SECRET_ENV_KEY, "")
if gateway_secret:
if len(gateway_secret) < MIN_INTERNAL_TOKEN_LENGTH:
errors.append(
f"{ENGINE_GATEWAY_SECRET_ENV_KEY} must contain at least "
f"{MIN_INTERNAL_TOKEN_LENGTH} characters"
)
if gateway_secret.lower().startswith(
("change-me", "replace-with", "dummy", "example")
):
errors.append(
f"{ENGINE_GATEWAY_SECRET_ENV_KEY} is placeholder or empty"
)
token_values: dict[str, list[str]] = {}
for key in INTERNAL_TOKEN_ENV_KEYS:
value = values.get(key, "")
if value and len(value) < MIN_INTERNAL_TOKEN_LENGTH:
errors.append(
f"{key} must contain at least {MIN_INTERNAL_TOKEN_LENGTH} characters"
)
if value.lower().startswith(("change-me", "replace-with", "dummy", "example")):
errors.append(f"{key} is placeholder or empty")
if value:
token_values.setdefault(value, []).append(key)
for duplicate_keys in token_values.values():
if len(duplicate_keys) > 1:
errors.append(
"internal tokens must be independently revocable: "
+ ", ".join(duplicate_keys)
+ " share one value"
)
engine_mode = values.get("ENGINE_MODE", "claude_cli")
if engine_mode not in VALID_ENGINE_MODES:
errors.append(f"ENGINE_MODE invalid: {engine_mode}")
@ -182,7 +243,11 @@ def check_env_file(path: Path, *, allow_placeholder_secrets: bool) -> Check:
errors.append(f"{key}=true is forbidden outside dev")
if errors:
return Check("env_file", False, "; ".join(errors))
return Check("env_file", True, f"{_rel(path)} has required deployment keys")
return Check(
"env_file",
True,
f"{_rel(path)} has required {deployment_profile} deployment keys",
)
async def check_database(database_url: str, *, require_app_role: bool) -> Check:
@ -212,6 +277,14 @@ async def check_database(database_url: str, *, require_app_role: bool) -> Check:
to_regclass('app.sessions') IS NOT NULL AS has_sessions,
to_regclass('app.turns') IS NOT NULL AS has_turns,
to_regclass('app.session_review_status') IS NOT NULL AS has_session_review_status,
to_regclass('app.measurement_event') IS NOT NULL AS has_measurement_event,
to_regclass('app.outcome_trajectory_revision') IS NOT NULL AS has_outcome_trajectory,
to_regclass('app.rupture_episode') IS NOT NULL AS has_rupture_episode,
to_regclass('app.practice_prescription') IS NOT NULL AS has_practice_prescription,
to_regclass('app.calibration_prediction_history') IS NOT NULL AS has_calibration_history,
to_regclass('app.supervision_attention_snapshot') IS NOT NULL AS has_supervision_snapshot,
to_regclass('app.multimodal_consent_snapshot') IS NOT NULL AS has_multimodal_consent,
to_regclass('app.ci_content_pipeline') IS NOT NULL AS has_ci_content_pipeline,
EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'app'
@ -287,6 +360,14 @@ async def check_database(database_url: str, *, require_app_role: bool) -> Check:
"has_sessions",
"has_turns",
"has_session_review_status",
"has_measurement_event",
"has_outcome_trajectory",
"has_rupture_episode",
"has_practice_prescription",
"has_calibration_history",
"has_supervision_snapshot",
"has_multimodal_consent",
"has_ci_content_pipeline",
"has_persona_triggers",
"has_turn_voice_metadata_columns",
"has_session_review_worksheet_columns",
@ -332,6 +413,15 @@ def build_parser() -> argparse.ArgumentParser:
action="store_true",
help="Allow example placeholder secrets when validating .env.example.",
)
parser.add_argument(
"--deployment-profile",
choices=("compose", "direct-runtime"),
default="compose",
help=(
"Validate compose packaging secrets or the direct Windows public "
"runtime's DATABASE_URL-based configuration."
),
)
parser.add_argument(
"--database-url",
default=os.getenv("DATABASE_URL", ""),
@ -357,12 +447,21 @@ async def main_async(argv: list[str]) -> int:
check_live_coach_kb(),
]
if args.env_file:
env_path = Path(args.env_file)
results.append(
check_env_file(
Path(args.env_file),
env_path,
allow_placeholder_secrets=args.allow_placeholder_secrets,
deployment_profile=args.deployment_profile,
)
)
if (
args.deployment_profile == "direct-runtime"
and not args.database_url
and not args.skip_db
and env_path.exists()
):
args.database_url = _parse_env_file(env_path).get("DATABASE_URL", "")
if args.database_url and not args.skip_db:
results.append(
await check_database(