Repository navigation
test: language-packs step 0 — identity net, forbidden-edge gate, plane reports - #596
Conversation
…-packs identity net scripts/graph_identity.py dumps nodes, edges and ingest_state with the corpus root normalised to <ROOT> 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 <noreply@anthropic.com>
…ar/layouts, TS monorepo/pnpm/tsconfig/node: 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 <noreply@anthropic.com>
`scripts/graph_identity.py snapshot <corpus>` 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 <noreply@anthropic.com>
…ge-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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…ump 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 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a graph identity testing framework to ensure that code migrations do not alter graph ingestion behavior. It adds a test corpus, a script to dump and diff SQLite graphs, and tests enforcing architectural invariants like forbidden edges and drift scoring. The review feedback highlights several robust improvements: correcting path normalization on Windows within recursive JSON parsing, explicitly verifying database file existence before connecting, capturing and printing subprocess stderr for better CI debugging, and specifying UTF-8 encoding when writing files to prevent platform-dependent test failures.
…xclude 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 <noreply@anthropic.com>
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a graph identity snapshotting tool to ensure consistent ingestion behavior across environments, along with new architecture tests for forbidden edges and drift monitoring. The review identified a resource leak in sqlite3.connect usage, a potential failure point in git-related tests when the git executable is missing, and a redundant database query in the drift tests. All three comments provide actionable improvements and have been retained.
| 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, | ||
| } |
There was a problem hiding this comment.
In Python, using sqlite3.connect as a context manager only manages transactions (committing on success, rolling back on exception); it does not close the connection. This can lead to unclosed database connections, file locks, and resource leaks, which is especially problematic on Windows when trying to clean up temporary directories. Use a try...finally block to ensure the connection is explicitly closed.
| 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, | |
| } | |
| conn = sqlite3.connect(db_path) | |
| try: | |
| 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, | |
| } | |
| finally: | |
| conn.close() |
There was a problem hiding this comment.
Adopted in d4c66b2, as with closing(sqlite3.connect(db_path)) as conn — same guarantee as try/finally, one line. Pinned by test_dump_closes_its_connection (records the connection, asserts it is closed after dump()); it failed before the change with DID NOT RAISE.
| ignored = subprocess.run( | ||
| ["git", "check-ignore", *files], cwd=REPO, capture_output=True, text=True, check=False | ||
| ).stdout.split() |
There was a problem hiding this comment.
Running git check-ignore via subprocess.run can raise a FileNotFoundError if the git executable is not installed in the environment (e.g., in minimal CI containers, Nix builds, or zip deployments). Additionally, if the tests are run outside of a git repository, the command will fail. It is safer to check if the .git directory exists and handle FileNotFoundError gracefully by skipping the test.
if not (REPO / '.git').exists():
pytest.skip('Not a git repository')
try:
result = subprocess.run(
['git', 'check-ignore', *files], cwd=REPO, capture_output=True, text=True, check=False
)
except FileNotFoundError:
pytest.skip('git executable not found')
ignored = result.stdout.split()There was a problem hiding this comment.
Declining, deliberately. This test exists because .gitignore's lib/ silently dropped three corpus files: the snapshot was taken with them and every fresh clone ingested without them. A skip when git is absent turns that guard into one that passes by not running — exactly the failure it was written to catch. git is already a hard dependency of this suite (seven other test files call it unconditionally), CI always has it, and a missing git should fail loudly here, not vanish into the skipped column.
| 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() |
There was a problem hiding this comment.
The root_graph_data fixture already provides nodes and edges as the second and third elements of the tuple. Unpacking only store and then querying the database again with store.get_all_nodes() and store.get_all_edges() is redundant and inefficient. We can directly unpack nodes and edges from the fixture.
| 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() | |
| store, nodes, edges = 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')} |
There was a problem hiding this comment.
Declining — the two are not the same input. Measured on this branch: the fixture tuple carries 8726 edges, the store 8715 (same node set, same distinct edge set; the store drops 11 duplicates). quotient_call_tally counts edges, so the swap would change the observed numbers. Reading back from the store also matches the gating quotient test above (line 149), so the planes and the domain quotient are measured on the same graph.
`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 <noreply@anthropic.com>
|



Step 0 of
docs/plans/2026-10-14-language-packs.md(specdocs/specs/2026-10-14-language-packs-design.md, #146). Tests and tooling only. Nosrc/change.What lands
scripts/graph_identity.py: a canonical dump of a stored graph and a diff of two dumps.ontology_classanddomains; every edge column; andingest_state.<ROOT>, and the mtime-derivedingested_atis dropped.snapshotingests three roots of a temp copy of the corpus, with a.gitmarker, the waycgis ingestdoes.tests/fixtures/graph_identity/corpus/(18 files). Each case was checked in the graph before committing:src/andlib/layouts, thealembiccollision;node:imports,.d.ts, a shadowed global, configs above the root.test_graph_identity.py: the snapshot is diffed underPYTHONHASHSEED0 and 1, plus one run in-process for coverage. Renaming one corpus function fails it and names the moved node and edge. A snapshot taken from a freshgit archivematches with 0 diff rows.Forbidden-edge gate (
test_architecture.py, Invariant 7): six rules, compared for equality withforbidden_edges_allowlist.json. Today:uplift → sqlite_storemcp_server/cli→ extractor classescli/api/guardian→storagePlanted edges fire every rule.
Plane quotients, observe-only: build vs
orchestrator0.238, read vslayered_dag0.676. These are the spec's numbers.Rulings made while executing
root. The test helper and the script recordrootand options ascgis ingestdoes.pipeline.runalone records no root, which would leave nothing to normalise.--out. The pipeline's structlog output goes to stdout, so the snapshot is read from the--outfile instead.py/srcis added because the whole-corpus ingest never strips the src layout.tests/fixturesexcluded from pytest collection and ruff. The corpus holds a test file, and a reformat would change the snapshot's input.core/paths.pyis circular and errors before the gate runs.Fixed after the final (Fable) review
.gitignore'slib/ignored three corpus files. The snapshot had been made with files a clone lacks. They are now un-ignored, a test fails if git ignores any corpus file, and the fresh-archive check is clean.py_src;scripts/graph_identity.pycoverage went from 57% to 92% under--cov=scripts.Deferred minors
test_architecture.py's module docstring still says "Six invariants"._PLANESnamesextraction/resolution. Step 6 must update it; it fails loudly if not..gitattributespins only*.pyto LF. TS edge ids carry no byte offsets today.files_stateis not dumped. It is a deterministic hash cache that steps 1–5 don't move.Test plan
make format && make lint && make type-check && make pytest && make doc-coverage: 3135 passed.git archive HEADcorpus: 0 diff rows in all three roots.🤖 Generated with Claude Code