"""Check G7 federation cards, reciprocal contracts, ownership and review provenance. Run from a project checkout beside the three canons. Requires PyYAML/jsonschema. Repository-qualified card paths resolve below --workspace, never below the card. """ from __future__ import annotations import argparse import hashlib import json from pathlib import Path import re import subprocess import jsonschema import yaml CARD_PATHS = { 'info-tech-canon': 'infospace/interfaces/federation.yaml', 'commerce-canon': 'infospace/interfaces/federation.yaml', 'the-custodian': 'canon/interfaces/federation.yaml', } def require(condition, message): if not condition: raise ValueError(message) def sha(data): return hashlib.sha256(data).hexdigest() def git_blob(repo, commit, path): require(bool(re.fullmatch(r'[0-9a-f]{40}', commit)), 'Source must use a full Git commit') return subprocess.check_output(['git', 'show', f'{commit}:{path}'], cwd=repo) def declared_concepts(text): """Support complete metadata and older explicit prose definitions. A section heading alone cannot establish ownership: transferred concepts deliberately keep their old anchors as imports. """ fm = yaml.safe_load(text.split('---', 2)[1]) if text.startswith('---\n') else {} return set(fm.get('owned_concepts', [])) | set( re.findall(r'^(?:An? )?\*\*(.+?)\*\* is\b', text, re.M) ) def validate(workspace): project = Path(__file__).resolve().parents[1] schema_path = 'infospace/schemas/interface-card.schema.yaml' schema = yaml.safe_load((workspace / 'info-tech-canon' / schema_path).read_text()) jsonschema.Draft202012Validator.check_schema(schema) cards = {repo: yaml.safe_load((workspace / repo / path).read_text()) for repo, path in CARD_PATHS.items()} ledger = json.loads((project / 'ledger/concept-ownership.json').read_text()) targets = [t for e in ledger['entries'] for t in e['targets'] if t['kind'] in {'concept', 'seed'}] targets += ledger['additional_required_concepts'] exports = {} ownership = {} import_edges = 0 peer_links = 0 for repo, card in cards.items(): jsonschema.validate(card, schema) require(card['consumer'] == repo, f'{repo}: wrong consumer') require(card['status'] == 'published', f'{repo}: unpublished') require(card['schema_source'] == {'repository': 'info-tech-canon', 'path': schema_path}, 'Schema drift') peers = card['peers'] require(len(peers) == 2 and {p['repository'] for p in peers} == set(cards) - {repo}, f'{repo}: peer coverage') for peer in peers: other = peer['repository'] require(peer['card'] == CARD_PATHS[other], f'{repo}: bad peer card path') require(any(p['repository'] == repo and p['card'] == CARD_PATHS[repo] for p in cards[other]['peers']), f'{repo}: nonreciprocal peer') peer_links += 1 require(card['canon_surfaces'] == [e['artifact'] for e in card['exports']], 'Export index drift') for entry in card['exports']: key = (repo, entry['artifact']) require(key not in exports, f'Duplicate export {key}') exports[key] = entry require((workspace / repo / entry['path']).is_file(), f'Missing export {key}') if entry['kind'] == 'ecosystem-governance': require(repo == 'the-custodian' and entry['concepts'] == [], 'Governance claims domain semantics') require(set(entry['governs']) == set(cards) - {repo}, 'Governance coverage') else: registry = yaml.safe_load((workspace / repo / 'canon.yaml').read_text()) matches = [m for m in registry['models'] + registry.get('concept_areas', []) if m['id'] == entry['artifact']] require(len(matches) == 1, f'Unregistered export {key}') for field in ['status', 'path']: require(entry[field] == matches[0][field], f'{key}: registry {field} drift') if entry['artifact'] == 'family-area': require(entry['kind'] == 'concept-area-seed' and entry['availability'] == 'seeded-not-authored', 'Family promoted') for concept in entry['concepts']: require(concept not in ownership, f'Duplicate domain owner: {concept}') ownership[concept] = (repo, entry['ledger_model']) authority = card['authority'] require(authority == cards['the-custodian']['authority'], 'Authority mismatch') require(authority['repository'] == 'the-custodian' and authority['artifact'] == 'CUST-ADR-006' and authority['revision'] == 'accepted-1', 'Unreviewed authority') adr = git_blob(workspace / 'the-custodian', authority['source_commit'], authority['path']) require(sha(adr) == authority['sha256'], 'Pinned ADR hash mismatch') require(adr == (workspace / 'the-custodian' / authority['path']).read_bytes(), 'ADR changed since review') for target in targets: require(ownership.get(target['concept']) == (target['owner']['canon'], target['owner']['model']), f"Ledger disagreement: {target['concept']}") for repo, card in cards.items(): seen = set() for entry in card['imports']: key = (entry['repository'], entry['artifact']) require(key not in seen, f'{repo}: duplicate import') seen.add(key) require(key in exports and entry['repository'] != repo, f'{repo}: missing upstream export {key}') require(entry['kind'] == exports[key]['kind'], 'Import kind mismatch') require(set(entry['concepts']) <= set(exports[key]['concepts']), 'Import exceeds export') require(not any(ownership[c][0] == repo for c in entry['concepts']), 'Import redefined locally') import_edges += 1 require(cards['the-custodian']['imports'] == [], 'Custodian domain dependency introduced') require(len(cards['info-tech-canon']['imports']) == 1, 'Unreviewed reverse domain import') require(len(cards['the-custodian']['exports']) == 1, 'Custodian federation scope drift') commerce = workspace / 'commerce-canon' model_dir = commerce / 'infospace/models/counterparty' manifest_path = model_dir / 'imports.json' current = json.loads(manifest_path.read_text()) review = current['review'] require((model_dir / review['record']).is_file(), 'Missing import review') previous_bytes = (model_dir / review['previous_manifest']).read_bytes() require(sha(previous_bytes) == review['previous_manifest_sha256'], 'Historical manifest changed') previous = json.loads(previous_bytes) require(previous['source_commit'] == '361c944325934ccdb190470baf6f55440b6b486e', 'Original review pin changed') require(review['concept_changes'] == [], 'Unexpected concept adoption') require([(r['model'], r['concepts']) for r in previous['imports']] == [(r['model'], r['concepts']) for r in current['imports']], 'Unreviewed import change') checked_blobs = 0 drift = [] for manifest in [previous, current]: require(manifest['source_repository'] == 'info-tech-canon', 'Wrong import source') for entry in manifest['imports']: blob = git_blob(workspace / 'info-tech-canon', manifest['source_commit'], entry['path']) require(sha(blob) == entry['sha256'], f"Pinned import hash mismatch: {entry['model']}") require(set(entry['concepts']) <= declared_concepts(blob.decode()), f"Import not owned: {entry['model']}") checked_blobs += 1 if manifest is current and blob != (workspace / 'info-tech-canon' / entry['path']).read_bytes(): drift.append(entry['path']) domain_imports = [e for e in cards['commerce-canon']['imports'] if e['kind'] == 'domain-model'] require(len(domain_imports) == len(current['imports']) == 4, 'Import model coverage') for row in current['imports']: exported = exports[('info-tech-canon', row['model'])] require(exported['path'] == row['path'], 'Import/export source mismatch') matches = [e for e in domain_imports if e['artifact'] == row['model']] require(len(matches) == 1 and matches[0]['concepts'] == row['concepts'], 'Card/manifest mismatch') require(commerce / matches[0]['manifest'] == manifest_path, 'Wrong manifest pointer') links = 0 for repo, path in CARD_PATHS.items(): root = workspace / repo for document in [root / 'README.md', root / Path(path).parent / 'README.md']: for link in re.findall(r'\]\(([^)]+)\)', document.read_text()): if '://' in link or link.startswith('#'): continue require((document.parent / link.split('#')[0]).exists(), f'Broken link: {document}: {link}') links += 1 return {'gate': 'G7', 'result': 'pass', 'cards': len(cards), 'reciprocal_peer_links': peer_links, 'import_edges': import_edges, 'ledger_concepts_and_seed_covered': len(targets), 'imported_concepts': sum(len(e['concepts']) for e in domain_imports), 'historical_and_current_import_blobs_checked': checked_blobs, 'source_commit': current['source_commit'], 'upstream_changes_since_review': drift, 'navigation_links_checked': links, 'custodian_domain_concepts': 0, 'family': 'seeded-not-authored'} if __name__ == '__main__': parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--workspace', type=Path, default=Path(__file__).resolve().parents[2]) args = parser.parse_args() print(json.dumps(validate(args.workspace.resolve()), indent=2))