cseq 0.0.1__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.
cseq/cli.py ADDED
@@ -0,0 +1,895 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import shutil
7
+ import sys
8
+
9
+ from . import __version__
10
+ from .binary import load_binary_image
11
+ from .dwarf import load_dwarf_lines
12
+ from .runtime_address import discover_runtime_address_evidence
13
+ from .project import Project
14
+ from .config import CseqConfig, default_config_text, validate_config
15
+ from .sequence import build_sequence, render_plantuml
16
+ from .html import render_html
17
+ from .html_bundle import write_html_bundle, write_html_range_bundle, write_html_range_bundle_from_event_store
18
+ from .marker import apply_marker, apply_marker_overlay, audit_markers, undo_marker
19
+ from .scanner import scan_sources
20
+ from .runtime import RuntimePattern, build_trace_session, iter_runtime_files
21
+ from .runtime_cpu import apply_runtime_cpu_evidence
22
+ from .explain import explain_call
23
+ from .query import query_calls, query_unresolved, query_paths
24
+ from .event_store import EventStore
25
+ from .trace_diff import diff_sessions
26
+ from .trace_analysis import reconstruct_static_paths, score_marker_suggestions, ReconstructionState
27
+ from .server import make_server
28
+ from .parser_environment import probe_parser_environment
29
+ from .parser_migration import build_g2b_resume_report
30
+ from .docs import export_document, export_all
31
+
32
+
33
+ def _build_parser() -> argparse.ArgumentParser:
34
+ p = argparse.ArgumentParser(prog="cseq")
35
+ p.add_argument("--version", action="version", version=f"cseq {__version__}")
36
+ sub = p.add_subparsers(dest="command", required=True)
37
+
38
+ doctor = sub.add_parser("doctor", help="check local execution environment")
39
+ doctor.add_argument("--parser-wheel-dir", help="verify the locked Tree-sitter parser wheels without installing them")
40
+ doctor.set_defaults(handler=_doctor)
41
+
42
+ analyze = sub.add_parser("analyze", help="scan and parse a C source tree")
43
+ analyze.add_argument("path")
44
+ analyze.add_argument("--json", action="store_true", dest="as_json")
45
+ analyze.add_argument("--entry", default="main")
46
+ analyze.add_argument("--plantuml")
47
+ analyze.add_argument("--html")
48
+ analyze.add_argument("--html-mode", choices=["portable","bundle","range","auto"], default="auto")
49
+ analyze.add_argument("--bundle-threshold-events", type=int, default=10000)
50
+ analyze.add_argument("--range-threshold-events", type=int, default=100000)
51
+ analyze.add_argument("--viewport-size", type=int, default=200)
52
+ analyze.add_argument("--config")
53
+ analyze.add_argument("--compile-db")
54
+ analyze.add_argument("--linker-map")
55
+ analyze.add_argument("--binary")
56
+ analyze.add_argument("--dwarf")
57
+ analyze.add_argument("--configuration")
58
+ analyze.add_argument("--define", action="append", default=[])
59
+ analyze.add_argument("--runtime-log", action="append", default=[])
60
+ analyze.add_argument("--runtime-pattern")
61
+ analyze.add_argument("--runtime-time-format")
62
+ analyze.add_argument("--runtime-session", default="default")
63
+ analyze.add_argument("--compare-runtime-log", action="append", default=[])
64
+ analyze.add_argument("--compare-session", default="compare")
65
+ analyze.add_argument("--diagnose-start")
66
+ analyze.add_argument("--diagnose-end")
67
+ analyze.add_argument("--no-cache", action="store_true")
68
+ analyze.add_argument("--jobs", type=int, default=1, help="parallel clang translation units")
69
+ analyze.add_argument("--static-store", help="SQLite path for out-of-core function metadata, or 'auto' for .cseq/static/symbols.db")
70
+ analyze.add_argument("--summary-only", action="store_true", help="omit per-function rows from analyze output")
71
+ analyze.set_defaults(handler=_analyze)
72
+
73
+ html_cmd = sub.add_parser("html", help="generate HTML from an analyzed source tree")
74
+ html_cmd.add_argument("path")
75
+ html_cmd.add_argument("--entry", default="main")
76
+ html_cmd.add_argument("--output", required=True)
77
+ html_cmd.add_argument("--mode", choices=["portable","bundle","range","auto"], default="auto")
78
+ html_cmd.add_argument("--bundle-threshold-events", type=int, default=10000)
79
+ html_cmd.add_argument("--range-threshold-events", type=int, default=100000)
80
+ html_cmd.add_argument("--viewport-size", type=int, default=200)
81
+ html_cmd.add_argument("--config")
82
+ html_cmd.add_argument("--compile-db")
83
+ html_cmd.add_argument("--linker-map")
84
+ html_cmd.add_argument("--binary")
85
+ html_cmd.add_argument("--dwarf")
86
+ html_cmd.add_argument("--configuration")
87
+ html_cmd.add_argument("--define", action="append", default=[])
88
+ html_cmd.add_argument("--runtime-log", action="append", default=[])
89
+ html_cmd.add_argument("--runtime-pattern")
90
+ html_cmd.add_argument("--runtime-time-format")
91
+ html_cmd.add_argument("--runtime-session", default="default")
92
+ html_cmd.add_argument("--compare-runtime-log", action="append", default=[])
93
+ html_cmd.add_argument("--compare-session", default="compare")
94
+ html_cmd.add_argument("--diagnose-start")
95
+ html_cmd.add_argument("--diagnose-end")
96
+ html_cmd.add_argument("--jobs", type=int, default=1, help="parallel clang translation units")
97
+ html_cmd.add_argument("--static-store", help="SQLite path for out-of-core function metadata, or 'auto' for .cseq/static/symbols.db")
98
+ html_cmd.set_defaults(handler=_html_command)
99
+
100
+ puml_cmd = sub.add_parser("plantuml", help="generate PlantUML from an analyzed source tree")
101
+ puml_cmd.add_argument("path")
102
+ puml_cmd.add_argument("--entry", default="main")
103
+ puml_cmd.add_argument("--output", required=True)
104
+ puml_cmd.add_argument("--config")
105
+ puml_cmd.add_argument("--compile-db")
106
+ puml_cmd.add_argument("--linker-map")
107
+ puml_cmd.add_argument("--binary")
108
+ puml_cmd.add_argument("--dwarf")
109
+ puml_cmd.add_argument("--configuration")
110
+ puml_cmd.add_argument("--define", action="append", default=[])
111
+ puml_cmd.add_argument("--jobs", type=int, default=1, help="parallel clang translation units")
112
+ puml_cmd.add_argument("--static-store", help="SQLite path for out-of-core function metadata, or 'auto' for .cseq/static/symbols.db")
113
+ puml_cmd.set_defaults(handler=_plantuml_command)
114
+
115
+ cache_cmd = sub.add_parser("cache", help="inspect or clear parser cache")
116
+ cache_sub = cache_cmd.add_subparsers(dest="cache_command", required=True)
117
+ cache_info = cache_sub.add_parser("info")
118
+ cache_info.add_argument("path")
119
+ cache_info.set_defaults(handler=_cache_info)
120
+ cache_clear = cache_sub.add_parser("clear")
121
+ cache_clear.add_argument("path")
122
+ cache_clear.set_defaults(handler=_cache_clear)
123
+
124
+ config_cmd = sub.add_parser("config", help="initialize or validate cseq.toml")
125
+ config_sub = config_cmd.add_subparsers(dest="config_command", required=True)
126
+ config_init = config_sub.add_parser("init", help="create a conservative cseq.toml")
127
+ config_init.add_argument("path", nargs="?", default=".")
128
+ config_init.add_argument("--force", action="store_true")
129
+ config_init.set_defaults(handler=_config_init)
130
+ config_validate = config_sub.add_parser("validate", help="validate cseq.toml")
131
+ config_validate.add_argument("path", nargs="?", default="cseq.toml")
132
+ config_validate.set_defaults(handler=_config_validate)
133
+
134
+ marker = sub.add_parser("marker", help="manage runtime source markers")
135
+ marker_sub = marker.add_subparsers(dest="marker_command", required=True)
136
+
137
+ marker_apply = marker_sub.add_parser("apply", help="insert a marker into an existing string literal")
138
+ marker_apply.add_argument("path")
139
+ marker_apply.add_argument("--contains", required=True, dest="literal_contains")
140
+ marker_apply.add_argument("--occurrence", type=int, default=0)
141
+ marker_apply.add_argument("--id", dest="marker_id")
142
+ marker_apply.add_argument("--expected-hash")
143
+ marker_apply.add_argument("--project-root")
144
+ marker_apply.add_argument("--manifest", required=True)
145
+ marker_apply.add_argument(
146
+ "--overlay-output",
147
+ help="write the marker-injected source to this path and leave the original unchanged",
148
+ )
149
+ marker_apply.set_defaults(handler=_marker_apply)
150
+
151
+ marker_undo = marker_sub.add_parser("undo", help="undo a marker rewrite using its manifest")
152
+ marker_undo.add_argument("path")
153
+ marker_undo.add_argument("--manifest", required=True)
154
+ marker_undo.set_defaults(handler=_marker_undo)
155
+
156
+ marker_audit = marker_sub.add_parser("audit", help="audit marker duplicates and malformed IDs")
157
+ marker_audit.add_argument("path")
158
+ marker_audit.set_defaults(handler=_marker_audit)
159
+
160
+ trace = sub.add_parser("trace", help="import runtime marker logs")
161
+ trace_sub = trace.add_subparsers(dest="trace_command", required=True)
162
+ trace_import = trace_sub.add_parser("import", help="parse one or more runtime logs")
163
+ trace_import.add_argument("paths", nargs="+")
164
+ trace_import.add_argument("--session", default="default")
165
+ trace_import.add_argument("--pattern")
166
+ trace_import.add_argument("--time-format")
167
+ trace_import.add_argument("--binary")
168
+ trace_import.add_argument("--dwarf")
169
+ trace_import.add_argument("--load-bias", type=lambda x: int(x, 0), default=0)
170
+ trace_import.add_argument("--address-key", action="append", default=[])
171
+ trace_import.add_argument("--json", action="store_true", dest="as_json")
172
+ trace_import.add_argument("--store", help="persist session to SQLite Event Store")
173
+ trace_import.add_argument("--stream", action="store_true", help="stream logs directly into Event Store without materializing TraceSession")
174
+ trace_import.add_argument("--chunk-size", type=int, default=10000)
175
+ trace_import.add_argument("--batch-size", type=int, default=50000)
176
+ trace_import.add_argument("--commit-every", type=int, default=250000)
177
+ trace_import.set_defaults(handler=_trace_import)
178
+
179
+ diff = sub.add_parser("diff", help="compare two runtime sessions")
180
+ diff.add_argument("--left", action="append", required=True)
181
+ diff.add_argument("--right", action="append", required=True)
182
+ diff.add_argument("--left-session", default="left")
183
+ diff.add_argument("--right-session", default="right")
184
+ diff.add_argument("--pattern")
185
+ diff.add_argument("--time-format")
186
+ diff.add_argument("--json", action="store_true", dest="as_json")
187
+ diff.add_argument("--html")
188
+ diff.set_defaults(handler=_diff)
189
+
190
+ explain = sub.add_parser("explain", help="explain why a call target was resolved")
191
+ explain.add_argument("path")
192
+ explain.add_argument("--caller", required=True)
193
+ explain.add_argument("--callee")
194
+ explain.add_argument("--config")
195
+ explain.add_argument("--compile-db")
196
+ explain.add_argument("--linker-map")
197
+ explain.add_argument("--binary")
198
+ explain.add_argument("--dwarf")
199
+ explain.add_argument("--configuration")
200
+ explain.add_argument("--define", action="append", default=[])
201
+ explain.add_argument("--jobs", type=int, default=1, help="parallel clang translation units")
202
+ explain.add_argument("--static-store", help="SQLite path for out-of-core function metadata, or 'auto' for .cseq/static/symbols.db")
203
+ explain.set_defaults(handler=_explain)
204
+
205
+ query = sub.add_parser("query", help="query calls, unresolved sites, or static paths")
206
+ query.add_argument("path")
207
+ query.add_argument("kind", choices=["calls", "unresolved", "paths"])
208
+ query.add_argument("--function")
209
+ query.add_argument("--start")
210
+ query.add_argument("--end")
211
+ query.add_argument("--config")
212
+ query.add_argument("--compile-db")
213
+ query.add_argument("--linker-map")
214
+ query.add_argument("--binary")
215
+ query.add_argument("--dwarf")
216
+ query.add_argument("--configuration")
217
+ query.add_argument("--define", action="append", default=[])
218
+ query.add_argument("--jobs", type=int, default=1, help="parallel clang translation units")
219
+ query.add_argument("--static-store", help="SQLite path for out-of-core function metadata, or 'auto' for .cseq/static/symbols.db")
220
+ query.set_defaults(handler=_query)
221
+
222
+ docs_cmd = sub.add_parser("docs", help="show or export bundled README, design document, and verification report")
223
+ docs_sub = docs_cmd.add_subparsers(dest="docs_command", required=True)
224
+ for kind, help_text in (
225
+ ("readme", "show/export README"),
226
+ ("design", "show/export the cleaned design document"),
227
+ ("report", "show/export the HTML implementation and verification report"),
228
+ ):
229
+ d = docs_sub.add_parser(kind, help=help_text)
230
+ d.add_argument("--output", help="write to this path instead of stdout")
231
+ d.set_defaults(handler=_docs)
232
+ docs_all = docs_sub.add_parser("all", help="export all bundled documents")
233
+ docs_all.add_argument("--output-dir", default="cseq-docs", help="destination directory (default: cseq-docs)")
234
+ docs_all.set_defaults(handler=_docs)
235
+
236
+ serve = sub.add_parser("serve", help="serve generated HTML/bundle files over local HTTP")
237
+ serve.add_argument("path", nargs="?", default=".")
238
+ serve.add_argument("--host", default="127.0.0.1")
239
+ serve.add_argument("--port", type=int, default=8000)
240
+ serve.set_defaults(handler=_serve)
241
+ return p
242
+
243
+
244
+ def _docs(args: argparse.Namespace) -> int:
245
+ if args.docs_command == "all":
246
+ paths = export_all(args.output_dir)
247
+ for path in paths:
248
+ print(path)
249
+ return 0
250
+ path = export_document(args.docs_command, args.output)
251
+ if path is not None:
252
+ print(path)
253
+ return 0
254
+
255
+
256
+ def _serve(args: argparse.Namespace) -> int:
257
+ from pathlib import Path
258
+ root = Path(args.path).resolve()
259
+ if not root.is_dir():
260
+ print(f"cseq: not a directory: {root}", file=sys.stderr)
261
+ return 3
262
+ try:
263
+ server = make_server(root, args.host, args.port)
264
+ except OSError as exc:
265
+ print(f"cseq: {exc}", file=sys.stderr)
266
+ return 3
267
+ host, port = server.server_address[:2]
268
+ print(f"cseq serving {root} at http://{host}:{port}/", flush=True)
269
+ try:
270
+ server.serve_forever()
271
+ except KeyboardInterrupt:
272
+ pass
273
+ finally:
274
+ server.server_close()
275
+ return 0
276
+
277
+
278
+ def _doctor(args: argparse.Namespace) -> int:
279
+ clang = shutil.which("clang")
280
+ parser_env = probe_parser_environment()
281
+ payload = {
282
+ "cseq_version": __version__,
283
+ "python": sys.version.split()[0],
284
+ "clang": clang,
285
+ "semantic_backend_ready": bool(clang),
286
+ "canonical_fast_frontend": "tree-sitter-c",
287
+ "parser_environment": parser_env.to_dict(),
288
+ "g2b_resume": build_g2b_resume_report(getattr(args, "parser_wheel_dir", None)).to_dict(),
289
+ "current_execution_mode": (
290
+ "tree-sitter+clang"
291
+ if parser_env.canonical_fast_frontend_ready and clang
292
+ else "lexical-fallback+clang" if clang else "unavailable"
293
+ ),
294
+ }
295
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
296
+ # `doctor` is a diagnostic/reporting command. Component readiness is
297
+ # represented explicitly in the JSON payload (semantic_backend_ready,
298
+ # canonical_fast_frontend_ready, g2b_resume.status, current_execution_mode)
299
+ # and must not be collapsed into an unrelated process exit code. A
300
+ # successfully produced diagnostic report therefore exits 0 even when an
301
+ # optional/currently-unavailable backend (for example clang) is absent.
302
+ return 0
303
+
304
+
305
+
306
+ def _advanced_html_inputs(index, args, runtime_session):
307
+ pattern = RuntimePattern(args.runtime_pattern, args.runtime_time_format) if getattr(args, "runtime_pattern", None) else None
308
+ trace_diff = None
309
+ compare_logs = getattr(args, "compare_runtime_log", []) or []
310
+ if runtime_session is not None and compare_logs:
311
+ other = build_trace_session(getattr(args, "compare_session", "compare"), compare_logs, pattern=pattern)
312
+ trace_diff = diff_sessions(runtime_session, other)
313
+ suggestions = []
314
+ start = getattr(args, "diagnose_start", None)
315
+ end = getattr(args, "diagnose_end", None)
316
+ if start and end:
317
+ reconstructed = reconstruct_static_paths(index, start, end)
318
+ if reconstructed.state == ReconstructionState.AMBIGUOUS_PATH:
319
+ suggestions = score_marker_suggestions(reconstructed.paths, count=5)
320
+ return trace_diff, suggestions
321
+
322
+ def _write_html_output(
323
+ output: str, model, *, runtime_session=None, cpu_evidence=(), explanations=None,
324
+ trace_diff=None, marker_suggestions=None, mode: str = "auto",
325
+ bundle_threshold_events: int = 10000, range_threshold_events: int = 100000, viewport_size: int = 200,
326
+ ) -> dict[str, object]:
327
+ from pathlib import Path
328
+ event_count = len(runtime_session.events) if runtime_session else 0
329
+ selected = mode
330
+ if selected == "auto":
331
+ selected = "range" if event_count > max(0, int(range_threshold_events)) else ("bundle" if event_count > max(0, int(bundle_threshold_events)) else "portable")
332
+ kwargs = dict(
333
+ runtime_session=runtime_session, cpu_evidence=cpu_evidence,
334
+ explanations=explanations, trace_diff=trace_diff,
335
+ marker_suggestions=marker_suggestions, viewport_size=viewport_size,
336
+ )
337
+ if selected == "bundle":
338
+ html_path, data_path = write_html_bundle(output, model, **kwargs)
339
+ return {"mode": "bundle", "html": str(html_path), "data": str(data_path), "events": event_count}
340
+ if selected == "range":
341
+ if runtime_session is None:
342
+ Path(output).write_text(render_html(model, **kwargs), encoding="utf-8")
343
+ return {"mode": "portable", "html": str(output), "data": None, "events": 0}
344
+ html_path, data_path, index_path = write_html_range_bundle(output, model, **kwargs)
345
+ return {"mode": "range", "html": str(html_path), "data": str(data_path), "index": str(index_path), "events": event_count}
346
+ Path(output).write_text(render_html(model, **kwargs), encoding="utf-8")
347
+ return {"mode": "portable", "html": str(output), "data": None, "events": event_count}
348
+
349
+
350
+ def _analyze(args: argparse.Namespace) -> int:
351
+ try:
352
+ project = Project(
353
+ args.path,
354
+ config=args.config,
355
+ configuration=args.configuration,
356
+ defines=tuple(args.define),
357
+ use_cache=not args.no_cache,
358
+ compile_db=args.compile_db,
359
+ linker_map=args.linker_map,
360
+ binary=args.binary,
361
+ dwarf=args.dwarf,
362
+ jobs=args.jobs,
363
+ static_store=args.static_store,
364
+ )
365
+ index = project.index()
366
+ except (OSError, RuntimeError) as exc:
367
+ print(f"cseq: {exc}", file=sys.stderr)
368
+ return 3
369
+
370
+ payload = {
371
+ "root": str(index.root),
372
+ "source_files": len(index.source_files),
373
+ "translation_units": len(index.parse_artifacts),
374
+ "cache": {
375
+ "parser": {"hits": project.cache_hits, "misses": project.cache_misses},
376
+ "analysis": {"hits": project.analysis_cache_hits, "misses": project.analysis_cache_misses},
377
+ "source_index": {
378
+ "hash_hits": project.source_index.hash_hits if project.source_index is not None else 0,
379
+ "hash_misses": project.source_index.hash_misses if project.source_index is not None else 0,
380
+ "dependency_hits": project.source_index.dependency_hits if project.source_index is not None else 0,
381
+ "dependency_misses": project.source_index.dependency_misses if project.source_index is not None else 0,
382
+ "path": str(project.source_index_path) if project.source_index is not None else None,
383
+ },
384
+ "enabled": not args.no_cache,
385
+ },
386
+ "parser_strategy": {
387
+ "fast_translation_units": project.fast_artifacts,
388
+ "semantic_translation_units": project.semantic_artifacts,
389
+ },
390
+ "static_store": str(project.static_symbol_store.path) if project.static_symbol_store is not None else None,
391
+ "static_store_stats": {
392
+ "hits": project.static_store_hits,
393
+ "misses": project.static_store_misses,
394
+ "removed_translation_units": project.static_store_removed_tus,
395
+ "translation_units": project.static_symbol_store.translation_unit_count() if project.static_symbol_store is not None else 0,
396
+ "artifact_sidecar_hits": project.static_artifact_hits,
397
+ "artifact_sidecar_misses": project.static_artifact_misses,
398
+ },
399
+ "function_count": index.function_count(),
400
+ "functions": [] if args.summary_only else [
401
+ {
402
+ "name": f.name,
403
+ "id": f.qualified_id,
404
+ "source": f.source_path,
405
+ "line": f.source_range.start_line,
406
+ "storage": f.storage_class,
407
+ }
408
+ for f in index.functions()
409
+ ],
410
+ "callsites": [
411
+ {
412
+ "caller": c.caller_name,
413
+ "callee": c.callee_name,
414
+ "raw": c.raw_text,
415
+ "target_kind": c.target_kind.value,
416
+ "target_function_ids": list(c.target_function_ids),
417
+ "unknown_possible": c.unknown_possible,
418
+ "callee_slot": c.callee_slot_key,
419
+ }
420
+ for c in index.callsites
421
+ ],
422
+ "cpu_evidence": [
423
+ {
424
+ "function_id": e.target_function_id,
425
+ "layer": e.layer.value,
426
+ "kind": e.value_kind.value,
427
+ "value": e.value,
428
+ "provenance": e.provenance,
429
+ "explicit_override": e.explicit_override,
430
+ }
431
+ for e in index.cpu_evidence
432
+ ],
433
+ "linker_evidence": [
434
+ {
435
+ "symbol": e.symbol_name, "address": e.address,
436
+ "object_file": e.object_file, "section": e.section,
437
+ "matched_function_ids": list(e.matched_function_ids),
438
+ "provenance": e.provenance,
439
+ }
440
+ for e in index.linker_evidence
441
+ ],
442
+ "binary_evidence": [
443
+ {
444
+ "symbol": e.symbol_name, "address": e.address, "size": e.size,
445
+ "kind": e.symbol_kind, "matched_function_ids": list(e.matched_function_ids),
446
+ "binary_hash": e.binary_hash, "provenance": e.provenance,
447
+ } for e in index.binary_evidence
448
+ ],
449
+ "dwarf_evidence": [
450
+ {
451
+ "function_id": e.function_id, "source": e.source_path,
452
+ "line": e.line, "address": e.address, "dwarf_file": e.dwarf_file,
453
+ "provenance": e.provenance,
454
+ } for e in index.dwarf_evidence
455
+ ],
456
+ "plugins": {
457
+ "enabled": list(project.config.plugins_enabled),
458
+ "loaded": [p.descriptor.name for p in project.plugins.plugins],
459
+ "evidence": [
460
+ {"plugin": e.plugin_name, "kind": e.kind.value, "target": e.target, "type": e.evidence_type, "value": e.value, "confidence": e.confidence}
461
+ for e in project.plugin_result.evidence
462
+ ],
463
+ "diagnostics": [
464
+ {"plugin": d.plugin_name, "severity": d.severity, "code": d.code, "message": d.message}
465
+ for d in project.plugin_result.diagnostics
466
+ ],
467
+ },
468
+ "artifacts": [
469
+ {
470
+ "source": a.translation_unit.source.project_relative_path,
471
+ "status": a.status.value,
472
+ "backend": a.parser_backend,
473
+ "diagnostics": [
474
+ {"severity": d.severity, "message": d.message} for d in a.diagnostics
475
+ ],
476
+ }
477
+ for a in index.parse_artifacts
478
+ ],
479
+ }
480
+ model = None
481
+ if args.plantuml or args.html:
482
+ model = build_sequence(index, args.entry)
483
+ if args.plantuml:
484
+ from pathlib import Path
485
+ Path(args.plantuml).write_text(render_plantuml(model), encoding="utf-8")
486
+ if args.html:
487
+ from pathlib import Path
488
+ runtime_session = None
489
+ if args.runtime_log:
490
+ runtime_pattern = RuntimePattern(args.runtime_pattern, args.runtime_time_format) if args.runtime_pattern else None
491
+ runtime_session = build_trace_session(args.runtime_session, args.runtime_log, pattern=runtime_pattern)
492
+ apply_runtime_cpu_evidence(index, runtime_session)
493
+ payload["cpu_evidence"] = [
494
+ {
495
+ "function_id": e.target_function_id,
496
+ "layer": e.layer.value,
497
+ "kind": e.value_kind.value,
498
+ "value": e.value,
499
+ "provenance": e.provenance,
500
+ "explicit_override": e.explicit_override,
501
+ }
502
+ for e in index.cpu_evidence
503
+ ]
504
+ explanation_map = {}
505
+ for caller in sorted({c.caller_name for c in index.callsites}):
506
+ explanation_map[caller] = explain_call(index, caller=caller).get("calls", [])
507
+ trace_diff, suggestions = _advanced_html_inputs(index, args, runtime_session)
508
+ html_result = _write_html_output(
509
+ args.html, model, runtime_session=runtime_session, cpu_evidence=index.cpu_evidence,
510
+ explanations=explanation_map, trace_diff=trace_diff, marker_suggestions=suggestions,
511
+ mode=args.html_mode, bundle_threshold_events=args.bundle_threshold_events,
512
+ range_threshold_events=args.range_threshold_events, viewport_size=args.viewport_size,
513
+ )
514
+ payload["html_output"] = html_result
515
+ if args.as_json:
516
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
517
+ else:
518
+ print(f"root: {payload['root']}")
519
+ print(f"source files: {payload['source_files']}")
520
+ print(f"translation units: {payload['translation_units']}")
521
+ print(f"functions: {len(payload['functions'])}")
522
+ for f in payload["functions"]:
523
+ print(f" {f['source']}:{f['line'] or '?'} {f['name']}")
524
+ for a in payload["artifacts"]:
525
+ if a["status"] != "OK":
526
+ print(f" [{a['status']}] {a['source']}")
527
+ return 0
528
+
529
+
530
+
531
+ def _project_for_output(args: argparse.Namespace):
532
+ project = Project(
533
+ args.path, config=args.config, configuration=args.configuration,
534
+ defines=tuple(args.define), compile_db=getattr(args, "compile_db", None),
535
+ linker_map=getattr(args, "linker_map", None),
536
+ binary=getattr(args, "binary", None),
537
+ dwarf=getattr(args, "dwarf", None),
538
+ jobs=getattr(args, "jobs", 1),
539
+ static_store=getattr(args, "static_store", None),
540
+ )
541
+ return project, project.index()
542
+
543
+
544
+ def _html_command(args: argparse.Namespace) -> int:
545
+ from pathlib import Path
546
+ try:
547
+ project, index = _project_for_output(args)
548
+ model = build_sequence(index, args.entry)
549
+ session = None
550
+ if args.runtime_log:
551
+ pattern = RuntimePattern(args.runtime_pattern, args.runtime_time_format) if args.runtime_pattern else None
552
+ session = build_trace_session(args.runtime_session, args.runtime_log, pattern=pattern)
553
+ apply_runtime_cpu_evidence(index, session)
554
+ explanation_map = {
555
+ caller: explain_call(index, caller=caller).get("calls", [])
556
+ for caller in sorted({c.caller_name for c in index.callsites})
557
+ }
558
+ trace_diff, suggestions = _advanced_html_inputs(index, args, session)
559
+ html_result = _write_html_output(
560
+ args.output, model, runtime_session=session, cpu_evidence=index.cpu_evidence,
561
+ explanations=explanation_map, trace_diff=trace_diff, marker_suggestions=suggestions,
562
+ mode=args.mode, bundle_threshold_events=args.bundle_threshold_events,
563
+ range_threshold_events=args.range_threshold_events, viewport_size=args.viewport_size,
564
+ )
565
+ except (OSError, RuntimeError, ValueError) as exc:
566
+ print(f"cseq: {exc}", file=sys.stderr); return 3
567
+ print(json.dumps({"output": args.output, "html": html_result, "cache_hits": project.cache_hits, "cache_misses": project.cache_misses, "analysis_cache_hits": project.analysis_cache_hits, "analysis_cache_misses": project.analysis_cache_misses, "fast_translation_units": project.fast_artifacts, "semantic_translation_units": project.semantic_artifacts, "source_index": {"hash_hits": project.source_index.hash_hits if project.source_index is not None else 0, "hash_misses": project.source_index.hash_misses if project.source_index is not None else 0, "dependency_hits": project.source_index.dependency_hits if project.source_index is not None else 0, "dependency_misses": project.source_index.dependency_misses if project.source_index is not None else 0}}, ensure_ascii=False))
568
+ return 0
569
+
570
+
571
+ def _plantuml_command(args: argparse.Namespace) -> int:
572
+ from pathlib import Path
573
+ try:
574
+ project, index = _project_for_output(args)
575
+ model = build_sequence(index, args.entry)
576
+ Path(args.output).write_text(render_plantuml(model), encoding="utf-8")
577
+ except (OSError, RuntimeError, ValueError) as exc:
578
+ print(f"cseq: {exc}", file=sys.stderr); return 3
579
+ print(json.dumps({"output": args.output, "cache_hits": project.cache_hits, "cache_misses": project.cache_misses, "analysis_cache_hits": project.analysis_cache_hits, "analysis_cache_misses": project.analysis_cache_misses, "fast_translation_units": project.fast_artifacts, "semantic_translation_units": project.semantic_artifacts, "source_index": {"hash_hits": project.source_index.hash_hits if project.source_index is not None else 0, "hash_misses": project.source_index.hash_misses if project.source_index is not None else 0, "dependency_hits": project.source_index.dependency_hits if project.source_index is not None else 0, "dependency_misses": project.source_index.dependency_misses if project.source_index is not None else 0}}, ensure_ascii=False))
580
+ return 0
581
+
582
+
583
+ def _cache_root(path: str) -> "Path":
584
+ from pathlib import Path
585
+ return Path(path).resolve() / ".cseq" / "cache"
586
+
587
+
588
+ def _cache_info(args: argparse.Namespace) -> int:
589
+ from pathlib import Path
590
+ root = _cache_root(args.path)
591
+ out = {}
592
+ for name in ("parser", "analysis", "static-artifact"):
593
+ part = root / name
594
+ files = list(part.rglob("*.json")) if part.exists() else []
595
+ out[name] = {"root": str(part), "entries": len(files), "size_bytes": sum(x.stat().st_size for x in files)}
596
+ source_db = Path(args.path).resolve() / ".cseq" / "source" / "index.db"
597
+ out["source-index"] = {
598
+ "root": str(source_db),
599
+ "entries": 1 if source_db.exists() else 0,
600
+ "size_bytes": source_db.stat().st_size if source_db.exists() else 0,
601
+ }
602
+ # Preserve the historical parser/analysis cache totals; Source Index is a
603
+ # separate persistent metadata accelerator and is reported explicitly.
604
+ out["entries"] = sum(out[name]["entries"] for name in ("parser", "analysis", "static-artifact"))
605
+ out["size_bytes"] = sum(out[name]["size_bytes"] for name in ("parser", "analysis", "static-artifact"))
606
+ print(json.dumps(out, ensure_ascii=False, indent=2))
607
+ return 0
608
+
609
+
610
+ def _cache_clear(args: argparse.Namespace) -> int:
611
+ import shutil as _shutil
612
+ from pathlib import Path
613
+ root = _cache_root(args.path)
614
+ entries = len(list(root.rglob("*.json"))) if root.exists() else 0
615
+ if root.exists():
616
+ _shutil.rmtree(root)
617
+ source_root = Path(args.path).resolve() / ".cseq" / "source"
618
+ source_removed = int((source_root / "index.db").exists())
619
+ if source_root.exists():
620
+ _shutil.rmtree(source_root)
621
+ print(json.dumps({"root": str(root), "removed_entries": entries, "source_index_removed": source_removed}, ensure_ascii=False))
622
+ return 0
623
+
624
+
625
+ def _config_init(args: argparse.Namespace) -> int:
626
+ from pathlib import Path
627
+ target = Path(args.path)
628
+ if target.is_dir() or target.suffix == "":
629
+ target = target / "cseq.toml"
630
+ target = target.resolve()
631
+ if target.exists() and not args.force:
632
+ print(f"cseq: config already exists: {target}", file=sys.stderr)
633
+ return 2
634
+ target.parent.mkdir(parents=True, exist_ok=True)
635
+ compile_present = (target.parent / "compile_commands.json").is_file()
636
+ target.write_text(default_config_text(compile_commands_present=compile_present), encoding="utf-8")
637
+ print(json.dumps({"path": str(target), "compile_commands_detected": compile_present}, ensure_ascii=False))
638
+ return 0
639
+
640
+
641
+ def _config_validate(args: argparse.Namespace) -> int:
642
+ try:
643
+ cfg = CseqConfig.load(args.path)
644
+ issues = validate_config(cfg)
645
+ except (OSError, ValueError) as exc:
646
+ print(json.dumps({"valid": False, "issues": [{"severity": "error", "code": "LOAD_ERROR", "message": str(exc)}]}, ensure_ascii=False, indent=2))
647
+ return 2
648
+ errors = [x for x in issues if x["severity"] == "error"]
649
+ print(json.dumps({"valid": not errors, "issues": issues}, ensure_ascii=False, indent=2))
650
+ return 0 if not errors else 2
651
+
652
+
653
+ def _marker_apply(args: argparse.Namespace) -> int:
654
+ try:
655
+ if args.overlay_output:
656
+ manifest = apply_marker_overlay(
657
+ args.path, args.overlay_output,
658
+ literal_contains=args.literal_contains,
659
+ occurrence=args.occurrence,
660
+ expected_hash=args.expected_hash,
661
+ project_root=args.project_root,
662
+ marker_id=args.marker_id,
663
+ manifest_path=args.manifest,
664
+ )
665
+ else:
666
+ manifest = apply_marker(
667
+ args.path,
668
+ literal_contains=args.literal_contains,
669
+ occurrence=args.occurrence,
670
+ expected_hash=args.expected_hash,
671
+ project_root=args.project_root,
672
+ marker_id=args.marker_id,
673
+ manifest_path=args.manifest,
674
+ )
675
+ except (OSError, RuntimeError, ValueError) as exc:
676
+ print(f"cseq: {exc}", file=sys.stderr)
677
+ return 6
678
+ print(manifest.to_json())
679
+ return 0
680
+
681
+
682
+ def _marker_undo(args: argparse.Namespace) -> int:
683
+ try:
684
+ undo_marker(args.path, args.manifest)
685
+ except (OSError, RuntimeError, ValueError) as exc:
686
+ print(f"cseq: {exc}", file=sys.stderr)
687
+ return 6
688
+ return 0
689
+
690
+
691
+ def _marker_audit(args: argparse.Namespace) -> int:
692
+ from pathlib import Path
693
+ root = Path(args.path)
694
+ if root.is_dir():
695
+ paths = [x.path for x in scan_sources(root)]
696
+ else:
697
+ paths = [root]
698
+ result = audit_markers(paths)
699
+ print(json.dumps(result, ensure_ascii=False, indent=2))
700
+ return 5 if result["duplicates"] or result["malformed"] else 0
701
+
702
+
703
+
704
+ def _diff(args: argparse.Namespace) -> int:
705
+ try:
706
+ pattern = RuntimePattern(args.pattern, args.time_format) if args.pattern else None
707
+ left = build_trace_session(args.left_session, args.left, pattern=pattern)
708
+ right = build_trace_session(args.right_session, args.right, pattern=pattern)
709
+ result = diff_sessions(left, right)
710
+ except (OSError, ValueError) as exc:
711
+ print(f"cseq: {exc}", file=sys.stderr); return 3
712
+ payload = {
713
+ "left_session": result.left_session,
714
+ "right_session": result.right_session,
715
+ "added_markers": list(result.added_markers),
716
+ "missing_markers": list(result.missing_markers),
717
+ "changed_cpu": list(result.changed_cpu),
718
+ "changed_task": list(result.changed_task),
719
+ }
720
+ if args.html:
721
+ from pathlib import Path
722
+ from .sequence import SequenceModel
723
+ Path(args.html).write_text(render_html(SequenceModel("trace-diff", []), trace_diff=result), encoding="utf-8")
724
+ if args.as_json:
725
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
726
+ else:
727
+ print(f"{result.left_session} -> {result.right_session}")
728
+ print(f"added markers: {len(result.added_markers)}")
729
+ print(f"missing markers: {len(result.missing_markers)}")
730
+ print(f"CPU changes: {len(result.changed_cpu)}")
731
+ print(f"Task changes: {len(result.changed_task)}")
732
+ return 0
733
+
734
+ def _trace_import(args: argparse.Namespace) -> int:
735
+ pattern = RuntimePattern(args.pattern, args.time_format) if args.pattern else None
736
+ if args.stream and (args.binary or args.dwarf or args.address_key):
737
+ print("cseq: runtime address evidence currently requires non-stream trace import", file=sys.stderr)
738
+ return 2
739
+ if args.stream:
740
+ if not args.store:
741
+ print("cseq: trace import --stream requires --store", file=sys.stderr)
742
+ return 2
743
+ try:
744
+ with EventStore(args.store) as store:
745
+ count = store.replace_session_events_chunked(
746
+ args.session,
747
+ iter_runtime_files(args.paths, pattern=pattern),
748
+ chunk_size=args.chunk_size,
749
+ batch_size=args.batch_size,
750
+ commit_every=args.commit_every,
751
+ rebuild_indexes=True,
752
+ )
753
+ store_summary = store.summary(args.session)
754
+ status = store.session_status(args.session)
755
+ except (OSError, ValueError) as exc:
756
+ print(f"cseq: {exc}", file=sys.stderr)
757
+ return 3
758
+ payload = {
759
+ "session": args.session,
760
+ "status": status,
761
+ "streamed": True,
762
+ "event_store": store_summary,
763
+ "source_logs": [str(x) for x in args.paths],
764
+ "event_count": count,
765
+ }
766
+ if args.as_json:
767
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
768
+ else:
769
+ print(f"session: {args.session}")
770
+ print(f"events: {count}")
771
+ print(f"status: {status}")
772
+ return 0
773
+
774
+ try:
775
+ session = build_trace_session(args.session, args.paths, pattern=pattern)
776
+ except (OSError, ValueError) as exc:
777
+ print(f"cseq: {exc}", file=sys.stderr)
778
+ return 3
779
+ address_evidence = []
780
+ if args.binary:
781
+ try:
782
+ image = load_binary_image(args.binary)
783
+ dwarf = load_dwarf_lines(args.dwarf) if args.dwarf else None
784
+ address_evidence = discover_runtime_address_evidence(
785
+ session.events, image, dwarf=dwarf, load_bias=args.load_bias, trusted_keys=args.address_key,
786
+ )
787
+ except (OSError, RuntimeError, ValueError) as exc:
788
+ print(f"cseq: {exc}", file=sys.stderr)
789
+ return 3
790
+ elif args.dwarf:
791
+ print("cseq: --dwarf requires --binary for runtime address resolution", file=sys.stderr)
792
+ return 2
793
+
794
+ if args.store:
795
+ with EventStore(args.store) as store:
796
+ store.replace_session(session)
797
+ store_summary = store.summary(session.name)
798
+ else:
799
+ store_summary = None
800
+ payload = {
801
+ "session": session.name,
802
+ "event_store": store_summary,
803
+ "source_logs": session.source_logs,
804
+ "runtime_address_evidence": [
805
+ {
806
+ "event_index": e.event_index, "marker_id": e.marker_id,
807
+ "key": e.payload_key, "value": e.payload_value,
808
+ "runtime_address": e.runtime_address, "linked_address": e.linked_address,
809
+ "symbol": e.symbol_name, "symbol_size": e.symbol_size,
810
+ "source_file": e.source_file, "source_line": e.source_line,
811
+ "confidence": e.confidence, "provenance": e.provenance,
812
+ } for e in address_evidence
813
+ ],
814
+ "events": [
815
+ {
816
+ "event_index": e.event_index,
817
+ "marker_id": e.marker_id,
818
+ "timestamp": e.timestamp_raw,
819
+ "cpu": e.cpu_hint,
820
+ "task": e.task_hint,
821
+ "payload": e.payload_dict(),
822
+ "raw": e.raw_line,
823
+ }
824
+ for e in session.events
825
+ ],
826
+ }
827
+ if args.as_json:
828
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
829
+ else:
830
+ print(f"session: {session.name}")
831
+ print(f"events: {len(session.events)}")
832
+ for e in session.events:
833
+ print(f" {e.event_index} {e.cpu_hint or '?'} {e.marker_id} {e.message or ''}")
834
+ return 0
835
+
836
+
837
+ def _explain(args: argparse.Namespace) -> int:
838
+ try:
839
+ index = Project(
840
+ args.path,
841
+ config=args.config,
842
+ configuration=args.configuration,
843
+ defines=tuple(args.define),
844
+ compile_db=getattr(args, "compile_db", None),
845
+ linker_map=getattr(args, "linker_map", None),
846
+ binary=getattr(args, "binary", None),
847
+ dwarf=getattr(args, "dwarf", None),
848
+ jobs=getattr(args, "jobs", 1),
849
+ ).index()
850
+ except (OSError, RuntimeError) as exc:
851
+ print(f"cseq: {exc}", file=sys.stderr)
852
+ return 3
853
+ payload = explain_call(index, caller=args.caller, callee_text=args.callee)
854
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
855
+ return 0 if payload["found"] else 5
856
+
857
+
858
+ def _query(args: argparse.Namespace) -> int:
859
+ try:
860
+ index = Project(
861
+ args.path,
862
+ config=args.config,
863
+ configuration=args.configuration,
864
+ defines=tuple(args.define),
865
+ compile_db=getattr(args, "compile_db", None),
866
+ linker_map=getattr(args, "linker_map", None),
867
+ binary=getattr(args, "binary", None),
868
+ dwarf=getattr(args, "dwarf", None),
869
+ jobs=getattr(args, "jobs", 1),
870
+ ).index()
871
+ except (OSError, RuntimeError) as exc:
872
+ print(f"cseq: {exc}", file=sys.stderr)
873
+ return 3
874
+ if args.kind == "calls":
875
+ if not args.function:
876
+ print("cseq: query calls requires --function", file=sys.stderr); return 2
877
+ payload = query_calls(index, args.function)
878
+ elif args.kind == "unresolved":
879
+ payload = query_unresolved(index)
880
+ else:
881
+ if not args.start or not args.end:
882
+ print("cseq: query paths requires --start and --end", file=sys.stderr); return 2
883
+ payload = query_paths(index, args.start, args.end)
884
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
885
+ return 0
886
+
887
+
888
+ def main(argv: list[str] | None = None) -> int:
889
+ parser = _build_parser()
890
+ args = parser.parse_args(argv)
891
+ return int(args.handler(args))
892
+
893
+
894
+ if __name__ == "__main__":
895
+ raise SystemExit(main())