codebeacon 0.6.1__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.
Files changed (153) hide show
  1. {codebeacon-0.6.1 → codebeacon-0.6.3}/.cursorrules +9 -9
  2. {codebeacon-0.6.1 → codebeacon-0.6.3}/AGENTS.md +9 -9
  3. {codebeacon-0.6.1 → codebeacon-0.6.3}/CLAUDE.md +9 -9
  4. {codebeacon-0.6.1 → codebeacon-0.6.3}/PKG-INFO +1 -1
  5. codebeacon-0.6.3/codebeacon/__init__.py +1 -0
  6. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/cache.py +35 -27
  7. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/common/safety.py +47 -0
  8. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/common/symbols.py +7 -1
  9. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/common/types.py +2 -0
  10. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/config.py +10 -4
  11. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/contextmap/generator.py +20 -8
  12. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/discover/detector.py +13 -4
  13. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/export/hooks.py +60 -10
  14. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/export/obsidian.py +17 -4
  15. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/dependencies.py +75 -0
  16. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/angular.scm +18 -0
  17. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/django.scm +12 -0
  18. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/fastapi.scm +12 -0
  19. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/flask.scm +12 -0
  20. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/nestjs.scm +18 -0
  21. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/routes.py +13 -6
  22. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/services.py +67 -1
  23. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/graph/analyze.py +13 -5
  24. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/graph/build.py +78 -1
  25. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/graph/cluster.py +40 -12
  26. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/graph/enrich.py +40 -29
  27. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/wiki/generator.py +11 -2
  28. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/wiki/templates.py +22 -25
  29. {codebeacon-0.6.1 → codebeacon-0.6.3}/pyproject.toml +1 -1
  30. codebeacon-0.6.3/tests/test_graphify_parity_0_6_3.py +235 -0
  31. codebeacon-0.6.3/tests/test_graphify_parity_fixes.py +335 -0
  32. codebeacon-0.6.3/tests/test_independent_audit_fixes.py +291 -0
  33. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_known_bugs.py +2 -2
  34. codebeacon-0.6.1/codebeacon/__init__.py +0 -1
  35. {codebeacon-0.6.1 → codebeacon-0.6.3}/.github/CODEOWNERS +0 -0
  36. {codebeacon-0.6.1 → codebeacon-0.6.3}/.github/dependabot.yml +0 -0
  37. {codebeacon-0.6.1 → codebeacon-0.6.3}/.github/workflows/ci.yml +0 -0
  38. {codebeacon-0.6.1 → codebeacon-0.6.3}/.github/workflows/release.yml +0 -0
  39. {codebeacon-0.6.1 → codebeacon-0.6.3}/.gitignore +0 -0
  40. {codebeacon-0.6.1 → codebeacon-0.6.3}/LICENSE +0 -0
  41. {codebeacon-0.6.1 → codebeacon-0.6.3}/README.de.md +0 -0
  42. {codebeacon-0.6.1 → codebeacon-0.6.3}/README.es.md +0 -0
  43. {codebeacon-0.6.1 → codebeacon-0.6.3}/README.fr.md +0 -0
  44. {codebeacon-0.6.1 → codebeacon-0.6.3}/README.ja.md +0 -0
  45. {codebeacon-0.6.1 → codebeacon-0.6.3}/README.ko.md +0 -0
  46. {codebeacon-0.6.1 → codebeacon-0.6.3}/README.md +0 -0
  47. {codebeacon-0.6.1 → codebeacon-0.6.3}/README.pt-BR.md +0 -0
  48. {codebeacon-0.6.1 → codebeacon-0.6.3}/README.zh-CN.md +0 -0
  49. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/__main__.py +0 -0
  50. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/affected.py +0 -0
  51. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/cli.py +0 -0
  52. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/common/__init__.py +0 -0
  53. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/common/filters.py +0 -0
  54. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/contextmap/__init__.py +0 -0
  55. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/diagnostics.py +0 -0
  56. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/discover/__init__.py +0 -0
  57. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/discover/ignore.py +0 -0
  58. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/discover/scanner.py +0 -0
  59. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/export/__init__.py +0 -0
  60. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/export/callflow_html.py +0 -0
  61. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/export/mcp.py +0 -0
  62. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/export/merge.py +0 -0
  63. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/export/tree_html.py +0 -0
  64. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/__init__.py +0 -0
  65. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/base.py +0 -0
  66. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/components.py +0 -0
  67. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/dotnet.py +0 -0
  68. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/entities.py +0 -0
  69. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/README.md +0 -0
  70. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/actix.scm +0 -0
  71. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/aspnet.scm +0 -0
  72. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/express.scm +0 -0
  73. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/gin.scm +0 -0
  74. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/ktor.scm +0 -0
  75. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/laravel.scm +0 -0
  76. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/rails.scm +0 -0
  77. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/react.scm +0 -0
  78. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/spring_boot.scm +0 -0
  79. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/svelte.scm +0 -0
  80. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/tauri.scm +0 -0
  81. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/vapor.scm +0 -0
  82. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/vue.scm +0 -0
  83. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/semantic.py +0 -0
  84. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/graph/__init__.py +0 -0
  85. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/graph/write.py +0 -0
  86. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/knowledge/__init__.py +0 -0
  87. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/knowledge/generator.py +0 -0
  88. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/pipeline.py +0 -0
  89. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/plugins/__init__.py +0 -0
  90. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/plugins/githooks.py +0 -0
  91. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/plugins/skills.py +0 -0
  92. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/semantic_pipeline.py +0 -0
  93. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/skill/SKILL.md +0 -0
  94. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/wave.py +0 -0
  95. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/wiki/__init__.py +0 -0
  96. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/wiki/index.py +0 -0
  97. {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon.yaml.example +0 -0
  98. {codebeacon-0.6.1 → codebeacon-0.6.3}/docs/TRANSLATION_STATUS.md +0 -0
  99. {codebeacon-0.6.1 → codebeacon-0.6.3}/public-plan.md +0 -0
  100. {codebeacon-0.6.1 → codebeacon-0.6.3}/skill/install.py +0 -0
  101. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/__init__.py +0 -0
  102. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/conftest.py +0 -0
  103. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/actix/main.rs +0 -0
  104. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/angular/app.component.ts +0 -0
  105. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/aspnet/UserController.cs +0 -0
  106. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/django/views.py +0 -0
  107. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/express/userRouter.js +0 -0
  108. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/fastapi/main.py +0 -0
  109. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/flask/app.py +0 -0
  110. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/gin/main.go +0 -0
  111. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/integration_workspace/api-python/pyproject.toml +0 -0
  112. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/integration_workspace/api-python/src/__init__.py +0 -0
  113. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/integration_workspace/api-python/src/main.py +0 -0
  114. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/integration_workspace/api-python/src/services.py +0 -0
  115. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/integration_workspace/web/package.json +0 -0
  116. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/integration_workspace/web/src/UserPage.tsx +0 -0
  117. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/ktor/UserRoutes.kt +0 -0
  118. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/laravel/UserController.php +0 -0
  119. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/nestjs/user.controller.ts +0 -0
  120. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/rails/users_controller.rb +0 -0
  121. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/react/UserPage.tsx +0 -0
  122. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/spring_boot/UserController.java +0 -0
  123. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/sveltekit/+page.svelte +0 -0
  124. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/vapor/routes.swift +0 -0
  125. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/vue/UserList.vue +0 -0
  126. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/integration/__init__.py +0 -0
  127. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/integration/test_full_pipeline.py +0 -0
  128. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_affected.py +0 -0
  129. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_affected_wiki.py +0 -0
  130. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_audit_bugfixes.py +0 -0
  131. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_cli_dispatch.py +0 -0
  132. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_contextmap_paths.py +0 -0
  133. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_dependencies.py +0 -0
  134. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_diagnostics.py +0 -0
  135. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_discover.py +0 -0
  136. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_dotnet.py +0 -0
  137. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_entities.py +0 -0
  138. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_filters.py +0 -0
  139. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_graph.py +0 -0
  140. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_knowledge.py +0 -0
  141. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_mcp_and_semantic.py +0 -0
  142. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_optional_grammars.py +0 -0
  143. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_pipeline_module.py +0 -0
  144. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_plugins.py +0 -0
  145. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_resolve.py +0 -0
  146. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_routes.py +0 -0
  147. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_safety_and_writes.py +0 -0
  148. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_scanner_sensitive.py +0 -0
  149. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_semantic.py +0 -0
  150. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_semantic_hardening.py +0 -0
  151. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_semantic_stats.py +0 -0
  152. {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_services.py +0 -0
  153. {codebeacon-0.6.1 → 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 | 398 services, 3 entities | `affected_from_paths.py.md` |
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 | 398 | 3 | 0 |
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: 398 | Entities: 3 | Components: 0
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/semantic_pipeline.py` (imported by 56 files)
66
- - `codebeacon/pipeline.py` (imported by 12 files)
67
- - `tests/test_graph.py` (imported by 6 files)
68
- - `tests/integration/test_full_pipeline.py` (imported by 3 files)
69
- - `tests/test_known_bugs.py` (imported by 2 files)
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-05-31_
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 | 398 services, 3 entities | `affected_from_paths.py.md` |
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 | 398 | 3 | 0 |
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: 398 | Entities: 3 | Components: 0
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/semantic_pipeline.py` (imported by 56 files)
66
- - `codebeacon/pipeline.py` (imported by 12 files)
67
- - `tests/test_graph.py` (imported by 6 files)
68
- - `tests/integration/test_full_pipeline.py` (imported by 3 files)
69
- - `tests/test_known_bugs.py` (imported by 2 files)
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-05-31_
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 | 398 services, 3 entities | `affected_from_paths.py.md` |
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 | 398 | 3 | 0 |
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: 398 | Entities: 3 | Components: 0
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/semantic_pipeline.py` (imported by 56 files)
71
- - `codebeacon/pipeline.py` (imported by 12 files)
72
- - `tests/test_graph.py` (imported by 6 files)
73
- - `tests/integration/test_full_pipeline.py` (imported by 3 files)
74
- - `tests/test_known_bugs.py` (imported by 2 files)
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-05-31_
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.1
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
- if file_path in self._hash_memo:
121
- return self._hash_memo[file_path]
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._hash_memo[file_path] = digest
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
- key = self._key(file_path)
171
- entry = self._data.get(key)
172
- if not entry:
173
- return None
174
- mtime_ns, size = self._file_stat(file_path)
175
- if entry.get("mtime_ns") == mtime_ns and entry.get("size") == size:
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
- mtime_ns, size = self._file_stat(file_path)
203
- self._data[self._key(file_path)] = {
204
- "hash": h,
205
- "result": result,
206
- "ts": time.time(),
207
- "mtime_ns": mtime_ns,
208
- "size": size,
209
- }
210
- self._hash_memo[file_path] = h
211
- self._dirty = True
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."""
@@ -18,6 +18,7 @@ Three concerns:
18
18
 
19
19
  from __future__ import annotations
20
20
 
21
+ import hashlib
21
22
  import re
22
23
  import subprocess
23
24
  from pathlib import Path
@@ -69,6 +70,52 @@ def escape_frontmatter_value(value: object) -> str:
69
70
  return text
70
71
 
71
72
 
73
+ def cap_filename(name: str, limit: int = 200) -> str:
74
+ """Cap a filename stem to ``limit`` UTF-8 *bytes*, collision-safely.
75
+
76
+ A single path component is capped at 255 bytes by every mainstream
77
+ filesystem, so a node label long enough to overflow that — roughly 85 CJK
78
+ characters at 3 bytes each, or 255 ASCII — makes ``Path.write_text`` raise
79
+ ``OSError`` (ENAMETOOLONG) and aborts the *entire* obsidian / wiki export,
80
+ not just the one note. We cap to 200 bytes to leave headroom for the
81
+ trailing ``.md`` and any ``_N`` dedup suffix the caller appends.
82
+
83
+ Two properties matter:
84
+
85
+ * The budget is counted in **bytes**, not characters, so multi-byte scripts
86
+ (CJK, emoji) cannot slip past a character-count guard.
87
+ * When truncation actually happens we append ``_<sha1[:8]>`` of the original
88
+ name, so two labels sharing a long common prefix
89
+ (``"z"*250 + "_ALPHA"`` vs ``"z"*250 + "_BETA"``) still resolve to
90
+ distinct files instead of silently colliding.
91
+
92
+ Truncation slices the UTF-8 byte string and decodes with ``"ignore"`` so a
93
+ multi-byte character straddling the cut is dropped rather than producing
94
+ mojibake. Short names are returned untouched. Mirrors graphify 690b4e5.
95
+ """
96
+ encoded = name.encode("utf-8")
97
+ if len(encoded) <= limit:
98
+ return name
99
+ digest = hashlib.sha1(name.encode("utf-8")).hexdigest()[:8]
100
+ keep = max(limit - 9, 0) # room for "_" + 8 hex chars
101
+ truncated = encoded[:keep].decode("utf-8", "ignore")
102
+ return f"{truncated}_{digest}"
103
+
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
+
72
119
  def git_head(repo_path: str | Path) -> str:
73
120
  """Return the full HEAD commit SHA for ``repo_path``, or "" if not a repo.
74
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=ref.source_node_id,
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(
@@ -62,6 +62,8 @@ class ServiceInfo:
62
62
  methods: list = field(default_factory=list) # list[str] - method names
63
63
  dependencies: list = field(default_factory=list) # list[str] - injected type names (unresolved)
64
64
  annotations: list = field(default_factory=list) # list[str] - @Service, @Injectable, etc.
65
+ implements: list = field(default_factory=list) # list[str] - implemented interface names
66
+ extends: list = field(default_factory=list) # list[str] - superclass names
65
67
 
66
68
 
67
69
  @dataclass(slots=True)
@@ -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
- output_raw = raw.get("output", {})
86
- context_map = output_raw.get("context_map", {})
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, import_count) for the most-imported source files."""
251
- counter: dict[str, int] = defaultdict(int)
252
- for _, _, data in G.edges(data=True):
253
- if data.get("relation") in ("imports", "imports_from"):
254
- sf = data.get("source_file", "")
255
- if sf:
256
- counter[sf] += 1
257
- ranked = sorted(counter.items(), key=lambda x: x[1], reverse=True)
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
- for entry in directory.rglob("*"):
222
- if entry.is_file() and entry.suffix in ext_to_lang:
223
- lang = ext_to_lang[entry.suffix]
224
- counts[lang] = counts.get(lang, 0) + 1
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
- _POST_COMMIT_HOOK = r"""#!/usr/bin/env bash
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
- nohup codebeacon scan . --update >>"$LOG" 2>&1 &
59
- ) >/dev/null 2>&1 &
60
- disown 2>/dev/null || true
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") or "codebeacon"
109
- cmd = f'{codebeacon} merge-driver "%O" "%A" "%B"'
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
- if hook.exists() and "codebeacon" in hook.read_text(encoding="utf-8"):
138
- return
139
- hook.write_text(_POST_COMMIT_HOOK, encoding="utf-8")
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
 
@@ -33,7 +33,7 @@ from typing import Any
33
33
 
34
34
  import networkx as nx
35
35
 
36
- from codebeacon.common.safety import escape_frontmatter_value, sanitize_label
36
+ from codebeacon.common.safety import cap_filename, escape_frontmatter_value, sanitize_label
37
37
 
38
38
 
39
39
  # ── Regexes ────────────────────────────────────────────────────────────────────
@@ -402,9 +402,18 @@ def _reverse_relation(relation: str) -> str:
402
402
 
403
403
 
404
404
  def _safe_note_name(label: str) -> str:
405
- """Convert node label to a safe filename stem (no path separators)."""
405
+ """Convert node label to a safe filename stem (no path separators).
406
+
407
+ The byte cap (via ``cap_filename``) prevents a very long label — or an 85+
408
+ character CJK class name — from overflowing the 255-byte filesystem limit
409
+ and crashing ``_step1_generate_notes`` with ENAMETOOLONG. Because every
410
+ wikilink target is generated through this same function, the note's
411
+ filename and the links pointing at it stay byte-for-byte consistent even
412
+ after truncation.
413
+ """
406
414
  # Replace characters that confuse Obsidian wikilinks
407
- return re.sub(r'[/\\#^|[\]]', "_", label).strip()
415
+ cleaned = re.sub(r'[/\\#^|[\]]', "_", label).strip()
416
+ return cap_filename(cleaned)
408
417
 
409
418
 
410
419
  # ── Step 2: Fix broken wikilinks ──────────────────────────────────────────────
@@ -632,7 +641,11 @@ def _step8_move_remaining(vault: Path) -> None:
632
641
  content = md.read_text(errors="ignore")
633
642
  m = _PROJECT_RE.search(content)
634
643
  if m and m.group(1):
635
- svc = m.group(1)
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))
636
649
  dest_dir = vault / svc
637
650
  dest_dir.mkdir(exist_ok=True)
638
651
  dest = dest_dir / md.name