sourcecode 2.6.0__tar.gz → 2.6.1__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.6.0 → sourcecode-2.6.1}/CHANGELOG.md +35 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/PKG-INFO +1 -1
- {sourcecode-2.6.0 → sourcecode-2.6.1}/pyproject.toml +1 -1
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/__init__.py +1 -1
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/repository_ir.py +32 -5
- sourcecode-2.6.1/src/sourcecode/retrieval/composition.py +496 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/errors.py +7 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/steps_endpoint.py +7 -1
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/steps_impact.py +7 -1
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/spring_impact.py +16 -8
- {sourcecode-2.6.0 → sourcecode-2.6.1}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/.gitignore +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/.ruff.toml +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/CONTRIBUTING.md +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/LICENSE +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/README.md +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/SECURITY.md +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/raw +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/scripts/compare_integration_engines.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/scripts/customer_smoke_test.sh +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/scripts/generate_jdk_exports.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/scripts/perf_harness.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/archetype.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/architectural_baseline.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/architectural_delta.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/cache.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/call_surface.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/caller_metrics.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/change_plan.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/classifier.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/cli.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/compare.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/constraint_diff.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/context_cache.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/context_graph.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/contract_diff.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/dynamic_argument_surface.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/endpoint_literals.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/endpoint_metrics.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/evidence_provider.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/explain.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/graph_evidence.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/hibernate_strat.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/jdk_exports.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/license.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/migrate_check.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/perf.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/progress.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/reconciliation.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/redactor.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/__init__.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/context.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/executor.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/planner.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/query.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/request.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/resolution.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/result.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/retriever.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/runtime.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/steps.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/steps_graph.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/steps_intf.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/steps_struct.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/steps_txsec.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/ris.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/scanner.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/schema.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/security_config.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/security_posture.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/semantic_impact_engine.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/semantic_integration_engine.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/semantic_services.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/serializer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/type_usage_surface.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/validation_inference.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/validation_surface.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/version_check.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/workspace.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/supabase/functions/README.md +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/supabase/sql/licensing_schema.sql +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.1}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.6.1] — 2026-07-22
|
|
4
|
+
|
|
5
|
+
**Stability patch — extraction/label fidelity on the 2.6.x line. No new
|
|
6
|
+
capability, no functional-contract change.** Two isolated corrections where the
|
|
7
|
+
system produced a correct result but described it imperfectly (P1-H) or gave a
|
|
8
|
+
provably-correct answer a misleading label (SIM-4). `2.6.0` remains the stable
|
|
9
|
+
baseline; this is a fidelity patch on top of it. Full suite: **3637 passed, 5
|
|
10
|
+
skipped, 1 deselected** (the deselected chunker test is a pre-existing external
|
|
11
|
+
fixture flake).
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **P1-H — lowercase-initial type declarations were silently dropped.**
|
|
16
|
+
`_CLASS_DECL_RE` assumed the PascalCase convention (`[A-Z]\w*`) for the
|
|
17
|
+
type-name start, so valid lowercase-initial Java types never entered the
|
|
18
|
+
symbol graph (e.g. `iFieldMetadata`, `i18nUpdateCartServiceExtensionHandler`;
|
|
19
|
+
JNA C-struct mirrors `passwd`/`group`/`pam_*`). The same assumption was
|
|
20
|
+
duplicated in the multi-line-join gate, so a lowercase declaration whose brace
|
|
21
|
+
sat on a continuation line stayed lost even after widening the primary regex.
|
|
22
|
+
Both name-start charsets widened to `[A-Za-z]\w*`, and a length-preserving
|
|
23
|
+
string-literal blanking guard was added at the declaration-scan sites so a
|
|
24
|
+
`class`/`interface` keyword appearing inside a string literal (e.g. a log
|
|
25
|
+
message `"... advice class for {} ..."`) can no longer mint a phantom type.
|
|
26
|
+
Fleet A/B over six real repositories: +9 real types recovered, zero phantoms,
|
|
27
|
+
zero symbols lost, control repositories byte-stable.
|
|
28
|
+
- **SIM-4 — a method-precise resolution was labelled `class_expanded`.** When a
|
|
29
|
+
`Class#method` query matched one class by short/suffix name and its method
|
|
30
|
+
resolved to exactly one method node, `impact-chain` returned the correct
|
|
31
|
+
method-scoped result but tagged it `class_expanded` — signalling "I broadened
|
|
32
|
+
your query to the whole class" over a narrow, correct answer. A distinct
|
|
33
|
+
resolution value `method_resolved` now describes that case (confidence stays
|
|
34
|
+
high; nothing was widened); the genuine no-method class-expansion case keeps
|
|
35
|
+
`class_expanded`. No behavioural change — the resolved symbol set, confidence,
|
|
36
|
+
and callers are identical; only the exposed label changed to match reality.
|
|
37
|
+
|
|
3
38
|
## [2.6.0] — 2026-07-22
|
|
4
39
|
|
|
5
40
|
**Engineering Decision Support — Part B (capability milestone) lands, plus the
|
|
@@ -134,7 +134,13 @@ _ANN_WITH_ARGS_RE = re.compile(
|
|
|
134
134
|
_CLASS_DECL_RE = re.compile(
|
|
135
135
|
r'(?:^|(?<=\s))'
|
|
136
136
|
r'(?P<kind>class|interface|enum|record|@interface)\s+'
|
|
137
|
-
|
|
137
|
+
# P1-H: Java identifiers may start lowercase — the uppercase-only start
|
|
138
|
+
# (PascalCase convention) silently dropped valid lowercase-initial types
|
|
139
|
+
# (Broadleaf `iFieldMetadata`, `i18nUpdateCartServiceExtensionHandler`).
|
|
140
|
+
# A literal type keyword + terminal `{` already gates the match, so widening
|
|
141
|
+
# the name-start does not admit prose/string phantoms the uppercase form
|
|
142
|
+
# was not already exposed to (verified fleet A/B, no new symbols on controls).
|
|
143
|
+
r'(?P<name>[A-Za-z]\w*)'
|
|
138
144
|
r'(?:\s*<[^{;]*?(?=>|\{))?'
|
|
139
145
|
# P1-F: record component list `record Point(int x, int y)`. `[^{;]*` is greedy
|
|
140
146
|
# to the LAST ')' before the body brace, so annotated components with
|
|
@@ -663,7 +669,22 @@ _ANN_NAME_RE = re.compile(r'^@\w+\s*')
|
|
|
663
669
|
_STRING_LITERAL_RE = re.compile(r'"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'')
|
|
664
670
|
|
|
665
671
|
# Module-level cache for class-keyword detection (avoids recompilation per _extract_symbols call)
|
|
666
|
-
|
|
672
|
+
# P1-H: lowercase-initial start mirrors _CLASS_DECL_RE so multi-line lowercase-named
|
|
673
|
+
# declarations (`class i18nHandler extends A\n implements B {`) are joined too.
|
|
674
|
+
_CLASS_KW_RE = re.compile(r'\b(?:class|interface|enum|record)\s+[A-Za-z]')
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def _blank_string_literals(text: str) -> str:
|
|
678
|
+
"""Return `text` with each string/char literal replaced by equal-length spaces.
|
|
679
|
+
|
|
680
|
+
P1-H: type-declaration scans (_CLASS_DECL_RE / _CLASS_KW_RE) run on lines whose
|
|
681
|
+
comments are stripped but whose STRING content survives. A log/message literal
|
|
682
|
+
such as `log.debug("... advice class for {} ...")` then matches `class for {`
|
|
683
|
+
and mints a phantom type `for`. Blanking the literal content (length-preserving,
|
|
684
|
+
so match offsets stay aligned with the original line) removes the false surface
|
|
685
|
+
without touching genuine declarations, which never live inside a string.
|
|
686
|
+
"""
|
|
687
|
+
return _STRING_LITERAL_RE.sub(lambda m: ' ' * len(m.group(0)), text)
|
|
667
688
|
|
|
668
689
|
|
|
669
690
|
# ---------------------------------------------------------------------------
|
|
@@ -958,7 +979,8 @@ def _extract_symbols(
|
|
|
958
979
|
_start = _i # 0-based source index where this joined entry begins
|
|
959
980
|
_line = _raw_lines[_i]
|
|
960
981
|
_stripped = _line.strip()
|
|
961
|
-
|
|
982
|
+
_kw_scan = _blank_string_literals(_stripped)
|
|
983
|
+
if (_CLASS_KW_RE.search(_kw_scan) and '{' not in _kw_scan
|
|
962
984
|
and not _stripped.startswith('//')
|
|
963
985
|
and not _stripped.startswith('*')):
|
|
964
986
|
# Continuation: join until we hit a line containing '{'
|
|
@@ -1043,7 +1065,10 @@ def _extract_symbols(
|
|
|
1043
1065
|
_pop_closed(class_stack, depth)
|
|
1044
1066
|
continue
|
|
1045
1067
|
|
|
1046
|
-
|
|
1068
|
+
# P1-H: search a string-blanked view so `class`/`interface` keywords that
|
|
1069
|
+
# appear inside string literals (log messages, templates) do not mint a
|
|
1070
|
+
# phantom type. Length-preserving, so cls_m offsets align with `stripped`.
|
|
1071
|
+
cls_m = _CLASS_DECL_RE.search(_blank_string_literals(stripped))
|
|
1047
1072
|
if cls_m:
|
|
1048
1073
|
kind_kw = cls_m.group("kind")
|
|
1049
1074
|
name = cls_m.group("name")
|
|
@@ -2311,7 +2336,9 @@ def _extract_field_types(
|
|
|
2311
2336
|
package = stripped[8:].rstrip(";").strip()
|
|
2312
2337
|
continue
|
|
2313
2338
|
net = _count_net_braces(stripped)
|
|
2314
|
-
|
|
2339
|
+
# P1-H: string-blanked search (see _blank_string_literals) — a keyword inside
|
|
2340
|
+
# a literal must not push a phantom nesting frame onto class_stack.
|
|
2341
|
+
cls_m = _CLASS_DECL_RE.search(_blank_string_literals(stripped))
|
|
2315
2342
|
if cls_m:
|
|
2316
2343
|
name = cls_m.group("name")
|
|
2317
2344
|
fqn = f"{class_stack[-1][0]}.{name}" if class_stack else (
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
"""Composition Semantics — Layer 1 foundations (ADR-0007).
|
|
2
|
+
|
|
3
|
+
This module is the single source of truth for the **identity namespace** of every
|
|
4
|
+
entity kind the retrieval step library emits. It realizes two ADR-0007 invariants
|
|
5
|
+
as executable contract:
|
|
6
|
+
|
|
7
|
+
- **INV-C10 (node-vs-facet).** A kind is a *base namespace* iff it carries an
|
|
8
|
+
independent identity; a kind whose identity is another entity's identity is a
|
|
9
|
+
*facet* of that entity. The three base namespaces are ``symbol`` (the join hub),
|
|
10
|
+
``endpoint`` and ``module``. Facet kinds (a tx-role, a public contract, a
|
|
11
|
+
validation point, a security finding) live in the base namespace of the identity
|
|
12
|
+
they are keyed by — e.g. ``transaction_boundary`` has ``identity = b.symbol`` and
|
|
13
|
+
therefore reports namespace ``symbol``.
|
|
14
|
+
- **INV-C5 (single bridge-graph / single mapping).** There is exactly ONE table
|
|
15
|
+
mapping kinds to namespaces. No capability may keep its own copy; the join engine
|
|
16
|
+
and every future composed intent read this one.
|
|
17
|
+
|
|
18
|
+
``integration`` and ``event_type`` carry a synthetic independent identity but have
|
|
19
|
+
low join value in v1 (ADR-0007 §3); they are *peripheral* namespaces, outside the
|
|
20
|
+
base graph. An unknown kind maps to ``None`` — the honest answer is "not classified",
|
|
21
|
+
never a guess (a mis-guess would silently enable a wrong join).
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from dataclasses import dataclass, replace
|
|
26
|
+
from typing import Any, Callable, Iterable, Optional
|
|
27
|
+
|
|
28
|
+
from .errors import CompositionError
|
|
29
|
+
from .result import KnowledgeEntity
|
|
30
|
+
|
|
31
|
+
# The three base namespaces (ADR-0007 §3). Order is the hub-first convention, not
|
|
32
|
+
# semantic — membership is what matters.
|
|
33
|
+
BASE_NAMESPACES: tuple[str, ...] = ("symbol", "endpoint", "module")
|
|
34
|
+
|
|
35
|
+
# Peripheral namespaces: real independent identity, deferred out of the v1 join graph.
|
|
36
|
+
PERIPHERAL_NAMESPACES: tuple[str, ...] = ("integration", "event")
|
|
37
|
+
|
|
38
|
+
# The single kind→namespace table. Derived from the real ``identity=`` of each kind
|
|
39
|
+
# emitted in ``steps_*.py`` (verified 2026-07-22). Adding a new emitted kind MUST add
|
|
40
|
+
# a row here (the join engine treats an unmapped kind as unclassifiable).
|
|
41
|
+
_KIND_TO_NAMESPACE: dict[str, str] = {
|
|
42
|
+
# ── symbol namespace (identity = FQN) — the join hub ────────────────────────
|
|
43
|
+
"dependent": "symbol", # impact reverse-reach
|
|
44
|
+
"caller": "symbol", # call graph, in-edge
|
|
45
|
+
"callee": "symbol", # call graph, out-edge
|
|
46
|
+
"execution_path": "symbol", # graph reachability
|
|
47
|
+
"type_dependent": "symbol", # type-usage reverse dep
|
|
48
|
+
"subsystem_member": "symbol", # module membership member
|
|
49
|
+
"coupling_hub": "symbol", # high-degree node
|
|
50
|
+
"transaction_boundary": "symbol", # facet: tx-role (identity = b.symbol)
|
|
51
|
+
"transaction_entrypoint": "symbol", # facet: tx-role
|
|
52
|
+
"propagation_point": "symbol", # facet: tx-role
|
|
53
|
+
"transaction_reaching": "symbol", # facet: reaches a tx (identity = b.symbol)
|
|
54
|
+
"security_finding": "symbol", # facet: security (identity = f.symbol)
|
|
55
|
+
# ── endpoint namespace (identity = route id / handler) ──────────────────────
|
|
56
|
+
"endpoint": "endpoint",
|
|
57
|
+
"endpoint_contract": "endpoint", # facet: public contract (identity = _endpoint_id)
|
|
58
|
+
"endpoint_security": "endpoint", # facet: security, keyed by endpoint
|
|
59
|
+
"validation_point": "endpoint", # facet: validation (identity = _route_id)
|
|
60
|
+
"validation_gap": "endpoint", # facet: validation
|
|
61
|
+
# ── module namespace (identity = path / label) ──────────────────────────────
|
|
62
|
+
"module": "module",
|
|
63
|
+
"subsystem": "module",
|
|
64
|
+
# ── peripheral (deferred, ADR-0007 §3) ──────────────────────────────────────
|
|
65
|
+
"integration": "integration",
|
|
66
|
+
"event_type": "event",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def namespace_of(kind: str) -> Optional[str]:
|
|
71
|
+
"""Return the identity namespace of an entity ``kind``, or ``None`` if the kind
|
|
72
|
+
is not classified. ``None`` is the honest answer for an unknown kind — the join
|
|
73
|
+
engine must not treat an unclassifiable entity as belonging to any namespace."""
|
|
74
|
+
return _KIND_TO_NAMESPACE.get(kind)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def is_base_namespace(namespace: Optional[str]) -> bool:
|
|
78
|
+
"""True iff ``namespace`` is one of the three base (join-graph) namespaces."""
|
|
79
|
+
return namespace in BASE_NAMESPACES
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def same_namespace(kind_a: str, kind_b: str) -> bool:
|
|
83
|
+
"""True iff both kinds are classified into the SAME namespace.
|
|
84
|
+
|
|
85
|
+
This is the join engine's discriminator (ADR-0007 §3): two entities in the same
|
|
86
|
+
namespace combine by a plain set operation; two in different namespaces require a
|
|
87
|
+
declared bridge (§4). An unknown kind (namespace ``None``) is never "the same" as
|
|
88
|
+
anything — an unclassifiable entity cannot be safely set-joined.
|
|
89
|
+
"""
|
|
90
|
+
ns_a = namespace_of(kind_a)
|
|
91
|
+
ns_b = namespace_of(kind_b)
|
|
92
|
+
return ns_a is not None and ns_a == ns_b
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ── The bridge graph (ADR-0007 §4) ──────────────────────────────────────────────
|
|
96
|
+
# A bridge is a declared relation that maps an entity in one base namespace to
|
|
97
|
+
# entities in another, backed by a fact repository_ir already emits. It is the ONLY
|
|
98
|
+
# legal way to join across namespaces (INV-C3); the set of bridges is a single source
|
|
99
|
+
# of truth (INV-C5). Two of the four carry VARIABLE confidence (INV-C4) — and they
|
|
100
|
+
# are exactly the two joins ASK already measures (endpoint resolution: SIM-1/2;
|
|
101
|
+
# call-graph reach: P1-A/B); the rest are structural (always high).
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(frozen=True)
|
|
105
|
+
class Bridge:
|
|
106
|
+
"""One declared cross-namespace relation.
|
|
107
|
+
|
|
108
|
+
``from_ns``/``to_ns`` record the natural backing direction; a join may traverse a
|
|
109
|
+
bridge either way (see :func:`are_bridged`) — the confidence is symmetric.
|
|
110
|
+
``variable_confidence`` marks a bridge whose per-edge confidence can be below
|
|
111
|
+
``high`` and therefore participates in the §5 ``min`` as a real term; a structural
|
|
112
|
+
bridge is always ``high`` and can never lower a composed result.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
from_ns: str
|
|
116
|
+
relation: str
|
|
117
|
+
to_ns: str
|
|
118
|
+
backing: str # the repository_ir fact that grounds the edge
|
|
119
|
+
variable_confidence: bool
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
BRIDGES: tuple[Bridge, ...] = (
|
|
123
|
+
Bridge("endpoint", "handled_by", "symbol", "route_surface[].handler", True),
|
|
124
|
+
Bridge("symbol", "reaches", "symbol", "reverse_call_graph", True),
|
|
125
|
+
Bridge("symbol", "enclosed_by", "symbol", "enclosing_class", False),
|
|
126
|
+
Bridge("module", "declares", "symbol", "subsystem_membership", False),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def bridges_between(from_ns: str, to_ns: str) -> tuple[Bridge, ...]:
|
|
131
|
+
"""Declared bridges whose natural direction is exactly ``from_ns`` → ``to_ns``."""
|
|
132
|
+
return tuple(b for b in BRIDGES if b.from_ns == from_ns and b.to_ns == to_ns)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def are_bridged(ns_a: str, ns_b: str) -> bool:
|
|
136
|
+
"""True iff a declared bridge connects the two namespaces in EITHER direction."""
|
|
137
|
+
return any(
|
|
138
|
+
{b.from_ns, b.to_ns} == {ns_a, ns_b} or (b.from_ns == b.to_ns == ns_a == ns_b)
|
|
139
|
+
for b in BRIDGES
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def is_join_legal(kind_a: str, kind_b: str) -> bool:
|
|
144
|
+
"""True iff two kinds may be composed: same namespace (a plain set op) OR a
|
|
145
|
+
declared bridge connects their namespaces (a bridged relate, ADR-0007 §4). An
|
|
146
|
+
unclassified kind (namespace ``None``) is never joinable — INV-C3 forbids an
|
|
147
|
+
implicit or guessed join."""
|
|
148
|
+
ns_a = namespace_of(kind_a)
|
|
149
|
+
ns_b = namespace_of(kind_b)
|
|
150
|
+
if ns_a is None or ns_b is None:
|
|
151
|
+
return False
|
|
152
|
+
return ns_a == ns_b or are_bridged(ns_a, ns_b)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ── The confidence lattice (ADR-0007 §5 / §11.2) ────────────────────────────────
|
|
156
|
+
# Confidence is a TOTAL ORDER; the ONLY combinator is min (greatest-lower-bound);
|
|
157
|
+
# `unproven` is absorbing. No max / average / heuristic exists (INV-C9). A composed
|
|
158
|
+
# result can be no more certain than its weakest constituent OR its weakest bridge.
|
|
159
|
+
|
|
160
|
+
# Strongest → weakest. Index is the rank; a larger index is weaker.
|
|
161
|
+
CONFIDENCE_ORDER: tuple[str, ...] = ("high", "medium", "low", "unproven")
|
|
162
|
+
_CONF_RANK: dict[str, int] = {level: i for i, level in enumerate(CONFIDENCE_ORDER)}
|
|
163
|
+
_UNPROVEN_RANK: int = _CONF_RANK["unproven"]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def confidence_min(*values: Optional[str]) -> str:
|
|
167
|
+
"""Combine confidences by greatest-lower-bound on the total order (ADR-0007 §5).
|
|
168
|
+
|
|
169
|
+
Returns the WEAKEST of ``values``. `unproven` is absorbing. An unrecognized or
|
|
170
|
+
``None`` value degrades to `unproven` — the honest floor, never silently ignored.
|
|
171
|
+
With no values at all the result is `unproven`: a composition standing on nothing
|
|
172
|
+
cannot claim confidence (this deliberately breaks the empty-min=top identity in
|
|
173
|
+
favour of honesty).
|
|
174
|
+
"""
|
|
175
|
+
worst_rank = -1
|
|
176
|
+
for v in values:
|
|
177
|
+
rank = _CONF_RANK.get(v if v is not None else "", _UNPROVEN_RANK)
|
|
178
|
+
if rank > worst_rank:
|
|
179
|
+
worst_rank = rank
|
|
180
|
+
if worst_rank < 0:
|
|
181
|
+
return "unproven"
|
|
182
|
+
return CONFIDENCE_ORDER[worst_rank]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ── The typed algebra operators (ADR-0007 §2 / §11.3) ───────────────────────────
|
|
186
|
+
# A small, closed, TOTAL, DETERMINISTIC operator set over KnowledgeEntity sets:
|
|
187
|
+
# • combine — typed union / difference / intersection WITHIN one namespace (§3.1)
|
|
188
|
+
# • filter — a predicate over an entity's own attributes (§3.3, no join)
|
|
189
|
+
# • relate — a bridged join ACROSS namespaces, through a declared bridge (§4)
|
|
190
|
+
# They CONSUME facts and never produce them (INV-C1): `relate` takes a *bridge
|
|
191
|
+
# resolver* (a function that reports, per source entity, the target identities an IR
|
|
192
|
+
# fact already links it to, with that edge's confidence). The operators resolve
|
|
193
|
+
# nothing themselves — wiring a resolver to a real repository_ir fact is a separate
|
|
194
|
+
# build step. Confidence only ever attenuates (INV-C2) via `confidence_min` (§5),
|
|
195
|
+
# and includes the traversed bridge edge (INV-C4). An illegal request (cross-namespace
|
|
196
|
+
# combine, undeclared bridge, unclassifiable kind) RAISES `CompositionError` — never a
|
|
197
|
+
# silently-empty result (INV-F1-6).
|
|
198
|
+
|
|
199
|
+
# Per-entity confidence lives in `attributes` under this key. Absent → the caller's
|
|
200
|
+
# supplied constituent confidence, ultimately `unproven` (never silently `high`).
|
|
201
|
+
ATTR_CONFIDENCE = "confidence"
|
|
202
|
+
# The symbol an endpoint is handled_by, as a first-class attribute (ADR-0007 §11.1b) —
|
|
203
|
+
# so the `handled_by` bridge reads a field instead of parsing the entity identity.
|
|
204
|
+
ATTR_HANDLER_SYMBOL = "handler_symbol"
|
|
205
|
+
# Provenance breadcrumbs a `relate` writes onto each produced entity (§8.1: composition
|
|
206
|
+
# adds provenance edges, removes none).
|
|
207
|
+
ATTR_BRIDGED_VIA = "bridged_via"
|
|
208
|
+
ATTR_BRIDGE_TARGET = "bridge_target"
|
|
209
|
+
|
|
210
|
+
# A bridge resolver reports, for one source entity, the edges an IR fact links it to,
|
|
211
|
+
# as (target_identity, edge_confidence) pairs. Zero pairs = this source has no such
|
|
212
|
+
# edge (an honest empty, not an error).
|
|
213
|
+
BridgeResolver = Callable[[KnowledgeEntity], Iterable[tuple[str, str]]]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def entity_confidence(entity: KnowledgeEntity, default: str = "unproven") -> str:
|
|
217
|
+
"""Read an entity's own confidence from its attributes.
|
|
218
|
+
|
|
219
|
+
An absent or unrecognized value degrades to ``default`` — a caller passes the
|
|
220
|
+
constituent surface's result-level confidence so a per-entity omission does not
|
|
221
|
+
silently read as ``high``. With no default supplied the floor is ``unproven``
|
|
222
|
+
(INV-C7: an input's uncertainty is never laundered away)."""
|
|
223
|
+
v = entity.attributes.get(ATTR_CONFIDENCE)
|
|
224
|
+
return v if v in _CONF_RANK else default
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _with_confidence(entity: KnowledgeEntity, confidence: str, **extra: Any) -> KnowledgeEntity:
|
|
228
|
+
"""Return a copy of ``entity`` carrying ``confidence`` (and any provenance extras)
|
|
229
|
+
in its attributes. Frozen entity in, frozen entity out — never mutated in place."""
|
|
230
|
+
attrs = dict(entity.attributes)
|
|
231
|
+
attrs[ATTR_CONFIDENCE] = confidence
|
|
232
|
+
attrs.update(extra)
|
|
233
|
+
return replace(entity, attributes=attrs)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _uniform_namespace(entities: Iterable[KnowledgeEntity], *, op: str) -> Optional[str]:
|
|
237
|
+
"""The single base namespace shared by every entity, or ``None`` for the empty set.
|
|
238
|
+
|
|
239
|
+
Raises ``CompositionError`` if the entities span more than one namespace or include
|
|
240
|
+
an unclassifiable kind — a set operation is only defined within one namespace (§3.1;
|
|
241
|
+
INV-C10)."""
|
|
242
|
+
ns: Optional[str] = None
|
|
243
|
+
for e in entities:
|
|
244
|
+
e_ns = namespace_of(e.kind)
|
|
245
|
+
if e_ns is None:
|
|
246
|
+
raise CompositionError(
|
|
247
|
+
f"{op}: kind {e.kind!r} is unclassified (no namespace); "
|
|
248
|
+
"an unclassifiable entity cannot take part in a set operation"
|
|
249
|
+
)
|
|
250
|
+
if ns is None:
|
|
251
|
+
ns = e_ns
|
|
252
|
+
elif ns != e_ns:
|
|
253
|
+
raise CompositionError(
|
|
254
|
+
f"{op}: operands span namespaces {ns!r} and {e_ns!r}; a set operation "
|
|
255
|
+
"is defined only within one namespace — use `relate` across a bridge"
|
|
256
|
+
)
|
|
257
|
+
return ns
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def combine(
|
|
261
|
+
left: Iterable[KnowledgeEntity],
|
|
262
|
+
right: Iterable[KnowledgeEntity],
|
|
263
|
+
op: str,
|
|
264
|
+
*,
|
|
265
|
+
default_confidence: str = "unproven",
|
|
266
|
+
) -> tuple[KnowledgeEntity, ...]:
|
|
267
|
+
"""Typed set operation on ONE namespace, keyed on ``identity`` (ADR-0007 §3.1).
|
|
268
|
+
|
|
269
|
+
``op`` is ``"union"``, ``"intersection"`` or ``"difference"`` (left minus right).
|
|
270
|
+
Both operands MUST resolve to the same base namespace; a cross-namespace combine is
|
|
271
|
+
a category error and RAISES (INV-C3). Intersection attenuates: an identity present
|
|
272
|
+
in both carries ``min`` of the two copies' confidence (§5). Union/difference carry
|
|
273
|
+
each surviving entity's own confidence unchanged. Order is deterministic: left order
|
|
274
|
+
first, then (for union) the right-only entities in their own order."""
|
|
275
|
+
left = tuple(left)
|
|
276
|
+
right = tuple(right)
|
|
277
|
+
ns_left = _uniform_namespace(left, op=f"combine/{op}")
|
|
278
|
+
ns_right = _uniform_namespace(right, op=f"combine/{op}")
|
|
279
|
+
if ns_left is not None and ns_right is not None and ns_left != ns_right:
|
|
280
|
+
raise CompositionError(
|
|
281
|
+
f"combine/{op}: operands are in namespaces {ns_left!r} and {ns_right!r}; "
|
|
282
|
+
"a set operation is defined only within one namespace"
|
|
283
|
+
)
|
|
284
|
+
right_by_id = {e.identity: e for e in right}
|
|
285
|
+
left_ids = {e.identity for e in left}
|
|
286
|
+
|
|
287
|
+
if op == "union":
|
|
288
|
+
out = list(left)
|
|
289
|
+
out.extend(e for e in right if e.identity not in left_ids)
|
|
290
|
+
return tuple(out)
|
|
291
|
+
if op == "difference":
|
|
292
|
+
return tuple(e for e in left if e.identity not in right_by_id)
|
|
293
|
+
if op == "intersection":
|
|
294
|
+
out = []
|
|
295
|
+
for e in left:
|
|
296
|
+
match = right_by_id.get(e.identity)
|
|
297
|
+
if match is None:
|
|
298
|
+
continue
|
|
299
|
+
conf = confidence_min(
|
|
300
|
+
entity_confidence(e, default_confidence),
|
|
301
|
+
entity_confidence(match, default_confidence),
|
|
302
|
+
)
|
|
303
|
+
out.append(_with_confidence(e, conf))
|
|
304
|
+
return tuple(out)
|
|
305
|
+
raise CompositionError(
|
|
306
|
+
f"combine: unknown set operation {op!r} (expected union/intersection/difference)"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def filter_entities(
|
|
311
|
+
entities: Iterable[KnowledgeEntity],
|
|
312
|
+
predicate: Callable[[KnowledgeEntity], bool],
|
|
313
|
+
) -> tuple[KnowledgeEntity, ...]:
|
|
314
|
+
"""Keep entities whose ``predicate`` holds (ADR-0007 §3.3 — a facet is reached by a
|
|
315
|
+
predicate on its own base entity, never a join). Pure subtraction: confidence is
|
|
316
|
+
unchanged (filtering welds no views) and input order is preserved."""
|
|
317
|
+
return tuple(e for e in entities if predicate(e))
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def attribute_predicate(name: str, op: str, value: Any) -> Callable[[KnowledgeEntity], bool]:
|
|
321
|
+
"""Build a predicate over one attribute (ADR-0007 §11.5). Supports ``==`` / ``!=``
|
|
322
|
+
and the numeric comparators ``>`` / ``>=`` / ``<`` / ``<=``. A missing attribute
|
|
323
|
+
makes the predicate False (never a raise) — an entity lacking the tested attribute
|
|
324
|
+
simply does not match."""
|
|
325
|
+
comparators: dict[str, Callable[[Any, Any], bool]] = {
|
|
326
|
+
"==": lambda a, b: a == b,
|
|
327
|
+
"!=": lambda a, b: a != b,
|
|
328
|
+
">": lambda a, b: a > b,
|
|
329
|
+
">=": lambda a, b: a >= b,
|
|
330
|
+
"<": lambda a, b: a < b,
|
|
331
|
+
"<=": lambda a, b: a <= b,
|
|
332
|
+
}
|
|
333
|
+
cmp = comparators.get(op)
|
|
334
|
+
if cmp is None:
|
|
335
|
+
raise CompositionError(f"attribute_predicate: unknown operator {op!r}")
|
|
336
|
+
|
|
337
|
+
def _pred(entity: KnowledgeEntity) -> bool:
|
|
338
|
+
if name not in entity.attributes:
|
|
339
|
+
return False
|
|
340
|
+
try:
|
|
341
|
+
return cmp(entity.attributes[name], value)
|
|
342
|
+
except TypeError:
|
|
343
|
+
# incomparable types (e.g. numeric op on a string) → no match, never raise
|
|
344
|
+
return False
|
|
345
|
+
|
|
346
|
+
return _pred
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def relate(
|
|
350
|
+
left: Iterable[KnowledgeEntity],
|
|
351
|
+
right: Iterable[KnowledgeEntity],
|
|
352
|
+
relation: str,
|
|
353
|
+
resolver: BridgeResolver,
|
|
354
|
+
*,
|
|
355
|
+
keep: str = "left",
|
|
356
|
+
default_confidence: str = "unproven",
|
|
357
|
+
) -> tuple[KnowledgeEntity, ...]:
|
|
358
|
+
"""Bridged join across two namespaces through a DECLARED bridge (ADR-0007 §4).
|
|
359
|
+
|
|
360
|
+
``relation`` names a bridge in :data:`BRIDGES`; ``resolver`` reports, per left
|
|
361
|
+
entity, the ``(target_identity, edge_confidence)`` pairs an IR fact links it to.
|
|
362
|
+
A left entity is kept iff at least one resolved target is present in ``right``
|
|
363
|
+
(a semi-join). ``keep`` selects which side survives: ``"left"`` returns the matched
|
|
364
|
+
left entities, ``"right"`` the matched right entities.
|
|
365
|
+
|
|
366
|
+
Confidence attenuates per §5: each surviving entity carries
|
|
367
|
+
``min(its own conf, the matched partner's conf, the bridge edge's conf)`` — the
|
|
368
|
+
bridge edge is a term in the minimum (INV-C4), so a strong pair welded through a
|
|
369
|
+
weak edge is weak. The bridge and matched target are recorded in the result's
|
|
370
|
+
attributes for provenance (§8.1).
|
|
371
|
+
|
|
372
|
+
RAISES ``CompositionError`` if ``relation`` is not a declared bridge or does not
|
|
373
|
+
connect the two operands' namespaces (INV-C3) — never a silently-empty result."""
|
|
374
|
+
left = tuple(left)
|
|
375
|
+
right = tuple(right)
|
|
376
|
+
# An undeclared bridge name is a wiring fault regardless of operand emptiness.
|
|
377
|
+
bridge = next((b for b in BRIDGES if b.relation == relation), None)
|
|
378
|
+
if bridge is None:
|
|
379
|
+
raise CompositionError(
|
|
380
|
+
f"relate: {relation!r} is not a declared bridge (INV-C3: no implicit joins)"
|
|
381
|
+
)
|
|
382
|
+
if keep not in ("left", "right"):
|
|
383
|
+
raise CompositionError(f"relate: keep must be 'left' or 'right', got {keep!r}")
|
|
384
|
+
ns_left = _uniform_namespace(left, op=f"relate/{relation}")
|
|
385
|
+
ns_right = _uniform_namespace(right, op=f"relate/{relation}")
|
|
386
|
+
if ns_left is None or ns_right is None:
|
|
387
|
+
# one side empty: nothing to relate, and no namespaces to validate against
|
|
388
|
+
return ()
|
|
389
|
+
if {bridge.from_ns, bridge.to_ns} != {ns_left, ns_right}:
|
|
390
|
+
raise CompositionError(
|
|
391
|
+
f"relate: bridge {relation!r} connects {bridge.from_ns!r}↔{bridge.to_ns!r}, "
|
|
392
|
+
f"not the operands' namespaces {ns_left!r}↔{ns_right!r}"
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
right_by_id = {e.identity: e for e in right}
|
|
396
|
+
out: list[KnowledgeEntity] = []
|
|
397
|
+
seen_right: set[str] = set()
|
|
398
|
+
for src in left:
|
|
399
|
+
for target_id, edge_conf in resolver(src):
|
|
400
|
+
match = right_by_id.get(target_id)
|
|
401
|
+
if match is None:
|
|
402
|
+
continue
|
|
403
|
+
conf = confidence_min(
|
|
404
|
+
entity_confidence(src, default_confidence),
|
|
405
|
+
entity_confidence(match, default_confidence),
|
|
406
|
+
edge_conf,
|
|
407
|
+
)
|
|
408
|
+
if keep == "left":
|
|
409
|
+
out.append(
|
|
410
|
+
_with_confidence(
|
|
411
|
+
src, conf,
|
|
412
|
+
**{ATTR_BRIDGED_VIA: relation, ATTR_BRIDGE_TARGET: target_id},
|
|
413
|
+
)
|
|
414
|
+
)
|
|
415
|
+
break # one qualifying edge is enough to keep the left entity
|
|
416
|
+
if match.identity not in seen_right:
|
|
417
|
+
seen_right.add(match.identity)
|
|
418
|
+
out.append(
|
|
419
|
+
_with_confidence(
|
|
420
|
+
match, conf,
|
|
421
|
+
**{ATTR_BRIDGED_VIA: relation, ATTR_BRIDGE_TARGET: src.identity},
|
|
422
|
+
)
|
|
423
|
+
)
|
|
424
|
+
return tuple(out)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
# ── Concrete bridge resolvers — wiring the two variable bridges to IR facts ─────────
|
|
428
|
+
# `relate` consumes a BridgeResolver; these builders produce the concrete resolvers for
|
|
429
|
+
# the `handled_by` and `reaches` bridges (§4). They EXTRACT nothing — the facts (the
|
|
430
|
+
# handler symbol, the reach class) were produced by the endpoint / impact surfaces; a
|
|
431
|
+
# resolver only PROJECTS them into (target, edge_confidence) pairs (INV-C1). The edge
|
|
432
|
+
# confidence is exactly the uncertainty ASK already measures on these two joins
|
|
433
|
+
# (endpoint resolution SIM-1/2; call-graph reach P1-A/B) — §5's `min` consumes it, it
|
|
434
|
+
# does not originate it.
|
|
435
|
+
|
|
436
|
+
# Impact reach class → `reaches`-bridge edge confidence. A direct caller edge is high;
|
|
437
|
+
# an indirect (transitive / DI-mediated) edge is medium; anything else is unproven —
|
|
438
|
+
# never silently high (INV-C7).
|
|
439
|
+
_REACH_CONFIDENCE: dict[str, str] = {"direct": "high", "indirect": "medium"}
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def reach_confidence(reach: Optional[str]) -> str:
|
|
443
|
+
"""The `reaches`-bridge edge confidence implied by an impact reach class (P1-A/B).
|
|
444
|
+
An unknown/absent reach degrades to `unproven`, the honest floor."""
|
|
445
|
+
return _REACH_CONFIDENCE.get(reach or "", "unproven")
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def endpoint_handler_symbol(endpoint: KnowledgeEntity) -> Optional[str]:
|
|
449
|
+
"""The symbol an endpoint is `handled_by`. Reads the first-class ``handler_symbol``
|
|
450
|
+
attribute (ADR-0007 §11.1b); falls back to the identity only when the identity IS a
|
|
451
|
+
handler FQN (not the ``"METHOD path"`` / ``"/path"`` fallback form). Returns ``None``
|
|
452
|
+
for an endpoint with no known handler — an unresolved locator chain (SIM-1) has no
|
|
453
|
+
bridge edge, and that absence must not be guessed away."""
|
|
454
|
+
handler = endpoint.attributes.get(ATTR_HANDLER_SYMBOL)
|
|
455
|
+
if handler:
|
|
456
|
+
return handler
|
|
457
|
+
ident = endpoint.identity
|
|
458
|
+
if ident and " " not in ident and not ident.startswith("/"):
|
|
459
|
+
return ident
|
|
460
|
+
return None
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def handled_by_resolver(
|
|
464
|
+
edge_confidence: str | Callable[[KnowledgeEntity], str] = "high",
|
|
465
|
+
) -> BridgeResolver:
|
|
466
|
+
"""Concrete resolver for the `handled_by` bridge (endpoint → handler symbol, §4).
|
|
467
|
+
|
|
468
|
+
Yields one ``(handler_symbol, edge_confidence)`` pair per endpoint that has a known
|
|
469
|
+
handler, and nothing for an unresolved endpoint. ``edge_confidence`` defaults to
|
|
470
|
+
``high`` (a direct ``@…Mapping``); pass a callable to lower it per endpoint for a
|
|
471
|
+
weak resolution (JAX-RS locator chain = medium/low, SIM-1), read from the endpoint's
|
|
472
|
+
own attributes — the resolver infers no weakening on its own."""
|
|
473
|
+
def _resolve(endpoint: KnowledgeEntity) -> Iterable[tuple[str, str]]:
|
|
474
|
+
handler = endpoint_handler_symbol(endpoint)
|
|
475
|
+
if not handler:
|
|
476
|
+
return ()
|
|
477
|
+
conf = edge_confidence(endpoint) if callable(edge_confidence) else edge_confidence
|
|
478
|
+
return ((handler, conf),)
|
|
479
|
+
|
|
480
|
+
return _resolve
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def stamp_reach_confidence(
|
|
484
|
+
entities: Iterable[KnowledgeEntity],
|
|
485
|
+
) -> tuple[KnowledgeEntity, ...]:
|
|
486
|
+
"""Return copies of impact ``dependent`` entities carrying a per-entity
|
|
487
|
+
``confidence`` derived from their ``reach`` class (:func:`reach_confidence`), so the
|
|
488
|
+
`reaches` bridge's variable confidence participates as a real term in §5's ``min``.
|
|
489
|
+
An entity that already carries a confidence is left unchanged (never re-raised)."""
|
|
490
|
+
out: list[KnowledgeEntity] = []
|
|
491
|
+
for e in entities:
|
|
492
|
+
if e.attributes.get(ATTR_CONFIDENCE) in _CONF_RANK:
|
|
493
|
+
out.append(e)
|
|
494
|
+
else:
|
|
495
|
+
out.append(_with_confidence(e, reach_confidence(e.attributes.get("reach"))))
|
|
496
|
+
return tuple(out)
|
|
@@ -39,3 +39,10 @@ class StepExecutionError(RetrievalError):
|
|
|
39
39
|
class StepContractError(RetrievalError):
|
|
40
40
|
"""A step required a shared-context slot that a prior step did not produce (a plan
|
|
41
41
|
composed steps in an invalid order)."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CompositionError(RetrievalError):
|
|
45
|
+
"""An illegal composition was requested (ADR-0007): a set operation across two
|
|
46
|
+
namespaces, a join over an undeclared bridge, or an unclassifiable kind. This is a
|
|
47
|
+
category error / plan-wiring fault — never a "no result" signal (INV-F1-6: a
|
|
48
|
+
composition fault is surfaced, not silently returned as an empty set)."""
|
|
@@ -83,7 +83,11 @@ def resolve_endpoint_contract(step: Step, ctx: "StepContext") -> None:
|
|
|
83
83
|
identity=_endpoint_id(ep),
|
|
84
84
|
kind="endpoint_contract",
|
|
85
85
|
score=0.0,
|
|
86
|
-
attributes={
|
|
86
|
+
attributes={
|
|
87
|
+
**{k: v for k, v in ep.items() if k not in ("source_file",)},
|
|
88
|
+
# first-class handler symbol for the ADR-0007 handled_by bridge (§11.1b)
|
|
89
|
+
"handler_symbol": ep.get("handler", "") or "",
|
|
90
|
+
},
|
|
87
91
|
)
|
|
88
92
|
for ep in matched
|
|
89
93
|
],
|
|
@@ -131,6 +135,8 @@ def resolve_endpoint_security(step: Step, ctx: "StepContext") -> None:
|
|
|
131
135
|
attributes={
|
|
132
136
|
"method": ep.method or "",
|
|
133
137
|
"path": ep.path or "",
|
|
138
|
+
# first-class handler symbol for the ADR-0007 handled_by bridge (§11.1b)
|
|
139
|
+
"handler_symbol": ep.handler_symbol or "",
|
|
134
140
|
"security": _security_summary(ep.security),
|
|
135
141
|
"findings": [f.pattern_id for f in findings if _finding_on_handler(f, {ep.handler_symbol})],
|
|
136
142
|
},
|