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,220 +1,204 @@
1
+ # Implements: REQ-int-d00003 (CLI Extension)
1
2
  """
2
- elspais.commands.hash_cmd - Hash management command.
3
+ elspais.commands.hash_cmd - Manage requirement hashes.
3
4
 
4
- Verify and update requirement hashes.
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.config.defaults import DEFAULT_CONFIG
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
- for req_id, req in requirements.items():
46
- if not req.hash:
47
- missing.append(req_id)
48
- else:
49
- expected = calculate_hash(req.body, length=hash_length, algorithm=algorithm)
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
- # Report results
54
- if missing:
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
- return 1 if mismatches else 0
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
- def run_update(args: argparse.Namespace) -> int:
72
- """Update requirement hashes."""
73
- config, requirements = load_requirements(args)
74
- if not requirements:
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
43
 
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]}
44
+ def _get_requirement_body(node) -> str:
45
+ """Extract hashable body content from a requirement node.
86
46
 
87
- updates = []
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
88
51
 
89
- for req_id, req in requirements.items():
90
- expected = calculate_hash(req.body, length=hash_length, algorithm=algorithm)
91
- if req.hash != expected:
92
- updates.append((req_id, req, expected))
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.
93
55
 
94
- if not updates:
95
- print("All hashes are up to date")
96
- return 0
56
+ Args:
57
+ node: The requirement GraphNode.
97
58
 
98
- # Show or apply updates
99
- if args.dry_run:
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(f" [WARN] Could not find End marker to update")
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", "")
117
64
 
118
- return 0
119
65
 
66
+ def _verify_hashes(graph, args) -> int:
67
+ """Verify all hashes match content."""
68
+ from elspais.utilities.hasher import calculate_hash
120
69
 
121
- def load_requirements(args: argparse.Namespace) -> tuple:
122
- """Load configuration and requirements."""
123
- config_path = args.config or find_config_file(Path.cwd())
124
- if config_path and config_path.exists():
125
- config = load_config(config_path)
126
- else:
127
- config = DEFAULT_CONFIG
70
+ mismatches = []
71
+ missing = []
128
72
 
129
- spec_dirs = get_spec_directories(args.spec_dir, config)
130
- if not spec_dirs:
131
- print("Error: No spec directories found", file=sys.stderr)
132
- return config, {}
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
+ )
133
91
 
134
- pattern_config = PatternConfig.from_dict(config.get("patterns", {}))
135
- spec_config = config.get("spec", {})
136
- no_reference_values = spec_config.get("no_reference_values")
137
- skip_files = spec_config.get("skip_files", [])
138
- parser = RequirementParser(pattern_config, no_reference_values=no_reference_values)
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")
139
110
 
140
- try:
141
- requirements = parser.parse_directories(spec_dirs, skip_files=skip_files)
142
- except Exception as e:
143
- print(f"Error parsing requirements: {e}", file=sys.stderr)
144
- return config, {}
111
+ return 1 if mismatches else 0
145
112
 
146
- return config, requirements
147
113
 
114
+ def _update_hashes(graph, args) -> int:
115
+ """Update hashes in spec files.
148
116
 
149
- def update_hash_in_file(req: Requirement, new_hash: str) -> dict:
150
- """Update the hash in the requirement's source file.
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
151
122
 
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.
123
+ from elspais.mcp.file_mutations import update_hash_in_file
124
+ from elspais.utilities.hasher import calculate_hash
155
125
 
156
- Args:
157
- req: Requirement object with file_path, title, and hash
158
- new_hash: New hash value to write
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)
159
129
 
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)
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
-
178
- if not req.file_path:
179
- return result
180
-
181
- content = req.file_path.read_text(encoding="utf-8")
182
- new_end_line = f"*End* *{req.title}* | **Hash**: {new_hash}"
183
-
184
- if req.hash:
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
130
+ # Get repo root from graph or default to cwd
131
+ repo_root = getattr(graph, "_repo_root", None) or Path.cwd()
132
+
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
+ )
165
+
166
+ # Handle dry run
167
+ if dry_run:
168
+ if json_output:
169
+ import json
170
+
171
+ print(json.dumps({"updates": updates, "count": len(updates)}, indent=2))
194
172
  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
209
- else:
210
- # Add hash to end marker (no existing hash)
211
- # Pattern: *End* *Title* (without hash)
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
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
216
180
 
217
- if result['updated']:
218
- req.file_path.write_text(content, encoding="utf-8")
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"],
188
+ )
189
+ if success:
190
+ updated_count += 1
191
+ if not json_output:
192
+ print(f"Updated {u['id']}: {u['old_hash']} -> {u['new_hash']}")
193
+
194
+ if json_output:
195
+ import json
196
+
197
+ print(json.dumps({"updated": updated_count, "total": len(updates)}, indent=2))
198
+ else:
199
+ if updated_count == 0:
200
+ print("No hashes needed updating.")
201
+ else:
202
+ print(f"Updated {updated_count} hash(es).")
219
203
 
220
- return result
204
+ return 0