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/analyze.py
CHANGED
|
@@ -1,218 +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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if req.id in printed:
|
|
59
|
-
return
|
|
60
|
-
printed.add(req.id)
|
|
61
|
-
|
|
62
|
-
prefix = " " * indent
|
|
63
|
-
status_icon = "✓" if req.status == "Active" else "○"
|
|
64
|
-
print(f"{prefix}{status_icon} {req.id}: {req.title}")
|
|
65
|
-
|
|
66
|
-
# Find children (requirements that implement this one)
|
|
67
|
-
children = find_children(req.id, requirements)
|
|
68
|
-
for child in children:
|
|
69
|
-
print_tree(child, indent + 1)
|
|
70
|
-
|
|
71
|
-
for root in sorted(roots, key=lambda r: r.id):
|
|
72
|
-
print_tree(root)
|
|
73
|
-
print()
|
|
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)
|
|
66
|
+
|
|
67
|
+
for root in sorted(roots, key=lambda n: n.id):
|
|
68
|
+
_print_tree(root, indent=0)
|
|
74
69
|
|
|
75
70
|
return 0
|
|
76
71
|
|
|
77
72
|
|
|
78
|
-
def
|
|
79
|
-
"""
|
|
80
|
-
|
|
81
|
-
if
|
|
82
|
-
|
|
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()}")
|
|
83
79
|
|
|
84
|
-
|
|
80
|
+
# Get child requirements
|
|
81
|
+
children = []
|
|
82
|
+
for child in node.iter_children():
|
|
83
|
+
if child.kind == NodeKind.REQUIREMENT:
|
|
84
|
+
children.append(child)
|
|
85
85
|
|
|
86
|
-
for
|
|
87
|
-
|
|
88
|
-
if req.level.upper() in ["PRD", "PRODUCT"]:
|
|
89
|
-
continue
|
|
86
|
+
for child in sorted(children, key=lambda n: n.id):
|
|
87
|
+
_print_tree(child, indent + 1)
|
|
90
88
|
|
|
91
|
-
# Check if this requirement implements anything
|
|
92
|
-
if not req.implements:
|
|
93
|
-
orphans.append(req)
|
|
94
|
-
else:
|
|
95
|
-
# Check if all implements references are valid
|
|
96
|
-
all_valid = True
|
|
97
|
-
for impl_id in req.implements:
|
|
98
|
-
if not find_requirement(impl_id, requirements):
|
|
99
|
-
all_valid = False
|
|
100
|
-
break
|
|
101
|
-
if not all_valid:
|
|
102
|
-
orphans.append(req)
|
|
103
89
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
for req in sorted(orphans, key=lambda r: r.id):
|
|
108
|
-
impl_str = ", ".join(req.implements) if req.implements else "(none)"
|
|
109
|
-
print(f" {req.id}: {req.title}")
|
|
110
|
-
print(f" Level: {req.level} | Implements: {impl_str}")
|
|
111
|
-
if req.file_path:
|
|
112
|
-
print(f" File: {req.file_path.name}:{req.line_number}")
|
|
113
|
-
print()
|
|
114
|
-
else:
|
|
115
|
-
print("✓ No orphaned requirements found")
|
|
90
|
+
def _analyze_orphans(graph: TraceGraph, args: argparse.Namespace) -> int:
|
|
91
|
+
"""Find orphaned requirements."""
|
|
92
|
+
orphans = []
|
|
116
93
|
|
|
117
|
-
|
|
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":
|
|
98
|
+
continue
|
|
118
99
|
|
|
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
|
|
119
106
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
requirements = load_requirements(args)
|
|
123
|
-
if not requirements:
|
|
124
|
-
return 1
|
|
107
|
+
if not has_req_parent:
|
|
108
|
+
orphans.append(node)
|
|
125
109
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
ops_count = sum(1 for r in requirements.values() if r.level.upper() in ["OPS", "OPERATIONS"])
|
|
129
|
-
dev_count = sum(1 for r in requirements.values() if r.level.upper() in ["DEV", "DEVELOPMENT"])
|
|
130
|
-
|
|
131
|
-
# Count PRD requirements that have implementations
|
|
132
|
-
implemented_prd = set()
|
|
133
|
-
for req in requirements.values():
|
|
134
|
-
for impl_id in req.implements:
|
|
135
|
-
# Resolve to full ID
|
|
136
|
-
target = find_requirement(impl_id, requirements)
|
|
137
|
-
if target and target.level.upper() in ["PRD", "PRODUCT"]:
|
|
138
|
-
implemented_prd.add(target.id)
|
|
139
|
-
|
|
140
|
-
print("Implementation Coverage Report")
|
|
141
|
-
print("=" * 60)
|
|
142
|
-
print()
|
|
143
|
-
print(f"Total Requirements: {len(requirements)}")
|
|
144
|
-
print(f" PRD: {prd_count}")
|
|
145
|
-
print(f" OPS: {ops_count}")
|
|
146
|
-
print(f" DEV: {dev_count}")
|
|
147
|
-
print()
|
|
148
|
-
print("PRD Implementation Coverage:")
|
|
149
|
-
print(f" Implemented: {len(implemented_prd)}/{prd_count}")
|
|
150
|
-
if prd_count > 0:
|
|
151
|
-
pct = (len(implemented_prd) / prd_count) * 100
|
|
152
|
-
print(f" Coverage: {pct:.1f}%")
|
|
153
|
-
|
|
154
|
-
# List unimplemented PRD
|
|
155
|
-
unimplemented = [
|
|
156
|
-
req for req in requirements.values()
|
|
157
|
-
if req.level.upper() in ["PRD", "PRODUCT"] and req.id not in implemented_prd
|
|
158
|
-
]
|
|
159
|
-
|
|
160
|
-
if unimplemented:
|
|
110
|
+
if orphans:
|
|
111
|
+
print(f"Found {len(orphans)} orphaned requirements:")
|
|
161
112
|
print()
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
print(f"
|
|
165
|
-
|
|
166
|
-
return 0
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
def load_requirements(args: argparse.Namespace) -> Dict[str, Requirement]:
|
|
170
|
-
"""Load requirements from spec directories."""
|
|
171
|
-
config_path = args.config or find_config_file(Path.cwd())
|
|
172
|
-
if config_path and config_path.exists():
|
|
173
|
-
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}")
|
|
174
117
|
else:
|
|
175
|
-
|
|
118
|
+
print("No orphaned requirements found.")
|
|
176
119
|
|
|
177
|
-
|
|
178
|
-
if not spec_dirs:
|
|
179
|
-
print("Error: No spec directories found", file=sys.stderr)
|
|
180
|
-
return {}
|
|
120
|
+
return 1 if orphans else 0
|
|
181
121
|
|
|
182
|
-
pattern_config = PatternConfig.from_dict(config.get("patterns", {}))
|
|
183
|
-
spec_config = config.get("spec", {})
|
|
184
|
-
no_reference_values = spec_config.get("no_reference_values")
|
|
185
|
-
skip_files = spec_config.get("skip_files", [])
|
|
186
|
-
parser = RequirementParser(pattern_config, no_reference_values=no_reference_values)
|
|
187
122
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
print(f"Error parsing requirements: {e}", file=sys.stderr)
|
|
192
|
-
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
|
|
193
126
|
|
|
127
|
+
# Group requirements by level using shared utility
|
|
128
|
+
by_level = group_by_level(graph)
|
|
194
129
|
|
|
195
|
-
|
|
196
|
-
""
|
|
197
|
-
|
|
198
|
-
|
|
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)}")
|
|
199
135
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
136
|
+
# PRD implementation coverage
|
|
137
|
+
prd_nodes = by_level["PRD"]
|
|
138
|
+
if prd_nodes:
|
|
139
|
+
print()
|
|
140
|
+
print("PRD Implementation Coverage")
|
|
141
|
+
print("-" * 40)
|
|
205
142
|
|
|
206
|
-
|
|
143
|
+
implemented = []
|
|
144
|
+
unimplemented = []
|
|
207
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)
|
|
208
156
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if impl_id in requirements:
|
|
212
|
-
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}%)")
|
|
213
159
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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()}")
|
|
217
165
|
|
|
218
|
-
return
|
|
166
|
+
return 0
|
elspais/commands/changed.py
CHANGED
|
@@ -9,39 +9,26 @@ Detects changes to requirement files using git:
|
|
|
9
9
|
|
|
10
10
|
import argparse
|
|
11
11
|
import json
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
from
|
|
15
|
-
|
|
16
|
-
from elspais.config.defaults import DEFAULT_CONFIG
|
|
17
|
-
from elspais.config.loader import find_config_file, load_config
|
|
18
|
-
from elspais.core.git import (
|
|
19
|
-
GitChangeInfo,
|
|
20
|
-
MovedRequirement,
|
|
12
|
+
from typing import Dict, Optional
|
|
13
|
+
|
|
14
|
+
from elspais.utilities.git import (
|
|
21
15
|
detect_moved_requirements,
|
|
22
16
|
filter_spec_files,
|
|
23
|
-
get_current_req_locations,
|
|
24
17
|
get_git_changes,
|
|
25
18
|
get_repo_root,
|
|
19
|
+
get_req_locations_from_graph,
|
|
26
20
|
)
|
|
27
21
|
|
|
28
22
|
|
|
29
23
|
def load_configuration(args: argparse.Namespace) -> Optional[Dict]:
|
|
30
|
-
"""Load configuration from file or use defaults.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
try:
|
|
39
|
-
return load_config(config_path)
|
|
40
|
-
except Exception as e:
|
|
41
|
-
print(f"Error loading config: {e}", file=sys.stderr)
|
|
42
|
-
return None
|
|
43
|
-
else:
|
|
44
|
-
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))
|
|
45
32
|
|
|
46
33
|
|
|
47
34
|
def run(args: argparse.Namespace) -> int:
|
|
@@ -74,11 +61,9 @@ def run(args: argparse.Namespace) -> int:
|
|
|
74
61
|
spec_untracked = filter_spec_files(changes.untracked_files, spec_dir)
|
|
75
62
|
spec_branch = filter_spec_files(changes.branch_changed_files, spec_dir)
|
|
76
63
|
|
|
77
|
-
# Detect moved requirements
|
|
78
|
-
current_locations =
|
|
79
|
-
moved = detect_moved_requirements(
|
|
80
|
-
changes.committed_req_locations, current_locations
|
|
81
|
-
)
|
|
64
|
+
# Detect moved requirements using graph-based approach
|
|
65
|
+
current_locations = get_req_locations_from_graph(repo_root)
|
|
66
|
+
moved = detect_moved_requirements(changes.committed_req_locations, current_locations)
|
|
82
67
|
|
|
83
68
|
# Build result
|
|
84
69
|
result = {
|
elspais/commands/config_cmd.py
CHANGED
|
@@ -8,15 +8,9 @@ import argparse
|
|
|
8
8
|
import json
|
|
9
9
|
import sys
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import Any, Dict, List, Optional, Tuple
|
|
11
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
12
12
|
|
|
13
|
-
from elspais.config
|
|
14
|
-
find_config_file,
|
|
15
|
-
load_config,
|
|
16
|
-
merge_configs,
|
|
17
|
-
parse_toml,
|
|
18
|
-
)
|
|
19
|
-
from elspais.config.defaults import DEFAULT_CONFIG
|
|
13
|
+
from elspais.config import DEFAULT_CONFIG, find_config_file, load_config, parse_toml
|
|
20
14
|
|
|
21
15
|
|
|
22
16
|
def run(args: argparse.Namespace) -> int:
|
|
@@ -58,7 +52,7 @@ def cmd_show(args: argparse.Namespace) -> int:
|
|
|
58
52
|
print("No configuration file found. Run 'elspais init' to create one.")
|
|
59
53
|
return 1
|
|
60
54
|
|
|
61
|
-
config = load_config(config_path)
|
|
55
|
+
config = load_config(config_path).get_raw()
|
|
62
56
|
section = getattr(args, "section", None)
|
|
63
57
|
|
|
64
58
|
if section:
|
|
@@ -84,14 +78,16 @@ def cmd_get(args: argparse.Namespace) -> int:
|
|
|
84
78
|
print("No configuration file found.", file=sys.stderr)
|
|
85
79
|
return 1
|
|
86
80
|
|
|
87
|
-
|
|
81
|
+
config_loader = load_config(config_path)
|
|
88
82
|
key = args.key
|
|
89
83
|
|
|
90
|
-
|
|
84
|
+
# Use ConfigLoader.get() for dot-notation access
|
|
85
|
+
value = config_loader.get(key)
|
|
91
86
|
if value is None:
|
|
92
|
-
# 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()
|
|
93
89
|
parts = key.split(".")
|
|
94
|
-
current =
|
|
90
|
+
current = raw
|
|
95
91
|
for part in parts[:-1]:
|
|
96
92
|
if part not in current:
|
|
97
93
|
print(f"Key not found: {key}", file=sys.stderr)
|
|
@@ -215,8 +211,8 @@ def cmd_remove(args: argparse.Namespace) -> int:
|
|
|
215
211
|
user_config = _load_user_config(config_path)
|
|
216
212
|
merged_config = load_config(config_path)
|
|
217
213
|
|
|
218
|
-
# Get current merged value
|
|
219
|
-
current =
|
|
214
|
+
# Get current merged value using ConfigLoader.get() for dot-notation access
|
|
215
|
+
current = merged_config.get(key)
|
|
220
216
|
|
|
221
217
|
if current is None or not isinstance(current, list):
|
|
222
218
|
print(f"Error: {key} is not an array or doesn't exist", file=sys.stderr)
|
|
@@ -255,6 +251,7 @@ def cmd_path(args: argparse.Namespace) -> int:
|
|
|
255
251
|
|
|
256
252
|
# Helper functions
|
|
257
253
|
|
|
254
|
+
|
|
258
255
|
def _get_config_path(args: argparse.Namespace) -> Optional[Path]:
|
|
259
256
|
"""Get configuration file path from args or by discovery."""
|
|
260
257
|
if hasattr(args, "config") and args.config:
|
|
@@ -363,7 +360,7 @@ def _print_value(value: Any, prefix: str = "") -> None:
|
|
|
363
360
|
if prefix:
|
|
364
361
|
print(f"{prefix} = {'true' if value else 'false'}")
|
|
365
362
|
else:
|
|
366
|
-
print(
|
|
363
|
+
print("true" if value else "false")
|
|
367
364
|
elif isinstance(value, str):
|
|
368
365
|
if prefix:
|
|
369
366
|
print(f'{prefix} = "{value}"')
|