sourcecode 2.5.8__tar.gz → 2.5.12__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.8 → sourcecode-2.5.12}/CHANGELOG.md +138 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/PKG-INFO +1 -1
- {sourcecode-2.5.8 → sourcecode-2.5.12}/pyproject.toml +1 -1
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/__init__.py +1 -1
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/architecture_analyzer.py +112 -16
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/architecture_summary.py +75 -21
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/classifier.py +1 -1
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/cli.py +34 -2
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/confidence_analyzer.py +14 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/prepare_context.py +24 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/reconciliation.py +210 -43
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/spring_impact.py +35 -4
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/summarizer.py +20 -4
- {sourcecode-2.5.8 → sourcecode-2.5.12}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/.gitignore +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/.ruff.toml +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/CONTRIBUTING.md +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/LICENSE +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/README.md +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/SECURITY.md +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/raw +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/scripts/compare_integration_engines.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/scripts/customer_smoke_test.sh +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/scripts/generate_jdk_exports.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/archetype.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/cache.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/call_surface.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/caller_metrics.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/context_cache.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/context_graph.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/dynamic_argument_surface.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/endpoint_literals.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/endpoint_metrics.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/evidence_provider.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/explain.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/graph_evidence.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/hibernate_strat.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/jdk_exports.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/license.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/migrate_check.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/progress.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/redactor.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/repository_ir.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/__init__.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/context.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/errors.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/executor.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/planner.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/query.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/request.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/resolution.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/result.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/retriever.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/runtime.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/steps.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/steps_graph.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/steps_impact.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/steps_intf.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/steps_struct.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/steps_txsec.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/ris.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/scanner.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/schema.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/security_config.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/security_posture.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/semantic_impact_engine.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/semantic_integration_engine.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/semantic_services.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/serializer.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/type_usage_surface.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/validation_inference.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/validation_surface.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/version_check.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/workspace.py +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/supabase/functions/README.md +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/supabase/sql/licensing_schema.sql +0 -0
- {sourcecode-2.5.8 → sourcecode-2.5.12}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,5 +1,143 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.5.12] — 2026-07-18
|
|
4
|
+
|
|
5
|
+
**F-1 (the Evidence Reconciliation Layer) is closed and its rule core frozen.** This release
|
|
6
|
+
lands the reconciliation gap-fixes accumulated since 2.5.11 and completes INV-F1-4 across
|
|
7
|
+
every surface that narrates an architectural result — the layer's last open honesty debt.
|
|
8
|
+
No new rule, no widened heuristic; the frozen core (`reconciliation.py`) was not touched.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **INV-F1-4 closed across the narrating surfaces** (`architecture_analyzer.py`,
|
|
13
|
+
`prepare_context.py`): `036eaa9` made the producer record every architecture refutation on
|
|
14
|
+
`arch.limitations`, but only the serializer surface transported them — `prepare-context`/
|
|
15
|
+
onboard computed the analysis and threw the refutations away, so ofbiz's pruned modules and
|
|
16
|
+
a refuted layered claim vanished silently there. onboard now runs the analysis once (a
|
|
17
|
+
single owned `sm.architecture`) and carries the refutation lines into `limitations`, the
|
|
18
|
+
same sink the serializer uses. A reader can distinguish *"no architecture"* from *"claim
|
|
19
|
+
refuted"* on every narrating surface. One ownership model governs which surface shows,
|
|
20
|
+
degrades, or only transports a refutation (ADR-0005 §6.5).
|
|
21
|
+
|
|
22
|
+
- **Directory contamination that manufactured phantom refutations** (`architecture_analyzer.py`):
|
|
23
|
+
`flatten_file_tree` emits directory nodes, so `sm.file_paths` carried entries like
|
|
24
|
+
`src/main` that `_detect_layers` counted as jvm-less module members — a directory holding
|
|
25
|
+
all the Java transitively read as "no jvm code" and was refuted (petclinic phantom
|
|
26
|
+
`orchestration`; the directory half of `data`). The layer census now drops ancestor
|
|
27
|
+
(directory) paths; leaf files, including template view-layers, are untouched. Side effect:
|
|
28
|
+
pattern detection on the previously-polluted input is corrected (petclinic → `mvc`).
|
|
29
|
+
|
|
30
|
+
- **The web-pattern cross-check is now an F-1 rule (gap #4)** (`reconciliation.py`,
|
|
31
|
+
`architecture_summary.py`): `_qualify_web_pattern` was an ad-hoc `if`; it is now the seventh
|
|
32
|
+
rule, `web_pattern_endpoint_attestation`. The migration fixed a latent lie — the gate
|
|
33
|
+
degraded at `high < 5`, so a repo with 1–4 real controllers reported *"no HTTP controllers
|
|
34
|
+
detected"*. The honest predicate is `high == 0` (provable absence), and the authority is
|
|
35
|
+
Java-file coverage (`stack == "java"`), not the JVM family.
|
|
36
|
+
|
|
37
|
+
- **A reconciliation rule that raises can no longer read as "no contradiction" (gap #2)**
|
|
38
|
+
(`reconciliation.py` and all six consumers): each entry point wraps rules per-rule; an
|
|
39
|
+
exception yields an execution-error finding routed to a surfaced channel, never silence.
|
|
40
|
+
|
|
41
|
+
- **JVM checks gate on the runtime family, reconciled to one source (gap #3)**
|
|
42
|
+
(`reconciliation.py`, `classifier.py`): `stack_runtime_family` is the single owner; the two
|
|
43
|
+
incomplete `{java,kotlin}` copies are gone. The mvc/rest-api authority gate is separately
|
|
44
|
+
corrected to Java-file coverage, fixing a Kotlin/Scala/Groovy false-negation (INV-F1-1).
|
|
45
|
+
|
|
46
|
+
### Documentation
|
|
47
|
+
|
|
48
|
+
- **ADR-0005 marked Accepted & FROZEN.** Seven rules, six invariants closed, ownership model
|
|
49
|
+
ratified (§6.5), freeze & re-open conditions recorded (§10). Stale "six rules" / "INV-F1-4
|
|
50
|
+
violated" text removed. Closure report and surface-integration map in `.planning/`.
|
|
51
|
+
|
|
52
|
+
## [2.5.11] — 2026-07-17
|
|
53
|
+
|
|
54
|
+
**Two ways a tool can lie by omission: analyze the wrong repository without
|
|
55
|
+
saying so, and refute a claim without saying so.** Both closed here. Also lands
|
|
56
|
+
ADR-0005, which ratifies the Evidence Reconciliation Layer as infrastructure and
|
|
57
|
+
records — with measurements — which of its "missing pieces" were never needed.
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
|
|
61
|
+
- **`ask <path> <subcommand>` silently analyzed the current directory instead
|
|
62
|
+
(P2-G)** (`cli.py`): the argument preprocessor popped the first positional into
|
|
63
|
+
a path that only the top-level `main` reads. Every subcommand declares its own
|
|
64
|
+
`path = Argument(".")`, so a leading path before a subcommand was not
|
|
65
|
+
misparsed — it was **discarded**, and the subcommand fell back to the CWD.
|
|
66
|
+
`ask spaghetti-api endpoints` reported **1077 endpoints from every repo in the
|
|
67
|
+
parent directory** (the real surface was 7), exit 0, no warning — an agent
|
|
68
|
+
consuming that JSON could not tell it was reading someone else's code. Now
|
|
69
|
+
refused: exit 2, empty stdout, naming the correct invocation. Reordering cannot
|
|
70
|
+
be guessed (`impact <symbol> <path>` has no free slot). The false-positive
|
|
71
|
+
guard (an option *value* that happens to match a subcommand name, e.g.
|
|
72
|
+
`--files endpoints`) is covered. The test suite was itself a victim: four tests
|
|
73
|
+
invoked the wrong-order form and passed while analyzing atlas-cli — they
|
|
74
|
+
asserted only "valid JSON", which any repo produces. Corrected to assert an
|
|
75
|
+
artifact only the intended fixture yields.
|
|
76
|
+
|
|
77
|
+
- **Refuted architecture claims were pruned silently (INV-F1-4)**
|
|
78
|
+
(`architecture_analyzer.py`, `reconciliation.py`): three reconciliation rules
|
|
79
|
+
(`layer_language_coherence`, `composition_runtime_split`,
|
|
80
|
+
`layer_code_attestation`) were consumed through name-only projections that
|
|
81
|
+
discarded the finding's explanation. The analyzer dropped a layer or rejected a
|
|
82
|
+
pattern and told the reader nothing — ofbiz showed fewer modules, jobrunr's
|
|
83
|
+
"fullstack" vanished, keycloak's foreign `view` layer disappeared, each without
|
|
84
|
+
a trace. A reader could not distinguish *"this repo has no architecture"* from
|
|
85
|
+
*"we refuted the architecture claim"* — the exact distinction the analyzer's
|
|
86
|
+
degrade-to-N/A discipline exists to preserve. Refutations that shape the
|
|
87
|
+
reported result now surface in `limitations`, prefixed *"Architecture claim
|
|
88
|
+
refuted on evidence: …"*. Scoped to avoid noise: the winning pattern's pruned
|
|
89
|
+
layers, the runtime-split contradiction that removed a contender, and every
|
|
90
|
+
pruning in the fallback paths (which run only when nothing else qualified).
|
|
91
|
+
|
|
92
|
+
### Added
|
|
93
|
+
|
|
94
|
+
- **ADR-0005 — Evidence Reconciliation Layer (F-1)**
|
|
95
|
+
(`docs/architecture/adr/0005-evidence-reconciliation.md`): the layer's contract,
|
|
96
|
+
invariants and limits. Ratifies the contract as **data** (immutable views +
|
|
97
|
+
pure-function rules + `ReconciliationFinding`) and **rejects** the proposed
|
|
98
|
+
provider protocol — an ABC spanning the six consumers would constrain nothing.
|
|
99
|
+
Documents that the "precursors" slated for migration (the Confidence envelope,
|
|
100
|
+
`coverage_confidence`, `zero_result_reason`, readiness N/A) are single-view
|
|
101
|
+
mechanisms that must stay out of F-1, and that pipelines — not commands — are
|
|
102
|
+
the unit of coverage (`onboard`/`review-pr`/`fix-bug` inherit reconciliation
|
|
103
|
+
from `prepare_context`).
|
|
104
|
+
|
|
105
|
+
## [2.5.9] — 2026-07-17
|
|
106
|
+
|
|
107
|
+
**A silently dead fast path.** The backlog carried the `entry_points`
|
|
108
|
+
list-vs-dict drift as a schema tidiness item. It was not: the drift had disabled
|
|
109
|
+
a feature, quietly, on exactly the repositories that feature exists for.
|
|
110
|
+
|
|
111
|
+
### Fixed
|
|
112
|
+
|
|
113
|
+
- **RIS stored `entry_points` in whichever shape it was handed
|
|
114
|
+
(P2-F-RESIDUE(b))** (`ris.py`): `agent_view` emits that key in two shapes under
|
|
115
|
+
one `schema_version` — `{"bootstrap": [path…], "security": […],
|
|
116
|
+
"controllers": {counts}}` when a bootstrap entry point is detected, otherwise
|
|
117
|
+
`[{"path": …, "kind": …}, …]`. RIS copied either one verbatim into
|
|
118
|
+
`structural_map["entrypoints"]`, a key its readers treat as a list of path
|
|
119
|
+
**strings**: they slice it, then guard `isinstance(fp, str)`.
|
|
120
|
+
|
|
121
|
+
Both shapes broke it, measured against the real function:
|
|
122
|
+
|
|
123
|
+
| shape | result |
|
|
124
|
+
|---|---|
|
|
125
|
+
| dict (Spring, …) | `entrypoints[:10]` → `KeyError: slice(None, 10, None)`. `prepare_context._try_ris_fast_path` wraps its whole body in `except Exception: return None`, so **the RIS fast path was silently dead on Java/Spring repos** — falling back to full analysis with no warning. |
|
|
126
|
+
| list of dicts (everything else) | `isinstance(fp, str)` is false for every item → **zero entry-point files recovered**, silently. |
|
|
127
|
+
|
|
128
|
+
The fast path therefore recovered entry points for **no repository at all**,
|
|
129
|
+
and a bare `except` kept it quiet. `_entrypoint_paths()` now normalizes either
|
|
130
|
+
shape — plus plain path lists and junk — to the paths RIS promises. Only
|
|
131
|
+
`bootstrap`/`security` carry paths, so the `controllers` count summary cannot
|
|
132
|
+
leak into the list.
|
|
133
|
+
|
|
134
|
+
The public shape drift is deliberately untouched: unifying it changes a
|
|
135
|
+
documented `schema_version` and belongs in its own release. RIS's contract now
|
|
136
|
+
holds whichever shape it is handed, which is what its readers depend on.
|
|
137
|
+
|
|
138
|
+
3 new tests; suite `3466 passed, 5 skipped, 1 deselected` (the deselection is a
|
|
139
|
+
pre-existing `file_chunker` failure unrelated to this work).
|
|
140
|
+
|
|
3
141
|
## [2.5.8] — 2026-07-17
|
|
4
142
|
|
|
5
143
|
**A count must name what it counted.** 2.5.7 taught the headline to say *"Polyglot
|
|
@@ -5,9 +5,8 @@ from pathlib import Path
|
|
|
5
5
|
from typing import Literal, Optional
|
|
6
6
|
|
|
7
7
|
from sourcecode.reconciliation import (
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
unattested_layers,
|
|
8
|
+
reconcile_architecture_evidence,
|
|
9
|
+
reconcile_code_modules,
|
|
11
10
|
)
|
|
12
11
|
|
|
13
12
|
from sourcecode.schema import (
|
|
@@ -280,6 +279,18 @@ class ArchitectureAnalyzer:
|
|
|
280
279
|
) -> ArchitectureAnalysis:
|
|
281
280
|
limitations: list[str] = []
|
|
282
281
|
evidence: list[dict] = []
|
|
282
|
+
# INV-F1-4: reconciliation rules that refute a layer/pattern must not
|
|
283
|
+
# apply their verdict silently — a pruned layer or a rejected pattern is
|
|
284
|
+
# a positively-detected contradiction the reader is entitled to see, or
|
|
285
|
+
# "no architecture" becomes indistinguishable from "we refuted the
|
|
286
|
+
# architecture claim". `_detect_layers` and its helpers append the
|
|
287
|
+
# findings' sink-neutral detail here; drained into `limitations` below.
|
|
288
|
+
self._refutations: list[str] = []
|
|
289
|
+
# Gap #2 (fail-open): a reconciliation rule that could not be evaluated
|
|
290
|
+
# is NOT a clean result — surfaced here as its own limitation, never as a
|
|
291
|
+
# refutation and never silently, so an unverified pattern cannot ship as
|
|
292
|
+
# confirmed.
|
|
293
|
+
self._reconciliation_errors: list[str] = []
|
|
283
294
|
|
|
284
295
|
# Step 1: filter paths
|
|
285
296
|
filtered = self._filter_paths(sm.file_paths)
|
|
@@ -482,6 +493,25 @@ class ArchitectureAnalyzer:
|
|
|
482
493
|
"confidence": "low",
|
|
483
494
|
})
|
|
484
495
|
|
|
496
|
+
# INV-F1-4 sink: surface every refutation that shaped the returned
|
|
497
|
+
# result. Prefix frames it as a refutation; the detail (unchanged) states
|
|
498
|
+
# the contradiction, keeping it sink-neutral (INV-F1-5). Dedup: the
|
|
499
|
+
# layered→modular fallthrough can refute the same directory twice.
|
|
500
|
+
_seen: set[str] = set()
|
|
501
|
+
for _detail in self._refutations:
|
|
502
|
+
if _detail in _seen:
|
|
503
|
+
continue
|
|
504
|
+
_seen.add(_detail)
|
|
505
|
+
limitations.append(f"Architecture claim refuted on evidence: {_detail}")
|
|
506
|
+
# Fail-open sink: a rule that could not run is surfaced distinctly — the
|
|
507
|
+
# pattern it would have checked is reported UNVERIFIED, never certified.
|
|
508
|
+
_seen_err: set[str] = set()
|
|
509
|
+
for _detail in self._reconciliation_errors:
|
|
510
|
+
if _detail in _seen_err:
|
|
511
|
+
continue
|
|
512
|
+
_seen_err.add(_detail)
|
|
513
|
+
limitations.append(f"Architecture reconciliation could not run: {_detail}")
|
|
514
|
+
|
|
485
515
|
return ArchitectureAnalysis(
|
|
486
516
|
requested=True,
|
|
487
517
|
pattern=pattern,
|
|
@@ -627,6 +657,24 @@ class ArchitectureAnalyzer:
|
|
|
627
657
|
return domains
|
|
628
658
|
|
|
629
659
|
def _detect_layers(self, paths: list[str]) -> tuple[str, list[ArchitectureLayer]]:
|
|
660
|
+
# Drop directory ENTRIES. `flatten_file_tree` emits every node — files AND
|
|
661
|
+
# the directories above them — so `sm.file_paths` carries `src`, `src/main`,
|
|
662
|
+
# `src/main/java/.../model` … A directory is not a file: counting it as a
|
|
663
|
+
# layer/module member makes it read as jvm-less in `runtime_census` (it holds
|
|
664
|
+
# no file OF ITS OWN, only children), and a directory that transitively holds
|
|
665
|
+
# all the Java is then refuted as "no jvm code" — the demonstrated
|
|
666
|
+
# contamination (petclinic phantom `orchestration` = `src/main`; the
|
|
667
|
+
# directory half of `data` = `.../model`). A path that is an ancestor of
|
|
668
|
+
# another path in the set is such an entry; leaf files (incl. template
|
|
669
|
+
# view-layers) are never ancestors and are kept untouched.
|
|
670
|
+
_norm = [p.replace("\\", "/") for p in paths]
|
|
671
|
+
_ancestors: set[str] = set()
|
|
672
|
+
for _p in _norm:
|
|
673
|
+
_segs = _p.split("/")
|
|
674
|
+
for _k in range(1, len(_segs)):
|
|
675
|
+
_ancestors.add("/".join(_segs[:_k]))
|
|
676
|
+
paths = [p for p, n in zip(paths, _norm) if n not in _ancestors]
|
|
677
|
+
|
|
630
678
|
# Exclude non-source paths (tests, benchmarks, docs, tooling, vendored assets)
|
|
631
679
|
# from layer scoring. Also exclude anything under a `resources/` segment: in
|
|
632
680
|
# Maven/Gradle layouts `src/main/resources/**` is bundled config/assets — e.g.
|
|
@@ -665,6 +713,13 @@ class ArchitectureAnalyzer:
|
|
|
665
713
|
best_priority = -1
|
|
666
714
|
best_matched: dict[str, list[str]] = {}
|
|
667
715
|
best_layer_files: dict[str, set[int]] = {}
|
|
716
|
+
# INV-F1-4: refutations that fired while assembling each candidate. Only
|
|
717
|
+
# those that shaped the RETURNED result are surfaced (below): the winning
|
|
718
|
+
# pattern's pruned layers, or — when no classical pattern qualifies — the
|
|
719
|
+
# contradictions that removed the contenders. A refutation of a candidate
|
|
720
|
+
# that lost on score anyway is not what the reader is being misled about.
|
|
721
|
+
cand_refutations: dict[str, list[str]] = {}
|
|
722
|
+
contradiction_details: list[str] = []
|
|
668
723
|
|
|
669
724
|
for pattern_name, layer_keys in LAYER_PATTERNS.items():
|
|
670
725
|
matched: dict[str, list[str]] = {}
|
|
@@ -692,16 +747,34 @@ class ArchitectureAnalyzer:
|
|
|
692
747
|
# template/asset-only layers are untouched, so a real Spring-MVC
|
|
693
748
|
# `templates/` view survives.
|
|
694
749
|
_layer_paths = {k: [source_paths[i] for i in v] for k, v in layer_files.items()}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
750
|
+
_refs: list[str] = []
|
|
751
|
+
_split_contradicted = False
|
|
752
|
+
for _f in reconcile_architecture_evidence(
|
|
753
|
+
pattern=pattern_name, layer_files=_layer_paths, repo_files=source_paths
|
|
754
|
+
):
|
|
755
|
+
if _f.error:
|
|
756
|
+
# A rule that could not run is surfaced always (not tied to
|
|
757
|
+
# whether this candidate wins) and never as a refutation.
|
|
758
|
+
self._reconciliation_errors.append(_f.detail)
|
|
759
|
+
continue
|
|
760
|
+
_refs.append(_f.detail)
|
|
761
|
+
if _f.rule == "composition_runtime_split":
|
|
762
|
+
# A claim that IS a runtime split must show one: JobRunr, a JVM
|
|
763
|
+
# library, read "Fullstack with frontend, backend layers" off a
|
|
764
|
+
# `dashboard/ui/model/` package of 16 Java files. Nothing to
|
|
765
|
+
# prune — the pattern itself is what the evidence contradicts.
|
|
766
|
+
_split_contradicted = True
|
|
767
|
+
else:
|
|
768
|
+
# layer_language_coherence: drop the layers owned by a
|
|
769
|
+
# different runtime; template/asset-only layers are untouched,
|
|
770
|
+
# so a real Spring-MVC `templates/` view survives.
|
|
771
|
+
for _bad in _f.symbols:
|
|
772
|
+
matched.pop(_bad, None)
|
|
773
|
+
layer_files.pop(_bad, None)
|
|
774
|
+
if _split_contradicted:
|
|
775
|
+
contradiction_details.extend(_refs)
|
|
704
776
|
continue
|
|
777
|
+
cand_refutations[pattern_name] = _refs
|
|
705
778
|
|
|
706
779
|
# A pattern with unmet required keys cannot qualify (e.g. mvc needs a view).
|
|
707
780
|
required = _PATTERN_REQUIRED_KEYS.get(pattern_name)
|
|
@@ -717,6 +790,10 @@ class ArchitectureAnalyzer:
|
|
|
717
790
|
best_layer_files = layer_files
|
|
718
791
|
|
|
719
792
|
if best_score >= 2:
|
|
793
|
+
# The reported pattern is the one whose refutations misled the reader
|
|
794
|
+
# if left silent (keycloak's dropped `view` layer). Contradictions of
|
|
795
|
+
# OTHER, losing candidates did not shape this result — omit them.
|
|
796
|
+
self._refutations.extend(cand_refutations.get(best_pattern, []))
|
|
720
797
|
layer_confidence: Literal["high", "medium", "low"] = "medium" if best_score >= 3 else "low"
|
|
721
798
|
layers: list[ArchitectureLayer] = []
|
|
722
799
|
for layer_key, matched_dirs in best_matched.items():
|
|
@@ -729,6 +806,11 @@ class ArchitectureAnalyzer:
|
|
|
729
806
|
))
|
|
730
807
|
return best_pattern, layers
|
|
731
808
|
|
|
809
|
+
# No classical pattern qualified. If a contender was removed by a runtime
|
|
810
|
+
# split contradiction (jobrunr's "fullstack"), that is why — and the
|
|
811
|
+
# eventual fallthrough result would otherwise look like a silent absence.
|
|
812
|
+
self._refutations.extend(contradiction_details)
|
|
813
|
+
|
|
732
814
|
# 2. Spring domain-module detection (petclinic-style: deep common prefix + feature dirs)
|
|
733
815
|
spring_result = self._detect_spring_domain_modules(source_paths)
|
|
734
816
|
if spring_result is not None:
|
|
@@ -872,8 +954,16 @@ class ArchitectureAnalyzer:
|
|
|
872
954
|
# Architecture with orchestration, data layers" rested on ONE directory
|
|
873
955
|
# entry plus a settings.gradle, with 47 Java files visible and ignored.
|
|
874
956
|
# A logical code layer that holds no code of this repo is not one.
|
|
875
|
-
|
|
876
|
-
|
|
957
|
+
# INV-F1-4: this path only runs when no classical pattern won, so a
|
|
958
|
+
# pruning here directly shapes the returned result (fewer layers, or a
|
|
959
|
+
# drop below 2 that falls through to flat/unknown) — surface it.
|
|
960
|
+
for _f in reconcile_code_modules(layer_files=non_empty, repo_files=paths):
|
|
961
|
+
if _f.error:
|
|
962
|
+
self._reconciliation_errors.append(_f.detail)
|
|
963
|
+
continue
|
|
964
|
+
for _bad in _f.symbols:
|
|
965
|
+
non_empty.pop(_bad, None)
|
|
966
|
+
self._refutations.append(_f.detail)
|
|
877
967
|
if len(non_empty) >= 2:
|
|
878
968
|
return "layered", [
|
|
879
969
|
ArchitectureLayer(name=k, pattern="layered", files=v, confidence="low")
|
|
@@ -904,8 +994,14 @@ class ArchitectureAnalyzer:
|
|
|
904
994
|
# count alone let build and asset directories be reported as modules —
|
|
905
995
|
# ofbiz "docker, framework, runtime, gradle", neo4j "packaging",
|
|
906
996
|
# eureka "images" (five PNGs). The real modules survive untouched.
|
|
907
|
-
|
|
908
|
-
|
|
997
|
+
# INV-F1-4: same as the layered path — surface each refuted module.
|
|
998
|
+
for _f in reconcile_code_modules(layer_files=meaningful, repo_files=paths):
|
|
999
|
+
if _f.error:
|
|
1000
|
+
self._reconciliation_errors.append(_f.detail)
|
|
1001
|
+
continue
|
|
1002
|
+
for _bad in _f.symbols:
|
|
1003
|
+
meaningful.pop(_bad, None)
|
|
1004
|
+
self._refutations.append(_f.detail)
|
|
909
1005
|
if len(meaningful) >= 2:
|
|
910
1006
|
return "modular", [
|
|
911
1007
|
ArchitectureLayer(name=k, pattern="modular", files=v, confidence="low")
|
|
@@ -210,23 +210,38 @@ class ArchitectureSummarizer:
|
|
|
210
210
|
# welds a foreign runtime's framework onto the primary's label. Drop what
|
|
211
211
|
# the claimed stack does not attest, and never let the TYPE label — which
|
|
212
212
|
# those same frameworks drove — survive its own evidence (P2-F).
|
|
213
|
-
from sourcecode.reconciliation import
|
|
213
|
+
from sourcecode.reconciliation import (
|
|
214
|
+
has_execution_error,
|
|
215
|
+
reconcile_headline_evidence,
|
|
216
|
+
type_claim_unattested,
|
|
217
|
+
)
|
|
214
218
|
|
|
215
|
-
|
|
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
|
+
_headline_findings = reconcile_headline_evidence(
|
|
219
220
|
claimed_stack=primary.stack, project_type=sm.project_type, stacks=sm.stacks
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
|
|
221
|
+
)
|
|
222
|
+
if has_execution_error(_headline_findings):
|
|
223
|
+
# Fail-open guard (gap #2): the attestation rule could not run, and
|
|
224
|
+
# this prose surface has no gap channel to warn on. Rather than assert
|
|
225
|
+
# an unverified weld ("C#/.NET … using JAX-RS"), withhold the framework
|
|
226
|
+
# clause — the claim is not made when its check did not execute.
|
|
227
|
+
fw_names = []
|
|
228
|
+
else:
|
|
229
|
+
foreign = frozenset(s for f in _headline_findings for s in f.symbols)
|
|
230
|
+
attested = [name for name in fw_names if name not in foreign]
|
|
231
|
+
dropped = [name for name in fw_names if name in foreign]
|
|
232
|
+
if dropped and not attested and type_claim_unattested(
|
|
233
|
+
claimed_stack=primary.stack, project_type=sm.project_type, stacks=sm.stacks
|
|
234
|
+
):
|
|
235
|
+
return self._describe_foreign_evidence(sm, stack_label, runtime, dropped)
|
|
236
|
+
fw_names = attested
|
|
223
237
|
|
|
224
238
|
fw_str = f" using {', '.join(fw_names)}" if fw_names else ""
|
|
225
239
|
if runtime:
|
|
226
240
|
# BUG #4: never assert "rest api" in the headline unless the endpoints
|
|
227
|
-
# command actually backs it
|
|
228
|
-
#
|
|
229
|
-
|
|
241
|
+
# command actually backs it. Authority is Java ONLY: extract_java_endpoints
|
|
242
|
+
# reads *.java and is blind to .kt — a Kotlin REST API would otherwise
|
|
243
|
+
# degrade to "only 0 endpoints detected" over routes it cannot see.
|
|
244
|
+
if sm.project_type == "api" and primary.stack == "java":
|
|
230
245
|
total, high = self._endpoint_support()
|
|
231
246
|
if high < _MIN_REST_ENDPOINTS_FOR_LABEL:
|
|
232
247
|
plural = "s" if total != 1 else ""
|
|
@@ -310,23 +325,62 @@ class ArchitectureSummarizer:
|
|
|
310
325
|
|
|
311
326
|
def _qualify_web_pattern(self, arch: Any, arch_line: str, sm: SourceMap) -> str:
|
|
312
327
|
"""BUG #1 (Jenkins field test): the "mvc" pattern is a directory-name
|
|
313
|
-
heuristic (dirs matching controller/model/view keywords). On a
|
|
314
|
-
|
|
328
|
+
heuristic (dirs matching controller/model/view keywords). On a JVM repo,
|
|
329
|
+
do not assert "MVC pattern with ... view layers" in prose when the
|
|
315
330
|
canonical endpoint extractor finds no HTTP controllers in the same run —
|
|
316
331
|
Jenkins (Stapler web framework) matches model/view-ish dirs but declares
|
|
317
|
-
zero Spring MVC controllers.
|
|
318
|
-
|
|
332
|
+
zero Spring MVC controllers.
|
|
333
|
+
|
|
334
|
+
This is a reconciliation rule (layer table × endpoint index), so the
|
|
335
|
+
judgement lives in F-1 (`reconcile_web_pattern_evidence`), not as an
|
|
336
|
+
ad-hoc `if` here (ADR-0005 §6.2/§8.4). The honest predicate is ZERO
|
|
337
|
+
modeled controllers, not `< 5`: with the old threshold, 1–4 real
|
|
338
|
+
controllers still degraded to the categorical "no HTTP controllers
|
|
339
|
+
detected" — a false statement. One controller backs the MVC claim; only
|
|
340
|
+
an empty index contradicts it.
|
|
341
|
+
|
|
342
|
+
**Authority = Java only.** The extractor (`extract_java_endpoints`) reads
|
|
343
|
+
`*.java`; it is blind to `.kt`/`.scala`/`.groovy`. So zero controllers is
|
|
344
|
+
a contradiction ONLY for a Java-primary repo. On Kotlin/Scala/Groovy an
|
|
345
|
+
empty index is the extractor's blindness, not absence of controllers —
|
|
346
|
+
degrading there asserted "no HTTP controllers detected" over real `.kt`
|
|
347
|
+
routes (INV-F1-1: never fire on absence of evidence). Gating the rule on
|
|
348
|
+
the JVM *family* was wrong; the boundary is the extractor's file coverage."""
|
|
349
|
+
from sourcecode.reconciliation import (
|
|
350
|
+
has_execution_error,
|
|
351
|
+
reconcile_web_pattern_evidence,
|
|
352
|
+
)
|
|
353
|
+
|
|
319
354
|
if not arch_line or arch.pattern not in ("mvc", "spring_mvc_layered"):
|
|
320
355
|
return arch_line
|
|
321
356
|
primary = next((s for s in sm.stacks if s.primary), sm.stacks[0] if sm.stacks else None)
|
|
322
|
-
if primary is None or primary.stack not in {"java", "kotlin"}:
|
|
323
|
-
return arch_line
|
|
324
357
|
_total, high = self._endpoint_support()
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
358
|
+
# The endpoint extractor reads .java only — it is authoritative for a
|
|
359
|
+
# Java-primary repo, blind elsewhere.
|
|
360
|
+
_authoritative = bool(primary) and primary.stack == "java"
|
|
361
|
+
_findings = reconcile_web_pattern_evidence(
|
|
362
|
+
pattern=arch.pattern,
|
|
363
|
+
endpoint_evidence_authoritative=_authoritative,
|
|
364
|
+
modeled_endpoint_count=high,
|
|
365
|
+
)
|
|
329
366
|
layer_names = [l.name for l in arch.layers[:4] if l.name != "view"] if arch.layers else []
|
|
367
|
+
if has_execution_error(_findings):
|
|
368
|
+
# Fail-open guard (gap #2): the cross-check could not run. Withhold the
|
|
369
|
+
# categorical MVC label AND the "no controllers" claim (both unverified);
|
|
370
|
+
# report only the directory-derived layers, or nothing.
|
|
371
|
+
if layer_names:
|
|
372
|
+
return (
|
|
373
|
+
f"Layered code organization ({', '.join(layer_names)}; "
|
|
374
|
+
"directory-based — endpoint cross-check unavailable)."
|
|
375
|
+
)
|
|
376
|
+
return ""
|
|
377
|
+
if not _findings:
|
|
378
|
+
# Non-Java (extractor blind), not a web pattern, or ≥1 controller backs
|
|
379
|
+
# the claim → keep. On Kotlin/Scala/Groovy the MVC label stays
|
|
380
|
+
# unverified rather than falsely negated.
|
|
381
|
+
return arch_line
|
|
382
|
+
# Contradiction: zero modeled controllers. Degrade — the claim is
|
|
383
|
+
# directory names, not routes; the "no HTTP controllers" wording is TRUE.
|
|
330
384
|
if layer_names:
|
|
331
385
|
return (
|
|
332
386
|
f"Layered code organization ({', '.join(layer_names)}; "
|
|
@@ -207,7 +207,7 @@ class TypeClassifier:
|
|
|
207
207
|
# structured codebase (e.g. JobRunr: core + per-framework adapter modules).
|
|
208
208
|
# This is checked BEFORE the weak `bin/`-directory CLI heuristic so a build
|
|
209
209
|
# output / wrapper `bin/` dir does not mislabel a library as a CLI.
|
|
210
|
-
if stack_names &
|
|
210
|
+
if stack_names & _JVM_STACKS and self._is_multi_module(file_tree):
|
|
211
211
|
return "library"
|
|
212
212
|
|
|
213
213
|
# Weak CLI heuristic: a top-level bin/ directory (only when nothing stronger).
|
|
@@ -7,7 +7,7 @@ import sys
|
|
|
7
7
|
import threading
|
|
8
8
|
import time
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Any, Optional, cast
|
|
10
|
+
from typing import Any, NoReturn, Optional, cast
|
|
11
11
|
|
|
12
12
|
import typer
|
|
13
13
|
|
|
@@ -293,6 +293,30 @@ _OPTIONS_WITH_VALUE: frozenset[str] = frozenset({
|
|
|
293
293
|
})
|
|
294
294
|
|
|
295
295
|
|
|
296
|
+
def _reject_path_before_subcommand(path_token: str, subcommand: str) -> "NoReturn":
|
|
297
|
+
"""Refuse `ask <path> <subcommand>` instead of silently scanning the CWD.
|
|
298
|
+
|
|
299
|
+
`_detected_path` is only read by the root analysis: a subcommand takes its own
|
|
300
|
+
`path` Argument, which defaults to the current directory. So the path token in
|
|
301
|
+
`ask spaghetti-api endpoints` was swallowed here and the subcommand ran against
|
|
302
|
+
the CWD — from a directory of checkouts that is every repository at once.
|
|
303
|
+
Measured: it answered with 1077 endpoints from keycloak, neo4j and eureka
|
|
304
|
+
instead of spaghetti-api's 7, exit code 0, no warning.
|
|
305
|
+
|
|
306
|
+
A wrong answer that looks right is the one failure this tool must not produce,
|
|
307
|
+
and the token order cannot be repaired by guessing: subcommands disagree about
|
|
308
|
+
where a path sits (`impact-chain <symbol> <path>`), so there is no slot to move
|
|
309
|
+
it to. Name the correct command and stop.
|
|
310
|
+
"""
|
|
311
|
+
print(
|
|
312
|
+
f"error: path '{path_token}' comes before the '{subcommand}' subcommand, "
|
|
313
|
+
f"so it would be ignored and the current directory scanned instead.\n"
|
|
314
|
+
f" use: ask {subcommand} {path_token}",
|
|
315
|
+
file=sys.stderr,
|
|
316
|
+
)
|
|
317
|
+
raise SystemExit(2)
|
|
318
|
+
|
|
319
|
+
|
|
296
320
|
def _preprocess_args(args: list[str]) -> list[str]:
|
|
297
321
|
"""Extract a repository path token from an args list and store it in _detected_path.
|
|
298
322
|
|
|
@@ -300,6 +324,8 @@ def _preprocess_args(args: list[str]) -> list[str]:
|
|
|
300
324
|
Correctly skips option values (e.g. ``yaml`` in ``--format yaml``).
|
|
301
325
|
If the first non-flag, non-value positional token is a known subcommand name,
|
|
302
326
|
args are returned unchanged so Click can dispatch the subcommand.
|
|
327
|
+
A path token followed by a subcommand is rejected — see
|
|
328
|
+
``_reject_path_before_subcommand``.
|
|
303
329
|
"""
|
|
304
330
|
result = list(args)
|
|
305
331
|
skip_next = False
|
|
@@ -315,11 +341,17 @@ def _preprocess_args(args: list[str]) -> list[str]:
|
|
|
315
341
|
skip_next = True
|
|
316
342
|
continue
|
|
317
343
|
if arg in _SUBCOMMANDS:
|
|
344
|
+
if _path_index >= 0:
|
|
345
|
+
# A path already claimed a slot, so this subcommand can only be
|
|
346
|
+
# the wrong-order form: the path would be dropped and the CWD
|
|
347
|
+
# scanned in its place.
|
|
348
|
+
_reject_path_before_subcommand(result[_path_index], arg)
|
|
318
349
|
return result # known subcommand — leave for Click to dispatch
|
|
350
|
+
if _path_index >= 0:
|
|
351
|
+
continue # a later positional is the subcommand's own business
|
|
319
352
|
# First genuine positional: treat as repository path
|
|
320
353
|
_set_detected_path(arg)
|
|
321
354
|
_path_index = i
|
|
322
|
-
break
|
|
323
355
|
if _path_index >= 0:
|
|
324
356
|
result.pop(_path_index)
|
|
325
357
|
return result
|
|
@@ -120,6 +120,20 @@ class ConfidenceAnalyzer:
|
|
|
120
120
|
project_type=sm.project_type,
|
|
121
121
|
stacks=sm.stacks,
|
|
122
122
|
):
|
|
123
|
+
if _rf.error:
|
|
124
|
+
# Fail-open guard (gap #2): the stack-attestation rule could
|
|
125
|
+
# not run. Never let that read as "primary attests every
|
|
126
|
+
# framework" — record it as an anomaly + gap, not silence.
|
|
127
|
+
anomalies.append(
|
|
128
|
+
"Stack/framework attestation could not be evaluated — "
|
|
129
|
+
"primary-stack attribution is UNVERIFIED"
|
|
130
|
+
)
|
|
131
|
+
gaps.append(AnalysisGap(
|
|
132
|
+
area="framework_stack_attestation",
|
|
133
|
+
reason=_rf.detail,
|
|
134
|
+
impact="medium",
|
|
135
|
+
))
|
|
136
|
+
continue
|
|
123
137
|
anomalies.append(
|
|
124
138
|
f"{', '.join(_rf.symbols)} declared by another runtime, not by the "
|
|
125
139
|
f"{_primary.stack} primary stack"
|
|
@@ -1249,6 +1249,17 @@ class TaskContextBuilder:
|
|
|
1249
1249
|
[ws.path for ws in workspace_analysis.workspaces],
|
|
1250
1250
|
)
|
|
1251
1251
|
|
|
1252
|
+
# F-1 single-ownership (ADR-0005 §INV-F1-4): run the architecture analysis
|
|
1253
|
+
# ONCE, here, and keep it on sm.architecture — the single owned analysis.
|
|
1254
|
+
# ArchitectureSummarizer/ContextSummarizer reuse it instead of each computing
|
|
1255
|
+
# (and discarding) a private copy, and its refutations are transported into
|
|
1256
|
+
# `limitations` below (surface #3, F1-SURFACE-INTEGRATION-2026-07-18).
|
|
1257
|
+
from sourcecode.architecture_analyzer import ArchitectureAnalyzer as _ArchAnalyzer
|
|
1258
|
+
try:
|
|
1259
|
+
sm.architecture = _ArchAnalyzer().analyze(self.root, sm)
|
|
1260
|
+
except Exception:
|
|
1261
|
+
sm.architecture = None
|
|
1262
|
+
|
|
1252
1263
|
project_summary = ProjectSummarizer(self.root).generate(sm)
|
|
1253
1264
|
architecture_summary = ArchitectureSummarizer(self.root).generate(sm)
|
|
1254
1265
|
|
|
@@ -1306,6 +1317,19 @@ class TaskContextBuilder:
|
|
|
1306
1317
|
key_dependencies: list[dict[str, Any]] = []
|
|
1307
1318
|
limitations: list[str] = []
|
|
1308
1319
|
|
|
1320
|
+
# INV-F1-4 transport: this surface narrates the architecture, so it must
|
|
1321
|
+
# carry the owner's refutations — the same lines surface #1 (serializer)
|
|
1322
|
+
# emits. Only the F-1 refutation / execution-error lines (their fixed
|
|
1323
|
+
# prefixes); pattern-inference notes stay out of the onboard surface. The
|
|
1324
|
+
# analysis is the single owned one (sm.architecture) — no re-run, no re-judge.
|
|
1325
|
+
if sm.architecture is not None:
|
|
1326
|
+
for _arch_lim in (sm.architecture.limitations or []):
|
|
1327
|
+
if _arch_lim.startswith((
|
|
1328
|
+
"Architecture claim refuted on evidence:",
|
|
1329
|
+
"Architecture reconciliation could not run:",
|
|
1330
|
+
)):
|
|
1331
|
+
limitations.append(_arch_lim)
|
|
1332
|
+
|
|
1309
1333
|
if spec.enable_dependencies:
|
|
1310
1334
|
from dataclasses import asdict
|
|
1311
1335
|
from sourcecode.dependency_analyzer import DependencyAnalyzer
|