elspais 0.11.1__py3-none-any.whl → 0.43.5__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 (148) hide show
  1. elspais/__init__.py +2 -11
  2. elspais/{sponsors/__init__.py → associates.py} +102 -58
  3. elspais/cli.py +395 -79
  4. elspais/commands/__init__.py +9 -3
  5. elspais/commands/analyze.py +121 -173
  6. elspais/commands/changed.py +15 -30
  7. elspais/commands/config_cmd.py +13 -16
  8. elspais/commands/edit.py +60 -44
  9. elspais/commands/example_cmd.py +319 -0
  10. elspais/commands/hash_cmd.py +167 -183
  11. elspais/commands/health.py +1177 -0
  12. elspais/commands/index.py +98 -114
  13. elspais/commands/init.py +103 -26
  14. elspais/commands/reformat_cmd.py +41 -444
  15. elspais/commands/rules_cmd.py +7 -3
  16. elspais/commands/trace.py +444 -321
  17. elspais/commands/validate.py +195 -415
  18. elspais/config/__init__.py +799 -5
  19. elspais/{core/content_rules.py → content_rules.py} +20 -3
  20. elspais/docs/cli/assertions.md +67 -0
  21. elspais/docs/cli/commands.md +304 -0
  22. elspais/docs/cli/config.md +262 -0
  23. elspais/docs/cli/format.md +66 -0
  24. elspais/docs/cli/git.md +45 -0
  25. elspais/docs/cli/health.md +190 -0
  26. elspais/docs/cli/hierarchy.md +60 -0
  27. elspais/docs/cli/ignore.md +72 -0
  28. elspais/docs/cli/mcp.md +245 -0
  29. elspais/docs/cli/quickstart.md +58 -0
  30. elspais/docs/cli/traceability.md +89 -0
  31. elspais/docs/cli/validation.md +96 -0
  32. elspais/graph/GraphNode.py +383 -0
  33. elspais/graph/__init__.py +40 -0
  34. elspais/graph/annotators.py +927 -0
  35. elspais/graph/builder.py +1886 -0
  36. elspais/graph/deserializer.py +248 -0
  37. elspais/graph/factory.py +284 -0
  38. elspais/graph/metrics.py +127 -0
  39. elspais/graph/mutations.py +161 -0
  40. elspais/graph/parsers/__init__.py +156 -0
  41. elspais/graph/parsers/code.py +213 -0
  42. elspais/graph/parsers/comments.py +112 -0
  43. elspais/graph/parsers/config_helpers.py +29 -0
  44. elspais/graph/parsers/heredocs.py +225 -0
  45. elspais/graph/parsers/journey.py +131 -0
  46. elspais/graph/parsers/remainder.py +79 -0
  47. elspais/graph/parsers/requirement.py +347 -0
  48. elspais/graph/parsers/results/__init__.py +6 -0
  49. elspais/graph/parsers/results/junit_xml.py +229 -0
  50. elspais/graph/parsers/results/pytest_json.py +313 -0
  51. elspais/graph/parsers/test.py +305 -0
  52. elspais/graph/relations.py +78 -0
  53. elspais/graph/serialize.py +216 -0
  54. elspais/html/__init__.py +8 -0
  55. elspais/html/generator.py +731 -0
  56. elspais/html/templates/trace_view.html.j2 +2151 -0
  57. elspais/mcp/__init__.py +47 -29
  58. elspais/mcp/__main__.py +5 -1
  59. elspais/mcp/file_mutations.py +138 -0
  60. elspais/mcp/server.py +2016 -247
  61. elspais/testing/__init__.py +4 -4
  62. elspais/testing/config.py +3 -0
  63. elspais/testing/mapper.py +1 -1
  64. elspais/testing/result_parser.py +25 -21
  65. elspais/testing/scanner.py +301 -12
  66. elspais/utilities/__init__.py +1 -0
  67. elspais/utilities/docs_loader.py +115 -0
  68. elspais/utilities/git.py +607 -0
  69. elspais/{core → utilities}/hasher.py +8 -22
  70. elspais/utilities/md_renderer.py +189 -0
  71. elspais/{core → utilities}/patterns.py +58 -57
  72. elspais/utilities/reference_config.py +626 -0
  73. elspais/validation/__init__.py +19 -0
  74. elspais/validation/format.py +264 -0
  75. {elspais-0.11.1.dist-info → elspais-0.43.5.dist-info}/METADATA +7 -4
  76. elspais-0.43.5.dist-info/RECORD +80 -0
  77. elspais/config/defaults.py +0 -173
  78. elspais/config/loader.py +0 -494
  79. elspais/core/__init__.py +0 -21
  80. elspais/core/git.py +0 -352
  81. elspais/core/models.py +0 -320
  82. elspais/core/parser.py +0 -640
  83. elspais/core/rules.py +0 -514
  84. elspais/mcp/context.py +0 -171
  85. elspais/mcp/serializers.py +0 -112
  86. elspais/reformat/__init__.py +0 -50
  87. elspais/reformat/detector.py +0 -119
  88. elspais/reformat/hierarchy.py +0 -246
  89. elspais/reformat/line_breaks.py +0 -220
  90. elspais/reformat/prompts.py +0 -123
  91. elspais/reformat/transformer.py +0 -264
  92. elspais/trace_view/__init__.py +0 -54
  93. elspais/trace_view/coverage.py +0 -183
  94. elspais/trace_view/generators/__init__.py +0 -12
  95. elspais/trace_view/generators/base.py +0 -329
  96. elspais/trace_view/generators/csv.py +0 -122
  97. elspais/trace_view/generators/markdown.py +0 -175
  98. elspais/trace_view/html/__init__.py +0 -31
  99. elspais/trace_view/html/generator.py +0 -1006
  100. elspais/trace_view/html/templates/base.html +0 -283
  101. elspais/trace_view/html/templates/components/code_viewer_modal.html +0 -14
  102. elspais/trace_view/html/templates/components/file_picker_modal.html +0 -20
  103. elspais/trace_view/html/templates/components/legend_modal.html +0 -69
  104. elspais/trace_view/html/templates/components/review_panel.html +0 -118
  105. elspais/trace_view/html/templates/partials/review/help/help-panel.json +0 -244
  106. elspais/trace_view/html/templates/partials/review/help/onboarding.json +0 -77
  107. elspais/trace_view/html/templates/partials/review/help/tooltips.json +0 -237
  108. elspais/trace_view/html/templates/partials/review/review-comments.js +0 -928
  109. elspais/trace_view/html/templates/partials/review/review-data.js +0 -961
  110. elspais/trace_view/html/templates/partials/review/review-help.js +0 -679
  111. elspais/trace_view/html/templates/partials/review/review-init.js +0 -177
  112. elspais/trace_view/html/templates/partials/review/review-line-numbers.js +0 -429
  113. elspais/trace_view/html/templates/partials/review/review-packages.js +0 -1029
  114. elspais/trace_view/html/templates/partials/review/review-position.js +0 -540
  115. elspais/trace_view/html/templates/partials/review/review-resize.js +0 -115
  116. elspais/trace_view/html/templates/partials/review/review-status.js +0 -659
  117. elspais/trace_view/html/templates/partials/review/review-sync.js +0 -992
  118. elspais/trace_view/html/templates/partials/review-styles.css +0 -2238
  119. elspais/trace_view/html/templates/partials/scripts.js +0 -1741
  120. elspais/trace_view/html/templates/partials/styles.css +0 -1756
  121. elspais/trace_view/models.py +0 -353
  122. elspais/trace_view/review/__init__.py +0 -60
  123. elspais/trace_view/review/branches.py +0 -1149
  124. elspais/trace_view/review/models.py +0 -1205
  125. elspais/trace_view/review/position.py +0 -609
  126. elspais/trace_view/review/server.py +0 -1056
  127. elspais/trace_view/review/status.py +0 -470
  128. elspais/trace_view/review/storage.py +0 -1367
  129. elspais/trace_view/scanning.py +0 -213
  130. elspais/trace_view/specs/README.md +0 -84
  131. elspais/trace_view/specs/tv-d00001-template-architecture.md +0 -36
  132. elspais/trace_view/specs/tv-d00002-css-extraction.md +0 -37
  133. elspais/trace_view/specs/tv-d00003-js-extraction.md +0 -43
  134. elspais/trace_view/specs/tv-d00004-build-embedding.md +0 -40
  135. elspais/trace_view/specs/tv-d00005-test-format.md +0 -78
  136. elspais/trace_view/specs/tv-d00010-review-data-models.md +0 -33
  137. elspais/trace_view/specs/tv-d00011-review-storage.md +0 -33
  138. elspais/trace_view/specs/tv-d00012-position-resolution.md +0 -33
  139. elspais/trace_view/specs/tv-d00013-git-branches.md +0 -31
  140. elspais/trace_view/specs/tv-d00014-review-api-server.md +0 -31
  141. elspais/trace_view/specs/tv-d00015-status-modifier.md +0 -27
  142. elspais/trace_view/specs/tv-d00016-js-integration.md +0 -33
  143. elspais/trace_view/specs/tv-p00001-html-generator.md +0 -33
  144. elspais/trace_view/specs/tv-p00002-review-system.md +0 -29
  145. elspais-0.11.1.dist-info/RECORD +0 -101
  146. {elspais-0.11.1.dist-info → elspais-0.43.5.dist-info}/WHEEL +0 -0
  147. {elspais-0.11.1.dist-info → elspais-0.43.5.dist-info}/entry_points.txt +0 -0
  148. {elspais-0.11.1.dist-info → elspais-0.43.5.dist-info}/licenses/LICENSE +0 -0
elspais/commands/edit.py CHANGED
@@ -18,11 +18,10 @@ from typing import Any, Dict, List, Optional
18
18
 
19
19
  def run(args: argparse.Namespace) -> int:
20
20
  """Run the edit command."""
21
- from elspais.config.defaults import DEFAULT_CONFIG
22
- from elspais.config.loader import find_config_file, get_spec_directories, load_config
21
+ from elspais.config import DEFAULT_CONFIG, find_config_file, get_spec_directories, load_config
23
22
 
24
23
  # Load configuration
25
- config_path = args.config if hasattr(args, 'config') else None
24
+ config_path = args.config if hasattr(args, "config") else None
26
25
  if config_path is None:
27
26
  config_path = find_config_file(Path.cwd())
28
27
  if config_path and config_path.exists():
@@ -31,7 +30,7 @@ def run(args: argparse.Namespace) -> int:
31
30
  config = DEFAULT_CONFIG
32
31
 
33
32
  # Get spec directories
34
- spec_dir = args.spec_dir if hasattr(args, 'spec_dir') and args.spec_dir else None
33
+ spec_dir = args.spec_dir if hasattr(args, "spec_dir") and args.spec_dir else None
35
34
  spec_dirs = get_spec_directories(spec_dir, config)
36
35
  if not spec_dirs:
37
36
  print("Error: No spec directories found", file=sys.stderr)
@@ -40,17 +39,17 @@ def run(args: argparse.Namespace) -> int:
40
39
  # Use first spec dir as base
41
40
  base_spec_dir = spec_dirs[0]
42
41
 
43
- dry_run = getattr(args, 'dry_run', False)
42
+ dry_run = getattr(args, "dry_run", False)
44
43
 
45
- validate_refs = getattr(args, 'validate_refs', False)
44
+ validate_refs = getattr(args, "validate_refs", False)
46
45
 
47
46
  # Handle batch mode
48
- if hasattr(args, 'from_json') and args.from_json:
47
+ if hasattr(args, "from_json") and args.from_json:
49
48
  return run_batch_edit(args.from_json, base_spec_dir, dry_run, validate_refs)
50
49
 
51
50
  # Handle single edit mode
52
- if hasattr(args, 'req_id') and args.req_id:
53
- return run_single_edit(args, base_spec_dir, dry_run)
51
+ if hasattr(args, "req_id") and args.req_id:
52
+ return run_single_edit(args, base_spec_dir, dry_run, validate_refs)
54
53
 
55
54
  print("Error: Must specify --req-id or --from-json", file=sys.stderr)
56
55
  return 1
@@ -95,7 +94,9 @@ def run_batch_edit(
95
94
  return 0
96
95
 
97
96
 
98
- def run_single_edit(args: argparse.Namespace, spec_dir: Path, dry_run: bool) -> int:
97
+ def run_single_edit(
98
+ args: argparse.Namespace, spec_dir: Path, dry_run: bool, validate_refs: bool = False
99
+ ) -> int:
99
100
  """Run single requirement edit."""
100
101
  req_id = args.req_id
101
102
 
@@ -108,19 +109,38 @@ def run_single_edit(args: argparse.Namespace, spec_dir: Path, dry_run: bool) ->
108
109
  file_path = location["file_path"]
109
110
  results = []
110
111
 
112
+ # Collect valid refs if validation is enabled
113
+ valid_refs: Optional[set] = None
114
+ if validate_refs:
115
+ valid_refs = collect_all_req_ids(spec_dir)
116
+
111
117
  # Apply implements change
112
- if hasattr(args, 'implements') and args.implements is not None:
118
+ if hasattr(args, "implements") and args.implements is not None:
113
119
  impl_list = [i.strip() for i in args.implements.split(",")]
120
+
121
+ # Validate references if enabled
122
+ if validate_refs and valid_refs:
123
+ invalid_refs = []
124
+ for ref in impl_list:
125
+ if ref not in valid_refs and f"REQ-{ref}" not in valid_refs:
126
+ invalid_refs.append(ref)
127
+ if invalid_refs:
128
+ print(
129
+ f"Error: Invalid implements references: {', '.join(invalid_refs)}",
130
+ file=sys.stderr,
131
+ )
132
+ return 1
133
+
114
134
  result = modify_implements(file_path, req_id, impl_list, dry_run=dry_run)
115
135
  results.append(("implements", result))
116
136
 
117
137
  # Apply status change
118
- if hasattr(args, 'status') and args.status:
138
+ if hasattr(args, "status") and args.status:
119
139
  result = modify_status(file_path, req_id, args.status, dry_run=dry_run)
120
140
  results.append(("status", result))
121
141
 
122
142
  # Apply move
123
- if hasattr(args, 'move_to') and args.move_to:
143
+ if hasattr(args, "move_to") and args.move_to:
124
144
  dest_path = spec_dir / args.move_to
125
145
  result = move_requirement(file_path, dest_path, req_id, dry_run=dry_run)
126
146
  results.append(("move", result))
@@ -156,15 +176,15 @@ def find_requirement_in_files(
156
176
  Returns:
157
177
  Dict with file_path, req_id, line_number, or None if not found
158
178
  """
159
- # Pattern to match requirement header
160
- pattern = re.compile(rf'^#\s*{re.escape(req_id)}:', re.MULTILINE)
179
+ # Pattern to match requirement header (any markdown header level ##, ###, etc.)
180
+ pattern = re.compile(rf"^#+\s*{re.escape(req_id)}:", re.MULTILINE)
161
181
 
162
182
  for md_file in spec_dir.rglob("*.md"):
163
183
  content = md_file.read_text()
164
184
  match = pattern.search(content)
165
185
  if match:
166
186
  # Count line number
167
- line_number = content[:match.start()].count('\n') + 1
187
+ line_number = content[: match.start()].count("\n") + 1
168
188
  return {
169
189
  "file_path": md_file,
170
190
  "req_id": req_id,
@@ -194,8 +214,8 @@ def modify_implements(
194
214
  """
195
215
  content = file_path.read_text()
196
216
 
197
- # Find the requirement header
198
- req_pattern = re.compile(rf'^(#\s*{re.escape(req_id)}:[^\n]*\n)', re.MULTILINE)
217
+ # Find the requirement header (any markdown header level)
218
+ req_pattern = re.compile(rf"^(#+\s*{re.escape(req_id)}:[^\n]*\n)", re.MULTILINE)
199
219
  req_match = req_pattern.search(content)
200
220
 
201
221
  if not req_match:
@@ -203,9 +223,9 @@ def modify_implements(
203
223
 
204
224
  # Find the **Implements**: field after the header
205
225
  start_pos = req_match.end()
206
- search_region = content[start_pos:start_pos + 500]
226
+ search_region = content[start_pos : start_pos + 500]
207
227
 
208
- impl_pattern = re.compile(r'(\*\*Implements\*\*:\s*)([^|\n]+)')
228
+ impl_pattern = re.compile(r"(\*\*Implements\*\*:\s*)([^|\n]+)")
209
229
  impl_match = impl_pattern.search(search_region)
210
230
 
211
231
  if not impl_match:
@@ -271,8 +291,8 @@ def modify_status(
271
291
  """
272
292
  content = file_path.read_text()
273
293
 
274
- # Find the requirement header
275
- req_pattern = re.compile(rf'^(#\s*{re.escape(req_id)}:[^\n]*\n)', re.MULTILINE)
294
+ # Find the requirement header (any markdown header level)
295
+ req_pattern = re.compile(rf"^(#+\s*{re.escape(req_id)}:[^\n]*\n)", re.MULTILINE)
276
296
  req_match = req_pattern.search(content)
277
297
 
278
298
  if not req_match:
@@ -280,9 +300,9 @@ def modify_status(
280
300
 
281
301
  # Find the **Status**: field after the header
282
302
  start_pos = req_match.end()
283
- search_region = content[start_pos:start_pos + 500]
303
+ search_region = content[start_pos : start_pos + 500]
284
304
 
285
- status_pattern = re.compile(r'(\*\*Status\*\*:\s*)(\w+)')
305
+ status_pattern = re.compile(r"(\*\*Status\*\*:\s*)(\w+)")
286
306
  status_match = status_pattern.search(search_region)
287
307
 
288
308
  if not status_match:
@@ -340,13 +360,10 @@ def move_requirement(
340
360
  source_content = source_file.read_text()
341
361
 
342
362
  # Find the requirement block
343
- # Pattern: # REQ-xxx: title ... *End* *title* | **Hash**: xxx\n---
363
+ # Pattern: ## REQ-xxx: title ... *End* *title* | **Hash**: xxx\n---
344
364
  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
365
+ rf"(^#+\s*{re.escape(req_id)}:[^\n]*\n" rf".*?" rf"\*End\*[^\n]*\n" rf"(?:---\n)?)",
366
+ re.MULTILINE | re.DOTALL,
350
367
  )
351
368
 
352
369
  req_match = req_pattern.search(source_content)
@@ -361,9 +378,9 @@ def move_requirement(
361
378
  req_block = req_block.rstrip() + "\n---\n"
362
379
 
363
380
  # Remove from source
364
- new_source_content = source_content[:req_match.start()] + source_content[req_match.end():]
381
+ new_source_content = source_content[: req_match.start()] + source_content[req_match.end() :]
365
382
  # Clean up extra blank lines
366
- new_source_content = re.sub(r'\n{3,}', '\n\n', new_source_content)
383
+ new_source_content = re.sub(r"\n{3,}", "\n\n", new_source_content)
367
384
 
368
385
  # Add to destination
369
386
  dest_content = dest_file.read_text() if dest_file.exists() else ""
@@ -400,8 +417,9 @@ def collect_all_req_ids(spec_dir: Path) -> set:
400
417
  Set of requirement IDs found (short form, e.g., "p00001")
401
418
  """
402
419
  import re
420
+
403
421
  req_ids = set()
404
- pattern = re.compile(r'^#\s*(REQ-[A-Za-z0-9-]+):', re.MULTILINE)
422
+ pattern = re.compile(r"^#+\s*(REQ-[A-Za-z0-9-]+):", re.MULTILINE)
405
423
 
406
424
  for md_file in spec_dir.rglob("*.md"):
407
425
  content = md_file.read_text()
@@ -453,11 +471,13 @@ def batch_edit(
453
471
  # Find the requirement
454
472
  location = find_requirement_in_files(spec_dir, req_id)
455
473
  if not location:
456
- results.append({
457
- "success": False,
458
- "req_id": req_id,
459
- "error": f"Requirement {req_id} not found",
460
- })
474
+ results.append(
475
+ {
476
+ "success": False,
477
+ "req_id": req_id,
478
+ "error": f"Requirement {req_id} not found",
479
+ }
480
+ )
461
481
  continue
462
482
 
463
483
  file_path = location["file_path"]
@@ -493,9 +513,7 @@ def batch_edit(
493
513
 
494
514
  # Apply status change
495
515
  if "status" in change:
496
- status_result = modify_status(
497
- file_path, req_id, change["status"], dry_run=dry_run
498
- )
516
+ status_result = modify_status(file_path, req_id, change["status"], dry_run=dry_run)
499
517
  if not status_result["success"]:
500
518
  result = status_result
501
519
  result["req_id"] = req_id
@@ -506,9 +524,7 @@ def batch_edit(
506
524
  # Apply move (must be last since it changes file location)
507
525
  if "move_to" in change:
508
526
  dest_path = spec_dir / change["move_to"]
509
- move_result = move_requirement(
510
- file_path, dest_path, req_id, dry_run=dry_run
511
- )
527
+ move_result = move_requirement(file_path, dest_path, req_id, dry_run=dry_run)
512
528
  if not move_result["success"]:
513
529
  result = move_result
514
530
  result["req_id"] = req_id
@@ -0,0 +1,319 @@
1
+ """
2
+ elspais.commands.example_cmd - Display requirement format examples.
3
+
4
+ Quick reference command for requirement format discovery.
5
+ """
6
+
7
+ import argparse
8
+ from pathlib import Path
9
+
10
+ # ============================================================================
11
+ # Requirement Format Templates
12
+ # ============================================================================
13
+
14
+ REQUIREMENT_TEMPLATE = """# REQ-{type}{id}: Requirement Title
15
+
16
+ **Level**: {level} | **Status**: Draft | **Implements**: {implements}
17
+
18
+ ## Assertions
19
+
20
+ A. The system SHALL <do something specific>.
21
+ B. The system SHALL <do another thing>.
22
+
23
+ ## Rationale
24
+
25
+ <optional non-normative explanation>
26
+
27
+ *End* *Requirement Title* | **Hash**: 00000000
28
+
29
+ ---
30
+ Level codes: p = PRD (Product), o = OPS (Operations), d = DEV (Development)
31
+ Implements: Use "-" for top-level requirements, or REQ-pXXXXX for children
32
+ Hash: Run `elspais hash update` to compute automatically
33
+ """
34
+
35
+ REQUIREMENT_TEMPLATE_PRD = REQUIREMENT_TEMPLATE.format(
36
+ type="p", id="00001", level="PRD", implements="-"
37
+ )
38
+
39
+ REQUIREMENT_TEMPLATE_OPS = REQUIREMENT_TEMPLATE.format(
40
+ type="o", id="00001", level="Ops", implements="REQ-p00001"
41
+ )
42
+
43
+ REQUIREMENT_TEMPLATE_DEV = REQUIREMENT_TEMPLATE.format(
44
+ type="d", id="00001", level="Dev", implements="REQ-o00001"
45
+ )
46
+
47
+ JOURNEY_TEMPLATE = """# JNY-{prefix}-01: User Journey Title
48
+
49
+ **Actor**: End User
50
+ **Goal**: <what the user wants to accomplish>
51
+
52
+ ## Steps
53
+
54
+ 1. User <does something>
55
+ 2. System <responds with something>
56
+ 3. User <completes action>
57
+
58
+ ## Requirements
59
+
60
+ - REQ-p00001: <requirement title>
61
+ - REQ-p00002: <requirement title>
62
+
63
+ *End* *User Journey Title*
64
+
65
+ ---
66
+ Journey prefix: Use meaningful 2-4 char codes (e.g., AUTH, PAY, ONBD)
67
+ Steps: Describe the happy path interaction
68
+ Requirements: Link to PRD requirements this journey validates
69
+ """
70
+
71
+ ASSERTION_RULES = """# Assertion Format Rules
72
+
73
+ ## Basic Format
74
+ Each assertion is a labeled statement using SHALL language:
75
+
76
+ A. The system SHALL <do something specific>.
77
+ B. The system SHALL <do another thing>.
78
+
79
+ ## Label Styles (configurable)
80
+ - uppercase: A, B, C ... Z (default, max 26)
81
+ - numeric: 1, 2, 3 ... or 01, 02, 03 ...
82
+ - alphanumeric: 0-9, A-Z (max 36)
83
+
84
+ ## Keywords
85
+ - SHALL: Required functionality (normative)
86
+ - SHOULD: Recommended but not required
87
+ - MAY: Optional functionality
88
+
89
+ ## Placeholders for Removed Assertions
90
+ When an assertion is removed, use a placeholder to maintain sequential labels:
91
+
92
+ A. The system SHALL validate user input.
93
+ B. Removed.
94
+ C. The system SHALL log all transactions.
95
+
96
+ Valid placeholder values: "Removed", "obsolete", "deprecated", "N/A", "-", "reserved"
97
+
98
+ ## Test References
99
+ Tests can reference specific assertions:
100
+
101
+ # test_auth.py
102
+ def test_user_validation():
103
+ \"\"\"Test REQ-d00001-A assertion.\"\"\"
104
+ ...
105
+
106
+ ## Configuration
107
+
108
+ ```toml
109
+ [patterns.assertions]
110
+ label_style = "uppercase" # "uppercase", "numeric", "alphanumeric"
111
+ max_count = 26
112
+
113
+ [rules.format]
114
+ require_assertions = true
115
+ require_shall = true
116
+ labels_sequential = true
117
+ ```
118
+ """
119
+
120
+ ID_PATTERNS_TEMPLATE = """# Requirement ID Patterns
121
+
122
+ ## Current Configuration
123
+
124
+ The ID pattern is built from these components:
125
+ prefix = {prefix}
126
+ id_template = {id_template}
127
+
128
+ ## Standard ID Formats
129
+
130
+ **Core repository:**
131
+ PRD: {prefix}-p00001
132
+ OPS: {prefix}-o00001
133
+ DEV: {prefix}-d00001
134
+
135
+ **With assertion reference:**
136
+ {prefix}-d00001-A (assertion A of DEV requirement)
137
+
138
+ **Associated repository (if enabled):**
139
+ TTN-{prefix}-p00001 (prefixed with associated code)
140
+
141
+ ## Type Levels
142
+
143
+ {types}
144
+
145
+ ## Examples in this project
146
+
147
+ {prefix}-p00001: PRD requirement (Product, Level 1)
148
+ {prefix}-o00001: OPS requirement (Operations, Level 2)
149
+ {prefix}-d00001: DEV requirement (Development, Level 3)
150
+
151
+ Run `elspais config show --section patterns` for full pattern configuration.
152
+ """
153
+
154
+ DEFAULT_TEMPLATE = """# Requirement Format Quick Reference
155
+
156
+ Use `elspais example <type>` for detailed templates:
157
+
158
+ elspais example requirement Show full requirement template
159
+ elspais example journey Show user journey template
160
+ elspais example assertion Show assertion rules and examples
161
+ elspais example ids Show ID patterns from your config
162
+ elspais example --full Display spec/requirements-spec.md
163
+
164
+ ## Basic Requirement Structure
165
+
166
+ ```markdown
167
+ # REQ-d00001: Title
168
+
169
+ **Level**: Dev | **Status**: Draft | **Implements**: REQ-o00001
170
+
171
+ ## Assertions
172
+
173
+ A. The system SHALL <do something>.
174
+
175
+ ## Rationale
176
+
177
+ <optional explanation>
178
+
179
+ *End* *Title* | **Hash**: 00000000
180
+ ```
181
+
182
+ ## Key Rules
183
+
184
+ 1. **Assertions** - Use SHALL for required behavior
185
+ 2. **Implements** - Children reference parents (dev -> ops -> prd)
186
+ 3. **Hash** - Auto-computed with `elspais hash update`
187
+ 4. **Sequential labels** - A, B, C... don't skip letters
188
+
189
+ Run `elspais validate` to check format compliance.
190
+ """
191
+
192
+
193
+ def run(args: argparse.Namespace) -> int:
194
+ """
195
+ Run the example command.
196
+
197
+ Args:
198
+ args: Parsed command line arguments
199
+
200
+ Returns:
201
+ Exit code (0 for success)
202
+ """
203
+ # Handle --full flag first
204
+ if args.full:
205
+ return show_full_spec(args)
206
+
207
+ # Handle subcommand
208
+ subcommand = args.example_type
209
+
210
+ if subcommand == "requirement":
211
+ return show_requirement_template(args)
212
+ elif subcommand == "journey":
213
+ return show_journey_template(args)
214
+ elif subcommand == "assertion":
215
+ return show_assertion_rules(args)
216
+ elif subcommand == "ids":
217
+ return show_id_patterns(args)
218
+ else:
219
+ # Default: show quick reference
220
+ print(DEFAULT_TEMPLATE)
221
+ return 0
222
+
223
+
224
+ def show_requirement_template(args: argparse.Namespace) -> int:
225
+ """Show requirement template."""
226
+ print("# Requirement Templates\n")
227
+ print("## PRD (Product Requirement)")
228
+ print(REQUIREMENT_TEMPLATE_PRD)
229
+ print("\n## OPS (Operations Requirement)")
230
+ print(REQUIREMENT_TEMPLATE_OPS)
231
+ print("\n## DEV (Development Requirement)")
232
+ print(REQUIREMENT_TEMPLATE_DEV)
233
+ return 0
234
+
235
+
236
+ def show_journey_template(args: argparse.Namespace) -> int:
237
+ """Show user journey template."""
238
+ print(JOURNEY_TEMPLATE)
239
+ return 0
240
+
241
+
242
+ def show_assertion_rules(args: argparse.Namespace) -> int:
243
+ """Show assertion format rules."""
244
+ print(ASSERTION_RULES)
245
+ return 0
246
+
247
+
248
+ def show_id_patterns(args: argparse.Namespace) -> int:
249
+ """Show ID patterns from current configuration."""
250
+ from elspais.config import load_config
251
+
252
+ try:
253
+ config = load_config(args.config if hasattr(args, "config") else None)
254
+ except Exception:
255
+ # Use defaults if no config found
256
+ config = {
257
+ "patterns": {
258
+ "prefix": "REQ",
259
+ "id_template": "{prefix}-{type}{id}",
260
+ "types": {
261
+ "prd": {"id": "p", "name": "Product Requirement", "level": 1},
262
+ "ops": {"id": "o", "name": "Operations Requirement", "level": 2},
263
+ "dev": {"id": "d", "name": "Development Requirement", "level": 3},
264
+ },
265
+ }
266
+ }
267
+
268
+ patterns = config.get("patterns", {})
269
+ prefix = patterns.get("prefix", "REQ")
270
+ id_template = patterns.get("id_template", "{prefix}-{type}{id}")
271
+ types = patterns.get("types", {})
272
+
273
+ # Format types section
274
+ types_text = ""
275
+ for type_key, type_info in types.items():
276
+ if isinstance(type_info, dict):
277
+ type_id = type_info.get("id", type_key[0])
278
+ type_name = type_info.get("name", type_key.upper())
279
+ type_level = type_info.get("level", "?")
280
+ types_text += f" {type_key.upper()}: {type_id} = Level {type_level} ({type_name})\n"
281
+
282
+ output = ID_PATTERNS_TEMPLATE.format(
283
+ prefix=prefix,
284
+ id_template=id_template,
285
+ types=types_text.strip() if types_text else " (no types configured)",
286
+ )
287
+ print(output)
288
+ return 0
289
+
290
+
291
+ def show_full_spec(args: argparse.Namespace) -> int:
292
+ """Display the full requirements-spec.md if it exists."""
293
+ from elspais.config import load_config
294
+
295
+ try:
296
+ config = load_config(args.config if hasattr(args, "config") else None)
297
+ except Exception:
298
+ config = {"directories": {"spec": "spec"}}
299
+
300
+ spec_dir = config.get("directories", {}).get("spec", "spec")
301
+ spec_path = Path.cwd() / spec_dir / "requirements-spec.md"
302
+
303
+ # Also check for requirements-format.md (alternative name)
304
+ alt_path = Path.cwd() / spec_dir / "requirements-format.md"
305
+
306
+ if spec_path.exists():
307
+ print(spec_path.read_text())
308
+ return 0
309
+ elif alt_path.exists():
310
+ print(alt_path.read_text())
311
+ return 0
312
+ else:
313
+ print("No requirements specification found.")
314
+ print("Searched:")
315
+ print(f" - {spec_path}")
316
+ print(f" - {alt_path}")
317
+ print()
318
+ print("Use `elspais format requirement` for a template instead.")
319
+ return 1