specmason 0.1.0__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.
- specmason/__init__.py +21 -0
- specmason/__main__.py +8 -0
- specmason/_version.py +24 -0
- specmason/cli.py +468 -0
- specmason/config.py +463 -0
- specmason/corpus.py +321 -0
- specmason/coverage.py +466 -0
- specmason/create.py +162 -0
- specmason/errors.py +175 -0
- specmason/evidence.py +187 -0
- specmason/external_identity.py +326 -0
- specmason/fixtures.py +221 -0
- specmason/gherkin/__init__.py +61 -0
- specmason/gherkin/lint.py +147 -0
- specmason/gherkin/model.py +336 -0
- specmason/gherkin/official.py +291 -0
- specmason/gherkin/parser.py +293 -0
- specmason/gherkin/step_vocab.py +139 -0
- specmason/gherkin/writer.py +233 -0
- specmason/ids.py +123 -0
- specmason/init.py +155 -0
- specmason/launcher.py +19 -0
- specmason/mappings.py +359 -0
- specmason/mappings.py.bak +350 -0
- specmason/py.typed +0 -0
- specmason/pytest_discovery.py +147 -0
- specmason/requirements.py +217 -0
- specmason/review.py +178 -0
- specmason-0.1.0.dist-info/METADATA +135 -0
- specmason-0.1.0.dist-info/RECORD +36 -0
- specmason-0.1.0.dist-info/WHEEL +5 -0
- specmason-0.1.0.dist-info/entry_points.txt +2 -0
- specmason-0.1.0.dist-info/licenses/LICENSE +201 -0
- specmason-0.1.0.dist-info/scm_file_list.json +65 -0
- specmason-0.1.0.dist-info/scm_version.json +8 -0
- specmason-0.1.0.dist-info/top_level.txt +1 -0
specmason/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""SpecMason: Ledgerwerk behavior/specification builder, checker, and reconciler.
|
|
2
|
+
|
|
3
|
+
SpecMason is the builder, checker, and reconciliation tool for behavior
|
|
4
|
+
artifacts. It connects accepted requirements (owned by ReqLedger) to concrete
|
|
5
|
+
behavior examples (Gherkin), pytest tests, mapping inventories, coverage
|
|
6
|
+
reports, and execution evidence.
|
|
7
|
+
|
|
8
|
+
SpecMason depends on :mod:`ledgercore` for generic storage/ledger primitives
|
|
9
|
+
(atomic IO, deterministic JSON, config and path resolution, prefixed IDs, ref
|
|
10
|
+
parsing, hashing, and timestamps). It never imports ReqLedger at runtime; it
|
|
11
|
+
only reads ReqLedger export JSON.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from specmason._version import __version__
|
|
18
|
+
except Exception: # pragma: no cover - fallback when _version.py absent
|
|
19
|
+
__version__ = "0.0.0+unknown"
|
|
20
|
+
|
|
21
|
+
__all__ = ["__version__"]
|
specmason/__main__.py
ADDED
specmason/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.1.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = 'gee7262de2'
|
specmason/cli.py
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
"""SpecMason Typer CLI.
|
|
2
|
+
|
|
3
|
+
All commands support ``--json`` for machine-readable output. Global options
|
|
4
|
+
(``--version``, ``--config``) are resolved at the callback level.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import replace
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
|
|
14
|
+
from specmason import __version__
|
|
15
|
+
from specmason.config import load_config
|
|
16
|
+
from specmason.coverage import build_coverage, render_markdown
|
|
17
|
+
from specmason.create import generate_features
|
|
18
|
+
from specmason.errors import Findings
|
|
19
|
+
from specmason.evidence import check_evidence_against_mappings, parse_junit_xml
|
|
20
|
+
from specmason.gherkin.model import Feature
|
|
21
|
+
from specmason.init import init_workspace
|
|
22
|
+
from specmason.mappings import (
|
|
23
|
+
MappingInventory,
|
|
24
|
+
build_inventory,
|
|
25
|
+
load_intentional_unmapped_policy,
|
|
26
|
+
)
|
|
27
|
+
from specmason.pytest_discovery import discover_tests
|
|
28
|
+
from specmason.requirements import RequirementsIndex, load_manifest
|
|
29
|
+
from specmason.review import run_review
|
|
30
|
+
|
|
31
|
+
app = typer.Typer(
|
|
32
|
+
name="specmason",
|
|
33
|
+
help=(
|
|
34
|
+
"SpecMason: build, check, and reconcile behavior/specification artifacts, "
|
|
35
|
+
"pytest mappings, reverse coverage, and execution evidence."
|
|
36
|
+
),
|
|
37
|
+
no_args_is_help=True,
|
|
38
|
+
add_completion=False,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# Callback
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _version_callback(value: bool) -> None:
|
|
48
|
+
if value:
|
|
49
|
+
typer.echo(__version__)
|
|
50
|
+
raise typer.Exit
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.callback()
|
|
54
|
+
def main_callback(
|
|
55
|
+
version: bool = typer.Option(
|
|
56
|
+
None,
|
|
57
|
+
"--version",
|
|
58
|
+
callback=_version_callback,
|
|
59
|
+
is_eager=True,
|
|
60
|
+
help="Show the SpecMason version and exit.",
|
|
61
|
+
),
|
|
62
|
+
) -> None:
|
|
63
|
+
"""SpecMason command group."""
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Helpers
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _to_json(payload: dict[str, object]) -> str:
|
|
73
|
+
from ledgercore.jsonio import dumps_json
|
|
74
|
+
|
|
75
|
+
return dumps_json(payload, indent=2, sort_keys=True)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _resolve_config(
|
|
79
|
+
*,
|
|
80
|
+
config: Path | None,
|
|
81
|
+
requirements: str | None,
|
|
82
|
+
) -> object:
|
|
83
|
+
cfg = load_config(config=config, requirements_override=requirements)
|
|
84
|
+
return cfg
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _load_index(cfg: object) -> RequirementsIndex | None:
|
|
88
|
+
from specmason.config import SpecMasonConfig
|
|
89
|
+
|
|
90
|
+
c: SpecMasonConfig = cfg # type: ignore[assignment]
|
|
91
|
+
if c.is_integrated:
|
|
92
|
+
return load_manifest(c.requirements_manifest)
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _load_inventory(cfg: object) -> MappingInventory:
|
|
97
|
+
from specmason.config import SpecMasonConfig
|
|
98
|
+
|
|
99
|
+
c: SpecMasonConfig = cfg # type: ignore[assignment]
|
|
100
|
+
waivers, _ = load_intentional_unmapped_policy(c.pytest_intentional_unmapped_policy)
|
|
101
|
+
discovered = discover_tests(c.tests_dir, root=c.workspace_root)
|
|
102
|
+
return build_inventory(discovered, central_waivers=waivers)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _load_features(cfg: object) -> tuple[list[Feature], Findings]:
|
|
106
|
+
from specmason.config import SpecMasonConfig
|
|
107
|
+
from specmason.corpus import parse_corpus
|
|
108
|
+
|
|
109
|
+
c: SpecMasonConfig = cfg # type: ignore[assignment]
|
|
110
|
+
if not c.features_dir.is_dir():
|
|
111
|
+
return [], Findings()
|
|
112
|
+
features, _, raw_findings = parse_corpus(
|
|
113
|
+
c.features_dir, official_parser=c.gherkin_official_parser
|
|
114
|
+
)
|
|
115
|
+
return features, Findings.of(*raw_findings)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _exit(result: dict[str, object], *, json_output: bool, errors: bool) -> None:
|
|
119
|
+
if json_output:
|
|
120
|
+
typer.echo(_to_json(result))
|
|
121
|
+
if errors:
|
|
122
|
+
raise typer.Exit(code=1)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
# Commands
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@app.command("init")
|
|
131
|
+
def init_command(
|
|
132
|
+
force: bool = typer.Option(False, "--force", help="Overwrite existing files."),
|
|
133
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON to stdout."),
|
|
134
|
+
config: Path | None = typer.Option(
|
|
135
|
+
None, "--config", help="Workspace root config path."
|
|
136
|
+
),
|
|
137
|
+
) -> None:
|
|
138
|
+
"""Initialize the SpecMason workspace layout."""
|
|
139
|
+
root = config.parent.resolve() if config else Path.cwd()
|
|
140
|
+
result = init_workspace(root, force=force)
|
|
141
|
+
if json_output:
|
|
142
|
+
typer.echo(_to_json(result.to_dict()))
|
|
143
|
+
return
|
|
144
|
+
typer.echo(f"Initialized SpecMason workspace at {result.root}")
|
|
145
|
+
if result.created:
|
|
146
|
+
typer.echo(f"created: {', '.join(result.created)}")
|
|
147
|
+
if result.existing:
|
|
148
|
+
typer.echo(f"existing: {', '.join(result.existing)}")
|
|
149
|
+
if result.skipped:
|
|
150
|
+
typer.echo(f"skipped: {', '.join(result.skipped)}")
|
|
151
|
+
if result.overwritten:
|
|
152
|
+
typer.echo(f"overwritten: {', '.join(result.overwritten)}")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@app.command("check")
|
|
156
|
+
def check_command(
|
|
157
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON to stdout."),
|
|
158
|
+
config: Path | None = typer.Option(None, "--config", help="Config file path."),
|
|
159
|
+
requirements: str | None = typer.Option(
|
|
160
|
+
None, "--requirements", help="ReqLedger manifest path."
|
|
161
|
+
),
|
|
162
|
+
) -> None:
|
|
163
|
+
"""Validate config, features, mappings, and waivers."""
|
|
164
|
+
from specmason.gherkin.lint import lint_feature_with_authority
|
|
165
|
+
|
|
166
|
+
cfg = _resolve_config(config=config, requirements=requirements)
|
|
167
|
+
|
|
168
|
+
from specmason.config import SpecMasonConfig
|
|
169
|
+
|
|
170
|
+
c: SpecMasonConfig = cfg # type: ignore[assignment]
|
|
171
|
+
index = _load_index(c)
|
|
172
|
+
features, findings = _load_features(c)
|
|
173
|
+
|
|
174
|
+
for feature in features:
|
|
175
|
+
findings = findings.extend(
|
|
176
|
+
Findings.of(
|
|
177
|
+
*lint_feature_with_authority(
|
|
178
|
+
feature,
|
|
179
|
+
known_requirement_ids=index.requirement_ids if index else None,
|
|
180
|
+
known_criterion_ids=index.criterion_ids if index else None,
|
|
181
|
+
require_req_tag=c.gherkin_require_req_tag,
|
|
182
|
+
require_ac_tag=c.gherkin_require_ac_tag,
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
waivers, policy_findings = load_intentional_unmapped_policy(
|
|
188
|
+
c.pytest_intentional_unmapped_policy
|
|
189
|
+
)
|
|
190
|
+
combined = findings.extend(Findings.of(*policy_findings))
|
|
191
|
+
errors = combined.has_errors
|
|
192
|
+
|
|
193
|
+
result = {"findings": combined.to_list(), "has_errors": errors}
|
|
194
|
+
if json_output:
|
|
195
|
+
typer.echo(_to_json(result))
|
|
196
|
+
else:
|
|
197
|
+
for line in combined.render_lines():
|
|
198
|
+
typer.echo(line)
|
|
199
|
+
typer.echo(f"{len(combined)} findings ({len(combined.errors)} errors)")
|
|
200
|
+
if errors:
|
|
201
|
+
raise typer.Exit(code=1)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@app.command("create-gherkin")
|
|
205
|
+
def create_gherkin_command(
|
|
206
|
+
from_manifest: str = typer.Option(
|
|
207
|
+
..., "--from", help="ReqLedger manifest JSON path."
|
|
208
|
+
),
|
|
209
|
+
area: str | None = typer.Option(
|
|
210
|
+
None, "--area", help="Filter requirements by area tag/kind."
|
|
211
|
+
),
|
|
212
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Don't write files."),
|
|
213
|
+
force: bool = typer.Option(False, "--force", help="Overwrite existing files."),
|
|
214
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON to stdout."),
|
|
215
|
+
config: Path | None = typer.Option(None, "--config", help="Config file path."),
|
|
216
|
+
) -> None:
|
|
217
|
+
"""Generate draft Gherkin feature files from accepted behavior criteria."""
|
|
218
|
+
cfg = _resolve_config(config=config, requirements=from_manifest)
|
|
219
|
+
from specmason.config import SpecMasonConfig
|
|
220
|
+
|
|
221
|
+
c: SpecMasonConfig = cfg # type: ignore[assignment]
|
|
222
|
+
index = load_manifest(from_manifest)
|
|
223
|
+
result = generate_features(
|
|
224
|
+
index,
|
|
225
|
+
c.features_dir,
|
|
226
|
+
area=area,
|
|
227
|
+
force=force,
|
|
228
|
+
dry_run=dry_run,
|
|
229
|
+
)
|
|
230
|
+
if json_output:
|
|
231
|
+
typer.echo(_to_json(result.to_dict()))
|
|
232
|
+
return
|
|
233
|
+
for f in result.features:
|
|
234
|
+
typer.echo(f"{f.status}: {f.path}")
|
|
235
|
+
typer.echo(f"{len(result.features)} features")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@app.command("discover-pytest")
|
|
239
|
+
def discover_pytest_command(
|
|
240
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON to stdout."),
|
|
241
|
+
config: Path | None = typer.Option(None, "--config", help="Config file path."),
|
|
242
|
+
) -> None:
|
|
243
|
+
"""Discover pytest tests without importing test modules."""
|
|
244
|
+
cfg = _resolve_config(config=config, requirements=None)
|
|
245
|
+
from specmason.config import SpecMasonConfig
|
|
246
|
+
|
|
247
|
+
c: SpecMasonConfig = cfg # type: ignore[assignment]
|
|
248
|
+
discovered = discover_tests(c.tests_dir, root=c.workspace_root)
|
|
249
|
+
result = {"tests": [t.nodeid for t in discovered], "count": len(discovered)}
|
|
250
|
+
if json_output:
|
|
251
|
+
typer.echo(_to_json(result))
|
|
252
|
+
else:
|
|
253
|
+
for t in discovered:
|
|
254
|
+
typer.echo(t.nodeid)
|
|
255
|
+
typer.echo(f"{len(discovered)} tests")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@app.command("coverage")
|
|
259
|
+
def coverage_command(
|
|
260
|
+
view: str = typer.Option("both", "--view", help="requirements|tests|both"),
|
|
261
|
+
show: str = typer.Option("all", "--show", help="gaps|all"),
|
|
262
|
+
requirements: str | None = typer.Option(
|
|
263
|
+
None, "--requirements", help="ReqLedger manifest path."
|
|
264
|
+
),
|
|
265
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON to stdout."),
|
|
266
|
+
config: Path | None = typer.Option(None, "--config", help="Config file path."),
|
|
267
|
+
) -> None:
|
|
268
|
+
"""Report requirement-to-test and test-to-requirement coverage."""
|
|
269
|
+
cfg = _resolve_config(config=config, requirements=requirements)
|
|
270
|
+
from specmason.config import SpecMasonConfig
|
|
271
|
+
|
|
272
|
+
c: SpecMasonConfig = cfg # type: ignore[assignment]
|
|
273
|
+
index = _load_index(c)
|
|
274
|
+
features, load_findings = _load_features(c)
|
|
275
|
+
inventory = _load_inventory(c)
|
|
276
|
+
report = build_coverage(features, inventory, index=index, mode=c.mode)
|
|
277
|
+
report = replace(report, findings=load_findings.extend(report.findings))
|
|
278
|
+
errors = report.has_errors
|
|
279
|
+
|
|
280
|
+
if json_output:
|
|
281
|
+
typer.echo(report.to_json())
|
|
282
|
+
else:
|
|
283
|
+
typer.echo(render_markdown(report))
|
|
284
|
+
if errors:
|
|
285
|
+
raise typer.Exit(code=1)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@app.command("mappings")
|
|
289
|
+
def mappings_command(
|
|
290
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON to stdout."),
|
|
291
|
+
config: Path | None = typer.Option(None, "--config", help="Config file path."),
|
|
292
|
+
) -> None:
|
|
293
|
+
"""Show the pytest mapping inventory."""
|
|
294
|
+
cfg = _resolve_config(config=config, requirements=None)
|
|
295
|
+
from specmason.config import SpecMasonConfig
|
|
296
|
+
|
|
297
|
+
c: SpecMasonConfig = cfg # type: ignore[assignment]
|
|
298
|
+
inventory = _load_inventory(c)
|
|
299
|
+
if json_output:
|
|
300
|
+
typer.echo(_to_json(inventory.to_dict()))
|
|
301
|
+
else:
|
|
302
|
+
for t in inventory.tests:
|
|
303
|
+
status = t.status
|
|
304
|
+
mappings = ", ".join(f"{m.req_id}/{m.ac_id}" for m in t.mappings) or "-"
|
|
305
|
+
typer.echo(f"{t.nodeid} {status} {mappings}")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@app.command("import-report")
|
|
309
|
+
def import_report_command(
|
|
310
|
+
format: str = typer.Argument("pytest-junit", help="Report format (pytest-junit)."),
|
|
311
|
+
path: str = typer.Argument(..., help="Path to JUnit XML report."),
|
|
312
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON to stdout."),
|
|
313
|
+
config: Path | None = typer.Option(None, "--config", help="Config file path."),
|
|
314
|
+
) -> None:
|
|
315
|
+
"""Import pytest JUnit XML evidence."""
|
|
316
|
+
cfg = _resolve_config(config=config, requirements=None)
|
|
317
|
+
from specmason.config import SpecMasonConfig
|
|
318
|
+
|
|
319
|
+
c: SpecMasonConfig = cfg # type: ignore[assignment]
|
|
320
|
+
report = parse_junit_xml(path)
|
|
321
|
+
inventory = _load_inventory(c)
|
|
322
|
+
mapped_nodeids = {t.nodeid for t in inventory.tests if t.is_mapped}
|
|
323
|
+
ev_findings = check_evidence_against_mappings(report, mapped_nodeids)
|
|
324
|
+
errors = ev_findings.has_errors
|
|
325
|
+
result = {
|
|
326
|
+
"entries": [e.to_dict() for e in report.entries],
|
|
327
|
+
"findings": ev_findings.to_list(),
|
|
328
|
+
}
|
|
329
|
+
if json_output:
|
|
330
|
+
typer.echo(_to_json(result))
|
|
331
|
+
else:
|
|
332
|
+
for e in report.entries:
|
|
333
|
+
typer.echo(f"{e.nodeid} {e.status} {e.time:.3f}s")
|
|
334
|
+
for f in ev_findings:
|
|
335
|
+
typer.echo(f.render())
|
|
336
|
+
if errors:
|
|
337
|
+
raise typer.Exit(code=1)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@app.command("review")
|
|
341
|
+
def review_command(
|
|
342
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON to stdout."),
|
|
343
|
+
config: Path | None = typer.Option(None, "--config", help="Config file path."),
|
|
344
|
+
requirements: str | None = typer.Option(
|
|
345
|
+
None, "--requirements", help="ReqLedger manifest path."
|
|
346
|
+
),
|
|
347
|
+
) -> None:
|
|
348
|
+
"""Run check + coverage + evidence and write reports."""
|
|
349
|
+
cfg = _resolve_config(config=config, requirements=requirements)
|
|
350
|
+
from specmason.config import SpecMasonConfig
|
|
351
|
+
|
|
352
|
+
c: SpecMasonConfig = cfg # type: ignore[assignment]
|
|
353
|
+
index = _load_index(c)
|
|
354
|
+
result = run_review(c, index=index)
|
|
355
|
+
if json_output:
|
|
356
|
+
typer.echo(_to_json(result.to_dict()))
|
|
357
|
+
else:
|
|
358
|
+
for f in result.findings:
|
|
359
|
+
typer.echo(f.render())
|
|
360
|
+
typer.echo(f"reports: {', '.join(result.reports_written)}")
|
|
361
|
+
if result.has_errors:
|
|
362
|
+
raise typer.Exit(code=1)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
# ---------------------------------------------------------------------------
|
|
366
|
+
# Corpus commands (external corpus mode)
|
|
367
|
+
# ---------------------------------------------------------------------------
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
corpus_app = typer.Typer(
|
|
371
|
+
name="corpus",
|
|
372
|
+
help="Inspect and inventory an external Gherkin corpus.",
|
|
373
|
+
no_args_is_help=True,
|
|
374
|
+
add_completion=False,
|
|
375
|
+
)
|
|
376
|
+
app.add_typer(corpus_app, name="corpus")
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@corpus_app.command("inspect")
|
|
380
|
+
def corpus_inspect_command(
|
|
381
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON to stdout."),
|
|
382
|
+
config: Path | None = typer.Option(None, "--config", help="Config file path."),
|
|
383
|
+
) -> None:
|
|
384
|
+
"""Inventory features, scenarios, outlines, steps, tags, fixtures, and findings."""
|
|
385
|
+
cfg = _resolve_config(config=config, requirements=None)
|
|
386
|
+
from specmason.config import SpecMasonConfig
|
|
387
|
+
|
|
388
|
+
c: SpecMasonConfig = cfg # type: ignore[assignment]
|
|
389
|
+
from specmason.corpus import run_corpus_inspect
|
|
390
|
+
|
|
391
|
+
result, findings = run_corpus_inspect(
|
|
392
|
+
c.features_dir,
|
|
393
|
+
official_parser=c.gherkin_official_parser,
|
|
394
|
+
fixture_roots=c.external_corpus_fixture_roots,
|
|
395
|
+
namespace=c.external_corpus_id_namespace,
|
|
396
|
+
)
|
|
397
|
+
if json_output:
|
|
398
|
+
typer.echo(result.to_json())
|
|
399
|
+
else:
|
|
400
|
+
inv = result.inventory
|
|
401
|
+
typer.echo(f"Features: {inv.feature_count}")
|
|
402
|
+
typer.echo(f"Scenarios: {inv.scenario_count}")
|
|
403
|
+
typer.echo(f"Outlines: {inv.outline_count}")
|
|
404
|
+
typer.echo(f"Expanded examples: {inv.expanded_example_count}")
|
|
405
|
+
typer.echo(f"Step patterns: {inv.step_pattern_count}")
|
|
406
|
+
typer.echo(f"Steps: {inv.step_count}")
|
|
407
|
+
typer.echo(f"Tags: {inv.tag_count}")
|
|
408
|
+
typer.echo(f"Fixture refs: {inv.fixture_ref_count}")
|
|
409
|
+
for f in findings:
|
|
410
|
+
typer.echo(f.render())
|
|
411
|
+
if any(f.is_error() for f in findings):
|
|
412
|
+
raise typer.Exit(code=1)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
@corpus_app.command("steps")
|
|
416
|
+
def corpus_steps_command(
|
|
417
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON to stdout."),
|
|
418
|
+
config: Path | None = typer.Option(None, "--config", help="Config file path."),
|
|
419
|
+
) -> None:
|
|
420
|
+
"""Report the normalized step vocabulary."""
|
|
421
|
+
cfg = _resolve_config(config=config, requirements=None)
|
|
422
|
+
from specmason.config import SpecMasonConfig
|
|
423
|
+
|
|
424
|
+
c: SpecMasonConfig = cfg # type: ignore[assignment]
|
|
425
|
+
from specmason.corpus import parse_corpus, render_steps_json, render_steps_markdown
|
|
426
|
+
from specmason.gherkin.step_vocab import build_step_vocabulary
|
|
427
|
+
|
|
428
|
+
features, _, findings = parse_corpus(
|
|
429
|
+
c.features_dir, official_parser=c.gherkin_official_parser
|
|
430
|
+
)
|
|
431
|
+
vocab = build_step_vocabulary(features)
|
|
432
|
+
if json_output:
|
|
433
|
+
typer.echo(render_steps_json(vocab))
|
|
434
|
+
else:
|
|
435
|
+
typer.echo(render_steps_markdown(vocab))
|
|
436
|
+
if any(f.is_error() for f in findings):
|
|
437
|
+
raise typer.Exit(code=1)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
@corpus_app.command("fixtures")
|
|
441
|
+
def corpus_fixtures_command(
|
|
442
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON to stdout."),
|
|
443
|
+
config: Path | None = typer.Option(None, "--config", help="Config file path."),
|
|
444
|
+
) -> None:
|
|
445
|
+
"""Report extracted fixture references with resolution metadata."""
|
|
446
|
+
cfg = _resolve_config(config=config, requirements=None)
|
|
447
|
+
from specmason.config import SpecMasonConfig
|
|
448
|
+
|
|
449
|
+
c: SpecMasonConfig = cfg # type: ignore[assignment]
|
|
450
|
+
from specmason.corpus import parse_corpus, render_fixtures_json
|
|
451
|
+
|
|
452
|
+
features, _, findings = parse_corpus(
|
|
453
|
+
c.features_dir, official_parser=c.gherkin_official_parser
|
|
454
|
+
)
|
|
455
|
+
if json_output:
|
|
456
|
+
typer.echo(render_fixtures_json(features, c.external_corpus_fixture_roots))
|
|
457
|
+
else:
|
|
458
|
+
from specmason.fixtures import extract_fixture_refs
|
|
459
|
+
|
|
460
|
+
for feat in features:
|
|
461
|
+
for ref in extract_fixture_refs(feat, c.external_corpus_fixture_roots):
|
|
462
|
+
status = "exists" if ref.exists else "MISSING"
|
|
463
|
+
typer.echo(f"{feat.path}: {ref.raw} [{ref.kind}] {status}")
|
|
464
|
+
if any(f.is_error() for f in findings):
|
|
465
|
+
raise typer.Exit(code=1)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
__all__ = ["app"]
|