Skip to content

test: language-packs step 0 — identity net, forbidden-edge gate, plane reports - #596

Merged
zaebee merged 8 commits into
mainfrom
feat/language-packs-step0
Oct 14, 2026
Merged

zaebee merged 8 commits into
mainfrom
feat/language-packs-step0

Conversation

@zaebee

@zaebee zaebee commented Oct 14, 2026

Copy link
Copy Markdown
Owner

Step 0 of docs/plans/2026-10-14-language-packs.md (spec docs/specs/2026-10-14-language-packs-design.md, #146). Tests and tooling only. No src/ change.

What lands

  • scripts/graph_identity.py: a canonical dump of a stored graph and a diff of two dumps.

    • The dump covers every node column, including metadata, ontology_class and domains; every edge column; and ingest_state.
    • Absolute paths are normalised to <ROOT>, and the mtime-derived ingested_at is dropped.
    • snapshot ingests three roots of a temp copy of the corpus, with a .git marker, the way cgis ingest does.
  • tests/fixtures/graph_identity/corpus/ (18 files). Each case was checked in the graph before committing:

    • Python: DI alias, ABC → DISPATCHES, a star-import chain, src/ and lib/ layouts, the alembic collision;
    • TS: a monorepo with package.json and pnpm workspaces, tsconfig paths, node: imports, .d.ts, a shadowed global, configs above the root.
  • test_graph_identity.py: the snapshot is diffed under PYTHONHASHSEED 0 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 fresh git archive matches with 0 diff rows.

  • Forbidden-edge gate (test_architecture.py, Invariant 7): six rules, compared for equality with forbidden_edges_allowlist.json. Today:

    rule edges
    uplift → sqlite_store 8
    mcp_server/cli → extractor classes 9
    cli/api/guardian → storage 65
    everything else 0

    Planted edges fire every rule.

  • Plane quotients, observe-only: build vs orchestrator 0.238, read vs layered_dag 0.676. These are the spec's numbers.

Rulings made while executing

  • Ingest records root. The test helper and the script record root and options as cgis ingest does. pipeline.run alone records no root, which would leave nothing to normalise.
  • Snapshot read from --out. The pipeline's structlog output goes to stdout, so the snapshot is read from the --out file instead.
  • Three roots instead of two. py/src is added because the whole-corpus ingest never strips the src layout.
  • tests/fixtures excluded from pytest collection and ruff. The corpus holds a test file, and a reformat would change the snapshot's input.
  • Gate mutation as a lazy import. A module-level import in core/paths.py is circular and errors before the gate runs.

Fixed after the final (Fable) review

  • Critical: .gitignore's lib/ 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.
  • Important:
    • the star-import chain now resolves in py_src;
    • the dump carries the fields uplift writes;
    • scripts/graph_identity.py coverage went from 57% to 92% under --cov=scripts.

Deferred minors

  • test_architecture.py's module docstring still says "Six invariants".
  • _PLANES names extraction/resolution. Step 6 must update it; it fails loudly if not.
  • .gitattributes pins only *.py to LF. TS edge ids carry no byte offsets today.
  • files_state is 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.
  • Test-count floor check.
  • Snapshot from a fresh git archive HEAD corpus: 0 diff rows in all three roots.

🤖 Generated with Claude Code

zaebee and others added 6 commits October 14, 2026 08:40
…-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>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread scripts/graph_identity.py Outdated
Comment thread scripts/graph_identity.py Outdated
Comment thread tests/unit/test_graph_identity.py Outdated
Comment thread tests/unit/test_graph_identity_dump.py Outdated
…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>
@zaebee

zaebee commented Oct 14, 2026

Copy link
Copy Markdown
Owner Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread scripts/graph_identity.py Outdated
Comment on lines +83 to +93
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,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
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()

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +68 to +70
ignored = subprocess.run(
["git", "check-ignore", *files], cwd=REPO, capture_output=True, text=True, check=False
).stdout.split()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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()

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +182 to +186
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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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')}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@sonarqubecloud

Copy link
Copy Markdown

@zaebee
zaebee merged commit c2f4a63 into main Oct 14, 2026
4 checks passed
@zaebee
zaebee deleted the feat/language-packs-step0 branch October 14, 2026 09:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant