139 lines
7.8 KiB
Python
139 lines
7.8 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) -> tuple[list[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:
|
|
reader = csv.DictReader(stream)
|
|
return reader.fieldnames or [], list(reader)
|
|
|
|
# JSON and SFC/static source sanity.
|
|
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').rglob('*.vue'):
|
|
text = path.read_text(encoding='utf-8')
|
|
if text.count('<template') != text.count('</template>'):
|
|
fail(f'unbalanced template tags: {path.relative_to(root)}')
|
|
if '<script setup' not in text and '<script>' not in text:
|
|
# Pure layout SFCs may be template-only.
|
|
if 'defineProps' in text or 'defineEmits' in text:
|
|
fail(f'missing script block: {path.relative_to(root)}')
|
|
|
|
for base in (root/'frontend/src/shared/ui', root/'src/KArtSell.Modules.ModelOperations', root/'docs/v14_0'):
|
|
if not base.exists(): continue
|
|
for path in base.rglob('*'):
|
|
if path.is_file() and path.suffix.lower() in {'.ts','.vue','.cs','.md','.sql'}:
|
|
text=path.read_text(encoding='utf-8')
|
|
if re.search(r'__[A-Z][A-Z0-9_]+__', text): fail(f'unresolved generation token: {path.relative_to(root)}')
|
|
|
|
# CSV master counts and keys.
|
|
checks = {
|
|
'docs/v14_0/08_DETAILED_WBS_MASTER.csv': (480, 'WBS_ID'),
|
|
'docs/v14_0/TECH_DEBT_REGISTER.csv': (100, 'ID'),
|
|
'docs/v14_0/DECISION_LOG.csv': (62, 'Decision_ID'),
|
|
'docs/v14_0/TRACEABILITY_MATRIX.csv': (81, 'Requirement_ID'),
|
|
'docs/v14_0/FE_COMPONENT_CATALOGUE.csv': (31, 'ID'),
|
|
'docs/v14_0/JOB_CATALOGUE.csv': (25, 'Job_ID'),
|
|
'docs/v14_0/SOURCE_COVERAGE_MATRIX.csv': (3, 'File'),
|
|
}
|
|
for relative,(expected,key) in checks.items():
|
|
fields, rows = read_csv(relative)
|
|
if len(rows) != expected: fail(f'{relative} count {len(rows)} != {expected}')
|
|
values=[r.get(key,'') for r in rows]
|
|
if any(not x for x in values): fail(f'{relative} blank {key}')
|
|
if key != 'Requirement_ID' and len(values)!=len(set(values)): fail(f'{relative} duplicate {key}')
|
|
|
|
_, wbs = read_csv('docs/v14_0/08_DETAILED_WBS_MASTER.csv')
|
|
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}')
|
|
if sum(1 for r in wbs if r.get('WBS_ID','').startswith('AEG-V14-')) != 80: fail('v14 WBS delta must contain 80 rows')
|
|
|
|
# UI provider boundary and assets.
|
|
contracts=json.loads((root/'contracts/ui/ui-adapter.v2.json').read_text(encoding='utf-8'))
|
|
if contracts.get('contractVersion')!='2.0': fail('UI adapter contract version is not 2.0')
|
|
if len(contracts.get('requiredCapabilities',[]))!=10: fail('UI adapter capability count is not 10')
|
|
for name in ('KsButton','KsTextField','KsTextArea','KsSelect','KsCheckbox','KsDateField','KsNumberField','KsDialog','KsStatusTag','KsDataGrid'):
|
|
if not (root/f'frontend/src/shared/ui/components/{name}.vue').exists(): fail(f'missing standard component {name}')
|
|
for provider in ('primevue','native'):
|
|
if not (root/f'frontend/src/shared/ui/adapter/{provider}/index.ts').exists(): fail(f'missing provider {provider}')
|
|
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|ag-grid-vue3)", text):
|
|
posix=path.as_posix()
|
|
if '/shared/ui/adapter/primevue/' not in posix: fail(f'vendor import outside adapter: {path.relative_to(root)}')
|
|
|
|
screen_contract=json.loads((root/'contracts/ui/screen-types.v2.json').read_text(encoding='utf-8'))
|
|
if len(screen_contract.get('templates',[])) != 10: fail('screen contract does not define 10 templates')
|
|
for item in screen_contract.get('templates',[]):
|
|
component=item['component']
|
|
if not (root/f'frontend/src/shared/ui/screen-types/v2/{component}.vue').exists(): fail(f'missing screen component {component}')
|
|
catalogue=(root/'frontend/src/shared/ui/screen-types/catalogue.ts').read_text(encoding='utf-8')
|
|
for number in range(1,11):
|
|
if f"id: 'T{number:02d}'" not in catalogue: fail(f'missing catalogue T{number:02d}')
|
|
|
|
# Model feedback registry, boundary and SQL.
|
|
registry=(root/'src/KArtSell.Modules.ModelOperations/Domain/ModelOperationDefinition.cs').read_text(encoding='utf-8')
|
|
contract=json.loads((root/'contracts/schedules/model-operations.v2.json').read_text(encoding='utf-8'))
|
|
migration=(root/'db/migrations/0019_v14_governed_feedback_cycle.sql').read_text(encoding='utf-8')
|
|
for number in range(31,40):
|
|
code=f'J{number}'
|
|
if f'"{code}"' not in registry: fail(f'missing model operation registry {code}')
|
|
if not any(x.get('operationCode')==code and x.get('enabledByDefault') is False for x in contract.get('operations',[])): fail(f'missing disabled contract {code}')
|
|
if "'J39'" not in migration or ',1,false,' not in migration: fail('J39 migration is not explicitly disabled')
|
|
for marker in ('model_feedback_cycle','model_feedback_transition','model_hypothesis_evidence','model_activation_decision','HUMAN_CHANGE_APPROVAL','prevent_append_only_change'):
|
|
if marker not in migration: fail(f'missing migration marker {marker}')
|
|
plan=(root/'src/KArtSell.Modules.ModelOperations/FeedbackLoop/ModelFeedbackPlan.cs').read_text(encoding='utf-8')
|
|
for marker in ('NO_AUTOMATIC_MODEL_ACTIVATION','AUTOMATION_FORBIDDEN','MANUAL_ONLY'):
|
|
if marker not in plan: fail(f'missing feedback boundary {marker}')
|
|
|
|
# Current attachments byte-for-byte.
|
|
index_path=root/'attachments/current_session/SOURCE_INDEX_V14_0.json'
|
|
if not index_path.exists(): fail('missing SOURCE_INDEX_V14_0.json')
|
|
else:
|
|
index=json.loads(index_path.read_text(encoding='utf-8'))
|
|
if len(index.get('files',[]))!=3 or index.get('all_match') is not True: fail('source index incomplete')
|
|
for item in index.get('files',[]):
|
|
path=root/'attachments/current_session'/item['file']
|
|
if not path.exists(): fail(f'missing source attachment {item["file"]}')
|
|
elif path.stat().st_size != item['size'] or sha256(path) != item['sha256']: fail(f'attachment mismatch {item["file"]}')
|
|
|
|
# Required v14 artifacts.
|
|
for relative in (
|
|
'tools/scaffold_ui_screen.py', 'tools/validate_v14.py',
|
|
'contracts/ui/ui-adapter.v2.json', 'contracts/ui/screen-types.v2.json',
|
|
'db/migrations/0019_v14_governed_feedback_cycle.sql',
|
|
'src/KArtSell.Modules.ModelOperations/FeedbackLoop/ModelFeedbackCycle.cs',
|
|
'src/KArtSell.Modules.ModelOperations/FeedbackLoop/ModelFeedbackPlan.cs',
|
|
'src/KArtSell.Modules.ModelOperations/FeedbackLoop/ModelImprovementHypothesis.cs',
|
|
'tests/KArtSell.ModelOperations.UnitTests/ModelFeedbackCycleTests.cs',
|
|
'docs/v14_0/00_EXECUTIVE_CRITICAL_PROPOSAL.md'):
|
|
if not (root/relative).exists(): fail(f'missing v14 artifact {relative}')
|
|
|
|
if not (root/'frontend/pnpm-lock.yaml').exists(): warn('pnpm-lock.yaml missing; frozen frontend validation not possible')
|
|
warn('.NET 10, PostgreSQL, pnpm and Playwright runtime validation require 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)
|