test: 다이어그램 드리프트 게이트 신설, 재생성 절차를 실행 가능하게
감사에서 나온 결론은 "그림은 지금 정확하지만 정확함을 지킬 장치가 없다" 였다. 코드 유래 리터럴 18개가 전부 코드와 일치하고 deliver 는 바이트 결정적인데, 드리프트를 막는 게이트·테스트·훅이 0개였고 문서에 적힌 재생성 절차는 그대로는 실행되지 않았다 — `archify` 실행 파일은 PATH 에 없고 실제로는 저장소 밖 전역 스킬을 node 로 불러야 한다. 다음 사람은 첫 명령에서 막혔을 것이다. scripts/rebuild-diagrams.ps1 validate -> deliver -> visual-check -> manifest scripts/update_diagram_manifest.py 수신증의 단일 작성자 docs/diagrams/manifest.json spec/html sha256 + generator 버전 tests/test_diagram_sync.py 게이트 14건 게이트는 브라우저도 Node 도 archify 도 없이 돈다. 산출물이 바이트 결정적이라 수신증 대조만으로 충분하고, 그래서 폐쇄망 CI 에서도 돈다. 두 사고 시나리오를 실제로 만들어 차단을 확인했다 — HTML 의 단어 하나를 고치면 html sha 가, spec 의 태그 하나를 고치면 spec sha 가 어긋나 verify 가 실패한다. 노드 id 가 HTML 에 data-node-id 로 박히므로 의미적 연결도 함께 본다. git 위생 두 건도 같이 정리했다. *.visual-check.json 이 개발 머신 절대 경로와 크롬 설치 경로를 평문으로 담은 채 미추적·비ignore 상태였다(24개) — 납품 저장소가 removable media 로 건너가는 구조라 한 번 커밋되면 제거가 어렵다. 그리고 15,000줄 생성 HTML 을 텍스트로 머지하려 드는 것을 막았다. 재생성해도 4종 sha256 이 전부 불변이라 바이트 결정성을 다시 확인했다. 감사에서 나온 나머지 부채(한글 폰트 미번들, 영어 뷰어 UI, architecture 좌표 하드코딩, lifecycle 손튜닝 8개소, 콘솔 CSP 비호환)는 PROJECT_MAP 부채 표와 다이어그램 절에 등재했다. 폰트와 UI 는 archify 쪽 한계라 spec 으로는 못 고친다. 검증: ruff clean, pytest 600 passed (게이트 +14). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BStHFrjQj2VypecSG9ex1V
This commit is contained in:
parent
d45018c4d0
commit
93a4a82bff
8 changed files with 402 additions and 4 deletions
7
.gitattributes
vendored
7
.gitattributes
vendored
|
|
@ -19,3 +19,10 @@
|
|||
*.webp binary
|
||||
*.ico binary
|
||||
*.pdf binary
|
||||
|
||||
# 생성 산출물 — 소스는 같은 폴더의 spec JSON 이다.
|
||||
#
|
||||
# 15,000줄짜리 생성 HTML 이라 텍스트 diff 는 리뷰에 쓸모가 없고(최장 라인 41,844자),
|
||||
# 두 브랜치가 각각 재생성하면 손으로 해소할 수 없는 충돌이 난다. 정답은 항상
|
||||
# "재생성"이므로 머지를 텍스트로 시도하지 않게 해 도구가 그 사실을 강제하게 한다.
|
||||
docs/diagrams/*.html linguist-generated=true -diff merge=binary
|
||||
|
|
|
|||
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -45,3 +45,10 @@ Thumbs.db
|
|||
|
||||
# PowerShell module-analysis cache written into cwd by some automation
|
||||
Microsoft/
|
||||
|
||||
# 다이어그램 검증 사이드카 (scripts/rebuild-diagrams.ps1 이 재생성한다)
|
||||
#
|
||||
# visual-check 산출물은 브라우저 증거이지 납품물이 아니다. 게다가 *.visual-check.json
|
||||
# 은 개발 머신의 절대 경로와 크롬 설치 경로를 평문으로 담는다 - 폐쇄망 납품 저장소가
|
||||
# removable media 로 건너가는 구조라 이력에 한 번 들어가면 제거가 어렵다.
|
||||
docs/diagrams/*.visual-check.*
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# Project INDEX
|
||||
|
||||
- 2026-09-19 · 다이어그램 4종으로 확장 — `system-architecture`(폐쇄망 경계·SSRF 가드 앞 호출 게이트), `operator-review-workflow`(4레인, 비상 차단 시 심사 계속 경로), `submission-lifecycle`(분석 실패·보류가 분석으로 복귀하는 두 고리)를 신설하고 기존 `evidence-dataflow` 포함 4종의 범례를 한글화했다. 렌더러 기본 범례가 "User UI / Agent logic / policy·PII" 같은 타 도메인 문구였다. 작성 중 내린 고도 판단: 아키텍처에서는 analyzer/scorer 를 한 컴포넌트로 합쳤고(세부는 dataflow 담당), 라이프사이클에서 "재분석 요청"은 decisionStatus 가 아닌 운영자 액션이라 상태에서 제외해 복귀 엣지를 하나로 줄였다. 4종 모두 archify showcase 통과(아티팩트 검사 9/9 · 합성 오류 0 · 경고 0) + visual-check 통과(1440×900 · 2048×1320 각 Light/Dark, 오버플로 0). 수신증 — dataflow html a62f0f5c / architecture f5a4b6e1 / workflow 402914a3 / lifecycle 62e934a3
|
||||
- 2026-09-21 · 다이어그램 감사 후 **드리프트 게이트 신설**. 에이전트 6축 감사가 주간 한도로 절반(2축)만 완주했고 검증 단계도 죽어, 완주분 15건을 저널에서 복구한 뒤 결정적 주장은 직접 재검증했다. 확정된 구멍 — 그림 4종의 재생성 절차가 **문서대로는 실행 불가**(`archify` 실행파일이 PATH 에 없음), 소스↔산출물 동기화를 보는 장치 0개, 사이드카 24개가 개발 머신 절대경로를 담은 채 미추적·비ignore, 임베드 폰트에 **한글 구간 0건**(`U+AC00` 매치 0 · 폴백은 중국어 폰트뿐). 실수정: `scripts/rebuild-diagrams.ps1`(validate→deliver→visual-check→manifest, 실행 확인)과 `scripts/update_diagram_manifest.py`, 수신증 `docs/diagrams/manifest.json`, 게이트 `tests/test_diagram_sync.py` 14건(HTML 직접 수정·spec 만 수정 두 시나리오 모두 실제로 차단됨을 실증), `.gitignore` 에 사이드카 제외(미추적 24→0), `.gitattributes` 에 `linguist-generated -diff merge=binary`. 재생성해도 4종 sha256 불변 — 바이트 결정성 재확인. 한글 폰트·영어 뷰어 UI·architecture 좌표 하드코딩·lifecycle 손튜닝 8개소는 PROJECT_MAP 부채 표에 등재. **ruff clean · pytest 600 passed**(게이트 +14)
|
||||
|
||||
- 2026-09-19 · 다이어그램 4종으로 확장 — `system-architecture`(폐쇄망 경계·SSRF 가드 앞 호출 게이트), `operator-review-workflow`(4레인, 비상 차단 시 심사 계속 경로), `submission-lifecycle`(분석 실패·보류가 분석으로 복귀하는 두 고리)를 신설하고 기존 `evidence-dataflow` 포함 4종의 **범례 항목**을 한글화했다(렌더러 기본값이 "User UI / Agent logic / policy·PII" 같은 타 도메인 문구였다). **뷰어 크롬은 영어로 남는다** — `locale` enum 이 en/zh-CN 뿐이고 템플릿이 `<html lang="en">` 을 하드코딩하므로 Light/Dark·Present·Export·범례 제목("Legend")은 저자가 바꿀 수 없다. 9/21 감사에서 확인. 작성 중 내린 고도 판단: 아키텍처에서는 analyzer/scorer 를 한 컴포넌트로 합쳤고(세부는 dataflow 담당), 라이프사이클에서 "재분석 요청"은 decisionStatus 가 아닌 운영자 액션이라 상태에서 제외해 복귀 엣지를 하나로 줄였다. 4종 모두 archify showcase 통과(아티팩트 검사 9/9 · 합성 오류 0 · 경고 0) + visual-check 통과(1440×900 · 2048×1320 각 Light/Dark, 오버플로 0). 수신증 — dataflow html a62f0f5c / architecture f5a4b6e1 / workflow 402914a3 / lifecycle 62e934a3
|
||||
|
||||
- 2026-09-19 · `docs/diagrams/evidence-dataflow.html` 신설 — 증거 데이터 흐름(수집 → 경계 검사 → 정규화·채점 → 영속화 → 소비) 자체 완결 다이어그램. 9/18 리뷰 수정의 불변식을 그림으로 고정한다: **제공자별 풀·버킷**(RiskScorer v3), **라운드트립이 원 점수를 재현**(`_evidence_payload` ↔ `_domain_evidence_from_ui`), **host 는 저장 시 해석·복원 시 우선 반환**, 지식DB 지문 매칭이 풀링 밖 유일한 high 승격 경로. 프로덕션 LLM 경로가 영속화 payload 를 복원해 쓴다는 사실(`store_operations.py:487`, `EvidenceEnricher` 는 테스트 전용)도 반영. archify showcase 검증 통과(아티팩트 검사 9/9 · 합성 오류 0 · 경고 0), 브라우저 증거 통과(1440×900 · 2048×1320 각 Light/Dark 4컷, 오버플로 0, 최소 투영 텍스트 6.71px ≥ 6px). spec sha256 42c334cd · html sha256 268f3352 (822,655 B). CDN·원격 fetch 없음 — 폐쇄망 규칙 준수
|
||||
|
||||
|
|
|
|||
|
|
@ -169,9 +169,28 @@ WAL 적용됨. DB 기본 경로는 `data/copyrighter.sqlite3`(gitignore).
|
|||
|
||||
## 다이어그램
|
||||
|
||||
소스 JSON 과 산출물 HTML 이 같은 폴더에 쌍으로 있다. **그림을 고치려면 JSON 을 고쳐
|
||||
`archify validate` → `deliver` → `visual-check` 로 재생성한다** (HTML 직접 수정 금지).
|
||||
모두 자체 완결 HTML 이라 외부 요청이 없다.
|
||||
소스 JSON 과 산출물 HTML 이 같은 폴더에 쌍으로 있다. **HTML 직접 수정 금지** —
|
||||
고칠 것은 언제나 spec JSON 이고, 재생성은 스크립트 하나다:
|
||||
|
||||
```powershell
|
||||
.\scripts
ebuild-diagrams.ps1 # validate -> deliver -> visual-check -> manifest
|
||||
.\scripts
ebuild-diagrams.ps1 -SkipVisualCheck # 브라우저 없는 호스트
|
||||
```
|
||||
|
||||
`tests/test_diagram_sync.py` 가 `manifest.json` 의 sha256 으로 이 계약을 강제한다
|
||||
(브라우저·Node·archify 없이 도는 순수 파일 비교). HTML 을 직접 고치거나 spec 만
|
||||
고치고 재생성을 잊으면 `verify.ps1` 에서 막힌다. 두 시나리오 모두 실증 확인됨.
|
||||
|
||||
**문서 재생성 전용 요구사항** (런타임 아님, 폐쇄망 타깃에는 불필요):
|
||||
Node 18+ 와 archify 스킬(`$env:ARCHIFY_HOME`, 기본값 `~/.claude/skills/archify`),
|
||||
visual-check 단계에만 Chrome/Chromium. 산출 당시 generator 는 `archify 2.17.0-dev.1`
|
||||
이고 `manifest.json` 에 기록된다 — 스킬 버전이 바뀌면 같은 spec 에서도 바이트가
|
||||
달라지므로, 해시가 깨졌을 때 코드 변경과 툴 변경을 구분하는 근거가 된다.
|
||||
|
||||
모두 자체 완결 HTML 이라 외부 요청이 없다(9/21 감사에서 실측: src/href/@import/fetch
|
||||
모두 0건). 단 **file:// 로 여는 산출물**이다 — 운영자 콘솔의 static_root 로 서빙하면
|
||||
app-shell CSP(`script-src 'self'`, `http_app.py:514`)가 인라인 스크립트 4개를 차단해
|
||||
상호작용이 죽는다. 서빙이 필요하면 CSP 완화가 아니라 별도 라우트에서 nonce 정책을 쓴다.
|
||||
|
||||
| 파일 | 내용 | 타입 |
|
||||
|---|---|---|
|
||||
|
|
@ -189,4 +208,8 @@ WAL 적용됨. DB 기본 경로는 `data/copyrighter.sqlite3`(gitignore).
|
|||
| 밴드 임계·풀 상한 미캘리브레이션 | `analysis/risk_scoring.py` | 판정 이력 0건이라 70/30 임계가 실제 판정과 대조된 적 없다. v3 의 두 풀(`WEB_PRESENCE_MAX_POINTS` 60 은 **제공자별** / `WEB_ENTITY_MAX_POINTS` 20)은 신호의 구조적 성질로 정한 값이고 상관 검증 전이다 — 판정 30~50건 후 `decision_stats` 교차표가 첫 근거가 된다 |
|
||||
| 출처 신호 2종 0점 | `analysis/provenance.py` | 캘리브레이션 전. 실측 코퍼스에서 가치 미입증. 방송캡처는 `MIN_CAPTURE_CATEGORIES=2` 에서 geometry 가 1개 카테고리라 자막 없는 캡처는 구조적으로 도달 불가 |
|
||||
| 테스트가 로컬 LLM 에 실접속 | `integrations/env_clients.py:260` | `_llm_assistant` 가 항상 `OLLAMA_BASE_URL` 기본값(`127.0.0.1:11434`)으로 클라이언트를 만들므로, provider_runtime 없이 만든 store 를 쓰는 테스트는 **ollama 가 떠 있으면 실호출**을 한다. `test_http_app.py` 3건이 그 상태에서 소켓 타임아웃으로 실패한다(`OLLAMA_BASE_URL=http://127.0.0.1:1` 로 돌리면 통과). 폐쇄망 프로젝트의 테스트가 외부 서비스의 **부재**에 의존하는 상태 |
|
||||
| 다이어그램에 한글 폰트 미번들 | `docs/diagrams/*.html` | 임베드된 JetBrains Mono 의 unicode-range 6개에 **한글 구간이 없다**(`U+AC00` 매치 0). 폴백 스택은 중국어 폰트뿐(`Noto Sans Mono CJK SC`/`PingFang SC`/`Microsoft YaHei`). 렌더 텍스트의 40~72% 가 한글이라 타깃 폰트에 의존한다 — 한국어 폰트 없는 최소 설치 호스트에서는 두부(tofu). archify 에 저자용 폰트 옵션이 없어 spec 으로는 못 고친다(레이아웃 폭은 전각으로 측정되므로 깨짐이 아니라 글자 문제). 고객 인수물로 확정되면 blocker |
|
||||
| 뷰어 UI 가 영어 고정 | `docs/diagrams/*.html` | `locale` enum 이 `en`/`zh-CN` 뿐이고 템플릿이 `<html lang="en">` 하드코딩. 저자 내용은 한국어인데 조작 UI·aria 라벨·범례 제목은 영어. 스크린리더가 한글 본문을 영어로 읽는다 |
|
||||
| architecture spec 만 좌표 하드코딩 | `docs/diagrams/system-architecture.architecture.json` | 컴포넌트 11개 전부 `pos`+`size` 수기, `size` 는 [124,62] 고정. 라벨 21자에서 validate 하드 실패(현재 `store_remote_fetch` 18자 → 여유 2글자). workflow spec 은 기하 하드코딩 0개이므로 같은 저장소에 더 나은 패턴이 있다 |
|
||||
| lifecycle 기하 손튜닝 8개소 | `docs/diagrams/submission-lifecycle.lifecycle.json` | `labelDy`×5·`channelX`×2·`yOffset`×1 이 현재 배치 전제의 절대 픽셀이다. 상태를 하나 추가하면 **손대지 않은 기존 전이까지** 검증 실패(실증: 오류 5건 중 4건이 기존 `t-reject`). 상태 추가 시 전량 재검증 필요 |
|
||||
| 7/11 리뷰 연기 항목 | [reviews/2026-07-11-remediation-checklist.md](reviews/2026-07-11-remediation-checklist.md) | 각 항목에 연기 근거 기재 |
|
||||
|
|
|
|||
38
docs/diagrams/manifest.json
Normal file
38
docs/diagrams/manifest.json
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"note": "archify deliver 는 바이트 결정적이다. 이 수신증은 tests/test_diagram_sync.py 가 강제한다 - HTML 을 직접 고치거나 spec 만 고치고 재생성을 잊으면 커밋 게이트가 막는다. 갱신은 손으로 하지 말고 scripts/rebuild-diagrams.ps1 로 한다.",
|
||||
"generator": "archify 2.17.0-dev.1",
|
||||
"diagrams": [
|
||||
{
|
||||
"type": "architecture",
|
||||
"spec": "system-architecture.architecture.json",
|
||||
"html": "system-architecture.html",
|
||||
"specSha256": "8359880ec5a7e64012cf7533169d279fff364b36c84855a1678ea8be2414d699",
|
||||
"htmlSha256": "f5a4b6e1c6f94ab65bd55d6b348c9d1257db194508285541e3fb94acdfabd44c",
|
||||
"htmlBytes": 816201
|
||||
},
|
||||
{
|
||||
"type": "dataflow",
|
||||
"spec": "evidence-dataflow.dataflow.json",
|
||||
"html": "evidence-dataflow.html",
|
||||
"specSha256": "7503e9be9f100440d965877f6bb7e9ce8863f85015a5144b7a30404a524ba65a",
|
||||
"htmlSha256": "a62f0f5cab31f880a98877f0d1b4353d09c86fb1152157fbdebe62bcd491114e",
|
||||
"htmlBytes": 822657
|
||||
},
|
||||
{
|
||||
"type": "workflow",
|
||||
"spec": "operator-review-workflow.workflow.json",
|
||||
"html": "operator-review-workflow.html",
|
||||
"specSha256": "222d2d7565ee06c783a93868775732d96a9300e3baa4e8ddff1f8a57a421e79c",
|
||||
"htmlSha256": "402914a3d29ad899d136b7e011bbe2aaf7d36a2a2880ee59fa33929bb614974d",
|
||||
"htmlBytes": 818137
|
||||
},
|
||||
{
|
||||
"type": "lifecycle",
|
||||
"spec": "submission-lifecycle.lifecycle.json",
|
||||
"html": "submission-lifecycle.html",
|
||||
"specSha256": "b53d44dc70393c7a0a7746d774563c61e412eceafed5d10fd1a5e0e509aa3ac9",
|
||||
"htmlSha256": "62e934a32e3b3c5b1660794d8919e2fc1910f3937e82e882f464abebbfbb2634",
|
||||
"htmlBytes": 812325
|
||||
}
|
||||
]
|
||||
}
|
||||
98
scripts/rebuild-diagrams.ps1
Normal file
98
scripts/rebuild-diagrams.ps1
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
<#
|
||||
.SYNOPSIS
|
||||
docs/diagrams 의 다이어그램 4종을 spec JSON 에서 다시 생성한다.
|
||||
|
||||
.DESCRIPTION
|
||||
PROJECT_MAP 에 적혀 있던 "archify validate -> deliver -> visual-check" 절차는
|
||||
그대로는 실행되지 않았다 - archify 라는 실행 파일이 PATH 에 없고, 실제로는
|
||||
저장소 밖 사용자 전역 스킬을 node 로 불러야 한다. 이 스크립트가 그 절차다.
|
||||
|
||||
순서:
|
||||
1. validate (--quality showcase) 합성/기하 검증
|
||||
2. deliver 산출물 HTML 을 원자적으로 교체
|
||||
3. visual-check 실제 브라우저 증거 (Chrome 필요)
|
||||
4. manifest 갱신 tests/test_diagram_sync.py 의 수신증
|
||||
|
||||
산출물은 바이트 결정적이다. 그래서 4번의 수신증만으로 드리프트를 잡을 수 있고,
|
||||
테스트는 브라우저도 Node 도 없이 돈다.
|
||||
|
||||
.PARAMETER SkipVisualCheck
|
||||
3단계를 건너뛴다. Chrome 이 없는 빌드 호스트용. 건너뛰면 그 사실을 출력한다
|
||||
(조용한 통과 금지).
|
||||
|
||||
.PARAMETER ArchifyHome
|
||||
archify 스킬 경로. 기본값은 $env:ARCHIFY_HOME, 없으면 ~/.claude/skills/archify.
|
||||
|
||||
.EXAMPLE
|
||||
.\scripts
|
||||
ebuild-diagrams.ps1
|
||||
.EXAMPLE
|
||||
.\scripts
|
||||
ebuild-diagrams.ps1 -SkipVisualCheck
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$SkipVisualCheck,
|
||||
[string]$ArchifyHome
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
Set-Location -LiteralPath $repoRoot
|
||||
|
||||
if (-not $ArchifyHome) {
|
||||
if ($env:ARCHIFY_HOME) { $ArchifyHome = $env:ARCHIFY_HOME }
|
||||
else { $ArchifyHome = Join-Path $HOME '.claude/skills/archify' }
|
||||
}
|
||||
$cli = Join-Path $ArchifyHome 'bin/archify.mjs'
|
||||
if (-not (Test-Path -LiteralPath $cli)) {
|
||||
Write-Host "archify CLI not found: $cli" -ForegroundColor Red
|
||||
Write-Host 'Set -ArchifyHome or $env:ARCHIFY_HOME to the archify skill directory.' -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# (type, basename) - tests/test_diagram_sync.py 및 update_diagram_manifest.py 와 같은 목록
|
||||
$diagrams = @(
|
||||
@{ Type = 'architecture'; Base = 'system-architecture' },
|
||||
@{ Type = 'dataflow'; Base = 'evidence-dataflow' },
|
||||
@{ Type = 'workflow'; Base = 'operator-review-workflow' },
|
||||
@{ Type = 'lifecycle'; Base = 'submission-lifecycle' }
|
||||
)
|
||||
|
||||
$failures = @()
|
||||
$skipped = @()
|
||||
|
||||
foreach ($d in $diagrams) {
|
||||
$spec = Join-Path $repoRoot ("docs/diagrams/{0}.{1}.json" -f $d.Base, $d.Type)
|
||||
$html = Join-Path $repoRoot ("docs/diagrams/{0}.html" -f $d.Base)
|
||||
|
||||
Write-Host "`n=== $($d.Base) ($($d.Type)) ===" -ForegroundColor Cyan
|
||||
|
||||
& node $cli validate $d.Type $spec --quality showcase
|
||||
if ($LASTEXITCODE -ne 0) { $failures += "validate:$($d.Base)"; continue }
|
||||
|
||||
& node $cli deliver $d.Type $spec $html --quality showcase
|
||||
if ($LASTEXITCODE -ne 0) { $failures += "deliver:$($d.Base)"; continue }
|
||||
|
||||
if ($SkipVisualCheck) { $skipped += $d.Base; continue }
|
||||
|
||||
& node $cli visual-check $html
|
||||
# exit 2 = skipped (Chrome/Chromium 부재). 실패와 구분해 기록한다.
|
||||
if ($LASTEXITCODE -eq 2) { $skipped += $d.Base }
|
||||
elseif ($LASTEXITCODE -ne 0) { $failures += "visual-check:$($d.Base)" }
|
||||
}
|
||||
|
||||
Write-Host "`n=== manifest ===" -ForegroundColor Cyan
|
||||
& python (Join-Path $PSScriptRoot 'update_diagram_manifest.py')
|
||||
if ($LASTEXITCODE -ne 0) { $failures += 'manifest' }
|
||||
|
||||
Write-Host ''
|
||||
if ($skipped.Count -gt 0) {
|
||||
Write-Host "VISUAL-CHECK SKIPPED (no browser evidence): $($skipped -join ', ')" -ForegroundColor Yellow
|
||||
}
|
||||
if ($failures.Count -gt 0) {
|
||||
Write-Host "REBUILD FAILED: $($failures -join ', ')" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Write-Host 'REBUILD OK - run verify.ps1 to confirm the diagram gate' -ForegroundColor Green
|
||||
exit 0
|
||||
93
scripts/update_diagram_manifest.py
Normal file
93
scripts/update_diagram_manifest.py
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
"""docs/diagrams/manifest.json 을 산출물 실제 바이트에서 다시 쓴다.
|
||||
|
||||
수신증의 단일 작성자다. PowerShell 과 Python 이 각자 JSON 을 쓰면 들여쓰기·키 순서가
|
||||
갈려 매니페스트가 왔다 갔다 하므로, rebuild-diagrams.ps1 도 이 스크립트를 부른다.
|
||||
|
||||
단독 실행도 된다: python scripts/update_diagram_manifest.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DIAGRAM_DIR = ROOT / "docs" / "diagrams"
|
||||
|
||||
# (타입, 파일 basename). 다이어그램을 추가하면 여기와 tests/test_diagram_sync.py 의
|
||||
# NODE_COLLECTION 을 함께 고친다.
|
||||
DIAGRAMS = [
|
||||
("architecture", "system-architecture"),
|
||||
("dataflow", "evidence-dataflow"),
|
||||
("workflow", "operator-review-workflow"),
|
||||
("lifecycle", "submission-lifecycle"),
|
||||
]
|
||||
|
||||
NOTE = (
|
||||
"archify deliver 는 바이트 결정적이다. 이 수신증은 tests/test_diagram_sync.py 가 "
|
||||
"강제한다 - HTML 을 직접 고치거나 spec 만 고치고 재생성을 잊으면 커밋 게이트가 막는다. "
|
||||
"갱신은 손으로 하지 말고 scripts/rebuild-diagrams.ps1 로 한다."
|
||||
)
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def _generator_stamp(html: Path) -> str:
|
||||
match = re.search(
|
||||
r'<meta name="generator" content="([^"]+)"', html.read_text(encoding="utf-8")
|
||||
)
|
||||
if not match:
|
||||
raise SystemExit(f"{html.name} 에 generator meta 가 없다 - archify 산출물이 맞는가?")
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
entries = []
|
||||
for diagram_type, base in DIAGRAMS:
|
||||
spec = DIAGRAM_DIR / f"{base}.{diagram_type}.json"
|
||||
html = DIAGRAM_DIR / f"{base}.html"
|
||||
for path in (spec, html):
|
||||
if not path.exists():
|
||||
raise SystemExit(f"없는 파일: {path}")
|
||||
entries.append(
|
||||
{
|
||||
"type": diagram_type,
|
||||
"spec": spec.name,
|
||||
"html": html.name,
|
||||
"specSha256": _sha256(spec),
|
||||
"htmlSha256": _sha256(html),
|
||||
"htmlBytes": html.stat().st_size,
|
||||
}
|
||||
)
|
||||
|
||||
generators = {_generator_stamp(DIAGRAM_DIR / e["html"]) for e in entries}
|
||||
if len(generators) != 1:
|
||||
raise SystemExit(
|
||||
f"산출물마다 generator 가 다르다: {sorted(generators)} - 전부 재생성해야 한다."
|
||||
)
|
||||
|
||||
manifest = {
|
||||
"note": NOTE,
|
||||
"generator": generators.pop(),
|
||||
"diagrams": entries,
|
||||
}
|
||||
target = DIAGRAM_DIR / "manifest.json"
|
||||
with io.open(target, "w", encoding="utf-8", newline="\n") as handle:
|
||||
json.dump(manifest, handle, ensure_ascii=False, indent=2)
|
||||
handle.write("\n")
|
||||
|
||||
# 비ASCII 는 stdout 에 넣지 않는다 (콘솔이 cp949 라 UnicodeEncodeError 로 죽은 이력).
|
||||
print("manifest updated: %d diagrams, generator %s" % (len(entries), manifest["generator"]))
|
||||
for entry in entries:
|
||||
print(" %-30s %s.. %d B" % (entry["html"], entry["htmlSha256"][:8], entry["htmlBytes"]))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
130
tests/test_diagram_sync.py
Normal file
130
tests/test_diagram_sync.py
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
"""다이어그램 소스(JSON)와 산출물(HTML)이 갈라지지 않았는지 검사하는 커밋 게이트.
|
||||
|
||||
`docs/diagrams/*.json` 이 소스이고 `*.html` 이 archify deliver 산출물이다. 둘의
|
||||
동기화를 확인하는 자동 장치가 없어서, spec 만 고치고 재생성을 잊거나 HTML 을
|
||||
직접 고쳐도 커밋이 통과했다. 폐쇄망 납품물에 동봉되는 문서라 고객이 보는 그림과
|
||||
실제 코드가 갈라져도 검출 경로가 없었다.
|
||||
|
||||
이 테스트는 **브라우저도 Node 도 archify 도 필요 없다** — 순수 파일 비교다.
|
||||
archify deliver 가 바이트 결정적이라는 사실(같은 spec 에서 항상 같은 HTML)이
|
||||
그 근거이고, 그래서 수신증 대조만으로 충분하다.
|
||||
|
||||
실패했을 때 고치는 법은 manifest 를 손으로 맞추는 게 아니라
|
||||
`scripts/rebuild-diagrams.ps1` 를 돌리는 것이다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DIAGRAM_DIR = ROOT / "docs" / "diagrams"
|
||||
MANIFEST = DIAGRAM_DIR / "manifest.json"
|
||||
|
||||
# spec 의 어느 배열이 노드를 담는지는 다이어그램 타입마다 다르다.
|
||||
NODE_COLLECTION = {
|
||||
"architecture": "components",
|
||||
"dataflow": "nodes",
|
||||
"workflow": "nodes",
|
||||
"lifecycle": "states",
|
||||
}
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def _manifest() -> dict:
|
||||
return json.loads(MANIFEST.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _entries() -> list[dict]:
|
||||
return _manifest()["diagrams"]
|
||||
|
||||
|
||||
def _ids() -> list[str]:
|
||||
return [entry["html"] for entry in _entries()]
|
||||
|
||||
|
||||
def test_manifest_covers_every_delivered_diagram():
|
||||
"""새 다이어그램을 추가하면 수신증에도 등재해야 한다."""
|
||||
# 사이드카(*.visual-check.html)는 검증 증거이지 납품물이 아니다.
|
||||
delivered = {
|
||||
path.name
|
||||
for path in DIAGRAM_DIR.glob("*.html")
|
||||
if ".visual-check." not in path.name
|
||||
}
|
||||
|
||||
assert delivered == set(_ids()), (
|
||||
"manifest.json 과 실제 산출물 목록이 다르다. "
|
||||
"다이어그램을 추가/제거했으면 scripts/rebuild-diagrams.ps1 로 갱신한다."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("entry", _entries(), ids=_ids())
|
||||
def test_html_matches_its_receipt(entry: dict):
|
||||
"""HTML 을 직접 고치면 여기서 막힌다."""
|
||||
html = DIAGRAM_DIR / entry["html"]
|
||||
|
||||
assert html.exists(), f"{entry['html']} 이 없다"
|
||||
assert html.stat().st_size == entry["htmlBytes"]
|
||||
assert _sha256(html) == entry["htmlSha256"], (
|
||||
f"{entry['html']} 이 수신증과 다르다. HTML 을 직접 고쳤거나 재생성 후 "
|
||||
"manifest 를 갱신하지 않았다 — scripts/rebuild-diagrams.ps1 로 재생성한다."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("entry", _entries(), ids=_ids())
|
||||
def test_spec_matches_its_receipt(entry: dict):
|
||||
"""spec 만 고치고 재생성을 잊으면 여기서 막힌다."""
|
||||
spec = DIAGRAM_DIR / entry["spec"]
|
||||
|
||||
assert spec.exists(), f"{entry['spec']} 이 없다"
|
||||
assert _sha256(spec) == entry["specSha256"], (
|
||||
f"{entry['spec']} 이 수신증과 다르다. spec 을 고쳤으면 "
|
||||
"scripts/rebuild-diagrams.ps1 로 HTML 을 재생성해야 한다."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("entry", _entries(), ids=_ids())
|
||||
def test_every_spec_node_is_present_in_the_html(entry: dict):
|
||||
"""sha 대조가 통과해도 이 검사가 소스↔산출물의 의미적 연결을 잡는다.
|
||||
|
||||
해시는 '둘 다 최신인가'만 말해준다. 이 검사는 산출물이 정말 그 spec 에서
|
||||
나왔는지를 본다 — 노드 id 는 HTML 에 data-node-id 로 그대로 박힌다.
|
||||
"""
|
||||
spec = json.loads((DIAGRAM_DIR / entry["spec"]).read_text(encoding="utf-8"))
|
||||
html = (DIAGRAM_DIR / entry["html"]).read_text(encoding="utf-8")
|
||||
|
||||
collection = NODE_COLLECTION[entry["type"]]
|
||||
node_ids = [node["id"] for node in spec[collection]]
|
||||
assert node_ids, f"{entry['spec']} 에 노드가 없다"
|
||||
|
||||
missing = [
|
||||
node_id
|
||||
for node_id in node_ids
|
||||
if f'data-node-id="{node_id}"' not in html
|
||||
]
|
||||
assert not missing, (
|
||||
f"{entry['html']} 에 spec 노드 {missing} 가 없다 — 재생성되지 않은 산출물이다."
|
||||
)
|
||||
|
||||
|
||||
def test_generator_version_is_recorded():
|
||||
"""산출물은 archify 버전에 따라 바이트가 달라진다. 그 버전이 기록돼 있어야
|
||||
나중에 '코드는 그대로인데 해시가 바뀐' 원인을 구분할 수 있다."""
|
||||
recorded = _manifest()["generator"]
|
||||
stamped = re.search(
|
||||
r'<meta name="generator" content="([^"]+)"',
|
||||
(DIAGRAM_DIR / _entries()[0]["html"]).read_text(encoding="utf-8"),
|
||||
)
|
||||
|
||||
assert stamped, "산출물에 generator meta 가 없다"
|
||||
assert stamped.group(1) == recorded, (
|
||||
f"manifest 의 generator({recorded}) 와 산출물 각인({stamped.group(1)}) 이 다르다"
|
||||
)
|
||||
Loading…
Reference in a new issue