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
elspais/config/loader.py DELETED
@@ -1,494 +0,0 @@
1
- """
2
- elspais.config.loader - Configuration loading and merging.
3
-
4
- Handles loading .elspais.toml files and merging with defaults.
5
- """
6
-
7
- import os
8
- import re
9
- from pathlib import Path
10
- from typing import Any, Dict, List, Optional
11
-
12
- from elspais.config.defaults import DEFAULT_CONFIG
13
-
14
-
15
- def load_config(config_path: Path) -> Dict[str, Any]:
16
- """
17
- Load configuration from a TOML file.
18
-
19
- Args:
20
- config_path: Path to the .elspais.toml file
21
-
22
- Returns:
23
- Merged configuration dictionary
24
- """
25
- user_config = parse_toml(config_path.read_text(encoding="utf-8"))
26
- merged = merge_configs(DEFAULT_CONFIG, user_config)
27
- merged = apply_env_overrides(merged)
28
- return merged
29
-
30
-
31
- def find_config_file(start_path: Path) -> Optional[Path]:
32
- """
33
- Find .elspais.toml configuration file.
34
-
35
- Searches from start_path up to git root or filesystem root.
36
-
37
- Args:
38
- start_path: Directory to start searching from
39
-
40
- Returns:
41
- Path to config file if found, None otherwise
42
- """
43
- current = start_path.resolve()
44
-
45
- # If start_path is a file, use its parent
46
- if current.is_file():
47
- current = current.parent
48
-
49
- while current != current.parent:
50
- config_path = current / ".elspais.toml"
51
- if config_path.exists():
52
- return config_path
53
-
54
- # Stop at git root
55
- if (current / ".git").exists():
56
- break
57
-
58
- current = current.parent
59
-
60
- return None
61
-
62
-
63
- def parse_toml(content: str) -> Dict[str, Any]:
64
- """
65
- Parse TOML content into a dictionary.
66
-
67
- Uses a simple parser for zero dependencies.
68
-
69
- Args:
70
- content: TOML file content
71
-
72
- Returns:
73
- Parsed dictionary
74
- """
75
- result: Dict[str, Any] = {}
76
- current_section: List[str] = []
77
- lines = content.split("\n")
78
- i = 0
79
-
80
- while i < len(lines):
81
- line = lines[i].strip()
82
-
83
- # Skip empty lines and comments
84
- if not line or line.startswith("#"):
85
- i += 1
86
- continue
87
-
88
- # Section header
89
- if line.startswith("[") and not line.startswith("[["):
90
- section = line.strip("[]").strip()
91
- # Handle nested sections like [patterns.types]
92
- current_section = section.split(".")
93
- # Create nested structure
94
- _ensure_nested(result, current_section)
95
- i += 1
96
- continue
97
-
98
- # Key-value pair
99
- if "=" in line:
100
- key, value = line.split("=", 1)
101
- key = key.strip()
102
- value = value.strip()
103
-
104
- # Handle multi-line arrays
105
- if value.startswith("[") and not value.endswith("]"):
106
- # Collect all lines until closing bracket
107
- value_lines = [value]
108
- i += 1
109
- while i < len(lines):
110
- next_line = lines[i].strip()
111
- value_lines.append(next_line)
112
- if "]" in next_line:
113
- break
114
- i += 1
115
- value = " ".join(value_lines)
116
-
117
- # Strip inline comments (but not from quoted strings)
118
- if not (value.startswith('"') or value.startswith("'")):
119
- # Find comment marker that's not inside brackets/braces
120
- comment_idx = _find_comment_start(value)
121
- if comment_idx >= 0:
122
- value = value[:comment_idx].strip()
123
-
124
- # Parse value
125
- parsed_value = _parse_value(value)
126
-
127
- # Set in current section
128
- target = _get_nested(result, current_section)
129
- target[key] = parsed_value
130
-
131
- i += 1
132
-
133
- return result
134
-
135
-
136
- def _parse_value(value: str) -> Any:
137
- """Parse a TOML value string."""
138
- value = value.strip()
139
-
140
- # Boolean
141
- if value.lower() == "true":
142
- return True
143
- if value.lower() == "false":
144
- return False
145
-
146
- # Integer
147
- if re.match(r"^-?\d+$", value):
148
- return int(value)
149
-
150
- # Float
151
- if re.match(r"^-?\d+\.\d+$", value):
152
- return float(value)
153
-
154
- # String (quoted)
155
- if (value.startswith('"') and value.endswith('"')) or (
156
- value.startswith("'") and value.endswith("'")
157
- ):
158
- return value[1:-1]
159
-
160
- # Array
161
- if value.startswith("[") and value.endswith("]"):
162
- inner = value[1:-1].strip()
163
- if not inner:
164
- return []
165
- # Simple array parsing
166
- items = []
167
- for item in _split_array(inner):
168
- item = item.strip()
169
- if item:
170
- items.append(_parse_value(item))
171
- return items
172
-
173
- # Inline table
174
- if value.startswith("{") and value.endswith("}"):
175
- inner = value[1:-1].strip()
176
- if not inner:
177
- return {}
178
- result = {}
179
- for pair in _split_array(inner):
180
- if "=" in pair:
181
- k, v = pair.split("=", 1)
182
- result[k.strip()] = _parse_value(v.strip())
183
- return result
184
-
185
- # Unquoted string
186
- return value
187
-
188
-
189
- def _find_comment_start(value: str) -> int:
190
- """Find the start of an inline comment, respecting brackets."""
191
- depth = 0
192
- for i, char in enumerate(value):
193
- if char in "[{":
194
- depth += 1
195
- elif char in "]}":
196
- depth -= 1
197
- elif char == "#" and depth == 0:
198
- return i
199
- return -1
200
-
201
-
202
- def _split_array(s: str) -> List[str]:
203
- """Split array/table content, respecting nested structures and quoted strings."""
204
- items = []
205
- current = ""
206
- depth = 0
207
- in_string = False
208
- string_char = None
209
-
210
- for char in s:
211
- if in_string:
212
- current += char
213
- if char == string_char:
214
- in_string = False
215
- string_char = None
216
- elif char in "\"'":
217
- in_string = True
218
- string_char = char
219
- current += char
220
- elif char in "[{":
221
- depth += 1
222
- current += char
223
- elif char in "]}":
224
- depth -= 1
225
- current += char
226
- elif char == "," and depth == 0:
227
- items.append(current.strip())
228
- current = ""
229
- else:
230
- current += char
231
-
232
- if current.strip():
233
- items.append(current.strip())
234
-
235
- return items
236
-
237
-
238
- def _ensure_nested(d: Dict, keys: List[str]) -> None:
239
- """Ensure nested dictionary structure exists."""
240
- current = d
241
- for key in keys:
242
- if key not in current:
243
- current[key] = {}
244
- current = current[key]
245
-
246
-
247
- def _get_nested(d: Dict, keys: List[str]) -> Dict:
248
- """Get nested dictionary by key path."""
249
- current = d
250
- for key in keys:
251
- current = current[key]
252
- return current
253
-
254
-
255
- def merge_configs(defaults: Dict[str, Any], user: Dict[str, Any]) -> Dict[str, Any]:
256
- """
257
- Deep merge user configuration over defaults.
258
-
259
- Args:
260
- defaults: Default configuration dictionary
261
- user: User configuration dictionary
262
-
263
- Returns:
264
- Merged configuration dictionary
265
- """
266
- result = {}
267
-
268
- # Start with all keys from defaults
269
- all_keys = set(defaults.keys()) | set(user.keys())
270
-
271
- for key in all_keys:
272
- default_val = defaults.get(key)
273
- user_val = user.get(key)
274
-
275
- if user_val is None:
276
- result[key] = default_val
277
- elif default_val is None:
278
- result[key] = user_val
279
- elif isinstance(default_val, dict) and isinstance(user_val, dict):
280
- result[key] = merge_configs(default_val, user_val)
281
- else:
282
- result[key] = user_val
283
-
284
- return result
285
-
286
-
287
- def apply_env_overrides(config: Dict[str, Any]) -> Dict[str, Any]:
288
- """
289
- Apply environment variable overrides to configuration.
290
-
291
- Pattern: ELSPAIS_<SECTION>_<KEY> (e.g., ELSPAIS_DIRECTORIES_SPEC)
292
-
293
- Args:
294
- config: Configuration dictionary
295
-
296
- Returns:
297
- Configuration with environment overrides applied
298
- """
299
- prefix = "ELSPAIS_"
300
-
301
- for key, value in os.environ.items():
302
- if not key.startswith(prefix):
303
- continue
304
-
305
- # Parse key: ELSPAIS_DIRECTORIES_SPEC -> directories.spec
306
- parts = key[len(prefix) :].lower().split("_")
307
-
308
- if len(parts) >= 2:
309
- section = parts[0]
310
- subkey = "_".join(parts[1:])
311
-
312
- if section in config and isinstance(config[section], dict):
313
- # Parse value (handle booleans, numbers)
314
- parsed: Any = value
315
- if value.lower() == "true":
316
- parsed = True
317
- elif value.lower() == "false":
318
- parsed = False
319
- elif value.isdigit():
320
- parsed = int(value)
321
-
322
- config[section][subkey] = parsed
323
-
324
- return config
325
-
326
-
327
- def validate_config(config: Dict[str, Any]) -> List[str]:
328
- """
329
- Validate configuration for required fields and valid values.
330
-
331
- Args:
332
- config: Configuration dictionary
333
-
334
- Returns:
335
- List of error messages (empty if valid)
336
- """
337
- errors = []
338
-
339
- # Check required sections
340
- if "project" not in config:
341
- errors.append("Missing required section: [project]")
342
- elif "type" in config["project"]:
343
- project_type = config["project"]["type"]
344
- if project_type not in ["core", "associated"]:
345
- errors.append(f"Invalid project type: {project_type}. Must be 'core' or 'associated'")
346
-
347
- # Validate associated config when type is associated
348
- if config.get("project", {}).get("type") == "associated":
349
- associated = config.get("associated", {})
350
- if not associated.get("prefix"):
351
- errors.append("Associated repository requires associated.prefix to be set")
352
-
353
- return errors
354
-
355
-
356
- def get_directories(
357
- config: Dict[str, Any],
358
- key: str,
359
- override: Optional[Path] = None,
360
- base_path: Optional[Path] = None,
361
- default: Optional[str] = None,
362
- require_exist: bool = True,
363
- recursive: bool = False,
364
- ignore: Optional[List[str]] = None,
365
- ) -> List[Path]:
366
- """Get directory paths from config, handling both strings and lists.
367
-
368
- Config can specify either a single directory or a list:
369
- - spec = "spec"
370
- - spec = ["spec", "spec/roadmap"]
371
- - code = ["apps", "packages", "server"]
372
-
373
- Args:
374
- config: Configuration dictionary
375
- key: Config key to look up under 'directories' section (e.g., "spec", "code")
376
- override: Explicit directory path override (e.g., from CLI --spec-dir)
377
- base_path: Base path to resolve relative directories (defaults to cwd)
378
- default: Default value if key not in config (defaults to key name)
379
- require_exist: If True, filter to only existing directories
380
- recursive: If True, include all subdirectories recursively
381
- ignore: List of directory names to ignore when recursing (e.g., ["node_modules", ".git"])
382
-
383
- Returns:
384
- List of directory paths (optionally filtered to existing ones)
385
- """
386
- if override:
387
- return [override]
388
-
389
- if base_path is None:
390
- base_path = Path.cwd()
391
-
392
- if default is None:
393
- default = key
394
-
395
- if ignore is None:
396
- ignore = config.get("directories", {}).get("ignore", [])
397
-
398
- dir_config = config.get("directories", {}).get(key, default)
399
-
400
- # Handle both string and list
401
- if isinstance(dir_config, str):
402
- dir_list = [dir_config]
403
- else:
404
- dir_list = list(dir_config)
405
-
406
- # Resolve paths
407
- result = []
408
- for dir_entry in dir_list:
409
- dir_path = base_path / dir_entry
410
- if require_exist and not (dir_path.exists() and dir_path.is_dir()):
411
- continue
412
-
413
- result.append(dir_path)
414
-
415
- # Recursively add subdirectories if requested
416
- if recursive and dir_path.exists() and dir_path.is_dir():
417
- for subdir in dir_path.rglob("*"):
418
- if subdir.is_dir():
419
- # Check if any part of the path is in ignore list
420
- if not any(ignored in subdir.parts for ignored in ignore):
421
- result.append(subdir)
422
-
423
- return result
424
-
425
-
426
- def get_code_directories(
427
- config: Dict[str, Any],
428
- base_path: Optional[Path] = None,
429
- ) -> List[Path]:
430
- """Get all code directories recursively, respecting ignore patterns.
431
-
432
- Convenience wrapper for get_directories with key="code" and recursive=True.
433
-
434
- Args:
435
- config: Configuration dictionary
436
- base_path: Base path to resolve relative directories (defaults to cwd)
437
-
438
- Returns:
439
- List of existing code directory paths (including all subdirectories)
440
- """
441
- return get_directories(
442
- config=config,
443
- key="code",
444
- base_path=base_path,
445
- recursive=True,
446
- )
447
-
448
-
449
- def get_spec_directories(
450
- spec_dir_override: Optional[Path],
451
- config: Dict[str, Any],
452
- base_path: Optional[Path] = None,
453
- ) -> List[Path]:
454
- """Get the spec directories from override or config.
455
-
456
- Convenience wrapper for get_directories with key="spec".
457
-
458
- Args:
459
- spec_dir_override: Explicit spec directory (e.g., from CLI --spec-dir)
460
- config: Configuration dictionary
461
- base_path: Base path to resolve relative directories (defaults to cwd)
462
-
463
- Returns:
464
- List of existing spec directory paths
465
- """
466
- return get_directories(
467
- config=config,
468
- key="spec",
469
- override=spec_dir_override,
470
- base_path=base_path,
471
- default="spec",
472
- )
473
-
474
-
475
- def get_content_rules(
476
- config: Dict[str, Any],
477
- base_path: Optional[Path] = None,
478
- ) -> List[Path]:
479
- """Get content rule file paths from configuration.
480
-
481
- Args:
482
- config: Configuration dictionary
483
- base_path: Base path to resolve relative paths (defaults to cwd)
484
-
485
- Returns:
486
- List of content rule file paths (may not exist)
487
- """
488
- if base_path is None:
489
- base_path = Path.cwd()
490
-
491
- rules_config = config.get("rules", {})
492
- rule_paths = rules_config.get("content_rules", [])
493
-
494
- return [base_path / rel_path for rel_path in rule_paths]
elspais/core/__init__.py DELETED
@@ -1,21 +0,0 @@
1
- """
2
- elspais.core - Core data models, pattern matching, and rule validation
3
- """
4
-
5
- from elspais.core.hasher import calculate_hash, verify_hash
6
- from elspais.core.models import ParsedRequirement, Requirement, RequirementType
7
- from elspais.core.patterns import PatternConfig, PatternValidator
8
- from elspais.core.rules import RuleEngine, RuleViolation, Severity
9
-
10
- __all__ = [
11
- "Requirement",
12
- "ParsedRequirement",
13
- "RequirementType",
14
- "PatternValidator",
15
- "PatternConfig",
16
- "RuleEngine",
17
- "RuleViolation",
18
- "Severity",
19
- "calculate_hash",
20
- "verify_hash",
21
- ]