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.
Files changed (147) hide show
  1. elspais/__init__.py +1 -10
  2. elspais/{sponsors/__init__.py → associates.py} +102 -56
  3. elspais/cli.py +366 -69
  4. elspais/commands/__init__.py +9 -3
  5. elspais/commands/analyze.py +118 -169
  6. elspais/commands/changed.py +12 -23
  7. elspais/commands/config_cmd.py +10 -13
  8. elspais/commands/edit.py +33 -13
  9. elspais/commands/example_cmd.py +319 -0
  10. elspais/commands/hash_cmd.py +161 -183
  11. elspais/commands/health.py +1177 -0
  12. elspais/commands/index.py +98 -115
  13. elspais/commands/init.py +99 -22
  14. elspais/commands/reformat_cmd.py +41 -433
  15. elspais/commands/rules_cmd.py +2 -2
  16. elspais/commands/trace.py +443 -324
  17. elspais/commands/validate.py +193 -411
  18. elspais/config/__init__.py +799 -5
  19. elspais/{core/content_rules.py → content_rules.py} +20 -2
  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 +45 -29
  58. elspais/mcp/__main__.py +5 -1
  59. elspais/mcp/file_mutations.py +138 -0
  60. elspais/mcp/server.py +1998 -244
  61. elspais/testing/__init__.py +3 -3
  62. elspais/testing/config.py +3 -0
  63. elspais/testing/mapper.py +1 -1
  64. elspais/testing/scanner.py +301 -12
  65. elspais/utilities/__init__.py +1 -0
  66. elspais/utilities/docs_loader.py +115 -0
  67. elspais/utilities/git.py +607 -0
  68. elspais/{core → utilities}/hasher.py +8 -22
  69. elspais/utilities/md_renderer.py +189 -0
  70. elspais/{core → utilities}/patterns.py +56 -51
  71. elspais/utilities/reference_config.py +626 -0
  72. elspais/validation/__init__.py +19 -0
  73. elspais/validation/format.py +264 -0
  74. {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/METADATA +7 -4
  75. elspais-0.43.5.dist-info/RECORD +80 -0
  76. elspais/config/defaults.py +0 -179
  77. elspais/config/loader.py +0 -494
  78. elspais/core/__init__.py +0 -21
  79. elspais/core/git.py +0 -346
  80. elspais/core/models.py +0 -320
  81. elspais/core/parser.py +0 -639
  82. elspais/core/rules.py +0 -509
  83. elspais/mcp/context.py +0 -172
  84. elspais/mcp/serializers.py +0 -112
  85. elspais/reformat/__init__.py +0 -50
  86. elspais/reformat/detector.py +0 -112
  87. elspais/reformat/hierarchy.py +0 -247
  88. elspais/reformat/line_breaks.py +0 -218
  89. elspais/reformat/prompts.py +0 -133
  90. elspais/reformat/transformer.py +0 -266
  91. elspais/trace_view/__init__.py +0 -55
  92. elspais/trace_view/coverage.py +0 -183
  93. elspais/trace_view/generators/__init__.py +0 -12
  94. elspais/trace_view/generators/base.py +0 -334
  95. elspais/trace_view/generators/csv.py +0 -118
  96. elspais/trace_view/generators/markdown.py +0 -170
  97. elspais/trace_view/html/__init__.py +0 -33
  98. elspais/trace_view/html/generator.py +0 -1140
  99. elspais/trace_view/html/templates/base.html +0 -283
  100. elspais/trace_view/html/templates/components/code_viewer_modal.html +0 -14
  101. elspais/trace_view/html/templates/components/file_picker_modal.html +0 -20
  102. elspais/trace_view/html/templates/components/legend_modal.html +0 -69
  103. elspais/trace_view/html/templates/components/review_panel.html +0 -118
  104. elspais/trace_view/html/templates/partials/review/help/help-panel.json +0 -244
  105. elspais/trace_view/html/templates/partials/review/help/onboarding.json +0 -77
  106. elspais/trace_view/html/templates/partials/review/help/tooltips.json +0 -237
  107. elspais/trace_view/html/templates/partials/review/review-comments.js +0 -928
  108. elspais/trace_view/html/templates/partials/review/review-data.js +0 -961
  109. elspais/trace_view/html/templates/partials/review/review-help.js +0 -679
  110. elspais/trace_view/html/templates/partials/review/review-init.js +0 -177
  111. elspais/trace_view/html/templates/partials/review/review-line-numbers.js +0 -429
  112. elspais/trace_view/html/templates/partials/review/review-packages.js +0 -1029
  113. elspais/trace_view/html/templates/partials/review/review-position.js +0 -540
  114. elspais/trace_view/html/templates/partials/review/review-resize.js +0 -115
  115. elspais/trace_view/html/templates/partials/review/review-status.js +0 -659
  116. elspais/trace_view/html/templates/partials/review/review-sync.js +0 -992
  117. elspais/trace_view/html/templates/partials/review-styles.css +0 -2238
  118. elspais/trace_view/html/templates/partials/scripts.js +0 -1741
  119. elspais/trace_view/html/templates/partials/styles.css +0 -1756
  120. elspais/trace_view/models.py +0 -378
  121. elspais/trace_view/review/__init__.py +0 -63
  122. elspais/trace_view/review/branches.py +0 -1142
  123. elspais/trace_view/review/models.py +0 -1200
  124. elspais/trace_view/review/position.py +0 -591
  125. elspais/trace_view/review/server.py +0 -1032
  126. elspais/trace_view/review/status.py +0 -455
  127. elspais/trace_view/review/storage.py +0 -1343
  128. elspais/trace_view/scanning.py +0 -213
  129. elspais/trace_view/specs/README.md +0 -84
  130. elspais/trace_view/specs/tv-d00001-template-architecture.md +0 -36
  131. elspais/trace_view/specs/tv-d00002-css-extraction.md +0 -37
  132. elspais/trace_view/specs/tv-d00003-js-extraction.md +0 -43
  133. elspais/trace_view/specs/tv-d00004-build-embedding.md +0 -40
  134. elspais/trace_view/specs/tv-d00005-test-format.md +0 -78
  135. elspais/trace_view/specs/tv-d00010-review-data-models.md +0 -33
  136. elspais/trace_view/specs/tv-d00011-review-storage.md +0 -33
  137. elspais/trace_view/specs/tv-d00012-position-resolution.md +0 -33
  138. elspais/trace_view/specs/tv-d00013-git-branches.md +0 -31
  139. elspais/trace_view/specs/tv-d00014-review-api-server.md +0 -31
  140. elspais/trace_view/specs/tv-d00015-status-modifier.md +0 -27
  141. elspais/trace_view/specs/tv-d00016-js-integration.md +0 -33
  142. elspais/trace_view/specs/tv-p00001-html-generator.md +0 -33
  143. elspais/trace_view/specs/tv-p00002-review-system.md +0 -29
  144. elspais-0.11.2.dist-info/RECORD +0 -101
  145. {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/WHEEL +0 -0
  146. {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/entry_points.txt +0 -0
  147. {elspais-0.11.2.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
- ]