codegraph-brain 0.8.0__tar.gz → 0.9.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 (255) hide show
  1. codegraph_brain-0.9.0/.release-please-manifest.json +3 -0
  2. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/CHANGELOG.md +8 -0
  3. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/PKG-INFO +1 -1
  4. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/pyproject.toml +1 -1
  5. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/extractors/python_extractor.py +60 -1
  6. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/drift/fingerprint.py +118 -2
  7. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_fingerprint.py +171 -1
  8. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_python_extractor.py +87 -0
  9. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/uv.lock +1 -1
  10. codegraph_brain-0.8.0/.release-please-manifest.json +0 -3
  11. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/.claude-plugin/marketplace.json +0 -0
  12. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/.github/workflows/autodoc.yml +0 -0
  13. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/.github/workflows/ci.yml +0 -0
  14. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/.github/workflows/guardian.yml +0 -0
  15. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/.github/workflows/pr-title.yml +0 -0
  16. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/.github/workflows/release-please.yml +0 -0
  17. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/.gitignore +0 -0
  18. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/.pre-commit-config.yaml +0 -0
  19. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/.python-version +0 -0
  20. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/CLAUDE.md +0 -0
  21. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/CONTRIBUTING.md +0 -0
  22. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/LICENSE +0 -0
  23. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/Makefile +0 -0
  24. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/PRIVACY.md +0 -0
  25. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/README.md +0 -0
  26. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/benchmarks/guardian/CURATION.md +0 -0
  27. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/benchmarks/guardian/pr-122.yaml +0 -0
  28. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/benchmarks/guardian/pr-140.yaml +0 -0
  29. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/benchmarks/guardian/pr-141.yaml +0 -0
  30. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/benchmarks/guardian/pr-142.yaml +0 -0
  31. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/benchmarks/guardian/pr-143.yaml +0 -0
  32. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/benchmarks/guardian/pr-144.yaml +0 -0
  33. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/benchmarks/guardian/pr-278.yaml +0 -0
  34. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/benchmarks/guardian/pr-313.yaml +0 -0
  35. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/benchmarks/guardian/results.jsonl +0 -0
  36. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/data/.gitkeep +0 -0
  37. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/AUDIT.md +0 -0
  38. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/CASE_STUDY.md +0 -0
  39. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/architecture/HOW_IT_WORKS.md +0 -0
  40. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/architecture/ONTOLOGY.md +0 -0
  41. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/architecture/PATTERNS_AND_TRIADS.md +0 -0
  42. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/architecture/SELF_PORTRAIT.md +0 -0
  43. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/architecture/diagrams/pipeline_flow.mermaid +0 -0
  44. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/architecture/health_badge.json +0 -0
  45. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/assets/.gitignore +0 -0
  46. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/assets/cgis-app-avatar.png +0 -0
  47. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/assets/cgis-app-avatar.svg +0 -0
  48. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/assets/generate_avatar.py +0 -0
  49. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/examples/.gitkeep +0 -0
  50. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/how-to/AGENT_ONBOARDING.md +0 -0
  51. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/how-to/CLI_USAGE.md +0 -0
  52. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/how-to/MCP_REFERENCE.md +0 -0
  53. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/lab-notes/2026-06-11-chunked-review-negative-result.md +0 -0
  54. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/ontology/.gitkeep +0 -0
  55. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/ontology/core.yaml +0 -0
  56. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/ontology/domains.yaml +0 -0
  57. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/ontology/patterns.yaml +0 -0
  58. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/ontology/tolerances.lock +0 -0
  59. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-09-domain-pattern-fingerprint-design.md +0 -0
  60. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-09-pattern-alphabet-motif-basis-design.md +0 -0
  61. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-10-guardian-sprint-design.md +0 -0
  62. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-11-fastapi-di-edges-design.md +0 -0
  63. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-11-guardian-chunked-review-design.md +0 -0
  64. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-11-guardian-chunker-design.md +0 -0
  65. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-11-mcp-drift-validate-fqn-design.md +0 -0
  66. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-11-resolver-split-design.md +0 -0
  67. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-11-symbol-import-edges-design.md +0 -0
  68. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-12-drift-empty-domains-design.md +0 -0
  69. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-12-gate-semantics-design.md +0 -0
  70. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-12-init-ontology-design.md +0 -0
  71. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-12-release-please-ci-design.md +0 -0
  72. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-13-suggest-packages-design.md +0 -0
  73. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-13-tangle-anti-pattern-design.md +0 -0
  74. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-07-29-guardian-skeptic-scoring-design.md +0 -0
  75. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-07-30-cgis-fractal-design.md +0 -0
  76. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-07-30-cgis-fractal-plan.md +0 -0
  77. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-07-30-chunk-source-filter-design.md +0 -0
  78. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-07-30-chunk-source-filter-plan.md +0 -0
  79. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-07-30-genai-client-close-design.md +0 -0
  80. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-07-30-genai-client-close-plan.md +0 -0
  81. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-07-30-guardian-precision-bench-design.md +0 -0
  82. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-07-30-guardian-precision-bench-plan.md +0 -0
  83. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-07-30-guardian-timeout-retry-design.md +0 -0
  84. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-07-30-guardian-timeout-retry-plan.md +0 -0
  85. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-07-31-finder-bug-class-taxonomy.md +0 -0
  86. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/BLUEPRINT.md +0 -0
  87. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/PRD.md +0 -0
  88. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/TDD.md +0 -0
  89. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-09-fingerprint-drift.md +0 -0
  90. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-10-guardian-context-skeptic-inline.md +0 -0
  91. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-10-guardian-structured-findings-bench.md +0 -0
  92. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-10-motif-basis-part-b.md +0 -0
  93. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-10-unified-pattern-alphabet.md +0 -0
  94. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-11-fastapi-di-edges.md +0 -0
  95. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-11-guardian-chunked-review.md +0 -0
  96. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-11-guardian-chunker.md +0 -0
  97. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-11-mcp-drift-validate-fqn.md +0 -0
  98. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-11-resolver-split.md +0 -0
  99. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-12-drift-empty-domains.md +0 -0
  100. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-12-gate-semantics.md +0 -0
  101. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-12-init-ontology.md +0 -0
  102. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-12-release-please-ci.md +0 -0
  103. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-12-symbol-import-edges.md +0 -0
  104. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-13-suggest-packages.md +0 -0
  105. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-07-29-guardian-skeptic-scoring.md +0 -0
  106. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/main.py +0 -0
  107. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/plugin/.claude-plugin/plugin.json +0 -0
  108. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/plugin/.mcp.json +0 -0
  109. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/plugin/README.md +0 -0
  110. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/plugin/skills/cgis/SKILL.md +0 -0
  111. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/plugin/skills/ingest/SKILL.md +0 -0
  112. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/release-please-config.json +0 -0
  113. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/scripts/gen_ideal_graph.py +0 -0
  114. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/scripts/generate_health.py +0 -0
  115. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/scripts/generate_mcp_ref.py +0 -0
  116. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/scripts/generate_schema_docs.py +0 -0
  117. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/scripts/guardian_bench.py +0 -0
  118. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/scripts/guardian_review.py +0 -0
  119. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/scripts/inject_readme_graph.py +0 -0
  120. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/scripts/probe_closure_gap.py +0 -0
  121. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/scripts/probe_tier_ladder.py +0 -0
  122. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/__init__.py +0 -0
  123. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/__main__.py +0 -0
  124. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/api/.gitkeep +0 -0
  125. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/api/__init__.py +0 -0
  126. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/api/mcp_server.py +0 -0
  127. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/cli.py +0 -0
  128. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/core/.gitkeep +0 -0
  129. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/core/models.py +0 -0
  130. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/extractors/.gitkeep +0 -0
  131. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/extractors/_python_ast.py +0 -0
  132. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/extractors/_python_classes.py +0 -0
  133. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/extractors/_python_functions.py +0 -0
  134. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/extractors/_python_imports.py +0 -0
  135. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/extractors/_python_types.py +0 -0
  136. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/extractors/base.py +0 -0
  137. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/extractors/typescript_extractor.py +0 -0
  138. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/__init__.py +0 -0
  139. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/bench.py +0 -0
  140. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/chunked.py +0 -0
  141. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/chunker.py +0 -0
  142. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/collector.py +0 -0
  143. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/core.py +0 -0
  144. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/diff_index.py +0 -0
  145. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/findings.py +0 -0
  146. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/github_poster.py +0 -0
  147. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/metrics.py +0 -0
  148. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/prompts.py +0 -0
  149. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/providers/__init__.py +0 -0
  150. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/providers/base.py +0 -0
  151. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/providers/gemini.py +0 -0
  152. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/providers/mistral.py +0 -0
  153. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/providers/ollama.py +0 -0
  154. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/recording.py +0 -0
  155. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/render.py +0 -0
  156. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/runner.py +0 -0
  157. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/skeptic.py +0 -0
  158. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/pipeline.py +0 -0
  159. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/py.typed +0 -0
  160. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/analysis/__init__.py +0 -0
  161. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/analysis/analyzer.py +0 -0
  162. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/analysis/anomaly.py +0 -0
  163. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/analysis/cohesion.py +0 -0
  164. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/analysis/health.py +0 -0
  165. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/analysis/suggest_service.py +0 -0
  166. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/context/__init__.py +0 -0
  167. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/context/audit.py +0 -0
  168. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/context/context_service.py +0 -0
  169. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/context/prompt.py +0 -0
  170. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/context/snippet.py +0 -0
  171. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/drift/__init__.py +0 -0
  172. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/drift/_scc.py +0 -0
  173. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/drift/drift.py +0 -0
  174. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/drift/drift_service.py +0 -0
  175. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/drift/fractal.py +0 -0
  176. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/drift/ontology_init.py +0 -0
  177. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/drift/quotient.py +0 -0
  178. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/drift/triads.py +0 -0
  179. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/engine.py +0 -0
  180. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/fqn.py +0 -0
  181. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/render/__init__.py +0 -0
  182. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/render/graph_json.py +0 -0
  183. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/render/mermaid.py +0 -0
  184. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/render/metrics.py +0 -0
  185. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/resolver/.gitkeep +0 -0
  186. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/resolver/__init__.py +0 -0
  187. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/resolver/engine.py +0 -0
  188. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/resolver/indices.py +0 -0
  189. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/resolver/symbols.py +0 -0
  190. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/resolver/uplift.py +0 -0
  191. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/storage/.gitkeep +0 -0
  192. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/storage/sqlite_store.py +0 -0
  193. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/integration/.gitkeep +0 -0
  194. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/self_parsing/.gitkeep +0 -0
  195. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/self_parsing/__init__.py +0 -0
  196. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/self_parsing/conftest.py +0 -0
  197. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/self_parsing/test_architecture.py +0 -0
  198. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/self_parsing/test_drift.py +0 -0
  199. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/self_parsing/test_fractal.py +0 -0
  200. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/self_parsing/test_init_ontology_roundtrip.py +0 -0
  201. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/self_parsing/test_self_parse.py +0 -0
  202. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/self_parsing/test_self_parse_ts.py +0 -0
  203. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/self_parsing/test_suggest.py +0 -0
  204. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/.gitkeep +0 -0
  205. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/conftest.py +0 -0
  206. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/guardian_stubs.py +0 -0
  207. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test___main__.py +0 -0
  208. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_analyzer.py +0 -0
  209. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_audit.py +0 -0
  210. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_cli.py +0 -0
  211. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_cohesion.py +0 -0
  212. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_context_service.py +0 -0
  213. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_di_acceptance.py +0 -0
  214. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_drift.py +0 -0
  215. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_drift_service.py +0 -0
  216. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_fqn.py +0 -0
  217. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_fractal.py +0 -0
  218. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_gen_ideal_graph.py +0 -0
  219. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_generate_mcp_ref.py +0 -0
  220. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_graph_json.py +0 -0
  221. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_guardian_bench.py +0 -0
  222. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_guardian_chunked.py +0 -0
  223. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_guardian_chunker.py +0 -0
  224. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_guardian_collector.py +0 -0
  225. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_guardian_core.py +0 -0
  226. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_guardian_diff_index.py +0 -0
  227. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_guardian_findings.py +0 -0
  228. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_guardian_metrics.py +0 -0
  229. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_guardian_poster.py +0 -0
  230. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_guardian_providers.py +0 -0
  231. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_guardian_recording.py +0 -0
  232. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_guardian_render.py +0 -0
  233. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_guardian_runner.py +0 -0
  234. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_guardian_skeptic.py +0 -0
  235. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_health_scorer.py +0 -0
  236. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_import_acceptance.py +0 -0
  237. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_mcp_server.py +0 -0
  238. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_mermaid.py +0 -0
  239. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_metrics.py +0 -0
  240. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_models.py +0 -0
  241. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_ontology_compliance.py +0 -0
  242. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_ontology_init.py +0 -0
  243. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_patterns_yaml.py +0 -0
  244. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_pipeline.py +0 -0
  245. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_prompt.py +0 -0
  246. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_quotient.py +0 -0
  247. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_resolver.py +0 -0
  248. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_resolver_indices.py +0 -0
  249. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_resolver_symbols.py +0 -0
  250. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_snippet.py +0 -0
  251. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_sqlite_store.py +0 -0
  252. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_suggest_service.py +0 -0
  253. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_triads.py +0 -0
  254. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_typescript_extractor.py +0 -0
  255. {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_uplift.py +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.9.0"
3
+ }
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.0](https://github.com/zaebee/codegraph-brain/compare/codegraph-brain-v0.8.0...codegraph-brain-v0.9.0) (2026-08-01)
4
+
5
+
6
+ ### Features
7
+
8
+ * **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))
9
+ * **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))
10
+
3
11
  ## [0.8.0](https://github.com/zaebee/codegraph-brain/compare/codegraph-brain-v0.7.5...codegraph-brain-v0.8.0) (2026-08-01)
4
12
 
5
13
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codegraph-brain
3
- Version: 0.8.0
3
+ Version: 0.9.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
@@ -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.9.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
 
@@ -1,6 +1,6 @@
1
1
  """PatternFingerprint dataclass and FingerprintExtractor."""
2
2
 
3
- from collections import Counter
3
+ from collections import Counter, defaultdict
4
4
  from dataclasses import dataclass, field
5
5
 
6
6
  from cgis.core.models import Edge, EdgeType, Node, NodeType
@@ -136,6 +136,121 @@ def _count_routers(domain_node_ids: set[str], all_edges: list[Edge]) -> int:
136
136
  return router_count
137
137
 
138
138
 
139
+ def _follow_forwarding(reexports: dict[str, dict[str, str]], symbol_fqn: str) -> str | None:
140
+ """Resolve a symbol through every passthrough hop, or None if it is not forwarded.
141
+
142
+ One hop is not enough: `A -> B -> C -> D` with both B and C forwarding would
143
+ otherwise stop at C and leave the A->D coupling hidden — a two-hop bypass of
144
+ the very check this implements. The visited set makes a re-export cycle
145
+ terminate instead of hanging.
146
+ """
147
+ via, _, name = symbol_fqn.rpartition(".")
148
+ forwarded = reexports.get(via, {}).get(name)
149
+ if forwarded is None:
150
+ return None
151
+ seen = {symbol_fqn}
152
+ while forwarded not in seen:
153
+ seen.add(forwarded)
154
+ next_via, _, next_name = forwarded.rpartition(".")
155
+ next_hop = reexports.get(next_via, {}).get(next_name)
156
+ if next_hop is None:
157
+ break
158
+ forwarded = next_hop
159
+ return forwarded
160
+
161
+
162
+ def _owning_module(fqn: str, modules: set[str]) -> str | None:
163
+ """Longest prefix of fqn that is a real module node — the fqn itself included.
164
+
165
+ `rpartition` alone is wrong at both ends. A forwarded *module*
166
+ (`from pkg import submodule as submodule`) would resolve to its package, and
167
+ a symbol nested in a class would resolve to the class — but IMPORTS edges
168
+ must land on FILE/MODULE nodes or the DAG-depth walk counts something that
169
+ is not a module.
170
+ """
171
+ parts = fqn.split(".")
172
+ for cut in range(len(parts), 0, -1):
173
+ candidate = ".".join(parts[:cut])
174
+ if candidate in modules:
175
+ return candidate
176
+ return None
177
+
178
+
179
+ def _reattributed_imports(
180
+ domain_nodes: list[Node],
181
+ domain_ids: set[str],
182
+ internal_edges: list[Edge],
183
+ all_edges: list[Edge],
184
+ ) -> list[Edge]:
185
+ """Add the IMPORTS edges that transparent re-exports hide (#182).
186
+
187
+ A passthrough — ``from C import X as X`` in B, with X unused there — lets B
188
+ absorb C's import edge, so ``A -> B -> C`` measures as a 021C chain when the
189
+ truth is a 030T triangle. Any N-way coupling can be linearised that way
190
+ without changing what actually depends on what, which is what makes the
191
+ IMPORTS layer gameable.
192
+
193
+ So the census looks *through* the passthrough: when A takes a name from B
194
+ that B merely forwards from C, the hidden ``A -> C`` edge is added.
195
+
196
+ Two deliberate limits:
197
+
198
+ - **Add, never replace.** A may also take a real name from B; dropping
199
+ ``A -> B`` would trade a hidden edge for a lost one.
200
+ - **Intra-domain only**, enforced by requiring the definer in ``domain_ids``.
201
+ A package ``__init__.py`` re-exporting for external consumers is legitimate
202
+ API surface, and punishing it would teach deep-importing internals to keep
203
+ the dashboard green.
204
+ """
205
+ reexports: dict[str, dict[str, str]] = {
206
+ n.id: rx for n in domain_nodes if (rx := n.metadata.get("reexports"))
207
+ }
208
+ if not reexports:
209
+ return internal_edges
210
+
211
+ modules = {n.id for n in domain_nodes if n.type in (NodeType.FILE, NodeType.MODULE)}
212
+ taken: dict[str, list[str]] = defaultdict(list)
213
+ for edge in all_edges:
214
+ if edge.type == EdgeType.IMPORTS_SYMBOL and edge.source in domain_ids:
215
+ taken[edge.source].append(edge.target)
216
+
217
+ seen_pairs = {(e.source, e.target) for e in internal_edges if e.type == EdgeType.IMPORTS}
218
+ added: list[Edge] = []
219
+ for importer, symbols in taken.items():
220
+ for symbol_fqn in symbols:
221
+ definer = _definer_for(symbol_fqn, reexports, modules, importer, seen_pairs)
222
+ if definer is None:
223
+ continue
224
+ seen_pairs.add((importer, definer))
225
+ added.append(
226
+ Edge(
227
+ id=f"{importer}:imports:{definer}:reattributed",
228
+ type=EdgeType.IMPORTS,
229
+ source=importer,
230
+ target=definer,
231
+ confidence=1.0,
232
+ )
233
+ )
234
+ return internal_edges + added
235
+
236
+
237
+ def _definer_for(
238
+ symbol_fqn: str,
239
+ reexports: dict[str, dict[str, str]],
240
+ modules: set[str],
241
+ importer: str,
242
+ seen_pairs: set[tuple[str, str]],
243
+ ) -> str | None:
244
+ """The module a forwarded symbol really lives in, or None if nothing to add."""
245
+ forwarded = _follow_forwarding(reexports, symbol_fqn)
246
+ if forwarded is None:
247
+ return None
248
+ definer = _owning_module(forwarded, modules)
249
+ if definer is None or definer == importer or (importer, definer) in seen_pairs:
250
+ return None
251
+ return definer
252
+
253
+
139
254
  class FingerprintExtractor:
140
255
  """Compute a PatternFingerprint for a given FQN domain prefix from a SQLiteStore."""
141
256
 
@@ -212,7 +327,8 @@ class FingerprintExtractor:
212
327
  raw_calls = [e for e in calls_edges if e.target.startswith(RAW_CALL_PREFIX)]
213
328
  unresolved_ratio = len(raw_calls) / len(calls_edges) if calls_edges else 0.0
214
329
 
215
- t_imports = normalized_census(triad_census(domain_ids, internal_edges, EdgeType.IMPORTS))
330
+ imports_edges = _reattributed_imports(domain_nodes, domain_ids, internal_edges, all_edges)
331
+ t_imports = normalized_census(triad_census(domain_ids, imports_edges, EdgeType.IMPORTS))
216
332
  t_calls = normalized_census(triad_census(domain_ids, internal_edges, EdgeType.CALLS))
217
333
 
218
334
  return PatternFingerprint(
@@ -8,7 +8,12 @@ from conftest import module_with_funcs
8
8
  from cgis.core.models import Edge, EdgeType, Node, NodeNamespace, NodeType
9
9
  from cgis.extractors.python_extractor import PythonExtractor
10
10
  from cgis.pipeline import IngestionPipeline
11
- from cgis.query.drift.fingerprint import FingerprintExtractor, PatternFingerprint
11
+ from cgis.query.drift.fingerprint import (
12
+ FingerprintExtractor,
13
+ PatternFingerprint,
14
+ _follow_forwarding,
15
+ _reattributed_imports,
16
+ )
12
17
  from cgis.query.drift.triads import TRIAD_ORDER
13
18
  from cgis.storage.sqlite_store import SQLiteStore
14
19
 
@@ -510,3 +515,168 @@ def test_tangle_ratio_resolved_calls_unchanged() -> None:
510
515
  # unresolved_ratio 0 → discount 1 → calls-tangle unchanged (regression guard).
511
516
  fp = _tangle_fp(t_calls=_onehot("300"), unresolved_ratio=0.0)
512
517
  assert fp.tangle_ratio == pytest.approx(1.0)
518
+
519
+
520
+ # ---------------------------------------------------------------------------
521
+ # Transparent re-export re-attribution in the IMPORTS census (#182)
522
+ # ---------------------------------------------------------------------------
523
+
524
+
525
+ def _mod(fqn: str, reexports: dict[str, str] | None = None) -> Node:
526
+ """A FILE node, optionally carrying transparent re-exports."""
527
+ return Node(
528
+ id=fqn,
529
+ type=NodeType.FILE,
530
+ name=fqn.rsplit(".", maxsplit=1)[-1],
531
+ file_path=fqn.replace(".", "/") + ".py",
532
+ start_line=1,
533
+ end_line=9,
534
+ metadata={"reexports": reexports} if reexports else {},
535
+ )
536
+
537
+
538
+ def _imports(source: str, target: str) -> Edge:
539
+ return Edge(id=f"{source}:i:{target}", type=EdgeType.IMPORTS, source=source, target=target)
540
+
541
+
542
+ def _imports_symbol(source: str, target: str) -> Edge:
543
+ return Edge(
544
+ id=f"{source}:s:{target}", type=EdgeType.IMPORTS_SYMBOL, source=source, target=target
545
+ )
546
+
547
+
548
+ def _census(nodes: list[Node], edges: list[Edge], prefix: str) -> dict[str, float]:
549
+ """Non-zero IMPORTS triad shares, keyed by class name."""
550
+ fp = FingerprintExtractor.from_graph(nodes, edges).extract(prefix)
551
+ return {name: share for name, share in zip(TRIAD_ORDER, fp.t_imports, strict=True) if share}
552
+
553
+
554
+ #: The PR #180 shape: engine -> symbols -> indices, with symbols forwarding a
555
+ #: name it never uses from indices. The real dependency graph is a triangle.
556
+ _LAUNDERED_EDGES = [
557
+ _imports("d.engine", "d.symbols"),
558
+ _imports("d.symbols", "d.indices"),
559
+ _imports_symbol("d.engine", "d.symbols.SymbolResolver"),
560
+ _imports_symbol("d.engine", "d.symbols.IndexBuilder"),
561
+ ]
562
+
563
+
564
+ def test_passthrough_reexport_measures_as_a_triangle() -> None:
565
+ """The laundering from #180: a chain on paper, a triangle in truth."""
566
+ nodes = [
567
+ _mod("d.engine"),
568
+ _mod("d.symbols", {"IndexBuilder": "d.indices.IndexBuilder"}),
569
+ _mod("d.indices"),
570
+ ]
571
+
572
+ assert _census(nodes, _LAUNDERED_EDGES, "d") == {"030T": 1.0}
573
+
574
+
575
+ def test_same_shape_without_a_reexport_stays_a_chain() -> None:
576
+ """Control: identical edges, no forwarding declared — nothing is re-attributed."""
577
+ nodes = [_mod("d.engine"), _mod("d.symbols"), _mod("d.indices")]
578
+
579
+ assert _census(nodes, _LAUNDERED_EDGES, "d") == {"021C": 1.0}
580
+
581
+
582
+ def test_reattribution_does_not_reach_outside_the_domain() -> None:
583
+ """A facade forwarding from another domain is API surface, not laundering.
584
+
585
+ Punishing it would teach deep-importing internals to keep the dashboard
586
+ green, so the definer must be in the same domain for the edge to be added.
587
+ """
588
+ nodes = [
589
+ _mod("d.engine"),
590
+ _mod("d.facade", {"Thing": "other.core.Thing"}),
591
+ _mod("d.indices"),
592
+ ]
593
+ edges = [
594
+ _imports("d.engine", "d.facade"),
595
+ _imports("d.facade", "d.indices"),
596
+ _imports_symbol("d.engine", "d.facade.Thing"),
597
+ ]
598
+
599
+ assert _census(nodes, edges, "d") == {"021C": 1.0}
600
+
601
+
602
+ def test_reattribution_adds_rather_than_replaces() -> None:
603
+ """The real A->B edge survives alongside the revealed A->C one.
604
+
605
+ engine takes SymbolResolver (real) as well as IndexBuilder (forwarded);
606
+ dropping engine->symbols would trade a hidden edge for a lost one.
607
+ """
608
+ nodes = [
609
+ _mod("d.engine"),
610
+ _mod("d.symbols", {"IndexBuilder": "d.indices.IndexBuilder"}),
611
+ _mod("d.indices"),
612
+ ]
613
+ fp = FingerprintExtractor.from_graph(nodes, _LAUNDERED_EDGES).extract("d")
614
+
615
+ # 030T requires all three edges: engine->symbols, symbols->indices, engine->indices.
616
+ assert fp.t_imports[TRIAD_ORDER.index("030T")] == 1.0
617
+
618
+
619
+ def _reattributed_targets(nodes: list[Node], edges: list[Edge], importer: str) -> set[str]:
620
+ """IMPORTS targets seen by the census for `importer`, including revealed ones."""
621
+ domain_ids = {n.id for n in nodes}
622
+ internal = [e for e in edges if e.source in domain_ids and e.target in domain_ids]
623
+ out = _reattributed_imports(nodes, domain_ids, internal, edges)
624
+ return {e.target for e in out if e.type == EdgeType.IMPORTS and e.source == importer}
625
+
626
+
627
+ def test_chained_reexports_are_followed_to_the_definer() -> None:
628
+ """Two passthroughs in a row must not be a bypass.
629
+
630
+ A -> B -> C -> D where both B and C only forward. Asserting on the edge set
631
+ rather than a triad share: stopping at C still produces a 030T (from the
632
+ a-b-c triangle), so a share-based check passes for the wrong reason.
633
+ """
634
+ nodes = [
635
+ _mod("d.a"),
636
+ _mod("d.b", {"Thing": "d.c.Thing"}),
637
+ _mod("d.c", {"Thing": "d.dd.Thing"}),
638
+ _mod("d.dd"),
639
+ ]
640
+ edges = [
641
+ _imports("d.a", "d.b"),
642
+ _imports("d.b", "d.c"),
643
+ _imports("d.c", "d.dd"),
644
+ _imports_symbol("d.a", "d.b.Thing"),
645
+ ]
646
+
647
+ assert "d.dd" in _reattributed_targets(nodes, edges, "d.a")
648
+
649
+
650
+ def test_reexported_module_resolves_to_the_module_not_its_package() -> None:
651
+ """`from pkg import submodule as submodule` forwards a module, not a symbol.
652
+
653
+ Taking the parent of the forwarded FQN would name the package; the import
654
+ actually lands on the submodule.
655
+ """
656
+ nodes = [_mod("d.a"), _mod("d.facade", {"leaf": "d.leaf"}), _mod("d.leaf")]
657
+ edges = [
658
+ _imports("d.a", "d.facade"),
659
+ _imports("d.facade", "d.leaf"),
660
+ _imports_symbol("d.a", "d.facade.leaf"),
661
+ ]
662
+
663
+ assert "d.leaf" in _reattributed_targets(nodes, edges, "d.a")
664
+
665
+
666
+ def test_reexport_cycle_terminates() -> None:
667
+ """Two modules forwarding the same name to each other must not hang.
668
+
669
+ The docstring claimed the visited set handles this; review pointed out it
670
+ was never tested. Both the mutual and the self-referential shape resolve to
671
+ the symbol they started from, which `_definer_for` then discards.
672
+ """
673
+ mutual = {"d.a": {"T": "d.b.T"}, "d.b": {"T": "d.a.T"}}
674
+ assert _follow_forwarding(mutual, "d.a.T") == "d.a.T"
675
+
676
+ self_ref = {"d.a": {"T": "d.a.T"}}
677
+ assert _follow_forwarding(self_ref, "d.a.T") == "d.a.T"
678
+
679
+
680
+ def test_dotless_symbol_is_not_a_forward() -> None:
681
+ """A top-level name has no `via` part, so it cannot be a passthrough."""
682
+ assert _follow_forwarding({"d.a": {"T": "d.b.T"}}, "Thing") is None
@@ -720,3 +720,90 @@ def test_module_imports_edge_coexists_with_symbol_edges(extractor: PythonExtract
720
720
  module_edges = [e for e in edges if e.type == EdgeType.IMPORTS]
721
721
  assert len(module_edges) == 1
722
722
  assert module_edges[0].target == "a.b"
723
+
724
+
725
+ # ---------------------------------------------------------------------------
726
+ # Transparent re-export detection (#182)
727
+ # ---------------------------------------------------------------------------
728
+
729
+
730
+ def _reexports(extractor: PythonExtractor, code: str) -> dict[str, str]:
731
+ """Return the FILE node's recorded re-exports for a snippet."""
732
+ nodes, _ = extractor.parse(code, "pkg/mod.py")
733
+ file_node = next(n for n in nodes if n.type == NodeType.FILE)
734
+ return file_node.metadata.get("reexports") or {}
735
+
736
+
737
+ def test_unused_import_is_recorded_as_a_reexport(extractor: PythonExtractor) -> None:
738
+ """`X as X` with no use in the body is the laundering shape from #182."""
739
+ code = "from pkg.other import Thing as Thing\n"
740
+
741
+ assert _reexports(extractor, code) == {"Thing": "pkg.other.Thing"}
742
+
743
+
744
+ def test_used_import_is_not_a_reexport(extractor: PythonExtractor) -> None:
745
+ """A re-export that the module also uses is not transparent.
746
+
747
+ Regression guard for the real case in guardian/chunker.py, where
748
+ `split_diff_by_file` is both re-exported and called — a detector that
749
+ flagged every `import X as X` would be wrong about it.
750
+ """
751
+ code = "from pkg.other import Thing as Thing\n\ndef go():\n return Thing()\n"
752
+
753
+ assert _reexports(extractor, code) == {}
754
+
755
+
756
+ def test_import_used_only_in_a_quoted_annotation_is_not_a_reexport(
757
+ extractor: PythonExtractor,
758
+ ) -> None:
759
+ """A forward reference holds no identifier nodes — but it is still a use.
760
+
761
+ Regression: this reported cgis.pipeline's TYPE_CHECKING import of SQLiteStore
762
+ as a re-export, because `store: "SQLiteStore | None"` is a string.
763
+ """
764
+ code = 'from pkg.store import Store\n\ndef go(s: "Store | None") -> None:\n pass\n'
765
+
766
+ assert _reexports(extractor, code) == {}
767
+
768
+
769
+ def test_a_name_mentioned_only_in_a_docstring_is_still_a_reexport(
770
+ extractor: PythonExtractor,
771
+ ) -> None:
772
+ """Only annotation strings count as uses — otherwise prose could mask laundering."""
773
+ code = 'from pkg.other import Thing as Thing\n\ndef go():\n """Mentions Thing."""\n'
774
+
775
+ assert _reexports(extractor, code) == {"Thing": "pkg.other.Thing"}
776
+
777
+
778
+ def test_nested_annotation_string_is_a_use(extractor: PythonExtractor) -> None:
779
+ """`list["Store"]` counts, because the grammar wraps each element in its own `type`.
780
+
781
+ Review raised this as a likely false positive on the assumption that the
782
+ string's parent would be `subscript`. It is not: tree-sitter nests
783
+ generic_type -> type_parameter -> type -> string, so the immediate-parent
784
+ check already covers it. Pinned so a future refactor cannot quietly break it.
785
+ """
786
+ code = 'from pkg.store import Store\n\ndef go(s: list["Store"]) -> None:\n pass\n'
787
+
788
+ assert _reexports(extractor, code) == {}
789
+
790
+
791
+ def test_attribute_name_does_not_count_as_a_use(extractor: PythonExtractor) -> None:
792
+ """`o.thing` mentions `thing` without using the import of that name."""
793
+ code = "from pkg.other import thing as thing\n\ndef go(o):\n return o.thing\n"
794
+
795
+ assert _reexports(extractor, code) == {"thing": "pkg.other.thing"}
796
+
797
+
798
+ def test_keyword_argument_name_does_not_count_as_a_use(extractor: PythonExtractor) -> None:
799
+ """`f(thing=1)` likewise — the keyword is not the imported symbol."""
800
+ code = "from pkg.other import thing as thing\n\ndef go(f):\n return f(thing=1)\n"
801
+
802
+ assert _reexports(extractor, code) == {"thing": "pkg.other.thing"}
803
+
804
+
805
+ def test_object_side_of_an_attribute_is_a_use(extractor: PythonExtractor) -> None:
806
+ """Only the attribute half is excluded — `thing.attr` genuinely uses `thing`."""
807
+ code = "from pkg.other import thing as thing\n\ndef go():\n return thing.attr\n"
808
+
809
+ assert _reexports(extractor, code) == {}
@@ -261,7 +261,7 @@ wheels = [
261
261
 
262
262
  [[package]]
263
263
  name = "codegraph-brain"
264
- version = "0.8.0"
264
+ version = "0.9.0"
265
265
  source = { editable = "." }
266
266
  dependencies = [
267
267
  { name = "httpx" },
@@ -1,3 +0,0 @@
1
- {
2
- ".": "0.8.0"
3
- }
File without changes