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.
- {codebeacon-0.6.1 → codebeacon-0.6.3}/.cursorrules +9 -9
- {codebeacon-0.6.1 → codebeacon-0.6.3}/AGENTS.md +9 -9
- {codebeacon-0.6.1 → codebeacon-0.6.3}/CLAUDE.md +9 -9
- {codebeacon-0.6.1 → codebeacon-0.6.3}/PKG-INFO +1 -1
- codebeacon-0.6.3/codebeacon/__init__.py +1 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/cache.py +35 -27
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/common/safety.py +47 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/common/symbols.py +7 -1
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/common/types.py +2 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/config.py +10 -4
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/contextmap/generator.py +20 -8
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/discover/detector.py +13 -4
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/export/hooks.py +60 -10
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/export/obsidian.py +17 -4
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/dependencies.py +75 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/angular.scm +18 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/django.scm +12 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/fastapi.scm +12 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/flask.scm +12 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/nestjs.scm +18 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/routes.py +13 -6
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/services.py +67 -1
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/graph/analyze.py +13 -5
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/graph/build.py +78 -1
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/graph/cluster.py +40 -12
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/graph/enrich.py +40 -29
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/wiki/generator.py +11 -2
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/wiki/templates.py +22 -25
- {codebeacon-0.6.1 → 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_graphify_parity_fixes.py +335 -0
- codebeacon-0.6.3/tests/test_independent_audit_fixes.py +291 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_known_bugs.py +2 -2
- codebeacon-0.6.1/codebeacon/__init__.py +0 -1
- {codebeacon-0.6.1 → codebeacon-0.6.3}/.github/CODEOWNERS +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/.github/dependabot.yml +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/.github/workflows/ci.yml +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/.github/workflows/release.yml +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/.gitignore +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/LICENSE +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/README.de.md +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/README.es.md +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/README.fr.md +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/README.ja.md +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/README.ko.md +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/README.md +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/README.pt-BR.md +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/README.zh-CN.md +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/__main__.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/affected.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/cli.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/common/__init__.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/common/filters.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/contextmap/__init__.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/diagnostics.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/discover/__init__.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/discover/ignore.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/discover/scanner.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/export/__init__.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/export/callflow_html.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/export/mcp.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/export/merge.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/export/tree_html.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/__init__.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/base.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/components.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/dotnet.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/entities.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/README.md +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/actix.scm +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/aspnet.scm +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/express.scm +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/gin.scm +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/ktor.scm +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/laravel.scm +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/rails.scm +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/react.scm +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/spring_boot.scm +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/svelte.scm +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/tauri.scm +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/vapor.scm +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/queries/vue.scm +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/extract/semantic.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/graph/__init__.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/graph/write.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/knowledge/__init__.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/knowledge/generator.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/pipeline.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/plugins/__init__.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/plugins/githooks.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/plugins/skills.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/semantic_pipeline.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/skill/SKILL.md +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/wave.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/wiki/__init__.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon/wiki/index.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/codebeacon.yaml.example +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/docs/TRANSLATION_STATUS.md +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/public-plan.md +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/skill/install.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/__init__.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/conftest.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/actix/main.rs +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/angular/app.component.ts +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/aspnet/UserController.cs +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/django/views.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/express/userRouter.js +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/fastapi/main.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/flask/app.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/gin/main.go +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/integration_workspace/api-python/pyproject.toml +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/integration_workspace/api-python/src/__init__.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/integration_workspace/api-python/src/main.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/integration_workspace/api-python/src/services.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/integration_workspace/web/package.json +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/integration_workspace/web/src/UserPage.tsx +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/ktor/UserRoutes.kt +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/laravel/UserController.php +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/nestjs/user.controller.ts +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/rails/users_controller.rb +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/react/UserPage.tsx +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/spring_boot/UserController.java +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/sveltekit/+page.svelte +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/vapor/routes.swift +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/fixtures/vue/UserList.vue +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/integration/__init__.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/integration/test_full_pipeline.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_affected.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_affected_wiki.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_audit_bugfixes.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_cli_dispatch.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_contextmap_paths.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_dependencies.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_diagnostics.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_discover.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_dotnet.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_entities.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_filters.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_graph.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_knowledge.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_mcp_and_semantic.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_optional_grammars.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_pipeline_module.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_plugins.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_resolve.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_routes.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_safety_and_writes.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_scanner_sensitive.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_semantic.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_semantic_hardening.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_semantic_stats.py +0 -0
- {codebeacon-0.6.1 → codebeacon-0.6.3}/tests/test_services.py +0 -0
- {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 |
|
|
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."""
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|