arcgraph 0.1.0__py3-none-any.whl
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.
- arcgraph/__init__.py +41 -0
- arcgraph/_build_provenance.json +10 -0
- arcgraph/adapters/__init__.py +6 -0
- arcgraph/adapters/arq_adapter.py +369 -0
- arcgraph/adapters/celery_adapter.py +440 -0
- arcgraph/adapters/common.py +436 -0
- arcgraph/adapters/django_adapter.py +765 -0
- arcgraph/adapters/fastapi_adapter.py +415 -0
- arcgraph/adapters/logging_adapter.py +335 -0
- arcgraph/adapters/mcp_adapter.py +100 -0
- arcgraph/adapters/pydantic_adapter.py +290 -0
- arcgraph/adapters/pytest_adapter.py +221 -0
- arcgraph/adapters/pytest_collection.py +96 -0
- arcgraph/adapters/pytest_fixtures.py +371 -0
- arcgraph/adapters/pytest_types.py +251 -0
- arcgraph/adapters/registry.py +34 -0
- arcgraph/adapters/service_container_adapter.py +548 -0
- arcgraph/adapters/sqlalchemy_adapter.py +446 -0
- arcgraph/adapters/typer_click_adapter.py +292 -0
- arcgraph/analyzers/__init__.py +1 -0
- arcgraph/analyzers/bindings.py +782 -0
- arcgraph/analyzers/calls/__init__.py +6 -0
- arcgraph/analyzers/calls/analyzer.py +2921 -0
- arcgraph/analyzers/calls/constants.py +702 -0
- arcgraph/analyzers/calls/context.py +298 -0
- arcgraph/analyzers/calls/lexical.py +237 -0
- arcgraph/analyzers/coverage.py +826 -0
- arcgraph/analyzers/exports.py +46 -0
- arcgraph/analyzers/har_trace.py +323 -0
- arcgraph/analyzers/imports.py +190 -0
- arcgraph/analyzers/otel_trace.py +352 -0
- arcgraph/analyzers/precise_references.py +1199 -0
- arcgraph/analyzers/python_scopes.py +56 -0
- arcgraph/analyzers/runtime_trace.py +516 -0
- arcgraph/analyzers/similarity.py +430 -0
- arcgraph/analyzers/symbols.py +363 -0
- arcgraph/analyzers/type_unions.py +89 -0
- arcgraph/analyzers/types.py +2293 -0
- arcgraph/assets/__init__.py +1 -0
- arcgraph/assets/workbench/__init__.py +1 -0
- arcgraph/assets/workbench/index.html +2398 -0
- arcgraph/assets/workbench/vendor/LICENSE.d3.txt +15 -0
- arcgraph/assets/workbench/vendor/d3.v7.min.js +2 -0
- arcgraph/change/__init__.py +10 -0
- arcgraph/change/baseline.py +145 -0
- arcgraph/change/changed_regions.py +728 -0
- arcgraph/change/contracts.py +927 -0
- arcgraph/change/errors.py +88 -0
- arcgraph/change/evidence.py +289 -0
- arcgraph/change/graph_delta.py +1422 -0
- arcgraph/change/identities.py +1277 -0
- arcgraph/change/paths.py +234 -0
- arcgraph/change/pins.py +426 -0
- arcgraph/change/planner.py +504 -0
- arcgraph/change/protected_surfaces.py +328 -0
- arcgraph/change/provider.py +46 -0
- arcgraph/change/service.py +1030 -0
- arcgraph/change/source_provider.py +311 -0
- arcgraph/change/store.py +1138 -0
- arcgraph/change/trusted_runners.py +154 -0
- arcgraph/change/verdicts.py +141 -0
- arcgraph/change/verification_plan.py +113 -0
- arcgraph/change/verifier.py +579 -0
- arcgraph/core/__init__.py +1 -0
- arcgraph/core/assurance.py +343 -0
- arcgraph/core/binding_metrics.py +75 -0
- arcgraph/core/cleanup.py +701 -0
- arcgraph/core/confidence_profiles.py +108 -0
- arcgraph/core/evidence_manifest.py +862 -0
- arcgraph/core/expression_kinds.py +11 -0
- arcgraph/core/force_graph_export.py +1125 -0
- arcgraph/core/freshness.py +94 -0
- arcgraph/core/graph_store.py +1099 -0
- arcgraph/core/ids.py +206 -0
- arcgraph/core/language_tiers.py +150 -0
- arcgraph/core/merge.py +649 -0
- arcgraph/core/metadata.py +30 -0
- arcgraph/core/operation_lock.py +218 -0
- arcgraph/core/passes.py +142 -0
- arcgraph/core/payload_policy.py +708 -0
- arcgraph/core/precise_references.py +124 -0
- arcgraph/core/query_engine.py +3078 -0
- arcgraph/core/recovery.py +18 -0
- arcgraph/core/scanner.py +1454 -0
- arcgraph/core/schemas.py +700 -0
- arcgraph/core/semantic.py +263 -0
- arcgraph/core/semantic_metrics.py +619 -0
- arcgraph/core/sharing_retry.py +84 -0
- arcgraph/core/structural.py +260 -0
- arcgraph/core/target_resolver.py +486 -0
- arcgraph/core/type_metrics.py +67 -0
- arcgraph/core/unresolved_classification.py +471 -0
- arcgraph/core/utils.py +100 -0
- arcgraph/core/visual_contract.py +240 -0
- arcgraph/core/visual_slice_engine.py +933 -0
- arcgraph/interfaces/__init__.py +1 -0
- arcgraph/interfaces/agent_capabilities.py +493 -0
- arcgraph/interfaces/agent_help.py +313 -0
- arcgraph/interfaces/benchmark.py +548 -0
- arcgraph/interfaces/ci.py +1992 -0
- arcgraph/interfaces/cli.py +2799 -0
- arcgraph/interfaces/cli_change.py +690 -0
- arcgraph/interfaces/cli_feedback.py +176 -0
- arcgraph/interfaces/cli_ops.py +69 -0
- arcgraph/interfaces/cli_support.py +113 -0
- arcgraph/interfaces/cli_trial.py +42 -0
- arcgraph/interfaces/cli_visual.py +601 -0
- arcgraph/interfaces/cli_workspace.py +300 -0
- arcgraph/interfaces/client_setup.py +481 -0
- arcgraph/interfaces/docs.py +1255 -0
- arcgraph/interfaces/human_output.py +409 -0
- arcgraph/interfaces/local_state.py +577 -0
- arcgraph/interfaces/mcp_server.py +457 -0
- arcgraph/interfaces/mcp_tools.py +1209 -0
- arcgraph/interfaces/metrics.py +571 -0
- arcgraph/interfaces/ops.py +330 -0
- arcgraph/interfaces/precision.py +1670 -0
- arcgraph/interfaces/project_probe.py +75 -0
- arcgraph/interfaces/reports.py +1121 -0
- arcgraph/interfaces/stdio_encoding.py +29 -0
- arcgraph/interfaces/trace.py +592 -0
- arcgraph/interfaces/trial_feedback.py +519 -0
- arcgraph/interfaces/trial_setup.py +535 -0
- arcgraph/interfaces/visual_server.py +354 -0
- arcgraph/interfaces/visual_slice.py +168 -0
- arcgraph/interfaces/visual_smoke.py +666 -0
- arcgraph/interfaces/workbench.py +576 -0
- arcgraph/interfaces/workspace.py +1513 -0
- arcgraph/pipeline/__init__.py +1 -0
- arcgraph/pipeline/contracts.py +122 -0
- arcgraph/pipeline/external_frontend.py +451 -0
- arcgraph/pipeline/external_l3_frontends.py +368 -0
- arcgraph/pipeline/external_protocol.py +358 -0
- arcgraph/pipeline/frontends.py +437 -0
- arcgraph/pipeline/indexer.py +661 -0
- arcgraph/pipeline/openapi_frontend.py +708 -0
- arcgraph/pipeline/python_frontend.py +298 -0
- arcgraph/pipeline/reindexer.py +1333 -0
- arcgraph/pipeline/scip_frontend.py +726 -0
- arcgraph/pipeline/typescript_extractor/declarations.mjs +183 -0
- arcgraph/pipeline/typescript_extractor/edges.mjs +2794 -0
- arcgraph/pipeline/typescript_extractor/graph_primitives.mjs +189 -0
- arcgraph/pipeline/typescript_extractor/module_resolution.mjs +877 -0
- arcgraph/pipeline/typescript_extractor/runtime.mjs +56 -0
- arcgraph/pipeline/typescript_extractor/similarity.mjs +619 -0
- arcgraph/pipeline/typescript_extractor/similarity_runner.mjs +19 -0
- arcgraph/pipeline/typescript_extractor/sources.mjs +23 -0
- arcgraph/pipeline/typescript_extractor.mjs +177 -0
- arcgraph/pipeline/typescript_frameworks.py +947 -0
- arcgraph/pipeline/typescript_frontend.py +656 -0
- arcgraph/pipeline/typescript_references.mjs +167 -0
- arcgraph/providers/__init__.py +27 -0
- arcgraph/providers/context_provider.py +1258 -0
- arcgraph/providers/flow_payload.py +117 -0
- arcgraph/providers/memory_connector.py +216 -0
- arcgraph/providers/orchestrator.py +475 -0
- arcgraph/providers/relationship_payload.py +102 -0
- arcgraph/providers/risk_provider.py +843 -0
- arcgraph/providers/why_provider.py +349 -0
- arcgraph/version_info.py +246 -0
- arcgraph-0.1.0.dist-info/METADATA +676 -0
- arcgraph-0.1.0.dist-info/RECORD +165 -0
- arcgraph-0.1.0.dist-info/WHEEL +4 -0
- arcgraph-0.1.0.dist-info/entry_points.txt +2 -0
- arcgraph-0.1.0.dist-info/licenses/LICENSE +203 -0
|
@@ -0,0 +1,2799 @@
|
|
|
1
|
+
"""Command line interface for ArcGraph."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
import traceback
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, cast
|
|
14
|
+
|
|
15
|
+
from arcgraph import __version__
|
|
16
|
+
from arcgraph.change.errors import ChangeSafetyError
|
|
17
|
+
from arcgraph.pipeline.indexer import ArcGraphIndexer
|
|
18
|
+
from arcgraph.pipeline.external_protocol import summarize_toolchain_status
|
|
19
|
+
from arcgraph.core.evidence_manifest import (
|
|
20
|
+
evidence_health_check_status,
|
|
21
|
+
evidence_plan,
|
|
22
|
+
evidence_status,
|
|
23
|
+
load_live_evidence_manifest,
|
|
24
|
+
write_evidence_sidecar,
|
|
25
|
+
)
|
|
26
|
+
from arcgraph.core.metadata import current_commit
|
|
27
|
+
from arcgraph.core.ids import route_id
|
|
28
|
+
from arcgraph.core.query_engine import (
|
|
29
|
+
QueryEngine,
|
|
30
|
+
SchemaVersionError,
|
|
31
|
+
)
|
|
32
|
+
from arcgraph.core.schemas import ContextRequest
|
|
33
|
+
from arcgraph.core.target_resolver import is_entrypoint_target, parse_route_target
|
|
34
|
+
from arcgraph.pipeline.reindexer import ArcGraphReindexer
|
|
35
|
+
from arcgraph.core.scanner import (
|
|
36
|
+
DEFAULT_IGNORE_RULES,
|
|
37
|
+
LEGACY_SOURCE_ROOTS,
|
|
38
|
+
ResolvedSourceRoots,
|
|
39
|
+
SourceRoot,
|
|
40
|
+
SourceRootDetection,
|
|
41
|
+
detect_source_roots,
|
|
42
|
+
infer_source_roots,
|
|
43
|
+
)
|
|
44
|
+
from arcgraph.core.unresolved_classification import UNRESOLVED_CATEGORIES
|
|
45
|
+
from arcgraph.interfaces.ci import evidence_requirement_summary, run_ci_checks
|
|
46
|
+
from arcgraph.interfaces.agent_help import (
|
|
47
|
+
HELP_SURFACES,
|
|
48
|
+
HELP_TOPICS,
|
|
49
|
+
HelpTopic,
|
|
50
|
+
build_agent_help,
|
|
51
|
+
format_agent_help_human,
|
|
52
|
+
)
|
|
53
|
+
from arcgraph.interfaces.docs import DOC_TOPICS, render_docs
|
|
54
|
+
from arcgraph.interfaces.cli_feedback import (
|
|
55
|
+
add_feedback_parser,
|
|
56
|
+
feedback_metrics_path_conflict_payload,
|
|
57
|
+
)
|
|
58
|
+
from arcgraph.interfaces.human_output import (
|
|
59
|
+
format_build_human,
|
|
60
|
+
format_doctor_human,
|
|
61
|
+
format_generic_human,
|
|
62
|
+
format_init_human,
|
|
63
|
+
)
|
|
64
|
+
from arcgraph.interfaces.metrics import (
|
|
65
|
+
CLI_METRICS_WRITE_WARNING,
|
|
66
|
+
METRICS_LOG_INVALID_ENCODING_ERROR_CODE,
|
|
67
|
+
METRICS_LOG_UNREADABLE_ERROR_CODE,
|
|
68
|
+
MetricsLogEncodingError,
|
|
69
|
+
MetricsRecorder,
|
|
70
|
+
metrics_unavailable_payload,
|
|
71
|
+
summarize_metrics,
|
|
72
|
+
summarize_trial_metrics,
|
|
73
|
+
)
|
|
74
|
+
from arcgraph.interfaces.local_state import (
|
|
75
|
+
PrivateLocalStateError,
|
|
76
|
+
local_state_paths_alias,
|
|
77
|
+
)
|
|
78
|
+
from arcgraph.interfaces.mcp_server import add_mcp_server_args, serve as serve_mcp
|
|
79
|
+
from arcgraph.interfaces.ops import (
|
|
80
|
+
prune_output,
|
|
81
|
+
rebuild_index,
|
|
82
|
+
stale_alert,
|
|
83
|
+
)
|
|
84
|
+
from arcgraph.interfaces.precision import (
|
|
85
|
+
convert_scip_index_to_json,
|
|
86
|
+
generate_pyright_json,
|
|
87
|
+
generate_scip_python_json,
|
|
88
|
+
)
|
|
89
|
+
from arcgraph.interfaces.project_probe import (
|
|
90
|
+
detect_project_languages,
|
|
91
|
+
source_file_counts,
|
|
92
|
+
)
|
|
93
|
+
from arcgraph.interfaces.stdio_encoding import use_utf8_stdio
|
|
94
|
+
from arcgraph.interfaces.reports import (
|
|
95
|
+
classify_unresolved_records,
|
|
96
|
+
render_ci_markdown,
|
|
97
|
+
render_impact_markdown,
|
|
98
|
+
render_metrics_dashboard_html,
|
|
99
|
+
render_pr_markdown,
|
|
100
|
+
render_static_html_report,
|
|
101
|
+
render_unresolved_classification_markdown,
|
|
102
|
+
)
|
|
103
|
+
from arcgraph.interfaces.trace import (
|
|
104
|
+
import_har_network,
|
|
105
|
+
import_otel_spans,
|
|
106
|
+
import_runtime_trace,
|
|
107
|
+
run_runtime_trace,
|
|
108
|
+
)
|
|
109
|
+
from arcgraph.version_info import version_info
|
|
110
|
+
from arcgraph.interfaces.trial_feedback import (
|
|
111
|
+
DISTINCT_METRICS_FEEDBACK_LOGS_MESSAGE,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Re-exported so `from arcgraph.interfaces.cli import handle_change_*` and
|
|
115
|
+
# the parser wiring below keep working unchanged after the split.
|
|
116
|
+
from arcgraph.interfaces.cli_workspace import ( # noqa: F401
|
|
117
|
+
add_benchmark_parser,
|
|
118
|
+
add_workspace_parser,
|
|
119
|
+
handle_workspace_status,
|
|
120
|
+
handle_workspace_resolve,
|
|
121
|
+
handle_workspace_context,
|
|
122
|
+
handle_benchmark_agent_startup,
|
|
123
|
+
handle_benchmark_suite,
|
|
124
|
+
)
|
|
125
|
+
from arcgraph.interfaces.cli_visual import ( # noqa: F401
|
|
126
|
+
add_visual_parser,
|
|
127
|
+
handle_visual_force,
|
|
128
|
+
handle_visual_workbench,
|
|
129
|
+
handle_visual_serve,
|
|
130
|
+
handle_visual_smoke,
|
|
131
|
+
_visual_graph_options,
|
|
132
|
+
)
|
|
133
|
+
from arcgraph.interfaces.cli_support import ( # noqa: F401
|
|
134
|
+
output_location,
|
|
135
|
+
print_json,
|
|
136
|
+
query_engine,
|
|
137
|
+
write_json_output,
|
|
138
|
+
_print_cli_error_payload,
|
|
139
|
+
)
|
|
140
|
+
from arcgraph.interfaces.client_setup import add_setup_parser
|
|
141
|
+
from arcgraph.interfaces.cli_trial import ( # noqa: F401
|
|
142
|
+
add_trial_parser,
|
|
143
|
+
handle_trial_setup,
|
|
144
|
+
)
|
|
145
|
+
from arcgraph.interfaces.cli_ops import handle_sync, handle_watch # noqa: F401
|
|
146
|
+
from arcgraph.interfaces.cli_change import ( # noqa: F401
|
|
147
|
+
add_change_parser,
|
|
148
|
+
_change_error_payload,
|
|
149
|
+
_change_json_object,
|
|
150
|
+
_change_output_dir,
|
|
151
|
+
_change_payload,
|
|
152
|
+
_change_repo_id,
|
|
153
|
+
_change_repo_id_from_argv,
|
|
154
|
+
_change_repo_root,
|
|
155
|
+
_change_service,
|
|
156
|
+
_change_target,
|
|
157
|
+
_evidence_summary_payload,
|
|
158
|
+
handle_change_abandon,
|
|
159
|
+
handle_change_approve,
|
|
160
|
+
handle_change_archive,
|
|
161
|
+
handle_change_audit_export,
|
|
162
|
+
handle_change_diff,
|
|
163
|
+
handle_change_evidence_add,
|
|
164
|
+
handle_change_evidence_list,
|
|
165
|
+
handle_change_evidence_purge,
|
|
166
|
+
handle_change_evidence_show,
|
|
167
|
+
handle_change_list,
|
|
168
|
+
handle_change_plan,
|
|
169
|
+
handle_change_reconcile_pins,
|
|
170
|
+
handle_change_reject,
|
|
171
|
+
handle_change_report_show,
|
|
172
|
+
handle_change_show,
|
|
173
|
+
handle_change_verify,
|
|
174
|
+
)
|
|
175
|
+
from arcgraph.core.payload_policy import (
|
|
176
|
+
TARGET_SCOPED_CLI_COMMANDS,
|
|
177
|
+
apply_target_payload_contract,
|
|
178
|
+
)
|
|
179
|
+
from arcgraph.providers.context_provider import ContextProvider
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _scope_payload_capabilities(payload: dict[str, Any]) -> dict[str, Any]:
|
|
183
|
+
"""Apply the target-scoped read contract at the CLI boundary.
|
|
184
|
+
|
|
185
|
+
On a minimal missing-symbol response, the complete capability table can
|
|
186
|
+
dominate the serialized payload while placing no caveat on the answer. The
|
|
187
|
+
shared policy keeps degraded entries, reports what was omitted, and
|
|
188
|
+
independently versions the bounded read contract. It is idempotent, so
|
|
189
|
+
provider-built payloads and direct QueryEngine payloads converge here.
|
|
190
|
+
"""
|
|
191
|
+
return apply_target_payload_contract(payload)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class _ArgumentParserError(ValueError):
|
|
195
|
+
def __init__(self, parser: argparse.ArgumentParser, message: str) -> None:
|
|
196
|
+
self.parser = parser
|
|
197
|
+
self.message = message
|
|
198
|
+
super().__init__(message)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class _ArcGraphArgumentParser(argparse.ArgumentParser):
|
|
202
|
+
"""Let ``main`` preserve the structured Change CLI error contract."""
|
|
203
|
+
|
|
204
|
+
def error(self, message: str) -> None:
|
|
205
|
+
raise _ArgumentParserError(self, message)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def main(argv: list[str] | None = None) -> int:
|
|
209
|
+
use_utf8_stdio()
|
|
210
|
+
parser = build_parser()
|
|
211
|
+
raw_argv = list(argv) if argv is not None else sys.argv[1:]
|
|
212
|
+
try:
|
|
213
|
+
args = parser.parse_args(raw_argv)
|
|
214
|
+
except _ArgumentParserError as exc:
|
|
215
|
+
parse_error_payload = getattr(
|
|
216
|
+
exc.parser,
|
|
217
|
+
"_arcgraph_parse_error_payload",
|
|
218
|
+
None,
|
|
219
|
+
)
|
|
220
|
+
if isinstance(parse_error_payload, dict):
|
|
221
|
+
print_json(dict(parse_error_payload))
|
|
222
|
+
return int(
|
|
223
|
+
getattr(
|
|
224
|
+
exc.parser,
|
|
225
|
+
"_arcgraph_parse_error_exit_code",
|
|
226
|
+
2,
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
if _top_level_command(raw_argv) == "change":
|
|
230
|
+
error_args = argparse.Namespace(
|
|
231
|
+
command="change",
|
|
232
|
+
change_repo_id=_change_repo_id_from_argv(raw_argv),
|
|
233
|
+
)
|
|
234
|
+
print_json(_change_error_payload(error_args, ValueError(exc.message)))
|
|
235
|
+
return 1
|
|
236
|
+
exc.parser.print_usage(sys.stderr)
|
|
237
|
+
exc.parser.exit(2, f"{exc.parser.prog}: error: {exc.message}\n")
|
|
238
|
+
|
|
239
|
+
if not hasattr(args, "handler"):
|
|
240
|
+
parser.print_help()
|
|
241
|
+
return 0
|
|
242
|
+
|
|
243
|
+
feedback_path_error = feedback_metrics_path_conflict_payload(args)
|
|
244
|
+
if feedback_path_error is not None:
|
|
245
|
+
exit_code = int(feedback_path_error.pop("_exit_code", 2))
|
|
246
|
+
print_json(feedback_path_error)
|
|
247
|
+
return exit_code
|
|
248
|
+
if (
|
|
249
|
+
getattr(args, "command", None) == "mcp"
|
|
250
|
+
and getattr(args, "mcp_command", None) == "serve"
|
|
251
|
+
and getattr(args, "metrics_log", None)
|
|
252
|
+
and getattr(args, "mcp_feedback_log", None)
|
|
253
|
+
and local_state_paths_alias(
|
|
254
|
+
Path(args.metrics_log),
|
|
255
|
+
Path(args.mcp_feedback_log),
|
|
256
|
+
)
|
|
257
|
+
):
|
|
258
|
+
print(
|
|
259
|
+
f"ArcGraph: {DISTINCT_METRICS_FEEDBACK_LOGS_MESSAGE}",
|
|
260
|
+
file=sys.stderr,
|
|
261
|
+
)
|
|
262
|
+
# Rejected before the handler runs, but it is still a failure a caller
|
|
263
|
+
# parses, so it gets the same envelope the handler failures get.
|
|
264
|
+
_print_cli_error_payload(
|
|
265
|
+
args, ValueError(DISTINCT_METRICS_FEEDBACK_LOGS_MESSAGE)
|
|
266
|
+
)
|
|
267
|
+
return 2
|
|
268
|
+
|
|
269
|
+
started = time.perf_counter()
|
|
270
|
+
payload: dict[str, Any] | str | None = None
|
|
271
|
+
streaming_output = False
|
|
272
|
+
exit_code = 0
|
|
273
|
+
status = "success"
|
|
274
|
+
error: str | None = None
|
|
275
|
+
try:
|
|
276
|
+
payload = args.handler(args)
|
|
277
|
+
except BrokenPipeError:
|
|
278
|
+
# Every CLI command writes to stdout, so a consumer that closes the
|
|
279
|
+
# pipe early (`arcgraph ... | head`) is a normal termination for all
|
|
280
|
+
# of them, not just the streaming ones. Detach stdout so interpreter
|
|
281
|
+
# shutdown cannot re-raise on flush, and report the conventional
|
|
282
|
+
# SIGPIPE status.
|
|
283
|
+
return _exit_on_broken_pipe()
|
|
284
|
+
except ChangeSafetyError as exc:
|
|
285
|
+
status = "error"
|
|
286
|
+
error = str(exc)
|
|
287
|
+
exit_code = 1 if getattr(args, "command", None) == "change" else 2
|
|
288
|
+
if getattr(args, "command", None) == "change":
|
|
289
|
+
payload = _change_error_payload(args, exc)
|
|
290
|
+
print_json(payload)
|
|
291
|
+
else:
|
|
292
|
+
print(f"ArcGraph: {exc}", file=sys.stderr)
|
|
293
|
+
_print_cli_error_payload(args, exc)
|
|
294
|
+
except (FileNotFoundError, RuntimeError, SchemaVersionError) as exc:
|
|
295
|
+
status = "error"
|
|
296
|
+
error = str(exc)
|
|
297
|
+
exit_code = 1 if getattr(args, "command", None) == "change" else 2
|
|
298
|
+
if getattr(args, "command", None) == "change":
|
|
299
|
+
payload = _change_error_payload(args, exc)
|
|
300
|
+
print_json(payload)
|
|
301
|
+
else:
|
|
302
|
+
print(f"ArcGraph: {exc}", file=sys.stderr)
|
|
303
|
+
_print_cli_error_payload(args, exc)
|
|
304
|
+
except (TypeError, ValueError) as exc:
|
|
305
|
+
if getattr(args, "command", None) != "change":
|
|
306
|
+
# An internal defect keeps its traceback -- swallowing it would
|
|
307
|
+
# hide a bug -- but stdout still gets the envelope, so an agent is
|
|
308
|
+
# told that the command failed and why rather than reading silence.
|
|
309
|
+
traceback.print_exc()
|
|
310
|
+
_print_cli_error_payload(args, exc)
|
|
311
|
+
return 1
|
|
312
|
+
status = "error"
|
|
313
|
+
error = str(exc)
|
|
314
|
+
exit_code = 1
|
|
315
|
+
payload = _change_error_payload(args, exc)
|
|
316
|
+
print_json(payload)
|
|
317
|
+
else:
|
|
318
|
+
if isinstance(payload, dict):
|
|
319
|
+
exit_code = int(payload.pop("_exit_code", 0))
|
|
320
|
+
streaming_output = bool(payload.pop("_streaming_output", False))
|
|
321
|
+
if getattr(args, "command", None) == "change":
|
|
322
|
+
status = str(payload.get("status", "success"))
|
|
323
|
+
else:
|
|
324
|
+
status = "success" if exit_code == 0 else "failed"
|
|
325
|
+
# --raw promises the QueryEngine payload unchanged, so it must not
|
|
326
|
+
# be shaped here either. Scoping it made `callers --raw` differ
|
|
327
|
+
# from `QueryEngine.callers()`, which is the one thing --raw exists
|
|
328
|
+
# to reproduce.
|
|
329
|
+
if getattr(
|
|
330
|
+
args, "command", None
|
|
331
|
+
) in TARGET_SCOPED_CLI_COMMANDS and not getattr(args, "raw", False):
|
|
332
|
+
payload = _scope_payload_capabilities(payload)
|
|
333
|
+
|
|
334
|
+
if payload is not None:
|
|
335
|
+
try:
|
|
336
|
+
_emit_cli_payload(args, payload, streaming_output, started)
|
|
337
|
+
except BrokenPipeError:
|
|
338
|
+
exit_code = _exit_on_broken_pipe()
|
|
339
|
+
finally:
|
|
340
|
+
metrics_log = getattr(args, "metrics_log", None)
|
|
341
|
+
if metrics_log:
|
|
342
|
+
try:
|
|
343
|
+
MetricsRecorder(Path(metrics_log)).record_cli_command(
|
|
344
|
+
command=getattr(args, "command", None),
|
|
345
|
+
status=status,
|
|
346
|
+
duration_ms=(time.perf_counter() - started) * 1000,
|
|
347
|
+
exit_code=exit_code,
|
|
348
|
+
payload=payload if isinstance(payload, dict) else None,
|
|
349
|
+
error=error,
|
|
350
|
+
)
|
|
351
|
+
except PrivateLocalStateError:
|
|
352
|
+
sys.stderr.write(CLI_METRICS_WRITE_WARNING)
|
|
353
|
+
return exit_code
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _exit_on_broken_pipe() -> int:
|
|
357
|
+
"""Detach stdout and report the conventional SIGPIPE exit status."""
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
devnull = os.open(os.devnull, os.O_WRONLY)
|
|
361
|
+
os.dup2(devnull, sys.stdout.fileno())
|
|
362
|
+
except (OSError, ValueError):
|
|
363
|
+
pass
|
|
364
|
+
return 141
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _emit_cli_payload(
|
|
368
|
+
args: argparse.Namespace,
|
|
369
|
+
payload: dict[str, Any] | str,
|
|
370
|
+
streaming_output: bool,
|
|
371
|
+
started: float,
|
|
372
|
+
) -> None:
|
|
373
|
+
if isinstance(payload, str):
|
|
374
|
+
print(payload, end="")
|
|
375
|
+
elif getattr(args, "human", False):
|
|
376
|
+
duration_s = time.perf_counter() - started
|
|
377
|
+
command = getattr(args, "command", None)
|
|
378
|
+
if command == "build":
|
|
379
|
+
print(format_build_human(payload, duration_s))
|
|
380
|
+
elif command == "init":
|
|
381
|
+
print(format_init_human(payload))
|
|
382
|
+
elif command == "doctor":
|
|
383
|
+
print(format_doctor_human(payload))
|
|
384
|
+
elif command == "help":
|
|
385
|
+
print(format_agent_help_human(payload), end="")
|
|
386
|
+
else:
|
|
387
|
+
print(format_generic_human(payload, command))
|
|
388
|
+
elif streaming_output:
|
|
389
|
+
print(json.dumps(payload, ensure_ascii=False, sort_keys=True))
|
|
390
|
+
else:
|
|
391
|
+
print_json(payload)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
395
|
+
parser = _ArcGraphArgumentParser(
|
|
396
|
+
prog="arcgraph", description="Build and query a local ArcGraph index."
|
|
397
|
+
)
|
|
398
|
+
parser.add_argument(
|
|
399
|
+
"--repo-root",
|
|
400
|
+
default=".",
|
|
401
|
+
help="Repository root. Defaults to current directory.",
|
|
402
|
+
)
|
|
403
|
+
parser.add_argument(
|
|
404
|
+
"--output-dir", default="output/arcgraph", help="Index output directory."
|
|
405
|
+
)
|
|
406
|
+
parser.add_argument(
|
|
407
|
+
"--metrics-log",
|
|
408
|
+
help="Optional JSONL path for CLI latency/status metrics.",
|
|
409
|
+
)
|
|
410
|
+
parser.add_argument(
|
|
411
|
+
"--version",
|
|
412
|
+
action="version",
|
|
413
|
+
version=f"%(prog)s {__version__}",
|
|
414
|
+
)
|
|
415
|
+
parser.add_argument(
|
|
416
|
+
"--human",
|
|
417
|
+
action="store_true",
|
|
418
|
+
help="Print human-readable output instead of JSON.",
|
|
419
|
+
)
|
|
420
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
421
|
+
|
|
422
|
+
def _add_legacy_roots_arg(sub: argparse.ArgumentParser) -> None:
|
|
423
|
+
sub.add_argument(
|
|
424
|
+
"--legacy-roots",
|
|
425
|
+
action="store_true",
|
|
426
|
+
help=(
|
|
427
|
+
"Use the hard-coded legacy source roots instead of "
|
|
428
|
+
"auto-detection. Mutually exclusive with --root."
|
|
429
|
+
),
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
def _add_agent_compact_args(sub: argparse.ArgumentParser) -> None:
|
|
433
|
+
shape = sub.add_mutually_exclusive_group()
|
|
434
|
+
shape.add_argument(
|
|
435
|
+
"--compact",
|
|
436
|
+
action="store_true",
|
|
437
|
+
help=(
|
|
438
|
+
"Accepted for compatibility and ignored: the bounded agent "
|
|
439
|
+
"payload is the default."
|
|
440
|
+
),
|
|
441
|
+
)
|
|
442
|
+
shape.add_argument(
|
|
443
|
+
"--raw",
|
|
444
|
+
action="store_true",
|
|
445
|
+
help=(
|
|
446
|
+
"Return the unbounded QueryEngine debug payload instead of the "
|
|
447
|
+
"bounded agent payload. It carries every node property and is "
|
|
448
|
+
"not size-bounded; on this repository the raw impact payload is "
|
|
449
|
+
"over 10 MB. Payload-shaping options are rejected with --raw."
|
|
450
|
+
),
|
|
451
|
+
)
|
|
452
|
+
# ``None`` defaults let the handler tell "user asked for it" apart from
|
|
453
|
+
# "argparse filled it in", so --raw can reject shaping it cannot honor
|
|
454
|
+
# instead of silently discarding it.
|
|
455
|
+
sub.add_argument("--max-results", type=int, default=None)
|
|
456
|
+
sub.add_argument(
|
|
457
|
+
"--detail-level",
|
|
458
|
+
default=None,
|
|
459
|
+
choices=["summary", "standard", "detailed"],
|
|
460
|
+
)
|
|
461
|
+
sub.add_argument(
|
|
462
|
+
"--include-source",
|
|
463
|
+
action="store_true",
|
|
464
|
+
default=None,
|
|
465
|
+
help="Include source snippets when the provider can safely expose them.",
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
def _add_relation_profile_arg(
|
|
469
|
+
sub: argparse.ArgumentParser, *, direction: str
|
|
470
|
+
) -> None:
|
|
471
|
+
sub.add_argument(
|
|
472
|
+
"--profile",
|
|
473
|
+
default="review_default",
|
|
474
|
+
help=f"Confidence profile to use for direct {direction} evidence.",
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
init = subparsers.add_parser(
|
|
478
|
+
"init", help="Detect source roots and add a [tool.arcgraph] config."
|
|
479
|
+
)
|
|
480
|
+
init.add_argument(
|
|
481
|
+
"--dry-run",
|
|
482
|
+
action="store_true",
|
|
483
|
+
help="Show the config that would be written without modifying pyproject.toml.",
|
|
484
|
+
)
|
|
485
|
+
init.set_defaults(handler=handle_init)
|
|
486
|
+
|
|
487
|
+
doctor = subparsers.add_parser(
|
|
488
|
+
"doctor", help="Run environment and index health checks."
|
|
489
|
+
)
|
|
490
|
+
doctor.set_defaults(handler=handle_doctor)
|
|
491
|
+
|
|
492
|
+
version = subparsers.add_parser(
|
|
493
|
+
"version",
|
|
494
|
+
help="Report the exact runtime, artifact, and source identity.",
|
|
495
|
+
)
|
|
496
|
+
version.add_argument(
|
|
497
|
+
"--json",
|
|
498
|
+
action="store_true",
|
|
499
|
+
help="Emit the machine-readable provenance payload (the default format).",
|
|
500
|
+
)
|
|
501
|
+
# The identity command must observe live working-tree state, never a
|
|
502
|
+
# reused capture.
|
|
503
|
+
version.set_defaults(handler=lambda _args: version_info(refresh=True))
|
|
504
|
+
|
|
505
|
+
build = subparsers.add_parser("build", help="Build a new ArcGraph index.")
|
|
506
|
+
build.add_argument(
|
|
507
|
+
"--root",
|
|
508
|
+
action="append",
|
|
509
|
+
default=[],
|
|
510
|
+
help="Source root to scan. Can be repeated.",
|
|
511
|
+
)
|
|
512
|
+
build.add_argument(
|
|
513
|
+
"--scip-index",
|
|
514
|
+
help="Optional SCIP-derived JSON occurrence file for confirmed references.",
|
|
515
|
+
)
|
|
516
|
+
build.add_argument(
|
|
517
|
+
"--scip-graph-index",
|
|
518
|
+
help=(
|
|
519
|
+
"Optional scip print --json payload to ingest as a language-neutral "
|
|
520
|
+
"protocol graph fragment. Distinct from --scip-index precision evidence."
|
|
521
|
+
),
|
|
522
|
+
)
|
|
523
|
+
build.add_argument(
|
|
524
|
+
"--openapi-spec",
|
|
525
|
+
help=(
|
|
526
|
+
"Optional OpenAPI 3.x JSON/YAML artifact to ingest as an explicit "
|
|
527
|
+
"protocol graph fragment."
|
|
528
|
+
),
|
|
529
|
+
)
|
|
530
|
+
build.add_argument(
|
|
531
|
+
"--pyright-export",
|
|
532
|
+
help="Optional Pyright-derived JSON export path for the precision input contract.",
|
|
533
|
+
)
|
|
534
|
+
build.add_argument(
|
|
535
|
+
"--runtime-trace",
|
|
536
|
+
help="Optional ArcGraph runtime trace JSON file to import as runtime-only evidence.",
|
|
537
|
+
)
|
|
538
|
+
build.add_argument(
|
|
539
|
+
"--coverage",
|
|
540
|
+
help="Optional coverage JSON or Cobertura XML file for runtime test evidence.",
|
|
541
|
+
)
|
|
542
|
+
build.add_argument(
|
|
543
|
+
"--semantic-call-resolution",
|
|
544
|
+
action="store_true",
|
|
545
|
+
help=(
|
|
546
|
+
"Compatibility flag retained for older scripts; V2 receiver-aware "
|
|
547
|
+
"call resolution is the default at schema 1.0."
|
|
548
|
+
),
|
|
549
|
+
)
|
|
550
|
+
build.add_argument(
|
|
551
|
+
"--legacy-call-resolution",
|
|
552
|
+
action="store_true",
|
|
553
|
+
help=(
|
|
554
|
+
"Use the legacy AST call resolver. This escape hatch is rejected by "
|
|
555
|
+
"ci --python-full."
|
|
556
|
+
),
|
|
557
|
+
)
|
|
558
|
+
_add_legacy_roots_arg(build)
|
|
559
|
+
build.set_defaults(handler=handle_build)
|
|
560
|
+
|
|
561
|
+
reindex = subparsers.add_parser(
|
|
562
|
+
"reindex", help="Incrementally reindex changed files."
|
|
563
|
+
)
|
|
564
|
+
reindex.add_argument(
|
|
565
|
+
"--changed",
|
|
566
|
+
action="store_true",
|
|
567
|
+
help="Reindex changed files using the current index as a baseline.",
|
|
568
|
+
)
|
|
569
|
+
reindex.set_defaults(handler=handle_reindex)
|
|
570
|
+
|
|
571
|
+
sync = subparsers.add_parser(
|
|
572
|
+
"sync", help="Explicitly publish an incremental index when needed."
|
|
573
|
+
)
|
|
574
|
+
sync.add_argument(
|
|
575
|
+
"--if-stale",
|
|
576
|
+
action="store_true",
|
|
577
|
+
help="Skip synchronization when the current index is fresh.",
|
|
578
|
+
)
|
|
579
|
+
sync.set_defaults(handler=handle_sync)
|
|
580
|
+
|
|
581
|
+
watch = subparsers.add_parser(
|
|
582
|
+
"watch", help="Watch for stale changes and debounce incremental sync."
|
|
583
|
+
)
|
|
584
|
+
watch.add_argument("--poll-interval", type=float, default=0.25)
|
|
585
|
+
watch.add_argument("--debounce", type=float, default=0.5)
|
|
586
|
+
watch.add_argument("--max-cycles", type=int, help=argparse.SUPPRESS)
|
|
587
|
+
watch.set_defaults(handler=handle_watch)
|
|
588
|
+
|
|
589
|
+
stats = subparsers.add_parser("stats", help="Show current index statistics.")
|
|
590
|
+
stats.set_defaults(handler=lambda args: query_engine(args).stats())
|
|
591
|
+
|
|
592
|
+
semantic_stats = subparsers.add_parser(
|
|
593
|
+
"semantic-stats", help="Show Python semantic callsite metrics."
|
|
594
|
+
)
|
|
595
|
+
semantic_stats.add_argument(
|
|
596
|
+
"--output",
|
|
597
|
+
help="Path to write semantic metrics JSON. Defaults to <output-dir>/metrics/semantic-stats.json.",
|
|
598
|
+
)
|
|
599
|
+
semantic_stats.set_defaults(handler=handle_semantic_stats)
|
|
600
|
+
|
|
601
|
+
unresolved = subparsers.add_parser(
|
|
602
|
+
"unresolved", help="Show unresolved semantic callsites."
|
|
603
|
+
)
|
|
604
|
+
unresolved.add_argument(
|
|
605
|
+
"target",
|
|
606
|
+
nargs="?",
|
|
607
|
+
help="Optional file path, symbol, entrypoint, or stable id filter.",
|
|
608
|
+
)
|
|
609
|
+
unresolved.add_argument(
|
|
610
|
+
"--limit", type=int, default=100, help="Maximum unresolved callsites to return."
|
|
611
|
+
)
|
|
612
|
+
unresolved.add_argument(
|
|
613
|
+
"--category",
|
|
614
|
+
choices=UNRESOLVED_CATEGORIES,
|
|
615
|
+
help="Only return unresolved callsites in this triage category.",
|
|
616
|
+
)
|
|
617
|
+
unresolved.add_argument(
|
|
618
|
+
"--release-blocking-only",
|
|
619
|
+
action="store_true",
|
|
620
|
+
help="Only return unresolved callsites that block the python_full gate.",
|
|
621
|
+
)
|
|
622
|
+
unresolved.set_defaults(
|
|
623
|
+
handler=lambda args: query_engine(args).unresolved(
|
|
624
|
+
args.target,
|
|
625
|
+
limit=args.limit,
|
|
626
|
+
category=args.category,
|
|
627
|
+
release_blocking_only=args.release_blocking_only,
|
|
628
|
+
)
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
bindings = subparsers.add_parser(
|
|
632
|
+
"bindings", help="Show bindings attached to an indexed scope."
|
|
633
|
+
)
|
|
634
|
+
bindings.add_argument("target", help="Dotted qualname, path, or stable symbol id.")
|
|
635
|
+
bindings.add_argument(
|
|
636
|
+
"--limit", type=int, default=200, help="Maximum binding records to return."
|
|
637
|
+
)
|
|
638
|
+
bindings.set_defaults(
|
|
639
|
+
handler=lambda args: query_engine(args).bindings(args.target, limit=args.limit)
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
types = subparsers.add_parser(
|
|
643
|
+
"types", help="Show TypeRef facts attached to an indexed scope."
|
|
644
|
+
)
|
|
645
|
+
types.add_argument("target", help="Dotted qualname, path, or stable symbol id.")
|
|
646
|
+
types.add_argument(
|
|
647
|
+
"--limit", type=int, default=200, help="Maximum TypeRef records to return."
|
|
648
|
+
)
|
|
649
|
+
types.set_defaults(
|
|
650
|
+
handler=lambda args: query_engine(args).types(args.target, limit=args.limit)
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
callsites = subparsers.add_parser(
|
|
654
|
+
"callsites", help="Show extracted callsites for an indexed scope."
|
|
655
|
+
)
|
|
656
|
+
callsites.add_argument("target", help="Dotted qualname, path, or stable symbol id.")
|
|
657
|
+
callsites.add_argument(
|
|
658
|
+
"--limit", type=int, default=200, help="Maximum callsite records to return."
|
|
659
|
+
)
|
|
660
|
+
callsites.set_defaults(
|
|
661
|
+
handler=lambda args: query_engine(args).callsites(args.target, limit=args.limit)
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
imports = subparsers.add_parser(
|
|
665
|
+
"imports", help="Show import in/out edges for a module."
|
|
666
|
+
)
|
|
667
|
+
imports.add_argument("module", help="Dotted module name or mod:<name> id.")
|
|
668
|
+
imports.set_defaults(handler=lambda args: query_engine(args).imports(args.module))
|
|
669
|
+
|
|
670
|
+
callers = subparsers.add_parser(
|
|
671
|
+
"callers", help="Show direct inferred callers for a symbol."
|
|
672
|
+
)
|
|
673
|
+
callers.add_argument("target", help="Dotted qualname, path, or stable symbol id.")
|
|
674
|
+
_add_agent_compact_args(callers)
|
|
675
|
+
_add_relation_profile_arg(callers, direction="caller")
|
|
676
|
+
callers.set_defaults(handler=handle_callers)
|
|
677
|
+
|
|
678
|
+
callees = subparsers.add_parser(
|
|
679
|
+
"callees", help="Show direct inferred callees for a symbol."
|
|
680
|
+
)
|
|
681
|
+
callees.add_argument("target", help="Dotted qualname, path, or stable symbol id.")
|
|
682
|
+
_add_agent_compact_args(callees)
|
|
683
|
+
_add_relation_profile_arg(callees, direction="callee")
|
|
684
|
+
callees.set_defaults(handler=handle_callees)
|
|
685
|
+
|
|
686
|
+
impact = subparsers.add_parser(
|
|
687
|
+
"impact", help="Show a call/import/entrypoint/resource impact report."
|
|
688
|
+
)
|
|
689
|
+
impact.add_argument("target", help="Dotted qualname, path, or stable symbol id.")
|
|
690
|
+
impact.add_argument(
|
|
691
|
+
"--max-depth",
|
|
692
|
+
type=int,
|
|
693
|
+
default=None,
|
|
694
|
+
help="Reverse call traversal depth. Defaults to the profile's default_max_depth.",
|
|
695
|
+
)
|
|
696
|
+
impact.add_argument(
|
|
697
|
+
"--profile",
|
|
698
|
+
default="review_default",
|
|
699
|
+
help="Impact confidence profile label to include in the response.",
|
|
700
|
+
)
|
|
701
|
+
impact.add_argument(
|
|
702
|
+
"--include-edge-kind",
|
|
703
|
+
action="append",
|
|
704
|
+
default=[],
|
|
705
|
+
help="Limit impact edges to this kind. Can be repeated.",
|
|
706
|
+
)
|
|
707
|
+
impact.add_argument(
|
|
708
|
+
"--exclude-edge-kind",
|
|
709
|
+
action="append",
|
|
710
|
+
default=[],
|
|
711
|
+
help="Exclude this edge kind from impact output. Can be repeated.",
|
|
712
|
+
)
|
|
713
|
+
_add_agent_compact_args(impact)
|
|
714
|
+
impact.set_defaults(handler=handle_impact)
|
|
715
|
+
|
|
716
|
+
report = subparsers.add_parser("report", help="Render ArcGraph reports.")
|
|
717
|
+
report_subparsers = report.add_subparsers(dest="report_command")
|
|
718
|
+
report_impact = report_subparsers.add_parser(
|
|
719
|
+
"impact", help="Render a Markdown impact report."
|
|
720
|
+
)
|
|
721
|
+
report_impact.add_argument(
|
|
722
|
+
"target", help="Dotted qualname, path, or stable symbol id."
|
|
723
|
+
)
|
|
724
|
+
report_impact.add_argument(
|
|
725
|
+
"--max-depth",
|
|
726
|
+
type=int,
|
|
727
|
+
default=None,
|
|
728
|
+
help="Reverse call traversal depth. Defaults to the profile's default_max_depth.",
|
|
729
|
+
)
|
|
730
|
+
report_impact.add_argument(
|
|
731
|
+
"--profile",
|
|
732
|
+
default="review_default",
|
|
733
|
+
help="Impact confidence profile label to include in the report.",
|
|
734
|
+
)
|
|
735
|
+
report_impact.add_argument(
|
|
736
|
+
"--include-edge-kind",
|
|
737
|
+
action="append",
|
|
738
|
+
default=[],
|
|
739
|
+
help="Limit impact edges to this kind. Can be repeated.",
|
|
740
|
+
)
|
|
741
|
+
report_impact.add_argument(
|
|
742
|
+
"--exclude-edge-kind",
|
|
743
|
+
action="append",
|
|
744
|
+
default=[],
|
|
745
|
+
help="Exclude this edge kind from impact output. Can be repeated.",
|
|
746
|
+
)
|
|
747
|
+
report_impact.set_defaults(
|
|
748
|
+
handler=lambda args: render_impact_markdown(
|
|
749
|
+
query_engine(args).impact(
|
|
750
|
+
args.target,
|
|
751
|
+
max_depth=args.max_depth,
|
|
752
|
+
profile=args.profile,
|
|
753
|
+
include_edge_kinds=args.include_edge_kind,
|
|
754
|
+
exclude_edge_kinds=args.exclude_edge_kind,
|
|
755
|
+
)
|
|
756
|
+
)
|
|
757
|
+
)
|
|
758
|
+
report_ci = report_subparsers.add_parser("ci", help="Render a Markdown CI summary.")
|
|
759
|
+
report_ci.add_argument("--target", action="append", default=[])
|
|
760
|
+
report_ci.add_argument("--changed-file", action="append", default=[])
|
|
761
|
+
report_ci.add_argument("--max-results", type=int, default=30)
|
|
762
|
+
report_ci.add_argument(
|
|
763
|
+
"--semantic-baseline",
|
|
764
|
+
help="Optional semantic-stats or CI JSON baseline for resolution trend checks.",
|
|
765
|
+
)
|
|
766
|
+
report_ci.add_argument(
|
|
767
|
+
"--performance-budget-ms",
|
|
768
|
+
action="append",
|
|
769
|
+
default=[],
|
|
770
|
+
metavar="OPERATION=MS",
|
|
771
|
+
help="Override a CI performance budget in milliseconds. Can be repeated.",
|
|
772
|
+
)
|
|
773
|
+
_add_ci_runtime_trace_args(report_ci)
|
|
774
|
+
report_ci.set_defaults(
|
|
775
|
+
handler=lambda args: render_ci_markdown(handle_ci_payload(args))
|
|
776
|
+
)
|
|
777
|
+
report_pr = report_subparsers.add_parser(
|
|
778
|
+
"pr", help="Render a Markdown PR impact summary."
|
|
779
|
+
)
|
|
780
|
+
report_pr.add_argument("--target", action="append", default=[])
|
|
781
|
+
report_pr.add_argument("--changed-file", action="append", default=[])
|
|
782
|
+
report_pr.add_argument("--max-results", type=int, default=30)
|
|
783
|
+
report_pr.add_argument(
|
|
784
|
+
"--semantic-baseline",
|
|
785
|
+
help="Optional semantic-stats or CI JSON baseline for resolution trend checks.",
|
|
786
|
+
)
|
|
787
|
+
report_pr.add_argument(
|
|
788
|
+
"--performance-budget-ms",
|
|
789
|
+
action="append",
|
|
790
|
+
default=[],
|
|
791
|
+
metavar="OPERATION=MS",
|
|
792
|
+
help="Override a CI performance budget in milliseconds. Can be repeated.",
|
|
793
|
+
)
|
|
794
|
+
_add_ci_runtime_trace_args(report_pr)
|
|
795
|
+
report_pr.add_argument("--output", help="Path to write the Markdown report.")
|
|
796
|
+
report_pr.set_defaults(handler=handle_report_pr)
|
|
797
|
+
report_html = report_subparsers.add_parser(
|
|
798
|
+
"html", help="Render a static HTML ArcGraph report."
|
|
799
|
+
)
|
|
800
|
+
report_html.add_argument("target", help="Dotted qualname, path, or symbol id.")
|
|
801
|
+
report_html.add_argument("--output", help="Path to write the HTML report.")
|
|
802
|
+
report_html.add_argument("--max-results", type=int, default=30)
|
|
803
|
+
report_html.add_argument(
|
|
804
|
+
"--profile",
|
|
805
|
+
default="review_default",
|
|
806
|
+
help="Impact confidence profile label to include in the report.",
|
|
807
|
+
)
|
|
808
|
+
report_html.add_argument(
|
|
809
|
+
"--include-edge-kind",
|
|
810
|
+
action="append",
|
|
811
|
+
default=[],
|
|
812
|
+
help="Limit impact edges to this kind. Can be repeated.",
|
|
813
|
+
)
|
|
814
|
+
report_html.add_argument(
|
|
815
|
+
"--exclude-edge-kind",
|
|
816
|
+
action="append",
|
|
817
|
+
default=[],
|
|
818
|
+
help="Exclude this edge kind from impact output. Can be repeated.",
|
|
819
|
+
)
|
|
820
|
+
report_html.set_defaults(handler=handle_report_html)
|
|
821
|
+
report_metrics = report_subparsers.add_parser(
|
|
822
|
+
"metrics-html", help="Render a static HTML metrics dashboard."
|
|
823
|
+
)
|
|
824
|
+
report_metrics.add_argument("path", help="Metrics JSONL path.")
|
|
825
|
+
report_metrics.add_argument("--limit", type=int, default=1000)
|
|
826
|
+
report_metrics.add_argument("--output", help="Path to write the HTML dashboard.")
|
|
827
|
+
report_metrics.set_defaults(handler=handle_report_metrics_html)
|
|
828
|
+
report_unresolved = report_subparsers.add_parser(
|
|
829
|
+
"unresolved", help="Render a Markdown unresolved classification report."
|
|
830
|
+
)
|
|
831
|
+
report_unresolved.add_argument(
|
|
832
|
+
"target",
|
|
833
|
+
nargs="?",
|
|
834
|
+
help="Optional file path, symbol, entrypoint, or stable id filter.",
|
|
835
|
+
)
|
|
836
|
+
report_unresolved.add_argument(
|
|
837
|
+
"--limit",
|
|
838
|
+
type=int,
|
|
839
|
+
default=10000,
|
|
840
|
+
help="Maximum unresolved diagnostics to classify.",
|
|
841
|
+
)
|
|
842
|
+
report_unresolved.add_argument(
|
|
843
|
+
"--output",
|
|
844
|
+
default="output/arcgraph/reports/unresolved-classification.md",
|
|
845
|
+
help="Path to write the Markdown report.",
|
|
846
|
+
)
|
|
847
|
+
report_unresolved.add_argument(
|
|
848
|
+
"--json-output",
|
|
849
|
+
help="Optional path to write machine-readable unresolved classification JSON.",
|
|
850
|
+
)
|
|
851
|
+
report_unresolved.set_defaults(handler=handle_report_unresolved)
|
|
852
|
+
|
|
853
|
+
tests = subparsers.add_parser(
|
|
854
|
+
"tests", help="Recommend related tests and coverage gaps."
|
|
855
|
+
)
|
|
856
|
+
tests.add_argument("target", help="Dotted qualname, path, or stable symbol id.")
|
|
857
|
+
tests.set_defaults(handler=lambda args: query_engine(args).tests(args.target))
|
|
858
|
+
|
|
859
|
+
references = subparsers.add_parser(
|
|
860
|
+
"references", help="Verify references with SCIP or TypeScript Language Service."
|
|
861
|
+
)
|
|
862
|
+
references.add_argument("target", help="Exact qualname or stable symbol id.")
|
|
863
|
+
references.add_argument(
|
|
864
|
+
"--backend",
|
|
865
|
+
choices=("auto", "scip", "typescript_language_service"),
|
|
866
|
+
default="auto",
|
|
867
|
+
)
|
|
868
|
+
references.add_argument("--max-results", type=int, default=200)
|
|
869
|
+
references.set_defaults(
|
|
870
|
+
handler=lambda args: query_engine(args).references(
|
|
871
|
+
args.target,
|
|
872
|
+
backend=args.backend,
|
|
873
|
+
max_results=args.max_results,
|
|
874
|
+
)
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
context = subparsers.add_parser(
|
|
878
|
+
"context", help="Return canonical agent context for one or more targets."
|
|
879
|
+
)
|
|
880
|
+
context.add_argument(
|
|
881
|
+
"target",
|
|
882
|
+
nargs="+",
|
|
883
|
+
help="Dotted qualname, path, stable symbol id, or entrypoint target.",
|
|
884
|
+
)
|
|
885
|
+
context.add_argument(
|
|
886
|
+
"--task", help="Optional task description for context ranking."
|
|
887
|
+
)
|
|
888
|
+
context.add_argument(
|
|
889
|
+
"--profile",
|
|
890
|
+
default="review_default",
|
|
891
|
+
help="Confidence profile to use for impact and risk grouping.",
|
|
892
|
+
)
|
|
893
|
+
context.add_argument("--max-results", type=int, default=30)
|
|
894
|
+
context.add_argument(
|
|
895
|
+
"--detail-level",
|
|
896
|
+
default="summary",
|
|
897
|
+
choices=["summary", "standard", "detailed"],
|
|
898
|
+
)
|
|
899
|
+
context.add_argument(
|
|
900
|
+
"--include-source",
|
|
901
|
+
action="store_true",
|
|
902
|
+
help="Include source snippets when the provider can safely expose them.",
|
|
903
|
+
)
|
|
904
|
+
context.set_defaults(handler=handle_context)
|
|
905
|
+
|
|
906
|
+
explain = subparsers.add_parser(
|
|
907
|
+
"explain", help="Explain target resolution, direct edges, and evidence."
|
|
908
|
+
)
|
|
909
|
+
explain.add_argument(
|
|
910
|
+
"target",
|
|
911
|
+
nargs="+",
|
|
912
|
+
help="Dotted qualname, path, stable symbol id, or entrypoint target.",
|
|
913
|
+
)
|
|
914
|
+
explain.add_argument(
|
|
915
|
+
"--task", help="Optional task description for explanation ranking."
|
|
916
|
+
)
|
|
917
|
+
explain.add_argument(
|
|
918
|
+
"--profile",
|
|
919
|
+
default="review_default",
|
|
920
|
+
help="Confidence profile to use for direct call evidence.",
|
|
921
|
+
)
|
|
922
|
+
explain.add_argument("--max-results", type=int, default=30)
|
|
923
|
+
explain.add_argument(
|
|
924
|
+
"--detail-level",
|
|
925
|
+
default="summary",
|
|
926
|
+
choices=["summary", "standard", "detailed"],
|
|
927
|
+
)
|
|
928
|
+
explain.add_argument(
|
|
929
|
+
"--include-source",
|
|
930
|
+
action="store_true",
|
|
931
|
+
help="Include source snippets when the provider can safely expose them.",
|
|
932
|
+
)
|
|
933
|
+
explain.set_defaults(handler=handle_explain)
|
|
934
|
+
|
|
935
|
+
similar = subparsers.add_parser(
|
|
936
|
+
"similar", help="Find indexed implementations similar to a symbol."
|
|
937
|
+
)
|
|
938
|
+
similar.add_argument("target", help="Dotted qualname or stable symbol id.")
|
|
939
|
+
similar.add_argument("--max-results", type=int, default=10)
|
|
940
|
+
similar.add_argument(
|
|
941
|
+
"--detail-level",
|
|
942
|
+
default="summary",
|
|
943
|
+
choices=["summary", "standard", "detailed"],
|
|
944
|
+
)
|
|
945
|
+
similar.set_defaults(
|
|
946
|
+
handler=lambda args: query_engine(args).similar(
|
|
947
|
+
args.target,
|
|
948
|
+
max_results=args.max_results,
|
|
949
|
+
detail_level=args.detail_level,
|
|
950
|
+
)
|
|
951
|
+
)
|
|
952
|
+
|
|
953
|
+
route = subparsers.add_parser(
|
|
954
|
+
"route", help="Trace an indexed route to handlers and middleware."
|
|
955
|
+
)
|
|
956
|
+
route.add_argument("method", help="HTTP method, for example GET or POST.")
|
|
957
|
+
route.add_argument("path", help="Route path, for example /api/v1/memories.")
|
|
958
|
+
route.set_defaults(
|
|
959
|
+
handler=lambda args: query_engine(args).route(args.method, args.path)
|
|
960
|
+
)
|
|
961
|
+
|
|
962
|
+
worker = subparsers.add_parser("worker", help="Trace an ARQ worker task or queue.")
|
|
963
|
+
worker.add_argument(
|
|
964
|
+
"target", help="worker:<id>, queue:<name>, task name, or queue name."
|
|
965
|
+
)
|
|
966
|
+
worker.set_defaults(handler=lambda args: query_engine(args).worker(args.target))
|
|
967
|
+
|
|
968
|
+
architecture = subparsers.add_parser(
|
|
969
|
+
"architecture", help="Show a basic architecture report."
|
|
970
|
+
)
|
|
971
|
+
architecture.set_defaults(handler=lambda args: query_engine(args).architecture())
|
|
972
|
+
|
|
973
|
+
add_visual_parser(subparsers)
|
|
974
|
+
trace = subparsers.add_parser("trace", help="Import runtime trace evidence.")
|
|
975
|
+
trace_subparsers = trace.add_subparsers(dest="trace_command")
|
|
976
|
+
trace_import = trace_subparsers.add_parser(
|
|
977
|
+
"import", help="Import a ArcGraph runtime trace JSON file."
|
|
978
|
+
)
|
|
979
|
+
trace_import.add_argument("path", help="Runtime trace JSON file to import.")
|
|
980
|
+
trace_import.add_argument(
|
|
981
|
+
"--max-events",
|
|
982
|
+
type=int,
|
|
983
|
+
default=50_000,
|
|
984
|
+
help="Maximum trace events to import from the file.",
|
|
985
|
+
)
|
|
986
|
+
trace_import.add_argument(
|
|
987
|
+
"--max-file-bytes",
|
|
988
|
+
type=int,
|
|
989
|
+
default=10 * 1024 * 1024,
|
|
990
|
+
help="Maximum runtime trace JSON file size to import.",
|
|
991
|
+
)
|
|
992
|
+
trace_import.add_argument(
|
|
993
|
+
"--max-seconds",
|
|
994
|
+
type=float,
|
|
995
|
+
default=120.0,
|
|
996
|
+
help="Maximum seconds to spend importing runtime trace events.",
|
|
997
|
+
)
|
|
998
|
+
trace_import.set_defaults(handler=handle_trace_import)
|
|
999
|
+
trace_import_otel = trace_subparsers.add_parser(
|
|
1000
|
+
"import-otel", help="Import offline OpenTelemetry span JSON/JSONL."
|
|
1001
|
+
)
|
|
1002
|
+
trace_import_otel.add_argument("path", help="OpenTelemetry span JSON/JSONL file.")
|
|
1003
|
+
trace_import_otel.add_argument(
|
|
1004
|
+
"--max-spans",
|
|
1005
|
+
type=int,
|
|
1006
|
+
default=50_000,
|
|
1007
|
+
help="Maximum OpenTelemetry spans to inspect from the file.",
|
|
1008
|
+
)
|
|
1009
|
+
trace_import_otel.add_argument(
|
|
1010
|
+
"--max-file-bytes",
|
|
1011
|
+
type=int,
|
|
1012
|
+
default=10 * 1024 * 1024,
|
|
1013
|
+
help="Maximum OpenTelemetry span file size to import.",
|
|
1014
|
+
)
|
|
1015
|
+
trace_import_otel.add_argument(
|
|
1016
|
+
"--max-seconds",
|
|
1017
|
+
type=float,
|
|
1018
|
+
default=120.0,
|
|
1019
|
+
help="Maximum seconds to spend importing OpenTelemetry spans.",
|
|
1020
|
+
)
|
|
1021
|
+
trace_import_otel.set_defaults(handler=handle_trace_import_otel)
|
|
1022
|
+
trace_import_har = trace_subparsers.add_parser(
|
|
1023
|
+
"import-har", help="Import offline HAR browser network JSON."
|
|
1024
|
+
)
|
|
1025
|
+
trace_import_har.add_argument("path", help="HAR 1.2 JSON file to import.")
|
|
1026
|
+
trace_import_har.add_argument(
|
|
1027
|
+
"--max-entries",
|
|
1028
|
+
type=int,
|
|
1029
|
+
default=50_000,
|
|
1030
|
+
help="Maximum HAR entries to inspect from the file.",
|
|
1031
|
+
)
|
|
1032
|
+
trace_import_har.add_argument(
|
|
1033
|
+
"--max-file-bytes",
|
|
1034
|
+
type=int,
|
|
1035
|
+
default=10 * 1024 * 1024,
|
|
1036
|
+
help="Maximum HAR file size to import.",
|
|
1037
|
+
)
|
|
1038
|
+
trace_import_har.add_argument(
|
|
1039
|
+
"--max-seconds",
|
|
1040
|
+
type=float,
|
|
1041
|
+
default=120.0,
|
|
1042
|
+
help="Maximum seconds to spend importing HAR entries.",
|
|
1043
|
+
)
|
|
1044
|
+
trace_import_har.set_defaults(handler=handle_trace_import_har)
|
|
1045
|
+
trace_run = trace_subparsers.add_parser(
|
|
1046
|
+
"run", help="Run pytest under a lightweight ArcGraph runtime tracer."
|
|
1047
|
+
)
|
|
1048
|
+
trace_run.add_argument(
|
|
1049
|
+
"--output",
|
|
1050
|
+
required=True,
|
|
1051
|
+
help="Path to write the runtime trace JSON file.",
|
|
1052
|
+
)
|
|
1053
|
+
trace_run.add_argument(
|
|
1054
|
+
"--root",
|
|
1055
|
+
action="append",
|
|
1056
|
+
default=[],
|
|
1057
|
+
help="Source root to map runtime frames. Can be repeated.",
|
|
1058
|
+
)
|
|
1059
|
+
trace_run.add_argument(
|
|
1060
|
+
"--max-events",
|
|
1061
|
+
type=int,
|
|
1062
|
+
default=50_000,
|
|
1063
|
+
help="Maximum runtime call events to record.",
|
|
1064
|
+
)
|
|
1065
|
+
trace_run.add_argument(
|
|
1066
|
+
"--max-seconds",
|
|
1067
|
+
type=float,
|
|
1068
|
+
default=120.0,
|
|
1069
|
+
help="Maximum seconds to trace each pytest test call.",
|
|
1070
|
+
)
|
|
1071
|
+
trace_run.add_argument(
|
|
1072
|
+
"pytest_args",
|
|
1073
|
+
nargs=argparse.REMAINDER,
|
|
1074
|
+
help="Arguments passed to pytest after an optional -- separator.",
|
|
1075
|
+
)
|
|
1076
|
+
_add_legacy_roots_arg(trace_run)
|
|
1077
|
+
trace_run.set_defaults(handler=handle_trace_run)
|
|
1078
|
+
|
|
1079
|
+
precision = subparsers.add_parser(
|
|
1080
|
+
"precision", help="Generate or convert precision input artifacts."
|
|
1081
|
+
)
|
|
1082
|
+
precision_subparsers = precision.add_subparsers(dest="precision_command")
|
|
1083
|
+
precision_scip_json = precision_subparsers.add_parser(
|
|
1084
|
+
"scip-json", help="Convert a binary index.scip file to JSON."
|
|
1085
|
+
)
|
|
1086
|
+
precision_scip_json.add_argument(
|
|
1087
|
+
"--input",
|
|
1088
|
+
required=True,
|
|
1089
|
+
help="Path to the binary SCIP index, usually index.scip.",
|
|
1090
|
+
)
|
|
1091
|
+
precision_scip_json.add_argument(
|
|
1092
|
+
"--output",
|
|
1093
|
+
required=True,
|
|
1094
|
+
help="Path to write ArcGraph-readable SCIP JSON.",
|
|
1095
|
+
)
|
|
1096
|
+
precision_scip_json.add_argument(
|
|
1097
|
+
"--scip-command",
|
|
1098
|
+
default="scip",
|
|
1099
|
+
help="SCIP CLI command to run. Defaults to scip.",
|
|
1100
|
+
)
|
|
1101
|
+
precision_scip_json.add_argument(
|
|
1102
|
+
"--root",
|
|
1103
|
+
action="append",
|
|
1104
|
+
default=[],
|
|
1105
|
+
help="Source root represented by the SCIP index. Defaults to ArcGraph roots.",
|
|
1106
|
+
)
|
|
1107
|
+
_add_legacy_roots_arg(precision_scip_json)
|
|
1108
|
+
precision_scip_json.set_defaults(handler=handle_precision_scip_json)
|
|
1109
|
+
|
|
1110
|
+
precision_scip_python = precision_subparsers.add_parser(
|
|
1111
|
+
"scip-python", help="Run scip-python and convert its index to JSON."
|
|
1112
|
+
)
|
|
1113
|
+
precision_scip_python.add_argument(
|
|
1114
|
+
"--output",
|
|
1115
|
+
required=True,
|
|
1116
|
+
help="Path to write ArcGraph-readable SCIP JSON.",
|
|
1117
|
+
)
|
|
1118
|
+
precision_scip_python.add_argument(
|
|
1119
|
+
"--index-file",
|
|
1120
|
+
help="Path to keep the generated binary SCIP index. Defaults beside --output.",
|
|
1121
|
+
)
|
|
1122
|
+
precision_scip_python.add_argument(
|
|
1123
|
+
"--project-name",
|
|
1124
|
+
help="Project name passed to scip-python. Defaults to the repository name.",
|
|
1125
|
+
)
|
|
1126
|
+
precision_scip_python.add_argument(
|
|
1127
|
+
"--project-version",
|
|
1128
|
+
help="Optional project version passed to scip-python.",
|
|
1129
|
+
)
|
|
1130
|
+
precision_scip_python.add_argument(
|
|
1131
|
+
"--target-only",
|
|
1132
|
+
action="append",
|
|
1133
|
+
default=[],
|
|
1134
|
+
help="Restrict scip-python to a target path. Can be repeated.",
|
|
1135
|
+
)
|
|
1136
|
+
precision_scip_python.add_argument(
|
|
1137
|
+
"--environment",
|
|
1138
|
+
help="Optional scip-python environment JSON file.",
|
|
1139
|
+
)
|
|
1140
|
+
precision_scip_python.add_argument(
|
|
1141
|
+
"--scip-python-command",
|
|
1142
|
+
default="scip-python",
|
|
1143
|
+
help="scip-python command to run. Defaults to scip-python.",
|
|
1144
|
+
)
|
|
1145
|
+
precision_scip_python.add_argument(
|
|
1146
|
+
"--scip-command",
|
|
1147
|
+
default="scip",
|
|
1148
|
+
help="SCIP CLI command to run for JSON conversion. Defaults to scip.",
|
|
1149
|
+
)
|
|
1150
|
+
precision_scip_python.add_argument(
|
|
1151
|
+
"--timeout-seconds",
|
|
1152
|
+
type=float,
|
|
1153
|
+
default=600.0,
|
|
1154
|
+
help="Maximum seconds to wait for scip-python.",
|
|
1155
|
+
)
|
|
1156
|
+
precision_scip_python.add_argument(
|
|
1157
|
+
"--root",
|
|
1158
|
+
action="append",
|
|
1159
|
+
default=[],
|
|
1160
|
+
help="Source root represented by the generated SCIP index. Defaults to ArcGraph roots.",
|
|
1161
|
+
)
|
|
1162
|
+
_add_legacy_roots_arg(precision_scip_python)
|
|
1163
|
+
precision_scip_python.set_defaults(handler=handle_precision_scip_python)
|
|
1164
|
+
|
|
1165
|
+
precision_pyright = precision_subparsers.add_parser(
|
|
1166
|
+
"pyright", help="Generate ArcGraph-readable Pyright precision JSON."
|
|
1167
|
+
)
|
|
1168
|
+
precision_pyright.add_argument(
|
|
1169
|
+
"--output",
|
|
1170
|
+
required=True,
|
|
1171
|
+
help="Path to write ArcGraph-readable Pyright JSON.",
|
|
1172
|
+
)
|
|
1173
|
+
precision_pyright.add_argument(
|
|
1174
|
+
"--target-only",
|
|
1175
|
+
action="append",
|
|
1176
|
+
default=[],
|
|
1177
|
+
help="Restrict Pyright probes to a target path. Can be repeated.",
|
|
1178
|
+
)
|
|
1179
|
+
precision_pyright.add_argument(
|
|
1180
|
+
"--root",
|
|
1181
|
+
action="append",
|
|
1182
|
+
default=[],
|
|
1183
|
+
help="Source root to scan for Pyright probes. Defaults to ArcGraph roots.",
|
|
1184
|
+
)
|
|
1185
|
+
precision_pyright.add_argument(
|
|
1186
|
+
"--python-version",
|
|
1187
|
+
default="3.11",
|
|
1188
|
+
help="Python version passed in the Pyright LSP initialization options.",
|
|
1189
|
+
)
|
|
1190
|
+
precision_pyright.add_argument(
|
|
1191
|
+
"--extra-path",
|
|
1192
|
+
action="append",
|
|
1193
|
+
default=[],
|
|
1194
|
+
help="Extra import path passed in the Pyright LSP initialization options.",
|
|
1195
|
+
)
|
|
1196
|
+
precision_pyright.add_argument(
|
|
1197
|
+
"--pyright-command",
|
|
1198
|
+
default="pyright-langserver",
|
|
1199
|
+
help="Pyright language server command. Defaults to pyright-langserver.",
|
|
1200
|
+
)
|
|
1201
|
+
precision_pyright.add_argument(
|
|
1202
|
+
"--timeout-seconds",
|
|
1203
|
+
type=float,
|
|
1204
|
+
default=300.0,
|
|
1205
|
+
help="Maximum seconds to wait for the Pyright LSP probe.",
|
|
1206
|
+
)
|
|
1207
|
+
_add_legacy_roots_arg(precision_pyright)
|
|
1208
|
+
precision_pyright.set_defaults(handler=handle_precision_pyright)
|
|
1209
|
+
|
|
1210
|
+
evidence = subparsers.add_parser(
|
|
1211
|
+
"evidence", help="Inspect and plan external evidence artifacts."
|
|
1212
|
+
)
|
|
1213
|
+
evidence_subparsers = evidence.add_subparsers(dest="evidence_command")
|
|
1214
|
+
evidence_status_cmd = evidence_subparsers.add_parser(
|
|
1215
|
+
"status", help="Report live evidence, index snapshot, and capability status."
|
|
1216
|
+
)
|
|
1217
|
+
evidence_status_cmd.set_defaults(handler=handle_evidence_status)
|
|
1218
|
+
evidence_plan_cmd = evidence_subparsers.add_parser(
|
|
1219
|
+
"plan", help="Report live evidence health and suggested generation commands."
|
|
1220
|
+
)
|
|
1221
|
+
evidence_plan_cmd.add_argument(
|
|
1222
|
+
"--profile",
|
|
1223
|
+
choices=["default", "python_full"],
|
|
1224
|
+
default="default",
|
|
1225
|
+
help="Evidence planning profile. python_full marks blockers for ci --python-full.",
|
|
1226
|
+
)
|
|
1227
|
+
evidence_plan_cmd.set_defaults(handler=handle_evidence_plan)
|
|
1228
|
+
evidence_stamp_cmd = evidence_subparsers.add_parser(
|
|
1229
|
+
"stamp", help="Write ArcGraph provenance metadata beside an evidence artifact."
|
|
1230
|
+
)
|
|
1231
|
+
evidence_stamp_cmd.add_argument(
|
|
1232
|
+
"--kind",
|
|
1233
|
+
required=True,
|
|
1234
|
+
choices=[
|
|
1235
|
+
"precision_scip",
|
|
1236
|
+
"precision_pyright",
|
|
1237
|
+
"coverage",
|
|
1238
|
+
"runtime_trace",
|
|
1239
|
+
],
|
|
1240
|
+
)
|
|
1241
|
+
evidence_stamp_cmd.add_argument(
|
|
1242
|
+
"--path",
|
|
1243
|
+
required=True,
|
|
1244
|
+
help="Evidence artifact path to stamp, such as output/arcgraph/coverage.xml.",
|
|
1245
|
+
)
|
|
1246
|
+
evidence_stamp_cmd.add_argument(
|
|
1247
|
+
"--tool-name",
|
|
1248
|
+
required=True,
|
|
1249
|
+
help="Tool that generated the artifact, such as coverage or pytest-cov.",
|
|
1250
|
+
)
|
|
1251
|
+
evidence_stamp_cmd.add_argument("--tool-version")
|
|
1252
|
+
evidence_stamp_cmd.set_defaults(handler=handle_evidence_stamp)
|
|
1253
|
+
|
|
1254
|
+
ci = subparsers.add_parser("ci", help="Run ArcGraph CI checks.")
|
|
1255
|
+
ci.add_argument("--target", action="append", default=[])
|
|
1256
|
+
ci.add_argument("--changed-file", action="append", default=[])
|
|
1257
|
+
ci.add_argument("--max-results", type=int, default=30)
|
|
1258
|
+
ci.add_argument(
|
|
1259
|
+
"--semantic-baseline",
|
|
1260
|
+
help="Optional semantic-stats or CI JSON baseline for resolution trend checks.",
|
|
1261
|
+
)
|
|
1262
|
+
ci.add_argument(
|
|
1263
|
+
"--performance-budget-ms",
|
|
1264
|
+
action="append",
|
|
1265
|
+
default=[],
|
|
1266
|
+
metavar="OPERATION=MS",
|
|
1267
|
+
help="Override a CI performance budget in milliseconds. Can be repeated.",
|
|
1268
|
+
)
|
|
1269
|
+
ci.add_argument(
|
|
1270
|
+
"--require-precision",
|
|
1271
|
+
action="store_true",
|
|
1272
|
+
help="Fail when SCIP/Pyright precision inputs are unavailable.",
|
|
1273
|
+
)
|
|
1274
|
+
ci.add_argument(
|
|
1275
|
+
"--require-coverage",
|
|
1276
|
+
action="store_true",
|
|
1277
|
+
help="Fail when coverage input is unavailable or partial.",
|
|
1278
|
+
)
|
|
1279
|
+
ci.add_argument(
|
|
1280
|
+
"--require-runtime-trace",
|
|
1281
|
+
action="store_true",
|
|
1282
|
+
help="Fail when runtime trace input is unavailable or partial.",
|
|
1283
|
+
)
|
|
1284
|
+
ci.add_argument(
|
|
1285
|
+
"--python-full",
|
|
1286
|
+
action="store_true",
|
|
1287
|
+
help=(
|
|
1288
|
+
"Require the full Python evidence profile: precision, coverage, "
|
|
1289
|
+
"and runtime trace inputs must all be available."
|
|
1290
|
+
),
|
|
1291
|
+
)
|
|
1292
|
+
_add_ci_runtime_trace_args(ci)
|
|
1293
|
+
ci.add_argument(
|
|
1294
|
+
"--fail-on-warnings",
|
|
1295
|
+
action="store_true",
|
|
1296
|
+
help="Return a non-zero exit code when any CI check warns.",
|
|
1297
|
+
)
|
|
1298
|
+
ci.set_defaults(handler=handle_ci)
|
|
1299
|
+
|
|
1300
|
+
ops = subparsers.add_parser("ops", help="Run ArcGraph operational tasks.")
|
|
1301
|
+
ops_subparsers = ops.add_subparsers(dest="ops_command")
|
|
1302
|
+
ops_rebuild = ops_subparsers.add_parser(
|
|
1303
|
+
"rebuild", help="Build or refresh the current ArcGraph index."
|
|
1304
|
+
)
|
|
1305
|
+
ops_rebuild.add_argument(
|
|
1306
|
+
"--if-stale",
|
|
1307
|
+
action="store_true",
|
|
1308
|
+
help="Skip rebuild when the current index is fresh.",
|
|
1309
|
+
)
|
|
1310
|
+
ops_rebuild.add_argument(
|
|
1311
|
+
"--root",
|
|
1312
|
+
action="append",
|
|
1313
|
+
default=[],
|
|
1314
|
+
help="Source root to scan. Can be repeated.",
|
|
1315
|
+
)
|
|
1316
|
+
ops_rebuild.add_argument(
|
|
1317
|
+
"--scip-index",
|
|
1318
|
+
help="Optional SCIP-derived JSON occurrence file for confirmed references.",
|
|
1319
|
+
)
|
|
1320
|
+
ops_rebuild.add_argument(
|
|
1321
|
+
"--pyright-export",
|
|
1322
|
+
help="Optional Pyright-derived JSON export path for the precision input contract.",
|
|
1323
|
+
)
|
|
1324
|
+
ops_rebuild.add_argument(
|
|
1325
|
+
"--runtime-trace",
|
|
1326
|
+
help="Optional ArcGraph runtime trace JSON file to import as runtime-only evidence.",
|
|
1327
|
+
)
|
|
1328
|
+
ops_rebuild.add_argument(
|
|
1329
|
+
"--coverage",
|
|
1330
|
+
help="Optional coverage JSON or Cobertura XML file for runtime test evidence.",
|
|
1331
|
+
)
|
|
1332
|
+
ops_rebuild.add_argument(
|
|
1333
|
+
"--semantic-call-resolution",
|
|
1334
|
+
action="store_true",
|
|
1335
|
+
help=(
|
|
1336
|
+
"Compatibility flag retained for older scripts; V2 receiver-aware "
|
|
1337
|
+
"call resolution is the default at schema 1.0."
|
|
1338
|
+
),
|
|
1339
|
+
)
|
|
1340
|
+
ops_rebuild.add_argument(
|
|
1341
|
+
"--legacy-call-resolution",
|
|
1342
|
+
action="store_true",
|
|
1343
|
+
help=(
|
|
1344
|
+
"Use the legacy AST call resolver. This escape hatch is rejected by "
|
|
1345
|
+
"ci --python-full."
|
|
1346
|
+
),
|
|
1347
|
+
)
|
|
1348
|
+
_add_legacy_roots_arg(ops_rebuild)
|
|
1349
|
+
ops_rebuild.set_defaults(handler=handle_ops_rebuild)
|
|
1350
|
+
ops_stale = ops_subparsers.add_parser(
|
|
1351
|
+
"stale-alert", help="Report whether the current index is stale."
|
|
1352
|
+
)
|
|
1353
|
+
ops_stale.add_argument(
|
|
1354
|
+
"--fail-on-stale",
|
|
1355
|
+
action="store_true",
|
|
1356
|
+
help="Return a non-zero exit code when the index is stale.",
|
|
1357
|
+
)
|
|
1358
|
+
ops_stale.set_defaults(handler=handle_ops_stale_alert)
|
|
1359
|
+
ops_prune = ops_subparsers.add_parser(
|
|
1360
|
+
"prune",
|
|
1361
|
+
help="Safely prune stale generated ArcGraph output.",
|
|
1362
|
+
)
|
|
1363
|
+
ops_prune.add_argument(
|
|
1364
|
+
"--keep-builds",
|
|
1365
|
+
type=int,
|
|
1366
|
+
default=3,
|
|
1367
|
+
help="Number of build directories to retain, always including current.",
|
|
1368
|
+
)
|
|
1369
|
+
ops_prune.add_argument(
|
|
1370
|
+
"--include-input-artifacts",
|
|
1371
|
+
action="store_true",
|
|
1372
|
+
help=(
|
|
1373
|
+
"Also delete generated root-level precision, coverage, and runtime "
|
|
1374
|
+
"input artifacts. Build history is the only default cleanup scope."
|
|
1375
|
+
),
|
|
1376
|
+
)
|
|
1377
|
+
ops_prune.add_argument(
|
|
1378
|
+
"--apply",
|
|
1379
|
+
action="store_true",
|
|
1380
|
+
help="Delete planned candidates. Without this flag, prune is a dry run.",
|
|
1381
|
+
)
|
|
1382
|
+
ops_prune.set_defaults(handler=handle_ops_prune)
|
|
1383
|
+
|
|
1384
|
+
metrics = subparsers.add_parser("metrics", help="Summarize ArcGraph metrics JSONL.")
|
|
1385
|
+
metrics.add_argument("path", help="Metrics JSONL path.")
|
|
1386
|
+
metrics.add_argument("--limit", type=int, default=1000)
|
|
1387
|
+
metrics.add_argument(
|
|
1388
|
+
"--trial-summary",
|
|
1389
|
+
action="store_true",
|
|
1390
|
+
help=(
|
|
1391
|
+
"Return a privacy-bounded trial aggregate without paths, timestamps, "
|
|
1392
|
+
"or raw events."
|
|
1393
|
+
),
|
|
1394
|
+
)
|
|
1395
|
+
metrics.set_defaults(handler=handle_metrics_summary)
|
|
1396
|
+
|
|
1397
|
+
add_change_parser(subparsers)
|
|
1398
|
+
mcp = subparsers.add_parser("mcp", help="Run local MCP server commands.")
|
|
1399
|
+
mcp_subparsers = mcp.add_subparsers(dest="mcp_command")
|
|
1400
|
+
mcp.set_defaults(handler=lambda args, parser=mcp: parser.print_help())
|
|
1401
|
+
mcp_serve = mcp_subparsers.add_parser(
|
|
1402
|
+
"serve",
|
|
1403
|
+
help=(
|
|
1404
|
+
"Run the local stdio MCP server. Default tools are read-only; an "
|
|
1405
|
+
"explicit feedback log enables one local append. Requires the "
|
|
1406
|
+
"optional MCP runtime extra."
|
|
1407
|
+
),
|
|
1408
|
+
)
|
|
1409
|
+
add_mcp_server_args(mcp_serve)
|
|
1410
|
+
mcp_serve.set_defaults(handler=handle_mcp_serve)
|
|
1411
|
+
|
|
1412
|
+
add_trial_parser(subparsers)
|
|
1413
|
+
add_setup_parser(subparsers)
|
|
1414
|
+
|
|
1415
|
+
symbol = subparsers.add_parser(
|
|
1416
|
+
"symbol", help="Find a module, class, function, or method definition."
|
|
1417
|
+
)
|
|
1418
|
+
symbol.add_argument("qualname", help="Dotted qualname or stable symbol id.")
|
|
1419
|
+
symbol.set_defaults(handler=lambda args: query_engine(args).symbol(args.qualname))
|
|
1420
|
+
|
|
1421
|
+
current = subparsers.add_parser("current", help="Show current index metadata.")
|
|
1422
|
+
current.set_defaults(handler=lambda args: query_engine(args).current())
|
|
1423
|
+
status = subparsers.add_parser("status", help="Alias for current index metadata.")
|
|
1424
|
+
status.set_defaults(handler=lambda args: query_engine(args).current())
|
|
1425
|
+
|
|
1426
|
+
add_workspace_parser(subparsers)
|
|
1427
|
+
add_benchmark_parser(subparsers)
|
|
1428
|
+
add_feedback_parser(subparsers)
|
|
1429
|
+
agent_help = subparsers.add_parser(
|
|
1430
|
+
"help",
|
|
1431
|
+
help="Show structured Agent guidance for ArcGraph capabilities.",
|
|
1432
|
+
)
|
|
1433
|
+
agent_help.add_argument(
|
|
1434
|
+
"--topic",
|
|
1435
|
+
default="overview",
|
|
1436
|
+
choices=HELP_TOPICS,
|
|
1437
|
+
help="Agent guidance topic to render.",
|
|
1438
|
+
)
|
|
1439
|
+
agent_help.add_argument(
|
|
1440
|
+
"--tool",
|
|
1441
|
+
help="Exact registered MCP tool name to explain.",
|
|
1442
|
+
)
|
|
1443
|
+
agent_help.add_argument(
|
|
1444
|
+
"--surface",
|
|
1445
|
+
default="all",
|
|
1446
|
+
choices=HELP_SURFACES,
|
|
1447
|
+
help="Capability surface to include.",
|
|
1448
|
+
)
|
|
1449
|
+
agent_help.set_defaults(
|
|
1450
|
+
handler=lambda args, choices=subparsers.choices: handle_agent_help(
|
|
1451
|
+
args,
|
|
1452
|
+
cli_commands=tuple(choices),
|
|
1453
|
+
)
|
|
1454
|
+
)
|
|
1455
|
+
docs = subparsers.add_parser(
|
|
1456
|
+
"docs", help="Show built-in ArcGraph user reference docs."
|
|
1457
|
+
)
|
|
1458
|
+
docs.add_argument(
|
|
1459
|
+
"topic",
|
|
1460
|
+
nargs="?",
|
|
1461
|
+
default="cli-reference",
|
|
1462
|
+
choices=DOC_TOPICS,
|
|
1463
|
+
help="Documentation topic to render.",
|
|
1464
|
+
)
|
|
1465
|
+
docs.add_argument(
|
|
1466
|
+
"--json",
|
|
1467
|
+
action="store_true",
|
|
1468
|
+
help="Render the docs topic as structured JSON.",
|
|
1469
|
+
)
|
|
1470
|
+
docs.set_defaults(handler=lambda args: render_docs(args.topic, as_json=args.json))
|
|
1471
|
+
|
|
1472
|
+
return parser
|
|
1473
|
+
|
|
1474
|
+
|
|
1475
|
+
def handle_agent_help(
|
|
1476
|
+
args: argparse.Namespace,
|
|
1477
|
+
*,
|
|
1478
|
+
cli_commands: tuple[str, ...],
|
|
1479
|
+
) -> dict[str, Any]:
|
|
1480
|
+
payload = build_agent_help(
|
|
1481
|
+
topic=cast(HelpTopic, args.topic),
|
|
1482
|
+
tool_name=args.tool,
|
|
1483
|
+
surface=args.surface,
|
|
1484
|
+
cli_commands=cli_commands,
|
|
1485
|
+
)
|
|
1486
|
+
if payload["status"] == "unavailable":
|
|
1487
|
+
payload["_exit_code"] = 2
|
|
1488
|
+
return payload
|
|
1489
|
+
|
|
1490
|
+
|
|
1491
|
+
def _resolve_source_roots(
|
|
1492
|
+
args: argparse.Namespace,
|
|
1493
|
+
repo_root: Path,
|
|
1494
|
+
) -> ResolvedSourceRoots:
|
|
1495
|
+
"""Unified source root resolution for all CLI commands.
|
|
1496
|
+
|
|
1497
|
+
Priority: --root (explicit) > --legacy-roots > auto-detect.
|
|
1498
|
+
"""
|
|
1499
|
+
has_root = bool(getattr(args, "root", None))
|
|
1500
|
+
has_legacy = getattr(args, "legacy_roots", False)
|
|
1501
|
+
|
|
1502
|
+
if has_root and has_legacy:
|
|
1503
|
+
raise SystemExit("--root and --legacy-roots are mutually exclusive")
|
|
1504
|
+
|
|
1505
|
+
if has_root:
|
|
1506
|
+
roots = infer_source_roots(args.root)
|
|
1507
|
+
return ResolvedSourceRoots(
|
|
1508
|
+
roots=tuple(roots),
|
|
1509
|
+
detection=SourceRootDetection(
|
|
1510
|
+
strategy="cli_explicit",
|
|
1511
|
+
roots=tuple(r.path for r in roots),
|
|
1512
|
+
),
|
|
1513
|
+
)
|
|
1514
|
+
|
|
1515
|
+
if has_legacy:
|
|
1516
|
+
return ResolvedSourceRoots(
|
|
1517
|
+
roots=LEGACY_SOURCE_ROOTS,
|
|
1518
|
+
detection=SourceRootDetection(
|
|
1519
|
+
strategy="legacy_roots",
|
|
1520
|
+
roots=tuple(r.path for r in LEGACY_SOURCE_ROOTS),
|
|
1521
|
+
),
|
|
1522
|
+
)
|
|
1523
|
+
|
|
1524
|
+
return detect_source_roots(repo_root)
|
|
1525
|
+
|
|
1526
|
+
|
|
1527
|
+
def handle_init(args: argparse.Namespace) -> dict[str, Any]:
|
|
1528
|
+
repo_root = Path(args.repo_root).resolve()
|
|
1529
|
+
pyproject_path = repo_root / "pyproject.toml"
|
|
1530
|
+
resolved = detect_source_roots(repo_root)
|
|
1531
|
+
snippet = _arcgraph_config_snippet(resolved.roots)
|
|
1532
|
+
base_payload: dict[str, Any] = {
|
|
1533
|
+
"status": "available",
|
|
1534
|
+
"repo_root": str(repo_root),
|
|
1535
|
+
"pyproject_path": str(pyproject_path),
|
|
1536
|
+
"pyproject_exists": pyproject_path.exists(),
|
|
1537
|
+
"source_root_detection": {
|
|
1538
|
+
"strategy": resolved.detection.strategy,
|
|
1539
|
+
"roots": list(resolved.detection.roots),
|
|
1540
|
+
"non_package_dirs": list(resolved.detection.non_package_dirs),
|
|
1541
|
+
},
|
|
1542
|
+
"source_roots": [
|
|
1543
|
+
{"path": root.path, "module_prefix": root.module_prefix}
|
|
1544
|
+
for root in resolved.roots
|
|
1545
|
+
],
|
|
1546
|
+
"config_snippet": snippet,
|
|
1547
|
+
"wrote": False,
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
if not pyproject_path.exists():
|
|
1551
|
+
base_payload.update(
|
|
1552
|
+
{
|
|
1553
|
+
"action": "manual_config",
|
|
1554
|
+
"message": (
|
|
1555
|
+
"No pyproject.toml found. Add the config snippet manually, "
|
|
1556
|
+
"or create pyproject.toml first."
|
|
1557
|
+
),
|
|
1558
|
+
"next_steps": [
|
|
1559
|
+
"Add the shown [tool.arcgraph] section to pyproject.toml.",
|
|
1560
|
+
"Run arcgraph build.",
|
|
1561
|
+
],
|
|
1562
|
+
}
|
|
1563
|
+
)
|
|
1564
|
+
return base_payload
|
|
1565
|
+
|
|
1566
|
+
pyproject_text = pyproject_path.read_text(encoding="utf-8")
|
|
1567
|
+
if _pyproject_has_arcgraph_config(pyproject_text):
|
|
1568
|
+
base_payload.update(
|
|
1569
|
+
{
|
|
1570
|
+
"action": "already_configured",
|
|
1571
|
+
"message": "pyproject.toml already contains [tool.arcgraph] source_roots.",
|
|
1572
|
+
"next_steps": ["Run arcgraph build."],
|
|
1573
|
+
}
|
|
1574
|
+
)
|
|
1575
|
+
return base_payload
|
|
1576
|
+
|
|
1577
|
+
if args.dry_run:
|
|
1578
|
+
base_payload.update(
|
|
1579
|
+
{
|
|
1580
|
+
"action": "dry_run",
|
|
1581
|
+
"message": "Dry run only; pyproject.toml was not modified.",
|
|
1582
|
+
"next_steps": [
|
|
1583
|
+
"Rerun without --dry-run to write the config.",
|
|
1584
|
+
"Run arcgraph build.",
|
|
1585
|
+
],
|
|
1586
|
+
}
|
|
1587
|
+
)
|
|
1588
|
+
return base_payload
|
|
1589
|
+
|
|
1590
|
+
separator = "" if pyproject_text.endswith("\n") else "\n"
|
|
1591
|
+
pyproject_path.write_text(
|
|
1592
|
+
f"{pyproject_text}{separator}\n{snippet}\n",
|
|
1593
|
+
encoding="utf-8",
|
|
1594
|
+
)
|
|
1595
|
+
base_payload.update(
|
|
1596
|
+
{
|
|
1597
|
+
"action": "written",
|
|
1598
|
+
"message": "Added [tool.arcgraph] to pyproject.toml.",
|
|
1599
|
+
"wrote": True,
|
|
1600
|
+
"next_steps": ["Run arcgraph build."],
|
|
1601
|
+
}
|
|
1602
|
+
)
|
|
1603
|
+
return base_payload
|
|
1604
|
+
|
|
1605
|
+
|
|
1606
|
+
def handle_doctor(args: argparse.Namespace) -> dict[str, Any]:
|
|
1607
|
+
"""Run environment and index health checks."""
|
|
1608
|
+
import shutil
|
|
1609
|
+
|
|
1610
|
+
repo_root = Path(args.repo_root).resolve()
|
|
1611
|
+
output_dir = (repo_root / args.output_dir).resolve()
|
|
1612
|
+
checks: list[dict[str, Any]] = []
|
|
1613
|
+
detected_source_roots: list[str] = []
|
|
1614
|
+
project_languages = detect_project_languages(repo_root)
|
|
1615
|
+
|
|
1616
|
+
# 1. Project config -------------------------------------------------------
|
|
1617
|
+
pyproject_path = repo_root / "pyproject.toml"
|
|
1618
|
+
if not pyproject_path.exists() and project_languages == {"node"}:
|
|
1619
|
+
checks.append(
|
|
1620
|
+
{
|
|
1621
|
+
"name": "project_config",
|
|
1622
|
+
"status": "pass",
|
|
1623
|
+
"message": (
|
|
1624
|
+
"Node/TypeScript project detected from package.json or tsconfig; "
|
|
1625
|
+
"pyproject.toml is not required."
|
|
1626
|
+
),
|
|
1627
|
+
}
|
|
1628
|
+
)
|
|
1629
|
+
elif not pyproject_path.exists():
|
|
1630
|
+
checks.append(
|
|
1631
|
+
{
|
|
1632
|
+
"name": "project_config",
|
|
1633
|
+
"status": "warn",
|
|
1634
|
+
"message": "No pyproject.toml found.",
|
|
1635
|
+
"fix": "Run arcgraph init or create pyproject.toml manually.",
|
|
1636
|
+
}
|
|
1637
|
+
)
|
|
1638
|
+
else:
|
|
1639
|
+
text = pyproject_path.read_text(encoding="utf-8")
|
|
1640
|
+
if _pyproject_has_arcgraph_config(text):
|
|
1641
|
+
checks.append(
|
|
1642
|
+
{
|
|
1643
|
+
"name": "project_config",
|
|
1644
|
+
"status": "pass",
|
|
1645
|
+
"message": "pyproject.toml has [tool.arcgraph] source_roots config.",
|
|
1646
|
+
}
|
|
1647
|
+
)
|
|
1648
|
+
else:
|
|
1649
|
+
checks.append(
|
|
1650
|
+
{
|
|
1651
|
+
"name": "project_config",
|
|
1652
|
+
"status": "warn",
|
|
1653
|
+
"message": "pyproject.toml exists but has no [tool.arcgraph] source_roots config.",
|
|
1654
|
+
"fix": "Run arcgraph init to add it.",
|
|
1655
|
+
}
|
|
1656
|
+
)
|
|
1657
|
+
|
|
1658
|
+
# 2. Source roots ----------------------------------------------------------
|
|
1659
|
+
external_toolchains: dict[str, Any] | None = None
|
|
1660
|
+
try:
|
|
1661
|
+
resolved = detect_source_roots(repo_root)
|
|
1662
|
+
detected_source_roots = [root.path for root in resolved.roots]
|
|
1663
|
+
root_paths = [repo_root / r.path for r in resolved.roots]
|
|
1664
|
+
missing = [r.path for r in resolved.roots if not (repo_root / r.path).is_dir()]
|
|
1665
|
+
if missing:
|
|
1666
|
+
checks.append(
|
|
1667
|
+
{
|
|
1668
|
+
"name": "source_roots",
|
|
1669
|
+
"status": "fail",
|
|
1670
|
+
"message": f"{len(missing)} source root(s) missing on disk: {', '.join(missing)}.",
|
|
1671
|
+
"fix": "Check your [tool.arcgraph] source_roots or project layout.",
|
|
1672
|
+
}
|
|
1673
|
+
)
|
|
1674
|
+
else:
|
|
1675
|
+
source_counts = source_file_counts(root_paths)
|
|
1676
|
+
source_count = sum(source_counts.values())
|
|
1677
|
+
checks.append(
|
|
1678
|
+
{
|
|
1679
|
+
"name": "source_roots",
|
|
1680
|
+
"status": "pass" if source_count > 0 else "warn",
|
|
1681
|
+
"message": (
|
|
1682
|
+
f"{len(resolved.roots)} root(s) detected ({resolved.detection.strategy}), "
|
|
1683
|
+
f"{source_count} supported source file(s) "
|
|
1684
|
+
f"({', '.join(f'{name}={count}' for name, count in source_counts.items() if count)})."
|
|
1685
|
+
),
|
|
1686
|
+
"fix": "Check source_roots config." if source_count == 0 else None,
|
|
1687
|
+
}
|
|
1688
|
+
)
|
|
1689
|
+
except Exception as exc:
|
|
1690
|
+
checks.append(
|
|
1691
|
+
{
|
|
1692
|
+
"name": "source_roots",
|
|
1693
|
+
"status": "fail",
|
|
1694
|
+
"message": f"Source root detection failed: {exc}",
|
|
1695
|
+
}
|
|
1696
|
+
)
|
|
1697
|
+
|
|
1698
|
+
# 3. Index -----------------------------------------------------------------
|
|
1699
|
+
current_path = output_dir / "current.json"
|
|
1700
|
+
if not current_path.exists():
|
|
1701
|
+
checks.append(
|
|
1702
|
+
{
|
|
1703
|
+
"name": "index",
|
|
1704
|
+
"status": "warn",
|
|
1705
|
+
"message": f"No index found at {output_dir}.",
|
|
1706
|
+
"fix": "Run arcgraph build.",
|
|
1707
|
+
}
|
|
1708
|
+
)
|
|
1709
|
+
else:
|
|
1710
|
+
try:
|
|
1711
|
+
engine = QueryEngine(output_dir)
|
|
1712
|
+
cur = engine.current()
|
|
1713
|
+
raw_toolchains = cur.get("toolchain_status", {})
|
|
1714
|
+
external_toolchains = (
|
|
1715
|
+
raw_toolchains if isinstance(raw_toolchains, dict) else {}
|
|
1716
|
+
)
|
|
1717
|
+
freshness = cur.get("freshness", {})
|
|
1718
|
+
fresh_status = freshness.get("status", "unknown")
|
|
1719
|
+
schema = cur.get("schema_version", "?")
|
|
1720
|
+
nodes = cur.get("node_count", 0)
|
|
1721
|
+
edges = cur.get("edge_count", 0)
|
|
1722
|
+
if fresh_status == "fresh":
|
|
1723
|
+
checks.append(
|
|
1724
|
+
{
|
|
1725
|
+
"name": "index",
|
|
1726
|
+
"status": "pass",
|
|
1727
|
+
"message": f"Index is fresh (schema {schema}, {nodes:,} nodes, {edges:,} edges).",
|
|
1728
|
+
}
|
|
1729
|
+
)
|
|
1730
|
+
else:
|
|
1731
|
+
stale_files = freshness.get("stale_files", [])
|
|
1732
|
+
checks.append(
|
|
1733
|
+
{
|
|
1734
|
+
"name": "index",
|
|
1735
|
+
"status": "warn",
|
|
1736
|
+
"message": (
|
|
1737
|
+
f"Index is {fresh_status} (schema {schema}, "
|
|
1738
|
+
f"{len(stale_files)} file(s) changed)."
|
|
1739
|
+
),
|
|
1740
|
+
"fix": (
|
|
1741
|
+
"Run arcgraph sync --if-stale; use arcgraph build "
|
|
1742
|
+
"when the index is missing or incompatible."
|
|
1743
|
+
),
|
|
1744
|
+
}
|
|
1745
|
+
)
|
|
1746
|
+
except SchemaVersionError as exc:
|
|
1747
|
+
checks.append(
|
|
1748
|
+
{
|
|
1749
|
+
"name": "index",
|
|
1750
|
+
"status": "fail",
|
|
1751
|
+
"message": str(exc),
|
|
1752
|
+
"fix": "Run arcgraph build to rebuild with the current schema.",
|
|
1753
|
+
}
|
|
1754
|
+
)
|
|
1755
|
+
except FileNotFoundError as exc:
|
|
1756
|
+
checks.append(
|
|
1757
|
+
{
|
|
1758
|
+
"name": "index",
|
|
1759
|
+
"status": "fail",
|
|
1760
|
+
"message": str(exc),
|
|
1761
|
+
"fix": "Run arcgraph build.",
|
|
1762
|
+
}
|
|
1763
|
+
)
|
|
1764
|
+
|
|
1765
|
+
# 4. External semantic extractor toolchains -------------------------------
|
|
1766
|
+
if external_toolchains:
|
|
1767
|
+
toolchain_summary = summarize_toolchain_status(external_toolchains)
|
|
1768
|
+
required_missing = toolchain_summary["required_unavailable"]
|
|
1769
|
+
checks.append(
|
|
1770
|
+
{
|
|
1771
|
+
"name": "external_toolchains",
|
|
1772
|
+
"status": "warn" if required_missing else "pass",
|
|
1773
|
+
"message": (
|
|
1774
|
+
"External semantic extractor toolchains are available or optional."
|
|
1775
|
+
if not required_missing
|
|
1776
|
+
else "Required external semantic extractor toolchains are unavailable."
|
|
1777
|
+
),
|
|
1778
|
+
"details": toolchain_summary,
|
|
1779
|
+
}
|
|
1780
|
+
)
|
|
1781
|
+
else:
|
|
1782
|
+
checks.append(
|
|
1783
|
+
{
|
|
1784
|
+
"name": "external_toolchains",
|
|
1785
|
+
"status": "pass",
|
|
1786
|
+
"message": "No external semantic extractor toolchains recorded.",
|
|
1787
|
+
}
|
|
1788
|
+
)
|
|
1789
|
+
|
|
1790
|
+
# 5. Evidence health ------------------------------------------------------
|
|
1791
|
+
try:
|
|
1792
|
+
manifest = load_live_evidence_manifest(
|
|
1793
|
+
repo_root=repo_root,
|
|
1794
|
+
output_dir=output_dir,
|
|
1795
|
+
commit_sha=current_commit(repo_root),
|
|
1796
|
+
source_roots=detected_source_roots or _detect_source_root_paths(repo_root),
|
|
1797
|
+
)
|
|
1798
|
+
evidence_status = evidence_health_check_status(manifest)
|
|
1799
|
+
checks.append(
|
|
1800
|
+
{
|
|
1801
|
+
"name": "evidence_health",
|
|
1802
|
+
"status": "pass" if evidence_status == "available" else "warn",
|
|
1803
|
+
"message": (
|
|
1804
|
+
"All external evidence artifacts are available."
|
|
1805
|
+
if evidence_status == "available"
|
|
1806
|
+
else f"External evidence health is {evidence_status}."
|
|
1807
|
+
),
|
|
1808
|
+
"fix": (
|
|
1809
|
+
None
|
|
1810
|
+
if evidence_status == "available"
|
|
1811
|
+
else "Run arcgraph evidence plan for generation commands."
|
|
1812
|
+
),
|
|
1813
|
+
"details": manifest.get("summary", {}),
|
|
1814
|
+
}
|
|
1815
|
+
)
|
|
1816
|
+
except Exception as exc:
|
|
1817
|
+
checks.append(
|
|
1818
|
+
{
|
|
1819
|
+
"name": "evidence_health",
|
|
1820
|
+
"status": "warn",
|
|
1821
|
+
"message": f"Evidence health could not be evaluated: {exc}",
|
|
1822
|
+
"fix": "Run arcgraph build to regenerate evidence manifest metadata.",
|
|
1823
|
+
}
|
|
1824
|
+
)
|
|
1825
|
+
|
|
1826
|
+
# 6. Precision tools -------------------------------------------------------
|
|
1827
|
+
tool_status: list[tuple[str, str | None]] = [
|
|
1828
|
+
("scip-python", shutil.which("scip-python")),
|
|
1829
|
+
("scip", shutil.which("scip")),
|
|
1830
|
+
("pyright-langserver", shutil.which("pyright-langserver")),
|
|
1831
|
+
]
|
|
1832
|
+
found = [(name, path) for name, path in tool_status if path]
|
|
1833
|
+
missing_tools = [name for name, path in tool_status if not path]
|
|
1834
|
+
if not missing_tools:
|
|
1835
|
+
checks.append(
|
|
1836
|
+
{
|
|
1837
|
+
"name": "precision_tools",
|
|
1838
|
+
"status": "pass",
|
|
1839
|
+
"message": f"All precision tools available ({', '.join(n for n, _ in found)}).",
|
|
1840
|
+
}
|
|
1841
|
+
)
|
|
1842
|
+
else:
|
|
1843
|
+
checks.append(
|
|
1844
|
+
{
|
|
1845
|
+
"name": "precision_tools",
|
|
1846
|
+
"status": "warn",
|
|
1847
|
+
"message": f"Missing: {', '.join(missing_tools)}.",
|
|
1848
|
+
"fix": (
|
|
1849
|
+
"Run scripts/install-arcgraph-precision-tools.ps1 (Windows) or "
|
|
1850
|
+
"install scip-python, scip, pyright manually. "
|
|
1851
|
+
"Precision tools are optional; builds work without them."
|
|
1852
|
+
),
|
|
1853
|
+
}
|
|
1854
|
+
)
|
|
1855
|
+
|
|
1856
|
+
import platform
|
|
1857
|
+
|
|
1858
|
+
py_ver = platform.python_version()
|
|
1859
|
+
py_tuple = tuple(int(x) for x in py_ver.split(".")[:2])
|
|
1860
|
+
if (3, 11) <= py_tuple < (3, 13):
|
|
1861
|
+
checks.append(
|
|
1862
|
+
{
|
|
1863
|
+
"name": "python_version",
|
|
1864
|
+
"status": "pass",
|
|
1865
|
+
"message": f"Python {py_ver}.",
|
|
1866
|
+
}
|
|
1867
|
+
)
|
|
1868
|
+
else:
|
|
1869
|
+
checks.append(
|
|
1870
|
+
{
|
|
1871
|
+
"name": "python_version",
|
|
1872
|
+
"status": "fail",
|
|
1873
|
+
"message": f"Python {py_ver} (requires >=3.11,<3.13).",
|
|
1874
|
+
"fix": "Recreate the tool environment with Python 3.11 or 3.12.",
|
|
1875
|
+
}
|
|
1876
|
+
)
|
|
1877
|
+
|
|
1878
|
+
if "node" in project_languages:
|
|
1879
|
+
node_command = shutil.which("node")
|
|
1880
|
+
if node_command is None:
|
|
1881
|
+
checks.append(
|
|
1882
|
+
{
|
|
1883
|
+
"name": "node_runtime",
|
|
1884
|
+
"status": "fail",
|
|
1885
|
+
"message": "Node/TypeScript project detected but Node.js is unavailable.",
|
|
1886
|
+
"fix": "Install a supported Node.js runtime (20, 22, or 24).",
|
|
1887
|
+
}
|
|
1888
|
+
)
|
|
1889
|
+
else:
|
|
1890
|
+
try:
|
|
1891
|
+
completed = subprocess.run(
|
|
1892
|
+
[node_command, "--version"],
|
|
1893
|
+
cwd=repo_root,
|
|
1894
|
+
check=False,
|
|
1895
|
+
capture_output=True,
|
|
1896
|
+
text=True,
|
|
1897
|
+
encoding="utf-8",
|
|
1898
|
+
errors="replace",
|
|
1899
|
+
timeout=10,
|
|
1900
|
+
)
|
|
1901
|
+
except subprocess.TimeoutExpired:
|
|
1902
|
+
completed = None
|
|
1903
|
+
checks.append(
|
|
1904
|
+
{
|
|
1905
|
+
"name": "node_runtime",
|
|
1906
|
+
"status": (
|
|
1907
|
+
"pass"
|
|
1908
|
+
if completed is not None and completed.returncode == 0
|
|
1909
|
+
else "fail"
|
|
1910
|
+
),
|
|
1911
|
+
"message": (
|
|
1912
|
+
f"Node.js {completed.stdout.strip()}."
|
|
1913
|
+
if completed is not None and completed.returncode == 0
|
|
1914
|
+
else (
|
|
1915
|
+
"Node.js version probe timed out."
|
|
1916
|
+
if completed is None
|
|
1917
|
+
else "Node.js version probe failed."
|
|
1918
|
+
)
|
|
1919
|
+
),
|
|
1920
|
+
}
|
|
1921
|
+
)
|
|
1922
|
+
try:
|
|
1923
|
+
compiler = subprocess.run(
|
|
1924
|
+
[node_command, "-e", "require.resolve('typescript')"],
|
|
1925
|
+
cwd=repo_root,
|
|
1926
|
+
check=False,
|
|
1927
|
+
capture_output=True,
|
|
1928
|
+
text=True,
|
|
1929
|
+
encoding="utf-8",
|
|
1930
|
+
errors="replace",
|
|
1931
|
+
timeout=10,
|
|
1932
|
+
)
|
|
1933
|
+
except subprocess.TimeoutExpired:
|
|
1934
|
+
compiler = None
|
|
1935
|
+
checks.append(
|
|
1936
|
+
{
|
|
1937
|
+
"name": "typescript_compiler",
|
|
1938
|
+
"status": (
|
|
1939
|
+
"pass"
|
|
1940
|
+
if compiler is not None and compiler.returncode == 0
|
|
1941
|
+
else "warn"
|
|
1942
|
+
),
|
|
1943
|
+
"message": (
|
|
1944
|
+
"TypeScript compiler API is resolvable from the project."
|
|
1945
|
+
if compiler is not None and compiler.returncode == 0
|
|
1946
|
+
else (
|
|
1947
|
+
"TypeScript compiler probe timed out."
|
|
1948
|
+
if compiler is None
|
|
1949
|
+
else "TypeScript compiler API is not resolvable from the project."
|
|
1950
|
+
)
|
|
1951
|
+
),
|
|
1952
|
+
"fix": (
|
|
1953
|
+
None
|
|
1954
|
+
if compiler is not None and compiler.returncode == 0
|
|
1955
|
+
else "Install the project's locked TypeScript dependency."
|
|
1956
|
+
),
|
|
1957
|
+
}
|
|
1958
|
+
)
|
|
1959
|
+
fail_count = sum(1 for c in checks if c["status"] == "fail")
|
|
1960
|
+
warn_count = sum(1 for c in checks if c["status"] == "warn")
|
|
1961
|
+
overall = "fail" if fail_count > 0 else "warn" if warn_count > 0 else "pass"
|
|
1962
|
+
|
|
1963
|
+
return {
|
|
1964
|
+
"status": overall,
|
|
1965
|
+
"project_languages": sorted(project_languages),
|
|
1966
|
+
"checks": checks,
|
|
1967
|
+
"summary": {
|
|
1968
|
+
"pass": sum(1 for c in checks if c["status"] == "pass"),
|
|
1969
|
+
"warn": warn_count,
|
|
1970
|
+
"fail": fail_count,
|
|
1971
|
+
},
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
|
|
1975
|
+
def handle_build(args: argparse.Namespace) -> dict[str, Any]:
|
|
1976
|
+
repo_root = Path(args.repo_root).resolve()
|
|
1977
|
+
output_dir = (repo_root / args.output_dir).resolve()
|
|
1978
|
+
resolved = _resolve_source_roots(args, repo_root)
|
|
1979
|
+
# Merge [tool.arcgraph] exclude patterns with default ignore_rules
|
|
1980
|
+
exclude = resolved.detection.exclude
|
|
1981
|
+
ignore_rules: list[str] | None = (
|
|
1982
|
+
list(DEFAULT_IGNORE_RULES) + list(exclude) if exclude else None
|
|
1983
|
+
)
|
|
1984
|
+
indexer = ArcGraphIndexer(
|
|
1985
|
+
repo_root,
|
|
1986
|
+
output_dir,
|
|
1987
|
+
resolved.roots,
|
|
1988
|
+
ignore_rules=ignore_rules,
|
|
1989
|
+
scip_index_path=args.scip_index,
|
|
1990
|
+
scip_graph_index_path=args.scip_graph_index,
|
|
1991
|
+
openapi_path=args.openapi_spec,
|
|
1992
|
+
pyright_export_path=args.pyright_export,
|
|
1993
|
+
runtime_trace_path=args.runtime_trace,
|
|
1994
|
+
coverage_path=args.coverage,
|
|
1995
|
+
enable_v2_call_resolution=not args.legacy_call_resolution,
|
|
1996
|
+
source_root_detection=resolved.detection,
|
|
1997
|
+
)
|
|
1998
|
+
metadata, build_dir = indexer.build()
|
|
1999
|
+
payload = metadata.model_dump(mode="json")
|
|
2000
|
+
payload["build_dir"] = str(build_dir)
|
|
2001
|
+
if indexer.last_cleanup_result is not None:
|
|
2002
|
+
payload["cleanup"] = indexer.last_cleanup_result
|
|
2003
|
+
return payload
|
|
2004
|
+
|
|
2005
|
+
|
|
2006
|
+
def handle_reindex(args: argparse.Namespace) -> dict[str, Any]:
|
|
2007
|
+
if not args.changed:
|
|
2008
|
+
raise RuntimeError("Phase 3 only supports ArcGraph reindex --changed")
|
|
2009
|
+
repo_root = Path(args.repo_root).resolve()
|
|
2010
|
+
output_dir = (repo_root / args.output_dir).resolve()
|
|
2011
|
+
return ArcGraphReindexer(repo_root, output_dir).reindex_changed()
|
|
2012
|
+
|
|
2013
|
+
|
|
2014
|
+
def handle_semantic_stats(args: argparse.Namespace) -> dict[str, Any]:
|
|
2015
|
+
repo_root = Path(args.repo_root).resolve()
|
|
2016
|
+
output_dir = (repo_root / args.output_dir).resolve()
|
|
2017
|
+
payload = query_engine(args).semantic_stats()
|
|
2018
|
+
output_path = (
|
|
2019
|
+
Path(args.output)
|
|
2020
|
+
if args.output
|
|
2021
|
+
else output_dir / "metrics" / "semantic-stats.json"
|
|
2022
|
+
)
|
|
2023
|
+
named = bool(args.output)
|
|
2024
|
+
payload["metrics_path"] = str(output_location(output_path, named_by_user=named))
|
|
2025
|
+
write_json_output(output_path, payload, named_by_user=named)
|
|
2026
|
+
return payload
|
|
2027
|
+
|
|
2028
|
+
|
|
2029
|
+
def handle_ci(args: argparse.Namespace) -> dict[str, Any]:
|
|
2030
|
+
payload = handle_ci_payload(args)
|
|
2031
|
+
payload["_exit_code"] = 1 if payload["status"] == "fail" else 0
|
|
2032
|
+
if args.fail_on_warnings and payload["status"] == "warn":
|
|
2033
|
+
payload["_exit_code"] = 1
|
|
2034
|
+
return payload
|
|
2035
|
+
|
|
2036
|
+
|
|
2037
|
+
def handle_ci_payload(args: argparse.Namespace) -> dict[str, Any]:
|
|
2038
|
+
_import_runtime_trace_for_ci(args)
|
|
2039
|
+
return run_ci_checks(
|
|
2040
|
+
query_engine(args),
|
|
2041
|
+
targets=args.target,
|
|
2042
|
+
changed_files=args.changed_file,
|
|
2043
|
+
max_results=args.max_results,
|
|
2044
|
+
semantic_baseline=_load_json_arg(args.semantic_baseline),
|
|
2045
|
+
performance_budgets_ms=_load_performance_budgets_arg(
|
|
2046
|
+
args.performance_budget_ms
|
|
2047
|
+
),
|
|
2048
|
+
require_precision=args.require_precision,
|
|
2049
|
+
require_coverage=args.require_coverage,
|
|
2050
|
+
require_runtime_trace=args.require_runtime_trace,
|
|
2051
|
+
require_python_full=args.python_full,
|
|
2052
|
+
)
|
|
2053
|
+
|
|
2054
|
+
|
|
2055
|
+
_SHAPING_OPTIONS = (
|
|
2056
|
+
("max_results", "--max-results"),
|
|
2057
|
+
("detail_level", "--detail-level"),
|
|
2058
|
+
("include_source", "--include-source"),
|
|
2059
|
+
)
|
|
2060
|
+
|
|
2061
|
+
|
|
2062
|
+
def _agent_payload_request(args: argparse.Namespace) -> ContextRequest:
|
|
2063
|
+
"""Build the bounded-payload request, applying only explicit options.
|
|
2064
|
+
|
|
2065
|
+
Options left at their ``None`` default fall through to the ``ContextRequest``
|
|
2066
|
+
defaults rather than pinning them here, so the schema stays the single
|
|
2067
|
+
source of truth for payload bounds.
|
|
2068
|
+
"""
|
|
2069
|
+
fields: dict[str, Any] = {"targets": [args.target], "profile": args.profile}
|
|
2070
|
+
for attr, _flag in _SHAPING_OPTIONS:
|
|
2071
|
+
value = getattr(args, attr)
|
|
2072
|
+
if value is not None:
|
|
2073
|
+
fields[attr] = value
|
|
2074
|
+
return ContextRequest(**fields)
|
|
2075
|
+
|
|
2076
|
+
|
|
2077
|
+
def _reject_raw_shaping(args: argparse.Namespace) -> None:
|
|
2078
|
+
"""Fail loudly when ``--raw`` is combined with options it cannot apply.
|
|
2079
|
+
|
|
2080
|
+
``--raw`` returns the QueryEngine payload unchanged, so it can honour none
|
|
2081
|
+
of the shaping options. Accepting them and dropping them silently is what
|
|
2082
|
+
let ``callers TARGET --max-results 5`` return all 132 callers.
|
|
2083
|
+
"""
|
|
2084
|
+
supplied = [
|
|
2085
|
+
flag for attr, flag in _SHAPING_OPTIONS if getattr(args, attr) is not None
|
|
2086
|
+
]
|
|
2087
|
+
if supplied:
|
|
2088
|
+
# RuntimeError, not ValueError: main() re-raises ValueError for
|
|
2089
|
+
# non-change commands, which would surface this as a traceback.
|
|
2090
|
+
raise RuntimeError(
|
|
2091
|
+
f"--raw returns the unbounded debug payload and cannot apply "
|
|
2092
|
+
f"{', '.join(supplied)}. Drop --raw to use them."
|
|
2093
|
+
)
|
|
2094
|
+
|
|
2095
|
+
|
|
2096
|
+
def handle_callers(args: argparse.Namespace) -> dict[str, Any]:
|
|
2097
|
+
if args.raw:
|
|
2098
|
+
_reject_raw_shaping(args)
|
|
2099
|
+
return query_engine(args).callers(args.target, profile=args.profile)
|
|
2100
|
+
request = _agent_payload_request(args)
|
|
2101
|
+
return ContextProvider(query_engine(args)).compact_callers(args.target, request)
|
|
2102
|
+
|
|
2103
|
+
|
|
2104
|
+
def handle_callees(args: argparse.Namespace) -> dict[str, Any]:
|
|
2105
|
+
if args.raw:
|
|
2106
|
+
_reject_raw_shaping(args)
|
|
2107
|
+
return query_engine(args).callees(args.target, profile=args.profile)
|
|
2108
|
+
request = _agent_payload_request(args)
|
|
2109
|
+
return ContextProvider(query_engine(args)).compact_callees(args.target, request)
|
|
2110
|
+
|
|
2111
|
+
|
|
2112
|
+
def handle_impact(args: argparse.Namespace) -> dict[str, Any]:
|
|
2113
|
+
if args.raw:
|
|
2114
|
+
_reject_raw_shaping(args)
|
|
2115
|
+
return query_engine(args).impact(
|
|
2116
|
+
args.target,
|
|
2117
|
+
max_depth=args.max_depth,
|
|
2118
|
+
profile=args.profile,
|
|
2119
|
+
include_edge_kinds=args.include_edge_kind,
|
|
2120
|
+
exclude_edge_kinds=args.exclude_edge_kind,
|
|
2121
|
+
)
|
|
2122
|
+
request = _agent_payload_request(args)
|
|
2123
|
+
return ContextProvider(query_engine(args)).compact_impact(
|
|
2124
|
+
args.target,
|
|
2125
|
+
request,
|
|
2126
|
+
max_depth=args.max_depth,
|
|
2127
|
+
include_edge_kinds=args.include_edge_kind,
|
|
2128
|
+
exclude_edge_kinds=args.exclude_edge_kind,
|
|
2129
|
+
)
|
|
2130
|
+
|
|
2131
|
+
|
|
2132
|
+
def handle_report_pr(args: argparse.Namespace) -> dict[str, Any] | str:
|
|
2133
|
+
repo_root = Path(args.repo_root).resolve()
|
|
2134
|
+
changed_files = args.changed_file or _infer_changed_files(repo_root)
|
|
2135
|
+
_import_runtime_trace_for_ci(args)
|
|
2136
|
+
ci_result = run_ci_checks(
|
|
2137
|
+
query_engine(args),
|
|
2138
|
+
targets=args.target,
|
|
2139
|
+
changed_files=changed_files,
|
|
2140
|
+
max_results=args.max_results,
|
|
2141
|
+
semantic_baseline=_load_json_arg(args.semantic_baseline),
|
|
2142
|
+
performance_budgets_ms=_load_performance_budgets_arg(
|
|
2143
|
+
args.performance_budget_ms
|
|
2144
|
+
),
|
|
2145
|
+
)
|
|
2146
|
+
markdown = render_pr_markdown(ci_result)
|
|
2147
|
+
if not args.output:
|
|
2148
|
+
return markdown
|
|
2149
|
+
output_path = Path(args.output)
|
|
2150
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2151
|
+
output_path.write_text(markdown, encoding="utf-8")
|
|
2152
|
+
return {
|
|
2153
|
+
"schema_version": ci_result["schema_version"],
|
|
2154
|
+
"index_version": ci_result.get("index_version"),
|
|
2155
|
+
"status": ci_result.get("status", "unknown"),
|
|
2156
|
+
"path": str(output_path.resolve()),
|
|
2157
|
+
"changed_files": changed_files,
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
|
|
2161
|
+
def handle_report_html(args: argparse.Namespace) -> dict[str, Any] | str:
|
|
2162
|
+
engine = query_engine(args)
|
|
2163
|
+
query_target = _normalized_report_target(args.target)
|
|
2164
|
+
impact = engine.impact(
|
|
2165
|
+
query_target,
|
|
2166
|
+
profile=args.profile,
|
|
2167
|
+
include_edge_kinds=args.include_edge_kind,
|
|
2168
|
+
exclude_edge_kinds=args.exclude_edge_kind,
|
|
2169
|
+
)
|
|
2170
|
+
similar = engine.similar(query_target, max_results=args.max_results)
|
|
2171
|
+
architecture = engine.architecture()
|
|
2172
|
+
entrypoint_flow = (
|
|
2173
|
+
engine.entrypoint_flow(args.target, max_results=args.max_results)
|
|
2174
|
+
if _is_entrypoint_target(args.target)
|
|
2175
|
+
else None
|
|
2176
|
+
)
|
|
2177
|
+
ci_result = run_ci_checks(
|
|
2178
|
+
engine,
|
|
2179
|
+
targets=[query_target],
|
|
2180
|
+
max_results=args.max_results,
|
|
2181
|
+
)
|
|
2182
|
+
html = render_static_html_report(
|
|
2183
|
+
target=args.target,
|
|
2184
|
+
impact=impact,
|
|
2185
|
+
similar=similar,
|
|
2186
|
+
architecture=architecture,
|
|
2187
|
+
entrypoint_flow=entrypoint_flow,
|
|
2188
|
+
ci_result=ci_result,
|
|
2189
|
+
)
|
|
2190
|
+
if not args.output:
|
|
2191
|
+
return html
|
|
2192
|
+
output_path = Path(args.output)
|
|
2193
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2194
|
+
output_path.write_text(html, encoding="utf-8")
|
|
2195
|
+
return {
|
|
2196
|
+
"schema_version": impact["schema_version"],
|
|
2197
|
+
"index_version": impact.get("index_version"),
|
|
2198
|
+
"status": "available",
|
|
2199
|
+
"path": str(output_path.resolve()),
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
|
|
2203
|
+
def handle_report_metrics_html(args: argparse.Namespace) -> dict[str, Any] | str:
|
|
2204
|
+
metrics_path = Path(args.path)
|
|
2205
|
+
try:
|
|
2206
|
+
metrics = summarize_metrics(metrics_path, limit=args.limit)
|
|
2207
|
+
except MetricsLogEncodingError:
|
|
2208
|
+
return _metrics_report_error_payload(metrics_path, invalid_encoding=True)
|
|
2209
|
+
except PrivateLocalStateError:
|
|
2210
|
+
return _metrics_report_error_payload(metrics_path)
|
|
2211
|
+
html = render_metrics_dashboard_html(metrics)
|
|
2212
|
+
if not args.output:
|
|
2213
|
+
return html
|
|
2214
|
+
output_path = Path(args.output)
|
|
2215
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2216
|
+
output_path.write_text(html, encoding="utf-8")
|
|
2217
|
+
return {
|
|
2218
|
+
"status": metrics.get("status", "unknown"),
|
|
2219
|
+
"path": str(output_path.resolve()),
|
|
2220
|
+
"event_count": metrics.get("event_count", 0),
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
|
|
2224
|
+
def handle_report_unresolved(args: argparse.Namespace) -> dict[str, Any] | str:
|
|
2225
|
+
payload = query_engine(args).unresolved(args.target, limit=args.limit)
|
|
2226
|
+
classification = classify_unresolved_records(payload)
|
|
2227
|
+
markdown = render_unresolved_classification_markdown(payload)
|
|
2228
|
+
if not args.output:
|
|
2229
|
+
return markdown
|
|
2230
|
+
output_path = Path(args.output)
|
|
2231
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2232
|
+
output_path.write_text(markdown, encoding="utf-8")
|
|
2233
|
+
json_output = Path(args.json_output) if args.json_output else None
|
|
2234
|
+
if json_output is not None:
|
|
2235
|
+
json_output.parent.mkdir(parents=True, exist_ok=True)
|
|
2236
|
+
json_output.write_text(
|
|
2237
|
+
json.dumps(classification, indent=2, sort_keys=True, ensure_ascii=False),
|
|
2238
|
+
encoding="utf-8",
|
|
2239
|
+
)
|
|
2240
|
+
return {
|
|
2241
|
+
"schema_version": payload["schema_version"],
|
|
2242
|
+
"index_version": payload.get("index_version"),
|
|
2243
|
+
"status": payload.get("status", "unknown"),
|
|
2244
|
+
"path": str(output_path.resolve()),
|
|
2245
|
+
"json_path": str(json_output.resolve()) if json_output else None,
|
|
2246
|
+
"total": payload.get("summary", {}).get("total", 0),
|
|
2247
|
+
"classified": payload.get("summary", {}).get("returned", 0),
|
|
2248
|
+
"category_counts": classification["category_counts"],
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
|
|
2252
|
+
def handle_context(args: argparse.Namespace) -> dict[str, Any]:
|
|
2253
|
+
request = ContextRequest(
|
|
2254
|
+
task=args.task,
|
|
2255
|
+
targets=args.target,
|
|
2256
|
+
max_results=args.max_results,
|
|
2257
|
+
detail_level=args.detail_level,
|
|
2258
|
+
profile=args.profile,
|
|
2259
|
+
include_source=args.include_source,
|
|
2260
|
+
)
|
|
2261
|
+
return ContextProvider(query_engine(args)).get_context(request)
|
|
2262
|
+
|
|
2263
|
+
|
|
2264
|
+
def handle_explain(args: argparse.Namespace) -> dict[str, Any]:
|
|
2265
|
+
request = ContextRequest(
|
|
2266
|
+
task=args.task,
|
|
2267
|
+
targets=args.target,
|
|
2268
|
+
max_results=args.max_results,
|
|
2269
|
+
detail_level=args.detail_level,
|
|
2270
|
+
profile=args.profile,
|
|
2271
|
+
include_source=args.include_source,
|
|
2272
|
+
)
|
|
2273
|
+
return ContextProvider(query_engine(args)).explain(request)
|
|
2274
|
+
|
|
2275
|
+
|
|
2276
|
+
def handle_ops_rebuild(args: argparse.Namespace) -> dict[str, Any]:
|
|
2277
|
+
repo_root = Path(args.repo_root).resolve()
|
|
2278
|
+
output_dir = (repo_root / args.output_dir).resolve()
|
|
2279
|
+
resolved = _resolve_source_roots(args, repo_root)
|
|
2280
|
+
return rebuild_index(
|
|
2281
|
+
repo_root=repo_root,
|
|
2282
|
+
output_dir=output_dir,
|
|
2283
|
+
source_roots=list(resolved.roots),
|
|
2284
|
+
if_stale=args.if_stale,
|
|
2285
|
+
scip_index_path=args.scip_index,
|
|
2286
|
+
pyright_export_path=args.pyright_export,
|
|
2287
|
+
runtime_trace_path=args.runtime_trace,
|
|
2288
|
+
coverage_path=args.coverage,
|
|
2289
|
+
enable_v2_call_resolution=not args.legacy_call_resolution,
|
|
2290
|
+
source_root_detection=resolved.detection,
|
|
2291
|
+
)
|
|
2292
|
+
|
|
2293
|
+
|
|
2294
|
+
def handle_ops_stale_alert(args: argparse.Namespace) -> dict[str, Any]:
|
|
2295
|
+
return stale_alert(query_engine(args), fail_on_stale=args.fail_on_stale)
|
|
2296
|
+
|
|
2297
|
+
|
|
2298
|
+
def handle_ops_prune(args: argparse.Namespace) -> dict[str, Any]:
|
|
2299
|
+
repo_root = Path(args.repo_root).resolve()
|
|
2300
|
+
output_dir = (repo_root / args.output_dir).resolve()
|
|
2301
|
+
return prune_output(
|
|
2302
|
+
output_dir=output_dir,
|
|
2303
|
+
keep_builds=args.keep_builds,
|
|
2304
|
+
include_input_artifacts=args.include_input_artifacts,
|
|
2305
|
+
apply=args.apply,
|
|
2306
|
+
)
|
|
2307
|
+
|
|
2308
|
+
|
|
2309
|
+
def handle_trace_import(args: argparse.Namespace) -> dict[str, Any]:
|
|
2310
|
+
repo_root = Path(args.repo_root).resolve()
|
|
2311
|
+
output_dir = (repo_root / args.output_dir).resolve()
|
|
2312
|
+
return import_runtime_trace(
|
|
2313
|
+
repo_root=repo_root,
|
|
2314
|
+
output_dir=output_dir,
|
|
2315
|
+
trace_path=args.path,
|
|
2316
|
+
max_events=args.max_events,
|
|
2317
|
+
max_file_bytes=args.max_file_bytes,
|
|
2318
|
+
max_seconds=args.max_seconds,
|
|
2319
|
+
)
|
|
2320
|
+
|
|
2321
|
+
|
|
2322
|
+
def handle_trace_import_otel(args: argparse.Namespace) -> dict[str, Any]:
|
|
2323
|
+
repo_root = Path(args.repo_root).resolve()
|
|
2324
|
+
output_dir = (repo_root / args.output_dir).resolve()
|
|
2325
|
+
return import_otel_spans(
|
|
2326
|
+
repo_root=repo_root,
|
|
2327
|
+
output_dir=output_dir,
|
|
2328
|
+
span_path=args.path,
|
|
2329
|
+
max_spans=args.max_spans,
|
|
2330
|
+
max_file_bytes=args.max_file_bytes,
|
|
2331
|
+
max_seconds=args.max_seconds,
|
|
2332
|
+
)
|
|
2333
|
+
|
|
2334
|
+
|
|
2335
|
+
def handle_trace_import_har(args: argparse.Namespace) -> dict[str, Any]:
|
|
2336
|
+
repo_root = Path(args.repo_root).resolve()
|
|
2337
|
+
output_dir = (repo_root / args.output_dir).resolve()
|
|
2338
|
+
return import_har_network(
|
|
2339
|
+
repo_root=repo_root,
|
|
2340
|
+
output_dir=output_dir,
|
|
2341
|
+
har_path=args.path,
|
|
2342
|
+
max_entries=args.max_entries,
|
|
2343
|
+
max_file_bytes=args.max_file_bytes,
|
|
2344
|
+
max_seconds=args.max_seconds,
|
|
2345
|
+
)
|
|
2346
|
+
|
|
2347
|
+
|
|
2348
|
+
def _add_ci_runtime_trace_args(parser: argparse.ArgumentParser) -> None:
|
|
2349
|
+
parser.add_argument(
|
|
2350
|
+
"--runtime-trace",
|
|
2351
|
+
help=(
|
|
2352
|
+
"Explicit runtime trace JSON to import before CI checks. "
|
|
2353
|
+
"CI never records or imports runtime evidence unless this is set."
|
|
2354
|
+
),
|
|
2355
|
+
)
|
|
2356
|
+
parser.add_argument(
|
|
2357
|
+
"--runtime-trace-max-events",
|
|
2358
|
+
type=int,
|
|
2359
|
+
default=50_000,
|
|
2360
|
+
help="Maximum runtime trace events to import for CI.",
|
|
2361
|
+
)
|
|
2362
|
+
parser.add_argument(
|
|
2363
|
+
"--runtime-trace-max-file-bytes",
|
|
2364
|
+
type=int,
|
|
2365
|
+
default=10 * 1024 * 1024,
|
|
2366
|
+
help="Maximum runtime trace JSON file size to import for CI.",
|
|
2367
|
+
)
|
|
2368
|
+
parser.add_argument(
|
|
2369
|
+
"--runtime-trace-max-seconds",
|
|
2370
|
+
type=float,
|
|
2371
|
+
default=120.0,
|
|
2372
|
+
help="Maximum seconds to spend importing runtime trace events for CI.",
|
|
2373
|
+
)
|
|
2374
|
+
|
|
2375
|
+
|
|
2376
|
+
def _import_runtime_trace_for_ci(args: argparse.Namespace) -> None:
|
|
2377
|
+
trace_path = getattr(args, "runtime_trace", None)
|
|
2378
|
+
if not trace_path:
|
|
2379
|
+
return
|
|
2380
|
+
repo_root = Path(args.repo_root).resolve()
|
|
2381
|
+
output_dir = (repo_root / args.output_dir).resolve()
|
|
2382
|
+
import_runtime_trace(
|
|
2383
|
+
repo_root=repo_root,
|
|
2384
|
+
output_dir=output_dir,
|
|
2385
|
+
trace_path=trace_path,
|
|
2386
|
+
max_events=args.runtime_trace_max_events,
|
|
2387
|
+
max_file_bytes=args.runtime_trace_max_file_bytes,
|
|
2388
|
+
max_seconds=args.runtime_trace_max_seconds,
|
|
2389
|
+
)
|
|
2390
|
+
|
|
2391
|
+
|
|
2392
|
+
def handle_trace_run(args: argparse.Namespace) -> dict[str, Any]:
|
|
2393
|
+
repo_root = Path(args.repo_root).resolve()
|
|
2394
|
+
resolved = _resolve_source_roots(args, repo_root)
|
|
2395
|
+
pytest_args = list(args.pytest_args or [])
|
|
2396
|
+
if pytest_args and pytest_args[0] == "--":
|
|
2397
|
+
pytest_args = pytest_args[1:]
|
|
2398
|
+
return run_runtime_trace(
|
|
2399
|
+
repo_root=repo_root,
|
|
2400
|
+
output_path=args.output,
|
|
2401
|
+
pytest_args=pytest_args,
|
|
2402
|
+
source_roots=list(resolved.roots),
|
|
2403
|
+
max_events=args.max_events,
|
|
2404
|
+
max_seconds=args.max_seconds,
|
|
2405
|
+
)
|
|
2406
|
+
|
|
2407
|
+
|
|
2408
|
+
def handle_precision_scip_json(args: argparse.Namespace) -> dict[str, Any]:
|
|
2409
|
+
repo_root = Path(args.repo_root).resolve()
|
|
2410
|
+
resolved = _resolve_source_roots(args, repo_root)
|
|
2411
|
+
return convert_scip_index_to_json(
|
|
2412
|
+
repo_root=repo_root,
|
|
2413
|
+
scip_index_path=args.input,
|
|
2414
|
+
output_path=args.output,
|
|
2415
|
+
scip_command=args.scip_command,
|
|
2416
|
+
source_roots=list(resolved.roots),
|
|
2417
|
+
commit_sha=current_commit(repo_root),
|
|
2418
|
+
)
|
|
2419
|
+
|
|
2420
|
+
|
|
2421
|
+
def handle_precision_scip_python(args: argparse.Namespace) -> dict[str, Any]:
|
|
2422
|
+
repo_root = Path(args.repo_root).resolve()
|
|
2423
|
+
resolved = _resolve_source_roots(args, repo_root)
|
|
2424
|
+
return generate_scip_python_json(
|
|
2425
|
+
repo_root=repo_root,
|
|
2426
|
+
output_path=args.output,
|
|
2427
|
+
index_file=args.index_file,
|
|
2428
|
+
project_name=args.project_name,
|
|
2429
|
+
project_version=args.project_version,
|
|
2430
|
+
target_only=args.target_only,
|
|
2431
|
+
environment_path=args.environment,
|
|
2432
|
+
scip_python_command=args.scip_python_command,
|
|
2433
|
+
scip_command=args.scip_command,
|
|
2434
|
+
timeout_seconds=args.timeout_seconds,
|
|
2435
|
+
source_roots=list(resolved.roots),
|
|
2436
|
+
commit_sha=current_commit(repo_root),
|
|
2437
|
+
)
|
|
2438
|
+
|
|
2439
|
+
|
|
2440
|
+
def handle_precision_pyright(args: argparse.Namespace) -> dict[str, Any]:
|
|
2441
|
+
repo_root = Path(args.repo_root).resolve()
|
|
2442
|
+
resolved = _resolve_source_roots(args, repo_root)
|
|
2443
|
+
return generate_pyright_json(
|
|
2444
|
+
repo_root=repo_root,
|
|
2445
|
+
output_path=args.output,
|
|
2446
|
+
target_only=args.target_only,
|
|
2447
|
+
source_roots=list(resolved.roots),
|
|
2448
|
+
python_version=args.python_version,
|
|
2449
|
+
extra_path=args.extra_path,
|
|
2450
|
+
pyright_command=args.pyright_command,
|
|
2451
|
+
timeout_seconds=args.timeout_seconds,
|
|
2452
|
+
commit_sha=current_commit(repo_root),
|
|
2453
|
+
)
|
|
2454
|
+
|
|
2455
|
+
|
|
2456
|
+
def handle_evidence_status(args: argparse.Namespace) -> dict[str, Any]:
|
|
2457
|
+
repo_root = Path(args.repo_root).resolve()
|
|
2458
|
+
output_dir = (repo_root / args.output_dir).resolve()
|
|
2459
|
+
current = _current_index_or_empty(args)
|
|
2460
|
+
return evidence_status(
|
|
2461
|
+
repo_root=repo_root,
|
|
2462
|
+
output_dir=output_dir,
|
|
2463
|
+
commit_sha=current_commit(repo_root),
|
|
2464
|
+
source_roots=_detect_source_root_paths(repo_root),
|
|
2465
|
+
snapshot_manifest=(
|
|
2466
|
+
current.get("evidence_manifest") if isinstance(current, dict) else None
|
|
2467
|
+
),
|
|
2468
|
+
capabilities=(
|
|
2469
|
+
current.get("capabilities", {}) if isinstance(current, dict) else {}
|
|
2470
|
+
),
|
|
2471
|
+
)
|
|
2472
|
+
|
|
2473
|
+
|
|
2474
|
+
def handle_evidence_plan(args: argparse.Namespace) -> dict[str, Any]:
|
|
2475
|
+
repo_root = Path(args.repo_root).resolve()
|
|
2476
|
+
output_dir = (repo_root / args.output_dir).resolve()
|
|
2477
|
+
current = _current_index_or_empty(args)
|
|
2478
|
+
requirements = (
|
|
2479
|
+
evidence_requirement_summary(current, require_python_full=True)["requirements"]
|
|
2480
|
+
if args.profile == "python_full" and current
|
|
2481
|
+
else None
|
|
2482
|
+
)
|
|
2483
|
+
return evidence_plan(
|
|
2484
|
+
repo_root=repo_root,
|
|
2485
|
+
output_dir=output_dir,
|
|
2486
|
+
commit_sha=current_commit(repo_root),
|
|
2487
|
+
source_roots=_detect_source_root_paths(repo_root),
|
|
2488
|
+
profile=args.profile,
|
|
2489
|
+
project_name=_project_name_for_evidence(repo_root, current),
|
|
2490
|
+
python_full_requirements=requirements,
|
|
2491
|
+
)
|
|
2492
|
+
|
|
2493
|
+
|
|
2494
|
+
def handle_evidence_stamp(args: argparse.Namespace) -> dict[str, Any]:
|
|
2495
|
+
repo_root = Path(args.repo_root).resolve()
|
|
2496
|
+
return write_evidence_sidecar(
|
|
2497
|
+
repo_root=repo_root,
|
|
2498
|
+
artifact_path=args.path,
|
|
2499
|
+
kind=args.kind,
|
|
2500
|
+
commit_sha=current_commit(repo_root),
|
|
2501
|
+
source_roots=_detect_source_root_paths(repo_root),
|
|
2502
|
+
tool_name=args.tool_name,
|
|
2503
|
+
tool_version=args.tool_version,
|
|
2504
|
+
)
|
|
2505
|
+
|
|
2506
|
+
|
|
2507
|
+
def handle_metrics_summary(args: argparse.Namespace) -> dict[str, Any]:
|
|
2508
|
+
path = Path(args.path)
|
|
2509
|
+
try:
|
|
2510
|
+
if args.trial_summary:
|
|
2511
|
+
return summarize_trial_metrics(path, limit=args.limit)
|
|
2512
|
+
return summarize_metrics(path, limit=args.limit)
|
|
2513
|
+
except MetricsLogEncodingError:
|
|
2514
|
+
return _metrics_read_error_payload(
|
|
2515
|
+
path,
|
|
2516
|
+
trial_summary=args.trial_summary,
|
|
2517
|
+
invalid_encoding=True,
|
|
2518
|
+
)
|
|
2519
|
+
except PrivateLocalStateError:
|
|
2520
|
+
return _metrics_read_error_payload(
|
|
2521
|
+
path,
|
|
2522
|
+
trial_summary=args.trial_summary,
|
|
2523
|
+
)
|
|
2524
|
+
|
|
2525
|
+
|
|
2526
|
+
def _metrics_read_error_warning(*, invalid_encoding: bool) -> str:
|
|
2527
|
+
if invalid_encoding:
|
|
2528
|
+
return "Metrics log is not valid UTF-8. Start a new private metrics log."
|
|
2529
|
+
return "Metrics log could not be read safely. Use a new canonical private path."
|
|
2530
|
+
|
|
2531
|
+
|
|
2532
|
+
def _metrics_read_error_payload(
|
|
2533
|
+
path: Path,
|
|
2534
|
+
*,
|
|
2535
|
+
trial_summary: bool,
|
|
2536
|
+
invalid_encoding: bool = False,
|
|
2537
|
+
) -> dict[str, Any]:
|
|
2538
|
+
payload = metrics_unavailable_payload(
|
|
2539
|
+
path,
|
|
2540
|
+
trial_summary=trial_summary,
|
|
2541
|
+
warning=_metrics_read_error_warning(invalid_encoding=invalid_encoding),
|
|
2542
|
+
)
|
|
2543
|
+
payload["_exit_code"] = 2
|
|
2544
|
+
return payload
|
|
2545
|
+
|
|
2546
|
+
|
|
2547
|
+
def _metrics_report_error_payload(
|
|
2548
|
+
metrics_path: Path,
|
|
2549
|
+
*,
|
|
2550
|
+
invalid_encoding: bool = False,
|
|
2551
|
+
) -> dict[str, Any]:
|
|
2552
|
+
"""Report a failed metrics read without borrowing the summary result shape.
|
|
2553
|
+
|
|
2554
|
+
``report metrics-html`` uses ``path`` for the report it wrote, and its
|
|
2555
|
+
success result can itself carry ``status: "unavailable"`` for an empty log.
|
|
2556
|
+
Reusing the summary payload here would make an unwritten report
|
|
2557
|
+
indistinguishable from a written one on every field a caller inspects.
|
|
2558
|
+
"""
|
|
2559
|
+
|
|
2560
|
+
return {
|
|
2561
|
+
"status": "error",
|
|
2562
|
+
"error_code": (
|
|
2563
|
+
METRICS_LOG_INVALID_ENCODING_ERROR_CODE
|
|
2564
|
+
if invalid_encoding
|
|
2565
|
+
else METRICS_LOG_UNREADABLE_ERROR_CODE
|
|
2566
|
+
),
|
|
2567
|
+
"metrics_path": str(metrics_path),
|
|
2568
|
+
"event_count": 0,
|
|
2569
|
+
"warnings": [_metrics_read_error_warning(invalid_encoding=invalid_encoding)],
|
|
2570
|
+
"_exit_code": 2,
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
|
|
2574
|
+
def _top_level_command(argv: list[str]) -> str | None:
|
|
2575
|
+
options_with_values = {"--repo-root", "--output-dir", "--metrics-log"}
|
|
2576
|
+
index = 0
|
|
2577
|
+
while index < len(argv):
|
|
2578
|
+
token = argv[index]
|
|
2579
|
+
if token in options_with_values:
|
|
2580
|
+
index += 2
|
|
2581
|
+
continue
|
|
2582
|
+
if any(token.startswith(f"{option}=") for option in options_with_values):
|
|
2583
|
+
index += 1
|
|
2584
|
+
continue
|
|
2585
|
+
if token.startswith("-"):
|
|
2586
|
+
index += 1
|
|
2587
|
+
continue
|
|
2588
|
+
return token
|
|
2589
|
+
return None
|
|
2590
|
+
|
|
2591
|
+
|
|
2592
|
+
def handle_mcp_serve(args: argparse.Namespace) -> None:
|
|
2593
|
+
serve_mcp(args)
|
|
2594
|
+
return None
|
|
2595
|
+
|
|
2596
|
+
|
|
2597
|
+
def _detect_source_root_paths(repo_root: Path) -> list[str]:
|
|
2598
|
+
return [root.path for root in detect_source_roots(repo_root).roots]
|
|
2599
|
+
|
|
2600
|
+
|
|
2601
|
+
def _current_index_or_empty(args: argparse.Namespace) -> dict[str, Any]:
|
|
2602
|
+
try:
|
|
2603
|
+
return query_engine(args).current()
|
|
2604
|
+
except (FileNotFoundError, SchemaVersionError, RuntimeError, ValueError):
|
|
2605
|
+
return {}
|
|
2606
|
+
|
|
2607
|
+
|
|
2608
|
+
def _project_name_for_evidence(repo_root: Path, current: dict[str, Any] | None) -> str:
|
|
2609
|
+
if isinstance(current, dict):
|
|
2610
|
+
current_name = current.get("project_name")
|
|
2611
|
+
if isinstance(current_name, str) and current_name:
|
|
2612
|
+
return current_name
|
|
2613
|
+
pyproject_path = repo_root / "pyproject.toml"
|
|
2614
|
+
if pyproject_path.exists():
|
|
2615
|
+
try:
|
|
2616
|
+
import tomllib
|
|
2617
|
+
|
|
2618
|
+
data = tomllib.loads(pyproject_path.read_text(encoding="utf-8"))
|
|
2619
|
+
project = data.get("project")
|
|
2620
|
+
if isinstance(project, dict):
|
|
2621
|
+
name = project.get("name")
|
|
2622
|
+
if isinstance(name, str) and name:
|
|
2623
|
+
return name
|
|
2624
|
+
except Exception:
|
|
2625
|
+
pass
|
|
2626
|
+
return repo_root.name
|
|
2627
|
+
|
|
2628
|
+
|
|
2629
|
+
def _pyproject_has_arcgraph_config(text: str) -> bool:
|
|
2630
|
+
try:
|
|
2631
|
+
import tomllib
|
|
2632
|
+
|
|
2633
|
+
data = tomllib.loads(text)
|
|
2634
|
+
except Exception as exc:
|
|
2635
|
+
raise RuntimeError(f"Unable to parse pyproject.toml: {exc}") from exc
|
|
2636
|
+
|
|
2637
|
+
tool = data["tool"] if "tool" in data else None
|
|
2638
|
+
if not isinstance(tool, dict):
|
|
2639
|
+
return False
|
|
2640
|
+
arcgraph = tool["arcgraph"] if "arcgraph" in tool else None
|
|
2641
|
+
if not isinstance(arcgraph, dict):
|
|
2642
|
+
return False
|
|
2643
|
+
source_roots = arcgraph["source_roots"] if "source_roots" in arcgraph else None
|
|
2644
|
+
if not isinstance(source_roots, list):
|
|
2645
|
+
return False
|
|
2646
|
+
|
|
2647
|
+
def path_value(entry: dict[str, Any]) -> Any:
|
|
2648
|
+
return entry["path"] if "path" in entry else None
|
|
2649
|
+
|
|
2650
|
+
return any(
|
|
2651
|
+
(isinstance(entry, str) and bool(entry))
|
|
2652
|
+
or (
|
|
2653
|
+
isinstance(entry, dict)
|
|
2654
|
+
and isinstance(path_value(entry), str)
|
|
2655
|
+
and bool(path_value(entry))
|
|
2656
|
+
)
|
|
2657
|
+
for entry in source_roots
|
|
2658
|
+
)
|
|
2659
|
+
|
|
2660
|
+
|
|
2661
|
+
def _arcgraph_config_snippet(roots: tuple[SourceRoot, ...]) -> str:
|
|
2662
|
+
lines = ["[tool.arcgraph]"]
|
|
2663
|
+
if all(not root.module_prefix for root in roots):
|
|
2664
|
+
values = ", ".join(_toml_string(root.path) for root in roots)
|
|
2665
|
+
if len(values) <= 72:
|
|
2666
|
+
lines.append(f"source_roots = [{values}]")
|
|
2667
|
+
return "\n".join(lines)
|
|
2668
|
+
|
|
2669
|
+
lines.append("source_roots = [")
|
|
2670
|
+
for root in roots:
|
|
2671
|
+
if root.module_prefix:
|
|
2672
|
+
lines.append(
|
|
2673
|
+
" { path = "
|
|
2674
|
+
f"{_toml_string(root.path)}, "
|
|
2675
|
+
f"module_prefix = {_toml_string(root.module_prefix)}"
|
|
2676
|
+
" },"
|
|
2677
|
+
)
|
|
2678
|
+
else:
|
|
2679
|
+
lines.append(f" {_toml_string(root.path)},")
|
|
2680
|
+
lines.append("]")
|
|
2681
|
+
return "\n".join(lines)
|
|
2682
|
+
|
|
2683
|
+
|
|
2684
|
+
def _toml_string(value: str) -> str:
|
|
2685
|
+
return json.dumps(value, ensure_ascii=False)
|
|
2686
|
+
|
|
2687
|
+
|
|
2688
|
+
# The classifier lives beside parse_route_target so every surface shares one
|
|
2689
|
+
# definition. A real def rather than an alias assignment: ArcGraph's own
|
|
2690
|
+
# resolver cannot follow calls through an alias binding, and the unresolved
|
|
2691
|
+
# callsite would trip the release gate.
|
|
2692
|
+
def _is_entrypoint_target(target: str) -> bool:
|
|
2693
|
+
return is_entrypoint_target(target)
|
|
2694
|
+
|
|
2695
|
+
|
|
2696
|
+
def _normalized_report_target(target: str) -> str:
|
|
2697
|
+
if target.startswith("mcp:"):
|
|
2698
|
+
return f"mcp_tool:{target.split(':', 1)[1]}"
|
|
2699
|
+
parsed = parse_route_target(target)
|
|
2700
|
+
if parsed is not None:
|
|
2701
|
+
method, path = parsed
|
|
2702
|
+
return route_id(method, path)
|
|
2703
|
+
return target
|
|
2704
|
+
|
|
2705
|
+
|
|
2706
|
+
def _infer_changed_files(repo_root: Path) -> list[str]:
|
|
2707
|
+
status_files = _git_status_files(repo_root)
|
|
2708
|
+
if status_files:
|
|
2709
|
+
return status_files
|
|
2710
|
+
|
|
2711
|
+
candidates: list[list[str]] = []
|
|
2712
|
+
base_ref = os.environ.get("GITHUB_BASE_REF")
|
|
2713
|
+
if base_ref:
|
|
2714
|
+
candidates.append(["diff", "--name-only", f"origin/{base_ref}...HEAD"])
|
|
2715
|
+
candidates.extend(
|
|
2716
|
+
[
|
|
2717
|
+
["diff", "--name-only", "HEAD~1", "HEAD"],
|
|
2718
|
+
["diff", "--name-only", "HEAD"],
|
|
2719
|
+
]
|
|
2720
|
+
)
|
|
2721
|
+
for args in candidates:
|
|
2722
|
+
files = _git_changed_files(repo_root, args)
|
|
2723
|
+
if files:
|
|
2724
|
+
return files
|
|
2725
|
+
return []
|
|
2726
|
+
|
|
2727
|
+
|
|
2728
|
+
def _load_json_arg(path: str | None) -> dict[str, Any] | None:
|
|
2729
|
+
if not path:
|
|
2730
|
+
return None
|
|
2731
|
+
return json.loads(Path(path).read_text(encoding="utf-8"))
|
|
2732
|
+
|
|
2733
|
+
|
|
2734
|
+
def _load_performance_budgets_arg(items: list[str]) -> dict[str, float] | None:
|
|
2735
|
+
if not items:
|
|
2736
|
+
return None
|
|
2737
|
+
budgets: dict[str, float] = {}
|
|
2738
|
+
for item in items:
|
|
2739
|
+
if "=" not in item:
|
|
2740
|
+
raise RuntimeError(
|
|
2741
|
+
"Performance budget must use OPERATION=MS, " f"got {item!r}."
|
|
2742
|
+
)
|
|
2743
|
+
operation, raw_value = item.split("=", 1)
|
|
2744
|
+
operation = operation.strip()
|
|
2745
|
+
if not operation:
|
|
2746
|
+
raise RuntimeError("Performance budget operation name must not be empty.")
|
|
2747
|
+
try:
|
|
2748
|
+
budgets[operation] = float(raw_value)
|
|
2749
|
+
except ValueError as exc:
|
|
2750
|
+
raise RuntimeError(
|
|
2751
|
+
f"Performance budget for {operation!r} must be a number."
|
|
2752
|
+
) from exc
|
|
2753
|
+
return budgets
|
|
2754
|
+
|
|
2755
|
+
|
|
2756
|
+
def _git_changed_files(repo_root: Path, args: list[str]) -> list[str]:
|
|
2757
|
+
completed = subprocess.run(
|
|
2758
|
+
["git", "-C", str(repo_root), *args],
|
|
2759
|
+
check=False,
|
|
2760
|
+
capture_output=True,
|
|
2761
|
+
encoding="utf-8",
|
|
2762
|
+
errors="replace",
|
|
2763
|
+
)
|
|
2764
|
+
if completed.returncode != 0:
|
|
2765
|
+
return []
|
|
2766
|
+
return _python_files(completed.stdout.splitlines())
|
|
2767
|
+
|
|
2768
|
+
|
|
2769
|
+
def _git_status_files(repo_root: Path) -> list[str]:
|
|
2770
|
+
completed = subprocess.run(
|
|
2771
|
+
["git", "-C", str(repo_root), "status", "--porcelain", "--untracked-files=all"],
|
|
2772
|
+
check=False,
|
|
2773
|
+
capture_output=True,
|
|
2774
|
+
encoding="utf-8",
|
|
2775
|
+
errors="replace",
|
|
2776
|
+
)
|
|
2777
|
+
if completed.returncode != 0:
|
|
2778
|
+
return []
|
|
2779
|
+
paths: list[str] = []
|
|
2780
|
+
for row in completed.stdout.splitlines():
|
|
2781
|
+
path = row[3:] if len(row) > 3 else ""
|
|
2782
|
+
if " -> " in path:
|
|
2783
|
+
path = path.rsplit(" -> ", 1)[-1]
|
|
2784
|
+
paths.append(path)
|
|
2785
|
+
return _python_files(paths)
|
|
2786
|
+
|
|
2787
|
+
|
|
2788
|
+
def _python_files(paths: list[str]) -> list[str]:
|
|
2789
|
+
return sorted(
|
|
2790
|
+
dict.fromkeys(
|
|
2791
|
+
path.replace("\\", "/")
|
|
2792
|
+
for path in paths
|
|
2793
|
+
if path.replace("\\", "/").endswith(".py")
|
|
2794
|
+
)
|
|
2795
|
+
)
|
|
2796
|
+
|
|
2797
|
+
|
|
2798
|
+
if __name__ == "__main__":
|
|
2799
|
+
raise SystemExit(main())
|