103 lines
6.2 KiB
Python
103 lines
6.2 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=[]; warnings=[]
|
|
def fail(x): errors.append(x)
|
|
def warn(x): warnings.append(x)
|
|
def sha(p):
|
|
h=hashlib.sha256()
|
|
with p.open('rb') as f:
|
|
for b in iter(lambda:f.read(1024*1024),b''): h.update(b)
|
|
return h.hexdigest()
|
|
def rows(rel):
|
|
p=root/rel
|
|
if not p.exists(): fail(f'missing {rel}'); return [],[]
|
|
with p.open(encoding='utf-8-sig',newline='') as f:
|
|
r=csv.DictReader(f); return r.fieldnames or [],list(r)
|
|
|
|
# Parse JSON and basic source tokens.
|
|
for p in root.rglob('*.json'):
|
|
try: json.loads(p.read_text(encoding='utf-8-sig'))
|
|
except Exception as e: fail(f'JSON {p.relative_to(root)}: {e}')
|
|
for p in (root/'frontend/src').rglob('*.vue'):
|
|
t=p.read_text(encoding='utf-8')
|
|
if t.count('<template') != t.count('</template>'): fail(f'unbalanced SFC template {p.relative_to(root)}')
|
|
for base in [root/'frontend/src',root/'src',root/'db/migrations',root/'docs/v15_0']:
|
|
for p in base.rglob('*'):
|
|
if p.is_file() and p.suffix.lower() in {'.ts','.vue','.cs','.sql','.md'}:
|
|
t=p.read_text(encoding='utf-8')
|
|
if re.search(r'__[A-Z][A-Z0-9_]+__',t): fail(f'unresolved token {p.relative_to(root)}')
|
|
|
|
# CSV cardinality and required fields.
|
|
checks={
|
|
'docs/v15_0/08_DETAILED_WBS_MASTER.csv':(576,'WBS_ID'),
|
|
'docs/v15_0/TECH_DEBT_REGISTER.csv':(124,'ID'),
|
|
'docs/v15_0/DECISION_LOG.csv':(80,'Decision_ID'),
|
|
'docs/v15_0/TRACEABILITY_MATRIX.csv':(101,'Requirement_ID'),
|
|
'docs/v15_0/FE_COMPONENT_CATALOGUE.csv':(43,'ID'),
|
|
'docs/v15_0/JOB_CATALOGUE.csv':(26,'Job_ID'),
|
|
'docs/v15_0/SOURCE_COVERAGE_MATRIX.csv':(4,'File')}
|
|
for rel,(count,key) in checks.items():
|
|
fields,data=rows(rel)
|
|
if len(data)!=count: fail(f'{rel} count {len(data)} != {count}')
|
|
vals=[x.get(key,'').strip() for x in data]
|
|
if any(not x for x in vals): fail(f'{rel} blank {key}')
|
|
if key!='Requirement_ID' and len(vals)!=len(set(vals)): fail(f'{rel} duplicate {key}')
|
|
fields,wbs=rows('docs/v15_0/08_DETAILED_WBS_MASTER.csv')
|
|
for col in ['Task','Artifact','Acceptance_Evidence','Primary_Owner','Secondary','PD','Dependency','Gate','Evidence_Class','Risk','Status']:
|
|
if any(not x.get(col,'').strip() for x in wbs): fail(f'blank WBS {col}')
|
|
if sum(x['WBS_ID'].startswith('AEG-V15-') for x in wbs)!=96: fail('v15 WBS delta must be 96')
|
|
|
|
# UI adapter v3 and vendor boundary.
|
|
contract=json.loads((root/'contracts/ui/ui-adapter.v3.json').read_text())
|
|
if contract.get('contractVersion')!='3.0': fail('UI adapter contract not v3')
|
|
if len(contract.get('requiredCapabilities',[]))!=14: fail('UI adapter capability count not 14')
|
|
for name in ['KsButton','KsTextField','KsTextArea','KsSelect','KsMultiSelect','KsCheckbox','KsDateField','KsNumberField','KsDialog','KsStatusTag','KsInlineMessage','KsPaginator','KsTabs','KsDataGrid']:
|
|
if not (root/f'frontend/src/shared/ui/components/{name}.vue').exists(): fail(f'missing {name}')
|
|
for p in (root/'frontend/src').rglob('*'):
|
|
if p.suffix not in {'.ts','.vue'}: continue
|
|
t=p.read_text(encoding='utf-8')
|
|
if re.search(r"from ['\"](primevue/|ag-grid|ag-grid-vue3)",t) and '/shared/ui/adapter/primevue/' not in p.as_posix():
|
|
fail(f'vendor import outside adapter {p.relative_to(root)}')
|
|
for rel in ['frontend/src/shared/crud/contracts.ts','frontend/src/shared/crud/queryCodec.ts','frontend/src/shared/crud/useCrudListState.ts','frontend/src/shared/crud/StandardCrudListPage.vue','frontend/src/shared/crud/StandardCrudFormPage.vue','frontend/src/shared/ui/layouts/CrudWorkspaceLayout.vue','frontend/src/shared/ui/layouts/OperationsConsoleLayout.vue']:
|
|
if not (root/rel).exists(): fail(f'missing CRUD/layout {rel}')
|
|
|
|
# Scheduler/model operations hardening.
|
|
registry=(root/'src/KArtSell.Modules.ModelOperations/Domain/ModelOperationDefinition.cs').read_text()
|
|
for code in ['J31','J32','J33','J34','J35','J36','J37','J38','J39','J40']:
|
|
if f'"{code}"' not in registry: fail(f'missing registry {code}')
|
|
for rel in ['src/KArtSell.Modules.ModelOperations/Domain/ScheduleOccurrencePlanner.cs','src/KArtSell.Modules.ModelOperations/Domain/EvaluationWindowPlanner.cs','src/KArtSell.Modules.ModelOperations/Domain/ModelOperationExecution.cs','src/KArtSell.Modules.ModelOperations/Domain/MetricDefinitionVersion.cs']:
|
|
if not (root/rel).exists(): fail(f'missing domain hardening {rel}')
|
|
mig=(root/'db/migrations/0020_v15_execution_and_ui_contract_hardening.sql').read_text()
|
|
for marker in ['dispatch_revision','prediction_evaluation_window','metric_cohort_definition','ui_contract_release',"'J40'",'false']:
|
|
if marker not in mig: fail(f'migration missing {marker}')
|
|
if 'automatic model activation' not in mig.lower(): fail('migration automation boundary missing')
|
|
|
|
# Prohibited capabilities and AI truth.
|
|
for rel in ['src/KArtSell.Host/appsettings.json','src/KArtSell.Host/appsettings.Development.json']:
|
|
t=(root/rel).read_text()
|
|
if re.search(r'"AutomaticOrder"\s*:\s*true',t,re.I) or re.search(r'"KisOrderAdapter"\s*:\s*true',t,re.I): fail(f'forbidden capability enabled {rel}')
|
|
|
|
# Source attachment bytes.
|
|
idx=root/'attachments/current_session/SOURCE_INDEX_V15_0.json'
|
|
if not idx.exists(): fail('missing source index')
|
|
else:
|
|
data=json.loads(idx.read_text())
|
|
if len(data.get('files',[]))!=4 or data.get('all_match') is not True: fail('source index incomplete')
|
|
for item in data.get('files',[]):
|
|
p=root/item['Relative_Path']
|
|
if not p.exists(): fail(f'missing source {item["File"]}')
|
|
elif p.stat().st_size!=item['Size'] or sha(p)!=item['SHA256']: fail(f'source mismatch {item["File"]}')
|
|
|
|
# Required release files.
|
|
for rel in ['global.json','RELEASE_NOTES_V15_0.md','PACKAGE_POLICY_V15_0.md','contracts/schedules/model-operations.v3.json','contracts/ui/crud-page.v1.json','contracts/ui/layout-regions.v1.json']:
|
|
if not (root/rel).exists(): fail(f'missing release artifact {rel}')
|
|
if not (root/'frontend/pnpm-lock.yaml').exists(): warn('pnpm-lock.yaml missing; frozen install cannot be claimed')
|
|
warn('.NET 10 build, PostgreSQL DbUp, pnpm/Vitest/Playwright and 252-session Shadow require approved runtime')
|
|
print(f'PASS={0 if errors else 1} WARN={len(warnings)} FAIL={len(errors)}')
|
|
for x in warnings: print('WARN',x)
|
|
for x in errors: print('FAIL',x)
|
|
sys.exit(1 if errors else 0)
|