## Summary
Five-phase sweep of the KG layer code-review backlog (Phase 0 / 1 / 2 /
2.5 / 3) plus pre-existing scaffold-fixture isolation bug and new P0-3
named CI gate.
- **SPARQL/IRI safety**: every literal / IRI interpolated into a query
now routes through the centralised `cataforge.kg._sparql_utils`
(`escape_sparql_literal`, `escape_iri_component`, `assert_safe_iri`,
`cf_namespace`). 16-test injection regression + 44-test escape suite.
- **Atomic writes**: pyoxigraph 0.5.x has no native transaction, so
writer/migrate/repair now do compensating snapshots + rollback.
`TransactionContext` rollback failures use PEP 678 `add_note` so the
original exception traceback is preserved.
- **Drift / pre-existing bug**: `core/scaffold.py:iter_scaffold_files`
was walking the framework's own `.cataforge/` in editable-install
fallback and copying `.deploy-state` / `.deploy-manifest.json` etc into
every scaffolded test project. Fixes 6 bootstrap/upgrade/smoke tests
that were already failing on main.
- **CI guards**: new `kg-injection-regression` pre-commit hook + named
CI step run the escape + injection + atomicity tests as a P0-3 gate
before the main pytest so a re-introduction has clear failure
attribution.
## Phase breakdown
### Phase 0 — shared escape helpers
- New `_sparql_utils.py`: SPARQL 1.1 ECHAR escapes + C0 control
encoding; RFC 3987 percent-encoding; boundary assertion.
- 44-test suite (`test_sparql_escape.py`).
### Phase 1 — HIGH severity
- Every ASK / SELECT in writer / transaction / shim / reconcile / verify
/ compare_read / validate / export / repair routes user-controlled
values through the escape helpers + `assert_safe_iri`.
- Compensating-transaction writes: `_atomic_replace_entity` in
`ingest/writer.py`, Phase 5 rollback in `ingest/migrate.py`, ghost
snapshot + restore in `repair.py`.
- `_SPARQL_KEYWORD_RE` in `_ask.py` rewritten — the `\s*` now applies to
both PREFIX and BASE alternatives (the previous regex left the parser
pointing at whitespace and rejected every ASK query).
- New tests: `test_injection_regression.py` (16),
`test_ingest_writer_atomicity.py`, `test_cli_query.py`,
`test_doctor_kg_ingestion.py`.
### Phase 2 — MEDIUM (15/21 done as Phase 2, 6 deferred to Phase 2.5)
- Transaction rollback robustness (PEP 678).
- `KnowledgeGraph.open_store()` public + try/finally lifecycle.
- `update_entity()` writes `cf:updated_at`.
- `_sort_key` fallback for malformed IDs.
- `kg_config_for` reads full `framework.json` kg section.
- `snapshot.restore_snapshot` wraps OS errors as `KGError`.
- `trace.bidirectional_coverage` uses `FILTER EXISTS` subqueries (no
more N×M Cartesian).
- `shim.plan_load` per-item dispatch for mixed active/legacy batches.
### Phase 2.5 — remaining MEDIUM
- `validate._check_orphans` + `_check_xref_targets` consolidated to
single SELECT + OPTIONAL (no more N+1 ASKs).
- New `export/_entity_meta.py` — shared `_RELATION_GROUPS` /
`_ENTITY_TYPE_TO_DOC_TYPE` / `_template_name` (no more sibling-private
import between pipeline.py and render.py).
- `export/pipeline.py` SPARQL formatting uses `escape_sparql_literal` +
`assert_safe_iri`.
- `shim.resolve_deps` verifies KG entity presence then falls back to
legacy index instead of silently returning empty.
- `shim._kg_extract` falls back to regex-parsed entity_id when the
SELECT row is missing the literal.
- `_slice_section` (both `_shim.py` and `query.py`) is now word-boundary
+ heading-level aware (`F-1` no longer matches `F-12`; deeper
sub-heading no longer terminates the slice prematurely).
### Phase 3 — LOW sweep
- 12 LOW-severity items: `entity_count` → `discovered_count` rename, 8
inline `rstrip` → `cf_namespace()`, helper dedup in `_shim.py`, magic
number → module constant, `try/except/pass` → `contextlib.suppress`,
internal-detail removal from `__all__`, version-milestone comment
cleanup.
## Verification
```
ruff check src tests → clean
scripts/checks/run_local.py → 11/11 OK
pytest tests/ --ignore=tests/integration (1456 passed, 5 skipped, 0 failed)
pytest tests/kg/test_sparql_escape.py tests/kg/test_injection_regression.py
tests/kg/test_ingest_writer_atomicity.py tests/cli/test_cli_query.py → 79/79 (P0-3 gate)
```
Pre-this-branch baseline: 1450 passed / 6 failed (all 6 in
bootstrap/upgrade/smoke, all fixed by the `iter_scaffold_files` filter).
## Reviewer notes
- `CompileResult.entity_count` is renamed to `discovered_count`. The
dataclass is internal to the exporter; `grep` against the repo (incl.
tests) shows no other references — safe rename.
- `_ask.py` regex change directly fixes a 172-test regression that the
Group E sub-agent introduced mid-edit and that this PR also closes — the
test suite proves the fix.
- The scaffold filter is an additive blacklist; existing scaffold
consumers see no behavior change.
- No KG store schema / IRI shape changes — content_hash logic is
untouched.
## Test plan
- [ ] CI green on Linux + Windows × Python 3.10 / 3.13.
- [ ] P0-3 named gate step appears in the workflow logs and passes.
- [ ] `pre-commit run --all-files` clean (Linux job).
- [ ] No new mypy errors in strict packages.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>