elspais 0.9.3__py3-none-any.whl → 0.11.1__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/cli.py +141 -10
- elspais/commands/hash_cmd.py +72 -26
- elspais/commands/reformat_cmd.py +458 -0
- elspais/commands/trace.py +157 -3
- elspais/commands/validate.py +44 -16
- elspais/core/models.py +2 -0
- elspais/core/parser.py +68 -24
- elspais/reformat/__init__.py +50 -0
- elspais/reformat/detector.py +119 -0
- elspais/reformat/hierarchy.py +246 -0
- elspais/reformat/line_breaks.py +220 -0
- elspais/reformat/prompts.py +123 -0
- elspais/reformat/transformer.py +264 -0
- elspais/sponsors/__init__.py +432 -0
- elspais/trace_view/__init__.py +54 -0
- elspais/trace_view/coverage.py +183 -0
- elspais/trace_view/generators/__init__.py +12 -0
- elspais/trace_view/generators/base.py +329 -0
- elspais/trace_view/generators/csv.py +122 -0
- elspais/trace_view/generators/markdown.py +175 -0
- elspais/trace_view/html/__init__.py +31 -0
- elspais/trace_view/html/generator.py +1006 -0
- elspais/trace_view/html/templates/base.html +283 -0
- elspais/trace_view/html/templates/components/code_viewer_modal.html +14 -0
- elspais/trace_view/html/templates/components/file_picker_modal.html +20 -0
- elspais/trace_view/html/templates/components/legend_modal.html +69 -0
- elspais/trace_view/html/templates/components/review_panel.html +118 -0
- elspais/trace_view/html/templates/partials/review/help/help-panel.json +244 -0
- elspais/trace_view/html/templates/partials/review/help/onboarding.json +77 -0
- elspais/trace_view/html/templates/partials/review/help/tooltips.json +237 -0
- elspais/trace_view/html/templates/partials/review/review-comments.js +928 -0
- elspais/trace_view/html/templates/partials/review/review-data.js +961 -0
- elspais/trace_view/html/templates/partials/review/review-help.js +679 -0
- elspais/trace_view/html/templates/partials/review/review-init.js +177 -0
- elspais/trace_view/html/templates/partials/review/review-line-numbers.js +429 -0
- elspais/trace_view/html/templates/partials/review/review-packages.js +1029 -0
- elspais/trace_view/html/templates/partials/review/review-position.js +540 -0
- elspais/trace_view/html/templates/partials/review/review-resize.js +115 -0
- elspais/trace_view/html/templates/partials/review/review-status.js +659 -0
- elspais/trace_view/html/templates/partials/review/review-sync.js +992 -0
- elspais/trace_view/html/templates/partials/review-styles.css +2238 -0
- elspais/trace_view/html/templates/partials/scripts.js +1741 -0
- elspais/trace_view/html/templates/partials/styles.css +1756 -0
- elspais/trace_view/models.py +353 -0
- elspais/trace_view/review/__init__.py +60 -0
- elspais/trace_view/review/branches.py +1149 -0
- elspais/trace_view/review/models.py +1205 -0
- elspais/trace_view/review/position.py +609 -0
- elspais/trace_view/review/server.py +1056 -0
- elspais/trace_view/review/status.py +470 -0
- elspais/trace_view/review/storage.py +1367 -0
- elspais/trace_view/scanning.py +213 -0
- elspais/trace_view/specs/README.md +84 -0
- elspais/trace_view/specs/tv-d00001-template-architecture.md +36 -0
- elspais/trace_view/specs/tv-d00002-css-extraction.md +37 -0
- elspais/trace_view/specs/tv-d00003-js-extraction.md +43 -0
- elspais/trace_view/specs/tv-d00004-build-embedding.md +40 -0
- elspais/trace_view/specs/tv-d00005-test-format.md +78 -0
- elspais/trace_view/specs/tv-d00010-review-data-models.md +33 -0
- elspais/trace_view/specs/tv-d00011-review-storage.md +33 -0
- elspais/trace_view/specs/tv-d00012-position-resolution.md +33 -0
- elspais/trace_view/specs/tv-d00013-git-branches.md +31 -0
- elspais/trace_view/specs/tv-d00014-review-api-server.md +31 -0
- elspais/trace_view/specs/tv-d00015-status-modifier.md +27 -0
- elspais/trace_view/specs/tv-d00016-js-integration.md +33 -0
- elspais/trace_view/specs/tv-p00001-html-generator.md +33 -0
- elspais/trace_view/specs/tv-p00002-review-system.md +29 -0
- {elspais-0.9.3.dist-info → elspais-0.11.1.dist-info}/METADATA +36 -18
- elspais-0.11.1.dist-info/RECORD +101 -0
- elspais-0.9.3.dist-info/RECORD +0 -40
- {elspais-0.9.3.dist-info → elspais-0.11.1.dist-info}/WHEEL +0 -0
- {elspais-0.9.3.dist-info → elspais-0.11.1.dist-info}/entry_points.txt +0 -0
- {elspais-0.9.3.dist-info → elspais-0.11.1.dist-info}/licenses/LICENSE +0 -0
elspais/cli.py
CHANGED
|
@@ -10,7 +10,7 @@ from pathlib import Path
|
|
|
10
10
|
from typing import List, Optional
|
|
11
11
|
|
|
12
12
|
from elspais import __version__
|
|
13
|
-
from elspais.commands import analyze, changed, config_cmd, edit, hash_cmd, index, init, rules_cmd, trace, validate
|
|
13
|
+
from elspais.commands import analyze, changed, config_cmd, edit, hash_cmd, index, init, rules_cmd, trace, validate, reformat_cmd
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def create_parser() -> argparse.ArgumentParser:
|
|
@@ -22,9 +22,16 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
22
22
|
epilog="""
|
|
23
23
|
Examples:
|
|
24
24
|
elspais validate # Validate all requirements
|
|
25
|
+
elspais validate --fix # Auto-fix fixable issues
|
|
25
26
|
elspais trace --format html # Generate HTML traceability matrix
|
|
27
|
+
elspais trace --view # Interactive HTML view
|
|
26
28
|
elspais hash update # Update all requirement hashes
|
|
29
|
+
elspais changed # Show uncommitted spec changes
|
|
30
|
+
elspais analyze hierarchy # Show requirement hierarchy tree
|
|
31
|
+
elspais config show # View current configuration
|
|
27
32
|
elspais init # Create .elspais.toml configuration
|
|
33
|
+
|
|
34
|
+
For detailed command help: elspais <command> --help
|
|
28
35
|
""",
|
|
29
36
|
)
|
|
30
37
|
|
|
@@ -64,6 +71,21 @@ Examples:
|
|
|
64
71
|
validate_parser = subparsers.add_parser(
|
|
65
72
|
"validate",
|
|
66
73
|
help="Validate requirements format, links, and hashes",
|
|
74
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
75
|
+
epilog="""
|
|
76
|
+
Examples:
|
|
77
|
+
elspais validate # Validate all requirements
|
|
78
|
+
elspais validate --fix # Auto-fix hashes and formatting
|
|
79
|
+
elspais validate --skip-rule hash.* # Skip all hash rules
|
|
80
|
+
elspais validate -j # Output JSON for tooling
|
|
81
|
+
elspais validate --mode core # Exclude associated repo specs
|
|
82
|
+
|
|
83
|
+
Common rules to skip:
|
|
84
|
+
hash.missing Hash footer is missing
|
|
85
|
+
hash.mismatch Hash doesn't match content
|
|
86
|
+
hierarchy.* All hierarchy rules
|
|
87
|
+
format.* All format rules
|
|
88
|
+
""",
|
|
67
89
|
)
|
|
68
90
|
validate_parser.add_argument(
|
|
69
91
|
"--fix",
|
|
@@ -79,7 +101,7 @@ Examples:
|
|
|
79
101
|
validate_parser.add_argument(
|
|
80
102
|
"--skip-rule",
|
|
81
103
|
action="append",
|
|
82
|
-
help="Skip
|
|
104
|
+
help="Skip validation rules (can be repeated, e.g., hash.*, format.*)",
|
|
83
105
|
metavar="RULE",
|
|
84
106
|
)
|
|
85
107
|
validate_parser.add_argument(
|
|
@@ -97,6 +119,12 @@ Examples:
|
|
|
97
119
|
action="store_true",
|
|
98
120
|
help="Skip test scanning",
|
|
99
121
|
)
|
|
122
|
+
validate_parser.add_argument(
|
|
123
|
+
"--mode",
|
|
124
|
+
choices=["core", "combined"],
|
|
125
|
+
default="combined",
|
|
126
|
+
help="Scope: core (this repo only), combined (include sponsor repos)",
|
|
127
|
+
)
|
|
100
128
|
|
|
101
129
|
# trace command
|
|
102
130
|
trace_parser = subparsers.add_parser(
|
|
@@ -107,7 +135,7 @@ Examples:
|
|
|
107
135
|
"--format",
|
|
108
136
|
choices=["markdown", "html", "csv", "both"],
|
|
109
137
|
default="both",
|
|
110
|
-
help="Output format (
|
|
138
|
+
help="Output format: markdown, html, csv, or both (markdown + csv)",
|
|
111
139
|
)
|
|
112
140
|
trace_parser.add_argument(
|
|
113
141
|
"--output",
|
|
@@ -115,11 +143,54 @@ Examples:
|
|
|
115
143
|
help="Output file path",
|
|
116
144
|
metavar="PATH",
|
|
117
145
|
)
|
|
146
|
+
# trace-view enhanced options (requires elspais[trace-view])
|
|
147
|
+
trace_parser.add_argument(
|
|
148
|
+
"--view",
|
|
149
|
+
action="store_true",
|
|
150
|
+
help="Generate interactive HTML traceability view (requires trace-view extra)",
|
|
151
|
+
)
|
|
152
|
+
trace_parser.add_argument(
|
|
153
|
+
"--embed-content",
|
|
154
|
+
action="store_true",
|
|
155
|
+
help="Embed full requirement markdown in HTML for offline viewing",
|
|
156
|
+
)
|
|
157
|
+
trace_parser.add_argument(
|
|
158
|
+
"--edit-mode",
|
|
159
|
+
action="store_true",
|
|
160
|
+
help="Enable in-browser editing of implements and status fields",
|
|
161
|
+
)
|
|
162
|
+
trace_parser.add_argument(
|
|
163
|
+
"--review-mode",
|
|
164
|
+
action="store_true",
|
|
165
|
+
help="Enable collaborative review with comments and flags",
|
|
166
|
+
)
|
|
167
|
+
trace_parser.add_argument(
|
|
168
|
+
"--server",
|
|
169
|
+
action="store_true",
|
|
170
|
+
help="Start review server (requires trace-review extra)",
|
|
171
|
+
)
|
|
172
|
+
trace_parser.add_argument(
|
|
173
|
+
"--port",
|
|
174
|
+
type=int,
|
|
175
|
+
default=8080,
|
|
176
|
+
help="Port for review server (default: 8080)",
|
|
177
|
+
)
|
|
178
|
+
trace_parser.add_argument(
|
|
179
|
+
"--mode",
|
|
180
|
+
choices=["core", "sponsor", "combined"],
|
|
181
|
+
default="core",
|
|
182
|
+
help="Report mode: core, sponsor, or combined (default: core)",
|
|
183
|
+
)
|
|
184
|
+
trace_parser.add_argument(
|
|
185
|
+
"--sponsor",
|
|
186
|
+
help="Sponsor name for sponsor-specific reports",
|
|
187
|
+
metavar="NAME",
|
|
188
|
+
)
|
|
118
189
|
|
|
119
190
|
# hash command
|
|
120
191
|
hash_parser = subparsers.add_parser(
|
|
121
192
|
"hash",
|
|
122
|
-
help="Manage requirement hashes",
|
|
193
|
+
help="Manage requirement hashes (verify, update)",
|
|
123
194
|
)
|
|
124
195
|
hash_subparsers = hash_parser.add_subparsers(dest="hash_action")
|
|
125
196
|
|
|
@@ -146,7 +217,7 @@ Examples:
|
|
|
146
217
|
# index command
|
|
147
218
|
index_parser = subparsers.add_parser(
|
|
148
219
|
"index",
|
|
149
|
-
help="Manage INDEX.md file",
|
|
220
|
+
help="Manage INDEX.md file (validate, regenerate)",
|
|
150
221
|
)
|
|
151
222
|
index_subparsers = index_parser.add_subparsers(dest="index_action")
|
|
152
223
|
|
|
@@ -162,7 +233,7 @@ Examples:
|
|
|
162
233
|
# analyze command
|
|
163
234
|
analyze_parser = subparsers.add_parser(
|
|
164
235
|
"analyze",
|
|
165
|
-
help="Analyze requirement hierarchy",
|
|
236
|
+
help="Analyze requirement hierarchy (hierarchy, orphans, coverage)",
|
|
166
237
|
)
|
|
167
238
|
analyze_subparsers = analyze_parser.add_subparsers(dest="analyze_action")
|
|
168
239
|
|
|
@@ -172,7 +243,7 @@ Examples:
|
|
|
172
243
|
)
|
|
173
244
|
analyze_subparsers.add_parser(
|
|
174
245
|
"orphans",
|
|
175
|
-
help="Find
|
|
246
|
+
help="Find requirements with no parent (missing or invalid Implements)",
|
|
176
247
|
)
|
|
177
248
|
analyze_subparsers.add_parser(
|
|
178
249
|
"coverage",
|
|
@@ -209,7 +280,7 @@ Examples:
|
|
|
209
280
|
version_parser.add_argument(
|
|
210
281
|
"check",
|
|
211
282
|
nargs="?",
|
|
212
|
-
help="Check for updates",
|
|
283
|
+
help="Check for updates (not yet implemented)",
|
|
213
284
|
)
|
|
214
285
|
|
|
215
286
|
# init command
|
|
@@ -237,6 +308,17 @@ Examples:
|
|
|
237
308
|
edit_parser = subparsers.add_parser(
|
|
238
309
|
"edit",
|
|
239
310
|
help="Edit requirements in-place (implements, status, move)",
|
|
311
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
312
|
+
epilog="""
|
|
313
|
+
Examples:
|
|
314
|
+
elspais edit --req-id REQ-d00001 --status Draft
|
|
315
|
+
elspais edit --req-id REQ-d00001 --implements REQ-p00001,REQ-p00002
|
|
316
|
+
elspais edit --req-id REQ-d00001 --move-to roadmap/future.md
|
|
317
|
+
elspais edit --from-json edits.json
|
|
318
|
+
|
|
319
|
+
JSON batch format:
|
|
320
|
+
{"edits": [{"req_id": "...", "status": "...", "implements": [...]}]}
|
|
321
|
+
""",
|
|
240
322
|
)
|
|
241
323
|
edit_parser.add_argument(
|
|
242
324
|
"--req-id",
|
|
@@ -277,7 +359,7 @@ Examples:
|
|
|
277
359
|
# config command
|
|
278
360
|
config_parser = subparsers.add_parser(
|
|
279
361
|
"config",
|
|
280
|
-
help="View and modify configuration",
|
|
362
|
+
help="View and modify configuration (show, get, set, ...)",
|
|
281
363
|
)
|
|
282
364
|
config_subparsers = config_parser.add_subparsers(dest="config_action")
|
|
283
365
|
|
|
@@ -373,7 +455,7 @@ Examples:
|
|
|
373
455
|
# rules command
|
|
374
456
|
rules_parser = subparsers.add_parser(
|
|
375
457
|
"rules",
|
|
376
|
-
help="View and manage content rules",
|
|
458
|
+
help="View and manage content rules (list, show)",
|
|
377
459
|
)
|
|
378
460
|
rules_subparsers = rules_parser.add_subparsers(dest="rules_action")
|
|
379
461
|
|
|
@@ -393,6 +475,53 @@ Examples:
|
|
|
393
475
|
help="Content rule file name (e.g., 'AI-AGENT.md')",
|
|
394
476
|
)
|
|
395
477
|
|
|
478
|
+
# reformat-with-claude command
|
|
479
|
+
reformat_parser = subparsers.add_parser(
|
|
480
|
+
"reformat-with-claude",
|
|
481
|
+
help="Reformat requirements using AI (Acceptance Criteria -> Assertions)",
|
|
482
|
+
)
|
|
483
|
+
reformat_parser.add_argument(
|
|
484
|
+
"--start-req",
|
|
485
|
+
help="Starting requirement ID (default: all PRD requirements)",
|
|
486
|
+
metavar="ID",
|
|
487
|
+
)
|
|
488
|
+
reformat_parser.add_argument(
|
|
489
|
+
"--depth",
|
|
490
|
+
type=int,
|
|
491
|
+
help="Maximum traversal depth (default: unlimited)",
|
|
492
|
+
)
|
|
493
|
+
reformat_parser.add_argument(
|
|
494
|
+
"--dry-run",
|
|
495
|
+
action="store_true",
|
|
496
|
+
help="Preview changes without applying",
|
|
497
|
+
)
|
|
498
|
+
reformat_parser.add_argument(
|
|
499
|
+
"--backup",
|
|
500
|
+
action="store_true",
|
|
501
|
+
help="Create .bak files before editing",
|
|
502
|
+
)
|
|
503
|
+
reformat_parser.add_argument(
|
|
504
|
+
"--force",
|
|
505
|
+
action="store_true",
|
|
506
|
+
help="Reformat even if already in new format",
|
|
507
|
+
)
|
|
508
|
+
reformat_parser.add_argument(
|
|
509
|
+
"--fix-line-breaks",
|
|
510
|
+
action="store_true",
|
|
511
|
+
help="Normalize line breaks (remove extra blank lines)",
|
|
512
|
+
)
|
|
513
|
+
reformat_parser.add_argument(
|
|
514
|
+
"--line-breaks-only",
|
|
515
|
+
action="store_true",
|
|
516
|
+
help="Only fix line breaks, skip AI-based reformatting",
|
|
517
|
+
)
|
|
518
|
+
reformat_parser.add_argument(
|
|
519
|
+
"--mode",
|
|
520
|
+
choices=["combined", "core-only", "local-only"],
|
|
521
|
+
default="combined",
|
|
522
|
+
help="Which repos to include in hierarchy (default: combined)",
|
|
523
|
+
)
|
|
524
|
+
|
|
396
525
|
# mcp command
|
|
397
526
|
mcp_parser = subparsers.add_parser(
|
|
398
527
|
"mcp",
|
|
@@ -489,6 +618,8 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
489
618
|
return config_cmd.run(args)
|
|
490
619
|
elif args.command == "rules":
|
|
491
620
|
return rules_cmd.run(args)
|
|
621
|
+
elif args.command == "reformat-with-claude":
|
|
622
|
+
return reformat_cmd.run(args)
|
|
492
623
|
elif args.command == "mcp":
|
|
493
624
|
return mcp_command(args)
|
|
494
625
|
else:
|
elspais/commands/hash_cmd.py
CHANGED
|
@@ -104,8 +104,16 @@ def run_update(args: argparse.Namespace) -> int:
|
|
|
104
104
|
else:
|
|
105
105
|
print(f"Updating {len(updates)} hashes...")
|
|
106
106
|
for req_id, req, new_hash in updates:
|
|
107
|
-
update_hash_in_file(req, new_hash)
|
|
108
|
-
|
|
107
|
+
result = update_hash_in_file(req, new_hash)
|
|
108
|
+
if result['updated']:
|
|
109
|
+
print(f" ✓ {req_id}")
|
|
110
|
+
old_hash = result['old_hash'] or "(none)"
|
|
111
|
+
print(f" [INFO] Hash: {old_hash} -> {result['new_hash']}")
|
|
112
|
+
if result['title_fixed']:
|
|
113
|
+
print(f" [INFO] Title fixed: \"{result['old_title']}\" -> \"{req.title}\"")
|
|
114
|
+
else:
|
|
115
|
+
print(f" ✗ {req_id}")
|
|
116
|
+
print(f" [WARN] Could not find End marker to update")
|
|
109
117
|
|
|
110
118
|
return 0
|
|
111
119
|
|
|
@@ -138,37 +146,75 @@ def load_requirements(args: argparse.Namespace) -> tuple:
|
|
|
138
146
|
return config, requirements
|
|
139
147
|
|
|
140
148
|
|
|
141
|
-
def update_hash_in_file(req: Requirement, new_hash: str) ->
|
|
149
|
+
def update_hash_in_file(req: Requirement, new_hash: str) -> dict:
|
|
142
150
|
"""Update the hash in the requirement's source file.
|
|
143
151
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
152
|
+
Finds the End marker by the old hash value, then replaces the entire line
|
|
153
|
+
with the correct title and new hash. This handles cases where the End
|
|
154
|
+
marker title doesn't match the header title.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
req: Requirement object with file_path, title, and hash
|
|
158
|
+
new_hash: New hash value to write
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Dict with change info:
|
|
162
|
+
- 'updated': bool - whether file was modified
|
|
163
|
+
- 'old_hash': str - previous hash (or None)
|
|
164
|
+
- 'new_hash': str - new hash value
|
|
165
|
+
- 'title_fixed': bool - whether title was corrected
|
|
166
|
+
- 'old_title': str - previous title (if different)
|
|
147
167
|
"""
|
|
168
|
+
import re
|
|
169
|
+
|
|
170
|
+
result = {
|
|
171
|
+
'updated': False,
|
|
172
|
+
'old_hash': req.hash,
|
|
173
|
+
'new_hash': new_hash,
|
|
174
|
+
'title_fixed': False,
|
|
175
|
+
'old_title': None,
|
|
176
|
+
}
|
|
177
|
+
|
|
148
178
|
if not req.file_path:
|
|
149
|
-
return
|
|
179
|
+
return result
|
|
150
180
|
|
|
151
181
|
content = req.file_path.read_text(encoding="utf-8")
|
|
152
|
-
|
|
153
|
-
import re
|
|
182
|
+
new_end_line = f"*End* *{req.title}* | **Hash**: {new_hash}"
|
|
154
183
|
|
|
155
184
|
if req.hash:
|
|
156
|
-
#
|
|
157
|
-
#
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
content,
|
|
163
|
-
|
|
185
|
+
# Strategy: Try title first (most specific), then hash if title not found
|
|
186
|
+
# This handles both: (1) normal case, (2) mismatched title case
|
|
187
|
+
|
|
188
|
+
# First try: match by correct title (handles case where titles match)
|
|
189
|
+
pattern_by_title = rf"^\*End\*\s+\*{re.escape(req.title)}\*\s*\|\s*\*\*Hash\*\*:\s*[a-fA-F0-9]+\s*$"
|
|
190
|
+
if re.search(pattern_by_title, content, re.MULTILINE):
|
|
191
|
+
content, count = re.subn(pattern_by_title, new_end_line, content, flags=re.MULTILINE)
|
|
192
|
+
if count > 0:
|
|
193
|
+
result['updated'] = True
|
|
194
|
+
else:
|
|
195
|
+
# Second try: find by hash value (handles mismatched title)
|
|
196
|
+
# Pattern: *End* *AnyTitle* | **Hash**: oldhash
|
|
197
|
+
pattern_by_hash = rf"^\*End\*\s+\*([^*]+)\*\s*\|\s*\*\*Hash\*\*:\s*{re.escape(req.hash)}\s*$"
|
|
198
|
+
match = re.search(pattern_by_hash, content, re.MULTILINE)
|
|
199
|
+
|
|
200
|
+
if match:
|
|
201
|
+
old_title = match.group(1)
|
|
202
|
+
if old_title != req.title:
|
|
203
|
+
result['title_fixed'] = True
|
|
204
|
+
result['old_title'] = old_title
|
|
205
|
+
|
|
206
|
+
# Replace entire line (only first match to avoid affecting other reqs)
|
|
207
|
+
content = re.sub(pattern_by_hash, new_end_line, content, count=1, flags=re.MULTILINE)
|
|
208
|
+
result['updated'] = True
|
|
164
209
|
else:
|
|
165
|
-
# Add hash to end marker
|
|
210
|
+
# Add hash to end marker (no existing hash)
|
|
166
211
|
# Pattern: *End* *Title* (without hash)
|
|
167
|
-
|
|
168
|
-
content = re.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
212
|
+
pattern = rf"^(\*End\*\s+\*{re.escape(req.title)}\*)(?!\s*\|\s*\*\*Hash\*\*)\s*$"
|
|
213
|
+
content, count = re.subn(pattern, new_end_line, content, flags=re.MULTILINE)
|
|
214
|
+
if count > 0:
|
|
215
|
+
result['updated'] = True
|
|
216
|
+
|
|
217
|
+
if result['updated']:
|
|
218
|
+
req.file_path.write_text(content, encoding="utf-8")
|
|
219
|
+
|
|
220
|
+
return result
|