codegraph-ir 0.1.0__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/.github/workflows/release.yml +3 -1
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/Code-IR.md +6 -1
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/PKG-INFO +3 -2
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/README.md +1 -1
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/docs/languages.md +1 -0
- codegraph_ir-0.2.1/docs/plan-0.2-0.4.md +109 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/docs/roadmap.md +2 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/docs/status.md +6 -1
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/pyproject.toml +2 -1
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/schemas/component_spec.schema.json +6 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/__init__.py +1 -1
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/analyses/cfg.py +15 -1
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/analyses/symbols.py +27 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/cli.py +139 -11
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/export/json_export.py +22 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/hooks.py +11 -6
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/ir/component_spec.py +2 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/languages/base.py +67 -0
- codegraph_ir-0.2.1/src/cgir/languages/go.py +519 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/languages/python.py +76 -10
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/languages/registry.py +4 -1
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/languages/typescript.py +101 -9
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/pipeline.py +10 -2
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/report/diff.py +75 -2
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/report/impact.py +33 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/report/pack.py +3 -0
- codegraph_ir-0.2.1/src/cgir/report/pins.py +116 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/slicing/slicer.py +2 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/sources/tree_sitter_source.py +1 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/verify.py +7 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/watch.py +3 -2
- codegraph_ir-0.2.1/tests/integration/test_init.py +73 -0
- codegraph_ir-0.2.1/tests/unit/test_go_adapter.py +189 -0
- codegraph_ir-0.2.1/tests/unit/test_impact_run.py +64 -0
- codegraph_ir-0.2.1/tests/unit/test_pins.py +231 -0
- codegraph_ir-0.2.1/tests/unit/test_shapes.py +126 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_slicer.py +66 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/.github/workflows/ci.yml +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/.gitignore +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/.pre-commit-config.yaml +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/.python-version +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/CLAUDE.md +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/LICENSE +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/RELEASING.md +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/action.yml +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/docs/README.md +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/docs/architecture-rules.md +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/docs/architecture.md +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/docs/development.md +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/docs/experiment-log.md +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/docs/feature-research.md +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/docs/gate-noise.md +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/docs/github-action.md +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/docs/strategy.md +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/examples/agent-pr-contract-drift.md +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/analyses/__init__.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/analyses/call_graph.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/analyses/effects.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/analyses/entrypoints.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/analyses/param_flow.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/analyses/pdg.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/analyses/purity.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/analyses/reaching_defs.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/api/__init__.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/api/mcp_server.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/api/server.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/config.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/export/__init__.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/export/graphml.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/export/html_viz.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/export/mermaid.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/export/neo4j.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/ir/__init__.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/ir/edges.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/ir/graph.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/ir/nodes.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/languages/__init__.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/languages/cache.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/manifest.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/py.typed +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/regenerate/__init__.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/regenerate/prompt_pack.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/regenerate/regenerator.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/report/__init__.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/report/flow.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/report/lint.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/report/stats.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/slicing/__init__.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/sources/__init__.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/sources/base.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/sources/codeql_source.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/sources/joern_source.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/trace/__init__.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/src/cgir/trace/trace_map.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/__init__.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/conftest.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/fixtures/python_sample/orchestrator.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/fixtures/python_sample/pricing.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/fixtures/ts_sample/api/service.ts +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/fixtures/ts_sample/util.ts +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/integration/__init__.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/integration/test_api.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/integration/test_cli_scan.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/integration/test_hook.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/__init__.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_action.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_call_graph.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_cfg.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_component_spec.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_diff.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_effects.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_entrypoints.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_graphml.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_html_viz.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_impact.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_ir_graph.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_language_adapter.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_lint.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_manifest.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_mcp_tools.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_mermaid.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_pack.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_param_flow.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_pdg.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_purity.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_python_di.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_reaching_defs.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_regenerator.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_source_cache.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_stats.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_symbols.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_trace_map.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_tree_sitter_source.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_typescript_adapter.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_verify.py +0 -0
- {codegraph_ir-0.1.0 → codegraph_ir-0.2.1}/tests/unit/test_watch.py +0 -0
|
@@ -55,10 +55,15 @@ H --> I[LLM regenerate / test / trace]
|
|
|
55
55
|
"inputs":{"type":"array","items":{"type":"string"}},"outputs":{"type":"array","items":{"type":"string"}},
|
|
56
56
|
"effects":{"type":"array","items":{"type":"string"}},"calls":{"type":"array","items":{"type":"string"}},
|
|
57
57
|
"reads":{"type":"array","items":{"type":"string"}},"writes":{"type":"array","items":{"type":"string"}},
|
|
58
|
-
"purity":{"type":"number"},"
|
|
58
|
+
"purity":{"type":"number"},"pins":{"type":"array","items":{"type":"string"}},
|
|
59
|
+
"algorithm":{"type":"array","items":{"type":"string"}},
|
|
59
60
|
"trace":{"type":"array","items":{"type":"string"}}}}
|
|
60
61
|
```
|
|
61
62
|
|
|
63
|
+
`pins` are developer-declared invariants extracted from `cgir:` comment
|
|
64
|
+
pragmas (`pure`, `no-<tag>`, `stable-signature`, `frozen`): state pins hold on
|
|
65
|
+
every scan; change pins hold across scan pairs and are always enforced.
|
|
66
|
+
|
|
62
67
|
**Example Python → ComponentSpec**
|
|
63
68
|
```python
|
|
64
69
|
def add_tax(price: float, rate: float) -> float: return price * (1 + rate)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codegraph-ir
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: The deterministic contract layer for AI-modified codebases — effects, purity, contracts, and blast radius, with zero LLM calls.
|
|
5
5
|
Project-URL: Homepage, https://github.com/asonkiya/llm-semantic-compilers
|
|
6
6
|
Project-URL: Repository, https://github.com/asonkiya/llm-semantic-compilers
|
|
@@ -43,6 +43,7 @@ Requires-Python: >=3.11
|
|
|
43
43
|
Requires-Dist: jsonschema>=4.21
|
|
44
44
|
Requires-Dist: networkx>=3.2
|
|
45
45
|
Requires-Dist: pydantic>=2.6
|
|
46
|
+
Requires-Dist: tree-sitter-go<0.25,>=0.23
|
|
46
47
|
Requires-Dist: tree-sitter-python<0.24,>=0.23
|
|
47
48
|
Requires-Dist: tree-sitter-typescript<0.24,>=0.23
|
|
48
49
|
Requires-Dist: tree-sitter<0.25,>=0.23
|
|
@@ -89,7 +90,7 @@ For library/agent use in a project: `uv pip install codegraph-ir`
|
|
|
89
90
|
## The local loop
|
|
90
91
|
|
|
91
92
|
```bash
|
|
92
|
-
cgir scan . # build the .cgir index (Python
|
|
93
|
+
cgir scan . # build the .cgir index (Python, TypeScript, Go)
|
|
93
94
|
cgir watch . # keep it live: re-scan + show contract drift on save
|
|
94
95
|
cgir pack app.service.charge --repo . # minimal context bundle for one component
|
|
95
96
|
cgir impact app.service.charge # blast radius: affected callers, entrypoints, tests
|
|
@@ -23,7 +23,7 @@ For library/agent use in a project: `uv pip install codegraph-ir`
|
|
|
23
23
|
## The local loop
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
cgir scan . # build the .cgir index (Python
|
|
26
|
+
cgir scan . # build the .cgir index (Python, TypeScript, Go)
|
|
27
27
|
cgir watch . # keep it live: re-scan + show contract drift on save
|
|
28
28
|
cgir pack app.service.charge --repo . # minimal context bundle for one component
|
|
29
29
|
cgir impact app.service.charge # blast radius: affected callers, entrypoints, tests
|
|
@@ -27,6 +27,7 @@ full test suite green.
|
|
|
27
27
|
| 2 | CFG statement classification + field extraction (normalized `StatementDesc`) | ✅ done |
|
|
28
28
|
| 3 | ingest structural dispatch + attr extraction (normalized `Declaration`s) | ✅ done |
|
|
29
29
|
| 4 | `TypeScriptAdapter` + per-file language dispatch | ✅ done |
|
|
30
|
+
| 5 | `GoAdapter` (package=directory merge, struct-field DI, panic≙raise) | ✅ done |
|
|
30
31
|
|
|
31
32
|
**Two languages ship (Python, TypeScript); a mixed repo scans both.**
|
|
32
33
|
Zero grammar node-type strings remain outside `cgir/languages/`. The CFG builder is pure topology over statement
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Plan: 0.2 → 0.4 (pins, init, impact --run; data-shape contracts; Go)
|
|
2
|
+
|
|
3
|
+
**Status: all three workstreams landed 2026-07-10** (single sitting; see git
|
|
4
|
+
log). Shipping as one 0.2.0 release rather than three.
|
|
5
|
+
|
|
6
|
+
Three workstreams, landed in this order — each one's machinery feeds the next.
|
|
7
|
+
Depth before width: pins + shapes strengthen the gate for the two languages
|
|
8
|
+
people can use today; Go then inherits all of it.
|
|
9
|
+
|
|
10
|
+
## Workstream A — "Make it personal" (→ 0.2.0)
|
|
11
|
+
|
|
12
|
+
### A1. Declarable contracts (pins)
|
|
13
|
+
|
|
14
|
+
Comment pragmas pinning *intent*, enforced everywhere the pipeline runs:
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
# cgir: pure
|
|
18
|
+
def score(events): ... # trailing form also supported
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Module-level pins at top of file apply to every component in it.
|
|
22
|
+
|
|
23
|
+
Pins split into two classes with different checkers:
|
|
24
|
+
|
|
25
|
+
| pin | class | meaning | checked by |
|
|
26
|
+
|---|---|---|---|
|
|
27
|
+
| `pure` | state (single scan) | kind pure_function, no impure effects incl. transitive | lint, watch |
|
|
28
|
+
| `no-<tag>` | state | tag not in transitive effects (closure over spec.calls) | lint, watch |
|
|
29
|
+
| `stable-signature` | change (scan pair) | signature/outputs may not change | diff, hook, verify, CI |
|
|
30
|
+
| `frozen` | change | no contract field may change; removal is a violation | diff, hook, verify, CI |
|
|
31
|
+
|
|
32
|
+
Change pins are always evaluated — the pin is the opt-in, no --fail-on needed.
|
|
33
|
+
State pins catch violations in newly added code too (drift rules can't).
|
|
34
|
+
|
|
35
|
+
Design: new `report/pins.py` with `state_violations(specs)` and
|
|
36
|
+
`change_violations(old, new)` — pure over specs; transitive effects computed
|
|
37
|
+
by closure over `spec.calls` (no schema change beyond the `pins` field).
|
|
38
|
+
Wire into lint, the diff CLI, hook, verify; pack renders `Pinned: ...` in the
|
|
39
|
+
target header so rewriting agents see the invariant.
|
|
40
|
+
|
|
41
|
+
Plumbing: adapter pragma extraction (generic row-based helper over tree-sitter
|
|
42
|
+
`comment` nodes; `#` / `//` prefixes) → FunctionDecl.pins → node attrs →
|
|
43
|
+
`pins: list[str]` on ComponentSpec (schema in both places + Code-IR.md).
|
|
44
|
+
|
|
45
|
+
Honest limit: a pin is only as strong as effect detection (lexical escapes per
|
|
46
|
+
status.md). Confidence tiers are the future systematic fix.
|
|
47
|
+
|
|
48
|
+
### A2. `cgir init`
|
|
49
|
+
|
|
50
|
+
One-command onboarding: scan → print "what your repo is" (histogram,
|
|
51
|
+
entrypoints, untested-effectful) → write starter `cgir.toml` (never clobbers
|
|
52
|
+
without --force) → `--hook` installs the seatbelt → append `.cgir/` to
|
|
53
|
+
.gitignore → print MCP + Action next-steps.
|
|
54
|
+
|
|
55
|
+
### A3. `cgir impact --run`
|
|
56
|
+
|
|
57
|
+
Execute the impact test list: qualname + trace path → pytest nodeid
|
|
58
|
+
(`path::Class::test_x`), subprocess, propagate exit code. Filter to
|
|
59
|
+
pytest-collectable names (covered_by can include fixture helpers). v1 runs
|
|
60
|
+
pytest only; TS prints the command (runner detection is v1.1).
|
|
61
|
+
|
|
62
|
+
## Workstream C — Data-shape contracts (→ 0.3.0)
|
|
63
|
+
|
|
64
|
+
"The rewrite dropped a field from the returned object" becomes drift — the
|
|
65
|
+
residual failure class in both benchmarks.
|
|
66
|
+
|
|
67
|
+
The DI work already built the foundation: `ClassDecl.fields` (name → type)
|
|
68
|
+
exists in both adapters, and Python's annotated-class-body extraction already
|
|
69
|
+
covers TypedDict/dataclass/pydantic. Additions:
|
|
70
|
+
|
|
71
|
+
- TS: `interface_declaration` / object type-alias members → ClassDecl.fields.
|
|
72
|
+
- Shape lives on the *type* (Class node), not a new spec field. `compute_diff`
|
|
73
|
+
gains a `types` section (added/removed/changed fields per type qualname),
|
|
74
|
+
with `referenced_by` computed via `referenced_type_names` over specs.
|
|
75
|
+
- New violation rule `shape-change`: fires when a drifted type is referenced
|
|
76
|
+
in any component's outputs/inputs.
|
|
77
|
+
- ScanResult carries `types`; diff CLI reads them from repo_graph.json; hook
|
|
78
|
+
and verify inherit.
|
|
79
|
+
|
|
80
|
+
v1 exclusions (stated): inheritance (base-class fields), un-annotated
|
|
81
|
+
dict-literal returns (literal-key fingerprints v1.1), Optional/default
|
|
82
|
+
subtleties beyond the annotation text.
|
|
83
|
+
|
|
84
|
+
## Workstream B — Go adapter (→ 0.4.0)
|
|
85
|
+
|
|
86
|
+
Lands after C so GoAdapter implements fields/shape extraction as part of its
|
|
87
|
+
contract rather than retrofitting.
|
|
88
|
+
|
|
89
|
+
- package = directory; `type X struct/interface` → Class (struct members →
|
|
90
|
+
fields — Go composition maps directly onto the DI machinery); method
|
|
91
|
+
receivers attach methods to the receiver's Class; `raise` ≙ `panic(` only
|
|
92
|
+
(Go errors are values).
|
|
93
|
+
- Effects: net (net/http), fs (os.*), db (database/sql receivers ×
|
|
94
|
+
Query/Exec), io (fmt.Print*/log.*), nondeterm (time.Now/rand/uuid).
|
|
95
|
+
- Same-package cross-file calls (no import in Go): symbol tables for go
|
|
96
|
+
modules sharing a directory are merged (keyed on the module language attr).
|
|
97
|
+
- v1 known limits (documented): cross-package import resolution needs go.mod
|
|
98
|
+
module-prefix stripping (follow-up); struct-in-one-file/methods-in-another
|
|
99
|
+
weakens field DI; `select`/`defer` CFG ordering approximated; `go f()` is a
|
|
100
|
+
call site (future concurrency-effect hook).
|
|
101
|
+
- Validation ritual: fixture tests mirroring the TS suite, scan a real OSS Go
|
|
102
|
+
repo, byte-identical self-diff on Python/TS indexes.
|
|
103
|
+
|
|
104
|
+
## Cross-cutting
|
|
105
|
+
|
|
106
|
+
Every spec-vocabulary change updates Code-IR.md §Data model + both schema
|
|
107
|
+
locations + a schema test. Red-green throughout. Each workstream ships as a
|
|
108
|
+
minor release (0.2.0, 0.3.0, 0.4.0) — release mechanics are push-button
|
|
109
|
+
(RELEASING.md).
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Roadmap
|
|
2
2
|
|
|
3
|
+
**Active plan: [`plan-0.2-0.4.md`](./plan-0.2-0.4.md)** — pins/init/impact-run (0.2.0), data-shape contracts (0.3.0), Go adapter (0.4.0).
|
|
4
|
+
|
|
3
5
|
Forward-looking sequencing. The grouping mirrors `Code-IR.md` §Architecture: **P0** is "you can produce a `ComponentSpec`," **P1** is "you can trust it," **P2** is "you can scale it." Within each tier the order below reflects current dependencies, not strict chronology — feel free to interleave when dependencies allow.
|
|
4
6
|
|
|
5
7
|
## Done
|
|
@@ -66,6 +66,11 @@ cgir scan tests/fixtures/python_sample --out /tmp/cgir-out
|
|
|
66
66
|
| Python DI field types (`__init__` annotations, class-level annotations, direct construction) → `self.<field>.<method>()` resolution + `self.`/`this.` receiver in packs | done | `src/cgir/languages/python.py:_class_fields`, `cli._call_receivers` |
|
|
67
67
|
| Per-component `language` on specs (multi-language repos) | done | `src/cgir/slicing/slicer.py:_node_language` |
|
|
68
68
|
| Packaging: distribution `codegraph-ir` (import + CLI stay `cgir`), py.typed, tag-triggered PyPI release via trusted publishing | done | `pyproject.toml`, `.github/workflows/release.yml`, `RELEASING.md` |
|
|
69
|
+
| Declarable contracts (`cgir:` pins — pure/no-tag/stable-signature/frozen) enforced by lint/diff/hook/verify; rendered in pack | done | `src/cgir/report/pins.py`, `languages/base.py:PinIndex` |
|
|
70
|
+
| `cgir init` (scan summary, starter cgir.toml, .gitignore, `--hook`) | done | `src/cgir/cli.py:init` |
|
|
71
|
+
| `cgir impact --run` (execute the blast-radius tests via pytest node-ids) | done | `report/impact.py:runnable_selectors` |
|
|
72
|
+
| Data-shape contracts (`types` diff section + opt-in `shape-change` rule; TS interfaces/type-aliases, Py TypedDict/dataclass) | done | `report/diff.py:_type_shape_changes`, `json_export.read_types` |
|
|
73
|
+
| Go adapter (package=dir symbol merge, struct-field DI, panic≙raise, effects tables) | done | `src/cgir/languages/go.py` |
|
|
69
74
|
| Entrypoint recognition (HTTP/CLI/task decorators) | done | `src/cgir/analyses/entrypoints.py` |
|
|
70
75
|
| Context packer (`cgir pack`, budget-aware, type closure + docstrings + raises) | done | `src/cgir/report/pack.py` |
|
|
71
76
|
| Docstring / raises / module-variable extraction | done | `tree_sitter_source._docstring_text`, `_raised_names`, `_add_module_variables` |
|
|
@@ -85,7 +90,7 @@ cgir scan tests/fixtures/python_sample --out /tmp/cgir-out
|
|
|
85
90
|
|
|
86
91
|
## Test coverage
|
|
87
92
|
|
|
88
|
-
`pytest -q` runs
|
|
93
|
+
`pytest -q` runs 444 tests, all green (the table below lists the load-bearing files; newer suites — `test_impact.py`, `test_watch.py`, `test_hook.py`, `test_python_di.py`, `test_typescript_adapter.py`, `test_diff.py` — are described by their docstrings):
|
|
89
94
|
|
|
90
95
|
| File | Covers |
|
|
91
96
|
|---|---|
|
|
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
|
|
6
6
|
# Distribution name (the import package and CLI command are both `cgir`;
|
|
7
7
|
# the PyPI project name `cgir` was already taken, so we ship as codegraph-ir).
|
|
8
8
|
name = "codegraph-ir"
|
|
9
|
-
version = "0.1
|
|
9
|
+
version = "0.2.1"
|
|
10
10
|
description = "The deterministic contract layer for AI-modified codebases — effects, purity, contracts, and blast radius, with zero LLM calls."
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
license = { file = "LICENSE" }
|
|
@@ -43,6 +43,7 @@ dependencies = [
|
|
|
43
43
|
"tree-sitter>=0.23,<0.25",
|
|
44
44
|
"tree-sitter-python>=0.23,<0.24",
|
|
45
45
|
"tree-sitter-typescript>=0.23,<0.24",
|
|
46
|
+
"tree-sitter-go>=0.23,<0.25",
|
|
46
47
|
"jsonschema>=4.21",
|
|
47
48
|
]
|
|
48
49
|
|
|
@@ -79,7 +79,13 @@ def build(graph: RepoGraph, repo_path: Path, adapter: LanguageAdapter | None = N
|
|
|
79
79
|
body = file_adapter.function_body(func_ts)
|
|
80
80
|
if body is None:
|
|
81
81
|
continue
|
|
82
|
-
builder = _CFGBuilder(
|
|
82
|
+
builder = _CFGBuilder(
|
|
83
|
+
graph=graph,
|
|
84
|
+
owner=func,
|
|
85
|
+
source=source,
|
|
86
|
+
adapter=file_adapter,
|
|
87
|
+
global_names=file_adapter.global_declared_names(func_ts, source),
|
|
88
|
+
)
|
|
83
89
|
builder.build_block(body, predecessors=[func.id], controller=None)
|
|
84
90
|
|
|
85
91
|
|
|
@@ -89,6 +95,9 @@ class _CFGBuilder:
|
|
|
89
95
|
owner: Node
|
|
90
96
|
source: bytes
|
|
91
97
|
adapter: LanguageAdapter
|
|
98
|
+
# names declared global/nonlocal in this function: writes to them mutate
|
|
99
|
+
# outer state and are recorded as `mutates`, not local `writes`.
|
|
100
|
+
global_names: set[str] = field(default_factory=set)
|
|
92
101
|
_counter: int = field(default=0, init=False)
|
|
93
102
|
|
|
94
103
|
def build_block(
|
|
@@ -102,6 +111,11 @@ class _CFGBuilder:
|
|
|
102
111
|
|
|
103
112
|
def build_stmt(self, ts_node: TSNode, preds: list[str], controller: str | None) -> list[str]:
|
|
104
113
|
desc = self.adapter.describe_statement(ts_node, self.source)
|
|
114
|
+
if isinstance(desc, AssignDesc) and self.global_names:
|
|
115
|
+
outer = [w for w in desc.writes if w in self.global_names]
|
|
116
|
+
if outer:
|
|
117
|
+
desc.writes = [w for w in desc.writes if w not in self.global_names]
|
|
118
|
+
desc.mutates = list(desc.mutates) + outer
|
|
105
119
|
if isinstance(desc, BranchDesc):
|
|
106
120
|
return self._build_branch(ts_node, desc, preds, controller)
|
|
107
121
|
if isinstance(desc, LoopDesc):
|
|
@@ -41,6 +41,8 @@ def build_symbol_tables(graph: RepoGraph) -> dict[str, SymbolTable]:
|
|
|
41
41
|
table.bindings[child.name] = child.id
|
|
42
42
|
tables[module.id] = table
|
|
43
43
|
|
|
44
|
+
_merge_go_packages(graph, tables)
|
|
45
|
+
|
|
44
46
|
for module in graph.nodes(NodeKind.Module):
|
|
45
47
|
table = tables[module.id]
|
|
46
48
|
for child in graph.children(module.id, NodeKind.Import):
|
|
@@ -52,6 +54,31 @@ def build_symbol_tables(graph: RepoGraph) -> dict[str, SymbolTable]:
|
|
|
52
54
|
return tables
|
|
53
55
|
|
|
54
56
|
|
|
57
|
+
def _merge_go_packages(graph: RepoGraph, tables: dict[str, SymbolTable]) -> None:
|
|
58
|
+
"""Go's package scope: files in one directory share top-level names.
|
|
59
|
+
|
|
60
|
+
Modules are per-file, but Go code calls sibling-file functions with no
|
|
61
|
+
import. Union the local bindings of go modules that share a directory
|
|
62
|
+
(existing bindings win, so a file's own names shadow siblings').
|
|
63
|
+
"""
|
|
64
|
+
by_dir: dict[str, list[Node]] = {}
|
|
65
|
+
for module in graph.nodes(NodeKind.Module):
|
|
66
|
+
if module.attrs.get("language") != "go" or not module.path:
|
|
67
|
+
continue
|
|
68
|
+
directory = module.path.rsplit("/", 1)[0] if "/" in module.path else ""
|
|
69
|
+
by_dir.setdefault(directory, []).append(module)
|
|
70
|
+
for modules in by_dir.values():
|
|
71
|
+
if len(modules) < 2:
|
|
72
|
+
continue
|
|
73
|
+
union: dict[str, str | None] = {}
|
|
74
|
+
for module in modules:
|
|
75
|
+
union.update(tables[module.id].bindings)
|
|
76
|
+
for module in modules:
|
|
77
|
+
merged = dict(union)
|
|
78
|
+
merged.update(tables[module.id].bindings) # own names shadow siblings
|
|
79
|
+
tables[module.id].bindings = merged
|
|
80
|
+
|
|
81
|
+
|
|
55
82
|
def _resolve_target(qualname_index: dict[str, str], target: str) -> str | None:
|
|
56
83
|
"""Exact qualname match, else a *unique* suffix match.
|
|
57
84
|
|
|
@@ -188,6 +188,75 @@ def stats(
|
|
|
188
188
|
typer.echo(render_text(result), nl=False)
|
|
189
189
|
|
|
190
190
|
|
|
191
|
+
_STARTER_CONFIG = """\
|
|
192
|
+
# CGIR architecture rules — see docs/architecture-rules.md.
|
|
193
|
+
# Pins (`# cgir: pure`, `no-net`, `stable-signature`, `frozen`) need no config;
|
|
194
|
+
# they are enforced by `cgir lint`, the pre-commit hook, and `cgir diff`.
|
|
195
|
+
#
|
|
196
|
+
# [[rule]]
|
|
197
|
+
# name = "core stays pure"
|
|
198
|
+
# in = "app.core.*"
|
|
199
|
+
# forbid-effect = ["net", "db"]
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
_NEXT_STEPS = """\
|
|
203
|
+
Next steps:
|
|
204
|
+
cgir watch {repo} # live index + contract drift on save
|
|
205
|
+
cgir hook install # pre-commit seatbelt (or: cgir init --hook)
|
|
206
|
+
cgir mcp --index {index} # serve the index to your agent over MCP
|
|
207
|
+
CI gate: see docs/github-action.md
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@app.command()
|
|
212
|
+
def init(
|
|
213
|
+
repo: Annotated[Path, typer.Argument(exists=True, file_okay=False)] = Path("."),
|
|
214
|
+
hook: Annotated[
|
|
215
|
+
bool, typer.Option("--hook", help="Also install the pre-commit seatbelt.")
|
|
216
|
+
] = False,
|
|
217
|
+
force: Annotated[
|
|
218
|
+
bool, typer.Option("--force", help="Overwrite an existing cgir.toml.")
|
|
219
|
+
] = False,
|
|
220
|
+
) -> None:
|
|
221
|
+
"""One-command onboarding: scan, starter config, .gitignore, next steps."""
|
|
222
|
+
result = scan_repo(repo, repo / ".cgir")
|
|
223
|
+
typer.echo(f"Indexed {len(result.specs)} components:")
|
|
224
|
+
_print_kind_histogram(result.specs)
|
|
225
|
+
entrypoints = [s for s in result.specs if s.entrypoint]
|
|
226
|
+
if entrypoints:
|
|
227
|
+
typer.echo(f" entrypoints: {len(entrypoints)}")
|
|
228
|
+
untested = [s for s in result.specs if s.effects and not s.covered_by]
|
|
229
|
+
if untested:
|
|
230
|
+
typer.echo(f" effectful without linked tests: {len(untested)}")
|
|
231
|
+
|
|
232
|
+
config = repo / "cgir.toml"
|
|
233
|
+
if config.exists() and not force:
|
|
234
|
+
typer.echo(f"kept existing {config.name}")
|
|
235
|
+
else:
|
|
236
|
+
config.write_text(_STARTER_CONFIG)
|
|
237
|
+
typer.echo(f"wrote {config.name} (report-only starter)")
|
|
238
|
+
|
|
239
|
+
gitignore = repo / ".gitignore"
|
|
240
|
+
existing = gitignore.read_text() if gitignore.exists() else ""
|
|
241
|
+
if ".cgir/" not in existing:
|
|
242
|
+
gitignore.write_text(
|
|
243
|
+
existing + ("" if existing.endswith("\n") or not existing else "\n") + ".cgir/\n"
|
|
244
|
+
)
|
|
245
|
+
typer.echo("added .cgir/ to .gitignore")
|
|
246
|
+
|
|
247
|
+
if hook:
|
|
248
|
+
from cgir.hooks import install as install_hook
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
path = install_hook(repo)
|
|
252
|
+
typer.echo(f"installed contract seatbelt at {path}")
|
|
253
|
+
except FileExistsError:
|
|
254
|
+
typer.echo("pre-commit hook already exists — skipped (cgir hook install --force)")
|
|
255
|
+
|
|
256
|
+
typer.echo("")
|
|
257
|
+
typer.echo(_NEXT_STEPS.format(repo=repo, index=repo / ".cgir"), nl=False)
|
|
258
|
+
|
|
259
|
+
|
|
191
260
|
@app.command()
|
|
192
261
|
def watch(
|
|
193
262
|
repo: Annotated[Path, typer.Argument(exists=True, file_okay=False)] = Path("."),
|
|
@@ -243,12 +312,20 @@ def impact(
|
|
|
243
312
|
),
|
|
244
313
|
] = None,
|
|
245
314
|
repo: Annotated[
|
|
246
|
-
Path | None,
|
|
315
|
+
Path | None,
|
|
316
|
+
typer.Option("--repo", help="Repo root (with --candidate or --run)."),
|
|
247
317
|
] = None,
|
|
318
|
+
run: Annotated[
|
|
319
|
+
bool,
|
|
320
|
+
typer.Option(
|
|
321
|
+
"--run", help="Execute the selected tests (pytest) and exit with their status."
|
|
322
|
+
),
|
|
323
|
+
] = False,
|
|
248
324
|
) -> None:
|
|
249
325
|
"""Blast radius of changing a component: affected callers, entrypoints at risk, tests to run.
|
|
250
326
|
|
|
251
327
|
Worst-case by default; narrowed by --changed or by a --candidate's actual contract delta.
|
|
328
|
+
--run executes exactly the tests the radius names.
|
|
252
329
|
"""
|
|
253
330
|
specs = _load_specs(index_dir)
|
|
254
331
|
try:
|
|
@@ -273,6 +350,35 @@ def impact(
|
|
|
273
350
|
)
|
|
274
351
|
except KeyError as exc:
|
|
275
352
|
raise typer.BadParameter(f"Unknown component: {component_id}") from exc
|
|
353
|
+
if run:
|
|
354
|
+
_run_impact_tests(specs, list(data["tests"]), repo or Path.cwd())
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _run_impact_tests(specs: list[ComponentSpec], test_ids: list[str], repo: Path) -> None:
|
|
358
|
+
"""Execute the impact-selected tests with pytest; exit with their status."""
|
|
359
|
+
import subprocess
|
|
360
|
+
import sys
|
|
361
|
+
|
|
362
|
+
from cgir.report.impact import runnable_selectors
|
|
363
|
+
|
|
364
|
+
selectors, skipped = runnable_selectors(specs, test_ids)
|
|
365
|
+
for test_id in skipped:
|
|
366
|
+
typer.echo(f" (skipped, not pytest-runnable: {test_id})")
|
|
367
|
+
if not selectors:
|
|
368
|
+
typer.echo("no runnable tests selected.")
|
|
369
|
+
return
|
|
370
|
+
typer.echo(f"running {len(selectors)} test(s):")
|
|
371
|
+
proc = subprocess.run(
|
|
372
|
+
[sys.executable, "-m", "pytest", "-q", *selectors],
|
|
373
|
+
cwd=repo,
|
|
374
|
+
capture_output=True,
|
|
375
|
+
text=True,
|
|
376
|
+
)
|
|
377
|
+
typer.echo(proc.stdout, nl=False)
|
|
378
|
+
if proc.stderr:
|
|
379
|
+
typer.echo(proc.stderr, nl=False)
|
|
380
|
+
if proc.returncode != 0:
|
|
381
|
+
raise typer.Exit(code=proc.returncode)
|
|
276
382
|
|
|
277
383
|
|
|
278
384
|
@app.command()
|
|
@@ -458,26 +564,36 @@ def lint(
|
|
|
458
564
|
config: Annotated[Path, typer.Option("--config", help="Rules file.")] = Path("cgir.toml"),
|
|
459
565
|
as_json: Annotated[bool, typer.Option("--json")] = False,
|
|
460
566
|
) -> None:
|
|
461
|
-
"""Check the index against semantic architecture rules (effects, kind, calls).
|
|
567
|
+
"""Check the index against semantic architecture rules (effects, kind, calls).
|
|
568
|
+
|
|
569
|
+
Pin invariants (`# cgir: pure`, `no-<tag>`) are always checked — no rules
|
|
570
|
+
file needed for those.
|
|
571
|
+
"""
|
|
462
572
|
from cgir.report.lint import lint as run_lint
|
|
463
573
|
from cgir.report.lint import load_rules, render_lint
|
|
574
|
+
from cgir.report.pins import state_violations
|
|
464
575
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
576
|
+
specs = _load_specs(index_dir)
|
|
577
|
+
violations = run_lint(specs, load_rules(config)) if config.exists() else []
|
|
578
|
+
pin_lines = state_violations(specs)
|
|
468
579
|
if as_json:
|
|
469
580
|
typer.echo(
|
|
470
581
|
json.dumps(
|
|
471
|
-
[
|
|
472
|
-
|
|
473
|
-
|
|
582
|
+
[{"rule": v.rule, "component": v.component, "detail": v.detail} for v in violations]
|
|
583
|
+
+ [
|
|
584
|
+
{"rule": "pin", "component": line.split(":")[0], "detail": line}
|
|
585
|
+
for line in pin_lines
|
|
474
586
|
],
|
|
475
587
|
indent=2,
|
|
476
588
|
)
|
|
477
589
|
)
|
|
478
590
|
else:
|
|
479
591
|
typer.echo(render_lint(violations), nl=False)
|
|
480
|
-
|
|
592
|
+
if pin_lines:
|
|
593
|
+
typer.echo(f"pin violations ({len(pin_lines)}):")
|
|
594
|
+
for line in pin_lines:
|
|
595
|
+
typer.echo(f" ! {line}")
|
|
596
|
+
if violations or pin_lines:
|
|
481
597
|
raise typer.Exit(code=1)
|
|
482
598
|
|
|
483
599
|
|
|
@@ -562,10 +678,22 @@ def diff(
|
|
|
562
678
|
),
|
|
563
679
|
] = None,
|
|
564
680
|
) -> None:
|
|
565
|
-
"""Compare two scan indexes: added/removed components and contract drift.
|
|
681
|
+
"""Compare two scan indexes: added/removed components and contract drift.
|
|
682
|
+
|
|
683
|
+
Pin invariants are always enforced: change pins (stable-signature, frozen)
|
|
684
|
+
across the pair, state pins (pure, no-<tag>) on the new index.
|
|
685
|
+
"""
|
|
686
|
+
from cgir.export.json_export import read_types
|
|
687
|
+
from cgir.report.pins import change_violations, state_violations
|
|
688
|
+
|
|
566
689
|
warning = compatibility_warning(read_manifest(old_index), read_manifest(new_index))
|
|
567
|
-
|
|
690
|
+
old_specs, new_specs = _load_specs(old_index), _load_specs(new_index)
|
|
691
|
+
result = compute_diff(
|
|
692
|
+
old_specs, new_specs, old_types=read_types(old_index), new_types=read_types(new_index)
|
|
693
|
+
)
|
|
568
694
|
found = violations(result, list(fail_on or []))
|
|
695
|
+
found += change_violations(old_specs, new_specs)
|
|
696
|
+
found += state_violations(new_specs)
|
|
569
697
|
if as_json:
|
|
570
698
|
payload = dict(result)
|
|
571
699
|
if warning:
|
|
@@ -19,6 +19,28 @@ def read_specs(index_dir: Path) -> list[ComponentSpec]:
|
|
|
19
19
|
]
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
def read_types(index_dir: Path) -> dict[str, dict[str, str]]:
|
|
23
|
+
"""Type shapes (qualname -> field name -> type text) from an index's graph.
|
|
24
|
+
|
|
25
|
+
Sourced from Class nodes' ``fields`` attr — TypedDict/dataclass/pydantic
|
|
26
|
+
bodies and TS interfaces/type-aliases. Empty-field types are skipped.
|
|
27
|
+
"""
|
|
28
|
+
graph_path = index_dir / "repo_graph.json"
|
|
29
|
+
if not graph_path.exists():
|
|
30
|
+
return {}
|
|
31
|
+
data = json.loads(graph_path.read_text())
|
|
32
|
+
out: dict[str, dict[str, str]] = {}
|
|
33
|
+
for node in data.get("nodes", []):
|
|
34
|
+
if node.get("kind") != "Class":
|
|
35
|
+
continue
|
|
36
|
+
attrs = node.get("attrs") or {}
|
|
37
|
+
fields = attrs.get("fields")
|
|
38
|
+
qual = attrs.get("qualname")
|
|
39
|
+
if isinstance(fields, dict) and fields and isinstance(qual, str):
|
|
40
|
+
out[qual] = {str(k): str(v) for k, v in fields.items()}
|
|
41
|
+
return out
|
|
42
|
+
|
|
43
|
+
|
|
22
44
|
def write_index(out_dir: Path, graph: RepoGraph, specs: list[ComponentSpec]) -> None:
|
|
23
45
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
24
46
|
(out_dir / "repo_graph.json").write_text(
|
|
@@ -32,6 +32,7 @@ from cgir.languages import ADAPTERS
|
|
|
32
32
|
from cgir.pipeline import scan_repo
|
|
33
33
|
from cgir.report.diff import compute_diff, render_diff, violations
|
|
34
34
|
from cgir.report.impact import compute_typed_impact
|
|
35
|
+
from cgir.report.pins import change_violations, state_violations
|
|
35
36
|
|
|
36
37
|
# The low-noise default from the real-history noise measurement: fail only
|
|
37
38
|
# when something starts — or silently stops — reaching the outside world.
|
|
@@ -78,15 +79,16 @@ def _staged_supported(repo: Path) -> bool:
|
|
|
78
79
|
return any(Path(n).suffix in _SUPPORTED_EXTS for n in names)
|
|
79
80
|
|
|
80
81
|
|
|
81
|
-
def
|
|
82
|
-
"""Scan the content of a git tree-ish out-of-tree."""
|
|
82
|
+
def _scan_tree(repo: Path, tree: str) -> tuple[list[ComponentSpec], dict[str, dict[str, str]]]:
|
|
83
|
+
"""Scan the content of a git tree-ish out-of-tree: (specs, type shapes)."""
|
|
83
84
|
with tempfile.TemporaryDirectory() as td:
|
|
84
85
|
src = Path(td) / "src"
|
|
85
86
|
src.mkdir()
|
|
86
87
|
archive = subprocess.check_output(["git", "-C", str(repo), "archive", tree])
|
|
87
88
|
subprocess.run(["tar", "-x", "-C", str(src)], input=archive, check=True)
|
|
88
89
|
with tempfile.TemporaryDirectory() as od:
|
|
89
|
-
|
|
90
|
+
result = scan_repo(src, out=Path(od))
|
|
91
|
+
return result.specs, result.types
|
|
90
92
|
|
|
91
93
|
|
|
92
94
|
def run_check(repo: Path, fail_on: list[str] | None = None) -> HookResult:
|
|
@@ -103,13 +105,16 @@ def run_check(repo: Path, fail_on: list[str] | None = None) -> HookResult:
|
|
|
103
105
|
if head_tree == staged_tree:
|
|
104
106
|
return HookResult(checked=False)
|
|
105
107
|
|
|
106
|
-
base_specs =
|
|
107
|
-
head_specs =
|
|
108
|
+
base_specs, base_types = _scan_tree(repo, head_tree) if head_tree else ([], {})
|
|
109
|
+
head_specs, head_types = _scan_tree(repo, staged_tree)
|
|
108
110
|
except Exception as exc: # never block a commit because of our own bug
|
|
109
111
|
return HookResult(checked=False, error=f"{type(exc).__name__}: {exc}")
|
|
110
112
|
|
|
111
|
-
diff = compute_diff(base_specs, head_specs)
|
|
113
|
+
diff = compute_diff(base_specs, head_specs, old_types=base_types, new_types=head_types)
|
|
112
114
|
found = violations(diff, rules)
|
|
115
|
+
# Pin invariants are always enforced, independent of --fail-on rules.
|
|
116
|
+
found += change_violations(base_specs, head_specs)
|
|
117
|
+
found += state_violations(head_specs)
|
|
113
118
|
changed = [c["id"] for c in diff["changed"]]
|
|
114
119
|
|
|
115
120
|
tests: set[str] = set()
|
|
@@ -41,6 +41,7 @@ COMPONENT_SPEC_SCHEMA: dict[str, Any] = {
|
|
|
41
41
|
"reads": {"type": "array", "items": {"type": "string"}},
|
|
42
42
|
"writes": {"type": "array", "items": {"type": "string"}},
|
|
43
43
|
"purity": {"type": "number", "minimum": 0, "maximum": 1},
|
|
44
|
+
"pins": {"type": "array", "items": {"type": "string"}},
|
|
44
45
|
"algorithm": {"type": "array", "items": {"type": "string"}},
|
|
45
46
|
"trace": {"type": "array", "items": {"type": "string"}},
|
|
46
47
|
},
|
|
@@ -75,6 +76,7 @@ class ComponentSpec:
|
|
|
75
76
|
reads: list[str] = field(default_factory=list)
|
|
76
77
|
writes: list[str] = field(default_factory=list)
|
|
77
78
|
purity: float | None = None
|
|
79
|
+
pins: list[str] = field(default_factory=list)
|
|
78
80
|
algorithm: list[str] = field(default_factory=list)
|
|
79
81
|
|
|
80
82
|
def to_dict(self) -> dict[str, Any]:
|