diff --git a/.github/workflows/paired-root.yml b/.github/workflows/paired-root.yml index 75b39ee..79e8f6c 100644 --- a/.github/workflows/paired-root.yml +++ b/.github/workflows/paired-root.yml @@ -22,6 +22,7 @@ jobs: timeout-minutes: 45 env: LIBRARIES_SHA: 0b099ec416de60f6adafb182f1d1e83795d05c56 + BASELINE_MIRROR_SHA: 8d2bb2a7f1cb6b05d5e0eaa51298929451976432 # pull_request uses the synthetic base+head merge revision whose workflow # file GitHub executes; push/dispatch use their exact event commit. MIRROR_SHA: ${{ github.sha }} @@ -40,6 +41,12 @@ jobs: path: Libraries ref: ${{ env.LIBRARIES_SHA }} persist-credentials: false + - name: Checkout exact held-difference baseline + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + path: baseline-mirror + ref: ${{ env.BASELINE_MIRROR_SHA }} + persist-credentials: false - uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy @@ -58,6 +65,14 @@ jobs: PYTHONDONTWRITEBYTECODE=1 python3 mirror/scripts/ci/paired_root.py check-forwarded \ --libraries Libraries --mirror mirror \ --libraries-sha "$LIBRARIES_SHA" --mirror-sha "$MIRROR_SHA" + - name: Fence shared Git path debt against exact baseline + run: | + set -euo pipefail + test "$(git -C baseline-mirror rev-parse HEAD)" = "$BASELINE_MIRROR_SHA" + PYTHONDONTWRITEBYTECODE=1 python3 mirror/scripts/ci/paired_root.py check-shared-baseline \ + --libraries Libraries --baseline baseline-mirror --mirror mirror \ + --libraries-sha "$LIBRARIES_SHA" --baseline-sha "$BASELINE_MIRROR_SHA" \ + --mirror-sha "$MIRROR_SHA" - name: Assemble pinned disposable source pair id: assemble shell: bash diff --git a/scripts/ci/paired_root.py b/scripts/ci/paired_root.py index 73941ab..d81dba1 100644 --- a/scripts/ci/paired_root.py +++ b/scripts/ci/paired_root.py @@ -24,6 +24,9 @@ 'tests/search_tests.rs', 'tests/storage_lifecycle.rs', ) +# Derived from pinned owner 0b099ec4 and baseline mirror 8d2bb2a7 Git trees. +# This census is a review tripwire, not a schema or semantic authority. +BASELINE_SHARED_COUNTS = (170, 157, 13, 117, 4) def require_exact_sha(observed: str, expected: str, owner: str) -> None: @@ -114,6 +117,42 @@ def check_forwarded_paths(libraries: Path, mirror: Path, libraries_sha: str, mir print(f'{len(FORWARDED_EXACT_PATHS)} forwarded paths match exact owner Git blobs and modes') +def check_shared_baseline(libraries: Path, baseline: Path, mirror: Path, + libraries_sha: str, baseline_sha: str, mirror_sha: str, + *, expected_counts: tuple[int, int, int, int, int] = BASELINE_SHARED_COUNTS) -> None: + """Fence shared committed paths, never interpret held differences as parity.""" + for sha in (libraries_sha, baseline_sha, mirror_sha): + if not re.fullmatch(r'[0-9a-f]{40}', sha): + raise ValueError('exact 40-hex source revisions required') + owner_tree = tracked_files(libraries, libraries_sha, strict=False) + owner = {path[len('semantic-memory/'):]: entry for path, entry in owner_tree.items() + if path.startswith('semantic-memory/')} + prior = tracked_files(baseline, baseline_sha, strict=True) + current = tracked_files(mirror, mirror_sha, strict=True) + shared = owner.keys() & prior.keys() + identical = {path for path in shared if owner[path] == prior[path]} + held = shared - identical + census = (len(shared), len(identical), len(held), len(owner.keys() - prior.keys()), + len(prior.keys() - owner.keys())) + if census != expected_counts: + raise ValueError(f'pinned baseline census changed: {census} != {expected_counts}') + if owner.keys() & current.keys() != shared: + raise ValueError('shared owner/mirror path set changed from pinned baseline') + for path in sorted(shared): + validate_entry(path, owner[path][0]) + allowed = {owner[path]} + if path in held: + allowed.add(prior[path]) + if current[path] not in allowed: + raise ValueError(f'shared path changed outside pinned owner/baseline tuples: {path}') + trees = [git(repo, 'rev-parse', sha + '^{tree}').decode().strip() + for repo, sha in ((libraries, libraries_sha), (baseline, baseline_sha), (mirror, mirror_sha))] + print(json.dumps({'schema': 'SharedGitPathFenceV1', 'libraries_sha': libraries_sha, + 'baseline_mirror_sha': baseline_sha, 'candidate_mirror_sha': mirror_sha, + 'tree_ids': trees, 'owner_prefix': 'semantic-memory/', 'census': census, + 'result': 'shared tuples restricted to pinned owner or held baseline'}, sort_keys=True)) + + def extract_archive(repo: Path, sha: str, destination: Path, *, owner: bool) -> None: proc = subprocess.Popen( ['git', '-C', str(repo), 'archive', '--format=tar', sha], @@ -207,6 +246,9 @@ def main() -> None: forwarded = sub.add_parser('check-forwarded') for flag in ('libraries', 'mirror', 'libraries-sha', 'mirror-sha'): forwarded.add_argument('--' + flag, required=True) + shared = sub.add_parser('check-shared-baseline') + for flag in ('libraries', 'baseline', 'mirror', 'libraries-sha', 'baseline-sha', 'mirror-sha'): + shared.add_argument('--' + flag, required=True) args = parser.parse_args() if args.action == 'assemble': assemble(Path(args.libraries), Path(args.mirror), args.libraries_sha, args.mirror_sha, @@ -214,8 +256,11 @@ def main() -> None: elif args.action == 'check-lock': validate_lock_delta(tomllib.loads(Path(args.before).read_text()), tomllib.loads(Path(args.after).read_text())) print('scratch lock delta is exactly the declared PolyKV FibQuant dependency drop') - else: + elif args.action == 'check-forwarded': check_forwarded_paths(Path(args.libraries), Path(args.mirror), args.libraries_sha, args.mirror_sha) + else: + check_shared_baseline(Path(args.libraries), Path(args.baseline), Path(args.mirror), + args.libraries_sha, args.baseline_sha, args.mirror_sha) if __name__ == '__main__': diff --git a/scripts/ci/test_paired_root.py b/scripts/ci/test_paired_root.py index b152b57..bff80da 100644 --- a/scripts/ci/test_paired_root.py +++ b/scripts/ci/test_paired_root.py @@ -96,6 +96,58 @@ def test_forwarded_blobs_bind_exact_paths_and_modes_not_unrelated_files(self): with self.assertRaisesRegex(ValueError, path): paired_root.check_forwarded_paths(owner, mirror, owner_sha, missing) + def test_shared_census_fences_new_drift_but_allows_exact_owner_forward(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + owner, baseline, current = root / 'owner', root / 'baseline', root / 'current' + owner_sha = self.fixture_repo(owner, { + 'semantic-memory/src/same.rs': 'same', + 'semantic-memory/src/held.rs': 'owner', + 'semantic-memory/src/owner_only.rs': 'future shared', + }) + baseline_sha = self.fixture_repo(baseline, { + 'src/same.rs': 'same', 'src/held.rs': 'held', 'src/mirror_only.rs': 'ci-only', + }) + current_sha = self.fixture_repo(current, { + 'src/same.rs': 'same', 'src/held.rs': 'held', 'src/mirror_only.rs': 'updated ci-only', + }) + census = (2, 1, 1, 1, 1) + def check(sha): + paired_root.check_shared_baseline(owner, baseline, current, + owner_sha, baseline_sha, sha, expected_counts=census) + def commit(message): + subprocess.run(['git', '-C', str(current), 'add', '-A'], check=True) + subprocess.run(['git', '-C', str(current), 'commit', '-qm', message], check=True) + return subprocess.check_output(['git', '-C', str(current), 'rev-parse', 'HEAD'], text=True).strip() + check(current_sha) + (current / 'src/held.rs').write_text('owner') + check(commit('exact owner forward')) + (current / 'src/held.rs').write_text('third value') + with self.assertRaisesRegex(ValueError, 'src/held.rs'): + check(commit('unauthorized held edit')) + (current / 'src/held.rs').write_text('held') + (current / 'src/same.rs').write_text('drift') + with self.assertRaisesRegex(ValueError, 'src/same.rs'): + check(commit('identical path drift')) + (current / 'src/same.rs').write_text('same') + (current / 'src/same.rs').chmod(0o755) + with self.assertRaisesRegex(ValueError, 'src/same.rs'): + check(commit('mode drift')) + (current / 'src/same.rs').unlink() + with self.assertRaisesRegex(ValueError, 'shared owner/mirror path set'): + check(commit('missing shared path')) + (current / 'src/same.rs').write_text('same') + (current / 'src/same.rs').chmod(0o644) + (current / 'src/owner_only.rs').write_text('future shared') + with self.assertRaisesRegex(ValueError, 'shared owner/mirror path set'): + check(commit('new shared path')) + with self.assertRaisesRegex(ValueError, '40-hex'): + paired_root.check_shared_baseline(owner, baseline, current, owner_sha, + 'invalid', current_sha, expected_counts=census) + with self.assertRaisesRegex(ValueError, 'baseline census changed'): + paired_root.check_shared_baseline(owner, baseline, current, owner_sha, + baseline_sha, current_sha, expected_counts=(3, 1, 1, 1, 1)) + def test_archive_member_rejects_duplicates_and_file_directory_collisions(self): seen = {} paired_root.claim_archive_member('src', 'directory', seen)