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.
- codegraph_brain-0.9.0/.release-please-manifest.json +3 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/CHANGELOG.md +8 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/PKG-INFO +1 -1
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/pyproject.toml +1 -1
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/extractors/python_extractor.py +60 -1
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/drift/fingerprint.py +118 -2
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_fingerprint.py +171 -1
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_python_extractor.py +87 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/uv.lock +1 -1
- codegraph_brain-0.8.0/.release-please-manifest.json +0 -3
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/.claude-plugin/marketplace.json +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/.github/workflows/autodoc.yml +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/.github/workflows/ci.yml +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/.github/workflows/guardian.yml +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/.github/workflows/pr-title.yml +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/.github/workflows/release-please.yml +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/.gitignore +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/.pre-commit-config.yaml +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/.python-version +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/CLAUDE.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/CONTRIBUTING.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/LICENSE +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/Makefile +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/PRIVACY.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/README.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/benchmarks/guardian/CURATION.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/benchmarks/guardian/pr-122.yaml +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/benchmarks/guardian/pr-140.yaml +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/benchmarks/guardian/pr-141.yaml +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/benchmarks/guardian/pr-142.yaml +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/benchmarks/guardian/pr-143.yaml +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/benchmarks/guardian/pr-144.yaml +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/benchmarks/guardian/pr-278.yaml +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/benchmarks/guardian/pr-313.yaml +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/benchmarks/guardian/results.jsonl +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/data/.gitkeep +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/AUDIT.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/CASE_STUDY.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/architecture/HOW_IT_WORKS.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/architecture/ONTOLOGY.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/architecture/PATTERNS_AND_TRIADS.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/architecture/SELF_PORTRAIT.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/architecture/diagrams/pipeline_flow.mermaid +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/architecture/health_badge.json +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/assets/.gitignore +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/assets/cgis-app-avatar.png +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/assets/cgis-app-avatar.svg +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/assets/generate_avatar.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/examples/.gitkeep +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/how-to/AGENT_ONBOARDING.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/how-to/CLI_USAGE.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/how-to/MCP_REFERENCE.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/lab-notes/2026-06-11-chunked-review-negative-result.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/ontology/.gitkeep +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/ontology/core.yaml +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/ontology/domains.yaml +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/ontology/patterns.yaml +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/ontology/tolerances.lock +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-09-domain-pattern-fingerprint-design.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-09-pattern-alphabet-motif-basis-design.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-10-guardian-sprint-design.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-11-fastapi-di-edges-design.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-11-guardian-chunked-review-design.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-11-guardian-chunker-design.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-11-mcp-drift-validate-fqn-design.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-11-resolver-split-design.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-11-symbol-import-edges-design.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-12-drift-empty-domains-design.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-12-gate-semantics-design.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-12-init-ontology-design.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-12-release-please-ci-design.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-13-suggest-packages-design.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-06-13-tangle-anti-pattern-design.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-07-29-guardian-skeptic-scoring-design.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-07-30-cgis-fractal-design.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-07-30-cgis-fractal-plan.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-07-30-chunk-source-filter-design.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-07-30-chunk-source-filter-plan.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-07-30-genai-client-close-design.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-07-30-genai-client-close-plan.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-07-30-guardian-precision-bench-design.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-07-30-guardian-precision-bench-plan.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-07-30-guardian-timeout-retry-design.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-07-30-guardian-timeout-retry-plan.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/2026-07-31-finder-bug-class-taxonomy.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/BLUEPRINT.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/PRD.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/TDD.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-09-fingerprint-drift.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-10-guardian-context-skeptic-inline.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-10-guardian-structured-findings-bench.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-10-motif-basis-part-b.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-10-unified-pattern-alphabet.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-11-fastapi-di-edges.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-11-guardian-chunked-review.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-11-guardian-chunker.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-11-mcp-drift-validate-fqn.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-11-resolver-split.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-12-drift-empty-domains.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-12-gate-semantics.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-12-init-ontology.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-12-release-please-ci.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-12-symbol-import-edges.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-06-13-suggest-packages.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/specs/plans/2026-07-29-guardian-skeptic-scoring.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/main.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/plugin/.claude-plugin/plugin.json +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/plugin/.mcp.json +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/plugin/README.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/plugin/skills/cgis/SKILL.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/plugin/skills/ingest/SKILL.md +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/release-please-config.json +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/scripts/gen_ideal_graph.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/scripts/generate_health.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/scripts/generate_mcp_ref.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/scripts/generate_schema_docs.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/scripts/guardian_bench.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/scripts/guardian_review.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/scripts/inject_readme_graph.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/scripts/probe_closure_gap.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/scripts/probe_tier_ladder.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/__init__.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/__main__.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/api/.gitkeep +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/api/__init__.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/api/mcp_server.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/cli.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/core/.gitkeep +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/core/models.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/extractors/.gitkeep +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/extractors/_python_ast.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/extractors/_python_classes.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/extractors/_python_functions.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/extractors/_python_imports.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/extractors/_python_types.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/extractors/base.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/extractors/typescript_extractor.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/__init__.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/bench.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/chunked.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/chunker.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/collector.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/core.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/diff_index.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/findings.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/github_poster.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/metrics.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/prompts.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/providers/__init__.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/providers/base.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/providers/gemini.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/providers/mistral.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/providers/ollama.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/recording.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/render.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/runner.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/guardian/skeptic.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/pipeline.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/py.typed +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/analysis/__init__.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/analysis/analyzer.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/analysis/anomaly.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/analysis/cohesion.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/analysis/health.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/analysis/suggest_service.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/context/__init__.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/context/audit.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/context/context_service.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/context/prompt.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/context/snippet.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/drift/__init__.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/drift/_scc.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/drift/drift.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/drift/drift_service.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/drift/fractal.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/drift/ontology_init.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/drift/quotient.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/drift/triads.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/engine.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/fqn.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/render/__init__.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/render/graph_json.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/render/mermaid.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/query/render/metrics.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/resolver/.gitkeep +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/resolver/__init__.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/resolver/engine.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/resolver/indices.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/resolver/symbols.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/resolver/uplift.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/storage/.gitkeep +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/src/cgis/storage/sqlite_store.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/integration/.gitkeep +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/self_parsing/.gitkeep +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/self_parsing/__init__.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/self_parsing/conftest.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/self_parsing/test_architecture.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/self_parsing/test_drift.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/self_parsing/test_fractal.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/self_parsing/test_init_ontology_roundtrip.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/self_parsing/test_self_parse.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/self_parsing/test_self_parse_ts.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/self_parsing/test_suggest.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/.gitkeep +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/conftest.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/guardian_stubs.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test___main__.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_analyzer.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_audit.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_cli.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_cohesion.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_context_service.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_di_acceptance.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_drift.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_drift_service.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_fqn.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_fractal.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_gen_ideal_graph.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_generate_mcp_ref.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_graph_json.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_guardian_bench.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_guardian_chunked.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_guardian_chunker.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_guardian_collector.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_guardian_core.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_guardian_diff_index.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_guardian_findings.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_guardian_metrics.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_guardian_poster.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_guardian_providers.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_guardian_recording.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_guardian_render.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_guardian_runner.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_guardian_skeptic.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_health_scorer.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_import_acceptance.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_mcp_server.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_mermaid.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_metrics.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_models.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_ontology_compliance.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_ontology_init.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_patterns_yaml.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_pipeline.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_prompt.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_quotient.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_resolver.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_resolver_indices.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_resolver_symbols.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_snippet.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_sqlite_store.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_suggest_service.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_triads.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_typescript_extractor.py +0 -0
- {codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/tests/unit/test_uplift.py +0 -0
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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) == {}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codegraph_brain-0.8.0 → codegraph_brain-0.9.0}/docs/architecture/diagrams/pipeline_flow.mermaid
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|