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.
- delphi_lsp/__init__.py +184 -0
- delphi_lsp/_version.py +1 -0
- delphi_lsp/agent_cache.py +1574 -0
- delphi_lsp/agent_cli.py +902 -0
- delphi_lsp/agent_context.py +3907 -0
- delphi_lsp/agent_cpg.py +349 -0
- delphi_lsp/agent_cpg_builder.py +666 -0
- delphi_lsp/agent_layers.py +1410 -0
- delphi_lsp/agent_metrics.py +526 -0
- delphi_lsp/agent_protocol.py +445 -0
- delphi_lsp/agent_relations.py +1881 -0
- delphi_lsp/agent_templates.py +614 -0
- delphi_lsp/agent_wiki.py +2853 -0
- delphi_lsp/agent_workspace.py +860 -0
- delphi_lsp/ast_serialize.py +126 -0
- delphi_lsp/binary.py +250 -0
- delphi_lsp/comment_builder.py +28 -0
- delphi_lsp/consts.py +345 -0
- delphi_lsp/delphi_lsp_native.pyd +0 -0
- delphi_lsp/delphiast_lexer.py +399 -0
- delphi_lsp/delphiast_parser.py +2189 -0
- delphi_lsp/delphiast_tokens.py +41 -0
- delphi_lsp/grammar.py +459 -0
- delphi_lsp/incremental.py +1139 -0
- delphi_lsp/lark_builder.py +2684 -0
- delphi_lsp/lark_tokens.py +236 -0
- delphi_lsp/lexical_scanner.py +30 -0
- delphi_lsp/lsp_server.py +6698 -0
- delphi_lsp/metrics.py +1452 -0
- delphi_lsp/native_bridge.py +300 -0
- delphi_lsp/navigation_cache.py +189 -0
- delphi_lsp/nodes.py +462 -0
- delphi_lsp/parallel_outline.py +662 -0
- delphi_lsp/parse_artifacts.py +969 -0
- delphi_lsp/parser.py +342 -0
- delphi_lsp/parser_backend.py +139 -0
- delphi_lsp/preprocessor.py +1122 -0
- delphi_lsp/progress.py +26 -0
- delphi_lsp/project_config.py +285 -0
- delphi_lsp/project_discovery.py +932 -0
- delphi_lsp/project_indexer.py +470 -0
- delphi_lsp/py.typed +1 -0
- delphi_lsp/response_cache.py +465 -0
- delphi_lsp/semantic.py +394 -0
- delphi_lsp/semantic_builder.py +2302 -0
- delphi_lsp/source_reader.py +17 -0
- delphi_lsp/wiki_external_sort.py +392 -0
- delphi_lsp/wiki_memory.py +548 -0
- delphi_lsp/workspace.py +83 -0
- delphi_lsp/writer.py +73 -0
- python_delphi_lsp-3.6.0.dist-info/METADATA +766 -0
- python_delphi_lsp-3.6.0.dist-info/RECORD +56 -0
- python_delphi_lsp-3.6.0.dist-info/WHEEL +4 -0
- python_delphi_lsp-3.6.0.dist-info/entry_points.txt +3 -0
- python_delphi_lsp-3.6.0.dist-info/licenses/LICENSE +373 -0
- python_delphi_lsp-3.6.0.dist-info/sboms/python-delphi-lsp.cyclonedx.json +770 -0
delphi_lsp/agent_wiki.py
ADDED
|
@@ -0,0 +1,2853 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import gc
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import shutil
|
|
9
|
+
import sqlite3
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
12
|
+
import unicodedata
|
|
13
|
+
from collections import OrderedDict
|
|
14
|
+
from collections.abc import Callable, Iterable, Iterator, Mapping
|
|
15
|
+
from contextlib import contextmanager
|
|
16
|
+
from dataclasses import asdict, dataclass
|
|
17
|
+
from functools import lru_cache
|
|
18
|
+
from itertools import chain
|
|
19
|
+
from pathlib import Path, PurePosixPath
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from ._version import __version__
|
|
23
|
+
from .agent_layers import (
|
|
24
|
+
CodebaseIndex,
|
|
25
|
+
_all_symbols,
|
|
26
|
+
_source_fragment,
|
|
27
|
+
build_codebase_index,
|
|
28
|
+
layer_payload,
|
|
29
|
+
)
|
|
30
|
+
from .agent_metrics import build_path_metrics
|
|
31
|
+
from .agent_protocol import (
|
|
32
|
+
SCHEMA_VERSION,
|
|
33
|
+
SUPPORTED_ACTIONS,
|
|
34
|
+
SUPPORTED_DETAILS,
|
|
35
|
+
SUPPORTED_DIRECTIONS,
|
|
36
|
+
SUPPORTED_GRAPHS,
|
|
37
|
+
SUPPORTED_RELATIONS,
|
|
38
|
+
)
|
|
39
|
+
from .parallel_outline import OutlineResult, ParallelBuildStats
|
|
40
|
+
from .parse_artifacts import semantic_model_from_mapping, semantic_model_to_mapping
|
|
41
|
+
from .parser import DelphiParser
|
|
42
|
+
from .parser_backend import ParserMode
|
|
43
|
+
from .project_config import workspace_include_loader
|
|
44
|
+
from .project_discovery import DelphiProjectDiscovery
|
|
45
|
+
from .project_indexer import ProjectIndexResult
|
|
46
|
+
from .semantic import Scope, Symbol, SymbolIndex, SymbolReference, normalize_name
|
|
47
|
+
from .semantic_builder import SemanticModel
|
|
48
|
+
from .source_reader import read_source_text
|
|
49
|
+
from .wiki_external_sort import sort_jsonl_records
|
|
50
|
+
from .wiki_memory import (
|
|
51
|
+
MemoryStage,
|
|
52
|
+
MemoryTransition,
|
|
53
|
+
WikiMemoryGuard,
|
|
54
|
+
WikiMemoryLimitExceeded,
|
|
55
|
+
resolve_memory_limit_bytes,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
OKF_VERSION = "0.2"
|
|
59
|
+
_WIKI_ARTIFACT_RECURSION_LIMIT = 50_000
|
|
60
|
+
_STREAMING_UNIT_GC_INTERVAL = 16
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@contextmanager
|
|
64
|
+
def _expanded_recursion_limit() -> Iterator[None]:
|
|
65
|
+
previous = sys.getrecursionlimit()
|
|
66
|
+
if previous < _WIKI_ARTIFACT_RECURSION_LIMIT:
|
|
67
|
+
sys.setrecursionlimit(_WIKI_ARTIFACT_RECURSION_LIMIT)
|
|
68
|
+
try:
|
|
69
|
+
yield
|
|
70
|
+
finally:
|
|
71
|
+
if previous < _WIKI_ARTIFACT_RECURSION_LIMIT:
|
|
72
|
+
sys.setrecursionlimit(previous)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class WikiExportError(RuntimeError):
|
|
76
|
+
"""Raised when an OKF bundle cannot be created safely."""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class WikiExportResult:
|
|
81
|
+
output: str
|
|
82
|
+
documents: int
|
|
83
|
+
projects: int
|
|
84
|
+
units: int
|
|
85
|
+
symbols: int
|
|
86
|
+
peak_process_tree_rss_bytes: int
|
|
87
|
+
phase_peak_process_tree_rss_bytes: dict[str, int]
|
|
88
|
+
|
|
89
|
+
def to_mapping(self) -> dict[str, Any]:
|
|
90
|
+
return {
|
|
91
|
+
"format": "okf",
|
|
92
|
+
"okf_version": OKF_VERSION,
|
|
93
|
+
**asdict(self),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(frozen=True, slots=True)
|
|
98
|
+
class WikiProgressEvent:
|
|
99
|
+
phase: str
|
|
100
|
+
completed: int
|
|
101
|
+
total: int | None
|
|
102
|
+
path: str
|
|
103
|
+
detail: str
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
WikiProgressCallback = Callable[[WikiProgressEvent], None]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True, slots=True)
|
|
110
|
+
class _UnitRecord:
|
|
111
|
+
source_path: str
|
|
112
|
+
name: str
|
|
113
|
+
page: PurePosixPath
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _iter_symbols(
|
|
117
|
+
scope: Scope,
|
|
118
|
+
*,
|
|
119
|
+
include_unit: bool = True,
|
|
120
|
+
seen: set[int] | None = None,
|
|
121
|
+
) -> Iterator[Symbol]:
|
|
122
|
+
"""Depth-first symbol traversal without Python call-stack growth."""
|
|
123
|
+
visited = set() if seen is None else seen
|
|
124
|
+
pending: list[Iterator[Symbol]] = []
|
|
125
|
+
|
|
126
|
+
def enter(candidate: Scope) -> bool:
|
|
127
|
+
scope_id = id(candidate)
|
|
128
|
+
if scope_id in visited:
|
|
129
|
+
return False
|
|
130
|
+
visited.add(scope_id)
|
|
131
|
+
pending.append(
|
|
132
|
+
iter(
|
|
133
|
+
symbol
|
|
134
|
+
for symbols in candidate.symbols.values()
|
|
135
|
+
for symbol in symbols
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
enter(scope)
|
|
141
|
+
while pending:
|
|
142
|
+
try:
|
|
143
|
+
symbol = next(pending[-1])
|
|
144
|
+
except StopIteration:
|
|
145
|
+
pending.pop()
|
|
146
|
+
continue
|
|
147
|
+
if include_unit or symbol.kind.value != "unit":
|
|
148
|
+
yield symbol
|
|
149
|
+
if symbol.member_scope is not None:
|
|
150
|
+
enter(symbol.member_scope)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass(frozen=True, slots=True)
|
|
154
|
+
class _StoredModelRecord:
|
|
155
|
+
source_path: str
|
|
156
|
+
name: str
|
|
157
|
+
artifact_path: Path
|
|
158
|
+
symbol_count: int
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass(slots=True)
|
|
162
|
+
class _WikiExecutionStrategy:
|
|
163
|
+
workers: int
|
|
164
|
+
outline_worker_limit: int = 2
|
|
165
|
+
sort_records_per_run: int = 50_000
|
|
166
|
+
sort_bytes_per_run: int = 8 * 1024 * 1024
|
|
167
|
+
|
|
168
|
+
def reduce_workers(self) -> None:
|
|
169
|
+
effective = self.workers if self.workers > 0 else min(8, os.cpu_count() or 1)
|
|
170
|
+
self.workers = max(1, effective // 2)
|
|
171
|
+
self.outline_worker_limit = max(1, self.outline_worker_limit // 2)
|
|
172
|
+
|
|
173
|
+
def sequentialize(self) -> None:
|
|
174
|
+
self.workers = 1
|
|
175
|
+
self.outline_worker_limit = 1
|
|
176
|
+
self.sort_records_per_run = min(self.sort_records_per_run, 5_000)
|
|
177
|
+
self.sort_bytes_per_run = min(self.sort_bytes_per_run, 8 * 1024 * 1024)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class _StoredWikiModels(Mapping[str, SemanticModel]):
|
|
181
|
+
"""Disk-backed outline models with a small, explicitly clearable LRU."""
|
|
182
|
+
|
|
183
|
+
def __init__(
|
|
184
|
+
self,
|
|
185
|
+
workspace: Path,
|
|
186
|
+
*,
|
|
187
|
+
max_cached_models: int = 128,
|
|
188
|
+
max_cached_model_bytes: int = 0,
|
|
189
|
+
) -> None:
|
|
190
|
+
if max_cached_models < 0:
|
|
191
|
+
raise ValueError("max_cached_models must not be negative")
|
|
192
|
+
if max_cached_model_bytes < 0:
|
|
193
|
+
raise ValueError("max_cached_model_bytes must not be negative")
|
|
194
|
+
self.workspace = workspace
|
|
195
|
+
self.model_directory = workspace / "models"
|
|
196
|
+
self.model_directory.mkdir(parents=True, exist_ok=True)
|
|
197
|
+
self.catalog_path = workspace / "symbols-raw.jsonl"
|
|
198
|
+
self._records: dict[str, _StoredModelRecord] = {}
|
|
199
|
+
self._ordered_sources: list[str] = []
|
|
200
|
+
self._source_by_unit: dict[str, str] = {}
|
|
201
|
+
self._max_cached_models = max_cached_models
|
|
202
|
+
self._max_cached_model_bytes = max_cached_model_bytes
|
|
203
|
+
self._cache: OrderedDict[str, SemanticModel] = OrderedDict()
|
|
204
|
+
self._cache_weights: dict[str, int] = {}
|
|
205
|
+
self._cached_model_bytes = 0
|
|
206
|
+
|
|
207
|
+
def store(self, result: OutlineResult) -> None:
|
|
208
|
+
if result.read_error or result.model is None:
|
|
209
|
+
return
|
|
210
|
+
digest = hashlib.sha256(result.source_path.encode("utf-8")).hexdigest()
|
|
211
|
+
artifact_path = self.model_directory / f"{digest}.json"
|
|
212
|
+
with (
|
|
213
|
+
_expanded_recursion_limit(),
|
|
214
|
+
artifact_path.open("w", encoding="utf-8", newline="\n") as stream,
|
|
215
|
+
):
|
|
216
|
+
json.dump(
|
|
217
|
+
semantic_model_to_mapping(result.model),
|
|
218
|
+
stream,
|
|
219
|
+
ensure_ascii=False,
|
|
220
|
+
separators=(",", ":"),
|
|
221
|
+
)
|
|
222
|
+
stream.write("\n")
|
|
223
|
+
symbol_count = 0
|
|
224
|
+
with self.catalog_path.open("a", encoding="utf-8", newline="\n") as catalog:
|
|
225
|
+
for symbol in _iter_symbols(result.model.unit_scope, include_unit=False):
|
|
226
|
+
symbol_count += 1
|
|
227
|
+
identity = _symbol_identity(symbol)
|
|
228
|
+
catalog.write(
|
|
229
|
+
json.dumps(
|
|
230
|
+
{
|
|
231
|
+
"identity": identity,
|
|
232
|
+
"kind": symbol.kind.value,
|
|
233
|
+
"model_source": result.source_path,
|
|
234
|
+
"name": symbol.name,
|
|
235
|
+
"name_key": symbol.name.casefold(),
|
|
236
|
+
"page": _symbol_page(symbol).as_posix(),
|
|
237
|
+
"source_key": _normalized_source(symbol.decl_range.file_name),
|
|
238
|
+
"start_col": symbol.decl_range.start_col,
|
|
239
|
+
"start_line": symbol.decl_range.start_line,
|
|
240
|
+
},
|
|
241
|
+
ensure_ascii=False,
|
|
242
|
+
separators=(",", ":"),
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
catalog.write("\n")
|
|
246
|
+
self._records[result.source_path] = _StoredModelRecord(
|
|
247
|
+
result.source_path,
|
|
248
|
+
result.model.unit_scope.name,
|
|
249
|
+
artifact_path,
|
|
250
|
+
symbol_count,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
def finalize(self, source_order: Iterable[str]) -> None:
|
|
254
|
+
self._ordered_sources = [source for source in source_order if source in self._records]
|
|
255
|
+
self._source_by_unit = {}
|
|
256
|
+
for source in self._ordered_sources:
|
|
257
|
+
self._source_by_unit[self._records[source].name.casefold()] = source
|
|
258
|
+
self.catalog_path.touch(exist_ok=True)
|
|
259
|
+
|
|
260
|
+
def source_for_unit(self, name: str) -> str | None:
|
|
261
|
+
return self._source_by_unit.get(name.casefold())
|
|
262
|
+
|
|
263
|
+
def unit_records(self) -> list[_UnitRecord]:
|
|
264
|
+
records = [
|
|
265
|
+
_UnitRecord(
|
|
266
|
+
source,
|
|
267
|
+
self._records[source].name,
|
|
268
|
+
PurePosixPath("units")
|
|
269
|
+
/ _document_name(
|
|
270
|
+
self._records[source].name,
|
|
271
|
+
_normalized_source(source),
|
|
272
|
+
),
|
|
273
|
+
)
|
|
274
|
+
for source in self._ordered_sources
|
|
275
|
+
]
|
|
276
|
+
return sorted(
|
|
277
|
+
records,
|
|
278
|
+
key=lambda record: (record.name.casefold(), record.source_path.casefold()),
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
def __getitem__(self, source_path: str) -> SemanticModel:
|
|
282
|
+
cached = self._cache.get(source_path)
|
|
283
|
+
if cached is not None:
|
|
284
|
+
self._cache.move_to_end(source_path)
|
|
285
|
+
cached.index = SymbolIndex()
|
|
286
|
+
return cached
|
|
287
|
+
record = self._records[source_path]
|
|
288
|
+
with _expanded_recursion_limit():
|
|
289
|
+
with record.artifact_path.open("r", encoding="utf-8") as stream:
|
|
290
|
+
payload = json.load(stream)
|
|
291
|
+
if not isinstance(payload, dict):
|
|
292
|
+
raise WikiExportError(f"Invalid stored outline model: {source_path}")
|
|
293
|
+
model = semantic_model_from_mapping(payload)
|
|
294
|
+
model.index = SymbolIndex()
|
|
295
|
+
weight = record.artifact_path.stat().st_size
|
|
296
|
+
if (
|
|
297
|
+
self._max_cached_models
|
|
298
|
+
and weight <= self._max_cached_model_bytes
|
|
299
|
+
):
|
|
300
|
+
self._cache[source_path] = model
|
|
301
|
+
self._cache_weights[source_path] = weight
|
|
302
|
+
self._cached_model_bytes += weight
|
|
303
|
+
self._cache.move_to_end(source_path)
|
|
304
|
+
while (
|
|
305
|
+
len(self._cache) > self._max_cached_models
|
|
306
|
+
or self._cached_model_bytes > self._max_cached_model_bytes
|
|
307
|
+
):
|
|
308
|
+
expired, _model = self._cache.popitem(last=False)
|
|
309
|
+
self._cached_model_bytes -= self._cache_weights.pop(expired)
|
|
310
|
+
return model
|
|
311
|
+
|
|
312
|
+
def clear_cache(self) -> None:
|
|
313
|
+
self._cache.clear()
|
|
314
|
+
self._cache_weights.clear()
|
|
315
|
+
self._cached_model_bytes = 0
|
|
316
|
+
|
|
317
|
+
def __iter__(self) -> Iterator[str]:
|
|
318
|
+
return iter(self._ordered_sources)
|
|
319
|
+
|
|
320
|
+
def __len__(self) -> int:
|
|
321
|
+
return len(self._ordered_sources)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class _LazyWikiSymbolIndex(SymbolIndex):
|
|
325
|
+
"""Load only units actually imported by the unit currently being parsed."""
|
|
326
|
+
|
|
327
|
+
def __init__(self, models: _StoredWikiModels) -> None:
|
|
328
|
+
super().__init__()
|
|
329
|
+
self.models = models
|
|
330
|
+
|
|
331
|
+
def register_unit(
|
|
332
|
+
self,
|
|
333
|
+
unit_name: str,
|
|
334
|
+
unit_scope: Scope,
|
|
335
|
+
*,
|
|
336
|
+
index_symbols: bool = True,
|
|
337
|
+
) -> None:
|
|
338
|
+
del index_symbols
|
|
339
|
+
super().register_unit(unit_name, unit_scope, index_symbols=False)
|
|
340
|
+
|
|
341
|
+
def lookup_unit(self, unit_name: str): # type: ignore[no-untyped-def]
|
|
342
|
+
normalized = normalize_name(unit_name)
|
|
343
|
+
existing = self.units.get(normalized)
|
|
344
|
+
if existing is not None:
|
|
345
|
+
return existing
|
|
346
|
+
source = self.models.source_for_unit(unit_name)
|
|
347
|
+
if source is None:
|
|
348
|
+
return None
|
|
349
|
+
model = self.models[source]
|
|
350
|
+
self.register_unit(model.unit_scope.name, model.unit_scope)
|
|
351
|
+
return self.units.get(normalized)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class _WikiProjectStore:
|
|
355
|
+
def __init__(self, path: Path) -> None:
|
|
356
|
+
self.path = path
|
|
357
|
+
self.count = 0
|
|
358
|
+
|
|
359
|
+
def append(self, project: str, result: ProjectIndexResult) -> None:
|
|
360
|
+
item = _project_item(project, result)
|
|
361
|
+
with self.path.open("a", encoding="utf-8", newline="\n") as stream:
|
|
362
|
+
stream.write(json.dumps(item, ensure_ascii=False, separators=(",", ":")))
|
|
363
|
+
stream.write("\n")
|
|
364
|
+
self.count += 1
|
|
365
|
+
|
|
366
|
+
def items(self) -> Iterator[dict[str, Any]]:
|
|
367
|
+
if not self.path.is_file():
|
|
368
|
+
return
|
|
369
|
+
with self.path.open("r", encoding="utf-8") as stream:
|
|
370
|
+
for line in stream:
|
|
371
|
+
item = json.loads(line)
|
|
372
|
+
if isinstance(item, dict):
|
|
373
|
+
yield item
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class _ProjectMembershipStore:
|
|
377
|
+
def __init__(self, path: Path) -> None:
|
|
378
|
+
self.connection = sqlite3.connect(path)
|
|
379
|
+
self.connection.execute(
|
|
380
|
+
"CREATE TABLE membership (source TEXT, ordinal INTEGER, title TEXT, page TEXT, "
|
|
381
|
+
"PRIMARY KEY (source, title, page))"
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
def add(
|
|
385
|
+
self,
|
|
386
|
+
source: str,
|
|
387
|
+
ordinal: int,
|
|
388
|
+
title: str,
|
|
389
|
+
page: PurePosixPath,
|
|
390
|
+
) -> None:
|
|
391
|
+
self.connection.execute(
|
|
392
|
+
"INSERT OR IGNORE INTO membership VALUES (?, ?, ?, ?)",
|
|
393
|
+
(_normalized_source(source), ordinal, title, page.as_posix()),
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
def for_source(self, source: str) -> list[tuple[str, PurePosixPath]]:
|
|
397
|
+
rows = self.connection.execute(
|
|
398
|
+
"SELECT title, page FROM membership WHERE source = ? ORDER BY ordinal",
|
|
399
|
+
(_normalized_source(source),),
|
|
400
|
+
)
|
|
401
|
+
return [(str(title), PurePosixPath(str(page))) for title, page in rows]
|
|
402
|
+
|
|
403
|
+
def commit(self) -> None:
|
|
404
|
+
self.connection.commit()
|
|
405
|
+
|
|
406
|
+
def close(self) -> None:
|
|
407
|
+
self.connection.close()
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
class _BoundedSourceCache(OrderedDict[str, list[str]]):
|
|
411
|
+
"""Keep only a few decoded source files resident during page generation."""
|
|
412
|
+
|
|
413
|
+
def __init__(self, max_entries: int = 4) -> None:
|
|
414
|
+
super().__init__()
|
|
415
|
+
self.max_entries = max_entries
|
|
416
|
+
|
|
417
|
+
def get(self, key: str, default: list[str] | None = None) -> list[str] | None:
|
|
418
|
+
value = super().get(key, default)
|
|
419
|
+
if key in self:
|
|
420
|
+
self.move_to_end(key)
|
|
421
|
+
return value
|
|
422
|
+
|
|
423
|
+
def __setitem__(self, key: str, value: list[str]) -> None:
|
|
424
|
+
super().__setitem__(key, value)
|
|
425
|
+
self.move_to_end(key)
|
|
426
|
+
while len(self) > self.max_entries:
|
|
427
|
+
self.popitem(last=False)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def export_okf_wiki(
|
|
431
|
+
root: str | Path,
|
|
432
|
+
output: str | Path,
|
|
433
|
+
*,
|
|
434
|
+
project_file: str | Path | None = None,
|
|
435
|
+
workers: int = 0,
|
|
436
|
+
max_memory_bytes: int | None = None,
|
|
437
|
+
force: bool = False,
|
|
438
|
+
on_progress: WikiProgressCallback | None = None,
|
|
439
|
+
) -> WikiExportResult:
|
|
440
|
+
"""Export all unique layered codebase knowledge as an OKF 0.2 bundle."""
|
|
441
|
+
|
|
442
|
+
root_path = Path(root).expanduser().resolve()
|
|
443
|
+
requested_output, output_path = _resolve_output_destination(output)
|
|
444
|
+
_validate_paths(root_path, output_path, force=force)
|
|
445
|
+
|
|
446
|
+
def forward_index_progress(event: Any) -> None:
|
|
447
|
+
if event.phase in {"inventory", "detail"} or event.detail in {
|
|
448
|
+
"project index started",
|
|
449
|
+
"project index complete",
|
|
450
|
+
}:
|
|
451
|
+
guard.check(phase="project indexing")
|
|
452
|
+
if event.phase in {"inventory", "detail"}:
|
|
453
|
+
return
|
|
454
|
+
if event.detail in {"project index started", "project index complete"}:
|
|
455
|
+
return
|
|
456
|
+
phase = {
|
|
457
|
+
"outline": "index",
|
|
458
|
+
"relations": "relations",
|
|
459
|
+
"projects": "projects",
|
|
460
|
+
"complete": "index",
|
|
461
|
+
}.get(event.phase, "discovery")
|
|
462
|
+
_emit_wiki_progress(
|
|
463
|
+
on_progress,
|
|
464
|
+
phase,
|
|
465
|
+
event.files_completed,
|
|
466
|
+
event.files_total,
|
|
467
|
+
event.path,
|
|
468
|
+
event.detail,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
472
|
+
temporary = Path(
|
|
473
|
+
tempfile.mkdtemp(prefix=f".{output_path.name}.tmp-", dir=output_path.parent)
|
|
474
|
+
)
|
|
475
|
+
workspace = Path(
|
|
476
|
+
tempfile.mkdtemp(prefix=f".{output_path.name}.work-", dir=output_path.parent)
|
|
477
|
+
)
|
|
478
|
+
strategy = _WikiExecutionStrategy(workers)
|
|
479
|
+
|
|
480
|
+
def memory_transition(transition: MemoryTransition) -> None:
|
|
481
|
+
_emit_wiki_progress(
|
|
482
|
+
on_progress,
|
|
483
|
+
"memory",
|
|
484
|
+
(MemoryStage.COMPACT, MemoryStage.REDUCE_WORKERS, MemoryStage.SEQUENTIAL).index(
|
|
485
|
+
transition.stage
|
|
486
|
+
)
|
|
487
|
+
+ 1,
|
|
488
|
+
3,
|
|
489
|
+
transition.phase,
|
|
490
|
+
(
|
|
491
|
+
f"memory guard {transition.stage.value}: "
|
|
492
|
+
f"{transition.used_bytes} of {transition.limit_bytes} bytes"
|
|
493
|
+
),
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
models = _StoredWikiModels(workspace)
|
|
497
|
+
writer: _StreamingWikiWriter | None = None
|
|
498
|
+
|
|
499
|
+
def compact_memory() -> None:
|
|
500
|
+
models.clear_cache()
|
|
501
|
+
if writer is not None:
|
|
502
|
+
writer.clear_caches()
|
|
503
|
+
gc.collect()
|
|
504
|
+
|
|
505
|
+
try:
|
|
506
|
+
memory_limit = resolve_memory_limit_bytes(max_memory_bytes)
|
|
507
|
+
except ValueError as error:
|
|
508
|
+
shutil.rmtree(temporary, ignore_errors=True)
|
|
509
|
+
shutil.rmtree(workspace, ignore_errors=True)
|
|
510
|
+
raise WikiExportError(str(error)) from error
|
|
511
|
+
guard = WikiMemoryGuard(
|
|
512
|
+
limit_bytes=memory_limit,
|
|
513
|
+
compact=compact_memory,
|
|
514
|
+
reduce_workers=strategy.reduce_workers,
|
|
515
|
+
sequentialize=strategy.sequentialize,
|
|
516
|
+
on_transition=memory_transition,
|
|
517
|
+
)
|
|
518
|
+
projects = _WikiProjectStore(workspace / "projects.jsonl")
|
|
519
|
+
|
|
520
|
+
def store_outline(result: OutlineResult) -> None:
|
|
521
|
+
models.store(result)
|
|
522
|
+
guard.check(phase="unit outlines")
|
|
523
|
+
|
|
524
|
+
def store_project(project: str, result: ProjectIndexResult) -> None:
|
|
525
|
+
projects.append(project, result)
|
|
526
|
+
guard.check(phase="project indexing")
|
|
527
|
+
|
|
528
|
+
try:
|
|
529
|
+
_emit_wiki_progress(
|
|
530
|
+
on_progress,
|
|
531
|
+
"discovery",
|
|
532
|
+
0,
|
|
533
|
+
None,
|
|
534
|
+
str(root_path),
|
|
535
|
+
"discovering repository sources",
|
|
536
|
+
)
|
|
537
|
+
index = _build_wiki_index(
|
|
538
|
+
root_path,
|
|
539
|
+
project_file=project_file,
|
|
540
|
+
workers=strategy.workers,
|
|
541
|
+
memory_limit_bytes=memory_limit,
|
|
542
|
+
on_project_result=store_project,
|
|
543
|
+
on_outline_result=store_outline,
|
|
544
|
+
outline_heartbeat=lambda: guard.check(
|
|
545
|
+
phase="unit outlines",
|
|
546
|
+
),
|
|
547
|
+
outline_force_heartbeat=lambda: guard.check(
|
|
548
|
+
phase="unit outlines",
|
|
549
|
+
force_process_tree=True,
|
|
550
|
+
),
|
|
551
|
+
outline_active_worker_limit=lambda: strategy.outline_worker_limit,
|
|
552
|
+
on_progress=forward_index_progress,
|
|
553
|
+
)
|
|
554
|
+
guard.check(phase="project indexing", force_process_tree=True)
|
|
555
|
+
models.finalize(index.discovery.source_files)
|
|
556
|
+
guard.check(phase="unit outlines", force_process_tree=True)
|
|
557
|
+
index.models = models
|
|
558
|
+
unique_catalog, name_catalog, symbol_count = _prepare_symbol_catalog(
|
|
559
|
+
models,
|
|
560
|
+
workspace,
|
|
561
|
+
strategy,
|
|
562
|
+
guard,
|
|
563
|
+
)
|
|
564
|
+
guard.check(phase="external symbol sort", force_process_tree=True)
|
|
565
|
+
writer = _StreamingWikiWriter(
|
|
566
|
+
index,
|
|
567
|
+
temporary,
|
|
568
|
+
models=models,
|
|
569
|
+
projects=projects,
|
|
570
|
+
unique_catalog=unique_catalog,
|
|
571
|
+
name_catalog=name_catalog,
|
|
572
|
+
symbol_count=symbol_count,
|
|
573
|
+
workspace=workspace,
|
|
574
|
+
guard=guard,
|
|
575
|
+
strategy=strategy,
|
|
576
|
+
on_progress=on_progress,
|
|
577
|
+
)
|
|
578
|
+
writer.write()
|
|
579
|
+
documents = writer.documents
|
|
580
|
+
symbol_count = writer.symbol_count
|
|
581
|
+
guard.check(phase="finalization", force_process_tree=True)
|
|
582
|
+
guard.check(phase="finalization", force_process_tree=True)
|
|
583
|
+
_emit_wiki_progress(
|
|
584
|
+
on_progress,
|
|
585
|
+
"install",
|
|
586
|
+
0,
|
|
587
|
+
1,
|
|
588
|
+
str(output_path),
|
|
589
|
+
"installing completed bundle",
|
|
590
|
+
)
|
|
591
|
+
_install_completed_bundle(
|
|
592
|
+
temporary,
|
|
593
|
+
output_path,
|
|
594
|
+
requested_output=requested_output,
|
|
595
|
+
force=force,
|
|
596
|
+
)
|
|
597
|
+
except WikiMemoryLimitExceeded as error:
|
|
598
|
+
if temporary.exists():
|
|
599
|
+
shutil.rmtree(temporary, ignore_errors=True)
|
|
600
|
+
raise WikiExportError(
|
|
601
|
+
f"{error} Retry with a larger --max-memory value or fewer workers."
|
|
602
|
+
) from error
|
|
603
|
+
except BaseException:
|
|
604
|
+
if temporary.exists():
|
|
605
|
+
shutil.rmtree(temporary, ignore_errors=True)
|
|
606
|
+
raise
|
|
607
|
+
finally:
|
|
608
|
+
if writer is not None:
|
|
609
|
+
writer.close()
|
|
610
|
+
shutil.rmtree(workspace, ignore_errors=True)
|
|
611
|
+
|
|
612
|
+
result = WikiExportResult(
|
|
613
|
+
output=str(output_path),
|
|
614
|
+
documents=documents,
|
|
615
|
+
projects=len(index.discovery.project_files),
|
|
616
|
+
units=len(index.models),
|
|
617
|
+
symbols=symbol_count,
|
|
618
|
+
peak_process_tree_rss_bytes=guard.peak_bytes,
|
|
619
|
+
phase_peak_process_tree_rss_bytes=dict(sorted(guard.phase_peaks.items())),
|
|
620
|
+
)
|
|
621
|
+
_emit_wiki_progress(
|
|
622
|
+
on_progress,
|
|
623
|
+
"complete",
|
|
624
|
+
1,
|
|
625
|
+
1,
|
|
626
|
+
str(output_path),
|
|
627
|
+
"wiki export complete",
|
|
628
|
+
)
|
|
629
|
+
return result
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _build_wiki_index(
|
|
633
|
+
root: Path,
|
|
634
|
+
*,
|
|
635
|
+
project_file: str | Path | None,
|
|
636
|
+
workers: int,
|
|
637
|
+
memory_limit_bytes: int,
|
|
638
|
+
on_project_result: Callable[[str, ProjectIndexResult], None],
|
|
639
|
+
on_outline_result: Callable[[OutlineResult], None],
|
|
640
|
+
outline_heartbeat: Callable[[], object],
|
|
641
|
+
outline_force_heartbeat: Callable[[], object],
|
|
642
|
+
outline_active_worker_limit: Callable[[], int],
|
|
643
|
+
on_progress: Callable[[Any], None],
|
|
644
|
+
) -> CodebaseIndex:
|
|
645
|
+
requested_outline_workers = (
|
|
646
|
+
workers
|
|
647
|
+
if workers > 0
|
|
648
|
+
else max(1, min(2, (os.cpu_count() or 1) - 1))
|
|
649
|
+
)
|
|
650
|
+
outline_workers = max(1, min(2, requested_outline_workers))
|
|
651
|
+
source_roots = (
|
|
652
|
+
_manifest_source_roots(root)
|
|
653
|
+
if project_file is None
|
|
654
|
+
else (root,)
|
|
655
|
+
)
|
|
656
|
+
merge_workspace_sources = len(source_roots) > 1
|
|
657
|
+
indexes = [
|
|
658
|
+
build_codebase_index(
|
|
659
|
+
source_root,
|
|
660
|
+
project_file=project_file if source_root == root else None,
|
|
661
|
+
index_projects=True,
|
|
662
|
+
main_projects_only=True,
|
|
663
|
+
scan_workspace_sources=(
|
|
664
|
+
True
|
|
665
|
+
if merge_workspace_sources
|
|
666
|
+
else None
|
|
667
|
+
),
|
|
668
|
+
retain_project_syntax=False,
|
|
669
|
+
retain_project_results=False,
|
|
670
|
+
retain_outline_models=False,
|
|
671
|
+
project_source_roots=(source_root,),
|
|
672
|
+
on_progress=on_progress,
|
|
673
|
+
on_project_result=on_project_result,
|
|
674
|
+
on_outline_result=on_outline_result,
|
|
675
|
+
outline_heartbeat=outline_heartbeat,
|
|
676
|
+
outline_force_heartbeat=outline_force_heartbeat,
|
|
677
|
+
outline_active_worker_limit=outline_active_worker_limit,
|
|
678
|
+
# Keep the Windows spawn multiplier bounded while recovering
|
|
679
|
+
# enough parse throughput for multi-million-line corpora.
|
|
680
|
+
workers=outline_workers,
|
|
681
|
+
project_workers=1,
|
|
682
|
+
outline_memory_budget_bytes=memory_limit_bytes * 8 // 10,
|
|
683
|
+
use_artifact_cache=False,
|
|
684
|
+
)
|
|
685
|
+
for source_root in source_roots
|
|
686
|
+
]
|
|
687
|
+
if len(indexes) == 1:
|
|
688
|
+
return indexes[0]
|
|
689
|
+
return _merge_wiki_indexes(root, indexes)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def _prepare_symbol_catalog(
|
|
693
|
+
models: _StoredWikiModels,
|
|
694
|
+
workspace: Path,
|
|
695
|
+
strategy: _WikiExecutionStrategy,
|
|
696
|
+
guard: WikiMemoryGuard,
|
|
697
|
+
) -> tuple[Path, Path, int]:
|
|
698
|
+
source_order = {source: ordinal for ordinal, source in enumerate(models)}
|
|
699
|
+
sorted_catalog = workspace / "symbols-by-identity.jsonl"
|
|
700
|
+
unique_catalog = workspace / "symbols-unique.jsonl"
|
|
701
|
+
name_catalog = workspace / "symbols-by-name.jsonl"
|
|
702
|
+
sort_jsonl_records(
|
|
703
|
+
models.catalog_path,
|
|
704
|
+
sorted_catalog,
|
|
705
|
+
key=lambda item: (
|
|
706
|
+
str(item["identity"]),
|
|
707
|
+
f"{source_order.get(str(item['model_source']), len(source_order)):012d}",
|
|
708
|
+
),
|
|
709
|
+
max_records_per_run=strategy.sort_records_per_run,
|
|
710
|
+
max_bytes_per_run=strategy.sort_bytes_per_run,
|
|
711
|
+
temp_dir=workspace,
|
|
712
|
+
heartbeat=lambda: guard.check(
|
|
713
|
+
phase="external symbol sort",
|
|
714
|
+
),
|
|
715
|
+
force_heartbeat=lambda: guard.check(
|
|
716
|
+
phase="external symbol sort",
|
|
717
|
+
force_process_tree=True,
|
|
718
|
+
),
|
|
719
|
+
)
|
|
720
|
+
symbol_count = 0
|
|
721
|
+
previous_identity: str | None = None
|
|
722
|
+
with (
|
|
723
|
+
sorted_catalog.open("r", encoding="utf-8") as source,
|
|
724
|
+
unique_catalog.open("w", encoding="utf-8", newline="\n") as target,
|
|
725
|
+
):
|
|
726
|
+
for line in source:
|
|
727
|
+
item = json.loads(line)
|
|
728
|
+
identity = str(item["identity"])
|
|
729
|
+
if identity == previous_identity:
|
|
730
|
+
continue
|
|
731
|
+
previous_identity = identity
|
|
732
|
+
target.write(line)
|
|
733
|
+
symbol_count += 1
|
|
734
|
+
sort_jsonl_records(
|
|
735
|
+
unique_catalog,
|
|
736
|
+
name_catalog,
|
|
737
|
+
key=lambda item: (
|
|
738
|
+
str(item["name_key"]),
|
|
739
|
+
str(item["source_key"]),
|
|
740
|
+
f"{int(item['start_line']):012d}",
|
|
741
|
+
f"{int(item['start_col']):012d}",
|
|
742
|
+
str(item["kind"]),
|
|
743
|
+
),
|
|
744
|
+
max_records_per_run=strategy.sort_records_per_run,
|
|
745
|
+
max_bytes_per_run=strategy.sort_bytes_per_run,
|
|
746
|
+
temp_dir=workspace,
|
|
747
|
+
heartbeat=lambda: guard.check(
|
|
748
|
+
phase="external symbol sort",
|
|
749
|
+
),
|
|
750
|
+
force_heartbeat=lambda: guard.check(
|
|
751
|
+
phase="external symbol sort",
|
|
752
|
+
force_process_tree=True,
|
|
753
|
+
),
|
|
754
|
+
)
|
|
755
|
+
return unique_catalog, name_catalog, symbol_count
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def _manifest_source_roots(root: Path) -> tuple[Path, ...]:
|
|
759
|
+
manifest = root / "corpus-manifest.json"
|
|
760
|
+
try:
|
|
761
|
+
payload = json.loads(manifest.read_text(encoding="utf-8"))
|
|
762
|
+
except (OSError, UnicodeError, ValueError):
|
|
763
|
+
return (root,)
|
|
764
|
+
corpora = payload.get("corpora") if isinstance(payload, dict) else None
|
|
765
|
+
if not isinstance(corpora, list):
|
|
766
|
+
return (root,)
|
|
767
|
+
roots: dict[str, Path] = {}
|
|
768
|
+
for corpus in corpora:
|
|
769
|
+
name = corpus.get("name") if isinstance(corpus, dict) else None
|
|
770
|
+
if not isinstance(name, str) or not name.strip():
|
|
771
|
+
return (root,)
|
|
772
|
+
candidate = (root / name).resolve()
|
|
773
|
+
if not candidate.is_relative_to(root) or not candidate.is_dir():
|
|
774
|
+
return (root,)
|
|
775
|
+
roots.setdefault(str(candidate).casefold(), candidate)
|
|
776
|
+
if len(roots) < 2:
|
|
777
|
+
return (root,)
|
|
778
|
+
return tuple(sorted(roots.values(), key=lambda path: str(path).casefold()))
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def _merge_wiki_indexes(
|
|
782
|
+
root: Path,
|
|
783
|
+
indexes: list[CodebaseIndex],
|
|
784
|
+
) -> CodebaseIndex:
|
|
785
|
+
discovery = DelphiProjectDiscovery(root=str(root))
|
|
786
|
+
|
|
787
|
+
def merge_values(target: list[str], values: Iterable[str]) -> None:
|
|
788
|
+
existing = {value.casefold() for value in target}
|
|
789
|
+
for value in values:
|
|
790
|
+
if value.casefold() not in existing:
|
|
791
|
+
target.append(value)
|
|
792
|
+
existing.add(value.casefold())
|
|
793
|
+
|
|
794
|
+
models: dict[str, SemanticModel] = {}
|
|
795
|
+
project_results: dict[str, ProjectIndexResult] = {}
|
|
796
|
+
configured_workers = 0
|
|
797
|
+
effective_workers = 0
|
|
798
|
+
files_completed = 0
|
|
799
|
+
elapsed_seconds = 0.0
|
|
800
|
+
fallbacks = 0
|
|
801
|
+
for index in indexes:
|
|
802
|
+
current = index.discovery
|
|
803
|
+
merge_values(discovery.project_files, current.project_files)
|
|
804
|
+
merge_values(discovery.config_files, current.config_files)
|
|
805
|
+
merge_values(discovery.search_paths, current.search_paths)
|
|
806
|
+
merge_values(discovery.include_paths, current.include_paths)
|
|
807
|
+
merge_values(discovery.defines, current.defines)
|
|
808
|
+
merge_values(discovery.source_files, current.source_files)
|
|
809
|
+
discovery.problems.extend(current.problems)
|
|
810
|
+
for target, source_origins in (
|
|
811
|
+
(discovery.search_path_origins, current.search_path_origins),
|
|
812
|
+
(discovery.include_path_origins, current.include_path_origins),
|
|
813
|
+
(discovery.define_origins, current.define_origins),
|
|
814
|
+
):
|
|
815
|
+
for key, values in source_origins.items():
|
|
816
|
+
merge_values(target.setdefault(key, []), values)
|
|
817
|
+
models.update(index.models)
|
|
818
|
+
project_results.update(index.project_results)
|
|
819
|
+
configured_workers = max(
|
|
820
|
+
configured_workers,
|
|
821
|
+
index.parallel_stats.configured_workers,
|
|
822
|
+
)
|
|
823
|
+
effective_workers = max(
|
|
824
|
+
effective_workers,
|
|
825
|
+
index.parallel_stats.effective_workers,
|
|
826
|
+
)
|
|
827
|
+
files_completed += index.parallel_stats.files_completed
|
|
828
|
+
elapsed_seconds += index.parallel_stats.elapsed_seconds
|
|
829
|
+
fallbacks += index.parallel_stats.fallbacks
|
|
830
|
+
|
|
831
|
+
discovery.project_files.sort(key=str.casefold)
|
|
832
|
+
discovery.config_files.sort(key=str.casefold)
|
|
833
|
+
discovery.search_paths.sort(key=str.casefold)
|
|
834
|
+
discovery.include_paths.sort(key=str.casefold)
|
|
835
|
+
discovery.defines.sort(key=str.casefold)
|
|
836
|
+
discovery.source_files.sort(key=str.casefold)
|
|
837
|
+
for source in discovery.source_files:
|
|
838
|
+
discovery.unit_paths.setdefault(
|
|
839
|
+
Path(source).stem.casefold(),
|
|
840
|
+
[],
|
|
841
|
+
).append(source)
|
|
842
|
+
|
|
843
|
+
symbol_index = SymbolIndex()
|
|
844
|
+
for model in models.values():
|
|
845
|
+
symbol_index.register_unit(model.unit_scope.name, model.unit_scope)
|
|
846
|
+
for model in models.values():
|
|
847
|
+
model.index = symbol_index
|
|
848
|
+
|
|
849
|
+
return CodebaseIndex(
|
|
850
|
+
root=str(root),
|
|
851
|
+
discovery=discovery,
|
|
852
|
+
models=models,
|
|
853
|
+
symbol_index=symbol_index,
|
|
854
|
+
project_results=project_results,
|
|
855
|
+
parallel_stats=ParallelBuildStats(
|
|
856
|
+
configured_workers,
|
|
857
|
+
effective_workers,
|
|
858
|
+
files_completed,
|
|
859
|
+
elapsed_seconds,
|
|
860
|
+
fallbacks,
|
|
861
|
+
),
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
def _populate_wiki_references(index: CodebaseIndex) -> None:
|
|
866
|
+
reference_index = SymbolIndex()
|
|
867
|
+
outline_by_exact: dict[tuple[str, str, str, int, int], Symbol] = {}
|
|
868
|
+
outline_by_source: dict[tuple[str, str, str], Symbol] = {}
|
|
869
|
+
outline_ids: set[int] = set()
|
|
870
|
+
for model in index.models.values():
|
|
871
|
+
reference_index.register_unit(model.unit_scope.name, model.unit_scope)
|
|
872
|
+
for symbol in _iter_symbols(model.unit_scope):
|
|
873
|
+
outline_ids.add(id(symbol))
|
|
874
|
+
source_key = (
|
|
875
|
+
symbol.name.casefold(),
|
|
876
|
+
symbol.kind.value,
|
|
877
|
+
symbol.decl_range.file_name.casefold(),
|
|
878
|
+
)
|
|
879
|
+
outline_by_source.setdefault(source_key, symbol)
|
|
880
|
+
outline_by_exact[
|
|
881
|
+
(
|
|
882
|
+
*source_key,
|
|
883
|
+
symbol.name_range.start_line,
|
|
884
|
+
symbol.name_range.start_col,
|
|
885
|
+
)
|
|
886
|
+
] = symbol
|
|
887
|
+
outline_units = dict(reference_index.units)
|
|
888
|
+
outline_name_counts = {
|
|
889
|
+
name: len(symbols)
|
|
890
|
+
for name, symbols in reference_index.name_index.items()
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
parser = DelphiParser(
|
|
894
|
+
include_paths=index.discovery.include_paths,
|
|
895
|
+
defines=index.discovery.defines,
|
|
896
|
+
include_loader=workspace_include_loader(
|
|
897
|
+
index.discovery.project_config,
|
|
898
|
+
index.discovery.include_paths,
|
|
899
|
+
),
|
|
900
|
+
mode=ParserMode.TOLERANT,
|
|
901
|
+
)
|
|
902
|
+
for source_path, outline_model in index.models.items():
|
|
903
|
+
try:
|
|
904
|
+
source = read_source_text(Path(source_path))
|
|
905
|
+
parsed = parser.parse(
|
|
906
|
+
source,
|
|
907
|
+
source_path,
|
|
908
|
+
build_semantic=True,
|
|
909
|
+
index=reference_index,
|
|
910
|
+
)
|
|
911
|
+
except (OSError, UnicodeError):
|
|
912
|
+
continue
|
|
913
|
+
if parsed.semantic is None:
|
|
914
|
+
continue
|
|
915
|
+
parsed_unit_key = normalize_name(parsed.semantic.unit_scope.name)
|
|
916
|
+
registered_names = {
|
|
917
|
+
normalize_name(symbol.name)
|
|
918
|
+
for symbol in _iter_symbols(parsed.semantic.unit_scope)
|
|
919
|
+
}
|
|
920
|
+
try:
|
|
921
|
+
references: list[SymbolReference] = []
|
|
922
|
+
for reference in parsed.semantic.references:
|
|
923
|
+
resolved = reference.resolved
|
|
924
|
+
mapped = (
|
|
925
|
+
resolved
|
|
926
|
+
if resolved is not None and id(resolved) in outline_ids
|
|
927
|
+
else None
|
|
928
|
+
)
|
|
929
|
+
if resolved is not None and mapped is None:
|
|
930
|
+
source_key = (
|
|
931
|
+
resolved.name.casefold(),
|
|
932
|
+
resolved.kind.value,
|
|
933
|
+
resolved.decl_range.file_name.casefold(),
|
|
934
|
+
)
|
|
935
|
+
mapped = outline_by_exact.get(
|
|
936
|
+
(
|
|
937
|
+
*source_key,
|
|
938
|
+
resolved.name_range.start_line,
|
|
939
|
+
resolved.name_range.start_col,
|
|
940
|
+
)
|
|
941
|
+
) or outline_by_source.get(source_key)
|
|
942
|
+
references.append(
|
|
943
|
+
SymbolReference(
|
|
944
|
+
name=reference.name,
|
|
945
|
+
kind=reference.kind,
|
|
946
|
+
ref_range=reference.ref_range,
|
|
947
|
+
scope=outline_model.unit_scope,
|
|
948
|
+
resolved=mapped,
|
|
949
|
+
)
|
|
950
|
+
)
|
|
951
|
+
outline_model.references = references
|
|
952
|
+
finally:
|
|
953
|
+
outline_unit = outline_units.get(parsed_unit_key)
|
|
954
|
+
if outline_unit is None:
|
|
955
|
+
reference_index.units.pop(parsed_unit_key, None)
|
|
956
|
+
else:
|
|
957
|
+
reference_index.units[parsed_unit_key] = outline_unit
|
|
958
|
+
for name in registered_names:
|
|
959
|
+
count = outline_name_counts.get(name)
|
|
960
|
+
if count is None:
|
|
961
|
+
reference_index.name_index.pop(name, None)
|
|
962
|
+
else:
|
|
963
|
+
del reference_index.name_index[name][count:]
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
def _emit_wiki_progress(
|
|
967
|
+
callback: WikiProgressCallback | None,
|
|
968
|
+
phase: str,
|
|
969
|
+
completed: int,
|
|
970
|
+
total: int | None,
|
|
971
|
+
path: str,
|
|
972
|
+
detail: str,
|
|
973
|
+
) -> None:
|
|
974
|
+
if callback is not None:
|
|
975
|
+
callback(WikiProgressEvent(phase, completed, total, path, detail))
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
def _resolve_output_destination(output: str | Path) -> tuple[Path, Path]:
|
|
979
|
+
requested = Path(os.path.abspath(Path(output).expanduser()))
|
|
980
|
+
if requested.is_symlink():
|
|
981
|
+
raise WikiExportError(f"Wiki destination must not be a symlink: {requested}")
|
|
982
|
+
canonical = requested.parent.resolve() / requested.name
|
|
983
|
+
return requested, canonical
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
def _validate_paths(root: Path, output: Path, *, force: bool) -> None:
|
|
987
|
+
if not root.is_dir():
|
|
988
|
+
raise WikiExportError(f"Repository root is not a directory: {root}")
|
|
989
|
+
if output == root:
|
|
990
|
+
raise WikiExportError("Wiki destination must not be the repository root.")
|
|
991
|
+
if root.is_relative_to(output):
|
|
992
|
+
raise WikiExportError("Wiki destination must not contain the repository root.")
|
|
993
|
+
if output.is_symlink():
|
|
994
|
+
raise WikiExportError(f"Wiki destination must not be a symlink: {output}")
|
|
995
|
+
if output.exists() and not output.is_dir():
|
|
996
|
+
raise WikiExportError(f"Wiki destination already exists and is not a directory: {output}")
|
|
997
|
+
if output.exists() and not force and _directory_has_entries(output):
|
|
998
|
+
raise WikiExportError(
|
|
999
|
+
f"Wiki destination already exists: {output}. Pass --force to replace it."
|
|
1000
|
+
)
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
def _directory_has_entries(path: Path) -> bool:
|
|
1004
|
+
try:
|
|
1005
|
+
next(path.iterdir())
|
|
1006
|
+
except StopIteration:
|
|
1007
|
+
return False
|
|
1008
|
+
return True
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
def _install_completed_bundle(
|
|
1012
|
+
temporary: Path,
|
|
1013
|
+
output: Path,
|
|
1014
|
+
*,
|
|
1015
|
+
requested_output: Path,
|
|
1016
|
+
force: bool,
|
|
1017
|
+
) -> None:
|
|
1018
|
+
_revalidate_output_destination(requested_output, output)
|
|
1019
|
+
if not output.exists():
|
|
1020
|
+
os.replace(temporary, output)
|
|
1021
|
+
return
|
|
1022
|
+
if not force and _directory_has_entries(output):
|
|
1023
|
+
raise WikiExportError(f"Wiki destination already exists: {output}")
|
|
1024
|
+
|
|
1025
|
+
backup = output.parent / f".{output.name}.backup-{os.getpid()}"
|
|
1026
|
+
suffix = 0
|
|
1027
|
+
while backup.exists():
|
|
1028
|
+
suffix += 1
|
|
1029
|
+
backup = output.parent / f".{output.name}.backup-{os.getpid()}-{suffix}"
|
|
1030
|
+
os.replace(output, backup)
|
|
1031
|
+
try:
|
|
1032
|
+
os.replace(temporary, output)
|
|
1033
|
+
except Exception:
|
|
1034
|
+
os.replace(backup, output)
|
|
1035
|
+
raise
|
|
1036
|
+
shutil.rmtree(backup)
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
def _revalidate_output_destination(requested: Path, canonical: Path) -> None:
|
|
1040
|
+
if requested.is_symlink():
|
|
1041
|
+
raise WikiExportError(f"Wiki destination became a symlink: {requested}")
|
|
1042
|
+
current = requested.parent.resolve() / requested.name
|
|
1043
|
+
if current != canonical:
|
|
1044
|
+
raise WikiExportError(
|
|
1045
|
+
"Wiki destination parent changed while the bundle was generated."
|
|
1046
|
+
)
|
|
1047
|
+
if canonical.is_symlink():
|
|
1048
|
+
raise WikiExportError(f"Wiki destination must not be a symlink: {canonical}")
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
class _WikiWriter:
|
|
1052
|
+
def __init__(
|
|
1053
|
+
self,
|
|
1054
|
+
index: CodebaseIndex,
|
|
1055
|
+
destination: Path,
|
|
1056
|
+
*,
|
|
1057
|
+
workers: int = 0,
|
|
1058
|
+
on_progress: WikiProgressCallback | None = None,
|
|
1059
|
+
) -> None:
|
|
1060
|
+
self.index = index
|
|
1061
|
+
self.destination = destination
|
|
1062
|
+
self.workers = workers
|
|
1063
|
+
self._on_progress = on_progress
|
|
1064
|
+
self.documents = 0
|
|
1065
|
+
self._source_cache: dict[str, list[str]] = _BoundedSourceCache()
|
|
1066
|
+
self._display_path_cache: dict[str, str] = {}
|
|
1067
|
+
self._symbol_count = sum(1 for _symbol in self._iter_unique_symbols())
|
|
1068
|
+
self.units = self._unit_records()
|
|
1069
|
+
self._unit_by_source = {
|
|
1070
|
+
_normalized_source(record.source_path): record for record in self.units
|
|
1071
|
+
}
|
|
1072
|
+
self._unit_by_name = {record.name.casefold(): record for record in self.units}
|
|
1073
|
+
self._references_by_target: dict[int, list[SymbolReference]] = {}
|
|
1074
|
+
self._projects_by_source: dict[str, list[tuple[str, PurePosixPath]]] = {}
|
|
1075
|
+
self._metric_page_by_source: dict[str, PurePosixPath] = {}
|
|
1076
|
+
self._collect_references()
|
|
1077
|
+
|
|
1078
|
+
@property
|
|
1079
|
+
def symbol_count(self) -> int:
|
|
1080
|
+
return self._symbol_count
|
|
1081
|
+
|
|
1082
|
+
def write(self) -> None:
|
|
1083
|
+
self._write_root_index()
|
|
1084
|
+
self._write_overview()
|
|
1085
|
+
self._write_manifest()
|
|
1086
|
+
self._write_projects()
|
|
1087
|
+
self._write_metrics()
|
|
1088
|
+
self._write_units()
|
|
1089
|
+
self._write_symbols()
|
|
1090
|
+
self._write_references()
|
|
1091
|
+
self._write_problems()
|
|
1092
|
+
self._write_reference_docs()
|
|
1093
|
+
|
|
1094
|
+
def _iter_unique_symbols(self) -> Iterator[Symbol]:
|
|
1095
|
+
seen: set[bytes] = set()
|
|
1096
|
+
for symbol in _all_symbols(self.index):
|
|
1097
|
+
if symbol.kind.value == "unit":
|
|
1098
|
+
continue
|
|
1099
|
+
digest = hashlib.sha256(
|
|
1100
|
+
_symbol_identity(symbol).encode("utf-8")
|
|
1101
|
+
).digest()
|
|
1102
|
+
if digest in seen:
|
|
1103
|
+
continue
|
|
1104
|
+
seen.add(digest)
|
|
1105
|
+
yield symbol
|
|
1106
|
+
|
|
1107
|
+
def _unit_records(self) -> list[_UnitRecord]:
|
|
1108
|
+
records = [
|
|
1109
|
+
_UnitRecord(
|
|
1110
|
+
source,
|
|
1111
|
+
model.unit_scope.name,
|
|
1112
|
+
PurePosixPath("units")
|
|
1113
|
+
/ _document_name(model.unit_scope.name, _normalized_source(source)),
|
|
1114
|
+
)
|
|
1115
|
+
for source, model in self.index.models.items()
|
|
1116
|
+
]
|
|
1117
|
+
return sorted(records, key=lambda record: (record.name.casefold(), record.source_path.casefold()))
|
|
1118
|
+
|
|
1119
|
+
def _collect_references(self) -> None:
|
|
1120
|
+
for model in self.index.models.values():
|
|
1121
|
+
for reference in model.references:
|
|
1122
|
+
if reference.resolved is None:
|
|
1123
|
+
continue
|
|
1124
|
+
key = id(reference.resolved)
|
|
1125
|
+
self._references_by_target.setdefault(key, []).append(reference)
|
|
1126
|
+
for references in self._references_by_target.values():
|
|
1127
|
+
references.sort(key=_reference_sort_key)
|
|
1128
|
+
|
|
1129
|
+
def _iter_unresolved_references(self) -> Iterator[SymbolReference]:
|
|
1130
|
+
seen: set[tuple[str, str, int, int, str]] = set()
|
|
1131
|
+
for _source, model in sorted(
|
|
1132
|
+
self.index.models.items(),
|
|
1133
|
+
key=lambda item: item[0].casefold(),
|
|
1134
|
+
):
|
|
1135
|
+
for reference in sorted(model.references, key=_reference_sort_key):
|
|
1136
|
+
if reference.resolved is not None:
|
|
1137
|
+
continue
|
|
1138
|
+
key = (
|
|
1139
|
+
reference.name.casefold(),
|
|
1140
|
+
reference.ref_range.file_name,
|
|
1141
|
+
reference.ref_range.start_line,
|
|
1142
|
+
reference.ref_range.start_col,
|
|
1143
|
+
reference.kind.value,
|
|
1144
|
+
)
|
|
1145
|
+
if key in seen:
|
|
1146
|
+
continue
|
|
1147
|
+
seen.add(key)
|
|
1148
|
+
yield reference
|
|
1149
|
+
|
|
1150
|
+
def _progress(
|
|
1151
|
+
self,
|
|
1152
|
+
phase: str,
|
|
1153
|
+
completed: int,
|
|
1154
|
+
total: int | None,
|
|
1155
|
+
path: str,
|
|
1156
|
+
detail: str,
|
|
1157
|
+
) -> None:
|
|
1158
|
+
_emit_wiki_progress(
|
|
1159
|
+
self._on_progress,
|
|
1160
|
+
phase,
|
|
1161
|
+
completed,
|
|
1162
|
+
total,
|
|
1163
|
+
path,
|
|
1164
|
+
detail,
|
|
1165
|
+
)
|
|
1166
|
+
|
|
1167
|
+
def _write_root_index(self) -> None:
|
|
1168
|
+
body = [
|
|
1169
|
+
"---",
|
|
1170
|
+
f'okf_version: "{OKF_VERSION}"',
|
|
1171
|
+
"---",
|
|
1172
|
+
"# Delphi Codebase Wiki",
|
|
1173
|
+
"",
|
|
1174
|
+
"Portable knowledge export generated by "
|
|
1175
|
+
f"`python-delphi-lsp {__version__}`.",
|
|
1176
|
+
"",
|
|
1177
|
+
"## Start here",
|
|
1178
|
+
"",
|
|
1179
|
+
f"- {_link('Overview', PurePosixPath('overview.md'))}",
|
|
1180
|
+
f"- {_link('Projects', PurePosixPath('projects/index.md'))}",
|
|
1181
|
+
f"- {_link('Units', PurePosixPath('units/index.md'))}",
|
|
1182
|
+
f"- {_link('Symbols', PurePosixPath('symbols/index.md'))}",
|
|
1183
|
+
f"- {_link('References', PurePosixPath('references/index.md'))}",
|
|
1184
|
+
f"- {_link('Problems', PurePosixPath('problems/index.md'))}",
|
|
1185
|
+
f"- {_link('Metrics', PurePosixPath('metrics/index.md'))}",
|
|
1186
|
+
f"- {_link('Query and graph reference', PurePosixPath('reference/index.md'))}",
|
|
1187
|
+
f"- {_link('Export manifest', PurePosixPath('manifest.md'))}",
|
|
1188
|
+
]
|
|
1189
|
+
self._write(PurePosixPath("index.md"), "\n".join(body) + "\n")
|
|
1190
|
+
|
|
1191
|
+
def _write_overview(self) -> None:
|
|
1192
|
+
overview = layer_payload(self.index, "overview")
|
|
1193
|
+
body = [
|
|
1194
|
+
f"# {_escape( Path(self.index.root).name or 'Delphi workspace')}",
|
|
1195
|
+
"",
|
|
1196
|
+
f"- Repository root: `{_escape_code(self.index.root)}`",
|
|
1197
|
+
f"- Projects: {overview['project_count']}",
|
|
1198
|
+
f"- Sources: {overview['source_count']}",
|
|
1199
|
+
f"- Units: {overview['unit_count']}",
|
|
1200
|
+
f"- Symbols: {self.symbol_count}",
|
|
1201
|
+
"",
|
|
1202
|
+
"## Defines",
|
|
1203
|
+
"",
|
|
1204
|
+
*_bullet_code_values(overview["defines"]),
|
|
1205
|
+
"",
|
|
1206
|
+
"## Search paths",
|
|
1207
|
+
"",
|
|
1208
|
+
*_bullet_code_values(overview["search_paths"]),
|
|
1209
|
+
"",
|
|
1210
|
+
"## Include paths",
|
|
1211
|
+
"",
|
|
1212
|
+
*_bullet_code_values(overview["include_paths"]),
|
|
1213
|
+
"",
|
|
1214
|
+
"## Related knowledge",
|
|
1215
|
+
"",
|
|
1216
|
+
f"- {_link('Projects', PurePosixPath('projects/index.md'))}",
|
|
1217
|
+
f"- {_link('Problems', PurePosixPath('problems/index.md'))}",
|
|
1218
|
+
f"- {_link('Workspace metrics', PurePosixPath('metrics/workspace.md'))}",
|
|
1219
|
+
]
|
|
1220
|
+
self._concept(
|
|
1221
|
+
PurePosixPath("overview.md"),
|
|
1222
|
+
concept_type="Delphi Codebase",
|
|
1223
|
+
title="Delphi codebase overview",
|
|
1224
|
+
description="Discovered projects, sources, units, paths, defines, and diagnostics.",
|
|
1225
|
+
tags=("delphi", "codebase", "overview"),
|
|
1226
|
+
body=body,
|
|
1227
|
+
)
|
|
1228
|
+
|
|
1229
|
+
def _write_manifest(self) -> None:
|
|
1230
|
+
body = [
|
|
1231
|
+
"# Export manifest",
|
|
1232
|
+
"",
|
|
1233
|
+
f"- Generator: `python-delphi-lsp/{__version__}`",
|
|
1234
|
+
f"- Open Knowledge Format: `{OKF_VERSION}`",
|
|
1235
|
+
f"- Projects: {len(self.index.discovery.project_files)}",
|
|
1236
|
+
f"- Units: {len(self.units)}",
|
|
1237
|
+
f"- Symbols: {self.symbol_count}",
|
|
1238
|
+
"",
|
|
1239
|
+
"## Coverage",
|
|
1240
|
+
"",
|
|
1241
|
+
"- Layered views: overview, projects, units, unit, symbols, symbol, "
|
|
1242
|
+
"implementation, references, problems, and metrics.",
|
|
1243
|
+
"- Semantic references are attached to their resolved symbol pages; "
|
|
1244
|
+
"unresolved references are retained separately.",
|
|
1245
|
+
"- Protocol v3 query, relation, CPG, pagination, and cache semantics "
|
|
1246
|
+
"are documented under the reference section.",
|
|
1247
|
+
"- Lazy CPG results are intentionally not expanded for every symbol, "
|
|
1248
|
+
"which avoids redundant parsing and unbounded bundle growth.",
|
|
1249
|
+
]
|
|
1250
|
+
self._concept(
|
|
1251
|
+
PurePosixPath("manifest.md"),
|
|
1252
|
+
concept_type="Knowledge Bundle Manifest",
|
|
1253
|
+
title="Export manifest",
|
|
1254
|
+
description="Generator, format, counts, and coverage of this knowledge bundle.",
|
|
1255
|
+
tags=("manifest", "okf", "delphi"),
|
|
1256
|
+
body=body,
|
|
1257
|
+
)
|
|
1258
|
+
|
|
1259
|
+
def _write_projects(self) -> None:
|
|
1260
|
+
pages: list[tuple[str, PurePosixPath]] = []
|
|
1261
|
+
total = len(self.index.discovery.project_files)
|
|
1262
|
+
for ordinal, item in enumerate(self._project_items(), start=1):
|
|
1263
|
+
project_path = str(item["path"])
|
|
1264
|
+
title = Path(project_path).name
|
|
1265
|
+
page = PurePosixPath("projects") / _document_name(title, _normalized_source(project_path))
|
|
1266
|
+
pages.append((title, page))
|
|
1267
|
+
project_membership = (
|
|
1268
|
+
project_path,
|
|
1269
|
+
*(str(unit["path"]) for unit in item["parsed_units"]),
|
|
1270
|
+
)
|
|
1271
|
+
for source_path in project_membership:
|
|
1272
|
+
self._record_project_membership(source_path, ordinal, title, page)
|
|
1273
|
+
body = [
|
|
1274
|
+
f"# {_escape(title)}",
|
|
1275
|
+
"",
|
|
1276
|
+
f"- Path: `{_escape_code(self._display_path(project_path))}`",
|
|
1277
|
+
f"- Parsed units: {len(item['parsed_units'])}",
|
|
1278
|
+
f"- Include files: {len(item['include_files'])}",
|
|
1279
|
+
f"- Missing units: {len(item['not_found_units'])}",
|
|
1280
|
+
f"- Problems: {len(item['problems'])}",
|
|
1281
|
+
"",
|
|
1282
|
+
"## Parsed units",
|
|
1283
|
+
"",
|
|
1284
|
+
]
|
|
1285
|
+
if item["parsed_units"]:
|
|
1286
|
+
for unit in item["parsed_units"]:
|
|
1287
|
+
unit_record = self._find_unit(str(unit["path"]), str(unit["name"]))
|
|
1288
|
+
label = f"{unit['name']} — {self._display_path(str(unit['path']))}"
|
|
1289
|
+
body.append(
|
|
1290
|
+
f"- {_link(label, unit_record.page) if unit_record else _escape(label)}"
|
|
1291
|
+
f"{' (parse error)' if unit['has_error'] else ''}"
|
|
1292
|
+
)
|
|
1293
|
+
else:
|
|
1294
|
+
body.append("- None")
|
|
1295
|
+
body.extend(["", "## Include files", ""])
|
|
1296
|
+
body.extend(
|
|
1297
|
+
f"- `{_escape_code(self._display_path(str(value['path'])))}`"
|
|
1298
|
+
for value in item["include_files"]
|
|
1299
|
+
)
|
|
1300
|
+
if not item["include_files"]:
|
|
1301
|
+
body.append("- None")
|
|
1302
|
+
body.extend(["", "## Missing units", ""])
|
|
1303
|
+
body.extend(f"- `{_escape_code(str(value))}`" for value in item["not_found_units"])
|
|
1304
|
+
if not item["not_found_units"]:
|
|
1305
|
+
body.append("- None")
|
|
1306
|
+
body.extend(["", "## Project-index problems", ""])
|
|
1307
|
+
body.extend(
|
|
1308
|
+
f"- **{_escape(str(problem['kind']))}:** "
|
|
1309
|
+
f"{_escape(str(problem['message']))} "
|
|
1310
|
+
f"(`{_escape_code(self._display_path(str(problem['file'])))}`)"
|
|
1311
|
+
for problem in item["problems"]
|
|
1312
|
+
)
|
|
1313
|
+
if not item["problems"]:
|
|
1314
|
+
body.append("- None")
|
|
1315
|
+
body.extend(["", "## Complete indexed data", "", _json_block(item)])
|
|
1316
|
+
self._concept(
|
|
1317
|
+
page,
|
|
1318
|
+
concept_type="Delphi Project",
|
|
1319
|
+
title=title,
|
|
1320
|
+
description=f"Project index for {title}.",
|
|
1321
|
+
tags=("delphi", "project"),
|
|
1322
|
+
body=body,
|
|
1323
|
+
)
|
|
1324
|
+
self._progress(
|
|
1325
|
+
"projects",
|
|
1326
|
+
ordinal,
|
|
1327
|
+
total,
|
|
1328
|
+
project_path,
|
|
1329
|
+
"project page written",
|
|
1330
|
+
)
|
|
1331
|
+
self._directory_index(
|
|
1332
|
+
PurePosixPath("projects/index.md"),
|
|
1333
|
+
"Projects",
|
|
1334
|
+
pages,
|
|
1335
|
+
empty="No Delphi project entry files were discovered.",
|
|
1336
|
+
)
|
|
1337
|
+
|
|
1338
|
+
def _project_items(self) -> Iterable[dict[str, Any]]:
|
|
1339
|
+
return layer_payload(self.index, "projects")["items"]
|
|
1340
|
+
|
|
1341
|
+
def _record_project_membership(
|
|
1342
|
+
self,
|
|
1343
|
+
source_path: str,
|
|
1344
|
+
ordinal: int,
|
|
1345
|
+
title: str,
|
|
1346
|
+
page: PurePosixPath,
|
|
1347
|
+
) -> None:
|
|
1348
|
+
del ordinal
|
|
1349
|
+
memberships = self._projects_by_source.setdefault(
|
|
1350
|
+
_normalized_source(source_path), []
|
|
1351
|
+
)
|
|
1352
|
+
membership = (title, page)
|
|
1353
|
+
if membership not in memberships:
|
|
1354
|
+
memberships.append(membership)
|
|
1355
|
+
|
|
1356
|
+
def _project_memberships_for(
|
|
1357
|
+
self,
|
|
1358
|
+
source_path: str,
|
|
1359
|
+
) -> list[tuple[str, PurePosixPath]]:
|
|
1360
|
+
return self._projects_by_source.get(_normalized_source(source_path), [])
|
|
1361
|
+
|
|
1362
|
+
def _write_units(self) -> None:
|
|
1363
|
+
pages: list[tuple[str, PurePosixPath]] = []
|
|
1364
|
+
total = len(self.units)
|
|
1365
|
+
for ordinal, unit in enumerate(self.units, start=1):
|
|
1366
|
+
pages.append((unit.name, unit.page))
|
|
1367
|
+
model = self.index.models[unit.source_path]
|
|
1368
|
+
symbols = sorted(
|
|
1369
|
+
_iter_symbols(model.unit_scope, include_unit=False),
|
|
1370
|
+
key=lambda symbol: (
|
|
1371
|
+
symbol.decl_range.start_line,
|
|
1372
|
+
symbol.decl_range.start_col,
|
|
1373
|
+
symbol.name.casefold(),
|
|
1374
|
+
symbol.kind.value,
|
|
1375
|
+
),
|
|
1376
|
+
)
|
|
1377
|
+
references = sorted(model.references, key=_reference_sort_key)
|
|
1378
|
+
projects = self._project_memberships_for(unit.source_path)
|
|
1379
|
+
body = [
|
|
1380
|
+
f"# {_escape(unit.name)}",
|
|
1381
|
+
"",
|
|
1382
|
+
f"- Source: `{_escape_code(self._display_path(unit.source_path))}`",
|
|
1383
|
+
f"- Symbols: {len(symbols)}",
|
|
1384
|
+
f"- Semantic references: {len(references)}",
|
|
1385
|
+
"",
|
|
1386
|
+
"## Projects",
|
|
1387
|
+
"",
|
|
1388
|
+
]
|
|
1389
|
+
body.extend(f"- {_link(title, page)}" for title, page in projects)
|
|
1390
|
+
if not projects:
|
|
1391
|
+
body.append("- No concrete project membership was resolved")
|
|
1392
|
+
body.extend(
|
|
1393
|
+
[
|
|
1394
|
+
"",
|
|
1395
|
+
"## Symbols",
|
|
1396
|
+
"",
|
|
1397
|
+
]
|
|
1398
|
+
)
|
|
1399
|
+
body.extend(
|
|
1400
|
+
f"- {_link(symbol.name, _symbol_page(symbol))} "
|
|
1401
|
+
f"— `{symbol.kind.value}` at line {symbol.decl_range.start_line}"
|
|
1402
|
+
for symbol in symbols
|
|
1403
|
+
)
|
|
1404
|
+
if not symbols:
|
|
1405
|
+
body.append("- None")
|
|
1406
|
+
body.extend(["", "## References found in this unit", ""])
|
|
1407
|
+
body.extend(self._reference_lines(references))
|
|
1408
|
+
if not references:
|
|
1409
|
+
body.append("- None")
|
|
1410
|
+
metrics_page = self._metric_page_by_source.get(
|
|
1411
|
+
_normalized_source(unit.source_path)
|
|
1412
|
+
)
|
|
1413
|
+
if metrics_page is not None:
|
|
1414
|
+
body.extend(
|
|
1415
|
+
[
|
|
1416
|
+
"",
|
|
1417
|
+
"## Related knowledge",
|
|
1418
|
+
"",
|
|
1419
|
+
f"- {_link('Unit metrics', metrics_page)}",
|
|
1420
|
+
]
|
|
1421
|
+
)
|
|
1422
|
+
self._concept(
|
|
1423
|
+
unit.page,
|
|
1424
|
+
concept_type="Delphi Unit",
|
|
1425
|
+
title=unit.name,
|
|
1426
|
+
description=f"Symbols and references in {unit.name}.",
|
|
1427
|
+
tags=("delphi", "unit"),
|
|
1428
|
+
body=body,
|
|
1429
|
+
)
|
|
1430
|
+
self._progress(
|
|
1431
|
+
"units",
|
|
1432
|
+
ordinal,
|
|
1433
|
+
total,
|
|
1434
|
+
unit.source_path,
|
|
1435
|
+
"unit page written",
|
|
1436
|
+
)
|
|
1437
|
+
pages.sort(key=lambda item: (item[0].casefold(), item[1].as_posix()))
|
|
1438
|
+
self._directory_index(
|
|
1439
|
+
PurePosixPath("units/index.md"),
|
|
1440
|
+
"Units",
|
|
1441
|
+
pages,
|
|
1442
|
+
empty="No Delphi units were indexed.",
|
|
1443
|
+
)
|
|
1444
|
+
|
|
1445
|
+
def _write_symbols(self) -> None:
|
|
1446
|
+
source_ordered_symbols = sorted(
|
|
1447
|
+
self._iter_unique_symbols(),
|
|
1448
|
+
key=lambda symbol: (
|
|
1449
|
+
_normalized_source(symbol.decl_range.file_name),
|
|
1450
|
+
symbol.decl_range.start_line,
|
|
1451
|
+
symbol.decl_range.start_col,
|
|
1452
|
+
symbol.name.casefold(),
|
|
1453
|
+
symbol.kind.value,
|
|
1454
|
+
),
|
|
1455
|
+
)
|
|
1456
|
+
total = len(source_ordered_symbols)
|
|
1457
|
+
for ordinal, symbol in enumerate(source_ordered_symbols, start=1):
|
|
1458
|
+
references = self._references_by_target.get(id(symbol), [])
|
|
1459
|
+
page, source, body = self._symbol_body_prefix(symbol)
|
|
1460
|
+
body.extend(["", "## Semantic references", ""])
|
|
1461
|
+
body.extend(self._reference_lines(references))
|
|
1462
|
+
if not references:
|
|
1463
|
+
body.append("- None")
|
|
1464
|
+
self._concept(
|
|
1465
|
+
page,
|
|
1466
|
+
concept_type=f"Delphi {symbol.kind.value.replace('_', ' ').title()} Symbol",
|
|
1467
|
+
title=symbol.name,
|
|
1468
|
+
description=(
|
|
1469
|
+
f"{symbol.kind.value} {symbol.name} in {source} at "
|
|
1470
|
+
f"line {symbol.decl_range.start_line}."
|
|
1471
|
+
),
|
|
1472
|
+
tags=("delphi", "symbol", symbol.kind.value),
|
|
1473
|
+
body=body,
|
|
1474
|
+
)
|
|
1475
|
+
self._progress(
|
|
1476
|
+
"symbols",
|
|
1477
|
+
ordinal,
|
|
1478
|
+
total,
|
|
1479
|
+
symbol.decl_range.file_name,
|
|
1480
|
+
f"symbol page written: {symbol.name}",
|
|
1481
|
+
)
|
|
1482
|
+
|
|
1483
|
+
name_ordered_symbols = sorted(
|
|
1484
|
+
self._iter_unique_symbols(),
|
|
1485
|
+
key=lambda symbol: (
|
|
1486
|
+
symbol.name.casefold(),
|
|
1487
|
+
_normalized_source(symbol.decl_range.file_name),
|
|
1488
|
+
symbol.decl_range.start_line,
|
|
1489
|
+
symbol.decl_range.start_col,
|
|
1490
|
+
symbol.kind.value,
|
|
1491
|
+
),
|
|
1492
|
+
)
|
|
1493
|
+
self._directory_index_iter(
|
|
1494
|
+
PurePosixPath("symbols/index.md"),
|
|
1495
|
+
"Symbols",
|
|
1496
|
+
(
|
|
1497
|
+
(f"{symbol.name} ({symbol.kind.value})", _symbol_page(symbol))
|
|
1498
|
+
for symbol in name_ordered_symbols
|
|
1499
|
+
),
|
|
1500
|
+
empty="No non-unit symbols were indexed.",
|
|
1501
|
+
)
|
|
1502
|
+
|
|
1503
|
+
def _symbol_body_prefix(
|
|
1504
|
+
self,
|
|
1505
|
+
symbol: Symbol,
|
|
1506
|
+
) -> tuple[PurePosixPath, str, list[str]]:
|
|
1507
|
+
page = _symbol_page(symbol)
|
|
1508
|
+
source = self._display_path(symbol.decl_range.file_name)
|
|
1509
|
+
unit = self._unit_by_source.get(_normalized_source(symbol.decl_range.file_name))
|
|
1510
|
+
owner = _symbol_owner(symbol)
|
|
1511
|
+
owner_symbol = symbol.scope.owner
|
|
1512
|
+
owner_page = (
|
|
1513
|
+
_symbol_page(owner_symbol)
|
|
1514
|
+
if owner_symbol is not None
|
|
1515
|
+
and owner_symbol.kind.value != "unit"
|
|
1516
|
+
and owner_symbol is not symbol
|
|
1517
|
+
else None
|
|
1518
|
+
)
|
|
1519
|
+
children = self._direct_symbol_children(symbol)
|
|
1520
|
+
fragment = _source_fragment(symbol.decl_range, "declaration", self._source_cache)
|
|
1521
|
+
modifiers = sorted(value.value for value in symbol.modifiers)
|
|
1522
|
+
body = [
|
|
1523
|
+
f"# {_escape(symbol.name)}",
|
|
1524
|
+
"",
|
|
1525
|
+
f"- Kind: `{symbol.kind.value}`",
|
|
1526
|
+
f"- Type: `{_escape_code(symbol.type_ref.display_name())}`",
|
|
1527
|
+
f"- Visibility: `{symbol.visibility.value}`",
|
|
1528
|
+
(
|
|
1529
|
+
f"- Owner: "
|
|
1530
|
+
f"{_link(owner, owner_page) if owner_page else f'`{_escape_code(owner)}`'}"
|
|
1531
|
+
),
|
|
1532
|
+
f"- Unit: {_link(unit.name, unit.page) if unit else '`unknown`'}",
|
|
1533
|
+
(
|
|
1534
|
+
f"- Location: `{_escape_code(source)}:{symbol.decl_range.start_line}:"
|
|
1535
|
+
f"{symbol.decl_range.start_col}`"
|
|
1536
|
+
),
|
|
1537
|
+
(
|
|
1538
|
+
f"- Range: `{symbol.decl_range.start_line}:{symbol.decl_range.start_col}-"
|
|
1539
|
+
f"{symbol.decl_range.end_line}:{symbol.decl_range.end_col}`"
|
|
1540
|
+
),
|
|
1541
|
+
f"- Modifiers: {', '.join(f'`{value}`' for value in modifiers) or 'None'}",
|
|
1542
|
+
(
|
|
1543
|
+
f"- Base types: "
|
|
1544
|
+
f"{', '.join(f'`{_escape_code(value.display_name())}`' for value in symbol.base_types) or 'None'}"
|
|
1545
|
+
),
|
|
1546
|
+
]
|
|
1547
|
+
if symbol.doc:
|
|
1548
|
+
body.extend(["", "## Documentation", "", symbol.doc])
|
|
1549
|
+
if symbol.attributes:
|
|
1550
|
+
body.extend(["", "## Attributes", "", _json_block(symbol.attributes)])
|
|
1551
|
+
body.extend(["", "## Declaration or implementation", ""])
|
|
1552
|
+
if fragment is None:
|
|
1553
|
+
body.append("Source fragment unavailable.")
|
|
1554
|
+
else:
|
|
1555
|
+
body.extend(
|
|
1556
|
+
[
|
|
1557
|
+
(
|
|
1558
|
+
f"`{_escape_code(self._display_path(str(fragment['range']['path'])))}:"
|
|
1559
|
+
f"{fragment['range']['start_line']}`"
|
|
1560
|
+
),
|
|
1561
|
+
"",
|
|
1562
|
+
_code_block(str(fragment["text"]), "pascal"),
|
|
1563
|
+
]
|
|
1564
|
+
)
|
|
1565
|
+
body.extend(["", "## Members", ""])
|
|
1566
|
+
body.extend(
|
|
1567
|
+
f"- {_link(child.name, _symbol_page(child))} — `{child.kind.value}`"
|
|
1568
|
+
for child in children
|
|
1569
|
+
)
|
|
1570
|
+
if not children:
|
|
1571
|
+
body.append("- None")
|
|
1572
|
+
return page, source, body
|
|
1573
|
+
|
|
1574
|
+
@staticmethod
|
|
1575
|
+
def _direct_symbol_children(symbol: Symbol) -> list[Symbol]:
|
|
1576
|
+
if symbol.member_scope is None:
|
|
1577
|
+
return []
|
|
1578
|
+
children = [
|
|
1579
|
+
child
|
|
1580
|
+
for symbols in symbol.member_scope.symbols.values()
|
|
1581
|
+
for child in symbols
|
|
1582
|
+
if child is not symbol and child.kind.value != "unit"
|
|
1583
|
+
]
|
|
1584
|
+
return sorted(
|
|
1585
|
+
children,
|
|
1586
|
+
key=lambda child: (
|
|
1587
|
+
child.name.casefold(),
|
|
1588
|
+
child.kind.value,
|
|
1589
|
+
child.decl_range.start_line,
|
|
1590
|
+
child.decl_range.start_col,
|
|
1591
|
+
),
|
|
1592
|
+
)
|
|
1593
|
+
|
|
1594
|
+
def _write_references(self) -> None:
|
|
1595
|
+
resolved_symbols = sorted(
|
|
1596
|
+
(
|
|
1597
|
+
symbol
|
|
1598
|
+
for symbol in self._iter_unique_symbols()
|
|
1599
|
+
if id(symbol) in self._references_by_target
|
|
1600
|
+
),
|
|
1601
|
+
key=lambda symbol: (
|
|
1602
|
+
symbol.name.casefold(),
|
|
1603
|
+
_normalized_source(symbol.decl_range.file_name),
|
|
1604
|
+
symbol.decl_range.start_line,
|
|
1605
|
+
symbol.decl_range.start_col,
|
|
1606
|
+
),
|
|
1607
|
+
)
|
|
1608
|
+
unresolved_page = PurePosixPath("references/unresolved.md")
|
|
1609
|
+
unresolved_count = sum(1 for _ in self._iter_unresolved_references())
|
|
1610
|
+
|
|
1611
|
+
def unresolved_body() -> Iterator[str]:
|
|
1612
|
+
yield "# Unresolved semantic references"
|
|
1613
|
+
yield ""
|
|
1614
|
+
yield (
|
|
1615
|
+
"These names were observed by the tolerant semantic index but "
|
|
1616
|
+
"could not be resolved soundly in the current workspace."
|
|
1617
|
+
)
|
|
1618
|
+
yield ""
|
|
1619
|
+
if unresolved_count:
|
|
1620
|
+
yield from self._iter_reference_lines(
|
|
1621
|
+
self._iter_unresolved_references()
|
|
1622
|
+
)
|
|
1623
|
+
else:
|
|
1624
|
+
yield "- None"
|
|
1625
|
+
|
|
1626
|
+
self._concept_iter(
|
|
1627
|
+
unresolved_page,
|
|
1628
|
+
concept_type="Delphi Unresolved References",
|
|
1629
|
+
title="Unresolved semantic references",
|
|
1630
|
+
description="References that remain unresolved in the tolerant semantic index.",
|
|
1631
|
+
tags=("delphi", "references", "diagnostics"),
|
|
1632
|
+
body=unresolved_body(),
|
|
1633
|
+
)
|
|
1634
|
+
|
|
1635
|
+
def reference_index_lines() -> Iterator[str]:
|
|
1636
|
+
yield "# References"
|
|
1637
|
+
yield ""
|
|
1638
|
+
yield "Resolved references are embedded on their target symbol pages."
|
|
1639
|
+
yield ""
|
|
1640
|
+
if resolved_symbols:
|
|
1641
|
+
for symbol in resolved_symbols:
|
|
1642
|
+
count = len(self._references_by_target[id(symbol)])
|
|
1643
|
+
yield f"- {_link(f'{symbol.name} ({count})', _symbol_page(symbol))}"
|
|
1644
|
+
else:
|
|
1645
|
+
yield "- No resolved references"
|
|
1646
|
+
yield ""
|
|
1647
|
+
yield f"- {_link('Unresolved references', unresolved_page)}"
|
|
1648
|
+
|
|
1649
|
+
self._write_lines(
|
|
1650
|
+
PurePosixPath("references/index.md"),
|
|
1651
|
+
reference_index_lines(),
|
|
1652
|
+
)
|
|
1653
|
+
self._progress(
|
|
1654
|
+
"references",
|
|
1655
|
+
1,
|
|
1656
|
+
1,
|
|
1657
|
+
str(unresolved_page),
|
|
1658
|
+
(
|
|
1659
|
+
f"reference pages written ({len(resolved_symbols)} resolved "
|
|
1660
|
+
f"targets, {unresolved_count} unresolved)"
|
|
1661
|
+
),
|
|
1662
|
+
)
|
|
1663
|
+
|
|
1664
|
+
def _write_problems(self) -> None:
|
|
1665
|
+
payload = layer_payload(self.index, "problems")
|
|
1666
|
+
page = PurePosixPath("problems/workspace.md")
|
|
1667
|
+
body = ["# Workspace problems", "", f"Problems: {len(payload['items'])}", ""]
|
|
1668
|
+
body.extend(
|
|
1669
|
+
f"- **{_escape(str(item['kind']))}:** {_escape(str(item['message']))} "
|
|
1670
|
+
f"(`{_escape_code(self._display_path(str(item.get('origin', ''))))}`)"
|
|
1671
|
+
for item in payload["items"]
|
|
1672
|
+
)
|
|
1673
|
+
if not payload["items"]:
|
|
1674
|
+
body.append("- None")
|
|
1675
|
+
body.extend(["", "## Complete problem data", "", _json_block(payload["items"])])
|
|
1676
|
+
self._concept(
|
|
1677
|
+
page,
|
|
1678
|
+
concept_type="Delphi Workspace Problems",
|
|
1679
|
+
title="Workspace problems",
|
|
1680
|
+
description="Discovery and deep-project indexing problems.",
|
|
1681
|
+
tags=("delphi", "problems", "diagnostics"),
|
|
1682
|
+
body=body,
|
|
1683
|
+
)
|
|
1684
|
+
self._directory_index(
|
|
1685
|
+
PurePosixPath("problems/index.md"),
|
|
1686
|
+
"Problems",
|
|
1687
|
+
[("Workspace problems", page)],
|
|
1688
|
+
empty="No problem report was generated.",
|
|
1689
|
+
)
|
|
1690
|
+
|
|
1691
|
+
def _write_metrics(self) -> None:
|
|
1692
|
+
project_name = "Workspace"
|
|
1693
|
+
if len(self.index.discovery.project_files) == 1:
|
|
1694
|
+
project_name = Path(self.index.discovery.project_files[0]).stem
|
|
1695
|
+
metric_source_total = len(self.index.discovery.source_files)
|
|
1696
|
+
self._progress(
|
|
1697
|
+
"metrics",
|
|
1698
|
+
0,
|
|
1699
|
+
metric_source_total or 1,
|
|
1700
|
+
self.index.root,
|
|
1701
|
+
"calculating source metrics",
|
|
1702
|
+
)
|
|
1703
|
+
metrics = build_path_metrics(
|
|
1704
|
+
self.index.root,
|
|
1705
|
+
self.index.discovery.source_files,
|
|
1706
|
+
defines=self.index.discovery.defines,
|
|
1707
|
+
include_paths=self.index.discovery.include_paths,
|
|
1708
|
+
project_config=self.index.discovery.project_config,
|
|
1709
|
+
project_name=project_name,
|
|
1710
|
+
workers=self.workers,
|
|
1711
|
+
on_progress=lambda completed, total, path: self._progress(
|
|
1712
|
+
"metrics",
|
|
1713
|
+
completed,
|
|
1714
|
+
total,
|
|
1715
|
+
path,
|
|
1716
|
+
"source metrics calculated",
|
|
1717
|
+
),
|
|
1718
|
+
)
|
|
1719
|
+
payload = {
|
|
1720
|
+
"project": metrics.to_mapping(),
|
|
1721
|
+
"items": [unit.to_mapping(detail=True) for unit in metrics.file_metrics],
|
|
1722
|
+
}
|
|
1723
|
+
workspace_page = PurePosixPath("metrics/workspace.md")
|
|
1724
|
+
project = payload["project"]
|
|
1725
|
+
workspace_body = [
|
|
1726
|
+
"# Workspace metrics",
|
|
1727
|
+
"",
|
|
1728
|
+
f"- Total LOC: {project['total_loc']}",
|
|
1729
|
+
f"- Total LOC with includes: {project['total_loc_with_includes']}",
|
|
1730
|
+
f"- Units: {project['unit_count']}",
|
|
1731
|
+
f"- Maintainability index: {project['maintainability_index']:.2f}",
|
|
1732
|
+
"",
|
|
1733
|
+
"## Complete metric data",
|
|
1734
|
+
"",
|
|
1735
|
+
_json_block(project),
|
|
1736
|
+
]
|
|
1737
|
+
self._concept(
|
|
1738
|
+
workspace_page,
|
|
1739
|
+
concept_type="Delphi Project Metrics",
|
|
1740
|
+
title="Workspace metrics",
|
|
1741
|
+
description="Aggregate architecture and maintainability metrics.",
|
|
1742
|
+
tags=("delphi", "metrics", "architecture"),
|
|
1743
|
+
body=workspace_body,
|
|
1744
|
+
)
|
|
1745
|
+
|
|
1746
|
+
unit_pages: list[tuple[str, PurePosixPath]] = []
|
|
1747
|
+
metric_item_total = len(payload["items"])
|
|
1748
|
+
for ordinal, item in enumerate(payload["items"], start=1):
|
|
1749
|
+
unit = self._find_unit(str(item["path"]), str(item["name"]))
|
|
1750
|
+
metric_identity = (
|
|
1751
|
+
_normalized_source(unit.source_path)
|
|
1752
|
+
if unit is not None
|
|
1753
|
+
else _normalized_source(str(item["path"]))
|
|
1754
|
+
)
|
|
1755
|
+
page = PurePosixPath("metrics/units") / _document_name(
|
|
1756
|
+
str(item["name"]), metric_identity
|
|
1757
|
+
)
|
|
1758
|
+
if unit is not None:
|
|
1759
|
+
self._metric_page_by_source[
|
|
1760
|
+
_normalized_source(unit.source_path)
|
|
1761
|
+
] = page
|
|
1762
|
+
unit_pages.append((str(item["name"]), page))
|
|
1763
|
+
body = [
|
|
1764
|
+
f"# {_escape(str(item['name']))} metrics",
|
|
1765
|
+
"",
|
|
1766
|
+
f"- Unit: {_link(str(item['name']), unit.page) if unit else _escape(str(item['name']))}",
|
|
1767
|
+
f"- Source: `{_escape_code(self._display_path(str(item['path'])))}`",
|
|
1768
|
+
f"- LOC: {item['lines']['total_lines']}",
|
|
1769
|
+
f"- Cyclomatic maximum: {item['cyclomatic']['maximum']}",
|
|
1770
|
+
f"- Maintainability index: {item['maintainability_index']:.2f}",
|
|
1771
|
+
f"- Instability: {item['instability']:.3f}",
|
|
1772
|
+
f"- Abstractness: {item['abstractness']:.3f}",
|
|
1773
|
+
f"- Distance: {item['distance']:.3f}",
|
|
1774
|
+
"",
|
|
1775
|
+
"## Complete metric data",
|
|
1776
|
+
"",
|
|
1777
|
+
_json_block(item),
|
|
1778
|
+
]
|
|
1779
|
+
self._concept(
|
|
1780
|
+
page,
|
|
1781
|
+
concept_type="Delphi Unit Metrics",
|
|
1782
|
+
title=f"{item['name']} metrics",
|
|
1783
|
+
description=f"Architecture and maintainability metrics for {item['name']}.",
|
|
1784
|
+
tags=("delphi", "metrics", "unit"),
|
|
1785
|
+
body=body,
|
|
1786
|
+
)
|
|
1787
|
+
self._progress(
|
|
1788
|
+
"metric-pages",
|
|
1789
|
+
ordinal,
|
|
1790
|
+
metric_item_total,
|
|
1791
|
+
str(item["path"]),
|
|
1792
|
+
"unit metric page written",
|
|
1793
|
+
)
|
|
1794
|
+
self._directory_index(
|
|
1795
|
+
PurePosixPath("metrics/units/index.md"),
|
|
1796
|
+
"Unit metrics",
|
|
1797
|
+
unit_pages,
|
|
1798
|
+
empty="No per-unit metrics were calculated.",
|
|
1799
|
+
)
|
|
1800
|
+
self._directory_index(
|
|
1801
|
+
PurePosixPath("metrics/index.md"),
|
|
1802
|
+
"Metrics",
|
|
1803
|
+
[("Workspace metrics", workspace_page), ("Unit metrics", PurePosixPath("metrics/units/index.md"))],
|
|
1804
|
+
empty="No metrics were calculated.",
|
|
1805
|
+
)
|
|
1806
|
+
|
|
1807
|
+
def _write_reference_docs(self) -> None:
|
|
1808
|
+
protocol_page = PurePosixPath("reference/protocol.md")
|
|
1809
|
+
protocol_body = [
|
|
1810
|
+
"# Agent Protocol v3",
|
|
1811
|
+
"",
|
|
1812
|
+
f"- Schema version: `{SCHEMA_VERSION}`",
|
|
1813
|
+
f"- Actions: {_inline_code(SUPPORTED_ACTIONS)}",
|
|
1814
|
+
f"- Detail modes: {_inline_code(SUPPORTED_DETAILS)}",
|
|
1815
|
+
f"- Relations: {_inline_code(SUPPORTED_RELATIONS)}",
|
|
1816
|
+
f"- Graphs: {_inline_code(SUPPORTED_GRAPHS)}",
|
|
1817
|
+
f"- Directions: {_inline_code(SUPPORTED_DIRECTIONS)}",
|
|
1818
|
+
"",
|
|
1819
|
+
"Query pages are revision-bound and may use cursors. Live requests "
|
|
1820
|
+
"bound result size with `max_items` and `max_chars`; this static "
|
|
1821
|
+
"bundle instead stores each unique exported fact once.",
|
|
1822
|
+
]
|
|
1823
|
+
self._concept(
|
|
1824
|
+
protocol_page,
|
|
1825
|
+
concept_type="Delphi Agent Protocol Reference",
|
|
1826
|
+
title="Agent Protocol v3",
|
|
1827
|
+
description="Supported query actions, detail modes, relations, and pagination.",
|
|
1828
|
+
tags=("delphi", "protocol", "query"),
|
|
1829
|
+
body=protocol_body,
|
|
1830
|
+
)
|
|
1831
|
+
|
|
1832
|
+
cpg_page = PurePosixPath("reference/cpg.md")
|
|
1833
|
+
cpg_body = [
|
|
1834
|
+
"# Code Property Graph",
|
|
1835
|
+
"",
|
|
1836
|
+
f"Available graph projections: {_inline_code(SUPPORTED_GRAPHS)}.",
|
|
1837
|
+
f"Traversal directions: {_inline_code(SUPPORTED_DIRECTIONS)}.",
|
|
1838
|
+
"",
|
|
1839
|
+
"CPG queries lazily combine AST, control-flow, data-flow, and call "
|
|
1840
|
+
"edges for a focused target. They remain live queries because "
|
|
1841
|
+
"eagerly materializing every overlapping target would repeat source "
|
|
1842
|
+
"parsing and make a multi-million-line export unbounded.",
|
|
1843
|
+
"",
|
|
1844
|
+
"The exporter retains symbol locations, implementations, semantic "
|
|
1845
|
+
"references, metrics, and the supported graph contract needed to "
|
|
1846
|
+
"select a target for a subsequent live CPG query.",
|
|
1847
|
+
]
|
|
1848
|
+
self._concept(
|
|
1849
|
+
cpg_page,
|
|
1850
|
+
concept_type="Delphi Code Property Graph Reference",
|
|
1851
|
+
title="Code Property Graph",
|
|
1852
|
+
description="Available CPG projections, traversal controls, and export boundaries.",
|
|
1853
|
+
tags=("delphi", "cpg", "graph"),
|
|
1854
|
+
body=cpg_body,
|
|
1855
|
+
)
|
|
1856
|
+
|
|
1857
|
+
cache_page = PurePosixPath("reference/cache.md")
|
|
1858
|
+
cache_body = [
|
|
1859
|
+
"# Cache behavior",
|
|
1860
|
+
"",
|
|
1861
|
+
"The shared cache daemon prewarms a repository navigation context, "
|
|
1862
|
+
"reuses it across CLI queries, invalidates it when the workspace "
|
|
1863
|
+
"revision changes, and keeps memory bounded by its configured limit.",
|
|
1864
|
+
"",
|
|
1865
|
+
"The wiki export is independent of daemon lifetime. It performs one "
|
|
1866
|
+
"parallel index build and writes a portable on-disk snapshot.",
|
|
1867
|
+
]
|
|
1868
|
+
self._concept(
|
|
1869
|
+
cache_page,
|
|
1870
|
+
concept_type="Delphi Cache Reference",
|
|
1871
|
+
title="Cache behavior",
|
|
1872
|
+
description="Relationship between the live bounded cache and static wiki exports.",
|
|
1873
|
+
tags=("delphi", "cache", "performance"),
|
|
1874
|
+
body=cache_body,
|
|
1875
|
+
)
|
|
1876
|
+
|
|
1877
|
+
layers_page = PurePosixPath("reference/layers.md")
|
|
1878
|
+
layers_body = [
|
|
1879
|
+
"# Layer mapping",
|
|
1880
|
+
"",
|
|
1881
|
+
"- `overview` → `overview.md`",
|
|
1882
|
+
"- `projects` → `projects/`",
|
|
1883
|
+
"- `units` and `unit` → `units/`",
|
|
1884
|
+
"- `symbols`, `symbol`, and `implementation` → `symbols/`",
|
|
1885
|
+
"- `references` → symbol pages and `references/`",
|
|
1886
|
+
"- `problems` → `problems/`",
|
|
1887
|
+
"- `metrics` → `metrics/`",
|
|
1888
|
+
]
|
|
1889
|
+
self._concept(
|
|
1890
|
+
layers_page,
|
|
1891
|
+
concept_type="Delphi Layer Export Reference",
|
|
1892
|
+
title="Layer mapping",
|
|
1893
|
+
description="Mapping from live layered views to static wiki concepts.",
|
|
1894
|
+
tags=("delphi", "layers", "export"),
|
|
1895
|
+
body=layers_body,
|
|
1896
|
+
)
|
|
1897
|
+
self._directory_index(
|
|
1898
|
+
PurePosixPath("reference/index.md"),
|
|
1899
|
+
"Query and graph reference",
|
|
1900
|
+
[
|
|
1901
|
+
("Agent Protocol v3", protocol_page),
|
|
1902
|
+
("Layer mapping", layers_page),
|
|
1903
|
+
("Code Property Graph", cpg_page),
|
|
1904
|
+
("Cache behavior", cache_page),
|
|
1905
|
+
],
|
|
1906
|
+
empty="No reference documents were generated.",
|
|
1907
|
+
)
|
|
1908
|
+
|
|
1909
|
+
def _reference_lines(self, references: Iterable[SymbolReference]) -> list[str]:
|
|
1910
|
+
return list(self._iter_reference_lines(references))
|
|
1911
|
+
|
|
1912
|
+
def _iter_reference_lines(
|
|
1913
|
+
self,
|
|
1914
|
+
references: Iterable[SymbolReference],
|
|
1915
|
+
) -> Iterator[str]:
|
|
1916
|
+
for reference in references:
|
|
1917
|
+
yield self._reference_line(
|
|
1918
|
+
reference,
|
|
1919
|
+
target_key=_reference_target_key(reference),
|
|
1920
|
+
)
|
|
1921
|
+
|
|
1922
|
+
def _reference_line(
|
|
1923
|
+
self,
|
|
1924
|
+
reference: SymbolReference,
|
|
1925
|
+
*,
|
|
1926
|
+
target_key: str | None,
|
|
1927
|
+
) -> str:
|
|
1928
|
+
location = (
|
|
1929
|
+
f"{self._display_path(reference.ref_range.file_name)}:"
|
|
1930
|
+
f"{reference.ref_range.start_line}:{reference.ref_range.start_col}"
|
|
1931
|
+
)
|
|
1932
|
+
target = reference.resolved
|
|
1933
|
+
target_text = (
|
|
1934
|
+
f" → {_link(target.name, PurePosixPath('symbols') / _document_name(target.name, target_key))}"
|
|
1935
|
+
if target is not None and target_key is not None
|
|
1936
|
+
else ""
|
|
1937
|
+
)
|
|
1938
|
+
return (
|
|
1939
|
+
f"- `{reference.kind.value}` `{_escape_code(reference.name)}` at "
|
|
1940
|
+
f"`{_escape_code(location)}`{target_text}"
|
|
1941
|
+
)
|
|
1942
|
+
|
|
1943
|
+
def _find_unit(self, source: str, name: str) -> _UnitRecord | None:
|
|
1944
|
+
source_path = Path(source).expanduser()
|
|
1945
|
+
if not source_path.is_absolute():
|
|
1946
|
+
source_path = Path(self.index.root) / source_path
|
|
1947
|
+
return self._unit_by_source.get(
|
|
1948
|
+
_normalized_source(str(source_path))
|
|
1949
|
+
) or self._unit_by_name.get(name.casefold())
|
|
1950
|
+
|
|
1951
|
+
def _display_path(self, value: str) -> str:
|
|
1952
|
+
if not value:
|
|
1953
|
+
return ""
|
|
1954
|
+
cached = self._display_path_cache.get(value)
|
|
1955
|
+
if cached is not None:
|
|
1956
|
+
return cached
|
|
1957
|
+
path = Path(value)
|
|
1958
|
+
try:
|
|
1959
|
+
displayed = path.resolve().relative_to(Path(self.index.root)).as_posix()
|
|
1960
|
+
except (OSError, ValueError):
|
|
1961
|
+
displayed = path.as_posix()
|
|
1962
|
+
self._display_path_cache[value] = displayed
|
|
1963
|
+
return displayed
|
|
1964
|
+
|
|
1965
|
+
def _concept(
|
|
1966
|
+
self,
|
|
1967
|
+
page: PurePosixPath,
|
|
1968
|
+
*,
|
|
1969
|
+
concept_type: str,
|
|
1970
|
+
title: str,
|
|
1971
|
+
description: str,
|
|
1972
|
+
tags: tuple[str, ...],
|
|
1973
|
+
body: list[str],
|
|
1974
|
+
) -> None:
|
|
1975
|
+
frontmatter = [
|
|
1976
|
+
"---",
|
|
1977
|
+
f"type: {json.dumps(concept_type, ensure_ascii=False)}",
|
|
1978
|
+
f"title: {json.dumps(title, ensure_ascii=False)}",
|
|
1979
|
+
f"description: {json.dumps(description, ensure_ascii=False)}",
|
|
1980
|
+
f"tags: {json.dumps(list(tags), ensure_ascii=False)}",
|
|
1981
|
+
'status: "stable"',
|
|
1982
|
+
"---",
|
|
1983
|
+
]
|
|
1984
|
+
self._write(page, "\n".join([*frontmatter, *body]).rstrip() + "\n")
|
|
1985
|
+
|
|
1986
|
+
def _concept_iter(
|
|
1987
|
+
self,
|
|
1988
|
+
page: PurePosixPath,
|
|
1989
|
+
*,
|
|
1990
|
+
concept_type: str,
|
|
1991
|
+
title: str,
|
|
1992
|
+
description: str,
|
|
1993
|
+
tags: tuple[str, ...],
|
|
1994
|
+
body: Iterable[str],
|
|
1995
|
+
) -> None:
|
|
1996
|
+
frontmatter = (
|
|
1997
|
+
"---",
|
|
1998
|
+
f"type: {json.dumps(concept_type, ensure_ascii=False)}",
|
|
1999
|
+
f"title: {json.dumps(title, ensure_ascii=False)}",
|
|
2000
|
+
f"description: {json.dumps(description, ensure_ascii=False)}",
|
|
2001
|
+
f"tags: {json.dumps(list(tags), ensure_ascii=False)}",
|
|
2002
|
+
'status: "stable"',
|
|
2003
|
+
"---",
|
|
2004
|
+
)
|
|
2005
|
+
self._write_lines(page, chain(frontmatter, body))
|
|
2006
|
+
|
|
2007
|
+
def _directory_index(
|
|
2008
|
+
self,
|
|
2009
|
+
page: PurePosixPath,
|
|
2010
|
+
title: str,
|
|
2011
|
+
items: list[tuple[str, PurePosixPath]],
|
|
2012
|
+
*,
|
|
2013
|
+
empty: str,
|
|
2014
|
+
) -> None:
|
|
2015
|
+
body = [f"# {_escape(title)}", ""]
|
|
2016
|
+
body.extend(f"- {_link(label, target)}" for label, target in items)
|
|
2017
|
+
if not items:
|
|
2018
|
+
body.append(empty)
|
|
2019
|
+
self._write(page, "\n".join(body).rstrip() + "\n")
|
|
2020
|
+
|
|
2021
|
+
def _directory_index_iter(
|
|
2022
|
+
self,
|
|
2023
|
+
page: PurePosixPath,
|
|
2024
|
+
title: str,
|
|
2025
|
+
items: Iterable[tuple[str, PurePosixPath]],
|
|
2026
|
+
*,
|
|
2027
|
+
empty: str,
|
|
2028
|
+
) -> None:
|
|
2029
|
+
iterator = iter(items)
|
|
2030
|
+
first = next(iterator, None)
|
|
2031
|
+
|
|
2032
|
+
def lines() -> Iterator[str]:
|
|
2033
|
+
yield f"# {_escape(title)}"
|
|
2034
|
+
yield ""
|
|
2035
|
+
if first is None:
|
|
2036
|
+
yield empty
|
|
2037
|
+
return
|
|
2038
|
+
yield f"- {_link(first[0], first[1])}"
|
|
2039
|
+
for label, target in iterator:
|
|
2040
|
+
yield f"- {_link(label, target)}"
|
|
2041
|
+
|
|
2042
|
+
self._write_lines(page, lines())
|
|
2043
|
+
|
|
2044
|
+
def _write(self, relative_path: PurePosixPath, text: str) -> None:
|
|
2045
|
+
target = self.destination.joinpath(*relative_path.parts)
|
|
2046
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
2047
|
+
target.write_text(text, encoding="utf-8", newline="\n")
|
|
2048
|
+
self.documents += 1
|
|
2049
|
+
|
|
2050
|
+
def _write_lines(
|
|
2051
|
+
self,
|
|
2052
|
+
relative_path: PurePosixPath,
|
|
2053
|
+
lines: Iterable[str],
|
|
2054
|
+
) -> None:
|
|
2055
|
+
target = self.destination.joinpath(*relative_path.parts)
|
|
2056
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
2057
|
+
with target.open("w", encoding="utf-8", newline="\n") as stream:
|
|
2058
|
+
for line in lines:
|
|
2059
|
+
stream.write(line)
|
|
2060
|
+
stream.write("\n")
|
|
2061
|
+
self.documents += 1
|
|
2062
|
+
|
|
2063
|
+
|
|
2064
|
+
class _StreamingWikiWriter(_WikiWriter):
|
|
2065
|
+
"""Write a wiki while retaining at most the current unit and reference group."""
|
|
2066
|
+
|
|
2067
|
+
def __init__(
|
|
2068
|
+
self,
|
|
2069
|
+
index: CodebaseIndex,
|
|
2070
|
+
destination: Path,
|
|
2071
|
+
*,
|
|
2072
|
+
models: _StoredWikiModels,
|
|
2073
|
+
projects: _WikiProjectStore,
|
|
2074
|
+
unique_catalog: Path,
|
|
2075
|
+
name_catalog: Path,
|
|
2076
|
+
symbol_count: int,
|
|
2077
|
+
workspace: Path,
|
|
2078
|
+
guard: WikiMemoryGuard,
|
|
2079
|
+
strategy: _WikiExecutionStrategy,
|
|
2080
|
+
on_progress: WikiProgressCallback | None,
|
|
2081
|
+
) -> None:
|
|
2082
|
+
self.index = index
|
|
2083
|
+
self.destination = destination
|
|
2084
|
+
self.workers = strategy.workers
|
|
2085
|
+
self._on_progress = on_progress
|
|
2086
|
+
self.documents = 0
|
|
2087
|
+
self._source_cache: dict[str, list[str]] = _BoundedSourceCache()
|
|
2088
|
+
self._display_path_cache: dict[str, str] = {}
|
|
2089
|
+
self._symbol_count = symbol_count
|
|
2090
|
+
self.units = models.unit_records()
|
|
2091
|
+
self._unit_by_source = {
|
|
2092
|
+
_normalized_source(record.source_path): record for record in self.units
|
|
2093
|
+
}
|
|
2094
|
+
self._unit_by_name = {record.name.casefold(): record for record in self.units}
|
|
2095
|
+
self._references_by_target: dict[int, list[SymbolReference]] = {}
|
|
2096
|
+
self._projects_by_source: dict[str, list[tuple[str, PurePosixPath]]] = {}
|
|
2097
|
+
self._metric_page_by_source: dict[str, PurePosixPath] = {}
|
|
2098
|
+
self.models = models
|
|
2099
|
+
self.projects = projects
|
|
2100
|
+
self.unique_catalog = unique_catalog
|
|
2101
|
+
self.name_catalog = name_catalog
|
|
2102
|
+
self.workspace = workspace
|
|
2103
|
+
self.guard = guard
|
|
2104
|
+
self.strategy = strategy
|
|
2105
|
+
self.memberships = _ProjectMembershipStore(workspace / "membership.sqlite3")
|
|
2106
|
+
self.raw_references = workspace / "references-raw.jsonl"
|
|
2107
|
+
self.raw_unresolved = workspace / "unresolved-raw.jsonl"
|
|
2108
|
+
|
|
2109
|
+
def close(self) -> None:
|
|
2110
|
+
self.memberships.close()
|
|
2111
|
+
|
|
2112
|
+
def clear_caches(self) -> None:
|
|
2113
|
+
self.models.clear_cache()
|
|
2114
|
+
self._source_cache.clear()
|
|
2115
|
+
self._display_path_cache.clear()
|
|
2116
|
+
|
|
2117
|
+
def write(self) -> None:
|
|
2118
|
+
self._write_root_index()
|
|
2119
|
+
self._write_overview()
|
|
2120
|
+
self._write_manifest()
|
|
2121
|
+
self._write_projects()
|
|
2122
|
+
self.memberships.commit()
|
|
2123
|
+
self._write_metrics()
|
|
2124
|
+
self.guard.check(phase="metrics")
|
|
2125
|
+
self._write_streaming_units_and_symbol_bases()
|
|
2126
|
+
sorted_references = self._sort_references()
|
|
2127
|
+
self._finish_symbol_pages_and_references(sorted_references)
|
|
2128
|
+
self._write_streaming_problems()
|
|
2129
|
+
self._write_reference_docs()
|
|
2130
|
+
|
|
2131
|
+
def _project_items(self) -> Iterable[dict[str, Any]]:
|
|
2132
|
+
return self.projects.items()
|
|
2133
|
+
|
|
2134
|
+
def _record_project_membership(
|
|
2135
|
+
self,
|
|
2136
|
+
source_path: str,
|
|
2137
|
+
ordinal: int,
|
|
2138
|
+
title: str,
|
|
2139
|
+
page: PurePosixPath,
|
|
2140
|
+
) -> None:
|
|
2141
|
+
self.memberships.add(source_path, ordinal, title, page)
|
|
2142
|
+
|
|
2143
|
+
def _project_memberships_for(
|
|
2144
|
+
self,
|
|
2145
|
+
source_path: str,
|
|
2146
|
+
) -> list[tuple[str, PurePosixPath]]:
|
|
2147
|
+
return self.memberships.for_source(source_path)
|
|
2148
|
+
|
|
2149
|
+
def _write_streaming_units_and_symbol_bases(self) -> None:
|
|
2150
|
+
parser = DelphiParser(
|
|
2151
|
+
include_paths=self.index.discovery.include_paths,
|
|
2152
|
+
defines=self.index.discovery.defines,
|
|
2153
|
+
include_loader=workspace_include_loader(
|
|
2154
|
+
self.index.discovery.project_config,
|
|
2155
|
+
self.index.discovery.include_paths,
|
|
2156
|
+
),
|
|
2157
|
+
mode=ParserMode.TOLERANT,
|
|
2158
|
+
)
|
|
2159
|
+
unit_pages: list[tuple[str, PurePosixPath]] = []
|
|
2160
|
+
self.raw_references.touch()
|
|
2161
|
+
self.raw_unresolved.touch()
|
|
2162
|
+
total = len(self.units)
|
|
2163
|
+
source_ordinal = {source: ordinal for ordinal, source in enumerate(self.models)}
|
|
2164
|
+
with (
|
|
2165
|
+
self.raw_references.open("a", encoding="utf-8", newline="\n") as resolved_stream,
|
|
2166
|
+
self.raw_unresolved.open("a", encoding="utf-8", newline="\n") as unresolved_stream,
|
|
2167
|
+
):
|
|
2168
|
+
for ordinal, unit in enumerate(self.units, start=1):
|
|
2169
|
+
outline_model = self.models[unit.source_path]
|
|
2170
|
+
references = self._parse_unit_references(
|
|
2171
|
+
unit,
|
|
2172
|
+
outline_model,
|
|
2173
|
+
parser,
|
|
2174
|
+
)
|
|
2175
|
+
indexed_references = list(enumerate(references))
|
|
2176
|
+
indexed_references.sort(key=lambda item: _reference_sort_key(item[1]))
|
|
2177
|
+
prepared_references = [
|
|
2178
|
+
(
|
|
2179
|
+
reference_ordinal,
|
|
2180
|
+
reference,
|
|
2181
|
+
_reference_target_key(reference),
|
|
2182
|
+
)
|
|
2183
|
+
for reference_ordinal, reference in indexed_references
|
|
2184
|
+
]
|
|
2185
|
+
reference_lines = [
|
|
2186
|
+
self._reference_line(reference, target_key=target_key)
|
|
2187
|
+
for _reference_ordinal, reference, target_key in prepared_references
|
|
2188
|
+
]
|
|
2189
|
+
self._write_streaming_unit_page(
|
|
2190
|
+
unit,
|
|
2191
|
+
outline_model,
|
|
2192
|
+
reference_lines,
|
|
2193
|
+
)
|
|
2194
|
+
unit_pages.append((unit.name, unit.page))
|
|
2195
|
+
self._write_symbol_bases(outline_model)
|
|
2196
|
+
model_ordinal = source_ordinal[unit.source_path]
|
|
2197
|
+
for (reference_ordinal, reference, target_key), line in zip(
|
|
2198
|
+
prepared_references,
|
|
2199
|
+
reference_lines,
|
|
2200
|
+
strict=True,
|
|
2201
|
+
):
|
|
2202
|
+
record = self._reference_record(
|
|
2203
|
+
reference,
|
|
2204
|
+
line=line,
|
|
2205
|
+
model_ordinal=model_ordinal,
|
|
2206
|
+
reference_ordinal=reference_ordinal,
|
|
2207
|
+
target_key=target_key,
|
|
2208
|
+
)
|
|
2209
|
+
if record["resolved"] and record.get("target_key") is None:
|
|
2210
|
+
continue
|
|
2211
|
+
stream = resolved_stream if record["resolved"] else unresolved_stream
|
|
2212
|
+
stream.write(
|
|
2213
|
+
json.dumps(record, ensure_ascii=False, separators=(",", ":"))
|
|
2214
|
+
)
|
|
2215
|
+
stream.write("\n")
|
|
2216
|
+
del indexed_references
|
|
2217
|
+
del prepared_references
|
|
2218
|
+
del references
|
|
2219
|
+
del outline_model
|
|
2220
|
+
if ordinal % _STREAMING_UNIT_GC_INTERVAL == 0:
|
|
2221
|
+
gc.collect()
|
|
2222
|
+
self._progress(
|
|
2223
|
+
"units",
|
|
2224
|
+
ordinal,
|
|
2225
|
+
total,
|
|
2226
|
+
unit.source_path,
|
|
2227
|
+
"unit and symbol pages streamed",
|
|
2228
|
+
)
|
|
2229
|
+
self.guard.check(phase="unit export")
|
|
2230
|
+
gc.collect()
|
|
2231
|
+
self.guard.check(phase="unit export", force_process_tree=True)
|
|
2232
|
+
unit_pages.sort(key=lambda item: (item[0].casefold(), item[1].as_posix()))
|
|
2233
|
+
self._directory_index(
|
|
2234
|
+
PurePosixPath("units/index.md"),
|
|
2235
|
+
"Units",
|
|
2236
|
+
unit_pages,
|
|
2237
|
+
empty="No Delphi units were indexed.",
|
|
2238
|
+
)
|
|
2239
|
+
|
|
2240
|
+
def _parse_unit_references(
|
|
2241
|
+
self,
|
|
2242
|
+
unit: _UnitRecord,
|
|
2243
|
+
outline_model: SemanticModel,
|
|
2244
|
+
parser: DelphiParser,
|
|
2245
|
+
) -> list[SymbolReference]:
|
|
2246
|
+
lazy_index = _LazyWikiSymbolIndex(self.models)
|
|
2247
|
+
outline_by_exact: dict[tuple[str, str, str, int, int], Symbol] = {}
|
|
2248
|
+
outline_by_source: dict[tuple[str, str, str], Symbol] = {}
|
|
2249
|
+
for symbol in _iter_symbols(outline_model.unit_scope):
|
|
2250
|
+
source_key = (
|
|
2251
|
+
symbol.name.casefold(),
|
|
2252
|
+
symbol.kind.value,
|
|
2253
|
+
symbol.decl_range.file_name.casefold(),
|
|
2254
|
+
)
|
|
2255
|
+
outline_by_source.setdefault(source_key, symbol)
|
|
2256
|
+
outline_by_exact[
|
|
2257
|
+
(
|
|
2258
|
+
*source_key,
|
|
2259
|
+
symbol.name_range.start_line,
|
|
2260
|
+
symbol.name_range.start_col,
|
|
2261
|
+
)
|
|
2262
|
+
] = symbol
|
|
2263
|
+
try:
|
|
2264
|
+
source = read_source_text(Path(unit.source_path))
|
|
2265
|
+
parsed = parser.parse(
|
|
2266
|
+
source,
|
|
2267
|
+
unit.source_path,
|
|
2268
|
+
build_semantic=True,
|
|
2269
|
+
index=lazy_index,
|
|
2270
|
+
)
|
|
2271
|
+
except (OSError, UnicodeError):
|
|
2272
|
+
return []
|
|
2273
|
+
if parsed.semantic is None:
|
|
2274
|
+
return []
|
|
2275
|
+
mapped_references: list[SymbolReference] = []
|
|
2276
|
+
for reference in parsed.semantic.references:
|
|
2277
|
+
resolved = reference.resolved
|
|
2278
|
+
mapped = (
|
|
2279
|
+
resolved
|
|
2280
|
+
if resolved is not None
|
|
2281
|
+
and _scope_root(resolved.scope) is not parsed.semantic.unit_scope
|
|
2282
|
+
else None
|
|
2283
|
+
)
|
|
2284
|
+
if resolved is not None and mapped is None:
|
|
2285
|
+
source_key = (
|
|
2286
|
+
resolved.name.casefold(),
|
|
2287
|
+
resolved.kind.value,
|
|
2288
|
+
resolved.decl_range.file_name.casefold(),
|
|
2289
|
+
)
|
|
2290
|
+
mapped = outline_by_exact.get(
|
|
2291
|
+
(
|
|
2292
|
+
*source_key,
|
|
2293
|
+
resolved.name_range.start_line,
|
|
2294
|
+
resolved.name_range.start_col,
|
|
2295
|
+
)
|
|
2296
|
+
) or outline_by_source.get(source_key)
|
|
2297
|
+
mapped_references.append(
|
|
2298
|
+
SymbolReference(
|
|
2299
|
+
name=reference.name,
|
|
2300
|
+
kind=reference.kind,
|
|
2301
|
+
ref_range=reference.ref_range,
|
|
2302
|
+
scope=outline_model.unit_scope,
|
|
2303
|
+
resolved=mapped,
|
|
2304
|
+
)
|
|
2305
|
+
)
|
|
2306
|
+
return mapped_references
|
|
2307
|
+
|
|
2308
|
+
def _write_streaming_unit_page(
|
|
2309
|
+
self,
|
|
2310
|
+
unit: _UnitRecord,
|
|
2311
|
+
model: SemanticModel,
|
|
2312
|
+
reference_lines: list[str],
|
|
2313
|
+
) -> None:
|
|
2314
|
+
symbols = sorted(
|
|
2315
|
+
_iter_symbols(model.unit_scope, include_unit=False),
|
|
2316
|
+
key=lambda symbol: (
|
|
2317
|
+
symbol.decl_range.start_line,
|
|
2318
|
+
symbol.decl_range.start_col,
|
|
2319
|
+
symbol.name.casefold(),
|
|
2320
|
+
symbol.kind.value,
|
|
2321
|
+
),
|
|
2322
|
+
)
|
|
2323
|
+
projects = self._project_memberships_for(unit.source_path)
|
|
2324
|
+
body = [
|
|
2325
|
+
f"# {_escape(unit.name)}",
|
|
2326
|
+
"",
|
|
2327
|
+
f"- Source: `{_escape_code(self._display_path(unit.source_path))}`",
|
|
2328
|
+
f"- Symbols: {len(symbols)}",
|
|
2329
|
+
f"- Semantic references: {len(reference_lines)}",
|
|
2330
|
+
"",
|
|
2331
|
+
"## Projects",
|
|
2332
|
+
"",
|
|
2333
|
+
]
|
|
2334
|
+
body.extend(f"- {_link(title, page)}" for title, page in projects)
|
|
2335
|
+
if not projects:
|
|
2336
|
+
body.append("- No concrete project membership was resolved")
|
|
2337
|
+
body.extend(["", "## Symbols", ""])
|
|
2338
|
+
body.extend(
|
|
2339
|
+
f"- {_link(symbol.name, _symbol_page(symbol))} "
|
|
2340
|
+
f"— `{symbol.kind.value}` at line {symbol.decl_range.start_line}"
|
|
2341
|
+
for symbol in symbols
|
|
2342
|
+
)
|
|
2343
|
+
if not symbols:
|
|
2344
|
+
body.append("- None")
|
|
2345
|
+
body.extend(["", "## References found in this unit", ""])
|
|
2346
|
+
body.extend(reference_lines or ["- None"])
|
|
2347
|
+
metrics_page = self._metric_page_by_source.get(
|
|
2348
|
+
_normalized_source(unit.source_path)
|
|
2349
|
+
)
|
|
2350
|
+
if metrics_page is not None:
|
|
2351
|
+
body.extend(
|
|
2352
|
+
["", "## Related knowledge", "", f"- {_link('Unit metrics', metrics_page)}"]
|
|
2353
|
+
)
|
|
2354
|
+
self._concept(
|
|
2355
|
+
unit.page,
|
|
2356
|
+
concept_type="Delphi Unit",
|
|
2357
|
+
title=unit.name,
|
|
2358
|
+
description=f"Symbols and references in {unit.name}.",
|
|
2359
|
+
tags=("delphi", "unit"),
|
|
2360
|
+
body=body,
|
|
2361
|
+
)
|
|
2362
|
+
|
|
2363
|
+
def _write_symbol_bases(self, model: SemanticModel) -> None:
|
|
2364
|
+
for symbol in _iter_symbols(model.unit_scope, include_unit=False):
|
|
2365
|
+
page = _symbol_page(symbol)
|
|
2366
|
+
target = self.destination.joinpath(*page.parts)
|
|
2367
|
+
if target.exists():
|
|
2368
|
+
continue
|
|
2369
|
+
page, source, body = self._symbol_body_prefix(symbol)
|
|
2370
|
+
body.extend(["", "## Semantic references", ""])
|
|
2371
|
+
self._concept(
|
|
2372
|
+
page,
|
|
2373
|
+
concept_type=f"Delphi {symbol.kind.value.replace('_', ' ').title()} Symbol",
|
|
2374
|
+
title=symbol.name,
|
|
2375
|
+
description=(
|
|
2376
|
+
f"{symbol.kind.value} {symbol.name} in {source} at "
|
|
2377
|
+
f"line {symbol.decl_range.start_line}."
|
|
2378
|
+
),
|
|
2379
|
+
tags=("delphi", "symbol", symbol.kind.value),
|
|
2380
|
+
body=body,
|
|
2381
|
+
)
|
|
2382
|
+
|
|
2383
|
+
def _reference_record(
|
|
2384
|
+
self,
|
|
2385
|
+
reference: SymbolReference,
|
|
2386
|
+
*,
|
|
2387
|
+
line: str,
|
|
2388
|
+
model_ordinal: int,
|
|
2389
|
+
reference_ordinal: int,
|
|
2390
|
+
target_key: str | None,
|
|
2391
|
+
) -> dict[str, Any]:
|
|
2392
|
+
target = reference.resolved
|
|
2393
|
+
source_symbol = (
|
|
2394
|
+
reference.scope.owner.name
|
|
2395
|
+
if reference.scope.owner is not None
|
|
2396
|
+
else reference.scope.name
|
|
2397
|
+
)
|
|
2398
|
+
reference_key = (
|
|
2399
|
+
_normalized_source(reference.ref_range.file_name),
|
|
2400
|
+
reference.ref_range.start_line,
|
|
2401
|
+
reference.ref_range.start_col,
|
|
2402
|
+
reference.kind.value,
|
|
2403
|
+
reference.name.casefold(),
|
|
2404
|
+
)
|
|
2405
|
+
dedupe_key = "|".join(
|
|
2406
|
+
(
|
|
2407
|
+
reference.name.casefold(),
|
|
2408
|
+
reference.ref_range.file_name,
|
|
2409
|
+
str(reference.ref_range.start_line),
|
|
2410
|
+
str(reference.ref_range.start_col),
|
|
2411
|
+
reference.kind.value,
|
|
2412
|
+
)
|
|
2413
|
+
)
|
|
2414
|
+
order_key = "|".join(
|
|
2415
|
+
(
|
|
2416
|
+
f"{model_ordinal:012d}",
|
|
2417
|
+
reference_key[0],
|
|
2418
|
+
f"{reference_key[1]:012d}",
|
|
2419
|
+
f"{reference_key[2]:012d}",
|
|
2420
|
+
reference_key[3],
|
|
2421
|
+
reference_key[4],
|
|
2422
|
+
f"{reference_ordinal:012d}",
|
|
2423
|
+
)
|
|
2424
|
+
)
|
|
2425
|
+
return {
|
|
2426
|
+
"column": reference.ref_range.start_col,
|
|
2427
|
+
"dedupe_key": dedupe_key,
|
|
2428
|
+
"file": reference.ref_range.file_name,
|
|
2429
|
+
"file_key": reference_key[0],
|
|
2430
|
+
"kind": reference.kind.value,
|
|
2431
|
+
"line": line,
|
|
2432
|
+
"line_number": reference.ref_range.start_line,
|
|
2433
|
+
"model_ordinal": f"{model_ordinal:012d}",
|
|
2434
|
+
"name": reference.name,
|
|
2435
|
+
"name_key": reference.name.casefold(),
|
|
2436
|
+
"order_key": order_key,
|
|
2437
|
+
"reference_ordinal": f"{reference_ordinal:012d}",
|
|
2438
|
+
"resolved": target is not None,
|
|
2439
|
+
"source_symbol": source_symbol,
|
|
2440
|
+
"target_key": target_key,
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
def _sort_references(self) -> Path:
|
|
2444
|
+
sorted_references = self.workspace / "references-sorted.jsonl"
|
|
2445
|
+
sort_jsonl_records(
|
|
2446
|
+
self.raw_references,
|
|
2447
|
+
sorted_references,
|
|
2448
|
+
key=lambda item: (
|
|
2449
|
+
str(item["target_key"]),
|
|
2450
|
+
str(item["file_key"]),
|
|
2451
|
+
f"{int(item['line_number']):012d}",
|
|
2452
|
+
f"{int(item['column']):012d}",
|
|
2453
|
+
str(item["kind"]),
|
|
2454
|
+
str(item["name_key"]),
|
|
2455
|
+
str(item["model_ordinal"]),
|
|
2456
|
+
str(item["reference_ordinal"]),
|
|
2457
|
+
),
|
|
2458
|
+
max_records_per_run=self.strategy.sort_records_per_run,
|
|
2459
|
+
max_bytes_per_run=self.strategy.sort_bytes_per_run,
|
|
2460
|
+
temp_dir=self.workspace,
|
|
2461
|
+
heartbeat=lambda: self.guard.check(
|
|
2462
|
+
phase="external reference sort",
|
|
2463
|
+
),
|
|
2464
|
+
force_heartbeat=lambda: self.guard.check(
|
|
2465
|
+
phase="external reference sort",
|
|
2466
|
+
force_process_tree=True,
|
|
2467
|
+
),
|
|
2468
|
+
)
|
|
2469
|
+
self.guard.check(phase="external reference sort", force_process_tree=True)
|
|
2470
|
+
return sorted_references
|
|
2471
|
+
|
|
2472
|
+
def _finish_symbol_pages_and_references(self, sorted_references: Path) -> None:
|
|
2473
|
+
resolved_index = self.workspace / "resolved-index-raw.jsonl"
|
|
2474
|
+
resolved_index.touch()
|
|
2475
|
+
references = _iter_json_objects(sorted_references)
|
|
2476
|
+
current_reference = next(references, None)
|
|
2477
|
+
symbol_total = self.symbol_count
|
|
2478
|
+
with resolved_index.open("a", encoding="utf-8", newline="\n") as resolved_stream:
|
|
2479
|
+
for ordinal, symbol_item in enumerate(
|
|
2480
|
+
_iter_json_objects(self.unique_catalog),
|
|
2481
|
+
start=1,
|
|
2482
|
+
):
|
|
2483
|
+
identity = str(symbol_item["identity"])
|
|
2484
|
+
page = PurePosixPath(str(symbol_item["page"]))
|
|
2485
|
+
page_path = self.destination.joinpath(*page.parts)
|
|
2486
|
+
count = 0
|
|
2487
|
+
with page_path.open("a", encoding="utf-8", newline="\n") as page_stream:
|
|
2488
|
+
page_stream.write("\n")
|
|
2489
|
+
while (
|
|
2490
|
+
current_reference is not None
|
|
2491
|
+
and str(current_reference["target_key"]) < identity
|
|
2492
|
+
):
|
|
2493
|
+
current_reference = next(references, None)
|
|
2494
|
+
while (
|
|
2495
|
+
current_reference is not None
|
|
2496
|
+
and str(current_reference["target_key"]) == identity
|
|
2497
|
+
):
|
|
2498
|
+
page_stream.write(str(current_reference["line"]))
|
|
2499
|
+
page_stream.write("\n")
|
|
2500
|
+
count += 1
|
|
2501
|
+
current_reference = next(references, None)
|
|
2502
|
+
if count == 0:
|
|
2503
|
+
page_stream.write("- None\n")
|
|
2504
|
+
if count:
|
|
2505
|
+
resolved_item = {
|
|
2506
|
+
"count": count,
|
|
2507
|
+
"kind": symbol_item["kind"],
|
|
2508
|
+
"name": symbol_item["name"],
|
|
2509
|
+
"name_key": symbol_item["name_key"],
|
|
2510
|
+
"page": symbol_item["page"],
|
|
2511
|
+
"source_key": symbol_item["source_key"],
|
|
2512
|
+
"start_col": symbol_item["start_col"],
|
|
2513
|
+
"start_line": symbol_item["start_line"],
|
|
2514
|
+
}
|
|
2515
|
+
resolved_stream.write(
|
|
2516
|
+
json.dumps(
|
|
2517
|
+
resolved_item,
|
|
2518
|
+
ensure_ascii=False,
|
|
2519
|
+
separators=(",", ":"),
|
|
2520
|
+
)
|
|
2521
|
+
)
|
|
2522
|
+
resolved_stream.write("\n")
|
|
2523
|
+
self._progress(
|
|
2524
|
+
"symbols",
|
|
2525
|
+
ordinal,
|
|
2526
|
+
symbol_total,
|
|
2527
|
+
str(symbol_item["source_key"]),
|
|
2528
|
+
f"symbol page finalized: {symbol_item['name']}",
|
|
2529
|
+
)
|
|
2530
|
+
if ordinal % 1_000 == 0:
|
|
2531
|
+
self.guard.check(phase="reverse references")
|
|
2532
|
+
self.guard.check(phase="reverse references", force_process_tree=True)
|
|
2533
|
+
self._write_symbol_index()
|
|
2534
|
+
unresolved_count = self._write_unresolved_page()
|
|
2535
|
+
resolved_count = self._write_reference_index(resolved_index)
|
|
2536
|
+
self._progress(
|
|
2537
|
+
"references",
|
|
2538
|
+
1,
|
|
2539
|
+
1,
|
|
2540
|
+
"references/unresolved.md",
|
|
2541
|
+
(
|
|
2542
|
+
f"reference pages written ({resolved_count} resolved "
|
|
2543
|
+
f"targets, {unresolved_count} unresolved)"
|
|
2544
|
+
),
|
|
2545
|
+
)
|
|
2546
|
+
|
|
2547
|
+
def _write_symbol_index(self) -> None:
|
|
2548
|
+
self._directory_index_iter(
|
|
2549
|
+
PurePosixPath("symbols/index.md"),
|
|
2550
|
+
"Symbols",
|
|
2551
|
+
(
|
|
2552
|
+
(
|
|
2553
|
+
f"{item['name']} ({item['kind']})",
|
|
2554
|
+
PurePosixPath(str(item["page"])),
|
|
2555
|
+
)
|
|
2556
|
+
for item in _iter_json_objects(self.name_catalog)
|
|
2557
|
+
),
|
|
2558
|
+
empty="No non-unit symbols were indexed.",
|
|
2559
|
+
)
|
|
2560
|
+
|
|
2561
|
+
def _write_unresolved_page(self) -> int:
|
|
2562
|
+
by_key = self.workspace / "unresolved-by-key.jsonl"
|
|
2563
|
+
unique = self.workspace / "unresolved-unique.jsonl"
|
|
2564
|
+
ordered = self.workspace / "unresolved-ordered.jsonl"
|
|
2565
|
+
sort_jsonl_records(
|
|
2566
|
+
self.raw_unresolved,
|
|
2567
|
+
by_key,
|
|
2568
|
+
key=lambda item: (str(item["dedupe_key"]), str(item["order_key"])),
|
|
2569
|
+
max_records_per_run=self.strategy.sort_records_per_run,
|
|
2570
|
+
max_bytes_per_run=self.strategy.sort_bytes_per_run,
|
|
2571
|
+
temp_dir=self.workspace,
|
|
2572
|
+
heartbeat=lambda: self.guard.check(
|
|
2573
|
+
phase="unresolved reference sort",
|
|
2574
|
+
),
|
|
2575
|
+
force_heartbeat=lambda: self.guard.check(
|
|
2576
|
+
phase="unresolved reference sort",
|
|
2577
|
+
force_process_tree=True,
|
|
2578
|
+
),
|
|
2579
|
+
)
|
|
2580
|
+
previous_key: str | None = None
|
|
2581
|
+
unresolved_count = 0
|
|
2582
|
+
with unique.open("w", encoding="utf-8", newline="\n") as target:
|
|
2583
|
+
for item in _iter_json_objects(by_key):
|
|
2584
|
+
dedupe_key = str(item["dedupe_key"])
|
|
2585
|
+
if dedupe_key == previous_key:
|
|
2586
|
+
continue
|
|
2587
|
+
previous_key = dedupe_key
|
|
2588
|
+
target.write(json.dumps(item, ensure_ascii=False, separators=(",", ":")))
|
|
2589
|
+
target.write("\n")
|
|
2590
|
+
unresolved_count += 1
|
|
2591
|
+
sort_jsonl_records(
|
|
2592
|
+
unique,
|
|
2593
|
+
ordered,
|
|
2594
|
+
fields=("order_key",),
|
|
2595
|
+
max_records_per_run=self.strategy.sort_records_per_run,
|
|
2596
|
+
max_bytes_per_run=self.strategy.sort_bytes_per_run,
|
|
2597
|
+
temp_dir=self.workspace,
|
|
2598
|
+
heartbeat=lambda: self.guard.check(
|
|
2599
|
+
phase="unresolved reference sort",
|
|
2600
|
+
),
|
|
2601
|
+
force_heartbeat=lambda: self.guard.check(
|
|
2602
|
+
phase="unresolved reference sort",
|
|
2603
|
+
force_process_tree=True,
|
|
2604
|
+
),
|
|
2605
|
+
)
|
|
2606
|
+
|
|
2607
|
+
def body() -> Iterator[str]:
|
|
2608
|
+
yield "# Unresolved semantic references"
|
|
2609
|
+
yield ""
|
|
2610
|
+
yield (
|
|
2611
|
+
"These names were observed by the tolerant semantic index but "
|
|
2612
|
+
"could not be resolved soundly in the current workspace."
|
|
2613
|
+
)
|
|
2614
|
+
yield ""
|
|
2615
|
+
if unresolved_count:
|
|
2616
|
+
for item in _iter_json_objects(ordered):
|
|
2617
|
+
yield str(item["line"])
|
|
2618
|
+
else:
|
|
2619
|
+
yield "- None"
|
|
2620
|
+
|
|
2621
|
+
self._concept_iter(
|
|
2622
|
+
PurePosixPath("references/unresolved.md"),
|
|
2623
|
+
concept_type="Delphi Unresolved References",
|
|
2624
|
+
title="Unresolved semantic references",
|
|
2625
|
+
description="References that remain unresolved in the tolerant semantic index.",
|
|
2626
|
+
tags=("delphi", "references", "diagnostics"),
|
|
2627
|
+
body=body(),
|
|
2628
|
+
)
|
|
2629
|
+
return unresolved_count
|
|
2630
|
+
|
|
2631
|
+
def _write_reference_index(self, raw_index: Path) -> int:
|
|
2632
|
+
ordered_index = self.workspace / "resolved-index.jsonl"
|
|
2633
|
+
sort_jsonl_records(
|
|
2634
|
+
raw_index,
|
|
2635
|
+
ordered_index,
|
|
2636
|
+
key=lambda item: (
|
|
2637
|
+
str(item["name_key"]),
|
|
2638
|
+
str(item["source_key"]),
|
|
2639
|
+
f"{int(item['start_line']):012d}",
|
|
2640
|
+
f"{int(item['start_col']):012d}",
|
|
2641
|
+
),
|
|
2642
|
+
max_records_per_run=self.strategy.sort_records_per_run,
|
|
2643
|
+
max_bytes_per_run=self.strategy.sort_bytes_per_run,
|
|
2644
|
+
temp_dir=self.workspace,
|
|
2645
|
+
heartbeat=lambda: self.guard.check(
|
|
2646
|
+
phase="reference index sort",
|
|
2647
|
+
),
|
|
2648
|
+
force_heartbeat=lambda: self.guard.check(
|
|
2649
|
+
phase="reference index sort",
|
|
2650
|
+
force_process_tree=True,
|
|
2651
|
+
),
|
|
2652
|
+
)
|
|
2653
|
+
resolved_count = sum(1 for _item in _iter_json_objects(ordered_index))
|
|
2654
|
+
|
|
2655
|
+
def lines() -> Iterator[str]:
|
|
2656
|
+
yield "# References"
|
|
2657
|
+
yield ""
|
|
2658
|
+
yield "Resolved references are embedded on their target symbol pages."
|
|
2659
|
+
yield ""
|
|
2660
|
+
if resolved_count:
|
|
2661
|
+
for item in _iter_json_objects(ordered_index):
|
|
2662
|
+
label = f"{item['name']} ({item['count']})"
|
|
2663
|
+
yield f"- {_link(label, PurePosixPath(str(item['page'])))}"
|
|
2664
|
+
else:
|
|
2665
|
+
yield "- No resolved references"
|
|
2666
|
+
yield ""
|
|
2667
|
+
yield f"- {_link('Unresolved references', PurePosixPath('references/unresolved.md'))}"
|
|
2668
|
+
|
|
2669
|
+
self._write_lines(PurePosixPath("references/index.md"), lines())
|
|
2670
|
+
return resolved_count
|
|
2671
|
+
|
|
2672
|
+
def _write_streaming_problems(self) -> None:
|
|
2673
|
+
payload = layer_payload(self.index, "problems")
|
|
2674
|
+
items = list(payload["items"])
|
|
2675
|
+
for project in self.projects.items():
|
|
2676
|
+
for problem in project["problems"]:
|
|
2677
|
+
items.append(
|
|
2678
|
+
{
|
|
2679
|
+
"kind": problem["kind"],
|
|
2680
|
+
"origin": project["path"],
|
|
2681
|
+
"message": problem["message"],
|
|
2682
|
+
"file": problem["file"],
|
|
2683
|
+
}
|
|
2684
|
+
)
|
|
2685
|
+
page = PurePosixPath("problems/workspace.md")
|
|
2686
|
+
body = ["# Workspace problems", "", f"Problems: {len(items)}", ""]
|
|
2687
|
+
body.extend(
|
|
2688
|
+
f"- **{_escape(str(item['kind']))}:** {_escape(str(item['message']))} "
|
|
2689
|
+
f"(`{_escape_code(self._display_path(str(item.get('origin', ''))))}`)"
|
|
2690
|
+
for item in items
|
|
2691
|
+
)
|
|
2692
|
+
if not items:
|
|
2693
|
+
body.append("- None")
|
|
2694
|
+
body.extend(["", "## Complete problem data", "", _json_block(items)])
|
|
2695
|
+
self._concept(
|
|
2696
|
+
page,
|
|
2697
|
+
concept_type="Delphi Workspace Problems",
|
|
2698
|
+
title="Workspace problems",
|
|
2699
|
+
description="Discovery and deep-project indexing problems.",
|
|
2700
|
+
tags=("delphi", "problems", "diagnostics"),
|
|
2701
|
+
body=body,
|
|
2702
|
+
)
|
|
2703
|
+
self._directory_index(
|
|
2704
|
+
PurePosixPath("problems/index.md"),
|
|
2705
|
+
"Problems",
|
|
2706
|
+
[("Workspace problems", page)],
|
|
2707
|
+
empty="No problem report was generated.",
|
|
2708
|
+
)
|
|
2709
|
+
|
|
2710
|
+
|
|
2711
|
+
def _iter_json_objects(path: Path) -> Iterator[dict[str, Any]]:
|
|
2712
|
+
with path.open("r", encoding="utf-8") as stream:
|
|
2713
|
+
for line_number, line in enumerate(stream, start=1):
|
|
2714
|
+
if not line.strip():
|
|
2715
|
+
continue
|
|
2716
|
+
item = json.loads(line)
|
|
2717
|
+
if not isinstance(item, dict):
|
|
2718
|
+
raise WikiExportError(
|
|
2719
|
+
f"Invalid wiki work record in {path} at line {line_number}"
|
|
2720
|
+
)
|
|
2721
|
+
yield item
|
|
2722
|
+
|
|
2723
|
+
|
|
2724
|
+
def _scope_root(scope: Scope) -> Scope:
|
|
2725
|
+
current = scope
|
|
2726
|
+
while current.parent is not None:
|
|
2727
|
+
current = current.parent
|
|
2728
|
+
return current
|
|
2729
|
+
|
|
2730
|
+
|
|
2731
|
+
def _project_item(project: str, result: ProjectIndexResult) -> dict[str, Any]:
|
|
2732
|
+
return {
|
|
2733
|
+
"path": project,
|
|
2734
|
+
"parsed_units": [
|
|
2735
|
+
{"name": unit.name, "path": unit.path, "has_error": unit.has_error}
|
|
2736
|
+
for unit in result.parsed_units
|
|
2737
|
+
],
|
|
2738
|
+
"include_files": [
|
|
2739
|
+
{"name": item.name, "path": item.path} for item in result.include_files
|
|
2740
|
+
],
|
|
2741
|
+
"not_found_units": result.not_found_units,
|
|
2742
|
+
"problems": [
|
|
2743
|
+
{
|
|
2744
|
+
"kind": problem.problem_type.value,
|
|
2745
|
+
"file": problem.file_name,
|
|
2746
|
+
"message": problem.description,
|
|
2747
|
+
}
|
|
2748
|
+
for problem in result.problems
|
|
2749
|
+
],
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
|
|
2753
|
+
def _symbol_identity(symbol: Symbol) -> str:
|
|
2754
|
+
source_range = symbol.decl_range
|
|
2755
|
+
return "|".join(
|
|
2756
|
+
(
|
|
2757
|
+
_normalized_source(source_range.file_name),
|
|
2758
|
+
symbol.name.casefold(),
|
|
2759
|
+
symbol.kind.value,
|
|
2760
|
+
str(source_range.start_line),
|
|
2761
|
+
str(source_range.start_col),
|
|
2762
|
+
str(source_range.end_line),
|
|
2763
|
+
str(source_range.end_col),
|
|
2764
|
+
)
|
|
2765
|
+
)
|
|
2766
|
+
|
|
2767
|
+
|
|
2768
|
+
def _symbol_page(symbol: Symbol) -> PurePosixPath:
|
|
2769
|
+
return PurePosixPath("symbols") / _document_name(
|
|
2770
|
+
symbol.name,
|
|
2771
|
+
_symbol_identity(symbol),
|
|
2772
|
+
)
|
|
2773
|
+
|
|
2774
|
+
|
|
2775
|
+
def _symbol_owner(symbol: Symbol) -> str:
|
|
2776
|
+
return symbol.scope.owner.name if symbol.scope.owner is not None else symbol.scope.name
|
|
2777
|
+
|
|
2778
|
+
|
|
2779
|
+
def _normalized_source(value: str) -> str:
|
|
2780
|
+
absolute = value if os.path.isabs(value) else os.path.abspath(value)
|
|
2781
|
+
return _normalized_absolute_source(absolute)
|
|
2782
|
+
|
|
2783
|
+
|
|
2784
|
+
@lru_cache(maxsize=2_048)
|
|
2785
|
+
def _normalized_absolute_source(value: str) -> str:
|
|
2786
|
+
return os.path.normcase(os.path.normpath(value)).replace("\\", "/")
|
|
2787
|
+
|
|
2788
|
+
|
|
2789
|
+
def _reference_target_key(reference: SymbolReference) -> str | None:
|
|
2790
|
+
target = reference.resolved
|
|
2791
|
+
if target is None or target.kind.value == "unit":
|
|
2792
|
+
return None
|
|
2793
|
+
return _symbol_identity(target)
|
|
2794
|
+
|
|
2795
|
+
|
|
2796
|
+
def _reference_sort_key(reference: SymbolReference) -> tuple[str, int, int, str, str]:
|
|
2797
|
+
return (
|
|
2798
|
+
_normalized_source(reference.ref_range.file_name),
|
|
2799
|
+
reference.ref_range.start_line,
|
|
2800
|
+
reference.ref_range.start_col,
|
|
2801
|
+
reference.kind.value,
|
|
2802
|
+
reference.name.casefold(),
|
|
2803
|
+
)
|
|
2804
|
+
|
|
2805
|
+
|
|
2806
|
+
@lru_cache(maxsize=2_048)
|
|
2807
|
+
def _document_name(label: str, identity: str) -> str:
|
|
2808
|
+
normalized = unicodedata.normalize("NFKD", label)
|
|
2809
|
+
ascii_label = normalized.encode("ascii", "ignore").decode("ascii").casefold()
|
|
2810
|
+
slug = re.sub(r"[^a-z0-9]+", "-", ascii_label).strip("-")[:64] or "concept"
|
|
2811
|
+
digest = hashlib.sha256(identity.encode("utf-8")).hexdigest()[:16]
|
|
2812
|
+
return f"{slug}-{digest}.md"
|
|
2813
|
+
|
|
2814
|
+
|
|
2815
|
+
def _link(label: str, target: PurePosixPath) -> str:
|
|
2816
|
+
return f"[{_escape(label)}](/{target.as_posix()})"
|
|
2817
|
+
|
|
2818
|
+
|
|
2819
|
+
def _escape(value: str) -> str:
|
|
2820
|
+
return re.sub(r"([\\`*_[\]<>])", r"\\\1", value)
|
|
2821
|
+
|
|
2822
|
+
|
|
2823
|
+
def _escape_code(value: str) -> str:
|
|
2824
|
+
return value.replace("`", "\\`")
|
|
2825
|
+
|
|
2826
|
+
|
|
2827
|
+
def _bullet_code_values(values: Iterable[Any]) -> list[str]:
|
|
2828
|
+
rendered = [f"- `{_escape_code(str(value))}`" for value in values]
|
|
2829
|
+
return rendered or ["- None"]
|
|
2830
|
+
|
|
2831
|
+
|
|
2832
|
+
def _inline_code(values: Iterable[str]) -> str:
|
|
2833
|
+
return ", ".join(f"`{_escape_code(value)}`" for value in values)
|
|
2834
|
+
|
|
2835
|
+
|
|
2836
|
+
def _json_block(value: Any) -> str:
|
|
2837
|
+
return _code_block(json.dumps(value, indent=2, ensure_ascii=False, sort_keys=True), "json")
|
|
2838
|
+
|
|
2839
|
+
|
|
2840
|
+
def _code_block(value: str, language: str) -> str:
|
|
2841
|
+
longest = max((len(match.group(0)) for match in re.finditer(r"`+", value)), default=0)
|
|
2842
|
+
fence = "`" * max(3, longest + 1)
|
|
2843
|
+
return f"{fence}{language}\n{value}\n{fence}"
|
|
2844
|
+
|
|
2845
|
+
|
|
2846
|
+
__all__ = [
|
|
2847
|
+
"OKF_VERSION",
|
|
2848
|
+
"WikiExportError",
|
|
2849
|
+
"WikiExportResult",
|
|
2850
|
+
"WikiProgressCallback",
|
|
2851
|
+
"WikiProgressEvent",
|
|
2852
|
+
"export_okf_wiki",
|
|
2853
|
+
]
|