← Files Unix CopilotARCHIVED FILE

scripts/validate_package.py

4.18 KB · Nov 7, 2026 · 12:49 UTC

↓ Download file

See the change to this file →

#!/usr/bin/env python3
"""Validate portable ZIP source layout and supported metadata; not runtime QA."""
from pathlib import Path
import json,re,sys
R=Path(__file__).resolve().parents[1]
errors=[];checks=0

def ck(cond,msg):
 global checks
 checks+=1
 if not cond:errors.append(msg)

def read_json(p):
 try:return json.loads((R/p).read_text(encoding='utf-8'))
 except Exception as e:errors.append(f"Could not read {p}: {e}");return {}

m=read_json('plugin.json');v=m.get('version');i=m.get('extensions',{}).get('com.openai',{}).get('interface',{})
ck(m.get('name')=='unix-copilot','internal package name must remain unix-copilot')
ck(v=='0.2.0','version must be 0.2.0')
ck(m.get('author',{}).get('name')=='Tekris Labs','author mismatch')
ck(i.get('displayName')=='Unix Copilot','public name mismatch')
ck(i.get('developerName')=='Tekris Labs','developer mismatch')
ck(m.get('skills','').strip('/').startswith('./skills'),'skills root missing')
ck(len(i.get('defaultPrompt',[]))==3,'exactly three prompts required')
for prompt in i.get('defaultPrompt',[]):ck(len(prompt)<=128,'prompt >128 chars')
ck(0<len(i.get('shortDescription',''))<30,'subtitle must be below 30 chars')
for field,url in [('websiteURL','https://tekrislabs.com/'),('supportURL','https://tekrislabs.com/contact'),('privacyPolicyURL','https://tekrislabs.com/privacy'),('termsOfServiceURL','https://tekrislabs.com/terms')]:ck(i.get(field)==url,f'{field} missing/incorrect')
for field in ['logo','composerIcon']:
 s=i.get(field,''); ck(s.startswith('./assets/') and (R/s).is_file(),f'{field} local asset invalid')
 ck((R/s).read_bytes().lstrip().startswith(b'<svg') or b'<svg' in (R/s).read_bytes()[:500],f'{field} not svg')
ck(not (R/'mcp.json').exists(),'skills-only package unexpectedly has MCP server config')
ck((R/'.codex-plugin/plugin.json').exists(),'compat manifest missing')
l=read_json('.codex-plugin/plugin.json')
ck(l.get('name')==m.get('name') and l.get('version')==v,'compat name/version mismatch')
ck(l.get('interface',{}).get('developerName')=='Tekris Labs','compat developer mismatch')
for iconfield in ['logo','composerIcon']:
 ck((R/'.codex-plugin'/l.get('interface',{}).get(iconfield,'')).is_file(),f'compat {iconfield} asset missing')
ck(l.get('interface',{}).get('defaultPrompt')==i.get('defaultPrompt'),'compat prompts mismatch')
active=list((R/'skills').glob('*/SKILL.md'))
ck(len(active)==19,f'expected 19 skills, found {len(active)}')
for skill in active:
 t=skill.read_text(encoding='utf-8')
 ck(t.startswith('---\n') and t.find('\n---\n',4)>0, f'frontmatter missing in {skill}')
 name=skill.parent.name
 ck(re.search(r'(?m)^name: '+re.escape(name)+r'\s*$',t) is not None,f'frontmatter name mismatch in {skill}')
 ck('## Procedure' in t and '## Deliverable' in t,f'skill lacks workflow/outputs in {skill}')
 for rel in re.findall(r'\.{2}/\.{2}/references/([a-z0-9-]+\.md)',t):
  ck((R/'references'/rel).is_file(),f'missing reference from {skill}: {rel}')
refs=list((R/'references').glob('*.md'))
ck(len(refs)==18,f'expected 18 references, found {len(refs)}')
ck(len(list((R/'templates').glob('*.md')))==10,'expected ten templates')
for p in refs:ck(p.stat().st_size>150,f'reference too short: {p}')
old=R/'legacy/original';ck((old/'SKILL.md').is_file(),'original skill missing')
ck(len(list((old/'references').glob('*.md')))==6,'original references missing')
bench=read_json('benchmarks/cases.json')['cases']
ck(len(bench)==30,'benchmarks must contain 30 cases')
ck(sum(x['kind']=='positive' for x in bench)==20,'20 positive cases required')
ck(sum(x['kind']=='negative' for x in bench)==10,'10 negative cases required')
ck(len({x['id'] for x in bench})==len(bench),'bench IDs not unique')
for case in bench:
 ck((R/'skills'/case['skill']/'SKILL.md').exists(),f'benchmark skill missing {case["skill"]}')
 ck(case.get('status')=='not_run','benchmark falsely marked passed')
for p in ['README.md','CHANGELOG.md','SUBMISSION.md','UPGRADE_AUDIT.md','scripts/command_review.py','tests/test_command_review.py','examples/select_csv_columns.py','examples/safe_file_inventory.py','tests/test_examples.py']:
 ck((R/p).is_file(),f'missing {p}')
if errors:
 print(f'FAILED: {len(errors)} of {checks} checks:\n'+'\n'.join(errors))
 sys.exit(1)
print(f'PASS: {checks} package/cross-reference checks')

SHA-256: abf247551ce0f0723c6104a2bb4d53ca3e1a839d87b60afa6b4dfad063307723