From e8ebf6abf9bfc7506de309b55d61fa709d800344 Mon Sep 17 00:00:00 2001 From: Mopati Ramaologa <102808043+Mopati123@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:23:41 +0200 Subject: [PATCH 1/9] feat(repo-intel): add architecture contracts and OpenHands scopes --- repo_intelligence/architecture.py | 85 +++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 repo_intelligence/architecture.py diff --git a/repo_intelligence/architecture.py b/repo_intelligence/architecture.py new file mode 100644 index 0000000..9d855f5 --- /dev/null +++ b/repo_intelligence/architecture.py @@ -0,0 +1,85 @@ +"""Architecture-component contract support for repository intelligence v3.""" +from __future__ import annotations + +import fnmatch +import hashlib +import json +from pathlib import Path + + +ALLOWED_EDGE_TYPES={"depends_on","configures","deploys"} + + +class ArchitectureContractError(ValueError): + pass + + +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 load_architecture_contract(path:str|Path)->dict: + data=json.loads(Path(path).read_text(encoding="utf-8")) + return validate_architecture_contract(data) + + +def validate_architecture_contract(data:dict)->dict: + if not isinstance(data,dict): + raise ArchitectureContractError("architecture contract must be an object") + if data.get("schema_version")!="1.0": + raise ArchitectureContractError("schema_version must be 1.0") + + components=data.get("components") + if not isinstance(components,list) or not components: + raise ArchitectureContractError("components must be a non-empty list") + + names=set() + normalized=[] + for item in components: + if not isinstance(item,dict): + raise ArchitectureContractError("component entry must be an object") + name=str(item.get("name","")).strip() + paths=item.get("paths") + if not name or name in names: + raise ArchitectureContractError("component names must be unique and non-empty") + if not isinstance(paths,list) or not paths or any(not isinstance(p,str) or not p.strip() for p in paths): + raise ArchitectureContractError(f"component {name} must define non-empty path globs") + names.add(name) + normalized.append({"name":name,"paths":sorted(set(p.strip() for p in paths))}) + + edges=[] + for item in data.get("dependencies",[]): + if not isinstance(item,dict): + raise ArchitectureContractError("dependency entry must be an object") + src=str(item.get("from","")).strip() + dst=str(item.get("to","")).strip() + typ=str(item.get("type","depends_on")).strip() + if src not in names or dst not in names: + raise ArchitectureContractError("dependency endpoints must reference declared components") + if typ not in ALLOWED_EDGE_TYPES: + raise ArchitectureContractError(f"unsupported dependency type: {typ}") + edges.append({"from":src,"to":dst,"type":typ}) + + core={ + "schema_version":"1.0", + "components":sorted(normalized,key=lambda x:x["name"]), + "dependencies":sorted(edges,key=lambda x:(x["from"],x["to"],x["type"])), + } + core["contract_sha256"]=_sha(core) + return core + + +def map_files_to_components(files:list[dict],contract:dict)->list[dict]: + out=[] + for file_entry in files: + path=str(file_entry.get("path","")) + matches=[] + for component in contract.get("components",[]): + if any(fnmatch.fnmatch(path,pattern) for pattern in component.get("paths",[])): + matches.append(component["name"]) + out.append({"file":path,"components":sorted(matches)}) + return sorted(out,key=lambda x:x["file"]) From 893f534591a848b9869f17b35fd56b82137f8196 Mon Sep 17 00:00:00 2001 From: Mopati Ramaologa <102808043+Mopati123@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:23:46 +0200 Subject: [PATCH 2/9] feat(repo-intel): add architecture contracts and OpenHands scopes --- repo_intelligence/scope.py | 63 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 repo_intelligence/scope.py diff --git a/repo_intelligence/scope.py b/repo_intelligence/scope.py new file mode 100644 index 0000000..97f9bdd --- /dev/null +++ b/repo_intelligence/scope.py @@ -0,0 +1,63 @@ +"""Machine-readable OpenHands proposal-scope generation. + +The result is not execution authority. It is a deterministic candidate scope +that a separate HPL admission decision may accept, narrow, or refuse. +""" +from __future__ import annotations + +import hashlib +import json + + +ALLOWED_OPERATIONS={"repo.read","test.execute","repo.patch"} + + +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_openhands_scope(graph:dict,impact:dict,operation:str)->dict: + if operation not in ALLOWED_OPERATIONS: + raise ValueError(f"unsupported OpenHands operation: {operation}") + + impacted=[x for x in impact.get("impacted_files",[]) if isinstance(x,dict)] + paths=sorted({str(x.get("path","")) for x in impacted if str(x.get("path","")).strip()}) + tests=sorted({ + str(x.get("path","")) + for x in impact.get("impacted_tests",[]) + if isinstance(x,dict) and str(x.get("path","")).strip() + }) + changed=sorted(set(str(x) for x in impact.get("changed_paths",[]) if str(x).strip())) + + if operation=="repo.patch": + writable=changed + readable=paths + executable_tests=tests + elif operation=="test.execute": + writable=[] + readable=paths + executable_tests=tests + else: + writable=[] + readable=paths + executable_tests=[] + + core={ + "schema_version":"1.0", + "authority_semantics":"proposal_scope_only", + "operation":operation, + "graph_sha256":graph.get("graph_sha256"), + "impact_sha256":impact.get("impact_sha256"), + "readable_paths":readable, + "writable_paths":writable, + "test_paths":executable_tests, + "impacted_components":sorted(set(impact.get("impacted_components",[]))), + "unknown_paths":sorted(set(impact.get("unknown_paths",[]))), + "execution_authorized":False, + } + core["scope_sha256"]=_sha(core) + return core From 834f378e38c46176ca59fe9c54d5aecb48402bee Mon Sep 17 00:00:00 2001 From: Mopati Ramaologa <102808043+Mopati123@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:23:52 +0200 Subject: [PATCH 3/9] feat(repo-intel): add architecture contracts and OpenHands scopes --- repo_intelligence/cli.py | 52 ++++++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/repo_intelligence/cli.py b/repo_intelligence/cli.py index 00b19f2..b27324f 100644 --- a/repo_intelligence/cli.py +++ b/repo_intelligence/cli.py @@ -1,19 +1,57 @@ from __future__ import annotations import argparse,json from pathlib import Path + +from .architecture import load_architecture_contract, map_files_to_components from .graph import build_repository_graph from .impact import analyze_change_impact +from .scope import build_openhands_scope + + +def _write(path:str,data:dict)->None: + Path(path).write_text(json.dumps(data,indent=2,sort_keys=True)+"\n",encoding="utf-8") + print(path) def main()->None: p=argparse.ArgumentParser(prog="repo-intel") sub=p.add_subparsers(dest="cmd",required=True) - g=sub.add_parser("graph"); g.add_argument("root"); g.add_argument("--out",default="repository-graph.json") - i=sub.add_parser("impact"); i.add_argument("graph"); i.add_argument("changed",nargs="+"); i.add_argument("--depth",type=int,default=3); i.add_argument("--out",default="change-impact.json") + + g=sub.add_parser("graph") + g.add_argument("root") + g.add_argument("--architecture") + g.add_argument("--out",default="repository-graph.json") + + i=sub.add_parser("impact") + i.add_argument("graph") + i.add_argument("changed",nargs="+") + i.add_argument("--depth",type=int,default=3) + i.add_argument("--out",default="change-impact.json") + + s=sub.add_parser("openhands-scope") + s.add_argument("graph") + s.add_argument("impact") + s.add_argument("--operation",required=True,choices=["repo.read","test.execute","repo.patch"]) + s.add_argument("--out",default="openhands-scope.json") + a=p.parse_args() - if a.cmd=="graph": data=build_repository_graph(a.root) - else: data=analyze_change_impact(json.loads(Path(a.graph).read_text(encoding="utf-8")),a.changed,a.depth) - Path(a.out).write_text(json.dumps(data,indent=2,sort_keys=True)+"\n",encoding="utf-8") - print(a.out) -if __name__=="__main__": main() + if a.cmd=="graph": + data=build_repository_graph(a.root) + if a.architecture: + contract=load_architecture_contract(a.architecture) + data["architecture_contract"]=contract + data["architecture_membership"]=map_files_to_components(data["files"],contract) + 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: + 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) + + _write(a.out,data) + + +if __name__=="__main__": + main() From e02773f7e415649a470f27fe7df39cd53a2967b2 Mon Sep 17 00:00:00 2001 From: Mopati Ramaologa <102808043+Mopati123@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:23:56 +0200 Subject: [PATCH 4/9] feat(repo-intel): add architecture contracts and OpenHands scopes --- repo_intelligence/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/repo_intelligence/__init__.py b/repo_intelligence/__init__.py index ded0177..d826406 100644 --- a/repo_intelligence/__init__.py +++ b/repo_intelligence/__init__.py @@ -1,6 +1,14 @@ """Deterministic repository intelligence primitives.""" +from .architecture import load_architecture_contract, validate_architecture_contract from .graph import build_repository_graph from .impact import analyze_change_impact +from .scope import build_openhands_scope -__all__ = ["build_repository_graph", "analyze_change_impact"] +__all__=[ + "build_repository_graph", + "analyze_change_impact", + "build_openhands_scope", + "load_architecture_contract", + "validate_architecture_contract", +] From b128d9b2b872f0bad6f2032f514c7a6b8917b3af Mon Sep 17 00:00:00 2001 From: Mopati Ramaologa <102808043+Mopati123@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:24:01 +0200 Subject: [PATCH 5/9] feat(repo-intel): add architecture contracts and OpenHands scopes --- tests/test_repository_intelligence.py | 64 +++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/test_repository_intelligence.py b/tests/test_repository_intelligence.py index bd0c881..c89d15b 100644 --- a/tests/test_repository_intelligence.py +++ b/tests/test_repository_intelligence.py @@ -48,3 +48,67 @@ def test_unknown_changed_path_is_reported(tmp_path:Path): graph=build_repository_graph(tmp_path) impact=analyze_change_impact(graph,["missing.py"]) assert impact["unknown_paths"]==["missing.py"] + + +from pathlib import Path +import json +from repo_intelligence.architecture import ( + ArchitectureContractError, + map_files_to_components, + validate_architecture_contract, +) +from repo_intelligence.graph import build_repository_graph +from repo_intelligence.impact import analyze_change_impact +from repo_intelligence.scope import build_openhands_scope + + +def test_architecture_contract_maps_files(tmp_path:Path): + (tmp_path/"src").mkdir() + (tmp_path/"tests").mkdir() + (tmp_path/"src"/"app.py").write_text("def f():\n return 1\n",encoding="utf-8") + (tmp_path/"tests"/"test_app.py").write_text("from src.app import f\n",encoding="utf-8") + graph=build_repository_graph(tmp_path) + contract=validate_architecture_contract({ + "schema_version":"1.0", + "components":[ + {"name":"runtime","paths":["src/**"]}, + {"name":"tests","paths":["tests/**"]}, + ], + "dependencies":[ + {"from":"tests","to":"runtime","type":"depends_on"} + ], + }) + membership=map_files_to_components(graph["files"],contract) + assert {"file":"src/app.py","components":["runtime"]} in membership + assert contract["contract_sha256"].startswith("sha256:") + + +def test_architecture_contract_rejects_unknown_dependency(): + try: + validate_architecture_contract({ + "schema_version":"1.0", + "components":[{"name":"runtime","paths":["src/**"]}], + "dependencies":[{"from":"runtime","to":"missing","type":"depends_on"}], + }) + except ArchitectureContractError: + pass + else: + raise AssertionError("expected ArchitectureContractError") + + +def test_openhands_scope_is_proposal_only(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"]) + scope=build_openhands_scope(graph,impact,"repo.patch") + assert scope["authority_semantics"]=="proposal_scope_only" + assert scope["execution_authorized"] is False + assert scope["writable_paths"]==["src/calc.py"] + assert "tests/test_calc.py" in scope["test_paths"] + assert scope["scope_sha256"].startswith("sha256:") From 3040d0c41c50cb3108790a5afaeaf2950767b374 Mon Sep 17 00:00:00 2001 From: Mopati Ramaologa <102808043+Mopati123@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:24:05 +0200 Subject: [PATCH 6/9] feat(repo-intel): add architecture contracts and OpenHands scopes --- docs/ARCHITECTURE_CONTRACT.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 docs/ARCHITECTURE_CONTRACT.md diff --git a/docs/ARCHITECTURE_CONTRACT.md b/docs/ARCHITECTURE_CONTRACT.md new file mode 100644 index 0000000..dbecd1d --- /dev/null +++ b/docs/ARCHITECTURE_CONTRACT.md @@ -0,0 +1,24 @@ +# Repository Intelligence Architecture Contract v1 + +An optional architecture contract gives repository intelligence an explicit, +machine-readable component model. + +Example: + +```json +{ + "schema_version": "1.0", + "components": [ + {"name": "runtime", "paths": ["src/runtime/**"]}, + {"name": "tests", "paths": ["tests/**"]} + ], + "dependencies": [ + {"from": "tests", "to": "runtime", "type": "depends_on"} + ] +} +``` + +Allowed dependency types are `depends_on`, `configures`, and `deploys`. +The contract is normalized and SHA-256 identified. + +This contract describes architecture; it does not grant execution authority. From 36475394856ef66f74902656e65510dd603f1fbc Mon Sep 17 00:00:00 2001 From: Mopati Ramaologa <102808043+Mopati123@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:24:08 +0200 Subject: [PATCH 7/9] feat(repo-intel): add architecture contracts and OpenHands scopes --- docs/OPENHANDS_SCOPE.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 docs/OPENHANDS_SCOPE.md diff --git a/docs/OPENHANDS_SCOPE.md b/docs/OPENHANDS_SCOPE.md new file mode 100644 index 0000000..976909d --- /dev/null +++ b/docs/OPENHANDS_SCOPE.md @@ -0,0 +1,34 @@ +# OpenHands proposal scope v1 + +Repository Intelligence can convert a deterministic change-impact result into a +machine-readable candidate scope for OpenHands. + +Supported operations: + +- `repo.read` +- `test.execute` +- `repo.patch` + +For `repo.patch`, only the originally changed paths are emitted as writable; +reverse-dependency files remain readable and impacted tests remain executable +test candidates. + +Every scope includes: + +- graph SHA-256; +- impact SHA-256; +- readable paths; +- writable paths; +- test paths; +- impacted components; +- unknown paths; +- deterministic scope SHA-256. + +The scope always records: + +```text +authority_semantics = proposal_scope_only +execution_authorized = false +``` + +A separate HPL admission decision must authorize any consequential effect. From 01ca8b4d0a6b508c16db1e43ae5456e1249572d6 Mon Sep 17 00:00:00 2001 From: Mopati Ramaologa <102808043+Mopati123@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:24:11 +0200 Subject: [PATCH 8/9] feat(repo-intel): add architecture contracts and OpenHands scopes --- README.md | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 91359e5..87e76a3 100644 --- a/README.md +++ b/README.md @@ -2,33 +2,39 @@ This repository is being evolved from file-dump prompting scripts into a deterministic repository-intelligence engine for governed software development. -## v2 capabilities - -- repository file inventory with SHA-256 content identity; -- Python top-level symbol graph; -- Python import dependency graph; -- conservative Python symbol-call graph; -- relative JS/TS import dependency graph; -- test-to-code dependency edges; -- top-level repository component mapping; -- reverse dependency change-impact analysis; -- impacted test and component identification; -- deterministic graph and impact digests; -- CLI suitable for CI and OpenHands/agent consumption. - -No LLM is required for graph construction. This keeps repository structure and change-impact evidence reproducible. +## v3 capabilities + +- SHA-256 repository file inventory; +- Python symbol/import/call graphs; +- relative JS/TS dependency graph; +- test-to-code mapping; +- change-impact traversal with impacted tests/components; +- optional architecture-component contracts with typed component dependencies; +- deterministic architecture-contract digests; +- machine-readable OpenHands proposal scopes; +- deterministic graph, impact, and scope digests; +- CLI suitable for CI and governed developer workflows. ## CLI ```bash -repo-intel graph /path/to/repo --out repository-graph.json +repo-intel graph /path/to/repo --architecture architecture.json --out repository-graph.json repo-intel impact repository-graph.json src/example.py --depth 3 --out change-impact.json +repo-intel openhands-scope repository-graph.json change-impact.json --operation repo.patch --out openhands-scope.json ``` +## Authority boundary + +Repository Intelligence proposes bounded read/test/write scope. It never grants +execution authority. Generated OpenHands scopes explicitly set +`execution_authorized=false`; HPL remains the separate execution authority. + ## Static-analysis truth boundary -The graph is intentionally conservative. It does not claim complete semantic program analysis. Dynamic imports, reflection, monkey-patching, generated code, runtime dependency injection, and many cross-language call relationships require later analyzers. +The graph is intentionally conservative. Dynamic imports, reflection, +monkey-patching, generated code, runtime dependency injection, and many +cross-language semantic relationships remain outside v3 coverage. ## Legacy scripts -`base_print.py` and `base_print_ai_model.py` are preserved as historical utilities. They are not the architectural foundation of the repository-intelligence layer. +`base_print.py` and `base_print_ai_model.py` remain preserved as historical utilities. From 67b07ca77b802fe2ba6a223264f9e5a46535c4bd Mon Sep 17 00:00:00 2001 From: Mopati Ramaologa <102808043+Mopati123@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:24:15 +0200 Subject: [PATCH 9/9] feat(repo-intel): add architecture contracts and OpenHands scopes --- .github/workflows/repository-intelligence.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/repository-intelligence.yml b/.github/workflows/repository-intelligence.yml index f6585b6..0667af6 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"] + branches: ["feat/repository-intelligence-v1", "feat/repository-intelligence-v2", "feat/repository-intelligence-v3"] permissions: contents: read jobs: @@ -17,3 +17,6 @@ jobs: - run: pytest -q - run: repo-intel graph . --out /tmp/repository-graph.json - run: python -c 'import json; d=json.load(open("/tmp/repository-graph.json")); assert d["graph_sha256"].startswith("sha256:")' + - 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'