src2sink 3.0.0__tar.gz → 3.2.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.
- {src2sink-3.0.0/src2sink.egg-info → src2sink-3.2.0}/PKG-INFO +1 -1
- {src2sink-3.0.0 → src2sink-3.2.0}/pyproject.toml +1 -1
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/api_client_discovery.py +118 -18
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/auth_cards.py +6 -3
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/crypto_cards.py +7 -2
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/data_stores.py +55 -18
- src2sink-3.2.0/src2sink/aggregators/fleet_pass.py +120 -0
- src2sink-3.2.0/src2sink/aggregators/graphs.py +121 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/index_v2.py +15 -5
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/payload_producers.py +5 -1
- src2sink-3.2.0/src2sink/aggregators/phase3.py +48 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/pii_cross_repo.py +24 -6
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/pii_flow_v2.py +40 -9
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/queues.py +64 -15
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/ropa.py +3 -1
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/service_call_report.py +12 -4
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/build_metabase_v2.py +188 -30
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/constants.py +11 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/dependencies.py +104 -29
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/derive.py +1 -1
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/extractors/ast_walk.py +280 -13
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/extractors/file_context.py +4 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/extractors/ts_extractors.py +42 -10
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/extractors/unified.py +12 -1
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/resolve.py +21 -4
- src2sink-3.2.0/src2sink/run_timing.py +166 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/schema.py +31 -1
- {src2sink-3.0.0 → src2sink-3.2.0/src2sink.egg-info}/PKG-INFO +1 -1
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink.egg-info/SOURCES.txt +8 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_cross_repo_caller_coverage.py +1 -1
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_detection_input_coverage.py +20 -2
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_fleet_index.py +3 -1
- src2sink-3.2.0/tests/test_fleet_pass.py +222 -0
- src2sink-3.2.0/tests/test_import_style.py +88 -0
- src2sink-3.2.0/tests/test_language_support_matrix.py +481 -0
- src2sink-3.2.0/tests/test_oi36_parse_failures.py +241 -0
- src2sink-3.2.0/tests/test_oi42_promote_validation.py +241 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_phase4_taint_caps.py +2 -1
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_run_manifest.py +15 -3
- src2sink-3.2.0/tests/test_run_timing.py +221 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_silent_failure_gate.py +8 -9
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_type_declarations.py +206 -0
- src2sink-3.0.0/src2sink/aggregators/graphs.py +0 -89
- src2sink-3.0.0/src2sink/aggregators/phase3.py +0 -30
- {src2sink-3.0.0 → src2sink-3.2.0}/LICENSE +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/README.md +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/setup.cfg +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/__init__.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/__init__.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/library_source_map.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/openapi_discovery.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/openapi_edges.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/openapi_match.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/openapi_models.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/openapi_report.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/pii_lifecycle.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/pii_lifecycle_report.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/pii_touchpoint_collect.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/pii_touchpoint_utils.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/service_call_collect.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/service_call_index.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/service_call_models.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/service_calls.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/taint_buckets.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/taint_catalogs.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/taint_writers.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/aggregators/traces_index.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/checkout_scan.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/curate_internal_libraries.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/extractors/__init__.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/extractors/base.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/extractors/config.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/extractors/go.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/extractors/http_out.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/extractors/java.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/extractors/javascript.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/extractors/kotlin.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/extractors/node_factory.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/extractors/patterns.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/extractors/python_extractor.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/extractors/regex_extractors.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/extractors/symbols.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/graph_common.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/index_store.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/internal_groups.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/known_api_clients.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/library_taint_java.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/limits.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/maven.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/models/__init__.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/models/auth_model.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/models/crypto_agility.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/models/pii_lifecycle.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/models/ropa.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/paths.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/prescreen.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/py.typed +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/record_fleet_baseline.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/renderers/__init__.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/renderers/markdown.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/renderers/mermaid.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/repo_utils.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/safe_paths.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/sanitize.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/trace.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/trace_batch.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink/vocabulary.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink.egg-info/dependency_links.txt +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink.egg-info/entry_points.txt +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink.egg-info/requires.txt +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/src2sink.egg-info/top_level.txt +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_aggregate_output_golden.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_aggregators_extra.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_api_client_discovery.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_ast_walk.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_build_internals.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_build_main.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_call_observations.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_call_resolution.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_characterization.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_checkout_scan.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_config_hygiene.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_curate.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_demand_side_discovery.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_dependency_pinning.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_derive_pass.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_detection_fingerprint_gate.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_detection_version.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_discovery_scan_cost.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_entry_points.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_field_precision.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_gradle_version_catalogs.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_graph_common.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_http_guard_evidence.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_import_graph.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_internal_groups.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_limits.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_maven_resolution.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_method_structure.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_module_imports.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_oi13_kotlin_parity.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_oi26_receiver_scope.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_oi28_index_fast_path.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_oi33_canonical_repo_id.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_oi37_express_anchor.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_oi38_trace_index_freshness.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_oi39_test_path_classification.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_oi40_client_repo_target.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_openapi.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_output_neutralisation.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_path_match_significance.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_phase0_smoke.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_phase1_config.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_phase1_extractors.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_phase2_graphs.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_phase3_pii.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_phase4_aggregators.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_phase4_extractor_snapshots.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_phase4_fleet_regression.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_phase4_regression.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_polyglot_dependencies.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_post_phase4_backlog.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_prescreen.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_producer_scan_single_pass.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_redos_bounds.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_renderers.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_repo_utils_helpers.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_sanitize.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_security_input_handling.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_source_map_fix.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_sql_classifier.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_sql_payload_out.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_sql_sink_evidence.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_sql_source_construction.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_tainted_paths.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_trace_batch.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_trace_fleet_scaling.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_trace_render.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_zz_security_coverage_gate.py +0 -0
- {src2sink-3.0.0 → src2sink-3.2.0}/tests/test_zz_untrusted_interpolation_guard.py +0 -0
|
@@ -27,6 +27,7 @@ from __future__ import annotations
|
|
|
27
27
|
|
|
28
28
|
import json
|
|
29
29
|
import re
|
|
30
|
+
import sys
|
|
30
31
|
from collections import defaultdict
|
|
31
32
|
from pathlib import Path
|
|
32
33
|
from typing import Any
|
|
@@ -609,16 +610,108 @@ def _write_discovered(metabase_root: Path, entries: list[dict[str, Any]]) -> Non
|
|
|
609
610
|
)
|
|
610
611
|
|
|
611
612
|
|
|
612
|
-
def
|
|
613
|
-
"""Return the authoritative
|
|
613
|
+
def _load_bindings_file(path: Path) -> dict[str, Any]:
|
|
614
|
+
"""Return the whole authoritative file, or an empty document if unreadable.
|
|
615
|
+
|
|
616
|
+
The *whole* document, not just its bindings. `promote` used to rewrite the
|
|
617
|
+
file as `{"bindings": [...]}` and drop every other top-level key — including
|
|
618
|
+
the `_comment` carrying the "never commit — internal topology" handling
|
|
619
|
+
notice on a gitignored, sensitivity-marked file. The marker disappeared
|
|
620
|
+
silently and the file still looked correct (`OI-42`).
|
|
621
|
+
"""
|
|
614
622
|
try:
|
|
615
623
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
616
|
-
except
|
|
617
|
-
|
|
618
|
-
|
|
624
|
+
except OSError:
|
|
625
|
+
# Absent is ordinary — the first promote creates it.
|
|
626
|
+
if path.exists():
|
|
627
|
+
print(f"WARNING: could not read {path}; promoting into an empty file",
|
|
628
|
+
file=sys.stderr)
|
|
629
|
+
return {}
|
|
630
|
+
except json.JSONDecodeError as exc:
|
|
631
|
+
# Malformed is not ordinary. Silently treating it as empty would discard
|
|
632
|
+
# every existing binding on the next write, which is exactly why
|
|
633
|
+
# `--allow-empty-api-clients` exists for the *load* path (`OI-36`).
|
|
634
|
+
print(f"WARNING: {path} is not valid JSON ({exc}); its existing bindings "
|
|
635
|
+
"will not be preserved by this promote", file=sys.stderr)
|
|
636
|
+
return {}
|
|
637
|
+
return data if isinstance(data, dict) else {}
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _load_bindings(path: Path) -> list[dict[str, Any]]:
|
|
641
|
+
"""Return the authoritative bindings list, or [] if the file is missing/invalid."""
|
|
642
|
+
bindings = _load_bindings_file(path).get("bindings")
|
|
619
643
|
return [b for b in bindings if isinstance(b, dict)] if isinstance(bindings, list) else []
|
|
620
644
|
|
|
621
645
|
|
|
646
|
+
def _promotion_rejections(
|
|
647
|
+
candidate: dict[str, Any],
|
|
648
|
+
known_repos: frozenset[str],
|
|
649
|
+
endpoints: dict[str, set[str]],
|
|
650
|
+
) -> list[str]:
|
|
651
|
+
"""Why this candidate must not be promoted, or an empty list.
|
|
652
|
+
|
|
653
|
+
`promote` merged on trust and validated nothing, so every check was the
|
|
654
|
+
reviewer's to perform by hand. Two of the five documented gates are pure
|
|
655
|
+
computation the tool already does at discovery time — 50 of 191 candidates
|
|
656
|
+
in the first fleet review failed one of them (`OI-42`).
|
|
657
|
+
|
|
658
|
+
The reviewer keeps the judgement calls. This takes the arithmetic.
|
|
659
|
+
"""
|
|
660
|
+
target = str(candidate.get("target_repo") or "")
|
|
661
|
+
artifact = candidate.get("maven_artifact") or "(none)"
|
|
662
|
+
|
|
663
|
+
if not target:
|
|
664
|
+
return [f"{artifact}: target_repo is empty — it resolved to no repo at all"]
|
|
665
|
+
if target not in known_repos:
|
|
666
|
+
return [
|
|
667
|
+
f"{artifact}: target_repo {target!r} is not a repo in this metabase; "
|
|
668
|
+
"promoting it would create an edge to a node that does not exist"
|
|
669
|
+
]
|
|
670
|
+
service = _service_for_client_repo(target, endpoints)
|
|
671
|
+
if service:
|
|
672
|
+
return [
|
|
673
|
+
f"{artifact}: target_repo {target!r} declares no inbound endpoints and "
|
|
674
|
+
f"looks like a client library fronting {service!r} — a binding's "
|
|
675
|
+
"target is the service that receives the calls, not the library"
|
|
676
|
+
]
|
|
677
|
+
return []
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def _screen_for_promotion(
|
|
681
|
+
metabase_root: Path, accepted: list[dict[str, Any]]
|
|
682
|
+
) -> list[dict[str, Any]]:
|
|
683
|
+
"""Return the candidates that pass the mechanical gates, naming the rest."""
|
|
684
|
+
records = load_v2_repo_records(metabase_root)
|
|
685
|
+
known_repos = frozenset(repo_id(d) for d in records)
|
|
686
|
+
endpoints = _http_in_paths_by_repo(records)
|
|
687
|
+
|
|
688
|
+
mergeable: list[dict[str, Any]] = []
|
|
689
|
+
for cand in accepted:
|
|
690
|
+
reasons = _promotion_rejections(cand, known_repos, endpoints)
|
|
691
|
+
for reason in reasons:
|
|
692
|
+
print(f"REFUSED: {reason}", file=sys.stderr)
|
|
693
|
+
if not reasons:
|
|
694
|
+
mergeable.append(cand)
|
|
695
|
+
return mergeable
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def _merge_bindings(
|
|
699
|
+
bindings: list[dict[str, Any]],
|
|
700
|
+
index: dict[tuple[Any, Any], list[dict[str, Any]]],
|
|
701
|
+
mergeable: list[dict[str, Any]],
|
|
702
|
+
) -> None:
|
|
703
|
+
"""Merge candidates into ``bindings`` in place, refreshing every duplicate."""
|
|
704
|
+
for c in mergeable:
|
|
705
|
+
binding = {k: c[k] for k in _TUNABLE_FIELDS if k in c}
|
|
706
|
+
key = (binding.get("target_repo"), binding.get("maven_artifact"))
|
|
707
|
+
if key in index:
|
|
708
|
+
for existing in index[key]:
|
|
709
|
+
existing.update(binding)
|
|
710
|
+
else:
|
|
711
|
+
bindings.append(binding)
|
|
712
|
+
index[key].append(binding)
|
|
713
|
+
|
|
714
|
+
|
|
622
715
|
def promote_api_clients(metabase_root: Path, target_path: Path) -> int:
|
|
623
716
|
"""Merge ``accepted`` candidates into the authoritative ``api-clients.json``.
|
|
624
717
|
|
|
@@ -626,25 +719,32 @@ def promote_api_clients(metabase_root: Path, target_path: Path) -> int:
|
|
|
626
719
|
(by ``(target_repo, maven_artifact)``): a new coordinate is appended, an
|
|
627
720
|
existing one is updated in place. Idempotent — re-running merges the same set
|
|
628
721
|
without creating duplicates. Returns the number of accepted candidates merged.
|
|
722
|
+
|
|
723
|
+
Candidates failing a mechanically-checkable gate are **refused and named**
|
|
724
|
+
rather than merged, and the file's other top-level keys survive the rewrite
|
|
725
|
+
(`OI-42`).
|
|
629
726
|
"""
|
|
630
727
|
candidates = _load_discovered(metabase_root / DISCOVERED_FILE).values()
|
|
631
728
|
accepted = [c for c in candidates if c.get("status") == STATUS_ACCEPTED]
|
|
632
729
|
if not accepted:
|
|
633
730
|
return 0
|
|
634
731
|
|
|
635
|
-
|
|
636
|
-
index = {(b.get("target_repo"), b.get("maven_artifact")): b for b in bindings}
|
|
637
|
-
for c in accepted:
|
|
638
|
-
binding = {k: c[k] for k in _TUNABLE_FIELDS if k in c}
|
|
639
|
-
key = (binding.get("target_repo"), binding.get("maven_artifact"))
|
|
640
|
-
if key in index:
|
|
641
|
-
index[key].update(binding)
|
|
642
|
-
else:
|
|
643
|
-
bindings.append(binding)
|
|
644
|
-
index[key] = binding
|
|
732
|
+
mergeable = _screen_for_promotion(metabase_root, accepted)
|
|
645
733
|
|
|
734
|
+
document = _load_bindings_file(target_path)
|
|
735
|
+
bindings = _load_bindings(target_path)
|
|
736
|
+
# Every copy of a key, not just the last. Indexing a list that may hold
|
|
737
|
+
# duplicates into a dict keeps only the final one, so earlier copies were
|
|
738
|
+
# left at their old values — still present, still loaded, now inconsistent.
|
|
739
|
+
index: dict[tuple[Any, Any], list[dict[str, Any]]] = defaultdict(list)
|
|
740
|
+
for b in bindings:
|
|
741
|
+
index[(b.get("target_repo"), b.get("maven_artifact"))].append(b)
|
|
742
|
+
_merge_bindings(bindings, index, mergeable)
|
|
743
|
+
|
|
744
|
+
# The whole document, so `_comment` — which carries the "never commit"
|
|
745
|
+
# handling notice on a gitignored, sensitivity-marked file — survives.
|
|
746
|
+
document["bindings"] = bindings
|
|
646
747
|
target_path.write_text(
|
|
647
|
-
json.dumps(
|
|
648
|
-
encoding="utf-8",
|
|
748
|
+
json.dumps(document, indent=2, sort_keys=False) + "\n", encoding="utf-8",
|
|
649
749
|
)
|
|
650
|
-
return len(
|
|
750
|
+
return len(mergeable)
|
|
@@ -7,7 +7,8 @@ from collections import Counter
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Any
|
|
9
9
|
|
|
10
|
-
from
|
|
10
|
+
from .fleet_pass import records_or_load
|
|
11
|
+
from ..graph_common import iter_nodes, repo_id
|
|
11
12
|
from ..renderers.markdown import md_table
|
|
12
13
|
from ..sanitize import UNTRUSTED_CONTENT_NOTICE
|
|
13
14
|
|
|
@@ -60,10 +61,12 @@ def _collect_repo_auth(data: dict[str, Any]) -> dict[str, Any]:
|
|
|
60
61
|
def write_auth_models_catalog(
|
|
61
62
|
metabase_root: Path,
|
|
62
63
|
repo_jsons: list[Path] | None = None,
|
|
64
|
+
*,
|
|
65
|
+
cards: list[dict[str, Any]] | None = None,
|
|
63
66
|
) -> list[dict[str, Any]]:
|
|
64
67
|
"""Write auth-model cards (JSONL + markdown) to conventions/ and return them."""
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
if cards is None:
|
|
69
|
+
cards = [_collect_repo_auth(d) for d in records_or_load(None, metabase_root, repo_jsons)]
|
|
67
70
|
|
|
68
71
|
conv_dir = metabase_root / "conventions"
|
|
69
72
|
conv_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -68,10 +68,15 @@ def _collect_repo_crypto(data: dict[str, Any]) -> dict[str, Any]:
|
|
|
68
68
|
def write_crypto_agility_catalog(
|
|
69
69
|
metabase_root: Path,
|
|
70
70
|
repo_jsons: list[Path] | None = None,
|
|
71
|
+
*,
|
|
72
|
+
cards: list[dict[str, Any]] | None = None,
|
|
71
73
|
) -> list[dict[str, Any]]:
|
|
72
74
|
"""Write crypto-agility cards (JSONL + markdown) to conventions/ and return them."""
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
if cards is None:
|
|
76
|
+
cards = [
|
|
77
|
+
_collect_repo_crypto(data)
|
|
78
|
+
for data in load_v2_repo_records(metabase_root, json_paths=repo_jsons)
|
|
79
|
+
]
|
|
75
80
|
|
|
76
81
|
conv_dir = metabase_root / "conventions"
|
|
77
82
|
conv_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -12,31 +12,56 @@ from ..sanitize import UNTRUSTED_CONTENT_NOTICE, for_markdown
|
|
|
12
12
|
from ..renderers.markdown import md_table
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
15
|
+
class StoreCollector:
|
|
16
|
+
"""Store->repos, store metadata, and per-repo SQL execution sink counts.
|
|
17
|
+
|
|
18
|
+
The reduction `_collect_stores` performed, turned inside out so it can run
|
|
19
|
+
inside a shared streamed pass (`OI-41`). It retains only what it derives —
|
|
20
|
+
a few thousand store keys — never the records it derived them from.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
"""Start an empty reduction."""
|
|
25
|
+
self.store_repos: dict[str, set[str]] = defaultdict(set)
|
|
26
|
+
self.store_meta: dict[str, dict[str, Any]] = {}
|
|
27
|
+
self.sql_execution_by_repo: Counter[str] = Counter()
|
|
28
|
+
|
|
29
|
+
def consume(self, record: dict[str, Any]) -> None:
|
|
30
|
+
"""Fold one repo record in."""
|
|
31
|
+
rid = repo_id(record)
|
|
32
|
+
for node in iter_nodes(record):
|
|
25
33
|
family = node.get("family", "")
|
|
26
34
|
kind = node.get("kind")
|
|
27
35
|
if family == "data-store" and kind == "store":
|
|
28
36
|
key = store_key_from_node(node)
|
|
29
37
|
if not key:
|
|
30
38
|
continue
|
|
31
|
-
store_repos[key].add(rid)
|
|
39
|
+
self.store_repos[key].add(rid)
|
|
32
40
|
detail = node.get("detail") or {}
|
|
33
|
-
store_meta.setdefault(key, {
|
|
41
|
+
self.store_meta.setdefault(key, {
|
|
34
42
|
"vendor": detail.get("vendor", "?"),
|
|
35
43
|
"sample_file": f"{node.get('file')}:{node.get('line')}",
|
|
36
44
|
})
|
|
37
45
|
elif family == "sql" and kind == "sink" and (node.get("detail") or {}).get("execution", True):
|
|
38
|
-
sql_execution_by_repo[rid] += 1
|
|
39
|
-
|
|
46
|
+
self.sql_execution_by_repo[rid] += 1
|
|
47
|
+
|
|
48
|
+
def result(self) -> tuple[dict[str, set[str]], dict[str, dict[str, Any]], "Counter[str]"]:
|
|
49
|
+
"""The finished reduction, in the shape the renderer expects."""
|
|
50
|
+
return self.store_repos, self.store_meta, self.sql_execution_by_repo
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _collect_stores(
|
|
54
|
+
records: list[dict[str, Any]],
|
|
55
|
+
) -> tuple[dict[str, set[str]], dict[str, dict[str, Any]], "Counter[str]"]:
|
|
56
|
+
"""Collect store->repos, store metadata, and per-repo SQL execution sink counts.
|
|
57
|
+
|
|
58
|
+
Retained as the non-streaming entry point; the reduction itself lives in
|
|
59
|
+
:class:`StoreCollector` so one pass can drive it alongside the others.
|
|
60
|
+
"""
|
|
61
|
+
collector = StoreCollector()
|
|
62
|
+
for data in records:
|
|
63
|
+
collector.consume(data)
|
|
64
|
+
return collector.result()
|
|
40
65
|
|
|
41
66
|
|
|
42
67
|
def _stores_by_vendor(
|
|
@@ -65,10 +90,22 @@ def _write_data_store_jsonl(
|
|
|
65
90
|
}, ensure_ascii=False) + "\n")
|
|
66
91
|
|
|
67
92
|
|
|
68
|
-
def write_data_store_graph(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
93
|
+
def write_data_store_graph(
|
|
94
|
+
metabase_root: Path,
|
|
95
|
+
repo_jsons: list[Path],
|
|
96
|
+
*,
|
|
97
|
+
collected: tuple[dict[str, set[str]], dict[str, dict[str, Any]], "Counter[str]"] | None = None,
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Write the bipartite data-store graph markdown + jsonl from v2 store nodes.
|
|
100
|
+
|
|
101
|
+
``collected`` lets a caller that has already streamed the fleet hand the
|
|
102
|
+
reduction in, rather than this parsing the metabase again (`OI-41`).
|
|
103
|
+
"""
|
|
104
|
+
if collected is None:
|
|
105
|
+
collected = _collect_stores(
|
|
106
|
+
load_v2_repo_records(metabase_root, json_paths=repo_jsons)
|
|
107
|
+
)
|
|
108
|
+
store_repos, store_meta, sql_execution_by_repo = collected
|
|
72
109
|
|
|
73
110
|
out_dir = metabase_root / "graphs"
|
|
74
111
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""One streamed pass over the metabase, shared by every aggregator (`OI-41`).
|
|
2
|
+
|
|
3
|
+
Aggregation parsed the whole metabase **fourteen times per run** — 2.2 GB each on
|
|
4
|
+
the observed fleet, so ~31 GB of JSON decoded to read the same bytes over and
|
|
5
|
+
over. Measured A/B, that repetition is **67% of aggregation time**, and
|
|
6
|
+
aggregation is 78% of the run.
|
|
7
|
+
|
|
8
|
+
The one-line fix is the wrong line. Memoising the load removes the repetition and
|
|
9
|
+
keeps one *held* copy of the fleet where re-parsing created and discarded:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
peak RSS, 14 parses : 148 MB
|
|
13
|
+
peak RSS, 1 parse : 266 MB (+118 MB on a 29 MB metabase)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
At the measured ~6.5x expansion that is several more GB resident on a fleet whose
|
|
17
|
+
aggregation already peaks at 5.75 GiB, on a host observed swapping — `OI-15`'s
|
|
18
|
+
ceiling, reached through the fix.
|
|
19
|
+
|
|
20
|
+
So: **stream once, and reduce as you go.** Each collector sees every record in
|
|
21
|
+
turn and retains only its own reduced state, which is orders smaller than the
|
|
22
|
+
records it came from. Time is one parse; memory is one record plus the results.
|
|
23
|
+
|
|
24
|
+
Every aggregator's `_collect_*` was already a pure `for data in records:`
|
|
25
|
+
reduction, so a collector is that loop turned inside out — accumulators in
|
|
26
|
+
`__init__`, body in `consume`, return in `result`. The conversion is mechanical,
|
|
27
|
+
and `tests/test_aggregate_output_golden.py` pins all 27 generated artefacts, so
|
|
28
|
+
one that changes any output fails immediately.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from collections.abc import Callable, Iterable
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Any, Generic, Protocol, TypeVar
|
|
36
|
+
|
|
37
|
+
from ..graph_common import iter_v2_repo_records, load_v2_repo_records
|
|
38
|
+
|
|
39
|
+
# Covariant: a collector only ever *produces* its result type, so a
|
|
40
|
+
# `RecordCollector[QueueGraph]` is usable wherever a `RecordCollector[object]`
|
|
41
|
+
# is expected.
|
|
42
|
+
T_co = TypeVar("T_co", covariant=True)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class RecordCollector(Protocol[T_co]):
|
|
46
|
+
"""A reduction over the fleet that sees one record at a time.
|
|
47
|
+
|
|
48
|
+
The contract is deliberately narrow: `consume` may not retain the record it
|
|
49
|
+
is given, because retaining records is the memory cost this exists to avoid.
|
|
50
|
+
It retains whatever it derives from them instead.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def consume(self, record: dict[str, Any]) -> None:
|
|
54
|
+
"""Fold one repo record into the accumulating result.
|
|
55
|
+
|
|
56
|
+
Must not retain ``record``: holding records is the memory cost this
|
|
57
|
+
whole mechanism exists to avoid.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def result(self) -> T_co:
|
|
61
|
+
"""The finished reduction. Called once, after the pass."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def run_fleet_pass(
|
|
65
|
+
metabase_root: Path,
|
|
66
|
+
collectors: Iterable[RecordCollector[Any]],
|
|
67
|
+
*,
|
|
68
|
+
json_paths: list[Path] | None = None,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Stream every record once, offering it to every collector.
|
|
71
|
+
|
|
72
|
+
The ordering guarantee matters: records arrive in the same order
|
|
73
|
+
`load_v2_repo_records` produced them, so a collector whose output depends on
|
|
74
|
+
record order — several sort only at the end — is unaffected by the move.
|
|
75
|
+
"""
|
|
76
|
+
live = list(collectors)
|
|
77
|
+
if not live:
|
|
78
|
+
return
|
|
79
|
+
for record in iter_v2_repo_records(metabase_root, json_paths=json_paths):
|
|
80
|
+
for collector in live:
|
|
81
|
+
collector.consume(record)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class MapCollector(Generic[T_co]):
|
|
85
|
+
"""Applies a per-record function and keeps the results, in record order.
|
|
86
|
+
|
|
87
|
+
Several aggregators reduce with a plain `[fn(data) for data in records]` —
|
|
88
|
+
one card per repo. They share this rather than each restating the loop, and
|
|
89
|
+
it keeps the derived cards while the records that produced them are released.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(self, fn: Callable[[dict[str, Any]], T_co]) -> None:
|
|
93
|
+
"""Collect ``fn`` applied to every record."""
|
|
94
|
+
self._fn = fn
|
|
95
|
+
self._results: list[T_co] = []
|
|
96
|
+
|
|
97
|
+
def consume(self, record: dict[str, Any]) -> None:
|
|
98
|
+
"""Map one record and keep only what came back."""
|
|
99
|
+
self._results.append(self._fn(record))
|
|
100
|
+
|
|
101
|
+
def result(self) -> list[T_co]:
|
|
102
|
+
"""The mapped results, in the order the records arrived."""
|
|
103
|
+
return self._results
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def records_or_load(
|
|
107
|
+
records: list[dict[str, Any]] | None,
|
|
108
|
+
metabase_root: Path,
|
|
109
|
+
json_paths: list[Path] | None = None,
|
|
110
|
+
) -> list[dict[str, Any]]:
|
|
111
|
+
"""Return the caller's records, loading the fleet only if it supplied none.
|
|
112
|
+
|
|
113
|
+
Every converted aggregator keeps a non-streaming entry point so it still
|
|
114
|
+
works when called directly. Writing that fallback as a guard clause inside
|
|
115
|
+
each `write_*` added a branch to functions already at the complexity limit,
|
|
116
|
+
for no reader benefit — the interesting code is below it.
|
|
117
|
+
"""
|
|
118
|
+
if records is not None:
|
|
119
|
+
return records
|
|
120
|
+
return load_v2_repo_records(metabase_root, json_paths=json_paths)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Orchestrate v2 cross-repo graph generation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..graph_common import (
|
|
9
|
+
iter_v2_repo_records,
|
|
10
|
+
load_v2_repo_records,
|
|
11
|
+
v2_record_paths,
|
|
12
|
+
)
|
|
13
|
+
from ..index_store import build_index
|
|
14
|
+
from .. import run_timing
|
|
15
|
+
from .data_stores import StoreCollector, write_data_store_graph
|
|
16
|
+
from .index_v2 import _row_from_record, write_index_v2
|
|
17
|
+
from .openapi_edges import write_openapi_artifacts
|
|
18
|
+
from .payload_producers import build_producer_indices, write_payload_producer_index
|
|
19
|
+
from .phase3 import aggregate_phase3_v2
|
|
20
|
+
from .pii_flow_v2 import PiiFlowCollector, write_pii_flow_v2
|
|
21
|
+
from .fleet_pass import MapCollector, run_fleet_pass
|
|
22
|
+
from .queues import QueueCollector, write_queue_graph
|
|
23
|
+
from .service_calls import collect_service_edges, write_service_call_graph
|
|
24
|
+
from .traces_index import write_traces_index
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def write_fleet_index(
|
|
28
|
+
metabase_root: Path,
|
|
29
|
+
repo_jsons: list[Path],
|
|
30
|
+
*,
|
|
31
|
+
repos_root: Path | None = None,
|
|
32
|
+
producer_indices: list[Any] | None = None,
|
|
33
|
+
call_edges: list[Any] | None = None,
|
|
34
|
+
) -> Path:
|
|
35
|
+
"""Persist the four things a trace consults, keyed by target repo (`OI-15`).
|
|
36
|
+
|
|
37
|
+
Built here because aggregation already walks every repo, so the index is one
|
|
38
|
+
more pass over data that is being read anyway rather than a second crawl.
|
|
39
|
+
|
|
40
|
+
The two fleet-wide derivations are computed the same way `trace` computed
|
|
41
|
+
them, from the same functions — the index is a cache of this code's output,
|
|
42
|
+
not a reimplementation of it, which is what keeps the two from drifting.
|
|
43
|
+
|
|
44
|
+
``producer_indices`` is taken from the caller when aggregation has already
|
|
45
|
+
built it. Rebuilding it here meant the fleet's source tree was scanned a
|
|
46
|
+
second time, which doubled the slowest step of the whole run (`OI-30`).
|
|
47
|
+
"""
|
|
48
|
+
paths = v2_record_paths(metabase_root, json_paths=repo_jsons)
|
|
49
|
+
records: list[dict[str, Any]] | None = None
|
|
50
|
+
if call_edges is None:
|
|
51
|
+
# Fleet-wide and target-independent, and three consumers each recomputed
|
|
52
|
+
# it — the derivation `OI-14` identified as dominating cost (`OI-41`).
|
|
53
|
+
records = load_v2_repo_records(metabase_root, json_paths=paths)
|
|
54
|
+
call_edges, _unmatched = collect_service_edges(records)
|
|
55
|
+
if producer_indices is None:
|
|
56
|
+
producer_indices = build_producer_indices(
|
|
57
|
+
metabase_root, repos_root=repos_root, json_paths=paths,
|
|
58
|
+
)
|
|
59
|
+
del records # the streamed pass below must not be served from this list
|
|
60
|
+
|
|
61
|
+
return build_index(
|
|
62
|
+
metabase_root,
|
|
63
|
+
paths,
|
|
64
|
+
call_edges=call_edges,
|
|
65
|
+
producer_indices=producer_indices,
|
|
66
|
+
records=iter_v2_repo_records(metabase_root, json_paths=paths),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def aggregate_graphs_v2(
|
|
71
|
+
metabase_root: Path,
|
|
72
|
+
repo_jsons: list[Path],
|
|
73
|
+
*,
|
|
74
|
+
repos_root: Path | None = None,
|
|
75
|
+
phase3: bool = True,
|
|
76
|
+
fleet_index: bool = True,
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Run all v2 cross-repo graph writers (service, queue, data-store, PII, phase 3)."""
|
|
79
|
+
# One load serves the report and the index, and the edges it computes are
|
|
80
|
+
# handed on rather than recomputed (`OI-41`).
|
|
81
|
+
with run_timing.phase("shared-load"):
|
|
82
|
+
shared = load_v2_repo_records(metabase_root, json_paths=repo_jsons)
|
|
83
|
+
with run_timing.phase("service-call-graph"):
|
|
84
|
+
call_edges, _unmatched = write_service_call_graph(
|
|
85
|
+
metabase_root, repo_jsons, repos_root=repos_root, records=shared,
|
|
86
|
+
)
|
|
87
|
+
if repos_root:
|
|
88
|
+
with run_timing.phase("openapi"):
|
|
89
|
+
write_openapi_artifacts(metabase_root, repos_root, repo_jsons)
|
|
90
|
+
# One streamed pass drives every converted aggregator, instead of each
|
|
91
|
+
# parsing the whole metabase for itself (`OI-41`). Collectors retain their
|
|
92
|
+
# reductions, never the records — so this buys the time without the resident
|
|
93
|
+
# copy that memoising would cost (`OI-15`).
|
|
94
|
+
queues, stores = QueueCollector(), StoreCollector()
|
|
95
|
+
index_rows = MapCollector(_row_from_record)
|
|
96
|
+
pii_flow = PiiFlowCollector()
|
|
97
|
+
with run_timing.phase("streamed-pass"):
|
|
98
|
+
run_fleet_pass(
|
|
99
|
+
metabase_root, (queues, stores, index_rows, pii_flow), json_paths=repo_jsons,
|
|
100
|
+
)
|
|
101
|
+
with run_timing.phase("queue-and-store-graphs"):
|
|
102
|
+
write_queue_graph(metabase_root, repo_jsons, graph=queues.result())
|
|
103
|
+
write_data_store_graph(metabase_root, repo_jsons, collected=stores.result())
|
|
104
|
+
with run_timing.phase("payload-producers"):
|
|
105
|
+
producer_indices = write_payload_producer_index(
|
|
106
|
+
metabase_root, repo_jsons, repos_root=repos_root, records=shared,
|
|
107
|
+
)
|
|
108
|
+
del shared # released before the streamed pass below
|
|
109
|
+
with run_timing.phase("index-and-pii"):
|
|
110
|
+
write_index_v2(metabase_root, repo_jsons, rows=index_rows.result())
|
|
111
|
+
write_pii_flow_v2(metabase_root, repo_jsons, counts=pii_flow.result())
|
|
112
|
+
write_traces_index(metabase_root)
|
|
113
|
+
if phase3:
|
|
114
|
+
with run_timing.phase("phase3"):
|
|
115
|
+
aggregate_phase3_v2(metabase_root, repo_jsons, edges=call_edges)
|
|
116
|
+
if fleet_index:
|
|
117
|
+
with run_timing.phase("fleet-index"):
|
|
118
|
+
write_fleet_index(
|
|
119
|
+
metabase_root, repo_jsons, repos_root=repos_root,
|
|
120
|
+
producer_indices=producer_indices, call_edges=call_edges,
|
|
121
|
+
)
|
|
@@ -8,7 +8,8 @@ from collections import Counter, defaultdict
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
|
-
from
|
|
11
|
+
from .fleet_pass import records_or_load
|
|
12
|
+
from ..graph_common import iter_nodes
|
|
12
13
|
from ..renderers.markdown import md_table
|
|
13
14
|
|
|
14
15
|
|
|
@@ -43,10 +44,19 @@ def _row_from_record(data: dict[str, Any]) -> dict[str, Any]:
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
|
|
46
|
-
def write_index_v2(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
def write_index_v2(
|
|
48
|
+
metabase_root: Path,
|
|
49
|
+
repo_jsons: list[Path],
|
|
50
|
+
*,
|
|
51
|
+
rows: list[dict[str, Any]] | None = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Write the v2 repo index JSON and the all/by-group/by-language markdown pages.
|
|
54
|
+
|
|
55
|
+
``rows`` lets a caller that has already streamed the fleet hand the mapped
|
|
56
|
+
rows in, rather than this parsing the metabase again (`OI-41`).
|
|
57
|
+
"""
|
|
58
|
+
if rows is None:
|
|
59
|
+
rows = [_row_from_record(d) for d in records_or_load(None, metabase_root, repo_jsons)]
|
|
50
60
|
index_dir = metabase_root / "index"
|
|
51
61
|
index_dir.mkdir(parents=True, exist_ok=True)
|
|
52
62
|
|
|
@@ -329,6 +329,7 @@ def build_producer_indices(
|
|
|
329
329
|
*,
|
|
330
330
|
repos_root: Path | None = None,
|
|
331
331
|
json_paths: list[Path] | None = None,
|
|
332
|
+
records: list[dict[str, Any]] | None = None,
|
|
332
333
|
) -> list[ProducerIndex]:
|
|
333
334
|
"""Build a producer index per known API-client binding.
|
|
334
335
|
|
|
@@ -343,7 +344,8 @@ def build_producer_indices(
|
|
|
343
344
|
Returns:
|
|
344
345
|
One ProducerIndex per binding, hits sorted by (source_repo, kind).
|
|
345
346
|
"""
|
|
346
|
-
records
|
|
347
|
+
if records is None:
|
|
348
|
+
records = load_v2_repo_records(metabase_root, json_paths=json_paths)
|
|
347
349
|
bindings = list(get_bindings())
|
|
348
350
|
|
|
349
351
|
# One walk of the fleet for every binding, rather than one walk each. The
|
|
@@ -403,6 +405,7 @@ def write_payload_producer_index(
|
|
|
403
405
|
repo_jsons: list[Path],
|
|
404
406
|
*,
|
|
405
407
|
repos_root: Path | None = None,
|
|
408
|
+
records: list[dict[str, Any]] | None = None,
|
|
406
409
|
) -> list[ProducerIndex]:
|
|
407
410
|
"""Write the payload-endpoint-producers markdown + jsonl catalogue.
|
|
408
411
|
|
|
@@ -419,6 +422,7 @@ def write_payload_producer_index(
|
|
|
419
422
|
metabase_root,
|
|
420
423
|
repos_root=root,
|
|
421
424
|
json_paths=repo_jsons,
|
|
425
|
+
records=records,
|
|
422
426
|
)
|
|
423
427
|
|
|
424
428
|
out_dir = metabase_root / "graphs"
|