prodc 0.2.0__tar.gz
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.
- prodc-0.2.0/.gitignore +43 -0
- prodc-0.2.0/CHANGELOG.md +7 -0
- prodc-0.2.0/LICENSE +21 -0
- prodc-0.2.0/PKG-INFO +71 -0
- prodc-0.2.0/README.md +45 -0
- prodc-0.2.0/examples/quickstart/features/orders.feature +7 -0
- prodc-0.2.0/examples/quickstart/needs/jordan.yaml +24 -0
- prodc-0.2.0/examples/quickstart/prodc.toml +13 -0
- prodc-0.2.0/pyproject.toml +70 -0
- prodc-0.2.0/src/prodc/__init__.py +3 -0
- prodc-0.2.0/src/prodc/adapters.py +194 -0
- prodc-0.2.0/src/prodc/cli.py +224 -0
- prodc-0.2.0/src/prodc/config.py +75 -0
- prodc-0.2.0/src/prodc/loader.py +141 -0
- prodc-0.2.0/src/prodc/model.py +111 -0
- prodc-0.2.0/src/prodc/py.typed +0 -0
- prodc-0.2.0/src/prodc/status.py +142 -0
- prodc-0.2.0/tests/test_cli.py +93 -0
- prodc-0.2.0/tests/test_loader.py +115 -0
- prodc-0.2.0/tests/test_status.py +64 -0
prodc-0.2.0/.gitignore
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
|
|
7
|
+
# Build artifacts
|
|
8
|
+
/dist/
|
|
9
|
+
/build/
|
|
10
|
+
/site/
|
|
11
|
+
/public/
|
|
12
|
+
|
|
13
|
+
# uv / venv
|
|
14
|
+
.venv/
|
|
15
|
+
uv.lock.bak
|
|
16
|
+
|
|
17
|
+
# Tool caches
|
|
18
|
+
.pytest_cache/
|
|
19
|
+
.ruff_cache/
|
|
20
|
+
.pyright/
|
|
21
|
+
.mypy_cache/
|
|
22
|
+
.moon/cache/
|
|
23
|
+
|
|
24
|
+
# Editor
|
|
25
|
+
.vscode/
|
|
26
|
+
.idea/
|
|
27
|
+
*.swp
|
|
28
|
+
|
|
29
|
+
# Local session logs
|
|
30
|
+
logs/
|
|
31
|
+
|
|
32
|
+
# Personal, machine-specific agent overrides (AGENTS.md + CLAUDE.md are tracked)
|
|
33
|
+
CLAUDE.local.md
|
|
34
|
+
|
|
35
|
+
# Local agent wiring: names cabildo-issues-mcp, which is not a dependency of this
|
|
36
|
+
# package. docs/issues/ IS tracked — the register is plain markdown anyone can read.
|
|
37
|
+
.mcp.json
|
|
38
|
+
|
|
39
|
+
# Local agent runtime state (scheduled-task locks, session scratch)
|
|
40
|
+
.claude/
|
|
41
|
+
|
|
42
|
+
# Local multi-agent coordination config (cabildo) — not part of the package
|
|
43
|
+
.cabildo/
|
prodc-0.2.0/CHANGELOG.md
ADDED
prodc-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jorge Cardona
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
prodc-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: prodc
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Product management as code: personas, flows, and features traced to evidence.
|
|
5
|
+
Project-URL: Homepage, https://gitlab.com/jorgeecardona/prodc
|
|
6
|
+
Project-URL: Repository, https://gitlab.com/jorgeecardona/prodc
|
|
7
|
+
Project-URL: Changelog, https://gitlab.com/jorgeecardona/prodc/-/blob/main/CHANGELOG.md
|
|
8
|
+
Author-email: Jorge Cardona <jorgeecardona@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering
|
|
17
|
+
Classifier: Topic :: Software Development :: Testing
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Requires-Dist: pyyaml>=6
|
|
21
|
+
Provides-Extra: docs
|
|
22
|
+
Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
|
|
23
|
+
Requires-Dist: mkdocs>=1.6; extra == 'docs'
|
|
24
|
+
Requires-Dist: mkdocstrings[python]>=0.27; extra == 'docs'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# prodc
|
|
28
|
+
|
|
29
|
+
Product management as code: personas, flows, and features traced to evidence, so a
|
|
30
|
+
product's state is read from the repo instead of chased in meetings.
|
|
31
|
+
|
|
32
|
+
prodc treats product thinking the way requirements-as-code tools (Doorstop, StrictDoc)
|
|
33
|
+
treat requirements — plain text, in git, with IDs that link to each other — but for the
|
|
34
|
+
artifacts a product person actually works with: **Persona → Flow → Feature → Assumption**,
|
|
35
|
+
each tied to **evidence**. Two rules give it teeth:
|
|
36
|
+
|
|
37
|
+
- **No evidence link = flagged as opinion.** Every "why" must trace to a user signal
|
|
38
|
+
(feedback, ticket, interview), or it is marked unsupported.
|
|
39
|
+
- **Status comes from the repo, not from a standup.** "Flow UF-004: 3 of 5 scenarios
|
|
40
|
+
passing", "Feature F-012: blocked, assumption A-003 has no data" — the report answers
|
|
41
|
+
"where are we" and "what's blocking" without a meeting.
|
|
42
|
+
|
|
43
|
+
The lineage is Toulmin's argument model (claim + grounds + warrant): a feature is a claim,
|
|
44
|
+
its evidence is the grounds, its assumptions are the warrant. The same shape safety
|
|
45
|
+
engineering uses for a "safety case", pointed at product decisions.
|
|
46
|
+
|
|
47
|
+
> Status: v0, early. The data model and the first cross-repo adapter are still being shaped
|
|
48
|
+
> (see `docs/issues/`). Not yet on PyPI.
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
uv sync # dependencies
|
|
54
|
+
make install-hooks
|
|
55
|
+
make check # lint + typecheck + test
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Why not just Doorstop / StrictDoc?
|
|
59
|
+
|
|
60
|
+
Those track "the system shall…" requirements for auditors in regulated industries. prodc
|
|
61
|
+
tracks personas, flows and features for product decisions, and derives live status from the
|
|
62
|
+
project's own tests. It reads a repo it is pointed at — including existing Gherkin/BDD and
|
|
63
|
+
whatever ID scheme already lives there — rather than asking you to re-author everything in a
|
|
64
|
+
new grammar.
|
|
65
|
+
|
|
66
|
+
## Layout
|
|
67
|
+
|
|
68
|
+
- `src/prodc/` — the package (typed; `py.typed`).
|
|
69
|
+
- `docs/issues/` — follow-ups (the `cabildo issues` convention).
|
|
70
|
+
- `Makefile` — `format` / `lint` / `typecheck` / `test` / `check` / `build` / `docs`.
|
|
71
|
+
- `AGENTS.md` — build/test/style/gotchas for coding agents.
|
prodc-0.2.0/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# prodc
|
|
2
|
+
|
|
3
|
+
Product management as code: personas, flows, and features traced to evidence, so a
|
|
4
|
+
product's state is read from the repo instead of chased in meetings.
|
|
5
|
+
|
|
6
|
+
prodc treats product thinking the way requirements-as-code tools (Doorstop, StrictDoc)
|
|
7
|
+
treat requirements — plain text, in git, with IDs that link to each other — but for the
|
|
8
|
+
artifacts a product person actually works with: **Persona → Flow → Feature → Assumption**,
|
|
9
|
+
each tied to **evidence**. Two rules give it teeth:
|
|
10
|
+
|
|
11
|
+
- **No evidence link = flagged as opinion.** Every "why" must trace to a user signal
|
|
12
|
+
(feedback, ticket, interview), or it is marked unsupported.
|
|
13
|
+
- **Status comes from the repo, not from a standup.** "Flow UF-004: 3 of 5 scenarios
|
|
14
|
+
passing", "Feature F-012: blocked, assumption A-003 has no data" — the report answers
|
|
15
|
+
"where are we" and "what's blocking" without a meeting.
|
|
16
|
+
|
|
17
|
+
The lineage is Toulmin's argument model (claim + grounds + warrant): a feature is a claim,
|
|
18
|
+
its evidence is the grounds, its assumptions are the warrant. The same shape safety
|
|
19
|
+
engineering uses for a "safety case", pointed at product decisions.
|
|
20
|
+
|
|
21
|
+
> Status: v0, early. The data model and the first cross-repo adapter are still being shaped
|
|
22
|
+
> (see `docs/issues/`). Not yet on PyPI.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uv sync # dependencies
|
|
28
|
+
make install-hooks
|
|
29
|
+
make check # lint + typecheck + test
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Why not just Doorstop / StrictDoc?
|
|
33
|
+
|
|
34
|
+
Those track "the system shall…" requirements for auditors in regulated industries. prodc
|
|
35
|
+
tracks personas, flows and features for product decisions, and derives live status from the
|
|
36
|
+
project's own tests. It reads a repo it is pointed at — including existing Gherkin/BDD and
|
|
37
|
+
whatever ID scheme already lives there — rather than asking you to re-author everything in a
|
|
38
|
+
new grammar.
|
|
39
|
+
|
|
40
|
+
## Layout
|
|
41
|
+
|
|
42
|
+
- `src/prodc/` — the package (typed; `py.typed`).
|
|
43
|
+
- `docs/issues/` — follow-ups (the `cabildo issues` convention).
|
|
44
|
+
- `Makefile` — `format` / `lint` / `typecheck` / `test` / `check` / `build` / `docs`.
|
|
45
|
+
- `AGENTS.md` — build/test/style/gotchas for coding agents.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
who: jordan
|
|
2
|
+
label: "design partner"
|
|
3
|
+
needs:
|
|
4
|
+
- id: N-1
|
|
5
|
+
text: "see which orders are past due at a glance"
|
|
6
|
+
must: true
|
|
7
|
+
sources:
|
|
8
|
+
- kind: interview
|
|
9
|
+
who: Jordan
|
|
10
|
+
date: 2026-09-20
|
|
11
|
+
quote: "I lose the past-due ones in the list"
|
|
12
|
+
proofs:
|
|
13
|
+
- "web:orders.feature#Past due orders are highlighted"
|
|
14
|
+
|
|
15
|
+
- id: N-2
|
|
16
|
+
text: "export the day's delivery run sheet"
|
|
17
|
+
sources:
|
|
18
|
+
- kind: artifact
|
|
19
|
+
locator: issues.md#N-2
|
|
20
|
+
date: 2026-09-21
|
|
21
|
+
proofs: [] # sourced HOLE: the need is evidenced, but no test proves it yet
|
|
22
|
+
|
|
23
|
+
- id: N-3
|
|
24
|
+
text: "implement a settings page" # opinion (no source) + solution-in-disguise warning
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# A minimal, runnable prodc project. From this directory:
|
|
2
|
+
# uv run prodc status
|
|
3
|
+
# N-1 shows DELIVERED (a tagged scenario proves it), N-2 is a sourced HOLE
|
|
4
|
+
# (evidence exists, no test yet), N-3 is an opinion with a solution-in-disguise warning.
|
|
5
|
+
|
|
6
|
+
[prodc]
|
|
7
|
+
needs = ["needs/*.yaml"]
|
|
8
|
+
|
|
9
|
+
[proofs.web]
|
|
10
|
+
kind = "gherkin"
|
|
11
|
+
paths = ["features/*.feature"]
|
|
12
|
+
delivered = ["@implemented"]
|
|
13
|
+
pending = ["@pending"]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "prodc"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Product management as code: personas, flows, and features traced to evidence."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "Jorge Cardona", email = "jorgeecardona@gmail.com" }]
|
|
10
|
+
keywords = []
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 4 - Beta",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
"Topic :: Software Development :: Testing",
|
|
18
|
+
"Topic :: Scientific/Engineering",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
]
|
|
21
|
+
dependencies = ["pyyaml>=6"]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
prodc = "prodc.cli:main"
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
# docs are an *extra* (not a uv dependency-group) so Read the Docs can install them via pip
|
|
28
|
+
docs = [
|
|
29
|
+
"mkdocs>=1.6",
|
|
30
|
+
"mkdocs-material>=9.5",
|
|
31
|
+
"mkdocstrings[python]>=0.27",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://gitlab.com/jorgeecardona/prodc"
|
|
36
|
+
Repository = "https://gitlab.com/jorgeecardona/prodc"
|
|
37
|
+
Changelog = "https://gitlab.com/jorgeecardona/prodc/-/blob/main/CHANGELOG.md"
|
|
38
|
+
|
|
39
|
+
[build-system]
|
|
40
|
+
requires = ["hatchling"]
|
|
41
|
+
build-backend = "hatchling.build"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/prodc"]
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.sdist]
|
|
47
|
+
include = ["src", "tests", "examples", "README.md", "CHANGELOG.md", "LICENSE"]
|
|
48
|
+
|
|
49
|
+
[dependency-groups]
|
|
50
|
+
dev = ["ruff>=0.6", "pyright>=1.1.390", "pytest>=8", "types-pyyaml>=6"]
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
line-length = 100
|
|
54
|
+
target-version = "py312"
|
|
55
|
+
src = ["src", "tests"]
|
|
56
|
+
|
|
57
|
+
[tool.ruff.lint]
|
|
58
|
+
select = ["E", "F", "W", "I", "B", "UP", "SIM", "RUF"]
|
|
59
|
+
ignore = ["E501"]
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint.isort]
|
|
62
|
+
known-first-party = ["prodc"]
|
|
63
|
+
|
|
64
|
+
[tool.pyright]
|
|
65
|
+
include = ["src", "tests", "examples"]
|
|
66
|
+
extraPaths = ["src"]
|
|
67
|
+
pythonVersion = "3.12"
|
|
68
|
+
typeCheckingMode = "strict"
|
|
69
|
+
venvPath = "."
|
|
70
|
+
venv = ".venv"
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Proof adapters: resolve a locator payload to a :class:`ProofState`.
|
|
2
|
+
|
|
3
|
+
An adapter reads a repo as it already is (Gherkin titles, pytest node ids). Status
|
|
4
|
+
comes from a test-results artifact (cucumber-json / junit) when configured and
|
|
5
|
+
present; otherwise it reports a *declared* state (from tags or existence) and says so.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
import xml.etree.ElementTree as ET
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Protocol, cast
|
|
16
|
+
|
|
17
|
+
from .model import ProofState
|
|
18
|
+
|
|
19
|
+
_TAG_RE = re.compile(r"@[\w\-.]+")
|
|
20
|
+
_SCENARIO_RE = re.compile(r"^\s*(?:Scenario|Scenario Outline):\s*(.*?)\s*$")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _steps_passed(steps: list[object]) -> bool:
|
|
24
|
+
"""A scenario passes iff every step's result status is 'passed' (cucumber-json)."""
|
|
25
|
+
for step in steps:
|
|
26
|
+
if not isinstance(step, dict):
|
|
27
|
+
return False
|
|
28
|
+
result = cast("dict[str, object]", step).get("result")
|
|
29
|
+
status = cast("dict[str, object]", result).get("status") if isinstance(result, dict) else None
|
|
30
|
+
if status != "passed":
|
|
31
|
+
return False
|
|
32
|
+
return True
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class ProofResult:
|
|
37
|
+
state: ProofState
|
|
38
|
+
declared: bool # True when the state rests on tags/existence, not a results artifact
|
|
39
|
+
detail: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Adapter(Protocol):
|
|
43
|
+
def resolve(self, payload: str) -> ProofResult: ...
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class _Scenario:
|
|
48
|
+
file: str
|
|
49
|
+
title: str
|
|
50
|
+
tags: frozenset[str]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class GherkinAdapter:
|
|
54
|
+
"""Resolves ``file#Scenario title`` or ``tag:@US-050`` against feature files."""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
root: Path,
|
|
59
|
+
paths: list[str],
|
|
60
|
+
delivered_tags: list[str],
|
|
61
|
+
pending_tags: list[str],
|
|
62
|
+
results: str | None,
|
|
63
|
+
) -> None:
|
|
64
|
+
self._root = root
|
|
65
|
+
self._paths = paths
|
|
66
|
+
self._delivered = frozenset(delivered_tags)
|
|
67
|
+
self._pending = frozenset(pending_tags)
|
|
68
|
+
self._results_path = results
|
|
69
|
+
self._scenarios: list[_Scenario] | None = None
|
|
70
|
+
self._results: dict[str, bool] | None = None
|
|
71
|
+
|
|
72
|
+
def _load(self) -> list[_Scenario]:
|
|
73
|
+
if self._scenarios is not None:
|
|
74
|
+
return self._scenarios
|
|
75
|
+
found: list[_Scenario] = []
|
|
76
|
+
for pattern in self._paths:
|
|
77
|
+
for fp in sorted(self._root.glob(pattern)):
|
|
78
|
+
pending: set[str] = set()
|
|
79
|
+
for line in fp.read_text(encoding="utf-8").splitlines():
|
|
80
|
+
stripped = line.strip()
|
|
81
|
+
if stripped.startswith("@"):
|
|
82
|
+
pending |= set(_TAG_RE.findall(stripped))
|
|
83
|
+
continue
|
|
84
|
+
m = _SCENARIO_RE.match(line)
|
|
85
|
+
if m:
|
|
86
|
+
found.append(
|
|
87
|
+
_Scenario(fp.name, m.group(1), frozenset(pending))
|
|
88
|
+
)
|
|
89
|
+
pending = set()
|
|
90
|
+
elif stripped and not stripped.startswith("#"):
|
|
91
|
+
pending = set()
|
|
92
|
+
self._scenarios = found
|
|
93
|
+
return found
|
|
94
|
+
|
|
95
|
+
def _load_results(self) -> dict[str, bool]:
|
|
96
|
+
"""Map scenario name -> passed, from a cucumber-json report if configured."""
|
|
97
|
+
if self._results is not None:
|
|
98
|
+
return self._results
|
|
99
|
+
out: dict[str, bool] = {}
|
|
100
|
+
if self._results_path:
|
|
101
|
+
rp = self._root / self._results_path
|
|
102
|
+
if rp.exists():
|
|
103
|
+
data: object = json.loads(rp.read_text(encoding="utf-8"))
|
|
104
|
+
if isinstance(data, list):
|
|
105
|
+
for feature in cast("list[object]", data):
|
|
106
|
+
if not isinstance(feature, dict):
|
|
107
|
+
continue
|
|
108
|
+
elements = cast("dict[str, object]", feature).get("elements")
|
|
109
|
+
if not isinstance(elements, list):
|
|
110
|
+
continue
|
|
111
|
+
for el in cast("list[object]", elements):
|
|
112
|
+
if not isinstance(el, dict):
|
|
113
|
+
continue
|
|
114
|
+
el_d = cast("dict[str, object]", el)
|
|
115
|
+
name = el_d.get("name")
|
|
116
|
+
steps = el_d.get("steps")
|
|
117
|
+
if isinstance(name, str) and isinstance(steps, list):
|
|
118
|
+
out[name] = _steps_passed(cast("list[object]", steps))
|
|
119
|
+
self._results = out
|
|
120
|
+
return out
|
|
121
|
+
|
|
122
|
+
def resolve(self, payload: str) -> ProofResult:
|
|
123
|
+
scenarios = self._load()
|
|
124
|
+
if payload.startswith("tag:"):
|
|
125
|
+
want = payload[4:]
|
|
126
|
+
matches = [s for s in scenarios if want in s.tags]
|
|
127
|
+
else:
|
|
128
|
+
file_part, _, title = payload.partition("#")
|
|
129
|
+
matches = [
|
|
130
|
+
s for s in scenarios if s.title == title and (not file_part or s.file == file_part)
|
|
131
|
+
]
|
|
132
|
+
if not matches:
|
|
133
|
+
return ProofResult(ProofState.BROKEN, False, f"no scenario matches '{payload}'")
|
|
134
|
+
|
|
135
|
+
results = self._load_results()
|
|
136
|
+
if results:
|
|
137
|
+
states = [results.get(s.title) for s in matches]
|
|
138
|
+
if all(v is True for v in states):
|
|
139
|
+
return ProofResult(ProofState.DELIVERED, False, "results: passed")
|
|
140
|
+
if any(v is False for v in states):
|
|
141
|
+
return ProofResult(ProofState.PENDING, False, "results: failing")
|
|
142
|
+
# matched scenarios absent from the report → declared only
|
|
143
|
+
tags: frozenset[str] = frozenset()
|
|
144
|
+
for s in matches:
|
|
145
|
+
tags |= s.tags
|
|
146
|
+
if tags & self._delivered:
|
|
147
|
+
return ProofResult(ProofState.DELIVERED, True, "tagged delivered")
|
|
148
|
+
if tags & self._pending:
|
|
149
|
+
return ProofResult(ProofState.PENDING, True, "tagged pending")
|
|
150
|
+
return ProofResult(ProofState.PENDING, True, "found, untagged")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class PytestAdapter:
|
|
154
|
+
"""Resolves ``path::test_name`` by existence, upgraded by a junit report if given."""
|
|
155
|
+
|
|
156
|
+
def __init__(self, root: Path, results: str | None) -> None:
|
|
157
|
+
self._root = root
|
|
158
|
+
self._results_path = results
|
|
159
|
+
self._results: dict[str, bool] | None = None
|
|
160
|
+
|
|
161
|
+
def _load_results(self) -> dict[str, bool]:
|
|
162
|
+
if self._results is not None:
|
|
163
|
+
return self._results
|
|
164
|
+
out: dict[str, bool] = {}
|
|
165
|
+
if self._results_path:
|
|
166
|
+
rp = self._root / self._results_path
|
|
167
|
+
if rp.exists():
|
|
168
|
+
tree = ET.parse(rp)
|
|
169
|
+
for case in tree.iter("testcase"):
|
|
170
|
+
name = case.get("name", "")
|
|
171
|
+
failed = any(c.tag in ("failure", "error") for c in case)
|
|
172
|
+
skipped = any(c.tag == "skipped" for c in case)
|
|
173
|
+
if name:
|
|
174
|
+
out[name] = not failed and not skipped
|
|
175
|
+
self._results = out
|
|
176
|
+
return out
|
|
177
|
+
|
|
178
|
+
def resolve(self, payload: str) -> ProofResult:
|
|
179
|
+
file_part, _, node = payload.partition("::")
|
|
180
|
+
func = node.split("[")[0] # strip parametrisation
|
|
181
|
+
fp = self._root / file_part
|
|
182
|
+
if not fp.exists():
|
|
183
|
+
return ProofResult(ProofState.BROKEN, False, f"no file '{file_part}'")
|
|
184
|
+
if func and f"def {func}" not in fp.read_text(encoding="utf-8"):
|
|
185
|
+
return ProofResult(ProofState.BROKEN, False, f"no test '{func}' in {file_part}")
|
|
186
|
+
results = self._load_results()
|
|
187
|
+
if func in results:
|
|
188
|
+
ok = results[func]
|
|
189
|
+
return ProofResult(
|
|
190
|
+
ProofState.DELIVERED if ok else ProofState.PENDING,
|
|
191
|
+
False,
|
|
192
|
+
"junit: passed" if ok else "junit: failing",
|
|
193
|
+
)
|
|
194
|
+
return ProofResult(ProofState.DELIVERED, True, "test exists")
|