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.
- elspais/__init__.py +2 -11
- elspais/{sponsors/__init__.py → associates.py} +102 -58
- elspais/cli.py +395 -79
- elspais/commands/__init__.py +9 -3
- elspais/commands/analyze.py +121 -173
- elspais/commands/changed.py +15 -30
- elspais/commands/config_cmd.py +13 -16
- elspais/commands/edit.py +60 -44
- elspais/commands/example_cmd.py +319 -0
- elspais/commands/hash_cmd.py +167 -183
- elspais/commands/health.py +1177 -0
- elspais/commands/index.py +98 -114
- elspais/commands/init.py +103 -26
- elspais/commands/reformat_cmd.py +41 -444
- elspais/commands/rules_cmd.py +7 -3
- elspais/commands/trace.py +444 -321
- elspais/commands/validate.py +195 -415
- elspais/config/__init__.py +799 -5
- elspais/{core/content_rules.py → content_rules.py} +20 -3
- elspais/docs/cli/assertions.md +67 -0
- elspais/docs/cli/commands.md +304 -0
- elspais/docs/cli/config.md +262 -0
- elspais/docs/cli/format.md +66 -0
- elspais/docs/cli/git.md +45 -0
- elspais/docs/cli/health.md +190 -0
- elspais/docs/cli/hierarchy.md +60 -0
- elspais/docs/cli/ignore.md +72 -0
- elspais/docs/cli/mcp.md +245 -0
- elspais/docs/cli/quickstart.md +58 -0
- elspais/docs/cli/traceability.md +89 -0
- elspais/docs/cli/validation.md +96 -0
- elspais/graph/GraphNode.py +383 -0
- elspais/graph/__init__.py +40 -0
- elspais/graph/annotators.py +927 -0
- elspais/graph/builder.py +1886 -0
- elspais/graph/deserializer.py +248 -0
- elspais/graph/factory.py +284 -0
- elspais/graph/metrics.py +127 -0
- elspais/graph/mutations.py +161 -0
- elspais/graph/parsers/__init__.py +156 -0
- elspais/graph/parsers/code.py +213 -0
- elspais/graph/parsers/comments.py +112 -0
- elspais/graph/parsers/config_helpers.py +29 -0
- elspais/graph/parsers/heredocs.py +225 -0
- elspais/graph/parsers/journey.py +131 -0
- elspais/graph/parsers/remainder.py +79 -0
- elspais/graph/parsers/requirement.py +347 -0
- elspais/graph/parsers/results/__init__.py +6 -0
- elspais/graph/parsers/results/junit_xml.py +229 -0
- elspais/graph/parsers/results/pytest_json.py +313 -0
- elspais/graph/parsers/test.py +305 -0
- elspais/graph/relations.py +78 -0
- elspais/graph/serialize.py +216 -0
- elspais/html/__init__.py +8 -0
- elspais/html/generator.py +731 -0
- elspais/html/templates/trace_view.html.j2 +2151 -0
- elspais/mcp/__init__.py +47 -29
- elspais/mcp/__main__.py +5 -1
- elspais/mcp/file_mutations.py +138 -0
- elspais/mcp/server.py +2016 -247
- elspais/testing/__init__.py +4 -4
- elspais/testing/config.py +3 -0
- elspais/testing/mapper.py +1 -1
- elspais/testing/result_parser.py +25 -21
- elspais/testing/scanner.py +301 -12
- elspais/utilities/__init__.py +1 -0
- elspais/utilities/docs_loader.py +115 -0
- elspais/utilities/git.py +607 -0
- elspais/{core → utilities}/hasher.py +8 -22
- elspais/utilities/md_renderer.py +189 -0
- elspais/{core → utilities}/patterns.py +58 -57
- elspais/utilities/reference_config.py +626 -0
- elspais/validation/__init__.py +19 -0
- elspais/validation/format.py +264 -0
- {elspais-0.11.1.dist-info → elspais-0.43.5.dist-info}/METADATA +7 -4
- elspais-0.43.5.dist-info/RECORD +80 -0
- elspais/config/defaults.py +0 -173
- elspais/config/loader.py +0 -494
- elspais/core/__init__.py +0 -21
- elspais/core/git.py +0 -352
- elspais/core/models.py +0 -320
- elspais/core/parser.py +0 -640
- elspais/core/rules.py +0 -514
- elspais/mcp/context.py +0 -171
- elspais/mcp/serializers.py +0 -112
- elspais/reformat/__init__.py +0 -50
- elspais/reformat/detector.py +0 -119
- elspais/reformat/hierarchy.py +0 -246
- elspais/reformat/line_breaks.py +0 -220
- elspais/reformat/prompts.py +0 -123
- elspais/reformat/transformer.py +0 -264
- elspais/trace_view/__init__.py +0 -54
- elspais/trace_view/coverage.py +0 -183
- elspais/trace_view/generators/__init__.py +0 -12
- elspais/trace_view/generators/base.py +0 -329
- elspais/trace_view/generators/csv.py +0 -122
- elspais/trace_view/generators/markdown.py +0 -175
- elspais/trace_view/html/__init__.py +0 -31
- elspais/trace_view/html/generator.py +0 -1006
- elspais/trace_view/html/templates/base.html +0 -283
- elspais/trace_view/html/templates/components/code_viewer_modal.html +0 -14
- elspais/trace_view/html/templates/components/file_picker_modal.html +0 -20
- elspais/trace_view/html/templates/components/legend_modal.html +0 -69
- elspais/trace_view/html/templates/components/review_panel.html +0 -118
- elspais/trace_view/html/templates/partials/review/help/help-panel.json +0 -244
- elspais/trace_view/html/templates/partials/review/help/onboarding.json +0 -77
- elspais/trace_view/html/templates/partials/review/help/tooltips.json +0 -237
- elspais/trace_view/html/templates/partials/review/review-comments.js +0 -928
- elspais/trace_view/html/templates/partials/review/review-data.js +0 -961
- elspais/trace_view/html/templates/partials/review/review-help.js +0 -679
- elspais/trace_view/html/templates/partials/review/review-init.js +0 -177
- elspais/trace_view/html/templates/partials/review/review-line-numbers.js +0 -429
- elspais/trace_view/html/templates/partials/review/review-packages.js +0 -1029
- elspais/trace_view/html/templates/partials/review/review-position.js +0 -540
- elspais/trace_view/html/templates/partials/review/review-resize.js +0 -115
- elspais/trace_view/html/templates/partials/review/review-status.js +0 -659
- elspais/trace_view/html/templates/partials/review/review-sync.js +0 -992
- elspais/trace_view/html/templates/partials/review-styles.css +0 -2238
- elspais/trace_view/html/templates/partials/scripts.js +0 -1741
- elspais/trace_view/html/templates/partials/styles.css +0 -1756
- elspais/trace_view/models.py +0 -353
- elspais/trace_view/review/__init__.py +0 -60
- elspais/trace_view/review/branches.py +0 -1149
- elspais/trace_view/review/models.py +0 -1205
- elspais/trace_view/review/position.py +0 -609
- elspais/trace_view/review/server.py +0 -1056
- elspais/trace_view/review/status.py +0 -470
- elspais/trace_view/review/storage.py +0 -1367
- elspais/trace_view/scanning.py +0 -213
- elspais/trace_view/specs/README.md +0 -84
- elspais/trace_view/specs/tv-d00001-template-architecture.md +0 -36
- elspais/trace_view/specs/tv-d00002-css-extraction.md +0 -37
- elspais/trace_view/specs/tv-d00003-js-extraction.md +0 -43
- elspais/trace_view/specs/tv-d00004-build-embedding.md +0 -40
- elspais/trace_view/specs/tv-d00005-test-format.md +0 -78
- elspais/trace_view/specs/tv-d00010-review-data-models.md +0 -33
- elspais/trace_view/specs/tv-d00011-review-storage.md +0 -33
- elspais/trace_view/specs/tv-d00012-position-resolution.md +0 -33
- elspais/trace_view/specs/tv-d00013-git-branches.md +0 -31
- elspais/trace_view/specs/tv-d00014-review-api-server.md +0 -31
- elspais/trace_view/specs/tv-d00015-status-modifier.md +0 -27
- elspais/trace_view/specs/tv-d00016-js-integration.md +0 -33
- elspais/trace_view/specs/tv-p00001-html-generator.md +0 -33
- elspais/trace_view/specs/tv-p00002-review-system.md +0 -29
- elspais-0.11.1.dist-info/RECORD +0 -101
- {elspais-0.11.1.dist-info → elspais-0.43.5.dist-info}/WHEEL +0 -0
- {elspais-0.11.1.dist-info → elspais-0.43.5.dist-info}/entry_points.txt +0 -0
- {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
|
|
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,
|
|
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,
|
|
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,
|
|
42
|
+
dry_run = getattr(args, "dry_run", False)
|
|
44
43
|
|
|
45
|
-
validate_refs = getattr(args,
|
|
44
|
+
validate_refs = getattr(args, "validate_refs", False)
|
|
46
45
|
|
|
47
46
|
# Handle batch mode
|
|
48
|
-
if hasattr(args,
|
|
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,
|
|
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(
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
363
|
+
# Pattern: ## REQ-xxx: title ... *End* *title* | **Hash**: xxx\n---
|
|
344
364
|
req_pattern = re.compile(
|
|
345
|
-
rf
|
|
346
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|