From 2d4a17bbe6bdadcc75dcc31579eb10e1f2f60fa1 Mon Sep 17 00:00:00 2001 From: Mopati Ramaologa <102808043+Mopati123@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:04:47 +0200 Subject: [PATCH 1/8] feat(repo-intel): implement deterministic repository intelligence v1 --- repo_intelligence/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 repo_intelligence/__init__.py diff --git a/repo_intelligence/__init__.py b/repo_intelligence/__init__.py new file mode 100644 index 0000000..ded0177 --- /dev/null +++ b/repo_intelligence/__init__.py @@ -0,0 +1,6 @@ +"""Deterministic repository intelligence primitives.""" + +from .graph import build_repository_graph +from .impact import analyze_change_impact + +__all__ = ["build_repository_graph", "analyze_change_impact"] From 35f23721dce5a6e2b3c47e03863060ac1a823044 Mon Sep 17 00:00:00 2001 From: Mopati Ramaologa <102808043+Mopati123@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:04:51 +0200 Subject: [PATCH 2/8] feat(repo-intel): implement deterministic repository intelligence v1 --- repo_intelligence/graph.py | 114 +++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 repo_intelligence/graph.py diff --git a/repo_intelligence/graph.py b/repo_intelligence/graph.py new file mode 100644 index 0000000..6f338cf --- /dev/null +++ b/repo_intelligence/graph.py @@ -0,0 +1,114 @@ +"""Deterministic repository graph construction. + +v1 intentionally avoids LLM dependence. It builds a reproducible graph from +repository files, Python imports/symbols, and selected JS/TS imports. +""" +from __future__ import annotations + +import ast +import hashlib +import json +import os +import re +from dataclasses import dataclass, asdict +from pathlib import Path +from typing import Iterable + +IGNORE_DIRS={".git",".venv","venv","node_modules","dist","build","__pycache__",".pytest_cache",".mypy_cache"} +TEXT_EXTS={".py",".js",".jsx",".ts",".tsx",".json",".md",".toml",".yaml",".yml"} +JS_IMPORT=re.compile(r"""(?:from\s+['\"]([^'\"]+)['\"]|import\s+['\"]([^'\"]+)['\"]|require\(\s*['\"]([^'\"]+)['\"]\s*\))""") + + +def _sha(data:bytes)->str: + return "sha256:"+hashlib.sha256(data).hexdigest() + + +def _canon(v:object)->str: + return json.dumps(v,sort_keys=True,separators=(",",":"),ensure_ascii=False) + + +def _iter_files(root:Path)->Iterable[Path]: + for base,dirs,files in os.walk(root): + dirs[:] = sorted(d for d in dirs if d not in IGNORE_DIRS) + for name in sorted(files): + p=Path(base)/name + if p.suffix.lower() in TEXT_EXTS and p.is_file() and not p.is_symlink(): + yield p + + +def _module_for(root:Path,path:Path)->str: + rel=path.relative_to(root).with_suffix("") + parts=list(rel.parts) + if parts and parts[-1]=="__init__": parts=parts[:-1] + return ".".join(parts) + + +def _resolve_py_import(root:Path,current:Path,module:str,level:int)->str|None: + current_mod=_module_for(root,current) + base=current_mod.split(".")[:-1] + if level: + keep=max(0,len(base)-level+1) + base=base[:keep] + target=".".join([*base,*([module] if module else [])]).strip(".") if level else module + if not target: return None + candidates=[root/Path(*target.split(".")).with_suffix(".py"),root/Path(*target.split("."))/ "__init__.py"] + for c in candidates: + if c.exists(): return c.relative_to(root).as_posix() + return None + + +def _parse_python(root:Path,path:Path,text:str)->tuple[list[dict],list[str]]: + symbols=[]; deps=[] + try: tree=ast.parse(text) + except SyntaxError: return symbols,deps + for node in tree.body: + if isinstance(node,(ast.FunctionDef,ast.AsyncFunctionDef,ast.ClassDef)): + symbols.append({"name":node.name,"kind":"class" if isinstance(node,ast.ClassDef) else "function","line":getattr(node,"lineno",None)}) + elif isinstance(node,ast.Import): + for alias in node.names: + d=_resolve_py_import(root,path,alias.name,0) + if d: deps.append(d) + elif isinstance(node,ast.ImportFrom): + d=_resolve_py_import(root,path,node.module or "",node.level) + if d: deps.append(d) + return sorted(symbols,key=lambda x:(x["name"],x["kind"],x["line"] or 0)),sorted(set(deps)) + + +def _parse_js_like(root:Path,path:Path,text:str)->list[str]: + out=[] + for m in JS_IMPORT.finditer(text): + spec=next((g for g in m.groups() if g),None) + if not spec or not spec.startswith("."): continue + base=(path.parent/spec).resolve() + for c in [base,base.with_suffix(".js"),base.with_suffix(".jsx"),base.with_suffix(".ts"),base.with_suffix(".tsx"),base/"index.js",base/"index.ts"]: + try: + if c.exists() and c.is_file() and root.resolve() in c.resolve().parents: + out.append(c.relative_to(root).as_posix()); break + except OSError: pass + return sorted(set(out)) + + +def build_repository_graph(root:str|Path)->dict: + root=Path(root).resolve() + if not root.is_dir(): raise ValueError("root must be an existing directory") + files=[]; edges=[]; symbols=[] + for path in _iter_files(root): + rel=path.relative_to(root).as_posix() + raw=path.read_bytes() + try: text=raw.decode("utf-8") + except UnicodeDecodeError: continue + entry={"path":rel,"bytes":len(raw),"sha256":_sha(raw),"extension":path.suffix.lower()} + deps=[] + if path.suffix.lower()==".py": + syms,deps=_parse_python(root,path,text) + for s in syms: symbols.append({"file":rel,**s}) + elif path.suffix.lower() in {".js",".jsx",".ts",".tsx"}: + deps=_parse_js_like(root,path,text) + files.append(entry) + for dep in deps: edges.append({"from":rel,"to":dep,"type":"imports"}) + files=sorted(files,key=lambda x:x["path"]) + edges=sorted(edges,key=lambda x:(x["from"],x["to"],x["type"])) + symbols=sorted(symbols,key=lambda x:(x["file"],x["line"] or 0,x["name"])) + core={"schema_version":"1.0","root_name":root.name,"files":files,"symbols":symbols,"edges":edges} + core["graph_sha256"]=_sha(_canon(core).encode()) + return core From 75531701e5d6e108c33ddb82b69484003750c064 Mon Sep 17 00:00:00 2001 From: Mopati Ramaologa <102808043+Mopati123@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:04:56 +0200 Subject: [PATCH 3/8] feat(repo-intel): implement deterministic repository intelligence v1 --- repo_intelligence/impact.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 repo_intelligence/impact.py diff --git a/repo_intelligence/impact.py b/repo_intelligence/impact.py new file mode 100644 index 0000000..7349b63 --- /dev/null +++ b/repo_intelligence/impact.py @@ -0,0 +1,37 @@ +"""Change-impact analysis over a deterministic repository graph.""" +from __future__ import annotations + +import hashlib,json +from collections import deque + + +def _canon(v:object)->str: + return json.dumps(v,sort_keys=True,separators=(",",":"),ensure_ascii=False) + + +def _sha(s:str)->str: + return "sha256:"+hashlib.sha256(s.encode()).hexdigest() + + +def analyze_change_impact(graph:dict,changed_paths:list[str],max_depth:int=3)->dict: + if max_depth<0: raise ValueError("max_depth must be >= 0") + known={f["path"] for f in graph.get("files",[]) if isinstance(f,dict) and "path" in f} + changed=sorted(set(changed_paths)) + unknown=sorted(p for p in changed if p not in known) + reverse={p:set() for p in known} + for e in graph.get("edges",[]): + if isinstance(e,dict) and e.get("type")=="imports" and e.get("from") in known and e.get("to") in known: + reverse[e["to"]].add(e["from"]) + distance={p:0 for p in changed if p in known} + q=deque(sorted(distance)) + while q: + cur=q.popleft(); d=distance[cur] + if d>=max_depth: continue + for dep in sorted(reverse.get(cur,())): + if dep not in distance: + distance[dep]=d+1; q.append(dep) + impacted=[{"path":p,"distance":distance[p]} for p in sorted(distance,key=lambda x:(distance[x],x))] + symbols=[s for s in graph.get("symbols",[]) if isinstance(s,dict) and s.get("file") in distance] + core={"schema_version":"1.0","graph_sha256":graph.get("graph_sha256"),"changed_paths":changed,"unknown_paths":unknown,"max_depth":max_depth,"impacted_files":impacted,"impacted_symbols":symbols} + core["impact_sha256"]=_sha(_canon(core)) + return core From 8170bb9996c25cccb6e63c220051ebf19f673b5d Mon Sep 17 00:00:00 2001 From: Mopati Ramaologa <102808043+Mopati123@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:05:01 +0200 Subject: [PATCH 4/8] feat(repo-intel): implement deterministic repository intelligence v1 --- repo_intelligence/cli.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 repo_intelligence/cli.py diff --git a/repo_intelligence/cli.py b/repo_intelligence/cli.py new file mode 100644 index 0000000..00b19f2 --- /dev/null +++ b/repo_intelligence/cli.py @@ -0,0 +1,19 @@ +from __future__ import annotations +import argparse,json +from pathlib import Path +from .graph import build_repository_graph +from .impact import analyze_change_impact + + +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") + 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() From c592bc7c595463a7235c509578f8c8a5409cfbc6 Mon Sep 17 00:00:00 2001 From: Mopati Ramaologa <102808043+Mopati123@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:05:06 +0200 Subject: [PATCH 5/8] feat(repo-intel): implement deterministic repository intelligence v1 --- tests/test_repository_intelligence.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tests/test_repository_intelligence.py diff --git a/tests/test_repository_intelligence.py b/tests/test_repository_intelligence.py new file mode 100644 index 0000000..c59741e --- /dev/null +++ b/tests/test_repository_intelligence.py @@ -0,0 +1,21 @@ +from pathlib import Path +import json +from repo_intelligence.graph import build_repository_graph +from repo_intelligence.impact import analyze_change_impact + + +def test_graph_and_impact_are_deterministic(tmp_path:Path): + (tmp_path/"a.py").write_text("from b import f\n\ndef g():\n return f()\n",encoding="utf-8") + (tmp_path/"b.py").write_text("def f():\n return 1\n",encoding="utf-8") + g1=build_repository_graph(tmp_path); g2=build_repository_graph(tmp_path) + assert g1==g2 + assert {"from":"a.py","to":"b.py","type":"imports"} in g1["edges"] + impact=analyze_change_impact(g1,["b.py"]) + assert impact["impacted_files"]==[{"path":"b.py","distance":0},{"path":"a.py","distance":1}] + + +def test_unknown_changed_path_is_reported(tmp_path:Path): + (tmp_path/"x.py").write_text("x=1\n",encoding="utf-8") + graph=build_repository_graph(tmp_path) + impact=analyze_change_impact(graph,["missing.py"]) + assert impact["unknown_paths"]==["missing.py"] From aa7d85dcff40081c5ab719c74cd4d8c01eb58067 Mon Sep 17 00:00:00 2001 From: Mopati Ramaologa <102808043+Mopati123@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:05:10 +0200 Subject: [PATCH 6/8] feat(repo-intel): implement deterministic repository intelligence v1 --- pyproject.toml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7e11a57 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "codebase-prompting-repo-intelligence" +version = "0.1.0" +description = "Deterministic repository graph and change-impact engine" +requires-python = ">=3.10" + +[project.optional-dependencies] +test = ["pytest>=8"] + +[project.scripts] +repo-intel = "repo_intelligence.cli:main" + +[tool.setuptools.packages.find] +include = ["repo_intelligence*"] From 013060160c1ab33ad9303c9f2c8780212bf378a8 Mon Sep 17 00:00:00 2001 From: Mopati Ramaologa <102808043+Mopati123@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:05:14 +0200 Subject: [PATCH 7/8] feat(repo-intel): implement deterministic repository intelligence v1 --- .github/workflows/repository-intelligence.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/repository-intelligence.yml diff --git a/.github/workflows/repository-intelligence.yml b/.github/workflows/repository-intelligence.yml new file mode 100644 index 0000000..85dfeef --- /dev/null +++ b/.github/workflows/repository-intelligence.yml @@ -0,0 +1,19 @@ +name: Repository Intelligence CI +on: + pull_request: + push: + branches: ["feat/repository-intelligence-v1"] +permissions: + contents: read +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - run: python -m pip install -e ".[test]" + - 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:")' From 6b23c394d7d927091919e0bfbab0bc6737cd4bb3 Mon Sep 17 00:00:00 2001 From: Mopati Ramaologa <102808043+Mopati123@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:05:19 +0200 Subject: [PATCH 8/8] feat(repo-intel): implement deterministic repository intelligence v1 --- README.md | 59 +++++++++++++++++-------------------------------------- 1 file changed, 18 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 0f0a4c6..0778647 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,26 @@ -# Project: Codebase Traversal and Gemini AI Integration +# Codebase Prompting → Repository Intelligence -This repository contains two Python scripts: `base_print.py` and `base_print_ai.py`. +This repository is being evolved from file-dump prompting scripts into a deterministic repository-intelligence engine for governed software development. -## `base_print.py` +## v1 capabilities -This script traverses a given directory, collects all text files (with certain extensions), and writes their content to an output file. It handles large files by skipping them and provides a continuation mechanism using a unique key. +- repository file inventory with SHA-256 content identity; +- Python top-level symbol graph; +- Python import dependency graph; +- relative JS/TS import dependency graph; +- reverse dependency change-impact analysis; +- deterministic graph and impact digests; +- CLI suitable for CI and OpenHands/agent consumption. -### Features -- Traverses directories, ignoring specified folders and files. -- Collects text file contents up to specified limits. -- Generates a continuation key to resume processing. -- Outputs a directory tree with indicators for processed and skipped files. +No LLM is required for v1 graph construction. This keeps repository structure and change-impact evidence reproducible. -### Usage -1. **Run the Script**: - ```bash - python base_print.py [directory] [continue_key] - ``` - - `directory`: The directory to search (default is the current directory). - - `continue_key`: The key to continue from where it left off (optional). +## CLI -2. **Output**: - - Generates `codebase_n.txt` with the collected contents and directory tree. - - Prints the continuation key if the line limit is reached. +```bash +repo-intel graph /path/to/repo --out repository-graph.json +repo-intel impact repository-graph.json src/example.py --depth 3 --out change-impact.json +``` -## `base_print_ai.py` +## Legacy scripts -This script extends the functionality of `base_print.py` by integrating with the Google Gemini API. It uses the API to determine relevant files based on a user prompt and processes those files. - -### Features -- Integrates with Google Gemini API for intelligent file selection. -- Processes files based on the relevance determined by the API. -- Outputs a directory tree with indicators for processed and skipped files. - -### Usage -1. **Set Up API Key**: - - Ensure you have set the `API_KEY` environment variable with your Google Gemini API key. - -2. **Run the Script**: - ```bash - python base_print_ai.py [directory] [continue_key] - ``` - - `directory`: The directory to search (default is the current directory). - - `continue_key`: The key to continue from where it left off (optional). - -3. **Output**: - - Generates `prompt_n.txt` with the collected contents and directory tree. - - Prompts the user for an input prompt to guide the file selection process. +`base_print.py` and `base_print_ai_model.py` are preserved as historical utilities. They are not the architectural foundation of the new repository-intelligence layer.