Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/repository-intelligence.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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'
42 changes: 24 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
24 changes: 24 additions & 0 deletions docs/ARCHITECTURE_CONTRACT.md
Original file line number Diff line number Diff line change
@@ -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.
34 changes: 34 additions & 0 deletions docs/OPENHANDS_SCOPE.md
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 9 additions & 1 deletion repo_intelligence/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
]
85 changes: 85 additions & 0 deletions repo_intelligence/architecture.py
Original file line number Diff line number Diff line change
@@ -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"])
52 changes: 45 additions & 7 deletions repo_intelligence/cli.py
Original file line number Diff line number Diff line change
@@ -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()
63 changes: 63 additions & 0 deletions repo_intelligence/scope.py
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading