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