elspais 0.11.2__py3-none-any.whl → 0.43.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- elspais/__init__.py +1 -10
- elspais/{sponsors/__init__.py → associates.py} +102 -56
- elspais/cli.py +366 -69
- elspais/commands/__init__.py +9 -3
- elspais/commands/analyze.py +118 -169
- elspais/commands/changed.py +12 -23
- elspais/commands/config_cmd.py +10 -13
- elspais/commands/edit.py +33 -13
- elspais/commands/example_cmd.py +319 -0
- elspais/commands/hash_cmd.py +161 -183
- elspais/commands/health.py +1177 -0
- elspais/commands/index.py +98 -115
- elspais/commands/init.py +99 -22
- elspais/commands/reformat_cmd.py +41 -433
- elspais/commands/rules_cmd.py +2 -2
- elspais/commands/trace.py +443 -324
- elspais/commands/validate.py +193 -411
- elspais/config/__init__.py +799 -5
- elspais/{core/content_rules.py → content_rules.py} +20 -2
- elspais/docs/cli/assertions.md +67 -0
- elspais/docs/cli/commands.md +304 -0
- elspais/docs/cli/config.md +262 -0
- elspais/docs/cli/format.md +66 -0
- elspais/docs/cli/git.md +45 -0
- elspais/docs/cli/health.md +190 -0
- elspais/docs/cli/hierarchy.md +60 -0
- elspais/docs/cli/ignore.md +72 -0
- elspais/docs/cli/mcp.md +245 -0
- elspais/docs/cli/quickstart.md +58 -0
- elspais/docs/cli/traceability.md +89 -0
- elspais/docs/cli/validation.md +96 -0
- elspais/graph/GraphNode.py +383 -0
- elspais/graph/__init__.py +40 -0
- elspais/graph/annotators.py +927 -0
- elspais/graph/builder.py +1886 -0
- elspais/graph/deserializer.py +248 -0
- elspais/graph/factory.py +284 -0
- elspais/graph/metrics.py +127 -0
- elspais/graph/mutations.py +161 -0
- elspais/graph/parsers/__init__.py +156 -0
- elspais/graph/parsers/code.py +213 -0
- elspais/graph/parsers/comments.py +112 -0
- elspais/graph/parsers/config_helpers.py +29 -0
- elspais/graph/parsers/heredocs.py +225 -0
- elspais/graph/parsers/journey.py +131 -0
- elspais/graph/parsers/remainder.py +79 -0
- elspais/graph/parsers/requirement.py +347 -0
- elspais/graph/parsers/results/__init__.py +6 -0
- elspais/graph/parsers/results/junit_xml.py +229 -0
- elspais/graph/parsers/results/pytest_json.py +313 -0
- elspais/graph/parsers/test.py +305 -0
- elspais/graph/relations.py +78 -0
- elspais/graph/serialize.py +216 -0
- elspais/html/__init__.py +8 -0
- elspais/html/generator.py +731 -0
- elspais/html/templates/trace_view.html.j2 +2151 -0
- elspais/mcp/__init__.py +45 -29
- elspais/mcp/__main__.py +5 -1
- elspais/mcp/file_mutations.py +138 -0
- elspais/mcp/server.py +1998 -244
- elspais/testing/__init__.py +3 -3
- elspais/testing/config.py +3 -0
- elspais/testing/mapper.py +1 -1
- elspais/testing/scanner.py +301 -12
- elspais/utilities/__init__.py +1 -0
- elspais/utilities/docs_loader.py +115 -0
- elspais/utilities/git.py +607 -0
- elspais/{core → utilities}/hasher.py +8 -22
- elspais/utilities/md_renderer.py +189 -0
- elspais/{core → utilities}/patterns.py +56 -51
- elspais/utilities/reference_config.py +626 -0
- elspais/validation/__init__.py +19 -0
- elspais/validation/format.py +264 -0
- {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/METADATA +7 -4
- elspais-0.43.5.dist-info/RECORD +80 -0
- elspais/config/defaults.py +0 -179
- elspais/config/loader.py +0 -494
- elspais/core/__init__.py +0 -21
- elspais/core/git.py +0 -346
- elspais/core/models.py +0 -320
- elspais/core/parser.py +0 -639
- elspais/core/rules.py +0 -509
- elspais/mcp/context.py +0 -172
- elspais/mcp/serializers.py +0 -112
- elspais/reformat/__init__.py +0 -50
- elspais/reformat/detector.py +0 -112
- elspais/reformat/hierarchy.py +0 -247
- elspais/reformat/line_breaks.py +0 -218
- elspais/reformat/prompts.py +0 -133
- elspais/reformat/transformer.py +0 -266
- elspais/trace_view/__init__.py +0 -55
- elspais/trace_view/coverage.py +0 -183
- elspais/trace_view/generators/__init__.py +0 -12
- elspais/trace_view/generators/base.py +0 -334
- elspais/trace_view/generators/csv.py +0 -118
- elspais/trace_view/generators/markdown.py +0 -170
- elspais/trace_view/html/__init__.py +0 -33
- elspais/trace_view/html/generator.py +0 -1140
- elspais/trace_view/html/templates/base.html +0 -283
- elspais/trace_view/html/templates/components/code_viewer_modal.html +0 -14
- elspais/trace_view/html/templates/components/file_picker_modal.html +0 -20
- elspais/trace_view/html/templates/components/legend_modal.html +0 -69
- elspais/trace_view/html/templates/components/review_panel.html +0 -118
- elspais/trace_view/html/templates/partials/review/help/help-panel.json +0 -244
- elspais/trace_view/html/templates/partials/review/help/onboarding.json +0 -77
- elspais/trace_view/html/templates/partials/review/help/tooltips.json +0 -237
- elspais/trace_view/html/templates/partials/review/review-comments.js +0 -928
- elspais/trace_view/html/templates/partials/review/review-data.js +0 -961
- elspais/trace_view/html/templates/partials/review/review-help.js +0 -679
- elspais/trace_view/html/templates/partials/review/review-init.js +0 -177
- elspais/trace_view/html/templates/partials/review/review-line-numbers.js +0 -429
- elspais/trace_view/html/templates/partials/review/review-packages.js +0 -1029
- elspais/trace_view/html/templates/partials/review/review-position.js +0 -540
- elspais/trace_view/html/templates/partials/review/review-resize.js +0 -115
- elspais/trace_view/html/templates/partials/review/review-status.js +0 -659
- elspais/trace_view/html/templates/partials/review/review-sync.js +0 -992
- elspais/trace_view/html/templates/partials/review-styles.css +0 -2238
- elspais/trace_view/html/templates/partials/scripts.js +0 -1741
- elspais/trace_view/html/templates/partials/styles.css +0 -1756
- elspais/trace_view/models.py +0 -378
- elspais/trace_view/review/__init__.py +0 -63
- elspais/trace_view/review/branches.py +0 -1142
- elspais/trace_view/review/models.py +0 -1200
- elspais/trace_view/review/position.py +0 -591
- elspais/trace_view/review/server.py +0 -1032
- elspais/trace_view/review/status.py +0 -455
- elspais/trace_view/review/storage.py +0 -1343
- elspais/trace_view/scanning.py +0 -213
- elspais/trace_view/specs/README.md +0 -84
- elspais/trace_view/specs/tv-d00001-template-architecture.md +0 -36
- elspais/trace_view/specs/tv-d00002-css-extraction.md +0 -37
- elspais/trace_view/specs/tv-d00003-js-extraction.md +0 -43
- elspais/trace_view/specs/tv-d00004-build-embedding.md +0 -40
- elspais/trace_view/specs/tv-d00005-test-format.md +0 -78
- elspais/trace_view/specs/tv-d00010-review-data-models.md +0 -33
- elspais/trace_view/specs/tv-d00011-review-storage.md +0 -33
- elspais/trace_view/specs/tv-d00012-position-resolution.md +0 -33
- elspais/trace_view/specs/tv-d00013-git-branches.md +0 -31
- elspais/trace_view/specs/tv-d00014-review-api-server.md +0 -31
- elspais/trace_view/specs/tv-d00015-status-modifier.md +0 -27
- elspais/trace_view/specs/tv-d00016-js-integration.md +0 -33
- elspais/trace_view/specs/tv-p00001-html-generator.md +0 -33
- elspais/trace_view/specs/tv-p00002-review-system.md +0 -29
- elspais-0.11.2.dist-info/RECORD +0 -101
- {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/WHEEL +0 -0
- {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/entry_points.txt +0 -0
- {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/licenses/LICENSE +0 -0
elspais/commands/trace.py
CHANGED
|
@@ -2,365 +2,484 @@
|
|
|
2
2
|
"""
|
|
3
3
|
elspais.commands.trace - Generate traceability matrix command.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Uses the graph-based system to generate traceability reports in various formats.
|
|
6
|
+
Commands only work with graph data (zero file I/O for reading requirements).
|
|
7
|
+
|
|
8
|
+
OUTPUT FORMATS:
|
|
9
|
+
- markdown: Table with columns based on report preset
|
|
10
|
+
- csv: Same columns, comma-separated with proper escaping
|
|
11
|
+
- html: Basic styled HTML table
|
|
12
|
+
- json: Full requirement data including body, assertions, hash, file_path
|
|
13
|
+
- both: Generates both markdown and csv (legacy mode)
|
|
14
|
+
|
|
15
|
+
REPORT PRESETS (--report):
|
|
16
|
+
- minimal: ID, Title, Status only (quick overview)
|
|
17
|
+
- standard: ID, Title, Level, Status, Implements (default)
|
|
18
|
+
- full: All fields including Body, Assertions, Hash, Code/Test refs
|
|
19
|
+
|
|
20
|
+
INTERACTIVE VIEW (--view):
|
|
21
|
+
- Uses elspais.html.HTMLGenerator
|
|
22
|
+
- Generates interactive HTML with collapsible hierarchy
|
|
23
|
+
- Default output: traceability_view.html
|
|
6
24
|
"""
|
|
7
25
|
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
8
28
|
import argparse
|
|
29
|
+
import json
|
|
9
30
|
import sys
|
|
31
|
+
from dataclasses import dataclass
|
|
10
32
|
from pathlib import Path
|
|
11
|
-
from typing import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
from elspais.
|
|
15
|
-
|
|
16
|
-
from elspais.
|
|
17
|
-
|
|
33
|
+
from typing import TYPE_CHECKING, Iterator
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from elspais.graph.builder import TraceGraph
|
|
37
|
+
|
|
38
|
+
from elspais.graph import NodeKind
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class ReportPreset:
|
|
43
|
+
"""Configuration for a report preset."""
|
|
44
|
+
|
|
45
|
+
name: str
|
|
46
|
+
columns: list[str]
|
|
47
|
+
include_body: bool = False
|
|
48
|
+
include_assertions: bool = False
|
|
49
|
+
include_code_refs: bool = False
|
|
50
|
+
include_test_refs: bool = False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Define report presets
|
|
54
|
+
REPORT_PRESETS = {
|
|
55
|
+
"minimal": ReportPreset(
|
|
56
|
+
name="minimal",
|
|
57
|
+
columns=["id", "title", "status"],
|
|
58
|
+
),
|
|
59
|
+
"standard": ReportPreset(
|
|
60
|
+
name="standard",
|
|
61
|
+
columns=["id", "title", "level", "status", "implements"],
|
|
62
|
+
),
|
|
63
|
+
"full": ReportPreset(
|
|
64
|
+
name="full",
|
|
65
|
+
columns=["id", "title", "level", "status", "implements", "hash", "file"],
|
|
66
|
+
include_body=True,
|
|
67
|
+
include_assertions=True,
|
|
68
|
+
include_code_refs=True,
|
|
69
|
+
include_test_refs=True,
|
|
70
|
+
),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
DEFAULT_PRESET = "standard"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _get_node_data(node, graph: TraceGraph) -> dict:
|
|
77
|
+
"""Extract data from a node for use in formatters."""
|
|
78
|
+
# Get implements IDs via parent iteration
|
|
79
|
+
impl_ids = []
|
|
80
|
+
for parent in node.iter_parents():
|
|
81
|
+
if parent.kind == NodeKind.REQUIREMENT:
|
|
82
|
+
impl_ids.append(parent.id)
|
|
83
|
+
|
|
84
|
+
# Get code references (CODE nodes that implement this requirement)
|
|
85
|
+
code_refs = []
|
|
86
|
+
for child in node.iter_children():
|
|
87
|
+
if child.kind == NodeKind.CODE:
|
|
88
|
+
code_refs.append(child.id)
|
|
89
|
+
|
|
90
|
+
# Get test references (TEST nodes that validate this requirement)
|
|
91
|
+
test_refs = []
|
|
92
|
+
for child in node.iter_children():
|
|
93
|
+
if child.kind == NodeKind.TEST:
|
|
94
|
+
test_refs.append(child.id)
|
|
95
|
+
|
|
96
|
+
# Get assertions
|
|
97
|
+
assertions = []
|
|
98
|
+
for child in node.iter_children():
|
|
99
|
+
if child.kind == NodeKind.ASSERTION:
|
|
100
|
+
assertions.append(
|
|
101
|
+
{"label": child.get_field("label", ""), "text": child.get_label() or ""}
|
|
102
|
+
)
|
|
18
103
|
|
|
104
|
+
return {
|
|
105
|
+
"id": node.id,
|
|
106
|
+
"title": node.get_label() or "",
|
|
107
|
+
"level": node.level or "",
|
|
108
|
+
"status": node.status or "",
|
|
109
|
+
"implements": impl_ids,
|
|
110
|
+
"hash": node.hash or "",
|
|
111
|
+
"file": node.source.path if node.source else "",
|
|
112
|
+
"body": node.get_field("body", "") or "",
|
|
113
|
+
"assertions": assertions,
|
|
114
|
+
"code_refs": code_refs,
|
|
115
|
+
"test_refs": test_refs,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def format_markdown(graph: TraceGraph, preset: ReportPreset | None = None) -> Iterator[str]:
|
|
120
|
+
"""Generate markdown table. Streams one node at a time."""
|
|
121
|
+
if preset is None:
|
|
122
|
+
preset = REPORT_PRESETS[DEFAULT_PRESET]
|
|
123
|
+
|
|
124
|
+
yield "# Traceability Matrix"
|
|
125
|
+
yield ""
|
|
126
|
+
|
|
127
|
+
# Build header based on preset columns
|
|
128
|
+
column_headers = {
|
|
129
|
+
"id": "ID",
|
|
130
|
+
"title": "Title",
|
|
131
|
+
"level": "Level",
|
|
132
|
+
"status": "Status",
|
|
133
|
+
"implements": "Implements",
|
|
134
|
+
"hash": "Hash",
|
|
135
|
+
"file": "File",
|
|
136
|
+
}
|
|
137
|
+
headers = [column_headers.get(col, col.title()) for col in preset.columns]
|
|
138
|
+
yield "| " + " | ".join(headers) + " |"
|
|
139
|
+
yield "|" + "|".join(["----"] * len(headers)) + "|"
|
|
140
|
+
|
|
141
|
+
for node in graph.nodes_by_kind(NodeKind.REQUIREMENT):
|
|
142
|
+
data = _get_node_data(node, graph)
|
|
143
|
+
row_values = []
|
|
144
|
+
for col in preset.columns:
|
|
145
|
+
if col == "implements":
|
|
146
|
+
row_values.append(", ".join(data["implements"]) or "-")
|
|
147
|
+
else:
|
|
148
|
+
row_values.append(str(data.get(col, "")))
|
|
149
|
+
yield "| " + " | ".join(row_values) + " |"
|
|
150
|
+
|
|
151
|
+
# For full preset, add body and assertions after the row
|
|
152
|
+
if preset.include_body and data["body"]:
|
|
153
|
+
yield ""
|
|
154
|
+
yield "<details><summary>Body</summary>"
|
|
155
|
+
yield ""
|
|
156
|
+
yield data["body"]
|
|
157
|
+
yield ""
|
|
158
|
+
yield "</details>"
|
|
159
|
+
|
|
160
|
+
if preset.include_assertions and data["assertions"]:
|
|
161
|
+
yield ""
|
|
162
|
+
yield f"<details><summary>Assertions ({len(data['assertions'])})</summary>"
|
|
163
|
+
yield ""
|
|
164
|
+
for a in data["assertions"]:
|
|
165
|
+
yield f"- **{a['label']}**: {a['text']}"
|
|
166
|
+
yield ""
|
|
167
|
+
yield "</details>"
|
|
168
|
+
|
|
169
|
+
if preset.include_code_refs and data["code_refs"]:
|
|
170
|
+
yield ""
|
|
171
|
+
yield f"<details><summary>Code Refs ({len(data['code_refs'])})</summary>"
|
|
172
|
+
yield ""
|
|
173
|
+
for ref in data["code_refs"]:
|
|
174
|
+
yield f"- `{ref}`"
|
|
175
|
+
yield ""
|
|
176
|
+
yield "</details>"
|
|
177
|
+
|
|
178
|
+
if preset.include_test_refs and data["test_refs"]:
|
|
179
|
+
yield ""
|
|
180
|
+
yield f"<details><summary>Test Refs ({len(data['test_refs'])})</summary>"
|
|
181
|
+
yield ""
|
|
182
|
+
for ref in data["test_refs"]:
|
|
183
|
+
yield f"- `{ref}`"
|
|
184
|
+
yield ""
|
|
185
|
+
yield "</details>"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def format_csv(graph: TraceGraph, preset: ReportPreset | None = None) -> Iterator[str]:
|
|
189
|
+
"""Generate CSV. Streams one node at a time."""
|
|
190
|
+
if preset is None:
|
|
191
|
+
preset = REPORT_PRESETS[DEFAULT_PRESET]
|
|
192
|
+
|
|
193
|
+
def escape(s: str) -> str:
|
|
194
|
+
if "," in s or '"' in s or "\n" in s:
|
|
195
|
+
return '"' + s.replace('"', '""') + '"'
|
|
196
|
+
return s
|
|
197
|
+
|
|
198
|
+
# Build header based on preset columns
|
|
199
|
+
base_columns = list(preset.columns)
|
|
200
|
+
extra_columns = []
|
|
201
|
+
if preset.include_assertions:
|
|
202
|
+
extra_columns.append("assertions")
|
|
203
|
+
if preset.include_code_refs:
|
|
204
|
+
extra_columns.append("code_refs")
|
|
205
|
+
if preset.include_test_refs:
|
|
206
|
+
extra_columns.append("test_refs")
|
|
207
|
+
|
|
208
|
+
yield ",".join(base_columns + extra_columns)
|
|
209
|
+
|
|
210
|
+
for node in graph.nodes_by_kind(NodeKind.REQUIREMENT):
|
|
211
|
+
data = _get_node_data(node, graph)
|
|
212
|
+
row_values = []
|
|
213
|
+
for col in base_columns:
|
|
214
|
+
if col == "implements":
|
|
215
|
+
row_values.append(escape(";".join(data["implements"])))
|
|
216
|
+
else:
|
|
217
|
+
row_values.append(escape(str(data.get(col, ""))))
|
|
218
|
+
|
|
219
|
+
# Add extra columns for full preset
|
|
220
|
+
if preset.include_assertions:
|
|
221
|
+
assertions_str = "; ".join(f"{a['label']}: {a['text']}" for a in data["assertions"])
|
|
222
|
+
row_values.append(escape(assertions_str))
|
|
223
|
+
if preset.include_code_refs:
|
|
224
|
+
row_values.append(escape(";".join(data["code_refs"])))
|
|
225
|
+
if preset.include_test_refs:
|
|
226
|
+
row_values.append(escape(";".join(data["test_refs"])))
|
|
227
|
+
|
|
228
|
+
yield ",".join(row_values)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def format_html(graph: TraceGraph, preset: ReportPreset | None = None) -> Iterator[str]:
|
|
232
|
+
"""Generate basic HTML table. Streams one node at a time."""
|
|
233
|
+
if preset is None:
|
|
234
|
+
preset = REPORT_PRESETS[DEFAULT_PRESET]
|
|
235
|
+
|
|
236
|
+
def escape_html(s: str) -> str:
|
|
237
|
+
return s.replace("&", "&").replace("<", "<").replace(">", ">")
|
|
238
|
+
|
|
239
|
+
yield "<!DOCTYPE html>"
|
|
240
|
+
yield "<html><head><style>"
|
|
241
|
+
yield "table { border-collapse: collapse; width: 100%; }"
|
|
242
|
+
yield "th, td { border: 1px solid #ddd; padding: 8px; text-align: left; vertical-align: top; }"
|
|
243
|
+
yield "th { background-color: #4CAF50; color: white; }"
|
|
244
|
+
yield "tr:nth-child(even) { background-color: #f2f2f2; }"
|
|
245
|
+
yield ".assertions, .refs { font-size: 0.9em; color: #666; }"
|
|
246
|
+
yield ".assertion-label { font-weight: bold; }"
|
|
247
|
+
yield "details { margin: 5px 0; }"
|
|
248
|
+
yield "summary { cursor: pointer; color: #4CAF50; }"
|
|
249
|
+
yield "</style></head><body>"
|
|
250
|
+
yield "<h1>Traceability Matrix</h1>"
|
|
251
|
+
|
|
252
|
+
# Build header based on preset columns
|
|
253
|
+
column_headers = {
|
|
254
|
+
"id": "ID",
|
|
255
|
+
"title": "Title",
|
|
256
|
+
"level": "Level",
|
|
257
|
+
"status": "Status",
|
|
258
|
+
"implements": "Implements",
|
|
259
|
+
"hash": "Hash",
|
|
260
|
+
"file": "File",
|
|
261
|
+
}
|
|
262
|
+
headers = [column_headers.get(col, col.title()) for col in preset.columns]
|
|
263
|
+
|
|
264
|
+
# Add extra columns for full preset
|
|
265
|
+
if preset.include_assertions:
|
|
266
|
+
headers.append("Assertions")
|
|
267
|
+
if preset.include_code_refs:
|
|
268
|
+
headers.append("Code Refs")
|
|
269
|
+
if preset.include_test_refs:
|
|
270
|
+
headers.append("Test Refs")
|
|
271
|
+
|
|
272
|
+
yield "<table>"
|
|
273
|
+
yield "<tr>" + "".join(f"<th>{h}</th>" for h in headers) + "</tr>"
|
|
274
|
+
|
|
275
|
+
for node in graph.nodes_by_kind(NodeKind.REQUIREMENT):
|
|
276
|
+
data = _get_node_data(node, graph)
|
|
277
|
+
cells = []
|
|
278
|
+
for col in preset.columns:
|
|
279
|
+
if col == "implements":
|
|
280
|
+
impl_str = ", ".join(data["implements"]) or "-"
|
|
281
|
+
cells.append(f"<td>{escape_html(impl_str)}</td>")
|
|
282
|
+
elif col == "title":
|
|
283
|
+
cells.append(f"<td>{escape_html(data['title'])}</td>")
|
|
284
|
+
else:
|
|
285
|
+
cells.append(f"<td>{escape_html(str(data.get(col, '')))}</td>")
|
|
286
|
+
|
|
287
|
+
# Add extra columns for full preset
|
|
288
|
+
if preset.include_assertions:
|
|
289
|
+
if data["assertions"]:
|
|
290
|
+
assertion_html = "<br>".join(
|
|
291
|
+
f"<span class='assertion-label'>{a['label']}:</span> {escape_html(a['text'])}"
|
|
292
|
+
for a in data["assertions"]
|
|
293
|
+
)
|
|
294
|
+
cells.append(f"<td class='assertions'>{assertion_html}</td>")
|
|
295
|
+
else:
|
|
296
|
+
cells.append("<td>-</td>")
|
|
19
297
|
|
|
20
|
-
|
|
21
|
-
|
|
298
|
+
if preset.include_code_refs:
|
|
299
|
+
if data["code_refs"]:
|
|
300
|
+
refs_html = "<br>".join(f"<code>{escape_html(r)}</code>" for r in data["code_refs"])
|
|
301
|
+
cells.append(f"<td class='refs'>{refs_html}</td>")
|
|
302
|
+
else:
|
|
303
|
+
cells.append("<td>-</td>")
|
|
22
304
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
or getattr(args, "edit_mode", False)
|
|
30
|
-
or getattr(args, "review_mode", False)
|
|
31
|
-
or getattr(args, "server", False)
|
|
32
|
-
)
|
|
305
|
+
if preset.include_test_refs:
|
|
306
|
+
if data["test_refs"]:
|
|
307
|
+
refs_html = "<br>".join(f"<code>{escape_html(r)}</code>" for r in data["test_refs"])
|
|
308
|
+
cells.append(f"<td class='refs'>{refs_html}</td>")
|
|
309
|
+
else:
|
|
310
|
+
cells.append("<td>-</td>")
|
|
33
311
|
|
|
34
|
-
|
|
35
|
-
return run_trace_view(args)
|
|
312
|
+
yield f"<tr>{''.join(cells)}</tr>"
|
|
36
313
|
|
|
37
|
-
|
|
38
|
-
return run_basic_trace(args)
|
|
314
|
+
yield "</table></body></html>"
|
|
39
315
|
|
|
40
316
|
|
|
41
|
-
def
|
|
42
|
-
"""
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if config_path and config_path.exists():
|
|
46
|
-
config = load_config(config_path)
|
|
47
|
-
else:
|
|
48
|
-
config = DEFAULT_CONFIG
|
|
317
|
+
def format_json(graph: TraceGraph, preset: ReportPreset | None = None) -> Iterator[str]:
|
|
318
|
+
"""Generate JSON array. Streams one node at a time."""
|
|
319
|
+
if preset is None:
|
|
320
|
+
preset = REPORT_PRESETS[DEFAULT_PRESET]
|
|
49
321
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
322
|
+
yield "["
|
|
323
|
+
first = True
|
|
324
|
+
for node in graph.nodes_by_kind(NodeKind.REQUIREMENT):
|
|
325
|
+
if not first:
|
|
326
|
+
yield ","
|
|
327
|
+
first = False
|
|
55
328
|
|
|
56
|
-
|
|
57
|
-
pattern_config = PatternConfig.from_dict(config.get("patterns", {}))
|
|
58
|
-
spec_config = config.get("spec", {})
|
|
59
|
-
no_reference_values = spec_config.get("no_reference_values")
|
|
60
|
-
skip_files = spec_config.get("skip_files", [])
|
|
61
|
-
parser = RequirementParser(pattern_config, no_reference_values=no_reference_values)
|
|
62
|
-
requirements = parser.parse_directories(spec_dirs, skip_files=skip_files)
|
|
329
|
+
data = _get_node_data(node, graph)
|
|
63
330
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if output_format in ["markdown", "both"]:
|
|
73
|
-
md_output = generate_markdown_matrix(requirements)
|
|
74
|
-
if args.output:
|
|
75
|
-
if output_format == "markdown":
|
|
76
|
-
output_path = args.output
|
|
331
|
+
# Build node dict based on preset columns
|
|
332
|
+
node_dict: dict = {}
|
|
333
|
+
for col in preset.columns:
|
|
334
|
+
if col == "file":
|
|
335
|
+
node_dict["source"] = {
|
|
336
|
+
"path": node.source.path if node.source else None,
|
|
337
|
+
"line": node.source.line if node.source else None,
|
|
338
|
+
}
|
|
77
339
|
else:
|
|
78
|
-
|
|
79
|
-
else:
|
|
80
|
-
output_path = Path("traceability.md")
|
|
81
|
-
output_path.write_text(md_output)
|
|
82
|
-
print(f"Generated: {output_path}")
|
|
340
|
+
node_dict[col] = data.get(col)
|
|
83
341
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
output_path.write_text(html_output)
|
|
94
|
-
print(f"Generated: {output_path}")
|
|
342
|
+
# Add extra fields for full preset
|
|
343
|
+
if preset.include_body:
|
|
344
|
+
node_dict["body"] = data["body"]
|
|
345
|
+
if preset.include_assertions:
|
|
346
|
+
node_dict["assertions"] = data["assertions"]
|
|
347
|
+
if preset.include_code_refs:
|
|
348
|
+
node_dict["code_refs"] = data["code_refs"]
|
|
349
|
+
if preset.include_test_refs:
|
|
350
|
+
node_dict["test_refs"] = data["test_refs"]
|
|
95
351
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
output_path = args.output or Path("traceability.csv")
|
|
99
|
-
output_path.write_text(csv_output)
|
|
100
|
-
print(f"Generated: {output_path}")
|
|
352
|
+
yield json.dumps(node_dict, indent=2)
|
|
353
|
+
yield "]"
|
|
101
354
|
|
|
102
|
-
return 0
|
|
103
355
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
"""Run enhanced trace-view features.
|
|
107
|
-
|
|
108
|
-
REQ-int-d00003-A: Trace-view features SHALL be accessible via elspais trace command.
|
|
109
|
-
REQ-int-d00003-B: New flags SHALL include: --view, --embed-content, --edit-mode,
|
|
110
|
-
--review-mode, --server.
|
|
111
|
-
"""
|
|
112
|
-
# Check if starting review server
|
|
113
|
-
if args.server:
|
|
114
|
-
return run_review_server(args)
|
|
115
|
-
|
|
116
|
-
# Import trace_view (requires jinja2)
|
|
356
|
+
def format_view(graph: TraceGraph, embed_content: bool = False, base_path: str = "") -> str:
|
|
357
|
+
"""Generate interactive HTML via HTMLGenerator."""
|
|
117
358
|
try:
|
|
118
|
-
from elspais.
|
|
119
|
-
except ImportError as
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
# Load configuration
|
|
127
|
-
config_path = args.config or find_config_file(Path.cwd())
|
|
128
|
-
if config_path and config_path.exists():
|
|
129
|
-
config = load_config(config_path)
|
|
130
|
-
else:
|
|
131
|
-
config = DEFAULT_CONFIG
|
|
132
|
-
|
|
133
|
-
# Determine spec directory
|
|
134
|
-
spec_dir = args.spec_dir
|
|
135
|
-
if not spec_dir:
|
|
136
|
-
spec_dirs = get_spec_directories(None, config)
|
|
137
|
-
spec_dir = spec_dirs[0] if spec_dirs else Path.cwd() / "spec"
|
|
138
|
-
|
|
139
|
-
repo_root = spec_dir.parent if spec_dir.name == "spec" else spec_dir.parent.parent
|
|
140
|
-
|
|
141
|
-
# Get implementation directories from config
|
|
142
|
-
impl_dirs = []
|
|
143
|
-
dirs_config = config.get("directories", {})
|
|
144
|
-
code_dirs = dirs_config.get("code", [])
|
|
145
|
-
for code_dir in code_dirs:
|
|
146
|
-
impl_path = repo_root / code_dir
|
|
147
|
-
if impl_path.exists():
|
|
148
|
-
impl_dirs.append(impl_path)
|
|
149
|
-
|
|
150
|
-
# Create generator
|
|
151
|
-
generator = TraceViewGenerator(
|
|
152
|
-
spec_dir=spec_dir,
|
|
153
|
-
impl_dirs=impl_dirs,
|
|
154
|
-
sponsor=getattr(args, "sponsor", None),
|
|
155
|
-
mode=getattr(args, "mode", "core"),
|
|
156
|
-
repo_root=repo_root,
|
|
157
|
-
config=config,
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
# Determine output format
|
|
161
|
-
# --view implies HTML
|
|
162
|
-
output_format = "html" if args.view else args.format
|
|
163
|
-
if output_format == "both":
|
|
164
|
-
output_format = "html"
|
|
165
|
-
|
|
166
|
-
# Determine output file
|
|
167
|
-
output_file = args.output
|
|
168
|
-
if output_file is None:
|
|
169
|
-
if output_format == "html":
|
|
170
|
-
output_file = Path("traceability_matrix.html")
|
|
171
|
-
elif output_format == "csv":
|
|
172
|
-
output_file = Path("traceability_matrix.csv")
|
|
173
|
-
else:
|
|
174
|
-
output_file = Path("traceability_matrix.md")
|
|
175
|
-
|
|
176
|
-
# Generate
|
|
177
|
-
quiet = getattr(args, "quiet", False)
|
|
178
|
-
generator.generate(
|
|
179
|
-
format=output_format,
|
|
180
|
-
output_file=output_file,
|
|
181
|
-
embed_content=getattr(args, "embed_content", False),
|
|
182
|
-
edit_mode=getattr(args, "edit_mode", False),
|
|
183
|
-
review_mode=getattr(args, "review_mode", False),
|
|
184
|
-
quiet=quiet,
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
return 0
|
|
359
|
+
from elspais.html import HTMLGenerator
|
|
360
|
+
except ImportError as err:
|
|
361
|
+
raise ImportError(
|
|
362
|
+
"HTMLGenerator requires the trace-view extra. "
|
|
363
|
+
"Install with: pip install elspais[trace-view]"
|
|
364
|
+
) from err
|
|
365
|
+
generator = HTMLGenerator(graph, base_path=base_path)
|
|
366
|
+
return generator.generate(embed_content=embed_content)
|
|
188
367
|
|
|
189
368
|
|
|
190
|
-
def
|
|
191
|
-
"""
|
|
369
|
+
def run(args: argparse.Namespace) -> int:
|
|
370
|
+
"""Run the trace command.
|
|
192
371
|
|
|
193
|
-
|
|
194
|
-
elspais[trace-review] extra.
|
|
372
|
+
Uses graph factory to build TraceGraph, then streams output in requested format.
|
|
195
373
|
"""
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
repo_root = spec_dir.parent if spec_dir.name == "spec" else spec_dir.parent.parent
|
|
374
|
+
# Handle not-implemented features
|
|
375
|
+
for flag in ("edit_mode", "review_mode", "server"):
|
|
376
|
+
if getattr(args, flag, False):
|
|
377
|
+
print(f"Error: --{flag.replace('_', '-')} not yet implemented", file=sys.stderr)
|
|
378
|
+
return 1
|
|
379
|
+
|
|
380
|
+
# Parse --report preset
|
|
381
|
+
report_name = getattr(args, "report", None)
|
|
382
|
+
if report_name:
|
|
383
|
+
if report_name not in REPORT_PRESETS:
|
|
384
|
+
available = ", ".join(REPORT_PRESETS.keys())
|
|
385
|
+
print(f"Error: Unknown report preset '{report_name}'", file=sys.stderr)
|
|
386
|
+
print(f"Available presets: {available}", file=sys.stderr)
|
|
387
|
+
return 1
|
|
388
|
+
preset = REPORT_PRESETS[report_name]
|
|
212
389
|
else:
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
port = getattr(args, "port", 8080)
|
|
390
|
+
preset = REPORT_PRESETS[DEFAULT_PRESET]
|
|
216
391
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
======================================
|
|
220
|
-
elspais Review Server
|
|
221
|
-
======================================
|
|
392
|
+
# Build graph using factory
|
|
393
|
+
from elspais.graph.factory import build_graph
|
|
222
394
|
|
|
223
|
-
|
|
224
|
-
|
|
395
|
+
spec_dir = getattr(args, "spec_dir", None)
|
|
396
|
+
config_path = getattr(args, "config", None)
|
|
225
397
|
|
|
226
|
-
|
|
227
|
-
|
|
398
|
+
graph = build_graph(
|
|
399
|
+
spec_dirs=[spec_dir] if spec_dir else None,
|
|
400
|
+
config_path=config_path,
|
|
228
401
|
)
|
|
229
402
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
403
|
+
# Handle --view mode (interactive HTML)
|
|
404
|
+
if getattr(args, "view", False):
|
|
405
|
+
try:
|
|
406
|
+
# Get absolute base path for VS Code links
|
|
407
|
+
base_path = str(Path.cwd().resolve())
|
|
408
|
+
content = format_view(graph, getattr(args, "embed_content", False), base_path=base_path)
|
|
409
|
+
except ImportError as e:
|
|
410
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
411
|
+
return 1
|
|
235
412
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
def generate_markdown_matrix(requirements: Dict[str, Requirement]) -> str:
|
|
240
|
-
"""Generate Markdown traceability matrix."""
|
|
241
|
-
lines = ["# Traceability Matrix", "", "## Requirements Hierarchy", ""]
|
|
242
|
-
|
|
243
|
-
# Group by type
|
|
244
|
-
prd_reqs = {k: v for k, v in requirements.items() if v.level.upper() in ["PRD", "PRODUCT"]}
|
|
245
|
-
ops_reqs = {k: v for k, v in requirements.items() if v.level.upper() in ["OPS", "OPERATIONS"]}
|
|
246
|
-
dev_reqs = {k: v for k, v in requirements.items() if v.level.upper() in ["DEV", "DEVELOPMENT"]}
|
|
247
|
-
|
|
248
|
-
# PRD table
|
|
249
|
-
if prd_reqs:
|
|
250
|
-
lines.extend(["### Product Requirements", ""])
|
|
251
|
-
lines.append("| ID | Title | Status | Implemented By |")
|
|
252
|
-
lines.append("|---|---|---|---|")
|
|
253
|
-
for req_id, req in sorted(prd_reqs.items()):
|
|
254
|
-
impl_by = find_implementers(req_id, requirements)
|
|
255
|
-
impl_str = ", ".join(impl_by) if impl_by else "-"
|
|
256
|
-
lines.append(f"| {req_id} | {req.title} | {req.status} | {impl_str} |")
|
|
257
|
-
lines.append("")
|
|
258
|
-
|
|
259
|
-
# OPS table
|
|
260
|
-
if ops_reqs:
|
|
261
|
-
lines.extend(["### Operations Requirements", ""])
|
|
262
|
-
lines.append("| ID | Title | Implements | Status |")
|
|
263
|
-
lines.append("|---|---|---|---|")
|
|
264
|
-
for req_id, req in sorted(ops_reqs.items()):
|
|
265
|
-
impl_str = ", ".join(req.implements) if req.implements else "-"
|
|
266
|
-
lines.append(f"| {req_id} | {req.title} | {impl_str} | {req.status} |")
|
|
267
|
-
lines.append("")
|
|
268
|
-
|
|
269
|
-
# DEV table
|
|
270
|
-
if dev_reqs:
|
|
271
|
-
lines.extend(["### Development Requirements", ""])
|
|
272
|
-
lines.append("| ID | Title | Implements | Status |")
|
|
273
|
-
lines.append("|---|---|---|---|")
|
|
274
|
-
for req_id, req in sorted(dev_reqs.items()):
|
|
275
|
-
impl_str = ", ".join(req.implements) if req.implements else "-"
|
|
276
|
-
lines.append(f"| {req_id} | {req.title} | {impl_str} | {req.status} |")
|
|
277
|
-
lines.append("")
|
|
278
|
-
|
|
279
|
-
lines.extend(["---", "*Generated by elspais*"])
|
|
280
|
-
return "\n".join(lines)
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
def generate_html_matrix(requirements: Dict[str, Requirement]) -> str:
|
|
284
|
-
"""Generate HTML traceability matrix."""
|
|
285
|
-
html = """<!DOCTYPE html>
|
|
286
|
-
<html lang="en">
|
|
287
|
-
<head>
|
|
288
|
-
<meta charset="UTF-8">
|
|
289
|
-
<title>Traceability Matrix</title>
|
|
290
|
-
<style>
|
|
291
|
-
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; margin: 2rem; }
|
|
292
|
-
h1 { color: #333; }
|
|
293
|
-
table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
|
|
294
|
-
th, td { border: 1px solid #ddd; padding: 0.5rem; text-align: left; }
|
|
295
|
-
th { background: #f5f5f5; }
|
|
296
|
-
tr:hover { background: #f9f9f9; }
|
|
297
|
-
.status-active { color: green; }
|
|
298
|
-
.status-draft { color: orange; }
|
|
299
|
-
.status-deprecated { color: red; }
|
|
300
|
-
</style>
|
|
301
|
-
</head>
|
|
302
|
-
<body>
|
|
303
|
-
<h1>Traceability Matrix</h1>
|
|
304
|
-
"""
|
|
305
|
-
|
|
306
|
-
# Group by type
|
|
307
|
-
prd_reqs = {k: v for k, v in requirements.items() if v.level.upper() in ["PRD", "PRODUCT"]}
|
|
308
|
-
ops_reqs = {k: v for k, v in requirements.items() if v.level.upper() in ["OPS", "OPERATIONS"]}
|
|
309
|
-
dev_reqs = {k: v for k, v in requirements.items() if v.level.upper() in ["DEV", "DEVELOPMENT"]}
|
|
310
|
-
|
|
311
|
-
for title, reqs in [
|
|
312
|
-
("Product Requirements", prd_reqs),
|
|
313
|
-
("Operations Requirements", ops_reqs),
|
|
314
|
-
("Development Requirements", dev_reqs),
|
|
315
|
-
]:
|
|
316
|
-
if not reqs:
|
|
317
|
-
continue
|
|
318
|
-
|
|
319
|
-
html += f" <h2>{title}</h2>\n"
|
|
320
|
-
html += " <table>\n"
|
|
321
|
-
html += " <tr><th>ID</th><th>Title</th><th>Implements</th><th>Status</th></tr>\n"
|
|
322
|
-
|
|
323
|
-
for req_id, req in sorted(reqs.items()):
|
|
324
|
-
impl_str = ", ".join(req.implements) if req.implements else "-"
|
|
325
|
-
status_class = f"status-{req.status.lower()}"
|
|
326
|
-
subdir_attr = f'data-subdir="{req.subdir}"'
|
|
327
|
-
html += (
|
|
328
|
-
f" <tr {subdir_attr}><td>{req_id}</td><td>{req.title}</td>"
|
|
329
|
-
f'<td>{impl_str}</td><td class="{status_class}">{req.status}</td></tr>\n'
|
|
330
|
-
)
|
|
331
|
-
|
|
332
|
-
html += " </table>\n"
|
|
333
|
-
|
|
334
|
-
html += """ <hr>
|
|
335
|
-
<p><em>Generated by elspais</em></p>
|
|
336
|
-
</body>
|
|
337
|
-
</html>"""
|
|
338
|
-
return html
|
|
413
|
+
output_path = args.output or Path("traceability_view.html")
|
|
414
|
+
Path(output_path).write_text(content)
|
|
339
415
|
|
|
416
|
+
if not getattr(args, "quiet", False):
|
|
417
|
+
print(f"Generated: {output_path}", file=sys.stderr)
|
|
418
|
+
return 0
|
|
340
419
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
for req_id, req in sorted(requirements.items()):
|
|
346
|
-
impl_str = ";".join(req.implements) if req.implements else ""
|
|
347
|
-
title = req.title.replace('"', '""')
|
|
348
|
-
lines.append(
|
|
349
|
-
f'"{req_id}","{title}","{req.level}","{req.status}","{impl_str}","{req.subdir}"'
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
return "\n".join(lines)
|
|
420
|
+
# Handle --graph-json mode
|
|
421
|
+
if getattr(args, "graph_json", False):
|
|
422
|
+
from elspais.graph.serialize import serialize_graph
|
|
353
423
|
|
|
424
|
+
output = json.dumps(serialize_graph(graph), indent=2)
|
|
425
|
+
if args.output:
|
|
426
|
+
Path(args.output).write_text(output)
|
|
427
|
+
if not getattr(args, "quiet", False):
|
|
428
|
+
print(f"Generated: {args.output}", file=sys.stderr)
|
|
429
|
+
else:
|
|
430
|
+
print(output)
|
|
431
|
+
return 0
|
|
432
|
+
|
|
433
|
+
# Select formatter based on format
|
|
434
|
+
fmt = getattr(args, "format", "markdown")
|
|
435
|
+
|
|
436
|
+
# Handle legacy "both" format
|
|
437
|
+
if fmt == "both":
|
|
438
|
+
# Generate both markdown and csv
|
|
439
|
+
output_base = args.output or Path("traceability")
|
|
440
|
+
if isinstance(output_base, str):
|
|
441
|
+
output_base = Path(output_base)
|
|
442
|
+
|
|
443
|
+
md_path = output_base.with_suffix(".md")
|
|
444
|
+
csv_path = output_base.with_suffix(".csv")
|
|
445
|
+
|
|
446
|
+
with open(md_path, "w") as f:
|
|
447
|
+
for line in format_markdown(graph, preset):
|
|
448
|
+
f.write(line + "\n")
|
|
449
|
+
|
|
450
|
+
with open(csv_path, "w") as f:
|
|
451
|
+
for line in format_csv(graph, preset):
|
|
452
|
+
f.write(line + "\n")
|
|
453
|
+
|
|
454
|
+
if not getattr(args, "quiet", False):
|
|
455
|
+
print(f"Generated: {md_path}", file=sys.stderr)
|
|
456
|
+
print(f"Generated: {csv_path}", file=sys.stderr)
|
|
457
|
+
return 0
|
|
458
|
+
|
|
459
|
+
# Single format output
|
|
460
|
+
formatters = {
|
|
461
|
+
"markdown": format_markdown,
|
|
462
|
+
"csv": format_csv,
|
|
463
|
+
"html": format_html,
|
|
464
|
+
"json": format_json,
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if fmt not in formatters:
|
|
468
|
+
print(f"Error: Unknown format '{fmt}'", file=sys.stderr)
|
|
469
|
+
return 1
|
|
354
470
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
implementers = []
|
|
358
|
-
short_id = req_id.split("-")[-1] if "-" in req_id else req_id
|
|
471
|
+
line_generator = formatters[fmt](graph, preset)
|
|
472
|
+
output_path = args.output
|
|
359
473
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
474
|
+
# Stream output line by line
|
|
475
|
+
if output_path:
|
|
476
|
+
with open(output_path, "w") as f:
|
|
477
|
+
for line in line_generator:
|
|
478
|
+
f.write(line + "\n")
|
|
479
|
+
if not getattr(args, "quiet", False):
|
|
480
|
+
print(f"Generated: {output_path}", file=sys.stderr)
|
|
481
|
+
else:
|
|
482
|
+
for line in line_generator:
|
|
483
|
+
print(line)
|
|
365
484
|
|
|
366
|
-
return
|
|
485
|
+
return 0
|