codegraph-brain 0.7.3__tar.gz → 0.7.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/.github/workflows/autodoc.yml +7 -7
  2. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/.github/workflows/ci.yml +11 -8
  3. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/.github/workflows/guardian.yml +4 -4
  4. codegraph_brain-0.7.4/.github/workflows/pr-title.yml +51 -0
  5. codegraph_brain-0.7.4/.release-please-manifest.json +3 -0
  6. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/CHANGELOG.md +12 -0
  7. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/PKG-INFO +1 -1
  8. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/how-to/MCP_REFERENCE.md +6 -1
  9. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/pyproject.toml +1 -1
  10. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/api/mcp_server.py +73 -1
  11. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/resolver/engine.py +1 -1
  12. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/resolver/indices.py +41 -22
  13. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_cohesion.py +3 -1
  14. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_guardian_chunker.py +3 -1
  15. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_mcp_server.py +151 -0
  16. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_mermaid.py +4 -2
  17. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_ontology_init.py +3 -1
  18. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_resolver_indices.py +57 -0
  19. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/uv.lock +1 -1
  20. codegraph_brain-0.7.3/.release-please-manifest.json +0 -3
  21. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/.claude-plugin/marketplace.json +0 -0
  22. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/.github/workflows/release-please.yml +0 -0
  23. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/.gitignore +0 -0
  24. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/.pre-commit-config.yaml +0 -0
  25. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/.python-version +0 -0
  26. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/CLAUDE.md +0 -0
  27. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/CONTRIBUTING.md +0 -0
  28. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/LICENSE +0 -0
  29. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/Makefile +0 -0
  30. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/PRIVACY.md +0 -0
  31. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/README.md +0 -0
  32. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/benchmarks/guardian/CURATION.md +0 -0
  33. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/benchmarks/guardian/pr-122.yaml +0 -0
  34. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/benchmarks/guardian/pr-140.yaml +0 -0
  35. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/benchmarks/guardian/pr-141.yaml +0 -0
  36. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/benchmarks/guardian/pr-142.yaml +0 -0
  37. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/benchmarks/guardian/pr-143.yaml +0 -0
  38. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/benchmarks/guardian/pr-144.yaml +0 -0
  39. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/benchmarks/guardian/pr-278.yaml +0 -0
  40. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/benchmarks/guardian/results.jsonl +0 -0
  41. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/data/.gitkeep +0 -0
  42. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/AUDIT.md +0 -0
  43. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/CASE_STUDY.md +0 -0
  44. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/architecture/HOW_IT_WORKS.md +0 -0
  45. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/architecture/ONTOLOGY.md +0 -0
  46. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/architecture/PATTERNS_AND_TRIADS.md +0 -0
  47. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/architecture/SELF_PORTRAIT.md +0 -0
  48. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/architecture/diagrams/pipeline_flow.mermaid +0 -0
  49. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/architecture/health_badge.json +0 -0
  50. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/assets/.gitignore +0 -0
  51. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/assets/cgis-app-avatar.png +0 -0
  52. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/assets/cgis-app-avatar.svg +0 -0
  53. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/assets/generate_avatar.py +0 -0
  54. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/examples/.gitkeep +0 -0
  55. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/how-to/AGENT_ONBOARDING.md +0 -0
  56. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/how-to/CLI_USAGE.md +0 -0
  57. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/lab-notes/2026-06-11-chunked-review-negative-result.md +0 -0
  58. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/ontology/.gitkeep +0 -0
  59. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/ontology/core.yaml +0 -0
  60. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/ontology/domains.yaml +0 -0
  61. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/ontology/patterns.yaml +0 -0
  62. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/ontology/tolerances.lock +0 -0
  63. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/2026-06-09-domain-pattern-fingerprint-design.md +0 -0
  64. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/2026-06-09-pattern-alphabet-motif-basis-design.md +0 -0
  65. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/2026-06-10-guardian-sprint-design.md +0 -0
  66. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/2026-06-11-fastapi-di-edges-design.md +0 -0
  67. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/2026-06-11-guardian-chunked-review-design.md +0 -0
  68. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/2026-06-11-guardian-chunker-design.md +0 -0
  69. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/2026-06-11-mcp-drift-validate-fqn-design.md +0 -0
  70. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/2026-06-11-resolver-split-design.md +0 -0
  71. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/2026-06-11-symbol-import-edges-design.md +0 -0
  72. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/2026-06-12-drift-empty-domains-design.md +0 -0
  73. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/2026-06-12-gate-semantics-design.md +0 -0
  74. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/2026-06-12-init-ontology-design.md +0 -0
  75. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/2026-06-12-release-please-ci-design.md +0 -0
  76. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/2026-06-13-suggest-packages-design.md +0 -0
  77. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/2026-06-13-tangle-anti-pattern-design.md +0 -0
  78. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/2026-07-29-guardian-skeptic-scoring-design.md +0 -0
  79. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/2026-07-30-cgis-fractal-design.md +0 -0
  80. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/2026-07-30-cgis-fractal-plan.md +0 -0
  81. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/2026-07-30-chunk-source-filter-design.md +0 -0
  82. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/2026-07-30-chunk-source-filter-plan.md +0 -0
  83. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/2026-07-30-genai-client-close-design.md +0 -0
  84. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/2026-07-30-genai-client-close-plan.md +0 -0
  85. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/2026-07-30-guardian-precision-bench-design.md +0 -0
  86. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/2026-07-30-guardian-precision-bench-plan.md +0 -0
  87. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/2026-07-30-guardian-timeout-retry-design.md +0 -0
  88. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/2026-07-30-guardian-timeout-retry-plan.md +0 -0
  89. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/2026-07-31-finder-bug-class-taxonomy.md +0 -0
  90. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/BLUEPRINT.md +0 -0
  91. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/PRD.md +0 -0
  92. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/TDD.md +0 -0
  93. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/plans/2026-06-09-fingerprint-drift.md +0 -0
  94. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/plans/2026-06-10-guardian-context-skeptic-inline.md +0 -0
  95. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/plans/2026-06-10-guardian-structured-findings-bench.md +0 -0
  96. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/plans/2026-06-10-motif-basis-part-b.md +0 -0
  97. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/plans/2026-06-10-unified-pattern-alphabet.md +0 -0
  98. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/plans/2026-06-11-fastapi-di-edges.md +0 -0
  99. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/plans/2026-06-11-guardian-chunked-review.md +0 -0
  100. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/plans/2026-06-11-guardian-chunker.md +0 -0
  101. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/plans/2026-06-11-mcp-drift-validate-fqn.md +0 -0
  102. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/plans/2026-06-11-resolver-split.md +0 -0
  103. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/plans/2026-06-12-drift-empty-domains.md +0 -0
  104. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/plans/2026-06-12-gate-semantics.md +0 -0
  105. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/plans/2026-06-12-init-ontology.md +0 -0
  106. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/plans/2026-06-12-release-please-ci.md +0 -0
  107. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/plans/2026-06-12-symbol-import-edges.md +0 -0
  108. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/plans/2026-06-13-suggest-packages.md +0 -0
  109. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/docs/specs/plans/2026-07-29-guardian-skeptic-scoring.md +0 -0
  110. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/main.py +0 -0
  111. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/plugin/.claude-plugin/plugin.json +0 -0
  112. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/plugin/.mcp.json +0 -0
  113. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/plugin/README.md +0 -0
  114. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/plugin/skills/cgis/SKILL.md +0 -0
  115. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/plugin/skills/ingest/SKILL.md +0 -0
  116. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/release-please-config.json +0 -0
  117. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/scripts/gen_ideal_graph.py +0 -0
  118. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/scripts/generate_health.py +0 -0
  119. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/scripts/generate_mcp_ref.py +0 -0
  120. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/scripts/generate_schema_docs.py +0 -0
  121. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/scripts/guardian_bench.py +0 -0
  122. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/scripts/guardian_review.py +0 -0
  123. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/scripts/inject_readme_graph.py +0 -0
  124. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/scripts/probe_closure_gap.py +0 -0
  125. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/scripts/probe_tier_ladder.py +0 -0
  126. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/__init__.py +0 -0
  127. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/__main__.py +0 -0
  128. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/api/.gitkeep +0 -0
  129. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/api/__init__.py +0 -0
  130. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/cli.py +0 -0
  131. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/core/.gitkeep +0 -0
  132. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/core/models.py +0 -0
  133. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/extractors/.gitkeep +0 -0
  134. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/extractors/_python_ast.py +0 -0
  135. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/extractors/_python_classes.py +0 -0
  136. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/extractors/_python_functions.py +0 -0
  137. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/extractors/_python_imports.py +0 -0
  138. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/extractors/_python_types.py +0 -0
  139. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/extractors/base.py +0 -0
  140. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/extractors/python_extractor.py +0 -0
  141. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/extractors/typescript_extractor.py +0 -0
  142. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/guardian/__init__.py +0 -0
  143. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/guardian/bench.py +0 -0
  144. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/guardian/chunked.py +0 -0
  145. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/guardian/chunker.py +0 -0
  146. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/guardian/collector.py +0 -0
  147. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/guardian/core.py +0 -0
  148. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/guardian/diff_index.py +0 -0
  149. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/guardian/findings.py +0 -0
  150. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/guardian/github_poster.py +0 -0
  151. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/guardian/metrics.py +0 -0
  152. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/guardian/prompts.py +0 -0
  153. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/guardian/providers/__init__.py +0 -0
  154. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/guardian/providers/base.py +0 -0
  155. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/guardian/providers/gemini.py +0 -0
  156. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/guardian/providers/mistral.py +0 -0
  157. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/guardian/providers/ollama.py +0 -0
  158. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/guardian/recording.py +0 -0
  159. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/guardian/render.py +0 -0
  160. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/guardian/runner.py +0 -0
  161. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/guardian/skeptic.py +0 -0
  162. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/pipeline.py +0 -0
  163. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/py.typed +0 -0
  164. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/query/analysis/__init__.py +0 -0
  165. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/query/analysis/analyzer.py +0 -0
  166. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/query/analysis/anomaly.py +0 -0
  167. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/query/analysis/cohesion.py +0 -0
  168. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/query/analysis/health.py +0 -0
  169. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/query/analysis/suggest_service.py +0 -0
  170. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/query/context/__init__.py +0 -0
  171. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/query/context/audit.py +0 -0
  172. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/query/context/context_service.py +0 -0
  173. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/query/context/prompt.py +0 -0
  174. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/query/context/snippet.py +0 -0
  175. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/query/drift/__init__.py +0 -0
  176. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/query/drift/_scc.py +0 -0
  177. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/query/drift/drift.py +0 -0
  178. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/query/drift/drift_service.py +0 -0
  179. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/query/drift/fingerprint.py +0 -0
  180. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/query/drift/fractal.py +0 -0
  181. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/query/drift/ontology_init.py +0 -0
  182. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/query/drift/quotient.py +0 -0
  183. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/query/drift/triads.py +0 -0
  184. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/query/engine.py +0 -0
  185. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/query/fqn.py +0 -0
  186. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/query/render/__init__.py +0 -0
  187. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/query/render/graph_json.py +0 -0
  188. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/query/render/mermaid.py +0 -0
  189. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/query/render/metrics.py +0 -0
  190. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/resolver/.gitkeep +0 -0
  191. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/resolver/__init__.py +0 -0
  192. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/resolver/symbols.py +0 -0
  193. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/resolver/uplift.py +0 -0
  194. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/storage/.gitkeep +0 -0
  195. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/src/cgis/storage/sqlite_store.py +0 -0
  196. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/integration/.gitkeep +0 -0
  197. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/self_parsing/.gitkeep +0 -0
  198. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/self_parsing/__init__.py +0 -0
  199. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/self_parsing/conftest.py +0 -0
  200. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/self_parsing/test_architecture.py +0 -0
  201. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/self_parsing/test_drift.py +0 -0
  202. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/self_parsing/test_fractal.py +0 -0
  203. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/self_parsing/test_init_ontology_roundtrip.py +0 -0
  204. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/self_parsing/test_self_parse.py +0 -0
  205. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/self_parsing/test_self_parse_ts.py +0 -0
  206. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/self_parsing/test_suggest.py +0 -0
  207. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/.gitkeep +0 -0
  208. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/conftest.py +0 -0
  209. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/guardian_stubs.py +0 -0
  210. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test___main__.py +0 -0
  211. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_analyzer.py +0 -0
  212. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_audit.py +0 -0
  213. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_cli.py +0 -0
  214. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_context_service.py +0 -0
  215. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_di_acceptance.py +0 -0
  216. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_drift.py +0 -0
  217. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_drift_service.py +0 -0
  218. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_fingerprint.py +0 -0
  219. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_fqn.py +0 -0
  220. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_fractal.py +0 -0
  221. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_gen_ideal_graph.py +0 -0
  222. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_generate_mcp_ref.py +0 -0
  223. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_graph_json.py +0 -0
  224. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_guardian_bench.py +0 -0
  225. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_guardian_chunked.py +0 -0
  226. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_guardian_collector.py +0 -0
  227. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_guardian_core.py +0 -0
  228. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_guardian_diff_index.py +0 -0
  229. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_guardian_findings.py +0 -0
  230. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_guardian_metrics.py +0 -0
  231. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_guardian_poster.py +0 -0
  232. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_guardian_providers.py +0 -0
  233. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_guardian_recording.py +0 -0
  234. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_guardian_render.py +0 -0
  235. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_guardian_runner.py +0 -0
  236. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_guardian_skeptic.py +0 -0
  237. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_health_scorer.py +0 -0
  238. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_import_acceptance.py +0 -0
  239. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_metrics.py +0 -0
  240. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_models.py +0 -0
  241. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_ontology_compliance.py +0 -0
  242. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_patterns_yaml.py +0 -0
  243. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_pipeline.py +0 -0
  244. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_prompt.py +0 -0
  245. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_python_extractor.py +0 -0
  246. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_quotient.py +0 -0
  247. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_resolver.py +0 -0
  248. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_resolver_symbols.py +0 -0
  249. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_snippet.py +0 -0
  250. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_sqlite_store.py +0 -0
  251. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_suggest_service.py +0 -0
  252. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_triads.py +0 -0
  253. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_typescript_extractor.py +0 -0
  254. {codegraph_brain-0.7.3 → codegraph_brain-0.7.4}/tests/unit/test_uplift.py +0 -0
@@ -28,15 +28,15 @@ jobs:
28
28
  enable-cache: true
29
29
 
30
30
  - name: Install CGIS
31
- run: uv sync
31
+ run: uv sync --frozen
32
32
 
33
33
  - name: Self-ingest core engine
34
- run: uv run cgis ingest ./src --output /tmp/self_graph.db
34
+ run: uv run --frozen cgis ingest ./src --output /tmp/self_graph.db
35
35
 
36
36
  - name: Generate pipeline flow diagram
37
37
  run: |
38
38
  mkdir -p docs/architecture/diagrams
39
- uv run cgis trace "cgis.pipeline.IngestionPipeline.run" \
39
+ uv run --frozen cgis trace "cgis.pipeline.IngestionPipeline.run" \
40
40
  --db /tmp/self_graph.db \
41
41
  --depth 1 \
42
42
  --format mermaid \
@@ -44,21 +44,21 @@ jobs:
44
44
  > docs/architecture/diagrams/pipeline_flow.mermaid
45
45
 
46
46
  - name: Compute graph health badge
47
- run: uv run python scripts/generate_health.py --db /tmp/self_graph.db
47
+ run: uv run --frozen python scripts/generate_health.py --db /tmp/self_graph.db
48
48
 
49
49
  - name: Inject graph into README.md
50
50
  run: |
51
- uv run python scripts/inject_readme_graph.py \
51
+ uv run --frozen python scripts/inject_readme_graph.py \
52
52
  --db /tmp/self_graph.db \
53
53
  --fqn "cgis.pipeline.IngestionPipeline.run" \
54
54
  --depth 1 \
55
55
  --path-prefix "src/"
56
56
 
57
57
  - name: Generate MCP reference manual
58
- run: uv run python scripts/generate_mcp_ref.py
58
+ run: uv run --frozen python scripts/generate_mcp_ref.py
59
59
 
60
60
  - name: Sync schema docs from source
61
- run: uv run python scripts/generate_schema_docs.py
61
+ run: uv run --frozen python scripts/generate_schema_docs.py
62
62
 
63
63
  - name: Open PR with generated docs
64
64
  uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7
@@ -20,24 +20,27 @@ jobs:
20
20
  enable-cache: true
21
21
 
22
22
  - name: Lockfile in step with pyproject
23
- # MUST run before `uv sync`: sync silently re-resolves and rewrites
24
- # uv.lock, so a check placed after it would always pass.
23
+ # This is the only step that validates the lockfile. `--frozen` below stops
24
+ # uv rewriting uv.lock, but it does not check that the lock is current
25
+ # that is `--locked`. Keeping the check explicit gives a clearer failure
26
+ # than a sync error, and it still runs first so an unfrozen uv could never
27
+ # repair the lock ahead of it.
25
28
  run: uv lock --check
26
29
 
27
30
  - name: Install Dependencies
28
- run: uv sync --group dev
31
+ run: uv sync --frozen --group dev
29
32
 
30
33
  - name: Ruff Format Check
31
- run: uv run ruff format --check .
34
+ run: uv run --frozen ruff format --check .
32
35
 
33
36
  - name: Ruff Lint
34
- run: uv run ruff check .
37
+ run: uv run --frozen ruff check .
35
38
 
36
39
  - name: Interrogate (≥90% docstring coverage)
37
- run: uv run interrogate -v --fail-under 90 src/cgis
40
+ run: uv run --frozen interrogate -v --fail-under 90 src/cgis
38
41
 
39
42
  - name: Mypy (strict)
40
- run: uv run mypy src
43
+ run: uv run --frozen mypy src
41
44
 
42
45
  - name: Pytest
43
- run: uv run pytest --cov=cgis --cov-report=term-missing
46
+ run: uv run --frozen pytest --cov=cgis --cov-report=term-missing
@@ -67,10 +67,10 @@ jobs:
67
67
  uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3
68
68
 
69
69
  - name: Install dependencies
70
- run: uv sync --extra guardian
70
+ run: uv sync --frozen --extra guardian
71
71
 
72
72
  - name: Build code graph
73
- run: uv run cgis ingest ./src --output graph.db
73
+ run: uv run --frozen cgis ingest ./src --output graph.db
74
74
 
75
75
  - name: Restore metrics from data branch
76
76
  run: |
@@ -91,7 +91,7 @@ jobs:
91
91
  GUARDIAN_SKEPTIC: ${{ vars.GUARDIAN_SKEPTIC }}
92
92
  GUARDIAN_SKEPTIC_MODEL: ${{ vars.GUARDIAN_SKEPTIC_MODEL }}
93
93
  run: |
94
- uv run python scripts/guardian_review.py \
94
+ uv run --frozen python scripts/guardian_review.py \
95
95
  --output guardian_report.md \
96
96
  --db graph.db \
97
97
  --pr "$PR_NUMBER" \
@@ -219,7 +219,7 @@ jobs:
219
219
  git show origin/data/guardian-metrics:guardian_metrics.jsonl \
220
220
  > guardian_metrics.jsonl
221
221
 
222
- uv run cgis guardian-rate "$PR_NUMBER" "$APPLIED" \
222
+ uv run --frozen cgis guardian-rate "$PR_NUMBER" "$APPLIED" \
223
223
  --metrics guardian_metrics.jsonl
224
224
 
225
225
  if git ls-remote --exit-code origin data/guardian-metrics > /dev/null 2>&1; then
@@ -0,0 +1,51 @@
1
+ name: PR Title
2
+
3
+ # The repository squash-merges, so the PR title becomes the commit subject that
4
+ # release-please parses. A title it does not recognise is dropped from the
5
+ # changelog silently — no warning, no failure. That has now happened twice:
6
+ # #297 ("drift trust batch: …", a descriptive title) and #313
7
+ # ("security(mcp): …", a type that does not exist), the second of which would
8
+ # have shipped a security fix invisibly.
9
+ on:
10
+ pull_request:
11
+ branches: [ main ]
12
+ # `edited` matters: without it, fixing a rejected title would not re-run
13
+ # the check. Kept in a separate workflow from ci.yml so retitling does not
14
+ # replay the whole test suite.
15
+ types: [ opened, edited, reopened, synchronize ]
16
+
17
+ permissions: {}
18
+
19
+ jobs:
20
+ conventional-title:
21
+ name: Conventional commit title
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - name: Check the title release-please will parse
25
+ env:
26
+ # Never interpolate an untrusted title straight into `run:` — it is
27
+ # attacker-controlled text. Through the environment it is just data.
28
+ PR_TITLE: ${{ github.event.pull_request.title }}
29
+ run: |
30
+ set -euo pipefail
31
+ # Types release-please understands. Anything else is dropped from the
32
+ # changelog without complaint, which is the failure being prevented.
33
+ TYPES='feat|fix|docs|chore|style|refactor|perf|test|build|ci|revert'
34
+ if printf '%s' "$PR_TITLE" | grep -qE "^($TYPES)(\([a-z0-9._/-]+\))?!?: .+"; then
35
+ printf '::notice::PR title is a conventional commit.\n'
36
+ exit 0
37
+ fi
38
+ printf '::error::PR title is not a conventional commit: %s\n' "$PR_TITLE"
39
+ cat >&2 <<'MSG'
40
+
41
+ This repository squash-merges, so the PR title becomes the commit
42
+ subject release-please reads. An unrecognised type is dropped from the
43
+ CHANGELOG silently.
44
+
45
+ Expected: <type>(<optional scope>): <description>
46
+ Types: feat fix docs chore style refactor perf test build ci revert
47
+
48
+ Note there is no `security` type — use `fix` with a scope:
49
+ fix(mcp): validate db_path before cgis_ingest creates a database
50
+ MSG
51
+ exit 1
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.7.4"
3
+ }
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.4](https://github.com/zaebee/codegraph-brain/compare/codegraph-brain-v0.7.3...codegraph-brain-v0.7.4) (2026-08-01)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **mcp:** validate db_path before cgis_ingest creates a database ([#313](https://github.com/zaebee/codegraph-brain/issues/313)) ([2c5499b](https://github.com/zaebee/codegraph-brain/commit/2c5499b8fb7f0d40a03144417c3210900371d499)), closes [#312](https://github.com/zaebee/codegraph-brain/issues/312)
9
+
10
+
11
+ ### Documentation
12
+
13
+ * auto-sync architecture graph and MCP reference ([#314](https://github.com/zaebee/codegraph-brain/issues/314)) ([7d5b666](https://github.com/zaebee/codegraph-brain/commit/7d5b66669f9dfa40e2fc86dd59a4baf492a9342e))
14
+
3
15
  ## [0.7.3](https://github.com/zaebee/codegraph-brain/compare/codegraph-brain-v0.7.2...codegraph-brain-v0.7.3) (2026-07-31)
4
16
 
5
17
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codegraph-brain
3
- Version: 0.7.3
3
+ Version: 0.7.4
4
4
  Summary: Semantic code graph for AI agents — deterministic FQN resolution, impact analysis and architectural drift gates, exposed over MCP.
5
5
  Project-URL: Homepage, https://github.com/zaebee/codegraph-brain
6
6
  Project-URL: Repository, https://github.com/zaebee/codegraph-brain
@@ -176,9 +176,14 @@ Show the structural layout (CONTAINS/DECLARES) of a module or class.
176
176
  Scan a local directory, extract all symbols, resolve links, and build the graph DB.
177
177
 
178
178
  Use this to initialise or refresh the code knowledge graph for a project.
179
- Paths are normalised relative to the workspace root so the database is
179
+ Node FQNs are normalised relative to the workspace root so the graph is
180
180
  portable across machines.
181
181
 
182
+ ``db_path`` must name a database — it has to end in ``.db``, ``.sqlite`` or
183
+ ``.sqlite3``, live in a directory that already exists, and not point at an
184
+ existing file that is not a SQLite database. cgis will not create parent
185
+ directories.
186
+
182
187
  By default the ingest is **incremental**: only changed/new files are
183
188
  re-scanned, and the summary reports both what changed this run and the
184
189
  whole-graph total. Set ``full_rebuild=True`` to re-scan every file and
@@ -7,7 +7,7 @@ packages = ["src/cgis"]
7
7
 
8
8
  [project]
9
9
  name = "codegraph-brain"
10
- version = "0.7.3"
10
+ version = "0.7.4"
11
11
  description = "Semantic code graph for AI agents — deterministic FQN resolution, impact analysis and architectural drift gates, exposed over MCP."
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.12"
@@ -85,20 +85,92 @@ def _render_subgraph(
85
85
  return f"❌ Unknown format '{output_format}'. Use 'mermaid' or 'json'."
86
86
 
87
87
 
88
+ #: Names cgis will create a database under. Everything else is refused, so an
89
+ #: agent cannot be talked into materialising `~/.ssh/authorized_keys` (#312).
90
+ _DB_SUFFIXES = frozenset({".db", ".sqlite", ".sqlite3"})
91
+
92
+ #: First 16 bytes of any SQLite file.
93
+ _SQLITE_MAGIC = b"SQLite format 3\x00"
94
+
95
+
96
+ def _reject_db_path(db_path: str) -> str | None:
97
+ """Return a refusal message for an unusable ``db_path``, or None if it is fine.
98
+
99
+ ``cgis_ingest`` is the only MCP tool that creates its database — the other
100
+ twelve refuse a path that does not already exist, which is guard enough for
101
+ them. It needs a different one: it reads untrusted repository content and is
102
+ then told by the same agent where to write, so ``db_path`` is attacker-
103
+ reachable in a way the read-only tools' paths are not.
104
+
105
+ Refuses an unexpected suffix, a missing parent directory (creating a tree is
106
+ never wanted), a directory target, and an existing file that is not a
107
+ database. The last is belt-and-braces — SQLite already declines to open a
108
+ non-database — but it fails with a message that says why.
109
+
110
+ Everything is judged on the **resolved** path. A dangling symlink is the
111
+ bypass this guard exists to stop: ``attack.db`` pointing at a target that
112
+ does not exist yet makes ``is_file()`` return False, every check passes, and
113
+ SQLite creates the target. Resolving first means the suffix rule applies to
114
+ where the write actually lands. (A symlink to an *existing* non-database was
115
+ already refused by the magic-byte check.)
116
+
117
+ The filesystem probes are wrapped: this runs *before* ``cgis_ingest``'s own
118
+ try/except, and ``Path.resolve()``, ``Path.is_dir()`` and friends propagate
119
+ ``OSError`` (a ``PermissionError`` on the parent, or a symlink loop), which
120
+ would escape the tool as a crash instead of a message the agent can act on.
121
+ """
122
+ try:
123
+ path = Path(db_path).resolve()
124
+ except OSError as exc:
125
+ return f"❌ Refusing db_path '{db_path}': path is inaccessible ({exc})."
126
+ # Case-insensitive: `.DB` carries the same intent, and on a case-insensitive
127
+ # filesystem it is literally the same file.
128
+ if path.suffix.lower() not in _DB_SUFFIXES:
129
+ allowed = ", ".join(sorted(_DB_SUFFIXES))
130
+ return f"❌ Refusing db_path '{db_path}': name must end in one of {allowed}."
131
+ try:
132
+ if path.is_dir():
133
+ return f"❌ Refusing db_path '{db_path}': it is a directory."
134
+ if not path.parent.is_dir():
135
+ return (
136
+ f"❌ Refusing db_path '{db_path}': parent directory does not exist. "
137
+ "cgis will not create one."
138
+ )
139
+ if path.is_file() and path.stat().st_size > 0:
140
+ with path.open("rb") as fh:
141
+ if fh.read(len(_SQLITE_MAGIC)) != _SQLITE_MAGIC:
142
+ return (
143
+ f"❌ Refusing db_path '{db_path}': existing file is not a SQLite database."
144
+ )
145
+ except OSError as exc:
146
+ return f"❌ Refusing db_path '{db_path}': path is inaccessible ({exc})."
147
+ return None
148
+
149
+
88
150
  @mcp.tool()
89
151
  def cgis_ingest(project_path: str, db_path: str = _DEFAULT_DB, full_rebuild: bool = False) -> str:
90
152
  """Scan a local directory, extract all symbols, resolve links, and build the graph DB.
91
153
 
92
154
  Use this to initialise or refresh the code knowledge graph for a project.
93
- Paths are normalised relative to the workspace root so the database is
155
+ Node FQNs are normalised relative to the workspace root so the graph is
94
156
  portable across machines.
95
157
 
158
+ ``db_path`` must name a database — it has to end in ``.db``, ``.sqlite`` or
159
+ ``.sqlite3``, live in a directory that already exists, and not point at an
160
+ existing file that is not a SQLite database. cgis will not create parent
161
+ directories.
162
+
96
163
  By default the ingest is **incremental**: only changed/new files are
97
164
  re-scanned, and the summary reports both what changed this run and the
98
165
  whole-graph total. Set ``full_rebuild=True`` to re-scan every file and
99
166
  overwrite the database from scratch — use this to drop nodes for files that
100
167
  were deleted or renamed, which an incremental run leaves behind.
101
168
  """
169
+ refusal = _reject_db_path(db_path)
170
+ if refusal is not None:
171
+ logger.warning("MCP ingest refused db_path", db=db_path)
172
+ return refusal
173
+
102
174
  pipeline = IngestionPipeline(_EXTRACTORS)
103
175
  try:
104
176
  with SQLiteStore(db_path) as store:
@@ -126,7 +126,7 @@ class ResolverEngine:
126
126
 
127
127
  def _ensure_virtual_node(self, target: str, virtual_nodes: dict[str, Node]) -> None:
128
128
  """Create a virtual boundary node for target if it is not already in the graph."""
129
- if target not in self._index.nodes and target not in virtual_nodes:
129
+ if not self._index.has_node(target) and target not in virtual_nodes:
130
130
  virtual_nodes[target] = self._make_virtual_node(
131
131
  target, self._index.classify_fqn(target)
132
132
  )
@@ -3,7 +3,9 @@
3
3
  import builtins
4
4
  import os
5
5
  import sys
6
+ from collections.abc import Mapping
6
7
  from dataclasses import dataclass
8
+ from types import MappingProxyType
7
9
 
8
10
  from cgis.core.models import SELF_PREFIX, Node, NodeNamespace, NodeType
9
11
 
@@ -15,30 +17,37 @@ class SymbolIndex:
15
17
  """Immutable lookup indices over the extracted node set.
16
18
 
17
19
  Built by IndexBuilder, consumed by SymbolResolver and ResolverEngine.
18
- Frozen by convention (field rebinding prevented); the contained dicts
19
- are never mutated after construction.
20
+
21
+ Immutable in substance, not only by convention: the dataclass prevents
22
+ field rebinding, and ``IndexBuilder`` hands over read-only views, so a
23
+ stray write (a cache line added to a shared index, say) raises instead of
24
+ silently corrupting resolution for every later lookup (#183).
25
+
26
+ The views are shallow — the inner ``list``/``dict`` values are still
27
+ mutable. Deep-freezing would mean copying every one of them on a hot path;
28
+ the realistic mistake this guards is adding or replacing a top-level key.
20
29
  """
21
30
 
22
31
  # node id (FQN) -> Node
23
- nodes: dict[str, Node]
32
+ nodes: Mapping[str, Node]
24
33
  # name -> list of FQNs
25
- global_symbols: dict[str, list[str]]
34
+ global_symbols: Mapping[str, list[str]]
26
35
  # (file_path, name) -> list of FQNs (list handles conditional redefinitions)
27
- file_global_symbols: dict[tuple[str, str], list[str]]
36
+ file_global_symbols: Mapping[tuple[str, str], list[str]]
28
37
  # class_fqn -> {method_name -> method_fqn}
29
- class_methods: dict[str, dict[str, str]]
38
+ class_methods: Mapping[str, dict[str, str]]
30
39
  # DI-alias (VARIABLE) indices for raw_dep: resolution; kept separate
31
40
  # from global_symbols so call resolution behavior does not change.
32
- variable_symbols: dict[str, list[str]]
33
- file_variable_symbols: dict[tuple[str, str], list[str]]
41
+ variable_symbols: Mapping[str, list[str]]
42
+ file_variable_symbols: Mapping[tuple[str, str], list[str]]
34
43
  # normalized file_path -> {local_alias: target_fqn} (from FILE node import_map)
35
- file_imports: dict[str, dict[str, str]]
44
+ file_imports: Mapping[str, dict[str, str]]
36
45
  # suffix_fqn -> [full_node_ids] (handles src/ layout prefix mismatch)
37
- suffix_map: dict[str, list[str]]
46
+ suffix_map: Mapping[str, list[str]]
38
47
  # top-level root segments of all internal nodes (for classify)
39
- internal_roots: set[str]
48
+ internal_roots: frozenset[str]
40
49
  # root segments of absolute imports (anything else is UNKNOWN)
41
- external_roots: set[str]
50
+ external_roots: frozenset[str]
42
51
 
43
52
  def map_to_node_fqn(self, imported_fqn: str) -> str | None:
44
53
  """Resolve an imported FQN to an actual node in the graph.
@@ -65,6 +74,14 @@ class SymbolIndex:
65
74
  return candidate
66
75
  return None
67
76
 
77
+ def has_node(self, fqn: str) -> bool:
78
+ """True if the graph holds a node with this exact FQN.
79
+
80
+ Saves callers reaching into ``nodes`` for a membership test, which is
81
+ the one place the index's internals leaked (#183).
82
+ """
83
+ return fqn in self.nodes
84
+
68
85
  def classify_fqn(self, fqn: str) -> NodeNamespace:
69
86
  """Classify an FQN as STDLIB, INTERNAL, EXTERNAL, or UNKNOWN.
70
87
 
@@ -145,17 +162,19 @@ class IndexBuilder:
145
162
  # "src.cgis.pipeline.X" → suffix "cgis.pipeline.X" also points to the node
146
163
  self._add_node_to_suffix_map(node.id, suffix_map, internal_roots)
147
164
 
165
+ # Read-only views, not copies: MappingProxyType wraps in O(1), so this
166
+ # buys write protection without touching ingest cost (#183).
148
167
  return SymbolIndex(
149
- nodes=nodes_by_id,
150
- global_symbols=global_symbols,
151
- file_global_symbols=file_global_symbols,
152
- class_methods=class_methods,
153
- variable_symbols=variable_symbols,
154
- file_variable_symbols=file_variable_symbols,
155
- file_imports=file_imports,
156
- suffix_map=suffix_map,
157
- internal_roots=internal_roots,
158
- external_roots=self._build_external_roots(file_imports),
168
+ nodes=MappingProxyType(nodes_by_id),
169
+ global_symbols=MappingProxyType(global_symbols),
170
+ file_global_symbols=MappingProxyType(file_global_symbols),
171
+ class_methods=MappingProxyType(class_methods),
172
+ variable_symbols=MappingProxyType(variable_symbols),
173
+ file_variable_symbols=MappingProxyType(file_variable_symbols),
174
+ file_imports=MappingProxyType(file_imports),
175
+ suffix_map=MappingProxyType(suffix_map),
176
+ internal_roots=frozenset(internal_roots),
177
+ external_roots=frozenset(self._build_external_roots(file_imports)),
159
178
  )
160
179
 
161
180
  def _add_node_to_suffix_map(
@@ -120,7 +120,9 @@ def test_modularity_isolated_files_are_singletons() -> None:
120
120
  def test_modularity_is_deterministic() -> None:
121
121
  adj = {**_clique("p", ["a", "b", "c"]), **_clique("p", ["x", "y", "z"])}
122
122
  g = FileGraph(files=tuple(sorted(adj)), adj=adj)
123
- assert greedy_modularity(g) == greedy_modularity(g)
123
+ first = greedy_modularity(g)
124
+ second = greedy_modularity(g)
125
+ assert first == second, "greedy_modularity must not depend on dict iteration order"
124
126
 
125
127
 
126
128
  def test_divergence_flat_layout_vs_multi_community_is_one() -> None:
@@ -313,7 +313,9 @@ def test_build_chunks_deterministic(tmp_path: Path) -> None:
313
313
  [_edge("src.cgis.a", "src.cgis.b")],
314
314
  )
315
315
  diff = fdiff("src/cgis/c.py") + fdiff("src/cgis/a.py") + fdiff("src/cgis/b.py")
316
- assert build_chunks(diff, store) == build_chunks(diff, store)
316
+ first = build_chunks(diff, store)
317
+ second = build_chunks(diff, store)
318
+ assert first == second, "chunk membership and ordering must be stable across runs"
317
319
 
318
320
 
319
321
  def test_split_duplicate_path_blocks_merged() -> None:
@@ -1,6 +1,7 @@
1
1
  """Unit tests for the MCP server tools."""
2
2
 
3
3
  import json
4
+ import os
4
5
  import re
5
6
  from pathlib import Path
6
7
 
@@ -806,3 +807,153 @@ def test_cgis_suggest_packages_missing_db(tmp_path: Path) -> None:
806
807
  """A missing db returns an error string, not a crash (#242)."""
807
808
  result = cgis_suggest_packages(str(tmp_path / "no.db"), prefix="mod")
808
809
  assert "not found" in result.lower()
810
+
811
+
812
+ # ---------------------------------------------------------------------------
813
+ # db_path guard for cgis_ingest (#312)
814
+ # ---------------------------------------------------------------------------
815
+ #
816
+ # cgis_ingest is the only MCP tool that creates its database — the other twelve
817
+ # refuse a db_path that does not already exist. It therefore needs a different
818
+ # guard, not the same one: it reads untrusted repository content and is then
819
+ # told by the same agent where to write, so a hostile db_path is a real shape.
820
+
821
+
822
+ def test_ingest_refuses_to_clobber_a_non_sqlite_file(tmp_path: Path) -> None:
823
+ """An existing file that is not a database must survive untouched."""
824
+ victim = tmp_path / "config.db"
825
+ original = "PermitRootLogin no\n"
826
+ victim.write_text(original, encoding="utf-8")
827
+ (tmp_path / "mod.py").write_text("def fn(): pass\n", encoding="utf-8")
828
+
829
+ result = cgis_ingest(str(tmp_path), str(victim))
830
+
831
+ assert "❌" in result
832
+ assert victim.read_text(encoding="utf-8") == original, "the target file was modified"
833
+
834
+
835
+ def test_ingest_refuses_to_create_parent_directories(tmp_path: Path) -> None:
836
+ """A missing parent must be an error, not an invitation to build a tree."""
837
+ (tmp_path / "mod.py").write_text("def fn(): pass\n", encoding="utf-8")
838
+ nested = tmp_path / "does" / "not" / "exist" / "graph.db"
839
+
840
+ result = cgis_ingest(str(tmp_path), str(nested))
841
+
842
+ assert "❌" in result
843
+ assert not nested.parent.exists(), "parent directories were created"
844
+
845
+
846
+ def test_ingest_refuses_an_unexpected_suffix(tmp_path: Path) -> None:
847
+ """Only database-shaped names are accepted, so `~/.ssh/config` cannot be a target."""
848
+ (tmp_path / "mod.py").write_text("def fn(): pass\n", encoding="utf-8")
849
+ target = tmp_path / "authorized_keys"
850
+
851
+ result = cgis_ingest(str(tmp_path), str(target))
852
+
853
+ assert "❌" in result
854
+ assert not target.exists()
855
+
856
+
857
+ def test_ingest_refuses_a_directory_as_db_path(tmp_path: Path) -> None:
858
+ """A directory target must be rejected rather than surfacing a sqlite error."""
859
+ (tmp_path / "mod.py").write_text("def fn(): pass\n", encoding="utf-8")
860
+ target = tmp_path / "graph.db"
861
+ target.mkdir()
862
+
863
+ result = cgis_ingest(str(tmp_path), str(target))
864
+
865
+ assert "❌" in result
866
+ assert target.is_dir()
867
+
868
+
869
+ def test_ingest_accepts_an_existing_empty_file(tmp_path: Path) -> None:
870
+ """An empty placeholder is what SQLite itself would create — not a clobber."""
871
+ (tmp_path / "mod.py").write_text("def fn(): pass\n", encoding="utf-8")
872
+ placeholder = tmp_path / "graph.db"
873
+ placeholder.touch()
874
+
875
+ result = cgis_ingest(str(tmp_path), str(placeholder))
876
+
877
+ assert "✅" in result
878
+ # Not just "it did not refuse": the placeholder must now be a real database,
879
+ # otherwise the guard could pass while ingestion quietly wrote nothing.
880
+ assert placeholder.stat().st_size > 0
881
+ with placeholder.open("rb") as fh:
882
+ assert fh.read(16) == b"SQLite format 3\x00"
883
+
884
+
885
+ def test_ingest_still_accepts_an_existing_database(tmp_path: Path) -> None:
886
+ """The guard must not break re-ingesting into a real database."""
887
+ (tmp_path / "mod.py").write_text("def fn(): pass\n", encoding="utf-8")
888
+ db = tmp_path / "graph.db"
889
+ assert "✅" in cgis_ingest(str(tmp_path), str(db))
890
+
891
+ result = cgis_ingest(str(tmp_path), str(db), full_rebuild=True)
892
+
893
+ assert "✅" in result
894
+
895
+
896
+ @pytest.mark.skipif(os.geteuid() == 0, reason="root bypasses directory permissions")
897
+ def test_ingest_reports_an_unreadable_path_instead_of_raising(tmp_path: Path) -> None:
898
+ """The guard runs before cgis_ingest's own try/except, so it must not raise.
899
+
900
+ `Path.is_dir()` and friends propagate OSError (a PermissionError on the
901
+ parent, for one). Without handling, that escapes the MCP tool as a crash
902
+ rather than a message the agent can act on.
903
+ """
904
+ (tmp_path / "mod.py").write_text("def fn(): pass\n", encoding="utf-8")
905
+ locked = tmp_path / "locked"
906
+ locked.mkdir()
907
+ (locked / "graph.db").write_text("junk", encoding="utf-8")
908
+ locked.chmod(0o000)
909
+ try:
910
+ result = cgis_ingest(str(tmp_path), str(locked / "graph.db"))
911
+ finally:
912
+ locked.chmod(0o755)
913
+
914
+ assert "❌" in result
915
+
916
+
917
+ def test_ingest_refuses_a_dangling_symlink(tmp_path: Path) -> None:
918
+ """A `.db` symlink whose target does not exist must not become a write primitive.
919
+
920
+ This is the bypass, and it hinges on the target being absent: `is_file()`
921
+ returns False for a dangling symlink, so every check passed and SQLite
922
+ happily created the target. A symlink pointing at an *existing* non-database
923
+ was already refused by the magic-byte check, so testing that variant would
924
+ have proved nothing.
925
+ """
926
+ (tmp_path / "mod.py").write_text("def fn(): pass\n", encoding="utf-8")
927
+ target = tmp_path / "elsewhere" / "authorized_keys"
928
+ target.parent.mkdir()
929
+ link = tmp_path / "attack.db"
930
+ link.symlink_to(target)
931
+
932
+ result = cgis_ingest(str(tmp_path), str(link))
933
+
934
+ assert "❌" in result
935
+ assert not target.exists(), "the symlink target was created"
936
+
937
+
938
+ def test_ingest_refuses_a_symlink_to_a_non_database(tmp_path: Path) -> None:
939
+ """The already-covered variant, kept as a regression guard."""
940
+ (tmp_path / "mod.py").write_text("def fn(): pass\n", encoding="utf-8")
941
+ victim = tmp_path / "secrets.txt"
942
+ victim.write_text("sensitive\n", encoding="utf-8")
943
+ link = tmp_path / "attack.db"
944
+ link.symlink_to(victim)
945
+
946
+ result = cgis_ingest(str(tmp_path), str(link))
947
+
948
+ assert "❌" in result
949
+ assert victim.read_text(encoding="utf-8") == "sensitive\n"
950
+
951
+
952
+ def test_ingest_accepts_an_uppercase_suffix(tmp_path: Path) -> None:
953
+ """Suffix matching is case-insensitive — `.DB` is the same intent as `.db`."""
954
+ (tmp_path / "mod.py").write_text("def fn(): pass\n", encoding="utf-8")
955
+ db = tmp_path / "graph.DB"
956
+
957
+ result = cgis_ingest(str(tmp_path), str(db))
958
+
959
+ assert "✅" in result
@@ -159,9 +159,11 @@ def test_compile_output_is_deterministic() -> None:
159
159
  nodes = [_make_node("src/cgis/cli.py:some_function")]
160
160
  edges = [_make_edge("src/cgis/cli.py:some_function", "src/cgis/other.py:helper")]
161
161
  compiler = MermaidCompiler()
162
- assert compiler.compile(nodes, edges) == compiler.compile(nodes, edges)
163
- # Hashed IDs must be alphanumeric — no raw slashes or colons in node declarations
164
162
  output = compiler.compile(nodes, edges)
163
+ second = compiler.compile(nodes, edges)
164
+ assert output == second, "compile output must be stable — IDs are hashed, not counted"
165
+
166
+ # Hashed IDs must be alphanumeric — no raw slashes or colons in node declarations
165
167
  for line in output.splitlines():
166
168
  if "-->|" in line:
167
169
  parts = line.strip().split(" -->|")
@@ -184,7 +184,9 @@ def test_propose_no_fit_goes_hygiene_only(tmp_path: Path) -> None:
184
184
 
185
185
  def test_propose_deterministic(two_domain_db: str) -> None:
186
186
  """Two runs over the same graph are byte-identical."""
187
- assert propose_ontology(two_domain_db) == propose_ontology(two_domain_db)
187
+ first = propose_ontology(two_domain_db)
188
+ second = propose_ontology(two_domain_db)
189
+ assert first == second, "propose_ontology must be byte-identical across runs"
188
190
 
189
191
 
190
192
  def test_header_templates_match_repo_ontology() -> None: