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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. elspais/cli.py +141 -10
  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.1.dist-info}/METADATA +36 -18
  69. elspais-0.11.1.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.1.dist-info}/WHEEL +0 -0
  72. {elspais-0.9.3.dist-info → elspais-0.11.1.dist-info}/entry_points.txt +0 -0
  73. {elspais-0.9.3.dist-info → elspais-0.11.1.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:
@@ -22,9 +22,16 @@ def create_parser() -> argparse.ArgumentParser:
22
22
  epilog="""
23
23
  Examples:
24
24
  elspais validate # Validate all requirements
25
+ elspais validate --fix # Auto-fix fixable issues
25
26
  elspais trace --format html # Generate HTML traceability matrix
27
+ elspais trace --view # Interactive HTML view
26
28
  elspais hash update # Update all requirement hashes
29
+ elspais changed # Show uncommitted spec changes
30
+ elspais analyze hierarchy # Show requirement hierarchy tree
31
+ elspais config show # View current configuration
27
32
  elspais init # Create .elspais.toml configuration
33
+
34
+ For detailed command help: elspais <command> --help
28
35
  """,
29
36
  )
30
37
 
@@ -64,6 +71,21 @@ Examples:
64
71
  validate_parser = subparsers.add_parser(
65
72
  "validate",
66
73
  help="Validate requirements format, links, and hashes",
74
+ formatter_class=argparse.RawDescriptionHelpFormatter,
75
+ epilog="""
76
+ Examples:
77
+ elspais validate # Validate all requirements
78
+ elspais validate --fix # Auto-fix hashes and formatting
79
+ elspais validate --skip-rule hash.* # Skip all hash rules
80
+ elspais validate -j # Output JSON for tooling
81
+ elspais validate --mode core # Exclude associated repo specs
82
+
83
+ Common rules to skip:
84
+ hash.missing Hash footer is missing
85
+ hash.mismatch Hash doesn't match content
86
+ hierarchy.* All hierarchy rules
87
+ format.* All format rules
88
+ """,
67
89
  )
68
90
  validate_parser.add_argument(
69
91
  "--fix",
@@ -79,7 +101,7 @@ Examples:
79
101
  validate_parser.add_argument(
80
102
  "--skip-rule",
81
103
  action="append",
82
- help="Skip specific validation rules",
104
+ help="Skip validation rules (can be repeated, e.g., hash.*, format.*)",
83
105
  metavar="RULE",
84
106
  )
85
107
  validate_parser.add_argument(
@@ -97,6 +119,12 @@ Examples:
97
119
  action="store_true",
98
120
  help="Skip test scanning",
99
121
  )
122
+ validate_parser.add_argument(
123
+ "--mode",
124
+ choices=["core", "combined"],
125
+ default="combined",
126
+ help="Scope: core (this repo only), combined (include sponsor repos)",
127
+ )
100
128
 
101
129
  # trace command
102
130
  trace_parser = subparsers.add_parser(
@@ -107,7 +135,7 @@ Examples:
107
135
  "--format",
108
136
  choices=["markdown", "html", "csv", "both"],
109
137
  default="both",
110
- help="Output format (default: both)",
138
+ help="Output format: markdown, html, csv, or both (markdown + csv)",
111
139
  )
112
140
  trace_parser.add_argument(
113
141
  "--output",
@@ -115,11 +143,54 @@ Examples:
115
143
  help="Output file path",
116
144
  metavar="PATH",
117
145
  )
146
+ # trace-view enhanced options (requires elspais[trace-view])
147
+ trace_parser.add_argument(
148
+ "--view",
149
+ action="store_true",
150
+ help="Generate interactive HTML traceability view (requires trace-view extra)",
151
+ )
152
+ trace_parser.add_argument(
153
+ "--embed-content",
154
+ action="store_true",
155
+ help="Embed full requirement markdown in HTML for offline viewing",
156
+ )
157
+ trace_parser.add_argument(
158
+ "--edit-mode",
159
+ action="store_true",
160
+ help="Enable in-browser editing of implements and status fields",
161
+ )
162
+ trace_parser.add_argument(
163
+ "--review-mode",
164
+ action="store_true",
165
+ help="Enable collaborative review with comments and flags",
166
+ )
167
+ trace_parser.add_argument(
168
+ "--server",
169
+ action="store_true",
170
+ help="Start review server (requires trace-review extra)",
171
+ )
172
+ trace_parser.add_argument(
173
+ "--port",
174
+ type=int,
175
+ default=8080,
176
+ help="Port for review server (default: 8080)",
177
+ )
178
+ trace_parser.add_argument(
179
+ "--mode",
180
+ choices=["core", "sponsor", "combined"],
181
+ default="core",
182
+ help="Report mode: core, sponsor, or combined (default: core)",
183
+ )
184
+ trace_parser.add_argument(
185
+ "--sponsor",
186
+ help="Sponsor name for sponsor-specific reports",
187
+ metavar="NAME",
188
+ )
118
189
 
119
190
  # hash command
120
191
  hash_parser = subparsers.add_parser(
121
192
  "hash",
122
- help="Manage requirement hashes",
193
+ help="Manage requirement hashes (verify, update)",
123
194
  )
124
195
  hash_subparsers = hash_parser.add_subparsers(dest="hash_action")
125
196
 
@@ -146,7 +217,7 @@ Examples:
146
217
  # index command
147
218
  index_parser = subparsers.add_parser(
148
219
  "index",
149
- help="Manage INDEX.md file",
220
+ help="Manage INDEX.md file (validate, regenerate)",
150
221
  )
151
222
  index_subparsers = index_parser.add_subparsers(dest="index_action")
152
223
 
@@ -162,7 +233,7 @@ Examples:
162
233
  # analyze command
163
234
  analyze_parser = subparsers.add_parser(
164
235
  "analyze",
165
- help="Analyze requirement hierarchy",
236
+ help="Analyze requirement hierarchy (hierarchy, orphans, coverage)",
166
237
  )
167
238
  analyze_subparsers = analyze_parser.add_subparsers(dest="analyze_action")
168
239
 
@@ -172,7 +243,7 @@ Examples:
172
243
  )
173
244
  analyze_subparsers.add_parser(
174
245
  "orphans",
175
- help="Find orphaned requirements",
246
+ help="Find requirements with no parent (missing or invalid Implements)",
176
247
  )
177
248
  analyze_subparsers.add_parser(
178
249
  "coverage",
@@ -209,7 +280,7 @@ Examples:
209
280
  version_parser.add_argument(
210
281
  "check",
211
282
  nargs="?",
212
- help="Check for updates",
283
+ help="Check for updates (not yet implemented)",
213
284
  )
214
285
 
215
286
  # init command
@@ -237,6 +308,17 @@ Examples:
237
308
  edit_parser = subparsers.add_parser(
238
309
  "edit",
239
310
  help="Edit requirements in-place (implements, status, move)",
311
+ formatter_class=argparse.RawDescriptionHelpFormatter,
312
+ epilog="""
313
+ Examples:
314
+ elspais edit --req-id REQ-d00001 --status Draft
315
+ elspais edit --req-id REQ-d00001 --implements REQ-p00001,REQ-p00002
316
+ elspais edit --req-id REQ-d00001 --move-to roadmap/future.md
317
+ elspais edit --from-json edits.json
318
+
319
+ JSON batch format:
320
+ {"edits": [{"req_id": "...", "status": "...", "implements": [...]}]}
321
+ """,
240
322
  )
241
323
  edit_parser.add_argument(
242
324
  "--req-id",
@@ -277,7 +359,7 @@ Examples:
277
359
  # config command
278
360
  config_parser = subparsers.add_parser(
279
361
  "config",
280
- help="View and modify configuration",
362
+ help="View and modify configuration (show, get, set, ...)",
281
363
  )
282
364
  config_subparsers = config_parser.add_subparsers(dest="config_action")
283
365
 
@@ -373,7 +455,7 @@ Examples:
373
455
  # rules command
374
456
  rules_parser = subparsers.add_parser(
375
457
  "rules",
376
- help="View and manage content rules",
458
+ help="View and manage content rules (list, show)",
377
459
  )
378
460
  rules_subparsers = rules_parser.add_subparsers(dest="rules_action")
379
461
 
@@ -393,6 +475,53 @@ Examples:
393
475
  help="Content rule file name (e.g., 'AI-AGENT.md')",
394
476
  )
395
477
 
478
+ # reformat-with-claude command
479
+ reformat_parser = subparsers.add_parser(
480
+ "reformat-with-claude",
481
+ help="Reformat requirements using AI (Acceptance Criteria -> Assertions)",
482
+ )
483
+ reformat_parser.add_argument(
484
+ "--start-req",
485
+ help="Starting requirement ID (default: all PRD requirements)",
486
+ metavar="ID",
487
+ )
488
+ reformat_parser.add_argument(
489
+ "--depth",
490
+ type=int,
491
+ help="Maximum traversal depth (default: unlimited)",
492
+ )
493
+ reformat_parser.add_argument(
494
+ "--dry-run",
495
+ action="store_true",
496
+ help="Preview changes without applying",
497
+ )
498
+ reformat_parser.add_argument(
499
+ "--backup",
500
+ action="store_true",
501
+ help="Create .bak files before editing",
502
+ )
503
+ reformat_parser.add_argument(
504
+ "--force",
505
+ action="store_true",
506
+ help="Reformat even if already in new format",
507
+ )
508
+ reformat_parser.add_argument(
509
+ "--fix-line-breaks",
510
+ action="store_true",
511
+ help="Normalize line breaks (remove extra blank lines)",
512
+ )
513
+ reformat_parser.add_argument(
514
+ "--line-breaks-only",
515
+ action="store_true",
516
+ help="Only fix line breaks, skip AI-based reformatting",
517
+ )
518
+ reformat_parser.add_argument(
519
+ "--mode",
520
+ choices=["combined", "core-only", "local-only"],
521
+ default="combined",
522
+ help="Which repos to include in hierarchy (default: combined)",
523
+ )
524
+
396
525
  # mcp command
397
526
  mcp_parser = subparsers.add_parser(
398
527
  "mcp",
@@ -489,6 +618,8 @@ def main(argv: Optional[List[str]] = None) -> int:
489
618
  return config_cmd.run(args)
490
619
  elif args.command == "rules":
491
620
  return rules_cmd.run(args)
621
+ elif args.command == "reformat-with-claude":
622
+ return reformat_cmd.run(args)
492
623
  elif args.command == "mcp":
493
624
  return mcp_command(args)
494
625
  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