Files
KArtSell.Aegis/tools/validate_v13.py
kjh2064 dcd1322d41
ci / backend (push) Failing after 12s
ci / frontend (push) Failing after 19s
ci / static (push) Failing after 45s
Initial commit: Add project files
2026-08-02 05:15:36 +09:00

130 lines
7.0 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
from pathlib import Path
import csv, hashlib, json, re, sys
root = Path(__file__).resolve().parents[1]
errors: list[str] = []
warnings: list[str] = []
def fail(message: str) -> None: errors.append(message)
def warn(message: str) -> None: warnings.append(message)
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open('rb') as stream:
for block in iter(lambda: stream.read(1024 * 1024), b''):
digest.update(block)
return digest.hexdigest()
def read_csv(relative: str) -> list[dict[str, str]]:
path = root / relative
if not path.exists():
fail(f'missing CSV {relative}')
return []
with path.open(encoding='utf-8-sig', newline='') as stream:
return list(csv.DictReader(stream))
# JSON syntax and unresolved generation tokens.
for path in root.rglob('*.json'):
try:
json.loads(path.read_text(encoding='utf-8-sig'))
except Exception as exc:
fail(f'JSON {path.relative_to(root)}: {exc}')
for path in [root/'frontend/src/shared/ui', root/'docs/v13_0']:
if not path.exists(): continue
for item in path.rglob('*'):
if item.is_file() and item.suffix.lower() in {'.ts','.vue','.cs','.md','.sql'}:
text=item.read_text(encoding='utf-8')
if re.search(r'__[A-Z][A-Z0-9_]+__', text):
fail(f'unresolved template token: {item.relative_to(root)}')
# WBS/debt/decision integrity.
wbs = read_csv('docs/v13_0/08_DETAILED_WBS_MASTER.csv')
if len(wbs) != 400: fail(f'WBS count {len(wbs)} != 400')
wbs_ids = [row.get('WBS_ID','') for row in wbs]
if any(not value for value in wbs_ids): fail('blank WBS ID')
if len(wbs_ids) != len(set(wbs_ids)): fail('duplicate WBS IDs')
for required in ('Task','Artifact','Acceptance_Evidence','Primary_Owner','PD','Gate','Risk','Status'):
if any(not row.get(required,'').strip() for row in wbs): fail(f'blank WBS field: {required}')
debts = read_csv('docs/v13_0/TECH_DEBT_REGISTER.csv')
if len(debts) != 80: fail(f'tech debt count {len(debts)} != 80')
decisions = read_csv('docs/v13_0/DECISION_LOG.csv')
if len(decisions) != 52: fail(f'decision count {len(decisions)} != 52')
# FE catalogue, adapter boundary and standard assets.
catalogue = root/'frontend/src/shared/ui/screen-types/catalogue.ts'
if not catalogue.exists(): fail('missing FE screen catalogue')
else:
text=catalogue.read_text(encoding='utf-8')
for number in range(1,11):
if f"id: 'T{number:02d}'" not in text: fail(f'missing T{number:02d}')
for name in ('AppShellLayout','PageLayout','ReviewWorkbenchLayout','FormPageLayout','DashboardLayout'):
if not (root/f'frontend/src/shared/ui/layouts/{name}.vue').exists(): fail(f'missing layout {name}')
for name in ('KsButton','KsTextField','KsTextArea','KsSelect','KsCheckbox','KsDialog','KsStatusTag','KsDataGrid'):
if not (root/f'frontend/src/shared/ui/components/{name}.vue').exists(): fail(f'missing standard component {name}')
for path in (root/'frontend/src').rglob('*'):
if path.suffix not in ('.ts','.vue'): continue
text=path.read_text(encoding='utf-8')
if re.search(r"from ['\"](primevue/|ag-grid)", text) and '/shared/ui/adapter/primevue/' not in path.as_posix():
fail(f'vendor import outside adapter: {path.relative_to(root)}')
# Model-operation registry/contract/migration fail-closed checks.
registry_path=root/'src/KArtSell.Modules.ModelOperations/Domain/ModelOperationDefinition.cs'
registry=registry_path.read_text(encoding='utf-8') if registry_path.exists() else ''
contract_path=root/'contracts/schedules/model-operations.v2.json'
contract=json.loads(contract_path.read_text(encoding='utf-8')) if contract_path.exists() else {}
migration_path=root/'db/migrations/0018_v13_model_feedback_loop.sql'
migration=migration_path.read_text(encoding='utf-8') if migration_path.exists() else ''
for number in range(31,39):
code=f'J{number}'
if f'"{code}"' not in registry: fail(f'missing registry {code}')
if not any(item.get('operationCode')==code and item.get('enabledByDefault') is False for item in contract.get('operations',[])):
fail(f'missing disabled contract {code}')
if f"'{code}'" not in migration: fail(f'missing migration schedule {code}')
if 'enabled, next_due_at' not in migration or migration.count(',2,false,') < 8: fail('J31-J38 schedules are not explicitly disabled')
for marker in ('maturity_revision','supersedes_maturity_id','supersedes_hypothesis_id','prevent_append_only_change'):
if marker not in migration: fail(f'missing append-only marker {marker}')
boundary=(root/'src/KArtSell.Modules.ModelOperations/Domain/ModelOperationExecutionBoundary.cs').read_text(encoding='utf-8')
if 'NO_AUTO_MODEL_OR_ORDER_MUTATION' not in boundary: fail('missing model operation fail-closed boundary')
for forbidden in contract.get('automationBoundary',{}).get('forbidden',[]):
if forbidden not in {'AUTO_PROMOTE','AUTO_ROLLBACK','AUTO_PARAMETER_CHANGE','AUTO_ORDER','KIS_SUBMISSION','CLIENT_PUBLICATION'}:
fail(f'unexpected automation boundary token {forbidden}')
# Current-request attachment byte-for-byte index.
index_path=root/'attachments/SOURCE_INDEX_V13_0.json'
if not index_path.exists(): fail('missing SOURCE_INDEX_V13_0.json')
else:
index=json.loads(index_path.read_text(encoding='utf-8'))
files=index.get('files',[])
if len(files)!=5 or index.get('all_match') is not True: fail('current request attachment index is incomplete')
for item in files:
package_path=root/item['package_path']
if not package_path.exists(): fail(f"missing attachment {item['package_path']}")
elif package_path.stat().st_size != item['size'] or sha256(package_path) != item['sha256']:
fail(f"attachment hash mismatch {item['package_path']}")
# Scaffolder and document evidence files.
for relative in (
'tools/scaffold_vertical_slice.py',
'templates/vertical-slice/backend/Endpoint.cs',
'templates/vertical-slice/frontend/pages/__FEATURE__Page.vue',
'SCAFFOLD_VALIDATION_V13_0.txt',
'FRONTEND_SYNTAX_VALIDATION_V13_0.txt',
'DOCX_QA_V13_0.txt',
'docs/v13_0/K-ArtSell_Aegis_v13_0_표준UI_지속모델운영_통합고도화_제안서.docx'):
if not (root/relative).exists(): fail(f'missing evidence/artifact {relative}')
syntax_path=root/'FRONTEND_SYNTAX_VALIDATION_V13_0.txt'
if syntax_path.exists() and 'SYNTAX_ERRORS=0' not in syntax_path.read_text(encoding='utf-8'): fail('frontend syntax validation not clean')
qa_path=root/'DOCX_QA_V13_0.txt'
if qa_path.exists() and 'Visual inspection: PASS' not in qa_path.read_text(encoding='utf-8'): fail('DOCX visual QA not passed')
# Runtime evidence remains deliberately external.
if not (root/'frontend/pnpm-lock.yaml').exists(): warn('pnpm-lock.yaml missing; frozen frontend validation not possible')
warn('.NET SDK/PostgreSQL runtime validation must be executed in approved CI')
print(f'PASS={0 if errors else 1} WARN={len(warnings)} FAIL={len(errors)}')
for message in warnings: print('WARN', message)
for message in errors: print('FAIL', message)
sys.exit(1 if errors else 0)