feat: add public cross-project calculation contract
This commit is contained in:
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"entries": []
|
||||
}
|
||||
@@ -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())
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user