python-delphi-lsp 2.0.0__tar.gz → 2.0.1__tar.gz

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 (127) hide show
  1. {python_delphi_lsp-2.0.0/python_delphi_lsp.egg-info → python_delphi_lsp-2.0.1}/PKG-INFO +60 -4
  2. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/README.md +59 -3
  3. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/delphi_lsp/__init__.py +20 -0
  4. python_delphi_lsp-2.0.1/delphi_lsp/_version.py +1 -0
  5. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/delphi_lsp/agent_cli.py +1 -0
  6. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/delphi_lsp/agent_context.py +64 -22
  7. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/delphi_lsp/agent_layers.py +65 -0
  8. python_delphi_lsp-2.0.1/delphi_lsp/agent_metrics.py +82 -0
  9. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/delphi_lsp/agent_protocol.py +1 -0
  10. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/delphi_lsp/agent_templates.py +5 -4
  11. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/delphi_lsp/agent_workspace.py +35 -1
  12. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/delphi_lsp/lark_builder.py +14 -2
  13. python_delphi_lsp-2.0.1/delphi_lsp/metrics.py +759 -0
  14. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/pyproject.toml +1 -1
  15. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1/python_delphi_lsp.egg-info}/PKG-INFO +60 -4
  16. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/python_delphi_lsp.egg-info/SOURCES.txt +4 -0
  17. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/scripts/bootstrap_vllm_codebase_skill_test.py +183 -4
  18. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/test_agent_codebase.py +48 -0
  19. python_delphi_lsp-2.0.1/tests/test_agent_metrics.py +149 -0
  20. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/test_agent_protocol.py +10 -0
  21. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/test_agent_worker.py +2 -0
  22. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/test_bootstrap_vllm_codebase_skill.py +57 -0
  23. python_delphi_lsp-2.0.1/tests/test_metrics.py +188 -0
  24. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/test_package_metadata.py +15 -2
  25. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/test_parser.py +50 -0
  26. python_delphi_lsp-2.0.0/delphi_lsp/_version.py +0 -1
  27. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/LICENSE +0 -0
  28. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/MANIFEST.in +0 -0
  29. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/delphi_lsp/agent_relations.py +0 -0
  30. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/delphi_lsp/binary.py +0 -0
  31. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/delphi_lsp/comment_builder.py +0 -0
  32. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/delphi_lsp/consts.py +0 -0
  33. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/delphi_lsp/grammar.py +0 -0
  34. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/delphi_lsp/lark_tokens.py +0 -0
  35. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/delphi_lsp/lsp_server.py +0 -0
  36. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/delphi_lsp/nodes.py +0 -0
  37. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/delphi_lsp/parser.py +0 -0
  38. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/delphi_lsp/preprocessor.py +0 -0
  39. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/delphi_lsp/project_discovery.py +0 -0
  40. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/delphi_lsp/project_indexer.py +0 -0
  41. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/delphi_lsp/semantic.py +0 -0
  42. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/delphi_lsp/semantic_builder.py +0 -0
  43. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/delphi_lsp/source_reader.py +0 -0
  44. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/delphi_lsp/workspace.py +0 -0
  45. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/delphi_lsp/writer.py +0 -0
  46. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/opencode.json +0 -0
  47. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/python_delphi_lsp.egg-info/dependency_links.txt +0 -0
  48. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/python_delphi_lsp.egg-info/entry_points.txt +0 -0
  49. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/python_delphi_lsp.egg-info/requires.txt +0 -0
  50. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/python_delphi_lsp.egg-info/top_level.txt +0 -0
  51. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/scripts/audit_delphi_language_features.py +0 -0
  52. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/scripts/bootstrap_vllm_opencode_test.ps1 +0 -0
  53. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/scripts/bootstrap_vllm_opencode_test.py +0 -0
  54. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/scripts/bootstrap_vllm_opencode_test.sh +0 -0
  55. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/scripts/check_ornith_cache.py +0 -0
  56. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/scripts/generate_release_evidence.py +0 -0
  57. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/scripts/ollama/ornith-lspctx.Modelfile +0 -0
  58. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/scripts/prepare_ornith_cache.py +0 -0
  59. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/scripts/run_opencode_lsp_probe.py +0 -0
  60. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/scripts/start_ornith_vllm.sh +0 -0
  61. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/scripts/summarize_opencode_request_payloads.py +0 -0
  62. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/setup.cfg +0 -0
  63. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/DeprecatedOnConst.pas +0 -0
  64. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/VariantRecordFieldAttributes.pas +0 -0
  65. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/alignedrecords.pas +0 -0
  66. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/constset.pas +0 -0
  67. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/deprecatedtype.pas +0 -0
  68. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/dottedtypes.pas +0 -0
  69. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/endtoken.pas +0 -0
  70. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/experimentals.pas +0 -0
  71. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/externalfunction.pas +0 -0
  72. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/finalizationinitializationexports.pas +0 -0
  73. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/forwardoverloaded.pas +0 -0
  74. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/forwardwithoutsemicolon.pas +0 -0
  75. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/genericconstraints.pas +0 -0
  76. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/genericinterfacemethoddelegation.pas +0 -0
  77. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/implementsgenerictype.pas +0 -0
  78. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/include file2.inc +0 -0
  79. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/includefile.inc +0 -0
  80. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/includefile.pas +0 -0
  81. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/managedrecords.pas +0 -0
  82. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/messagemethod.pas +0 -0
  83. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/multiline.pas +0 -0
  84. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/nonalignedrecords.pas +0 -0
  85. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/numbers.pas +0 -0
  86. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/pointerchars.pas +0 -0
  87. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/properties.pas +0 -0
  88. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/strictvisibility.pas +0 -0
  89. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/tryexcept.pas +0 -0
  90. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/umlauts.pas +0 -0
  91. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/legacy_snippets/whitespacearoundifdefcondition.pas +0 -0
  92. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/library_demo.dpr +0 -0
  93. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/package_demo.dpk +0 -0
  94. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/program_demo.dpr +0 -0
  95. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/unit_advanced.pas +0 -0
  96. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/unit_attributes.pas +0 -0
  97. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/unit_basic.pas +0 -0
  98. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/unit_consumer.pas +0 -0
  99. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/unit_generics.pas +0 -0
  100. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/unit_inheritance.pas +0 -0
  101. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/unit_math.pas +0 -0
  102. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/unit_properties.pas +0 -0
  103. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/unit_sections.pas +0 -0
  104. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/unit_statements.pas +0 -0
  105. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/unit_types.pas +0 -0
  106. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/unit_unresolved.pas +0 -0
  107. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/fixtures/unit_with.pas +0 -0
  108. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/test_agent_context.py +0 -0
  109. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/test_agent_relations.py +0 -0
  110. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/test_agent_workspace.py +0 -0
  111. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/test_bootstrap_vllm_opencode.py +0 -0
  112. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/test_diagnostics.py +0 -0
  113. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/test_language_feature_matrix.py +0 -0
  114. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/test_legacy_snippets.py +0 -0
  115. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/test_lsp_features.py +0 -0
  116. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/test_lsp_support.py +0 -0
  117. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/test_opencode_config.py +0 -0
  118. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/test_opencode_probe.py +0 -0
  119. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/test_opencode_request_payloads.py +0 -0
  120. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/test_ornith_cache_prepare.py +0 -0
  121. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/test_preprocessor.py +0 -0
  122. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/test_project_discovery.py +0 -0
  123. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/test_project_indexer.py +0 -0
  124. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/test_release_evidence.py +0 -0
  125. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/test_semantic.py +0 -0
  126. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/test_vllm_start_script.py +0 -0
  127. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.1}/tests/test_workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-delphi-lsp
3
- Version: 2.0.0
3
+ Version: 2.0.1
4
4
  Summary: Python Delphi/Object Pascal parser, semantic indexer, and language server.
5
5
  Author: Dark Light
6
6
  License-Expression: MPL-2.0
@@ -41,7 +41,7 @@ Dynamic: license-file
41
41
 
42
42
  `python-delphi-lsp` parses Delphi/Object Pascal, builds semantic and project
43
43
  indexes, serves LSP, and provides bounded codebase navigation for agents.
44
- Version 2.0.0 is authored by Dark Light and supports Windows, macOS, and Linux.
44
+ Version 2.0.1 is authored by Dark Light and supports Windows, macOS, and Linux.
45
45
 
46
46
  ## Install and quick start
47
47
 
@@ -82,6 +82,39 @@ project = ProjectIndexer(
82
82
  print(project.parsed_units)
83
83
  ```
84
84
 
85
+ ## Architecture metrics
86
+
87
+ The public metrics API analyzes a single unit or aggregates a complete project:
88
+
89
+ ```python
90
+ from delphi_lsp import analyze_project, analyze_unit
91
+
92
+ unit = analyze_unit(
93
+ "unit Alpha; interface implementation procedure Run; begin end; end.",
94
+ "Alpha.pas",
95
+ )
96
+ print(unit.lines.total_lines, unit.cyclomatic.maximum)
97
+
98
+ project = analyze_project({
99
+ "Main.dpr": "program Main; uses Alpha; begin end.",
100
+ "Alpha.pas": "unit Alpha; interface implementation end.",
101
+ })
102
+ print(project.total_loc)
103
+ ```
104
+
105
+ Line results distinguish total, source, blank, comment-only, mixed-comment, and
106
+ compiler-directive lines. Project `total_loc` counts each `.dpr`, `.dpk`, and
107
+ `.pas` source once; `include_loc` counts unique `.inc` inputs separately, and
108
+ `total_loc_with_includes` combines both totals.
109
+
110
+ Cyclomatic complexity is reported per routine and as unit/project aggregates.
111
+ The result also includes complete Halstead counts and derived values, a
112
+ normalized 0–100 maintainability index, symbol counts, dependency edges,
113
+ afferent coupling (fan-in), efferent coupling (fan-out), instability,
114
+ abstractness, and distance from the main sequence. Coupling detail separates
115
+ internal from external dependencies. Empty or partial inputs produce finite
116
+ JSON values; unreadable agent-workspace inputs are reported as metric problems.
117
+
85
118
  Run the stdio language server with `delphi-lsp`, or equivalently:
86
119
 
87
120
  ```bash
@@ -149,14 +182,21 @@ delphi-lsp-agent worker --root PATH [--project-file FILE]
149
182
  ```
150
183
 
151
184
  `view --layer` accepts `overview`, `projects`, `units`, `unit`,
152
- `symbols`, `symbol`, `implementation`, `references`, and `problems`.
185
+ `symbols`, `symbol`, `implementation`, `references`, `problems`, and
186
+ `metrics`. For example, `delphi-lsp-agent view --layer metrics --format json`
187
+ returns a project summary and detailed unit metric objects; `--query` filters
188
+ units by name or path.
153
189
  `index` materializes overview, projects, and problems JSON. `skill install`
154
190
  writes the skill; `opencode install` writes both integration files, while
155
191
  `--write-config` additionally writes the restricted agent configuration.
156
192
  `worker` serves NDJSON over standard input/output.
157
193
 
158
194
  Protocol v2 actions are `open`, `find`, `inspect`, `trace`, `focus`,
159
- and `problems`. Detail values are `summary`, `declaration`, `members`,
195
+ `problems`, and `metrics`. A `metrics` request without a query returns the
196
+ project summary followed by unit cards. A query filters units, while a unit
197
+ `target_id` from `open` selects one unit; `detail: "members"` adds routine,
198
+ Halstead, dependency, and symbol-count detail without returning source text.
199
+ Detail values are `summary`, `declaration`, `members`,
160
200
  `context`, `body`, and `implementations`. Relations are `references`,
161
201
  `callers`, `callees`, `uses`, `used_by`, `inherits`, and
162
202
  `implements`.
@@ -231,6 +271,10 @@ Select `vllm-delphi-codebase`, ask it to load
231
271
  `open`, `find`, `focus`, and `inspect`. Use semantic tool calls, not raw
232
272
  source tools.
233
273
 
274
+ For architecture questions, call `metrics` without a query to compare unit
275
+ cards and read project LOC. Then select a returned unit ID with another
276
+ `metrics` call and `detail: "members"` to inspect its routines and coupling.
277
+
234
278
  For the root LSP configuration, a local model can be used as follows:
235
279
 
236
280
  ```bash
@@ -297,6 +341,18 @@ local model server, increase it explicitly with `--probe-timeout SECONDS`.
297
341
 
298
342
  The bundled automatic helper is not a cross-platform startup mechanism.
299
343
 
344
+ The architecture-metrics proof uses a separate deterministic 34-LOC project.
345
+ It requires the restricted model to load the skill, call `metrics` for the
346
+ project and most-complex unit, and report exact LOC, cyclomatic maximum, and
347
+ instability values. Raw source, search, shell, and write tools remain forbidden:
348
+
349
+ ```bash
350
+ python scripts/bootstrap_vllm_codebase_skill_test.py --probe metrics --use-running-server
351
+ ```
352
+
353
+ Use `--probe metrics --start-vllm --max-model-len 24576` for the cached macOS
354
+ auto-start path when no endpoint is already running.
355
+
300
356
  ## Migration to 2.0
301
357
 
302
358
  `delphi_lsp` is the only supported import namespace. Update imports directly;
@@ -2,7 +2,7 @@
2
2
 
3
3
  `python-delphi-lsp` parses Delphi/Object Pascal, builds semantic and project
4
4
  indexes, serves LSP, and provides bounded codebase navigation for agents.
5
- Version 2.0.0 is authored by Dark Light and supports Windows, macOS, and Linux.
5
+ Version 2.0.1 is authored by Dark Light and supports Windows, macOS, and Linux.
6
6
 
7
7
  ## Install and quick start
8
8
 
@@ -43,6 +43,39 @@ project = ProjectIndexer(
43
43
  print(project.parsed_units)
44
44
  ```
45
45
 
46
+ ## Architecture metrics
47
+
48
+ The public metrics API analyzes a single unit or aggregates a complete project:
49
+
50
+ ```python
51
+ from delphi_lsp import analyze_project, analyze_unit
52
+
53
+ unit = analyze_unit(
54
+ "unit Alpha; interface implementation procedure Run; begin end; end.",
55
+ "Alpha.pas",
56
+ )
57
+ print(unit.lines.total_lines, unit.cyclomatic.maximum)
58
+
59
+ project = analyze_project({
60
+ "Main.dpr": "program Main; uses Alpha; begin end.",
61
+ "Alpha.pas": "unit Alpha; interface implementation end.",
62
+ })
63
+ print(project.total_loc)
64
+ ```
65
+
66
+ Line results distinguish total, source, blank, comment-only, mixed-comment, and
67
+ compiler-directive lines. Project `total_loc` counts each `.dpr`, `.dpk`, and
68
+ `.pas` source once; `include_loc` counts unique `.inc` inputs separately, and
69
+ `total_loc_with_includes` combines both totals.
70
+
71
+ Cyclomatic complexity is reported per routine and as unit/project aggregates.
72
+ The result also includes complete Halstead counts and derived values, a
73
+ normalized 0–100 maintainability index, symbol counts, dependency edges,
74
+ afferent coupling (fan-in), efferent coupling (fan-out), instability,
75
+ abstractness, and distance from the main sequence. Coupling detail separates
76
+ internal from external dependencies. Empty or partial inputs produce finite
77
+ JSON values; unreadable agent-workspace inputs are reported as metric problems.
78
+
46
79
  Run the stdio language server with `delphi-lsp`, or equivalently:
47
80
 
48
81
  ```bash
@@ -110,14 +143,21 @@ delphi-lsp-agent worker --root PATH [--project-file FILE]
110
143
  ```
111
144
 
112
145
  `view --layer` accepts `overview`, `projects`, `units`, `unit`,
113
- `symbols`, `symbol`, `implementation`, `references`, and `problems`.
146
+ `symbols`, `symbol`, `implementation`, `references`, `problems`, and
147
+ `metrics`. For example, `delphi-lsp-agent view --layer metrics --format json`
148
+ returns a project summary and detailed unit metric objects; `--query` filters
149
+ units by name or path.
114
150
  `index` materializes overview, projects, and problems JSON. `skill install`
115
151
  writes the skill; `opencode install` writes both integration files, while
116
152
  `--write-config` additionally writes the restricted agent configuration.
117
153
  `worker` serves NDJSON over standard input/output.
118
154
 
119
155
  Protocol v2 actions are `open`, `find`, `inspect`, `trace`, `focus`,
120
- and `problems`. Detail values are `summary`, `declaration`, `members`,
156
+ `problems`, and `metrics`. A `metrics` request without a query returns the
157
+ project summary followed by unit cards. A query filters units, while a unit
158
+ `target_id` from `open` selects one unit; `detail: "members"` adds routine,
159
+ Halstead, dependency, and symbol-count detail without returning source text.
160
+ Detail values are `summary`, `declaration`, `members`,
121
161
  `context`, `body`, and `implementations`. Relations are `references`,
122
162
  `callers`, `callees`, `uses`, `used_by`, `inherits`, and
123
163
  `implements`.
@@ -192,6 +232,10 @@ Select `vllm-delphi-codebase`, ask it to load
192
232
  `open`, `find`, `focus`, and `inspect`. Use semantic tool calls, not raw
193
233
  source tools.
194
234
 
235
+ For architecture questions, call `metrics` without a query to compare unit
236
+ cards and read project LOC. Then select a returned unit ID with another
237
+ `metrics` call and `detail: "members"` to inspect its routines and coupling.
238
+
195
239
  For the root LSP configuration, a local model can be used as follows:
196
240
 
197
241
  ```bash
@@ -258,6 +302,18 @@ local model server, increase it explicitly with `--probe-timeout SECONDS`.
258
302
 
259
303
  The bundled automatic helper is not a cross-platform startup mechanism.
260
304
 
305
+ The architecture-metrics proof uses a separate deterministic 34-LOC project.
306
+ It requires the restricted model to load the skill, call `metrics` for the
307
+ project and most-complex unit, and report exact LOC, cyclomatic maximum, and
308
+ instability values. Raw source, search, shell, and write tools remain forbidden:
309
+
310
+ ```bash
311
+ python scripts/bootstrap_vllm_codebase_skill_test.py --probe metrics --use-running-server
312
+ ```
313
+
314
+ Use `--probe metrics --start-vllm --max-model-len 24576` for the cached macOS
315
+ auto-start path when no endpoint is already running.
316
+
261
317
  ## Migration to 2.0
262
318
 
263
319
  `delphi_lsp` is the only supported import namespace. Update imports directly;
@@ -59,6 +59,17 @@ from .project_indexer import (
59
59
  UnitInfo,
60
60
  UnitParsedHook,
61
61
  )
62
+ from .metrics import (
63
+ CyclomaticMetrics,
64
+ HalsteadMetrics,
65
+ LineMetrics,
66
+ MetricProblem,
67
+ ProjectMetrics,
68
+ RoutineComplexity,
69
+ UnitMetrics,
70
+ analyze_project,
71
+ analyze_unit,
72
+ )
62
73
 
63
74
  __all__ = [
64
75
  '__version__',
@@ -134,6 +145,15 @@ __all__ = [
134
145
  'IncludeFileInfo',
135
146
  'GetUnitSyntaxHook',
136
147
  'UnitParsedHook',
148
+ 'MetricProblem',
149
+ 'LineMetrics',
150
+ 'HalsteadMetrics',
151
+ 'RoutineComplexity',
152
+ 'CyclomaticMetrics',
153
+ 'UnitMetrics',
154
+ 'ProjectMetrics',
155
+ 'analyze_unit',
156
+ 'analyze_project',
137
157
  'dumps',
138
158
  'loads',
139
159
  ]
@@ -0,0 +1 @@
1
+ __version__ = "2.0.1"
@@ -44,6 +44,7 @@ def build_parser() -> argparse.ArgumentParser:
44
44
  "implementation",
45
45
  "references",
46
46
  "problems",
47
+ "metrics",
47
48
  ],
48
49
  )
49
50
  view.add_argument("--query", default="")
@@ -18,14 +18,16 @@ from .agent_protocol import (
18
18
  make_target_id,
19
19
  paginate_items,
20
20
  )
21
+ from .agent_metrics import build_workspace_metrics, project_metric_item, unit_metric_item
21
22
  from .agent_relations import ProjectRelationIndex, RelationTarget
22
- from .agent_workspace import AgentUnit, AgentWorkspace
23
+ from .agent_workspace import AgentWorkspace, unit_display_path, unit_source_path, unit_target_id
23
24
  from .consts import AttributeName, SyntaxNodeType
24
25
  from .lsp_server import build_outline_semantic_model, multiline_string_block_end
25
26
  from .nodes import CompoundSyntaxNode, SyntaxNode
26
27
  from .parser import DelphiParser
27
28
  from .semantic import Scope, ScopeKind, Symbol, SymbolKind
28
29
  from .source_reader import read_source_text
30
+ from .metrics import ProjectMetrics
29
31
 
30
32
 
31
33
  _ROUTINE_KINDS = frozenset(
@@ -238,6 +240,8 @@ class AgentContext:
238
240
  self._last_revision = workspace.workspace_revision
239
241
  self._registry: _Registry | None = None
240
242
  self._relation_index: ProjectRelationIndex | None = None
243
+ self._metrics: ProjectMetrics | None = None
244
+ self._metrics_revision = ""
241
245
 
242
246
  @classmethod
243
247
  def open(
@@ -270,6 +274,8 @@ class AgentContext:
270
274
  self._require_selected_project()
271
275
  items = self._problem_items()
272
276
  return self._response(parsed, revision, items)
277
+ if parsed.action == "metrics":
278
+ return self._handle_metrics(parsed, revision)
273
279
  if parsed.action == "focus":
274
280
  return self._handle_focus(parsed, revision)
275
281
  if parsed.action == "find":
@@ -294,10 +300,14 @@ class AgentContext:
294
300
  if current_project_id != previous_project_id:
295
301
  self._registry = None
296
302
  self._relation_index = None
303
+ self._metrics = None
304
+ self._metrics_revision = ""
297
305
  self._focus = Focus(project_id=current_project_id) if current_project_id else Focus()
298
306
  elif revision != self._last_revision:
299
307
  self._registry = None
300
308
  self._relation_index = None
309
+ self._metrics = None
310
+ self._metrics_revision = ""
301
311
  elif self._focus.project_id != current_project_id:
302
312
  self._focus = Focus(project_id=current_project_id) if current_project_id else Focus()
303
313
  self._last_revision = revision
@@ -323,12 +333,11 @@ class AgentContext:
323
333
  return items
324
334
 
325
335
  for unit in self._workspace.units:
326
- source_path = _unit_source_path(self._workspace.root, unit)
327
- display_path = _stable_source_display_path(self._workspace.root, unit, source_path)
336
+ display_path = unit_display_path(self._workspace.root, unit)
328
337
  items.append(
329
338
  {
330
339
  "item_type": "unit",
331
- "unit_id": make_target_id("unit", display_path, unit.name),
340
+ "unit_id": unit_target_id(self._workspace.root, unit),
332
341
  "name": unit.name,
333
342
  "path": display_path,
334
343
  "has_error": unit.has_error,
@@ -407,6 +416,55 @@ class AgentContext:
407
416
  self._focus = Focus(project_id=self._workspace.active_project_id)
408
417
  return self._response(request, revision, [self._focus.to_mapping()])
409
418
 
419
+ def _handle_metrics(self, request: AgentRequest, revision: str) -> AgentResponse:
420
+ if request.detail not in {"summary", "members"}:
421
+ raise AgentProtocolError(
422
+ "invalid_detail",
423
+ "Metrics supports only summary or members detail.",
424
+ )
425
+ metrics = self._require_metrics(revision)
426
+ detail = request.detail == "members"
427
+ if request.target_id:
428
+ unit = next(
429
+ (candidate for candidate in metrics.units if candidate.unit_id == request.target_id),
430
+ None,
431
+ )
432
+ if unit is None:
433
+ raise AgentProtocolError(
434
+ "target_not_found",
435
+ f"Target not found: {request.target_id}.",
436
+ )
437
+ return self._response(
438
+ request,
439
+ revision,
440
+ [unit_metric_item(unit, detail=detail)],
441
+ target_id=unit.unit_id,
442
+ )
443
+
444
+ units = metrics.units
445
+ if request.query:
446
+ query = request.query.casefold()
447
+ units = tuple(
448
+ unit
449
+ for unit in units
450
+ if query in unit.name.casefold() or query in unit.path.casefold()
451
+ )
452
+ items = [unit_metric_item(unit, detail=detail) for unit in units]
453
+ else:
454
+ items = [
455
+ project_metric_item(metrics),
456
+ *(unit_metric_item(unit, detail=detail) for unit in units),
457
+ ]
458
+ return self._response(request, revision, items)
459
+
460
+ def _require_metrics(self, revision: str) -> ProjectMetrics:
461
+ self._require_selected_project()
462
+ if self._metrics is not None and self._metrics_revision == revision:
463
+ return self._metrics
464
+ self._metrics = build_workspace_metrics(self._workspace)
465
+ self._metrics_revision = revision
466
+ return self._metrics
467
+
410
468
  def _require_registry(self, revision: str) -> _Registry:
411
469
  project_id = self._require_selected_project()
412
470
  if (
@@ -608,8 +666,8 @@ def _build_registry(workspace: AgentWorkspace, project_id: str, revision: str) -
608
666
  raw_symbols: list[_RawSymbol] = []
609
667
  sources: dict[Path, _SourceDocument] = {}
610
668
  for unit in workspace.units:
611
- source_path = _unit_source_path(workspace.root, unit)
612
- display_path = _stable_source_display_path(workspace.root, unit, source_path)
669
+ source_path = unit_source_path(workspace.root, unit)
670
+ display_path = unit_display_path(workspace.root, unit)
613
671
  try:
614
672
  text = read_source_text(source_path)
615
673
  except OSError as exc:
@@ -727,22 +785,6 @@ def _build_registry(workspace: AgentWorkspace, project_id: str, revision: str) -
727
785
  )
728
786
 
729
787
 
730
- def _unit_source_path(root: Path, unit: AgentUnit) -> Path:
731
- path = Path(unit.path)
732
- if not path.is_absolute():
733
- path = root / path
734
- return path.expanduser().resolve()
735
-
736
-
737
- def _stable_source_display_path(root: Path, unit: AgentUnit, source_path: Path) -> str:
738
- try:
739
- return source_path.relative_to(root).as_posix()
740
- except ValueError:
741
- unit_component = _stable_path_component(unit.name or source_path.stem)
742
- file_component = _stable_path_component(source_path.name)
743
- return f"@external/{unit_component}/{file_component}"
744
-
745
-
746
788
  def _stable_path_component(value: str) -> str:
747
789
  normalized = unicodedata.normalize("NFC", value).replace("\\", "_").replace("/", "_")
748
790
  return normalized or "unknown"
@@ -6,6 +6,7 @@ from typing import Any, Iterable
6
6
  import json
7
7
 
8
8
  from .lsp_server import build_outline_semantic_model, outline_source
9
+ from .metrics import analyze_project
9
10
  from .project_discovery import DelphiProjectDiscovery, discover_delphi_project
10
11
  from .project_indexer import ProjectIndexResult, ProjectIndexer
11
12
  from .semantic import Scope, SourceRange, Symbol, SymbolIndex, SymbolKind
@@ -98,6 +99,8 @@ def layer_payload(index: CodebaseIndex, layer: str, *, query: str = "") -> dict[
98
99
  return _references_payload(index, query=query)
99
100
  if normalized_layer == "problems":
100
101
  return _problems_payload(index)
102
+ if normalized_layer == "metrics":
103
+ return _metrics_payload(index, query=query)
101
104
  raise ValueError(f"Unknown layer: {layer}")
102
105
 
103
106
 
@@ -369,6 +372,44 @@ def _problems_payload(index: CodebaseIndex) -> dict[str, Any]:
369
372
  return {"layer": "problems", "root": index.root, "items": items}
370
373
 
371
374
 
375
+ def _metrics_payload(index: CodebaseIndex, *, query: str) -> dict[str, Any]:
376
+ sources: dict[str, str] = {}
377
+ include_sources: dict[str, str] = {}
378
+ for value in index.discovery.source_files:
379
+ path = Path(value)
380
+ try:
381
+ text = read_source_text(path)
382
+ except (OSError, UnicodeError):
383
+ continue
384
+ if path.suffix.casefold() == ".inc":
385
+ include_sources[str(path)] = text
386
+ elif path.suffix.casefold() in {".pas", ".dpr", ".dpk"}:
387
+ sources[str(path)] = text
388
+
389
+ project_name = "Workspace"
390
+ if len(index.discovery.project_files) == 1:
391
+ project_name = Path(index.discovery.project_files[0]).stem
392
+ metrics = analyze_project(
393
+ sources,
394
+ include_sources=include_sources,
395
+ defines=index.discovery.defines,
396
+ include_paths=index.discovery.include_paths,
397
+ project_name=project_name,
398
+ )
399
+ needle = query.casefold().strip()
400
+ units = [
401
+ unit.to_mapping(detail=True)
402
+ for unit in metrics.units
403
+ if not needle or needle in unit.name.casefold() or needle in unit.path.casefold()
404
+ ]
405
+ return {
406
+ "layer": "metrics",
407
+ "root": index.root,
408
+ "project": metrics.to_mapping(),
409
+ "items": units,
410
+ }
411
+
412
+
372
413
  def _all_symbols(index: CodebaseIndex) -> Iterable[Symbol]:
373
414
  for model in index.models.values():
374
415
  yield from _iter_symbols(model.unit_scope)
@@ -486,6 +527,30 @@ def _render_markdown(payload: dict[str, Any]) -> str:
486
527
  elif payload["layer"] == "problems":
487
528
  for item in payload["items"]:
488
529
  lines.append(f"- {item['kind']}: {item['message']} (`{item.get('origin', '')}`)")
530
+ elif payload["layer"] == "metrics":
531
+ project = payload["project"]
532
+ lines.extend(
533
+ [
534
+ f"- Project LOC: {project['total_loc']}",
535
+ f"- Project LOC with includes: {project['total_loc_with_includes']}",
536
+ f"- Units: {project['unit_count']}",
537
+ f"- Maintainability index: {project['maintainability_index']:.2f}",
538
+ ]
539
+ )
540
+ for item in payload["items"]:
541
+ lines.extend(
542
+ [
543
+ "",
544
+ f"## {item['name']}",
545
+ f"- Path: `{item['path']}`",
546
+ f"- LOC: {item['lines']['total_lines']}",
547
+ f"- Cyclomatic maximum: {item['cyclomatic']['maximum']}",
548
+ f"- Maintainability index: {item['maintainability_index']:.2f}",
549
+ f"- Instability: {item['instability']:.3f}",
550
+ f"- Abstractness: {item['abstractness']:.3f}",
551
+ f"- Distance: {item['distance']:.3f}",
552
+ ]
553
+ )
489
554
  return "\n".join(lines).rstrip() + "\n"
490
555
 
491
556
 
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import replace
4
+ from pathlib import Path
5
+
6
+ from .agent_workspace import (
7
+ AgentWorkspace,
8
+ unit_display_path,
9
+ unit_source_path,
10
+ unit_target_id,
11
+ )
12
+ from .metrics import MetricProblem, ProjectMetrics, UnitMetrics, analyze_project
13
+ from .source_reader import read_source_text
14
+
15
+
16
+ def build_workspace_metrics(workspace: AgentWorkspace) -> ProjectMetrics:
17
+ sources: dict[str, str] = {}
18
+ include_sources: dict[str, str] = {}
19
+ unit_ids: dict[str, str] = {}
20
+ problems: list[MetricProblem] = []
21
+
22
+ for unit in workspace.units:
23
+ display_path = unit_display_path(workspace.root, unit)
24
+ source_path = unit_source_path(workspace.root, unit)
25
+ try:
26
+ sources[display_path] = read_source_text(source_path)
27
+ except OSError:
28
+ problems.append(
29
+ MetricProblem(
30
+ kind="source_unavailable",
31
+ path=display_path,
32
+ message="Could not read source.",
33
+ )
34
+ )
35
+ continue
36
+ unit_ids[display_path] = unit_target_id(workspace.root, unit)
37
+
38
+ for include_file in workspace.include_files:
39
+ display_path = include_file["path"]
40
+ source_path = _workspace_path(workspace.root, display_path)
41
+ try:
42
+ include_sources[display_path] = read_source_text(source_path)
43
+ except OSError:
44
+ problems.append(
45
+ MetricProblem(
46
+ kind="include_unavailable",
47
+ path=display_path,
48
+ message="Could not read include file.",
49
+ )
50
+ )
51
+
52
+ project = workspace.active_project
53
+ metrics = analyze_project(
54
+ sources,
55
+ include_sources=include_sources,
56
+ defines=workspace.defines,
57
+ include_paths=workspace.include_paths,
58
+ project_id=workspace.active_project_id,
59
+ project_name=project.name if project is not None else "",
60
+ unit_ids=unit_ids,
61
+ )
62
+ if problems:
63
+ metrics = replace(metrics, problems=(*metrics.problems, *problems))
64
+ return metrics
65
+
66
+
67
+ def project_metric_item(metrics: ProjectMetrics) -> dict[str, object]:
68
+ return {"item_type": "project_metrics", **metrics.to_mapping()}
69
+
70
+
71
+ def unit_metric_item(metrics: UnitMetrics, *, detail: bool = False) -> dict[str, object]:
72
+ return {"item_type": "unit_metrics", **metrics.to_mapping(detail=detail)}
73
+
74
+
75
+ def _workspace_path(root: Path, value: str) -> Path:
76
+ path = Path(value).expanduser()
77
+ if not path.is_absolute():
78
+ path = root / path
79
+ return path.resolve()
80
+
81
+
82
+ __all__ = ["build_workspace_metrics", "project_metric_item", "unit_metric_item"]
@@ -19,6 +19,7 @@ SUPPORTED_ACTIONS = (
19
19
  'trace',
20
20
  'focus',
21
21
  'problems',
22
+ 'metrics',
22
23
  )
23
24
 
24
25
  SUPPORTED_DETAILS = (
@@ -181,13 +181,14 @@ Inspect Delphi/Object Pascal only through `delphi_codebase`; never raw bash/read
181
181
  3. Inspect focused details in this order as needed: `summary`, `declaration`, `members`, `context`, `body`, `implementations`.
182
182
  4. Trace relations with `references`, `callers`, `callees`, `uses`, `used_by`, `inherits`, or `implements`.
183
183
  5. Call `problems` before declaring evidence missing. Explain any `sound_partial` relation metadata rather than treating partial results as complete.
184
- 6. Follow `page.next_cursor` with `cursor` until the needed evidence is available.
184
+ 6. Call `metrics` for project LOC and architecture metrics. Use `query` for unit summaries or a unit `target_id` with `detail=members` for full Halstead, complexity, coupling, abstractness, instability, and distance details.
185
+ 7. Follow `page.next_cursor` with `cursor` until the needed evidence is available.
185
186
 
186
187
  Prefer `summary` and `declaration`, narrow `max_items` and `max_chars`, and request `body` only when it is necessary. Keep the focused target stable while collecting evidence.
187
188
 
188
189
  ## Tool calls
189
190
 
190
- `delphi_codebase` accepts `action` (`open`, `find`, `inspect`, `trace`, `focus`, `problems`), optional `query`, `target_id`, `project_id`, `detail`, `relation`, `cursor`, `max_items` (1-50), and `max_chars` (256-40000). It has no root or path argument; the active OpenCode worktree is used.
191
+ `delphi_codebase` accepts `action` (`open`, `find`, `inspect`, `trace`, `focus`, `problems`, `metrics`), optional `query`, `target_id`, `project_id`, `detail`, `relation`, `cursor`, `max_items` (1-50), and `max_chars` (256-40000). It has no root or path argument; the active OpenCode worktree is used.
191
192
  """
192
193
 
193
194
 
@@ -199,7 +200,7 @@ const PYTHON = __PYTHON_EXECUTABLE__
199
200
  const DEFAULT_REQUEST_TIMEOUT_MS = 120_000
200
201
 
201
202
  type AgentRequest = {
202
- action: "open" | "find" | "inspect" | "trace" | "focus" | "problems"
203
+ action: "open" | "find" | "inspect" | "trace" | "focus" | "problems" | "metrics"
203
204
  query?: string
204
205
  target_id?: string
205
206
  project_id?: string
@@ -464,7 +465,7 @@ export const DelphiCodebasePlugin: Plugin = async (_input) => {
464
465
  delphi_codebase: tool({
465
466
  description: "Protocol v2 semantic Delphi/Object Pascal codebase navigation.",
466
467
  args: {
467
- action: tool.schema.enum(["open", "find", "inspect", "trace", "focus", "problems"]),
468
+ action: tool.schema.enum(["open", "find", "inspect", "trace", "focus", "problems", "metrics"]),
468
469
  query: tool.schema.string().optional(),
469
470
  target_id: tool.schema.string().optional(),
470
471
  project_id: tool.schema.string().optional(),