Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a070b5-4994-7271-bd8b-7c3dbcedec4b
176 lines
9.6 KiB
Python
176 lines
9.6 KiB
Python
"""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))
|