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 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/
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ All notable changes are documented here. Versioning is [SemVer](https://semver.org/).
4
+
5
+ ## [0.1.0] - YYYY-MM-DD
6
+
7
+ Initial release.
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,7 @@
1
+ Feature: Orders board
2
+
3
+ @implemented
4
+ Scenario: Past due orders are highlighted
5
+ Given an order whose due date has passed
6
+ When I open the orders board
7
+ Then the order is highlighted as past due
@@ -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,3 @@
1
+ """prodc: product management as code."""
2
+
3
+ __version__ = "0.1.0"
@@ -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")