python-delphi-lsp 3.1.2__py3-none-any.whl → 3.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. delphi_lsp/__init__.py +105 -89
  2. delphi_lsp/_version.py +1 -1
  3. delphi_lsp/agent_cache.py +95 -42
  4. delphi_lsp/agent_cli.py +33 -7
  5. delphi_lsp/agent_context.py +47 -18
  6. delphi_lsp/agent_cpg.py +3 -4
  7. delphi_lsp/agent_cpg_builder.py +0 -1
  8. delphi_lsp/agent_layers.py +17 -8
  9. delphi_lsp/agent_metrics.py +2 -4
  10. delphi_lsp/agent_protocol.py +2 -3
  11. delphi_lsp/agent_relations.py +52 -8
  12. delphi_lsp/agent_templates.py +1 -2
  13. delphi_lsp/agent_wiki.py +6 -6
  14. delphi_lsp/agent_workspace.py +9 -5
  15. delphi_lsp/binary.py +5 -5
  16. delphi_lsp/comment_builder.py +1 -2
  17. delphi_lsp/delphiast_lexer.py +3 -5
  18. delphi_lsp/delphiast_parser.py +384 -52
  19. delphi_lsp/grammar.py +0 -1
  20. delphi_lsp/lark_builder.py +5 -10
  21. delphi_lsp/lark_tokens.py +0 -1
  22. delphi_lsp/lexical_scanner.py +30 -0
  23. delphi_lsp/lsp_server.py +603 -110
  24. delphi_lsp/metrics.py +50 -15
  25. delphi_lsp/navigation_cache.py +3 -4
  26. delphi_lsp/nodes.py +19 -19
  27. delphi_lsp/parallel_outline.py +3 -4
  28. delphi_lsp/parser.py +101 -17
  29. delphi_lsp/preprocessor.py +232 -25
  30. delphi_lsp/progress.py +1 -1
  31. delphi_lsp/project_config.py +2 -1
  32. delphi_lsp/project_discovery.py +38 -21
  33. delphi_lsp/project_indexer.py +18 -18
  34. delphi_lsp/semantic.py +28 -17
  35. delphi_lsp/semantic_builder.py +29 -20
  36. delphi_lsp/workspace.py +1 -1
  37. {python_delphi_lsp-3.1.2.dist-info → python_delphi_lsp-3.2.1.dist-info}/METADATA +18 -4
  38. python_delphi_lsp-3.2.1.dist-info/RECORD +48 -0
  39. python_delphi_lsp-3.1.2.dist-info/RECORD +0 -47
  40. {python_delphi_lsp-3.1.2.dist-info → python_delphi_lsp-3.2.1.dist-info}/WHEEL +0 -0
  41. {python_delphi_lsp-3.1.2.dist-info → python_delphi_lsp-3.2.1.dist-info}/entry_points.txt +0 -0
  42. {python_delphi_lsp-3.1.2.dist-info → python_delphi_lsp-3.2.1.dist-info}/licenses/LICENSE +0 -0
  43. {python_delphi_lsp-3.1.2.dist-info → python_delphi_lsp-3.2.1.dist-info}/top_level.txt +0 -0
delphi_lsp/__init__.py CHANGED
@@ -1,6 +1,26 @@
1
1
  from ._version import __version__
2
2
  from .binary import BinarySerializer, dumps, loads
3
+ from .comment_builder import build_comment_nodes
3
4
  from .consts import AttributeName, SyntaxNodeType
5
+ from .grammar import GRAMMAR_RULES, build_grammar
6
+ from .lark_tokens import (
7
+ BASE_TERMINALS,
8
+ KEYWORD_TERMINALS,
9
+ KEYWORDS,
10
+ build_grammar_snippet,
11
+ )
12
+ from .lsp_server import LspWorkspaceState, create_server
13
+ from .metrics import (
14
+ CyclomaticMetrics,
15
+ HalsteadMetrics,
16
+ LineMetrics,
17
+ MetricProblem,
18
+ ProjectMetrics,
19
+ RoutineComplexity,
20
+ UnitMetrics,
21
+ analyze_project,
22
+ analyze_unit,
23
+ )
4
24
  from .nodes import (
5
25
  CommentNode,
6
26
  CompoundSyntaxNode,
@@ -9,12 +29,31 @@ from .nodes import (
9
29
  SyntaxTreeException,
10
30
  ValuedSyntaxNode,
11
31
  )
12
- from .writer import SyntaxTreeWriter
13
- from .preprocessor import CommentInfo, IncludeLoader, PreprocessedSource, Preprocessor, PreprocessorOptions
14
- from .lark_tokens import KEYWORDS, KEYWORD_TERMINALS, BASE_TERMINALS, build_grammar_snippet
15
- from .grammar import build_grammar, GRAMMAR_RULES
16
- from .comment_builder import build_comment_nodes
17
- from .parser import DelphiParser, ParseResult, StringTransform, parse
32
+ from .parser import DelphiParseError, DelphiParser, ParseResult, StringTransform, parse
33
+ from .preprocessor import (
34
+ CommentInfo,
35
+ IncludeLoader,
36
+ PreprocessedSource,
37
+ Preprocessor,
38
+ PreprocessorOptions,
39
+ )
40
+ from .progress import ProgressCallback, ProgressEvent
41
+ from .project_discovery import (
42
+ DelphiProjectDiscovery,
43
+ DiscoveryProblem,
44
+ discover_delphi_project,
45
+ )
46
+ from .project_indexer import (
47
+ GetUnitSyntaxHook,
48
+ IncludeFileInfo,
49
+ ProjectIndexer,
50
+ ProjectIndexResult,
51
+ ProjectProblem,
52
+ ProjectProblemType,
53
+ UnitErrorInfo,
54
+ UnitInfo,
55
+ UnitParsedHook,
56
+ )
18
57
  from .semantic import (
19
58
  ArrayTypeRef,
20
59
  ClassOfTypeRef,
@@ -27,136 +66,113 @@ from .semantic import (
27
66
  NamedTypeRef,
28
67
  PointerTypeRef,
29
68
  ProcTypeRef,
69
+ RecordTypeRef,
30
70
  ReferenceKind,
31
71
  ReferenceTypeRef,
32
- RecordTypeRef,
33
72
  Scope,
34
73
  ScopeKind,
74
+ SetTypeRef,
35
75
  SourceRange,
36
76
  SubrangeTypeRef,
37
- SetTypeRef,
38
77
  Symbol,
39
78
  SymbolIndex,
40
79
  SymbolKind,
41
80
  SymbolReference,
42
- TypeRef,
43
81
  TypeParameterRef,
82
+ TypeRef,
44
83
  UnknownTypeRef,
45
84
  Visibility,
46
85
  )
47
86
  from .semantic_builder import SemanticBuilder, SemanticModel, SemanticProblem
48
87
  from .workspace import WorkspaceSemanticResult, build_workspace_semantics
49
- from .lsp_server import LspWorkspaceState, create_server
50
- from .project_discovery import DelphiProjectDiscovery, DiscoveryProblem, discover_delphi_project
51
- from .progress import ProgressCallback, ProgressEvent
52
- from .project_indexer import (
53
- GetUnitSyntaxHook,
54
- IncludeFileInfo,
55
- ProjectIndexer,
56
- ProjectIndexResult,
57
- ProjectProblem,
58
- ProjectProblemType,
59
- UnitErrorInfo,
60
- UnitInfo,
61
- UnitParsedHook,
62
- )
63
- from .metrics import (
64
- CyclomaticMetrics,
65
- HalsteadMetrics,
66
- LineMetrics,
67
- MetricProblem,
68
- ProjectMetrics,
69
- RoutineComplexity,
70
- UnitMetrics,
71
- analyze_project,
72
- analyze_unit,
73
- )
88
+ from .writer import SyntaxTreeWriter
74
89
 
75
90
  __all__ = [
76
- '__version__',
77
- 'AttributeName',
78
- 'BinarySerializer',
79
- 'CommentNode',
80
- 'CompoundSyntaxNode',
81
- 'ParserException',
82
- 'SyntaxNode',
83
- 'SyntaxNodeType',
84
- 'SyntaxTreeException',
85
- 'SyntaxTreeWriter',
86
- 'ValuedSyntaxNode',
87
- 'PreprocessedSource',
88
- 'Preprocessor',
89
- 'PreprocessorOptions',
90
- 'IncludeLoader',
91
- 'CommentInfo',
92
- 'KEYWORDS',
93
- 'KEYWORD_TERMINALS',
94
91
  'BASE_TERMINALS',
95
- 'build_grammar_snippet',
96
- 'build_grammar',
97
92
  'GRAMMAR_RULES',
98
- 'build_comment_nodes',
99
- 'DelphiParser',
100
- 'ParseResult',
101
- 'StringTransform',
102
- 'parse',
103
- 'Modifier',
93
+ 'KEYWORDS',
94
+ 'KEYWORD_TERMINALS',
104
95
  'ArrayTypeRef',
96
+ 'AttributeName',
97
+ 'BinarySerializer',
105
98
  'ClassOfTypeRef',
106
99
  'ClassTypeRef',
100
+ 'CommentInfo',
101
+ 'CommentNode',
102
+ 'CompoundSyntaxNode',
103
+ 'CyclomaticMetrics',
104
+ 'DelphiParseError',
105
+ 'DelphiParser',
106
+ 'DelphiProjectDiscovery',
107
+ 'DiscoveryProblem',
107
108
  'EnumTypeRef',
108
109
  'FileTypeRef',
109
110
  'GenericInstanceTypeRef',
111
+ 'GetUnitSyntaxHook',
112
+ 'HalsteadMetrics',
113
+ 'IncludeFileInfo',
114
+ 'IncludeLoader',
110
115
  'InterfaceTypeRef',
116
+ 'LineMetrics',
117
+ 'LspWorkspaceState',
118
+ 'MetricProblem',
119
+ 'Modifier',
111
120
  'NamedTypeRef',
121
+ 'ParseResult',
122
+ 'ParserException',
112
123
  'PointerTypeRef',
124
+ 'PreprocessedSource',
125
+ 'Preprocessor',
126
+ 'PreprocessorOptions',
113
127
  'ProcTypeRef',
128
+ 'ProgressCallback',
129
+ 'ProgressEvent',
130
+ 'ProjectIndexResult',
131
+ 'ProjectIndexer',
132
+ 'ProjectMetrics',
133
+ 'ProjectProblem',
134
+ 'ProjectProblemType',
135
+ 'RecordTypeRef',
114
136
  'ReferenceKind',
115
137
  'ReferenceTypeRef',
116
- 'RecordTypeRef',
138
+ 'RoutineComplexity',
117
139
  'Scope',
118
140
  'ScopeKind',
141
+ 'SemanticBuilder',
142
+ 'SemanticModel',
143
+ 'SemanticProblem',
144
+ 'SetTypeRef',
119
145
  'SourceRange',
146
+ 'StringTransform',
120
147
  'SubrangeTypeRef',
121
- 'SetTypeRef',
122
148
  'Symbol',
123
149
  'SymbolIndex',
124
150
  'SymbolKind',
125
151
  'SymbolReference',
126
- 'TypeRef',
152
+ 'SyntaxNode',
153
+ 'SyntaxNodeType',
154
+ 'SyntaxTreeException',
155
+ 'SyntaxTreeWriter',
127
156
  'TypeParameterRef',
157
+ 'TypeRef',
158
+ 'UnitErrorInfo',
159
+ 'UnitInfo',
160
+ 'UnitMetrics',
161
+ 'UnitParsedHook',
128
162
  'UnknownTypeRef',
163
+ 'ValuedSyntaxNode',
129
164
  'Visibility',
130
- 'SemanticBuilder',
131
- 'SemanticModel',
132
- 'SemanticProblem',
133
165
  'WorkspaceSemanticResult',
166
+ '__version__',
167
+ 'analyze_project',
168
+ 'analyze_unit',
169
+ 'build_comment_nodes',
170
+ 'build_grammar',
171
+ 'build_grammar_snippet',
134
172
  'build_workspace_semantics',
135
- 'LspWorkspaceState',
136
173
  'create_server',
137
- 'DelphiProjectDiscovery',
138
- 'DiscoveryProblem',
139
174
  'discover_delphi_project',
140
- 'ProgressCallback',
141
- 'ProgressEvent',
142
- 'ProjectIndexer',
143
- 'ProjectIndexResult',
144
- 'ProjectProblemType',
145
- 'ProjectProblem',
146
- 'UnitInfo',
147
- 'UnitErrorInfo',
148
- 'IncludeFileInfo',
149
- 'GetUnitSyntaxHook',
150
- 'UnitParsedHook',
151
- 'MetricProblem',
152
- 'LineMetrics',
153
- 'HalsteadMetrics',
154
- 'RoutineComplexity',
155
- 'CyclomaticMetrics',
156
- 'UnitMetrics',
157
- 'ProjectMetrics',
158
- 'analyze_unit',
159
- 'analyze_project',
160
175
  'dumps',
161
176
  'loads',
177
+ 'parse',
162
178
  ]
delphi_lsp/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "3.1.2"
1
+ __version__ = "3.2.1"
delphi_lsp/agent_cache.py CHANGED
@@ -18,6 +18,7 @@ import time
18
18
  from collections.abc import Callable, Collection, Mapping
19
19
  from concurrent.futures import ThreadPoolExecutor
20
20
  from dataclasses import dataclass, fields, is_dataclass, replace
21
+ from functools import lru_cache
21
22
  from pathlib import Path
22
23
  from types import ModuleType
23
24
 
@@ -40,6 +41,8 @@ _CONNECTION_TIMEOUT = 2.0
40
41
  _PREAUTH_CONNECTION_TIMEOUT = 0.25
41
42
  _CONNECTION_WORKERS = 8
42
43
  _CLIENT_RESPONSE_TIMEOUT = 120.0
44
+ _MAX_CLIENT_RESPONSE_TIMEOUT = 600.0
45
+ _CLIENT_SECONDS_PER_SOURCE = 0.25
43
46
  _MEMORY_SIZE = re.compile(r"^(?P<count>[1-9][0-9]*)(?P<suffix>[KMG]?)$", re.IGNORECASE)
44
47
  _STARTUP_DIAGNOSTIC_BYTES = 16 * 1024
45
48
  _STARTUP_TOKEN_RE = re.compile(r"(?i)(token\b[^\n\r]*?:?\s*['\"]?[A-Za-z0-9_-]+['\"]?|\b[a-zA-Z0-9_-]{32,})")
@@ -623,7 +626,7 @@ def _process_start_lock(root: str | Path):
623
626
  def _client_exchange(metadata: CacheMetadata, request: dict[str, object]) -> CacheClientResponse:
624
627
  try:
625
628
  with socket.create_connection(("127.0.0.1", metadata.port), timeout=2) as connection:
626
- connection.settimeout(_CLIENT_RESPONSE_TIMEOUT)
629
+ connection.settimeout(_client_response_timeout(metadata, request))
627
630
  request_without_token = {key: value for key, value in request.items() if key != "token"}
628
631
  connection.sendall(json.dumps({"token": metadata.token, **request_without_token}, separators=(",", ":")).encode("utf-8") + b"\n")
629
632
  response = _read_line(connection)
@@ -646,6 +649,43 @@ def _client_exchange(metadata: CacheMetadata, request: dict[str, object]) -> Cac
646
649
  return CacheClientResponse(payload, str(decoded.get("warning", "")))
647
650
 
648
651
 
652
+ def _client_response_timeout(
653
+ metadata: CacheMetadata,
654
+ request: Mapping[str, object],
655
+ ) -> float:
656
+ if request.get("action") in {"status", "stop"}:
657
+ return _CLIENT_RESPONSE_TIMEOUT
658
+ return min(
659
+ _MAX_CLIENT_RESPONSE_TIMEOUT,
660
+ _CLIENT_RESPONSE_TIMEOUT
661
+ + _workspace_source_count(metadata.root) * _CLIENT_SECONDS_PER_SOURCE,
662
+ )
663
+
664
+
665
+ @lru_cache(maxsize=64)
666
+ def _workspace_source_count(root: str) -> int:
667
+ count = 0
668
+ source_suffixes = {".pas", ".pp", ".inc", ".dpr", ".dpk"}
669
+ maximum_relevant_count = int(
670
+ (_MAX_CLIENT_RESPONSE_TIMEOUT - _CLIENT_RESPONSE_TIMEOUT)
671
+ / _CLIENT_SECONDS_PER_SOURCE
672
+ )
673
+ for _current, directories, files in os.walk(Path(root).expanduser().resolve()):
674
+ directories[:] = [
675
+ directory
676
+ for directory in directories
677
+ if directory not in SKIP_DIRS
678
+ ]
679
+ count += sum(
680
+ 1
681
+ for file_name in files
682
+ if Path(file_name).suffix.casefold() in source_suffixes
683
+ )
684
+ if count >= maximum_relevant_count:
685
+ return maximum_relevant_count
686
+ return count
687
+
688
+
649
689
  def _read_line(connection: socket.socket) -> bytes:
650
690
  chunks = bytearray()
651
691
  while len(chunks) <= _MAX_MESSAGE_BYTES:
@@ -768,14 +808,7 @@ class _CacheService:
768
808
  if error.code != "project_required":
769
809
  raise
770
810
  self.cache_state = "ready"
771
- self.last_budget = self.budget.enforce(
772
- measure=self._measure_retained_bytes,
773
- evict_auxiliary=self.context.evict_auxiliary_caches,
774
- evict_navigation=self.context.evict_navigation_caches,
775
- )
776
- if self.last_budget.compacted:
777
- self.stats.evictions += 1
778
- self.cache_state = "compact"
811
+ self._enforce_budget()
779
812
  self.prewarm_seconds = time.monotonic() - started
780
813
  self.stats.parallel_fallbacks = context.parallel_stats.fallbacks
781
814
 
@@ -835,13 +868,16 @@ class _CacheService:
835
868
  before = self.last_revision
836
869
  was_warm = self.context.navigation_cache_is_warm
837
870
  try:
838
- response = self.context.handle(request).to_mapping()
839
- except AgentProtocolError as error:
840
- raise CacheClientError(error.code, error.message) from None
841
- except (OSError, UnicodeError):
842
- raise CacheClientError("source_unavailable", "Source is unavailable.") from None
843
- except Exception:
844
- raise CacheClientError("internal_error", "Internal cache error.") from None
871
+ try:
872
+ response = self.context.handle(request).to_mapping()
873
+ except AgentProtocolError as error:
874
+ raise CacheClientError(error.code, error.message) from None
875
+ except (OSError, UnicodeError):
876
+ raise CacheClientError("source_unavailable", "Source is unavailable.") from None
877
+ except Exception:
878
+ raise CacheClientError("internal_error", "Internal cache error.") from None
879
+ finally:
880
+ self._enforce_budget()
845
881
  after = str(response["workspace_revision"])
846
882
  self.last_revision = after
847
883
  self.stats.requests += 1
@@ -852,18 +888,24 @@ class _CacheService:
852
888
  self.stats.parallel_fallbacks += self.context.parallel_stats.fallbacks
853
889
  if before != after:
854
890
  self.stats.invalidations += 1
855
- self.last_budget = self.budget.enforce(
856
- measure=self._measure_retained_bytes,
857
- evict_auxiliary=self.context.evict_auxiliary_caches,
858
- evict_navigation=self.context.evict_navigation_caches,
859
- )
860
- if self.last_budget.compacted:
861
- self.stats.evictions += 1
862
- self.cache_state = "compact"
863
- else:
864
- self.cache_state = "warm" if self.context.navigation_cache_is_warm else "ready"
865
891
  return CacheClientResponse(response, self._consume_warning())
866
892
 
893
+ def _enforce_budget(self) -> None:
894
+ self.last_budget = self.budget.enforce(
895
+ measure=self._measure_retained_bytes,
896
+ evict_auxiliary=self.context.evict_auxiliary_caches,
897
+ evict_navigation=self.context.evict_navigation_caches,
898
+ )
899
+ if self.last_budget.compacted:
900
+ self.stats.evictions += 1
901
+ self.cache_state = "compact"
902
+ else:
903
+ self.cache_state = (
904
+ "warm"
905
+ if self.context.navigation_cache_is_warm
906
+ else "ready"
907
+ )
908
+
867
909
  def _consume_warning(self) -> str:
868
910
  warning = cache_warning(self.last_budget, self.budget.max_bytes)
869
911
  if self.last_budget.warning_triggered and not self.last_budget.warning_active:
@@ -874,6 +916,17 @@ class _CacheService:
874
916
  now = time.monotonic()
875
917
  idle = max(0.0, now - self.last_activity)
876
918
  context = self._context
919
+ live_rss = current_process_rss_bytes()
920
+ current_bytes = (
921
+ live_rss
922
+ if live_rss > 0
923
+ else self.last_budget.retained_bytes
924
+ )
925
+ current_utilization = current_bytes * 100.0 / self.budget.max_bytes
926
+ peak_utilization = max(
927
+ self.last_budget.peak_utilization_percent,
928
+ current_utilization,
929
+ )
877
930
  parallel_stats = (
878
931
  context.parallel_stats
879
932
  if context is not None
@@ -883,10 +936,11 @@ class _CacheService:
883
936
  "pid": self.metadata.pid, "root": self.metadata.root, "project_file": self.metadata.project_file,
884
937
  "version": self.metadata.version, "uptime": now - self.started, "idle_seconds": idle,
885
938
  "last_activity_at": time.time() - idle,
886
- "max_memory_bytes": self.budget.max_bytes, "current_bytes": self.last_budget.retained_bytes,
887
- "current_utilization_percent": self.last_budget.utilization_percent,
888
- "peak_utilization_percent": self.last_budget.peak_utilization_percent,
889
- "warning_active": self.last_budget.warning_active, "warning_threshold_percent": WARNING_THRESHOLD_PERCENT,
939
+ "max_memory_bytes": self.budget.max_bytes, "current_bytes": current_bytes,
940
+ "current_utilization_percent": current_utilization,
941
+ "peak_utilization_percent": peak_utilization,
942
+ "warning_active": current_utilization >= self.budget.warning_percent,
943
+ "warning_threshold_percent": WARNING_THRESHOLD_PERCENT,
890
944
  "cache_state": self.cache_state, "requests": self.stats.requests, "warm_hits": self.stats.warm_hits,
891
945
  "rebuilds": self.stats.rebuilds, "invalidations": self.stats.invalidations, "evictions": self.stats.evictions,
892
946
  "workers_configured": "auto" if self.metadata.workers == 0 else self.metadata.workers,
@@ -1073,7 +1127,7 @@ def run_cache_daemon(
1073
1127
  ):
1074
1128
  try:
1075
1129
  connection, _ = listener.accept()
1076
- except socket.timeout:
1130
+ except TimeoutError:
1077
1131
  continue
1078
1132
  if not connection_slots.acquire(blocking=False):
1079
1133
  connection.close()
@@ -1240,17 +1294,16 @@ def start_cache(
1240
1294
  ):
1241
1295
  raise ValueError("max_disk_cache_bytes must be non-negative.")
1242
1296
  load_project_path_config(root)
1243
- with _process_start_lock(root):
1244
- with _start_lock(root, startup_timeout):
1245
- return _start_cache_unlocked(
1246
- root,
1247
- project_file=project_file,
1248
- max_memory_bytes=max_memory_bytes,
1249
- workers=workers,
1250
- idle_timeout=idle_timeout,
1251
- startup_timeout=startup_timeout,
1252
- max_disk_cache_bytes=max_disk_cache_bytes,
1253
- )
1297
+ with _process_start_lock(root), _start_lock(root, startup_timeout):
1298
+ return _start_cache_unlocked(
1299
+ root,
1300
+ project_file=project_file,
1301
+ max_memory_bytes=max_memory_bytes,
1302
+ workers=workers,
1303
+ idle_timeout=idle_timeout,
1304
+ startup_timeout=startup_timeout,
1305
+ max_disk_cache_bytes=max_disk_cache_bytes,
1306
+ )
1254
1307
 
1255
1308
 
1256
1309
  def query_cache(root: str | Path, request: dict[str, object]) -> CacheClientResponse:
delphi_lsp/agent_cli.py CHANGED
@@ -10,37 +10,36 @@ import threading
10
10
  from pathlib import Path
11
11
  from typing import BinaryIO, TextIO
12
12
 
13
- from .agent_context import AgentContext
14
13
  from .agent_cache import (
15
- CacheClientError,
16
14
  DEFAULT_IDLE_TIMEOUT,
17
15
  DEFAULT_MAX_DISK_CACHE_BYTES,
18
16
  DEFAULT_MAX_MEMORY_BYTES,
19
17
  DEFAULT_STARTUP_TIMEOUT,
20
- parse_memory_size,
18
+ CacheClientError,
21
19
  navigation_cache_path,
20
+ parse_memory_size,
22
21
  query_cache,
23
22
  run_cache_daemon,
24
23
  start_cache,
25
24
  stop_cache,
26
25
  watch_workspace_changes,
27
26
  )
27
+ from .agent_context import AgentContext
28
28
  from .agent_layers import build_codebase_index, layer_payload, render_layer
29
29
  from .agent_protocol import (
30
30
  SCHEMA_VERSION,
31
- AgentProtocolError,
32
31
  SUPPORTED_ACTIONS,
33
32
  SUPPORTED_DETAILS,
34
33
  SUPPORTED_DIRECTIONS,
35
34
  SUPPORTED_GRAPHS,
36
35
  SUPPORTED_RELATIONS,
36
+ AgentProtocolError,
37
37
  )
38
38
  from .agent_templates import install_opencode_support, install_skill
39
39
  from .agent_wiki import WikiExportError, WikiProgressEvent, export_okf_wiki
40
40
  from .parallel_outline import ParallelOutlineError, parse_worker_setting
41
41
  from .project_config import ProjectConfigError
42
42
 
43
-
44
43
  _MAX_WORKER_RECORD_BYTES = 1024 * 1024
45
44
  _INVALID_JSON_MESSAGE = "Invalid JSON request."
46
45
  _INVALID_ENCODING_MESSAGE = "Invalid UTF-8 request."
@@ -94,6 +93,11 @@ def build_parser() -> argparse.ArgumentParser:
94
93
  index.add_argument("--project-file", type=Path)
95
94
  index.add_argument("--out", type=Path, default=Path(".delphi-lsp") / "agent-index" / "index.json")
96
95
  index.add_argument("--workers", type=parse_worker_setting, default=0)
96
+ index.add_argument(
97
+ "--deep-projects",
98
+ action="store_true",
99
+ help="Deep-parse project dependencies for the projects layer.",
100
+ )
97
101
  index.set_defaults(func=_index)
98
102
 
99
103
  wiki = subcommands.add_parser("wiki", help="Export a portable Markdown knowledge wiki.")
@@ -312,10 +316,11 @@ def _discard_broken_stdout() -> None:
312
316
 
313
317
  def _view(args: argparse.Namespace) -> None:
314
318
  args.root = _workspace_root(args.root)
319
+ args.project_file = _project_file(args.root, args.project_file)
315
320
  index = build_codebase_index(
316
321
  args.root,
317
322
  project_file=args.project_file,
318
- index_projects=args.deep_projects,
323
+ index_projects=args.deep_projects or args.layer == "problems",
319
324
  workers=args.workers,
320
325
  )
321
326
  sys.stdout.write(render_layer(index, args.layer, query=args.query, output_format=args.format))
@@ -323,14 +328,16 @@ def _view(args: argparse.Namespace) -> None:
323
328
 
324
329
  def _index(args: argparse.Namespace) -> None:
325
330
  args.root = _workspace_root(args.root)
331
+ args.project_file = _project_file(args.root, args.project_file)
326
332
  output = _resolve_output_path(args.root, args.out)
327
333
  index = build_codebase_index(
328
334
  args.root,
329
335
  project_file=args.project_file,
330
- index_projects=True,
336
+ index_projects=args.deep_projects,
331
337
  workers=args.workers,
332
338
  )
333
339
  payload = {
340
+ "deep_projects": bool(args.deep_projects),
334
341
  "overview": layer_payload(index, "overview"),
335
342
  "projects": layer_payload(index, "projects"),
336
343
  "problems": layer_payload(index, "problems"),
@@ -342,6 +349,7 @@ def _index(args: argparse.Namespace) -> None:
342
349
 
343
350
  def _wiki_export(args: argparse.Namespace) -> None:
344
351
  args.root = _workspace_root(args.root)
352
+ args.project_file = _project_file(args.root, args.project_file)
345
353
  output = _resolve_output_path(args.root, args.out)
346
354
  renderer = None if args.quiet else _WikiProgressRenderer(sys.stderr)
347
355
  try:
@@ -449,6 +457,7 @@ def _require_install_directory(target: str | Path) -> None:
449
457
 
450
458
  def _worker(args: argparse.Namespace) -> None:
451
459
  args.root = _workspace_root(args.root)
460
+ args.project_file = _project_file(args.root, args.project_file)
452
461
  context = _open_worker_context(args.root, args.project_file, args.workers)
453
462
  watcher_stop = threading.Event()
454
463
  watcher = threading.Thread(
@@ -509,6 +518,7 @@ def _cache_error(error: CacheClientError) -> int:
509
518
 
510
519
  def _cache_start(args: argparse.Namespace) -> int:
511
520
  args.root = _workspace_root(args.root)
521
+ args.project_file = _project_file(args.root, args.project_file)
512
522
  try:
513
523
  start_cache(
514
524
  args.root,
@@ -583,6 +593,7 @@ def _cache_clear(args: argparse.Namespace) -> int:
583
593
 
584
594
  def _cache_serve(args: argparse.Namespace) -> int:
585
595
  args.root = _workspace_root(args.root)
596
+ args.project_file = _project_file(args.root, args.project_file)
586
597
  run_cache_daemon(
587
598
  args.root,
588
599
  project_file=str(args.project_file or ""),
@@ -658,6 +669,21 @@ def _workspace_root(value: str | Path) -> Path:
658
669
  return root.resolve()
659
670
 
660
671
 
672
+ def _project_file(root: Path, value: Path | None) -> Path | None:
673
+ if value is None:
674
+ return None
675
+ project = value.expanduser()
676
+ if not project.is_absolute():
677
+ project = root / project
678
+ project = project.resolve()
679
+ if not project.is_file():
680
+ raise _CliError(
681
+ "project_file_not_found",
682
+ f"Explicit project file does not exist: {project}",
683
+ )
684
+ return project
685
+
686
+
661
687
  def _resolve_output_path(root: Path, output: Path) -> Path:
662
688
  return output if output.is_absolute() else root / output
663
689