Initial commit: Add project files
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
TEMPLATES = {
|
||||
'list': 'CrudListPage.vue.template',
|
||||
'form': 'CrudFormPage.vue.template',
|
||||
'detail': 'CrudDetailPage.vue.template',
|
||||
'review': 'CrudReviewPage.vue.template',
|
||||
}
|
||||
|
||||
def pascal(value: str) -> str:
|
||||
parts = re.findall(r'[A-Za-z0-9]+', value)
|
||||
if not parts:
|
||||
raise ValueError('feature must contain letters or digits')
|
||||
return ''.join(x[:1].upper() + x[1:] for x in parts)
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description='Scaffold a K-ArtSell standard UI screen without vendor imports.')
|
||||
parser.add_argument('--feature', required=True)
|
||||
parser.add_argument('--kind', choices=sorted(TEMPLATES), required=True)
|
||||
parser.add_argument('--title', required=True)
|
||||
parser.add_argument('--force', action='store_true')
|
||||
args = parser.parse_args()
|
||||
|
||||
feature = pascal(args.feature)
|
||||
template = (ROOT / 'templates/vue/screens' / TEMPLATES[args.kind]).read_text(encoding='utf-8')
|
||||
output = ROOT / 'frontend/src/features' / re.sub(r'(?<!^)(?=[A-Z])', '-', feature).lower() / 'pages' / f'{feature}Page.vue'
|
||||
if output.exists() and not args.force:
|
||||
raise SystemExit(f'refusing to overwrite {output.relative_to(ROOT)}; pass --force')
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(template.replace('__TITLE__', args.title).replace('__CONTRACT_VERSION__', 'UI-CONTRACT-2.0'), encoding='utf-8')
|
||||
print(output.relative_to(ROOT))
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
import argparse, re
|
||||
from pathlib import Path
|
||||
|
||||
def valid_identifier(value: str) -> str:
|
||||
if not re.fullmatch(r'[A-Za-z][A-Za-z0-9]*', value):
|
||||
raise argparse.ArgumentTypeError('Use PascalCase alphanumeric identifiers only.')
|
||||
return value
|
||||
|
||||
def render_tree(source: Path, target: Path, tokens: dict[str, str], dry_run: bool) -> list[Path]:
|
||||
generated: list[Path] = []
|
||||
for item in source.rglob('*'):
|
||||
if item.is_dir(): continue
|
||||
relative = Path(str(item.relative_to(source)).replace('__MODULE__', tokens['MODULE']).replace('__FEATURE__', tokens['FEATURE']))
|
||||
output = target / relative
|
||||
content = item.read_text(encoding='utf-8')
|
||||
for key, value in tokens.items(): content = content.replace(f'__{key}__', value)
|
||||
if '__' in content: raise RuntimeError(f'Unresolved template token in {item}')
|
||||
generated.append(output)
|
||||
if not dry_run:
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
if output.exists(): raise FileExistsError(f'Refusing to overwrite {output}')
|
||||
output.write_text(content, encoding='utf-8')
|
||||
return generated
|
||||
|
||||
def main() -> int:
|
||||
parser=argparse.ArgumentParser(description='Generate a K-ArtSell Vertical Slice backend/frontend packet.')
|
||||
parser.add_argument('--module', required=True, type=valid_identifier)
|
||||
parser.add_argument('--feature', required=True, type=valid_identifier)
|
||||
parser.add_argument('--slice-id', required=True, help='e.g. VS-14')
|
||||
parser.add_argument('--root', type=Path, default=Path.cwd())
|
||||
parser.add_argument('--dry-run', action='store_true')
|
||||
args=parser.parse_args()
|
||||
template_root=Path(__file__).resolve().parents[1] / 'templates' / 'vertical-slice'
|
||||
tokens={'MODULE':args.module,'FEATURE':args.feature,'SLICE_ID':args.slice_id}
|
||||
targets=[(template_root/'backend',args.root/'src'/f'KArtSell.Modules.{args.module}'/'Features'/args.feature),(template_root/'frontend',args.root/'frontend'/'src'/'features'/re.sub(r'(?<!^)(?=[A-Z])','-',args.feature).lower())]
|
||||
generated=[]
|
||||
for source,target in targets: generated.extend(render_tree(source,target,tokens,args.dry_run))
|
||||
for path in generated: print(path)
|
||||
return 0
|
||||
if __name__=='__main__': raise SystemExit(main())
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/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)
|
||||
@@ -0,0 +1,138 @@
|
||||
#!/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)
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/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)
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
from pathlib import Path
|
||||
import csv, hashlib, json, re, sys, zipfile
|
||||
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)
|
||||
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/v16_0']:
|
||||
for p in base.rglob('*'):
|
||||
if p.is_file() and p.suffix.lower() in {'.ts','.vue','.cs','.sql','.md'} and re.search(r'__[A-Z][A-Z0-9_]+__',p.read_text(encoding='utf-8')): fail(f'unresolved token {p.relative_to(root)}')
|
||||
checks={'docs/v16_0/08_DETAILED_WBS_MASTER.csv':(664,'WBS_ID'),'docs/v16_0/TECH_DEBT_REGISTER.csv':(148,'ID'),'docs/v16_0/DECISION_LOG.csv':(96,'Decision_ID'),'docs/v16_0/TRACEABILITY_MATRIX.csv':(121,'Requirement_ID'),'docs/v16_0/FE_COMPONENT_CATALOGUE.csv':(58,'ID'),'docs/v16_0/JOB_CATALOGUE.csv':(28,'Job_ID'),'docs/v16_0/SOURCE_COVERAGE_MATRIX.csv':(4,'File')}
|
||||
for rel,(count,key) in checks.items():
|
||||
_,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}')
|
||||
_,wbs=rows('docs/v16_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-V16-') for x in wbs)!=88: fail('v16 WBS delta must be 88')
|
||||
contract=json.loads((root/'contracts/ui/ui-adapter.v4.json').read_text())
|
||||
if contract.get('contractVersion')!='4.0' or len(contract.get('requiredCapabilities',[]))!=14: fail('UI adapter v4 contract invalid')
|
||||
for rel in ['frontend/src/shared/ui/adapter/compatibility.ts','frontend/src/shared/ui/components/FieldShell.vue','frontend/src/shared/ui/components/KsDataContextHeader.vue','frontend/src/shared/ui/components/KsCommandBar.vue','frontend/src/shared/crud/resourceDefinition.ts','frontend/src/shared/crud/useOptimisticCommand.ts']:
|
||||
if not (root/rel).exists(): fail(f'missing FE v16 {rel}')
|
||||
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)}')
|
||||
reg=(root/'src/KArtSell.Modules.ModelOperations/Domain/ModelOperationDefinition.cs').read_text()
|
||||
for code in ['J41','J42']:
|
||||
if f'"{code}"' not in reg: fail(f'missing registry {code}')
|
||||
for rel in ['src/KArtSell.BuildingBlocks/Execution/ExecutionEnvelope.cs','src/KArtSell.BuildingBlocks/ReadModels/ProjectionContract.cs','src/KArtSell.Modules.ModelOperations/Domain/ModelOperationLease.cs','src/KArtSell.Modules.ModelOperations/Domain/EvaluationReconciliationPlanner.cs','src/KArtSell.Modules.SignalEngine/Domain/SellDecisionEvidenceGuard.cs']:
|
||||
if not (root/rel).exists(): fail(f'missing domain v16 {rel}')
|
||||
mig=(root/'db/migrations/0021_v16_reference_implementation_closure.sql').read_text()
|
||||
for marker in ['model_operation_lease','fencing_token','model_evaluation_reconciliation','projection_rebuild_request','ui_adapter_compatibility_evidence',"'J41'","'J42'",'false']:
|
||||
if marker not in mig: fail(f'0021 missing {marker}')
|
||||
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}')
|
||||
idx=root/'attachments/current_session/SOURCE_INDEX_V16_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() or p.stat().st_size!=item['Size'] or sha(p)!=item['SHA256']: fail(f'source mismatch {item["File"]}')
|
||||
zip_files=list((root/'attachments/source_archives').glob('*.zip'))
|
||||
if len(zip_files)!=1: fail(f'Full source archive count {len(zip_files)} != 1')
|
||||
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, scheduler chaos 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)
|
||||
Reference in New Issue
Block a user