diff --git a/src/repo_manager/historical_sources.py b/src/repo_manager/historical_sources.py new file mode 100644 index 0000000..6eb2072 --- /dev/null +++ b/src/repo_manager/historical_sources.py @@ -0,0 +1,85 @@ +"""Reviewed closed-source exceptions; enable only after receiving deployment. + +No aliases, new IDs, binding replacement or source edits are authorized. +Strict identity scans retain nonconformance findings for these legacy labels. +""" +from hashlib import sha256 +import json + +BINDINGS = json.loads(r'''[ + { + "repo": "activity-core", + "path": "workplans/archived/260707-custodian-WP-0003-event-bridge.md", + "sha256": "83ea8bcb151e003419b73c59dbd028f4a1ce3d1120a82e4a128a772503ff3b76", + "record_id": "custodian-WP-0003", + "uuid": "b4eb45a9-69e3-4ab0-b00c-67a53c3117c5", + "parent_record_id": "custodian-WP-0003" + }, + { + "repo": "activity-core", + "path": "workplans/custodian-WP-0001-temporal-backbone.md", + "sha256": "020e2eac973f2daa41c1687f8e867da9d521f945547e6aa82c87993b3ff6fc64", + "record_id": "custodian-WP-0001", + "uuid": "e22516cc-4fdf-4dad-a928-a9e04a9fa7c7", + "parent_record_id": "custodian-WP-0001" + }, + { + "repo": "activity-core", + "path": "workplans/custodian-WP-0002-triggers-ops.md", + "sha256": "d8cbc61631c01b6f11a03a63f2fb6275fc853ff7f7459ce65973ed1a233daa01", + "record_id": "custodian-WP-0002", + "uuid": "3a4f47d9-8bc1-434e-acb4-bed5d4dacda0", + "parent_record_id": "custodian-WP-0002" + }, + { + "repo": "activity-core", + "path": "workplans/custodian-WP-0003a-event-bridge-model-rules-registry.md", + "sha256": "ffa6682e9ba3aaf559bd44203d561c90dea69bcf1b1e778209a7b209e6e5cdd3", + "record_id": "custodian-WP-0003a", + "uuid": "b4eb45a9-69e3-4ab0-b00c-67a53c3117c5", + "parent_record_id": "custodian-WP-0003" + }, + { + "repo": "activity-core", + "path": "workplans/custodian-WP-0003b-event-bridge-parser-workflow-triggers.md", + "sha256": "4fc34e0f821e2df51e5c08c7ab34da35b65690381d01f0cfdf55d00c970b414c", + "record_id": "custodian-WP-0003b", + "uuid": "b4eb45a9-69e3-4ab0-b00c-67a53c3117c5", + "parent_record_id": "custodian-WP-0003" + }, + { + "repo": "activity-core", + "path": "workplans/custodian-WP-0003c-event-bridge-context-integration.md", + "sha256": "b6cd62b26515e509670ca39736b6bcec9633beedd46c9bd8f7598c06bac7276b", + "record_id": "custodian-WP-0003c", + "uuid": "b4eb45a9-69e3-4ab0-b00c-67a53c3117c5", + "parent_record_id": "custodian-WP-0003" + }, + { + "repo": "activity-core", + "path": "workplans/custodian-WP-0004-railiance-ops.md", + "sha256": "20df02eea740e109c937130e7f8801a636fc0990ed8c6d2ced55659f8957adb1", + "record_id": "custodian-WP-0004", + "uuid": "759b1255-aa78-42b5-8fab-c5fcf168f5f4", + "parent_record_id": "custodian-WP-0004" + }, + { + "repo": "the-custodian", + "path": "workplans/ADHOC-2026-08-25-retired.md", + "sha256": "344a1293dbb3a67a944eb34ca4a0337487d8f047226027a2107922346fbab2dd", + "record_id": "CUST-WP-ADHOC-2026-08-25-RETIRED", + "uuid": "1c53d3db-6633-5e2d-987f-10706d73be9e", + "parent_record_id": "CUST-WP-ADHOC-2026-08-25-RETIRED" + } +]''') + + +def historical_source_paths(repo_root): + entries = [entry for entry in BINDINGS if entry["repo"] == repo_root.name] + paths = set() + for entry in entries: + path = repo_root / entry["path"] + if path.is_symlink() or not path.is_file() or sha256(path.read_bytes()).hexdigest() != entry["sha256"]: + raise ValueError("Historical source binding requires owner review: " + entry["path"]) + paths.add(entry["path"]) + return paths diff --git a/src/repo_manager/identifiers.py b/src/repo_manager/identifiers.py index 45ae24a..186854d 100644 --- a/src/repo_manager/identifiers.py +++ b/src/repo_manager/identifiers.py @@ -854,7 +854,8 @@ def ensure_missing_work_record_identifiers( # allocation candidates. Keep conformance reporting strict while allowing # ordinary registration to preserve those bindings. Local aliases still # require the explicit parent allowlist in the kind registry. - historical_sources: set[str] = set() + from repo_manager.historical_sources import historical_source_paths + historical_sources: set[str] = historical_source_paths(repo_root) for path in iter_workplan_files(repo_root): parsed = parse_workplan_file(path, repo_root=repo_root) if parsed.status not in {"finished", "archived", "completed"} or not parsed.id: diff --git a/tests/test_historical_sources.py b/tests/test_historical_sources.py new file mode 100644 index 0000000..4fea579 --- /dev/null +++ b/tests/test_historical_sources.py @@ -0,0 +1,56 @@ +"""Exact frozen sources retain bytes and bindings while new work stays canonical.""" +import hashlib +from pathlib import Path + +import pytest + +from repo_manager import historical_sources as hs +from repo_manager.identifiers import ensure_missing_work_record_identifiers +from repo_manager.record_identity import scan_record_identities + + +def _fixture(tmp_path,monkeypatch,repo='activity-core'): + root=tmp_path/repo + path=root/'workplans/old.md';path.parent.mkdir(parents=True) + rid='custodian-WP-0001' if repo=='activity-core' else 'CUST-WP-ADHOC-2026-08-25-RETIRED' + data=(f'---\nid: {rid}\ntype: workplan\nrepo: {repo}\nstatus: finished\n' + 'state_hub_workstream_id: e22516cc-4fdf-4dad-a928-a9e04a9fa7c7\n---\n').encode() + path.write_bytes(data) + monkeypatch.setattr(hs,'BINDINGS',[dict(repo=repo,path='workplans/old.md',sha256=hashlib.sha256(data).hexdigest())]) + prefix='ACTIVITY' if repo=='activity-core' else 'CUST' + current=root/'workplans/current.md' + current.write_text(f'---\nid: {prefix}-WP-9999\ntype: workplan\nstatus: active\n---\n') + return root,path,current,data + + +@pytest.mark.parametrize('repo',['activity-core','the-custodian']) +def test_frozen_source_retains_bytes_and_existing_uuid(tmp_path,monkeypatch,repo): + root,path,current,data=_fixture(tmp_path,monkeypatch,repo) + report=ensure_missing_work_record_identifiers(root) + assert path.read_bytes()==data + assert len(report['historical_invalid_identifiers'])==1 + assert len(report['assignments'])==1 and report['assignments'][0]['path']=='workplans/current.md' + assert not scan_record_identities(root)['ok'] + assert ensure_missing_work_record_identifiers(root)['assignments']==[] + + +@pytest.mark.parametrize('change',['id','uuid','status','missing','symlink','extra']) +def test_unreviewed_history_refuses_before_any_allocation(tmp_path,monkeypatch,change): + root,path,current,data=_fixture(tmp_path,monkeypatch) + original=current.read_bytes() + if change=='id':path.write_bytes(data.replace(b'custodian-WP-0001',b'custodian-WP-0002')) + elif change=='uuid':path.write_bytes(data.replace(b'e22516cc',b'f22516cc')) + elif change=='status':path.write_bytes(data.replace(b'finished',b'active')) + elif change=='missing':path.unlink() + elif change=='symlink': + path.unlink();target=root/'other.md';target.write_bytes(data);path.symlink_to(target) + else:(root/'workplans/unreviewed.md').write_bytes(data.replace(b'custodian-WP-0001',b'custodian-WP-0099')) + with pytest.raises(ValueError):ensure_missing_work_record_identifiers(root) + assert current.read_bytes()==original + + +def test_other_repo_cannot_inherit_frozen_exception(tmp_path,monkeypatch): + root,path,current,data=_fixture(tmp_path,monkeypatch) + other=root.with_name('other-repo');root.rename(other) + with pytest.raises(ValueError,match='identities are not safe'): + ensure_missing_work_record_identifiers(other)