elspais 0.11.2__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 +1 -10
- elspais/{sponsors/__init__.py → associates.py} +102 -56
- elspais/cli.py +366 -69
- elspais/commands/__init__.py +9 -3
- elspais/commands/analyze.py +118 -169
- elspais/commands/changed.py +12 -23
- elspais/commands/config_cmd.py +10 -13
- elspais/commands/edit.py +33 -13
- elspais/commands/example_cmd.py +319 -0
- elspais/commands/hash_cmd.py +161 -183
- elspais/commands/health.py +1177 -0
- elspais/commands/index.py +98 -115
- elspais/commands/init.py +99 -22
- elspais/commands/reformat_cmd.py +41 -433
- elspais/commands/rules_cmd.py +2 -2
- elspais/commands/trace.py +443 -324
- elspais/commands/validate.py +193 -411
- elspais/config/__init__.py +799 -5
- elspais/{core/content_rules.py → content_rules.py} +20 -2
- 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 +45 -29
- elspais/mcp/__main__.py +5 -1
- elspais/mcp/file_mutations.py +138 -0
- elspais/mcp/server.py +1998 -244
- elspais/testing/__init__.py +3 -3
- elspais/testing/config.py +3 -0
- elspais/testing/mapper.py +1 -1
- 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 +56 -51
- elspais/utilities/reference_config.py +626 -0
- elspais/validation/__init__.py +19 -0
- elspais/validation/format.py +264 -0
- {elspais-0.11.2.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 -179
- elspais/config/loader.py +0 -494
- elspais/core/__init__.py +0 -21
- elspais/core/git.py +0 -346
- elspais/core/models.py +0 -320
- elspais/core/parser.py +0 -639
- elspais/core/rules.py +0 -509
- elspais/mcp/context.py +0 -172
- elspais/mcp/serializers.py +0 -112
- elspais/reformat/__init__.py +0 -50
- elspais/reformat/detector.py +0 -112
- elspais/reformat/hierarchy.py +0 -247
- elspais/reformat/line_breaks.py +0 -218
- elspais/reformat/prompts.py +0 -133
- elspais/reformat/transformer.py +0 -266
- elspais/trace_view/__init__.py +0 -55
- elspais/trace_view/coverage.py +0 -183
- elspais/trace_view/generators/__init__.py +0 -12
- elspais/trace_view/generators/base.py +0 -334
- elspais/trace_view/generators/csv.py +0 -118
- elspais/trace_view/generators/markdown.py +0 -170
- elspais/trace_view/html/__init__.py +0 -33
- elspais/trace_view/html/generator.py +0 -1140
- 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 -378
- elspais/trace_view/review/__init__.py +0 -63
- elspais/trace_view/review/branches.py +0 -1142
- elspais/trace_view/review/models.py +0 -1200
- elspais/trace_view/review/position.py +0 -591
- elspais/trace_view/review/server.py +0 -1032
- elspais/trace_view/review/status.py +0 -455
- elspais/trace_view/review/storage.py +0 -1343
- 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.2.dist-info/RECORD +0 -101
- {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/WHEEL +0 -0
- {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/entry_points.txt +0 -0
- {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/licenses/LICENSE +0 -0
elspais/commands/analyze.py
CHANGED
|
@@ -1,217 +1,166 @@
|
|
|
1
|
+
# Implements: REQ-int-d00003 (CLI Extension)
|
|
1
2
|
"""
|
|
2
3
|
elspais.commands.analyze - Analyze requirements command.
|
|
4
|
+
|
|
5
|
+
Uses graph-based system for analysis:
|
|
6
|
+
- `elspais analyze hierarchy` - Display requirement hierarchy tree
|
|
7
|
+
- `elspais analyze orphans` - Find orphaned requirements
|
|
8
|
+
- `elspais analyze coverage` - Show implementation coverage report
|
|
3
9
|
"""
|
|
4
10
|
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
5
13
|
import argparse
|
|
6
14
|
import sys
|
|
7
|
-
from
|
|
8
|
-
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from elspais.graph.builder import TraceGraph
|
|
9
19
|
|
|
10
|
-
from elspais.
|
|
11
|
-
from elspais.config.loader import find_config_file, get_spec_directories, load_config
|
|
12
|
-
from elspais.core.models import Requirement
|
|
13
|
-
from elspais.core.parser import RequirementParser
|
|
14
|
-
from elspais.core.patterns import PatternConfig
|
|
20
|
+
from elspais.graph import GraphNode, NodeKind
|
|
15
21
|
|
|
16
22
|
|
|
17
23
|
def run(args: argparse.Namespace) -> int:
|
|
18
24
|
"""Run the analyze command."""
|
|
19
|
-
|
|
20
|
-
print("Usage: elspais analyze {hierarchy|orphans|coverage}")
|
|
21
|
-
return 1
|
|
25
|
+
from elspais.graph.factory import build_graph
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
elif args.analyze_action == "orphans":
|
|
26
|
-
return run_orphans(args)
|
|
27
|
-
elif args.analyze_action == "coverage":
|
|
28
|
-
return run_coverage(args)
|
|
27
|
+
spec_dir = getattr(args, "spec_dir", None)
|
|
28
|
+
config_path = getattr(args, "config", None)
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
graph = build_graph(
|
|
31
|
+
spec_dirs=[spec_dir] if spec_dir else None,
|
|
32
|
+
config_path=config_path,
|
|
33
|
+
)
|
|
31
34
|
|
|
35
|
+
action = getattr(args, "analyze_action", None)
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
if action == "hierarchy":
|
|
38
|
+
return _analyze_hierarchy(graph, args)
|
|
39
|
+
elif action == "orphans":
|
|
40
|
+
return _analyze_orphans(graph, args)
|
|
41
|
+
elif action == "coverage":
|
|
42
|
+
return _analyze_coverage(graph, args)
|
|
43
|
+
else:
|
|
44
|
+
print("Usage: elspais analyze <hierarchy|orphans|coverage>", file=sys.stderr)
|
|
37
45
|
return 1
|
|
38
46
|
|
|
47
|
+
|
|
48
|
+
def _analyze_hierarchy(graph: TraceGraph, args: argparse.Namespace) -> int:
|
|
49
|
+
"""Display requirement hierarchy tree."""
|
|
39
50
|
print("Requirement Hierarchy")
|
|
40
51
|
print("=" * 60)
|
|
41
52
|
|
|
42
|
-
# Find root requirements (PRD
|
|
43
|
-
roots = [
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
# Find root requirements (PRD level or no parents)
|
|
54
|
+
roots = []
|
|
55
|
+
for node in graph.nodes_by_kind(NodeKind.REQUIREMENT):
|
|
56
|
+
level = (node.level or "").upper()
|
|
57
|
+
if level == "PRD" or node.parent_count() == 0:
|
|
58
|
+
# Check if this has no parent requirements
|
|
59
|
+
has_req_parent = False
|
|
60
|
+
for parent in node.iter_parents():
|
|
61
|
+
if parent.kind == NodeKind.REQUIREMENT:
|
|
62
|
+
has_req_parent = True
|
|
63
|
+
break
|
|
64
|
+
if not has_req_parent:
|
|
65
|
+
roots.append(node)
|
|
52
66
|
|
|
53
|
-
|
|
67
|
+
for root in sorted(roots, key=lambda n: n.id):
|
|
68
|
+
_print_tree(root, indent=0)
|
|
54
69
|
|
|
55
|
-
|
|
56
|
-
if req.id in printed:
|
|
57
|
-
return
|
|
58
|
-
printed.add(req.id)
|
|
70
|
+
return 0
|
|
59
71
|
|
|
60
|
-
prefix = " " * indent
|
|
61
|
-
status_icon = "✓" if req.status == "Active" else "○"
|
|
62
|
-
print(f"{prefix}{status_icon} {req.id}: {req.title}")
|
|
63
72
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
def _print_tree(node: GraphNode, indent: int) -> None:
|
|
74
|
+
"""Recursively print node and children."""
|
|
75
|
+
prefix = " " * indent
|
|
76
|
+
status_icon = "[x]" if (node.status or "").lower() == "active" else "[ ]"
|
|
77
|
+
level = node.level or "?"
|
|
78
|
+
print(f"{prefix}{status_icon} {node.id} ({level}) - {node.get_label()}")
|
|
68
79
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
# Get child requirements
|
|
81
|
+
children = []
|
|
82
|
+
for child in node.iter_children():
|
|
83
|
+
if child.kind == NodeKind.REQUIREMENT:
|
|
84
|
+
children.append(child)
|
|
72
85
|
|
|
73
|
-
|
|
86
|
+
for child in sorted(children, key=lambda n: n.id):
|
|
87
|
+
_print_tree(child, indent + 1)
|
|
74
88
|
|
|
75
89
|
|
|
76
|
-
def
|
|
90
|
+
def _analyze_orphans(graph: TraceGraph, args: argparse.Namespace) -> int:
|
|
77
91
|
"""Find orphaned requirements."""
|
|
78
|
-
requirements = load_requirements(args)
|
|
79
|
-
if not requirements:
|
|
80
|
-
return 1
|
|
81
|
-
|
|
82
92
|
orphans = []
|
|
83
93
|
|
|
84
|
-
for
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
for node in graph.nodes_by_kind(NodeKind.REQUIREMENT):
|
|
95
|
+
level = (node.level or "").upper()
|
|
96
|
+
# PRD level should not have parents
|
|
97
|
+
if level == "PRD":
|
|
87
98
|
continue
|
|
88
99
|
|
|
89
|
-
# Check
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
for impl_id in req.implements:
|
|
96
|
-
if not find_requirement(impl_id, requirements):
|
|
97
|
-
all_valid = False
|
|
98
|
-
break
|
|
99
|
-
if not all_valid:
|
|
100
|
-
orphans.append(req)
|
|
101
|
-
|
|
102
|
-
if orphans:
|
|
103
|
-
print(f"Orphaned Requirements ({len(orphans)}):")
|
|
104
|
-
print("-" * 40)
|
|
105
|
-
for req in sorted(orphans, key=lambda r: r.id):
|
|
106
|
-
impl_str = ", ".join(req.implements) if req.implements else "(none)"
|
|
107
|
-
print(f" {req.id}: {req.title}")
|
|
108
|
-
print(f" Level: {req.level} | Implements: {impl_str}")
|
|
109
|
-
if req.file_path:
|
|
110
|
-
print(f" File: {req.file_path.name}:{req.line_number}")
|
|
111
|
-
print()
|
|
112
|
-
else:
|
|
113
|
-
print("✓ No orphaned requirements found")
|
|
114
|
-
|
|
115
|
-
return 0
|
|
116
|
-
|
|
100
|
+
# Check for parent requirements
|
|
101
|
+
has_req_parent = False
|
|
102
|
+
for parent in node.iter_parents():
|
|
103
|
+
if parent.kind == NodeKind.REQUIREMENT:
|
|
104
|
+
has_req_parent = True
|
|
105
|
+
break
|
|
117
106
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
requirements = load_requirements(args)
|
|
121
|
-
if not requirements:
|
|
122
|
-
return 1
|
|
107
|
+
if not has_req_parent:
|
|
108
|
+
orphans.append(node)
|
|
123
109
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
ops_count = sum(1 for r in requirements.values() if r.level.upper() in ["OPS", "OPERATIONS"])
|
|
127
|
-
dev_count = sum(1 for r in requirements.values() if r.level.upper() in ["DEV", "DEVELOPMENT"])
|
|
128
|
-
|
|
129
|
-
# Count PRD requirements that have implementations
|
|
130
|
-
implemented_prd = set()
|
|
131
|
-
for req in requirements.values():
|
|
132
|
-
for impl_id in req.implements:
|
|
133
|
-
# Resolve to full ID
|
|
134
|
-
target = find_requirement(impl_id, requirements)
|
|
135
|
-
if target and target.level.upper() in ["PRD", "PRODUCT"]:
|
|
136
|
-
implemented_prd.add(target.id)
|
|
137
|
-
|
|
138
|
-
print("Implementation Coverage Report")
|
|
139
|
-
print("=" * 60)
|
|
140
|
-
print()
|
|
141
|
-
print(f"Total Requirements: {len(requirements)}")
|
|
142
|
-
print(f" PRD: {prd_count}")
|
|
143
|
-
print(f" OPS: {ops_count}")
|
|
144
|
-
print(f" DEV: {dev_count}")
|
|
145
|
-
print()
|
|
146
|
-
print("PRD Implementation Coverage:")
|
|
147
|
-
print(f" Implemented: {len(implemented_prd)}/{prd_count}")
|
|
148
|
-
if prd_count > 0:
|
|
149
|
-
pct = (len(implemented_prd) / prd_count) * 100
|
|
150
|
-
print(f" Coverage: {pct:.1f}%")
|
|
151
|
-
|
|
152
|
-
# List unimplemented PRD
|
|
153
|
-
unimplemented = [
|
|
154
|
-
req
|
|
155
|
-
for req in requirements.values()
|
|
156
|
-
if req.level.upper() in ["PRD", "PRODUCT"] and req.id not in implemented_prd
|
|
157
|
-
]
|
|
158
|
-
|
|
159
|
-
if unimplemented:
|
|
110
|
+
if orphans:
|
|
111
|
+
print(f"Found {len(orphans)} orphaned requirements:")
|
|
160
112
|
print()
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
print(f"
|
|
164
|
-
|
|
165
|
-
return 0
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
def load_requirements(args: argparse.Namespace) -> Dict[str, Requirement]:
|
|
169
|
-
"""Load requirements from spec directories."""
|
|
170
|
-
config_path = args.config or find_config_file(Path.cwd())
|
|
171
|
-
if config_path and config_path.exists():
|
|
172
|
-
config = load_config(config_path)
|
|
113
|
+
for node in sorted(orphans, key=lambda n: n.id):
|
|
114
|
+
loc = f"{node.source.path}:{node.source.line}" if node.source else "unknown"
|
|
115
|
+
print(f" {node.id} ({node.level or '?'}) - {node.get_label()}")
|
|
116
|
+
print(f" Location: {loc}")
|
|
173
117
|
else:
|
|
174
|
-
|
|
118
|
+
print("No orphaned requirements found.")
|
|
175
119
|
|
|
176
|
-
|
|
177
|
-
if not spec_dirs:
|
|
178
|
-
print("Error: No spec directories found", file=sys.stderr)
|
|
179
|
-
return {}
|
|
120
|
+
return 1 if orphans else 0
|
|
180
121
|
|
|
181
|
-
pattern_config = PatternConfig.from_dict(config.get("patterns", {}))
|
|
182
|
-
spec_config = config.get("spec", {})
|
|
183
|
-
no_reference_values = spec_config.get("no_reference_values")
|
|
184
|
-
skip_files = spec_config.get("skip_files", [])
|
|
185
|
-
parser = RequirementParser(pattern_config, no_reference_values=no_reference_values)
|
|
186
122
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
print(f"Error parsing requirements: {e}", file=sys.stderr)
|
|
191
|
-
return {}
|
|
123
|
+
def _analyze_coverage(graph: TraceGraph, args: argparse.Namespace) -> int:
|
|
124
|
+
"""Show implementation coverage report."""
|
|
125
|
+
from elspais.graph.annotators import group_by_level
|
|
192
126
|
|
|
127
|
+
# Group requirements by level using shared utility
|
|
128
|
+
by_level = group_by_level(graph)
|
|
193
129
|
|
|
194
|
-
|
|
195
|
-
""
|
|
196
|
-
|
|
197
|
-
|
|
130
|
+
print("Requirements by Level")
|
|
131
|
+
print("=" * 40)
|
|
132
|
+
for level, nodes in by_level.items():
|
|
133
|
+
if nodes:
|
|
134
|
+
print(f" {level}: {len(nodes)}")
|
|
198
135
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
136
|
+
# PRD implementation coverage
|
|
137
|
+
prd_nodes = by_level["PRD"]
|
|
138
|
+
if prd_nodes:
|
|
139
|
+
print()
|
|
140
|
+
print("PRD Implementation Coverage")
|
|
141
|
+
print("-" * 40)
|
|
204
142
|
|
|
205
|
-
|
|
143
|
+
implemented = []
|
|
144
|
+
unimplemented = []
|
|
206
145
|
|
|
146
|
+
for prd in prd_nodes:
|
|
147
|
+
has_children = False
|
|
148
|
+
for child in prd.iter_children():
|
|
149
|
+
if child.kind == NodeKind.REQUIREMENT:
|
|
150
|
+
has_children = True
|
|
151
|
+
break
|
|
152
|
+
if has_children:
|
|
153
|
+
implemented.append(prd)
|
|
154
|
+
else:
|
|
155
|
+
unimplemented.append(prd)
|
|
207
156
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if impl_id in requirements:
|
|
211
|
-
return requirements[impl_id]
|
|
157
|
+
pct = len(implemented) / len(prd_nodes) * 100 if prd_nodes else 0
|
|
158
|
+
print(f" Implemented: {len(implemented)}/{len(prd_nodes)} ({pct:.1f}%)")
|
|
212
159
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
160
|
+
if unimplemented:
|
|
161
|
+
print()
|
|
162
|
+
print(" Unimplemented PRD requirements:")
|
|
163
|
+
for node in sorted(unimplemented, key=lambda n: n.id):
|
|
164
|
+
print(f" {node.id} - {node.get_label()}")
|
|
216
165
|
|
|
217
|
-
return
|
|
166
|
+
return 0
|
elspais/commands/changed.py
CHANGED
|
@@ -9,37 +9,26 @@ Detects changes to requirement files using git:
|
|
|
9
9
|
|
|
10
10
|
import argparse
|
|
11
11
|
import json
|
|
12
|
-
import sys
|
|
13
|
-
from pathlib import Path
|
|
14
12
|
from typing import Dict, Optional
|
|
15
13
|
|
|
16
|
-
from elspais.
|
|
17
|
-
from elspais.config.loader import find_config_file, load_config
|
|
18
|
-
from elspais.core.git import (
|
|
14
|
+
from elspais.utilities.git import (
|
|
19
15
|
detect_moved_requirements,
|
|
20
16
|
filter_spec_files,
|
|
21
|
-
get_current_req_locations,
|
|
22
17
|
get_git_changes,
|
|
23
18
|
get_repo_root,
|
|
19
|
+
get_req_locations_from_graph,
|
|
24
20
|
)
|
|
25
21
|
|
|
26
22
|
|
|
27
23
|
def load_configuration(args: argparse.Namespace) -> Optional[Dict]:
|
|
28
|
-
"""Load configuration from file or use defaults.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
try:
|
|
37
|
-
return load_config(config_path)
|
|
38
|
-
except Exception as e:
|
|
39
|
-
print(f"Error loading config: {e}", file=sys.stderr)
|
|
40
|
-
return None
|
|
41
|
-
else:
|
|
42
|
-
return DEFAULT_CONFIG
|
|
24
|
+
"""Load configuration from file or use defaults.
|
|
25
|
+
|
|
26
|
+
Note: This is a wrapper for get_config() that returns Optional[Dict]
|
|
27
|
+
for backward compatibility. New code should use get_config() directly.
|
|
28
|
+
"""
|
|
29
|
+
from elspais.config import get_config
|
|
30
|
+
|
|
31
|
+
return get_config(config_path=getattr(args, "config", None))
|
|
43
32
|
|
|
44
33
|
|
|
45
34
|
def run(args: argparse.Namespace) -> int:
|
|
@@ -72,8 +61,8 @@ def run(args: argparse.Namespace) -> int:
|
|
|
72
61
|
spec_untracked = filter_spec_files(changes.untracked_files, spec_dir)
|
|
73
62
|
spec_branch = filter_spec_files(changes.branch_changed_files, spec_dir)
|
|
74
63
|
|
|
75
|
-
# Detect moved requirements
|
|
76
|
-
current_locations =
|
|
64
|
+
# Detect moved requirements using graph-based approach
|
|
65
|
+
current_locations = get_req_locations_from_graph(repo_root)
|
|
77
66
|
moved = detect_moved_requirements(changes.committed_req_locations, current_locations)
|
|
78
67
|
|
|
79
68
|
# Build result
|
elspais/commands/config_cmd.py
CHANGED
|
@@ -10,12 +10,7 @@ import sys
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Any, Dict, List, Optional, Tuple
|
|
12
12
|
|
|
13
|
-
from elspais.config
|
|
14
|
-
from elspais.config.loader import (
|
|
15
|
-
find_config_file,
|
|
16
|
-
load_config,
|
|
17
|
-
parse_toml,
|
|
18
|
-
)
|
|
13
|
+
from elspais.config import DEFAULT_CONFIG, find_config_file, load_config, parse_toml
|
|
19
14
|
|
|
20
15
|
|
|
21
16
|
def run(args: argparse.Namespace) -> int:
|
|
@@ -57,7 +52,7 @@ def cmd_show(args: argparse.Namespace) -> int:
|
|
|
57
52
|
print("No configuration file found. Run 'elspais init' to create one.")
|
|
58
53
|
return 1
|
|
59
54
|
|
|
60
|
-
config = load_config(config_path)
|
|
55
|
+
config = load_config(config_path).get_raw()
|
|
61
56
|
section = getattr(args, "section", None)
|
|
62
57
|
|
|
63
58
|
if section:
|
|
@@ -83,14 +78,16 @@ def cmd_get(args: argparse.Namespace) -> int:
|
|
|
83
78
|
print("No configuration file found.", file=sys.stderr)
|
|
84
79
|
return 1
|
|
85
80
|
|
|
86
|
-
|
|
81
|
+
config_loader = load_config(config_path)
|
|
87
82
|
key = args.key
|
|
88
83
|
|
|
89
|
-
|
|
84
|
+
# Use ConfigLoader.get() for dot-notation access
|
|
85
|
+
value = config_loader.get(key)
|
|
90
86
|
if value is None:
|
|
91
|
-
# Check if it's truly None vs not found
|
|
87
|
+
# Check if it's truly None vs not found using raw dict
|
|
88
|
+
raw = config_loader.get_raw()
|
|
92
89
|
parts = key.split(".")
|
|
93
|
-
current =
|
|
90
|
+
current = raw
|
|
94
91
|
for part in parts[:-1]:
|
|
95
92
|
if part not in current:
|
|
96
93
|
print(f"Key not found: {key}", file=sys.stderr)
|
|
@@ -214,8 +211,8 @@ def cmd_remove(args: argparse.Namespace) -> int:
|
|
|
214
211
|
user_config = _load_user_config(config_path)
|
|
215
212
|
merged_config = load_config(config_path)
|
|
216
213
|
|
|
217
|
-
# Get current merged value
|
|
218
|
-
current =
|
|
214
|
+
# Get current merged value using ConfigLoader.get() for dot-notation access
|
|
215
|
+
current = merged_config.get(key)
|
|
219
216
|
|
|
220
217
|
if current is None or not isinstance(current, list):
|
|
221
218
|
print(f"Error: {key} is not an array or doesn't exist", file=sys.stderr)
|
elspais/commands/edit.py
CHANGED
|
@@ -18,8 +18,7 @@ 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
24
|
config_path = args.config if hasattr(args, "config") else None
|
|
@@ -50,7 +49,7 @@ def run(args: argparse.Namespace) -> int:
|
|
|
50
49
|
|
|
51
50
|
# Handle single edit mode
|
|
52
51
|
if hasattr(args, "req_id") and args.req_id:
|
|
53
|
-
return run_single_edit(args, base_spec_dir, dry_run)
|
|
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,9 +109,28 @@ 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
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
|
|
|
@@ -156,8 +176,8 @@ 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()
|
|
@@ -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:
|
|
@@ -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:
|
|
@@ -340,9 +360,9 @@ 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"(
|
|
365
|
+
rf"(^#+\s*{re.escape(req_id)}:[^\n]*\n" rf".*?" rf"\*End\*[^\n]*\n" rf"(?:---\n)?)",
|
|
346
366
|
re.MULTILINE | re.DOTALL,
|
|
347
367
|
)
|
|
348
368
|
|
|
@@ -399,7 +419,7 @@ def collect_all_req_ids(spec_dir: Path) -> set:
|
|
|
399
419
|
import re
|
|
400
420
|
|
|
401
421
|
req_ids = set()
|
|
402
|
-
pattern = re.compile(r"
|
|
422
|
+
pattern = re.compile(r"^#+\s*(REQ-[A-Za-z0-9-]+):", re.MULTILINE)
|
|
403
423
|
|
|
404
424
|
for md_file in spec_dir.rglob("*.md"):
|
|
405
425
|
content = md_file.read_text()
|