codegraph-brain 0.9.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.9.0 → codegraph_brain-0.10.0}/CHANGELOG.md +13 -0
  3. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/PKG-INFO +1 -1
  4. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/2026-07-31-finder-bug-class-taxonomy.md +211 -0
  5. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/pyproject.toml +1 -1
  6. codegraph_brain-0.10.0/src/cgis/guardian/axes.py +130 -0
  7. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/guardian/chunked.py +14 -21
  8. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/guardian/collector.py +1 -1
  9. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/guardian/findings.py +20 -0
  10. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/guardian/prompts.py +85 -32
  11. codegraph_brain-0.10.0/tests/unit/test_guardian_axes.py +214 -0
  12. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_guardian_chunked.py +3 -4
  13. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/uv.lock +1 -1
  14. codegraph_brain-0.9.0/.release-please-manifest.json +0 -3
  15. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/.claude-plugin/marketplace.json +0 -0
  16. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/.github/workflows/autodoc.yml +0 -0
  17. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/.github/workflows/ci.yml +0 -0
  18. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/.github/workflows/guardian.yml +0 -0
  19. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/.github/workflows/pr-title.yml +0 -0
  20. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/.github/workflows/release-please.yml +0 -0
  21. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/.gitignore +0 -0
  22. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/.pre-commit-config.yaml +0 -0
  23. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/.python-version +0 -0
  24. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/CLAUDE.md +0 -0
  25. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/CONTRIBUTING.md +0 -0
  26. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/LICENSE +0 -0
  27. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/Makefile +0 -0
  28. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/PRIVACY.md +0 -0
  29. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/README.md +0 -0
  30. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/benchmarks/guardian/CURATION.md +0 -0
  31. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/benchmarks/guardian/pr-122.yaml +0 -0
  32. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/benchmarks/guardian/pr-140.yaml +0 -0
  33. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/benchmarks/guardian/pr-141.yaml +0 -0
  34. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/benchmarks/guardian/pr-142.yaml +0 -0
  35. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/benchmarks/guardian/pr-143.yaml +0 -0
  36. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/benchmarks/guardian/pr-144.yaml +0 -0
  37. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/benchmarks/guardian/pr-278.yaml +0 -0
  38. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/benchmarks/guardian/pr-313.yaml +0 -0
  39. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/benchmarks/guardian/results.jsonl +0 -0
  40. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/data/.gitkeep +0 -0
  41. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/AUDIT.md +0 -0
  42. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/CASE_STUDY.md +0 -0
  43. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/architecture/HOW_IT_WORKS.md +0 -0
  44. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/architecture/ONTOLOGY.md +0 -0
  45. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/architecture/PATTERNS_AND_TRIADS.md +0 -0
  46. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/architecture/SELF_PORTRAIT.md +0 -0
  47. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/architecture/diagrams/pipeline_flow.mermaid +0 -0
  48. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/architecture/health_badge.json +0 -0
  49. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/assets/.gitignore +0 -0
  50. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/assets/cgis-app-avatar.png +0 -0
  51. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/assets/cgis-app-avatar.svg +0 -0
  52. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/assets/generate_avatar.py +0 -0
  53. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/examples/.gitkeep +0 -0
  54. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/how-to/AGENT_ONBOARDING.md +0 -0
  55. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/how-to/CLI_USAGE.md +0 -0
  56. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/how-to/MCP_REFERENCE.md +0 -0
  57. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/lab-notes/2026-06-11-chunked-review-negative-result.md +0 -0
  58. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/ontology/.gitkeep +0 -0
  59. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/ontology/core.yaml +0 -0
  60. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/ontology/domains.yaml +0 -0
  61. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/ontology/patterns.yaml +0 -0
  62. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/ontology/tolerances.lock +0 -0
  63. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-09-domain-pattern-fingerprint-design.md +0 -0
  64. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-09-pattern-alphabet-motif-basis-design.md +0 -0
  65. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-10-guardian-sprint-design.md +0 -0
  66. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-11-fastapi-di-edges-design.md +0 -0
  67. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-11-guardian-chunked-review-design.md +0 -0
  68. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-11-guardian-chunker-design.md +0 -0
  69. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-11-mcp-drift-validate-fqn-design.md +0 -0
  70. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-11-resolver-split-design.md +0 -0
  71. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-11-symbol-import-edges-design.md +0 -0
  72. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-12-drift-empty-domains-design.md +0 -0
  73. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-12-gate-semantics-design.md +0 -0
  74. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-12-init-ontology-design.md +0 -0
  75. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-12-release-please-ci-design.md +0 -0
  76. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-13-suggest-packages-design.md +0 -0
  77. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/2026-06-13-tangle-anti-pattern-design.md +0 -0
  78. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/2026-07-29-guardian-skeptic-scoring-design.md +0 -0
  79. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/2026-07-30-cgis-fractal-design.md +0 -0
  80. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/2026-07-30-cgis-fractal-plan.md +0 -0
  81. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/2026-07-30-chunk-source-filter-design.md +0 -0
  82. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/2026-07-30-chunk-source-filter-plan.md +0 -0
  83. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/2026-07-30-genai-client-close-design.md +0 -0
  84. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/2026-07-30-genai-client-close-plan.md +0 -0
  85. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/2026-07-30-guardian-precision-bench-design.md +0 -0
  86. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/2026-07-30-guardian-precision-bench-plan.md +0 -0
  87. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/2026-07-30-guardian-timeout-retry-design.md +0 -0
  88. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/2026-07-30-guardian-timeout-retry-plan.md +0 -0
  89. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/BLUEPRINT.md +0 -0
  90. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/PRD.md +0 -0
  91. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/TDD.md +0 -0
  92. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-09-fingerprint-drift.md +0 -0
  93. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-10-guardian-context-skeptic-inline.md +0 -0
  94. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-10-guardian-structured-findings-bench.md +0 -0
  95. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-10-motif-basis-part-b.md +0 -0
  96. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-10-unified-pattern-alphabet.md +0 -0
  97. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-11-fastapi-di-edges.md +0 -0
  98. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-11-guardian-chunked-review.md +0 -0
  99. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-11-guardian-chunker.md +0 -0
  100. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-11-mcp-drift-validate-fqn.md +0 -0
  101. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-11-resolver-split.md +0 -0
  102. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-12-drift-empty-domains.md +0 -0
  103. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-12-gate-semantics.md +0 -0
  104. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-12-init-ontology.md +0 -0
  105. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-12-release-please-ci.md +0 -0
  106. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-12-symbol-import-edges.md +0 -0
  107. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-06-13-suggest-packages.md +0 -0
  108. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/docs/specs/plans/2026-07-29-guardian-skeptic-scoring.md +0 -0
  109. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/main.py +0 -0
  110. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/plugin/.claude-plugin/plugin.json +0 -0
  111. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/plugin/.mcp.json +0 -0
  112. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/plugin/README.md +0 -0
  113. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/plugin/skills/cgis/SKILL.md +0 -0
  114. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/plugin/skills/ingest/SKILL.md +0 -0
  115. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/release-please-config.json +0 -0
  116. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/scripts/gen_ideal_graph.py +0 -0
  117. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/scripts/generate_health.py +0 -0
  118. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/scripts/generate_mcp_ref.py +0 -0
  119. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/scripts/generate_schema_docs.py +0 -0
  120. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/scripts/guardian_bench.py +0 -0
  121. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/scripts/guardian_review.py +0 -0
  122. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/scripts/inject_readme_graph.py +0 -0
  123. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/scripts/probe_closure_gap.py +0 -0
  124. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/scripts/probe_tier_ladder.py +0 -0
  125. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/__init__.py +0 -0
  126. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/__main__.py +0 -0
  127. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/api/.gitkeep +0 -0
  128. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/api/__init__.py +0 -0
  129. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/api/mcp_server.py +0 -0
  130. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/cli.py +0 -0
  131. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/core/.gitkeep +0 -0
  132. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/core/models.py +0 -0
  133. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/extractors/.gitkeep +0 -0
  134. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/extractors/_python_ast.py +0 -0
  135. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/extractors/_python_classes.py +0 -0
  136. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/extractors/_python_functions.py +0 -0
  137. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/extractors/_python_imports.py +0 -0
  138. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/extractors/_python_types.py +0 -0
  139. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/extractors/base.py +0 -0
  140. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/extractors/python_extractor.py +0 -0
  141. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/extractors/typescript_extractor.py +0 -0
  142. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/guardian/__init__.py +0 -0
  143. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/guardian/bench.py +0 -0
  144. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/guardian/chunker.py +0 -0
  145. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/guardian/core.py +0 -0
  146. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/guardian/diff_index.py +0 -0
  147. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/guardian/github_poster.py +0 -0
  148. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/guardian/metrics.py +0 -0
  149. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/guardian/providers/__init__.py +0 -0
  150. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/guardian/providers/base.py +0 -0
  151. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/guardian/providers/gemini.py +0 -0
  152. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/guardian/providers/mistral.py +0 -0
  153. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/guardian/providers/ollama.py +0 -0
  154. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/guardian/recording.py +0 -0
  155. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/guardian/render.py +0 -0
  156. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/guardian/runner.py +0 -0
  157. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/guardian/skeptic.py +0 -0
  158. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/pipeline.py +0 -0
  159. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/py.typed +0 -0
  160. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/query/analysis/__init__.py +0 -0
  161. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/query/analysis/analyzer.py +0 -0
  162. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/query/analysis/anomaly.py +0 -0
  163. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/query/analysis/cohesion.py +0 -0
  164. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/query/analysis/health.py +0 -0
  165. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/query/analysis/suggest_service.py +0 -0
  166. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/query/context/__init__.py +0 -0
  167. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/query/context/audit.py +0 -0
  168. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/query/context/context_service.py +0 -0
  169. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/query/context/prompt.py +0 -0
  170. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/query/context/snippet.py +0 -0
  171. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/query/drift/__init__.py +0 -0
  172. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/query/drift/_scc.py +0 -0
  173. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/query/drift/drift.py +0 -0
  174. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/query/drift/drift_service.py +0 -0
  175. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/query/drift/fingerprint.py +0 -0
  176. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/query/drift/fractal.py +0 -0
  177. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/query/drift/ontology_init.py +0 -0
  178. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/query/drift/quotient.py +0 -0
  179. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/query/drift/triads.py +0 -0
  180. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/query/engine.py +0 -0
  181. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/query/fqn.py +0 -0
  182. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/query/render/__init__.py +0 -0
  183. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/query/render/graph_json.py +0 -0
  184. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/query/render/mermaid.py +0 -0
  185. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/query/render/metrics.py +0 -0
  186. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/resolver/.gitkeep +0 -0
  187. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/resolver/__init__.py +0 -0
  188. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/resolver/engine.py +0 -0
  189. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/resolver/indices.py +0 -0
  190. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/resolver/symbols.py +0 -0
  191. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/resolver/uplift.py +0 -0
  192. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/storage/.gitkeep +0 -0
  193. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/src/cgis/storage/sqlite_store.py +0 -0
  194. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/integration/.gitkeep +0 -0
  195. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/self_parsing/.gitkeep +0 -0
  196. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/self_parsing/__init__.py +0 -0
  197. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/self_parsing/conftest.py +0 -0
  198. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/self_parsing/test_architecture.py +0 -0
  199. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/self_parsing/test_drift.py +0 -0
  200. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/self_parsing/test_fractal.py +0 -0
  201. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/self_parsing/test_init_ontology_roundtrip.py +0 -0
  202. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/self_parsing/test_self_parse.py +0 -0
  203. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/self_parsing/test_self_parse_ts.py +0 -0
  204. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/self_parsing/test_suggest.py +0 -0
  205. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/.gitkeep +0 -0
  206. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/conftest.py +0 -0
  207. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/guardian_stubs.py +0 -0
  208. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test___main__.py +0 -0
  209. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_analyzer.py +0 -0
  210. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_audit.py +0 -0
  211. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_cli.py +0 -0
  212. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_cohesion.py +0 -0
  213. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_context_service.py +0 -0
  214. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_di_acceptance.py +0 -0
  215. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_drift.py +0 -0
  216. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_drift_service.py +0 -0
  217. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_fingerprint.py +0 -0
  218. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_fqn.py +0 -0
  219. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_fractal.py +0 -0
  220. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_gen_ideal_graph.py +0 -0
  221. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_generate_mcp_ref.py +0 -0
  222. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_graph_json.py +0 -0
  223. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_guardian_bench.py +0 -0
  224. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_guardian_chunker.py +0 -0
  225. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_guardian_collector.py +0 -0
  226. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_guardian_core.py +0 -0
  227. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_guardian_diff_index.py +0 -0
  228. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_guardian_findings.py +0 -0
  229. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_guardian_metrics.py +0 -0
  230. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_guardian_poster.py +0 -0
  231. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_guardian_providers.py +0 -0
  232. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_guardian_recording.py +0 -0
  233. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_guardian_render.py +0 -0
  234. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_guardian_runner.py +0 -0
  235. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_guardian_skeptic.py +0 -0
  236. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_health_scorer.py +0 -0
  237. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_import_acceptance.py +0 -0
  238. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_mcp_server.py +0 -0
  239. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_mermaid.py +0 -0
  240. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_metrics.py +0 -0
  241. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_models.py +0 -0
  242. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_ontology_compliance.py +0 -0
  243. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_ontology_init.py +0 -0
  244. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_patterns_yaml.py +0 -0
  245. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_pipeline.py +0 -0
  246. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_prompt.py +0 -0
  247. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_python_extractor.py +0 -0
  248. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_quotient.py +0 -0
  249. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_resolver.py +0 -0
  250. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_resolver_indices.py +0 -0
  251. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_resolver_symbols.py +0 -0
  252. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_snippet.py +0 -0
  253. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_sqlite_store.py +0 -0
  254. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_suggest_service.py +0 -0
  255. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_triads.py +0 -0
  256. {codegraph_brain-0.9.0 → codegraph_brain-0.10.0}/tests/unit/test_typescript_extractor.py +0 -0
  257. {codegraph_brain-0.9.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,18 @@
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
+
3
16
  ## [0.9.0](https://github.com/zaebee/codegraph-brain/compare/codegraph-brain-v0.8.0...codegraph-brain-v0.9.0) (2026-08-01)
4
17
 
5
18
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codegraph-brain
3
- Version: 0.9.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.9.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"
@@ -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]
@@ -1,5 +1,75 @@
1
1
  """Builds system and user prompts for the Guardian LLM reviewer."""
2
2
 
3
+ #: The finder's focus areas, one entry per axis. A mapping rather than one blob
4
+ #: so a review can ask about a single axis at a time (#331): axes sharing a
5
+ #: context window compete for what looks like a fixed attention budget, measured
6
+ #: twice (#247, #330). Joining every value reproduces the original prompt exactly.
7
+ FOCUS_AREAS: dict[str, str] = {
8
+ "logic": """\
9
+ **Logic bugs** — inputs that produce wrong output, division by zero, off-by-one errors,
10
+ incorrect algorithm behaviour. Think: empty collections, None values, boundary conditions.""",
11
+ "external_data": """\
12
+ **Unvalidated external data** — a value read from config/YAML/JSON, env, or a request is
13
+ used as a `dict`/`list`/`set`/iterable (subscripted, iterated, passed to `set()`/`dict()`,
14
+ `.items()`, `for x in value`) without first checking its type or presence. A YAML key the
15
+ author expects to be a mapping can legally be a scalar or a list; a `value or {}` idiom
16
+ catches `None` but lets a non-dict truthy value through to operations that then misbehave
17
+ silently rather than erroring. Flag each such use that lacks a type/shape guard.""",
18
+ "float_equality": """\
19
+ **Exact-equality on floats / money** — bare `==` or `!=` comparing floating-point or
20
+ Decimal values, *including in test assertions* (`assert x == 0.3`). Floating-point
21
+ rounding makes these flaky or wrong; they should use a tolerance compare. Check changed
22
+ test files for this too.""",
23
+ "test_coverage": """\
24
+ **Missing test coverage** — code paths in the diff that have no test. Focus on edge cases
25
+ that could silently return wrong results (not just "coverage for coverage's sake").""",
26
+ "type_safety": """\
27
+ **Type safety** — implicit `Any`, missing return type annotations, unsafe casts, Pydantic
28
+ models constructed with wrong field types.""",
29
+ "library_contract": """\
30
+ **Library boundary contracts** — for each call into a third-party library, verify that
31
+ the data passed in matches the library's expected convention:
32
+ - Units and coordinate systems (e.g. center vs top-left, row/col vs x/y,
33
+ naive vs timezone-aware datetimes)
34
+ - Ordering assumptions (e.g. does array order affect rendering or sort stability?)
35
+ - Ownership and mutation (e.g. does the library mutate its input?)
36
+ Trace the value from where it's produced to where it's consumed across the library call.
37
+ If the producing code and consuming code have different assumptions, that's a bug.""",
38
+ "ontology": """\
39
+ **Ontology compliance** — wrong NodeType/EdgeType assignments, FQNs not derived from file
40
+ paths, unresolved calls not using `raw_call:` prefix.""",
41
+ }
42
+
43
+
44
+ #: How the axes are batched into finder calls. Per-axis measured 4.3x the
45
+ #: single-prompt noise across four fixtures (#331) — the superlinear growth that
46
+ #: killed chunked review (#160) — while recall rose on every fixture that had
47
+ #: anything to find. Noise scaled roughly linearly with call count, so fewer,
48
+ #: kinship-grouped calls should keep part of the recall and most of the quiet.
49
+ #:
50
+ #: The split is by what a defect *is*: "wrong answer" shapes in one call,
51
+ #: "declared contract vs actual" in the other. Axes that competed in the two
52
+ #: measured cases (#247, #330) were dissimilar ones, so kin travel together.
53
+ AXIS_GROUPS: dict[str, tuple[str, ...]] = {
54
+ "correctness": ("logic", "external_data", "float_equality"),
55
+ "contracts": ("type_safety", "library_contract", "ontology", "test_coverage"),
56
+ }
57
+
58
+ #: One call per axis — the seven-way fan-out benched in #331.
59
+ PER_AXIS_GROUPS: dict[str, tuple[str, ...]] = {name: (name,) for name in FOCUS_AREAS}
60
+
61
+
62
+ def render_group(axes: tuple[str, ...]) -> str:
63
+ """Render the focus areas for one batch of axes."""
64
+ return "\n\n".join(FOCUS_AREAS[axis] for axis in axes)
65
+
66
+
67
+ def render_focus_areas(axis: str | None = None) -> str:
68
+ """Return every focus area joined, or a single one when `axis` names it."""
69
+ if axis is not None:
70
+ return FOCUS_AREAS[axis]
71
+ return "\n\n".join(FOCUS_AREAS.values())
72
+
3
73
 
4
74
  class PromptBuilder:
5
75
  """Constructs the system and user prompts."""
@@ -82,6 +152,20 @@ observe-only — do NOT flag it.
82
152
 
83
153
  """
84
154
 
155
+ # `focus_axis` narrows the prompt to one axis (#331). Absent = every axis,
156
+ # which renders exactly the text this block replaced.
157
+ # `focus_group` carries the axes for this call, comma-separated. Absent =
158
+ # every axis, which renders exactly the text the inline block used to be.
159
+ group = context.get("focus_group")
160
+ # Whitespace-tolerant: the only producer joins without spaces, but this is
161
+ # a public context key and a stray space would surface as a KeyError deep
162
+ # inside prompt building rather than as anything a caller could act on.
163
+ focus_areas_section = (
164
+ render_group(tuple(part.strip() for part in group.split(",")))
165
+ if group
166
+ else render_focus_areas()
167
+ )
168
+
85
169
  return f"""Review the following Pull Request diff for real defects.
86
170
 
87
171
  {contributing_section}{ontology_section}### 3. CHANGES TO REVIEW (git diff)
@@ -126,38 +210,7 @@ observe-only — do NOT flag it.
126
210
  ---
127
211
  ### WHAT TO LOOK FOR (focus areas):
128
212
 
129
- **Logic bugs** — inputs that produce wrong output, division by zero, off-by-one errors,
130
- incorrect algorithm behaviour. Think: empty collections, None values, boundary conditions.
131
-
132
- **Unvalidated external data** — a value read from config/YAML/JSON, env, or a request is
133
- used as a `dict`/`list`/`set`/iterable (subscripted, iterated, passed to `set()`/`dict()`,
134
- `.items()`, `for x in value`) without first checking its type or presence. A YAML key the
135
- author expects to be a mapping can legally be a scalar or a list; a `value or {{}}` idiom
136
- catches `None` but lets a non-dict truthy value through to operations that then misbehave
137
- silently rather than erroring. Flag each such use that lacks a type/shape guard.
138
-
139
- **Exact-equality on floats / money** — bare `==` or `!=` comparing floating-point or
140
- Decimal values, *including in test assertions* (`assert x == 0.3`). Floating-point
141
- rounding makes these flaky or wrong; they should use a tolerance compare. Check changed
142
- test files for this too.
143
-
144
- **Missing test coverage** — code paths in the diff that have no test. Focus on edge cases
145
- that could silently return wrong results (not just "coverage for coverage's sake").
146
-
147
- **Type safety** — implicit `Any`, missing return type annotations, unsafe casts, Pydantic
148
- models constructed with wrong field types.
149
-
150
- **Library boundary contracts** — for each call into a third-party library, verify that
151
- the data passed in matches the library's expected convention:
152
- - Units and coordinate systems (e.g. center vs top-left, row/col vs x/y,
153
- naive vs timezone-aware datetimes)
154
- - Ordering assumptions (e.g. does array order affect rendering or sort stability?)
155
- - Ownership and mutation (e.g. does the library mutate its input?)
156
- Trace the value from where it's produced to where it's consumed across the library call.
157
- If the producing code and consuming code have different assumptions, that's a bug.
158
-
159
- **Ontology compliance** — wrong NodeType/EdgeType assignments, FQNs not derived from file
160
- paths, unresolved calls not using `raw_call:` prefix.
213
+ {focus_areas_section}
161
214
 
162
215
  ---
163
216
  ### WORKED EXAMPLES (how the per-function walk turns into a finding):