python-delphi-lsp 3.6.0__cp310-abi3-win_amd64.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.
Files changed (56) hide show
  1. delphi_lsp/__init__.py +184 -0
  2. delphi_lsp/_version.py +1 -0
  3. delphi_lsp/agent_cache.py +1574 -0
  4. delphi_lsp/agent_cli.py +902 -0
  5. delphi_lsp/agent_context.py +3907 -0
  6. delphi_lsp/agent_cpg.py +349 -0
  7. delphi_lsp/agent_cpg_builder.py +666 -0
  8. delphi_lsp/agent_layers.py +1410 -0
  9. delphi_lsp/agent_metrics.py +526 -0
  10. delphi_lsp/agent_protocol.py +445 -0
  11. delphi_lsp/agent_relations.py +1881 -0
  12. delphi_lsp/agent_templates.py +614 -0
  13. delphi_lsp/agent_wiki.py +2853 -0
  14. delphi_lsp/agent_workspace.py +860 -0
  15. delphi_lsp/ast_serialize.py +126 -0
  16. delphi_lsp/binary.py +250 -0
  17. delphi_lsp/comment_builder.py +28 -0
  18. delphi_lsp/consts.py +345 -0
  19. delphi_lsp/delphi_lsp_native.pyd +0 -0
  20. delphi_lsp/delphiast_lexer.py +399 -0
  21. delphi_lsp/delphiast_parser.py +2189 -0
  22. delphi_lsp/delphiast_tokens.py +41 -0
  23. delphi_lsp/grammar.py +459 -0
  24. delphi_lsp/incremental.py +1139 -0
  25. delphi_lsp/lark_builder.py +2684 -0
  26. delphi_lsp/lark_tokens.py +236 -0
  27. delphi_lsp/lexical_scanner.py +30 -0
  28. delphi_lsp/lsp_server.py +6698 -0
  29. delphi_lsp/metrics.py +1452 -0
  30. delphi_lsp/native_bridge.py +300 -0
  31. delphi_lsp/navigation_cache.py +189 -0
  32. delphi_lsp/nodes.py +462 -0
  33. delphi_lsp/parallel_outline.py +662 -0
  34. delphi_lsp/parse_artifacts.py +969 -0
  35. delphi_lsp/parser.py +342 -0
  36. delphi_lsp/parser_backend.py +139 -0
  37. delphi_lsp/preprocessor.py +1122 -0
  38. delphi_lsp/progress.py +26 -0
  39. delphi_lsp/project_config.py +285 -0
  40. delphi_lsp/project_discovery.py +932 -0
  41. delphi_lsp/project_indexer.py +470 -0
  42. delphi_lsp/py.typed +1 -0
  43. delphi_lsp/response_cache.py +465 -0
  44. delphi_lsp/semantic.py +394 -0
  45. delphi_lsp/semantic_builder.py +2302 -0
  46. delphi_lsp/source_reader.py +17 -0
  47. delphi_lsp/wiki_external_sort.py +392 -0
  48. delphi_lsp/wiki_memory.py +548 -0
  49. delphi_lsp/workspace.py +83 -0
  50. delphi_lsp/writer.py +73 -0
  51. python_delphi_lsp-3.6.0.dist-info/METADATA +766 -0
  52. python_delphi_lsp-3.6.0.dist-info/RECORD +56 -0
  53. python_delphi_lsp-3.6.0.dist-info/WHEEL +4 -0
  54. python_delphi_lsp-3.6.0.dist-info/entry_points.txt +3 -0
  55. python_delphi_lsp-3.6.0.dist-info/licenses/LICENSE +373 -0
  56. python_delphi_lsp-3.6.0.dist-info/sboms/python-delphi-lsp.cyclonedx.json +770 -0
@@ -0,0 +1,1410 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from collections import OrderedDict
5
+ from collections.abc import Callable, Iterable, Mapping, MutableMapping, Sequence
6
+ from dataclasses import dataclass, replace
7
+ from pathlib import Path
8
+ from typing import Any, cast
9
+
10
+ from .agent_context import AgentContext
11
+ from .agent_metrics import build_path_metrics
12
+ from .agent_protocol import AgentRequest
13
+ from .lsp_server import outline_unit_name
14
+ from .parallel_outline import (
15
+ OutlineResult,
16
+ OutlineTask,
17
+ ParallelBuildStats,
18
+ run_outline_tasks,
19
+ )
20
+ from .parse_artifacts import OUTLINE_PRODUCER_VERSION
21
+ from .progress import ProgressCallback, ProgressEvent
22
+ from .project_config import ProjectPathConfig
23
+ from .project_discovery import (
24
+ DelphiProjectDiscovery,
25
+ discover_delphi_project,
26
+ populate_workspace_sources,
27
+ )
28
+ from .project_indexer import ProjectIndexer, ProjectIndexResult
29
+ from .semantic import Scope, SourceRange, Symbol, SymbolIndex, SymbolKind
30
+ from .semantic_builder import SemanticModel
31
+ from .source_reader import read_source_text
32
+
33
+ _MAX_IMPLEMENTATION_ITEMS = 50
34
+ _MAX_IMPLEMENTATION_SOURCE_FILES = 4
35
+ _MAX_REFERENCE_ITEMS = 50
36
+ _MAX_REFERENCE_PAGES = 8
37
+ # A references view performs an internal find followed by one or more trace
38
+ # requests. Keep that bounded request on the workspace revision opened for it;
39
+ # the 60-second window is deliberately well above the 20-second query budget.
40
+ _REFERENCE_INITIAL_REVISION_REUSE_SECONDS = 60.0
41
+ _REFERENCE_LIMITATIONS = [
42
+ "Only resolved semantic references are reported; text matches are excluded."
43
+ ]
44
+
45
+
46
+ @dataclass
47
+ class CodebaseIndex:
48
+ root: str
49
+ discovery: DelphiProjectDiscovery
50
+ models: Mapping[str, SemanticModel]
51
+ symbol_index: SymbolIndex
52
+ project_results: dict[str, ProjectIndexResult]
53
+ parallel_stats: ParallelBuildStats = ParallelBuildStats(0, 0, 0, 0.0, 0)
54
+
55
+
56
+ @dataclass(frozen=True, slots=True)
57
+ class _ProjectIndexTask:
58
+ ordinal: int
59
+ source_path: str
60
+ search_paths: tuple[str, ...]
61
+ include_paths: tuple[str, ...]
62
+ defines: tuple[str, ...]
63
+ retain_project_syntax: bool
64
+ source_roots: tuple[str, ...]
65
+ project_config: ProjectPathConfig | None
66
+
67
+
68
+ @dataclass(frozen=True, slots=True)
69
+ class _ProjectIndexTaskResult:
70
+ ordinal: int
71
+ source_path: str
72
+ result: ProjectIndexResult
73
+
74
+
75
+ def _index_project_task(task: _ProjectIndexTask) -> _ProjectIndexTaskResult:
76
+ indexer = ProjectIndexer(
77
+ search_paths=task.search_paths,
78
+ include_paths=task.include_paths,
79
+ defines=task.defines,
80
+ source_roots=task.source_roots,
81
+ project_config=task.project_config,
82
+ retain_syntax=task.retain_project_syntax,
83
+ )
84
+ raw_result = indexer.index(task.source_path)
85
+ result = raw_result
86
+ if not task.retain_project_syntax:
87
+ result = replace(
88
+ raw_result,
89
+ parsed_units=[
90
+ replace(unit, syntax_tree=None, preprocessed=None)
91
+ for unit in raw_result.parsed_units
92
+ ],
93
+ )
94
+ return _ProjectIndexTaskResult(task.ordinal, task.source_path, result)
95
+
96
+
97
+ def build_codebase_index(
98
+ root: str | Path,
99
+ *,
100
+ project_file: str | Path | None = None,
101
+ index_projects: bool = False,
102
+ main_projects_only: bool = False,
103
+ scan_workspace_sources: bool | None = None,
104
+ retain_project_syntax: bool = True,
105
+ retain_project_results: bool = True,
106
+ retain_outline_models: bool = True,
107
+ project_source_roots: Iterable[str | Path] = (),
108
+ on_progress: ProgressCallback | None = None,
109
+ on_project_result: Callable[[str, ProjectIndexResult], None] | None = None,
110
+ on_outline_result: Callable[[OutlineResult], None] | None = None,
111
+ workers: int = 0,
112
+ project_workers: int | None = None,
113
+ outline_memory_budget_bytes: int | None = None,
114
+ outline_heartbeat: Callable[[], object] | None = None,
115
+ outline_force_heartbeat: Callable[[], object] | None = None,
116
+ outline_active_worker_limit: Callable[[], int] | None = None,
117
+ use_artifact_cache: bool = True,
118
+ discovery: DelphiProjectDiscovery | None = None,
119
+ outline_source_files: Iterable[str] | None = None,
120
+ ) -> CodebaseIndex:
121
+ progress = _MonotonicProgress(on_progress)
122
+ root_path = Path(root).expanduser().resolve()
123
+ if scan_workspace_sources is None:
124
+ scan_workspace_sources = not (main_projects_only and index_projects)
125
+ if discovery is None:
126
+ discovery = discover_delphi_project(
127
+ root_path,
128
+ project_file=project_file,
129
+ main_projects_only=main_projects_only,
130
+ scan_workspace_sources=scan_workspace_sources,
131
+ on_progress=progress,
132
+ )
133
+ project_results: dict[str, ProjectIndexResult] = {}
134
+ if index_projects and main_projects_only:
135
+ streamed_sources = (
136
+ {source.casefold(): source for source in discovery.source_files}
137
+ if scan_workspace_sources
138
+ else {}
139
+ )
140
+
141
+ def accept_project_result(
142
+ path: str,
143
+ result: ProjectIndexResult,
144
+ ) -> None:
145
+ _add_project_result_sources(streamed_sources, result)
146
+ if on_project_result is not None:
147
+ on_project_result(path, result)
148
+
149
+ project_results = _build_project_results(
150
+ discovery,
151
+ retain_project_syntax=retain_project_syntax,
152
+ retain_results=retain_project_results,
153
+ source_roots=tuple(project_source_roots),
154
+ progress=progress,
155
+ on_progress=on_progress,
156
+ on_result=accept_project_result,
157
+ workers=workers if project_workers is None else project_workers,
158
+ )
159
+ if retain_project_results:
160
+ _set_project_source_inventory(
161
+ discovery,
162
+ project_results,
163
+ merge_existing=scan_workspace_sources,
164
+ )
165
+ else:
166
+ _set_source_inventory(discovery, streamed_sources)
167
+ if not discovery.project_files:
168
+ populate_workspace_sources(discovery, on_progress=progress)
169
+
170
+ models: dict[str, SemanticModel] = {}
171
+ lines_processed = 0
172
+ symbols_discovered = 0
173
+ completed_outlines = 0
174
+ sources_to_outline = (
175
+ tuple(discovery.source_files)
176
+ if outline_source_files is None
177
+ else tuple(outline_source_files)
178
+ )
179
+ outline_tasks = tuple(
180
+ OutlineTask(
181
+ ordinal,
182
+ source,
183
+ tuple(discovery.defines),
184
+ False,
185
+ workspace_root=str(root_path),
186
+ include_paths=tuple(discovery.include_paths),
187
+ project_config=discovery.project_config,
188
+ producer_version=OUTLINE_PRODUCER_VERSION,
189
+ )
190
+ for ordinal, source in enumerate(sources_to_outline)
191
+ if Path(source).suffix.casefold() in {".pas", ".dpr", ".dpk", ".inc"}
192
+ )
193
+ outline_task_count = len(outline_tasks)
194
+
195
+ def on_outline_complete(result: OutlineResult) -> None:
196
+ nonlocal completed_outlines, lines_processed, symbols_discovered
197
+ completed_outlines += 1
198
+ if not result.read_error:
199
+ lines_processed += result.lines_processed
200
+ symbols_discovered += result.symbols_discovered
201
+ _emit_progress(
202
+ progress,
203
+ "outline",
204
+ result.source_path,
205
+ len(discovery.source_files),
206
+ completed_outlines,
207
+ outline_task_count,
208
+ "source unreadable" if result.read_error else "source outlined",
209
+ lines_processed=lines_processed,
210
+ symbols_discovered=symbols_discovered,
211
+ )
212
+ if on_outline_result is not None:
213
+ on_outline_result(result)
214
+
215
+ outline_batch = run_outline_tasks(
216
+ outline_tasks,
217
+ configured_workers=workers,
218
+ use_artifact_cache=use_artifact_cache,
219
+ memory_budget_bytes=outline_memory_budget_bytes,
220
+ include_files=discovery.source_files,
221
+ on_complete=on_outline_complete,
222
+ retain_results=retain_outline_models,
223
+ heartbeat=outline_heartbeat,
224
+ force_heartbeat=outline_force_heartbeat,
225
+ active_worker_limit=outline_active_worker_limit,
226
+ )
227
+ for result in outline_batch.results:
228
+ if result.read_error or result.model is None:
229
+ continue
230
+ models[result.source_path] = result.model
231
+
232
+ symbol_index = SymbolIndex()
233
+ for model in models.values():
234
+ symbol_index.register_unit(model.unit_scope.name, model.unit_scope)
235
+ for model in models.values():
236
+ model.index = symbol_index
237
+
238
+ _emit_progress(
239
+ progress,
240
+ "relations",
241
+ str(root_path),
242
+ len(discovery.source_files),
243
+ len(discovery.source_files),
244
+ len(discovery.source_files),
245
+ "semantic relations indexed",
246
+ lines_processed=lines_processed,
247
+ symbols_discovered=sum(len(items) for items in symbol_index.name_index.values()),
248
+ )
249
+
250
+ if index_projects and not main_projects_only:
251
+ project_results = _build_project_results(
252
+ discovery,
253
+ retain_project_syntax=retain_project_syntax,
254
+ retain_results=retain_project_results,
255
+ source_roots=tuple(project_source_roots),
256
+ progress=progress,
257
+ on_progress=on_progress,
258
+ on_result=on_project_result,
259
+ workers=workers if project_workers is None else project_workers,
260
+ )
261
+
262
+ index = CodebaseIndex(
263
+ root=str(root_path),
264
+ discovery=discovery,
265
+ models=models,
266
+ symbol_index=symbol_index,
267
+ project_results=project_results,
268
+ parallel_stats=outline_batch.stats,
269
+ )
270
+ _emit_progress(
271
+ progress,
272
+ "complete",
273
+ str(root_path),
274
+ len(discovery.source_files),
275
+ len(discovery.source_files),
276
+ len(discovery.source_files),
277
+ "codebase index complete",
278
+ lines_processed=lines_processed,
279
+ symbols_discovered=sum(len(items) for items in symbol_index.name_index.values()),
280
+ )
281
+ return index
282
+
283
+
284
+ def build_units_payload(
285
+ root: str | Path,
286
+ *,
287
+ query: str,
288
+ project_file: str | Path | None = None,
289
+ index_projects: bool = False,
290
+ workers: int = 0,
291
+ use_artifact_cache: bool = True,
292
+ discovery: DelphiProjectDiscovery | None = None,
293
+ ) -> dict[str, Any]:
294
+ root_path = Path(root).expanduser().resolve()
295
+ if discovery is None:
296
+ discovery = discover_delphi_project(
297
+ root_path,
298
+ project_file=project_file,
299
+ scan_workspace_sources=True,
300
+ )
301
+ needle = query.casefold().strip()
302
+ candidates = tuple(
303
+ source
304
+ for source in discovery.source_files
305
+ if _source_matches_units_query(source, needle)
306
+ )
307
+ index = build_codebase_index(
308
+ root_path,
309
+ project_file=project_file,
310
+ index_projects=index_projects,
311
+ workers=workers,
312
+ use_artifact_cache=use_artifact_cache,
313
+ discovery=discovery,
314
+ outline_source_files=candidates,
315
+ )
316
+ return _units_payload(index, query=query)
317
+
318
+
319
+ def _source_matches_units_query(source: str, needle: str) -> bool:
320
+ if not needle or needle in source.casefold():
321
+ return True
322
+ try:
323
+ text = read_source_text(Path(source))
324
+ except OSError:
325
+ return False
326
+ return needle in outline_unit_name(text, source).casefold()
327
+
328
+
329
+ def _build_project_results(
330
+ discovery: DelphiProjectDiscovery,
331
+ *,
332
+ retain_project_syntax: bool,
333
+ retain_results: bool,
334
+ source_roots: tuple[str | Path, ...],
335
+ progress: ProgressCallback,
336
+ on_progress: ProgressCallback | None,
337
+ on_result: Callable[[str, ProjectIndexResult], None] | None,
338
+ workers: int,
339
+ ) -> dict[str, ProjectIndexResult]:
340
+ project_results: dict[str, ProjectIndexResult] = {}
341
+ project_total = len(discovery.project_files)
342
+ if project_total > 1 and workers != 1:
343
+ completed = 0
344
+
345
+ def on_complete(result: _ProjectIndexTaskResult) -> None:
346
+ nonlocal completed
347
+ completed += 1
348
+ _emit_progress(
349
+ on_progress,
350
+ "projects",
351
+ result.source_path,
352
+ project_total,
353
+ completed,
354
+ project_total,
355
+ "main project indexed",
356
+ )
357
+ if on_result is not None:
358
+ on_result(result.source_path, result.result)
359
+
360
+ tasks = tuple(
361
+ _ProjectIndexTask(
362
+ ordinal=ordinal,
363
+ source_path=project,
364
+ search_paths=tuple(discovery.search_paths),
365
+ include_paths=tuple(discovery.include_paths),
366
+ defines=tuple(discovery.defines),
367
+ retain_project_syntax=retain_project_syntax,
368
+ source_roots=tuple(str(root) for root in source_roots),
369
+ project_config=discovery.project_config,
370
+ )
371
+ for ordinal, project in enumerate(discovery.project_files)
372
+ )
373
+ batch = run_outline_tasks(
374
+ cast(Iterable[OutlineTask], tasks),
375
+ configured_workers=workers,
376
+ use_artifact_cache=False,
377
+ on_complete=cast(Callable[[OutlineResult], None], on_complete),
378
+ retain_results=retain_results,
379
+ task_runner=cast(
380
+ Callable[[OutlineTask], OutlineResult],
381
+ _index_project_task,
382
+ ),
383
+ )
384
+ results = cast(tuple[_ProjectIndexTaskResult, ...], batch.results)
385
+ return {
386
+ result.source_path: result.result
387
+ for result in results
388
+ } if retain_results else {}
389
+
390
+ for ordinal, project in enumerate(discovery.project_files, start=1):
391
+ indexer = ProjectIndexer(
392
+ search_paths=discovery.search_paths,
393
+ include_paths=discovery.include_paths,
394
+ defines=discovery.defines,
395
+ on_progress=progress,
396
+ source_roots=source_roots,
397
+ project_config=discovery.project_config,
398
+ retain_syntax=retain_project_syntax,
399
+ )
400
+ raw_result = indexer.index(project)
401
+ result = raw_result
402
+ if not retain_project_syntax:
403
+ result = replace(
404
+ raw_result,
405
+ parsed_units=[
406
+ replace(unit, syntax_tree=None, preprocessed=None)
407
+ for unit in raw_result.parsed_units
408
+ ],
409
+ )
410
+ # Do not invoke streaming consumers while the parser/indexer and its
411
+ # raw syntax/preprocessor graph are still reachable. The compact
412
+ # result is self-contained, so releasing those owners first keeps a
413
+ # sequential project export genuinely bounded.
414
+ del raw_result
415
+ del indexer
416
+ if on_result is not None:
417
+ on_result(project, result)
418
+ if retain_results:
419
+ project_results[project] = result
420
+ _emit_progress(
421
+ on_progress,
422
+ "projects",
423
+ project,
424
+ project_total,
425
+ ordinal,
426
+ project_total,
427
+ "main project indexed",
428
+ )
429
+ return project_results
430
+
431
+
432
+ def _add_project_result_sources(
433
+ sources: dict[str, str],
434
+ result: ProjectIndexResult,
435
+ ) -> None:
436
+ for unit in result.parsed_units:
437
+ resolved = str(Path(unit.path).expanduser().resolve())
438
+ sources.setdefault(resolved.casefold(), resolved)
439
+ for include in result.include_files:
440
+ resolved = str(Path(include.path).expanduser().resolve())
441
+ sources.setdefault(resolved.casefold(), resolved)
442
+
443
+
444
+ def _set_source_inventory(
445
+ discovery: DelphiProjectDiscovery,
446
+ sources: Mapping[str, str],
447
+ ) -> None:
448
+ discovery.source_files = sorted(sources.values(), key=str.casefold)
449
+ discovery.unit_paths = {}
450
+ for source in discovery.source_files:
451
+ key = Path(source).stem.casefold()
452
+ discovery.unit_paths.setdefault(key, []).append(source)
453
+
454
+
455
+ def _set_project_source_inventory(
456
+ discovery: DelphiProjectDiscovery,
457
+ project_results: dict[str, ProjectIndexResult],
458
+ *,
459
+ merge_existing: bool = False,
460
+ ) -> None:
461
+ sources = (
462
+ {source.casefold(): source for source in discovery.source_files}
463
+ if merge_existing
464
+ else {}
465
+ )
466
+ for result in project_results.values():
467
+ for unit in result.parsed_units:
468
+ resolved = str(Path(unit.path).expanduser().resolve())
469
+ sources.setdefault(resolved.casefold(), resolved)
470
+ for include in result.include_files:
471
+ resolved = str(Path(include.path).expanduser().resolve())
472
+ sources.setdefault(resolved.casefold(), resolved)
473
+ discovery.source_files = sorted(sources.values(), key=str.casefold)
474
+ discovery.unit_paths = {}
475
+ for source in discovery.source_files:
476
+ key = Path(source).stem.casefold()
477
+ discovery.unit_paths.setdefault(key, []).append(source)
478
+
479
+
480
+ def _emit_progress(
481
+ callback: ProgressCallback | None,
482
+ phase: str,
483
+ path: str,
484
+ files_discovered: int,
485
+ files_completed: int,
486
+ files_total: int | None,
487
+ detail: str,
488
+ *,
489
+ lines_processed: int = 0,
490
+ symbols_discovered: int = 0,
491
+ ) -> None:
492
+ if callback is not None:
493
+ callback(
494
+ ProgressEvent(
495
+ phase,
496
+ "delphi",
497
+ path,
498
+ files_discovered,
499
+ files_completed,
500
+ files_total,
501
+ lines_processed,
502
+ symbols_discovered,
503
+ 0,
504
+ detail,
505
+ )
506
+ )
507
+
508
+
509
+ class _MonotonicProgress:
510
+ def __init__(self, callback: ProgressCallback | None) -> None:
511
+ self._callback = callback
512
+ self._files_discovered = 0
513
+ self._files_completed = 0
514
+ self._files_total: int | None = None
515
+ self._lines_processed = 0
516
+ self._symbols_discovered = 0
517
+ self._cached_files = 0
518
+
519
+ def __call__(self, event: ProgressEvent) -> None:
520
+ self._files_discovered = max(self._files_discovered, event.files_discovered)
521
+ self._files_completed = max(self._files_completed, event.files_completed)
522
+ if event.files_total is not None:
523
+ self._files_total = max(self._files_total or 0, event.files_total)
524
+ self._lines_processed = max(self._lines_processed, event.lines_processed)
525
+ self._symbols_discovered = max(self._symbols_discovered, event.symbols_discovered)
526
+ self._cached_files = max(self._cached_files, event.cached_files)
527
+ if self._callback is not None:
528
+ self._callback(
529
+ replace(
530
+ event,
531
+ files_discovered=self._files_discovered,
532
+ files_completed=self._files_completed,
533
+ files_total=self._files_total,
534
+ lines_processed=self._lines_processed,
535
+ symbols_discovered=self._symbols_discovered,
536
+ cached_files=self._cached_files,
537
+ )
538
+ )
539
+
540
+
541
+ def render_layer(
542
+ index: CodebaseIndex,
543
+ layer: str,
544
+ *,
545
+ query: str = "",
546
+ output_format: str = "markdown",
547
+ ) -> str:
548
+ payload = layer_payload(index, layer, query=query)
549
+ return render_payload(payload, output_format=output_format)
550
+
551
+
552
+ def render_payload(
553
+ payload: dict[str, Any],
554
+ *,
555
+ output_format: str = "markdown",
556
+ ) -> str:
557
+ if output_format == "json":
558
+ return json.dumps(payload, indent=2, sort_keys=True) + "\n"
559
+ return _render_markdown(payload)
560
+
561
+
562
+ def layer_payload(index: CodebaseIndex, layer: str, *, query: str = "") -> dict[str, Any]:
563
+ normalized_layer = layer.casefold()
564
+ if normalized_layer == "overview":
565
+ return _overview_payload(index)
566
+ if normalized_layer == "projects":
567
+ return _projects_payload(index)
568
+ if normalized_layer == "units":
569
+ return _units_payload(index, query=query)
570
+ if normalized_layer == "unit":
571
+ return _unit_payload(index, query=query)
572
+ if normalized_layer == "symbols":
573
+ return _symbols_payload(index, query=query)
574
+ if normalized_layer == "symbol":
575
+ return _symbol_payload(index, query=query)
576
+ if normalized_layer == "implementation":
577
+ return _implementation_payload(index, query=query)
578
+ if normalized_layer == "references":
579
+ return _references_payload(index, query=query)
580
+ if normalized_layer == "problems":
581
+ return _problems_payload(index)
582
+ if normalized_layer == "metrics":
583
+ return _metrics_payload(index, query=query)
584
+ raise ValueError(f"Unknown layer: {layer}")
585
+
586
+
587
+ def _overview_payload(index: CodebaseIndex) -> dict[str, Any]:
588
+ return {
589
+ "layer": "overview",
590
+ "root": index.root,
591
+ "project_count": len(index.discovery.project_files),
592
+ "source_count": len(index.discovery.source_files),
593
+ "unit_count": len(index.models),
594
+ "search_paths": index.discovery.search_paths,
595
+ "include_paths": index.discovery.include_paths,
596
+ "defines": index.discovery.defines,
597
+ "problems": [_problem_item(problem) for problem in index.discovery.problems],
598
+ "projects": index.discovery.project_files,
599
+ }
600
+
601
+
602
+ def _projects_payload(index: CodebaseIndex) -> dict[str, Any]:
603
+ items: list[dict[str, Any]] = []
604
+ if index.project_results:
605
+ for project, result in index.project_results.items():
606
+ items.append(
607
+ {
608
+ "path": project,
609
+ "parsed_units": [
610
+ {"name": unit.name, "path": unit.path, "has_error": unit.has_error}
611
+ for unit in result.parsed_units
612
+ ],
613
+ "include_files": [{"name": item.name, "path": item.path} for item in result.include_files],
614
+ "not_found_units": result.not_found_units,
615
+ "problems": [
616
+ {"kind": problem.problem_type.value, "file": problem.file_name, "message": problem.description}
617
+ for problem in result.problems
618
+ ],
619
+ }
620
+ )
621
+ else:
622
+ for project in index.discovery.project_files:
623
+ items.append(
624
+ {
625
+ "path": project,
626
+ "parsed_units": [],
627
+ "include_files": [],
628
+ "not_found_units": [],
629
+ "problems": [],
630
+ "deep_indexed": False,
631
+ }
632
+ )
633
+ return {"layer": "projects", "root": index.root, "items": items}
634
+
635
+
636
+ def _units_payload(index: CodebaseIndex, *, query: str) -> dict[str, Any]:
637
+ needle = query.casefold().strip()
638
+ items = []
639
+ for file_name, model in sorted(index.models.items(), key=lambda item: item[0].casefold()):
640
+ if needle and needle not in model.unit_scope.name.casefold() and needle not in file_name.casefold():
641
+ continue
642
+ items.append(
643
+ {
644
+ "name": model.unit_scope.name,
645
+ "path": file_name,
646
+ "symbol_count": sum(1 for _ in _iter_symbols(model.unit_scope, include_unit=False)),
647
+ }
648
+ )
649
+ return {"layer": "units", "root": index.root, "items": items}
650
+
651
+
652
+ def _unit_payload(index: CodebaseIndex, *, query: str) -> dict[str, Any]:
653
+ needle = query.casefold().strip()
654
+ items = []
655
+ for file_name, model in sorted(index.models.items(), key=lambda item: item[0].casefold()):
656
+ if needle and needle not in model.unit_scope.name.casefold() and needle not in file_name.casefold():
657
+ continue
658
+ items.append(
659
+ {
660
+ "name": model.unit_scope.name,
661
+ "path": file_name,
662
+ "symbols": [_symbol_item(symbol) for symbol in _iter_symbols(model.unit_scope, include_unit=False)],
663
+ }
664
+ )
665
+ return {"layer": "unit", "root": index.root, "items": items}
666
+
667
+
668
+ def _symbols_payload(index: CodebaseIndex, *, query: str) -> dict[str, Any]:
669
+ needle = query.casefold().strip()
670
+ symbols = []
671
+ for symbol in _all_symbols(index):
672
+ if symbol.kind.value == "unit":
673
+ continue
674
+ if needle and needle not in symbol.name.casefold():
675
+ continue
676
+ symbols.append(_symbol_item(symbol))
677
+ symbols.sort(key=lambda item: (item["name"].casefold(), item["path"].casefold(), item["line"]))
678
+ return {"layer": "symbols", "root": index.root, "query": query, "items": symbols}
679
+
680
+
681
+ def _symbol_payload(index: CodebaseIndex, *, query: str) -> dict[str, Any]:
682
+ needle = query.casefold().strip()
683
+ matches = []
684
+ for symbol in _all_symbols(index):
685
+ if needle and needle not in symbol.name.casefold():
686
+ continue
687
+ item = _symbol_item(symbol)
688
+ if symbol.member_scope is not None:
689
+ item["children"] = [_symbol_item(child) for child in _iter_symbols(symbol.member_scope, include_unit=False)]
690
+ matches.append(item)
691
+ return {"layer": "symbol", "root": index.root, "query": query, "items": matches[:50]}
692
+
693
+
694
+ def _implementation_payload(index: CodebaseIndex, *, query: str) -> dict[str, Any]:
695
+ needle = query.casefold().strip()
696
+ if not needle:
697
+ return {
698
+ "layer": "implementation",
699
+ "root": index.root,
700
+ "query": query,
701
+ "items": [],
702
+ "message": "Pass a class, routine, or member name in query to read focused source.",
703
+ }
704
+
705
+ all_symbols = tuple(_all_symbols(index))
706
+ matches = [symbol for symbol in all_symbols if symbol.name.casefold() == needle]
707
+ if not matches:
708
+ matches = [
709
+ symbol
710
+ for symbol in all_symbols
711
+ if symbol.kind.value != "unit" and needle in symbol.name.casefold()
712
+ ]
713
+ unique_matches: dict[tuple[str, str, int, int, str], Symbol] = {}
714
+ for symbol in matches:
715
+ unique_matches.setdefault(
716
+ (
717
+ symbol.decl_range.file_name,
718
+ symbol.name.casefold(),
719
+ symbol.decl_range.start_line,
720
+ symbol.decl_range.start_col,
721
+ symbol.kind.value,
722
+ ),
723
+ symbol,
724
+ )
725
+ matches = sorted(
726
+ unique_matches.values(),
727
+ key=lambda symbol: (
728
+ symbol.decl_range.file_name.casefold(),
729
+ symbol.decl_range.start_line,
730
+ symbol.decl_range.start_col,
731
+ symbol.name.casefold(),
732
+ symbol.kind.value,
733
+ ),
734
+ )[:_MAX_IMPLEMENTATION_ITEMS]
735
+
736
+ items: list[dict[str, Any]] = []
737
+ source_cache = _BoundedSourceCache(_MAX_IMPLEMENTATION_SOURCE_FILES)
738
+ routine_lookup = _build_routine_lookup(all_symbols)
739
+ seen_keys: set[tuple[str, str, int, str]] = set()
740
+ for symbol in matches:
741
+ item = _implementation_item(symbol, source_cache, routine_lookup)
742
+ if item is None:
743
+ continue
744
+ key = (item["name"].casefold(), item["path"], item["line"], item["kind"])
745
+ if key in seen_keys:
746
+ continue
747
+ seen_keys.add(key)
748
+ items.append(item)
749
+ items.sort(key=lambda item: (item["path"].casefold(), item["line"], item["name"].casefold()))
750
+ return {"layer": "implementation", "root": index.root, "query": query, "items": items}
751
+
752
+
753
+ class _BoundedSourceCache(OrderedDict[str, list[str]]):
754
+ def __init__(self, max_entries: int) -> None:
755
+ super().__init__()
756
+ self.max_entries = max_entries
757
+
758
+ def get(self, key: str, default: list[str] | None = None) -> list[str] | None:
759
+ value = super().get(key, default)
760
+ if key in self:
761
+ self.move_to_end(key)
762
+ return value
763
+
764
+ def __setitem__(self, key: str, value: list[str]) -> None:
765
+ super().__setitem__(key, value)
766
+ self.move_to_end(key)
767
+ while len(self) > self.max_entries:
768
+ self.popitem(last=False)
769
+
770
+
771
+ @dataclass(frozen=True)
772
+ class _RoutineLookup:
773
+ by_name: dict[tuple[str, str], tuple[Symbol, ...]]
774
+ by_owner: dict[tuple[str, str], tuple[Symbol, ...]]
775
+
776
+
777
+ def _build_routine_lookup(symbols: Iterable[Symbol]) -> _RoutineLookup:
778
+ by_name_lists: dict[tuple[str, str], list[Symbol]] = {}
779
+ by_owner_lists: dict[tuple[str, str], list[Symbol]] = {}
780
+ for symbol in symbols:
781
+ if symbol.kind not in _ROUTINE_KINDS:
782
+ continue
783
+ normalized_name = symbol.name.casefold()
784
+ path = symbol.decl_range.file_name
785
+ by_name_lists.setdefault((path, normalized_name), []).append(symbol)
786
+ owner, separator, _member = normalized_name.rpartition(".")
787
+ if separator:
788
+ by_owner_lists.setdefault((path, owner), []).append(symbol)
789
+ return _RoutineLookup(
790
+ by_name={key: tuple(value) for key, value in by_name_lists.items()},
791
+ by_owner={key: tuple(value) for key, value in by_owner_lists.items()},
792
+ )
793
+
794
+
795
+ def _implementation_item(
796
+ symbol: Symbol,
797
+ source_cache: MutableMapping[str, list[str]],
798
+ routine_lookup: _RoutineLookup,
799
+ ) -> dict[str, Any] | None:
800
+ fragments: list[dict[str, Any]] = []
801
+ if symbol.kind in {SymbolKind.CLASS, SymbolKind.RECORD, SymbolKind.INTERFACE}:
802
+ declaration = _source_fragment(symbol.decl_range, "declaration", source_cache)
803
+ if declaration is not None:
804
+ fragments.append(declaration)
805
+ fragments.extend(
806
+ _implementation_fragments_for_type(
807
+ symbol, source_cache, routine_lookup
808
+ )
809
+ )
810
+ elif symbol.kind in _ROUTINE_KINDS:
811
+ fragment = _source_fragment(symbol.decl_range, "implementation", source_cache)
812
+ if fragment is not None:
813
+ fragments.append(fragment)
814
+ else:
815
+ fragment = _source_fragment(symbol.decl_range, "declaration", source_cache)
816
+ if fragment is not None:
817
+ fragments.append(fragment)
818
+ fragments.extend(
819
+ _implementation_fragments_for_member(
820
+ symbol, source_cache, routine_lookup
821
+ )
822
+ )
823
+ if not fragments:
824
+ return None
825
+ item = _symbol_item(symbol)
826
+ item["fragments"] = fragments
827
+ return item
828
+
829
+
830
+ def _implementation_fragments_for_type(
831
+ symbol: Symbol,
832
+ source_cache: MutableMapping[str, list[str]],
833
+ routine_lookup: _RoutineLookup,
834
+ ) -> list[dict[str, Any]]:
835
+ fragments: list[dict[str, Any]] = []
836
+ seen_ranges: set[SourceRange] = set()
837
+ candidates = routine_lookup.by_owner.get(
838
+ (symbol.decl_range.file_name, symbol.name.casefold()), ()
839
+ )
840
+ for candidate in candidates:
841
+ fragment = _source_fragment(candidate.decl_range, "implementation", source_cache)
842
+ if fragment is None or candidate.decl_range in seen_ranges:
843
+ continue
844
+ seen_ranges.add(candidate.decl_range)
845
+ fragment["symbol"] = candidate.name
846
+ fragments.append(fragment)
847
+ fragments.sort(key=lambda item: (item["range"]["start_line"], item["range"]["start_col"]))
848
+ return fragments
849
+
850
+
851
+ def _implementation_fragments_for_member(
852
+ symbol: Symbol,
853
+ source_cache: MutableMapping[str, list[str]],
854
+ routine_lookup: _RoutineLookup,
855
+ ) -> list[dict[str, Any]]:
856
+ owner = symbol.scope.owner
857
+ if owner is None or owner.kind not in {SymbolKind.CLASS, SymbolKind.RECORD, SymbolKind.INTERFACE}:
858
+ return []
859
+ qualified_name = f"{owner.name.casefold()}.{symbol.name.casefold()}"
860
+ fragments: list[dict[str, Any]] = []
861
+ candidates = routine_lookup.by_name.get(
862
+ (symbol.decl_range.file_name, qualified_name), ()
863
+ )
864
+ for candidate in candidates:
865
+ fragment = _source_fragment(candidate.decl_range, "implementation", source_cache)
866
+ if fragment is not None:
867
+ fragment["symbol"] = candidate.name
868
+ fragments.append(fragment)
869
+ return fragments
870
+
871
+
872
+ def _source_fragment(
873
+ source_range: SourceRange,
874
+ fragment_kind: str,
875
+ source_cache: MutableMapping[str, list[str]],
876
+ ) -> dict[str, Any] | None:
877
+ lines = source_cache.get(source_range.file_name)
878
+ if lines is None:
879
+ try:
880
+ text = read_source_text(Path(source_range.file_name))
881
+ except (OSError, UnicodeError):
882
+ return None
883
+ lines = text.splitlines(keepends=True)
884
+ source_cache[source_range.file_name] = lines
885
+ if source_range.start_line < 1 or source_range.start_line > len(lines):
886
+ return None
887
+ end_line = min(max(source_range.end_line, source_range.start_line), len(lines))
888
+ snippet = "".join(lines[source_range.start_line - 1 : end_line]).rstrip("\r\n")
889
+ return {
890
+ "fragment_kind": fragment_kind,
891
+ "range": _range_item(source_range),
892
+ "line_count": end_line - source_range.start_line + 1,
893
+ "text": snippet,
894
+ }
895
+
896
+
897
+ def _references_payload(index: CodebaseIndex, *, query: str) -> dict[str, Any]:
898
+ return build_references_payload(index.root, query=query)
899
+
900
+
901
+ def build_references_payload(
902
+ root: str | Path,
903
+ *,
904
+ query: str,
905
+ project_file: str | Path | None = None,
906
+ workers: int = 0,
907
+ ) -> dict[str, Any]:
908
+ root_path = Path(root).expanduser().resolve()
909
+ payload: dict[str, Any] = {
910
+ "layer": "references",
911
+ "root": str(root_path),
912
+ "query": query,
913
+ "target": None,
914
+ "items": [],
915
+ "completeness": "sound_partial",
916
+ "scan_complete": False,
917
+ "items_truncated": False,
918
+ "limitations": list(_REFERENCE_LIMITATIONS),
919
+ }
920
+ symbol_query = query.strip()
921
+ selector_path = ""
922
+ if "::" in symbol_query:
923
+ path_part, symbol_part = symbol_query.rsplit("::", 1)
924
+ if path_part.strip() and symbol_part.strip():
925
+ selector_path = path_part.strip().replace("\\", "/").casefold()
926
+ symbol_query = symbol_part.strip()
927
+ needle = symbol_query.casefold()
928
+ if not needle:
929
+ payload["message"] = (
930
+ "Pass one exact symbol name or qualified name in query to find "
931
+ "resolved semantic references."
932
+ )
933
+ return payload
934
+
935
+ context = AgentContext.open(
936
+ root_path,
937
+ project_file=project_file,
938
+ workers=workers,
939
+ revision_check_interval_seconds=_REFERENCE_INITIAL_REVISION_REUSE_SECONDS,
940
+ reference_query=symbol_query,
941
+ reference_path=selector_path,
942
+ )
943
+ found = context.handle(
944
+ AgentRequest(
945
+ action="find",
946
+ query=symbol_query,
947
+ max_items=50,
948
+ max_chars=40000,
949
+ )
950
+ )
951
+ candidates = _mapping_items(found.result)
952
+ exact_targets: dict[str, dict[str, object]] = {}
953
+ for candidate in candidates:
954
+ name = candidate.get("name")
955
+ qualified_name = candidate.get("qualified_name")
956
+ candidate_path = candidate.get("path")
957
+ target_id = candidate.get("target_id")
958
+ if not (
959
+ isinstance(name, str)
960
+ and isinstance(qualified_name, str)
961
+ and (name.casefold() == needle or qualified_name.casefold() == needle)
962
+ ):
963
+ continue
964
+ if selector_path and not (
965
+ isinstance(candidate_path, str)
966
+ and candidate_path.replace("\\", "/").casefold() == selector_path
967
+ ):
968
+ continue
969
+ if isinstance(target_id, str):
970
+ exact_targets.setdefault(target_id, dict(candidate))
971
+
972
+ logical_targets = _logical_reference_targets(exact_targets.values())
973
+ if not logical_targets:
974
+ payload["scan_complete"] = not found.page.truncated
975
+ payload["message"] = f"No exact symbol target found for query: {query}"
976
+ return payload
977
+ if len(logical_targets) > 1:
978
+ payload["candidates"] = logical_targets
979
+ payload["items_truncated"] = found.page.truncated
980
+ payload["message"] = (
981
+ "The query resolves to multiple symbols; pass one candidate as "
982
+ "'<path>::<qualified_name>'."
983
+ )
984
+ return payload
985
+
986
+ target = logical_targets[0]
987
+ payload["target"] = target
988
+ target_id = target["target_id"]
989
+ assert isinstance(target_id, str)
990
+ trace_request = AgentRequest(
991
+ action="trace",
992
+ target_id=target_id,
993
+ relation="references",
994
+ max_items=_MAX_REFERENCE_ITEMS,
995
+ max_chars=40000,
996
+ )
997
+ trace_items: list[dict[str, object]] = []
998
+ remaining_cursor = ""
999
+ for _page_number in range(_MAX_REFERENCE_PAGES):
1000
+ traced = context.handle(trace_request)
1001
+ trace_items.extend(_mapping_items(traced.result))
1002
+ remaining_cursor = traced.page.next_cursor
1003
+ if not remaining_cursor:
1004
+ break
1005
+ trace_request = replace(trace_request, cursor=remaining_cursor)
1006
+
1007
+ metadata = next(
1008
+ (
1009
+ item
1010
+ for item in trace_items
1011
+ if item.get("item_type") == "relation_metadata"
1012
+ ),
1013
+ None,
1014
+ )
1015
+ reference_items = [
1016
+ _reference_view_item(target, item)
1017
+ for item in trace_items
1018
+ if item.get("item_type") == "relation"
1019
+ ]
1020
+ payload["items"] = _dedupe_reference_view_items(reference_items)
1021
+ if metadata is None:
1022
+ payload["items_truncated"] = True
1023
+ payload["message"] = "Reference metadata was truncated by the bounded trace."
1024
+ return payload
1025
+
1026
+ payload["completeness"] = metadata.get("completeness", "sound_partial")
1027
+ payload["scan_complete"] = metadata.get("scan_complete") is True
1028
+ payload["items_truncated"] = (
1029
+ bool(remaining_cursor) or metadata.get("scan_complete") is not True
1030
+ )
1031
+ for key in (
1032
+ "unresolved_references",
1033
+ "ambiguous_references",
1034
+ "deep_problem_count",
1035
+ "problems",
1036
+ "problems_truncated",
1037
+ ):
1038
+ if key in metadata:
1039
+ payload[key] = metadata[key]
1040
+ limitations = metadata.get("limitations")
1041
+ if isinstance(limitations, list):
1042
+ payload["limitations"] = limitations
1043
+ return payload
1044
+
1045
+
1046
+ def _mapping_items(value: object) -> list[dict[str, object]]:
1047
+ if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
1048
+ return []
1049
+ return [dict(item) for item in value if isinstance(item, Mapping)]
1050
+
1051
+
1052
+ def _logical_reference_targets(
1053
+ targets: Iterable[dict[str, object]],
1054
+ ) -> list[dict[str, object]]:
1055
+ logical: dict[tuple[object, object, object], dict[str, object]] = {}
1056
+ for target in targets:
1057
+ key = (
1058
+ target.get("path"),
1059
+ target.get("qualified_name"),
1060
+ target.get("kind"),
1061
+ )
1062
+ logical.setdefault(key, target)
1063
+ return sorted(
1064
+ logical.values(),
1065
+ key=lambda target: (
1066
+ str(target.get("qualified_name", "")).casefold(),
1067
+ str(target.get("path", "")).casefold(),
1068
+ _integer(target.get("line")),
1069
+ ),
1070
+ )
1071
+
1072
+
1073
+ def _reference_view_item(
1074
+ target: Mapping[str, object],
1075
+ relation: Mapping[str, object],
1076
+ ) -> dict[str, object]:
1077
+ evidence = relation.get("evidence")
1078
+ referencing_symbol = relation.get("referencing_symbol")
1079
+ evidence_item = dict(evidence) if isinstance(evidence, Mapping) else {}
1080
+ symbol_item = (
1081
+ dict(referencing_symbol)
1082
+ if isinstance(referencing_symbol, Mapping)
1083
+ else {}
1084
+ )
1085
+ return {
1086
+ "name": target.get("name", ""),
1087
+ "target_id": target.get("target_id", ""),
1088
+ "path": evidence_item.get("path", ""),
1089
+ "line": evidence_item.get("line", 0),
1090
+ "column": evidence_item.get("column", 0),
1091
+ "kind": evidence_item.get("kind", ""),
1092
+ "referencing_symbol": symbol_item,
1093
+ }
1094
+
1095
+
1096
+ def _dedupe_reference_view_items(
1097
+ items: Iterable[dict[str, object]],
1098
+ ) -> list[dict[str, object]]:
1099
+ groups: dict[tuple[object, ...], list[dict[str, object]]] = {}
1100
+ for item in items:
1101
+ key = (
1102
+ item["target_id"],
1103
+ item["path"],
1104
+ item["line"],
1105
+ item["column"],
1106
+ item["kind"],
1107
+ )
1108
+ groups.setdefault(key, []).append(item)
1109
+ unique: list[dict[str, object]] = []
1110
+ seen_exact: set[str] = set()
1111
+ for group in groups.values():
1112
+ rich = [
1113
+ item
1114
+ for item in group
1115
+ if isinstance(item.get("referencing_symbol"), dict)
1116
+ and bool(item["referencing_symbol"])
1117
+ ]
1118
+ for item in rich or group[:1]:
1119
+ exact_key = json.dumps(
1120
+ item,
1121
+ ensure_ascii=False,
1122
+ sort_keys=True,
1123
+ separators=(",", ":"),
1124
+ )
1125
+ if exact_key in seen_exact:
1126
+ continue
1127
+ seen_exact.add(exact_key)
1128
+ unique.append(item)
1129
+ return sorted(
1130
+ unique,
1131
+ key=lambda item: (
1132
+ str(item["path"]).casefold(),
1133
+ _integer(item["line"]),
1134
+ _integer(item["column"]),
1135
+ str(item["kind"]),
1136
+ str(item["target_id"]),
1137
+ json.dumps(
1138
+ item,
1139
+ ensure_ascii=False,
1140
+ sort_keys=True,
1141
+ separators=(",", ":"),
1142
+ ),
1143
+ ),
1144
+ )
1145
+
1146
+
1147
+ def _integer(value: object) -> int:
1148
+ return value if isinstance(value, int) else 0
1149
+
1150
+
1151
+ def _problems_payload(index: CodebaseIndex) -> dict[str, Any]:
1152
+ items = [_problem_item(problem) for problem in index.discovery.problems]
1153
+ for project, result in index.project_results.items():
1154
+ for problem in result.problems:
1155
+ items.append(
1156
+ {
1157
+ "kind": problem.problem_type.value,
1158
+ "origin": project,
1159
+ "message": problem.description,
1160
+ "file": problem.file_name,
1161
+ }
1162
+ )
1163
+ return {
1164
+ "layer": "problems",
1165
+ "root": index.root,
1166
+ "items": items,
1167
+ "deep_indexed": bool(index.project_results),
1168
+ }
1169
+
1170
+
1171
+ def _metrics_payload(index: CodebaseIndex, *, query: str) -> dict[str, Any]:
1172
+ return _metrics_payload_from_discovery(
1173
+ index.root,
1174
+ index.discovery,
1175
+ query=query,
1176
+ )
1177
+
1178
+
1179
+ def build_metrics_payload(
1180
+ root: str | Path,
1181
+ *,
1182
+ query: str = "",
1183
+ project_file: str | Path | None = None,
1184
+ workers: int = 0,
1185
+ discovery: DelphiProjectDiscovery | None = None,
1186
+ ) -> dict[str, Any]:
1187
+ root_path = Path(root).expanduser().resolve()
1188
+ if discovery is None:
1189
+ discovery = discover_delphi_project(
1190
+ root_path,
1191
+ project_file=project_file,
1192
+ scan_workspace_sources=True,
1193
+ )
1194
+ return _metrics_payload_from_discovery(
1195
+ str(root_path),
1196
+ discovery,
1197
+ query=query,
1198
+ workers=workers,
1199
+ )
1200
+
1201
+
1202
+ def _metrics_payload_from_discovery(
1203
+ root: str,
1204
+ discovery: DelphiProjectDiscovery,
1205
+ *,
1206
+ query: str,
1207
+ workers: int = 0,
1208
+ ) -> dict[str, Any]:
1209
+ project_name = "Workspace"
1210
+ if len(discovery.project_files) == 1:
1211
+ project_name = Path(discovery.project_files[0]).stem
1212
+ metrics = build_path_metrics(
1213
+ root,
1214
+ discovery.source_files,
1215
+ defines=discovery.defines,
1216
+ include_paths=discovery.include_paths,
1217
+ project_config=discovery.project_config,
1218
+ project_name=project_name,
1219
+ workers=workers,
1220
+ )
1221
+ needle = query.casefold().strip()
1222
+ units = [
1223
+ unit.to_mapping(detail=True)
1224
+ for unit in metrics.file_metrics
1225
+ if not needle or needle in unit.name.casefold() or needle in unit.path.casefold()
1226
+ ]
1227
+ return {
1228
+ "layer": "metrics",
1229
+ "root": root,
1230
+ "project": metrics.to_mapping(),
1231
+ "items": units,
1232
+ }
1233
+
1234
+
1235
+ def _all_symbols(index: CodebaseIndex) -> Iterable[Symbol]:
1236
+ for model in index.models.values():
1237
+ yield from _iter_symbols(model.unit_scope)
1238
+
1239
+
1240
+ def _iter_symbols(scope: Scope, *, include_unit: bool = True, seen: set[int] | None = None) -> Iterable[Symbol]:
1241
+ if seen is None:
1242
+ seen = set()
1243
+ scope_id = id(scope)
1244
+ if scope_id in seen:
1245
+ return
1246
+ seen.add(scope_id)
1247
+ for symbols in scope.symbols.values():
1248
+ for symbol in symbols:
1249
+ if include_unit or symbol.kind.value != "unit":
1250
+ yield symbol
1251
+ if symbol.member_scope is not None:
1252
+ yield from _iter_symbols(symbol.member_scope, include_unit=include_unit, seen=seen)
1253
+
1254
+
1255
+ def _symbol_item(symbol: Symbol) -> dict[str, Any]:
1256
+ return {
1257
+ "name": symbol.name,
1258
+ "kind": symbol.kind.value,
1259
+ "path": symbol.decl_range.file_name,
1260
+ "line": symbol.decl_range.start_line,
1261
+ "column": symbol.decl_range.start_col,
1262
+ "range": _range_item(symbol.decl_range),
1263
+ "visibility": symbol.visibility.value,
1264
+ "type": symbol.type_ref.display_name(),
1265
+ "owner": symbol.scope.owner.name if symbol.scope.owner is not None else symbol.scope.name,
1266
+ }
1267
+
1268
+
1269
+ _ROUTINE_KINDS = {
1270
+ SymbolKind.PROCEDURE,
1271
+ SymbolKind.FUNCTION,
1272
+ SymbolKind.CONSTRUCTOR,
1273
+ SymbolKind.DESTRUCTOR,
1274
+ }
1275
+
1276
+
1277
+ def _range_item(source_range: SourceRange) -> dict[str, int | str]:
1278
+ return {
1279
+ "path": source_range.file_name,
1280
+ "start_line": source_range.start_line,
1281
+ "start_col": source_range.start_col,
1282
+ "end_line": source_range.end_line,
1283
+ "end_col": source_range.end_col,
1284
+ }
1285
+
1286
+
1287
+ def _problem_item(problem: Any) -> dict[str, str]:
1288
+ return {
1289
+ "kind": problem.kind,
1290
+ "origin": problem.origin,
1291
+ "message": problem.message,
1292
+ }
1293
+
1294
+
1295
+ def _render_markdown(payload: dict[str, Any]) -> str:
1296
+ lines = [f"# Delphi Codebase Layer: {payload['layer']}", ""]
1297
+ if payload["layer"] == "overview":
1298
+ lines.extend(
1299
+ [
1300
+ f"- Root: `{payload['root']}`",
1301
+ f"- Projects: {payload['project_count']}",
1302
+ f"- Sources: {payload['source_count']}",
1303
+ f"- Units: {payload['unit_count']}",
1304
+ f"- Defines: {', '.join(payload['defines']) or '(none)'}",
1305
+ "",
1306
+ "## Search Paths",
1307
+ ]
1308
+ )
1309
+ lines.extend(f"- `{path}`" for path in payload["search_paths"])
1310
+ lines.append("")
1311
+ lines.append("## Include Paths")
1312
+ lines.extend(f"- `{path}`" for path in payload["include_paths"])
1313
+ elif payload["layer"] in {"units", "symbols"}:
1314
+ for item in payload["items"]:
1315
+ lines.append(f"- `{item['name']}` {item.get('kind', 'unit')} at `{item['path']}:{item.get('line', 1)}`")
1316
+ elif payload["layer"] == "references":
1317
+ if payload.get("message"):
1318
+ lines.append(str(payload["message"]))
1319
+ for item in payload["items"]:
1320
+ referencing_symbol = item.get("referencing_symbol", {})
1321
+ owner = (
1322
+ referencing_symbol.get("qualified_name", "")
1323
+ if isinstance(referencing_symbol, dict)
1324
+ else ""
1325
+ )
1326
+ lines.append(
1327
+ f"- `{item['name']}` {item['kind']} at "
1328
+ f"`{item['path']}:{item['line']}:{item['column']}`"
1329
+ + (f" from `{owner}`" if owner else "")
1330
+ )
1331
+ lines.extend(
1332
+ [
1333
+ "",
1334
+ f"- Completeness: `{payload['completeness']}`",
1335
+ f"- Scan complete: `{str(payload['scan_complete']).lower()}`",
1336
+ f"- Items truncated: `{str(payload['items_truncated']).lower()}`",
1337
+ ]
1338
+ )
1339
+ elif payload["layer"] in {"unit", "symbol"}:
1340
+ for item in payload["items"]:
1341
+ lines.append(f"## {item['name']}")
1342
+ lines.append(f"- Path: `{item['path']}`")
1343
+ for symbol in item.get("symbols", item.get("children", [])):
1344
+ lines.append(
1345
+ f"- `{symbol['name']}` {symbol['kind']} at `{symbol['path']}:{symbol['line']}`"
1346
+ )
1347
+ elif payload["layer"] == "implementation":
1348
+ if payload.get("message"):
1349
+ lines.append(payload["message"])
1350
+ for item in payload["items"]:
1351
+ lines.append(f"## {item['name']}")
1352
+ lines.append(f"- Kind: `{item['kind']}`")
1353
+ lines.append(f"- Path: `{item['path']}`")
1354
+ for fragment in item["fragments"]:
1355
+ source_range = fragment["range"]
1356
+ lines.append("")
1357
+ lines.append(
1358
+ f"### {fragment['fragment_kind']} `{source_range['path']}:{source_range['start_line']}`"
1359
+ )
1360
+ if fragment.get("symbol"):
1361
+ lines.append(f"- Symbol: `{fragment['symbol']}`")
1362
+ lines.append(f"- Lines: {fragment['line_count']}")
1363
+ lines.append("")
1364
+ lines.append("```pascal")
1365
+ lines.append(fragment["text"])
1366
+ lines.append("```")
1367
+ elif payload["layer"] == "projects":
1368
+ for item in payload["items"]:
1369
+ lines.append(f"## `{item['path']}`")
1370
+ for unit in item["parsed_units"]:
1371
+ lines.append(f"- Unit `{unit['name']}` at `{unit['path']}`")
1372
+ elif payload["layer"] == "problems":
1373
+ for item in payload["items"]:
1374
+ lines.append(f"- {item['kind']}: {item['message']} (`{item.get('origin', '')}`)")
1375
+ elif payload["layer"] == "metrics":
1376
+ project = payload["project"]
1377
+ lines.extend(
1378
+ [
1379
+ f"- Project LOC: {project['total_loc']}",
1380
+ f"- Project LOC with includes: {project['total_loc_with_includes']}",
1381
+ f"- Units: {project['unit_count']}",
1382
+ f"- Maintainability index: {project['maintainability_index']:.2f}",
1383
+ ]
1384
+ )
1385
+ for item in payload["items"]:
1386
+ lines.extend(
1387
+ [
1388
+ "",
1389
+ f"## {item['name']}",
1390
+ f"- Path: `{item['path']}`",
1391
+ f"- LOC: {item['lines']['total_lines']}",
1392
+ f"- Cyclomatic maximum: {item['cyclomatic']['maximum']}",
1393
+ f"- Maintainability index: {item['maintainability_index']:.2f}",
1394
+ f"- Instability: {item['instability']:.3f}",
1395
+ f"- Abstractness: {item['abstractness']:.3f}",
1396
+ f"- Distance: {item['distance']:.3f}",
1397
+ ]
1398
+ )
1399
+ return "\n".join(lines).rstrip() + "\n"
1400
+
1401
+
1402
+ __all__ = [
1403
+ "CodebaseIndex",
1404
+ "build_codebase_index",
1405
+ "build_metrics_payload",
1406
+ "build_references_payload",
1407
+ "layer_payload",
1408
+ "render_layer",
1409
+ "render_payload",
1410
+ ]