sourcecode 2.5.16__tar.gz → 2.5.18__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. {sourcecode-2.5.16 → sourcecode-2.5.18}/CHANGELOG.md +73 -0
  2. {sourcecode-2.5.16 → sourcecode-2.5.18}/PKG-INFO +1 -1
  3. {sourcecode-2.5.16 → sourcecode-2.5.18}/pyproject.toml +1 -1
  4. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/adaptive_scanner.py +12 -36
  6. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/ast_extractor.py +0 -8
  7. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/coverage_parser.py +0 -14
  8. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/env_analyzer.py +0 -5
  9. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/repository_ir.py +0 -39
  10. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/scanner.py +55 -37
  11. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/serializer.py +0 -61
  12. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/spring_tx_analyzer.py +0 -24
  13. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/validation_surface.py +128 -0
  14. {sourcecode-2.5.16 → sourcecode-2.5.18}/.github/workflows/build-windows.yml +0 -0
  15. {sourcecode-2.5.16 → sourcecode-2.5.18}/.gitignore +0 -0
  16. {sourcecode-2.5.16 → sourcecode-2.5.18}/.ruff.toml +0 -0
  17. {sourcecode-2.5.16 → sourcecode-2.5.18}/CONTRIBUTING.md +0 -0
  18. {sourcecode-2.5.16 → sourcecode-2.5.18}/LICENSE +0 -0
  19. {sourcecode-2.5.16 → sourcecode-2.5.18}/README.md +0 -0
  20. {sourcecode-2.5.16 → sourcecode-2.5.18}/SECURITY.md +0 -0
  21. {sourcecode-2.5.16 → sourcecode-2.5.18}/raw +0 -0
  22. {sourcecode-2.5.16 → sourcecode-2.5.18}/scripts/compare_integration_engines.py +0 -0
  23. {sourcecode-2.5.16 → sourcecode-2.5.18}/scripts/customer_smoke_test.sh +0 -0
  24. {sourcecode-2.5.16 → sourcecode-2.5.18}/scripts/generate_jdk_exports.py +0 -0
  25. {sourcecode-2.5.16 → sourcecode-2.5.18}/scripts/perf_harness.py +0 -0
  26. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/archetype.py +0 -0
  27. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/architecture_analyzer.py +0 -0
  28. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/architecture_summary.py +0 -0
  29. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/cache.py +0 -0
  30. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/call_surface.py +0 -0
  31. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/caller_metrics.py +0 -0
  32. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/canonical_ir.py +0 -0
  33. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/cir_graphs.py +0 -0
  34. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/classifier.py +0 -0
  35. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/cli.py +0 -0
  36. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/code_notes_analyzer.py +0 -0
  37. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/confidence_analyzer.py +0 -0
  38. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/context_cache.py +0 -0
  39. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/context_graph.py +0 -0
  40. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/context_scorer.py +0 -0
  41. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/context_summarizer.py +0 -0
  42. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/contract_model.py +0 -0
  43. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/contract_pipeline.py +0 -0
  44. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/dependency_analyzer.py +0 -0
  45. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/__init__.py +0 -0
  46. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/base.py +0 -0
  47. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/csproj_parser.py +0 -0
  48. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/dart.py +0 -0
  49. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/dotnet.py +0 -0
  50. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/elixir.py +0 -0
  51. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/go.py +0 -0
  52. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/heuristic.py +0 -0
  53. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/hybrid.py +0 -0
  54. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/java.py +0 -0
  55. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/jvm_ext.py +0 -0
  56. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/nodejs.py +0 -0
  57. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/parsers.py +0 -0
  58. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/php.py +0 -0
  59. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/project.py +0 -0
  60. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/python.py +0 -0
  61. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/ruby.py +0 -0
  62. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/rust.py +0 -0
  63. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/systems.py +0 -0
  64. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/terraform.py +0 -0
  65. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/tooling.py +0 -0
  66. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/doc_analyzer.py +0 -0
  67. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/dynamic_argument_surface.py +0 -0
  68. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/endpoint_literals.py +0 -0
  69. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/endpoint_metrics.py +0 -0
  70. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/entrypoint_classifier.py +0 -0
  71. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/error_schema.py +0 -0
  72. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/evidence_provider.py +0 -0
  73. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/explain.py +0 -0
  74. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/file_chunker.py +0 -0
  75. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/file_classifier.py +0 -0
  76. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/format_contract.py +0 -0
  77. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/fqn_utils.py +0 -0
  78. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/git_analyzer.py +0 -0
  79. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/graph_analyzer.py +0 -0
  80. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/graph_evidence.py +0 -0
  81. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/hibernate_strat.py +0 -0
  82. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/jdk_exports.py +0 -0
  83. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/license.py +0 -0
  84. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/mcp/__init__.py +0 -0
  85. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  86. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  87. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  88. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  89. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  90. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/mcp/orchestrator.py +0 -0
  91. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/mcp/registry.py +0 -0
  92. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/mcp/runner.py +0 -0
  93. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/mcp/server.py +0 -0
  94. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/mcp_nudge.py +0 -0
  95. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/metrics_analyzer.py +0 -0
  96. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/migrate_check.py +0 -0
  97. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/openapi_surface.py +0 -0
  98. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/output_budget.py +0 -0
  99. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/path_filters.py +0 -0
  100. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/perf.py +0 -0
  101. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/pr_comment_renderer.py +0 -0
  102. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/pr_impact.py +0 -0
  103. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/prepare_context.py +0 -0
  104. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/progress.py +0 -0
  105. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/ranking_engine.py +0 -0
  106. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/reconciliation.py +0 -0
  107. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/redactor.py +0 -0
  108. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/relevance_scorer.py +0 -0
  109. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/rename_refactor.py +0 -0
  110. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/repo_classifier.py +0 -0
  111. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/__init__.py +0 -0
  112. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/context.py +0 -0
  113. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/errors.py +0 -0
  114. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/executor.py +0 -0
  115. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/planner.py +0 -0
  116. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/query.py +0 -0
  117. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/request.py +0 -0
  118. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/resolution.py +0 -0
  119. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/result.py +0 -0
  120. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/retriever.py +0 -0
  121. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/runtime.py +0 -0
  122. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/steps.py +0 -0
  123. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
  124. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/steps_graph.py +0 -0
  125. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/steps_impact.py +0 -0
  126. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/steps_intf.py +0 -0
  127. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/steps_struct.py +0 -0
  128. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/steps_txsec.py +0 -0
  129. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/ris.py +0 -0
  130. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/runtime_classifier.py +0 -0
  131. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/schema.py +0 -0
  132. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/security_config.py +0 -0
  133. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/security_posture.py +0 -0
  134. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/semantic_analyzer.py +0 -0
  135. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/semantic_impact_engine.py +0 -0
  136. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/semantic_integration_engine.py +0 -0
  137. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/semantic_services.py +0 -0
  138. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/spring_event_topology.py +0 -0
  139. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/spring_findings.py +0 -0
  140. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/spring_impact.py +0 -0
  141. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/spring_model.py +0 -0
  142. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/spring_security_audit.py +0 -0
  143. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/spring_semantic.py +0 -0
  144. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/summarizer.py +0 -0
  145. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/telemetry/__init__.py +0 -0
  146. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/telemetry/config.py +0 -0
  147. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/telemetry/consent.py +0 -0
  148. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/telemetry/events.py +0 -0
  149. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/telemetry/filters.py +0 -0
  150. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/telemetry/transport.py +0 -0
  151. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/tree_utils.py +0 -0
  152. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/type_usage_surface.py +0 -0
  153. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/validation_inference.py +0 -0
  154. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/version_check.py +0 -0
  155. {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/workspace.py +0 -0
  156. {sourcecode-2.5.16 → sourcecode-2.5.18}/supabase/functions/README.md +0 -0
  157. {sourcecode-2.5.16 → sourcecode-2.5.18}/supabase/functions/get-license/index.ts +0 -0
  158. {sourcecode-2.5.16 → sourcecode-2.5.18}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  159. {sourcecode-2.5.16 → sourcecode-2.5.18}/supabase/functions/telemetry/index.ts +0 -0
  160. {sourcecode-2.5.16 → sourcecode-2.5.18}/supabase/sql/license_event_ordering.sql +0 -0
  161. {sourcecode-2.5.16 → sourcecode-2.5.18}/supabase/sql/licensing_schema.sql +0 -0
  162. {sourcecode-2.5.16 → sourcecode-2.5.18}/supabase/sql/telemetry_events.sql +0 -0
@@ -1,5 +1,78 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.5.18] — 2026-07-20
4
+
5
+ **Engineering Decision Support — Part A, F1: standalone OpenAPI specs now reach the
6
+ validation surface.** First correctness-foundation fix of the capability milestone
7
+ (`docs/DEVELOPMENT-ROADMAP.md`): trust-critical, stability-permitted (consumer-wiring
8
+ over an existing producer, no new inference). A repo shipping a plain `openapi.yml`
9
+ with no openapi-generator interface-controller was reported "No OpenAPI spec found" —
10
+ a confirmed false negative that would launder into any downstream decision. Full suite
11
+ (3538 passed, 5 skipped) confirms confinement.
12
+
13
+ ### Fixed
14
+
15
+ - **F1 — standalone-spec discovery reaches the validation surface.** `extract_java_endpoints`
16
+ only links a spec when an `@RestController implements XxxApi` interface-controller
17
+ matches it; a repo with a plain `openapi.yml` and no such controller fell through to
18
+ "No OpenAPI spec found". The validation surface now consults the existing
19
+ `openapi_surface.build_openapi_surface` producer directly in that branch: a discovered
20
+ spec's body operations (POST/PUT/PATCH) contribute their declared constraints as
21
+ `source="openapi-spec"` routes (deduped against source-annotated routes by
22
+ method+path), the `openapi_spec` claim flips from `null` to the spec path, and the
23
+ note explains the standalone discovery. Genuinely absent specs are unchanged — the
24
+ `null` claim and source-DTO recovery path stand. New `TestStandaloneSpecRecovery`
25
+ regression class (4 tests).
26
+ - **F1 — `zero_result_reason` on genuine absence.** When no spec is on disk and no
27
+ in-repo DTO constraints are recoverable, the validation surface now carries a
28
+ machine-readable `zero_result_reason = "no_openapi_spec_and_no_source_dto_constraints"`
29
+ beside the prose note, so a consumer can distinguish an attested negative ("looked,
30
+ found nothing") from an un-run analysis. Absent on any positive result.
31
+
32
+ ### Notes
33
+
34
+ - **Reconciliation rule deliberately NOT added for F1.** The roadmap's F1 strategy
35
+ suggested a "spec-present vs spec-absent" reconciliation rule as the honesty guard.
36
+ The flip was instead achieved at the true source (the validation surface now reads
37
+ the spec), and the two sibling surfaces that also report spec presence
38
+ (`serializer` arch_reasons, `confidence_analyzer` api_contract gap) were verified to
39
+ already detect a standalone `openapi.yml` via their filename check — so **no surface
40
+ makes the false "no spec" claim**, leaving the reconciliation layer no provable
41
+ contradiction to fire on. Adding a rule that never fires on any real repo violates
42
+ the layer's own contract (fire only on contradiction, EPV). Recorded here so the
43
+ roadmap item is closed by decision, not oversight.
44
+
45
+ ## [2.5.17] — 2026-07-19
46
+
47
+ **Semantic Stability cleanup: dead code removed and a scanner fork collapsed, with
48
+ zero behavioural change.** Per `docs/DEVELOPMENT-ROADMAP.md` the 2.5.x line is scoped
49
+ to documentation, cleanup, small bugs and profiling — no new inference. This release
50
+ is pure internal hygiene; every user-facing surface is byte-for-byte unchanged and the
51
+ full suite (3534 passed, 5 skipped) confirms it.
52
+
53
+ ### Removed
54
+
55
+ - **8 dead module-level private helpers** (−151 lines), each verified to have zero
56
+ references across `src` + `tests` (call sites, string names, and dynamic lookups) and
57
+ confirmed long-orphaned (all last touched May 2026, superseded since):
58
+ `repository_ir._resolve_type`, `repository_ir._common_package_prefix`,
59
+ `ast_extractor._find_all`, `ast_extractor._named_children`,
60
+ `coverage_parser._decode_numbits`, `spring_tx_analyzer._extract_method_body`,
61
+ `serializer._tiered_display_score`, `env_analyzer._replace_required`.
62
+ Kept `perf._reset_for_tests` and `migrate_check._find_xml_config_files` — both live
63
+ via tests.
64
+
65
+ ### Changed
66
+
67
+ - **Collapsed the `FileScanner` / `AdaptiveScanner` fork duplication.** AdaptiveScanner
68
+ was forked from FileScanner and carried three byte-identical method bodies
69
+ (`find_manifests`, `_get_or_create_node`, `_is_excluded_by_gitignore`) — one of which
70
+ documented "Identical logic to FileScanner.find_manifests()". The shared logic now
71
+ lives as three module-level free functions in `scanner.py`
72
+ (`find_manifest_paths` / `get_or_create_tree_node` / `match_gitignore`) and both
73
+ classes delegate. No new coupling (AdaptiveScanner already imported from `scanner`),
74
+ no circular import; dropped its now-unused `cast` / `MANIFEST_NAMES` imports.
75
+
3
76
  ## [2.5.16] — 2026-07-19
4
77
 
5
78
  **Performance campaign close-out: one real algorithmic win, cost-per-symbol
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 2.5.16
3
+ Version: 2.5.18
4
4
  Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
5
5
  License-File: LICENSE
6
6
  Keywords: agents,ai,codebase,context,developer-tools,llm
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "2.5.16"
7
+ version = "2.5.18"
8
8
  description = "Persistent structural context and ultra-fast repeated analysis for AI coding agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -4,4 +4,4 @@ ASK Engine is the product. ``ask`` is the canonical CLI command; ``sourcecode``
4
4
  the legacy compatibility alias and the Python/PyPI package name. See
5
5
  docs/PRODUCT_IDENTITY.md (normative)."""
6
6
 
7
- __version__ = "2.5.16"
7
+ __version__ = "2.5.18"
@@ -14,12 +14,18 @@ interface, same output format (None = file, dict = directory).
14
14
 
15
15
  import os
16
16
  from pathlib import Path
17
- from typing import Any, Optional, cast
17
+ from typing import Any, Optional
18
18
 
19
19
  from pathspec import GitIgnoreSpec
20
20
 
21
21
  from sourcecode.repo_classifier import RepoTopology
22
- from sourcecode.scanner import DEFAULT_EXCLUDES, EXCLUDED_FILE_SUFFIXES, MANIFEST_NAMES
22
+ from sourcecode.scanner import (
23
+ DEFAULT_EXCLUDES,
24
+ EXCLUDED_FILE_SUFFIXES,
25
+ find_manifest_paths,
26
+ get_or_create_tree_node,
27
+ match_gitignore,
28
+ )
23
29
 
24
30
 
25
31
  class AdaptiveScanner:
@@ -100,9 +106,7 @@ class AdaptiveScanner:
100
106
  return self._gitignore_spec
101
107
 
102
108
  def _is_excluded_by_gitignore(self, rel_path: str, is_dir: bool) -> bool:
103
- spec = self._load_gitignore_spec()
104
- path_to_match = rel_path + "/" if is_dir else rel_path
105
- return spec.match_file(path_to_match)
109
+ return match_gitignore(self._load_gitignore_spec(), rel_path, is_dir)
106
110
 
107
111
  # ------------------------------------------------------------------
108
112
  # Depth budget computation — the core of adaptive traversal
@@ -224,40 +228,12 @@ class AdaptiveScanner:
224
228
  def _get_or_create_node(
225
229
  self, tree: dict[str, Any], parts: tuple[str, ...]
226
230
  ) -> dict[str, Any]:
227
- node = tree
228
- for part in parts:
229
- if part not in node or node[part] is None:
230
- node[part] = {}
231
- node = cast(dict[str, Any], node[part])
232
- return node
231
+ return get_or_create_tree_node(tree, parts)
233
232
 
234
233
  # ------------------------------------------------------------------
235
234
  # Manifest discovery — same interface as FileScanner
236
235
  # ------------------------------------------------------------------
237
236
 
238
237
  def find_manifests(self) -> list[str]:
239
- """Find manifest files at depth 0-1.
240
-
241
- Identical logic to FileScanner.find_manifests() — depth-0 root
242
- manifests plus depth-1 sub-package manifests, hidden dirs excluded.
243
- """
244
- manifests: list[str] = []
245
- for name in MANIFEST_NAMES:
246
- candidate = self.root / name
247
- if candidate.exists() and not candidate.is_symlink():
248
- manifests.append(str(candidate))
249
- try:
250
- for child in self.root.iterdir():
251
- if (
252
- child.is_dir()
253
- and not child.is_symlink()
254
- and child.name not in self._excludes
255
- and not child.name.startswith(".")
256
- ):
257
- for name in MANIFEST_NAMES:
258
- candidate = child / name
259
- if candidate.exists() and not candidate.is_symlink():
260
- manifests.append(str(candidate))
261
- except PermissionError:
262
- pass
263
- return manifests
238
+ """Find manifest files at depth 0-1 (delegates to the shared helper)."""
239
+ return find_manifest_paths(self.root, self._excludes)
@@ -176,18 +176,10 @@ def _find_child(node: Any, *types: str) -> Optional[Any]:
176
176
  return None
177
177
 
178
178
 
179
- def _find_all(node: Any, *types: str) -> list[Any]:
180
- return [child for child in node.children if child.type in types]
181
-
182
-
183
179
  def _text(node: Any, src: bytes) -> str:
184
180
  return src[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
185
181
 
186
182
 
187
- def _named_children(node: Any) -> list[Any]:
188
- return [c for c in node.children if c.is_named]
189
-
190
-
191
183
  # ---------------------------------------------------------------------------
192
184
  # Tree-sitter TS/JS extraction
193
185
  # ---------------------------------------------------------------------------
@@ -39,20 +39,6 @@ def _safe_int(value: Optional[str]) -> Optional[int]:
39
39
  return None
40
40
 
41
41
 
42
- def _decode_numbits(blob: bytes) -> list[int]:
43
- """Decodifica bitset little-endian de coverage.py .coverage SQLite.
44
-
45
- Byte i, bit j set => linea (i*8 + j + 1) ejecutada.
46
- Ejemplo: bytes([0b00000101]) => [1, 3]
47
- """
48
- return [
49
- i * 8 + j + 1
50
- for i, byte in enumerate(blob)
51
- for j in range(8)
52
- if byte & (1 << j)
53
- ]
54
-
55
-
56
42
  # ---------------------------------------------------------------------------
57
43
  # CoverageParser
58
44
  # ---------------------------------------------------------------------------
@@ -534,8 +534,3 @@ class EnvAnalyzer:
534
534
  def _replace_description(record, description: str):
535
535
  from dataclasses import replace
536
536
  return replace(record, description=description)
537
-
538
-
539
- def _replace_required(record, required: bool, default: Optional[str]):
540
- from dataclasses import replace
541
- return replace(record, required=required, default=default if not record.default else record.default)
@@ -807,11 +807,6 @@ def _pop_closed(class_stack: list[tuple[str, int]], depth: int) -> None:
807
807
  class_stack.pop()
808
808
 
809
809
 
810
- def _resolve_type(simple: str, import_map: dict[str, str]) -> Optional[str]:
811
- base = re.sub(r'<.*', '', simple).strip().split('.')[-1]
812
- return import_map.get(base)
813
-
814
-
815
810
  def _resolve_types_from_text(text: str, import_map: dict[str, str]) -> list[str]:
816
811
  resolved = []
817
812
  for token in re.findall(r'\b([A-Z]\w*)\b', text):
@@ -3597,40 +3592,6 @@ def _build_evidence_bundles(
3597
3592
  return bundles
3598
3593
 
3599
3594
 
3600
- def _common_package_prefix(fqns: list[str]) -> str:
3601
- """Longest common dot-separated package prefix across a list of FQNs."""
3602
- if not fqns:
3603
- return ""
3604
- # Strip class/method suffix — keep only package parts (lowercase segments)
3605
- def pkg_parts(fqn: str) -> list[str]:
3606
- parts = fqn.split(".")
3607
- # Drop trailing class/method names (PascalCase or after '#')
3608
- result = []
3609
- for p in parts:
3610
- if "#" in p:
3611
- break
3612
- if p and p[0].isupper():
3613
- break
3614
- result.append(p)
3615
- return result
3616
-
3617
- segs = [pkg_parts(f) for f in fqns if pkg_parts(f)]
3618
- if not segs:
3619
- return fqns[0].rsplit(".", 1)[0] if "." in fqns[0] else fqns[0]
3620
- common = segs[0]
3621
- for s in segs[1:]:
3622
- new_common = []
3623
- for a, b in zip(common, s):
3624
- if a == b:
3625
- new_common.append(a)
3626
- else:
3627
- break
3628
- common = new_common
3629
- if not common:
3630
- break
3631
- return ".".join(common) if common else fqns[0].rsplit(".", 1)[0]
3632
-
3633
-
3634
3595
  def _subsystem_label(package_prefix: str) -> str:
3635
3596
  """Derive short human label enforcing minimum meaningful depth.
3636
3597
 
@@ -115,6 +115,57 @@ def classify_manifest(manifest_path: str, root: Path) -> str:
115
115
  return "auxiliary"
116
116
 
117
117
 
118
+ # ── Shared scan primitives (used by both FileScanner and AdaptiveScanner) ────
119
+ # AdaptiveScanner was forked from FileScanner and carried byte-identical copies of
120
+ # these three; they live here as the single source of truth, each class method a
121
+ # thin delegate.
122
+
123
+ def find_manifest_paths(root: Path, excludes: frozenset[str]) -> list[str]:
124
+ """Manifest files at depth 0–1 (SCAN-04): root manifests plus depth-1
125
+ sub-package manifests, with hidden/excluded dirs and symlinks skipped."""
126
+ manifests: list[str] = []
127
+ # Depth 0: root
128
+ for name in MANIFEST_NAMES:
129
+ candidate = root / name
130
+ if candidate.exists() and not candidate.is_symlink():
131
+ manifests.append(str(candidate))
132
+ # Depth 1: first level (hidden dirs excluded — tooling, not project)
133
+ try:
134
+ for child in root.iterdir():
135
+ if (
136
+ child.is_dir()
137
+ and not child.is_symlink()
138
+ and child.name not in excludes
139
+ and not child.name.startswith(".")
140
+ ):
141
+ for name in MANIFEST_NAMES:
142
+ candidate = child / name
143
+ if candidate.exists() and not candidate.is_symlink():
144
+ manifests.append(str(candidate))
145
+ except PermissionError:
146
+ pass
147
+ return manifests
148
+
149
+
150
+ def get_or_create_tree_node(
151
+ tree: dict[str, Any], parts: tuple[str, ...]
152
+ ) -> dict[str, Any]:
153
+ """Navigate/create the nested-dict tree node for a path's parts."""
154
+ node = tree
155
+ for part in parts:
156
+ if part not in node or node[part] is None:
157
+ node[part] = {}
158
+ node = cast(dict[str, Any], node[part])
159
+ return node
160
+
161
+
162
+ def match_gitignore(spec: GitIgnoreSpec, rel_path: str, is_dir: bool) -> bool:
163
+ """True if a root-relative path is excluded by ``spec``. GitIgnoreSpec expects
164
+ a trailing '/' for directories."""
165
+ path_to_match = rel_path + "/" if is_dir else rel_path
166
+ return spec.match_file(path_to_match)
167
+
168
+
118
169
  class FileScanner:
119
170
  """Escanea un directorio de proyecto y produce un arbol de ficheros filtrado.
120
171
 
@@ -148,10 +199,7 @@ class FileScanner:
148
199
 
149
200
  def _is_excluded_by_gitignore(self, rel_path: str, is_dir: bool) -> bool:
150
201
  """Comprueba si una ruta relativa (a self.root) esta excluida por .gitignore."""
151
- spec = self._load_gitignore_spec()
152
- # GitIgnoreSpec espera rutas con / al final para directorios
153
- path_to_match = rel_path + "/" if is_dir else rel_path
154
- return spec.match_file(path_to_match)
202
+ return match_gitignore(self._load_gitignore_spec(), rel_path, is_dir)
155
203
 
156
204
  def scan_tree(self) -> dict[str, Any]:
157
205
  """Construye el arbol JSON anidado del proyecto.
@@ -223,38 +271,8 @@ class FileScanner:
223
271
  self, tree: dict[str, Any], parts: tuple[str, ...]
224
272
  ) -> dict[str, Any]:
225
273
  """Navega/crea el nodo del arbol para la ruta indicada."""
226
- node = tree
227
- for part in parts:
228
- if part not in node or node[part] is None:
229
- node[part] = {}
230
- node = cast(dict[str, Any], node[part])
231
- return node
274
+ return get_or_create_tree_node(tree, parts)
232
275
 
233
276
  def find_manifests(self) -> list[str]:
234
- """Encuentra ficheros de manifiesto en profundidad 0-1 (SCAN-04).
235
-
236
- Retorna:
237
- Lista de paths absolutos de manifiestos encontrados.
238
- """
239
- manifests: list[str] = []
240
- # Profundidad 0: raiz
241
- for name in MANIFEST_NAMES:
242
- candidate = self.root / name
243
- if candidate.exists() and not candidate.is_symlink():
244
- manifests.append(str(candidate))
245
- # Profundidad 1: primer nivel (excluir directorios ocultos — son tooling, no proyecto)
246
- try:
247
- for child in self.root.iterdir():
248
- if (
249
- child.is_dir()
250
- and not child.is_symlink()
251
- and child.name not in self._excludes
252
- and not child.name.startswith(".")
253
- ):
254
- for name in MANIFEST_NAMES:
255
- candidate = child / name
256
- if candidate.exists() and not candidate.is_symlink():
257
- manifests.append(str(candidate))
258
- except PermissionError:
259
- pass
260
- return manifests
277
+ """Encuentra ficheros de manifiesto en profundidad 0-1 (SCAN-04)."""
278
+ return find_manifest_paths(self.root, self._excludes)
@@ -913,67 +913,6 @@ def _jndi_datasources(sm: "SourceMap") -> "Optional[list[dict[str, Any]]]":
913
913
  return datasources
914
914
 
915
915
 
916
- def _tiered_display_score(
917
- pre_bonus_combined: float,
918
- file_class: Any,
919
- path: str,
920
- entry_paths: set,
921
- has_structural_signals: bool = False,
922
- ) -> float:
923
- """Evidence-tiered display score [0.0, 1.0].
924
-
925
- Tiers enforce: strong evidence > medium evidence > filesystem/path only.
926
- M3 sort bonuses must NOT be included in pre_bonus_combined — they are for
927
- ordering only and must not inflate the displayed score.
928
-
929
- Tier ceilings:
930
- T1 confirmed production entrypoint 0.92–1.00
931
- T2 entrypoint (weaker category) 0.80–0.91
932
- T3 annotation-confirmed stereotype 0.40–0.90 (table-calibrated)
933
- T4 framework import evidence 0.55–0.79
934
- T5 code definitions + imports 0.38–0.54
935
- T6 build manifest / tooling / test 0.25–0.45
936
- T7 path/filesystem signal only 0.10–0.39
937
- """
938
- from sourcecode.file_classifier import JAVA_STEREOTYPE_CATEGORIES
939
-
940
- cat = file_class.category if file_class else None
941
- base_rel = file_class.relevance if file_class else 0.0
942
-
943
- # T1: confirmed production entrypoint
944
- if path in entry_paths and cat in ("runtime_core", "cli_entrypoint"):
945
- return round(min(1.0, max(0.92, base_rel)), 3)
946
-
947
- # T2: in entry_paths but weaker evidence category
948
- if path in entry_paths:
949
- return round(min(0.91, max(0.80, pre_bonus_combined / 2.0)), 3)
950
-
951
- # T3: annotation-confirmed stereotype — table values are already calibrated
952
- if file_class and cat in JAVA_STEREOTYPE_CATEGORIES:
953
- return round(base_rel, 3)
954
-
955
- # T4: framework import evidence (medium strength)
956
- if cat in ("api_layer", "database_layer", "infrastructure"):
957
- return round(min(0.79, max(0.55, pre_bonus_combined / 2.0)), 3)
958
-
959
- # T5: code definitions with imports (medium-low)
960
- if cat in ("application_logic", "domain_model"):
961
- return round(min(0.54, max(0.38, pre_bonus_combined / 2.0)), 3)
962
-
963
- # T6: build manifest / tooling / test
964
- if cat == "build_system":
965
- return round(min(0.45, base_rel), 3)
966
- if cat in ("tests", "tooling"):
967
- return round(min(0.35, base_rel), 3)
968
-
969
- # T7: no content classification — filesystem/structural signals only
970
- # has_structural_signals: fan_in, churn, export — allows up to 0.54
971
- # pure path/filename only — hard cap 0.39
972
- if has_structural_signals:
973
- return round(min(0.54, max(0.10, pre_bonus_combined / 2.0)), 3)
974
- return round(min(0.39, max(0.10, pre_bonus_combined / 2.0)), 3)
975
-
976
-
977
916
  def _build_file_signals(
978
917
  file_class: Any,
979
918
  path: str,
@@ -112,30 +112,6 @@ def _caught_types(catch_header_inner: str) -> list[str]:
112
112
  return types
113
113
 
114
114
 
115
- def _extract_method_body(source: str, method_name: str) -> str:
116
- """Extract the first method body matching method_name using brace counting.
117
-
118
- Returns the text from '{' to the matching '}', or empty string if not found.
119
- Needed to scope TX-005 regex to the specific method instead of the whole file.
120
- """
121
- pattern = re.compile(r'\b' + re.escape(method_name) + r'\s*\(')
122
- for m in pattern.finditer(source):
123
- brace_pos = source.find('{', m.end())
124
- if brace_pos < 0:
125
- continue
126
- depth = 1
127
- i = brace_pos + 1
128
- while i < len(source) and depth > 0:
129
- c = source[i]
130
- if c == '{':
131
- depth += 1
132
- elif c == '}':
133
- depth -= 1
134
- i += 1
135
- return source[brace_pos:i]
136
- return ""
137
-
138
-
139
115
  # ---------------------------------------------------------------------------
140
116
  # Pattern protocol
141
117
  # ---------------------------------------------------------------------------
@@ -363,6 +363,98 @@ def _recover_source_endpoints(
363
363
  return routes, total
364
364
 
365
365
 
366
+ def _recover_spec_endpoints(
367
+ root: Path,
368
+ existing_keys: "set[tuple[Any, Any]]",
369
+ catalog: "dict[str, Any]",
370
+ custom_used: "set[str]",
371
+ ) -> "tuple[Optional[str], list[dict[str, Any]], int]":
372
+ """Recover validation routes from a standalone OpenAPI spec on disk.
373
+
374
+ F1: ``extract_java_endpoints`` only links a spec when an openapi-generator
375
+ interface-controller (``@RestController implements XxxApi``) matches it. A
376
+ repo that ships a plain ``openapi.yml`` with no such controller therefore
377
+ reported "No OpenAPI spec found" — a confirmed false negative. This consults
378
+ the existing :func:`build_openapi_surface` producer directly so a discovered
379
+ spec's body operations contribute their declared constraints and flip the
380
+ "no spec" claim.
381
+
382
+ Returns ``(spec_path, routes, validated_field_count)``; ``spec_path`` is None
383
+ when no parseable spec with operations is present.
384
+ """
385
+ from sourcecode.openapi_surface import build_openapi_surface
386
+
387
+ surface = build_openapi_surface(root)
388
+ if surface is None or not surface.operations:
389
+ return None, [], 0
390
+
391
+ routes: "list[dict[str, Any]]" = []
392
+ total = 0
393
+ for op in surface.operations:
394
+ if str(op.method).upper() not in ("POST", "PUT", "PATCH"):
395
+ continue
396
+ key = (op.method, op.path)
397
+ if key in existing_keys:
398
+ continue # already surfaced from source — dedupe by method+path
399
+ schema = surface.schemas.get(op.request_body_schema) if op.request_body_schema else None
400
+ constraints = [f.to_dict() for f in schema.fields] if schema is not None else []
401
+ validated = _validated_fields_from_constraints(constraints, catalog, custom_used)
402
+ total += len(validated)
403
+ route: "dict[str, Any]" = {
404
+ "method": op.method,
405
+ "path": op.path,
406
+ "controller": None,
407
+ "handler": op.operation_id or "(operation)",
408
+ "schema": op.request_body_schema,
409
+ "source": "openapi-spec",
410
+ "confidence": "high",
411
+ "validatedFields": validated,
412
+ }
413
+ routes.append(route)
414
+ existing_keys.add(key)
415
+ return surface.spec_path, routes, total
416
+
417
+
418
+ def _validated_fields_from_constraints(
419
+ constraints: "list[Any]",
420
+ catalog: "dict[str, Any]",
421
+ custom_used: "set[str]",
422
+ ) -> "list[dict[str, Any]]":
423
+ """Render a schema's constraint dicts into ``validatedFields`` entries.
424
+
425
+ Mirrors the built-in-rule + custom-validator linkage the main loop applies to
426
+ OpenAPI request bodies; shared so the standalone-spec path (F1) stays byte-
427
+ consistent with the interface-controller path."""
428
+ out: "list[dict[str, Any]]" = []
429
+ for fc in constraints:
430
+ if not isinstance(fc, dict):
431
+ continue
432
+ rules = _field_rules(fc)
433
+ customs: "list[dict[str, Any]]" = []
434
+ for ann in fc.get("extraAnnotations", []) or []:
435
+ cc = catalog.get(ann)
436
+ entry: "dict[str, Any]" = {"annotation": ann}
437
+ if cc is not None:
438
+ custom_used.add(ann)
439
+ if cc.validators:
440
+ entry["validators"] = cc.validators
441
+ if cc.message is not None:
442
+ entry["message"] = cc.message
443
+ entry["resolved"] = True
444
+ else:
445
+ entry["resolved"] = False
446
+ customs.append(entry)
447
+ if not rules and not customs:
448
+ continue
449
+ field_entry: "dict[str, Any]" = {"name": fc.get("name")}
450
+ if rules:
451
+ field_entry["rules"] = rules
452
+ if customs:
453
+ field_entry["customValidators"] = customs
454
+ out.append(field_entry)
455
+ return out
456
+
457
+
366
458
  def _field_rules(fieldc: "dict[str, Any]") -> "list[dict[str, Any]]":
367
459
  """Render a constraint dict's built-in rules as a list of {kind, value}."""
368
460
  rules: "list[dict[str, Any]]" = []
@@ -491,6 +583,36 @@ def build_validation_surface(
491
583
  if spec_path:
492
584
  result["openapi_spec"] = spec_path
493
585
  else:
586
+ # No spec linked by extract_java_endpoints (no openapi-generator
587
+ # interface-controller matched). F1: consult the standalone-spec
588
+ # producer directly — a plain openapi.yml shipped in the repo is a real
589
+ # spec, so its body operations contribute constraints and flip the
590
+ # "no spec found" claim instead of reporting a false negative.
591
+ existing_keys = {(r.get("method"), r.get("path")) for r in out_endpoints}
592
+ spec_path2, spec_routes, spec_fields = _recover_spec_endpoints(
593
+ root, existing_keys, catalog, custom_used
594
+ )
595
+ if spec_path2 is not None:
596
+ result["openapi_spec"] = spec_path2
597
+ out_endpoints.extend(spec_routes)
598
+ total_validated_fields += spec_fields
599
+ spec_keys = {(r.get("method"), r.get("path")) for r in spec_routes}
600
+ gaps = [g for g in gaps if (g.get("method"), g.get("path")) not in spec_keys]
601
+ result["endpoints"] = out_endpoints
602
+ result["gaps"] = gaps
603
+ result["custom_validators"] = [catalog[k].to_dict() for k in sorted(catalog)]
604
+ result["summary"]["endpoints_with_body"] = len(out_endpoints)
605
+ result["summary"]["validated_fields"] = total_validated_fields
606
+ result["summary"]["custom_validators_linked"] = len(custom_used)
607
+ result["summary"]["gaps"] = len(gaps)
608
+ result["summary"]["spec_derived_routes"] = len(spec_routes)
609
+ result["note"] = (
610
+ "OpenAPI spec discovered on disk (standalone, not linked to a "
611
+ "generated interface-controller); body operations contributed "
612
+ "their declared constraints. Response shapes and non-body "
613
+ "operations are not modeled here."
614
+ )
615
+ return result
494
616
  # No OpenAPI spec on disk / under target/generated-sources. Recover
495
617
  # declarative constraints from the graph's DTO field nodes (bean-
496
618
  # validation annotations on @Valid/@Validated handler bodies), so a
@@ -525,6 +647,12 @@ def build_validation_surface(
525
647
  "partial; OpenAPI-carried constraints would be more complete."
526
648
  )
527
649
  else:
650
+ # Genuine absence — no spec on disk AND no in-repo DTO constraints.
651
+ # Emit a machine-readable zero_result_reason (F1) so a consumer can
652
+ # tell "we looked and found nothing" from "we did not look": an empty
653
+ # surface here is an attested negative, not a missing-validation
654
+ # finding.
655
+ result["zero_result_reason"] = "no_openapi_spec_and_no_source_dto_constraints"
528
656
  result["note"] = (
529
657
  "No OpenAPI spec found and no source DTO constraints recovered "
530
658
  "(no handler validates an in-repo DTO via @Valid/@Validated, or "
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes