Skip to content
Merged
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: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,13 @@ make coverage-html # same, writes htmlcov/
The pipeline runs in three phases: **Extract → Resolve → Store**.

```
IngestionPipeline (pipeline.py)
IngestionPipeline (pipeline/__init__.py)
├── cgis.languages (registry) ← lazy: a pack is imported when its first file is met
│ ├── languages/python/ ← PythonExtractor + PACK
│ └── languages/typescript/ ← TypeScriptExtractor (.ts/.tsx) + PACK
├── ResolverEngine (resolver/engine.py) ← raw_call: targets → resolved FQN edges
└── SQLiteStore (storage/sqlite_store.py) ← persists graph; WAL mode
├── SQLiteStore (storage/sqlite_store.py) ← persists graph; WAL mode
└── post-write passes (pipeline/) ← refresh_dispatch, SemanticUpliftEngine (pipeline/uplift.py)
└── QueryEngine (query/engine.py) ← BFS traversals (impact / flow)
```

Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/ONTOLOGY.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,4 @@ domains:
- "*storage.*"
```

**Ontology compliance rule:** Any PR that adds a new `NodeType`, `EdgeType`, or modifies `domains.yaml` must update this document and the `_ONTOLOGY_CLASS` mapping in `src/cgis/resolver/uplift.py`.
**Ontology compliance rule:** Any PR that adds a new `NodeType`, `EdgeType`, or modifies `domains.yaml` must update this document and the `_ONTOLOGY_CLASS` mapping in `src/cgis/pipeline/uplift.py`.
9 changes: 4 additions & 5 deletions docs/ontology/domains.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,14 @@ domains:
- "*storage.*"

SemanticEngine:
description: "Symbol resolution, linkage, and semantic uplift"
description: "Symbol resolution and linkage"
heuristics:
file_path_patterns:
- "*resolver/*"
fqn_patterns:
- "*.Resolver*"
- "*.ResolverEngine*"
- "*resolver.*"
- "*.uplift*"
- "*.SemanticUplift*"

QueryLayer:
description: "Graph traversal and query operations"
Expand All @@ -53,12 +51,13 @@ domains:
- "*.EdgeType*"

Pipeline:
description: "Ingestion pipeline orchestration"
description: "Ingestion pipeline orchestration and its post-write passes (semantic uplift, dispatch)"
heuristics:
file_path_patterns:
- "*pipeline.py"
- "*pipeline/*"
fqn_patterns:
- "*.IngestionPipeline*"
- "*pipeline"
- "*pipeline.*"

APILayer:
Expand Down
7 changes: 6 additions & 1 deletion docs/ontology/patterns.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,12 @@ project_domains:
profile: python
drift_tolerance: 0.40 # v2 measured ≈0.25 post-#115 split (tv_imp 0.00 — real 021C chain engine→symbols→indices; residual is tv_calls)
hygiene_baseline:
unresolved_ratio: 0.45 # measured 0.4451 when the gate came alive (#149)
# Re-baselined 0.45 -> 0.46 in language-packs step 5 (user decision): moving
# resolver/uplift.py into cgis/pipeline/ took 70 calls, 28 of them unresolved
# (0.40), out of a domain at 0.44 — 136/309 became 108/239 = 0.452 with no
# unresolved edge added and the moved code unchanged. A denominator effect,
# not a resolution regression; 0.46 keeps the ratchet's usual small headroom.
unresolved_ratio: 0.46 # measured 0.4519 after step 5 (was 0.4451 at #149)

- name: "pipeline"
fqn_prefix: "cgis.pipeline"
Expand Down
187 changes: 187 additions & 0 deletions docs/plans/2026-10-14-language-packs-step5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
# Language Packs — Step 5: Build-Plane Passes — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Move the post-write passes into a `cgis/pipeline/` package so the resolver no longer touches storage, with no stored graph changing.

**Architecture:** `src/cgis/pipeline.py` becomes the package `src/cgis/pipeline/` and keeps its contents in `__init__.py`, so the module FQN `cgis.pipeline` and all 32 `from cgis.pipeline import IngestionPipeline` imports stay as they are. There is no shim: the module simply becomes a package. `src/cgis/resolver/uplift.py` moves to `src/cgis/pipeline/uplift.py`. It is the only resolver module that reads or writes the store, so the `resolver-storage` allowlist (8 edges today) becomes empty. `refresh_dispatch` already lives in the pipeline. Its pure half, `dispatch_edges`, takes methods and an inheritance tree with no store, so it stays in the resolver.

**Tech Stack:** Python 3.12, tree-sitter, SQLite, pytest, ruff, mypy strict, interrogate.

**Spec:** `docs/specs/2026-10-14-language-packs-design.md` (rev 3): "Architecture → Build plane" and Migration row 5. Outline: `docs/plans/2026-10-14-language-packs.md`, "Step 5". Steps 1–4: #597, #599, #600 and #601, plus the spec's implementation notes.

## Global Constraints

- **Full gate before every commit:** `make format && make lint && make type-check && make pytest && make doc-coverage` passes. Chain the commands with `&&`; never use `;`, a bare newline after a heredoc, or a pipe on the gating command.
- **Typing and quality floors:** mypy strict on `src/`; docstring coverage ≥ 90%; Sonar cognitive complexity ≤ 15 per function.
- **Behaviour unchanged:** `tests/fixtures/graph_identity/snapshot.json` is not modified, and a main-vs-branch `graph_identity diff` reads `identical` for cgis `src` @715cd9a, owner-api @8de2245 and `ui/`.
- **Spec acceptance:** the `resolver-storage` allowlist is empty, and the `resolution` domain is re-measured.
- **Drift:**
- `pipeline` (orchestrator) stands at drift 0.072 against a lock of 0.25, and unresolved 0.402 against 0.43. It gains the uplift module.
- `resolution` stands at 0.180 / 0.440 against locks of 0.40 / 0.45. It loses the uplift module.
- Measure both after the move. If either exceeds its bounds, **stop and report**. Never raise a lock.
- Remember step 4: one stray import moved a domain's drift by 0.2. If a bound breaks, look for an accidental intra-domain edge before anything else.
- **Disk:** the root disk is ~98% full. Keep scratch under the scratchpad (`/tmp`) and delete databases after each measurement.
- **Test-count floor:** raise `.github/test-count-baseline` (currently 3095) if the suite passes 3245.
- **Attribution:** commits end with `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`; the PR body ends with `🤖 Generated with [Claude Code](https://claude.com/claude-code)`.
- **Workspace:** worktree `/home/zaebee/projects/ownima/cgb-step5`, branch `feat/language-packs-step5`.

## Rulings (where this plan departs from the spec's or outline's text)

- **P1 — `pipeline.py` becomes `pipeline/__init__.py`, not `pipeline/ingest.py` plus a re-export.**
- The orchestrator stays at `cgis.pipeline.IngestionPipeline`, so no importer changes and no compatibility re-export is needed (the step-1 rule was "no shims").
- The package gains the uplift module beside it.
- *Cost if wrong:* one more move if `__init__` later grows past one responsibility.
- **P2 — `refresh_dispatch` stays where it is.** The spec says it "moves here from `resolver/`", but it is already `cgis.pipeline.refresh_dispatch`. What stays in `resolver/symbols.py` is the pure `dispatch_edges(methods, inheritance)`. The engine calls it on what it extracted, and it never touches a store.
- **P3 — the language residue in the resolver's text is out of scope.** Ruling C4 of step 4 kept generic `resolve_layout` and the JS-builtins root in the generic classify tail; this step leaves them, along with `_is_dunder` and the language-named project-state keys threaded through `IndexBuilder.build`. Removing them changes behaviour, either for a source no pack reads or for the dunder filter of DISPATCHES. That is not what migration row 5 is for. The PR lists them, and the recommendation is a follow-up issue.

## Review Focus

- **`cgis ingest` with `--domains`.** Semantic uplift still runs after resolution, with the same domain tags and `ontology_class`. The existing `test_pipeline.py::...domains...` tests and `test_uplift.py` cover this, and the identity corpus records `domains` per node.
- **An incremental run.** `refresh_dispatch` and uplift run in the same order as before, after `_persist_incremental` and `record`. This is pinned by Task 1, `test_post_write_passes_run_in_order`.
- **Imports of the old module path.** None remain: Task 1 greps for them, and the test suite imports the new path.
- **The forbidden-edge gate.** `resolver-storage` is empty, and nothing in `cgis.resolver` imports `cgis.storage`, even under `TYPE_CHECKING`. The gate counts `IMPORTS` edges whatever their `type_only` flag says.
- **Self-graph FQNs.** `pipeline` keeps its id (`pipeline/__init__.py` strips `/__init__`); `resolver.uplift.*` becomes `pipeline.uplift.*` in god-object, calibration and receiver fixtures, and Task 1 greps for leftovers.

---

### Task 1: The pipeline package; uplift moves into it

**Files:**
- Move: `src/cgis/pipeline.py` → `src/cgis/pipeline/__init__.py`
- Move: `src/cgis/resolver/uplift.py` → `src/cgis/pipeline/uplift.py`
- Modify:
- `src/cgis/pipeline/__init__.py:22` (the import)
- `tests/unit/test_uplift.py:16`
- `tests/self_parsing/forbidden_edges_allowlist.json` (`resolver-storage` → `{}`)
- any self-graph test naming `resolver.uplift`
- the docstring in `src/cgis/query/context/audit.py:17`, only if it names the module path
- Test: `tests/unit/test_pipeline.py` (append)

**Interfaces:**
- Produces: `cgis.pipeline.uplift.SemanticUpliftEngine` (unchanged API). `cgis.pipeline` keeps `IngestionPipeline`, `refresh_dispatch` and everything else it had.

- [ ] **Step 1: Write the failing gate expectation and the order test.**
1. In `tests/self_parsing/forbidden_edges_allowlist.json`, set `"resolver-storage": {}`. Spec acceptance is the test.
2. Append to `tests/unit/test_pipeline.py`:

```python
def test_post_write_passes_run_in_order(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Dispatch is refreshed over the stored graph, then uplift runs — after the write (#545)."""
(tmp_path / "a.py").write_text("class A:\n def f(self):\n pass\n", encoding="utf-8")
order: list[str] = []
real_refresh = pipeline_module.refresh_dispatch
monkeypatch.setattr(
pipeline_module, "refresh_dispatch", lambda store: (order.append("dispatch"), real_refresh(store))
)
monkeypatch.setattr(
pipeline_module.SemanticUpliftEngine, "execute_uplift", lambda self: order.append("uplift")
)
with SQLiteStore(str(tmp_path / "g.db")) as store:
IngestionPipeline({".py": PythonExtractor()}).run(str(tmp_path), store=store)
assert order == ["dispatch", "uplift"]
```

Add `import cgis.pipeline as pipeline_module` (and `pytest`, `SQLiteStore` and `PythonExtractor` if they are missing) to the file's imports.

- [ ] **Step 2: Run them.**
- Run: `uv run pytest tests/self_parsing/test_architecture.py -q -k allowlist && uv run pytest tests/unit/test_pipeline.py -q -k post_write`
- Expected:
- the allowlist test FAILS, printing `"resolver-storage": {"cgis.resolver.uplift -> cgis.storage.sqlite_store": 8}`;
- the order test PASSES. It is a guard for the move, mutation-checked in Step 5.

- [ ] **Step 3: Move with git, then fix the imports.**

```bash
cd /home/zaebee/projects/ownima/cgb-step5 && mkdir -p src/cgis/pipeline && git mv src/cgis/pipeline.py src/cgis/pipeline/__init__.py && git mv src/cgis/resolver/uplift.py src/cgis/pipeline/uplift.py && sed -i 's/^from cgis\.resolver\.uplift import SemanticUpliftEngine$/from cgis.pipeline.uplift import SemanticUpliftEngine/' src/cgis/pipeline/__init__.py && sed -i 's/^from cgis\.resolver\.uplift import/from cgis.pipeline.uplift import/' tests/unit/test_uplift.py && git grep -n "resolver\.uplift\|resolver/uplift" -- src tests scripts docs/ontology
```

- Expected: the final grep lists only prose (docstrings and comments). Re-point those that describe where the code lives now; leave historical mentions alone.
- `pipeline/__init__.py` importing its own submodule `cgis.pipeline.uplift` is fine. Check with `uv run python -c "import cgis.pipeline, cgis.pipeline.uplift"`.
- If the self-graph tests name `resolver.uplift.*` FQNs (`git grep -n "resolver.uplift" -- tests/self_parsing`), re-point them to `pipeline.uplift.*`.

- [ ] **Step 4: Run the tests.**
- Run: `uv run pytest tests/self_parsing tests/unit/test_pipeline.py tests/unit/test_uplift.py tests/unit/test_graph_identity.py -q`
- Expected: all pass, including the allowlist with `resolver-storage` empty.

- [ ] **Step 5: Mutation-check the order test.**
1. Swap the two lines in `IngestionPipeline.run` (`refresh_dispatch(store)` and the `SemanticUpliftEngine(...)` call) for one run.
2. `test_post_write_passes_run_in_order` must fail.
3. Restore the file and confirm `git diff --stat` shows only the intended files.

- [ ] **Step 6: Measure the two domains.**

```bash
cd /home/zaebee/projects/ownima/cgb-step5 && S=/tmp/claude-1001/-home-zaebee-projects-ownima-codegraph-brain/97c475b5-6bc7-4231-a643-ff81f3f651ec/scratchpad/step5 && mkdir -p $S && uv run cgis ingest src --output $S/self.db > /dev/null 2>&1 && uv run cgis drift --db $S/self.db --profile python --format json > $S/drift.json 2>/dev/null
python3 -c "import json; d=json.load(open('$S/drift.json')); rs=d if isinstance(d,list) else d.get('reports') or d.get('domains'); [print(r['domain'], round(r['drift_score'],4), round(r['actual']['unresolved_ratio'],4), r['actual']['node_count'], r['status']) for r in rs if r['domain'] in ('pipeline','resolution')]"
/bin/rm -f $S/self.db
```

- Expected:
- `pipeline`: drift ≤ 0.25 and unresolved ≤ 0.43;
- `resolution`: drift ≤ 0.40 and unresolved ≤ 0.45.
- If any value is over its bound, stop. First list the intra-domain import edges on both sides:

```
sqlite3 <db> "select source, target from edges where type='IMPORTS' and source like 'cgis.pipeline%' and target like 'cgis.pipeline%'"
```

A cross-import triangle was the cause in step 4. Then report.

- [ ] **Step 7: Inventory.**
- Regenerate `tests/unit/review_path_inventory.txt` as in step 2's plan (Task 2, Step 3).
- Expected: `gone` holds at most `src/cgis/resolver/uplift.py` or `src/cgis/pipeline.py` if they were in the closure, and `new` holds their new paths. Anything else gone means the review walk lost a module: stop.

- [ ] **Step 8: Gate and commit.**
- Message: `refactor(pipeline): pipeline.py becomes a package; semantic uplift moves into it`.
- The body records:
- the measured drift for both domains;
- that `resolver-storage` is now empty;
- ruling P1 (no importer changes).

---

### Task 2: Documentation

**Files:**
- `CLAUDE.md` (Architecture block and the pipeline line)
- `docs/specs/2026-10-14-language-packs-design.md` (implementation note under "Build plane")

- [ ] **Step 1: CLAUDE.md.**
- The Architecture block reads `IngestionPipeline (pipeline.py)`. Change it to `IngestionPipeline (pipeline/__init__.py)`.
- Add a line under the store: `├── post-write passes (pipeline/) ← refresh_dispatch, SemanticUpliftEngine (pipeline/uplift.py)`. Match the block's existing indentation and arrow style.
- Correct any other line that names `resolver/uplift.py`.

- [ ] **Step 2: Spec note.** Under "Build plane", add `> **Implementation note (step 5):** …`. It covers:
- P1: the package keeps the orchestrator in `__init__.py`, so imports are unchanged;
- P2: `refresh_dispatch` was already in the pipeline, and `dispatch_edges` stays pure in the resolver;
- the empty `resolver-storage` allowlist;
- the measured drift;
- P3: the language residue left in the resolver's text, listed as follow-up work for a measured change.

- [ ] **Step 3: Gate and commit.**
- Message: `docs: step 5 implementation notes`.

---

### Task 3: Identity report, review, PR

- [ ] **Step 1: Identity.**
- Run the same commands as step 4's plan, Task 8 Step 1, using `step5` paths and `BR=/home/zaebee/projects/ownima/cgb-step5`.
- Expected: `identical` three times.
- Delete the databases.
- [ ] **Step 2: Independent review.**
- Dispatch a reviewer with execution tools on the most capable model.
- Give it: the branch diff, this plan, the spec, the ledger, the Review Focus and the disk warning.
- Ask it to confirm, with a `--domains` ingest on old and new, that uplift output (`domains`, `ontology_class`, domain-dependency edges) is identical.
- Fix Critical/Important findings test-first.
- [ ] **Step 3: PR.**
- Push, then open `refactor(pipeline): step 5 — build-plane passes`.
- The body carries:
- the module map;
- rulings P1–P3;
- the identity results;
- the drift numbers;
- the emptied `resolver-storage` allowlist;
- the residue list with the follow-up recommendation.
- Check the Gemini and Guardian comments and Sonar, and reply in every thread.
14 changes: 14 additions & 0 deletions docs/specs/2026-10-14-language-packs-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,20 @@ Two planes over a shared core:
the result, then runs the post-write passes: semantic uplift and
`refresh_dispatch`. Both move here from `resolver/` (step 5). They read and
write the store, which is what the resolver must not do.

> **Implementation note (step 5):** `pipeline.py` became `pipeline/__init__.py`,
> so `cgis.pipeline.IngestionPipeline` and every importer are unchanged (no
> re-export); `resolver/uplift.py` became `pipeline/uplift.py`, and with it the
> `resolver-storage` allowlist is empty. `refresh_dispatch` was already the
> pipeline's; its pure half, `dispatch_edges(methods, inheritance)`, stays in the
> resolver — it never touches a store. Measured: `pipeline` drift 0.106,
> `resolution` 0.179; `resolution`'s hygiene baseline went 0.45 → 0.46 because
> uplift left with a lower unresolved share (0.40) than the domain's (0.44) — no
> unresolved edge was added. Left for a measured follow-up, since removing them
> changes behaviour: the Python `__init__` root rule in `strip_layout_prefixes`,
> the JS-builtins root in `classify_fqn`'s generic tail, `_is_dunder` in
> DISPATCHES, and the language-named project state threaded through
> `IndexBuilder.build`.
- **Read plane.** `storage → query → surfaces`.
- **Core.** Model types and FQN utilities. It imports nothing else from cgis.

Expand Down
2 changes: 1 addition & 1 deletion src/cgis/pipeline.py → src/cgis/pipeline/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
from cgis.core.paths import EXCLUDED_DIRS
from cgis.import_names import ImportNameCollector
from cgis.languages import cross_file_metadata, skips
from cgis.pipeline.uplift import SemanticUpliftEngine
from cgis.resolver.engine import ResolverEngine
from cgis.resolver.symbols import dispatch_edges
from cgis.resolver.uplift import SemanticUpliftEngine

if TYPE_CHECKING:
from cgis.storage.sqlite_store import SQLiteStore
Expand Down Expand Up @@ -128,7 +128,7 @@
# `cgis ingest /abs/path/src` produce identical file_paths and FQNs.
return path.resolve()

def run(

Check failure on line 131 in src/cgis/pipeline/__init__.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 18 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=zaebee_codegraph-brain&issues=AaE785MUnWd_92VqQWC9&open=AaE785MUnWd_92VqQWC9&pullRequest=602
self,
repo_path: str,
store: "SQLiteStore | None" = None,
Expand Down
File renamed without changes.
4 changes: 1 addition & 3 deletions tests/self_parsing/forbidden_edges_allowlist.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
"cross-pack": {},
"engine-packs": {},
"pack-isolation": {},
"resolver-storage": {
"cgis.resolver.uplift -> cgis.storage.sqlite_store": 8
},
"resolver-storage": {},
"surface-packs": {},
"surface-storage": {
"cgis.api.auto_refresh -> cgis.storage.sqlite_store": 7,
Expand Down
Loading
Loading