codegraph-brain 0.8.0__tar.gz → 0.10.0__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 (257) hide show
  1. codegraph_brain-0.10.0/.release-please-manifest.json +3 -0
  2. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/CHANGELOG.md +21 -0
  3. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/PKG-INFO +1 -1
  4. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/2026-07-31-finder-bug-class-taxonomy.md +211 -0
  5. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/pyproject.toml +1 -1
  6. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/extractors/python_extractor.py +60 -1
  7. codegraph_brain-0.10.0/src/cgis/guardian/axes.py +130 -0
  8. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/guardian/chunked.py +14 -21
  9. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/guardian/collector.py +1 -1
  10. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/guardian/findings.py +20 -0
  11. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/guardian/prompts.py +85 -32
  12. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/query/drift/fingerprint.py +118 -2
  13. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_fingerprint.py +171 -1
  14. codegraph_brain-0.10.0/tests/unit/test_guardian_axes.py +214 -0
  15. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_guardian_chunked.py +3 -4
  16. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_python_extractor.py +87 -0
  17. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/uv.lock +1 -1
  18. codegraph_brain-0.8.0/.release-please-manifest.json +0 -3
  19. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/.claude-plugin/marketplace.json +0 -0
  20. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/.github/workflows/autodoc.yml +0 -0
  21. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/.github/workflows/ci.yml +0 -0
  22. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/.github/workflows/guardian.yml +0 -0
  23. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/.github/workflows/pr-title.yml +0 -0
  24. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/.github/workflows/release-please.yml +0 -0
  25. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/.gitignore +0 -0
  26. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/.pre-commit-config.yaml +0 -0
  27. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/.python-version +0 -0
  28. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/CLAUDE.md +0 -0
  29. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/CONTRIBUTING.md +0 -0
  30. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/LICENSE +0 -0
  31. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/Makefile +0 -0
  32. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/PRIVACY.md +0 -0
  33. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/README.md +0 -0
  34. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/benchmarks/guardian/CURATION.md +0 -0
  35. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/benchmarks/guardian/pr-122.yaml +0 -0
  36. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/benchmarks/guardian/pr-140.yaml +0 -0
  37. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/benchmarks/guardian/pr-141.yaml +0 -0
  38. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/benchmarks/guardian/pr-142.yaml +0 -0
  39. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/benchmarks/guardian/pr-143.yaml +0 -0
  40. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/benchmarks/guardian/pr-144.yaml +0 -0
  41. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/benchmarks/guardian/pr-278.yaml +0 -0
  42. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/benchmarks/guardian/pr-313.yaml +0 -0
  43. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/benchmarks/guardian/results.jsonl +0 -0
  44. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/data/.gitkeep +0 -0
  45. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/AUDIT.md +0 -0
  46. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/CASE_STUDY.md +0 -0
  47. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/architecture/HOW_IT_WORKS.md +0 -0
  48. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/architecture/ONTOLOGY.md +0 -0
  49. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/architecture/PATTERNS_AND_TRIADS.md +0 -0
  50. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/architecture/SELF_PORTRAIT.md +0 -0
  51. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/architecture/diagrams/pipeline_flow.mermaid +0 -0
  52. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/architecture/health_badge.json +0 -0
  53. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/assets/.gitignore +0 -0
  54. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/assets/cgis-app-avatar.png +0 -0
  55. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/assets/cgis-app-avatar.svg +0 -0
  56. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/assets/generate_avatar.py +0 -0
  57. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/examples/.gitkeep +0 -0
  58. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/how-to/AGENT_ONBOARDING.md +0 -0
  59. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/how-to/CLI_USAGE.md +0 -0
  60. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/how-to/MCP_REFERENCE.md +0 -0
  61. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/lab-notes/2026-06-11-chunked-review-negative-result.md +0 -0
  62. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/ontology/.gitkeep +0 -0
  63. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/ontology/core.yaml +0 -0
  64. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/ontology/domains.yaml +0 -0
  65. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/ontology/patterns.yaml +0 -0
  66. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/ontology/tolerances.lock +0 -0
  67. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-09-domain-pattern-fingerprint-design.md +0 -0
  68. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-09-pattern-alphabet-motif-basis-design.md +0 -0
  69. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-10-guardian-sprint-design.md +0 -0
  70. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-11-fastapi-di-edges-design.md +0 -0
  71. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-11-guardian-chunked-review-design.md +0 -0
  72. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-11-guardian-chunker-design.md +0 -0
  73. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-11-mcp-drift-validate-fqn-design.md +0 -0
  74. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-11-resolver-split-design.md +0 -0
  75. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-11-symbol-import-edges-design.md +0 -0
  76. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-12-drift-empty-domains-design.md +0 -0
  77. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-12-gate-semantics-design.md +0 -0
  78. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-12-init-ontology-design.md +0 -0
  79. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-12-release-please-ci-design.md +0 -0
  80. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-13-suggest-packages-design.md +0 -0
  81. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-13-tangle-anti-pattern-design.md +0 -0
  82. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/2026-07-29-guardian-skeptic-scoring-design.md +0 -0
  83. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/2026-07-30-cgis-fractal-design.md +0 -0
  84. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/2026-07-30-cgis-fractal-plan.md +0 -0
  85. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/2026-07-30-chunk-source-filter-design.md +0 -0
  86. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/2026-07-30-chunk-source-filter-plan.md +0 -0
  87. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/2026-07-30-genai-client-close-design.md +0 -0
  88. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/2026-07-30-genai-client-close-plan.md +0 -0
  89. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/2026-07-30-guardian-precision-bench-design.md +0 -0
  90. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/2026-07-30-guardian-precision-bench-plan.md +0 -0
  91. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/2026-07-30-guardian-timeout-retry-design.md +0 -0
  92. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/2026-07-30-guardian-timeout-retry-plan.md +0 -0
  93. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/BLUEPRINT.md +0 -0
  94. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/PRD.md +0 -0
  95. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/TDD.md +0 -0
  96. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-09-fingerprint-drift.md +0 -0
  97. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-10-guardian-context-skeptic-inline.md +0 -0
  98. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-10-guardian-structured-findings-bench.md +0 -0
  99. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-10-motif-basis-part-b.md +0 -0
  100. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-10-unified-pattern-alphabet.md +0 -0
  101. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-11-fastapi-di-edges.md +0 -0
  102. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-11-guardian-chunked-review.md +0 -0
  103. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-11-guardian-chunker.md +0 -0
  104. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-11-mcp-drift-validate-fqn.md +0 -0
  105. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-11-resolver-split.md +0 -0
  106. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-12-drift-empty-domains.md +0 -0
  107. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-12-gate-semantics.md +0 -0
  108. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-12-init-ontology.md +0 -0
  109. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-12-release-please-ci.md +0 -0
  110. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-12-symbol-import-edges.md +0 -0
  111. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-13-suggest-packages.md +0 -0
  112. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-07-29-guardian-skeptic-scoring.md +0 -0
  113. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/main.py +0 -0
  114. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/plugin/.claude-plugin/plugin.json +0 -0
  115. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/plugin/.mcp.json +0 -0
  116. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/plugin/README.md +0 -0
  117. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/plugin/skills/cgis/SKILL.md +0 -0
  118. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/plugin/skills/ingest/SKILL.md +0 -0
  119. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/release-please-config.json +0 -0
  120. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/scripts/gen_ideal_graph.py +0 -0
  121. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/scripts/generate_health.py +0 -0
  122. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/scripts/generate_mcp_ref.py +0 -0
  123. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/scripts/generate_schema_docs.py +0 -0
  124. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/scripts/guardian_bench.py +0 -0
  125. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/scripts/guardian_review.py +0 -0
  126. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/scripts/inject_readme_graph.py +0 -0
  127. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/scripts/probe_closure_gap.py +0 -0
  128. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/scripts/probe_tier_ladder.py +0 -0
  129. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/__init__.py +0 -0
  130. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/__main__.py +0 -0
  131. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/api/.gitkeep +0 -0
  132. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/api/__init__.py +0 -0
  133. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/api/mcp_server.py +0 -0
  134. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/cli.py +0 -0
  135. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/core/.gitkeep +0 -0
  136. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/core/models.py +0 -0
  137. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/extractors/.gitkeep +0 -0
  138. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/extractors/_python_ast.py +0 -0
  139. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/extractors/_python_classes.py +0 -0
  140. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/extractors/_python_functions.py +0 -0
  141. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/extractors/_python_imports.py +0 -0
  142. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/extractors/_python_types.py +0 -0
  143. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/extractors/base.py +0 -0
  144. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/extractors/typescript_extractor.py +0 -0
  145. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/guardian/__init__.py +0 -0
  146. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/guardian/bench.py +0 -0
  147. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/guardian/chunker.py +0 -0
  148. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/guardian/core.py +0 -0
  149. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/guardian/diff_index.py +0 -0
  150. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/guardian/github_poster.py +0 -0
  151. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/guardian/metrics.py +0 -0
  152. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/guardian/providers/__init__.py +0 -0
  153. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/guardian/providers/base.py +0 -0
  154. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/guardian/providers/gemini.py +0 -0
  155. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/guardian/providers/mistral.py +0 -0
  156. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/guardian/providers/ollama.py +0 -0
  157. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/guardian/recording.py +0 -0
  158. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/guardian/render.py +0 -0
  159. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/guardian/runner.py +0 -0
  160. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/guardian/skeptic.py +0 -0
  161. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/pipeline.py +0 -0
  162. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/py.typed +0 -0
  163. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/query/analysis/__init__.py +0 -0
  164. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/query/analysis/analyzer.py +0 -0
  165. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/query/analysis/anomaly.py +0 -0
  166. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/query/analysis/cohesion.py +0 -0
  167. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/query/analysis/health.py +0 -0
  168. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/query/analysis/suggest_service.py +0 -0
  169. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/query/context/__init__.py +0 -0
  170. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/query/context/audit.py +0 -0
  171. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/query/context/context_service.py +0 -0
  172. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/query/context/prompt.py +0 -0
  173. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/query/context/snippet.py +0 -0
  174. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/query/drift/__init__.py +0 -0
  175. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/query/drift/_scc.py +0 -0
  176. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/query/drift/drift.py +0 -0
  177. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/query/drift/drift_service.py +0 -0
  178. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/query/drift/fractal.py +0 -0
  179. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/query/drift/ontology_init.py +0 -0
  180. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/query/drift/quotient.py +0 -0
  181. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/query/drift/triads.py +0 -0
  182. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/query/engine.py +0 -0
  183. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/query/fqn.py +0 -0
  184. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/query/render/__init__.py +0 -0
  185. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/query/render/graph_json.py +0 -0
  186. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/query/render/mermaid.py +0 -0
  187. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/query/render/metrics.py +0 -0
  188. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/resolver/.gitkeep +0 -0
  189. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/resolver/__init__.py +0 -0
  190. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/resolver/engine.py +0 -0
  191. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/resolver/indices.py +0 -0
  192. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/resolver/symbols.py +0 -0
  193. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/resolver/uplift.py +0 -0
  194. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/storage/.gitkeep +0 -0
  195. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/src/cgis/storage/sqlite_store.py +0 -0
  196. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/integration/.gitkeep +0 -0
  197. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/self_parsing/.gitkeep +0 -0
  198. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/self_parsing/__init__.py +0 -0
  199. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/self_parsing/conftest.py +0 -0
  200. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/self_parsing/test_architecture.py +0 -0
  201. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/self_parsing/test_drift.py +0 -0
  202. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/self_parsing/test_fractal.py +0 -0
  203. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/self_parsing/test_init_ontology_roundtrip.py +0 -0
  204. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/self_parsing/test_self_parse.py +0 -0
  205. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/self_parsing/test_self_parse_ts.py +0 -0
  206. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/self_parsing/test_suggest.py +0 -0
  207. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/.gitkeep +0 -0
  208. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/conftest.py +0 -0
  209. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/guardian_stubs.py +0 -0
  210. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test___main__.py +0 -0
  211. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_analyzer.py +0 -0
  212. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_audit.py +0 -0
  213. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_cli.py +0 -0
  214. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_cohesion.py +0 -0
  215. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_context_service.py +0 -0
  216. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_di_acceptance.py +0 -0
  217. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_drift.py +0 -0
  218. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_drift_service.py +0 -0
  219. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_fqn.py +0 -0
  220. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_fractal.py +0 -0
  221. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_gen_ideal_graph.py +0 -0
  222. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_generate_mcp_ref.py +0 -0
  223. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_graph_json.py +0 -0
  224. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_guardian_bench.py +0 -0
  225. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_guardian_chunker.py +0 -0
  226. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_guardian_collector.py +0 -0
  227. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_guardian_core.py +0 -0
  228. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_guardian_diff_index.py +0 -0
  229. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_guardian_findings.py +0 -0
  230. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_guardian_metrics.py +0 -0
  231. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_guardian_poster.py +0 -0
  232. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_guardian_providers.py +0 -0
  233. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_guardian_recording.py +0 -0
  234. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_guardian_render.py +0 -0
  235. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_guardian_runner.py +0 -0
  236. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_guardian_skeptic.py +0 -0
  237. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_health_scorer.py +0 -0
  238. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_import_acceptance.py +0 -0
  239. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_mcp_server.py +0 -0
  240. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_mermaid.py +0 -0
  241. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_metrics.py +0 -0
  242. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_models.py +0 -0
  243. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_ontology_compliance.py +0 -0
  244. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_ontology_init.py +0 -0
  245. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_patterns_yaml.py +0 -0
  246. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_pipeline.py +0 -0
  247. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_prompt.py +0 -0
  248. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_quotient.py +0 -0
  249. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_resolver.py +0 -0
  250. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_resolver_indices.py +0 -0
  251. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_resolver_symbols.py +0 -0
  252. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_snippet.py +0 -0
  253. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_sqlite_store.py +0 -0
  254. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_suggest_service.py +0 -0
  255. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_triads.py +0 -0
  256. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_typescript_extractor.py +0 -0
  257. {codegraph_brain-0.8.0 → codegraph_brain-0.10.0}/tests/unit/test_uplift.py +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.10.0"
3
+ }
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.0](https://github.com/zaebee/codegraph-brain/compare/codegraph-brain-v0.9.0...codegraph-brain-v0.10.0) (2026-08-01)
4
+
5
+
6
+ ### Features
7
+
8
+ * **guardian:** kinship-grouped axis batches behind GUARDIAN_FEATURES=axes_paired ([#334](https://github.com/zaebee/codegraph-brain/issues/334)) ([7951dcb](https://github.com/zaebee/codegraph-brain/commit/7951dcb60a2fff2f232b144af76043a9d06d3eee))
9
+ * **guardian:** per-axis review fan-out behind GUARDIAN_FEATURES=axes ([#333](https://github.com/zaebee/codegraph-brain/issues/333)) ([ada3205](https://github.com/zaebee/codegraph-brain/commit/ada3205e54433356f3a1e9ef4944ed5475ed2d7c)), closes [#331](https://github.com/zaebee/codegraph-brain/issues/331)
10
+
11
+
12
+ ### Documentation
13
+
14
+ * **spec:** record the Arm A result — fails its gate, and shows the mechanism ([#330](https://github.com/zaebee/codegraph-brain/issues/330)) ([af8d543](https://github.com/zaebee/codegraph-brain/commit/af8d543fb656af233a961ef081bc8fed5f4331a0))
15
+
16
+ ## [0.9.0](https://github.com/zaebee/codegraph-brain/compare/codegraph-brain-v0.8.0...codegraph-brain-v0.9.0) (2026-08-01)
17
+
18
+
19
+ ### Features
20
+
21
+ * **drift:** look through transparent re-exports in the IMPORTS census ([#329](https://github.com/zaebee/codegraph-brain/issues/329)) ([25b2ce6](https://github.com/zaebee/codegraph-brain/commit/25b2ce6049ae878ebeef05cd47b08dd67faa763d))
22
+ * **extractor:** detect transparent re-exports ([#182](https://github.com/zaebee/codegraph-brain/issues/182) direction 1, slice 1) ([#327](https://github.com/zaebee/codegraph-brain/issues/327)) ([6e6e654](https://github.com/zaebee/codegraph-brain/commit/6e6e654b4ce17c52487507d2d425a4f81045f731))
23
+
3
24
  ## [0.8.0](https://github.com/zaebee/codegraph-brain/compare/codegraph-brain-v0.7.5...codegraph-brain-v0.8.0) (2026-08-01)
4
25
 
5
26
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codegraph-brain
3
- Version: 0.8.0
3
+ Version: 0.10.0
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
@@ -243,3 +243,214 @@ say so explicitly rather than reporting the combined delta.
243
243
  Unchanged from above, plus: do not run Arm B before Arm A has a number. The
244
244
  float few-shot regression happened because two effects were entangled in one
245
245
  measurement.
246
+
247
+ ---
248
+
249
+ # Arm A result — 2026-08-01: fails its gate, and shows why
250
+
251
+ Run with the prod pairing (finder `mistral-medium-latest`, skeptic
252
+ `gemini-2.5-flash`), one run per fixture over all eight, baseline and treatment
253
+ sweeps back to back.
254
+
255
+ **Treatment:** each of five focus areas gained one clause naming what makes an
256
+ instance *not* a finding — a guard that dominates the use, an existing tolerance
257
+ compare, a test that already reaches the path, `Any` as the honest type of
258
+ parsed data. No per-class few-shot, per #247.
259
+
260
+ ## The gate says no
261
+
262
+ | PR | noise | matched |
263
+ |---|---|---|
264
+ | 122 | 11 → **19** | 4 → 4 |
265
+ | 140 | 20 → 13 | 6 → **5** |
266
+ | 141 | 7 → **8** | 0 → 0 |
267
+ | 142 | 0 → 0 | 0 → 0 |
268
+ | 143 | 6 → **14** | 4 → **3** |
269
+ | 144 | 6 → 2 | 2 → 2 |
270
+ | 278 | 7 → 7 | 1 → 1 |
271
+ | **313** | 6 → 4 | **0 → 2** |
272
+
273
+ ```
274
+ 1. mean noise -25% FAIL 7.88 -> 8.38 (+6%, wrong direction)
275
+ 2. matched drops nowhere FAIL pr-140, pr-143
276
+ 3. pr-141 does not regress FAIL 7 -> 8
277
+ ```
278
+
279
+ Three of three. The clauses do not ship.
280
+
281
+ ## The finding inside the failure
282
+
283
+ **pr-313 went from recall 0.000 to 1.000** — the finder surfaced both ground-truth
284
+ entries it had previously missed entirely. Precisely: one is categorised
285
+ `security` (`dangling-symlink-write-primitive`) and one `logic`
286
+ (`oserror-escapes-the-tool`), so this is one security-class hit plus one logic
287
+ hit, not two security hits. That is the fixture added in #315 because the finder
288
+ was blind to the class.
289
+
290
+ And the total is unchanged: **matched 17 → 17**. Two lost on pr-140/143, two
291
+ gained on pr-313. Attention moved; precision did not improve.
292
+
293
+ That is the same mechanism that killed the float few-shot experiment: the model
294
+ reallocates focus rather than applying an extra criterion. It is worth stating
295
+ plainly because the naive reading of "noise went up, recall moved around" is
296
+ "the change did nothing", and that is not what happened — it did something
297
+ specific and undesired.
298
+
299
+ ## What this implies for Arm B
300
+
301
+ It strengthens the case for testing Arm B rather than weakening it. If a clause
302
+ that merely *mentions* guards can pull the finder onto a class it was blind to,
303
+ an explicit named class is a more direct instrument for the same effect — and
304
+ Arm B's gate is written around exactly that (pr-278 or pr-313 matched at least
305
+ once in three runs).
306
+
307
+ It also confirms the sequencing rule already in this document: these two effects
308
+ must not be measured together. Had both shipped at once, the pr-313 gain and the
309
+ pr-140/143 loss would have arrived as one number.
310
+
311
+ ## Honest limits
312
+
313
+ - **n=1 per arm.** Finder variance on an unchanged diff has been measured at
314
+ 6 → 46 → 36 findings. Criteria 2 and 3 turn on single-finding deltas and are
315
+ fragile at this sample size. Criterion 1 is the sturdier signal: noise moved
316
+ the wrong way, by +6% across eight fixtures.
317
+ - Cost: ~1.1M tokens for the two sweeps.
318
+
319
+ ## Why the raw rows are not in results.jsonl
320
+
321
+ Both arms carry the same `guardian_sha` — the treatment was a working-tree edit,
322
+ never committed — so the rows would be indistinguishable once merged into the
323
+ corpus. The aggregate above is the record. Anyone repeating this should either
324
+ commit the prompt variant behind a flag first, or add an arm label to the
325
+ metrics row.
326
+
327
+ **Methodology note for whoever repeats this:** the first attempt was ruined by
328
+ launching the sweep twice against one results file, after an empty (buffered)
329
+ log was misread as a dead process. Check for a running bench by process name,
330
+ not by looking at its log.
331
+
332
+ `pgrep -f guardian_bench.py` is **not** enough — measured with the bench fully
333
+ stopped it still returns 2, because the shell wrapper running the check carries
334
+ the pattern in its own command line. pgrep excludes its own pid, not its
335
+ parent's. Match on the process name instead:
336
+
337
+ ```sh
338
+ ps -eo comm,cmd | awk '$1 ~ /^python/ && /guardian_bench\.py/' | wc -l
339
+ ```
340
+
341
+ Returns 0 when nothing is running. A false positive here is not harmless: it
342
+ fires the pre-launch guard and silently skips the sweep.
343
+
344
+ ---
345
+
346
+ # Per-axis fan-out (#331) — measured 2026-08-01: the hypothesis holds, the cost does not
347
+
348
+ Seven finder calls per PR, one per focus area, each seeing the whole diff.
349
+ Stopped at four of eight fixtures: the verdict was already decided and the
350
+ remaining four could not change it.
351
+
352
+ | PR | noise | recall |
353
+ |---|---|---|
354
+ | 122 | 11 → **55** (5.0×) | 0.364 → 0.455 |
355
+ | 140 | 20 → **70** (3.5×) | 0.400 → 0.533 |
356
+ | 141 | 7 → **39** (5.6×) | 1.000 → 1.000 |
357
+ | 142 | 0 → 0 | 0.000 → 0.000 |
358
+
359
+ Mean noise **9.5 → 41.0, a 4.3× rise**. Gate criterion 1 fails by an order of
360
+ magnitude, not a margin.
361
+
362
+ ## Both halves of the hypothesis were right
363
+
364
+ **Separating axes does lift recall** — up on every fixture that had anything to
365
+ find (+0.09, +0.13). Attention competition is real, and removing it works.
366
+
367
+ **And it multiplies noise, exactly as chunking did.** pr-141 is the sharpest
368
+ evidence: it is the noise probe, recall was already 1.000, there was nothing to
369
+ gain — and noise still rose 5.6×. On that fixture the change bought only noise.
370
+
371
+ Raw finding counts show the mechanism directly: 126, 139 and 94 findings per PR
372
+ before dedup (82, 102, 60 after), against roughly 15 for the single prompt.
373
+ Every extra call is another opportunity to invent something.
374
+
375
+ ## Why it stopped at four
376
+
377
+ #331 pre-registered the rule: *"If noise inflates but per-axis recall improves,
378
+ that is a result, not a partial pass — record it and stop, exactly as #160 did."*
379
+ The multiplier was consistent across all four, so four more fixtures would have
380
+ cost real money to confirm a verdict already in hand.
381
+
382
+ ## What the numbers say about the fix
383
+
384
+ Noise scaled roughly **linearly with call count**: 7 calls, 4.3×. That predicts
385
+ ~1.8× at three calls and ~1.2× at two — the last of which would sit close enough
386
+ to baseline to pass criterion 1 while keeping part of the recall.
387
+
388
+ So the next variant is not "abandon fan-out", it is "fan out less". Shipped
389
+ behind `GUARDIAN_FEATURES=axes_paired`: two calls, grouped by what a defect *is*
390
+ rather than arbitrarily —
391
+
392
+ - **correctness**: logic, unvalidated external data, float equality
393
+ - **contracts**: type safety, library boundaries, ontology, test coverage
394
+
395
+ Kin travel together because the two measured competitions (#247, #330) were both
396
+ between *dissimilar* classes.
397
+
398
+ A caveat for whoever runs it: there is no `security` axis to separate, since Arm
399
+ B was never shipped. The recall gains above therefore came from separation in
400
+ general, not from isolating a blind class — so this variant tests the cheaper
401
+ half of the idea, and the blind-class question stays open in #258.
402
+
403
+ ## Paired grouping — measured the same day, on the same four fixtures
404
+
405
+ Two calls instead of seven, batched by kinship.
406
+
407
+ | PR | baseline | paired (2) | per-axis (7) |
408
+ |---|---|---|---|
409
+ | 122 | 11 / 0.364 | **21 / 0.545** | 55 / 0.455 |
410
+ | 140 | 20 / 0.400 | 27 / **0.333** | 70 / 0.533 |
411
+ | 141 | 7 / 1.000 | **9** / 1.000 | 39 / 1.000 |
412
+ | 142 | 0 / 0.000 | 0 / 0.000 | 0 / 0.000 |
413
+
414
+ ```
415
+ mean noise 9.50 -> 14.25 (1.5x) -> 41.00 (4.3x)
416
+ mean recall 0.441 -> 0.470 -> 0.497
417
+ ```
418
+
419
+ ### The linear model held, and the recall did too
420
+
421
+ Noise was predicted at ~1.2× for two calls from the linear-in-call-count fit;
422
+ the measurement is **1.5×**, against 4.3× at seven. The order of magnitude was
423
+ right.
424
+
425
+ The part that was **not** predicted: recall barely moved. Seven calls give
426
+ 0.497, two give 0.470 — **three times less noise for 0.027 of recall.** Most of
427
+ the benefit of separation is already there at two calls.
428
+
429
+ On pr-122 two calls beat seven on *both* axes — recall 0.545 against 0.455, at
430
+ noise 21 against 55. More calls is not more recall: seven axes appear to
431
+ fragment too far, leaving each call too little to reason with.
432
+
433
+ ### It still fails the gate
434
+
435
+ - **Criterion 1** — noise must not exceed baseline. 1.5× does not pass.
436
+ - **Criterion 3** — no PR more than 0.05 below baseline recall. pr-140 dropped
437
+ 0.400 → 0.333.
438
+
439
+ So this does not ship either. But it is a qualitatively different result from
440
+ the seven-axis run: not "the hypothesis failed" but "the cost is down threefold
441
+ and 1.5× remains".
442
+
443
+ ### Limits
444
+
445
+ n=1 per variant across four fixtures, against a finder whose variance on an
446
+ unchanged diff has been measured at 6 → 46 → 36 findings. The gap between 0.470
447
+ and 0.497 is well inside that. The noise multipliers (1.5× vs 4.3×) are the
448
+ sturdy part — they are large and consistent per fixture.
449
+
450
+ ### What it points at
451
+
452
+ Since recall survives batching, the next thing worth testing is **one call that
453
+ walks the axes in sequence** — separation of attention without a second call at
454
+ all. Failing that, keep two calls and attack the residual noise from the skeptic
455
+ side, which is currently tuned for single-prompt volume rather than for a merged
456
+ union.
@@ -7,7 +7,7 @@ packages = ["src/cgis"]
7
7
 
8
8
  [project]
9
9
  name = "codegraph-brain"
10
- version = "0.8.0"
10
+ version = "0.10.0"
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"
@@ -1,5 +1,7 @@
1
1
  """Implements Python Extractor."""
2
2
 
3
+ import re
4
+
3
5
  import tree_sitter_python as tspython
4
6
  from tree_sitter import Language, Parser
5
7
  from tree_sitter import Node as BaseNode
@@ -29,6 +31,61 @@ def file_path_to_module_fqn(file_path: str, source_root: str | None = None) -> s
29
31
  return _file_path_to_module_fqn(file_path, source_root)
30
32
 
31
33
 
34
+ def _identifiers_outside_imports(root: BaseNode, code_bytes: bytes) -> set[str]:
35
+ """Every identifier used in the module body, excluding import statements.
36
+
37
+ The walker already refuses to recurse into import nodes, so the same rule is
38
+ applied here: a name that appears *only* in an import is never used by this
39
+ module.
40
+ """
41
+ used: set[str] = set()
42
+ stack = [root]
43
+ while stack:
44
+ node = stack.pop()
45
+ if node.type in ("import_statement", "import_from_statement"):
46
+ continue
47
+ if node.type == "identifier":
48
+ # `o.thing` and `f(thing=1)` mention `thing` without using the imported
49
+ # name — counting them would let an unrelated attribute or keyword
50
+ # silence a real re-export of that name. Only the `attribute`/`name`
51
+ # field is excluded; the object side of `o.thing` is a genuine use.
52
+ parent = node.parent
53
+ shadowed = parent is not None and (
54
+ (parent.type == "attribute" and parent.child_by_field_name("attribute") == node)
55
+ or (
56
+ parent.type == "keyword_argument" and parent.child_by_field_name("name") == node
57
+ )
58
+ )
59
+ if not shadowed:
60
+ used.add(code_bytes[node.start_byte : node.end_byte].decode("utf8"))
61
+ elif node.type == "string" and node.parent is not None and node.parent.type == "type":
62
+ # A forward-reference annotation — `store: "SQLiteStore | None"` — holds
63
+ # no identifier nodes, so without this a TYPE_CHECKING import used only
64
+ # in quoted annotations reads as unused. Restricted to annotation
65
+ # strings (parent `type`): scanning every string would let a docstring
66
+ # mentioning a name mask a real re-export.
67
+ text = code_bytes[node.start_byte : node.end_byte].decode("utf8")
68
+ used.update(re.findall(r"[A-Za-z_][A-Za-z0-9_]*", text))
69
+ stack.extend(node.children)
70
+ return used
71
+
72
+
73
+ def find_reexports(root: BaseNode, code_bytes: bytes, import_map: dict[str, str]) -> dict[str, str]:
74
+ """Return the imports this module never uses — i.e. passes straight through (#182).
75
+
76
+ A transparent re-export lets one module absorb another's import edges, so an
77
+ N-way coupling can be linearised on paper without changing what actually
78
+ depends on what. Naming them is the precondition for the IMPORTS census
79
+ being able to look through them.
80
+
81
+ "Imported but unused" is the whole test, and in a linted tree that is a
82
+ precise proxy: ruff's F401 removes genuine dead imports, so what survives is
83
+ deliberate — `X as X`, an `__all__` entry, or an explicit noqa.
84
+ """
85
+ used = _identifiers_outside_imports(root, code_bytes)
86
+ return {local: target for local, target in import_map.items() if local not in used}
87
+
88
+
32
89
  class PythonExtractor(BaseExtractor):
33
90
  """
34
91
  A concrete extractor for Python source code using tree-sitter.
@@ -83,6 +140,8 @@ class PythonExtractor(BaseExtractor):
83
140
  update={"metadata": {**nodes[i].metadata, "local_types": lt}}
84
141
  )
85
142
 
143
+ reexports = find_reexports(root_node, code_bytes, import_map)
144
+
86
145
  file_node = Node(
87
146
  id=module_fqn,
88
147
  type=NodeType.FILE,
@@ -90,7 +149,7 @@ class PythonExtractor(BaseExtractor):
90
149
  file_path=file_path,
91
150
  start_line=1,
92
151
  end_line=root_node.end_point.row + 1,
93
- metadata={"import_map": import_map},
152
+ metadata={"import_map": import_map, "reexports": reexports},
94
153
  )
95
154
  nodes.insert(0, file_node)
96
155
 
@@ -0,0 +1,130 @@
1
+ """Per-axis review: one finder call per focus area instead of one carrying all (#331).
2
+
3
+ The finder appears to work against a roughly fixed attention budget spread over
4
+ the axes named in a single prompt. Editing that prompt reallocates the budget
5
+ rather than adding capacity — measured twice:
6
+
7
+ - a targeted float few-shot cracked nothing and dropped pr-144 from 3/5 to 1/5,
8
+ pulling focus off the yaml class it had been catching (#247);
9
+ - exculpating clauses took pr-313 from recall 0.000 to 1.000 while the corpus
10
+ total stayed at matched 17 -> 17, two lost elsewhere (#330).
11
+
12
+ Axes cannot compete for attention if they are not in the same context window.
13
+ Each call still sees the *whole* diff — only the question narrows, which is what
14
+ separates this from chunked review (#154), where each call saw a fragment and
15
+ part of the failure was dilution inside the chunk.
16
+
17
+ The known risk is the one chunking failed on: every extra call is another
18
+ opportunity to invent a false positive, and there noise grew superlinearly
19
+ (#160). That is why this ships behind a flag and why the pre-registered gate in
20
+ #331 puts noise first.
21
+ """
22
+
23
+ import asyncio
24
+
25
+ import structlog
26
+
27
+ from cgis.guardian.collector import ContextCollector
28
+ from cgis.guardian.core import finder_pass
29
+ from cgis.guardian.findings import Finding, ReviewResult, dedup_findings
30
+ from cgis.guardian.prompts import AXIS_GROUPS, PER_AXIS_GROUPS
31
+ from cgis.guardian.providers.base import BaseProvider
32
+ from cgis.guardian.skeptic import apply_judgements, judge_all, skeptic_status_for
33
+
34
+ log = structlog.getLogger(__name__)
35
+
36
+
37
+ async def _axis_pass(
38
+ provider: BaseProvider, context: dict[str, str], label: str, axes: tuple[str, ...]
39
+ ) -> ReviewResult | None:
40
+ """Run the finder for one axis; None when that axis's call fails.
41
+
42
+ A flaky call must cost one axis, not the review — the same guard the chunked
43
+ orchestrator puts around each chunk.
44
+ """
45
+ try:
46
+ return await finder_pass(provider, {**context, "focus_group": ",".join(axes)})
47
+ except Exception:
48
+ log.warning("Axis finder call failed; group skipped.", group=label, exc_info=True)
49
+ return None
50
+
51
+
52
+ async def run_axis_review(
53
+ *,
54
+ provider: BaseProvider,
55
+ collector: ContextCollector,
56
+ skeptic_provider: BaseProvider | None,
57
+ concurrency: int = 4,
58
+ ) -> ReviewResult:
59
+ """Fan out over focus areas, merge, dedup, then one skeptic pass over the union.
60
+
61
+ One skeptic pass rather than one per axis: judging is already the larger half
62
+ of the token bill, and per-axis judging would multiply it for no expected
63
+ gain — the axes disagree about *what to look for*, not about whether a given
64
+ finding is real.
65
+ """
66
+ context = collector.collect_all()
67
+ # "axes" = one call per axis; "axes_paired" = kinship batches (#331 follow-up).
68
+ groups = AXIS_GROUPS if "axes_paired" in collector.features else PER_AXIS_GROUPS
69
+ axes = list(groups)
70
+
71
+ semaphore = asyncio.Semaphore(concurrency)
72
+
73
+ async def guarded(axis: str) -> ReviewResult | None:
74
+ async with semaphore:
75
+ return await _axis_pass(provider, context, axis, groups[axis])
76
+
77
+ results = await asyncio.gather(*(guarded(axis) for axis in axes))
78
+
79
+ findings: list[Finding] = []
80
+ summaries: list[str] = []
81
+ failed = 0
82
+ for axis, result in zip(axes, results, strict=True):
83
+ if result is None:
84
+ failed += 1
85
+ continue
86
+ if result.parse_failed:
87
+ # Unparseable output is not a finding-free axis; count it as failed so
88
+ # the summary cannot read as "this axis found nothing".
89
+ failed += 1
90
+ log.warning("Axis output did not parse; axis skipped.", axis=axis)
91
+ continue
92
+ findings.extend(result.findings)
93
+ summaries.append(f"- [{axis}]: {result.summary}")
94
+
95
+ if failed == len(axes):
96
+ return ReviewResult(
97
+ findings=[],
98
+ summary="Every axis pass failed; no review produced.",
99
+ parse_failed=True,
100
+ )
101
+
102
+ merged = ReviewResult(
103
+ findings=dedup_findings(findings),
104
+ summary="\n".join(summaries),
105
+ )
106
+ log.info(
107
+ "Axis fan-out complete.",
108
+ axes=len(axes),
109
+ failed=failed,
110
+ raw_findings=len(findings),
111
+ after_dedup=len(merged.findings),
112
+ )
113
+
114
+ if skeptic_provider is None or not merged.findings:
115
+ return merged
116
+
117
+ judgements = await judge_all(
118
+ skeptic_provider, merged.findings, context.get("diff", ""), concurrency
119
+ )
120
+ judged = sum(1 for j in judgements if j is not None)
121
+ if judged == 0:
122
+ log.warning("Every skeptic judgement failed; returning unverified findings.")
123
+ return merged.model_copy(
124
+ update={
125
+ "findings": apply_judgements(merged.findings, judgements),
126
+ "skeptic_status": skeptic_status_for(judged, len(judgements)),
127
+ "skeptic_judged": judged,
128
+ "skeptic_total": len(judgements),
129
+ }
130
+ )
@@ -8,10 +8,11 @@ complete world instead — its own diff, full files, and impact graph.
8
8
  import structlog
9
9
  from pydantic import BaseModel
10
10
 
11
+ from cgis.guardian.axes import run_axis_review
11
12
  from cgis.guardian.chunker import Chunk, build_chunks, split_diff_by_file
12
13
  from cgis.guardian.collector import ContextCollector
13
14
  from cgis.guardian.core import GuardianReviewer, finder_pass
14
- from cgis.guardian.findings import Finding, ReviewResult
15
+ from cgis.guardian.findings import Finding, ReviewResult, dedup_findings
15
16
  from cgis.guardian.providers.base import BaseProvider
16
17
  from cgis.guardian.skeptic import (
17
18
  apply_judgements,
@@ -85,25 +86,6 @@ def _chunk_survivors(chunk: Chunk, findings: list[Finding]) -> list[Finding]:
85
86
  return survivors
86
87
 
87
88
 
88
- def _dedup(findings: list[Finding]) -> list[Finding]:
89
- """Drop duplicate (file, line, category) findings, keeping the higher confidence.
90
-
91
- Cross-chunk duplicates are impossible after the per-chunk file filter
92
- (chunks partition files) — this is insurance against intra-pass
93
- duplicates. First-occurrence order is preserved.
94
- """
95
- best: dict[tuple[str, int | None, str], Finding] = {}
96
- order: list[tuple[str, int | None, str]] = []
97
- for finding in findings:
98
- key = (finding.file, finding.line, finding.category)
99
- if key not in best:
100
- best[key] = finding
101
- order.append(key)
102
- elif finding.confidence > best[key].confidence:
103
- best[key] = finding
104
- return [best[k] for k in order]
105
-
106
-
107
89
  async def _single_pass(
108
90
  provider: BaseProvider,
109
91
  collector: ContextCollector,
@@ -187,7 +169,7 @@ async def run_chunked_review(
187
169
  bullets.append(f"- [{label}]: {result.summary}")
188
170
 
189
171
  merged = ReviewResult(
190
- findings=_dedup(kept),
172
+ findings=dedup_findings(kept),
191
173
  summary="\n".join(bullets),
192
174
  parse_failed=failed == len(chunks),
193
175
  )
@@ -229,6 +211,17 @@ async def run_review_routed(
229
211
  degrade to all-isolated chunks = one API call per file with zero
230
212
  connectivity benefit — strictly worse than the status quo.
231
213
  """
214
+ if collector.features & {"axes", "axes_paired"}:
215
+ # Per-axis fan-out (#331) is checked first: it needs no graph DB, and
216
+ # combining it with chunked would multiply calls by axes x chunks while
217
+ # entangling two effects in one measurement — the mistake #330 recorded.
218
+ return RoutedReview(
219
+ result=await run_axis_review(
220
+ provider=provider, collector=collector, skeptic_provider=skeptic_provider
221
+ ),
222
+ chunk_count=None,
223
+ )
224
+
232
225
  chunked = "chunked" in collector.features
233
226
  if chunked and (collector.db_path is None or not collector.db_path.exists()):
234
227
  log.warning("chunked requested but no graph DB; falling back to single pass.")
@@ -16,7 +16,7 @@ from cgis.storage.sqlite_store import SQLiteStore
16
16
 
17
17
  log = structlog.getLogger(__name__)
18
18
 
19
- VALID_FEATURES = frozenset({"full_files", "flow", "drift", "chunked"})
19
+ VALID_FEATURES = frozenset({"full_files", "flow", "drift", "chunked", "axes", "axes_paired"})
20
20
 
21
21
  _MAX_FILE_LINES = 1200
22
22
  _MAX_TOTAL_CHARS = 120_000
@@ -72,3 +72,23 @@ def extract_json(text: str) -> str:
72
72
  # No newline before the closing fence (e.g. `{...}```` on one line):
73
73
  # valid JSON never ends with backticks, so stripping the suffix is safe.
74
74
  return body.strip().removesuffix("```").strip()
75
+
76
+
77
+ def dedup_findings(findings: list[Finding]) -> list[Finding]:
78
+ """Drop duplicate (file, line, category) findings, keeping the higher confidence.
79
+
80
+ Shared by every fan-out reviewer: chunked review partitions files so
81
+ cross-chunk duplicates cannot occur and this is insurance, but per-axis
82
+ review (#331) asks several axes about the same lines, where collisions are
83
+ expected rather than exceptional. First-occurrence order is preserved.
84
+ """
85
+ best: dict[tuple[str, int | None, str], Finding] = {}
86
+ order: list[tuple[str, int | None, str]] = []
87
+ for finding in findings:
88
+ key = (finding.file, finding.line, finding.category)
89
+ if key not in best:
90
+ best[key] = finding
91
+ order.append(key)
92
+ elif finding.confidence > best[key].confidence:
93
+ best[key] = finding
94
+ return [best[k] for k in order]