From f4d8148fc031cfa581bce7f30410fcd63fc89202 Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Thu, 16 Jul 2026 12:18:03 +0800 Subject: [PATCH] feat: add public cross-project calculation contract --- ...16-dual-project-contract-implementation.md | 115 ++++++++++++++ .../fixture_manifest.v1.json | 27 ++++ .../cross_project_contract/sync_ledger.json | 4 + scripts/cross_project_contract.py | 141 ++++++++++++++++++ tests/test_cross_project_contract.py | 73 +++++++++ 5 files changed, 360 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-16-dual-project-contract-implementation.md create mode 100644 references/cross_project_contract/fixture_manifest.v1.json create mode 100644 references/cross_project_contract/sync_ledger.json create mode 100644 scripts/cross_project_contract.py create mode 100644 tests/test_cross_project_contract.py diff --git a/docs/superpowers/plans/2026-07-16-dual-project-contract-implementation.md b/docs/superpowers/plans/2026-07-16-dual-project-contract-implementation.md new file mode 100644 index 00000000..9fd0f01d --- /dev/null +++ b/docs/superpowers/plans/2026-07-16-dual-project-contract-implementation.md @@ -0,0 +1,115 @@ +# Dual-Project Contract Implementation Plan + +> Execute after design approval. Two independent repositories; no runtime coupling. + +## Scope + +Implement the first synchronization foundation in both repositories: + +1. versioned public synthetic-fixture manifest; +2. stable compatibility-hash comparator; +3. append-only cross-project synchronization ledger; +4. focused tests and CI-friendly commands. + +No production deployment, secret access, user-data migration, or formula change is +in this phase. + +## Shared Files + +Create byte-identical copies in both repositories: + +- `references/cross_project_contract/fixture_manifest.v1.json` +- `references/cross_project_contract/sync_ledger.json` +- `scripts/cross_project_contract.py` +- `tests/test_cross_project_contract.py` + +The fixture uses only generic public synthetic birth inputs. It declares the +effective Ayanamsa and node mode. It contains a `compatibility_hash`, not a full +internal `result_hash`: the former is generated from the fixed public contract +fields below and is intentionally insensitive to extra report/evidence fields. + +```text +birth/effective params +ascendant longitude/sign +D1 Sun..Saturn/Rahu/Ketu longitude/sign +``` + +## Task 1: Research Repository Contract + +Files: + +- Modify: `scripts/cross_project_contract.py` (new) +- Modify: `references/cross_project_contract/fixture_manifest.v1.json` (new) +- Modify: `references/cross_project_contract/sync_ledger.json` (new) +- Modify: `tests/test_cross_project_contract.py` (new) + +Tests first: + +1. fixture accepts only synthetic/public metadata and complete effective settings; +2. local calculation reproduces declared compatibility hash; +3. altered node mode or expected hash produces non-zero comparator result; +4. ledger entries require source/target commit, class, file allow-list, secret + review, tests, hash result and rollback reference. + +Implementation: + +1. call common `jyotish_engine.compute_chart_data()` directly; +2. normalize only the contract fields into sorted JSON; +3. SHA-256 the normalized bytes; +4. expose `--manifest`, `--format json`, `--require-match`; +5. validate ledger shape without reading either repository's Git history. + +Verification: + +```bash +python3 -m pytest -q tests/test_cross_project_contract.py +python3 scripts/cross_project_contract.py --require-match --format json +``` + +## Task 2: Commercial Repository Port + +Working tree: `/tmp/Jyotisha-jesse-ux` only after reading its `AGENTS.md` and +running its pre-work check. + +Tests first: copy the same contract tests. The initial test must fail because the +contract files do not exist. Port the source commit's four files without copying +deployment configuration or secrets. + +Verification: + +```bash +.venv/bin/python -m pytest -q tests/test_cross_project_contract.py +.venv/bin/python scripts/cross_project_contract.py --require-match --format json +``` + +If the commercial virtual environment does not exist, use its documented Python +environment and report the missing dependency rather than installing into +production or changing deployment configuration. + +## Task 3: Bidirectional Check + +1. run both comparator commands; +2. compare their JSON `compatibility_hash` and manifest SHA-256; +3. append a research-to-commercial ledger entry with exact commits; +4. run public privacy scanning in both repositories; +5. commit research changes to `codex/release-hygiene-ci` and commercial changes + to `codex/cross-project-contract`; push both branches, but do not merge or + deploy commercial `main`. + +## Failure Handling + +- Hash mismatch: do not normalize it away; record mismatch and identify the + effective parameter/longitude difference. +- Missing external raw: not relevant to a local compatibility hash; retain its + existing `blocked` state in reports. +- Private or production identifier found: reject the port, record the failure, + and do not stage it. +- Existing unrelated dirty files: leave untouched. + +## Discovery Adjustment + +Jyotisha does not yet contain the research repository's +`domain_calculation_service.py`. This is a phase-two safety port, not a +prerequisite for the shared raw-chart compatibility hash. The phase-one +comparator therefore targets the common engine API and does not claim REST/API +calculation-contract parity. diff --git a/references/cross_project_contract/fixture_manifest.v1.json b/references/cross_project_contract/fixture_manifest.v1.json new file mode 100644 index 00000000..dd399d9d --- /dev/null +++ b/references/cross_project_contract/fixture_manifest.v1.json @@ -0,0 +1,27 @@ +{ + "schema_version": 1, + "privacy_scope": "public_synthetic_only", + "fixtures": [ + { + "id": "public_synthetic_delhi_1990_noon_mean_lahiri", + "birth": { + "synthetic": true, + "year": 1990, + "month": 1, + "day": 1, + "hour": 12, + "minute": 0, + "second": 0, + "lat": 28.6139, + "lon": 77.209, + "tz": 5.5 + }, + "effective": { + "ayanamsa": "lahiri", + "node_mode": "mean", + "timezone_offset": 5.5 + }, + "compatibility_hash": "257042461fe303aa3bff5a8333f65090832ce8a2be395f038523838d724b474b" + } + ] +} diff --git a/references/cross_project_contract/sync_ledger.json b/references/cross_project_contract/sync_ledger.json new file mode 100644 index 00000000..df0e369a --- /dev/null +++ b/references/cross_project_contract/sync_ledger.json @@ -0,0 +1,4 @@ +{ + "schema_version": 1, + "entries": [] +} diff --git a/scripts/cross_project_contract.py b/scripts/cross_project_contract.py new file mode 100644 index 00000000..96cf3585 --- /dev/null +++ b/scripts/cross_project_contract.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +"""Validate public synthetic calculation fixtures shared across Jyotish projects.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import sys +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT / "scripts") not in sys.path: + sys.path.insert(0, str(ROOT / "scripts")) + +from jyotish_engine import compute_chart_data + + +PLANETS = ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu") +REQUIRED_LEDGER_FIELDS = { + "source_repository", + "source_commit", + "target_repository", + "target_commit", + "change_class", + "copied_files", + "dependency_delta", + "privacy_review", + "focused_tests", + "hash_contract_result", + "rollback", +} + + +def load_manifest(path: Path) -> dict[str, Any]: + manifest = json.loads(path.read_text(encoding="utf-8")) + if manifest.get("schema_version") != 1: + raise ValueError("fixture manifest schema_version must be 1") + if manifest.get("privacy_scope") != "public_synthetic_only": + raise ValueError("fixture manifest must be public_synthetic_only") + if not isinstance(manifest.get("fixtures"), list) or not manifest["fixtures"]: + raise ValueError("fixture manifest must contain fixtures") + return manifest + + +def load_ledger(path: Path) -> dict[str, Any]: + ledger = json.loads(path.read_text(encoding="utf-8")) + if ledger.get("schema_version") != 1 or not isinstance(ledger.get("entries"), list): + raise ValueError("sync ledger must contain schema_version=1 and entries array") + return ledger + + +def validate_ledger_entry(entry: dict[str, Any]) -> list[str]: + return sorted(REQUIRED_LEDGER_FIELDS - entry.keys()) + + +def _calculate_fixture_chart(fixture: dict[str, Any]) -> dict[str, Any]: + birth = fixture["birth"] + effective = fixture["effective"] + chart, _asc_idx, _jd, _ayanamsa = compute_chart_data( + birth["year"], birth["month"], birth["day"], birth["hour"], birth["minute"], + birth["lat"], birth["lon"], birth["tz"], node_mode=effective["node_mode"], + second=birth.get("second", 0), ayanamsa_name=effective["ayanamsa"], + ) + return chart + + +def _longitude(row: dict[str, Any]) -> float: + value = row.get("lon", row.get("degree")) + if not isinstance(value, (int, float)): + raise ValueError("chart row must provide numeric lon or degree") + return float(value) + + +def compatibility_payload(chart: dict[str, Any], fixture: dict[str, Any]) -> dict[str, Any]: + return { + "fixture_id": fixture["id"], + "birth": {key: value for key, value in fixture["birth"].items() if key != "synthetic"}, + "effective": { + "ayanamsa": fixture["effective"]["ayanamsa"], + "node_mode": fixture["effective"]["node_mode"], + "timezone_offset": fixture["effective"]["timezone_offset"], + }, + "ascendant": { + "sign": chart["ascendant"]["sign"], + "lon": _longitude(chart["ascendant"]), + }, + "planets": { + planet: {"sign": chart["planets"][planet]["sign"], "lon": _longitude(chart["planets"][planet])} + for planet in PLANETS + }, + } + + +def compatibility_hash(chart: dict[str, Any], fixture: dict[str, Any]) -> str: + encoded = json.dumps( + compatibility_payload(chart, fixture), ensure_ascii=True, sort_keys=True, separators=(",", ":") + ).encode("utf-8") + return hashlib.sha256(encoded).hexdigest() + + +def evaluate_manifest(path: Path) -> dict[str, Any]: + manifest = load_manifest(path) + fixtures = [] + for fixture in manifest["fixtures"]: + chart = _calculate_fixture_chart(fixture) + actual = compatibility_hash(chart, fixture) + expected = fixture["compatibility_hash"] + fixtures.append( + { + "id": fixture["id"], + "expected_compatibility_hash": expected, + "actual_compatibility_hash": actual, + "matches": actual == expected, + } + ) + return {"schema_version": 1, "manifest": str(path), "fixtures": fixtures, "matches": all(row["matches"] for row in fixtures)} + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--manifest", + type=Path, + default=ROOT / "references" / "cross_project_contract" / "fixture_manifest.v1.json", + ) + parser.add_argument("--format", choices=("text", "json"), default="text") + parser.add_argument("--require-match", action="store_true") + args = parser.parse_args(argv) + report = evaluate_manifest(args.manifest) + if args.format == "json": + print(json.dumps(report, ensure_ascii=False, indent=2)) + else: + for row in report["fixtures"]: + print(f"{row['id']}: {'match' if row['matches'] else 'mismatch'}") + return 0 if report["matches"] or not args.require_match else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_cross_project_contract.py b/tests/test_cross_project_contract.py new file mode 100644 index 00000000..5a5e9fb4 --- /dev/null +++ b/tests/test_cross_project_contract.py @@ -0,0 +1,73 @@ +"""Public synthetic-fixture contract shared with the commercial repository.""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SCRIPTS = ROOT / "scripts" +if str(SCRIPTS) not in sys.path: + sys.path.insert(0, str(SCRIPTS)) + +import cross_project_contract as contract # noqa: E402 + + +MANIFEST = ROOT / "references" / "cross_project_contract" / "fixture_manifest.v1.json" +LEDGER = ROOT / "references" / "cross_project_contract" / "sync_ledger.json" + + +def test_public_fixture_manifest_has_complete_effective_settings() -> None: + manifest = contract.load_manifest(MANIFEST) + + assert manifest["schema_version"] == 1 + assert manifest["privacy_scope"] == "public_synthetic_only" + fixture = manifest["fixtures"][0] + assert fixture["birth"]["synthetic"] is True + assert fixture["effective"] == {"ayanamsa": "lahiri", "node_mode": "mean", "timezone_offset": 5.5} + + +def test_local_calculation_matches_public_compatibility_hash() -> None: + report = contract.evaluate_manifest(MANIFEST) + + assert report["matches"] is True + assert report["fixtures"][0]["matches"] is True + + +def test_comparator_reports_tampered_expected_hash(tmp_path: Path) -> None: + manifest = json.loads(MANIFEST.read_text(encoding="utf-8")) + manifest["fixtures"][0]["compatibility_hash"] = "0" * 64 + changed = tmp_path / "fixture_manifest.v1.json" + changed.write_text(json.dumps(manifest), encoding="utf-8") + + report = contract.evaluate_manifest(changed) + + assert report["matches"] is False + assert report["fixtures"][0]["matches"] is False + + +def test_compatibility_payload_normalizes_engine_degree_field() -> None: + fixture = contract.load_manifest(MANIFEST)["fixtures"][0] + chart = { + "ascendant": {"sign": "Aries", "degree": 1.25}, + "planets": { + planet: {"sign": "Aries", "degree": float(index)} + for index, planet in enumerate(contract.PLANETS) + }, + } + + payload = contract.compatibility_payload(chart, fixture) + + assert payload["ascendant"]["lon"] == 1.25 + assert payload["planets"]["Sun"]["lon"] == 0.0 + + +def test_sync_ledger_requires_provenance_privacy_tests_hash_and_rollback() -> None: + ledger = contract.load_ledger(LEDGER) + + assert ledger == {"schema_version": 1, "entries": []} + missing = contract.validate_ledger_entry({"source_repository": "x"}) + assert "target_commit" in missing + assert "privacy_review" in missing + assert "rollback" in missing