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

@ -0,0 +1,128 @@
param(
[string]$ContainerName = 'vignette-dev-db',
[string]$BackupDirectory = '',
[string]$DatabaseName = '',
[string]$DatabaseUser = ''
)
$ErrorActionPreference = 'Stop'
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
$OutputEncoding = [System.Text.UTF8Encoding]::new($false)
$repo = Split-Path -Parent $PSScriptRoot
if ([string]::IsNullOrWhiteSpace($BackupDirectory)) {
$workspace = Split-Path -Parent $repo
$BackupDirectory = Join-Path $workspace 'vignette-backups'
}
function Invoke-DockerChecked([string[]]$Arguments, [string]$FailureMessage) {
& docker @Arguments
if ($LASTEXITCODE -ne 0) {
throw ("{0} (exit={1})" -f $FailureMessage, $LASTEXITCODE)
}
}
function Get-ContainerEnvValue([string]$Key, [string]$Fallback) {
$envLines = @(& docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' $ContainerName 2>$null)
if ($LASTEXITCODE -ne 0) { return $Fallback }
$prefix = "${Key}="
foreach ($line in $envLines) {
$text = "$line"
if ($text.StartsWith($prefix, [StringComparison]::Ordinal)) {
return $text.Substring($prefix.Length)
}
}
return $Fallback
}
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
throw 'docker CLI를 찾을 수 없어 DB 백업을 시작하지 않았다.'
}
$containerState = & docker inspect -f '{{.State.Status}}' $ContainerName 2>$null
if ($LASTEXITCODE -ne 0 -or "$containerState".Trim() -ne 'running') {
throw "DB container '$ContainerName'이 running 상태가 아니어서 백업을 시작하지 않았다."
}
if ([string]::IsNullOrWhiteSpace($DatabaseName)) {
$DatabaseName = Get-ContainerEnvValue 'POSTGRES_DB' 'vignette'
}
if ([string]::IsNullOrWhiteSpace($DatabaseUser)) {
$DatabaseUser = Get-ContainerEnvValue 'POSTGRES_USER' 'vignette_owner'
}
Invoke-DockerChecked @('exec', $ContainerName, 'pg_isready', '-U', $DatabaseUser, '-d', $DatabaseName) `
'PostgreSQL readiness 확인 실패. 불완전한 백업을 만들지 않았다.' | Out-Null
New-Item -ItemType Directory -Force -Path $BackupDirectory | Out-Null
$resolvedBackupDirectory = (Resolve-Path -LiteralPath $BackupDirectory).Path
$timestamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd-HHmmss')
$safeContainer = $ContainerName -replace '[^A-Za-z0-9_.-]', '_'
$fileName = "${safeContainer}-${DatabaseName}-${timestamp}Z.dump"
$finalPath = Join-Path $resolvedBackupDirectory $fileName
$partialPath = "${finalPath}.partial"
$manifestPath = "${finalPath}.json"
$manifestPartialPath = "${manifestPath}.partial"
$containerTempPath = "/tmp/${safeContainer}-${timestamp}-$PID.dump"
if ((Test-Path -LiteralPath $finalPath) -or (Test-Path -LiteralPath $manifestPath)) {
throw "동일 이름의 백업이 이미 존재한다: $finalPath"
}
$backupSucceeded = $false
try {
Invoke-DockerChecked @(
'exec', $ContainerName,
'pg_dump', '-U', $DatabaseUser, '-d', $DatabaseName,
'--format=custom', '--no-owner', '--no-privileges',
'--file', $containerTempPath
) 'pg_dump 실패. 기존 DB에는 변경을 가하지 않았다.' | Out-Null
Invoke-DockerChecked @('exec', $ContainerName, 'pg_restore', '--list', $containerTempPath) `
'pg_restore TOC 검증 실패. 백업을 게시하지 않았다.' | Out-Null
Invoke-DockerChecked @('cp', "${ContainerName}:${containerTempPath}", $partialPath) `
'검증된 dump를 로컬 임시 파일로 복사하지 못했다.' | Out-Null
$file = Get-Item -LiteralPath $partialPath
if ($file.Length -le 0) {
throw '로컬 dump 크기가 0이어서 백업을 게시하지 않았다.'
}
$sha256 = (Get-FileHash -LiteralPath $partialPath -Algorithm SHA256).Hash.ToLowerInvariant()
Move-Item -LiteralPath $partialPath -Destination $finalPath
$containerId = (& docker inspect -f '{{.Id}}' $ContainerName).Trim()
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($containerId)) {
throw '백업 후 container identity를 고정하지 못했다.'
}
$manifest = [ordered]@{
schema_version = 1
created_at_utc = (Get-Date).ToUniversalTime().ToString('o')
container_name = $ContainerName
container_id = $containerId
database_name = $DatabaseName
format = 'postgres-custom'
dump_file = $fileName
size_bytes = (Get-Item -LiteralPath $finalPath).Length
sha256 = $sha256
verified_with = 'pg_restore --list'
}
$json = $manifest | ConvertTo-Json -Depth 3
[IO.File]::WriteAllText($manifestPartialPath, $json + [Environment]::NewLine, [Text.UTF8Encoding]::new($false))
Move-Item -LiteralPath $manifestPartialPath -Destination $manifestPath
$backupSucceeded = $true
Write-Output ("Backup : {0}" -f $finalPath)
Write-Output ("SHA-256: {0}" -f $sha256)
Write-Output ("Manifest: {0}" -f $manifestPath)
} finally {
& docker exec $ContainerName rm -f $containerTempPath 1>$null 2>$null
if (-not $backupSucceeded) {
if (Test-Path -LiteralPath $partialPath) {
Remove-Item -LiteralPath $partialPath -Force
}
if (Test-Path -LiteralPath $manifestPartialPath) {
Remove-Item -LiteralPath $manifestPartialPath -Force
}
}
}