From bc9f1e44eff198246e515ae1f341e373c294ebc8 Mon Sep 17 00:00:00 2001 From: zaebee Date: Wed, 14 Oct 2026 08:40:48 +0000 Subject: [PATCH 1/8] =?UTF-8?q?feat(bench):=20canonical=20dump=20and=20dif?= =?UTF-8?q?f=20of=20a=20stored=20graph=20=E2=80=94=20the=20language-packs?= =?UTF-8?q?=20identity=20net?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scripts/graph_identity.py dumps nodes, edges and ingest_state with the corpus root normalised to and the mtime-derived ingested_at dropped, and diffs two dumps row by row. Step 0, task 1 of docs/plans/2026-10-14-language-packs.md. Co-Authored-By: Claude Opus 5.5 --- scripts/graph_identity.py | 110 +++++++++++++++++++++++++ tests/unit/test_graph_identity_dump.py | 73 ++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 scripts/graph_identity.py create mode 100644 tests/unit/test_graph_identity_dump.py diff --git a/scripts/graph_identity.py b/scripts/graph_identity.py new file mode 100644 index 00000000..581c4337 --- /dev/null +++ b/scripts/graph_identity.py @@ -0,0 +1,110 @@ +"""Canonical dump and diff of a stored cgis graph — the language-packs identity net. + +Steps 1-5 of docs/specs/2026-10-14-language-packs-design.md move code without +changing behaviour. This is how that is checked: the same input ingested by two +cgis versions must dump identically. Absolute paths become `` and the +mtime-derived `ingested_at` is dropped, so a dump compares across machines and +temp directories. +""" + +import argparse +import json +import sqlite3 +import sys +from collections.abc import Mapping +from pathlib import Path +from typing import cast + +_NODE_COLUMNS = ( + "id", "type", "name", "file_path", "start_line", "end_line", "language", + "namespace", "is_test", "is_generated", "metadata", +) # fmt: skip +_EDGE_COLUMNS = ( + "id", "source", "target", "type", "weight", "confidence", "context", + "file_path", "line_number", "type_only", +) # fmt: skip +_VOLATILE_STATE = frozenset({"ingested_at"}) + + +def _normalise(value: object, root: str) -> object: + """`value` with the corpus root replaced by ``, JSON text parsed to data.""" + if isinstance(value, str): + text = value.replace(root, "") + if text[:1] in "[{": + try: + return _normalise(json.loads(text), "\0") + except ValueError: + return text + return text + if isinstance(value, list): + return [_normalise(v, root) for v in value] + if isinstance(value, dict): + return {k: _normalise(v, root) for k, v in sorted(value.items())} + return value + + +def _rows( + conn: sqlite3.Connection, table: str, columns: tuple[str, ...], root: str +) -> list[dict[str, object]]: + """Every row of `table` as a normalised dict, sorted by id.""" + query = f"SELECT {', '.join(columns)} FROM {table} ORDER BY id" # table/columns are constants + return [ + {col: _normalise(val, root) for col, val in zip(columns, row, strict=True)} + for row in conn.execute(query) + ] + + +def dump(db_path: Path, root: Path) -> dict[str, object]: + """The canonical content of one stored graph, with `root` normalised away.""" + prefix = str(root.resolve()) + with sqlite3.connect(db_path) as conn: + state = { + key: _normalise(value, prefix) + for key, value in conn.execute("SELECT key, value FROM ingest_state ORDER BY key") + if key not in _VOLATILE_STATE + } + return { + "nodes": _rows(conn, "nodes", _NODE_COLUMNS, prefix), + "edges": _rows(conn, "edges", _EDGE_COLUMNS, prefix), + "ingest_state": state, + } + + +Row = dict[str, object] + + +def _by_id(dumped: Mapping[str, object], table: str) -> dict[object, Row]: + """One table of a dump, keyed by row id.""" + return {row["id"]: row for row in cast("list[Row]", dumped[table])} + + +def diff(before: Mapping[str, object], after: Mapping[str, object]) -> list[str]: + """One line per row that differs between two dumps, keyed by table and id.""" + lines: list[str] = [] + for table in ("nodes", "edges"): + old, new = _by_id(before, table), _by_id(after, table) + lines.extend( + f"{table} {key}: {json.dumps(old.get(key))} → {json.dumps(new.get(key))}" + for key in sorted(old.keys() | new.keys(), key=str) + if old.get(key) != new.get(key) + ) + if before["ingest_state"] != after["ingest_state"]: + lines.append(f"ingest_state: {before['ingest_state']} → {after['ingest_state']}") + return lines + + +def main(argv: list[str] | None = None) -> int: + """`diff `: exit 1 and print when the graphs differ.""" + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + sub = parser.add_subparsers(dest="command", required=True) + d = sub.add_parser("diff") + for name in ("db_a", "root_a", "db_b", "root_b"): + d.add_argument(name, type=Path) + args = parser.parse_args(argv) + lines = diff(dump(args.db_a, args.root_a), dump(args.db_b, args.root_b)) + print("\n".join(lines) if lines else "identical") + return 1 if lines else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/unit/test_graph_identity_dump.py b/tests/unit/test_graph_identity_dump.py new file mode 100644 index 00000000..950b0feb --- /dev/null +++ b/tests/unit/test_graph_identity_dump.py @@ -0,0 +1,73 @@ +"""The canonical graph dump the identity net compares (spec: Migration, safety net).""" + +import json +import sqlite3 +import sys +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent.parent +sys.path.insert(0, str(REPO / "scripts")) + +import graph_identity as gi # noqa: E402 + +from cgis.extractors.registry import build_extractors # noqa: E402 +from cgis.pipeline import IngestionPipeline # noqa: E402 +from cgis.storage.sqlite_store import SQLiteStore # noqa: E402 + + +def _ingest(root: Path, db: Path) -> None: + """As `cgis ingest` does it, ingest state included.""" + with SQLiteStore(str(db)) as store: + pipeline = IngestionPipeline(build_extractors([])) + pipeline.run(str(root), store=store) + store.record_ingest(str(root), pipeline.observed_mtimes) + store.record_ingest_options([], None) + + +def _repo(root: Path) -> Path: + (root / "pkg").mkdir(parents=True) + (root / "pkg" / "a.py").write_text("def f():\n g()\n\ndef g():\n pass\n") + return root + + +def test_dump_carries_nodes_edges_and_ingest_state(tmp_path: Path) -> None: + root = _repo(tmp_path / "r") + _ingest(root, tmp_path / "g.db") + out = gi.dump(tmp_path / "g.db", root) + + assert {n["id"] for n in out["nodes"]} >= {"pkg.a", "pkg.a.f", "pkg.a.g"} + call = next(e for e in out["edges"] if e["type"] == "CALLS") + assert (call["source"], call["target"]) == ("pkg.a.f", "pkg.a.g") + assert "ingested_at" not in out["ingest_state"] + assert set(out["nodes"][0]) >= {"metadata", "language", "is_test", "is_generated"} + + +def test_paths_are_normalised(tmp_path: Path) -> None: + root = _repo(tmp_path / "r") + _ingest(root, tmp_path / "g.db") + text = json.dumps(gi.dump(tmp_path / "g.db", root)) + + assert str(root) not in text + assert "" in text + + +def test_the_same_ingest_twice_dumps_identically(tmp_path: Path) -> None: + a, b = _repo(tmp_path / "a"), _repo(tmp_path / "b") + _ingest(a, tmp_path / "a.db") + _ingest(b, tmp_path / "b.db") + + assert gi.diff(gi.dump(tmp_path / "a.db", a), gi.dump(tmp_path / "b.db", b)) == [] + + +def test_diff_names_a_changed_edge(tmp_path: Path) -> None: + root = _repo(tmp_path / "r") + _ingest(root, tmp_path / "g.db") + before = gi.dump(tmp_path / "g.db", root) + with sqlite3.connect(tmp_path / "g.db") as conn: + conn.execute("UPDATE edges SET confidence = 0.5 WHERE type = 'CALLS'") + after = gi.dump(tmp_path / "g.db", root) + + lines = gi.diff(before, after) + assert lines, "a changed confidence must show" + assert all(line.startswith("edges ") for line in lines), "only edge rows changed" + assert any("pkg.a.f" in line and "0.5" in line for line in lines) From d8966144ddd9ba400e707d94e101b9aaa78e47b1 Mon Sep 17 00:00:00 2001 From: zaebee Date: Wed, 14 Oct 2026 08:42:10 +0000 Subject: [PATCH 2/8] =?UTF-8?q?test(fixtures):=20the=20language-packs=20id?= =?UTF-8?q?entity=20corpus=20=E2=80=94=20Python=20DI/ABC/star/layouts,=20T?= =?UTF-8?q?S=20monorepo/pnpm/tsconfig/node:?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Checked against today's cgis before committing: - whole corpus: DI alias (OrderService.place → Repo.save), DISPATCHES Repo.save → SqlRepo.save, node:crypto STDLIB, react and lodash.debounce EXTERNAL, shadowed setTimeout INTERNAL; - ts/apps/web/src (configs above the root): @x.lib UNKNOWN; - py/src (src layout): ids without a layout prefix, and the local alembic package INTERNAL beside the declared alembic dependency. pyproject: tests/fixtures is excluded from ruff (a reformat would change what the snapshot measures) and from pytest collection (the corpus holds a test file). Co-Authored-By: Claude Opus 5.5 --- pyproject.toml | 5 +++++ .../graph_identity/corpus/py/pyproject.toml | 3 +++ .../graph_identity/corpus/py/src/alembic/env.py | 2 ++ .../corpus/py/src/shop/__init__.py | 1 + .../graph_identity/corpus/py/src/shop/deps.py | 12 ++++++++++++ .../graph_identity/corpus/py/src/shop/repo.py | 11 +++++++++++ .../graph_identity/corpus/py/src/shop/service.py | 16 ++++++++++++++++ .../corpus/py/tests/test_service.py | 5 +++++ .../corpus/ts/apps/web/package.json | 1 + .../corpus/ts/apps/web/src/main.ts | 16 ++++++++++++++++ .../corpus/ts/apps/web/src/types.d.ts | 3 +++ .../corpus/ts/apps/web/src/util.ts | 3 +++ .../graph_identity/corpus/ts/package.json | 1 + .../graph_identity/corpus/ts/pnpm-workspace.yaml | 3 +++ .../graph_identity/corpus/ts/tsconfig.json | 1 + 15 files changed, 83 insertions(+) create mode 100644 tests/fixtures/graph_identity/corpus/py/pyproject.toml create mode 100644 tests/fixtures/graph_identity/corpus/py/src/alembic/env.py create mode 100644 tests/fixtures/graph_identity/corpus/py/src/shop/__init__.py create mode 100644 tests/fixtures/graph_identity/corpus/py/src/shop/deps.py create mode 100644 tests/fixtures/graph_identity/corpus/py/src/shop/repo.py create mode 100644 tests/fixtures/graph_identity/corpus/py/src/shop/service.py create mode 100644 tests/fixtures/graph_identity/corpus/py/tests/test_service.py create mode 100644 tests/fixtures/graph_identity/corpus/ts/apps/web/package.json create mode 100644 tests/fixtures/graph_identity/corpus/ts/apps/web/src/main.ts create mode 100644 tests/fixtures/graph_identity/corpus/ts/apps/web/src/types.d.ts create mode 100644 tests/fixtures/graph_identity/corpus/ts/apps/web/src/util.ts create mode 100644 tests/fixtures/graph_identity/corpus/ts/package.json create mode 100644 tests/fixtures/graph_identity/corpus/ts/pnpm-workspace.yaml create mode 100644 tests/fixtures/graph_identity/corpus/ts/tsconfig.json diff --git a/pyproject.toml b/pyproject.toml index 68b17ef1..7dfe056e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,6 +101,9 @@ codegraph-brain = "cgis.api.mcp_server:mcp.run" # Target Python 3.12+ target-version = "py312" line-length = 100 +# Fixture corpora are inputs, not code: reformatting one changes what the +# graph-identity snapshot measures (docs/plans/2026-10-14-language-packs.md). +extend-exclude = ["tests/fixtures"] [tool.ruff.lint] # Enable many plugins: @@ -237,6 +240,8 @@ color = true [tool.pytest.ini_options] testpaths = ["tests"] +# pytest's defaults plus `fixtures`: the identity corpus holds a test file of its own. +norecursedirs = [".*", "*.egg", "_darcs", "build", "CVS", "dist", "node_modules", "venv", "{arch}", "fixtures"] asyncio_mode = "auto" markers = [ "slow: marks tests as slow — run with '-m slow' or skip with '-m \"not slow\"'", diff --git a/tests/fixtures/graph_identity/corpus/py/pyproject.toml b/tests/fixtures/graph_identity/corpus/py/pyproject.toml new file mode 100644 index 00000000..3e26e0a2 --- /dev/null +++ b/tests/fixtures/graph_identity/corpus/py/pyproject.toml @@ -0,0 +1,3 @@ +[project] +name = "shop" +dependencies = ["fastapi>=0.110", "alembic>=1.13"] diff --git a/tests/fixtures/graph_identity/corpus/py/src/alembic/env.py b/tests/fixtures/graph_identity/corpus/py/src/alembic/env.py new file mode 100644 index 00000000..ce92c5af --- /dev/null +++ b/tests/fixtures/graph_identity/corpus/py/src/alembic/env.py @@ -0,0 +1,2 @@ +def run_migrations() -> None: + pass diff --git a/tests/fixtures/graph_identity/corpus/py/src/shop/__init__.py b/tests/fixtures/graph_identity/corpus/py/src/shop/__init__.py new file mode 100644 index 00000000..42c8321c --- /dev/null +++ b/tests/fixtures/graph_identity/corpus/py/src/shop/__init__.py @@ -0,0 +1 @@ +from .service import * # noqa: F403 — star-import chain under test diff --git a/tests/fixtures/graph_identity/corpus/py/src/shop/deps.py b/tests/fixtures/graph_identity/corpus/py/src/shop/deps.py new file mode 100644 index 00000000..546d8b9f --- /dev/null +++ b/tests/fixtures/graph_identity/corpus/py/src/shop/deps.py @@ -0,0 +1,12 @@ +from typing import Annotated + +from fastapi import Depends + +from .repo import Repo, SqlRepo + + +def get_repo() -> Repo: + return SqlRepo() + + +RepoDep = Annotated[Repo, Depends(get_repo)] diff --git a/tests/fixtures/graph_identity/corpus/py/src/shop/repo.py b/tests/fixtures/graph_identity/corpus/py/src/shop/repo.py new file mode 100644 index 00000000..f08c0a39 --- /dev/null +++ b/tests/fixtures/graph_identity/corpus/py/src/shop/repo.py @@ -0,0 +1,11 @@ +from abc import ABC, abstractmethod + + +class Repo(ABC): + @abstractmethod + def save(self, item: str) -> None: ... + + +class SqlRepo(Repo): + def save(self, item: str) -> None: + print(item) diff --git a/tests/fixtures/graph_identity/corpus/py/src/shop/service.py b/tests/fixtures/graph_identity/corpus/py/src/shop/service.py new file mode 100644 index 00000000..40394918 --- /dev/null +++ b/tests/fixtures/graph_identity/corpus/py/src/shop/service.py @@ -0,0 +1,16 @@ +from .deps import RepoDep +from .repo import Repo + +__all__ = ["OrderService", "place"] + + +class OrderService: + def __init__(self, repo: RepoDep) -> None: + self.repo = repo + + def place(self, item: str) -> None: + self.repo.save(item) + + +def place(repo: Repo, item: str) -> None: + repo.save(item) diff --git a/tests/fixtures/graph_identity/corpus/py/tests/test_service.py b/tests/fixtures/graph_identity/corpus/py/tests/test_service.py new file mode 100644 index 00000000..20e9109a --- /dev/null +++ b/tests/fixtures/graph_identity/corpus/py/tests/test_service.py @@ -0,0 +1,5 @@ +from shop.service import place + + +def test_place() -> None: + place(None, "x") diff --git a/tests/fixtures/graph_identity/corpus/ts/apps/web/package.json b/tests/fixtures/graph_identity/corpus/ts/apps/web/package.json new file mode 100644 index 00000000..46081e15 --- /dev/null +++ b/tests/fixtures/graph_identity/corpus/ts/apps/web/package.json @@ -0,0 +1 @@ +{"name": "web", "dependencies": {"@x/lib": "^1.0.0", "react": "^19.0.0", "lodash.debounce": "^4.0.8"}} diff --git a/tests/fixtures/graph_identity/corpus/ts/apps/web/src/main.ts b/tests/fixtures/graph_identity/corpus/ts/apps/web/src/main.ts new file mode 100644 index 00000000..18c50eef --- /dev/null +++ b/tests/fixtures/graph_identity/corpus/ts/apps/web/src/main.ts @@ -0,0 +1,16 @@ +import { randomUUID } from "node:crypto"; +import debounce from "lodash.debounce"; +import { useState } from "react"; +import { libFn } from "@x/lib"; +import { helper } from "@app/util"; + +function setTimeout(fn: () => void): void { + fn(); +} + +export function main(): number { + useState(0); + randomUUID(); + setTimeout(() => debounce(() => helper(libFn()), 10)); + return libFn(); +} diff --git a/tests/fixtures/graph_identity/corpus/ts/apps/web/src/types.d.ts b/tests/fixtures/graph_identity/corpus/ts/apps/web/src/types.d.ts new file mode 100644 index 00000000..43ce8579 --- /dev/null +++ b/tests/fixtures/graph_identity/corpus/ts/apps/web/src/types.d.ts @@ -0,0 +1,3 @@ +export interface Shape { + area(): number; +} diff --git a/tests/fixtures/graph_identity/corpus/ts/apps/web/src/util.ts b/tests/fixtures/graph_identity/corpus/ts/apps/web/src/util.ts new file mode 100644 index 00000000..13859f52 --- /dev/null +++ b/tests/fixtures/graph_identity/corpus/ts/apps/web/src/util.ts @@ -0,0 +1,3 @@ +export function helper(n: number): number { + return n + 1; +} diff --git a/tests/fixtures/graph_identity/corpus/ts/package.json b/tests/fixtures/graph_identity/corpus/ts/package.json new file mode 100644 index 00000000..86cabc0d --- /dev/null +++ b/tests/fixtures/graph_identity/corpus/ts/package.json @@ -0,0 +1 @@ +{"name": "mono", "private": true, "workspaces": ["packages/*"], "dependencies": {"react": "^19.0.0"}} diff --git a/tests/fixtures/graph_identity/corpus/ts/pnpm-workspace.yaml b/tests/fixtures/graph_identity/corpus/ts/pnpm-workspace.yaml new file mode 100644 index 00000000..997c74d5 --- /dev/null +++ b/tests/fixtures/graph_identity/corpus/ts/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - "apps/*" + - "!**/fixtures/**" diff --git a/tests/fixtures/graph_identity/corpus/ts/tsconfig.json b/tests/fixtures/graph_identity/corpus/ts/tsconfig.json new file mode 100644 index 00000000..b1a065eb --- /dev/null +++ b/tests/fixtures/graph_identity/corpus/ts/tsconfig.json @@ -0,0 +1 @@ +{"compilerOptions": {"baseUrl": ".", "paths": {"@app/*": ["apps/web/src/*"]}}} From ecab024baaf6276bb498c4ce7eaf766148f9e406 Mon Sep 17 00:00:00 2001 From: zaebee Date: Wed, 14 Oct 2026 08:43:38 +0000 Subject: [PATCH 3/8] test: the Python/TS identity snapshot, compared under two hash seeds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `scripts/graph_identity.py snapshot ` ingests three roots of a temp copy that has a `.git` marker — whole, ts/apps/web/src, py/src — the way `cgis ingest` does, and writes their canonical dumps. The test takes it in a subprocess under PYTHONHASHSEED 0 and 1 and diffs each root against the checked-in snapshot.json. Read from --out, not stdout: the pipeline logs to stdout. Renaming one function in the corpus fails both runs, naming the node and the edge that moved. Co-Authored-By: Claude Opus 5.5 --- scripts/graph_identity.py | 51 +- tests/fixtures/graph_identity/snapshot.json | 2671 +++++++++++++++++++ tests/unit/test_graph_identity.py | 52 + 3 files changed, 2773 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/graph_identity/snapshot.json create mode 100644 tests/unit/test_graph_identity.py diff --git a/scripts/graph_identity.py b/scripts/graph_identity.py index 581c4337..d7960942 100644 --- a/scripts/graph_identity.py +++ b/scripts/graph_identity.py @@ -9,12 +9,18 @@ import argparse import json +import shutil import sqlite3 import sys +import tempfile from collections.abc import Mapping from pathlib import Path from typing import cast +from cgis.extractors.registry import build_extractors +from cgis.pipeline import IngestionPipeline +from cgis.storage.sqlite_store import SQLiteStore + _NODE_COLUMNS = ( "id", "type", "name", "file_path", "start_line", "end_line", "language", "namespace", "is_test", "is_generated", "metadata", @@ -25,6 +31,11 @@ ) # fmt: skip _VOLATILE_STATE = frozenset({"ingested_at"}) +#: The corpus roots a snapshot ingests, by name. `sub` reads configs above its +#: root (#555); `py_src` is the src layout, where the local `alembic` package +#: meets the declared `alembic` dependency. +SNAPSHOT_ROOTS: dict[str, str] = {"whole": ".", "sub": "ts/apps/web/src", "py_src": "py/src"} + def _normalise(value: object, root: str) -> object: """`value` with the corpus root replaced by ``, JSON text parsed to data.""" @@ -93,14 +104,52 @@ def diff(before: Mapping[str, object], after: Mapping[str, object]) -> list[str] return lines +def _ingest(root: Path, db: Path) -> None: + """One full ingest of `root` into `db`, as `cgis ingest` does it, ingest state included.""" + with SQLiteStore(str(db)) as store: + pipeline = IngestionPipeline(build_extractors([])) + pipeline.run(str(root), store=store) + store.record_ingest(str(root), pipeline.observed_mtimes) + store.record_ingest_options([], None) + + +def snapshot(corpus: Path) -> dict[str, object]: + """Every snapshot root, ingested from a temp copy of `corpus` with a `.git` marker. + + The copy keeps the checked-in corpus free of `.git` (which git cannot hold) + and makes the repository boundary of the upward config search (#555) the + corpus itself, wherever the tests run. + """ + with tempfile.TemporaryDirectory() as tmp: + copy = Path(tmp) / "corpus" + shutil.copytree(corpus, copy) + (copy / ".git").mkdir() + dumps: dict[str, object] = {} + for name, relative in SNAPSHOT_ROOTS.items(): + db = Path(tmp) / f"{name}.db" + _ingest(copy / relative, db) + dumps[name] = dump(db, copy) + return dumps + + def main(argv: list[str] | None = None) -> int: - """`diff `: exit 1 and print when the graphs differ.""" + """`diff` two stored graphs (exit 1 when they differ), or `snapshot` the corpus.""" parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) sub = parser.add_subparsers(dest="command", required=True) d = sub.add_parser("diff") for name in ("db_a", "root_a", "db_b", "root_b"): d.add_argument(name, type=Path) + s = sub.add_parser("snapshot") + s.add_argument("corpus", type=Path) + s.add_argument("--out", type=Path) args = parser.parse_args(argv) + if args.command == "snapshot": + text = json.dumps(snapshot(args.corpus), indent=1, sort_keys=True, ensure_ascii=False) + if args.out: + args.out.write_text(text + "\n", encoding="utf-8") + else: + print(text) + return 0 lines = diff(dump(args.db_a, args.root_a), dump(args.db_b, args.root_b)) print("\n".join(lines) if lines else "identical") return 1 if lines else 0 diff --git a/tests/fixtures/graph_identity/snapshot.json b/tests/fixtures/graph_identity/snapshot.json new file mode 100644 index 00000000..9c879a76 --- /dev/null +++ b/tests/fixtures/graph_identity/snapshot.json @@ -0,0 +1,2671 @@ +{ + "py_src": { + "edges": [ + { + "confidence": 1.0, + "context": null, + "file_path": "alembic/env.py", + "id": "alembic.env:structural:alembic.env.run_migrations", + "line_number": null, + "source": "alembic.env", + "target": "alembic.env.run_migrations", + "type": "CONTAINS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 0.5, + "context": "override of shop.repo.Repo.save", + "file_path": "shop/repo.py", + "id": "dispatch:shop.repo.Repo.save->shop.repo.SqlRepo.save", + "line_number": 10, + "source": "shop.repo.Repo.save", + "target": "shop.repo.SqlRepo.save", + "type": "DISPATCHES", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "shop/deps.py", + "id": "shop.deps:imports:fastapi", + "line_number": null, + "source": "shop.deps", + "target": "fastapi", + "type": "IMPORTS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "shop/deps.py", + "id": "shop.deps:imports:shop.repo", + "line_number": null, + "source": "shop.deps", + "target": "shop.repo", + "type": "IMPORTS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "shop/deps.py", + "id": "shop.deps:imports:typing", + "line_number": null, + "source": "shop.deps", + "target": "typing", + "type": "IMPORTS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "shop/deps.py", + "id": "shop.deps:imports_symbol:shop.repo.Repo", + "line_number": null, + "source": "shop.deps", + "target": "shop.repo.Repo", + "type": "IMPORTS_SYMBOL", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "shop/deps.py", + "id": "shop.deps:imports_symbol:shop.repo.SqlRepo", + "line_number": null, + "source": "shop.deps", + "target": "shop.repo.SqlRepo", + "type": "IMPORTS_SYMBOL", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "shop/deps.py", + "id": "shop.deps:structural:shop.deps.get_repo", + "line_number": null, + "source": "shop.deps", + "target": "shop.deps.get_repo", + "type": "CONTAINS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "shop/repo.py", + "id": "shop.repo.Repo.save:decorator:0:abstractmethod", + "line_number": null, + "source": "shop.repo.Repo.save", + "target": "abc.abstractmethod", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "shop/repo.py", + "id": "shop.repo.Repo:extends:ABC", + "line_number": null, + "source": "shop.repo.Repo", + "target": "abc.ABC", + "type": "EXTENDS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "shop/repo.py", + "id": "shop.repo.Repo:structural:shop.repo.Repo.save", + "line_number": null, + "source": "shop.repo.Repo", + "target": "shop.repo.Repo.save", + "type": "DECLARES", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "shop/repo.py", + "id": "shop.repo.SqlRepo:extends:Repo", + "line_number": null, + "source": "shop.repo.SqlRepo", + "target": "shop.repo.Repo", + "type": "EXTENDS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "shop/repo.py", + "id": "shop.repo.SqlRepo:structural:shop.repo.SqlRepo.save", + "line_number": null, + "source": "shop.repo.SqlRepo", + "target": "shop.repo.SqlRepo.save", + "type": "DECLARES", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "shop/repo.py", + "id": "shop.repo:imports:abc", + "line_number": null, + "source": "shop.repo", + "target": "abc", + "type": "IMPORTS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "shop/repo.py", + "id": "shop.repo:structural:shop.repo.Repo", + "line_number": null, + "source": "shop.repo", + "target": "shop.repo.Repo", + "type": "CONTAINS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "shop/repo.py", + "id": "shop.repo:structural:shop.repo.SqlRepo", + "line_number": null, + "source": "shop.repo", + "target": "shop.repo.SqlRepo", + "type": "CONTAINS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "shop/service.py", + "id": "shop.service.OrderService:structural:shop.service.OrderService.__init__", + "line_number": null, + "source": "shop.service.OrderService", + "target": "shop.service.OrderService.__init__", + "type": "DECLARES", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "shop/service.py", + "id": "shop.service.OrderService:structural:shop.service.OrderService.place", + "line_number": null, + "source": "shop.service.OrderService", + "target": "shop.service.OrderService.place", + "type": "DECLARES", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "shop/service.py", + "id": "shop.service:imports:shop.deps", + "line_number": null, + "source": "shop.service", + "target": "shop.deps", + "type": "IMPORTS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "shop/service.py", + "id": "shop.service:imports:shop.repo", + "line_number": null, + "source": "shop.service", + "target": "shop.repo", + "type": "IMPORTS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "shop/service.py", + "id": "shop.service:imports_symbol:shop.deps.RepoDep", + "line_number": null, + "source": "shop.service", + "target": "shop.deps.RepoDep", + "type": "IMPORTS_SYMBOL", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "shop/service.py", + "id": "shop.service:imports_symbol:shop.repo.Repo", + "line_number": null, + "source": "shop.service", + "target": "shop.repo.Repo", + "type": "IMPORTS_SYMBOL", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "shop/service.py", + "id": "shop.service:structural:shop.service.OrderService", + "line_number": null, + "source": "shop.service", + "target": "shop.service.OrderService", + "type": "CONTAINS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "shop/service.py", + "id": "shop.service:structural:shop.service.place", + "line_number": null, + "source": "shop.service", + "target": "shop.service.place", + "type": "CONTAINS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": "DI alias for get_repo", + "file_path": "shop/deps.py", + "id": "shop/deps.py:dep_166_183", + "line_number": 12, + "source": "shop.deps.RepoDep", + "target": "shop.deps.get_repo", + "type": "DEPENDS_ON", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": "Call to SqlRepo", + "file_path": "shop/deps.py", + "id": "shop/deps.py:edge_128_137", + "line_number": 9, + "source": "shop.deps.get_repo", + "target": "shop.repo.SqlRepo", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": "Call to Depends", + "file_path": "shop/deps.py", + "id": "shop/deps.py:edge_166_183", + "line_number": 12, + "source": "shop.deps", + "target": "fastapi.Depends", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": "Name reference candidate Repo", + "file_path": "shop/deps.py", + "id": "shop/deps.py:nameref_shop.deps|Repo", + "line_number": 12, + "source": "shop.deps", + "target": "shop.repo.Repo", + "type": "REFERENCES", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": "Annotation candidate Repo", + "file_path": "shop/deps.py", + "id": "shop/deps.py:rawdep_111_115_Repo", + "line_number": 8, + "source": "shop.deps.get_repo", + "target": "shop.repo.Repo", + "type": "REFERENCES", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 0.8, + "context": "Call to print", + "file_path": "shop/repo.py", + "id": "shop/repo.py:edge_188_199", + "line_number": 11, + "source": "shop.repo.SqlRepo.save", + "target": "print", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": "Name reference candidate Repo", + "file_path": "shop/repo.py", + "id": "shop/repo.py:nameref_shop.repo.SqlRepo|Repo", + "line_number": 9, + "source": "shop.repo.SqlRepo", + "target": "shop.repo.Repo", + "type": "REFERENCES", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": "Call to self.repo.save", + "file_path": "shop/service.py", + "id": "shop/service.py:edge_229_249", + "line_number": 12, + "source": "shop.service.OrderService.place", + "target": "shop.repo.Repo.save", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": "Call to repo.save", + "file_path": "shop/service.py", + "id": "shop/service.py:edge_298_313", + "line_number": 16, + "source": "shop.service.place", + "target": "shop.repo.Repo.save", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": "Annotation candidate RepoDep", + "file_path": "shop/service.py", + "id": "shop/service.py:rawdep_137_144_RepoDep", + "line_number": 8, + "source": "shop.service.OrderService.__init__", + "target": "shop.deps.RepoDep", + "type": "DEPENDS_ON", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": "Annotation candidate Repo", + "file_path": "shop/service.py", + "id": "shop/service.py:rawdep_268_272_Repo", + "line_number": 15, + "source": "shop.service.place", + "target": "shop.repo.Repo", + "type": "REFERENCES", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "shop/__init__.py", + "id": "shop:imports:shop.service", + "line_number": null, + "source": "shop", + "target": "shop.service", + "type": "IMPORTS", + "type_only": 0, + "weight": 1.0 + } + ], + "ingest_state": { + "ingest_options": { + "domains": null, + "source_roots": [] + }, + "npm_dependencies": [], + "python_dependencies": [ + "alembic", + "fastapi" + ], + "root": "/py/src", + "tsconfig_paths": {}, + "workspace_packages": {} + }, + "nodes": [ + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "abc", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "abc", + "namespace": "STDLIB", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "abc.ABC", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "ABC", + "namespace": "STDLIB", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "abc.abstractmethod", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "abstractmethod", + "namespace": "STDLIB", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 3, + "file_path": "alembic/env.py", + "id": "alembic.env", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "import_map": {}, + "reexports": {}, + "star_imports": [] + }, + "name": "env.py", + "namespace": "INTERNAL", + "start_line": 1, + "type": "FILE" + }, + { + "end_line": 2, + "file_path": "alembic/env.py", + "id": "alembic.env.run_migrations", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "run_migrations", + "namespace": "INTERNAL", + "start_line": 1, + "type": "FUNCTION" + }, + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "fastapi", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "fastapi", + "namespace": "EXTERNAL", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "fastapi.Depends", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "Depends", + "namespace": "EXTERNAL", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "print", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "print", + "namespace": "STDLIB", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 2, + "file_path": "shop/__init__.py", + "id": "shop", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "import_map": {}, + "reexports": {}, + "star_imports": [ + "shop.service" + ] + }, + "name": "__init__.py", + "namespace": "INTERNAL", + "start_line": 1, + "type": "FILE" + }, + { + "end_line": 13, + "file_path": "shop/deps.py", + "id": "shop.deps", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "import_map": { + "Annotated": "typing.Annotated", + "Depends": "fastapi.Depends", + "Repo": "shop.repo.Repo", + "SqlRepo": "shop.repo.SqlRepo" + }, + "reexports": {}, + "star_imports": [] + }, + "name": "deps.py", + "namespace": "INTERNAL", + "start_line": 1, + "type": "FILE" + }, + { + "end_line": 12, + "file_path": "shop/deps.py", + "id": "shop.deps.RepoDep", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "annotated_type": "shop.repo.Repo" + }, + "name": "RepoDep", + "namespace": "INTERNAL", + "start_line": 12, + "type": "VARIABLE" + }, + { + "end_line": 9, + "file_path": "shop/deps.py", + "id": "shop.deps.get_repo", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "get_repo", + "namespace": "INTERNAL", + "start_line": 8, + "type": "FUNCTION" + }, + { + "end_line": 12, + "file_path": "shop/repo.py", + "id": "shop.repo", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "import_map": { + "ABC": "abc.ABC", + "abstractmethod": "abc.abstractmethod" + }, + "reexports": {}, + "star_imports": [] + }, + "name": "repo.py", + "namespace": "INTERNAL", + "start_line": 1, + "type": "FILE" + }, + { + "end_line": 6, + "file_path": "shop/repo.py", + "id": "shop.repo.Repo", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "is_abstract": true + }, + "name": "Repo", + "namespace": "INTERNAL", + "start_line": 4, + "type": "CLASS" + }, + { + "end_line": 6, + "file_path": "shop/repo.py", + "id": "shop.repo.Repo.save", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "decorators": [ + "abstractmethod" + ], + "is_abstract": true, + "local_types": { + "item": "shop.repo.str" + }, + "self_attrs": [] + }, + "name": "save", + "namespace": "INTERNAL", + "start_line": 6, + "type": "METHOD" + }, + { + "end_line": 11, + "file_path": "shop/repo.py", + "id": "shop.repo.SqlRepo", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "SqlRepo", + "namespace": "INTERNAL", + "start_line": 9, + "type": "CLASS" + }, + { + "end_line": 11, + "file_path": "shop/repo.py", + "id": "shop.repo.SqlRepo.save", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "local_types": { + "item": "shop.repo.str" + }, + "self_attrs": [] + }, + "name": "save", + "namespace": "INTERNAL", + "start_line": 10, + "type": "METHOD" + }, + { + "end_line": 17, + "file_path": "shop/service.py", + "id": "shop.service", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "import_map": { + "Repo": "shop.repo.Repo", + "RepoDep": "shop.deps.RepoDep" + }, + "reexports": {}, + "star_imports": [] + }, + "name": "service.py", + "namespace": "INTERNAL", + "start_line": 1, + "type": "FILE" + }, + { + "end_line": 12, + "file_path": "shop/service.py", + "id": "shop.service.OrderService", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "self_types": { + "repo": "shop.deps.RepoDep" + } + }, + "name": "OrderService", + "namespace": "INTERNAL", + "start_line": 7, + "type": "CLASS" + }, + { + "end_line": 9, + "file_path": "shop/service.py", + "id": "shop.service.OrderService.__init__", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "local_types": { + "repo": "shop.deps.RepoDep" + }, + "self_attrs": [ + "repo" + ] + }, + "name": "__init__", + "namespace": "INTERNAL", + "start_line": 8, + "type": "METHOD" + }, + { + "end_line": 12, + "file_path": "shop/service.py", + "id": "shop.service.OrderService.place", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "local_types": { + "item": "shop.service.str" + }, + "self_attrs": [ + "repo" + ] + }, + "name": "place", + "namespace": "INTERNAL", + "start_line": 11, + "type": "METHOD" + }, + { + "end_line": 16, + "file_path": "shop/service.py", + "id": "shop.service.place", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "local_types": { + "item": "shop.service.str", + "repo": "shop.repo.Repo" + } + }, + "name": "place", + "namespace": "INTERNAL", + "start_line": 15, + "type": "FUNCTION" + }, + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "typing", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "typing", + "namespace": "STDLIB", + "start_line": 0, + "type": "FUNCTION" + } + ] + }, + "sub": { + "edges": [ + { + "confidence": 1.0, + "context": null, + "file_path": null, + "id": "main->import:@app.util", + "line_number": null, + "source": "main", + "target": "util", + "type": "IMPORTS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 0.8, + "context": null, + "file_path": null, + "id": "main->import:@x.lib", + "line_number": null, + "source": "main", + "target": "@x.lib", + "type": "IMPORTS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": null, + "id": "main->import:lodash.debounce", + "line_number": null, + "source": "main", + "target": "lodash.debounce", + "type": "IMPORTS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": null, + "id": "main->import:node:crypto", + "line_number": null, + "source": "main", + "target": "node:crypto", + "type": "IMPORTS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": null, + "id": "main->import:react", + "line_number": null, + "source": "main", + "target": "react", + "type": "IMPORTS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": null, + "id": "main->main.main", + "line_number": null, + "source": "main", + "target": "main.main", + "type": "CONTAINS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": null, + "id": "main->main.setTimeout", + "line_number": null, + "source": "main", + "target": "main.setTimeout", + "type": "CONTAINS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 0.6, + "context": null, + "file_path": null, + "id": "main.main->raw_call:debounce@13:19", + "line_number": null, + "source": "main.main", + "target": "lodash.debounce", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 0.6, + "context": null, + "file_path": null, + "id": "main.main->raw_call:helper@13:34", + "line_number": null, + "source": "main.main", + "target": "util.helper", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 0.8, + "context": null, + "file_path": null, + "id": "main.main->raw_call:libFn@13:41", + "line_number": null, + "source": "main.main", + "target": "libFn", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 0.8, + "context": null, + "file_path": null, + "id": "main.main->raw_call:libFn@14:9", + "line_number": null, + "source": "main.main", + "target": "libFn", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 0.6, + "context": null, + "file_path": null, + "id": "main.main->raw_call:randomUUID@12:2", + "line_number": null, + "source": "main.main", + "target": "node:crypto.randomUUID", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 0.6, + "context": null, + "file_path": null, + "id": "main.main->raw_call:setTimeout@13:2", + "line_number": null, + "source": "main.main", + "target": "main.setTimeout", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 0.6, + "context": null, + "file_path": null, + "id": "main.main->raw_call:useState@11:2", + "line_number": null, + "source": "main.main", + "target": "react.useState", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 0.8, + "context": null, + "file_path": null, + "id": "main.setTimeout->raw_call:fn@7:2", + "line_number": null, + "source": "main.setTimeout", + "target": "fn", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": null, + "id": "util->util.helper", + "line_number": null, + "source": "util", + "target": "util.helper", + "type": "CONTAINS", + "type_only": 0, + "weight": 1.0 + } + ], + "ingest_state": { + "ingest_options": { + "domains": null, + "source_roots": [] + }, + "npm_dependencies": [ + "lodash.debounce", + "react" + ], + "python_dependencies": [], + "root": "/ts/apps/web/src", + "tsconfig_paths": { + "": { + "@app.*": [ + "*" + ] + } + }, + "workspace_packages": {} + }, + "nodes": [ + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "@x.lib", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "lib", + "namespace": "UNKNOWN", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "fn", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "fn", + "namespace": "UNKNOWN", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "libFn", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "libFn", + "namespace": "UNKNOWN", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "lodash.debounce", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "debounce", + "namespace": "EXTERNAL", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 17, + "file_path": "main.ts", + "id": "main", + "is_generated": 0, + "is_test": 0, + "language": "typescript", + "metadata": { + "bare_bindings": { + "debounce": "lodash.debounce", + "helper": "@app.util.helper", + "libFn": "@x.lib.libFn", + "randomUUID": "node:crypto.randomUUID", + "useState": "react.useState" + }, + "bare_imports": [ + "@app/util", + "@x/lib", + "lodash.debounce", + "node:crypto", + "react" + ], + "shadowed_globals": [ + "setTimeout" + ] + }, + "name": "main.ts", + "namespace": "INTERNAL", + "start_line": 1, + "type": "FILE" + }, + { + "end_line": 16, + "file_path": "main.ts", + "id": "main.main", + "is_generated": 0, + "is_test": 0, + "language": "typescript", + "metadata": {}, + "name": "main", + "namespace": "INTERNAL", + "start_line": 11, + "type": "FUNCTION" + }, + { + "end_line": 9, + "file_path": "main.ts", + "id": "main.setTimeout", + "is_generated": 0, + "is_test": 0, + "language": "typescript", + "metadata": {}, + "name": "setTimeout", + "namespace": "INTERNAL", + "start_line": 7, + "type": "FUNCTION" + }, + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "node:crypto", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "node:crypto", + "namespace": "STDLIB", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "node:crypto.randomUUID", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "randomUUID", + "namespace": "STDLIB", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "react", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "react", + "namespace": "EXTERNAL", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "react.useState", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "useState", + "namespace": "EXTERNAL", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 4, + "file_path": "types.d.ts", + "id": "types.d", + "is_generated": 0, + "is_test": 0, + "language": "typescript", + "metadata": {}, + "name": "types.d.ts", + "namespace": "INTERNAL", + "start_line": 1, + "type": "FILE" + }, + { + "end_line": 4, + "file_path": "util.ts", + "id": "util", + "is_generated": 0, + "is_test": 0, + "language": "typescript", + "metadata": {}, + "name": "util.ts", + "namespace": "INTERNAL", + "start_line": 1, + "type": "FILE" + }, + { + "end_line": 3, + "file_path": "util.ts", + "id": "util.helper", + "is_generated": 0, + "is_test": 0, + "language": "typescript", + "metadata": {}, + "name": "helper", + "namespace": "INTERNAL", + "start_line": 1, + "type": "FUNCTION" + } + ] + }, + "whole": { + "edges": [ + { + "confidence": 0.5, + "context": "override of py.src.shop.repo.Repo.save", + "file_path": "py/src/shop/repo.py", + "id": "dispatch:py.src.shop.repo.Repo.save->py.src.shop.repo.SqlRepo.save", + "line_number": 10, + "source": "py.src.shop.repo.Repo.save", + "target": "py.src.shop.repo.SqlRepo.save", + "type": "DISPATCHES", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/lib/legacy/tool.py", + "id": "py.lib.legacy.tool:imports:shop", + "line_number": null, + "source": "py.lib.legacy.tool", + "target": "shop", + "type": "IMPORTS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/lib/legacy/tool.py", + "id": "py.lib.legacy.tool:structural:py.lib.legacy.tool.main", + "line_number": null, + "source": "py.lib.legacy.tool", + "target": "py.lib.legacy.tool.main", + "type": "CONTAINS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/src/alembic/env.py", + "id": "py.src.alembic.env:structural:py.src.alembic.env.run_migrations", + "line_number": null, + "source": "py.src.alembic.env", + "target": "py.src.alembic.env.run_migrations", + "type": "CONTAINS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/src/shop/deps.py", + "id": "py.src.shop.deps:imports:fastapi", + "line_number": null, + "source": "py.src.shop.deps", + "target": "fastapi", + "type": "IMPORTS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/src/shop/deps.py", + "id": "py.src.shop.deps:imports:py.src.shop.repo", + "line_number": null, + "source": "py.src.shop.deps", + "target": "py.src.shop.repo", + "type": "IMPORTS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/src/shop/deps.py", + "id": "py.src.shop.deps:imports:typing", + "line_number": null, + "source": "py.src.shop.deps", + "target": "typing", + "type": "IMPORTS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/src/shop/deps.py", + "id": "py.src.shop.deps:imports_symbol:py.src.shop.repo.Repo", + "line_number": null, + "source": "py.src.shop.deps", + "target": "py.src.shop.repo.Repo", + "type": "IMPORTS_SYMBOL", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/src/shop/deps.py", + "id": "py.src.shop.deps:imports_symbol:py.src.shop.repo.SqlRepo", + "line_number": null, + "source": "py.src.shop.deps", + "target": "py.src.shop.repo.SqlRepo", + "type": "IMPORTS_SYMBOL", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/src/shop/deps.py", + "id": "py.src.shop.deps:structural:py.src.shop.deps.get_repo", + "line_number": null, + "source": "py.src.shop.deps", + "target": "py.src.shop.deps.get_repo", + "type": "CONTAINS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/src/shop/repo.py", + "id": "py.src.shop.repo.Repo.save:decorator:0:abstractmethod", + "line_number": null, + "source": "py.src.shop.repo.Repo.save", + "target": "abc.abstractmethod", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/src/shop/repo.py", + "id": "py.src.shop.repo.Repo:extends:ABC", + "line_number": null, + "source": "py.src.shop.repo.Repo", + "target": "abc.ABC", + "type": "EXTENDS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/src/shop/repo.py", + "id": "py.src.shop.repo.Repo:structural:py.src.shop.repo.Repo.save", + "line_number": null, + "source": "py.src.shop.repo.Repo", + "target": "py.src.shop.repo.Repo.save", + "type": "DECLARES", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/src/shop/repo.py", + "id": "py.src.shop.repo.SqlRepo:extends:Repo", + "line_number": null, + "source": "py.src.shop.repo.SqlRepo", + "target": "py.src.shop.repo.Repo", + "type": "EXTENDS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/src/shop/repo.py", + "id": "py.src.shop.repo.SqlRepo:structural:py.src.shop.repo.SqlRepo.save", + "line_number": null, + "source": "py.src.shop.repo.SqlRepo", + "target": "py.src.shop.repo.SqlRepo.save", + "type": "DECLARES", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/src/shop/repo.py", + "id": "py.src.shop.repo:imports:abc", + "line_number": null, + "source": "py.src.shop.repo", + "target": "abc", + "type": "IMPORTS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/src/shop/repo.py", + "id": "py.src.shop.repo:structural:py.src.shop.repo.Repo", + "line_number": null, + "source": "py.src.shop.repo", + "target": "py.src.shop.repo.Repo", + "type": "CONTAINS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/src/shop/repo.py", + "id": "py.src.shop.repo:structural:py.src.shop.repo.SqlRepo", + "line_number": null, + "source": "py.src.shop.repo", + "target": "py.src.shop.repo.SqlRepo", + "type": "CONTAINS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/src/shop/service.py", + "id": "py.src.shop.service.OrderService:structural:py.src.shop.service.OrderService.__init__", + "line_number": null, + "source": "py.src.shop.service.OrderService", + "target": "py.src.shop.service.OrderService.__init__", + "type": "DECLARES", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/src/shop/service.py", + "id": "py.src.shop.service.OrderService:structural:py.src.shop.service.OrderService.place", + "line_number": null, + "source": "py.src.shop.service.OrderService", + "target": "py.src.shop.service.OrderService.place", + "type": "DECLARES", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/src/shop/service.py", + "id": "py.src.shop.service:imports:py.src.shop.deps", + "line_number": null, + "source": "py.src.shop.service", + "target": "py.src.shop.deps", + "type": "IMPORTS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/src/shop/service.py", + "id": "py.src.shop.service:imports:py.src.shop.repo", + "line_number": null, + "source": "py.src.shop.service", + "target": "py.src.shop.repo", + "type": "IMPORTS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/src/shop/service.py", + "id": "py.src.shop.service:imports_symbol:py.src.shop.deps.RepoDep", + "line_number": null, + "source": "py.src.shop.service", + "target": "py.src.shop.deps.RepoDep", + "type": "IMPORTS_SYMBOL", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/src/shop/service.py", + "id": "py.src.shop.service:imports_symbol:py.src.shop.repo.Repo", + "line_number": null, + "source": "py.src.shop.service", + "target": "py.src.shop.repo.Repo", + "type": "IMPORTS_SYMBOL", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/src/shop/service.py", + "id": "py.src.shop.service:structural:py.src.shop.service.OrderService", + "line_number": null, + "source": "py.src.shop.service", + "target": "py.src.shop.service.OrderService", + "type": "CONTAINS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/src/shop/service.py", + "id": "py.src.shop.service:structural:py.src.shop.service.place", + "line_number": null, + "source": "py.src.shop.service", + "target": "py.src.shop.service.place", + "type": "CONTAINS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/src/shop/__init__.py", + "id": "py.src.shop:imports:py.src.shop.service", + "line_number": null, + "source": "py.src.shop", + "target": "py.src.shop.service", + "type": "IMPORTS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/tests/test_service.py", + "id": "py.tests.test_service:imports:shop.service", + "line_number": null, + "source": "py.tests.test_service", + "target": "shop.service", + "type": "IMPORTS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/tests/test_service.py", + "id": "py.tests.test_service:imports_symbol:shop.service.place", + "line_number": null, + "source": "py.tests.test_service", + "target": "py.src.shop.service.place", + "type": "IMPORTS_SYMBOL", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/tests/test_service.py", + "id": "py.tests.test_service:structural:py.tests.test_service.test_place", + "line_number": null, + "source": "py.tests.test_service", + "target": "py.tests.test_service.test_place", + "type": "CONTAINS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": "Call to place", + "file_path": "py/lib/legacy/tool.py", + "id": "py/lib/legacy/tool.py:edge_49_65", + "line_number": 5, + "source": "py.lib.legacy.tool.main", + "target": "shop.place", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": "DI alias for get_repo", + "file_path": "py/src/shop/deps.py", + "id": "py/src/shop/deps.py:dep_166_183", + "line_number": 12, + "source": "py.src.shop.deps.RepoDep", + "target": "py.src.shop.deps.get_repo", + "type": "DEPENDS_ON", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": "Call to SqlRepo", + "file_path": "py/src/shop/deps.py", + "id": "py/src/shop/deps.py:edge_128_137", + "line_number": 9, + "source": "py.src.shop.deps.get_repo", + "target": "py.src.shop.repo.SqlRepo", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": "Call to Depends", + "file_path": "py/src/shop/deps.py", + "id": "py/src/shop/deps.py:edge_166_183", + "line_number": 12, + "source": "py.src.shop.deps", + "target": "fastapi.Depends", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": "Name reference candidate Repo", + "file_path": "py/src/shop/deps.py", + "id": "py/src/shop/deps.py:nameref_py.src.shop.deps|Repo", + "line_number": 12, + "source": "py.src.shop.deps", + "target": "py.src.shop.repo.Repo", + "type": "REFERENCES", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": "Annotation candidate Repo", + "file_path": "py/src/shop/deps.py", + "id": "py/src/shop/deps.py:rawdep_111_115_Repo", + "line_number": 8, + "source": "py.src.shop.deps.get_repo", + "target": "py.src.shop.repo.Repo", + "type": "REFERENCES", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 0.8, + "context": "Call to print", + "file_path": "py/src/shop/repo.py", + "id": "py/src/shop/repo.py:edge_188_199", + "line_number": 11, + "source": "py.src.shop.repo.SqlRepo.save", + "target": "print", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": "Name reference candidate Repo", + "file_path": "py/src/shop/repo.py", + "id": "py/src/shop/repo.py:nameref_py.src.shop.repo.SqlRepo|Repo", + "line_number": 9, + "source": "py.src.shop.repo.SqlRepo", + "target": "py.src.shop.repo.Repo", + "type": "REFERENCES", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": "Call to self.repo.save", + "file_path": "py/src/shop/service.py", + "id": "py/src/shop/service.py:edge_229_249", + "line_number": 12, + "source": "py.src.shop.service.OrderService.place", + "target": "py.src.shop.repo.Repo.save", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": "Call to repo.save", + "file_path": "py/src/shop/service.py", + "id": "py/src/shop/service.py:edge_298_313", + "line_number": 16, + "source": "py.src.shop.service.place", + "target": "py.src.shop.repo.Repo.save", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": "Annotation candidate RepoDep", + "file_path": "py/src/shop/service.py", + "id": "py/src/shop/service.py:rawdep_137_144_RepoDep", + "line_number": 8, + "source": "py.src.shop.service.OrderService.__init__", + "target": "py.src.shop.deps.RepoDep", + "type": "DEPENDS_ON", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": "Annotation candidate Repo", + "file_path": "py/src/shop/service.py", + "id": "py/src/shop/service.py:rawdep_268_272_Repo", + "line_number": 15, + "source": "py.src.shop.service.place", + "target": "py.src.shop.repo.Repo", + "type": "REFERENCES", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": "Call to place", + "file_path": "py/tests/test_service.py", + "id": "py/tests/test_service.py:edge_63_79", + "line_number": 5, + "source": "py.tests.test_service.test_place", + "target": "py.src.shop.service.place", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": null, + "id": "ts.apps.web.src.main->import:@app.util", + "line_number": null, + "source": "ts.apps.web.src.main", + "target": "ts.apps.web.src.util", + "type": "IMPORTS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": null, + "id": "ts.apps.web.src.main->import:@x.lib", + "line_number": null, + "source": "ts.apps.web.src.main", + "target": "ts.packages.lib", + "type": "IMPORTS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": null, + "id": "ts.apps.web.src.main->import:lodash.debounce", + "line_number": null, + "source": "ts.apps.web.src.main", + "target": "lodash.debounce", + "type": "IMPORTS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": null, + "id": "ts.apps.web.src.main->import:node:crypto", + "line_number": null, + "source": "ts.apps.web.src.main", + "target": "node:crypto", + "type": "IMPORTS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": null, + "id": "ts.apps.web.src.main->import:react", + "line_number": null, + "source": "ts.apps.web.src.main", + "target": "react", + "type": "IMPORTS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": null, + "id": "ts.apps.web.src.main->ts.apps.web.src.main.main", + "line_number": null, + "source": "ts.apps.web.src.main", + "target": "ts.apps.web.src.main.main", + "type": "CONTAINS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": null, + "id": "ts.apps.web.src.main->ts.apps.web.src.main.setTimeout", + "line_number": null, + "source": "ts.apps.web.src.main", + "target": "ts.apps.web.src.main.setTimeout", + "type": "CONTAINS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 0.6, + "context": null, + "file_path": null, + "id": "ts.apps.web.src.main.main->raw_call:debounce@13:19", + "line_number": null, + "source": "ts.apps.web.src.main.main", + "target": "lodash.debounce", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 0.6, + "context": null, + "file_path": null, + "id": "ts.apps.web.src.main.main->raw_call:helper@13:34", + "line_number": null, + "source": "ts.apps.web.src.main.main", + "target": "ts.apps.web.src.util.helper", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 0.6, + "context": null, + "file_path": null, + "id": "ts.apps.web.src.main.main->raw_call:libFn@13:41", + "line_number": null, + "source": "ts.apps.web.src.main.main", + "target": "ts.packages.lib.libFn", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 0.6, + "context": null, + "file_path": null, + "id": "ts.apps.web.src.main.main->raw_call:libFn@14:9", + "line_number": null, + "source": "ts.apps.web.src.main.main", + "target": "ts.packages.lib.libFn", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 0.6, + "context": null, + "file_path": null, + "id": "ts.apps.web.src.main.main->raw_call:randomUUID@12:2", + "line_number": null, + "source": "ts.apps.web.src.main.main", + "target": "node:crypto.randomUUID", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 0.6, + "context": null, + "file_path": null, + "id": "ts.apps.web.src.main.main->raw_call:setTimeout@13:2", + "line_number": null, + "source": "ts.apps.web.src.main.main", + "target": "ts.apps.web.src.main.setTimeout", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 0.6, + "context": null, + "file_path": null, + "id": "ts.apps.web.src.main.main->raw_call:useState@11:2", + "line_number": null, + "source": "ts.apps.web.src.main.main", + "target": "react.useState", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 0.8, + "context": null, + "file_path": null, + "id": "ts.apps.web.src.main.setTimeout->raw_call:fn@7:2", + "line_number": null, + "source": "ts.apps.web.src.main.setTimeout", + "target": "fn", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": null, + "id": "ts.apps.web.src.util->ts.apps.web.src.util.helper", + "line_number": null, + "source": "ts.apps.web.src.util", + "target": "ts.apps.web.src.util.helper", + "type": "CONTAINS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": null, + "id": "ts.packages.lib->ts.packages.lib.libFn", + "line_number": null, + "source": "ts.packages.lib", + "target": "ts.packages.lib.libFn", + "type": "CONTAINS", + "type_only": 0, + "weight": 1.0 + } + ], + "ingest_state": { + "ingest_options": { + "domains": null, + "source_roots": [] + }, + "npm_dependencies": [ + "lodash.debounce", + "react" + ], + "python_dependencies": [], + "root": "", + "tsconfig_paths": { + "ts": { + "@app.*": [ + "ts.apps.web.src.*" + ] + } + }, + "workspace_packages": { + "@x.lib": "ts.packages.lib", + "mono": "ts", + "web": "ts.apps.web" + } + }, + "nodes": [ + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "abc", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "abc", + "namespace": "STDLIB", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "abc.ABC", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "ABC", + "namespace": "STDLIB", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "abc.abstractmethod", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "abstractmethod", + "namespace": "STDLIB", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "fastapi", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "fastapi", + "namespace": "EXTERNAL", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "fastapi.Depends", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "Depends", + "namespace": "EXTERNAL", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "fn", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "fn", + "namespace": "UNKNOWN", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "lodash.debounce", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "debounce", + "namespace": "EXTERNAL", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "node:crypto", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "node:crypto", + "namespace": "STDLIB", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "node:crypto.randomUUID", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "randomUUID", + "namespace": "STDLIB", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "print", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "print", + "namespace": "STDLIB", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 6, + "file_path": "py/lib/legacy/tool.py", + "id": "py.lib.legacy.tool", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "import_map": { + "place": "shop.place" + }, + "reexports": {}, + "star_imports": [] + }, + "name": "tool.py", + "namespace": "INTERNAL", + "start_line": 1, + "type": "FILE" + }, + { + "end_line": 5, + "file_path": "py/lib/legacy/tool.py", + "id": "py.lib.legacy.tool.main", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "main", + "namespace": "INTERNAL", + "start_line": 4, + "type": "FUNCTION" + }, + { + "end_line": 3, + "file_path": "py/src/alembic/env.py", + "id": "py.src.alembic.env", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "import_map": {}, + "reexports": {}, + "star_imports": [] + }, + "name": "env.py", + "namespace": "INTERNAL", + "start_line": 1, + "type": "FILE" + }, + { + "end_line": 2, + "file_path": "py/src/alembic/env.py", + "id": "py.src.alembic.env.run_migrations", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "run_migrations", + "namespace": "INTERNAL", + "start_line": 1, + "type": "FUNCTION" + }, + { + "end_line": 2, + "file_path": "py/src/shop/__init__.py", + "id": "py.src.shop", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "import_map": {}, + "reexports": {}, + "star_imports": [ + "py.src.shop.service" + ] + }, + "name": "__init__.py", + "namespace": "INTERNAL", + "start_line": 1, + "type": "FILE" + }, + { + "end_line": 13, + "file_path": "py/src/shop/deps.py", + "id": "py.src.shop.deps", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "import_map": { + "Annotated": "typing.Annotated", + "Depends": "fastapi.Depends", + "Repo": "py.src.shop.repo.Repo", + "SqlRepo": "py.src.shop.repo.SqlRepo" + }, + "reexports": {}, + "star_imports": [] + }, + "name": "deps.py", + "namespace": "INTERNAL", + "start_line": 1, + "type": "FILE" + }, + { + "end_line": 12, + "file_path": "py/src/shop/deps.py", + "id": "py.src.shop.deps.RepoDep", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "annotated_type": "py.src.shop.repo.Repo" + }, + "name": "RepoDep", + "namespace": "INTERNAL", + "start_line": 12, + "type": "VARIABLE" + }, + { + "end_line": 9, + "file_path": "py/src/shop/deps.py", + "id": "py.src.shop.deps.get_repo", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "get_repo", + "namespace": "INTERNAL", + "start_line": 8, + "type": "FUNCTION" + }, + { + "end_line": 12, + "file_path": "py/src/shop/repo.py", + "id": "py.src.shop.repo", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "import_map": { + "ABC": "abc.ABC", + "abstractmethod": "abc.abstractmethod" + }, + "reexports": {}, + "star_imports": [] + }, + "name": "repo.py", + "namespace": "INTERNAL", + "start_line": 1, + "type": "FILE" + }, + { + "end_line": 6, + "file_path": "py/src/shop/repo.py", + "id": "py.src.shop.repo.Repo", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "is_abstract": true + }, + "name": "Repo", + "namespace": "INTERNAL", + "start_line": 4, + "type": "CLASS" + }, + { + "end_line": 6, + "file_path": "py/src/shop/repo.py", + "id": "py.src.shop.repo.Repo.save", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "decorators": [ + "abstractmethod" + ], + "is_abstract": true, + "local_types": { + "item": "py.src.shop.repo.str" + }, + "self_attrs": [] + }, + "name": "save", + "namespace": "INTERNAL", + "start_line": 6, + "type": "METHOD" + }, + { + "end_line": 11, + "file_path": "py/src/shop/repo.py", + "id": "py.src.shop.repo.SqlRepo", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "SqlRepo", + "namespace": "INTERNAL", + "start_line": 9, + "type": "CLASS" + }, + { + "end_line": 11, + "file_path": "py/src/shop/repo.py", + "id": "py.src.shop.repo.SqlRepo.save", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "local_types": { + "item": "py.src.shop.repo.str" + }, + "self_attrs": [] + }, + "name": "save", + "namespace": "INTERNAL", + "start_line": 10, + "type": "METHOD" + }, + { + "end_line": 17, + "file_path": "py/src/shop/service.py", + "id": "py.src.shop.service", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "import_map": { + "Repo": "py.src.shop.repo.Repo", + "RepoDep": "py.src.shop.deps.RepoDep" + }, + "reexports": {}, + "star_imports": [] + }, + "name": "service.py", + "namespace": "INTERNAL", + "start_line": 1, + "type": "FILE" + }, + { + "end_line": 12, + "file_path": "py/src/shop/service.py", + "id": "py.src.shop.service.OrderService", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "self_types": { + "repo": "py.src.shop.deps.RepoDep" + } + }, + "name": "OrderService", + "namespace": "INTERNAL", + "start_line": 7, + "type": "CLASS" + }, + { + "end_line": 9, + "file_path": "py/src/shop/service.py", + "id": "py.src.shop.service.OrderService.__init__", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "local_types": { + "repo": "py.src.shop.deps.RepoDep" + }, + "self_attrs": [ + "repo" + ] + }, + "name": "__init__", + "namespace": "INTERNAL", + "start_line": 8, + "type": "METHOD" + }, + { + "end_line": 12, + "file_path": "py/src/shop/service.py", + "id": "py.src.shop.service.OrderService.place", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "local_types": { + "item": "py.src.shop.service.str" + }, + "self_attrs": [ + "repo" + ] + }, + "name": "place", + "namespace": "INTERNAL", + "start_line": 11, + "type": "METHOD" + }, + { + "end_line": 16, + "file_path": "py/src/shop/service.py", + "id": "py.src.shop.service.place", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "local_types": { + "item": "py.src.shop.service.str", + "repo": "py.src.shop.repo.Repo" + } + }, + "name": "place", + "namespace": "INTERNAL", + "start_line": 15, + "type": "FUNCTION" + }, + { + "end_line": 6, + "file_path": "py/tests/test_service.py", + "id": "py.tests.test_service", + "is_generated": 0, + "is_test": 1, + "language": "python", + "metadata": { + "import_map": { + "place": "shop.service.place" + }, + "reexports": {}, + "star_imports": [] + }, + "name": "test_service.py", + "namespace": "INTERNAL", + "start_line": 1, + "type": "FILE" + }, + { + "end_line": 5, + "file_path": "py/tests/test_service.py", + "id": "py.tests.test_service.test_place", + "is_generated": 0, + "is_test": 1, + "language": "python", + "metadata": {}, + "name": "test_place", + "namespace": "INTERNAL", + "start_line": 4, + "type": "FUNCTION" + }, + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "react", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "react", + "namespace": "EXTERNAL", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "react.useState", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "useState", + "namespace": "EXTERNAL", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "shop", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "shop", + "namespace": "EXTERNAL", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "shop.place", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "place", + "namespace": "EXTERNAL", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "shop.service", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "service", + "namespace": "EXTERNAL", + "start_line": 0, + "type": "FUNCTION" + }, + { + "end_line": 17, + "file_path": "ts/apps/web/src/main.ts", + "id": "ts.apps.web.src.main", + "is_generated": 0, + "is_test": 0, + "language": "typescript", + "metadata": { + "bare_bindings": { + "debounce": "lodash.debounce", + "helper": "@app.util.helper", + "libFn": "@x.lib.libFn", + "randomUUID": "node:crypto.randomUUID", + "useState": "react.useState" + }, + "bare_imports": [ + "@app/util", + "@x/lib", + "lodash.debounce", + "node:crypto", + "react" + ], + "shadowed_globals": [ + "setTimeout" + ] + }, + "name": "main.ts", + "namespace": "INTERNAL", + "start_line": 1, + "type": "FILE" + }, + { + "end_line": 16, + "file_path": "ts/apps/web/src/main.ts", + "id": "ts.apps.web.src.main.main", + "is_generated": 0, + "is_test": 0, + "language": "typescript", + "metadata": {}, + "name": "main", + "namespace": "INTERNAL", + "start_line": 11, + "type": "FUNCTION" + }, + { + "end_line": 9, + "file_path": "ts/apps/web/src/main.ts", + "id": "ts.apps.web.src.main.setTimeout", + "is_generated": 0, + "is_test": 0, + "language": "typescript", + "metadata": {}, + "name": "setTimeout", + "namespace": "INTERNAL", + "start_line": 7, + "type": "FUNCTION" + }, + { + "end_line": 4, + "file_path": "ts/apps/web/src/types.d.ts", + "id": "ts.apps.web.src.types.d", + "is_generated": 0, + "is_test": 0, + "language": "typescript", + "metadata": {}, + "name": "types.d.ts", + "namespace": "INTERNAL", + "start_line": 1, + "type": "FILE" + }, + { + "end_line": 4, + "file_path": "ts/apps/web/src/util.ts", + "id": "ts.apps.web.src.util", + "is_generated": 0, + "is_test": 0, + "language": "typescript", + "metadata": {}, + "name": "util.ts", + "namespace": "INTERNAL", + "start_line": 1, + "type": "FILE" + }, + { + "end_line": 3, + "file_path": "ts/apps/web/src/util.ts", + "id": "ts.apps.web.src.util.helper", + "is_generated": 0, + "is_test": 0, + "language": "typescript", + "metadata": {}, + "name": "helper", + "namespace": "INTERNAL", + "start_line": 1, + "type": "FUNCTION" + }, + { + "end_line": 4, + "file_path": "ts/packages/lib/index.ts", + "id": "ts.packages.lib", + "is_generated": 0, + "is_test": 0, + "language": "typescript", + "metadata": {}, + "name": "index.ts", + "namespace": "INTERNAL", + "start_line": 1, + "type": "FILE" + }, + { + "end_line": 3, + "file_path": "ts/packages/lib/index.ts", + "id": "ts.packages.lib.libFn", + "is_generated": 0, + "is_test": 0, + "language": "typescript", + "metadata": {}, + "name": "libFn", + "namespace": "INTERNAL", + "start_line": 1, + "type": "FUNCTION" + }, + { + "end_line": 0, + "file_path": "EXTERNAL", + "id": "typing", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "typing", + "namespace": "STDLIB", + "start_line": 0, + "type": "FUNCTION" + } + ] + } +} diff --git a/tests/unit/test_graph_identity.py b/tests/unit/test_graph_identity.py new file mode 100644 index 00000000..010bb5de --- /dev/null +++ b/tests/unit/test_graph_identity.py @@ -0,0 +1,52 @@ +"""The Python/TS graph a fixed corpus produces must not change (language-packs spec, Migration). + +Regenerate only in a step meant to change behaviour, with the reason in the +commit message: + + uv run python scripts/graph_identity.py snapshot tests/fixtures/graph_identity/corpus \ + --out tests/fixtures/graph_identity/snapshot.json +""" + +import json +import os +import subprocess +import sys +from pathlib import Path + +import pytest + +REPO = Path(__file__).resolve().parent.parent.parent +sys.path.insert(0, str(REPO / "scripts")) + +import graph_identity as gi # noqa: E402 + +CORPUS = REPO / "tests" / "fixtures" / "graph_identity" / "corpus" +SNAPSHOT = REPO / "tests" / "fixtures" / "graph_identity" / "snapshot.json" + + +def _snapshot(seed: str, out: Path) -> dict[str, dict[str, object]]: + """The corpus snapshot, taken in a subprocess under one hash seed. + + Read from `--out`, not stdout: the pipeline logs to stdout through structlog. + """ + env = {**os.environ, "PYTHONHASHSEED": seed} + script = str(REPO / "scripts" / "graph_identity.py") + subprocess.run( + [sys.executable, script, "snapshot", str(CORPUS), "--out", str(out)], + env=env, + capture_output=True, + check=True, + ) + snapshot: dict[str, dict[str, object]] = json.loads(out.read_text(encoding="utf-8")) + return snapshot + + +@pytest.mark.parametrize("seed", ["0", "1"]) +def test_the_corpus_graph_matches_the_snapshot(seed: str, tmp_path: Path) -> None: + """Same input, same graph — whatever the hash seed, the machine or the temp directory.""" + recorded = json.loads(SNAPSHOT.read_text(encoding="utf-8")) + actual = _snapshot(seed, tmp_path / "snapshot.json") + assert set(actual) == set(recorded) == set(gi.SNAPSHOT_ROOTS) + for root in gi.SNAPSHOT_ROOTS: + lines = gi.diff(recorded[root], actual[root]) + assert not lines, f"{root} graph changed (PYTHONHASHSEED={seed}):\n" + "\n".join(lines[:40]) From e827b196e3a8266b296c1c3ab38b34965271a7d9 Mon Sep 17 00:00:00 2001 From: zaebee Date: Wed, 14 Oct 2026 08:44:55 +0000 Subject: [PATCH 4/8] test(architecture): forbidden-edge gate with exact allowlists (language-packs spec) Invariant 7 in test_architecture.py: six rules over the self-graph's IMPORTS, IMPORTS_SYMBOL and CALLS edges into INTERNAL nodes, attributed to FILE modules, compared for equality with forbidden_edges_allowlist.json, so a new edge, a grown count and a vanished entry all fail. Today's violations: - resolver-storage: cgis.resolver.uplift -> cgis.storage.sqlite_store (8) - surface-packs: cgis.api.mcp_server and cgis.cli import extractor classes (9) - surface-storage: cli/api/guardian -> storage (65 in 6 pairs) - cross-pack, pack-isolation, core-isolation: none. A probe that makes cgis.core.paths call SQLiteStore fails the gate under core-isolation. Co-Authored-By: Claude Opus 5.5 --- .../forbidden_edges_allowlist.json | 21 +++ tests/self_parsing/test_architecture.py | 135 +++++++++++++++++- 2 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 tests/self_parsing/forbidden_edges_allowlist.json diff --git a/tests/self_parsing/forbidden_edges_allowlist.json b/tests/self_parsing/forbidden_edges_allowlist.json new file mode 100644 index 00000000..9c4150a0 --- /dev/null +++ b/tests/self_parsing/forbidden_edges_allowlist.json @@ -0,0 +1,21 @@ +{ + "core-isolation": {}, + "cross-pack": {}, + "pack-isolation": {}, + "resolver-storage": { + "cgis.resolver.uplift -> cgis.storage.sqlite_store": 8 + }, + "surface-packs": { + "cgis.api.mcp_server -> cgis.extractors.python_extractor": 3, + "cgis.api.mcp_server -> cgis.extractors.typescript_extractor": 4, + "cgis.cli -> cgis.extractors.python_extractor": 2 + }, + "surface-storage": { + "cgis.api.auto_refresh -> cgis.storage.sqlite_store": 7, + "cgis.api.mcp_server -> cgis.storage.sqlite_store": 20, + "cgis.cli -> cgis.storage.sqlite_store": 25, + "cgis.guardian.chunked -> cgis.storage.sqlite_store": 3, + "cgis.guardian.chunker -> cgis.storage.sqlite_store": 4, + "cgis.guardian.collector -> cgis.storage.sqlite_store": 6 + } +} diff --git a/tests/self_parsing/test_architecture.py b/tests/self_parsing/test_architecture.py index 42d50a0d..c860d52f 100644 --- a/tests/self_parsing/test_architecture.py +++ b/tests/self_parsing/test_architecture.py @@ -9,7 +9,11 @@ 6. God Object Baseline — number of God Objects does not grow beyond known set. """ -from cgis.core.models import Edge, EdgeType, Node +import json +from dataclasses import dataclass, field +from pathlib import Path + +from cgis.core.models import Edge, EdgeType, Node, NodeNamespace, NodeType from cgis.query.analysis.analyzer import AnalyzerEngine from cgis.storage.sqlite_store import SQLiteStore @@ -205,3 +209,132 @@ def test_god_object_baseline_not_exceeded( assert not new_violations, _GOD_OBJECT_VIOLATION + "\n".join( f" {fqn}" for fqn in sorted(new_violations) ) + + +# --------------------------------------------------------------------------- +# Invariant 7 — Forbidden edges (language-packs spec, Drift gates) +# --------------------------------------------------------------------------- + +_ALLOWLIST = Path(__file__).parent / "forbidden_edges_allowlist.json" +_RULE_EDGES = frozenset({EdgeType.IMPORTS, EdgeType.IMPORTS_SYMBOL, EdgeType.CALLS}) + + +@dataclass(frozen=True) +class _Rule: + """No edge from a module under `sources` to one under `targets`, bar the exemptions.""" + + name: str + sources: tuple[str, ...] + targets: tuple[str, ...] + exempt_modules: frozenset[str] = field(default_factory=frozenset) # exact module ids + exempt_prefixes: tuple[str, ...] = () + cross_pack: bool = False # fire only when the `cgis.languages.` segments differ + + +RULES: tuple[_Rule, ...] = ( + _Rule("cross-pack", ("cgis.languages",), ("cgis.languages",), + exempt_modules=frozenset({"cgis.languages"}), cross_pack=True), + _Rule("pack-isolation", ("cgis.languages", "cgis.extractors"), + ("cgis.storage", "cgis.query", "cgis.api", "cgis.guardian", "cgis.pipeline")), + _Rule("resolver-storage", ("cgis.resolver",), ("cgis.storage",)), + _Rule("surface-packs", ("cgis.query", "cgis.guardian", "cgis.cli", "cgis.api"), + ("cgis.languages", "cgis.extractors"), + exempt_modules=frozenset({"cgis.languages", "cgis.extractors.registry"})), + _Rule("surface-storage", ("cgis.cli", "cgis.api", "cgis.guardian"), ("cgis.storage",)), + _Rule("core-isolation", ("cgis.core",), ("cgis",), exempt_prefixes=("cgis.core",)), +) # fmt: skip + + +def _under(module: str, prefix: str) -> bool: + return module == prefix or module.startswith(prefix + ".") + + +def _pack(module: str) -> str | None: + parts = module.split(".") + return parts[2] if len(parts) > 2 and parts[:2] == ["cgis", "languages"] else None + + +def _fires(rule: _Rule, src: str, dst: str) -> bool: + if not any(_under(src, p) for p in rule.sources) or not any( + _under(dst, p) for p in rule.targets + ): + return False + if dst in rule.exempt_modules or any(_under(dst, p) for p in rule.exempt_prefixes): + return False + return not rule.cross_pack or (_pack(src) is not None and _pack(src) != _pack(dst)) + + +def forbidden_edges(nodes: list[Node], edges: list[Edge]) -> dict[str, dict[str, int]]: + """Rule → "src module -> dst module" → edge count, over INTERNAL targets only.""" + module_of_file = {n.file_path: n.id for n in nodes if n.type == NodeType.FILE} + by_id = {n.id: n for n in nodes} + found: dict[str, dict[str, int]] = {rule.name: {} for rule in RULES} + for e in edges: + src, dst = by_id.get(e.source), by_id.get(e.target) + if e.type not in _RULE_EDGES or src is None or dst is None: + continue + if dst.namespace != NodeNamespace.INTERNAL: + continue + a, b = module_of_file.get(src.file_path), module_of_file.get(dst.file_path) + if a is None or b is None or a == b: + continue + for rule in RULES: + if _fires(rule, a, b): + key = f"{a} -> {b}" + found[rule.name][key] = found[rule.name].get(key, 0) + 1 + return found + + +def test_forbidden_edges_match_the_allowlist_exactly( + root_graph_data: tuple[SQLiteStore, list[Node], list[Edge]], +) -> None: + """Exact equality: a new edge fails, a grown count fails, a vanished entry fails. + + When a refactor removes violations, lower or delete the entries in the same PR. + Never add one: a new forbidden edge is the regression this gate exists for. + """ + _, nodes, edges = root_graph_data + actual = forbidden_edges(nodes, edges) + allowed = json.loads(_ALLOWLIST.read_text(encoding="utf-8")) + assert actual == allowed, ( + "Forbidden edges differ from forbidden_edges_allowlist.json. Actual:\n" + + json.dumps(actual, indent=1, sort_keys=True) + ) + + +def test_external_targets_are_never_attributed() -> None: + """A call into a stdlib or unresolved node is not an edge between our modules.""" + file_a = Node( + id="cgis.resolver.x", + type=NodeType.FILE, + name="x", + file_path="cgis/resolver/x.py", + start_line=1, + end_line=1, + ) + fn = Node( + id="cgis.resolver.x.f", + type=NodeType.FUNCTION, + name="f", + file_path="cgis/resolver/x.py", + start_line=1, + end_line=1, + ) + ext = Node( + id="cgis.storage.fake", + type=NodeType.FUNCTION, + name="fake", + file_path="", + start_line=0, + end_line=0, + namespace=NodeNamespace.UNKNOWN, + ) + edge = Edge(id="e", source=fn.id, target=ext.id, type=EdgeType.CALLS) + assert forbidden_edges([file_a, fn, ext], [edge])["resolver-storage"] == {} + + +def test_cross_pack_fires_only_between_different_packs() -> None: + rule = RULES[0] + assert _fires(rule, "cgis.languages.go.extractor", "cgis.languages.python.indices") + assert not _fires(rule, "cgis.languages.go.extractor", "cgis.languages.go.rules") + assert not _fires(rule, "cgis.languages.go.extractor", "cgis.languages") From 938f3214ffc7e7e27d42d73365b173b2460bdfa8 Mon Sep 17 00:00:00 2001 From: zaebee Date: Wed, 14 Oct 2026 08:47:25 +0000 Subject: [PATCH 5/8] test(drift): plane quotients reported observe-only (language-packs spec) Build plane {pipeline, extraction, resolution, storage} against orchestrator and read plane {storage, query, guardian, cli, api} against layered_dag, printed each run and never enforced: 0.238 and 0.676 at this commit, as the spec measured. A plane whose domain matches nothing fails loudly instead of scoring an empty quotient. Co-Authored-By: Claude Opus 5.5 --- tests/self_parsing/test_drift.py | 33 ++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/self_parsing/test_drift.py b/tests/self_parsing/test_drift.py index 174f042c..027b6cca 100644 --- a/tests/self_parsing/test_drift.py +++ b/tests/self_parsing/test_drift.py @@ -7,6 +7,8 @@ consciously re-negotiated in the YAML, with a comment). """ +import dataclasses + from cgis.core.models import Edge, Node from cgis.query.drift.drift import DomainConfig, DriftScorer from cgis.query.drift.fingerprint import FingerprintExtractor @@ -161,3 +163,34 @@ def test_py_quotient_drift_observed_not_enforced( f"(tolerance {binding.drift_tolerance:.2f}, tv_imports={report.tv_imports}, " f"tv_calls={report.tv_calls})\n violations: {report.violations}" ) + + +_PLANES = { + "build": (("pipeline", "extraction", "resolution", "storage"), "orchestrator"), + "read": (("storage", "query", "guardian", "cli", "api"), "layered_dag"), +} + + +def test_plane_quotients_observed_not_enforced( + root_graph_data: tuple[SQLiteStore, list[Node], list[Edge]], +) -> None: + """Score each plane's quotient against its template; print, never fail (spec: Drift gates). + + The enforced topology gate is the forbidden-edge test in test_architecture.py. + These numbers are kept for the record and for #554. + """ + store, _, _ = root_graph_data + scorer = DriftScorer(_PATTERNS) + binding = scorer.load_project_level()[0] + by_name = {d.name: d for d in _selected_domains(scorer, graph="python")} + nodes, edges = store.get_all_nodes(), store.get_all_edges() + for plane, (names, template) in _PLANES.items(): + domains = [by_name[n] for n in names] + qnodes, qedges = build_quotient(nodes, edges, domains) + extractor = FingerprintExtractor.from_graph( + qnodes, qedges, quotient_call_tally(nodes, edges, domains) + ) + fingerprint = extractor.extract(binding.fqn_prefix) + assert fingerprint.node_count == len(domains), f"{plane}: a domain matched nothing" + report = scorer.score(fingerprint, dataclasses.replace(binding, expected_pattern=template)) + print(f"\n[plane observe-only] {plane} vs {template}: drift={report.drift_score:.3f}") From 7101f35054872e46b4793c78deac262515c527b7 Mon Sep 17 00:00:00 2001 From: zaebee Date: Wed, 14 Oct 2026 08:55:14 +0000 Subject: [PATCH 6/8] fix(test): track the corpus lib/ dirs; resolve a star-import chain; dump uplift fields; cover scripts Final review of step 0: - Critical: `.gitignore`'s `lib/` swallowed three corpus files (py/lib/legacy/tool.py, ts/packages/lib/*). The snapshot was taken with them, and a fresh clone would have ingested without them and failed in CI. The corpus lib/ dirs are now un-ignored, and a test fails if git ignores any corpus file. __pycache__ is skipped when copying the corpus. - The star-import chain was recorded but never resolved: in `whole`, src layout is not stripped. py/src/shop/cli.py now calls `from shop import place`, and `shop.cli.run -> shop.service.place` (1.0) is in the py_src snapshot. - The dump now carries ontology_class, domains and confidence_score, which uplift writes and step 5 moves. - One snapshot runs in-process through main(), so --cov=scripts sees the script; a diff test covers ingest_state. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 2 + scripts/graph_identity.py | 5 +- .../corpus/py/lib/legacy/tool.py | 5 + .../graph_identity/corpus/py/src/shop/cli.py | 5 + .../corpus/ts/packages/lib/index.ts | 3 + .../corpus/ts/packages/lib/package.json | 1 + tests/fixtures/graph_identity/snapshot.json | 403 ++++++++++++++++++ tests/unit/test_graph_identity.py | 39 ++ tests/unit/test_graph_identity_dump.py | 22 + 9 files changed, 483 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/graph_identity/corpus/py/lib/legacy/tool.py create mode 100644 tests/fixtures/graph_identity/corpus/py/src/shop/cli.py create mode 100644 tests/fixtures/graph_identity/corpus/ts/packages/lib/index.ts create mode 100644 tests/fixtures/graph_identity/corpus/ts/packages/lib/package.json diff --git a/.gitignore b/.gitignore index db9294df..773fc5cc 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,8 @@ downloads/ eggs/ .eggs/ lib/ +# The graph-identity corpus has lib/ directories that are inputs, not build output. +!tests/fixtures/graph_identity/corpus/**/lib/ lib64/ parts/ sdist/ diff --git a/scripts/graph_identity.py b/scripts/graph_identity.py index d7960942..dd45346e 100644 --- a/scripts/graph_identity.py +++ b/scripts/graph_identity.py @@ -23,7 +23,8 @@ _NODE_COLUMNS = ( "id", "type", "name", "file_path", "start_line", "end_line", "language", - "namespace", "is_test", "is_generated", "metadata", + "namespace", "is_test", "is_generated", "metadata", "ontology_class", + "domains", "confidence_score", ) # fmt: skip _EDGE_COLUMNS = ( "id", "source", "target", "type", "weight", "confidence", "context", @@ -122,7 +123,7 @@ def snapshot(corpus: Path) -> dict[str, object]: """ with tempfile.TemporaryDirectory() as tmp: copy = Path(tmp) / "corpus" - shutil.copytree(corpus, copy) + shutil.copytree(corpus, copy, ignore=shutil.ignore_patterns("__pycache__")) (copy / ".git").mkdir() dumps: dict[str, object] = {} for name, relative in SNAPSHOT_ROOTS.items(): diff --git a/tests/fixtures/graph_identity/corpus/py/lib/legacy/tool.py b/tests/fixtures/graph_identity/corpus/py/lib/legacy/tool.py new file mode 100644 index 00000000..c6c807e6 --- /dev/null +++ b/tests/fixtures/graph_identity/corpus/py/lib/legacy/tool.py @@ -0,0 +1,5 @@ +from shop import place + + +def main() -> None: + place(None, "x") diff --git a/tests/fixtures/graph_identity/corpus/py/src/shop/cli.py b/tests/fixtures/graph_identity/corpus/py/src/shop/cli.py new file mode 100644 index 00000000..ed50b3d5 --- /dev/null +++ b/tests/fixtures/graph_identity/corpus/py/src/shop/cli.py @@ -0,0 +1,5 @@ +from shop import place + + +def run() -> None: + place(None, "x") diff --git a/tests/fixtures/graph_identity/corpus/ts/packages/lib/index.ts b/tests/fixtures/graph_identity/corpus/ts/packages/lib/index.ts new file mode 100644 index 00000000..6d19120b --- /dev/null +++ b/tests/fixtures/graph_identity/corpus/ts/packages/lib/index.ts @@ -0,0 +1,3 @@ +export function libFn(): number { + return 1; +} diff --git a/tests/fixtures/graph_identity/corpus/ts/packages/lib/package.json b/tests/fixtures/graph_identity/corpus/ts/packages/lib/package.json new file mode 100644 index 00000000..793f6424 --- /dev/null +++ b/tests/fixtures/graph_identity/corpus/ts/packages/lib/package.json @@ -0,0 +1 @@ +{"name": "@x/lib", "version": "1.0.0"} diff --git a/tests/fixtures/graph_identity/snapshot.json b/tests/fixtures/graph_identity/snapshot.json index 9c879a76..ff1db8c6 100644 --- a/tests/fixtures/graph_identity/snapshot.json +++ b/tests/fixtures/graph_identity/snapshot.json @@ -25,6 +25,42 @@ "type_only": 0, "weight": 1.0 }, + { + "confidence": 1.0, + "context": null, + "file_path": "shop/cli.py", + "id": "shop.cli:imports:shop", + "line_number": null, + "source": "shop.cli", + "target": "shop", + "type": "IMPORTS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "shop/cli.py", + "id": "shop.cli:imports_symbol:shop.place", + "line_number": null, + "source": "shop.cli", + "target": "shop.service.place", + "type": "IMPORTS_SYMBOL", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "shop/cli.py", + "id": "shop.cli:structural:shop.cli.run", + "line_number": null, + "source": "shop.cli", + "target": "shop.cli.run", + "type": "CONTAINS", + "type_only": 0, + "weight": 1.0 + }, { "confidence": 1.0, "context": null, @@ -289,6 +325,18 @@ "type_only": 0, "weight": 1.0 }, + { + "confidence": 1.0, + "context": "Call to place", + "file_path": "shop/cli.py", + "id": "shop/cli.py:edge_48_64", + "line_number": 5, + "source": "shop.cli.run", + "target": "shop.service.place", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, { "confidence": 1.0, "context": "DI alias for get_repo", @@ -450,6 +498,8 @@ }, "nodes": [ { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "abc", @@ -459,10 +509,13 @@ "metadata": {}, "name": "abc", "namespace": "STDLIB", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "abc.ABC", @@ -472,10 +525,13 @@ "metadata": {}, "name": "ABC", "namespace": "STDLIB", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "abc.abstractmethod", @@ -485,10 +541,13 @@ "metadata": {}, "name": "abstractmethod", "namespace": "STDLIB", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 3, "file_path": "alembic/env.py", "id": "alembic.env", @@ -502,10 +561,13 @@ }, "name": "env.py", "namespace": "INTERNAL", + "ontology_class": "File", "start_line": 1, "type": "FILE" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 2, "file_path": "alembic/env.py", "id": "alembic.env.run_migrations", @@ -515,10 +577,13 @@ "metadata": {}, "name": "run_migrations", "namespace": "INTERNAL", + "ontology_class": "Function", "start_line": 1, "type": "FUNCTION" }, { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "fastapi", @@ -528,10 +593,13 @@ "metadata": {}, "name": "fastapi", "namespace": "EXTERNAL", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "fastapi.Depends", @@ -541,10 +609,13 @@ "metadata": {}, "name": "Depends", "namespace": "EXTERNAL", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "print", @@ -554,10 +625,13 @@ "metadata": {}, "name": "print", "namespace": "STDLIB", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 2, "file_path": "shop/__init__.py", "id": "shop", @@ -573,10 +647,51 @@ }, "name": "__init__.py", "namespace": "INTERNAL", + "ontology_class": "File", + "start_line": 1, + "type": "FILE" + }, + { + "confidence_score": 1.0, + "domains": [], + "end_line": 6, + "file_path": "shop/cli.py", + "id": "shop.cli", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "import_map": { + "place": "shop.place" + }, + "reexports": {}, + "star_imports": [] + }, + "name": "cli.py", + "namespace": "INTERNAL", + "ontology_class": "File", "start_line": 1, "type": "FILE" }, { + "confidence_score": 1.0, + "domains": [], + "end_line": 5, + "file_path": "shop/cli.py", + "id": "shop.cli.run", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "run", + "namespace": "INTERNAL", + "ontology_class": "Function", + "start_line": 4, + "type": "FUNCTION" + }, + { + "confidence_score": 1.0, + "domains": [], "end_line": 13, "file_path": "shop/deps.py", "id": "shop.deps", @@ -595,10 +710,13 @@ }, "name": "deps.py", "namespace": "INTERNAL", + "ontology_class": "File", "start_line": 1, "type": "FILE" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 12, "file_path": "shop/deps.py", "id": "shop.deps.RepoDep", @@ -610,10 +728,13 @@ }, "name": "RepoDep", "namespace": "INTERNAL", + "ontology_class": "Variable", "start_line": 12, "type": "VARIABLE" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 9, "file_path": "shop/deps.py", "id": "shop.deps.get_repo", @@ -623,10 +744,13 @@ "metadata": {}, "name": "get_repo", "namespace": "INTERNAL", + "ontology_class": "Function", "start_line": 8, "type": "FUNCTION" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 12, "file_path": "shop/repo.py", "id": "shop.repo", @@ -643,10 +767,13 @@ }, "name": "repo.py", "namespace": "INTERNAL", + "ontology_class": "File", "start_line": 1, "type": "FILE" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 6, "file_path": "shop/repo.py", "id": "shop.repo.Repo", @@ -658,10 +785,13 @@ }, "name": "Repo", "namespace": "INTERNAL", + "ontology_class": "Class", "start_line": 4, "type": "CLASS" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 6, "file_path": "shop/repo.py", "id": "shop.repo.Repo.save", @@ -680,10 +810,13 @@ }, "name": "save", "namespace": "INTERNAL", + "ontology_class": "Method", "start_line": 6, "type": "METHOD" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 11, "file_path": "shop/repo.py", "id": "shop.repo.SqlRepo", @@ -693,10 +826,13 @@ "metadata": {}, "name": "SqlRepo", "namespace": "INTERNAL", + "ontology_class": "Class", "start_line": 9, "type": "CLASS" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 11, "file_path": "shop/repo.py", "id": "shop.repo.SqlRepo.save", @@ -711,10 +847,13 @@ }, "name": "save", "namespace": "INTERNAL", + "ontology_class": "Method", "start_line": 10, "type": "METHOD" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 17, "file_path": "shop/service.py", "id": "shop.service", @@ -731,10 +870,13 @@ }, "name": "service.py", "namespace": "INTERNAL", + "ontology_class": "File", "start_line": 1, "type": "FILE" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 12, "file_path": "shop/service.py", "id": "shop.service.OrderService", @@ -748,10 +890,13 @@ }, "name": "OrderService", "namespace": "INTERNAL", + "ontology_class": "Class", "start_line": 7, "type": "CLASS" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 9, "file_path": "shop/service.py", "id": "shop.service.OrderService.__init__", @@ -768,10 +913,13 @@ }, "name": "__init__", "namespace": "INTERNAL", + "ontology_class": "Method", "start_line": 8, "type": "METHOD" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 12, "file_path": "shop/service.py", "id": "shop.service.OrderService.place", @@ -788,10 +936,13 @@ }, "name": "place", "namespace": "INTERNAL", + "ontology_class": "Method", "start_line": 11, "type": "METHOD" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 16, "file_path": "shop/service.py", "id": "shop.service.place", @@ -806,10 +957,13 @@ }, "name": "place", "namespace": "INTERNAL", + "ontology_class": "Function", "start_line": 15, "type": "FUNCTION" }, { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "typing", @@ -819,6 +973,7 @@ "metadata": {}, "name": "typing", "namespace": "STDLIB", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" } @@ -1041,6 +1196,8 @@ }, "nodes": [ { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "@x.lib", @@ -1050,10 +1207,13 @@ "metadata": {}, "name": "lib", "namespace": "UNKNOWN", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "fn", @@ -1063,10 +1223,13 @@ "metadata": {}, "name": "fn", "namespace": "UNKNOWN", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "libFn", @@ -1076,10 +1239,13 @@ "metadata": {}, "name": "libFn", "namespace": "UNKNOWN", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "lodash.debounce", @@ -1089,10 +1255,13 @@ "metadata": {}, "name": "debounce", "namespace": "EXTERNAL", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 17, "file_path": "main.ts", "id": "main", @@ -1120,10 +1289,13 @@ }, "name": "main.ts", "namespace": "INTERNAL", + "ontology_class": "File", "start_line": 1, "type": "FILE" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 16, "file_path": "main.ts", "id": "main.main", @@ -1133,10 +1305,13 @@ "metadata": {}, "name": "main", "namespace": "INTERNAL", + "ontology_class": "Function", "start_line": 11, "type": "FUNCTION" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 9, "file_path": "main.ts", "id": "main.setTimeout", @@ -1146,10 +1321,13 @@ "metadata": {}, "name": "setTimeout", "namespace": "INTERNAL", + "ontology_class": "Function", "start_line": 7, "type": "FUNCTION" }, { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "node:crypto", @@ -1159,10 +1337,13 @@ "metadata": {}, "name": "node:crypto", "namespace": "STDLIB", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "node:crypto.randomUUID", @@ -1172,10 +1353,13 @@ "metadata": {}, "name": "randomUUID", "namespace": "STDLIB", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "react", @@ -1185,10 +1369,13 @@ "metadata": {}, "name": "react", "namespace": "EXTERNAL", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "react.useState", @@ -1198,10 +1385,13 @@ "metadata": {}, "name": "useState", "namespace": "EXTERNAL", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 4, "file_path": "types.d.ts", "id": "types.d", @@ -1211,10 +1401,13 @@ "metadata": {}, "name": "types.d.ts", "namespace": "INTERNAL", + "ontology_class": "File", "start_line": 1, "type": "FILE" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 4, "file_path": "util.ts", "id": "util", @@ -1224,10 +1417,13 @@ "metadata": {}, "name": "util.ts", "namespace": "INTERNAL", + "ontology_class": "File", "start_line": 1, "type": "FILE" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 3, "file_path": "util.ts", "id": "util.helper", @@ -1237,6 +1433,7 @@ "metadata": {}, "name": "helper", "namespace": "INTERNAL", + "ontology_class": "Function", "start_line": 1, "type": "FUNCTION" } @@ -1292,6 +1489,30 @@ "type_only": 0, "weight": 1.0 }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/src/shop/cli.py", + "id": "py.src.shop.cli:imports:shop", + "line_number": null, + "source": "py.src.shop.cli", + "target": "shop", + "type": "IMPORTS", + "type_only": 0, + "weight": 1.0 + }, + { + "confidence": 1.0, + "context": null, + "file_path": "py/src/shop/cli.py", + "id": "py.src.shop.cli:structural:py.src.shop.cli.run", + "line_number": null, + "source": "py.src.shop.cli", + "target": "py.src.shop.cli.run", + "type": "CONTAINS", + "type_only": 0, + "weight": 1.0 + }, { "confidence": 1.0, "context": null, @@ -1616,6 +1837,18 @@ "type_only": 0, "weight": 1.0 }, + { + "confidence": 1.0, + "context": "Call to place", + "file_path": "py/src/shop/cli.py", + "id": "py/src/shop/cli.py:edge_48_64", + "line_number": 5, + "source": "py.src.shop.cli.run", + "target": "shop.place", + "type": "CALLS", + "type_only": 0, + "weight": 1.0 + }, { "confidence": 1.0, "context": "DI alias for get_repo", @@ -1991,6 +2224,8 @@ }, "nodes": [ { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "abc", @@ -2000,10 +2235,13 @@ "metadata": {}, "name": "abc", "namespace": "STDLIB", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "abc.ABC", @@ -2013,10 +2251,13 @@ "metadata": {}, "name": "ABC", "namespace": "STDLIB", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "abc.abstractmethod", @@ -2026,10 +2267,13 @@ "metadata": {}, "name": "abstractmethod", "namespace": "STDLIB", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "fastapi", @@ -2039,10 +2283,13 @@ "metadata": {}, "name": "fastapi", "namespace": "EXTERNAL", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "fastapi.Depends", @@ -2052,10 +2299,13 @@ "metadata": {}, "name": "Depends", "namespace": "EXTERNAL", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "fn", @@ -2065,10 +2315,13 @@ "metadata": {}, "name": "fn", "namespace": "UNKNOWN", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "lodash.debounce", @@ -2078,10 +2331,13 @@ "metadata": {}, "name": "debounce", "namespace": "EXTERNAL", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "node:crypto", @@ -2091,10 +2347,13 @@ "metadata": {}, "name": "node:crypto", "namespace": "STDLIB", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "node:crypto.randomUUID", @@ -2104,10 +2363,13 @@ "metadata": {}, "name": "randomUUID", "namespace": "STDLIB", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "print", @@ -2117,10 +2379,13 @@ "metadata": {}, "name": "print", "namespace": "STDLIB", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 6, "file_path": "py/lib/legacy/tool.py", "id": "py.lib.legacy.tool", @@ -2136,10 +2401,13 @@ }, "name": "tool.py", "namespace": "INTERNAL", + "ontology_class": "File", "start_line": 1, "type": "FILE" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 5, "file_path": "py/lib/legacy/tool.py", "id": "py.lib.legacy.tool.main", @@ -2149,10 +2417,13 @@ "metadata": {}, "name": "main", "namespace": "INTERNAL", + "ontology_class": "Function", "start_line": 4, "type": "FUNCTION" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 3, "file_path": "py/src/alembic/env.py", "id": "py.src.alembic.env", @@ -2166,10 +2437,13 @@ }, "name": "env.py", "namespace": "INTERNAL", + "ontology_class": "File", "start_line": 1, "type": "FILE" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 2, "file_path": "py/src/alembic/env.py", "id": "py.src.alembic.env.run_migrations", @@ -2179,10 +2453,13 @@ "metadata": {}, "name": "run_migrations", "namespace": "INTERNAL", + "ontology_class": "Function", "start_line": 1, "type": "FUNCTION" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 2, "file_path": "py/src/shop/__init__.py", "id": "py.src.shop", @@ -2198,10 +2475,51 @@ }, "name": "__init__.py", "namespace": "INTERNAL", + "ontology_class": "File", + "start_line": 1, + "type": "FILE" + }, + { + "confidence_score": 1.0, + "domains": [], + "end_line": 6, + "file_path": "py/src/shop/cli.py", + "id": "py.src.shop.cli", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": { + "import_map": { + "place": "shop.place" + }, + "reexports": {}, + "star_imports": [] + }, + "name": "cli.py", + "namespace": "INTERNAL", + "ontology_class": "File", "start_line": 1, "type": "FILE" }, { + "confidence_score": 1.0, + "domains": [], + "end_line": 5, + "file_path": "py/src/shop/cli.py", + "id": "py.src.shop.cli.run", + "is_generated": 0, + "is_test": 0, + "language": "python", + "metadata": {}, + "name": "run", + "namespace": "INTERNAL", + "ontology_class": "Function", + "start_line": 4, + "type": "FUNCTION" + }, + { + "confidence_score": 1.0, + "domains": [], "end_line": 13, "file_path": "py/src/shop/deps.py", "id": "py.src.shop.deps", @@ -2220,10 +2538,13 @@ }, "name": "deps.py", "namespace": "INTERNAL", + "ontology_class": "File", "start_line": 1, "type": "FILE" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 12, "file_path": "py/src/shop/deps.py", "id": "py.src.shop.deps.RepoDep", @@ -2235,10 +2556,13 @@ }, "name": "RepoDep", "namespace": "INTERNAL", + "ontology_class": "Variable", "start_line": 12, "type": "VARIABLE" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 9, "file_path": "py/src/shop/deps.py", "id": "py.src.shop.deps.get_repo", @@ -2248,10 +2572,13 @@ "metadata": {}, "name": "get_repo", "namespace": "INTERNAL", + "ontology_class": "Function", "start_line": 8, "type": "FUNCTION" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 12, "file_path": "py/src/shop/repo.py", "id": "py.src.shop.repo", @@ -2268,10 +2595,13 @@ }, "name": "repo.py", "namespace": "INTERNAL", + "ontology_class": "File", "start_line": 1, "type": "FILE" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 6, "file_path": "py/src/shop/repo.py", "id": "py.src.shop.repo.Repo", @@ -2283,10 +2613,13 @@ }, "name": "Repo", "namespace": "INTERNAL", + "ontology_class": "Class", "start_line": 4, "type": "CLASS" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 6, "file_path": "py/src/shop/repo.py", "id": "py.src.shop.repo.Repo.save", @@ -2305,10 +2638,13 @@ }, "name": "save", "namespace": "INTERNAL", + "ontology_class": "Method", "start_line": 6, "type": "METHOD" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 11, "file_path": "py/src/shop/repo.py", "id": "py.src.shop.repo.SqlRepo", @@ -2318,10 +2654,13 @@ "metadata": {}, "name": "SqlRepo", "namespace": "INTERNAL", + "ontology_class": "Class", "start_line": 9, "type": "CLASS" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 11, "file_path": "py/src/shop/repo.py", "id": "py.src.shop.repo.SqlRepo.save", @@ -2336,10 +2675,13 @@ }, "name": "save", "namespace": "INTERNAL", + "ontology_class": "Method", "start_line": 10, "type": "METHOD" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 17, "file_path": "py/src/shop/service.py", "id": "py.src.shop.service", @@ -2356,10 +2698,13 @@ }, "name": "service.py", "namespace": "INTERNAL", + "ontology_class": "File", "start_line": 1, "type": "FILE" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 12, "file_path": "py/src/shop/service.py", "id": "py.src.shop.service.OrderService", @@ -2373,10 +2718,13 @@ }, "name": "OrderService", "namespace": "INTERNAL", + "ontology_class": "Class", "start_line": 7, "type": "CLASS" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 9, "file_path": "py/src/shop/service.py", "id": "py.src.shop.service.OrderService.__init__", @@ -2393,10 +2741,13 @@ }, "name": "__init__", "namespace": "INTERNAL", + "ontology_class": "Method", "start_line": 8, "type": "METHOD" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 12, "file_path": "py/src/shop/service.py", "id": "py.src.shop.service.OrderService.place", @@ -2413,10 +2764,13 @@ }, "name": "place", "namespace": "INTERNAL", + "ontology_class": "Method", "start_line": 11, "type": "METHOD" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 16, "file_path": "py/src/shop/service.py", "id": "py.src.shop.service.place", @@ -2431,10 +2785,13 @@ }, "name": "place", "namespace": "INTERNAL", + "ontology_class": "Function", "start_line": 15, "type": "FUNCTION" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 6, "file_path": "py/tests/test_service.py", "id": "py.tests.test_service", @@ -2450,10 +2807,13 @@ }, "name": "test_service.py", "namespace": "INTERNAL", + "ontology_class": "File", "start_line": 1, "type": "FILE" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 5, "file_path": "py/tests/test_service.py", "id": "py.tests.test_service.test_place", @@ -2463,10 +2823,13 @@ "metadata": {}, "name": "test_place", "namespace": "INTERNAL", + "ontology_class": "Function", "start_line": 4, "type": "FUNCTION" }, { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "react", @@ -2476,10 +2839,13 @@ "metadata": {}, "name": "react", "namespace": "EXTERNAL", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "react.useState", @@ -2489,10 +2855,13 @@ "metadata": {}, "name": "useState", "namespace": "EXTERNAL", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "shop", @@ -2502,10 +2871,13 @@ "metadata": {}, "name": "shop", "namespace": "EXTERNAL", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "shop.place", @@ -2515,10 +2887,13 @@ "metadata": {}, "name": "place", "namespace": "EXTERNAL", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "shop.service", @@ -2528,10 +2903,13 @@ "metadata": {}, "name": "service", "namespace": "EXTERNAL", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 17, "file_path": "ts/apps/web/src/main.ts", "id": "ts.apps.web.src.main", @@ -2559,10 +2937,13 @@ }, "name": "main.ts", "namespace": "INTERNAL", + "ontology_class": "File", "start_line": 1, "type": "FILE" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 16, "file_path": "ts/apps/web/src/main.ts", "id": "ts.apps.web.src.main.main", @@ -2572,10 +2953,13 @@ "metadata": {}, "name": "main", "namespace": "INTERNAL", + "ontology_class": "Function", "start_line": 11, "type": "FUNCTION" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 9, "file_path": "ts/apps/web/src/main.ts", "id": "ts.apps.web.src.main.setTimeout", @@ -2585,10 +2969,13 @@ "metadata": {}, "name": "setTimeout", "namespace": "INTERNAL", + "ontology_class": "Function", "start_line": 7, "type": "FUNCTION" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 4, "file_path": "ts/apps/web/src/types.d.ts", "id": "ts.apps.web.src.types.d", @@ -2598,10 +2985,13 @@ "metadata": {}, "name": "types.d.ts", "namespace": "INTERNAL", + "ontology_class": "File", "start_line": 1, "type": "FILE" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 4, "file_path": "ts/apps/web/src/util.ts", "id": "ts.apps.web.src.util", @@ -2611,10 +3001,13 @@ "metadata": {}, "name": "util.ts", "namespace": "INTERNAL", + "ontology_class": "File", "start_line": 1, "type": "FILE" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 3, "file_path": "ts/apps/web/src/util.ts", "id": "ts.apps.web.src.util.helper", @@ -2624,10 +3017,13 @@ "metadata": {}, "name": "helper", "namespace": "INTERNAL", + "ontology_class": "Function", "start_line": 1, "type": "FUNCTION" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 4, "file_path": "ts/packages/lib/index.ts", "id": "ts.packages.lib", @@ -2637,10 +3033,13 @@ "metadata": {}, "name": "index.ts", "namespace": "INTERNAL", + "ontology_class": "File", "start_line": 1, "type": "FILE" }, { + "confidence_score": 1.0, + "domains": [], "end_line": 3, "file_path": "ts/packages/lib/index.ts", "id": "ts.packages.lib.libFn", @@ -2650,10 +3049,13 @@ "metadata": {}, "name": "libFn", "namespace": "INTERNAL", + "ontology_class": "Function", "start_line": 1, "type": "FUNCTION" }, { + "confidence_score": 0.8, + "domains": [], "end_line": 0, "file_path": "EXTERNAL", "id": "typing", @@ -2663,6 +3065,7 @@ "metadata": {}, "name": "typing", "namespace": "STDLIB", + "ontology_class": "Function", "start_line": 0, "type": "FUNCTION" } diff --git a/tests/unit/test_graph_identity.py b/tests/unit/test_graph_identity.py index 010bb5de..4f22b329 100644 --- a/tests/unit/test_graph_identity.py +++ b/tests/unit/test_graph_identity.py @@ -50,3 +50,42 @@ def test_the_corpus_graph_matches_the_snapshot(seed: str, tmp_path: Path) -> Non for root in gi.SNAPSHOT_ROOTS: lines = gi.diff(recorded[root], actual[root]) assert not lines, f"{root} graph changed (PYTHONHASHSEED={seed}):\n" + "\n".join(lines[:40]) + + +def test_every_corpus_file_is_tracked_by_git() -> None: + """A corpus file git ignores exists only on the machine that made the snapshot. + + `.gitignore`'s `lib/` swallowed three corpus files: the snapshot was taken with + them and every fresh clone ingested without them (step-0 review). + """ + files = [ + str(p.relative_to(REPO)) + for p in CORPUS.rglob("*") + if p.is_file() and "__pycache__" not in p.parts + ] + ignored = subprocess.run( + ["git", "check-ignore", *files], cwd=REPO, capture_output=True, text=True, check=False + ).stdout.split() + assert not ignored, f"git ignores corpus files: {ignored}" + + +def test_the_snapshot_resolves_a_star_import_chain() -> None: + """`from shop import place` reaches `shop.service.place` through `shop/__init__`'s star import. + + In the `whole` root the src layout is not stripped, so the chain resolves only + under `py_src`; it must be in the snapshot, since steps 3-4 move that code. + """ + recorded = json.loads(SNAPSHOT.read_text(encoding="utf-8")) + calls = { + (e["source"], e["target"]) for e in recorded["py_src"]["edges"] if e["type"] == "CALLS" + } + assert ("shop.cli.run", "shop.service.place") in calls + + +def test_the_snapshot_command_runs_in_process(tmp_path: Path) -> None: + """The same snapshot through `main`, so coverage sees the script (`--cov=scripts`).""" + out = tmp_path / "snapshot.json" + assert gi.main(["snapshot", str(CORPUS), "--out", str(out)]) == 0 + recorded = json.loads(SNAPSHOT.read_text(encoding="utf-8")) + actual = json.loads(out.read_text(encoding="utf-8")) + assert all(gi.diff(recorded[r], actual[r]) == [] for r in gi.SNAPSHOT_ROOTS) diff --git a/tests/unit/test_graph_identity_dump.py b/tests/unit/test_graph_identity_dump.py index 950b0feb..e175110d 100644 --- a/tests/unit/test_graph_identity_dump.py +++ b/tests/unit/test_graph_identity_dump.py @@ -71,3 +71,25 @@ def test_diff_names_a_changed_edge(tmp_path: Path) -> None: assert lines, "a changed confidence must show" assert all(line.startswith("edges ") for line in lines), "only edge rows changed" assert any("pkg.a.f" in line and "0.5" in line for line in lines) + + +def test_dump_carries_what_uplift_writes(tmp_path: Path) -> None: + """Uplift sets ontology_class and domains; step 5 moves it (step-0 review).""" + root = _repo(tmp_path / "r") + _ingest(root, tmp_path / "g.db") + node = gi.dump(tmp_path / "g.db", root)["nodes"][0] + + assert {"ontology_class", "domains", "confidence_score"} <= set(node) + + +def test_diff_names_a_changed_ingest_state(tmp_path: Path) -> None: + root = _repo(tmp_path / "r") + _ingest(root, tmp_path / "g.db") + before = gi.dump(tmp_path / "g.db", root) + with sqlite3.connect(tmp_path / "g.db") as conn: + conn.execute("UPDATE ingest_state SET value = '[\"x\"]' WHERE key = 'npm_dependencies'") + after = gi.dump(tmp_path / "g.db", root) + + assert gi.diff(before, after) == [ + f"ingest_state: {before['ingest_state']} → {after['ingest_state']}" + ] From fbfc240b8af15fb00a386dbc4f6e65b10c121a09 Mon Sep 17 00:00:00 2001 From: zaebee Date: Wed, 14 Oct 2026 09:06:19 +0000 Subject: [PATCH 7/8] fix(graph-identity): validate the db path, normalise escaped roots, exclude fixtures from Sonar - dump() refuses a missing or non-.db/.sqlite/.sqlite3 path instead of letting sqlite3.connect create an empty file (Gemini; pythonsecurity:S8706) - _normalise passes the root into parsed JSON, so a Windows root escaped as C:\\x inside JSON text is still replaced (Gemini) - snapshot subprocess failures surface stderr; write_text pins utf-8 (Gemini) - ci: tests/fixtures/** excluded from Sonar (corpus package.json, text:S8564); S8706 exempted for scripts/ on the S2083 reasoning, with rationale Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 18 +++++++++++++++- scripts/graph_identity.py | 15 +++++++++++-- tests/unit/test_graph_identity.py | 6 ++++-- tests/unit/test_graph_identity_dump.py | 29 +++++++++++++++++++++++++- 4 files changed, 62 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6fba7e15..42a3ea98 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -162,6 +162,18 @@ jobs: # this exemption hides it — that is the cost, and it is the reason the # rule stays on for src/. # + # pythonsecurity:S8706 (connection-string injection) is the same + # finding at sqlite3.connect: graph_identity.py opens a database named + # on its own command line. `dump()` requires an existing .db/.sqlite/ + # .sqlite3 regular file before connecting, and passes no `uri=True`, so + # the argument is a file name and never a connection string. Exempted + # for scripts/ on the S2083 reasoning above — the engine does not model + # the check — and kept on for src/. + # + # tests/fixtures/ is excluded outright: it holds input corpora, not + # code. Its package.json files exist to be parsed by cgis, and a lock + # file next to them (text:S8564) would be a fixture nothing reads. + # # `sonar.newCode.referenceBranch` overrides the server-side new-code # definition for this analysis. The instance default is # `previous_version` (confirmed: sonar.leak.period=previous_version, @@ -218,13 +230,17 @@ jobs: -Dsonar.newCode.referenceBranch=main -Dsonar.sources=src,scripts,.github -Dsonar.tests=tests + -Dsonar.exclusions=tests/fixtures/** + -Dsonar.test.exclusions=tests/fixtures/** -Dsonar.python.version=3.12 -Dsonar.python.coverage.reportPaths=coverage.xml - -Dsonar.issue.ignore.multicriteria=e1,e2,e3,e4 + -Dsonar.issue.ignore.multicriteria=e1,e2,e3,e4,e5 -Dsonar.issue.ignore.multicriteria.e1.ruleKey=pythonsecurity:S8707 -Dsonar.issue.ignore.multicriteria.e1.resourceKey=scripts/**/*.py -Dsonar.issue.ignore.multicriteria.e4.ruleKey=pythonsecurity:S2083 -Dsonar.issue.ignore.multicriteria.e4.resourceKey=scripts/**/*.py + -Dsonar.issue.ignore.multicriteria.e5.ruleKey=pythonsecurity:S8706 + -Dsonar.issue.ignore.multicriteria.e5.resourceKey=scripts/**/*.py -Dsonar.issue.ignore.multicriteria.e2.ruleKey=githubactions:S8541 -Dsonar.issue.ignore.multicriteria.e2.resourceKey=.github/workflows/*.yml -Dsonar.issue.ignore.multicriteria.e3.ruleKey=githubactions:S8544 diff --git a/scripts/graph_identity.py b/scripts/graph_identity.py index dd45346e..e21a15fc 100644 --- a/scripts/graph_identity.py +++ b/scripts/graph_identity.py @@ -31,6 +31,7 @@ "file_path", "line_number", "type_only", ) # fmt: skip _VOLATILE_STATE = frozenset({"ingested_at"}) +_DB_SUFFIXES = frozenset({".db", ".sqlite", ".sqlite3"}) #: The corpus roots a snapshot ingests, by name. `sub` reads configs above its #: root (#555); `py_src` is the src layout, where the local `alembic` package @@ -44,7 +45,7 @@ def _normalise(value: object, root: str) -> object: text = value.replace(root, "") if text[:1] in "[{": try: - return _normalise(json.loads(text), "\0") + return _normalise(json.loads(text), root) except ValueError: return text return text @@ -67,7 +68,17 @@ def _rows( def dump(db_path: Path, root: Path) -> dict[str, object]: - """The canonical content of one stored graph, with `root` normalised away.""" + """The canonical content of one stored graph, with `root` normalised away. + + `db_path` must be an existing database file: `sqlite3.connect` would create + a missing one, and its URI-free path keeps the argument a plain file name. + """ + if db_path.suffix not in _DB_SUFFIXES: + msg = f"not a graph database (expected {sorted(_DB_SUFFIXES)}): {db_path}" + raise ValueError(msg) + if not db_path.is_file(): + msg = f"no graph database at {db_path}" + raise FileNotFoundError(msg) prefix = str(root.resolve()) with sqlite3.connect(db_path) as conn: state = { diff --git a/tests/unit/test_graph_identity.py b/tests/unit/test_graph_identity.py index 4f22b329..6b9f9591 100644 --- a/tests/unit/test_graph_identity.py +++ b/tests/unit/test_graph_identity.py @@ -31,12 +31,14 @@ def _snapshot(seed: str, out: Path) -> dict[str, dict[str, object]]: """ env = {**os.environ, "PYTHONHASHSEED": seed} script = str(REPO / "scripts" / "graph_identity.py") - subprocess.run( + result = subprocess.run( [sys.executable, script, "snapshot", str(CORPUS), "--out", str(out)], env=env, capture_output=True, - check=True, + text=True, + check=False, ) + assert result.returncode == 0, f"snapshot failed (PYTHONHASHSEED={seed}):\n{result.stderr}" snapshot: dict[str, dict[str, object]] = json.loads(out.read_text(encoding="utf-8")) return snapshot diff --git a/tests/unit/test_graph_identity_dump.py b/tests/unit/test_graph_identity_dump.py index e175110d..35d7fe08 100644 --- a/tests/unit/test_graph_identity_dump.py +++ b/tests/unit/test_graph_identity_dump.py @@ -5,6 +5,8 @@ import sys from pathlib import Path +import pytest + REPO = Path(__file__).resolve().parent.parent.parent sys.path.insert(0, str(REPO / "scripts")) @@ -26,7 +28,9 @@ def _ingest(root: Path, db: Path) -> None: def _repo(root: Path) -> Path: (root / "pkg").mkdir(parents=True) - (root / "pkg" / "a.py").write_text("def f():\n g()\n\ndef g():\n pass\n") + (root / "pkg" / "a.py").write_text( + "def f():\n g()\n\ndef g():\n pass\n", encoding="utf-8" + ) return root @@ -93,3 +97,26 @@ def test_diff_names_a_changed_ingest_state(tmp_path: Path) -> None: assert gi.diff(before, after) == [ f"ingest_state: {before['ingest_state']} → {after['ingest_state']}" ] + + +def test_dump_refuses_a_missing_database(tmp_path: Path) -> None: + """sqlite3.connect would create an empty file and fail later on a missing table.""" + missing = tmp_path / "nope.db" + with pytest.raises(FileNotFoundError, match=r"nope\.db"): + gi.dump(missing, tmp_path) + assert not missing.exists() + + +def test_dump_refuses_a_file_that_is_not_a_database(tmp_path: Path) -> None: + """Only a .db/.sqlite/.sqlite3 file is opened — the argument is never a connection string.""" + other = tmp_path / "notes.txt" + other.write_text("x", encoding="utf-8") + with pytest.raises(ValueError, match=r"notes\.txt"): + gi.dump(other, tmp_path) + + +def test_normalise_reaches_a_root_escaped_inside_json_text() -> None: + """A Windows root is `C:\\x` in the column but `C:\\\\x` in JSON text.""" + text = json.dumps({"p": "C:\\x\\f.py"}) + normalised = gi._normalise(text, "C:\\x") # noqa: SLF001 + assert normalised == {"p": "\\f.py"} From d4c66b27d66725fb20ff0b964ea5a4dd74a2b9f6 Mon Sep 17 00:00:00 2001 From: zaebee Date: Wed, 14 Oct 2026 09:18:03 +0000 Subject: [PATCH 8/8] fix(graph-identity): close the sqlite connection in dump() `with sqlite3.connect()` only manages the transaction; the connection stayed open, which locks the file on Windows (Gemini). Co-Authored-By: Claude Opus 5.5 --- scripts/graph_identity.py | 3 ++- tests/unit/test_graph_identity_dump.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/scripts/graph_identity.py b/scripts/graph_identity.py index e21a15fc..04089412 100644 --- a/scripts/graph_identity.py +++ b/scripts/graph_identity.py @@ -14,6 +14,7 @@ import sys import tempfile from collections.abc import Mapping +from contextlib import closing from pathlib import Path from typing import cast @@ -80,7 +81,7 @@ def dump(db_path: Path, root: Path) -> dict[str, object]: msg = f"no graph database at {db_path}" raise FileNotFoundError(msg) prefix = str(root.resolve()) - with sqlite3.connect(db_path) as conn: + with closing(sqlite3.connect(db_path)) as conn: # `with conn` alone never closes state = { key: _normalise(value, prefix) for key, value in conn.execute("SELECT key, value FROM ingest_state ORDER BY key") diff --git a/tests/unit/test_graph_identity_dump.py b/tests/unit/test_graph_identity_dump.py index 35d7fe08..459c90d5 100644 --- a/tests/unit/test_graph_identity_dump.py +++ b/tests/unit/test_graph_identity_dump.py @@ -120,3 +120,22 @@ def test_normalise_reaches_a_root_escaped_inside_json_text() -> None: text = json.dumps({"p": "C:\\x\\f.py"}) normalised = gi._normalise(text, "C:\\x") # noqa: SLF001 assert normalised == {"p": "\\f.py"} + + +def test_dump_closes_its_connection(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """`with sqlite3.connect(...)` only commits; the file stays open (and locked on Windows).""" + root = _repo(tmp_path) + _ingest(root, tmp_path / "g.db") + opened: list[sqlite3.Connection] = [] + real_connect = sqlite3.connect + + def recording_connect(path: Path) -> sqlite3.Connection: + conn = real_connect(path) + opened.append(conn) + return conn + + monkeypatch.setattr(gi.sqlite3, "connect", recording_connect) + gi.dump(tmp_path / "g.db", root) + assert len(opened) == 1 + with pytest.raises(sqlite3.ProgrammingError, match="closed"): + opened[0].execute("SELECT 1")