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
@@ -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 pathlib import Path
8
- from typing import Dict, List, Optional
15
+ from typing import TYPE_CHECKING
16
+
17
+ if TYPE_CHECKING:
18
+ from elspais.graph.builder import TraceGraph
9
19
 
10
- from elspais.config.defaults import DEFAULT_CONFIG
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
- if not args.analyze_action:
20
- print("Usage: elspais analyze {hierarchy|orphans|coverage}")
21
- return 1
25
+ from elspais.graph.factory import build_graph
22
26
 
23
- if args.analyze_action == "hierarchy":
24
- return run_hierarchy(args)
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
- return 1
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
- def run_hierarchy(args: argparse.Namespace) -> int:
34
- """Show requirement hierarchy tree."""
35
- requirements = load_requirements(args)
36
- if not requirements:
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 with no implements)
43
- roots = [
44
- req for req in requirements.values()
45
- if req.level.upper() in ["PRD", "PRODUCT"] and not req.implements
46
- ]
47
-
48
- if not roots:
49
- # Fall back to all PRD requirements
50
- roots = [
51
- req for req in requirements.values()
52
- if req.level.upper() in ["PRD", "PRODUCT"]
53
- ]
54
-
55
- printed = set()
56
-
57
- def print_tree(req: Requirement, indent: int = 0) -> None:
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 run_orphans(args: argparse.Namespace) -> int:
79
- """Find orphaned requirements."""
80
- requirements = load_requirements(args)
81
- if not requirements:
82
- return 1
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
- orphans = []
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 req in requirements.values():
87
- # Skip PRD (they can be roots)
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
- if orphans:
105
- print(f"Orphaned Requirements ({len(orphans)}):")
106
- print("-" * 40)
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
- return 0
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
- def run_coverage(args: argparse.Namespace) -> int:
121
- """Show implementation coverage report."""
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
- # Group by type
127
- prd_count = sum(1 for r in requirements.values() if r.level.upper() in ["PRD", "PRODUCT"])
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
- print(f"Unimplemented PRD ({len(unimplemented)}):")
163
- for req in sorted(unimplemented, key=lambda r: r.id):
164
- print(f" - {req.id}: {req.title}")
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
- config = DEFAULT_CONFIG
118
+ print("No orphaned requirements found.")
176
119
 
177
- spec_dirs = get_spec_directories(args.spec_dir, config)
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
- try:
189
- return parser.parse_directories(spec_dirs, skip_files=skip_files)
190
- except Exception as e:
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
- def find_children(req_id: str, requirements: Dict[str, Requirement]) -> List[Requirement]:
196
- """Find requirements that implement the given requirement."""
197
- children = []
198
- short_id = req_id.split("-")[-1] if "-" in req_id else req_id
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
- for other_req in requirements.values():
201
- for impl in other_req.implements:
202
- if impl == req_id or impl == short_id or impl.endswith(short_id):
203
- children.append(other_req)
204
- break
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
- return sorted(children, key=lambda r: r.id)
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
- def find_requirement(impl_id: str, requirements: Dict[str, Requirement]) -> Optional[Requirement]:
210
- """Find a requirement by full or partial ID."""
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
- for req_id, req in requirements.items():
215
- if req_id.endswith(impl_id) or req_id.endswith(f"-{impl_id}"):
216
- return req
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 None
166
+ return 0
@@ -9,39 +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
- from typing import Dict, List, Optional
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
- config_path = getattr(args, "config", None)
32
- if config_path:
33
- pass # Use provided path
34
- else:
35
- config_path = find_config_file(Path.cwd())
36
-
37
- if config_path and config_path.exists():
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 = get_current_req_locations(repo_root, spec_dir)
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 = {
@@ -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, Union
11
+ from typing import Any, Dict, List, Optional, Tuple
12
12
 
13
- from elspais.config.loader import (
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
- config = load_config(config_path)
81
+ config_loader = load_config(config_path)
88
82
  key = args.key
89
83
 
90
- value = _get_by_path(config, key)
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 = config
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 to see what's there
219
- current = _get_by_path(merged_config, key)
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('true' if value else 'false')
363
+ print("true" if value else "false")
367
364
  elif isinstance(value, str):
368
365
  if prefix:
369
366
  print(f'{prefix} = "{value}"')