sourcecode 1.53.0__tar.gz → 1.55.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sourcecode-1.53.0 → sourcecode-1.55.0}/CHANGELOG.md +50 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/PKG-INFO +3 -3
- {sourcecode-1.53.0 → sourcecode-1.55.0}/README.md +2 -2
- {sourcecode-1.53.0 → sourcecode-1.55.0}/pyproject.toml +1 -1
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/cli.py +79 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/contract_pipeline.py +4 -2
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/license.py +53 -4
- {sourcecode-1.53.0 → sourcecode-1.55.0}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/.gitignore +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/.ruff.toml +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/CONTRIBUTING.md +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/LICENSE +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/SECURITY.md +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/raw +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/cache.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/classifier.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/explain.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/flow_analyzer.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/integration_detector.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/migrate_check.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/progress.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/repository_ir.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/ris.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/schema.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/security_config.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/serializer.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/validation_surface.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/version_check.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/supabase/functions/README.md +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-1.53.0 → sourcecode-1.55.0}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.55.0] — 2026-06-19
|
|
4
|
+
|
|
5
|
+
### Security
|
|
6
|
+
- **License secret no longer written world-readable.** `~/.sourcecode/license.json`
|
|
7
|
+
stores the Pro `license_key` and account email but was written with the default
|
|
8
|
+
umask (typically `0644`, dir `0755`), so any other local user could read the
|
|
9
|
+
credential on a shared host. Now the directory is created/tightened to `0700` and
|
|
10
|
+
the file is `chmod 0600` on the tmp file *before* the atomic rename (no
|
|
11
|
+
world-readable window at the final path). `_secure_dir()` is applied at every
|
|
12
|
+
write site (activation, license file, delta-run counter).
|
|
13
|
+
|
|
14
|
+
- **License-endpoint override now scheme-validated.** `SOURCECODE_SUPABASE_URL` was
|
|
15
|
+
trusted verbatim; an `http://` value sent the license key over plaintext. The
|
|
16
|
+
override is now accepted only when `https://` (any host) or `http://` to loopback
|
|
17
|
+
(preserves Supabase local dev on `http://127.0.0.1:54321`); anything else is
|
|
18
|
+
rejected back to the default endpoint with a warning.
|
|
19
|
+
|
|
20
|
+
- **Hardened the symbol grep in the contract pipeline.** The deep-scan symbol
|
|
21
|
+
search ran `grep <symbol> .` with the symbol in pattern position and as a regex,
|
|
22
|
+
while the Python fallback matched literally — inconsistent, and a leading-dash
|
|
23
|
+
symbol could be parsed as a flag. Switched to `grep -F -e <symbol> -- .`: literal
|
|
24
|
+
match (matches the fallback, removes the regex/ReDoS surface) and `-e`/`--` guard
|
|
25
|
+
option parsing. No `shell=True` anywhere; this is defense-in-depth on argv.
|
|
26
|
+
|
|
27
|
+
Dependency audit (`pip-audit`): no known vulnerabilities in runtime dependencies.
|
|
28
|
+
|
|
29
|
+
## [1.54.0] — 2026-06-19
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
- **`export --c4` now emits `components.module_roots` — architectural module
|
|
33
|
+
enumeration + DDD/legacy classification.** Field test (saint-server C4 doc pass)
|
|
34
|
+
surfaced that the C4 export keyed modules by *leaf source directory*
|
|
35
|
+
(`dirname(source_file)`), so a DDD module split across
|
|
36
|
+
`domain/` / `application/` / `infrastructure/` subdirs fragmented into several
|
|
37
|
+
unrelated "modules". A downstream consumer had to infer module boundaries from
|
|
38
|
+
directory names, which produced a module **undercount** and **DDD-vs-legacy
|
|
39
|
+
misclassification** in the generated docs.
|
|
40
|
+
|
|
41
|
+
Fix: `_detect_module_roots()` rolls leaf dirs up to their architectural module
|
|
42
|
+
root (the directory above the shallowest recognized layer dir) and classifies
|
|
43
|
+
each `layered` (≥2 of `domain`/`application`/`infrastructure`) vs `flat`
|
|
44
|
+
(legacy/flat package). `c4.components.module_roots` carries the per-module
|
|
45
|
+
`{root, pattern, layers, symbol_count, leaf_dir_count}` plus a summary with a
|
|
46
|
+
verifiable `module_count` / `layered_module_count` / `flat_module_count`, so a
|
|
47
|
+
consumer enumerates real modules instead of guessing.
|
|
48
|
+
|
|
49
|
+
Pure-structural (no extra file reads). Non-breaking: top-level `c4` keys and the
|
|
50
|
+
leaf-level `--module-graph` dependency view are unchanged. 3 regression tests
|
|
51
|
+
(layered rollup, flat classification, summary counts).
|
|
52
|
+
|
|
3
53
|
## [1.50.0] — 2026-06-17
|
|
4
54
|
|
|
5
55
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.55.0
|
|
4
4
|
Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Keywords: agents,ai,codebase,context,developer-tools,llm
|
|
@@ -40,7 +40,7 @@ Description-Content-Type: text/markdown
|
|
|
40
40
|
|
|
41
41
|
**Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
|
|
42
42
|
|
|
43
|
-

|
|
44
44
|

|
|
45
45
|
|
|
46
46
|
---
|
|
@@ -404,7 +404,7 @@ Emits **structured, tool-agnostic** codebase views as plain JSON/YAML — the ki
|
|
|
404
404
|
| `--by-directory` | One group per source directory, each symbol with a `source_file:line` reference. |
|
|
405
405
|
| `--module-graph` | `{nodes, edges, summary}` — directories as modules, inter-module dependencies rolled up from class-level relation edges with hit counts + edge types. |
|
|
406
406
|
| `--integrations` | Outbound integrations (`RestTemplate`, `WebClient`, `@FeignClient`, `LdapTemplate`, `JmsTemplate`, ActiveMQ) with `file:line` evidence and a literal `target` URL/name when present. |
|
|
407
|
-
| `--c4` | Unified document: `c4.{context, containers, components, code}` + `api_surface` + a `manifest` with per-directory content hashes for **incremental** consumers (skip directories whose hash is unchanged). |
|
|
407
|
+
| `--c4` | Unified document: `c4.{context, containers, components, code}` + `api_surface` + a `manifest` with per-directory content hashes for **incremental** consumers (skip directories whose hash is unchanged). `components.module_roots` rolls leaf source dirs up to architectural module roots and classifies each `layered` (DDD: ≥2 of `domain`/`application`/`infrastructure`) vs `flat` (legacy/flat package), with a verifiable `module_count` — so a consumer enumerates real modules instead of inferring boundaries from leaf directories. |
|
|
408
408
|
|
|
409
409
|
The section flags compose (pass several for one multi-section document); `--c4` assembles the full export on its own. URLs assembled at runtime yield `target: null` (honest absence, never a guess); containers are derived from build files (Maven/Gradle) and reported as a limitation when none are found.
|
|
410
410
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
|
|
4
4
|
|
|
5
|
-

|
|
6
6
|

|
|
7
7
|
|
|
8
8
|
---
|
|
@@ -366,7 +366,7 @@ Emits **structured, tool-agnostic** codebase views as plain JSON/YAML — the ki
|
|
|
366
366
|
| `--by-directory` | One group per source directory, each symbol with a `source_file:line` reference. |
|
|
367
367
|
| `--module-graph` | `{nodes, edges, summary}` — directories as modules, inter-module dependencies rolled up from class-level relation edges with hit counts + edge types. |
|
|
368
368
|
| `--integrations` | Outbound integrations (`RestTemplate`, `WebClient`, `@FeignClient`, `LdapTemplate`, `JmsTemplate`, ActiveMQ) with `file:line` evidence and a literal `target` URL/name when present. |
|
|
369
|
-
| `--c4` | Unified document: `c4.{context, containers, components, code}` + `api_surface` + a `manifest` with per-directory content hashes for **incremental** consumers (skip directories whose hash is unchanged). |
|
|
369
|
+
| `--c4` | Unified document: `c4.{context, containers, components, code}` + `api_surface` + a `manifest` with per-directory content hashes for **incremental** consumers (skip directories whose hash is unchanged). `components.module_roots` rolls leaf source dirs up to architectural module roots and classifies each `layered` (DDD: ≥2 of `domain`/`application`/`infrastructure`) vs `flat` (legacy/flat package), with a verifiable `module_count` — so a consumer enumerates real modules instead of inferring boundaries from leaf directories. |
|
|
370
370
|
|
|
371
371
|
The section flags compose (pass several for one multi-section document); `--c4` assembles the full export on its own. URLs assembled at runtime yield `target: null` (honest absence, never a guess); containers are derived from build files (Maven/Gradle) and reported as a limitation when none are found.
|
|
372
372
|
|
|
@@ -4137,6 +4137,82 @@ def _directory_hashes(file_list: "list[str]", root: "Path") -> "dict[str, str]":
|
|
|
4137
4137
|
return out
|
|
4138
4138
|
|
|
4139
4139
|
|
|
4140
|
+
# Architectural layer directory names used to recognize a layered module
|
|
4141
|
+
# (DDD / hexagonal). The module *root* is the directory directly above the
|
|
4142
|
+
# shallowest layer dir, so symbols living in domain/application/infrastructure
|
|
4143
|
+
# subdirs all roll up to one module — a consumer counts modules, not leaf dirs.
|
|
4144
|
+
_LAYER_MARKERS: "frozenset[str]" = frozenset({
|
|
4145
|
+
"domain", "application", "infrastructure",
|
|
4146
|
+
"interfaces", "presentation", "adapters", "ports", "api",
|
|
4147
|
+
})
|
|
4148
|
+
# Core DDD layers — presence of >=2 marks a module as DDD-layered vs flat/legacy.
|
|
4149
|
+
_DDD_CORE_LAYERS: "frozenset[str]" = frozenset({
|
|
4150
|
+
"domain", "application", "infrastructure",
|
|
4151
|
+
})
|
|
4152
|
+
|
|
4153
|
+
|
|
4154
|
+
def _module_root_of(leaf_dir: "str") -> "tuple[str, str | None]":
|
|
4155
|
+
"""Map a leaf source directory to its architectural module root.
|
|
4156
|
+
|
|
4157
|
+
For a layered module ``<root>/<layer>/...`` the root is the path above the
|
|
4158
|
+
shallowest recognized layer dir, and the layer name is returned alongside.
|
|
4159
|
+
Flat directories (no layer marker) are their own root with a ``None`` layer.
|
|
4160
|
+
Pure-structural — no file reads.
|
|
4161
|
+
"""
|
|
4162
|
+
parts = leaf_dir.split("/")
|
|
4163
|
+
for i, seg in enumerate(parts):
|
|
4164
|
+
if seg.lower() in _LAYER_MARKERS:
|
|
4165
|
+
return "/".join(parts[:i]) or ".", seg.lower()
|
|
4166
|
+
return leaf_dir, None
|
|
4167
|
+
|
|
4168
|
+
|
|
4169
|
+
def _detect_module_roots(by_directory: "dict[str, list]") -> "dict":
|
|
4170
|
+
"""Roll leaf source dirs up to architectural module roots and classify them.
|
|
4171
|
+
|
|
4172
|
+
Resolves the leaf-directory-vs-module mismatch in the C4 component view: a
|
|
4173
|
+
DDD module split across ``domain/`` / ``application/`` / ``infrastructure/``
|
|
4174
|
+
subdirs is reported once, with its layers and a structural ``pattern``
|
|
4175
|
+
(``layered`` when it carries >=2 core DDD layers, else ``flat``). Gives a
|
|
4176
|
+
downstream consumer a verifiable module enumeration and a DDD-vs-legacy
|
|
4177
|
+
signal instead of forcing it to infer module boundaries from directory names.
|
|
4178
|
+
"""
|
|
4179
|
+
roots: "dict[str, dict]" = {}
|
|
4180
|
+
for leaf, symbols in by_directory.items():
|
|
4181
|
+
root, layer = _module_root_of(leaf)
|
|
4182
|
+
slot = roots.setdefault(
|
|
4183
|
+
root, {"layers": set(), "symbol_count": 0, "leaf_dirs": 0}
|
|
4184
|
+
)
|
|
4185
|
+
if layer:
|
|
4186
|
+
slot["layers"].add(layer)
|
|
4187
|
+
slot["symbol_count"] += len(symbols)
|
|
4188
|
+
slot["leaf_dirs"] += 1
|
|
4189
|
+
|
|
4190
|
+
modules: "list[dict]" = []
|
|
4191
|
+
layered = flat = 0
|
|
4192
|
+
for root in sorted(roots):
|
|
4193
|
+
s = roots[root]
|
|
4194
|
+
pattern = "layered" if len(s["layers"] & _DDD_CORE_LAYERS) >= 2 else "flat"
|
|
4195
|
+
if pattern == "layered":
|
|
4196
|
+
layered += 1
|
|
4197
|
+
else:
|
|
4198
|
+
flat += 1
|
|
4199
|
+
modules.append({
|
|
4200
|
+
"root": root,
|
|
4201
|
+
"pattern": pattern,
|
|
4202
|
+
"layers": sorted(s["layers"]),
|
|
4203
|
+
"symbol_count": s["symbol_count"],
|
|
4204
|
+
"leaf_dir_count": s["leaf_dirs"],
|
|
4205
|
+
})
|
|
4206
|
+
return {
|
|
4207
|
+
"modules": modules,
|
|
4208
|
+
"summary": {
|
|
4209
|
+
"module_count": len(modules),
|
|
4210
|
+
"layered_module_count": layered,
|
|
4211
|
+
"flat_module_count": flat,
|
|
4212
|
+
},
|
|
4213
|
+
}
|
|
4214
|
+
|
|
4215
|
+
|
|
4140
4216
|
def _build_c4_export(
|
|
4141
4217
|
root: "Path",
|
|
4142
4218
|
file_list: "list[str]",
|
|
@@ -4155,6 +4231,9 @@ def _build_c4_export(
|
|
|
4155
4231
|
"""
|
|
4156
4232
|
by_directory = _group_symbols_by_directory(nodes)
|
|
4157
4233
|
module_graph = _build_module_graph(nodes, edges)
|
|
4234
|
+
# Architectural module-root rollup + DDD/legacy classification, so a
|
|
4235
|
+
# consumer counts/classifies modules instead of inferring them from leaf dirs.
|
|
4236
|
+
module_graph["module_roots"] = _detect_module_roots(by_directory)
|
|
4158
4237
|
api_surface = _group_endpoints_by_controller(endpoints)
|
|
4159
4238
|
containers = _detect_containers(root)
|
|
4160
4239
|
|
|
@@ -634,11 +634,13 @@ def _find_symbol_files(
|
|
|
634
634
|
try:
|
|
635
635
|
result = subprocess.run(
|
|
636
636
|
[
|
|
637
|
-
"grep", "-
|
|
637
|
+
"grep", "-rlF",
|
|
638
638
|
"--include=*.ts", "--include=*.tsx",
|
|
639
639
|
"--include=*.js", "--include=*.jsx",
|
|
640
640
|
"--include=*.py", "--include=*.java",
|
|
641
|
-
symbol
|
|
641
|
+
# -e guards a symbol that begins with '-' from being parsed as a
|
|
642
|
+
# flag; -- terminates options so the search root can't be either.
|
|
643
|
+
"-e", symbol, "--", ".",
|
|
642
644
|
],
|
|
643
645
|
cwd=str(root),
|
|
644
646
|
capture_output=True,
|
|
@@ -45,7 +45,33 @@ _DEFAULT_SUPABASE_URL: str = "https://qkndlmyekvujjdgthtmz.supabase.co"
|
|
|
45
45
|
# Paste your project's anon key here so `sourcecode activate` works out of the
|
|
46
46
|
# box; env var still overrides for testing against another project.
|
|
47
47
|
_DEFAULT_SUPABASE_ANON_KEY: str = "sb_publishable_qiJFLWjbBbTqjg-fb0mAGA_cl8PBOKH"
|
|
48
|
-
|
|
48
|
+
def _safe_supabase_url(override: "Optional[str]") -> str:
|
|
49
|
+
"""Validate the SOURCECODE_SUPABASE_URL override before trusting it.
|
|
50
|
+
|
|
51
|
+
The license key is POSTed to this host, so a plaintext ``http://`` endpoint
|
|
52
|
+
would expose the credential on the wire. Allow ``https://`` to any host, and
|
|
53
|
+
``http://`` only to loopback (Supabase local dev serves on
|
|
54
|
+
``http://127.0.0.1:54321``). Anything else is rejected back to the default.
|
|
55
|
+
"""
|
|
56
|
+
if not override or override == _DEFAULT_SUPABASE_URL:
|
|
57
|
+
return _DEFAULT_SUPABASE_URL
|
|
58
|
+
from urllib.parse import urlparse
|
|
59
|
+
|
|
60
|
+
parsed = urlparse(override)
|
|
61
|
+
host = (parsed.hostname or "").lower()
|
|
62
|
+
is_loopback = host in {"localhost", "127.0.0.1", "::1"}
|
|
63
|
+
if parsed.scheme == "https" or (parsed.scheme == "http" and is_loopback):
|
|
64
|
+
return override
|
|
65
|
+
sys.stderr.write(
|
|
66
|
+
f"[sourcecode] WARNING: ignoring SOURCECODE_SUPABASE_URL={override!r} — "
|
|
67
|
+
"must be https:// (or http:// to localhost). Using the default endpoint "
|
|
68
|
+
"to avoid sending the license key over plaintext.\n"
|
|
69
|
+
)
|
|
70
|
+
sys.stderr.flush()
|
|
71
|
+
return _DEFAULT_SUPABASE_URL
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
_SUPABASE_URL: str = _safe_supabase_url(os.environ.get("SOURCECODE_SUPABASE_URL"))
|
|
49
75
|
_SUPABASE_ANON_KEY: str = os.environ.get(
|
|
50
76
|
"SOURCECODE_SUPABASE_ANON_KEY",
|
|
51
77
|
_DEFAULT_SUPABASE_ANON_KEY,
|
|
@@ -152,12 +178,35 @@ _license_data: Optional[dict] = None
|
|
|
152
178
|
is_pro: bool = False
|
|
153
179
|
|
|
154
180
|
|
|
181
|
+
def _secure_dir() -> None:
|
|
182
|
+
"""Create ~/.sourcecode owner-only (0700). Holds the license secret.
|
|
183
|
+
|
|
184
|
+
``mkdir(mode=...)`` is ignored when the dir already exists, so chmod
|
|
185
|
+
unconditionally. Best-effort: a chmod failure (e.g. Windows) is non-fatal.
|
|
186
|
+
"""
|
|
187
|
+
try:
|
|
188
|
+
_LICENSE_DIR.mkdir(parents=True, exist_ok=True)
|
|
189
|
+
os.chmod(_LICENSE_DIR, 0o700)
|
|
190
|
+
except OSError:
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
|
|
155
194
|
def _write_license_file(data: dict) -> None:
|
|
156
|
-
"""Atomically write license data via tmp file + rename.
|
|
195
|
+
"""Atomically write license data via tmp file + rename, owner-only (0600).
|
|
196
|
+
|
|
197
|
+
The payload contains the Pro ``license_key`` and account email, so the file
|
|
198
|
+
must not be world/group-readable on shared hosts. Permissions are set on the
|
|
199
|
+
tmp file *before* the rename so there is no readable window at the final path.
|
|
200
|
+
"""
|
|
201
|
+
_secure_dir()
|
|
157
202
|
payload = json.dumps(data, indent=2, ensure_ascii=False).encode("utf-8")
|
|
158
203
|
tmp = _LICENSE_FILE.with_suffix(".tmp")
|
|
159
204
|
try:
|
|
160
205
|
tmp.write_bytes(payload)
|
|
206
|
+
try:
|
|
207
|
+
os.chmod(tmp, 0o600)
|
|
208
|
+
except OSError:
|
|
209
|
+
pass
|
|
161
210
|
tmp.replace(_LICENSE_FILE)
|
|
162
211
|
except Exception:
|
|
163
212
|
try:
|
|
@@ -192,7 +241,7 @@ def check_delta_free_tier(repo_path: str) -> "tuple[bool, int, int]":
|
|
|
192
241
|
new_used = used + 1
|
|
193
242
|
runs[key] = new_used
|
|
194
243
|
try:
|
|
195
|
-
|
|
244
|
+
_secure_dir()
|
|
196
245
|
tmp = _DELTA_RUNS_FILE.with_suffix(".tmp")
|
|
197
246
|
tmp.write_text(json.dumps(runs, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
198
247
|
tmp.replace(_DELTA_RUNS_FILE)
|
|
@@ -506,7 +555,7 @@ def activate_license(license_key: str) -> None:
|
|
|
506
555
|
_emit_telemetry("activation", feature="key", success=False, error_kind="NotPro")
|
|
507
556
|
_fail("not_pro", "This license is not a Pro license.")
|
|
508
557
|
|
|
509
|
-
|
|
558
|
+
_secure_dir()
|
|
510
559
|
now = datetime.now(timezone.utc).isoformat()
|
|
511
560
|
data = {
|
|
512
561
|
"license_key": license_key,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|