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
@@ -0,0 +1,58 @@
1
+ # ELSPAIS QUICK START GUIDE
2
+
3
+ **elspais** validates requirements and traces them through code to tests.
4
+ Requirements live as Markdown files in your `spec/` directory.
5
+
6
+ ## 1. Initialize Your Project
7
+
8
+ $ elspais init # Creates .elspais.toml
9
+ $ elspais init --template # Also creates example requirement
10
+
11
+ ## 2. Write Your First Requirement
12
+
13
+ Create `spec/prd-auth.md`:
14
+
15
+ ```
16
+ # REQ-p00001: User Authentication
17
+
18
+ **Level**: PRD | **Status**: Active
19
+
20
+ **Purpose:** Enable secure user login.
21
+
22
+ ## Assertions
23
+
24
+ A. The system SHALL authenticate users via email and password.
25
+ B. The system SHALL lock accounts after 5 failed login attempts.
26
+
27
+ *End* *User Authentication* | **Hash**: 00000000
28
+ ```
29
+
30
+ ## 3. Validate and Update Hashes
31
+
32
+ $ elspais validate # Check format, links, hierarchy
33
+ $ elspais hash update # Compute content hashes
34
+
35
+ ## 4. Create Implementing Requirements
36
+
37
+ DEV requirements implement PRD requirements:
38
+
39
+ ```
40
+ # REQ-d00001: Password Hashing
41
+
42
+ **Level**: DEV | **Status**: Active | **Implements**: REQ-p00001-A
43
+
44
+ ## Assertions
45
+
46
+ A. The system SHALL use bcrypt with cost factor 12.
47
+ ```
48
+
49
+ ## 5. Generate Traceability Report
50
+
51
+ $ elspais trace --view # Interactive HTML tree
52
+ $ elspais trace --format html -o trace.html
53
+
54
+ ## Next Steps
55
+
56
+ $ elspais docs format # Full format reference
57
+ $ elspais docs hierarchy # Learn about PRD/OPS/DEV
58
+ $ elspais docs all # Complete documentation
@@ -0,0 +1,89 @@
1
+ # TRACEABILITY
2
+
3
+ ## What is Traceability?
4
+
5
+ Traceability connects requirements to their implementations and tests:
6
+
7
+ **Requirement** -> **Assertion** -> **Code** -> **Test** -> **Result**
8
+
9
+ This answers: "How do we know this requirement is satisfied?"
10
+
11
+ ## Generating Reports
12
+
13
+ $ elspais trace --view # Interactive HTML tree
14
+ $ elspais trace --format html # Basic HTML matrix
15
+ $ elspais trace --format csv # Spreadsheet export
16
+ $ elspais trace --graph # Full requirement->code->test graph
17
+ $ elspais trace --graph-json # Export graph as JSON
18
+
19
+ ## Command Options
20
+
21
+ `--format {markdown,html,csv,both}` Output format
22
+ `--output PATH` Output file path
23
+ `--view` Interactive HTML traceability tree
24
+ `--embed-content` Embed full markdown in HTML (offline viewing)
25
+ `--graph` Use unified traceability graph
26
+ `--graph-json` Output graph as JSON
27
+ `--report NAME` Report preset (minimal, standard, full)
28
+ `--depth LEVEL` Max depth (requirements, assertions, implementation, full)
29
+ `--mode {core,sponsor,combined}` Report scope
30
+ `--sponsor NAME` Sponsor name for filtered reports
31
+
32
+ ## Marking Code as Implementing
33
+
34
+ In Python, JavaScript, Go, etc., use comments:
35
+
36
+ ```python
37
+ # Implements: REQ-d00001-A
38
+ def hash_password(plain: str) -> str:
39
+ ...
40
+ ```
41
+
42
+ Or:
43
+ ```javascript
44
+ // Implements: REQ-d00001
45
+ function hashPassword(plain) { ... }
46
+ ```
47
+
48
+ ## Marking Tests as Validating
49
+
50
+ Reference requirement IDs in test docstrings or names:
51
+
52
+ ```python
53
+ def test_password_uses_bcrypt():
54
+ """REQ-d00001-A: Verify bcrypt with cost 12"""
55
+ ...
56
+ ```
57
+
58
+ Or in test names:
59
+ ```python
60
+ def test_REQ_d00001_A_bcrypt_cost():
61
+ ```
62
+
63
+ ## Coverage Indicators
64
+
65
+ In trace view:
66
+ **○** None - No code implements this assertion
67
+ **◐** Partial - Some assertions have implementations
68
+ **●** Full - All assertions have implementations
69
+ **⚡** Failure - Test failures detected
70
+ **◆** Changed - Modified vs main branch
71
+
72
+ ## Depth Levels
73
+
74
+ Control how much detail is shown:
75
+
76
+ `--depth 0` or `requirements` Show only requirements
77
+ `--depth 1` or `assertions` Include assertions
78
+ `--depth 2` or `implementation` Include code references
79
+ `--depth full` Unlimited depth (tests, results)
80
+
81
+ ## Understanding the Graph
82
+
83
+ $ elspais trace --graph-json > graph.json
84
+
85
+ The graph shows:
86
+ - Requirements and their assertions
87
+ - Which code files implement which assertions
88
+ - Which tests validate which requirements
89
+ - Test pass/fail status from JUnit/pytest results
@@ -0,0 +1,96 @@
1
+ # VALIDATION
2
+
3
+ ## Running Validation
4
+
5
+ $ elspais validate # Check all rules
6
+ $ elspais validate --fix # Auto-fix what's fixable
7
+ $ elspais validate --fix --dry-run # Preview fixes without applying
8
+ $ elspais validate -v # Verbose output
9
+
10
+ ## Command Options
11
+
12
+ `--fix` Auto-fix hashes and formatting issues
13
+ `--dry-run` Preview fixes without modifying files (use with --fix)
14
+ `--skip-rule RULE` Skip validation rules (repeatable)
15
+ `-j, --json` Output requirements as JSON
16
+
17
+ ## What Can Be Auto-Fixed
18
+
19
+ The `--fix` flag automatically corrects:
20
+
21
+ **Fixable:**
22
+
23
+ - Missing hash → Computes and inserts from assertion text
24
+ - Stale hash → Recomputes from current content
25
+ - Missing Status field → Adds default "Active"
26
+
27
+ **Not fixable (report only):**
28
+
29
+ - Broken references to non-existent requirements
30
+ - Orphaned requirements (no parent)
31
+ - Hierarchy violations
32
+
33
+ ## What Gets Validated
34
+
35
+ **Format** - Header line structure, hash presence
36
+ **Hierarchy** - Implements relationships follow level rules
37
+ **Links** - Referenced requirements exist
38
+ **Hashes** - Content matches stored hash
39
+ **IDs** - No duplicate requirement IDs
40
+
41
+ ## Skip Rule Patterns
42
+
43
+ Skip specific validation rules:
44
+
45
+ $ elspais validate --skip-rule hash.missing
46
+ $ elspais validate --skip-rule 'hash.*' # All hash rules
47
+ $ elspais validate --skip-rule hierarchy.* # All hierarchy rules
48
+
49
+ **Available Patterns:**
50
+
51
+ `hash.missing` Hash footer is missing
52
+ `hash.mismatch` Hash doesn't match content
53
+ `hash.*` All hash rules
54
+ `hierarchy.*` All hierarchy rules
55
+ `format.*` All format rules
56
+
57
+ ## Common Validation Errors
58
+
59
+ **Missing hash**
60
+ Fix: $ elspais hash update
61
+
62
+ **Stale hash** (content changed)
63
+ Fix: $ elspais hash update after reviewing changes
64
+
65
+ **Broken link** (implements non-existent requirement)
66
+ Fix: Correct the ID or create the missing requirement
67
+
68
+ **Hierarchy violation** (PRD implements DEV)
69
+ Fix: Reverse the relationship or change levels
70
+
71
+ ## Suppressing Warnings
72
+
73
+ For expected issues, add inline suppression:
74
+
75
+ ```markdown
76
+ # elspais: expected-broken-links 2
77
+ **Implements**: REQ-future-001, REQ-future-002
78
+ ```
79
+
80
+ ## JSON Output
81
+
82
+ For tooling and CI integration:
83
+
84
+ $ elspais validate -j > requirements.json
85
+
86
+ ## CI Integration
87
+
88
+ Add to your CI pipeline:
89
+
90
+ ```yaml
91
+ # .github/workflows/validate.yml
92
+ steps:
93
+ - uses: actions/checkout@v4
94
+ - run: pip install elspais
95
+ - run: elspais validate
96
+ ```
@@ -0,0 +1,383 @@
1
+ """GraphNode - Unified node representation for traceability graph.
2
+
3
+ This module provides the core data structures for Architecture 3.0:
4
+ - NodeKind: Enum of node types
5
+ - SourceLocation: Portable file location reference
6
+ - GraphNode: Unified node with typed content and edge management
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections import deque
12
+ from dataclasses import InitVar, dataclass, field
13
+ from enum import Enum
14
+ from pathlib import Path
15
+ from typing import TYPE_CHECKING, Any, Callable, Iterator
16
+ from uuid import uuid4
17
+
18
+ if TYPE_CHECKING:
19
+ from elspais.graph.relations import Edge, EdgeKind
20
+
21
+
22
+ class NodeKind(Enum):
23
+ """Types of nodes in the traceability graph."""
24
+
25
+ REQUIREMENT = "requirement"
26
+ ASSERTION = "assertion"
27
+ CODE = "code"
28
+ TEST = "test"
29
+ TEST_RESULT = "result"
30
+ USER_JOURNEY = "journey"
31
+ REMAINDER = "remainder"
32
+
33
+
34
+ @dataclass
35
+ class SourceLocation:
36
+ """Portable reference to a location in a file.
37
+
38
+ Paths are stored relative to the repository root, enabling
39
+ consistent references across different working directories.
40
+ """
41
+
42
+ path: str # Relative to repo root
43
+ line: int # 1-based line number
44
+ end_line: int | None = None
45
+ repo: str | None = None # For multi-repo: "CAL", None = core
46
+
47
+ def absolute(self, repo_root: Path) -> Path:
48
+ """Resolve to absolute path."""
49
+ return repo_root / self.path
50
+
51
+ def __str__(self) -> str:
52
+ """Return string representation for display."""
53
+ if self.repo:
54
+ return f"{self.repo}:{self.path}:{self.line}"
55
+ return f"{self.path}:{self.line}"
56
+
57
+
58
+ @dataclass
59
+ class GraphNode:
60
+ """A node in the traceability graph.
61
+
62
+ GraphNode is the unified representation for all entities in the
63
+ traceability graph. The `kind` field determines the expected
64
+ structure of `content`.
65
+
66
+ Attributes:
67
+ id: Unique identifier for this node (mutable via set_id()).
68
+ kind: The type of node (requirement, assertion, etc.).
69
+ source: Where this node is defined in source files.
70
+ uuid: Stable 32-char hex string for GUI/DOM referencing.
71
+
72
+ Use get_label()/set_label() for label access.
73
+ Use set_id() for ID mutations.
74
+ """
75
+
76
+ id: str
77
+ kind: NodeKind
78
+ label: InitVar[str] = "" # Constructor parameter, stored in _label
79
+ source: SourceLocation | None = None
80
+ uuid: str = field(default_factory=lambda: uuid4().hex)
81
+
82
+ # Label stored internally - use get_label()/set_label() or label property
83
+ _label: str = field(default="", init=False)
84
+
85
+ # Internal storage (prefixed) - excluded from constructor
86
+ _children: list[GraphNode] = field(default_factory=list, init=False)
87
+ _parents: list[GraphNode] = field(default_factory=list, init=False, repr=False)
88
+ _outgoing_edges: list[Edge] = field(default_factory=list, init=False, repr=False)
89
+ _incoming_edges: list[Edge] = field(default_factory=list, init=False, repr=False)
90
+ _content: dict[str, Any] = field(default_factory=dict, init=False)
91
+ _metrics: dict[str, Any] = field(default_factory=dict, init=False)
92
+
93
+ def __post_init__(self, label: str) -> None:
94
+ """Initialize internal label from constructor parameter."""
95
+ self._label = label
96
+
97
+ def set_id(self, value: str) -> None:
98
+ """Set the node's unique identifier.
99
+
100
+ Use this method for mutations - it allows graph updates
101
+ without breaking the dataclass contract.
102
+
103
+ Note: This only changes the node's internal ID. The graph's
104
+ _index must be updated separately by the mutation method.
105
+ """
106
+ # Dataclass field assignment - directly mutate the id attribute
107
+ object.__setattr__(self, "id", value)
108
+
109
+ # Label accessors - encapsulated for future hooks (e.g., index updates)
110
+ def get_label(self) -> str:
111
+ """Get the node's display label."""
112
+ return self._label
113
+
114
+ def set_label(self, value: str) -> None:
115
+ """Set the node's display label.
116
+
117
+ Use this method for mutations - it may trigger graph updates
118
+ in the future (e.g., search index, change tracking).
119
+ """
120
+ self._label = value
121
+
122
+ # Iterator access
123
+ def iter_children(self) -> Iterator[GraphNode]:
124
+ """Iterate over child nodes."""
125
+ yield from self._children
126
+
127
+ def iter_parents(self) -> Iterator[GraphNode]:
128
+ """Iterate over parent nodes."""
129
+ yield from self._parents
130
+
131
+ def iter_outgoing_edges(self) -> Iterator[Edge]:
132
+ """Iterate over outgoing edges."""
133
+ yield from self._outgoing_edges
134
+
135
+ def iter_incoming_edges(self) -> Iterator[Edge]:
136
+ """Iterate over incoming edges."""
137
+ yield from self._incoming_edges
138
+
139
+ def iter_edges_by_kind(self, edge_kind: EdgeKind) -> Iterator[Edge]:
140
+ """Iterate outgoing edges of a specific kind."""
141
+ for e in self._outgoing_edges:
142
+ if e.kind == edge_kind:
143
+ yield e
144
+
145
+ # Count and membership checks (avoid materializing lists)
146
+ def child_count(self) -> int:
147
+ """Return number of children."""
148
+ return len(self._children)
149
+
150
+ def parent_count(self) -> int:
151
+ """Return number of parents."""
152
+ return len(self._parents)
153
+
154
+ def has_child(self, node: GraphNode) -> bool:
155
+ """Check if node is a child."""
156
+ return node in self._children
157
+
158
+ def has_parent(self, node: GraphNode) -> bool:
159
+ """Check if node is a parent."""
160
+ return node in self._parents
161
+
162
+ @property
163
+ def is_root(self) -> bool:
164
+ """True if this node has no parents."""
165
+ return len(self._parents) == 0
166
+
167
+ @property
168
+ def is_leaf(self) -> bool:
169
+ """True if this node has no children."""
170
+ return len(self._children) == 0
171
+
172
+ # Field accessors (return single value)
173
+ def get_field(self, key: str, default: Any = None) -> Any:
174
+ """Get a field from content."""
175
+ return self._content.get(key, default)
176
+
177
+ def set_field(self, key: str, value: Any) -> None:
178
+ """Set a field in content."""
179
+ self._content[key] = value
180
+
181
+ def get_all_content(self) -> dict[str, Any]:
182
+ """Get a copy of all content fields.
183
+
184
+ Returns a shallow copy for serialization purposes.
185
+ Prefer get_field() for accessing individual fields.
186
+ """
187
+ return dict(self._content)
188
+
189
+ def get_metric(self, key: str, default: Any = None) -> Any:
190
+ """Get a metric value."""
191
+ return self._metrics.get(key, default)
192
+
193
+ def set_metric(self, key: str, value: Any) -> None:
194
+ """Set a metric value."""
195
+ self._metrics[key] = value
196
+
197
+ # Convenience properties for common fields
198
+ @property
199
+ def level(self) -> str | None:
200
+ """Get the requirement level (PRD, OPS, DEV)."""
201
+ return self._content.get("level")
202
+
203
+ @property
204
+ def status(self) -> str | None:
205
+ """Get the requirement status."""
206
+ return self._content.get("status")
207
+
208
+ @property
209
+ def hash(self) -> str | None:
210
+ """Get the content hash."""
211
+ return self._content.get("hash")
212
+
213
+ def add_child(self, child: GraphNode) -> None:
214
+ """Add a child node with bidirectional linking.
215
+
216
+ This is the simple form without edge type. For typed edges,
217
+ use link() instead.
218
+
219
+ Args:
220
+ child: The child node to add.
221
+ """
222
+ if child not in self._children:
223
+ self._children.append(child)
224
+ if self not in child._parents:
225
+ child._parents.append(self)
226
+
227
+ def remove_child(self, child: GraphNode) -> bool:
228
+ """Remove a child node and clean up all links.
229
+
230
+ Removes the bidirectional node link and any edges between
231
+ this node and the child.
232
+
233
+ Args:
234
+ child: The child node to remove.
235
+
236
+ Returns:
237
+ True if the child was found and removed, False otherwise.
238
+ """
239
+ if child not in self._children:
240
+ return False
241
+
242
+ # Remove bidirectional node links
243
+ self._children.remove(child)
244
+ if self in child._parents:
245
+ child._parents.remove(self)
246
+
247
+ # Remove outgoing edges to this child
248
+ self._outgoing_edges = [e for e in self._outgoing_edges if e.target is not child]
249
+
250
+ # Remove incoming edges from this parent on the child
251
+ child._incoming_edges = [e for e in child._incoming_edges if e.source is not self]
252
+
253
+ return True
254
+
255
+ def link(
256
+ self,
257
+ child: GraphNode,
258
+ edge_kind: EdgeKind,
259
+ assertion_targets: list[str] | None = None,
260
+ ) -> Edge:
261
+ """Create a typed edge to a child node.
262
+
263
+ Args:
264
+ child: The child node to link.
265
+ edge_kind: The type of relationship.
266
+ assertion_targets: Optional list of assertion labels targeted.
267
+
268
+ Returns:
269
+ The created Edge.
270
+ """
271
+ from elspais.graph.relations import Edge
272
+
273
+ # Create the edge
274
+ edge = Edge(
275
+ source=self,
276
+ target=child,
277
+ kind=edge_kind,
278
+ assertion_targets=assertion_targets or [],
279
+ )
280
+
281
+ # Add bidirectional node links
282
+ if child not in self._children:
283
+ self._children.append(child)
284
+ if self not in child._parents:
285
+ child._parents.append(self)
286
+
287
+ # Track edges
288
+ self._outgoing_edges.append(edge)
289
+ child._incoming_edges.append(edge)
290
+
291
+ return edge
292
+
293
+ @property
294
+ def depth(self) -> int:
295
+ """Calculate depth from root (0 for roots).
296
+
297
+ For DAG structures, returns minimum depth (shortest path to root).
298
+ """
299
+ if not self._parents:
300
+ return 0
301
+ return 1 + min(p.depth for p in self._parents)
302
+
303
+ def walk(self, order: str = "pre") -> Iterator[GraphNode]:
304
+ """Iterate over this node and descendants.
305
+
306
+ Args:
307
+ order: Traversal order:
308
+ - "pre": Parent first (depth-first, pre-order)
309
+ - "post": Children first (depth-first, post-order)
310
+ - "level": Breadth-first (level order)
311
+
312
+ Yields:
313
+ GraphNode instances in the specified order.
314
+ """
315
+ if order == "pre":
316
+ yield from self._walk_preorder()
317
+ elif order == "post":
318
+ yield from self._walk_postorder()
319
+ elif order == "level":
320
+ yield from self._walk_level()
321
+ else:
322
+ raise ValueError(f"Unknown traversal order: {order}")
323
+
324
+ def _walk_preorder(self) -> Iterator[GraphNode]:
325
+ """Pre-order traversal (parent before children)."""
326
+ yield self
327
+ for child in self._children:
328
+ yield from child._walk_preorder()
329
+
330
+ def _walk_postorder(self) -> Iterator[GraphNode]:
331
+ """Post-order traversal (children before parent)."""
332
+ for child in self._children:
333
+ yield from child._walk_postorder()
334
+ yield self
335
+
336
+ def _walk_level(self) -> Iterator[GraphNode]:
337
+ """Level-order (breadth-first) traversal."""
338
+ queue: deque[GraphNode] = deque([self])
339
+ while queue:
340
+ node = queue.popleft()
341
+ yield node
342
+ queue.extend(node._children)
343
+
344
+ def ancestors(self) -> Iterator[GraphNode]:
345
+ """Iterate up through all ancestor paths (BFS).
346
+
347
+ For DAG structures, visits each unique ancestor once.
348
+
349
+ Yields:
350
+ Ancestor GraphNode instances.
351
+ """
352
+ visited: set[str] = set()
353
+ queue: deque[GraphNode] = deque(self._parents)
354
+ while queue:
355
+ node = queue.popleft()
356
+ if node.id not in visited:
357
+ visited.add(node.id)
358
+ yield node
359
+ queue.extend(node._parents)
360
+
361
+ def find(self, predicate: Callable[[GraphNode], bool]) -> Iterator[GraphNode]:
362
+ """Find all descendants matching predicate.
363
+
364
+ Args:
365
+ predicate: Function that returns True for matching nodes.
366
+
367
+ Yields:
368
+ Matching GraphNode instances.
369
+ """
370
+ for node in self.walk():
371
+ if predicate(node):
372
+ yield node
373
+
374
+ def find_by_kind(self, kind: NodeKind) -> Iterator[GraphNode]:
375
+ """Find all descendants of a specific kind.
376
+
377
+ Args:
378
+ kind: The NodeKind to filter by.
379
+
380
+ Yields:
381
+ GraphNode instances of the specified kind.
382
+ """
383
+ return self.find(lambda n: n.kind == kind)
@@ -0,0 +1,40 @@
1
+ """Graph module - Core graph data structures.
2
+
3
+ Exports:
4
+ - NodeKind: Enum of node types
5
+ - SourceLocation: Portable file location reference
6
+ - GraphNode: Unified node representation
7
+ - Edge: Typed edge between nodes
8
+ - EdgeKind: Enum of edge types
9
+ - BrokenReference: Reference to non-existent target (detection)
10
+ - CoverageSource: Enum for coverage origin type
11
+ - CoverageContribution: Single coverage claim on an assertion
12
+ - RollupMetrics: Aggregated coverage metrics for a requirement
13
+
14
+ Note: TraceGraph is in elspais.graph.builder (use graph.factory.build_graph() to construct)
15
+ """
16
+
17
+ from elspais.graph.annotators import annotate_coverage
18
+ from elspais.graph.GraphNode import (
19
+ GraphNode,
20
+ NodeKind,
21
+ SourceLocation,
22
+ )
23
+ from elspais.graph.metrics import CoverageContribution, CoverageSource, RollupMetrics
24
+ from elspais.graph.mutations import BrokenReference, MutationEntry, MutationLog
25
+ from elspais.graph.relations import Edge, EdgeKind
26
+
27
+ __all__ = [
28
+ "NodeKind",
29
+ "SourceLocation",
30
+ "GraphNode",
31
+ "Edge",
32
+ "EdgeKind",
33
+ "BrokenReference",
34
+ "MutationEntry",
35
+ "MutationLog",
36
+ "CoverageSource",
37
+ "CoverageContribution",
38
+ "RollupMetrics",
39
+ "annotate_coverage",
40
+ ]