sourcecode 2.5.6__tar.gz → 2.5.8__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-2.5.6 → sourcecode-2.5.8}/CHANGELOG.md +126 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/PKG-INFO +1 -1
- {sourcecode-2.5.6 → sourcecode-2.5.8}/pyproject.toml +1 -1
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/__init__.py +1 -1
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/architecture_summary.py +55 -6
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/confidence_analyzer.py +39 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/reconciliation.py +191 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/ris.py +39 -2
- {sourcecode-2.5.6 → sourcecode-2.5.8}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/.gitignore +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/.ruff.toml +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/CONTRIBUTING.md +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/LICENSE +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/README.md +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/SECURITY.md +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/raw +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/scripts/compare_integration_engines.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/scripts/customer_smoke_test.sh +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/scripts/generate_jdk_exports.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/archetype.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/cache.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/call_surface.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/caller_metrics.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/classifier.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/cli.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/context_cache.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/context_graph.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/dynamic_argument_surface.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/endpoint_literals.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/endpoint_metrics.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/evidence_provider.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/explain.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/graph_evidence.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/hibernate_strat.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/jdk_exports.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/license.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/migrate_check.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/progress.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/redactor.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/repository_ir.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/__init__.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/context.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/errors.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/executor.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/planner.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/query.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/request.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/resolution.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/result.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/retriever.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/runtime.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/steps.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/steps_graph.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/steps_impact.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/steps_intf.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/steps_struct.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/steps_txsec.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/scanner.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/schema.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/security_config.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/security_posture.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/semantic_impact_engine.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/semantic_integration_engine.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/semantic_services.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/serializer.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/type_usage_surface.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/validation_inference.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/validation_surface.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/version_check.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/workspace.py +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/supabase/functions/README.md +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/supabase/sql/licensing_schema.sql +0 -0
- {sourcecode-2.5.6 → sourcecode-2.5.8}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,5 +1,131 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.5.8] — 2026-07-17
|
|
4
|
+
|
|
5
|
+
**A count must name what it counted.** 2.5.7 taught the headline to say *"Polyglot
|
|
6
|
+
repository … no single stack owns this repository's architecture."* Three lines
|
|
7
|
+
below it, the same field still said:
|
|
8
|
+
|
|
9
|
+
> 1 project detected.
|
|
10
|
+
> Stack: console.
|
|
11
|
+
> Target: net6.0.
|
|
12
|
+
|
|
13
|
+
on the confluent `examples` monorepo — 20+ subprojects, mostly Java/Python/Go.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **`.NET` summary lines were narrated as claims about the repository
|
|
18
|
+
(P2-F-RESIDUE(c))** (`architecture_summary.py`): every one of those lines
|
|
19
|
+
counts `.csproj` files. They are facts about the .NET stack alone —
|
|
20
|
+
*"1 project detected"* is true of the dotnet stack and false of the repo, and
|
|
21
|
+
it landed directly beside a headline that had just called the repo polyglot.
|
|
22
|
+
Each line now names the stack it measured:
|
|
23
|
+
|
|
24
|
+
> C#/.NET projects: 1.
|
|
25
|
+
> C#/.NET project types: console.
|
|
26
|
+
> C#/.NET target frameworks: net6.0.
|
|
27
|
+
|
|
28
|
+
Naming the stack costs one word and makes each line true on **any** repo, so no
|
|
29
|
+
polyglot detection is needed to decide the wording — a pure .NET solution reads
|
|
30
|
+
*"C#/.NET projects: 1."* just as correctly. The label comes from the same
|
|
31
|
+
`_STACK_LABELS` table the headline uses, never a second literal.
|
|
32
|
+
|
|
33
|
+
Two user-facing Spanish strings in an otherwise English summary went with them:
|
|
34
|
+
*"Patrón detectado: …"* and *"Solución .NET detectada"* now read *"C#/.NET
|
|
35
|
+
solution pattern: …"* and *"C#/.NET solution detected."*
|
|
36
|
+
|
|
37
|
+
### Corrected
|
|
38
|
+
|
|
39
|
+
- **The 2.5.7 entry overstated its own tests.** It read *"16 new tests; suite
|
|
40
|
+
3467 passed"*. Both numbers were derived arithmetically from a
|
|
41
|
+
`--collect-only` total — which includes skips — instead of being read off a
|
|
42
|
+
pytest summary line. Measured: **14 new tests**, and the suite reports
|
|
43
|
+
`3462 passed, 5 skipped, 1 deselected`. The entry is fixed above. A changelog
|
|
44
|
+
that inflates its own evidence is the same defect class these releases are
|
|
45
|
+
about; the numbers here are quoted from the summary line, not computed.
|
|
46
|
+
|
|
47
|
+
1 new test; suite `3463 passed, 5 skipped, 1 deselected` (the deselection is a
|
|
48
|
+
pre-existing `file_chunker` failure unrelated to this work — the real ofbiz file
|
|
49
|
+
shrank below the test's 7500-line assertion).
|
|
50
|
+
|
|
51
|
+
## [2.5.7] — 2026-07-17
|
|
52
|
+
|
|
53
|
+
**A framework claim must be attested by its own runtime.** The `examples` repo
|
|
54
|
+
read *"C#/.NET rest api using JAX-RS."* — a JVM-only framework attributed to a
|
|
55
|
+
.NET headline, self-contradictory inside a single clause. This release closes the
|
|
56
|
+
last of the narrative cluster, and settles how reconciliation findings reach the
|
|
57
|
+
surfaces that report them.
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
|
|
61
|
+
- **A polyglot repository was narrated as one program (P2-F)**
|
|
62
|
+
(`reconciliation.py`, `architecture_summary.py`): the sentence was assembled
|
|
63
|
+
from two unrelated sources —
|
|
64
|
+
|
|
65
|
+
| part | comes from | on `examples` |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `C#/.NET` | the **primary** stack | dotnet, root `.`, declares **zero** frameworks |
|
|
68
|
+
| `using JAX-RS` | **every** stack | java, workspace `microservices-orders`, the sole owner of JAX-RS |
|
|
69
|
+
|
|
70
|
+
New Evidence Reconciliation rule **`framework_stack_attestation`**: a framework
|
|
71
|
+
named in a single-program headline whose owning stacks are all of another
|
|
72
|
+
runtime family contradicts the claim. Ownership is already structural — a
|
|
73
|
+
`FrameworkDetection` is discovered inside the `StackDetection` whose manifest
|
|
74
|
+
declares it — so the rule reasons from evidence and never from what a framework
|
|
75
|
+
is called. Grouped by runtime family, so a Kotlin framework beside Java is one
|
|
76
|
+
program; `fullstack`/`monorepo`/`microservices` are exempt, their headline is
|
|
77
|
+
meant to span runtimes.
|
|
78
|
+
|
|
79
|
+
The type label had to go with it. `project_type` is decided by frameworks
|
|
80
|
+
pooled across every stack, while a separate vote picks the primary, so
|
|
81
|
+
*"rest api"* was java's evidence too — dropping only the `using` clause would
|
|
82
|
+
have left *"C#/.NET rest api."*, the same claim one clause shorter. The
|
|
83
|
+
headline now degrades to what is actually known:
|
|
84
|
+
|
|
85
|
+
> C#/.NET project. Polyglot repository: the rest api evidence (JAX-RS) is
|
|
86
|
+
> declared by the Java stack, not by C#/.NET — no single stack owns this
|
|
87
|
+
> repository's architecture. See `stacks`.
|
|
88
|
+
|
|
89
|
+
Every fact survives, correctly attributed.
|
|
90
|
+
|
|
91
|
+
- **The structured fields kept asserting what the sentence had retracted
|
|
92
|
+
(P2-F-RESIDUE(a))** (`confidence_analyzer.py`, `reconciliation.py`):
|
|
93
|
+
`project_type` and `primary_stack` are read on their own by agents that never
|
|
94
|
+
see the prose, and on `examples` they still said `"api"` + `"dotnet"` silently.
|
|
95
|
+
`ConfidenceAnalyzer` now consumes the same rule, emitting an
|
|
96
|
+
`analysis_gaps` entry (`area="framework_stack_attestation"`) plus an anomaly
|
|
97
|
+
beside its existing "ambiguous primary" siblings.
|
|
98
|
+
|
|
99
|
+
`project_type` deliberately keeps its value: the classifier's vote is a
|
|
100
|
+
separate decision, and minting a *"polyglot"* type here would silently rewrite
|
|
101
|
+
what every consumer branches on. Flagging the claim is honest; changing the
|
|
102
|
+
field is a different change.
|
|
103
|
+
|
|
104
|
+
### Internal
|
|
105
|
+
|
|
106
|
+
- `_FRAMEWORK_DRIVEN_TYPES` moved out of `architecture_summary` into
|
|
107
|
+
`reconciliation.type_claim_unattested()`. Two surfaces must judge *"did the
|
|
108
|
+
frameworks decide this type"* identically — a duplicated table is how the
|
|
109
|
+
headline kept lying in the first place.
|
|
110
|
+
- `ReconciliationFinding.detail` is now **sink-neutral**. It used to narrate
|
|
111
|
+
itself (*"framework(s) named in the headline … the sentence attributes them
|
|
112
|
+
to"*), which the rule that strips the claim promptly made false. Details are
|
|
113
|
+
read by prose and by structured gaps alike; none may assume prose.
|
|
114
|
+
|
|
115
|
+
Validation across 12 field-test repos through the real CLI: `examples` emits the
|
|
116
|
+
degraded headline, the gap and the anomaly (*"JAX-RS declared by another runtime,
|
|
117
|
+
not by the dotnet primary stack"*); petclinic, keycloak, eureka, jobrunr, ofbiz,
|
|
118
|
+
jenkins, broadleaf, alfresco, openmrs, neo4j and spaghetti-api are **silent —
|
|
119
|
+
every headline byte-identical**, no gap, no anomaly. Every control's primary
|
|
120
|
+
stack declares its own frameworks, which is exactly why the rule stays quiet.
|
|
121
|
+
|
|
122
|
+
Measured along the way, and worth knowing: on a polyglot repo the **primary stack
|
|
123
|
+
is a coin flip** — `max(stacks, …)` breaks an exact tie between java and dotnet
|
|
124
|
+
(both manifest-detected, both high-confidence) by list order, and the narrative
|
|
125
|
+
then welded the loser's evidence onto the winner's label.
|
|
126
|
+
|
|
127
|
+
14 new tests; suite 3462 passed, 5 skipped.
|
|
128
|
+
|
|
3
129
|
## [2.5.6] — 2026-07-17
|
|
4
130
|
|
|
5
131
|
**A layer must be attested by code.** The architecture fallback chain reported
|
|
@@ -205,6 +205,22 @@ class ArchitectureSummarizer:
|
|
|
205
205
|
for f in s.frameworks[:2]
|
|
206
206
|
))[:3]
|
|
207
207
|
|
|
208
|
+
# F-1 reconciliation: the headline describes ONE program, but fw_names is
|
|
209
|
+
# gathered from every stack in the repo. On a polyglot repository that
|
|
210
|
+
# welds a foreign runtime's framework onto the primary's label. Drop what
|
|
211
|
+
# the claimed stack does not attest, and never let the TYPE label — which
|
|
212
|
+
# those same frameworks drove — survive its own evidence (P2-F).
|
|
213
|
+
from sourcecode.reconciliation import type_claim_unattested, unattested_frameworks
|
|
214
|
+
|
|
215
|
+
foreign = unattested_frameworks(primary.stack, sm.project_type, sm.stacks)
|
|
216
|
+
attested = [name for name in fw_names if name not in foreign]
|
|
217
|
+
dropped = [name for name in fw_names if name in foreign]
|
|
218
|
+
if dropped and not attested and type_claim_unattested(
|
|
219
|
+
claimed_stack=primary.stack, project_type=sm.project_type, stacks=sm.stacks
|
|
220
|
+
):
|
|
221
|
+
return self._describe_foreign_evidence(sm, stack_label, runtime, dropped)
|
|
222
|
+
fw_names = attested
|
|
223
|
+
|
|
208
224
|
fw_str = f" using {', '.join(fw_names)}" if fw_names else ""
|
|
209
225
|
if runtime:
|
|
210
226
|
# BUG #4: never assert "rest api" in the headline unless the endpoints
|
|
@@ -237,6 +253,29 @@ class ArchitectureSummarizer:
|
|
|
237
253
|
return f"{stack_label} {runtime.lower()}{fw_str}."
|
|
238
254
|
return f"{stack_label} project{fw_str}."
|
|
239
255
|
|
|
256
|
+
def _describe_foreign_evidence(
|
|
257
|
+
self, sm: SourceMap, stack_label: str, runtime: str, dropped: list[str]
|
|
258
|
+
) -> str:
|
|
259
|
+
"""Headline for a claim whose defining evidence belongs to another stack.
|
|
260
|
+
|
|
261
|
+
Says what IS known instead of asserting a program that does not exist:
|
|
262
|
+
the primary stack, the foreign frameworks, and the stacks that actually
|
|
263
|
+
declare them. The reader gets the same facts, correctly attributed.
|
|
264
|
+
"""
|
|
265
|
+
from sourcecode.context_summarizer import _STACK_LABELS
|
|
266
|
+
from sourcecode.reconciliation import framework_owner_stacks
|
|
267
|
+
|
|
268
|
+
owners = framework_owner_stacks(sm.stacks)
|
|
269
|
+
owner_ids = sorted({sid for name in dropped for sid in owners.get(name, ())})
|
|
270
|
+
owner_labels = [_STACK_LABELS.get(sid, sid) for sid in owner_ids] or ["another"]
|
|
271
|
+
plural = "s" if len(owner_labels) > 1 else ""
|
|
272
|
+
return (
|
|
273
|
+
f"{stack_label} project. Polyglot repository: the {runtime.lower()} "
|
|
274
|
+
f"evidence ({', '.join(dropped)}) is declared by the "
|
|
275
|
+
f"{', '.join(owner_labels)} stack{plural}, not by {stack_label} — no "
|
|
276
|
+
"single stack owns this repository's architecture. See `stacks`."
|
|
277
|
+
)
|
|
278
|
+
|
|
240
279
|
def _endpoint_support(self) -> tuple[int, int]:
|
|
241
280
|
"""Return (total, high_confidence) endpoint counts from the canonical
|
|
242
281
|
Java endpoint extractor — the same source the `endpoints` command uses,
|
|
@@ -412,41 +451,51 @@ class ArchitectureSummarizer:
|
|
|
412
451
|
return None
|
|
413
452
|
|
|
414
453
|
def _summarize_dotnet_entry(self, stacks: list[StackDetection]) -> list[str]:
|
|
454
|
+
# P2-F-RESIDUE(c): every line here counts .csproj files — facts about the
|
|
455
|
+
# .NET stack ALONE. Unscoped, they read as claims about the repository:
|
|
456
|
+
# the confluent examples monorepo (20+ subprojects, mostly Java/Python/Go)
|
|
457
|
+
# reported a bare "1 project detected." beside a headline that had just
|
|
458
|
+
# called it polyglot. Naming the stack costs one word and makes each line
|
|
459
|
+
# true on any repo, so no polyglot test is needed to decide the wording.
|
|
460
|
+
from sourcecode.context_summarizer import _STACK_LABELS
|
|
461
|
+
|
|
415
462
|
dotnet_stacks = [s for s in stacks if s.stack == "dotnet"]
|
|
416
463
|
if not dotnet_stacks:
|
|
417
464
|
return []
|
|
465
|
+
label = _STACK_LABELS.get("dotnet", "dotnet") # same table as the headline
|
|
418
466
|
lines: list[str] = []
|
|
419
467
|
signals = [sig for s in dotnet_stacks for sig in s.signals]
|
|
420
468
|
|
|
421
469
|
for sig in signals:
|
|
422
470
|
if "project" in sig and "detected" in sig:
|
|
423
|
-
|
|
471
|
+
count = sig.split(" ", 1)[0]
|
|
472
|
+
lines.append(f"{label} projects: {count}.")
|
|
424
473
|
break
|
|
425
474
|
|
|
426
475
|
for sig in signals:
|
|
427
476
|
if sig.startswith("project types:"):
|
|
428
477
|
types = sig.removeprefix("project types:").strip()
|
|
429
|
-
lines.append(f"
|
|
478
|
+
lines.append(f"{label} project types: {types}.")
|
|
430
479
|
break
|
|
431
480
|
|
|
432
481
|
for sig in signals:
|
|
433
482
|
if sig.startswith("target frameworks:"):
|
|
434
483
|
fws = sig.removeprefix("target frameworks:").strip()
|
|
435
|
-
lines.append(f"
|
|
484
|
+
lines.append(f"{label} target frameworks: {fws}.")
|
|
436
485
|
break
|
|
437
486
|
|
|
438
487
|
for sig in signals:
|
|
439
488
|
if sig.startswith("architecture:"):
|
|
440
489
|
pattern = sig.removeprefix("architecture:").strip()
|
|
441
|
-
lines.append(f"
|
|
490
|
+
lines.append(f"{label} solution pattern: {pattern}.")
|
|
442
491
|
break
|
|
443
492
|
|
|
444
493
|
framework_names = [f.name for s in dotnet_stacks for f in s.frameworks]
|
|
445
494
|
if framework_names:
|
|
446
|
-
lines.append(f"
|
|
495
|
+
lines.append(f"{label} frameworks: {', '.join(framework_names)}.")
|
|
447
496
|
|
|
448
497
|
if not lines:
|
|
449
|
-
lines.append("
|
|
498
|
+
lines.append(f"{label} solution detected.")
|
|
450
499
|
return lines
|
|
451
500
|
|
|
452
501
|
def _summarize_go_entry(self, path: str, content: str) -> list[str]:
|
|
@@ -102,6 +102,45 @@ class ConfidenceAnalyzer:
|
|
|
102
102
|
if heuristic_only and not any(s.detection_method != "heuristic" for s in sm.stacks):
|
|
103
103
|
anomalies.append("All stacks detected via heuristic only — no manifest found")
|
|
104
104
|
|
|
105
|
+
# ── Anomaly: a claim attributed to a stack that does not attest it ────
|
|
106
|
+
# F-1 reconciliation, second sink. The headline degrades itself
|
|
107
|
+
# (architecture_summary), but `project_type` and `primary_stack` are
|
|
108
|
+
# separate structured fields an agent can read on their own — and they
|
|
109
|
+
# keep asserting the claim this rule refutes. Same rule, same judgement,
|
|
110
|
+
# different surface: here it becomes an anomaly + gap instead of prose.
|
|
111
|
+
if primary_stacks:
|
|
112
|
+
from sourcecode.reconciliation import (
|
|
113
|
+
reconcile_headline_evidence,
|
|
114
|
+
type_claim_unattested,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
_primary = primary_stacks[0]
|
|
118
|
+
for _rf in reconcile_headline_evidence(
|
|
119
|
+
claimed_stack=_primary.stack,
|
|
120
|
+
project_type=sm.project_type,
|
|
121
|
+
stacks=sm.stacks,
|
|
122
|
+
):
|
|
123
|
+
anomalies.append(
|
|
124
|
+
f"{', '.join(_rf.symbols)} declared by another runtime, not by the "
|
|
125
|
+
f"{_primary.stack} primary stack"
|
|
126
|
+
)
|
|
127
|
+
_reason = _rf.detail
|
|
128
|
+
if type_claim_unattested(
|
|
129
|
+
claimed_stack=_primary.stack,
|
|
130
|
+
project_type=sm.project_type,
|
|
131
|
+
stacks=sm.stacks,
|
|
132
|
+
):
|
|
133
|
+
_reason += (
|
|
134
|
+
f' project_type="{sm.project_type}" rests on that same evidence — '
|
|
135
|
+
f"it is not attested for the {_primary.stack} primary stack, and "
|
|
136
|
+
"the repository is polyglot rather than one program."
|
|
137
|
+
)
|
|
138
|
+
gaps.append(AnalysisGap(
|
|
139
|
+
area=_rf.rule,
|
|
140
|
+
reason=_reason,
|
|
141
|
+
impact="medium",
|
|
142
|
+
))
|
|
143
|
+
|
|
105
144
|
# ── Anomaly: entry points all low-confidence ──────────────────────────
|
|
106
145
|
if normalized_entry_points and all(ep.confidence == "low" for ep in normalized_entry_points):
|
|
107
146
|
anomalies.append("All entry points are low-confidence (heuristic/code_signal only)")
|
|
@@ -42,6 +42,15 @@ Rule registry (architecture):
|
|
|
42
42
|
subsystems (SIM-3: keycloak's "MVC … view layer" was a React SPA, zero
|
|
43
43
|
Java files, sitting beside a 1088-file Java model).
|
|
44
44
|
|
|
45
|
+
Rule registry (narrative):
|
|
46
|
+
framework_stack_attestation — the headline welds a stack label to a
|
|
47
|
+
framework that no stack of that runtime declares. The stack table and
|
|
48
|
+
the framework-ownership view are contradicting each other: the sentence
|
|
49
|
+
describes one program while the evidence names two (P2-F: the confluent
|
|
50
|
+
examples repo read "C#/.NET rest api using JAX-RS" — a JVM framework,
|
|
51
|
+
declared by a Java stack in one sub-workspace, attributed to a .NET
|
|
52
|
+
primary that declares no framework at all).
|
|
53
|
+
|
|
45
54
|
Never raises. Empty evidence → no findings.
|
|
46
55
|
"""
|
|
47
56
|
from __future__ import annotations
|
|
@@ -438,6 +447,188 @@ def incoherent_layers(
|
|
|
438
447
|
return frozenset(out)
|
|
439
448
|
|
|
440
449
|
|
|
450
|
+
# ---------------------------------------------------------------------------
|
|
451
|
+
# Narrative reconciliation (headline claim × stack ownership)
|
|
452
|
+
# ---------------------------------------------------------------------------
|
|
453
|
+
|
|
454
|
+
# Stack identifier → RUNTIME FAMILY. Same reasoning as `_RUNTIME_FAMILY` above,
|
|
455
|
+
# applied to the stack ontology instead of file extensions: a Kotlin framework
|
|
456
|
+
# named in a Java headline is ONE program (same runtime, direct interop), while a
|
|
457
|
+
# Java framework named in a .NET headline is a different one. Stacks absent here
|
|
458
|
+
# are deliberately unjudgeable: an unknown stack can never prove a contradiction.
|
|
459
|
+
_STACK_FAMILY: dict[str, str] = {
|
|
460
|
+
"java": "jvm", "kotlin": "jvm", "scala": "jvm", "groovy": "jvm",
|
|
461
|
+
"nodejs": "js", "deno": "js", "bun": "js",
|
|
462
|
+
"python": "python",
|
|
463
|
+
"go": "go",
|
|
464
|
+
"rust": "rust",
|
|
465
|
+
"ruby": "ruby",
|
|
466
|
+
"dotnet": "dotnet",
|
|
467
|
+
"php": "php",
|
|
468
|
+
"swift": "swift",
|
|
469
|
+
"dart": "dart",
|
|
470
|
+
"elixir": "beam", "erlang": "beam",
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
# Project types that are SUPPOSED to span runtimes — their headline names a tier
|
|
474
|
+
# split, so a framework from another runtime is the point, not a contradiction.
|
|
475
|
+
# Same carve-out (and same names) as `_CROSS_RUNTIME_PATTERNS` for layer claims.
|
|
476
|
+
_CROSS_RUNTIME_TYPES = _CROSS_RUNTIME_PATTERNS
|
|
477
|
+
|
|
478
|
+
# Project types DRIVEN by framework presence: `_classify_project_type` pools
|
|
479
|
+
# frameworks across every stack to pick them, while a separate vote picks the
|
|
480
|
+
# primary stack — so on a polyglot repo the type can be decided by one stack's
|
|
481
|
+
# evidence and reported as another's. When the frameworks that drove the type are
|
|
482
|
+
# all foreign to the primary, the type inherits their lie: the sentence
|
|
483
|
+
# "C#/.NET rest api" is the same claim as "using JAX-RS", one clause shorter.
|
|
484
|
+
# Lives here, not in a consumer: BOTH the headline and the confidence/gap surface
|
|
485
|
+
# must judge this identically or the duplicated table lies again.
|
|
486
|
+
_FRAMEWORK_DRIVEN_TYPES: frozenset[str] = frozenset({
|
|
487
|
+
"api", "webapp", "web_mvc", "angular-spa",
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def framework_owner_stacks(stacks) -> dict[str, frozenset[str]]:
|
|
492
|
+
"""{framework name -> stack ids that declare it} — the ownership view.
|
|
493
|
+
|
|
494
|
+
Ownership is already structural: a FrameworkDetection is discovered inside a
|
|
495
|
+
StackDetection's manifests, so which runtime a framework belongs to is
|
|
496
|
+
evidence the detector recorded, never a name lookup (VAI-safe — no rule here
|
|
497
|
+
branches on what the framework is called).
|
|
498
|
+
"""
|
|
499
|
+
out: dict[str, set[str]] = {}
|
|
500
|
+
for stack in stacks or []:
|
|
501
|
+
sid = getattr(stack, "stack", "") or ""
|
|
502
|
+
if not sid:
|
|
503
|
+
continue
|
|
504
|
+
for fw in getattr(stack, "frameworks", None) or []:
|
|
505
|
+
name = getattr(fw, "name", "") or ""
|
|
506
|
+
if name:
|
|
507
|
+
out.setdefault(name, set()).add(sid)
|
|
508
|
+
return {name: frozenset(ids) for name, ids in out.items()}
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def framework_owner_families(stacks) -> dict[str, frozenset[str]]:
|
|
512
|
+
"""{framework name -> runtime families that declare it} — the ownership view."""
|
|
513
|
+
return {
|
|
514
|
+
name: frozenset(
|
|
515
|
+
fam for sid in ids if (fam := _STACK_FAMILY.get(sid)) is not None
|
|
516
|
+
)
|
|
517
|
+
for name, ids in framework_owner_stacks(stacks).items()
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _rule_framework_stack_attestation(
|
|
522
|
+
claimed_family: str | None,
|
|
523
|
+
owner_families: dict[str, frozenset[str]],
|
|
524
|
+
) -> list[ReconciliationFinding]:
|
|
525
|
+
"""A framework named in a headline is declared by no stack of the claimed runtime.
|
|
526
|
+
|
|
527
|
+
The headline describes ONE program: it welds a stack label ("C#/.NET") to the
|
|
528
|
+
frameworks found anywhere in the repo. On a polyglot repository those can be
|
|
529
|
+
different programs, and the sentence asserts a relationship the evidence
|
|
530
|
+
denies — the confluent examples repo read "C#/.NET rest api using JAX-RS",
|
|
531
|
+
attributing a JVM-only framework, declared by a Java stack in one
|
|
532
|
+
sub-workspace, to a .NET primary that declares no framework at all.
|
|
533
|
+
|
|
534
|
+
Fires only when ownership is PROVEN and disjoint: the framework has at least
|
|
535
|
+
one known owning family and the claimed family is not among them. An unknown
|
|
536
|
+
stack, or a framework no stack claims, yields nothing.
|
|
537
|
+
"""
|
|
538
|
+
if not claimed_family:
|
|
539
|
+
return []
|
|
540
|
+
foreign = sorted(
|
|
541
|
+
name for name, fams in owner_families.items()
|
|
542
|
+
if fams and claimed_family not in fams
|
|
543
|
+
)
|
|
544
|
+
if not foreign:
|
|
545
|
+
return []
|
|
546
|
+
others = sorted({fam for name in foreign for fam in owner_families[name]})
|
|
547
|
+
return [
|
|
548
|
+
ReconciliationFinding(
|
|
549
|
+
rule="framework_stack_attestation",
|
|
550
|
+
symbols=tuple(foreign),
|
|
551
|
+
# Sink-neutral: this detail is read by a prose surface AND by a
|
|
552
|
+
# structured gap, so it states the contradiction, never "the
|
|
553
|
+
# headline says…" — the headline may already have dropped the claim.
|
|
554
|
+
detail=(
|
|
555
|
+
f"{len(foreign)} framework(s) are declared only by "
|
|
556
|
+
f"{', '.join(others)} stack(s), not by the {claimed_family} "
|
|
557
|
+
"primary stack — attributing them to it describes two separate "
|
|
558
|
+
"programs as one."
|
|
559
|
+
),
|
|
560
|
+
)
|
|
561
|
+
]
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def reconcile_headline_evidence(
|
|
565
|
+
*,
|
|
566
|
+
claimed_stack: str,
|
|
567
|
+
project_type: str | None,
|
|
568
|
+
stacks,
|
|
569
|
+
) -> list[ReconciliationFinding]:
|
|
570
|
+
"""Run headline reconciliation rules. Never raises.
|
|
571
|
+
|
|
572
|
+
Args:
|
|
573
|
+
claimed_stack: stack id the headline's label is built from (the primary).
|
|
574
|
+
project_type: claimed type — cross-runtime types are exempt, their
|
|
575
|
+
headline is MEANT to name another tier's framework.
|
|
576
|
+
stacks: every StackDetection, each owning its own frameworks.
|
|
577
|
+
"""
|
|
578
|
+
if project_type in _CROSS_RUNTIME_TYPES:
|
|
579
|
+
return []
|
|
580
|
+
findings: list[ReconciliationFinding] = []
|
|
581
|
+
try:
|
|
582
|
+
findings.extend(
|
|
583
|
+
_rule_framework_stack_attestation(
|
|
584
|
+
_STACK_FAMILY.get(claimed_stack or ""),
|
|
585
|
+
framework_owner_families(stacks),
|
|
586
|
+
)
|
|
587
|
+
)
|
|
588
|
+
except Exception:
|
|
589
|
+
pass
|
|
590
|
+
return findings
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def type_claim_unattested(
|
|
594
|
+
*, claimed_stack: str, project_type: str | None, stacks
|
|
595
|
+
) -> bool:
|
|
596
|
+
"""True when a framework-driven type label rests ONLY on foreign evidence.
|
|
597
|
+
|
|
598
|
+
The sibling of `_rule_framework_stack_attestation` for the type clause: the
|
|
599
|
+
frameworks that decided `project_type` are declared, every one of them, by
|
|
600
|
+
stacks of another runtime. The narrative consumer degrades the sentence; the
|
|
601
|
+
confidence consumer records the gap — same judgement, two sinks, one table.
|
|
602
|
+
|
|
603
|
+
Provable only: silent when the type is not framework-driven, when the claimed
|
|
604
|
+
stack's runtime is unknown, or when no framework is owned at all.
|
|
605
|
+
"""
|
|
606
|
+
try:
|
|
607
|
+
if project_type not in _FRAMEWORK_DRIVEN_TYPES:
|
|
608
|
+
return False
|
|
609
|
+
family = _STACK_FAMILY.get(claimed_stack or "")
|
|
610
|
+
if not family:
|
|
611
|
+
return False
|
|
612
|
+
owners = framework_owner_families(stacks)
|
|
613
|
+
if not owners:
|
|
614
|
+
return False
|
|
615
|
+
return not any(family in fams for fams in owners.values())
|
|
616
|
+
except Exception:
|
|
617
|
+
return False
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def unattested_frameworks(
|
|
621
|
+
claimed_stack: str, project_type: str | None, stacks
|
|
622
|
+
) -> frozenset[str]:
|
|
623
|
+
"""Framework names the claimed stack does not attest (convenience view)."""
|
|
624
|
+
out: set[str] = set()
|
|
625
|
+
for f in reconcile_headline_evidence(
|
|
626
|
+
claimed_stack=claimed_stack, project_type=project_type, stacks=stacks
|
|
627
|
+
):
|
|
628
|
+
out.update(f.symbols)
|
|
629
|
+
return frozenset(out)
|
|
630
|
+
|
|
631
|
+
|
|
441
632
|
def reconcile_extraction_evidence(
|
|
442
633
|
*,
|
|
443
634
|
type_decl_files: frozenset[str],
|
|
@@ -29,7 +29,7 @@ import subprocess
|
|
|
29
29
|
from dataclasses import asdict, dataclass
|
|
30
30
|
from datetime import datetime, timezone
|
|
31
31
|
from pathlib import Path
|
|
32
|
-
from typing import Optional
|
|
32
|
+
from typing import Any, Optional
|
|
33
33
|
|
|
34
34
|
RIS_SCHEMA_VERSION: str = "1.0"
|
|
35
35
|
_RIS_FILENAME: str = "ris.json.gz"
|
|
@@ -107,9 +107,46 @@ def save_ris(repo_root: Path, ris: RepositoryIntelligenceSnapshot) -> None:
|
|
|
107
107
|
# Data extraction from L1 core_dict
|
|
108
108
|
# ---------------------------------------------------------------------------
|
|
109
109
|
|
|
110
|
+
def _entrypoint_paths(raw: Any) -> list[str]:
|
|
111
|
+
"""Normalize agent_view's `entry_points` to the file paths RIS promises.
|
|
112
|
+
|
|
113
|
+
`structural_map["entrypoints"]` is consumed as a list of path STRINGS (it is
|
|
114
|
+
sliced, and every reader guards with `isinstance(fp, str)`), but agent_view
|
|
115
|
+
emits that key in two different shapes under one schema_version:
|
|
116
|
+
|
|
117
|
+
{"bootstrap": [path, …], "security": […], "controllers": {counts}}
|
|
118
|
+
when a bootstrap/security entry point was detected (Spring, …)
|
|
119
|
+
[{"path": …, "kind": …}, …]
|
|
120
|
+
otherwise
|
|
121
|
+
|
|
122
|
+
Storing either verbatim broke BOTH readers: the dict raised `KeyError` on
|
|
123
|
+
`entrypoints[:10]` — swallowed by a bare `except Exception`, silently
|
|
124
|
+
disabling the RIS fast path on exactly the Java/Spring repos it exists for —
|
|
125
|
+
and the list of dicts recovered zero paths, because a dict is not a `str`.
|
|
126
|
+
|
|
127
|
+
The shape drift itself is a separate schema question; normalizing here keeps
|
|
128
|
+
RIS's own contract true whichever shape it is handed, including plain paths.
|
|
129
|
+
"""
|
|
130
|
+
if isinstance(raw, dict):
|
|
131
|
+
# Only bootstrap/security carry paths; `controllers` is a count summary.
|
|
132
|
+
out: list[str] = []
|
|
133
|
+
for key in ("bootstrap", "security"):
|
|
134
|
+
out.extend(p for p in (raw.get(key) or []) if isinstance(p, str))
|
|
135
|
+
return out
|
|
136
|
+
if isinstance(raw, list):
|
|
137
|
+
out = []
|
|
138
|
+
for item in raw:
|
|
139
|
+
if isinstance(item, str):
|
|
140
|
+
out.append(item)
|
|
141
|
+
elif isinstance(item, dict) and isinstance(item.get("path"), str):
|
|
142
|
+
out.append(item["path"])
|
|
143
|
+
return out
|
|
144
|
+
return []
|
|
145
|
+
|
|
146
|
+
|
|
110
147
|
def _extract_structural_map(agent_data: dict) -> dict:
|
|
111
148
|
"""Extract structural map from agent_view output."""
|
|
112
|
-
entry_points = agent_data.get("entry_points", [])
|
|
149
|
+
entry_points = _entrypoint_paths(agent_data.get("entry_points", []))
|
|
113
150
|
|
|
114
151
|
layers = []
|
|
115
152
|
domains = []
|
|
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
|
|
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
|