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/reformat_cmd.py
CHANGED
|
@@ -2,457 +2,54 @@
|
|
|
2
2
|
"""
|
|
3
3
|
elspais.commands.reformat_cmd - Reformat requirements using AI.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
FUNCTIONALITY (to be reimplemented using Graph):
|
|
6
|
+
- `elspais reformat-with-claude` - Transform requirements format using Claude AI
|
|
7
|
+
- Converts old "Acceptance Criteria" format to new "Assertions" format
|
|
8
|
+
- Uses elspais.reformat module for AI calls and content assembly
|
|
9
|
+
|
|
10
|
+
OPTIONS:
|
|
11
|
+
- --start-req REQ_ID: Start from specific requirement (traverse descendants)
|
|
12
|
+
- --depth N: Maximum traversal depth
|
|
13
|
+
- --dry-run: Preview changes without writing
|
|
14
|
+
- --backup: Create .bak files before modifying
|
|
15
|
+
- --force: Reformat even if already in new format
|
|
16
|
+
- --fix-line-breaks: Normalize line breaks in output
|
|
17
|
+
- --line-breaks-only: Only fix line breaks, no format conversion
|
|
18
|
+
- --mode core|combined|local-only: Which repos to include
|
|
19
|
+
|
|
20
|
+
WORKFLOW:
|
|
21
|
+
1. Build requirement graph from spec directories
|
|
22
|
+
2. Identify requirements needing reformat (via validation)
|
|
23
|
+
3. Traverse from start_req or all PRD requirements
|
|
24
|
+
4. For each requirement:
|
|
25
|
+
- Call Claude to generate new assertions format
|
|
26
|
+
- Validate the result
|
|
27
|
+
- Replace content in source file (with backup if requested)
|
|
28
|
+
|
|
29
|
+
GRAPH INTEGRATION:
|
|
30
|
+
- Receives TraceGraph from CLI dispatcher
|
|
31
|
+
- Uses graph.find_by_id() to locate start requirement
|
|
32
|
+
- Uses node.children for BFS traversal
|
|
33
|
+
- Uses node.requirement.file_path for file modifications
|
|
34
|
+
- Filters by NodeKind.REQUIREMENT
|
|
12
35
|
"""
|
|
13
36
|
|
|
14
37
|
import argparse
|
|
15
|
-
import shutil
|
|
16
|
-
import sys
|
|
17
|
-
from pathlib import Path
|
|
18
|
-
from typing import List, Optional
|
|
19
|
-
|
|
20
|
-
from elspais.config.loader import load_config, find_config_file, get_spec_directories
|
|
21
|
-
from elspais.core.parser import RequirementParser
|
|
22
|
-
from elspais.core.patterns import PatternValidator, PatternConfig
|
|
23
|
-
from elspais.core.rules import RuleEngine, RulesConfig
|
|
24
38
|
|
|
25
39
|
|
|
26
40
|
def run(args: argparse.Namespace) -> int:
|
|
27
41
|
"""Run the reformat-with-claude command.
|
|
28
42
|
|
|
29
|
-
This command
|
|
30
|
-
to the new Assertions format using Claude AI.
|
|
43
|
+
This command requires reimplementation using the graph-based system.
|
|
31
44
|
"""
|
|
32
|
-
|
|
33
|
-
get_all_requirements,
|
|
34
|
-
build_hierarchy,
|
|
35
|
-
traverse_top_down,
|
|
36
|
-
normalize_req_id,
|
|
37
|
-
reformat_requirement,
|
|
38
|
-
assemble_new_format,
|
|
39
|
-
validate_reformatted_content,
|
|
40
|
-
normalize_line_breaks,
|
|
41
|
-
fix_requirement_line_breaks,
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
print("elspais reformat-with-claude")
|
|
45
|
-
print()
|
|
46
|
-
|
|
47
|
-
# Handle line-breaks-only mode
|
|
48
|
-
if args.line_breaks_only:
|
|
49
|
-
return run_line_breaks_only(args)
|
|
50
|
-
|
|
51
|
-
# Configuration
|
|
52
|
-
start_req = args.start_req
|
|
53
|
-
max_depth = args.depth
|
|
54
|
-
dry_run = args.dry_run
|
|
55
|
-
backup = args.backup
|
|
56
|
-
force = args.force
|
|
57
|
-
fix_line_breaks = args.fix_line_breaks
|
|
58
|
-
verbose = getattr(args, 'verbose', False)
|
|
59
|
-
mode = getattr(args, 'mode', 'combined')
|
|
60
|
-
|
|
61
|
-
print(f"Options:")
|
|
62
|
-
print(f" Start REQ: {start_req or 'All PRD requirements'}")
|
|
63
|
-
print(f" Max depth: {max_depth or 'Unlimited'}")
|
|
64
|
-
print(f" Mode: {mode}")
|
|
65
|
-
print(f" Dry run: {dry_run}")
|
|
66
|
-
print(f" Backup: {backup}")
|
|
67
|
-
print(f" Force reformat: {force}")
|
|
68
|
-
print(f" Fix line breaks: {fix_line_breaks}")
|
|
45
|
+
print("Error: 'reformat-with-claude' command not yet implemented with graph-based system")
|
|
69
46
|
print()
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
validator = PatternValidator(pattern_config)
|
|
80
|
-
|
|
81
|
-
# Determine local base path for filtering (only modify local files)
|
|
82
|
-
local_base_path = config_path.parent if config_path else Path.cwd()
|
|
83
|
-
|
|
84
|
-
# Get all requirements (including cross-repo if mode allows)
|
|
85
|
-
print("Loading requirements...", end=" ", flush=True)
|
|
86
|
-
requirements = get_all_requirements(mode=mode)
|
|
87
|
-
if not requirements:
|
|
88
|
-
print("FAILED")
|
|
89
|
-
print("Error: Could not load requirements. Run 'elspais validate' first.",
|
|
90
|
-
file=sys.stderr)
|
|
91
|
-
return 1
|
|
92
|
-
print(f"found {len(requirements)} requirements")
|
|
93
|
-
|
|
94
|
-
# Build hierarchy
|
|
95
|
-
print("Building hierarchy...", end=" ", flush=True)
|
|
96
|
-
build_hierarchy(requirements)
|
|
97
|
-
print("done", flush=True)
|
|
98
|
-
|
|
99
|
-
# Determine which requirements to process
|
|
100
|
-
if start_req:
|
|
101
|
-
# Normalize and validate start requirement
|
|
102
|
-
print(f"Normalizing {start_req}...", end=" ", flush=True)
|
|
103
|
-
start_req = normalize_req_id(start_req, validator)
|
|
104
|
-
print(f"-> {start_req}", flush=True)
|
|
105
|
-
if start_req not in requirements:
|
|
106
|
-
print(f"Error: Requirement {start_req} not found", file=sys.stderr)
|
|
107
|
-
return 1
|
|
108
|
-
|
|
109
|
-
print(f"Traversing from {start_req}...", flush=True)
|
|
110
|
-
req_ids = traverse_top_down(requirements, start_req, max_depth)
|
|
111
|
-
print(f"Traversal complete", flush=True)
|
|
112
|
-
else:
|
|
113
|
-
# Process all PRD requirements first, then their descendants
|
|
114
|
-
prd_reqs = [
|
|
115
|
-
req_id for req_id, node in requirements.items()
|
|
116
|
-
if node.level.upper() == 'PRD'
|
|
117
|
-
]
|
|
118
|
-
prd_reqs.sort()
|
|
119
|
-
|
|
120
|
-
print(f"Processing {len(prd_reqs)} PRD requirements and their descendants...")
|
|
121
|
-
req_ids = []
|
|
122
|
-
seen = set()
|
|
123
|
-
for prd_id in prd_reqs:
|
|
124
|
-
for req_id in traverse_top_down(requirements, prd_id, max_depth):
|
|
125
|
-
if req_id not in seen:
|
|
126
|
-
req_ids.append(req_id)
|
|
127
|
-
seen.add(req_id)
|
|
128
|
-
|
|
129
|
-
print(f"Found {len(req_ids)} requirements to process", flush=True)
|
|
130
|
-
|
|
131
|
-
# Run validation to identify requirements with acceptance_criteria issues
|
|
132
|
-
print("Running validation to identify old format...", end=" ", flush=True)
|
|
133
|
-
needs_reformat_ids = get_requirements_needing_reformat(config, local_base_path)
|
|
134
|
-
print(f"found {len(needs_reformat_ids)} with old format", flush=True)
|
|
135
|
-
print(flush=True)
|
|
136
|
-
|
|
137
|
-
# Filter to only requirements that need reformatting (unless --force)
|
|
138
|
-
if not force:
|
|
139
|
-
req_ids = [r for r in req_ids if r in needs_reformat_ids]
|
|
140
|
-
print(f"Filtered to {len(req_ids)} requirements needing reformat")
|
|
141
|
-
print(flush=True)
|
|
142
|
-
|
|
143
|
-
# Process each requirement
|
|
144
|
-
reformatted = 0
|
|
145
|
-
skipped = 0
|
|
146
|
-
errors = 0
|
|
147
|
-
line_break_fixes = 0
|
|
148
|
-
|
|
149
|
-
for i, req_id in enumerate(req_ids):
|
|
150
|
-
if i % 10 == 0 and i > 0:
|
|
151
|
-
print(f"Processing {i}/{len(req_ids)}...", flush=True)
|
|
152
|
-
node = requirements[req_id]
|
|
153
|
-
|
|
154
|
-
# Skip non-local files (from core/associated repos)
|
|
155
|
-
if not is_local_file(node.file_path, local_base_path):
|
|
156
|
-
skipped += 1
|
|
157
|
-
continue
|
|
158
|
-
|
|
159
|
-
print(f"[PROC] {req_id}: {node.title[:50]}...")
|
|
160
|
-
|
|
161
|
-
# Call Claude to reformat
|
|
162
|
-
result, success, error_msg = reformat_requirement(node, verbose=verbose)
|
|
163
|
-
|
|
164
|
-
if not success:
|
|
165
|
-
print(f" ERROR: {error_msg}")
|
|
166
|
-
errors += 1
|
|
167
|
-
continue
|
|
168
|
-
|
|
169
|
-
# Validate the result
|
|
170
|
-
rationale = result.get('rationale', '')
|
|
171
|
-
assertions = result.get('assertions', [])
|
|
172
|
-
|
|
173
|
-
is_valid, warnings = validate_reformatted_content(node, rationale, assertions)
|
|
174
|
-
|
|
175
|
-
if warnings:
|
|
176
|
-
for warning in warnings:
|
|
177
|
-
print(f" WARNING: {warning}")
|
|
178
|
-
|
|
179
|
-
if not is_valid:
|
|
180
|
-
print(f" INVALID: Skipping due to validation errors")
|
|
181
|
-
errors += 1
|
|
182
|
-
continue
|
|
183
|
-
|
|
184
|
-
# Assemble the new format
|
|
185
|
-
new_content = assemble_new_format(
|
|
186
|
-
req_id=node.req_id,
|
|
187
|
-
title=node.title,
|
|
188
|
-
level=node.level,
|
|
189
|
-
status=node.status,
|
|
190
|
-
implements=node.implements,
|
|
191
|
-
rationale=rationale,
|
|
192
|
-
assertions=assertions
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
# Optionally normalize line breaks
|
|
196
|
-
if fix_line_breaks:
|
|
197
|
-
new_content = normalize_line_breaks(new_content)
|
|
198
|
-
line_break_fixes += 1
|
|
199
|
-
|
|
200
|
-
if dry_run:
|
|
201
|
-
print(f" Would write to: {node.file_path}")
|
|
202
|
-
print(f" Assertions: {len(assertions)}")
|
|
203
|
-
reformatted += 1
|
|
204
|
-
else:
|
|
205
|
-
# Write the reformatted content
|
|
206
|
-
try:
|
|
207
|
-
file_path = Path(node.file_path)
|
|
208
|
-
|
|
209
|
-
if backup:
|
|
210
|
-
backup_path = file_path.with_suffix(file_path.suffix + '.bak')
|
|
211
|
-
shutil.copy2(file_path, backup_path)
|
|
212
|
-
print(f" Backup: {backup_path}")
|
|
213
|
-
|
|
214
|
-
# Read the entire file
|
|
215
|
-
content = file_path.read_text()
|
|
216
|
-
|
|
217
|
-
# Find and replace this requirement's content
|
|
218
|
-
# The requirement starts with its header and ends before the next
|
|
219
|
-
# requirement or end of file
|
|
220
|
-
updated_content = replace_requirement_content(
|
|
221
|
-
content, node.req_id, node.title, new_content
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
if updated_content:
|
|
225
|
-
file_path.write_text(updated_content)
|
|
226
|
-
print(f" Written: {file_path}")
|
|
227
|
-
reformatted += 1
|
|
228
|
-
else:
|
|
229
|
-
print(f" ERROR: Could not locate requirement in file")
|
|
230
|
-
errors += 1
|
|
231
|
-
|
|
232
|
-
except Exception as e:
|
|
233
|
-
print(f" ERROR: {e}")
|
|
234
|
-
errors += 1
|
|
235
|
-
|
|
236
|
-
# Summary
|
|
237
|
-
print()
|
|
238
|
-
print("=" * 60)
|
|
239
|
-
print(f"Summary:")
|
|
240
|
-
print(f" Reformatted: {reformatted}")
|
|
241
|
-
print(f" Skipped: {skipped}")
|
|
242
|
-
print(f" Errors: {errors}")
|
|
243
|
-
if fix_line_breaks:
|
|
244
|
-
print(f" Line breaks: {line_break_fixes} files normalized")
|
|
245
|
-
|
|
246
|
-
return 0 if errors == 0 else 1
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
def replace_requirement_content(
|
|
250
|
-
file_content: str,
|
|
251
|
-
req_id: str,
|
|
252
|
-
title: str,
|
|
253
|
-
new_content: str
|
|
254
|
-
) -> Optional[str]:
|
|
255
|
-
"""
|
|
256
|
-
Replace a requirement's content in a file.
|
|
257
|
-
|
|
258
|
-
Finds the requirement by its header pattern and replaces everything
|
|
259
|
-
up to the footer line.
|
|
260
|
-
|
|
261
|
-
Args:
|
|
262
|
-
file_content: Full file content
|
|
263
|
-
req_id: Requirement ID (e.g., 'REQ-d00027')
|
|
264
|
-
title: Requirement title
|
|
265
|
-
new_content: New requirement content
|
|
266
|
-
|
|
267
|
-
Returns:
|
|
268
|
-
Updated file content, or None if requirement not found
|
|
269
|
-
"""
|
|
270
|
-
import re
|
|
271
|
-
|
|
272
|
-
# Pattern to match the requirement header
|
|
273
|
-
# # REQ-d00027: Title
|
|
274
|
-
header_pattern = rf'^# {re.escape(req_id)}:\s*'
|
|
275
|
-
|
|
276
|
-
# Pattern to match the footer
|
|
277
|
-
# *End* *Title* | **Hash**: xxxxxxxx
|
|
278
|
-
footer_pattern = rf'^\*End\*\s+\*{re.escape(title)}\*\s+\|\s+\*\*Hash\*\*:\s*[a-fA-F0-9]+'
|
|
279
|
-
|
|
280
|
-
lines = file_content.split('\n')
|
|
281
|
-
result_lines = []
|
|
282
|
-
in_requirement = False
|
|
283
|
-
found = False
|
|
284
|
-
|
|
285
|
-
i = 0
|
|
286
|
-
while i < len(lines):
|
|
287
|
-
line = lines[i]
|
|
288
|
-
|
|
289
|
-
if not in_requirement:
|
|
290
|
-
# Check if this line starts the requirement
|
|
291
|
-
if re.match(header_pattern, line, re.IGNORECASE):
|
|
292
|
-
in_requirement = True
|
|
293
|
-
found = True
|
|
294
|
-
# Insert new content (without trailing newline, we'll add it)
|
|
295
|
-
new_lines = new_content.rstrip('\n').split('\n')
|
|
296
|
-
result_lines.extend(new_lines)
|
|
297
|
-
i += 1
|
|
298
|
-
continue
|
|
299
|
-
else:
|
|
300
|
-
result_lines.append(line)
|
|
301
|
-
i += 1
|
|
302
|
-
else:
|
|
303
|
-
# We're inside the requirement, skip until we find the footer
|
|
304
|
-
if re.match(footer_pattern, line, re.IGNORECASE):
|
|
305
|
-
# Found the footer, we've already added the new content
|
|
306
|
-
# with its own footer, so skip this old footer
|
|
307
|
-
in_requirement = False
|
|
308
|
-
i += 1
|
|
309
|
-
# Skip any trailing blank lines after the footer
|
|
310
|
-
while i < len(lines) and lines[i].strip() == '':
|
|
311
|
-
i += 1
|
|
312
|
-
else:
|
|
313
|
-
# Skip this line (part of old requirement)
|
|
314
|
-
i += 1
|
|
315
|
-
|
|
316
|
-
if not found:
|
|
317
|
-
return None
|
|
318
|
-
|
|
319
|
-
return '\n'.join(result_lines)
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
def run_line_breaks_only(args: argparse.Namespace) -> int:
|
|
323
|
-
"""Run line break normalization only."""
|
|
324
|
-
from elspais.reformat import (
|
|
325
|
-
get_all_requirements,
|
|
326
|
-
normalize_line_breaks,
|
|
327
|
-
detect_line_break_issues,
|
|
328
|
-
)
|
|
329
|
-
|
|
330
|
-
dry_run = args.dry_run
|
|
331
|
-
backup = args.backup
|
|
332
|
-
|
|
333
|
-
print("Line break normalization mode")
|
|
334
|
-
print(f" Dry run: {dry_run}")
|
|
335
|
-
print(f" Backup: {backup}")
|
|
336
|
-
print()
|
|
337
|
-
|
|
338
|
-
# Get all requirements
|
|
339
|
-
print("Loading requirements...", end=" ", flush=True)
|
|
340
|
-
requirements = get_all_requirements()
|
|
341
|
-
if not requirements:
|
|
342
|
-
print("FAILED")
|
|
343
|
-
print("Error: Could not load requirements.", file=sys.stderr)
|
|
344
|
-
return 1
|
|
345
|
-
print(f"found {len(requirements)} requirements")
|
|
346
|
-
|
|
347
|
-
# Group by file
|
|
348
|
-
files_to_process = {}
|
|
349
|
-
for req_id, node in requirements.items():
|
|
350
|
-
if node.file_path not in files_to_process:
|
|
351
|
-
files_to_process[node.file_path] = []
|
|
352
|
-
files_to_process[node.file_path].append(req_id)
|
|
353
|
-
|
|
354
|
-
print(f"Processing {len(files_to_process)} files...")
|
|
355
|
-
print()
|
|
356
|
-
|
|
357
|
-
fixed = 0
|
|
358
|
-
unchanged = 0
|
|
359
|
-
errors = 0
|
|
360
|
-
|
|
361
|
-
for file_path_str, req_ids in sorted(files_to_process.items()):
|
|
362
|
-
file_path = Path(file_path_str)
|
|
363
|
-
|
|
364
|
-
try:
|
|
365
|
-
content = file_path.read_text()
|
|
366
|
-
issues = detect_line_break_issues(content)
|
|
367
|
-
|
|
368
|
-
if not issues:
|
|
369
|
-
unchanged += 1
|
|
370
|
-
continue
|
|
371
|
-
|
|
372
|
-
print(f"[FIX] {file_path}")
|
|
373
|
-
for issue in issues:
|
|
374
|
-
print(f" - {issue}")
|
|
375
|
-
|
|
376
|
-
if dry_run:
|
|
377
|
-
fixed += 1
|
|
378
|
-
continue
|
|
379
|
-
|
|
380
|
-
# Apply fixes
|
|
381
|
-
fixed_content = normalize_line_breaks(content)
|
|
382
|
-
|
|
383
|
-
if backup:
|
|
384
|
-
backup_path = file_path.with_suffix(file_path.suffix + '.bak')
|
|
385
|
-
shutil.copy2(file_path, backup_path)
|
|
386
|
-
|
|
387
|
-
file_path.write_text(fixed_content)
|
|
388
|
-
fixed += 1
|
|
389
|
-
|
|
390
|
-
except Exception as e:
|
|
391
|
-
print(f"[ERR] {file_path}: {e}")
|
|
392
|
-
errors += 1
|
|
393
|
-
|
|
394
|
-
print()
|
|
395
|
-
print("=" * 60)
|
|
396
|
-
print(f"Summary:")
|
|
397
|
-
print(f" Fixed: {fixed}")
|
|
398
|
-
print(f" Unchanged: {unchanged}")
|
|
399
|
-
print(f" Errors: {errors}")
|
|
400
|
-
|
|
401
|
-
return 0 if errors == 0 else 1
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
def get_requirements_needing_reformat(config: dict, base_path: Path) -> set:
|
|
405
|
-
"""Run validation to identify requirements with old format.
|
|
406
|
-
|
|
407
|
-
Args:
|
|
408
|
-
config: Configuration dictionary
|
|
409
|
-
base_path: Base path of the local repository
|
|
410
|
-
|
|
411
|
-
Returns:
|
|
412
|
-
Set of requirement IDs that have format.acceptance_criteria violations
|
|
413
|
-
"""
|
|
414
|
-
# Get local spec directories only
|
|
415
|
-
spec_dirs = get_spec_directories(None, config, base_path)
|
|
416
|
-
if not spec_dirs:
|
|
417
|
-
return set()
|
|
418
|
-
|
|
419
|
-
# Parse local requirements
|
|
420
|
-
pattern_config = PatternConfig.from_dict(config.get("patterns", {}))
|
|
421
|
-
spec_config = config.get("spec", {})
|
|
422
|
-
no_reference_values = spec_config.get("no_reference_values")
|
|
423
|
-
parser = RequirementParser(pattern_config, no_reference_values=no_reference_values)
|
|
424
|
-
skip_files = spec_config.get("skip_files", [])
|
|
425
|
-
|
|
426
|
-
try:
|
|
427
|
-
parse_result = parser.parse_directories(spec_dirs, skip_files=skip_files)
|
|
428
|
-
requirements = dict(parse_result)
|
|
429
|
-
except Exception:
|
|
430
|
-
return set()
|
|
431
|
-
|
|
432
|
-
# Run validation
|
|
433
|
-
rules_config = RulesConfig.from_dict(config.get("rules", {}))
|
|
434
|
-
engine = RuleEngine(rules_config)
|
|
435
|
-
violations = engine.validate(requirements)
|
|
436
|
-
|
|
437
|
-
# Filter to acceptance_criteria violations
|
|
438
|
-
return {
|
|
439
|
-
v.requirement_id for v in violations
|
|
440
|
-
if v.rule_name == "format.acceptance_criteria"
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
def is_local_file(file_path: str, base_path: Path) -> bool:
|
|
445
|
-
"""Check if file is in the local repo (not core/associated).
|
|
446
|
-
|
|
447
|
-
Args:
|
|
448
|
-
file_path: Path to the file (string)
|
|
449
|
-
base_path: Base path of the local repository
|
|
450
|
-
|
|
451
|
-
Returns:
|
|
452
|
-
True if file is within the local repo, False otherwise
|
|
453
|
-
"""
|
|
454
|
-
try:
|
|
455
|
-
Path(file_path).resolve().relative_to(base_path.resolve())
|
|
456
|
-
return True
|
|
457
|
-
except ValueError:
|
|
458
|
-
return False
|
|
47
|
+
print("Planned features:")
|
|
48
|
+
print(" --start-req REQ_ID")
|
|
49
|
+
print(" --depth N")
|
|
50
|
+
print(" --dry-run")
|
|
51
|
+
print(" --backup")
|
|
52
|
+
print(" --force")
|
|
53
|
+
print(" --fix-line-breaks")
|
|
54
|
+
print(" --line-breaks-only")
|
|
55
|
+
return 1
|
elspais/commands/rules_cmd.py
CHANGED
|
@@ -9,8 +9,8 @@ import sys
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import Optional
|
|
11
11
|
|
|
12
|
-
from elspais.config
|
|
13
|
-
from elspais.
|
|
12
|
+
from elspais.config import find_config_file, load_config
|
|
13
|
+
from elspais.content_rules import load_content_rule, load_content_rules
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def run(args: argparse.Namespace) -> int:
|
|
@@ -56,7 +56,11 @@ def cmd_list(args: argparse.Namespace) -> int:
|
|
|
56
56
|
print("Content Rules:")
|
|
57
57
|
print("-" * 60)
|
|
58
58
|
for rule in rules:
|
|
59
|
-
rel_path =
|
|
59
|
+
rel_path = (
|
|
60
|
+
rule.file_path.relative_to(base_path)
|
|
61
|
+
if base_path in rule.file_path.parents
|
|
62
|
+
else rule.file_path
|
|
63
|
+
)
|
|
60
64
|
print(f" {rel_path}")
|
|
61
65
|
print(f" Title: {rule.title}")
|
|
62
66
|
print(f" Type: {rule.type}")
|