from pathlib import Path import hashlib import json import shutil ROOT = Path(__file__).resolve().parent REPO = ROOT.parent.parent def write_source(relative, text): path = ROOT / relative assert not path.exists(), path path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(text.replace('\r\n', '\n').replace('\n', '\r\n').encode('utf-8')) files = { 'project.arcadia': '''arcadia 7; project navigation_trial "Navigation — 救援" id "9e8f0000-0000-4000-8000-000000000001" { description "Prose @assess must not be a lexical use."; include "救援/assess.arcadia"; include "救援/confirm.arcadia"; include "links.arcadia"; } ''', '救援/assess.arcadia': '''// Comment @assess must not be a lexical use. oa_activity assess "Assess 救援 request" id "9e8f0000-0000-4000-8000-000000000002" { description "Keep @assess as prose, not a use."; } ''', '救援/confirm.arcadia': '''oa_activity confirm "Confirm 救援 response" id "9e8f0000-0000-4000-8000-000000000003" {} ''', 'links.arcadia': '''interaction handoff "Assessment handoff" id "9e8f0000-0000-4000-8000-000000000004" { description "救援 @assess is prose"; source @assess; target @confirm; // Another comment @assess is excluded. } ''', } for name, text in files.items(): write_source('valid-baseline/' + name, text) shutil.copytree(ROOT / 'valid-baseline', ROOT / 'valid-work') for case in ['ambiguous-open', 'missing-include-open']: for name, text in files.items(): if case == 'ambiguous-open' and name == '救援/confirm.arcadia': text += 'oa_activity assess "Duplicate draft assessment" id "9e8f0000-0000-4000-8000-000000000005" {}\n' if case == 'missing-include-open' and name == 'project.arcadia': text = text.replace(' include "links.arcadia";', ' include "links.arcadia";\n include "missing/not-loaded.arcadia";') write_source(case + '/' + name, text) # A realistic existing teaching model, copied without changing source bytes. shutil.copytree(REPO / 'examples/release1', ROOT / 'release1-baseline') shutil.copytree(ROOT / 'release1-baseline', ROOT / 'release1-work') def position(raw, offset): line_start = raw.rfind(b'\n', 0, offset) + 1 return {'byte_offset': offset, 'line_1based': raw.count(b'\n', 0, offset) + 1, 'utf8_byte_column_1based': offset - line_start + 1} locations = [] for folder in ['valid-baseline', 'ambiguous-open', 'missing-include-open']: for path in sorted((ROOT / folder).rglob('*.arcadia')): raw = path.read_bytes() text = raw.decode('utf-8') assert b'\n' not in raw.replace(b'\r\n', b'') for alias in ['assess', 'confirm']: token = alias.encode('ascii') start = 0 while (found := raw.find(token, start)) != -1: start = found + len(token) line_start = raw.rfind(b'\n', 0, found) + 1 prefix = raw[line_start:found] # Explicit fixture expectations, independent of the application lexer. if prefix == b'oa_activity ': kind = 'declaration' elif prefix.endswith(b'source @') or prefix.endswith(b'target @'): kind = 'lexical-use' elif found > 0 and raw[found-1:found] == b'@': kind = 'excluded-comment-or-string' else: continue locations.append({'fixture': folder, 'path': path.relative_to(ROOT / folder).as_posix(), 'alias': alias, 'kind': kind, 'span': [found, start], **position(raw, found)}) expected = {'encoding': 'UTF-8 without BOM; every authored fixture newline CRLF', 'valid_totals': {'assess': {'declarations': 1, 'lexical_uses': 1}, 'confirm': {'declarations': 1, 'lexical_uses': 1}}, 'ambiguous_totals': {'assess': {'declarations': 2, 'lexical_uses': 1}}, 'locations': locations} (ROOT / 'expected-locations.json').write_text(json.dumps(expected, ensure_ascii=False, indent=2) + '\n', encoding='utf-8') manifest = [{'path': p.relative_to(ROOT).as_posix(), 'bytes': p.stat().st_size, 'sha256': hashlib.sha256(p.read_bytes()).hexdigest()} for folder in ['valid-baseline', 'valid-work', 'ambiguous-open', 'missing-include-open', 'release1-baseline', 'release1-work'] for p in sorted((ROOT / folder).rglob('*')) if p.is_file()] (ROOT / 'fixture-manifest.json').write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + '\n', encoding='utf-8') print(json.dumps({'files': len(manifest), 'bytes': sum(p['bytes'] for p in manifest), 'expected_locations': len(locations)}))