sourcecode 2.5.19__tar.gz → 2.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sourcecode-2.5.19 → sourcecode-2.6.0}/CHANGELOG.md +99 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/PKG-INFO +1 -1
- {sourcecode-2.5.19 → sourcecode-2.6.0}/pyproject.toml +1 -1
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/__init__.py +1 -1
- sourcecode-2.6.0/src/sourcecode/architectural_baseline.py +408 -0
- sourcecode-2.6.0/src/sourcecode/architectural_delta.py +292 -0
- sourcecode-2.6.0/src/sourcecode/change_plan.py +195 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/cli.py +640 -1
- sourcecode-2.6.0/src/sourcecode/compare.py +159 -0
- sourcecode-2.6.0/src/sourcecode/constraint_diff.py +114 -0
- sourcecode-2.6.0/src/sourcecode/contract_diff.py +224 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/format_contract.py +6 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/repository_ir.py +152 -24
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/validation_inference.py +59 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/.gitignore +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/.ruff.toml +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/CONTRIBUTING.md +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/LICENSE +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/README.md +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/SECURITY.md +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/raw +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/scripts/compare_integration_engines.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/scripts/customer_smoke_test.sh +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/scripts/generate_jdk_exports.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/scripts/perf_harness.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/archetype.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/cache.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/call_surface.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/caller_metrics.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/classifier.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/context_cache.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/context_graph.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/dynamic_argument_surface.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/endpoint_literals.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/endpoint_metrics.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/evidence_provider.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/explain.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/graph_evidence.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/hibernate_strat.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/jdk_exports.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/license.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/migrate_check.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/perf.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/progress.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/reconciliation.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/retrieval/__init__.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/retrieval/context.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/retrieval/errors.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/retrieval/executor.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/retrieval/planner.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/retrieval/query.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/retrieval/request.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/retrieval/resolution.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/retrieval/result.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/retrieval/retriever.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/retrieval/runtime.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/retrieval/steps.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/retrieval/steps_graph.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/retrieval/steps_impact.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/retrieval/steps_intf.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/retrieval/steps_struct.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/retrieval/steps_txsec.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/ris.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/schema.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/security_config.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/security_posture.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/semantic_impact_engine.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/semantic_integration_engine.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/semantic_services.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/serializer.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/type_usage_surface.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/validation_surface.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/version_check.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/supabase/functions/README.md +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/supabase/sql/licensing_schema.sql +0 -0
- {sourcecode-2.5.19 → sourcecode-2.6.0}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,5 +1,104 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.6.0] — 2026-07-22
|
|
4
|
+
|
|
5
|
+
**Engineering Decision Support — Part B (capability milestone) lands, plus the
|
|
6
|
+
DR-1..DR-7 signature-cleanliness family and the P1 validation-confidence
|
|
7
|
+
projection.** Built directly on 2.5.20 (Part A / F1–F3). Everything here is a
|
|
8
|
+
deterministic projection of evidence ASK already computes — no ROI, no verdicts,
|
|
9
|
+
no business judgement. Full suite: **3631 passed, 5 skipped**. The ContextGraph
|
|
10
|
+
baseline oracle was regenerated for this release (it was ~3 weeks stale,
|
|
11
|
+
predating Part A); the fleet drift is additive and fully explained — every one
|
|
12
|
+
of the 99 cells captures `ok`, no errors, endpoint sets preserved, only additive
|
|
13
|
+
keys.
|
|
14
|
+
|
|
15
|
+
### Added — Part B decision support (deterministic, agent-owned choice stays out)
|
|
16
|
+
|
|
17
|
+
- **D1 — `delta` architectural before/after.** Builds IR for two refs and diffs
|
|
18
|
+
the existing structural metrics (Δ fan-in, Δ coupling, Δ import-cycles, Δ
|
|
19
|
+
blast-radius, Δ public-API surface). Import-cycle deltas and blast-radius
|
|
20
|
+
shifts included (D1-b).
|
|
21
|
+
- **D2 — `contract-diff` public-contract break detection.** Classifies each
|
|
22
|
+
change `breaking | additive | non_breaking` over a stable public-contract
|
|
23
|
+
projection (endpoints + public signatures + validation constraints);
|
|
24
|
+
`non_breaking` is filled from proven constraint loosening (D2-b). No
|
|
25
|
+
behavioural-equivalence claim — structural contract deltas only.
|
|
26
|
+
- **D3 — `plan` change-plan artifact.** Affected set (reverse graph), covering
|
|
27
|
+
tests, rollback surface, validation checklist. No generated code, no
|
|
28
|
+
step-by-step sequence beyond dependency ordering.
|
|
29
|
+
- **D4 — `compare` candidate ranking by measured architectural cost.** Per-candidate
|
|
30
|
+
cost vector (endpoints touched, blast-radius, coupling delta, tests at risk).
|
|
31
|
+
No ROI / "best choice" field — that judgement is the engineer's / agent's.
|
|
32
|
+
- **D5 — `baseline` architectural knowledge persistence.** Capture / diff / trend
|
|
33
|
+
a versioned architectural baseline per ref, mirroring the perf-harness
|
|
34
|
+
frozen-schema + same-baseline-diff discipline; comparability contract emits
|
|
35
|
+
`NOT_COMPARABLE` rather than a misleading delta (D5-b).
|
|
36
|
+
- **P1 — single-repo validation-confidence block.** An Explainability projection
|
|
37
|
+
of the existing `Confidence` / `fp_causes` / `zero_result_reason` evidence into
|
|
38
|
+
a human-readable confidence block. Projection only, no new inference.
|
|
39
|
+
|
|
40
|
+
### Fixed — DR-1..DR-7 parameter-signature correctness (IR extractor)
|
|
41
|
+
|
|
42
|
+
Annotated-parameter method signatures were corrupted in several ways, all of
|
|
43
|
+
which fed `signature` / `param_types` (and therefore `contract-diff` trust):
|
|
44
|
+
|
|
45
|
+
- **DR-1 — paren-balanced parameter extraction.** A parameter annotation with
|
|
46
|
+
parenthesised args (`@PathVariable(name=…, required=…)`) truncated the naive
|
|
47
|
+
first-`)` scan, so any add/remove/retype of a parameter read as UNCHANGED.
|
|
48
|
+
Now uses a paren- and string-literal-aware `_closing_paren_index` on both the
|
|
49
|
+
method and constructor paths.
|
|
50
|
+
- **DR-2 — sub-second baseline capture timestamps** so `baseline trend` orders by
|
|
51
|
+
time instead of tie-breaking on a meaningless commit string.
|
|
52
|
+
- **DR-3 — import-cycle metric docstring** scoped honestly to import-visible
|
|
53
|
+
cycles (the function was over-promising cross-edge coverage).
|
|
54
|
+
- **DR-4 — annotate the two distinct body-endpoint axes** (`summary` vs
|
|
55
|
+
`validation_pattern`) with a `scope_note` so the two counts read coherently.
|
|
56
|
+
- **DR-5 — strip `//` line-comments (string-literal-aware) before the multi-line
|
|
57
|
+
parameter-list join**, so a commented token cannot leak into a signature.
|
|
58
|
+
- **DR-6 — strip `final` + parameter annotations to a fixpoint.** `final @Ann Type`
|
|
59
|
+
kept its annotation because the anchored `^@` strip ran before the unanchored
|
|
60
|
+
`final` strip; the two now alternate until stable, regardless of order.
|
|
61
|
+
- **DR-7 — string-literal-aware annotation-argument strip.** An annotation whose
|
|
62
|
+
quoted value contains parens (`@Description("a URL (http/ftp).")`) leaked its
|
|
63
|
+
tail into the type; the `\([^)]*\)` regex is replaced by
|
|
64
|
+
`_strip_param_annotations`, consuming each `@Name(...)` with the DR-1
|
|
65
|
+
paren-balanced discipline.
|
|
66
|
+
|
|
67
|
+
### Changed
|
|
68
|
+
|
|
69
|
+
- Version `2.5.20` → `2.6.0` (new command surface: `delta`, `contract-diff`,
|
|
70
|
+
`plan`, `compare`, `baseline`).
|
|
71
|
+
- ContextGraph baseline oracle (`tests/contextgraph_baseline/baseline/index.json`)
|
|
72
|
+
regenerated against this release (99 cells, all `ok`).
|
|
73
|
+
|
|
74
|
+
## [2.5.20] — 2026-07-21
|
|
75
|
+
|
|
76
|
+
**Engineering Decision Support — Part A, F3: JAX-RS composed-path canonicalization.**
|
|
77
|
+
Composed `@Path` templates built on a constant whose own value is a concatenation
|
|
78
|
+
(`ROOT_PATH = "/{" + DB_PATH_PARAM_NAME + "}/query/v2"`) produced malformed endpoint
|
|
79
|
+
paths — the class prefix collapsed to `/{`, yielding `/{`, `/{/tx`, `/{/tx/{txId}`.
|
|
80
|
+
Correct endpoint identity is the key of every impact / blast-radius / delta decision,
|
|
81
|
+
so a malformed path corrupts each of them. A pure extraction-correctness fix (no new
|
|
82
|
+
inference). Full suite (3545 passed, 5 skipped) with zero control-repo drift.
|
|
83
|
+
|
|
84
|
+
### Fixed
|
|
85
|
+
|
|
86
|
+
- **F3 — `_collect_file_constants` now folds concat / constant-ref constant values.**
|
|
87
|
+
The collector captured only the FIRST string literal of a `static final String`
|
|
88
|
+
declaration (`_STATIC_FINAL_STR_RE`), so a constant defined as an expression
|
|
89
|
+
(`"/{" + DB_PATH_PARAM_NAME + "}/query/v2"`) was stored truncated as `/{`. Every
|
|
90
|
+
`@Path(SomeResource.ROOT_PATH)` class prefix built on it then collapsed, and each
|
|
91
|
+
composed method path inherited the collapse. The collector now captures the full
|
|
92
|
+
right-hand side (`_STATIC_FINAL_STR_DECL_RE`, up to `;`) and resolves it through
|
|
93
|
+
the existing `_resolve_const_concat`, iterating to a fixpoint so a constant defined
|
|
94
|
+
in terms of another (in either declaration order) resolves. Inline method-level
|
|
95
|
+
concat (`@Path("tx/{" + TX_ID_PATH_PARAM_NAME + "}")`) already resolved via
|
|
96
|
+
`_resolve_ann_path_expr`; this was the one remaining gap. Measured on neo4j
|
|
97
|
+
(`QueryResource`): 8 malformed paths → canonical (`/{databaseName}/query/v2`,
|
|
98
|
+
`/{databaseName}/query/v2/tx/{txId}/commit`). Fleet A/B: neo4j corrected, the other
|
|
99
|
+
7 control repos byte-identical — the fix only ever *completes* a truncated value,
|
|
100
|
+
never rewrites a literal one.
|
|
101
|
+
|
|
3
102
|
## [2.5.19] — 2026-07-20
|
|
4
103
|
|
|
5
104
|
**Engineering Decision Support — Part A, F2: manual validation (guard clauses) is
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
"""architectural_baseline.py — D5 Architectural knowledge persistence.
|
|
2
|
+
|
|
3
|
+
Engineering Decision Support, Part B / D5. Turns ASK from a stateless per-run tool
|
|
4
|
+
into one that REMEMBERS a repository's architecture over time. Generalizes the perf
|
|
5
|
+
harness's frozen-schema + same-baseline-diff discipline (`perf-baseline-v1`,
|
|
6
|
+
`docs/perf/baselines/<ver>/`) from timing to architectural metrics: a committed,
|
|
7
|
+
versioned `architectural-baseline-v1` artifact per ref that persists the measured
|
|
8
|
+
structural fingerprint (totals + fan-in hotspots + the full endpoint surface and
|
|
9
|
+
fan-in map needed to diff faithfully).
|
|
10
|
+
|
|
11
|
+
Three capabilities, one schema:
|
|
12
|
+
* capture — freeze the current CIR's measured metrics into a baseline artifact.
|
|
13
|
+
* diff — diff the live working tree against a stored baseline (this is D1's
|
|
14
|
+
`diff_metrics` with the base snapshot loaded from disk instead of a
|
|
15
|
+
second checkout — "D1 auto-diffs against the stored baseline").
|
|
16
|
+
* trend — a multi-ref series over N stored baselines (totals over time + per
|
|
17
|
+
hotspot fan-in trajectory).
|
|
18
|
+
|
|
19
|
+
Moat line: every field is MEASURED and derived from the CIR alone (via D1's
|
|
20
|
+
`extract_metrics`). Persistence adds no inference, no verdict, no ROI — it stores and
|
|
21
|
+
replays measured facts. Trend reports movement; it never labels a trajectory
|
|
22
|
+
"improving"/"degrading" (that reading is the engineer's).
|
|
23
|
+
|
|
24
|
+
Determinism & comparability (carried from the perf harness): the metric content of a
|
|
25
|
+
baseline is a pure function of the CIR — same repo state → identical totals/hotspots/
|
|
26
|
+
surface (only `captured_at`/`commit`/env vary). An env fingerprint is recorded so a
|
|
27
|
+
consumer can refuse to compare baselines captured on incomparable hosts, exactly as
|
|
28
|
+
the perf baselines do.
|
|
29
|
+
"""
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
import subprocess
|
|
34
|
+
from datetime import datetime, timezone
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import TYPE_CHECKING
|
|
37
|
+
|
|
38
|
+
from sourcecode import __version__ as TOOL_VERSION
|
|
39
|
+
from sourcecode.architectural_delta import ArchMetrics, diff_metrics, extract_metrics
|
|
40
|
+
from sourcecode.perf import collect_env
|
|
41
|
+
|
|
42
|
+
if TYPE_CHECKING:
|
|
43
|
+
from sourcecode.canonical_ir import CanonicalRepositoryIR
|
|
44
|
+
|
|
45
|
+
# Frozen schema tags — versioned like perf-baseline-v1 / architectural-delta-v1.
|
|
46
|
+
ARCH_BASELINE_SCHEMA: str = "architectural-baseline-v1"
|
|
47
|
+
ARCH_TREND_SCHEMA: str = "architectural-trend-v1"
|
|
48
|
+
# Emitted by `diff_baseline` when the two snapshots are not comparable and the
|
|
49
|
+
# caller did NOT opt into an override — a refusal, not a delta.
|
|
50
|
+
ARCH_NOT_COMPARABLE_SCHEMA: str = "architectural-not-comparable-v1"
|
|
51
|
+
|
|
52
|
+
# Comparability verdicts.
|
|
53
|
+
COMPARABLE: str = "COMPARABLE"
|
|
54
|
+
NOT_COMPARABLE: str = "NOT_COMPARABLE"
|
|
55
|
+
# `diff_baseline(..., allow_incomparable=True)` still emits a delta but stamps this
|
|
56
|
+
# so the output can never be mistaken for a clean comparison.
|
|
57
|
+
OVERRIDDEN: str = "OVERRIDDEN"
|
|
58
|
+
|
|
59
|
+
# The axes that make two architectural baselines comparable. Architectural metrics
|
|
60
|
+
# are a PURE FUNCTION OF SOURCE — they do not depend on CPU, cores, or host — so the
|
|
61
|
+
# comparability contract is SEMANTIC, not hardware. Gating on hostname/cpu (as the
|
|
62
|
+
# perf harness must, because timing IS hardware-sensitive) would be theater here and
|
|
63
|
+
# would falsely refuse a valid cross-host architectural diff. What genuinely makes a
|
|
64
|
+
# delta misleading is a change in the measurement itself: the baseline schema, the
|
|
65
|
+
# analysis (tool) version that extracted the metrics, or the underlying IR schema.
|
|
66
|
+
# A difference in any of these means a symbol/endpoint/fan-in delta could reflect the
|
|
67
|
+
# extractor changing rather than the code changing — so ASK refuses.
|
|
68
|
+
_COMPARABILITY_AXES: tuple[tuple[str, str], ...] = (
|
|
69
|
+
("schema", "baseline schema"),
|
|
70
|
+
("tool_version", "analysis (tool) version"),
|
|
71
|
+
("ir_schema_version", "IR schema"),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# How many top fan-in symbols to record as named hotspots. The FULL fan-in map is
|
|
75
|
+
# stored separately for faithful diffs; hotspots are the human-facing projection.
|
|
76
|
+
_HOTSPOT_N: int = 20
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _utc_now() -> str:
|
|
80
|
+
# Microsecond precision (DR-2): whole-second timestamps collided whenever two
|
|
81
|
+
# baselines were captured in the same second (common in CI / rapid capture), and
|
|
82
|
+
# the trend sort then tie-broke on the commit STRING — which carries no temporal
|
|
83
|
+
# order — scrambling the series. Sub-second resolution makes real captures
|
|
84
|
+
# distinct; lexicographic ISO order stays chronological.
|
|
85
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def git_commit(repo: Path) -> str | None:
|
|
89
|
+
"""Best-effort short commit of `repo`'s HEAD, or None when not a git repo."""
|
|
90
|
+
try:
|
|
91
|
+
out = subprocess.run(
|
|
92
|
+
["git", "-C", str(repo), "rev-parse", "--short", "HEAD"],
|
|
93
|
+
capture_output=True, text=True, timeout=10,
|
|
94
|
+
)
|
|
95
|
+
except (OSError, subprocess.SubprocessError):
|
|
96
|
+
return None
|
|
97
|
+
commit = out.stdout.strip()
|
|
98
|
+
return commit or None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _hotspots(fan_in: dict[str, int], top_n: int) -> list[dict]:
|
|
102
|
+
"""Top-N symbols by fan-in (desc), then FQN asc. Zero-fan-in symbols excluded."""
|
|
103
|
+
ranked = sorted(
|
|
104
|
+
((s, c) for s, c in fan_in.items() if c > 0),
|
|
105
|
+
key=lambda kv: (-kv[1], kv[0]),
|
|
106
|
+
)
|
|
107
|
+
return [{"symbol": s, "fan_in": c} for s, c in ranked[:top_n]]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def build_baseline(
|
|
111
|
+
cir: "CanonicalRepositoryIR",
|
|
112
|
+
*,
|
|
113
|
+
ref: str | None = None,
|
|
114
|
+
commit: str | None = None,
|
|
115
|
+
hotspot_n: int = _HOTSPOT_N,
|
|
116
|
+
captured_at: str | None = None,
|
|
117
|
+
env: dict | None = None,
|
|
118
|
+
tool_version: str | None = None,
|
|
119
|
+
) -> dict:
|
|
120
|
+
"""Freeze a CIR's measured architectural metrics into a baseline artifact.
|
|
121
|
+
|
|
122
|
+
Stores the full endpoint surface and fan-in map (not just hotspots) so a later
|
|
123
|
+
`diff_baseline` reproduces exactly what a two-checkout D1 delta would report.
|
|
124
|
+
Records the analysis provenance (`tool_version`, `ir_schema_version`) that the
|
|
125
|
+
comparability contract gates on — a delta is only trustworthy between baselines
|
|
126
|
+
that share them."""
|
|
127
|
+
m = extract_metrics(cir)
|
|
128
|
+
endpoints = sorted([meth, path] for (meth, path) in m.endpoints)
|
|
129
|
+
fan_in = dict(sorted(m.fan_in.items()))
|
|
130
|
+
cycles = sorted((sorted(c) for c in m.cycles), key=lambda mm: (len(mm), mm))
|
|
131
|
+
return {
|
|
132
|
+
"schema": ARCH_BASELINE_SCHEMA,
|
|
133
|
+
"tool_version": tool_version if tool_version is not None else TOOL_VERSION,
|
|
134
|
+
"ir_schema_version": str(getattr(cir, "schema_version", "")),
|
|
135
|
+
"ref": ref,
|
|
136
|
+
"commit": commit,
|
|
137
|
+
"captured_at": captured_at or _utc_now(),
|
|
138
|
+
"cir_hash": m.cir_hash,
|
|
139
|
+
"totals": {
|
|
140
|
+
"files": m.file_count,
|
|
141
|
+
"symbols": m.symbol_count,
|
|
142
|
+
"endpoints": m.endpoint_count,
|
|
143
|
+
"dependency_edges": m.dependency_edge_count,
|
|
144
|
+
"import_cycles": len(m.cycles),
|
|
145
|
+
},
|
|
146
|
+
"hotspots": _hotspots(m.fan_in, hotspot_n),
|
|
147
|
+
"endpoint_surface": endpoints,
|
|
148
|
+
"fan_in": fan_in,
|
|
149
|
+
"import_cycles": cycles,
|
|
150
|
+
"env": env if env is not None else collect_env(),
|
|
151
|
+
"provenance": (
|
|
152
|
+
"architectural_baseline (D5): a measured structural fingerprint of one "
|
|
153
|
+
"CIR, persisted per ref. Metric content is a pure function of the repo "
|
|
154
|
+
"state. No verdict, no ROI — stores and replays measured facts."
|
|
155
|
+
),
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _analysis_meta(source: dict) -> dict:
|
|
160
|
+
"""Extract the comparability axes from a baseline (or a synthesized head meta)."""
|
|
161
|
+
return {axis: source.get(axis) for axis, _ in _COMPARABILITY_AXES}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _head_meta(head_cir: "CanonicalRepositoryIR") -> dict:
|
|
165
|
+
"""The comparability meta for a live head CIR: current tool + its IR schema."""
|
|
166
|
+
return {
|
|
167
|
+
"schema": ARCH_BASELINE_SCHEMA,
|
|
168
|
+
"tool_version": TOOL_VERSION,
|
|
169
|
+
"ir_schema_version": str(getattr(head_cir, "schema_version", "")),
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def comparability(base: dict, head: dict) -> dict:
|
|
174
|
+
"""Verdict on whether two snapshots' metrics are comparable.
|
|
175
|
+
|
|
176
|
+
Gates on the SEMANTIC axes only (schema / tool version / IR schema) — see
|
|
177
|
+
`_COMPARABILITY_AXES` for why hardware is deliberately excluded. Any mismatch
|
|
178
|
+
yields NOT_COMPARABLE with an itemized reason per differing axis."""
|
|
179
|
+
reasons: list[dict] = []
|
|
180
|
+
for axis, label in _COMPARABILITY_AXES:
|
|
181
|
+
bv, hv = base.get(axis), head.get(axis)
|
|
182
|
+
if bv != hv:
|
|
183
|
+
reasons.append({
|
|
184
|
+
"axis": axis,
|
|
185
|
+
"base": bv,
|
|
186
|
+
"head": hv,
|
|
187
|
+
"detail": f"{label} differs: {bv!r} (base) vs {hv!r} (head)",
|
|
188
|
+
})
|
|
189
|
+
return {
|
|
190
|
+
"status": COMPARABLE if not reasons else NOT_COMPARABLE,
|
|
191
|
+
"reasons": reasons,
|
|
192
|
+
"base": _analysis_meta(base),
|
|
193
|
+
"head": _analysis_meta(head),
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def baseline_to_metrics(baseline: dict) -> ArchMetrics:
|
|
198
|
+
"""Reconstruct an `ArchMetrics` from a stored baseline for a faithful D1 diff."""
|
|
199
|
+
totals = baseline.get("totals", {})
|
|
200
|
+
endpoints = frozenset(
|
|
201
|
+
(str(pair[0]).upper(), str(pair[1]))
|
|
202
|
+
for pair in baseline.get("endpoint_surface", [])
|
|
203
|
+
if isinstance(pair, (list, tuple)) and len(pair) == 2
|
|
204
|
+
)
|
|
205
|
+
return ArchMetrics(
|
|
206
|
+
cir_hash=str(baseline.get("cir_hash", "")),
|
|
207
|
+
file_count=int(totals.get("files", 0)),
|
|
208
|
+
symbol_count=int(totals.get("symbols", 0)),
|
|
209
|
+
endpoint_count=int(totals.get("endpoints", 0)),
|
|
210
|
+
dependency_edge_count=int(totals.get("dependency_edges", 0)),
|
|
211
|
+
endpoints=endpoints,
|
|
212
|
+
fan_in={str(k): int(v) for k, v in (baseline.get("fan_in") or {}).items()},
|
|
213
|
+
cycles=frozenset(
|
|
214
|
+
frozenset(str(m) for m in members)
|
|
215
|
+
for members in baseline.get("import_cycles", [])
|
|
216
|
+
if isinstance(members, (list, tuple))
|
|
217
|
+
),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _base_descriptor(baseline: dict) -> dict:
|
|
222
|
+
"""Self-describing block naming the stored baseline the head was diffed against."""
|
|
223
|
+
return {
|
|
224
|
+
"ref": baseline.get("ref"),
|
|
225
|
+
"commit": baseline.get("commit"),
|
|
226
|
+
"captured_at": baseline.get("captured_at"),
|
|
227
|
+
"tool_version": baseline.get("tool_version"),
|
|
228
|
+
"ir_schema_version": baseline.get("ir_schema_version"),
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def diff_baseline(
|
|
233
|
+
baseline: dict,
|
|
234
|
+
head_cir: "CanonicalRepositoryIR",
|
|
235
|
+
*,
|
|
236
|
+
allow_incomparable: bool = False,
|
|
237
|
+
) -> dict:
|
|
238
|
+
"""Diff a live CIR against a stored baseline — D1's delta, base loaded from disk.
|
|
239
|
+
|
|
240
|
+
Comparability first: if the base and the live head disagree on the semantic axes
|
|
241
|
+
(baseline schema / analysis version / IR schema), a delta would conflate the
|
|
242
|
+
measurement change with the code change. ASK REFUSES by default, returning an
|
|
243
|
+
`architectural-not-comparable-v1` payload (status NOT_COMPARABLE) instead of a
|
|
244
|
+
misleading delta — an honest refusal is preferred over a plausible-looking lie.
|
|
245
|
+
|
|
246
|
+
Set `allow_incomparable=True` to override: the delta is still computed but stamped
|
|
247
|
+
`status=OVERRIDDEN` with the reasons, so it can never be read as a clean diff.
|
|
248
|
+
|
|
249
|
+
When comparable, the payload is the `architectural-delta-v1` schema (identical to
|
|
250
|
+
a two-checkout `ask delta`), stamped `status=COMPARABLE`, plus a `base` block."""
|
|
251
|
+
verdict = comparability(baseline, _head_meta(head_cir))
|
|
252
|
+
|
|
253
|
+
if verdict["status"] == NOT_COMPARABLE and not allow_incomparable:
|
|
254
|
+
return {
|
|
255
|
+
"schema": ARCH_NOT_COMPARABLE_SCHEMA,
|
|
256
|
+
"status": NOT_COMPARABLE,
|
|
257
|
+
"comparability": verdict,
|
|
258
|
+
"base": _base_descriptor(baseline),
|
|
259
|
+
"message": (
|
|
260
|
+
"Refusing to diff: base and head are not comparable "
|
|
261
|
+
f"({'; '.join(r['detail'] for r in verdict['reasons'])}). "
|
|
262
|
+
"A delta would conflate the analysis change with the code change. "
|
|
263
|
+
"Re-capture the base with the current tool (ask baseline capture) for "
|
|
264
|
+
"a clean diff, or pass --allow-incomparable to override."
|
|
265
|
+
),
|
|
266
|
+
"provenance": (
|
|
267
|
+
"architectural_baseline (D5): comparability refusal. Architectural "
|
|
268
|
+
"metrics are a pure function of source; a delta is only trustworthy "
|
|
269
|
+
"when the baseline schema, analysis version, and IR schema all match."
|
|
270
|
+
),
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
base = baseline_to_metrics(baseline)
|
|
274
|
+
head = extract_metrics(head_cir)
|
|
275
|
+
delta = diff_metrics(base, head)
|
|
276
|
+
delta["status"] = (
|
|
277
|
+
OVERRIDDEN if verdict["status"] == NOT_COMPARABLE else COMPARABLE
|
|
278
|
+
)
|
|
279
|
+
delta["comparability"] = verdict
|
|
280
|
+
delta["base"] = _base_descriptor(baseline)
|
|
281
|
+
return delta
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# ── Persistence I/O ───────────────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _baseline_filename(baseline: dict) -> str:
|
|
288
|
+
"""Stable filename for a baseline: commit if known, else the capture timestamp."""
|
|
289
|
+
stem = baseline.get("commit") or baseline.get("captured_at", "baseline")
|
|
290
|
+
safe = "".join(c if (c.isalnum() or c in "-_.") else "-" for c in str(stem))
|
|
291
|
+
return f"{safe}.json"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def write_baseline(baseline: dict, out_dir: Path) -> Path:
|
|
295
|
+
"""Write `baseline` as deterministic JSON under `out_dir`; return the file path."""
|
|
296
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
297
|
+
path = out_dir / _baseline_filename(baseline)
|
|
298
|
+
path.write_text(
|
|
299
|
+
json.dumps(baseline, sort_keys=True, indent=2) + "\n", encoding="utf-8"
|
|
300
|
+
)
|
|
301
|
+
return path
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def load_baseline(path: Path) -> dict:
|
|
305
|
+
"""Load and validate one baseline artifact."""
|
|
306
|
+
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
307
|
+
if not isinstance(data, dict) or data.get("schema") != ARCH_BASELINE_SCHEMA:
|
|
308
|
+
raise ValueError(
|
|
309
|
+
f"{path}: not an {ARCH_BASELINE_SCHEMA} artifact "
|
|
310
|
+
f"(schema={data.get('schema') if isinstance(data, dict) else type(data).__name__})."
|
|
311
|
+
)
|
|
312
|
+
return data
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def load_baselines_dir(directory: Path) -> list[dict]:
|
|
316
|
+
"""Load every architectural baseline in `directory`, sorted by capture time."""
|
|
317
|
+
out: list[dict] = []
|
|
318
|
+
for p in sorted(Path(directory).glob("*.json")):
|
|
319
|
+
try:
|
|
320
|
+
out.append(load_baseline(p))
|
|
321
|
+
except (ValueError, json.JSONDecodeError):
|
|
322
|
+
continue # skip foreign JSON; a baseline dir may hold unrelated files
|
|
323
|
+
# Order by capture time (DR-2). captured_at now carries microsecond resolution,
|
|
324
|
+
# so real captures are distinct and lexicographic ISO order == chronological.
|
|
325
|
+
# commit is only a deterministic fallback for the degenerate exact-timestamp tie
|
|
326
|
+
# (same instant → order is genuinely ambiguous); it never reorders distinct times.
|
|
327
|
+
out.sort(key=lambda b: (str(b.get("captured_at", "")), str(b.get("commit", ""))))
|
|
328
|
+
return out
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
# ── Trend (multi-ref series) ──────────────────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def build_trend(baselines: list[dict]) -> dict:
|
|
335
|
+
"""A multi-ref series over N baselines: totals over time + hotspot fan-in tracks.
|
|
336
|
+
|
|
337
|
+
Deterministic projection: points ordered by capture time; hotspot tracks cover
|
|
338
|
+
the union of every baseline's named hotspots, each track a per-point fan-in
|
|
339
|
+
series (0 where the symbol is absent). Reports movement only — no trajectory
|
|
340
|
+
label, no verdict."""
|
|
341
|
+
points = [
|
|
342
|
+
{
|
|
343
|
+
"ref": b.get("ref"),
|
|
344
|
+
"commit": b.get("commit"),
|
|
345
|
+
"captured_at": b.get("captured_at"),
|
|
346
|
+
"tool_version": b.get("tool_version"),
|
|
347
|
+
"ir_schema_version": b.get("ir_schema_version"),
|
|
348
|
+
"totals": b.get("totals", {}),
|
|
349
|
+
}
|
|
350
|
+
for b in baselines
|
|
351
|
+
]
|
|
352
|
+
|
|
353
|
+
# Comparability across the SERIES. A trend is not refused — cross-version history
|
|
354
|
+
# is still worth seeing — but every point-to-point transition that crosses an
|
|
355
|
+
# analysis-version boundary is flagged, because a metric jump there may be the
|
|
356
|
+
# extractor changing, not the code. The reader must attribute those with care.
|
|
357
|
+
boundaries: list[dict] = []
|
|
358
|
+
for i in range(1, len(baselines)):
|
|
359
|
+
v = comparability(baselines[i - 1], baselines[i])
|
|
360
|
+
if v["status"] == NOT_COMPARABLE:
|
|
361
|
+
boundaries.append({
|
|
362
|
+
"from_index": i - 1,
|
|
363
|
+
"to_index": i,
|
|
364
|
+
"reasons": v["reasons"],
|
|
365
|
+
})
|
|
366
|
+
distinct_versions = sorted(
|
|
367
|
+
{str(b.get("tool_version")) for b in baselines}
|
|
368
|
+
)
|
|
369
|
+
series_comparability = {
|
|
370
|
+
"status": COMPARABLE if not boundaries else NOT_COMPARABLE,
|
|
371
|
+
"distinct_tool_versions": distinct_versions,
|
|
372
|
+
"boundaries": boundaries,
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
tracked: set[str] = set()
|
|
376
|
+
for b in baselines:
|
|
377
|
+
for h in b.get("hotspots", []):
|
|
378
|
+
if isinstance(h, dict) and h.get("symbol"):
|
|
379
|
+
tracked.add(str(h["symbol"]))
|
|
380
|
+
|
|
381
|
+
hotspot_trends: list[dict] = []
|
|
382
|
+
for sym in sorted(tracked):
|
|
383
|
+
series = [int((b.get("fan_in") or {}).get(sym, 0)) for b in baselines]
|
|
384
|
+
first, last = series[0], series[-1]
|
|
385
|
+
hotspot_trends.append({
|
|
386
|
+
"symbol": sym,
|
|
387
|
+
"fan_in_series": series,
|
|
388
|
+
"first": first,
|
|
389
|
+
"last": last,
|
|
390
|
+
"delta": last - first,
|
|
391
|
+
})
|
|
392
|
+
# Largest absolute movement first, then symbol name — deterministic.
|
|
393
|
+
hotspot_trends.sort(key=lambda t: (-abs(t["delta"]), t["symbol"]))
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
"schema": ARCH_TREND_SCHEMA,
|
|
397
|
+
"baseline_count": len(baselines),
|
|
398
|
+
"comparability": series_comparability,
|
|
399
|
+
"points": points,
|
|
400
|
+
"hotspot_trends": hotspot_trends,
|
|
401
|
+
"provenance": (
|
|
402
|
+
"architectural_trend (D5): a measured series over persisted baselines. "
|
|
403
|
+
"Reports totals and fan-in movement over time; assigns no "
|
|
404
|
+
"improving/degrading label — the reading is the engineer's. Transitions "
|
|
405
|
+
"that cross an analysis-version boundary are flagged in comparability: a "
|
|
406
|
+
"metric jump there may reflect the extractor changing, not the code."
|
|
407
|
+
),
|
|
408
|
+
}
|