python-delphi-lsp 2.0.0__tar.gz → 2.0.2__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.2}/PKG-INFO +88 -33
  2. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/README.md +87 -32
  3. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/delphi_lsp/__init__.py +20 -0
  4. python_delphi_lsp-2.0.2/delphi_lsp/_version.py +1 -0
  5. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/delphi_lsp/agent_cli.py +13 -5
  6. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/delphi_lsp/agent_context.py +64 -22
  7. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/delphi_lsp/agent_layers.py +65 -0
  8. python_delphi_lsp-2.0.2/delphi_lsp/agent_metrics.py +82 -0
  9. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/delphi_lsp/agent_protocol.py +1 -0
  10. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/delphi_lsp/agent_templates.py +94 -68
  11. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/delphi_lsp/agent_workspace.py +35 -1
  12. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/delphi_lsp/lark_builder.py +14 -2
  13. python_delphi_lsp-2.0.2/delphi_lsp/metrics.py +759 -0
  14. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/pyproject.toml +1 -1
  15. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2/python_delphi_lsp.egg-info}/PKG-INFO +88 -33
  16. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/python_delphi_lsp.egg-info/SOURCES.txt +4 -0
  17. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/scripts/bootstrap_vllm_codebase_skill_test.py +186 -8
  18. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/test_agent_codebase.py +200 -61
  19. python_delphi_lsp-2.0.2/tests/test_agent_metrics.py +149 -0
  20. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/test_agent_protocol.py +10 -0
  21. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/test_agent_worker.py +2 -0
  22. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/test_bootstrap_vllm_codebase_skill.py +63 -15
  23. python_delphi_lsp-2.0.2/tests/test_metrics.py +188 -0
  24. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/test_opencode_probe.py +1 -1
  25. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/test_package_metadata.py +18 -4
  26. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/test_parser.py +50 -0
  27. python_delphi_lsp-2.0.0/delphi_lsp/_version.py +0 -1
  28. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/LICENSE +0 -0
  29. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/MANIFEST.in +0 -0
  30. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/delphi_lsp/agent_relations.py +0 -0
  31. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/delphi_lsp/binary.py +0 -0
  32. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/delphi_lsp/comment_builder.py +0 -0
  33. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/delphi_lsp/consts.py +0 -0
  34. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/delphi_lsp/grammar.py +0 -0
  35. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/delphi_lsp/lark_tokens.py +0 -0
  36. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/delphi_lsp/lsp_server.py +0 -0
  37. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/delphi_lsp/nodes.py +0 -0
  38. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/delphi_lsp/parser.py +0 -0
  39. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/delphi_lsp/preprocessor.py +0 -0
  40. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/delphi_lsp/project_discovery.py +0 -0
  41. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/delphi_lsp/project_indexer.py +0 -0
  42. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/delphi_lsp/semantic.py +0 -0
  43. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/delphi_lsp/semantic_builder.py +0 -0
  44. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/delphi_lsp/source_reader.py +0 -0
  45. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/delphi_lsp/workspace.py +0 -0
  46. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/delphi_lsp/writer.py +0 -0
  47. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/opencode.json +0 -0
  48. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/python_delphi_lsp.egg-info/dependency_links.txt +0 -0
  49. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/python_delphi_lsp.egg-info/entry_points.txt +0 -0
  50. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/python_delphi_lsp.egg-info/requires.txt +0 -0
  51. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/python_delphi_lsp.egg-info/top_level.txt +0 -0
  52. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/scripts/audit_delphi_language_features.py +0 -0
  53. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/scripts/bootstrap_vllm_opencode_test.ps1 +0 -0
  54. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/scripts/bootstrap_vllm_opencode_test.py +0 -0
  55. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/scripts/bootstrap_vllm_opencode_test.sh +0 -0
  56. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/scripts/check_ornith_cache.py +0 -0
  57. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/scripts/generate_release_evidence.py +0 -0
  58. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/scripts/ollama/ornith-lspctx.Modelfile +0 -0
  59. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/scripts/prepare_ornith_cache.py +0 -0
  60. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/scripts/run_opencode_lsp_probe.py +0 -0
  61. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/scripts/start_ornith_vllm.sh +0 -0
  62. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/scripts/summarize_opencode_request_payloads.py +0 -0
  63. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/setup.cfg +0 -0
  64. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/DeprecatedOnConst.pas +0 -0
  65. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/VariantRecordFieldAttributes.pas +0 -0
  66. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/alignedrecords.pas +0 -0
  67. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/constset.pas +0 -0
  68. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/deprecatedtype.pas +0 -0
  69. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/dottedtypes.pas +0 -0
  70. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/endtoken.pas +0 -0
  71. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/experimentals.pas +0 -0
  72. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/externalfunction.pas +0 -0
  73. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/finalizationinitializationexports.pas +0 -0
  74. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/forwardoverloaded.pas +0 -0
  75. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/forwardwithoutsemicolon.pas +0 -0
  76. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/genericconstraints.pas +0 -0
  77. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/genericinterfacemethoddelegation.pas +0 -0
  78. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/implementsgenerictype.pas +0 -0
  79. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/include file2.inc +0 -0
  80. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/includefile.inc +0 -0
  81. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/includefile.pas +0 -0
  82. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/managedrecords.pas +0 -0
  83. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/messagemethod.pas +0 -0
  84. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/multiline.pas +0 -0
  85. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/nonalignedrecords.pas +0 -0
  86. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/numbers.pas +0 -0
  87. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/pointerchars.pas +0 -0
  88. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/properties.pas +0 -0
  89. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/strictvisibility.pas +0 -0
  90. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/tryexcept.pas +0 -0
  91. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/umlauts.pas +0 -0
  92. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/legacy_snippets/whitespacearoundifdefcondition.pas +0 -0
  93. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/library_demo.dpr +0 -0
  94. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/package_demo.dpk +0 -0
  95. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/program_demo.dpr +0 -0
  96. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/unit_advanced.pas +0 -0
  97. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/unit_attributes.pas +0 -0
  98. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/unit_basic.pas +0 -0
  99. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/unit_consumer.pas +0 -0
  100. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/unit_generics.pas +0 -0
  101. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/unit_inheritance.pas +0 -0
  102. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/unit_math.pas +0 -0
  103. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/unit_properties.pas +0 -0
  104. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/unit_sections.pas +0 -0
  105. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/unit_statements.pas +0 -0
  106. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/unit_types.pas +0 -0
  107. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/unit_unresolved.pas +0 -0
  108. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/fixtures/unit_with.pas +0 -0
  109. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/test_agent_context.py +0 -0
  110. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/test_agent_relations.py +0 -0
  111. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/test_agent_workspace.py +0 -0
  112. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/test_bootstrap_vllm_opencode.py +0 -0
  113. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/test_diagnostics.py +0 -0
  114. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/test_language_feature_matrix.py +0 -0
  115. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/test_legacy_snippets.py +0 -0
  116. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/test_lsp_features.py +0 -0
  117. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/test_lsp_support.py +0 -0
  118. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/test_opencode_config.py +0 -0
  119. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/test_opencode_request_payloads.py +0 -0
  120. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/test_ornith_cache_prepare.py +0 -0
  121. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/test_preprocessor.py +0 -0
  122. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/test_project_discovery.py +0 -0
  123. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/test_project_indexer.py +0 -0
  124. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/test_release_evidence.py +0 -0
  125. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/test_semantic.py +0 -0
  126. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/tests/test_vllm_start_script.py +0 -0
  127. {python_delphi_lsp-2.0.0 → python_delphi_lsp-2.0.2}/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.2
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.2 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
@@ -144,19 +177,27 @@ delphi-lsp-agent view --root PATH [--project-file FILE] --layer LAYER
144
177
  delphi-lsp-agent index --root PATH [--project-file FILE] [--out FILE]
145
178
  delphi-lsp-agent skill install [--target PATH] [--force]
146
179
  delphi-lsp-agent opencode install [--target PATH] [--python PYTHON]
147
- [--force] [--write-config]
180
+ [--force] [--write-agent|--write-config]
148
181
  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
- writes the skill; `opencode install` writes both integration files, while
155
- `--write-config` additionally writes the restricted agent configuration.
190
+ writes the skill; `opencode install` writes the package-named skill, Markdown
191
+ agent, and plugin. The two deprecated write flags are harmless aliases and do
192
+ not change user configuration.
156
193
  `worker` serves NDJSON over standard input/output.
157
194
 
158
195
  Protocol v2 actions are `open`, `find`, `inspect`, `trace`, `focus`,
159
- and `problems`. Detail values are `summary`, `declaration`, `members`,
196
+ `problems`, and `metrics`. A `metrics` request without a query returns the
197
+ project summary followed by unit cards. A query filters units, while a unit
198
+ `target_id` from `open` selects one unit; `detail: "members"` adds routine,
199
+ Halstead, dependency, and symbol-count detail without returning source text.
200
+ Detail values are `summary`, `declaration`, `members`,
160
201
  `context`, `body`, and `implementations`. Relations are `references`,
161
202
  `callers`, `callees`, `uses`, `used_by`, `inherits`, and
162
203
  `implements`.
@@ -185,52 +226,54 @@ optimization does not remove LSP functionality.
185
226
  Install the generated integration in a worktree:
186
227
 
187
228
  ```bash
188
- delphi-lsp-agent opencode install --target . --write-config
229
+ delphi-lsp-agent opencode install --target .
189
230
  ```
190
231
 
191
232
  It writes:
192
233
 
193
234
  ```text
194
- .agents/skills/delphi-codebase-navigator/SKILL.md
235
+ .agents/skills/python-delphi-lsp/SKILL.md
195
236
  .opencode/plugins/delphi_codebase.ts
237
+ .opencode/agents/python-delphi-lsp.md
196
238
  ```
197
239
 
198
- The generated configuration enables only the named
199
- `delphi-codebase-navigator` skill and `delphi_codebase`. It denies
240
+ The package-named Markdown agent enables only the
241
+ `python-delphi-lsp` skill and `delphi_codebase`. It denies
200
242
  `bash`, `read`, `glob`, `grep`, and `lsp`, along with edit/write and
201
243
  other raw source tools. The skill is enabled. The installer does not use the
202
- retired `.opencode/tools` path.
244
+ retired `.opencode/tools` path and never reads or changes `opencode.json`; that
245
+ file remains entirely user-owned. The deprecated `--write-config` and
246
+ `--write-agent` options are accepted harmlessly for compatibility.
203
247
 
204
248
  The plugin maintains one worker per session/root, reusing focus and indexes.
205
249
  During compaction it restores the focus and summary into the new context.
206
250
  Transport failure, session deletion, and plugin disposal clean up the worker.
207
251
 
208
- A generated OpenCode agent looks like this; providers and unrelated agents stay
209
- unchanged:
210
-
211
- ```json
212
- {
213
- "agent": {
214
- "vllm-delphi-codebase": {
215
- "tools": {
216
- "delphi_codebase": true, "skill": true, "lsp": false,
217
- "bash": false, "read": false, "glob": false, "grep": false
218
- },
219
- "permission": {
220
- "delphi_codebase": "allow",
221
- "skill": {"*": "deny", "delphi-codebase-navigator": "allow"},
222
- "lsp": "deny"
223
- }
224
- }
225
- }
226
- }
252
+ A generated OpenCode agent starts with this Markdown frontmatter:
253
+
254
+ ```markdown
255
+ ---
256
+ description: Inspect Delphi and Object Pascal codebases through python-delphi-lsp.
257
+ mode: subagent
258
+ temperature: 0
259
+ permission:
260
+ "*": deny
261
+ delphi_codebase: allow
262
+ skill:
263
+ "*": deny
264
+ python-delphi-lsp: allow
265
+ ---
227
266
  ```
228
267
 
229
- Select `vllm-delphi-codebase`, ask it to load
230
- `delphi-codebase-navigator`, then use `delphi_codebase` actions such as
268
+ Select `python-delphi-lsp`, ask it to load the `python-delphi-lsp` skill, then
269
+ use `delphi_codebase` actions such as
231
270
  `open`, `find`, `focus`, and `inspect`. Use semantic tool calls, not raw
232
271
  source tools.
233
272
 
273
+ For architecture questions, call `metrics` without a query to compare unit
274
+ cards and read project LOC. Then select a returned unit ID with another
275
+ `metrics` call and `detail: "members"` to inspect its routines and coupling.
276
+
234
277
  For the root LSP configuration, a local model can be used as follows:
235
278
 
236
279
  ```bash
@@ -297,6 +340,18 @@ local model server, increase it explicitly with `--probe-timeout SECONDS`.
297
340
 
298
341
  The bundled automatic helper is not a cross-platform startup mechanism.
299
342
 
343
+ The architecture-metrics proof uses a separate deterministic 34-LOC project.
344
+ It requires the restricted model to load the skill, call `metrics` for the
345
+ project and most-complex unit, and report exact LOC, cyclomatic maximum, and
346
+ instability values. Raw source, search, shell, and write tools remain forbidden:
347
+
348
+ ```bash
349
+ python scripts/bootstrap_vllm_codebase_skill_test.py --probe metrics --use-running-server
350
+ ```
351
+
352
+ Use `--probe metrics --start-vllm --max-model-len 24576` for the cached macOS
353
+ auto-start path when no endpoint is already running.
354
+
300
355
  ## Migration to 2.0
301
356
 
302
357
  `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.2 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
@@ -105,19 +138,27 @@ delphi-lsp-agent view --root PATH [--project-file FILE] --layer LAYER
105
138
  delphi-lsp-agent index --root PATH [--project-file FILE] [--out FILE]
106
139
  delphi-lsp-agent skill install [--target PATH] [--force]
107
140
  delphi-lsp-agent opencode install [--target PATH] [--python PYTHON]
108
- [--force] [--write-config]
141
+ [--force] [--write-agent|--write-config]
109
142
  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
- writes the skill; `opencode install` writes both integration files, while
116
- `--write-config` additionally writes the restricted agent configuration.
151
+ writes the skill; `opencode install` writes the package-named skill, Markdown
152
+ agent, and plugin. The two deprecated write flags are harmless aliases and do
153
+ not change user configuration.
117
154
  `worker` serves NDJSON over standard input/output.
118
155
 
119
156
  Protocol v2 actions are `open`, `find`, `inspect`, `trace`, `focus`,
120
- and `problems`. Detail values are `summary`, `declaration`, `members`,
157
+ `problems`, and `metrics`. A `metrics` request without a query returns the
158
+ project summary followed by unit cards. A query filters units, while a unit
159
+ `target_id` from `open` selects one unit; `detail: "members"` adds routine,
160
+ Halstead, dependency, and symbol-count detail without returning source text.
161
+ Detail values are `summary`, `declaration`, `members`,
121
162
  `context`, `body`, and `implementations`. Relations are `references`,
122
163
  `callers`, `callees`, `uses`, `used_by`, `inherits`, and
123
164
  `implements`.
@@ -146,52 +187,54 @@ optimization does not remove LSP functionality.
146
187
  Install the generated integration in a worktree:
147
188
 
148
189
  ```bash
149
- delphi-lsp-agent opencode install --target . --write-config
190
+ delphi-lsp-agent opencode install --target .
150
191
  ```
151
192
 
152
193
  It writes:
153
194
 
154
195
  ```text
155
- .agents/skills/delphi-codebase-navigator/SKILL.md
196
+ .agents/skills/python-delphi-lsp/SKILL.md
156
197
  .opencode/plugins/delphi_codebase.ts
198
+ .opencode/agents/python-delphi-lsp.md
157
199
  ```
158
200
 
159
- The generated configuration enables only the named
160
- `delphi-codebase-navigator` skill and `delphi_codebase`. It denies
201
+ The package-named Markdown agent enables only the
202
+ `python-delphi-lsp` skill and `delphi_codebase`. It denies
161
203
  `bash`, `read`, `glob`, `grep`, and `lsp`, along with edit/write and
162
204
  other raw source tools. The skill is enabled. The installer does not use the
163
- retired `.opencode/tools` path.
205
+ retired `.opencode/tools` path and never reads or changes `opencode.json`; that
206
+ file remains entirely user-owned. The deprecated `--write-config` and
207
+ `--write-agent` options are accepted harmlessly for compatibility.
164
208
 
165
209
  The plugin maintains one worker per session/root, reusing focus and indexes.
166
210
  During compaction it restores the focus and summary into the new context.
167
211
  Transport failure, session deletion, and plugin disposal clean up the worker.
168
212
 
169
- A generated OpenCode agent looks like this; providers and unrelated agents stay
170
- unchanged:
171
-
172
- ```json
173
- {
174
- "agent": {
175
- "vllm-delphi-codebase": {
176
- "tools": {
177
- "delphi_codebase": true, "skill": true, "lsp": false,
178
- "bash": false, "read": false, "glob": false, "grep": false
179
- },
180
- "permission": {
181
- "delphi_codebase": "allow",
182
- "skill": {"*": "deny", "delphi-codebase-navigator": "allow"},
183
- "lsp": "deny"
184
- }
185
- }
186
- }
187
- }
213
+ A generated OpenCode agent starts with this Markdown frontmatter:
214
+
215
+ ```markdown
216
+ ---
217
+ description: Inspect Delphi and Object Pascal codebases through python-delphi-lsp.
218
+ mode: subagent
219
+ temperature: 0
220
+ permission:
221
+ "*": deny
222
+ delphi_codebase: allow
223
+ skill:
224
+ "*": deny
225
+ python-delphi-lsp: allow
226
+ ---
188
227
  ```
189
228
 
190
- Select `vllm-delphi-codebase`, ask it to load
191
- `delphi-codebase-navigator`, then use `delphi_codebase` actions such as
229
+ Select `python-delphi-lsp`, ask it to load the `python-delphi-lsp` skill, then
230
+ use `delphi_codebase` actions such as
192
231
  `open`, `find`, `focus`, and `inspect`. Use semantic tool calls, not raw
193
232
  source tools.
194
233
 
234
+ For architecture questions, call `metrics` without a query to compare unit
235
+ cards and read project LOC. Then select a returned unit ID with another
236
+ `metrics` call and `detail: "members"` to inspect its routines and coupling.
237
+
195
238
  For the root LSP configuration, a local model can be used as follows:
196
239
 
197
240
  ```bash
@@ -258,6 +301,18 @@ local model server, increase it explicitly with `--probe-timeout SECONDS`.
258
301
 
259
302
  The bundled automatic helper is not a cross-platform startup mechanism.
260
303
 
304
+ The architecture-metrics proof uses a separate deterministic 34-LOC project.
305
+ It requires the restricted model to load the skill, call `metrics` for the
306
+ project and most-complex unit, and report exact LOC, cyclomatic maximum, and
307
+ instability values. Raw source, search, shell, and write tools remain forbidden:
308
+
309
+ ```bash
310
+ python scripts/bootstrap_vllm_codebase_skill_test.py --probe metrics --use-running-server
311
+ ```
312
+
313
+ Use `--probe metrics --start-vllm --max-model-len 24576` for the cached macOS
314
+ auto-start path when no endpoint is already running.
315
+
261
316
  ## Migration to 2.0
262
317
 
263
318
  `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.2"
@@ -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="")
@@ -66,11 +67,19 @@ def build_parser() -> argparse.ArgumentParser:
66
67
 
67
68
  opencode = subcommands.add_parser("opencode", help="Install opencode integration.")
68
69
  opencode_commands = opencode.add_subparsers(dest="opencode_command", required=True)
69
- opencode_install = opencode_commands.add_parser("install", help="Install .agents skill and opencode plugin.")
70
+ opencode_install = opencode_commands.add_parser(
71
+ "install", help="Install the package-named skill, Markdown agent, and OpenCode plugin."
72
+ )
70
73
  opencode_install.add_argument("--target", type=Path, default=Path("."))
71
74
  opencode_install.add_argument("--python", default=sys.executable)
72
75
  opencode_install.add_argument("--force", action="store_true")
73
- opencode_install.add_argument("--write-config", action="store_true")
76
+ opencode_install.add_argument(
77
+ "--write-agent",
78
+ "--write-config",
79
+ dest="write_config",
80
+ action="store_true",
81
+ help="Deprecated compatibility option; the Markdown agent is always installed and opencode.json is never touched.",
82
+ )
74
83
  opencode_install.set_defaults(func=_opencode_install)
75
84
 
76
85
  worker = subcommands.add_parser("worker", help="Serve Protocol v2 NDJSON requests.")
@@ -138,7 +147,7 @@ def _skill_install(args: argparse.Namespace) -> None:
138
147
 
139
148
 
140
149
  def _opencode_install(args: argparse.Namespace) -> None:
141
- skill_path, plugin_path, config_path = install_opencode_support(
150
+ skill_path, plugin_path, agent_path = install_opencode_support(
142
151
  args.target,
143
152
  python_executable=args.python,
144
153
  force=args.force,
@@ -146,8 +155,7 @@ def _opencode_install(args: argparse.Namespace) -> None:
146
155
  )
147
156
  print(skill_path)
148
157
  print(plugin_path)
149
- if config_path is not None:
150
- print(config_path)
158
+ print(agent_path)
151
159
 
152
160
 
153
161
  def _worker(args: argparse.Namespace) -> None:
@@ -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"