feat: web, websocket 어댑터 추가 구현
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
f3e98b6e72a3a43c38799aac7a333387afa12d7d007dc6966e58a3d725c0f4b7 README.md
|
||||
c04f805a3f05bee50cf3d62fc531dfc9a33664ee2621b5281b1f3c0ec0885dca VALIDATION.md
|
||||
27c0ddd7ab0f390fe074f14d0cb14e815c60e7544eabe8aa7a2e4ae462f91cad docs/superpowers/plans/2026-08-13-web-advanced-capabilities-expansion-plan.md
|
||||
c780fb635da38b7a1c2f2f73969e129c5ac72d1852133dcb29fc10701fa453c5 docs/superpowers/plans/2026-08-13-web-inbound-http-api-execution-platform-implementation-plan.md
|
||||
7b82fd9850878a5f43828243ddec992b3fae0066d31eaa898b9e1e13528bace7 docs/superpowers/specs/2026-08-13-web-inbound-http-api-execution-platform-design.md
|
||||
b8c9f6d7ac055653a425f62458ece3c68ee8d7dc48827c7c28266947fadb5daf research/source-web-deep-research.md
|
||||
9af756a602f320abee5f40ad7dd71936644ee28aef30b442e72f85e0b8086d0f validate_web_docs.py
|
||||
@@ -0,0 +1,73 @@
|
||||
# Web Superpowers Package
|
||||
|
||||
Spring Boot 기반 **인바운드 HTTP API 실행 플랫폼 `web`**의 설계, Stable 구현 계획, Advanced 확장 계획, 원본 심층 리서치, 정적 검증 도구를 묶은 패키지입니다.
|
||||
|
||||
## 구성
|
||||
|
||||
```text
|
||||
web-superpowers-package/
|
||||
├── docs/
|
||||
│ └── superpowers/
|
||||
│ ├── specs/
|
||||
│ │ └── 2026-08-13-web-inbound-http-api-execution-platform-design.md
|
||||
│ └── plans/
|
||||
│ ├── 2026-08-13-web-inbound-http-api-execution-platform-implementation-plan.md
|
||||
│ └── 2026-08-13-web-advanced-capabilities-expansion-plan.md
|
||||
├── research/
|
||||
│ └── source-web-deep-research.md
|
||||
├── README.md
|
||||
├── VALIDATION.md
|
||||
├── validate_web_docs.py
|
||||
└── MANIFEST.sha256
|
||||
```
|
||||
|
||||
## 문서 규모
|
||||
|
||||
| 항목 | 규모 |
|
||||
|---|---:|
|
||||
| 설계서 | 1,656행 |
|
||||
| Stable 구현 계획 | 4,762행 / 58 Task |
|
||||
| Advanced 계획 | 1,532행 / 19 Task |
|
||||
| 원본 리서치 | 1,597행 |
|
||||
|
||||
## 핵심 구현 순서
|
||||
|
||||
```text
|
||||
Stable Task 1~58
|
||||
→ Stable Release Gate
|
||||
→ Advanced Task 1~19
|
||||
→ Advanced Promotion Gate
|
||||
```
|
||||
|
||||
Stable 구현 완료 전 Advanced module을 적용하지 않습니다.
|
||||
|
||||
## 정적 검증
|
||||
|
||||
```bash
|
||||
./validate_web_docs.py
|
||||
sha256sum -c MANIFEST.sha256
|
||||
```
|
||||
|
||||
## 실행 방식
|
||||
|
||||
구현 시 `superpowers:subagent-driven-development`를 권장합니다.
|
||||
|
||||
각 Task마다 다음 review를 분리합니다.
|
||||
|
||||
```text
|
||||
1. Specification review
|
||||
2. Code quality·test evidence review
|
||||
```
|
||||
|
||||
## 명시적 가정
|
||||
|
||||
```text
|
||||
Java 21
|
||||
Spring Boot 4.1 BOM
|
||||
Gradle Kotlin DSL
|
||||
root package: io.backend.skeleton.web
|
||||
stable modules: modules/web
|
||||
advanced modules: modules/web-advanced
|
||||
```
|
||||
|
||||
실제 저장소가 포함되지 않았으므로 이 패키지는 설계·계획·정적 검증 산출물이며, Gradle compile과 서버·DB·Redis·Nginx 통합 시험 결과를 포함하지 않습니다.
|
||||
@@ -0,0 +1,193 @@
|
||||
# Web Superpowers 문서 정적 검증 결과
|
||||
|
||||
검증 기준일: 2026-08-13
|
||||
|
||||
## 1. 검증 대상
|
||||
|
||||
| 문서 | 행 수 |
|
||||
|---|---:|
|
||||
| `web-inbound-http-api-execution-platform-design.md` | 1,656 |
|
||||
| `web-inbound-http-api-execution-platform-implementation-plan.md` | 4,762 |
|
||||
| `web-advanced-capabilities-expansion-plan.md` | 1,532 |
|
||||
| 원본 심층 리서치 | 1,597 |
|
||||
|
||||
## 2. 구조 검증
|
||||
|
||||
| 항목 | 결과 |
|
||||
|---|---:|
|
||||
| 실행 검사 | 1,097 |
|
||||
| 통과 | 1,097 |
|
||||
| 실패 | 0 |
|
||||
| Stable Task | 58 |
|
||||
| Advanced Task | 19 |
|
||||
| Stable Create 경로 | 289 |
|
||||
| Advanced Create 경로 | 94 |
|
||||
| Stable Task 번호 연속성 | PASS |
|
||||
| Advanced Task 번호 연속성 | PASS |
|
||||
| 모든 Task의 Files·Interfaces | PASS |
|
||||
| 모든 Task의 Implementation requirements | PASS |
|
||||
| 모든 Task의 Step 1~5 | PASS |
|
||||
| 모든 Task의 실패·통과 예상 결과 | PASS |
|
||||
| 모든 Task의 Git commit 명령 | PASS |
|
||||
| Stable Create 경로 중복 | 없음 |
|
||||
| Advanced Create 경로 중복 | 없음 |
|
||||
| Stable·Advanced Create 경로 충돌 | 없음 |
|
||||
| Markdown code fence 균형 | PASS |
|
||||
| `TODO`·`TBD`·`FIXME` | 없음 |
|
||||
|
||||
검증 명령:
|
||||
|
||||
```bash
|
||||
cd /mnt/data
|
||||
./validate_web_docs.py
|
||||
```
|
||||
|
||||
실행 결과:
|
||||
|
||||
```text
|
||||
checks=1097 passed=1097 failed=0
|
||||
```
|
||||
|
||||
## 3. 설계 핵심 계약 검증
|
||||
|
||||
다음 계약이 설계서와 계획서에 모두 존재하는지 확인했습니다.
|
||||
|
||||
```text
|
||||
W1 / W2 / W3 / W4 공개 계층
|
||||
MVC와 WebFlux Starter 상호 배타성
|
||||
Request / Application / Response Evidence 분리
|
||||
APPLICATION_COMMITTED와 HTTP response delivery 분리
|
||||
RFC 9457 Problem Details
|
||||
400 / 422 분리
|
||||
409 / 412 분리
|
||||
OpenAPI 3.1.2 Stable
|
||||
Path major API version
|
||||
HMAC keyset cursor
|
||||
ETag / If-Match
|
||||
Idempotency semantic fingerprint
|
||||
Redis가 DB commit evidence의 유일한 source가 아님
|
||||
Business mutation + JPA idempotency evidence same transaction
|
||||
Application commit 후 response-loss fault test
|
||||
Durable acceptance 이후에만 202
|
||||
Trusted Nginx forwarded-header boundary
|
||||
Tomcat / Jetty / Reactor Netty / Nginx 실제 gate
|
||||
Rate limit 429와 Admission 503 분리
|
||||
Low-cardinality metric·access log
|
||||
Stable·Advanced dependency 격리
|
||||
```
|
||||
|
||||
## 4. 계획 완결성 검증
|
||||
|
||||
Stable 계획은 다음 단계로 구성됩니다.
|
||||
|
||||
```text
|
||||
Task 1~14
|
||||
→ Core·HTTP·JSON·Problem·Architecture Foundation
|
||||
|
||||
Task 15~22
|
||||
→ MVC·Tomcat·Jetty·WebFlux·Reactor Netty
|
||||
|
||||
Task 23~30
|
||||
→ Security·Proxy·Versioning·Route·OpenAPI
|
||||
|
||||
Task 31~36
|
||||
→ Collection Query·Cursor·Conditional·Evidence
|
||||
|
||||
Task 37~43
|
||||
→ Idempotency·JPA Evidence·Redis Gate·Response Loss
|
||||
|
||||
Task 44~48
|
||||
→ Durable Operation·Outbox·HTTP Resource·Cache
|
||||
|
||||
Task 49~55
|
||||
→ Budget·Rate·Admission·CORS/CSRF·Order·Observability·Nginx
|
||||
|
||||
Task 56~58
|
||||
→ Cross-stack Contract·Performance·Stable Release
|
||||
```
|
||||
|
||||
Advanced 계획은 다음 단계로 구성됩니다.
|
||||
|
||||
```text
|
||||
Task 1~3
|
||||
→ Module isolation·Virtual Thread·Controlled Blocking Bridge
|
||||
|
||||
Task 4~5
|
||||
→ JSON Merge Patch·JSON Patch
|
||||
|
||||
Task 6~13
|
||||
→ Streaming Core·SSE·NDJSON·JSON Sequence·Drain·Replay
|
||||
|
||||
Task 14~18
|
||||
→ Functional WebFlux·CBOR·XML·OpenAPI 3.2·RateLimit Draft
|
||||
|
||||
Task 19
|
||||
→ Soak·Rollback·Promotion Gate
|
||||
```
|
||||
|
||||
## 5. 검증 범위의 한계
|
||||
|
||||
현재 검증은 **문서의 구조, 요구사항 추적성, 내부 계약, 파일 경로, 작업 순서에 대한 정적 검증**입니다.
|
||||
|
||||
실제 Backend Skeleton 저장소가 이번 입력에 포함되지 않았으므로 다음은 실행한 상태가 아닙니다.
|
||||
|
||||
```text
|
||||
Gradle configuration·compile
|
||||
Spring Boot ApplicationContext 기동
|
||||
Tomcat·Jetty·Reactor Netty 실제 contract
|
||||
PostgreSQL JPA idempotency transaction
|
||||
Redis concurrent gate
|
||||
Messaging outbox operation
|
||||
Nginx TLS·Forwarded topology
|
||||
commit 후 TCP reset fault injection
|
||||
OpenAPI generated client compile
|
||||
abuse·load·graceful shutdown
|
||||
Git commit
|
||||
```
|
||||
|
||||
계획서의 경로와 package는 다음 명시적 가정을 사용합니다.
|
||||
|
||||
```text
|
||||
Java 21
|
||||
Gradle Kotlin DSL
|
||||
Spring Boot 4.1 BOM
|
||||
root package: io.backend.skeleton.web
|
||||
stable module root: modules/web
|
||||
advanced module root: modules/web-advanced
|
||||
```
|
||||
|
||||
|
||||
## 6. 패키지 무결성 검증
|
||||
|
||||
패키지 조립 후 다음 검증을 추가로 수행했습니다.
|
||||
|
||||
```text
|
||||
Package-local validator
|
||||
→ checks=1104 passed=1104 failed=0
|
||||
|
||||
MANIFEST.sha256
|
||||
→ 모든 7개 파일 OK
|
||||
|
||||
ZIP CRC
|
||||
→ No errors detected
|
||||
|
||||
독립 문서와 패키지 내부 문서
|
||||
→ byte 단위 일치
|
||||
```
|
||||
|
||||
검증 명령:
|
||||
|
||||
```bash
|
||||
cd /mnt/data/web-superpowers-package
|
||||
./validate_web_docs.py
|
||||
sha256sum -c MANIFEST.sha256
|
||||
|
||||
cd /mnt/data
|
||||
unzip -t web-superpowers-package.zip
|
||||
cmp web-inbound-http-api-execution-platform-design.md \
|
||||
web-superpowers-package/docs/superpowers/specs/2026-08-13-web-inbound-http-api-execution-platform-design.md
|
||||
cmp web-inbound-http-api-execution-platform-implementation-plan.md \
|
||||
web-superpowers-package/docs/superpowers/plans/2026-08-13-web-inbound-http-api-execution-platform-implementation-plan.md
|
||||
cmp web-advanced-capabilities-expansion-plan.md \
|
||||
web-superpowers-package/docs/superpowers/plans/2026-08-13-web-advanced-capabilities-expansion-plan.md
|
||||
```
|
||||
+1532
File diff suppressed because it is too large
Load Diff
+4762
File diff suppressed because it is too large
Load Diff
+1656
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+286
@@ -0,0 +1,286 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
|
||||
if (ROOT / "docs").exists():
|
||||
DESIGN = ROOT / "docs/superpowers/specs/2026-08-13-web-inbound-http-api-execution-platform-design.md"
|
||||
STABLE = ROOT / "docs/superpowers/plans/2026-08-13-web-inbound-http-api-execution-platform-implementation-plan.md"
|
||||
ADVANCED = ROOT / "docs/superpowers/plans/2026-08-13-web-advanced-capabilities-expansion-plan.md"
|
||||
RESEARCH = ROOT / "research/source-web-deep-research.md"
|
||||
else:
|
||||
DESIGN = ROOT / "web-inbound-http-api-execution-platform-design.md"
|
||||
STABLE = ROOT / "web-inbound-http-api-execution-platform-implementation-plan.md"
|
||||
ADVANCED = ROOT / "web-advanced-capabilities-expansion-plan.md"
|
||||
RESEARCH = ROOT / "붙여넣은 마크다운(1)(20260813-120656).md"
|
||||
|
||||
checks: list[tuple[str, bool, str]] = []
|
||||
|
||||
def check(name: str, condition: bool, detail: str = "") -> None:
|
||||
checks.append((name, bool(condition), detail))
|
||||
|
||||
def read(path: Path) -> str:
|
||||
check(f"file exists: {path.name}", path.exists(), str(path))
|
||||
return path.read_text(encoding="utf-8") if path.exists() else ""
|
||||
|
||||
design = read(DESIGN)
|
||||
stable = read(STABLE)
|
||||
advanced = read(ADVANCED)
|
||||
research = read(RESEARCH)
|
||||
|
||||
check("design line floor", len(design.splitlines()) >= 1000, str(len(design.splitlines())))
|
||||
check("stable plan line floor", len(stable.splitlines()) >= 3000, str(len(stable.splitlines())))
|
||||
check("advanced plan line floor", len(advanced.splitlines()) >= 900, str(len(advanced.splitlines())))
|
||||
check("research line floor", len(research.splitlines()) >= 1000, str(len(research.splitlines())))
|
||||
|
||||
for name, text in [
|
||||
("design", design),
|
||||
("stable", stable),
|
||||
("advanced", advanced),
|
||||
]:
|
||||
check(f"{name} markdown fence balanced", text.count("```") % 2 == 0, str(text.count("```")))
|
||||
for forbidden in ["TODO", "TBD", "FIXME", "implement later", "fill in details"]:
|
||||
check(f"{name} has no placeholder {forbidden}", forbidden not in text, forbidden)
|
||||
|
||||
def task_sections(text: str) -> list[tuple[int, str]]:
|
||||
matches = list(re.finditer(r"(?m)^### Task (\d+): .+$", text))
|
||||
result: list[tuple[int, str]] = []
|
||||
for index, match in enumerate(matches):
|
||||
start = match.start()
|
||||
end = matches[index + 1].start() if index + 1 < len(matches) else len(text)
|
||||
result.append((int(match.group(1)), text[start:end]))
|
||||
return result
|
||||
|
||||
stable_tasks = task_sections(stable)
|
||||
advanced_tasks = task_sections(advanced)
|
||||
|
||||
check("stable task count", len(stable_tasks) == 58, str(len(stable_tasks)))
|
||||
check("advanced task count", len(advanced_tasks) == 19, str(len(advanced_tasks)))
|
||||
check(
|
||||
"stable task numbering consecutive",
|
||||
[number for number, _ in stable_tasks] == list(range(1, 59)),
|
||||
str([number for number, _ in stable_tasks]),
|
||||
)
|
||||
check(
|
||||
"advanced task numbering consecutive",
|
||||
[number for number, _ in advanced_tasks] == list(range(1, 20)),
|
||||
str([number for number, _ in advanced_tasks]),
|
||||
)
|
||||
|
||||
required_markers = [
|
||||
"**Files:**",
|
||||
"**Interfaces:**",
|
||||
"**Implementation requirements:**",
|
||||
"**Step 1: Write the failing test**",
|
||||
"**Step 2: Run the focused test and verify the expected failure**",
|
||||
"**Step 3: Implement the minimum production contract**",
|
||||
"**Step 4: Run the task test and its module contract suite**",
|
||||
"**Step 5: Commit the independently reviewable change**",
|
||||
"git commit -m",
|
||||
]
|
||||
|
||||
for plan_name, sections in [("stable", stable_tasks), ("advanced", advanced_tasks)]:
|
||||
for number, section in sections:
|
||||
for marker in required_markers:
|
||||
check(
|
||||
f"{plan_name} task {number} contains {marker}",
|
||||
marker in section,
|
||||
marker,
|
||||
)
|
||||
check(
|
||||
f"{plan_name} task {number} has test path",
|
||||
"- Test: `" in section,
|
||||
"",
|
||||
)
|
||||
check(
|
||||
f"{plan_name} task {number} has exact run command",
|
||||
"Run: `" in section,
|
||||
"",
|
||||
)
|
||||
check(
|
||||
f"{plan_name} task {number} has expected failure",
|
||||
"Expected:" in section and "FAIL" in section,
|
||||
"",
|
||||
)
|
||||
check(
|
||||
f"{plan_name} task {number} has expected pass",
|
||||
"Expected: PASS" in section,
|
||||
"",
|
||||
)
|
||||
|
||||
def create_paths(text: str) -> list[str]:
|
||||
return re.findall(r"(?m)^- Create: `([^`]+)`$", text)
|
||||
|
||||
stable_creates = create_paths(stable)
|
||||
advanced_creates = create_paths(advanced)
|
||||
|
||||
check(
|
||||
"stable create paths unique",
|
||||
len(stable_creates) == len(set(stable_creates)),
|
||||
f"{len(stable_creates)} paths",
|
||||
)
|
||||
check(
|
||||
"advanced create paths unique",
|
||||
len(advanced_creates) == len(set(advanced_creates)),
|
||||
f"{len(advanced_creates)} paths",
|
||||
)
|
||||
check(
|
||||
"stable and advanced create paths do not collide",
|
||||
set(stable_creates).isdisjoint(set(advanced_creates)),
|
||||
str(set(stable_creates) & set(advanced_creates)),
|
||||
)
|
||||
|
||||
design_terms = [
|
||||
"W1",
|
||||
"W2",
|
||||
"W3",
|
||||
"W4",
|
||||
"APPLICATION_COMMITTED",
|
||||
"CLIENT_OBSERVATION_UNKNOWN",
|
||||
"RFC 9457",
|
||||
"OpenAPI 3.1.2",
|
||||
"If-Match",
|
||||
"Idempotency",
|
||||
"202 Accepted",
|
||||
"Tomcat",
|
||||
"Jetty",
|
||||
"Reactor Netty",
|
||||
"Nginx",
|
||||
"business mutation + authoritative evidence same DB transaction",
|
||||
"Redis",
|
||||
"Request Evidence",
|
||||
"Application Evidence",
|
||||
"Response Evidence",
|
||||
]
|
||||
for term in design_terms:
|
||||
check(f"design contains key term: {term}", term in design, term)
|
||||
|
||||
stable_terms = [
|
||||
"same-PostgreSQL-transaction",
|
||||
"Application Commit 후 HTTP Response 유실 Fault Test",
|
||||
"Redis Concurrent Gate와 Replay Cache Adapter",
|
||||
"실제 Nginx Trusted Proxy",
|
||||
"실제 Tomcat MVC HTTP 계약 Gate",
|
||||
"Jetty MVC 호환성 Gate",
|
||||
"실제 Reactor Netty WebFlux 계약 Gate",
|
||||
"OpenAPI 3.1.2 Snapshot",
|
||||
"OpenAPI Breaking Diff",
|
||||
"Durable Operation",
|
||||
"same-PostgreSQL-transaction",
|
||||
"DB commit evidence의 유일한 source가 아니다",
|
||||
"webStableCheck",
|
||||
]
|
||||
for term in stable_terms:
|
||||
check(f"stable plan contains key term: {term}", term in stable, term)
|
||||
|
||||
advanced_terms = [
|
||||
"Virtual Thread",
|
||||
"Controlled Blocking Bridge",
|
||||
"JSON Merge Patch RFC 7396",
|
||||
"JSON Patch RFC 6902",
|
||||
"MVC SSE",
|
||||
"WebFlux SSE",
|
||||
"NDJSON",
|
||||
"JSON Text Sequence",
|
||||
"Messaging-backed SSE Replay",
|
||||
"OpenAPI 3.2 Experimental",
|
||||
"RateLimit Draft",
|
||||
"10k",
|
||||
"rollback",
|
||||
]
|
||||
for term in advanced_terms:
|
||||
check(f"advanced plan contains key term: {term}", term in advanced, term)
|
||||
|
||||
# Stable/advanced dependency boundary.
|
||||
check(
|
||||
"stable module map excludes modules/web-advanced",
|
||||
"modules/web-advanced/" not in stable.split("## 1. Stable 파일·모듈 구조", 1)[1].split("## 2.", 1)[0],
|
||||
"",
|
||||
)
|
||||
check(
|
||||
"advanced plan requires stable completion",
|
||||
"Stable Task 1~58" in advanced,
|
||||
"",
|
||||
)
|
||||
|
||||
# Evidence and idempotency invariants.
|
||||
for text_name, text in [("design", design), ("stable", stable)]:
|
||||
check(
|
||||
f"{text_name} separates ETag and idempotency",
|
||||
"ETag/If-Match" in text and "Idempotency" in text,
|
||||
"",
|
||||
)
|
||||
check(
|
||||
f"{text_name} says Redis is not sole commit evidence",
|
||||
("sole DB commit evidence" in text)
|
||||
or ("유일한 source" in text)
|
||||
or ("유일한 Source" in text),
|
||||
"",
|
||||
)
|
||||
check(
|
||||
f"{text_name} includes commit-response-loss",
|
||||
("Response Loss" in text)
|
||||
or ("response-loss" in text)
|
||||
or ("response write 전 TCP reset" in text),
|
||||
"",
|
||||
)
|
||||
|
||||
# No unsupported architecture in design/plan.
|
||||
for name, text in [("design", design), ("stable", stable)]:
|
||||
check(
|
||||
f"{name} forbids controller transaction",
|
||||
"Controller transaction" in text or "Controller @Transactional" in text or "Controller 또는 HTTP adapter에 업무 `@Transactional`" in text,
|
||||
"",
|
||||
)
|
||||
check(
|
||||
f"{name} forbids entity/document wire types",
|
||||
"Entity/Document" in text or "Entity·Document" in text or "JPA Entity·MongoDB Document" in text,
|
||||
"",
|
||||
)
|
||||
check(
|
||||
f"{name} does not declare Idempotency-Key as final RFC",
|
||||
"IETF 표준" not in text or "금지" in text,
|
||||
"",
|
||||
)
|
||||
|
||||
# Research grounding.
|
||||
check(
|
||||
"design title matches research topic",
|
||||
"인바운드 HTTP API 실행 플랫폼" in design and "인바운드 HTTP API 실행 플랫폼" in research,
|
||||
"",
|
||||
)
|
||||
check(
|
||||
"research includes execution evidence chain",
|
||||
"HTTP_RECEIVED" in research and "CLIENT_OBSERVATION_UNKNOWN" in research,
|
||||
"",
|
||||
)
|
||||
check(
|
||||
"research includes actual server matrix",
|
||||
"MVC + Tomcat" in research and "WebFlux + Reactor Netty" in research,
|
||||
"",
|
||||
)
|
||||
|
||||
# Optional package integrity.
|
||||
manifest = ROOT / "MANIFEST.sha256"
|
||||
if manifest.exists():
|
||||
for line in manifest.read_text(encoding="utf-8").splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
digest, relative = line.split(" ", 1)
|
||||
target = ROOT / relative
|
||||
actual = hashlib.sha256(target.read_bytes()).hexdigest() if target.exists() else ""
|
||||
check(f"manifest: {relative}", actual == digest, actual)
|
||||
|
||||
passed = sum(1 for _, ok, _ in checks if ok)
|
||||
failed = [(name, detail) for name, ok, detail in checks if not ok]
|
||||
|
||||
print(f"checks={len(checks)} passed={passed} failed={len(failed)}")
|
||||
for name, detail in failed:
|
||||
print(f"FAIL: {name} :: {detail}")
|
||||
|
||||
sys.exit(1 if failed else 0)
|
||||
Reference in New Issue
Block a user