0.8
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check file structure, card IDs and explicit typed links; never assert historical truth."""
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ID_FORMAT = re.compile(r'^(?:SRC-[PS]|EV|L|Q|H|C|CON|SYN|D|W)-\d{4}$')
|
||||
ID_PREFIX = re.compile(r'^(SRC-[PS]|EV|L|Q|H|C|CON|SYN|D|W)-\d{4}(?=-|\.md$)')
|
||||
REL_FIELDS = {
|
||||
'source_ids', 'evidence_ids', 'question_ids', 'hypothesis_ids',
|
||||
'claim_ids', 'synthesis_ids', 'related_ids', 'contradiction_ids', 'conclusion_ids',
|
||||
}
|
||||
CARD_DIRS = [
|
||||
'01_sources/records/primary', '01_sources/records/secondary',
|
||||
'02_evidence', '03_inquiry/leads', '03_inquiry/questions',
|
||||
'03_inquiry/hypotheses', '05_historiography/works',
|
||||
'06_arguments/claims', '06_arguments/syntheses', '06_arguments/conclusions',
|
||||
]
|
||||
CARD_SINGLE_DIRS = ['00_governance']
|
||||
|
||||
|
||||
def parse_header(text: str) -> dict[str,str]:
|
||||
if not text.startswith('---\n'): return {}
|
||||
_, sep, after = text.partition('\n---\n')
|
||||
if not sep: return {}
|
||||
first = after # Read before divider, not body
|
||||
header = text[4:].split('\n---\n',1)[0]
|
||||
result = {}
|
||||
for line in header.splitlines():
|
||||
m = re.match(r'^([a-z_]+):\s*(.*)$', line)
|
||||
if m: result[m.group(1)] = m.group(2).split(' #',1)[0].strip()
|
||||
return result
|
||||
|
||||
|
||||
def check(root: Path) -> tuple[list[str],list[str]]:
|
||||
errors=[]; warnings=[]
|
||||
needed=['README.md','code4history.md','AGENTS.md','USERINFO.md','topic.md','STATE.md',
|
||||
'DISCUSSION_TO_TOPIC.md','MIGRATION.md','INTERACTION.md','tools/interaction.py',
|
||||
'upload/readme.md','todo/readme.md','requests/readme.md','handoff/readme.md',
|
||||
'.agents/skills/history-writing-workflow-c4h/SKILL.md',
|
||||
'.agents/skills/source-intake-c4h/SKILL.md',
|
||||
'.agents/skills/discussion-to-topic-c4h/SKILL.md']
|
||||
for file in needed:
|
||||
if not (root/file).is_file(): errors.append(f'Missing required file: {file}')
|
||||
for obsolete in ('agent.md', 'AGENT.md', '.agents/skills/history-writing-workflow/SKILL.md'):
|
||||
if (root/obsolete).exists(): errors.append(f'Retired entry still present: {obsolete}')
|
||||
for folder in root.rglob('*'):
|
||||
if not folder.is_dir() or any(x in folder.parts for x in ('.git','__pycache__','.pytest_cache')): continue
|
||||
if not (folder/'readme.md').is_file(): errors.append(f'Missing readme.md: {folder.relative_to(root)}')
|
||||
seen={}; records=[]
|
||||
for d in CARD_DIRS+CARD_SINGLE_DIRS:
|
||||
p=root/d
|
||||
if not p.exists(): errors.append(f'Missing card folder: {d}'); continue
|
||||
for f in sorted(p.glob('*.md')):
|
||||
if f.name in {'readme.md','research-log.md','decisions.md','bibliography.md','source-policy.md'}: continue
|
||||
# In governance root, only numbered D cards count; ignore other docs.
|
||||
m=ID_PREFIX.match(f.name)
|
||||
if not m: continue
|
||||
head=parse_header(f.read_text(encoding='utf-8'))
|
||||
ident=head.get('id','').strip('"\'')
|
||||
if not ID_FORMAT.fullmatch(ident):
|
||||
errors.append(f'{f.relative_to(root)}: missing/invalid frontmatter id'); continue
|
||||
if not f.name.startswith(ident+'-') and f.name != ident+'.md':
|
||||
errors.append(f'{f.relative_to(root)}: filename and id do not match')
|
||||
if ident in seen: errors.append(f'Duplicate ID {ident}: {seen[ident]}, {f.relative_to(root)}')
|
||||
seen[ident]=str(f.relative_to(root)); records.append((f,ident,head))
|
||||
for f,ident,head in records:
|
||||
for key in REL_FIELDS:
|
||||
if key not in head: continue
|
||||
raw=head[key].strip()
|
||||
if raw in ('','[]','null'): continue
|
||||
if not (raw.startswith('[') and raw.endswith(']')):
|
||||
errors.append(f'{ident}: {key} must be a one-line [ID, ID] array'); continue
|
||||
for token in raw[1:-1].split(','):
|
||||
ref=token.strip(' "\'')
|
||||
if not ref: continue
|
||||
if not ID_FORMAT.fullmatch(ref): errors.append(f'{ident}: malformed linked ID {ref}');continue
|
||||
if ref not in seen: errors.append(f'{ident}: broken {key} link to {ref}')
|
||||
if ident.startswith('EV-'):
|
||||
if not head.get('source_ids','').strip(' []'): warnings.append(f'{ident}: no source_ids yet')
|
||||
if not head.get('source_locator','').strip(' "'): warnings.append(f'{ident}: missing source_locator')
|
||||
if head.get('verification','')=='checked' and not head.get('source_locator','').strip(' "'):
|
||||
errors.append(f'{ident}: verified evidence lacks source_locator')
|
||||
if ident.startswith('C-') and not head.get('evidence_ids','').strip(' []'):
|
||||
warnings.append(f'{ident}: claim has no evidence_ids yet')
|
||||
for skill_name in ('history-writing-workflow-c4h','source-intake-c4h','discussion-to-topic-c4h'):
|
||||
skill=root/'.agents/skills'/skill_name/'SKILL.md'
|
||||
if skill.is_file():
|
||||
head=parse_header(skill.read_text(encoding='utf-8'))
|
||||
if head.get('name')!=skill_name or not head.get('description'):
|
||||
errors.append(f'Skill frontmatter must include name: {skill_name} and description')
|
||||
return errors,warnings
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap=argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument('--strict',action='store_true',help='treat incomplete-card warnings as errors')
|
||||
args=ap.parse_args()
|
||||
errors,warnings=check(ROOT)
|
||||
print(f'Code4History validation: {len(errors)} errors; {len(warnings)} warnings')
|
||||
for msg in errors: print('ERROR:',msg)
|
||||
for msg in warnings: print('WARN: ',msg)
|
||||
if errors or (warnings and args.strict): raise SystemExit(1)
|
||||
|
||||
if __name__=='__main__': main()
|
||||
Reference in New Issue
Block a user