diff --git a/.github/workflows/repository-intelligence.yml b/.github/workflows/repository-intelligence.yml index 0667af6..50a1eeb 100644 --- a/.github/workflows/repository-intelligence.yml +++ b/.github/workflows/repository-intelligence.yml @@ -2,7 +2,7 @@ name: Repository Intelligence CI on: pull_request: push: - branches: ["feat/repository-intelligence-v1", "feat/repository-intelligence-v2", "feat/repository-intelligence-v3"] + branches: ["feat/repository-intelligence-v1", "feat/repository-intelligence-v2", "feat/repository-intelligence-v3", "feat/governed-developer-os-v1"] permissions: contents: read jobs: @@ -20,3 +20,5 @@ jobs: - run: repo-intel impact /tmp/repository-graph.json README.md --out /tmp/change-impact.json - run: repo-intel openhands-scope /tmp/repository-graph.json /tmp/change-impact.json --operation repo.read --out /tmp/openhands-scope.json - run: python -c 'import json; d=json.load(open("/tmp/openhands-scope.json")); assert d["execution_authorized"] is False' + - run: repo-intel developer-plan /tmp/repository-graph.json /tmp/change-impact.json --change-request 'CI deterministic developer plan' --out /tmp/developer-plan.json + - run: python -c 'import json; d=json.load(open("/tmp/developer-plan.json")); assert d["execution_authorized"] is False and d["invariants"]["patch_requires_hpl_authority"] is True' diff --git a/README.md b/README.md index 87e76a3..41732a6 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This repository is being evolved from file-dump prompting scripts into a deterministic repository-intelligence engine for governed software development. -## v3 capabilities +## v3 capabilities + Phase N Developer OS v1 - SHA-256 repository file inventory; - Python symbol/import/call graphs; @@ -38,3 +38,17 @@ cross-language semantic relationships remain outside v3 coverage. ## Legacy scripts `base_print.py` and `base_print_ai_model.py` remain preserved as historical utilities. + + +## Governed Developer OS v1 + +```bash +repo-intel developer-plan repository-graph.json change-impact.json \ + --change-request "Fix the requested behavior" \ + --out developer-plan.json +``` + +The generated plan is proposal-only. It composes bounded `repo.read`, +`test.execute`, and `repo.patch` scopes, requires retesting after mutation, +requires repository-intelligence recomputation, and requires final evidence. +The patch stage remains subject to separate HPL execution authority. diff --git a/docs/GOVERNED_DEVELOPER_OS_V1.md b/docs/GOVERNED_DEVELOPER_OS_V1.md new file mode 100644 index 0000000..dd13605 --- /dev/null +++ b/docs/GOVERNED_DEVELOPER_OS_V1.md @@ -0,0 +1,34 @@ +# Governed Developer OS v1 + +Phase N v1 composes Repository Intelligence and the already certified OpenHands +capabilities into a deterministic proposal-only developer plan. + +The lifecycle is: + +```text +change request + -> repository graph + -> change impact + -> repo.read scope + -> baseline test scope + -> repo.patch scope + -> retest scope + -> repository intelligence recompute + -> evidence finalization +``` + +The planner does not call OpenHands and does not mint HPL authority. + +Every generated plan records: + +```text +authority_semantics = proposal_plan_only +execution_authorized = false +``` + +The patch stage explicitly requires HPL execution authority. A run is not +reconciled unless every stage has a receipt, every stage succeeded, and the patch +receipt proves an execution token was present. + +v1 is orchestration-contract infrastructure. It does not yet perform cross-repo +execution against the governed kernel. diff --git a/repo_intelligence/__init__.py b/repo_intelligence/__init__.py index d826406..7ea7c25 100644 --- a/repo_intelligence/__init__.py +++ b/repo_intelligence/__init__.py @@ -4,11 +4,14 @@ from .graph import build_repository_graph from .impact import analyze_change_impact from .scope import build_openhands_scope +from .developer_os import build_developer_plan, reconcile_developer_run __all__=[ "build_repository_graph", "analyze_change_impact", "build_openhands_scope", + "build_developer_plan", + "reconcile_developer_run", "load_architecture_contract", "validate_architecture_contract", ] diff --git a/repo_intelligence/cli.py b/repo_intelligence/cli.py index b27324f..3c7caa2 100644 --- a/repo_intelligence/cli.py +++ b/repo_intelligence/cli.py @@ -6,6 +6,7 @@ from .graph import build_repository_graph from .impact import analyze_change_impact from .scope import build_openhands_scope +from .developer_os import build_developer_plan def _write(path:str,data:dict)->None: @@ -34,6 +35,12 @@ def main()->None: s.add_argument("--operation",required=True,choices=["repo.read","test.execute","repo.patch"]) s.add_argument("--out",default="openhands-scope.json") + d=sub.add_parser("developer-plan") + d.add_argument("graph") + d.add_argument("impact") + d.add_argument("--change-request",required=True) + d.add_argument("--out",default="developer-plan.json") + a=p.parse_args() if a.cmd=="graph": @@ -45,10 +52,14 @@ def main()->None: elif a.cmd=="impact": graph=json.loads(Path(a.graph).read_text(encoding="utf-8")) data=analyze_change_impact(graph,a.changed,a.depth) - else: + elif a.cmd=="openhands-scope": graph=json.loads(Path(a.graph).read_text(encoding="utf-8")) impact=json.loads(Path(a.impact).read_text(encoding="utf-8")) data=build_openhands_scope(graph,impact,a.operation) + else: + graph=json.loads(Path(a.graph).read_text(encoding="utf-8")) + impact=json.loads(Path(a.impact).read_text(encoding="utf-8")) + data=build_developer_plan(graph,impact,a.change_request) _write(a.out,data) diff --git a/repo_intelligence/developer_os.py b/repo_intelligence/developer_os.py new file mode 100644 index 0000000..c101640 --- /dev/null +++ b/repo_intelligence/developer_os.py @@ -0,0 +1,138 @@ +"""Deterministic governed Developer OS planning primitives. + +This module composes repository intelligence into a proposal-only developer +execution plan. It never performs repository IO through OpenHands and never +mints execution authority; HPL must separately admit each consequential effect. +""" +from __future__ import annotations + +import hashlib +import json + +from .scope import build_openhands_scope + + +STAGES=("repo.read","test.execute","repo.patch","test.execute") + + +def _canon(v:object)->str: + return json.dumps(v,sort_keys=True,separators=(",",":"),ensure_ascii=False) + + +def _sha(v:object)->str: + return "sha256:"+hashlib.sha256(_canon(v).encode("utf-8")).hexdigest() + + +def build_developer_plan(graph:dict,impact:dict,change_request:str)->dict: + if not isinstance(change_request,str) or not change_request.strip(): + raise ValueError("change_request must be non-empty text") + if impact.get("unknown_paths"): + raise ValueError("cannot build developer plan with unknown changed paths") + + read_scope=build_openhands_scope(graph,impact,"repo.read") + test_scope=build_openhands_scope(graph,impact,"test.execute") + patch_scope=build_openhands_scope(graph,impact,"repo.patch") + + stages=[ + { + "ordinal":0, + "name":"inspect", + "capability":"repo.read", + "scope_sha256":read_scope["scope_sha256"], + "requires_execution_authority":False, + "must_reconcile":True, + }, + { + "ordinal":1, + "name":"baseline_tests", + "capability":"test.execute", + "scope_sha256":test_scope["scope_sha256"], + "requires_execution_authority":False, + "must_reconcile":True, + }, + { + "ordinal":2, + "name":"patch", + "capability":"repo.patch", + "scope_sha256":patch_scope["scope_sha256"], + "requires_execution_authority":True, + "must_reconcile":True, + }, + { + "ordinal":3, + "name":"retest", + "capability":"test.execute", + "scope_sha256":test_scope["scope_sha256"], + "requires_execution_authority":False, + "must_reconcile":True, + }, + { + "ordinal":4, + "name":"recompute_repository_intelligence", + "capability":"repository.recompute", + "scope_sha256":None, + "requires_execution_authority":False, + "must_reconcile":True, + }, + { + "ordinal":5, + "name":"evidence_finalize", + "capability":"evidence.finalize", + "scope_sha256":None, + "requires_execution_authority":False, + "must_reconcile":True, + }, + ] + + core={ + "schema_version":"1.0", + "authority_semantics":"proposal_plan_only", + "execution_authorized":False, + "change_request_sha256":"sha256:"+hashlib.sha256(change_request.strip().encode("utf-8")).hexdigest(), + "graph_sha256":graph.get("graph_sha256"), + "impact_sha256":impact.get("impact_sha256"), + "scopes":{ + "repo.read":read_scope, + "test.execute":test_scope, + "repo.patch":patch_scope, + }, + "stages":stages, + "invariants":{ + "patch_requires_hpl_authority":True, + "no_stage_skips_reconciliation":True, + "retest_required_after_patch":True, + "repository_intelligence_recompute_required":True, + "evidence_finalize_required":True, + }, + } + core["plan_sha256"]=_sha(core) + return core + + +def reconcile_developer_run(plan:dict,stage_receipts:list[dict])->dict: + expected=[s["name"] for s in plan.get("stages",[])] + observed=[str(r.get("stage","")) for r in stage_receipts if isinstance(r,dict)] + missing=[stage for stage in expected if stage not in observed] + failed=[ + str(r.get("stage","")) + for r in stage_receipts + if isinstance(r,dict) and r.get("ok") is not True + ] + patch_receipts=[ + r for r in stage_receipts + if isinstance(r,dict) and r.get("stage")=="patch" + ] + patch_authorized=all(r.get("execution_token_present") is True for r in patch_receipts) if patch_receipts else False + + ok=(not missing and not failed and patch_authorized) + core={ + "schema_version":"1.0", + "plan_sha256":plan.get("plan_sha256"), + "observed_stages":observed, + "missing_stages":missing, + "failed_stages":failed, + "patch_execution_token_verified":patch_authorized, + "reconciled":ok, + } + core["reconciliation_sha256"]=_sha(core) + return core diff --git a/tests/test_developer_os.py b/tests/test_developer_os.py new file mode 100644 index 0000000..b24b2c8 --- /dev/null +++ b/tests/test_developer_os.py @@ -0,0 +1,72 @@ +from pathlib import Path +from repo_intelligence.developer_os import build_developer_plan, reconcile_developer_run +from repo_intelligence.graph import build_repository_graph +from repo_intelligence.impact import analyze_change_impact + + +def _fixture(tmp_path:Path): + (tmp_path/"src").mkdir() + (tmp_path/"tests").mkdir() + (tmp_path/"src"/"calc.py").write_text("def add(a,b):\n return a+b\n",encoding="utf-8") + (tmp_path/"tests"/"test_calc.py").write_text( + "from src.calc import add\n\ndef test_add():\n assert add(1,2)==3\n", + encoding="utf-8", + ) + graph=build_repository_graph(tmp_path) + impact=analyze_change_impact(graph,["src/calc.py"]) + return graph,impact + + +def test_developer_plan_is_deterministic_and_proposal_only(tmp_path:Path): + graph,impact=_fixture(tmp_path) + p1=build_developer_plan(graph,impact,"Fix calc behavior") + p2=build_developer_plan(graph,impact,"Fix calc behavior") + assert p1==p2 + assert p1["authority_semantics"]=="proposal_plan_only" + assert p1["execution_authorized"] is False + patch=[s for s in p1["stages"] if s["name"]=="patch"][0] + assert patch["requires_execution_authority"] is True + assert p1["invariants"]["retest_required_after_patch"] is True + assert p1["plan_sha256"].startswith("sha256:") + + +def test_developer_plan_refuses_unknown_changed_paths(tmp_path:Path): + graph=build_repository_graph(tmp_path) + impact=analyze_change_impact(graph,["missing.py"]) + try: + build_developer_plan(graph,impact,"Fix missing") + except ValueError: + pass + else: + raise AssertionError("expected ValueError") + + +def test_developer_run_reconciliation_requires_all_receipts_and_patch_token(tmp_path:Path): + graph,impact=_fixture(tmp_path) + plan=build_developer_plan(graph,impact,"Fix calc behavior") + receipts=[ + {"stage":"inspect","ok":True}, + {"stage":"baseline_tests","ok":True}, + {"stage":"patch","ok":True,"execution_token_present":True}, + {"stage":"retest","ok":True}, + {"stage":"recompute_repository_intelligence","ok":True}, + {"stage":"evidence_finalize","ok":True}, + ] + result=reconcile_developer_run(plan,receipts) + assert result["reconciled"] is True + assert result["patch_execution_token_verified"] is True + + +def test_reconciliation_refuses_missing_retest(tmp_path:Path): + graph,impact=_fixture(tmp_path) + plan=build_developer_plan(graph,impact,"Fix calc behavior") + receipts=[ + {"stage":"inspect","ok":True}, + {"stage":"baseline_tests","ok":True}, + {"stage":"patch","ok":True,"execution_token_present":True}, + {"stage":"recompute_repository_intelligence","ok":True}, + {"stage":"evidence_finalize","ok":True}, + ] + result=reconcile_developer_run(plan,receipts) + assert result["reconciled"] is False + assert "retest" in result["missing_stages"]