codebeacon 0.6.2__tar.gz → 0.6.3__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.
- {codebeacon-0.6.2 → codebeacon-0.6.3}/.cursorrules +9 -9
- {codebeacon-0.6.2 → codebeacon-0.6.3}/AGENTS.md +9 -9
- {codebeacon-0.6.2 → codebeacon-0.6.3}/CLAUDE.md +9 -9
- {codebeacon-0.6.2 → codebeacon-0.6.3}/PKG-INFO +1 -1
- codebeacon-0.6.3/codebeacon/__init__.py +1 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/cache.py +35 -27
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/common/safety.py +14 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/common/symbols.py +7 -1
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/config.py +10 -4
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/contextmap/generator.py +20 -8
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/discover/detector.py +13 -4
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/export/hooks.py +60 -10
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/export/obsidian.py +5 -1
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/extract/dependencies.py +75 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/extract/queries/django.scm +12 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/extract/queries/fastapi.scm +12 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/extract/queries/flask.scm +12 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/extract/routes.py +13 -6
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/extract/services.py +6 -1
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/graph/analyze.py +13 -5
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/graph/build.py +5 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/graph/enrich.py +40 -29
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/wiki/generator.py +4 -3
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/wiki/templates.py +22 -25
- {codebeacon-0.6.2 → codebeacon-0.6.3}/pyproject.toml +1 -1
- codebeacon-0.6.3/tests/test_graphify_parity_0_6_3.py +235 -0
- codebeacon-0.6.3/tests/test_independent_audit_fixes.py +291 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/test_known_bugs.py +2 -2
- codebeacon-0.6.2/codebeacon/__init__.py +0 -1
- {codebeacon-0.6.2 → codebeacon-0.6.3}/.github/CODEOWNERS +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/.github/dependabot.yml +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/.github/workflows/ci.yml +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/.github/workflows/release.yml +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/.gitignore +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/LICENSE +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/README.de.md +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/README.es.md +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/README.fr.md +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/README.ja.md +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/README.ko.md +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/README.md +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/README.pt-BR.md +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/README.zh-CN.md +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/__main__.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/affected.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/cli.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/common/__init__.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/common/filters.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/common/types.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/contextmap/__init__.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/diagnostics.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/discover/__init__.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/discover/ignore.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/discover/scanner.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/export/__init__.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/export/callflow_html.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/export/mcp.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/export/merge.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/export/tree_html.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/extract/__init__.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/extract/base.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/extract/components.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/extract/dotnet.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/extract/entities.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/extract/queries/README.md +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/extract/queries/actix.scm +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/extract/queries/angular.scm +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/extract/queries/aspnet.scm +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/extract/queries/express.scm +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/extract/queries/gin.scm +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/extract/queries/ktor.scm +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/extract/queries/laravel.scm +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/extract/queries/nestjs.scm +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/extract/queries/rails.scm +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/extract/queries/react.scm +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/extract/queries/spring_boot.scm +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/extract/queries/svelte.scm +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/extract/queries/tauri.scm +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/extract/queries/vapor.scm +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/extract/queries/vue.scm +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/extract/semantic.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/graph/__init__.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/graph/cluster.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/graph/write.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/knowledge/__init__.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/knowledge/generator.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/pipeline.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/plugins/__init__.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/plugins/githooks.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/plugins/skills.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/semantic_pipeline.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/skill/SKILL.md +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/wave.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/wiki/__init__.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon/wiki/index.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/codebeacon.yaml.example +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/docs/TRANSLATION_STATUS.md +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/public-plan.md +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/skill/install.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/__init__.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/conftest.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/fixtures/actix/main.rs +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/fixtures/angular/app.component.ts +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/fixtures/aspnet/UserController.cs +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/fixtures/django/views.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/fixtures/express/userRouter.js +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/fixtures/fastapi/main.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/fixtures/flask/app.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/fixtures/gin/main.go +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/fixtures/integration_workspace/api-python/pyproject.toml +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/fixtures/integration_workspace/api-python/src/__init__.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/fixtures/integration_workspace/api-python/src/main.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/fixtures/integration_workspace/api-python/src/services.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/fixtures/integration_workspace/web/package.json +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/fixtures/integration_workspace/web/src/UserPage.tsx +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/fixtures/ktor/UserRoutes.kt +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/fixtures/laravel/UserController.php +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/fixtures/nestjs/user.controller.ts +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/fixtures/rails/users_controller.rb +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/fixtures/react/UserPage.tsx +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/fixtures/spring_boot/UserController.java +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/fixtures/sveltekit/+page.svelte +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/fixtures/vapor/routes.swift +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/fixtures/vue/UserList.vue +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/integration/__init__.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/integration/test_full_pipeline.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/test_affected.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/test_affected_wiki.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/test_audit_bugfixes.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/test_cli_dispatch.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/test_contextmap_paths.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/test_dependencies.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/test_diagnostics.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/test_discover.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/test_dotnet.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/test_entities.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/test_filters.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/test_graph.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/test_graphify_parity_fixes.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/test_knowledge.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/test_mcp_and_semantic.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/test_optional_grammars.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/test_pipeline_module.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/test_plugins.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/test_resolve.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/test_routes.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/test_safety_and_writes.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/test_scanner_sensitive.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/test_semantic.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/test_semantic_hardening.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/test_semantic_stats.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/test_services.py +0 -0
- {codebeacon-0.6.2 → codebeacon-0.6.3}/tests/test_wiki.py +0 -0
|
@@ -27,7 +27,7 @@ Look up by class name — replace `{project}` with the relevant folder:
|
|
|
27
27
|
|
|
28
28
|
| Project | Notes | Example |
|
|
29
29
|
| --- | --- | --- |
|
|
30
|
-
| codebeacon |
|
|
30
|
+
| codebeacon | 409 services, 3 entities | `affected_from_paths.py.md` |
|
|
31
31
|
|
|
32
32
|
### Step 3 — source file
|
|
33
33
|
Open the paths surfaced by Steps 1–2.
|
|
@@ -38,7 +38,7 @@ Open the paths surfaced by Steps 1–2.
|
|
|
38
38
|
|
|
39
39
|
| Project | Framework | Routes | Services | Entities | Components |
|
|
40
40
|
| --- | --- | --- | --- | --- | --- |
|
|
41
|
-
| codebeacon | python | 5 |
|
|
41
|
+
| codebeacon | python | 5 | 409 | 3 | 0 |
|
|
42
42
|
|
|
43
43
|
---
|
|
44
44
|
|
|
@@ -56,19 +56,19 @@ Open the paths surfaced by Steps 1–2.
|
|
|
56
56
|
## Architecture
|
|
57
57
|
|
|
58
58
|
**codebeacon**: **python** · python
|
|
59
|
-
Routes: 5 | Services:
|
|
59
|
+
Routes: 5 | Services: 409 | Entities: 3 | Components: 0
|
|
60
60
|
|
|
61
61
|
## High-Impact Files
|
|
62
62
|
|
|
63
63
|
Changes here affect many other files:
|
|
64
64
|
|
|
65
|
-
- `codebeacon/
|
|
66
|
-
- `codebeacon/
|
|
67
|
-
- `
|
|
68
|
-
- `
|
|
69
|
-
- `
|
|
65
|
+
- `codebeacon/common/safety.py` (imported by 9 files)
|
|
66
|
+
- `codebeacon/discover/detector.py` (imported by 6 files)
|
|
67
|
+
- `codebeacon/wiki/generator.py` (imported by 6 files)
|
|
68
|
+
- `codebeacon/affected.py` (imported by 5 files)
|
|
69
|
+
- `codebeacon/common/symbols.py` (imported by 5 files)
|
|
70
70
|
|
|
71
71
|
---
|
|
72
72
|
|
|
73
|
-
_Generated by [codebeacon](https://github.com/codebeacon/codebeacon) · 2026-
|
|
73
|
+
_Generated by [codebeacon](https://github.com/codebeacon/codebeacon) · 2026-06-10_
|
|
74
74
|
<!-- codebeacon:end -->
|
|
@@ -27,7 +27,7 @@ Look up by class name — replace `{project}` with the relevant folder:
|
|
|
27
27
|
|
|
28
28
|
| Project | Notes | Example |
|
|
29
29
|
| --- | --- | --- |
|
|
30
|
-
| codebeacon |
|
|
30
|
+
| codebeacon | 409 services, 3 entities | `affected_from_paths.py.md` |
|
|
31
31
|
|
|
32
32
|
### Step 3 — source file
|
|
33
33
|
Open the paths surfaced by Steps 1–2.
|
|
@@ -38,7 +38,7 @@ Open the paths surfaced by Steps 1–2.
|
|
|
38
38
|
|
|
39
39
|
| Project | Framework | Routes | Services | Entities | Components |
|
|
40
40
|
| --- | --- | --- | --- | --- | --- |
|
|
41
|
-
| codebeacon | python | 5 |
|
|
41
|
+
| codebeacon | python | 5 | 409 | 3 | 0 |
|
|
42
42
|
|
|
43
43
|
---
|
|
44
44
|
|
|
@@ -56,19 +56,19 @@ Open the paths surfaced by Steps 1–2.
|
|
|
56
56
|
## Architecture
|
|
57
57
|
|
|
58
58
|
**codebeacon**: **python** · python
|
|
59
|
-
Routes: 5 | Services:
|
|
59
|
+
Routes: 5 | Services: 409 | Entities: 3 | Components: 0
|
|
60
60
|
|
|
61
61
|
## High-Impact Files
|
|
62
62
|
|
|
63
63
|
Changes here affect many other files:
|
|
64
64
|
|
|
65
|
-
- `codebeacon/
|
|
66
|
-
- `codebeacon/
|
|
67
|
-
- `
|
|
68
|
-
- `
|
|
69
|
-
- `
|
|
65
|
+
- `codebeacon/common/safety.py` (imported by 9 files)
|
|
66
|
+
- `codebeacon/discover/detector.py` (imported by 6 files)
|
|
67
|
+
- `codebeacon/wiki/generator.py` (imported by 6 files)
|
|
68
|
+
- `codebeacon/affected.py` (imported by 5 files)
|
|
69
|
+
- `codebeacon/common/symbols.py` (imported by 5 files)
|
|
70
70
|
|
|
71
71
|
---
|
|
72
72
|
|
|
73
|
-
_Generated by [codebeacon](https://github.com/codebeacon/codebeacon) · 2026-
|
|
73
|
+
_Generated by [codebeacon](https://github.com/codebeacon/codebeacon) · 2026-06-10_
|
|
74
74
|
<!-- codebeacon:end -->
|
|
@@ -27,7 +27,7 @@ Look up by class name — replace `{project}` with the relevant folder:
|
|
|
27
27
|
|
|
28
28
|
| Project | Notes | Example |
|
|
29
29
|
| --- | --- | --- |
|
|
30
|
-
| codebeacon |
|
|
30
|
+
| codebeacon | 409 services, 3 entities | `affected_from_paths.py.md` |
|
|
31
31
|
|
|
32
32
|
### Step 3 — source file
|
|
33
33
|
Open the paths surfaced by Steps 1–2.
|
|
@@ -43,7 +43,7 @@ searches that span projects.
|
|
|
43
43
|
|
|
44
44
|
| Project | Framework | Routes | Services | Entities | Components |
|
|
45
45
|
| --- | --- | --- | --- | --- | --- |
|
|
46
|
-
| codebeacon | python | 5 |
|
|
46
|
+
| codebeacon | python | 5 | 409 | 3 | 0 |
|
|
47
47
|
|
|
48
48
|
---
|
|
49
49
|
|
|
@@ -61,19 +61,19 @@ searches that span projects.
|
|
|
61
61
|
## Architecture
|
|
62
62
|
|
|
63
63
|
**codebeacon**: **python** · python
|
|
64
|
-
Routes: 5 | Services:
|
|
64
|
+
Routes: 5 | Services: 409 | Entities: 3 | Components: 0
|
|
65
65
|
|
|
66
66
|
## High-Impact Files
|
|
67
67
|
|
|
68
68
|
Changes here affect many other files:
|
|
69
69
|
|
|
70
|
-
- `codebeacon/
|
|
71
|
-
- `codebeacon/
|
|
72
|
-
- `
|
|
73
|
-
- `
|
|
74
|
-
- `
|
|
70
|
+
- `codebeacon/common/safety.py` (imported by 9 files)
|
|
71
|
+
- `codebeacon/discover/detector.py` (imported by 6 files)
|
|
72
|
+
- `codebeacon/wiki/generator.py` (imported by 6 files)
|
|
73
|
+
- `codebeacon/affected.py` (imported by 5 files)
|
|
74
|
+
- `codebeacon/common/symbols.py` (imported by 5 files)
|
|
75
75
|
|
|
76
76
|
---
|
|
77
77
|
|
|
78
|
-
_Generated by [codebeacon](https://github.com/codebeacon/codebeacon) · 2026-
|
|
78
|
+
_Generated by [codebeacon](https://github.com/codebeacon/codebeacon) · 2026-06-10_
|
|
79
79
|
<!-- codebeacon:end -->
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codebeacon
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.3
|
|
4
4
|
Summary: Source code AST analysis tool for AI context generation — unified multi-framework knowledge graph
|
|
5
5
|
Project-URL: Homepage, https://github.com/codebeacon/codebeacon
|
|
6
6
|
Project-URL: Repository, https://github.com/codebeacon/codebeacon
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.6.3"
|
|
@@ -33,6 +33,7 @@ from __future__ import annotations
|
|
|
33
33
|
import hashlib
|
|
34
34
|
import json
|
|
35
35
|
import os
|
|
36
|
+
import threading
|
|
36
37
|
import time
|
|
37
38
|
from pathlib import Path
|
|
38
39
|
from typing import Any, Optional
|
|
@@ -63,6 +64,9 @@ class Cache:
|
|
|
63
64
|
self._hash_memo: dict[str, str] = {}
|
|
64
65
|
# Memoize stat() results within a single run as well
|
|
65
66
|
self._stat_memo: dict[str, tuple[int, int]] = {}
|
|
67
|
+
# One Cache instance is shared by the wave ThreadPoolExecutor workers;
|
|
68
|
+
# the get()/put() check-then-act sequences need a lock to stay atomic.
|
|
69
|
+
self._lock = threading.RLock()
|
|
66
70
|
|
|
67
71
|
def _key(self, file_path: str) -> str:
|
|
68
72
|
"""Map a (possibly absolute) ``file_path`` to its on-disk cache key.
|
|
@@ -117,8 +121,9 @@ class Cache:
|
|
|
117
121
|
|
|
118
122
|
def file_hash(self, file_path: str) -> str:
|
|
119
123
|
"""Compute (and memoize) the SHA-256 hex digest of a file's contents."""
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
with self._lock:
|
|
125
|
+
if file_path in self._hash_memo:
|
|
126
|
+
return self._hash_memo[file_path]
|
|
122
127
|
|
|
123
128
|
h = hashlib.sha256()
|
|
124
129
|
try:
|
|
@@ -129,7 +134,8 @@ class Cache:
|
|
|
129
134
|
except OSError:
|
|
130
135
|
digest = ""
|
|
131
136
|
|
|
132
|
-
self.
|
|
137
|
+
with self._lock:
|
|
138
|
+
self._hash_memo[file_path] = digest
|
|
133
139
|
return digest
|
|
134
140
|
|
|
135
141
|
def _file_stat(self, file_path: str) -> tuple[int, int]:
|
|
@@ -167,21 +173,22 @@ class Cache:
|
|
|
167
173
|
|
|
168
174
|
def get(self, file_path: str) -> Optional[dict]:
|
|
169
175
|
"""Return the cached extraction result dict, or None if stale/missing."""
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
with self._lock:
|
|
177
|
+
key = self._key(file_path)
|
|
178
|
+
entry = self._data.get(key)
|
|
179
|
+
if not entry:
|
|
180
|
+
return None
|
|
181
|
+
mtime_ns, size = self._file_stat(file_path)
|
|
182
|
+
if entry.get("mtime_ns") == mtime_ns and entry.get("size") == size:
|
|
183
|
+
return entry.get("result")
|
|
184
|
+
if entry.get("hash") != self.file_hash(file_path):
|
|
185
|
+
return None
|
|
186
|
+
# Content unchanged despite mtime bump — refresh the stat fields so
|
|
187
|
+
# the next run skips hashing.
|
|
188
|
+
entry["mtime_ns"] = mtime_ns
|
|
189
|
+
entry["size"] = size
|
|
190
|
+
self._dirty = True
|
|
176
191
|
return entry.get("result")
|
|
177
|
-
if entry.get("hash") != self.file_hash(file_path):
|
|
178
|
-
return None
|
|
179
|
-
# Content unchanged despite mtime bump — refresh the stat fields so the
|
|
180
|
-
# next run skips hashing.
|
|
181
|
-
entry["mtime_ns"] = mtime_ns
|
|
182
|
-
entry["size"] = size
|
|
183
|
-
self._dirty = True
|
|
184
|
-
return entry.get("result")
|
|
185
192
|
|
|
186
193
|
def put(self, file_path: str, result: Any, file_hash: Optional[str] = None) -> None:
|
|
187
194
|
"""Store an extraction result for a file.
|
|
@@ -199,16 +206,17 @@ class Cache:
|
|
|
199
206
|
except (TypeError, ImportError):
|
|
200
207
|
result = {"_raw": str(result)}
|
|
201
208
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
209
|
+
with self._lock:
|
|
210
|
+
mtime_ns, size = self._file_stat(file_path)
|
|
211
|
+
self._data[self._key(file_path)] = {
|
|
212
|
+
"hash": h,
|
|
213
|
+
"result": result,
|
|
214
|
+
"ts": time.time(),
|
|
215
|
+
"mtime_ns": mtime_ns,
|
|
216
|
+
"size": size,
|
|
217
|
+
}
|
|
218
|
+
self._hash_memo[file_path] = h
|
|
219
|
+
self._dirty = True
|
|
212
220
|
|
|
213
221
|
def invalidate(self, file_path: str) -> None:
|
|
214
222
|
"""Remove a specific file's cache entry."""
|
|
@@ -102,6 +102,20 @@ def cap_filename(name: str, limit: int = 200) -> str:
|
|
|
102
102
|
return f"{truncated}_{digest}"
|
|
103
103
|
|
|
104
104
|
|
|
105
|
+
def safe_wiki_filename(label: str) -> str:
|
|
106
|
+
"""Filename stem for a wiki article: filesystem-safe and byte-capped.
|
|
107
|
+
|
|
108
|
+
Lives here (not in wiki/generator.py) because BOTH sides of every wiki
|
|
109
|
+
link must agree on it: the generator names the file with it, and the
|
|
110
|
+
templates build `./<stem>.md` links with it. When the two used different
|
|
111
|
+
transforms, any label with a character outside [-_.\\w] (spaces, `#`,
|
|
112
|
+
parentheses, `<>` from generics) produced a file at one path and links
|
|
113
|
+
pointing at another — every such link was dead on arrival.
|
|
114
|
+
"""
|
|
115
|
+
cleaned = "".join(c if c.isalnum() or c in "-_." else "_" for c in label)
|
|
116
|
+
return cap_filename(cleaned)
|
|
117
|
+
|
|
118
|
+
|
|
105
119
|
def git_head(repo_path: str | Path) -> str:
|
|
106
120
|
"""Return the full HEAD commit SHA for ``repo_path``, or "" if not a repo.
|
|
107
121
|
|
|
@@ -27,6 +27,8 @@ class SymbolTable:
|
|
|
27
27
|
self._implements_map: dict[str, list[str]] = {}
|
|
28
28
|
# All known node IDs
|
|
29
29
|
self._node_ids: set[str] = set()
|
|
30
|
+
# node_id → source_file, so resolved edges carry a real file path
|
|
31
|
+
self._node_files: dict[str, str] = {}
|
|
30
32
|
|
|
31
33
|
def build(self, nodes: list[Node]) -> None:
|
|
32
34
|
"""Build symbol maps from a flat list of all extracted nodes.
|
|
@@ -35,6 +37,7 @@ class SymbolTable:
|
|
|
35
37
|
"""
|
|
36
38
|
for node in nodes:
|
|
37
39
|
self._node_ids.add(node.id)
|
|
40
|
+
self._node_files[node.id] = node.source_file or ""
|
|
38
41
|
|
|
39
42
|
label = node.label
|
|
40
43
|
if label not in self._class_map:
|
|
@@ -91,7 +94,10 @@ class SymbolTable:
|
|
|
91
94
|
relation="injects",
|
|
92
95
|
confidence="INFERRED" if is_interface_resolved else "EXTRACTED",
|
|
93
96
|
confidence_score=0.8 if is_interface_resolved else 1.0,
|
|
94
|
-
source_file
|
|
97
|
+
# The source node's actual file — Edge.source_file is a file path
|
|
98
|
+
# everywhere else; stamping the node ID here leaked "proj::Name"
|
|
99
|
+
# strings into beacon.json's source_file fields.
|
|
100
|
+
source_file=self._node_files.get(ref.source_node_id, ""),
|
|
95
101
|
)
|
|
96
102
|
|
|
97
103
|
def resolve_all(
|
|
@@ -70,6 +70,10 @@ def load_config(path: str | Path) -> CodebeaconConfig:
|
|
|
70
70
|
|
|
71
71
|
projects = []
|
|
72
72
|
for p in projects_raw:
|
|
73
|
+
# A bare `-` in the YAML list arrives as None; a scalar entry as str.
|
|
74
|
+
# Both must surface as a config error, not a TypeError traceback.
|
|
75
|
+
if not isinstance(p, dict):
|
|
76
|
+
raise ValueError(f"Invalid project entry (expected mapping): {p!r}")
|
|
73
77
|
if "name" not in p or "path" not in p:
|
|
74
78
|
raise ValueError(f"Project entry missing 'name' or 'path': {p}")
|
|
75
79
|
# Resolve path relative to config file location
|
|
@@ -82,8 +86,10 @@ def load_config(path: str | Path) -> CodebeaconConfig:
|
|
|
82
86
|
type=p.get("type", "auto"),
|
|
83
87
|
))
|
|
84
88
|
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
# `.get(key, {})` is not enough: a section present-but-empty in the YAML
|
|
90
|
+
# (`output:` on its own line) loads as None, and None.get(...) crashes.
|
|
91
|
+
output_raw = raw.get("output") or {}
|
|
92
|
+
context_map = output_raw.get("context_map") or {}
|
|
87
93
|
output = OutputConfig(
|
|
88
94
|
dir=output_raw.get("dir", ".codebeacon"),
|
|
89
95
|
wiki=output_raw.get("wiki", True),
|
|
@@ -91,14 +97,14 @@ def load_config(path: str | Path) -> CodebeaconConfig:
|
|
|
91
97
|
context_map_targets=context_map.get("targets", ["CLAUDE.md", ".cursorrules", "AGENTS.md"]),
|
|
92
98
|
)
|
|
93
99
|
|
|
94
|
-
wave_raw = raw.get("wave"
|
|
100
|
+
wave_raw = raw.get("wave") or {}
|
|
95
101
|
wave = WaveConfig(
|
|
96
102
|
auto=wave_raw.get("auto", True),
|
|
97
103
|
chunk_size=wave_raw.get("chunk_size", 300),
|
|
98
104
|
max_parallel=wave_raw.get("max_parallel", 5),
|
|
99
105
|
)
|
|
100
106
|
|
|
101
|
-
semantic_raw = raw.get("semantic"
|
|
107
|
+
semantic_raw = raw.get("semantic") or {}
|
|
102
108
|
semantic = SemanticConfig(
|
|
103
109
|
enabled=semantic_raw.get("enabled", False),
|
|
104
110
|
)
|
|
@@ -247,14 +247,26 @@ def _collect_stats(G: nx.DiGraph) -> dict[str, dict[str, int]]:
|
|
|
247
247
|
|
|
248
248
|
|
|
249
249
|
def _hub_files(G: nx.DiGraph, top_n: int = 5) -> list[tuple[str, int]]:
|
|
250
|
-
"""Return (file_path,
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
250
|
+
"""Return (file_path, importer_count) for the most-imported source files.
|
|
251
|
+
|
|
252
|
+
Counts DISTINCT importing files per edge-target file, mirroring
|
|
253
|
+
analyze.hub_files. The old version counted `edge["source_file"]` — the
|
|
254
|
+
importer's own file — so CLAUDE.md's "High-Impact Files" actually listed
|
|
255
|
+
the files that *import the most* (entry points), with counts inflated by
|
|
256
|
+
per-node edge remapping (e.g. "imported by 392 files" in a 60-file repo).
|
|
257
|
+
"""
|
|
258
|
+
importers: dict[str, set[str]] = defaultdict(set)
|
|
259
|
+
for src, tgt, data in G.edges(data=True):
|
|
260
|
+
if data.get("relation") not in ("imports", "imports_from"):
|
|
261
|
+
continue
|
|
262
|
+
tgt_file = G.nodes[tgt].get("source_file", "")
|
|
263
|
+
src_file = G.nodes[src].get("source_file", "")
|
|
264
|
+
if tgt_file and src_file and tgt_file != src_file:
|
|
265
|
+
importers[tgt_file].add(src_file)
|
|
266
|
+
ranked = sorted(
|
|
267
|
+
((fp, len(srcs)) for fp, srcs in importers.items()),
|
|
268
|
+
key=lambda x: (-x[1], x[0]),
|
|
269
|
+
)
|
|
258
270
|
return ranked[:top_n]
|
|
259
271
|
|
|
260
272
|
|
|
@@ -218,10 +218,19 @@ def _detect_language_from_files(directory: Path) -> str:
|
|
|
218
218
|
".swift": "swift",
|
|
219
219
|
}
|
|
220
220
|
try:
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
221
|
+
# os.walk with pruning, not rglob: an unbounded rglob descends into
|
|
222
|
+
# node_modules / .git / dist, which on a Node repo means enumerating
|
|
223
|
+
# tens of thousands of vendored files (and skews the language vote
|
|
224
|
+
# toward vendored JS) just to answer "what language is this project".
|
|
225
|
+
for dirpath, dirnames, filenames in os.walk(directory):
|
|
226
|
+
dirnames[:] = [
|
|
227
|
+
d for d in dirnames if d not in _SKIP_DIRS and not d.startswith(".")
|
|
228
|
+
]
|
|
229
|
+
for fname in filenames:
|
|
230
|
+
ext = os.path.splitext(fname)[1]
|
|
231
|
+
if ext in ext_to_lang:
|
|
232
|
+
lang = ext_to_lang[ext]
|
|
233
|
+
counts[lang] = counts.get(lang, 0) + 1
|
|
225
234
|
except (PermissionError, OSError):
|
|
226
235
|
pass
|
|
227
236
|
|
|
@@ -25,7 +25,7 @@ import sys
|
|
|
25
25
|
from pathlib import Path
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
_POST_COMMIT_HOOK_TEMPLATE = r"""#!/usr/bin/env bash
|
|
29
29
|
# codebeacon: incremental rebuild after each commit.
|
|
30
30
|
# Detaches so `git commit` returns immediately; output goes to the log below.
|
|
31
31
|
#
|
|
@@ -54,14 +54,51 @@ if [ -z "$CHANGED" ] || ! echo "$CHANGED" | grep -v '^\.codebeacon/' | grep -E "
|
|
|
54
54
|
exit 0
|
|
55
55
|
fi
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
# Interpreter pinned at install time so the hook still works where the
|
|
58
|
+
# `codebeacon` launcher is not on PATH — GUI git clients and CI runners
|
|
59
|
+
# don't inherit a login shell's PATH, which made the old `nohup codebeacon`
|
|
60
|
+
# form silently no-op. The heredoc detaches via subprocess so no `nohup`
|
|
61
|
+
# is needed (it doesn't exist on Windows). Mirrors graphify #1127/#1161.
|
|
62
|
+
CODEBEACON_PYTHON=__CODEBEACON_PYTHON__
|
|
63
|
+
|
|
64
|
+
if [ -x "$CODEBEACON_PYTHON" ]; then
|
|
65
|
+
"$CODEBEACON_PYTHON" - "$LOG" <<'CODEBEACON_PY' >>"$LOG" 2>&1
|
|
66
|
+
import subprocess, sys
|
|
67
|
+
kwargs = {"stdin": subprocess.DEVNULL}
|
|
68
|
+
if sys.platform == "win32":
|
|
69
|
+
kwargs["creationflags"] = 0x00000208 # DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP
|
|
70
|
+
else:
|
|
71
|
+
kwargs["start_new_session"] = True
|
|
72
|
+
log = open(sys.argv[1], "ab")
|
|
73
|
+
subprocess.Popen(
|
|
74
|
+
[sys.executable, "-m", "codebeacon", "scan", ".", "--update"],
|
|
75
|
+
stdout=log, stderr=log, **kwargs,
|
|
76
|
+
)
|
|
77
|
+
CODEBEACON_PY
|
|
78
|
+
else
|
|
79
|
+
# Pinned interpreter vanished (reinstall, deleted venv) — fall back to PATH.
|
|
80
|
+
(
|
|
81
|
+
nohup codebeacon scan . --update >>"$LOG" 2>&1 &
|
|
82
|
+
) >/dev/null 2>&1 &
|
|
83
|
+
disown 2>/dev/null || true
|
|
84
|
+
fi
|
|
61
85
|
exit 0
|
|
62
86
|
"""
|
|
63
87
|
|
|
64
88
|
|
|
89
|
+
def _render_post_commit_hook() -> str:
|
|
90
|
+
"""Render the hook with the current interpreter pinned.
|
|
91
|
+
|
|
92
|
+
``as_posix()`` keeps the path usable from git's bundled sh on Windows
|
|
93
|
+
(``C:/Python311/python.exe``); single quotes stop spaces or shell
|
|
94
|
+
metacharacters in the path from being interpreted.
|
|
95
|
+
"""
|
|
96
|
+
python_path = "'" + Path(sys.executable).as_posix().replace("'", "'\\''") + "'"
|
|
97
|
+
# Plain token replacement — str.format would choke on the template's own
|
|
98
|
+
# shell braces (`${HOME}`, the heredoc's dict literal).
|
|
99
|
+
return _POST_COMMIT_HOOK_TEMPLATE.replace("__CODEBEACON_PYTHON__", python_path)
|
|
100
|
+
|
|
101
|
+
|
|
65
102
|
def install_hooks(repo_path: str | Path) -> int:
|
|
66
103
|
"""Install the merge driver, ``.gitattributes`` entry, and post-commit hook.
|
|
67
104
|
|
|
@@ -105,8 +142,13 @@ def _is_git_repo(repo: Path) -> bool:
|
|
|
105
142
|
|
|
106
143
|
def _register_merge_driver(repo: Path) -> None:
|
|
107
144
|
"""Register the ``codebeacon`` merge driver in ``.git/config``."""
|
|
108
|
-
codebeacon = shutil.which("codebeacon")
|
|
109
|
-
|
|
145
|
+
codebeacon = shutil.which("codebeacon")
|
|
146
|
+
if codebeacon:
|
|
147
|
+
cmd = f'"{Path(codebeacon).as_posix()}" merge-driver "%O" "%A" "%B"'
|
|
148
|
+
else:
|
|
149
|
+
# Launcher not on PATH (uv tool / pipx install, GUI client) — invoke
|
|
150
|
+
# the module through the pinned interpreter instead.
|
|
151
|
+
cmd = f'"{Path(sys.executable).as_posix()}" -m codebeacon merge-driver "%O" "%A" "%B"'
|
|
110
152
|
subprocess.run(
|
|
111
153
|
["git", "config", "--local", "merge.codebeacon.name", "codebeacon union merge"],
|
|
112
154
|
cwd=str(repo),
|
|
@@ -134,9 +176,17 @@ def _install_post_commit(repo: Path) -> None:
|
|
|
134
176
|
hooks_dir = _hooks_dir(repo)
|
|
135
177
|
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
136
178
|
hook = hooks_dir / "post-commit"
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
179
|
+
content = _render_post_commit_hook()
|
|
180
|
+
if hook.exists():
|
|
181
|
+
existing = hook.read_text(encoding="utf-8")
|
|
182
|
+
if existing == content:
|
|
183
|
+
return
|
|
184
|
+
# A codebeacon-authored hook (our marker header) gets refreshed so
|
|
185
|
+
# re-running `hook install` picks up template fixes; anything else
|
|
186
|
+
# that merely mentions codebeacon is the user's — leave it alone.
|
|
187
|
+
if "codebeacon: incremental rebuild" not in existing and "codebeacon" in existing:
|
|
188
|
+
return
|
|
189
|
+
hook.write_text(content, encoding="utf-8")
|
|
140
190
|
hook.chmod(hook.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
141
191
|
|
|
142
192
|
|
|
@@ -641,7 +641,11 @@ def _step8_move_remaining(vault: Path) -> None:
|
|
|
641
641
|
content = md.read_text(errors="ignore")
|
|
642
642
|
m = _PROJECT_RE.search(content)
|
|
643
643
|
if m and m.group(1):
|
|
644
|
-
|
|
644
|
+
# Cap here so the folder, the Step-9 hub note (`<svc>/<svc>.md`),
|
|
645
|
+
# and every backlink derived from the folder name stay consistent
|
|
646
|
+
# and under the 255-byte filesystem limit — note filenames are
|
|
647
|
+
# capped in _safe_note_name but this path wasn't.
|
|
648
|
+
svc = cap_filename(m.group(1))
|
|
645
649
|
dest_dir = vault / svc
|
|
646
650
|
dest_dir.mkdir(exist_ok=True)
|
|
647
651
|
dest = dest_dir / md.name
|
|
@@ -60,6 +60,52 @@ _JS_REQUIRE_RE = re.compile(
|
|
|
60
60
|
_JS_FAMILY_EXTS = frozenset({".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"})
|
|
61
61
|
|
|
62
62
|
|
|
63
|
+
def _strip_js_comments(source: str) -> str:
|
|
64
|
+
"""Blank out ``//`` and ``/* */`` comments, preserving string literals.
|
|
65
|
+
|
|
66
|
+
The regex passes below run on raw text, so a commented-out
|
|
67
|
+
``export * from './foo'`` or ``require('./x')`` — or a module path
|
|
68
|
+
mentioned in prose like ``// re-exported from './index'`` — would
|
|
69
|
+
otherwise produce a phantom edge and false import cycles. Mirrors
|
|
70
|
+
graphify #1193. String/template literals are scanned so a ``//``
|
|
71
|
+
inside ``'http://…'`` is not treated as a comment.
|
|
72
|
+
"""
|
|
73
|
+
out: list[str] = []
|
|
74
|
+
i, n = 0, len(source)
|
|
75
|
+
while i < n:
|
|
76
|
+
c = source[i]
|
|
77
|
+
if c in "'\"`":
|
|
78
|
+
quote = c
|
|
79
|
+
out.append(c)
|
|
80
|
+
i += 1
|
|
81
|
+
while i < n:
|
|
82
|
+
ch = source[i]
|
|
83
|
+
out.append(ch)
|
|
84
|
+
if ch == "\\" and i + 1 < n:
|
|
85
|
+
out.append(source[i + 1])
|
|
86
|
+
i += 2
|
|
87
|
+
continue
|
|
88
|
+
i += 1
|
|
89
|
+
if ch == quote:
|
|
90
|
+
break
|
|
91
|
+
if ch == "\n" and quote != "`":
|
|
92
|
+
break # unterminated ' / " string ends at the line
|
|
93
|
+
elif c == "/" and i + 1 < n and source[i + 1] == "/":
|
|
94
|
+
while i < n and source[i] != "\n":
|
|
95
|
+
i += 1
|
|
96
|
+
elif c == "/" and i + 1 < n and source[i + 1] == "*":
|
|
97
|
+
i += 2
|
|
98
|
+
while i + 1 < n and not (source[i] == "*" and source[i + 1] == "/"):
|
|
99
|
+
if source[i] == "\n":
|
|
100
|
+
out.append("\n")
|
|
101
|
+
i += 1
|
|
102
|
+
i = min(i + 2, n)
|
|
103
|
+
else:
|
|
104
|
+
out.append(c)
|
|
105
|
+
i += 1
|
|
106
|
+
return "".join(out)
|
|
107
|
+
|
|
108
|
+
|
|
63
109
|
# ── Framework → query file stem ───────────────────────────────────────────────
|
|
64
110
|
|
|
65
111
|
_FW_TO_QUERY: dict[str, str] = {
|
|
@@ -175,6 +221,34 @@ def extract_dependencies(file_path: str, framework: str) -> list[Edge]:
|
|
|
175
221
|
source_file=file_path,
|
|
176
222
|
))
|
|
177
223
|
|
|
224
|
+
# Python `from pkg import name` — the module path alone loses the
|
|
225
|
+
# actual import target (`from auth.services import UserService` only
|
|
226
|
+
# yielded "auth.services", so the edge to UserService was never even
|
|
227
|
+
# attempted, and `from pkg import submodule` left the importer
|
|
228
|
+
# disconnected from pkg/submodule.py). The Python queries capture the
|
|
229
|
+
# imported names as @import.item; emit a dotted `module.name` target
|
|
230
|
+
# for each so label resolution can bind the real symbol. Mirrors
|
|
231
|
+
# graphify #1146.
|
|
232
|
+
if "import.item" in caps and "import.path" in caps:
|
|
233
|
+
module_raw = node_text(caps["import.path"][0]).strip("'\"` ")
|
|
234
|
+
for item_node in caps["import.item"]:
|
|
235
|
+
item = node_text(item_node).strip()
|
|
236
|
+
if not item:
|
|
237
|
+
continue
|
|
238
|
+
dotted = f"{module_raw}.{item}" if module_raw else item
|
|
239
|
+
key = ("imports_from", dotted)
|
|
240
|
+
if key in seen:
|
|
241
|
+
continue
|
|
242
|
+
seen.add(key)
|
|
243
|
+
edges.append(Edge(
|
|
244
|
+
source=file_path,
|
|
245
|
+
target=dotted,
|
|
246
|
+
relation="imports_from",
|
|
247
|
+
confidence="EXTRACTED",
|
|
248
|
+
confidence_score=1.0,
|
|
249
|
+
source_file=file_path,
|
|
250
|
+
))
|
|
251
|
+
|
|
178
252
|
# Vapor uses @import.name instead of @import.path
|
|
179
253
|
if "import.name" in caps:
|
|
180
254
|
for import_node in caps["import.name"]:
|
|
@@ -201,6 +275,7 @@ def extract_dependencies(file_path: str, framework: str) -> list[Edge]:
|
|
|
201
275
|
source = Path(file_path).read_text(encoding="utf-8", errors="replace")
|
|
202
276
|
except OSError:
|
|
203
277
|
source = ""
|
|
278
|
+
source = _strip_js_comments(source)
|
|
204
279
|
for m in _JS_REEXPORT_RE.finditer(source):
|
|
205
280
|
raw = m.group(1).strip()
|
|
206
281
|
key = ("re_exports", raw)
|
|
@@ -117,6 +117,18 @@
|
|
|
117
117
|
module_name: _ @import.path
|
|
118
118
|
) @import.from
|
|
119
119
|
|
|
120
|
+
; `from pkg import name` — also capture each imported name so the edge can
|
|
121
|
+
; bind the real symbol/submodule, not just the module path (graphify #1146).
|
|
122
|
+
(import_from_statement
|
|
123
|
+
module_name: _ @import.path
|
|
124
|
+
name: (dotted_name) @import.item
|
|
125
|
+
) @import.from_item
|
|
126
|
+
|
|
127
|
+
(import_from_statement
|
|
128
|
+
module_name: _ @import.path
|
|
129
|
+
name: (aliased_import name: (dotted_name) @import.item)
|
|
130
|
+
) @import.from_item
|
|
131
|
+
|
|
120
132
|
(import_statement
|
|
121
133
|
name: _ @import.path
|
|
122
134
|
) @import.plain
|
|
@@ -147,6 +147,18 @@
|
|
|
147
147
|
module_name: _ @import.path
|
|
148
148
|
) @import.from
|
|
149
149
|
|
|
150
|
+
; `from pkg import name` — also capture each imported name so the edge can
|
|
151
|
+
; bind the real symbol/submodule, not just the module path (graphify #1146).
|
|
152
|
+
(import_from_statement
|
|
153
|
+
module_name: _ @import.path
|
|
154
|
+
name: (dotted_name) @import.item
|
|
155
|
+
) @import.from_item
|
|
156
|
+
|
|
157
|
+
(import_from_statement
|
|
158
|
+
module_name: _ @import.path
|
|
159
|
+
name: (aliased_import name: (dotted_name) @import.item)
|
|
160
|
+
) @import.from_item
|
|
161
|
+
|
|
150
162
|
(import_statement
|
|
151
163
|
name: _ @import.path
|
|
152
164
|
) @import.plain
|