elspais 0.11.2__py3-none-any.whl → 0.43.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- elspais/__init__.py +1 -10
- elspais/{sponsors/__init__.py → associates.py} +102 -56
- elspais/cli.py +366 -69
- elspais/commands/__init__.py +9 -3
- elspais/commands/analyze.py +118 -169
- elspais/commands/changed.py +12 -23
- elspais/commands/config_cmd.py +10 -13
- elspais/commands/edit.py +33 -13
- elspais/commands/example_cmd.py +319 -0
- elspais/commands/hash_cmd.py +161 -183
- elspais/commands/health.py +1177 -0
- elspais/commands/index.py +98 -115
- elspais/commands/init.py +99 -22
- elspais/commands/reformat_cmd.py +41 -433
- elspais/commands/rules_cmd.py +2 -2
- elspais/commands/trace.py +443 -324
- elspais/commands/validate.py +193 -411
- elspais/config/__init__.py +799 -5
- elspais/{core/content_rules.py → content_rules.py} +20 -2
- elspais/docs/cli/assertions.md +67 -0
- elspais/docs/cli/commands.md +304 -0
- elspais/docs/cli/config.md +262 -0
- elspais/docs/cli/format.md +66 -0
- elspais/docs/cli/git.md +45 -0
- elspais/docs/cli/health.md +190 -0
- elspais/docs/cli/hierarchy.md +60 -0
- elspais/docs/cli/ignore.md +72 -0
- elspais/docs/cli/mcp.md +245 -0
- elspais/docs/cli/quickstart.md +58 -0
- elspais/docs/cli/traceability.md +89 -0
- elspais/docs/cli/validation.md +96 -0
- elspais/graph/GraphNode.py +383 -0
- elspais/graph/__init__.py +40 -0
- elspais/graph/annotators.py +927 -0
- elspais/graph/builder.py +1886 -0
- elspais/graph/deserializer.py +248 -0
- elspais/graph/factory.py +284 -0
- elspais/graph/metrics.py +127 -0
- elspais/graph/mutations.py +161 -0
- elspais/graph/parsers/__init__.py +156 -0
- elspais/graph/parsers/code.py +213 -0
- elspais/graph/parsers/comments.py +112 -0
- elspais/graph/parsers/config_helpers.py +29 -0
- elspais/graph/parsers/heredocs.py +225 -0
- elspais/graph/parsers/journey.py +131 -0
- elspais/graph/parsers/remainder.py +79 -0
- elspais/graph/parsers/requirement.py +347 -0
- elspais/graph/parsers/results/__init__.py +6 -0
- elspais/graph/parsers/results/junit_xml.py +229 -0
- elspais/graph/parsers/results/pytest_json.py +313 -0
- elspais/graph/parsers/test.py +305 -0
- elspais/graph/relations.py +78 -0
- elspais/graph/serialize.py +216 -0
- elspais/html/__init__.py +8 -0
- elspais/html/generator.py +731 -0
- elspais/html/templates/trace_view.html.j2 +2151 -0
- elspais/mcp/__init__.py +45 -29
- elspais/mcp/__main__.py +5 -1
- elspais/mcp/file_mutations.py +138 -0
- elspais/mcp/server.py +1998 -244
- elspais/testing/__init__.py +3 -3
- elspais/testing/config.py +3 -0
- elspais/testing/mapper.py +1 -1
- elspais/testing/scanner.py +301 -12
- elspais/utilities/__init__.py +1 -0
- elspais/utilities/docs_loader.py +115 -0
- elspais/utilities/git.py +607 -0
- elspais/{core → utilities}/hasher.py +8 -22
- elspais/utilities/md_renderer.py +189 -0
- elspais/{core → utilities}/patterns.py +56 -51
- elspais/utilities/reference_config.py +626 -0
- elspais/validation/__init__.py +19 -0
- elspais/validation/format.py +264 -0
- {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/METADATA +7 -4
- elspais-0.43.5.dist-info/RECORD +80 -0
- elspais/config/defaults.py +0 -179
- elspais/config/loader.py +0 -494
- elspais/core/__init__.py +0 -21
- elspais/core/git.py +0 -346
- elspais/core/models.py +0 -320
- elspais/core/parser.py +0 -639
- elspais/core/rules.py +0 -509
- elspais/mcp/context.py +0 -172
- elspais/mcp/serializers.py +0 -112
- elspais/reformat/__init__.py +0 -50
- elspais/reformat/detector.py +0 -112
- elspais/reformat/hierarchy.py +0 -247
- elspais/reformat/line_breaks.py +0 -218
- elspais/reformat/prompts.py +0 -133
- elspais/reformat/transformer.py +0 -266
- elspais/trace_view/__init__.py +0 -55
- elspais/trace_view/coverage.py +0 -183
- elspais/trace_view/generators/__init__.py +0 -12
- elspais/trace_view/generators/base.py +0 -334
- elspais/trace_view/generators/csv.py +0 -118
- elspais/trace_view/generators/markdown.py +0 -170
- elspais/trace_view/html/__init__.py +0 -33
- elspais/trace_view/html/generator.py +0 -1140
- elspais/trace_view/html/templates/base.html +0 -283
- elspais/trace_view/html/templates/components/code_viewer_modal.html +0 -14
- elspais/trace_view/html/templates/components/file_picker_modal.html +0 -20
- elspais/trace_view/html/templates/components/legend_modal.html +0 -69
- elspais/trace_view/html/templates/components/review_panel.html +0 -118
- elspais/trace_view/html/templates/partials/review/help/help-panel.json +0 -244
- elspais/trace_view/html/templates/partials/review/help/onboarding.json +0 -77
- elspais/trace_view/html/templates/partials/review/help/tooltips.json +0 -237
- elspais/trace_view/html/templates/partials/review/review-comments.js +0 -928
- elspais/trace_view/html/templates/partials/review/review-data.js +0 -961
- elspais/trace_view/html/templates/partials/review/review-help.js +0 -679
- elspais/trace_view/html/templates/partials/review/review-init.js +0 -177
- elspais/trace_view/html/templates/partials/review/review-line-numbers.js +0 -429
- elspais/trace_view/html/templates/partials/review/review-packages.js +0 -1029
- elspais/trace_view/html/templates/partials/review/review-position.js +0 -540
- elspais/trace_view/html/templates/partials/review/review-resize.js +0 -115
- elspais/trace_view/html/templates/partials/review/review-status.js +0 -659
- elspais/trace_view/html/templates/partials/review/review-sync.js +0 -992
- elspais/trace_view/html/templates/partials/review-styles.css +0 -2238
- elspais/trace_view/html/templates/partials/scripts.js +0 -1741
- elspais/trace_view/html/templates/partials/styles.css +0 -1756
- elspais/trace_view/models.py +0 -378
- elspais/trace_view/review/__init__.py +0 -63
- elspais/trace_view/review/branches.py +0 -1142
- elspais/trace_view/review/models.py +0 -1200
- elspais/trace_view/review/position.py +0 -591
- elspais/trace_view/review/server.py +0 -1032
- elspais/trace_view/review/status.py +0 -455
- elspais/trace_view/review/storage.py +0 -1343
- elspais/trace_view/scanning.py +0 -213
- elspais/trace_view/specs/README.md +0 -84
- elspais/trace_view/specs/tv-d00001-template-architecture.md +0 -36
- elspais/trace_view/specs/tv-d00002-css-extraction.md +0 -37
- elspais/trace_view/specs/tv-d00003-js-extraction.md +0 -43
- elspais/trace_view/specs/tv-d00004-build-embedding.md +0 -40
- elspais/trace_view/specs/tv-d00005-test-format.md +0 -78
- elspais/trace_view/specs/tv-d00010-review-data-models.md +0 -33
- elspais/trace_view/specs/tv-d00011-review-storage.md +0 -33
- elspais/trace_view/specs/tv-d00012-position-resolution.md +0 -33
- elspais/trace_view/specs/tv-d00013-git-branches.md +0 -31
- elspais/trace_view/specs/tv-d00014-review-api-server.md +0 -31
- elspais/trace_view/specs/tv-d00015-status-modifier.md +0 -27
- elspais/trace_view/specs/tv-d00016-js-integration.md +0 -33
- elspais/trace_view/specs/tv-p00001-html-generator.md +0 -33
- elspais/trace_view/specs/tv-p00002-review-system.md +0 -29
- elspais-0.11.2.dist-info/RECORD +0 -101
- {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/WHEEL +0 -0
- {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/entry_points.txt +0 -0
- {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/licenses/LICENSE +0 -0
elspais/commands/hash_cmd.py
CHANGED
|
@@ -1,226 +1,204 @@
|
|
|
1
|
+
# Implements: REQ-int-d00003 (CLI Extension)
|
|
1
2
|
"""
|
|
2
|
-
elspais.commands.hash_cmd -
|
|
3
|
+
elspais.commands.hash_cmd - Manage requirement hashes.
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
Uses the graph-based system for hash verification and updates.
|
|
5
6
|
"""
|
|
6
7
|
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
7
10
|
import argparse
|
|
8
11
|
import sys
|
|
9
|
-
from pathlib import Path
|
|
10
12
|
|
|
11
|
-
from elspais.
|
|
12
|
-
from elspais.config.loader import find_config_file, get_spec_directories, load_config
|
|
13
|
-
from elspais.core.hasher import calculate_hash, verify_hash
|
|
14
|
-
from elspais.core.models import Requirement
|
|
15
|
-
from elspais.core.parser import RequirementParser
|
|
16
|
-
from elspais.core.patterns import PatternConfig
|
|
13
|
+
from elspais.graph import NodeKind
|
|
17
14
|
|
|
18
15
|
|
|
19
16
|
def run(args: argparse.Namespace) -> int:
|
|
20
|
-
"""Run the hash command.
|
|
21
|
-
if not args.hash_action:
|
|
22
|
-
print("Usage: elspais hash {verify|update}")
|
|
23
|
-
return 1
|
|
24
|
-
|
|
25
|
-
if args.hash_action == "verify":
|
|
26
|
-
return run_verify(args)
|
|
27
|
-
elif args.hash_action == "update":
|
|
28
|
-
return run_update(args)
|
|
29
|
-
|
|
30
|
-
return 1
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def run_verify(args: argparse.Namespace) -> int:
|
|
34
|
-
"""Verify all requirement hashes."""
|
|
35
|
-
config, requirements = load_requirements(args)
|
|
36
|
-
if not requirements:
|
|
37
|
-
return 1
|
|
38
|
-
|
|
39
|
-
hash_length = config.get("validation", {}).get("hash_length", 8)
|
|
40
|
-
algorithm = config.get("validation", {}).get("hash_algorithm", "sha256")
|
|
41
|
-
|
|
42
|
-
mismatches = []
|
|
43
|
-
missing = []
|
|
17
|
+
"""Run the hash command.
|
|
44
18
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if not verify_hash(req.body, req.hash, length=hash_length, algorithm=algorithm):
|
|
51
|
-
mismatches.append((req_id, req.hash, expected))
|
|
19
|
+
Subcommands:
|
|
20
|
+
- verify: Check hashes match content
|
|
21
|
+
- update: Recalculate and update hashes
|
|
22
|
+
"""
|
|
23
|
+
from elspais.graph.factory import build_graph
|
|
52
24
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
print(f"Missing hashes: {len(missing)}")
|
|
56
|
-
for req_id in missing:
|
|
57
|
-
print(f" - {req_id}")
|
|
58
|
-
|
|
59
|
-
if mismatches:
|
|
60
|
-
print(f"\nHash mismatches: {len(mismatches)}")
|
|
61
|
-
for req_id, current, expected in mismatches:
|
|
62
|
-
print(f" - {req_id}: {current} (expected: {expected})")
|
|
63
|
-
|
|
64
|
-
if not missing and not mismatches:
|
|
65
|
-
print(f"✓ All {len(requirements)} hashes verified")
|
|
66
|
-
return 0
|
|
25
|
+
spec_dir = getattr(args, "spec_dir", None)
|
|
26
|
+
config_path = getattr(args, "config", None)
|
|
67
27
|
|
|
68
|
-
|
|
28
|
+
graph = build_graph(
|
|
29
|
+
spec_dirs=[spec_dir] if spec_dir else None,
|
|
30
|
+
config_path=config_path,
|
|
31
|
+
)
|
|
69
32
|
|
|
33
|
+
action = getattr(args, "hash_action", None)
|
|
70
34
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
35
|
+
if action == "verify":
|
|
36
|
+
return _verify_hashes(graph, args)
|
|
37
|
+
elif action == "update":
|
|
38
|
+
return _update_hashes(graph, args)
|
|
39
|
+
else:
|
|
40
|
+
print("Usage: elspais hash <verify|update>", file=sys.stderr)
|
|
75
41
|
return 1
|
|
76
42
|
|
|
77
|
-
hash_length = config.get("validation", {}).get("hash_length", 8)
|
|
78
|
-
algorithm = config.get("validation", {}).get("hash_algorithm", "sha256")
|
|
79
|
-
|
|
80
|
-
# Filter to specific requirement if specified
|
|
81
|
-
if args.req_id:
|
|
82
|
-
if args.req_id not in requirements:
|
|
83
|
-
print(f"Requirement not found: {args.req_id}")
|
|
84
|
-
return 1
|
|
85
|
-
requirements = {args.req_id: requirements[args.req_id]}
|
|
86
43
|
|
|
87
|
-
|
|
44
|
+
def _get_requirement_body(node) -> str:
|
|
45
|
+
"""Extract hashable body content from a requirement node.
|
|
88
46
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
47
|
+
Per spec/requirements-spec.md:
|
|
48
|
+
> The hash SHALL be calculated from:
|
|
49
|
+
> - every line AFTER the Header line
|
|
50
|
+
> - every line BEFORE the Footer line
|
|
93
51
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
52
|
+
The body_text is extracted during parsing and stored in the node.
|
|
53
|
+
This includes metadata, intro text, assertions - everything between
|
|
54
|
+
the header and footer markers.
|
|
97
55
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
print(f"Would update {len(updates)} hashes:")
|
|
101
|
-
for req_id, req, new_hash in updates:
|
|
102
|
-
old_hash = req.hash or "(none)"
|
|
103
|
-
print(f" {req_id}: {old_hash} -> {new_hash}")
|
|
104
|
-
else:
|
|
105
|
-
print(f"Updating {len(updates)} hashes...")
|
|
106
|
-
for req_id, req, new_hash in updates:
|
|
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(" [WARN] Could not find End marker to update")
|
|
56
|
+
Args:
|
|
57
|
+
node: The requirement GraphNode.
|
|
117
58
|
|
|
118
|
-
|
|
59
|
+
Returns:
|
|
60
|
+
Body text for hashing.
|
|
61
|
+
"""
|
|
62
|
+
# Use the stored body_text which was extracted during parsing
|
|
63
|
+
return node.get_field("body_text", "")
|
|
119
64
|
|
|
120
65
|
|
|
121
|
-
def
|
|
122
|
-
"""
|
|
123
|
-
|
|
124
|
-
if config_path and config_path.exists():
|
|
125
|
-
config = load_config(config_path)
|
|
126
|
-
else:
|
|
127
|
-
config = DEFAULT_CONFIG
|
|
66
|
+
def _verify_hashes(graph, args) -> int:
|
|
67
|
+
"""Verify all hashes match content."""
|
|
68
|
+
from elspais.utilities.hasher import calculate_hash
|
|
128
69
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
print("Error: No spec directories found", file=sys.stderr)
|
|
132
|
-
return config, {}
|
|
70
|
+
mismatches = []
|
|
71
|
+
missing = []
|
|
133
72
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
73
|
+
for node in graph.nodes_by_kind(NodeKind.REQUIREMENT):
|
|
74
|
+
stored_hash = node.hash
|
|
75
|
+
if not stored_hash:
|
|
76
|
+
missing.append(node.id)
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
# Get body content from the node's assertions
|
|
80
|
+
body = _get_requirement_body(node)
|
|
81
|
+
if body:
|
|
82
|
+
computed = calculate_hash(body)
|
|
83
|
+
if computed != stored_hash:
|
|
84
|
+
mismatches.append(
|
|
85
|
+
{
|
|
86
|
+
"id": node.id,
|
|
87
|
+
"stored": stored_hash,
|
|
88
|
+
"computed": computed,
|
|
89
|
+
}
|
|
90
|
+
)
|
|
139
91
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
92
|
+
# Report results
|
|
93
|
+
if not getattr(args, "quiet", False):
|
|
94
|
+
if missing:
|
|
95
|
+
print(f"Missing hashes: {len(missing)}")
|
|
96
|
+
for req_id in missing[:10]: # Show first 10
|
|
97
|
+
print(f" {req_id}")
|
|
98
|
+
if len(missing) > 10:
|
|
99
|
+
print(f" ... and {len(missing) - 10} more")
|
|
100
|
+
|
|
101
|
+
if mismatches:
|
|
102
|
+
print(f"Hash mismatches: {len(mismatches)}")
|
|
103
|
+
for m in mismatches[:10]:
|
|
104
|
+
print(f" {m['id']}: stored={m['stored']} computed={m['computed']}")
|
|
105
|
+
if len(mismatches) > 10:
|
|
106
|
+
print(f" ... and {len(mismatches) - 10} more")
|
|
107
|
+
|
|
108
|
+
if not missing and not mismatches:
|
|
109
|
+
print("All hashes valid")
|
|
145
110
|
|
|
146
|
-
return
|
|
111
|
+
return 1 if mismatches else 0
|
|
147
112
|
|
|
148
113
|
|
|
149
|
-
def
|
|
150
|
-
"""Update
|
|
114
|
+
def _update_hashes(graph, args) -> int:
|
|
115
|
+
"""Update hashes in spec files.
|
|
151
116
|
|
|
152
|
-
Finds
|
|
153
|
-
|
|
154
|
-
|
|
117
|
+
Finds requirements with mismatched hashes and updates them.
|
|
118
|
+
Supports --dry-run to preview changes without applying them.
|
|
119
|
+
Supports --req-id to update a specific requirement only.
|
|
120
|
+
"""
|
|
121
|
+
from pathlib import Path
|
|
155
122
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
new_hash: New hash value to write
|
|
123
|
+
from elspais.mcp.file_mutations import update_hash_in_file
|
|
124
|
+
from elspais.utilities.hasher import calculate_hash
|
|
159
125
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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)
|
|
167
|
-
"""
|
|
168
|
-
import re
|
|
126
|
+
dry_run = getattr(args, "dry_run", False)
|
|
127
|
+
target_req_id = getattr(args, "req_id", None)
|
|
128
|
+
json_output = getattr(args, "json_output", False)
|
|
169
129
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
"old_hash": req.hash,
|
|
173
|
-
"new_hash": new_hash,
|
|
174
|
-
"title_fixed": False,
|
|
175
|
-
"old_title": None,
|
|
176
|
-
}
|
|
130
|
+
# Get repo root from graph or default to cwd
|
|
131
|
+
repo_root = getattr(graph, "_repo_root", None) or Path.cwd()
|
|
177
132
|
|
|
178
|
-
|
|
179
|
-
|
|
133
|
+
updates = []
|
|
134
|
+
for node in graph.nodes_by_kind(NodeKind.REQUIREMENT):
|
|
135
|
+
# Filter to specific requirement if requested
|
|
136
|
+
if target_req_id and node.id != target_req_id:
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
stored_hash = node.hash
|
|
140
|
+
body = _get_requirement_body(node)
|
|
141
|
+
|
|
142
|
+
# Skip if no body content (can't compute hash)
|
|
143
|
+
if not body:
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
computed_hash = calculate_hash(body)
|
|
147
|
+
|
|
148
|
+
# Check if hash needs updating
|
|
149
|
+
if stored_hash != computed_hash:
|
|
150
|
+
# Get file path from source location
|
|
151
|
+
source = node.source
|
|
152
|
+
if source is None:
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
file_path = Path(repo_root) / source.path
|
|
156
|
+
|
|
157
|
+
updates.append(
|
|
158
|
+
{
|
|
159
|
+
"id": node.id,
|
|
160
|
+
"old_hash": stored_hash or "(none)",
|
|
161
|
+
"new_hash": computed_hash,
|
|
162
|
+
"file": str(file_path),
|
|
163
|
+
}
|
|
164
|
+
)
|
|
180
165
|
|
|
181
|
-
|
|
182
|
-
|
|
166
|
+
# Handle dry run
|
|
167
|
+
if dry_run:
|
|
168
|
+
if json_output:
|
|
169
|
+
import json
|
|
183
170
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
171
|
+
print(json.dumps({"updates": updates, "count": len(updates)}, indent=2))
|
|
172
|
+
else:
|
|
173
|
+
if not updates:
|
|
174
|
+
print("All hashes are up to date.")
|
|
175
|
+
else:
|
|
176
|
+
print(f"Would update {len(updates)} hash(es):")
|
|
177
|
+
for u in updates:
|
|
178
|
+
print(f" {u['id']}: {u['old_hash']} -> {u['new_hash']}")
|
|
179
|
+
return 0
|
|
187
180
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
181
|
+
# Apply updates
|
|
182
|
+
updated_count = 0
|
|
183
|
+
for u in updates:
|
|
184
|
+
success = update_hash_in_file(
|
|
185
|
+
file_path=Path(u["file"]),
|
|
186
|
+
req_id=u["id"],
|
|
187
|
+
new_hash=u["new_hash"],
|
|
191
188
|
)
|
|
192
|
-
if
|
|
193
|
-
|
|
194
|
-
if
|
|
195
|
-
|
|
196
|
-
else:
|
|
197
|
-
# Second try: find by hash value (handles mismatched title)
|
|
198
|
-
# Pattern: *End* *AnyTitle* | **Hash**: oldhash
|
|
199
|
-
pattern_by_hash = (
|
|
200
|
-
rf"^\*End\*\s+\*([^*]+)\*\s*\|\s*\*\*Hash\*\*:\s*{re.escape(req.hash)}\s*$"
|
|
201
|
-
)
|
|
202
|
-
match = re.search(pattern_by_hash, content, re.MULTILINE)
|
|
189
|
+
if success:
|
|
190
|
+
updated_count += 1
|
|
191
|
+
if not json_output:
|
|
192
|
+
print(f"Updated {u['id']}: {u['old_hash']} -> {u['new_hash']}")
|
|
203
193
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if old_title != req.title:
|
|
207
|
-
result["title_fixed"] = True
|
|
208
|
-
result["old_title"] = old_title
|
|
194
|
+
if json_output:
|
|
195
|
+
import json
|
|
209
196
|
|
|
210
|
-
|
|
211
|
-
content = re.sub(
|
|
212
|
-
pattern_by_hash, new_end_line, content, count=1, flags=re.MULTILINE
|
|
213
|
-
)
|
|
214
|
-
result["updated"] = True
|
|
197
|
+
print(json.dumps({"updated": updated_count, "total": len(updates)}, indent=2))
|
|
215
198
|
else:
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if count > 0:
|
|
221
|
-
result["updated"] = True
|
|
222
|
-
|
|
223
|
-
if result["updated"]:
|
|
224
|
-
req.file_path.write_text(content, encoding="utf-8")
|
|
199
|
+
if updated_count == 0:
|
|
200
|
+
print("No hashes needed updating.")
|
|
201
|
+
else:
|
|
202
|
+
print(f"Updated {updated_count} hash(es).")
|
|
225
203
|
|
|
226
|
-
return
|
|
204
|
+
return 0
|