elspais 0.9.3__py3-none-any.whl → 0.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. elspais/cli.py +99 -1
  2. elspais/commands/hash_cmd.py +72 -26
  3. elspais/commands/reformat_cmd.py +458 -0
  4. elspais/commands/trace.py +157 -3
  5. elspais/commands/validate.py +44 -16
  6. elspais/core/models.py +2 -0
  7. elspais/core/parser.py +68 -24
  8. elspais/reformat/__init__.py +50 -0
  9. elspais/reformat/detector.py +119 -0
  10. elspais/reformat/hierarchy.py +246 -0
  11. elspais/reformat/line_breaks.py +220 -0
  12. elspais/reformat/prompts.py +123 -0
  13. elspais/reformat/transformer.py +264 -0
  14. elspais/sponsors/__init__.py +432 -0
  15. elspais/trace_view/__init__.py +54 -0
  16. elspais/trace_view/coverage.py +183 -0
  17. elspais/trace_view/generators/__init__.py +12 -0
  18. elspais/trace_view/generators/base.py +329 -0
  19. elspais/trace_view/generators/csv.py +122 -0
  20. elspais/trace_view/generators/markdown.py +175 -0
  21. elspais/trace_view/html/__init__.py +31 -0
  22. elspais/trace_view/html/generator.py +1006 -0
  23. elspais/trace_view/html/templates/base.html +283 -0
  24. elspais/trace_view/html/templates/components/code_viewer_modal.html +14 -0
  25. elspais/trace_view/html/templates/components/file_picker_modal.html +20 -0
  26. elspais/trace_view/html/templates/components/legend_modal.html +69 -0
  27. elspais/trace_view/html/templates/components/review_panel.html +118 -0
  28. elspais/trace_view/html/templates/partials/review/help/help-panel.json +244 -0
  29. elspais/trace_view/html/templates/partials/review/help/onboarding.json +77 -0
  30. elspais/trace_view/html/templates/partials/review/help/tooltips.json +237 -0
  31. elspais/trace_view/html/templates/partials/review/review-comments.js +928 -0
  32. elspais/trace_view/html/templates/partials/review/review-data.js +961 -0
  33. elspais/trace_view/html/templates/partials/review/review-help.js +679 -0
  34. elspais/trace_view/html/templates/partials/review/review-init.js +177 -0
  35. elspais/trace_view/html/templates/partials/review/review-line-numbers.js +429 -0
  36. elspais/trace_view/html/templates/partials/review/review-packages.js +1029 -0
  37. elspais/trace_view/html/templates/partials/review/review-position.js +540 -0
  38. elspais/trace_view/html/templates/partials/review/review-resize.js +115 -0
  39. elspais/trace_view/html/templates/partials/review/review-status.js +659 -0
  40. elspais/trace_view/html/templates/partials/review/review-sync.js +992 -0
  41. elspais/trace_view/html/templates/partials/review-styles.css +2238 -0
  42. elspais/trace_view/html/templates/partials/scripts.js +1741 -0
  43. elspais/trace_view/html/templates/partials/styles.css +1756 -0
  44. elspais/trace_view/models.py +353 -0
  45. elspais/trace_view/review/__init__.py +60 -0
  46. elspais/trace_view/review/branches.py +1149 -0
  47. elspais/trace_view/review/models.py +1205 -0
  48. elspais/trace_view/review/position.py +609 -0
  49. elspais/trace_view/review/server.py +1056 -0
  50. elspais/trace_view/review/status.py +470 -0
  51. elspais/trace_view/review/storage.py +1367 -0
  52. elspais/trace_view/scanning.py +213 -0
  53. elspais/trace_view/specs/README.md +84 -0
  54. elspais/trace_view/specs/tv-d00001-template-architecture.md +36 -0
  55. elspais/trace_view/specs/tv-d00002-css-extraction.md +37 -0
  56. elspais/trace_view/specs/tv-d00003-js-extraction.md +43 -0
  57. elspais/trace_view/specs/tv-d00004-build-embedding.md +40 -0
  58. elspais/trace_view/specs/tv-d00005-test-format.md +78 -0
  59. elspais/trace_view/specs/tv-d00010-review-data-models.md +33 -0
  60. elspais/trace_view/specs/tv-d00011-review-storage.md +33 -0
  61. elspais/trace_view/specs/tv-d00012-position-resolution.md +33 -0
  62. elspais/trace_view/specs/tv-d00013-git-branches.md +31 -0
  63. elspais/trace_view/specs/tv-d00014-review-api-server.md +31 -0
  64. elspais/trace_view/specs/tv-d00015-status-modifier.md +27 -0
  65. elspais/trace_view/specs/tv-d00016-js-integration.md +33 -0
  66. elspais/trace_view/specs/tv-p00001-html-generator.md +33 -0
  67. elspais/trace_view/specs/tv-p00002-review-system.md +29 -0
  68. {elspais-0.9.3.dist-info → elspais-0.11.0.dist-info}/METADATA +33 -18
  69. elspais-0.11.0.dist-info/RECORD +101 -0
  70. elspais-0.9.3.dist-info/RECORD +0 -40
  71. {elspais-0.9.3.dist-info → elspais-0.11.0.dist-info}/WHEEL +0 -0
  72. {elspais-0.9.3.dist-info → elspais-0.11.0.dist-info}/entry_points.txt +0 -0
  73. {elspais-0.9.3.dist-info → elspais-0.11.0.dist-info}/licenses/LICENSE +0 -0
elspais/cli.py CHANGED
@@ -10,7 +10,7 @@ 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
13
+ from elspais.commands import analyze, changed, config_cmd, edit, hash_cmd, index, init, rules_cmd, trace, validate, reformat_cmd
14
14
 
15
15
 
16
16
  def create_parser() -> argparse.ArgumentParser:
@@ -97,6 +97,12 @@ Examples:
97
97
  action="store_true",
98
98
  help="Skip test scanning",
99
99
  )
100
+ validate_parser.add_argument(
101
+ "--mode",
102
+ choices=["core", "combined"],
103
+ default="combined",
104
+ help="core: skip sponsor repos, combined: include all (default: combined)",
105
+ )
100
106
 
101
107
  # trace command
102
108
  trace_parser = subparsers.add_parser(
@@ -115,6 +121,49 @@ Examples:
115
121
  help="Output file path",
116
122
  metavar="PATH",
117
123
  )
124
+ # trace-view enhanced options (requires elspais[trace-view])
125
+ trace_parser.add_argument(
126
+ "--view",
127
+ action="store_true",
128
+ help="Generate interactive HTML traceability view (requires trace-view extra)",
129
+ )
130
+ trace_parser.add_argument(
131
+ "--embed-content",
132
+ action="store_true",
133
+ help="Embed full requirement content in HTML output",
134
+ )
135
+ trace_parser.add_argument(
136
+ "--edit-mode",
137
+ action="store_true",
138
+ help="Include edit mode UI in HTML output",
139
+ )
140
+ trace_parser.add_argument(
141
+ "--review-mode",
142
+ action="store_true",
143
+ help="Include review mode UI in HTML output",
144
+ )
145
+ trace_parser.add_argument(
146
+ "--server",
147
+ action="store_true",
148
+ help="Start review server (requires trace-review extra)",
149
+ )
150
+ trace_parser.add_argument(
151
+ "--port",
152
+ type=int,
153
+ default=8080,
154
+ help="Port for review server (default: 8080)",
155
+ )
156
+ trace_parser.add_argument(
157
+ "--mode",
158
+ choices=["core", "sponsor", "combined"],
159
+ default="core",
160
+ help="Report mode: core, sponsor, or combined (default: core)",
161
+ )
162
+ trace_parser.add_argument(
163
+ "--sponsor",
164
+ help="Sponsor name for sponsor-specific reports",
165
+ metavar="NAME",
166
+ )
118
167
 
119
168
  # hash command
120
169
  hash_parser = subparsers.add_parser(
@@ -393,6 +442,53 @@ Examples:
393
442
  help="Content rule file name (e.g., 'AI-AGENT.md')",
394
443
  )
395
444
 
445
+ # reformat-with-claude command
446
+ reformat_parser = subparsers.add_parser(
447
+ "reformat-with-claude",
448
+ help="Reformat requirements using AI (Acceptance Criteria -> Assertions)",
449
+ )
450
+ reformat_parser.add_argument(
451
+ "--start-req",
452
+ help="Starting requirement ID (default: all PRD requirements)",
453
+ metavar="ID",
454
+ )
455
+ reformat_parser.add_argument(
456
+ "--depth",
457
+ type=int,
458
+ help="Maximum traversal depth (default: unlimited)",
459
+ )
460
+ reformat_parser.add_argument(
461
+ "--dry-run",
462
+ action="store_true",
463
+ help="Preview changes without applying",
464
+ )
465
+ reformat_parser.add_argument(
466
+ "--backup",
467
+ action="store_true",
468
+ help="Create .bak files before editing",
469
+ )
470
+ reformat_parser.add_argument(
471
+ "--force",
472
+ action="store_true",
473
+ help="Reformat even if already in new format",
474
+ )
475
+ reformat_parser.add_argument(
476
+ "--fix-line-breaks",
477
+ action="store_true",
478
+ help="Normalize line breaks (remove extra blank lines)",
479
+ )
480
+ reformat_parser.add_argument(
481
+ "--line-breaks-only",
482
+ action="store_true",
483
+ help="Only fix line breaks, skip AI-based reformatting",
484
+ )
485
+ reformat_parser.add_argument(
486
+ "--mode",
487
+ choices=["combined", "core-only", "local-only"],
488
+ default="combined",
489
+ help="Which repos to include in hierarchy (default: combined)",
490
+ )
491
+
396
492
  # mcp command
397
493
  mcp_parser = subparsers.add_parser(
398
494
  "mcp",
@@ -489,6 +585,8 @@ def main(argv: Optional[List[str]] = None) -> int:
489
585
  return config_cmd.run(args)
490
586
  elif args.command == "rules":
491
587
  return rules_cmd.run(args)
588
+ elif args.command == "reformat-with-claude":
589
+ return reformat_cmd.run(args)
492
590
  elif args.command == "mcp":
493
591
  return mcp_command(args)
494
592
  else:
@@ -104,8 +104,16 @@ def run_update(args: argparse.Namespace) -> int:
104
104
  else:
105
105
  print(f"Updating {len(updates)} hashes...")
106
106
  for req_id, req, new_hash in updates:
107
- update_hash_in_file(req, new_hash)
108
- print(f" ✓ {req_id}")
107
+ result = update_hash_in_file(req, new_hash)
108
+ if result['updated']:
109
+ print(f" ✓ {req_id}")
110
+ old_hash = result['old_hash'] or "(none)"
111
+ print(f" [INFO] Hash: {old_hash} -> {result['new_hash']}")
112
+ if result['title_fixed']:
113
+ print(f" [INFO] Title fixed: \"{result['old_title']}\" -> \"{req.title}\"")
114
+ else:
115
+ print(f" ✗ {req_id}")
116
+ print(f" [WARN] Could not find End marker to update")
109
117
 
110
118
  return 0
111
119
 
@@ -138,37 +146,75 @@ def load_requirements(args: argparse.Namespace) -> tuple:
138
146
  return config, requirements
139
147
 
140
148
 
141
- def update_hash_in_file(req: Requirement, new_hash: str) -> None:
149
+ def update_hash_in_file(req: Requirement, new_hash: str) -> dict:
142
150
  """Update the hash in the requirement's source file.
143
151
 
144
- The replacement is scoped to the specific requirement's end marker
145
- (identified by title) to avoid accidentally updating other requirements
146
- in the same file that might have the same hash value.
152
+ Finds the End marker by the old hash value, then replaces the entire line
153
+ with the correct title and new hash. This handles cases where the End
154
+ marker title doesn't match the header title.
155
+
156
+ Args:
157
+ req: Requirement object with file_path, title, and hash
158
+ new_hash: New hash value to write
159
+
160
+ Returns:
161
+ Dict with change info:
162
+ - 'updated': bool - whether file was modified
163
+ - 'old_hash': str - previous hash (or None)
164
+ - 'new_hash': str - new hash value
165
+ - 'title_fixed': bool - whether title was corrected
166
+ - 'old_title': str - previous title (if different)
147
167
  """
168
+ import re
169
+
170
+ result = {
171
+ 'updated': False,
172
+ 'old_hash': req.hash,
173
+ 'new_hash': new_hash,
174
+ 'title_fixed': False,
175
+ 'old_title': None,
176
+ }
177
+
148
178
  if not req.file_path:
149
- return
179
+ return result
150
180
 
151
181
  content = req.file_path.read_text(encoding="utf-8")
152
-
153
- import re
182
+ new_end_line = f"*End* *{req.title}* | **Hash**: {new_hash}"
154
183
 
155
184
  if req.hash:
156
- # Replace existing hash - SCOPED to this requirement's end marker
157
- # Match: *End* *Title* | **Hash**: oldhash
158
- # Replace hash only for THIS requirement (identified by title)
159
- content = re.sub(
160
- rf"(\*End\*\s+\*{re.escape(req.title)}\*\s*\|\s*)\*\*Hash\*\*:\s*{re.escape(req.hash)}",
161
- rf"\1**Hash**: {new_hash}",
162
- content,
163
- )
185
+ # Strategy: Try title first (most specific), then hash if title not found
186
+ # This handles both: (1) normal case, (2) mismatched title case
187
+
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*$"
190
+ if re.search(pattern_by_title, content, re.MULTILINE):
191
+ content, count = re.subn(pattern_by_title, new_end_line, content, flags=re.MULTILINE)
192
+ if count > 0:
193
+ result['updated'] = True
194
+ else:
195
+ # Second try: find by hash value (handles mismatched title)
196
+ # Pattern: *End* *AnyTitle* | **Hash**: oldhash
197
+ pattern_by_hash = rf"^\*End\*\s+\*([^*]+)\*\s*\|\s*\*\*Hash\*\*:\s*{re.escape(req.hash)}\s*$"
198
+ match = re.search(pattern_by_hash, content, re.MULTILINE)
199
+
200
+ if match:
201
+ old_title = match.group(1)
202
+ if old_title != req.title:
203
+ result['title_fixed'] = True
204
+ result['old_title'] = old_title
205
+
206
+ # 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
164
209
  else:
165
- # Add hash to end marker
210
+ # Add hash to end marker (no existing hash)
166
211
  # Pattern: *End* *Title* (without hash)
167
- # Add: | **Hash**: XXXX
168
- content = re.sub(
169
- rf"(\*End\*\s+\*{re.escape(req.title)}\*)(?!\s*\|\s*\*\*Hash\*\*)",
170
- rf"\1 | **Hash**: {new_hash}",
171
- content,
172
- )
173
-
174
- req.file_path.write_text(content, encoding="utf-8")
212
+ pattern = rf"^(\*End\*\s+\*{re.escape(req.title)}\*)(?!\s*\|\s*\*\*Hash\*\*)\s*$"
213
+ content, count = re.subn(pattern, new_end_line, content, flags=re.MULTILINE)
214
+ if count > 0:
215
+ result['updated'] = True
216
+
217
+ if result['updated']:
218
+ req.file_path.write_text(content, encoding="utf-8")
219
+
220
+ return result