elspais 0.11.1__tar.gz → 0.11.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 (165) hide show
  1. {elspais-0.11.1 → elspais-0.11.2}/CHANGELOG.md +19 -0
  2. {elspais-0.11.1 → elspais-0.11.2}/PKG-INFO +1 -1
  3. {elspais-0.11.1 → elspais-0.11.2}/pyproject.toml +1 -1
  4. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/__init__.py +1 -1
  5. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/cli.py +29 -10
  6. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/commands/analyze.py +5 -6
  7. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/commands/changed.py +2 -6
  8. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/commands/config_cmd.py +4 -4
  9. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/commands/edit.py +32 -36
  10. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/commands/hash_cmd.py +24 -18
  11. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/commands/index.py +8 -7
  12. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/commands/init.py +4 -4
  13. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/commands/reformat_cmd.py +32 -43
  14. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/commands/rules_cmd.py +6 -2
  15. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/commands/trace.py +23 -19
  16. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/commands/validate.py +8 -10
  17. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/config/defaults.py +7 -1
  18. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/core/content_rules.py +0 -1
  19. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/core/git.py +4 -10
  20. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/core/parser.py +55 -56
  21. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/core/patterns.py +2 -6
  22. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/core/rules.py +10 -15
  23. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/mcp/__init__.py +2 -0
  24. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/mcp/context.py +1 -0
  25. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/mcp/serializers.py +1 -1
  26. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/mcp/server.py +54 -39
  27. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/reformat/__init__.py +13 -13
  28. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/reformat/detector.py +9 -16
  29. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/reformat/hierarchy.py +8 -7
  30. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/reformat/line_breaks.py +36 -38
  31. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/reformat/prompts.py +22 -12
  32. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/reformat/transformer.py +43 -41
  33. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/sponsors/__init__.py +0 -2
  34. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/testing/__init__.py +1 -1
  35. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/testing/result_parser.py +25 -21
  36. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/__init__.py +4 -3
  37. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/coverage.py +5 -5
  38. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/generators/base.py +17 -12
  39. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/generators/csv.py +2 -6
  40. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/generators/markdown.py +3 -8
  41. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/html/__init__.py +4 -2
  42. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/html/generator.py +423 -289
  43. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/models.py +25 -0
  44. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/review/__init__.py +21 -18
  45. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/review/branches.py +114 -121
  46. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/review/models.py +232 -237
  47. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/review/position.py +53 -71
  48. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/review/server.py +264 -288
  49. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/review/status.py +43 -58
  50. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/review/storage.py +48 -72
  51. {elspais-0.11.1 → elspais-0.11.2}/.gitignore +0 -0
  52. {elspais-0.11.1 → elspais-0.11.2}/LICENSE +0 -0
  53. {elspais-0.11.1 → elspais-0.11.2}/README.md +0 -0
  54. {elspais-0.11.1 → elspais-0.11.2}/docs/commands.md +0 -0
  55. {elspais-0.11.1 → elspais-0.11.2}/docs/configuration.md +0 -0
  56. {elspais-0.11.1 → elspais-0.11.2}/docs/multi-repo.md +0 -0
  57. {elspais-0.11.1 → elspais-0.11.2}/docs/patterns.md +0 -0
  58. {elspais-0.11.1 → elspais-0.11.2}/docs/roadmap/llm-reformatting-integration.md +0 -0
  59. {elspais-0.11.1 → elspais-0.11.2}/docs/roadmap/new-format.md +0 -0
  60. {elspais-0.11.1 → elspais-0.11.2}/docs/roadmap/plantuml-diagram-support.md +0 -0
  61. {elspais-0.11.1 → elspais-0.11.2}/docs/roadmap/requirements-format-enhancements.md +0 -0
  62. {elspais-0.11.1 → elspais-0.11.2}/docs/rules.md +0 -0
  63. {elspais-0.11.1 → elspais-0.11.2}/docs/trace-view.md +0 -0
  64. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/__main__.py +0 -0
  65. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/commands/__init__.py +0 -0
  66. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/config/__init__.py +0 -0
  67. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/config/loader.py +0 -0
  68. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/core/__init__.py +0 -0
  69. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/core/hasher.py +0 -0
  70. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/core/models.py +0 -0
  71. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/mcp/__main__.py +0 -0
  72. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/testing/config.py +0 -0
  73. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/testing/mapper.py +0 -0
  74. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/testing/scanner.py +0 -0
  75. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/generators/__init__.py +1 -1
  76. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/html/templates/base.html +0 -0
  77. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/html/templates/components/code_viewer_modal.html +0 -0
  78. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/html/templates/components/file_picker_modal.html +0 -0
  79. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/html/templates/components/legend_modal.html +0 -0
  80. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/html/templates/components/review_panel.html +0 -0
  81. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/html/templates/partials/review/help/help-panel.json +0 -0
  82. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/html/templates/partials/review/help/onboarding.json +0 -0
  83. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/html/templates/partials/review/help/tooltips.json +0 -0
  84. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/html/templates/partials/review/review-comments.js +0 -0
  85. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/html/templates/partials/review/review-data.js +0 -0
  86. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/html/templates/partials/review/review-help.js +0 -0
  87. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/html/templates/partials/review/review-init.js +0 -0
  88. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/html/templates/partials/review/review-line-numbers.js +0 -0
  89. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/html/templates/partials/review/review-packages.js +0 -0
  90. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/html/templates/partials/review/review-position.js +0 -0
  91. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/html/templates/partials/review/review-resize.js +0 -0
  92. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/html/templates/partials/review/review-status.js +0 -0
  93. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/html/templates/partials/review/review-sync.js +0 -0
  94. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/html/templates/partials/review-styles.css +0 -0
  95. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/html/templates/partials/scripts.js +0 -0
  96. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/html/templates/partials/styles.css +0 -0
  97. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/scanning.py +0 -0
  98. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/specs/README.md +0 -0
  99. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/specs/tv-d00001-template-architecture.md +0 -0
  100. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/specs/tv-d00002-css-extraction.md +0 -0
  101. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/specs/tv-d00003-js-extraction.md +0 -0
  102. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/specs/tv-d00004-build-embedding.md +0 -0
  103. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/specs/tv-d00005-test-format.md +0 -0
  104. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/specs/tv-d00010-review-data-models.md +0 -0
  105. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/specs/tv-d00011-review-storage.md +0 -0
  106. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/specs/tv-d00012-position-resolution.md +0 -0
  107. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/specs/tv-d00013-git-branches.md +0 -0
  108. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/specs/tv-d00014-review-api-server.md +0 -0
  109. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/specs/tv-d00015-status-modifier.md +0 -0
  110. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/specs/tv-d00016-js-integration.md +0 -0
  111. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/specs/tv-p00001-html-generator.md +0 -0
  112. {elspais-0.11.1 → elspais-0.11.2}/src/elspais/trace_view/specs/tv-p00002-review-system.md +0 -0
  113. {elspais-0.11.1 → elspais-0.11.2}/tests/conftest.py +0 -0
  114. {elspais-0.11.1 → elspais-0.11.2}/tests/fixtures/assertions/.elspais.toml +0 -0
  115. {elspais-0.11.1 → elspais-0.11.2}/tests/fixtures/assertions/spec/dev-impl.md +0 -0
  116. {elspais-0.11.1 → elspais-0.11.2}/tests/fixtures/assertions/spec/prd-sample.md +0 -0
  117. {elspais-0.11.1 → elspais-0.11.2}/tests/fixtures/associated-repo/.elspais.toml +0 -0
  118. {elspais-0.11.1 → elspais-0.11.2}/tests/fixtures/associated-repo/spec/dev-sponsor.md +0 -0
  119. {elspais-0.11.1 → elspais-0.11.2}/tests/fixtures/associated-repo/spec/prd-sponsor.md +0 -0
  120. {elspais-0.11.1 → elspais-0.11.2}/tests/fixtures/fda-style/.elspais.toml +0 -0
  121. {elspais-0.11.1 → elspais-0.11.2}/tests/fixtures/fda-style/spec/dev-impl.md +0 -0
  122. {elspais-0.11.1 → elspais-0.11.2}/tests/fixtures/fda-style/spec/prd-core.md +0 -0
  123. {elspais-0.11.1 → elspais-0.11.2}/tests/fixtures/hht-like/.elspais.toml +0 -0
  124. {elspais-0.11.1 → elspais-0.11.2}/tests/fixtures/hht-like/database/schema.sql +0 -0
  125. {elspais-0.11.1 → elspais-0.11.2}/tests/fixtures/hht-like/spec/INDEX.md +0 -0
  126. {elspais-0.11.1 → elspais-0.11.2}/tests/fixtures/hht-like/spec/dev-impl.md +0 -0
  127. {elspais-0.11.1 → elspais-0.11.2}/tests/fixtures/hht-like/spec/ops-deploy.md +0 -0
  128. {elspais-0.11.1 → elspais-0.11.2}/tests/fixtures/hht-like/spec/prd-core.md +0 -0
  129. {elspais-0.11.1 → elspais-0.11.2}/tests/fixtures/invalid/broken-links/.elspais.toml +0 -0
  130. {elspais-0.11.1 → elspais-0.11.2}/tests/fixtures/invalid/broken-links/spec/broken.md +0 -0
  131. {elspais-0.11.1 → elspais-0.11.2}/tests/fixtures/invalid/circular-deps/.elspais.toml +0 -0
  132. {elspais-0.11.1 → elspais-0.11.2}/tests/fixtures/invalid/circular-deps/spec/circular.md +0 -0
  133. {elspais-0.11.1 → elspais-0.11.2}/tests/fixtures/invalid/missing-hash/.elspais.toml +0 -0
  134. {elspais-0.11.1 → elspais-0.11.2}/tests/fixtures/invalid/missing-hash/spec/missing.md +0 -0
  135. {elspais-0.11.1 → elspais-0.11.2}/tests/fixtures/jira-style/.elspais.toml +0 -0
  136. {elspais-0.11.1 → elspais-0.11.2}/tests/fixtures/jira-style/requirements/features.md +0 -0
  137. {elspais-0.11.1 → elspais-0.11.2}/tests/fixtures/named-reqs/.elspais.toml +0 -0
  138. {elspais-0.11.1 → elspais-0.11.2}/tests/fixtures/named-reqs/spec/features.md +0 -0
  139. {elspais-0.11.1 → elspais-0.11.2}/tests/mcp/__init__.py +0 -0
  140. {elspais-0.11.1 → elspais-0.11.2}/tests/mcp/test_context.py +0 -0
  141. {elspais-0.11.1 → elspais-0.11.2}/tests/mcp/test_serializers.py +0 -0
  142. {elspais-0.11.1 → elspais-0.11.2}/tests/test_config.py +0 -0
  143. {elspais-0.11.1 → elspais-0.11.2}/tests/test_content_rules.py +0 -0
  144. {elspais-0.11.1 → elspais-0.11.2}/tests/test_doc_sync.py +0 -0
  145. {elspais-0.11.1 → elspais-0.11.2}/tests/test_edit.py +0 -0
  146. {elspais-0.11.1 → elspais-0.11.2}/tests/test_git.py +0 -0
  147. {elspais-0.11.1 → elspais-0.11.2}/tests/test_hash_bugs.py +0 -0
  148. {elspais-0.11.1 → elspais-0.11.2}/tests/test_hasher.py +0 -0
  149. {elspais-0.11.1 → elspais-0.11.2}/tests/test_models.py +0 -0
  150. {elspais-0.11.1 → elspais-0.11.2}/tests/test_parser.py +0 -0
  151. {elspais-0.11.1 → elspais-0.11.2}/tests/test_parser_resilience.py +0 -0
  152. {elspais-0.11.1 → elspais-0.11.2}/tests/test_patterns.py +0 -0
  153. {elspais-0.11.1 → elspais-0.11.2}/tests/test_rules.py +0 -0
  154. {elspais-0.11.1 → elspais-0.11.2}/tests/test_sponsors.py +0 -0
  155. {elspais-0.11.1 → elspais-0.11.2}/tests/test_trace_view/__init__.py +0 -0
  156. {elspais-0.11.1 → elspais-0.11.2}/tests/test_trace_view/test_integration.py +0 -0
  157. {elspais-0.11.1 → elspais-0.11.2}/tests/test_validate_json.py +0 -0
  158. {elspais-0.11.1 → elspais-0.11.2}/tests/testing/__init__.py +0 -0
  159. {elspais-0.11.1 → elspais-0.11.2}/tests/testing/fixtures/junit_results.xml +0 -0
  160. {elspais-0.11.1 → elspais-0.11.2}/tests/testing/fixtures/pytest_results.json +0 -0
  161. {elspais-0.11.1 → elspais-0.11.2}/tests/testing/fixtures/sample_test.py +0 -0
  162. {elspais-0.11.1 → elspais-0.11.2}/tests/testing/test_config.py +0 -0
  163. {elspais-0.11.1 → elspais-0.11.2}/tests/testing/test_mapper.py +0 -0
  164. {elspais-0.11.1 → elspais-0.11.2}/tests/testing/test_result_parser.py +0 -0
  165. {elspais-0.11.1 → elspais-0.11.2}/tests/testing/test_scanner.py +0 -0
@@ -1,5 +1,8 @@
1
1
  # Changelog
2
2
 
3
+ <!-- markdownlint-disable MD022 MD032 -->
4
+ <!-- Compact changelog format: no blank lines around headings/lists -->
5
+
3
6
  All notable changes to elspais will be documented in this file.
4
7
 
5
8
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
@@ -7,6 +10,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
10
 
8
11
  ## [Unreleased]
9
12
 
13
+ ## [0.11.2] - 2026-01-21
14
+
15
+ ### Fixed
16
+
17
+ - Fixed `elspais trace --view` crash caused by missing `is_cycle` and `cycle_path` properties in `TraceViewRequirement`
18
+
19
+ ### Added
20
+
21
+ - Comprehensive git hooks (pre-commit, pre-push, commit-msg) with branch protection, linting, secret detection, and commit message format validation
22
+ - Commit message format validation requiring `[TICKET-NUMBER]` prefix (e.g., `[CUR-514]`)
23
+ - Markdownlint configuration (`.markdownlint.json`) disabling line length and duplicate heading rules
24
+
25
+ ### Changed
26
+
27
+ - Applied ruff and black formatting fixes across the codebase
28
+
10
29
  ## [0.11.1] - 2026-01-15
11
30
 
12
31
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elspais
3
- Version: 0.11.1
3
+ Version: 0.11.2
4
4
  Summary: Requirements validation and traceability tools - L-Space connects all libraries
5
5
  Project-URL: Homepage, https://github.com/anspar/elspais
6
6
  Project-URL: Documentation, https://github.com/anspar/elspais#readme
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "elspais"
7
- version = "0.11.1"
7
+ version = "0.11.2"
8
8
  description = "Requirements validation and traceability tools - L-Space connects all libraries"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -10,7 +10,7 @@ and supports multi-repository requirement management with configurable
10
10
  ID patterns and validation rules.
11
11
  """
12
12
 
13
- from importlib.metadata import version, PackageNotFoundError
13
+ from importlib.metadata import PackageNotFoundError, version
14
14
 
15
15
  try:
16
16
  __version__ = version("elspais")
@@ -10,7 +10,19 @@ from pathlib import Path
10
10
  from typing import List, Optional
11
11
 
12
12
  from elspais import __version__
13
- from elspais.commands import analyze, changed, config_cmd, edit, hash_cmd, index, init, rules_cmd, trace, validate, reformat_cmd
13
+ from elspais.commands import (
14
+ analyze,
15
+ changed,
16
+ config_cmd,
17
+ edit,
18
+ hash_cmd,
19
+ index,
20
+ init,
21
+ reformat_cmd,
22
+ rules_cmd,
23
+ trace,
24
+ validate,
25
+ )
14
26
 
15
27
 
16
28
  def create_parser() -> argparse.ArgumentParser:
@@ -54,12 +66,14 @@ For detailed command help: elspais <command> --help
54
66
  metavar="PATH",
55
67
  )
56
68
  parser.add_argument(
57
- "-v", "--verbose",
69
+ "-v",
70
+ "--verbose",
58
71
  action="store_true",
59
72
  help="Verbose output",
60
73
  )
61
74
  parser.add_argument(
62
- "-q", "--quiet",
75
+ "-q",
76
+ "--quiet",
63
77
  action="store_true",
64
78
  help="Suppress non-error output",
65
79
  )
@@ -105,7 +119,8 @@ Common rules to skip:
105
119
  metavar="RULE",
106
120
  )
107
121
  validate_parser.add_argument(
108
- "-j", "--json",
122
+ "-j",
123
+ "--json",
109
124
  action="store_true",
110
125
  help="Output requirements as JSON (hht_diary compatible format)",
111
126
  )
@@ -262,12 +277,14 @@ Common rules to skip:
262
277
  metavar="BRANCH",
263
278
  )
264
279
  changed_parser.add_argument(
265
- "-j", "--json",
280
+ "-j",
281
+ "--json",
266
282
  action="store_true",
267
283
  help="Output as JSON",
268
284
  )
269
285
  changed_parser.add_argument(
270
- "-a", "--all",
286
+ "-a",
287
+ "--all",
271
288
  action="store_true",
272
289
  help="Include all changed files (not just spec)",
273
290
  )
@@ -374,7 +391,8 @@ JSON batch format:
374
391
  metavar="SECTION",
375
392
  )
376
393
  config_show.add_argument(
377
- "-j", "--json",
394
+ "-j",
395
+ "--json",
378
396
  action="store_true",
379
397
  help="Output as JSON",
380
398
  )
@@ -389,7 +407,8 @@ JSON batch format:
389
407
  help="Configuration key (dot-notation, e.g., 'patterns.prefix')",
390
408
  )
391
409
  config_get.add_argument(
392
- "-j", "--json",
410
+ "-j",
411
+ "--json",
393
412
  action="store_true",
394
413
  help="Output as JSON",
395
414
  )
@@ -405,7 +424,7 @@ JSON batch format:
405
424
  )
406
425
  config_set.add_argument(
407
426
  "value",
408
- help="Value to set (type auto-detected: true/false, numbers, JSON arrays/objects, or string)",
427
+ help="Value to set (auto-detected: bool, number, JSON array/object, string)",
409
428
  )
410
429
 
411
430
  # config unset
@@ -662,7 +681,7 @@ def mcp_command(args: argparse.Namespace) -> int:
662
681
  if hasattr(args, "spec_dir") and args.spec_dir:
663
682
  working_dir = args.spec_dir.parent
664
683
 
665
- print(f"Starting elspais MCP server...")
684
+ print("Starting elspais MCP server...")
666
685
  print(f"Working directory: {working_dir}")
667
686
  print(f"Transport: {args.transport}")
668
687
 
@@ -41,16 +41,14 @@ def run_hierarchy(args: argparse.Namespace) -> int:
41
41
 
42
42
  # Find root requirements (PRD with no implements)
43
43
  roots = [
44
- req for req in requirements.values()
44
+ req
45
+ for req in requirements.values()
45
46
  if req.level.upper() in ["PRD", "PRODUCT"] and not req.implements
46
47
  ]
47
48
 
48
49
  if not roots:
49
50
  # Fall back to all PRD requirements
50
- roots = [
51
- req for req in requirements.values()
52
- if req.level.upper() in ["PRD", "PRODUCT"]
53
- ]
51
+ roots = [req for req in requirements.values() if req.level.upper() in ["PRD", "PRODUCT"]]
54
52
 
55
53
  printed = set()
56
54
 
@@ -153,7 +151,8 @@ def run_coverage(args: argparse.Namespace) -> int:
153
151
 
154
152
  # List unimplemented PRD
155
153
  unimplemented = [
156
- req for req in requirements.values()
154
+ req
155
+ for req in requirements.values()
157
156
  if req.level.upper() in ["PRD", "PRODUCT"] and req.id not in implemented_prd
158
157
  ]
159
158
 
@@ -11,13 +11,11 @@ import argparse
11
11
  import json
12
12
  import sys
13
13
  from pathlib import Path
14
- from typing import Dict, List, Optional
14
+ from typing import Dict, Optional
15
15
 
16
16
  from elspais.config.defaults import DEFAULT_CONFIG
17
17
  from elspais.config.loader import find_config_file, load_config
18
18
  from elspais.core.git import (
19
- GitChangeInfo,
20
- MovedRequirement,
21
19
  detect_moved_requirements,
22
20
  filter_spec_files,
23
21
  get_current_req_locations,
@@ -76,9 +74,7 @@ def run(args: argparse.Namespace) -> int:
76
74
 
77
75
  # Detect moved requirements
78
76
  current_locations = get_current_req_locations(repo_root, spec_dir)
79
- moved = detect_moved_requirements(
80
- changes.committed_req_locations, current_locations
81
- )
77
+ moved = detect_moved_requirements(changes.committed_req_locations, current_locations)
82
78
 
83
79
  # Build result
84
80
  result = {
@@ -8,15 +8,14 @@ import argparse
8
8
  import json
9
9
  import sys
10
10
  from pathlib import Path
11
- from typing import Any, Dict, List, Optional, Tuple, Union
11
+ from typing import Any, Dict, List, Optional, Tuple
12
12
 
13
+ from elspais.config.defaults import DEFAULT_CONFIG
13
14
  from elspais.config.loader import (
14
15
  find_config_file,
15
16
  load_config,
16
- merge_configs,
17
17
  parse_toml,
18
18
  )
19
- from elspais.config.defaults import DEFAULT_CONFIG
20
19
 
21
20
 
22
21
  def run(args: argparse.Namespace) -> int:
@@ -255,6 +254,7 @@ def cmd_path(args: argparse.Namespace) -> int:
255
254
 
256
255
  # Helper functions
257
256
 
257
+
258
258
  def _get_config_path(args: argparse.Namespace) -> Optional[Path]:
259
259
  """Get configuration file path from args or by discovery."""
260
260
  if hasattr(args, "config") and args.config:
@@ -363,7 +363,7 @@ def _print_value(value: Any, prefix: str = "") -> None:
363
363
  if prefix:
364
364
  print(f"{prefix} = {'true' if value else 'false'}")
365
365
  else:
366
- print('true' if value else 'false')
366
+ print("true" if value else "false")
367
367
  elif isinstance(value, str):
368
368
  if prefix:
369
369
  print(f'{prefix} = "{value}"')
@@ -22,7 +22,7 @@ def run(args: argparse.Namespace) -> int:
22
22
  from elspais.config.loader import find_config_file, get_spec_directories, load_config
23
23
 
24
24
  # Load configuration
25
- config_path = args.config if hasattr(args, 'config') else None
25
+ config_path = args.config if hasattr(args, "config") else None
26
26
  if config_path is None:
27
27
  config_path = find_config_file(Path.cwd())
28
28
  if config_path and config_path.exists():
@@ -31,7 +31,7 @@ def run(args: argparse.Namespace) -> int:
31
31
  config = DEFAULT_CONFIG
32
32
 
33
33
  # Get spec directories
34
- spec_dir = args.spec_dir if hasattr(args, 'spec_dir') and args.spec_dir else None
34
+ spec_dir = args.spec_dir if hasattr(args, "spec_dir") and args.spec_dir else None
35
35
  spec_dirs = get_spec_directories(spec_dir, config)
36
36
  if not spec_dirs:
37
37
  print("Error: No spec directories found", file=sys.stderr)
@@ -40,16 +40,16 @@ def run(args: argparse.Namespace) -> int:
40
40
  # Use first spec dir as base
41
41
  base_spec_dir = spec_dirs[0]
42
42
 
43
- dry_run = getattr(args, 'dry_run', False)
43
+ dry_run = getattr(args, "dry_run", False)
44
44
 
45
- validate_refs = getattr(args, 'validate_refs', False)
45
+ validate_refs = getattr(args, "validate_refs", False)
46
46
 
47
47
  # Handle batch mode
48
- if hasattr(args, 'from_json') and args.from_json:
48
+ if hasattr(args, "from_json") and args.from_json:
49
49
  return run_batch_edit(args.from_json, base_spec_dir, dry_run, validate_refs)
50
50
 
51
51
  # Handle single edit mode
52
- if hasattr(args, 'req_id') and args.req_id:
52
+ if hasattr(args, "req_id") and args.req_id:
53
53
  return run_single_edit(args, base_spec_dir, dry_run)
54
54
 
55
55
  print("Error: Must specify --req-id or --from-json", file=sys.stderr)
@@ -109,18 +109,18 @@ def run_single_edit(args: argparse.Namespace, spec_dir: Path, dry_run: bool) ->
109
109
  results = []
110
110
 
111
111
  # Apply implements change
112
- if hasattr(args, 'implements') and args.implements is not None:
112
+ if hasattr(args, "implements") and args.implements is not None:
113
113
  impl_list = [i.strip() for i in args.implements.split(",")]
114
114
  result = modify_implements(file_path, req_id, impl_list, dry_run=dry_run)
115
115
  results.append(("implements", result))
116
116
 
117
117
  # Apply status change
118
- if hasattr(args, 'status') and args.status:
118
+ if hasattr(args, "status") and args.status:
119
119
  result = modify_status(file_path, req_id, args.status, dry_run=dry_run)
120
120
  results.append(("status", result))
121
121
 
122
122
  # Apply move
123
- if hasattr(args, 'move_to') and args.move_to:
123
+ if hasattr(args, "move_to") and args.move_to:
124
124
  dest_path = spec_dir / args.move_to
125
125
  result = move_requirement(file_path, dest_path, req_id, dry_run=dry_run)
126
126
  results.append(("move", result))
@@ -157,14 +157,14 @@ def find_requirement_in_files(
157
157
  Dict with file_path, req_id, line_number, or None if not found
158
158
  """
159
159
  # Pattern to match requirement header
160
- pattern = re.compile(rf'^#\s*{re.escape(req_id)}:', re.MULTILINE)
160
+ pattern = re.compile(rf"^#\s*{re.escape(req_id)}:", re.MULTILINE)
161
161
 
162
162
  for md_file in spec_dir.rglob("*.md"):
163
163
  content = md_file.read_text()
164
164
  match = pattern.search(content)
165
165
  if match:
166
166
  # Count line number
167
- line_number = content[:match.start()].count('\n') + 1
167
+ line_number = content[: match.start()].count("\n") + 1
168
168
  return {
169
169
  "file_path": md_file,
170
170
  "req_id": req_id,
@@ -195,7 +195,7 @@ def modify_implements(
195
195
  content = file_path.read_text()
196
196
 
197
197
  # Find the requirement header
198
- req_pattern = re.compile(rf'^(#\s*{re.escape(req_id)}:[^\n]*\n)', re.MULTILINE)
198
+ req_pattern = re.compile(rf"^(#\s*{re.escape(req_id)}:[^\n]*\n)", re.MULTILINE)
199
199
  req_match = req_pattern.search(content)
200
200
 
201
201
  if not req_match:
@@ -203,9 +203,9 @@ def modify_implements(
203
203
 
204
204
  # Find the **Implements**: field after the header
205
205
  start_pos = req_match.end()
206
- search_region = content[start_pos:start_pos + 500]
206
+ search_region = content[start_pos : start_pos + 500]
207
207
 
208
- impl_pattern = re.compile(r'(\*\*Implements\*\*:\s*)([^|\n]+)')
208
+ impl_pattern = re.compile(r"(\*\*Implements\*\*:\s*)([^|\n]+)")
209
209
  impl_match = impl_pattern.search(search_region)
210
210
 
211
211
  if not impl_match:
@@ -272,7 +272,7 @@ def modify_status(
272
272
  content = file_path.read_text()
273
273
 
274
274
  # Find the requirement header
275
- req_pattern = re.compile(rf'^(#\s*{re.escape(req_id)}:[^\n]*\n)', re.MULTILINE)
275
+ req_pattern = re.compile(rf"^(#\s*{re.escape(req_id)}:[^\n]*\n)", re.MULTILINE)
276
276
  req_match = req_pattern.search(content)
277
277
 
278
278
  if not req_match:
@@ -280,9 +280,9 @@ def modify_status(
280
280
 
281
281
  # Find the **Status**: field after the header
282
282
  start_pos = req_match.end()
283
- search_region = content[start_pos:start_pos + 500]
283
+ search_region = content[start_pos : start_pos + 500]
284
284
 
285
- status_pattern = re.compile(r'(\*\*Status\*\*:\s*)(\w+)')
285
+ status_pattern = re.compile(r"(\*\*Status\*\*:\s*)(\w+)")
286
286
  status_match = status_pattern.search(search_region)
287
287
 
288
288
  if not status_match:
@@ -342,11 +342,8 @@ def move_requirement(
342
342
  # Find the requirement block
343
343
  # Pattern: # REQ-xxx: title ... *End* *title* | **Hash**: xxx\n---
344
344
  req_pattern = re.compile(
345
- rf'(^#\s*{re.escape(req_id)}:[^\n]*\n'
346
- rf'.*?'
347
- rf'\*End\*[^\n]*\n'
348
- rf'(?:---\n)?)',
349
- re.MULTILINE | re.DOTALL
345
+ rf"(^#\s*{re.escape(req_id)}:[^\n]*\n" rf".*?" rf"\*End\*[^\n]*\n" rf"(?:---\n)?)",
346
+ re.MULTILINE | re.DOTALL,
350
347
  )
351
348
 
352
349
  req_match = req_pattern.search(source_content)
@@ -361,9 +358,9 @@ def move_requirement(
361
358
  req_block = req_block.rstrip() + "\n---\n"
362
359
 
363
360
  # Remove from source
364
- new_source_content = source_content[:req_match.start()] + source_content[req_match.end():]
361
+ new_source_content = source_content[: req_match.start()] + source_content[req_match.end() :]
365
362
  # Clean up extra blank lines
366
- new_source_content = re.sub(r'\n{3,}', '\n\n', new_source_content)
363
+ new_source_content = re.sub(r"\n{3,}", "\n\n", new_source_content)
367
364
 
368
365
  # Add to destination
369
366
  dest_content = dest_file.read_text() if dest_file.exists() else ""
@@ -400,8 +397,9 @@ def collect_all_req_ids(spec_dir: Path) -> set:
400
397
  Set of requirement IDs found (short form, e.g., "p00001")
401
398
  """
402
399
  import re
400
+
403
401
  req_ids = set()
404
- pattern = re.compile(r'^#\s*(REQ-[A-Za-z0-9-]+):', re.MULTILINE)
402
+ pattern = re.compile(r"^#\s*(REQ-[A-Za-z0-9-]+):", re.MULTILINE)
405
403
 
406
404
  for md_file in spec_dir.rglob("*.md"):
407
405
  content = md_file.read_text()
@@ -453,11 +451,13 @@ def batch_edit(
453
451
  # Find the requirement
454
452
  location = find_requirement_in_files(spec_dir, req_id)
455
453
  if not location:
456
- results.append({
457
- "success": False,
458
- "req_id": req_id,
459
- "error": f"Requirement {req_id} not found",
460
- })
454
+ results.append(
455
+ {
456
+ "success": False,
457
+ "req_id": req_id,
458
+ "error": f"Requirement {req_id} not found",
459
+ }
460
+ )
461
461
  continue
462
462
 
463
463
  file_path = location["file_path"]
@@ -493,9 +493,7 @@ def batch_edit(
493
493
 
494
494
  # Apply status change
495
495
  if "status" in change:
496
- status_result = modify_status(
497
- file_path, req_id, change["status"], dry_run=dry_run
498
- )
496
+ status_result = modify_status(file_path, req_id, change["status"], dry_run=dry_run)
499
497
  if not status_result["success"]:
500
498
  result = status_result
501
499
  result["req_id"] = req_id
@@ -506,9 +504,7 @@ def batch_edit(
506
504
  # Apply move (must be last since it changes file location)
507
505
  if "move_to" in change:
508
506
  dest_path = spec_dir / change["move_to"]
509
- move_result = move_requirement(
510
- file_path, dest_path, req_id, dry_run=dry_run
511
- )
507
+ move_result = move_requirement(file_path, dest_path, req_id, dry_run=dry_run)
512
508
  if not move_result["success"]:
513
509
  result = move_result
514
510
  result["req_id"] = req_id
@@ -105,15 +105,15 @@ def run_update(args: argparse.Namespace) -> int:
105
105
  print(f"Updating {len(updates)} hashes...")
106
106
  for req_id, req, new_hash in updates:
107
107
  result = update_hash_in_file(req, new_hash)
108
- if result['updated']:
108
+ if result["updated"]:
109
109
  print(f" ✓ {req_id}")
110
- old_hash = result['old_hash'] or "(none)"
110
+ old_hash = result["old_hash"] or "(none)"
111
111
  print(f" [INFO] Hash: {old_hash} -> {result['new_hash']}")
112
- if result['title_fixed']:
112
+ if result["title_fixed"]:
113
113
  print(f" [INFO] Title fixed: \"{result['old_title']}\" -> \"{req.title}\"")
114
114
  else:
115
115
  print(f" ✗ {req_id}")
116
- print(f" [WARN] Could not find End marker to update")
116
+ print(" [WARN] Could not find End marker to update")
117
117
 
118
118
  return 0
119
119
 
@@ -168,11 +168,11 @@ def update_hash_in_file(req: Requirement, new_hash: str) -> dict:
168
168
  import re
169
169
 
170
170
  result = {
171
- 'updated': False,
172
- 'old_hash': req.hash,
173
- 'new_hash': new_hash,
174
- 'title_fixed': False,
175
- 'old_title': None,
171
+ "updated": False,
172
+ "old_hash": req.hash,
173
+ "new_hash": new_hash,
174
+ "title_fixed": False,
175
+ "old_title": None,
176
176
  }
177
177
 
178
178
  if not req.file_path:
@@ -186,35 +186,41 @@ def update_hash_in_file(req: Requirement, new_hash: str) -> dict:
186
186
  # This handles both: (1) normal case, (2) mismatched title case
187
187
 
188
188
  # First try: match by correct title (handles case where titles match)
189
- pattern_by_title = rf"^\*End\*\s+\*{re.escape(req.title)}\*\s*\|\s*\*\*Hash\*\*:\s*[a-fA-F0-9]+\s*$"
189
+ pattern_by_title = (
190
+ rf"^\*End\*\s+\*{re.escape(req.title)}\*\s*\|\s*\*\*Hash\*\*:\s*[a-fA-F0-9]+\s*$"
191
+ )
190
192
  if re.search(pattern_by_title, content, re.MULTILINE):
191
193
  content, count = re.subn(pattern_by_title, new_end_line, content, flags=re.MULTILINE)
192
194
  if count > 0:
193
- result['updated'] = True
195
+ result["updated"] = True
194
196
  else:
195
197
  # Second try: find by hash value (handles mismatched title)
196
198
  # Pattern: *End* *AnyTitle* | **Hash**: oldhash
197
- pattern_by_hash = rf"^\*End\*\s+\*([^*]+)\*\s*\|\s*\*\*Hash\*\*:\s*{re.escape(req.hash)}\s*$"
199
+ pattern_by_hash = (
200
+ rf"^\*End\*\s+\*([^*]+)\*\s*\|\s*\*\*Hash\*\*:\s*{re.escape(req.hash)}\s*$"
201
+ )
198
202
  match = re.search(pattern_by_hash, content, re.MULTILINE)
199
203
 
200
204
  if match:
201
205
  old_title = match.group(1)
202
206
  if old_title != req.title:
203
- result['title_fixed'] = True
204
- result['old_title'] = old_title
207
+ result["title_fixed"] = True
208
+ result["old_title"] = old_title
205
209
 
206
210
  # Replace entire line (only first match to avoid affecting other reqs)
207
- content = re.sub(pattern_by_hash, new_end_line, content, count=1, flags=re.MULTILINE)
208
- result['updated'] = True
211
+ content = re.sub(
212
+ pattern_by_hash, new_end_line, content, count=1, flags=re.MULTILINE
213
+ )
214
+ result["updated"] = True
209
215
  else:
210
216
  # Add hash to end marker (no existing hash)
211
217
  # Pattern: *End* *Title* (without hash)
212
218
  pattern = rf"^(\*End\*\s+\*{re.escape(req.title)}\*)(?!\s*\|\s*\*\*Hash\*\*)\s*$"
213
219
  content, count = re.subn(pattern, new_end_line, content, flags=re.MULTILINE)
214
220
  if count > 0:
215
- result['updated'] = True
221
+ result["updated"] = True
216
222
 
217
- if result['updated']:
223
+ if result["updated"]:
218
224
  req.file_path.write_text(content, encoding="utf-8")
219
225
 
220
226
  return result
@@ -58,6 +58,7 @@ def run_validate(args: argparse.Namespace) -> int:
58
58
  indexed_ids = set()
59
59
 
60
60
  import re
61
+
61
62
  for match in re.finditer(r"\|\s*([A-Z]+-(?:[A-Z]+-)?[a-zA-Z]?\d+)\s*\|", index_content):
62
63
  indexed_ids.add(match.group(1))
63
64
 
@@ -155,12 +156,12 @@ def generate_index(requirements: dict, config: dict) -> str:
155
156
 
156
157
  lines.append("")
157
158
 
158
- lines.extend([
159
- "---",
160
- "",
161
- "*Generated by elspais*",
162
- ])
159
+ lines.extend(
160
+ [
161
+ "---",
162
+ "",
163
+ "*Generated by elspais*",
164
+ ]
165
+ )
163
166
 
164
167
  return "\n".join(lines)
165
-
166
-
@@ -50,7 +50,7 @@ def generate_config(project_type: str, associated_prefix: Optional[str] = None)
50
50
  if project_type == "associated":
51
51
  if associated_prefix is None:
52
52
  associated_prefix = "XXX" # Placeholder if not provided
53
- return f'''# elspais configuration - Associated Repository
53
+ return f"""# elspais configuration - Associated Repository
54
54
  # Generated by: elspais init --type associated
55
55
 
56
56
  [project]
@@ -102,10 +102,10 @@ allow_orphans = true # More permissive for associated development
102
102
  [rules.format]
103
103
  require_hash = true
104
104
  require_assertions = true
105
- '''
105
+ """
106
106
 
107
107
  else: # core
108
- return '''# elspais configuration - Core Repository
108
+ return """# elspais configuration - Core Repository
109
109
  # Generated by: elspais init
110
110
 
111
111
  [project]
@@ -174,4 +174,4 @@ scan_patterns = [
174
174
  "src/**/*.py",
175
175
  "apps/**/*.dart",
176
176
  ]
177
- '''
177
+ """