prodc 0.4.6__tar.gz → 0.4.7__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.4.7/PKG-INFO +200 -0
- prodc-0.4.7/README.md +173 -0
- {prodc-0.4.6 → prodc-0.4.7}/examples/quickstart/needs/jordan.yaml +0 -1
- {prodc-0.4.6 → prodc-0.4.7}/pyproject.toml +5 -4
- {prodc-0.4.6 → prodc-0.4.7}/src/prodc/cli/commands.py +4 -0
- {prodc-0.4.6 → prodc-0.4.7}/src/prodc/config.py +22 -0
- {prodc-0.4.6 → prodc-0.4.7}/src/prodc/loader.py +22 -0
- {prodc-0.4.6 → prodc-0.4.7}/src/prodc/status.py +45 -0
- prodc-0.4.7/tests/test_flowsteps.py +172 -0
- prodc-0.4.6/PKG-INFO +0 -82
- prodc-0.4.6/README.md +0 -56
- {prodc-0.4.6 → prodc-0.4.7}/.gitignore +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/CHANGELOG.md +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/LICENSE +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/examples/quickstart/features/orders.feature +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/examples/quickstart/prodc.toml +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/src/prodc/__init__.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/src/prodc/adapters.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/src/prodc/agent.md +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/src/prodc/board.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/src/prodc/callbacks.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/src/prodc/cli/__init__.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/src/prodc/cli/app.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/src/prodc/cli/common.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/src/prodc/cli/render.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/src/prodc/cli/serialize.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/src/prodc/dedup.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/src/prodc/guide.md +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/src/prodc/model.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/src/prodc/py.typed +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/src/prodc/report.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/src/prodc/report_html.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/src/prodc/writer.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_adapters.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_answer.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_callbacks.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_citation.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_claim_gating.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_cli.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_command_hook.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_contradicts.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_decisions.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_dedup.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_description.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_drafts.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_evalgate.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_external_adapter.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_gitlab_source.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_importance.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_loader.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_models.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_needref.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_principles.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_relink.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_report.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_report_html.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_review.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_roles.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_source_role.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_sources.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_staleness.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_status.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_suggest.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_variants.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_who_variants.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_withdrawn.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_writer.py +0 -0
- {prodc-0.4.6 → prodc-0.4.7}/tests/test_writer_comments.py +0 -0
prodc-0.4.7/PKG-INFO
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: prodc
|
|
3
|
+
Version: 0.4.7
|
|
4
|
+
Summary: Product management as code: roles, flows, and features traced to evidence.
|
|
5
|
+
Project-URL: Homepage, https://gitlab.com/cardona-oss/prodc
|
|
6
|
+
Project-URL: Repository, https://gitlab.com/cardona-oss/prodc
|
|
7
|
+
Project-URL: Changelog, https://gitlab.com/cardona-oss/prodc/-/blob/main/CHANGELOG.md
|
|
8
|
+
Project-URL: Documentation, https://cardona-oss.gitlab.io/prodc/
|
|
9
|
+
Author-email: Jorge Cardona <jorgeecardona@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering
|
|
18
|
+
Classifier: Topic :: Software Development :: Testing
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Requires-Dist: pyyaml>=6
|
|
22
|
+
Provides-Extra: docs
|
|
23
|
+
Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
|
|
24
|
+
Requires-Dist: mkdocs>=1.6; extra == 'docs'
|
|
25
|
+
Requires-Dist: mkdocstrings[python]>=0.27; extra == 'docs'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# prodc
|
|
29
|
+
|
|
30
|
+
[](https://pypi.org/project/prodc/)
|
|
31
|
+
[](https://gitlab.com/cardona-oss/prodc/-/blob/main/LICENSE)
|
|
32
|
+
|
|
33
|
+
Product management as code: the *needs* your product exists to meet, each traced to the evidence a real user gave for it and the test that proves it is delivered. For teams that keep product work in the repo, next to the code, and want a tool to check it holds together.
|
|
34
|
+
|
|
35
|
+
prodc reads plain YAML files and your test suite and shows you the gaps. It does not call a model, host a UI, or replace your tracker. Two rules carry it:
|
|
36
|
+
|
|
37
|
+
1. **A need with no evidence is an opinion.** Write down who asked — an interview quote, a ticket, an email — or prodc flags the need as an opinion, because that is what it is until someone outside the room has asked.
|
|
38
|
+
2. **"Done" is a claim your tests back.** You never mark a need delivered. You attach a *proof* (a locator into your test suite), and `prodc status` resolves it: DELIVERED, PENDING, HOLE (an honest gap), or BROKEN (points at nothing). The state is read from the repo, not remembered from a standup.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install prodc # or: uv add prodc
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Python 3.12+. The only runtime dependency is PyYAML.
|
|
47
|
+
|
|
48
|
+
## Quickstart
|
|
49
|
+
|
|
50
|
+
Three files. The first tells prodc where to look, the second holds what people need, the third is a test you have probably already written.
|
|
51
|
+
|
|
52
|
+
`prodc.toml`
|
|
53
|
+
|
|
54
|
+
```toml
|
|
55
|
+
[prodc]
|
|
56
|
+
needs = ["needs/*.yaml"]
|
|
57
|
+
|
|
58
|
+
[proofs.web]
|
|
59
|
+
kind = "gherkin"
|
|
60
|
+
paths = ["features/*.feature"]
|
|
61
|
+
delivered = ["@implemented"]
|
|
62
|
+
pending = ["@pending"]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`needs/jordan.yaml`
|
|
66
|
+
|
|
67
|
+
```yaml
|
|
68
|
+
who: jordan
|
|
69
|
+
needs:
|
|
70
|
+
- id: N-1
|
|
71
|
+
text: "see which orders are past due at a glance"
|
|
72
|
+
must: true
|
|
73
|
+
sources:
|
|
74
|
+
- kind: primary
|
|
75
|
+
who: Jordan
|
|
76
|
+
date: 2026-09-20
|
|
77
|
+
quote: "I lose the past-due ones in the list"
|
|
78
|
+
proofs:
|
|
79
|
+
- "web:orders.feature#Past due orders are highlighted"
|
|
80
|
+
|
|
81
|
+
- id: N-2
|
|
82
|
+
text: "export the day's delivery run sheet"
|
|
83
|
+
sources:
|
|
84
|
+
- kind: secondary
|
|
85
|
+
locator: issues.md#N-2
|
|
86
|
+
date: 2026-09-21
|
|
87
|
+
|
|
88
|
+
- id: N-3
|
|
89
|
+
text: "implement a settings page"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`features/orders.feature`
|
|
93
|
+
|
|
94
|
+
```gherkin
|
|
95
|
+
Feature: Orders board
|
|
96
|
+
|
|
97
|
+
@implemented
|
|
98
|
+
Scenario: Past due orders are highlighted
|
|
99
|
+
Given an order whose due date has passed
|
|
100
|
+
When I open the orders board
|
|
101
|
+
Then the order is highlighted as past due
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Run the board (output trimmed; the real one also carries a legend and phrasing hints):
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
$ prodc status
|
|
108
|
+
gaps: 2 HOLE · 1 opinion coverage 1/3 (33%)
|
|
109
|
+
|
|
110
|
+
jordan 1 DELIVERED 2 HOLE
|
|
111
|
+
provenance: 1 primary · 1 secondary · 0 assumption · 1 opinion
|
|
112
|
+
|
|
113
|
+
N-2 HOLE export the day's delivery run sheet secondary
|
|
114
|
+
N-3 HOLE implement a settings page opinion
|
|
115
|
+
⚠ text may be a solution in disguise (starts with 'implement')
|
|
116
|
+
!! N-1 DELIVERED* 1/1 see which orders are past due at a glance primary:Jordan
|
|
117
|
+
⚠ DELIVERED on an unattested proof link — warrant not reviewed
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
N-1 is DELIVERED because a tagged scenario proves it (the `*` means declared by tag, no test results loaded yet). N-2 has evidence but no proof: a HOLE. N-3 has neither: an opinion.
|
|
121
|
+
|
|
122
|
+
Now close the loop from the command line. Each write command rewrites one YAML file, re-validates the whole model, and prints the diff.
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# trace the opinion to evidence
|
|
126
|
+
prodc capture N-3 --kind primary --who Jordan --date 2026-10-01 \
|
|
127
|
+
--quote "I keep changing the printer in three places"
|
|
128
|
+
|
|
129
|
+
# add the scenario to your suite, tagged @pending until it is green:
|
|
130
|
+
# @pending
|
|
131
|
+
# Scenario: Export the run sheet
|
|
132
|
+
# then point the need at it:
|
|
133
|
+
prodc set N-2 proofs+="web:orders.feature#Export the run sheet"
|
|
134
|
+
|
|
135
|
+
prodc status # gaps: 1 PENDING · 1 HOLE — the scenario exists, so N-2 is PENDING, not BROKEN
|
|
136
|
+
# (skip writing the scenario and the proof points at nothing: BROKEN, lint exits 1)
|
|
137
|
+
prodc lint # CI gate: exit 1 on a BROKEN proof or a must-have with no evidence
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Change a scenario title without updating the need and the proof turns BROKEN; `prodc lint` exits 1. That is the whole idea: the board cannot drift from the code.
|
|
141
|
+
|
|
142
|
+
## Why prodc
|
|
143
|
+
|
|
144
|
+
**Versus Jira, Linear, and other trackers.** A tracker records work items and a status someone set by hand. prodc records *needs* and computes their status from the tests. It has no assignees, sprints, comments, or notifications, and it is not trying to. Keep the tracker for the work; keep prodc for the question "did a real user ask for this, and does a test prove it?" The two connect at the edges: a need's source can be a ticket locator (checked read-only against GitLab with `[sources.<name>] kind = "gitlab"`), a proof can be resolved by any script you point at a tracker (`kind = "command"`), and `prodc sync` fires a hook when a need changes state.
|
|
145
|
+
|
|
146
|
+
**Versus a PRD in Notion or a wiki.** A document says what should be true; nothing checks it later. prodc's files are validated on every load (ids, cross-references, source hygiene), linted in CI, and reviewed in merge requests like code. "Is this done?" is answered by `prodc show N-1`, not by finding the right page.
|
|
147
|
+
|
|
148
|
+
**Versus plain YAML in the repo.** You could keep this structure by hand. prodc adds the schema, the proof resolution against real test suites, the evidence ladder, the lint, and the write-back commands that keep the files consistent.
|
|
149
|
+
|
|
150
|
+
**What it does not do.** It does not run your tests, call an LLM, or ping a tracker on its own. It does not model the full discovery loop (opportunity-solution-tree restructuring, parallel solution testing); it records needs, the evidence under them, and the decisions taken on them.
|
|
151
|
+
|
|
152
|
+
## Key features
|
|
153
|
+
|
|
154
|
+
- **Evidence ladder.** A source is `primary` (the user's own words), `secondary` (a report, a
|
|
155
|
+
ticket, an internal doc), or `assumption` (a guess). With neither primary nor secondary, the need
|
|
156
|
+
is an opinion: `lint` fails a must-have opinion, `status --strict` fails on any.
|
|
157
|
+
- **Proofs resolve against the repo.** Adapters for Gherkin scenarios and tags, pytest node ids,
|
|
158
|
+
file existence, human sign-off, prototypes, citations, eval metric gates, external results files,
|
|
159
|
+
and a generic `command` hook. Feed it cucumber-json or junit XML and declared states become real.
|
|
160
|
+
- **Five states, honest semantics.** DELIVERED, PENDING, HOLE, BROKEN, UNRESOLVED. A HOLE is never
|
|
161
|
+
a failure; BROKEN always is; a proof in an absent sibling repo is UNRESOLVED, not BROKEN.
|
|
162
|
+
- **Flows.** A need with `children` rolls them up, with `seam` proofs for the behaviour between
|
|
163
|
+
steps. Any BROKEN child breaks the flow; a flow is DELIVERED only when every child and seam is
|
|
164
|
+
green.
|
|
165
|
+
- **The roll-up is machine-checked.** `tests/test_models.py` builds a
|
|
166
|
+
[musil](https://pypi.org/project/musil/) model of the lifecycle and explores every reachable
|
|
167
|
+
state against the real resolver, so the documented invariants cannot drift from the code.
|
|
168
|
+
- **Write-back that respects your files.** `set`, `capture`, `accept`, `attest`, `withdraw`, `add`
|
|
169
|
+
splice one need's block back into the original YAML, keeping other comments. All take `--dry-run`
|
|
170
|
+
and `--expect-sha`.
|
|
171
|
+
- **Boards for humans.** `status`, `show`, `brief`, `plan`, `report --format html` (a self-contained
|
|
172
|
+
interactive viewer), `portfolio`, and `diff` against a snapshot. The read commands (`status`,
|
|
173
|
+
`lint`, `show`, `brief`, `plan`, `diff`, `portfolio`, `dupes`, `recurrence`, `suggest`, `review`)
|
|
174
|
+
take `--json`; exit codes are fixed (0 ok, 1 gate failed, 2 config or load error).
|
|
175
|
+
|
|
176
|
+
prodc also models goals, releases, roles, principles, decisions, attestations, withdrawals and
|
|
177
|
+
settled answers, and ships hygiene commands (`dupes`, `recurrence`, `suggest`); the
|
|
178
|
+
[docs](https://cardona-oss.gitlab.io/prodc/) have the full surface.
|
|
179
|
+
|
|
180
|
+
## Use it with a coding agent
|
|
181
|
+
|
|
182
|
+
prodc ships a playbook that teaches an agent to act as the project's product manager: read the board, capture evidence with honest provenance, find proofs, gate before a handoff. An agent may record `assumption` or `secondary` sources; `primary` is reserved for a real user's own words.
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
prodc agent >> AGENTS.md # or: prodc agent --write AGENTS.md
|
|
186
|
+
prodc guide # the short human onboarding guide
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Links
|
|
190
|
+
|
|
191
|
+
- Repository: <https://gitlab.com/cardona-oss/prodc>
|
|
192
|
+
- PyPI: <https://pypi.org/project/prodc/>
|
|
193
|
+
- Docs: <https://cardona-oss.gitlab.io/prodc/> — [Quickstart](https://cardona-oss.gitlab.io/prodc/quickstart/), [Commands](https://cardona-oss.gitlab.io/prodc/commands/), [Model](https://cardona-oss.gitlab.io/prodc/model/), [Config](https://cardona-oss.gitlab.io/prodc/config/), [Lifecycle](https://cardona-oss.gitlab.io/prodc/lifecycle/)
|
|
194
|
+
- Changelog: [CHANGELOG.md](https://gitlab.com/cardona-oss/prodc/-/blob/main/CHANGELOG.md)
|
|
195
|
+
|
|
196
|
+
`AGENTS.md` holds build and test instructions for coding agents working on prodc itself.
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
MIT. See [LICENSE](https://gitlab.com/cardona-oss/prodc/-/blob/main/LICENSE).
|
prodc-0.4.7/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# prodc
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/prodc/)
|
|
4
|
+
[](https://gitlab.com/cardona-oss/prodc/-/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
Product management as code: the *needs* your product exists to meet, each traced to the evidence a real user gave for it and the test that proves it is delivered. For teams that keep product work in the repo, next to the code, and want a tool to check it holds together.
|
|
7
|
+
|
|
8
|
+
prodc reads plain YAML files and your test suite and shows you the gaps. It does not call a model, host a UI, or replace your tracker. Two rules carry it:
|
|
9
|
+
|
|
10
|
+
1. **A need with no evidence is an opinion.** Write down who asked — an interview quote, a ticket, an email — or prodc flags the need as an opinion, because that is what it is until someone outside the room has asked.
|
|
11
|
+
2. **"Done" is a claim your tests back.** You never mark a need delivered. You attach a *proof* (a locator into your test suite), and `prodc status` resolves it: DELIVERED, PENDING, HOLE (an honest gap), or BROKEN (points at nothing). The state is read from the repo, not remembered from a standup.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install prodc # or: uv add prodc
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Python 3.12+. The only runtime dependency is PyYAML.
|
|
20
|
+
|
|
21
|
+
## Quickstart
|
|
22
|
+
|
|
23
|
+
Three files. The first tells prodc where to look, the second holds what people need, the third is a test you have probably already written.
|
|
24
|
+
|
|
25
|
+
`prodc.toml`
|
|
26
|
+
|
|
27
|
+
```toml
|
|
28
|
+
[prodc]
|
|
29
|
+
needs = ["needs/*.yaml"]
|
|
30
|
+
|
|
31
|
+
[proofs.web]
|
|
32
|
+
kind = "gherkin"
|
|
33
|
+
paths = ["features/*.feature"]
|
|
34
|
+
delivered = ["@implemented"]
|
|
35
|
+
pending = ["@pending"]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`needs/jordan.yaml`
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
who: jordan
|
|
42
|
+
needs:
|
|
43
|
+
- id: N-1
|
|
44
|
+
text: "see which orders are past due at a glance"
|
|
45
|
+
must: true
|
|
46
|
+
sources:
|
|
47
|
+
- kind: primary
|
|
48
|
+
who: Jordan
|
|
49
|
+
date: 2026-09-20
|
|
50
|
+
quote: "I lose the past-due ones in the list"
|
|
51
|
+
proofs:
|
|
52
|
+
- "web:orders.feature#Past due orders are highlighted"
|
|
53
|
+
|
|
54
|
+
- id: N-2
|
|
55
|
+
text: "export the day's delivery run sheet"
|
|
56
|
+
sources:
|
|
57
|
+
- kind: secondary
|
|
58
|
+
locator: issues.md#N-2
|
|
59
|
+
date: 2026-09-21
|
|
60
|
+
|
|
61
|
+
- id: N-3
|
|
62
|
+
text: "implement a settings page"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`features/orders.feature`
|
|
66
|
+
|
|
67
|
+
```gherkin
|
|
68
|
+
Feature: Orders board
|
|
69
|
+
|
|
70
|
+
@implemented
|
|
71
|
+
Scenario: Past due orders are highlighted
|
|
72
|
+
Given an order whose due date has passed
|
|
73
|
+
When I open the orders board
|
|
74
|
+
Then the order is highlighted as past due
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Run the board (output trimmed; the real one also carries a legend and phrasing hints):
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
$ prodc status
|
|
81
|
+
gaps: 2 HOLE · 1 opinion coverage 1/3 (33%)
|
|
82
|
+
|
|
83
|
+
jordan 1 DELIVERED 2 HOLE
|
|
84
|
+
provenance: 1 primary · 1 secondary · 0 assumption · 1 opinion
|
|
85
|
+
|
|
86
|
+
N-2 HOLE export the day's delivery run sheet secondary
|
|
87
|
+
N-3 HOLE implement a settings page opinion
|
|
88
|
+
⚠ text may be a solution in disguise (starts with 'implement')
|
|
89
|
+
!! N-1 DELIVERED* 1/1 see which orders are past due at a glance primary:Jordan
|
|
90
|
+
⚠ DELIVERED on an unattested proof link — warrant not reviewed
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
N-1 is DELIVERED because a tagged scenario proves it (the `*` means declared by tag, no test results loaded yet). N-2 has evidence but no proof: a HOLE. N-3 has neither: an opinion.
|
|
94
|
+
|
|
95
|
+
Now close the loop from the command line. Each write command rewrites one YAML file, re-validates the whole model, and prints the diff.
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# trace the opinion to evidence
|
|
99
|
+
prodc capture N-3 --kind primary --who Jordan --date 2026-10-01 \
|
|
100
|
+
--quote "I keep changing the printer in three places"
|
|
101
|
+
|
|
102
|
+
# add the scenario to your suite, tagged @pending until it is green:
|
|
103
|
+
# @pending
|
|
104
|
+
# Scenario: Export the run sheet
|
|
105
|
+
# then point the need at it:
|
|
106
|
+
prodc set N-2 proofs+="web:orders.feature#Export the run sheet"
|
|
107
|
+
|
|
108
|
+
prodc status # gaps: 1 PENDING · 1 HOLE — the scenario exists, so N-2 is PENDING, not BROKEN
|
|
109
|
+
# (skip writing the scenario and the proof points at nothing: BROKEN, lint exits 1)
|
|
110
|
+
prodc lint # CI gate: exit 1 on a BROKEN proof or a must-have with no evidence
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Change a scenario title without updating the need and the proof turns BROKEN; `prodc lint` exits 1. That is the whole idea: the board cannot drift from the code.
|
|
114
|
+
|
|
115
|
+
## Why prodc
|
|
116
|
+
|
|
117
|
+
**Versus Jira, Linear, and other trackers.** A tracker records work items and a status someone set by hand. prodc records *needs* and computes their status from the tests. It has no assignees, sprints, comments, or notifications, and it is not trying to. Keep the tracker for the work; keep prodc for the question "did a real user ask for this, and does a test prove it?" The two connect at the edges: a need's source can be a ticket locator (checked read-only against GitLab with `[sources.<name>] kind = "gitlab"`), a proof can be resolved by any script you point at a tracker (`kind = "command"`), and `prodc sync` fires a hook when a need changes state.
|
|
118
|
+
|
|
119
|
+
**Versus a PRD in Notion or a wiki.** A document says what should be true; nothing checks it later. prodc's files are validated on every load (ids, cross-references, source hygiene), linted in CI, and reviewed in merge requests like code. "Is this done?" is answered by `prodc show N-1`, not by finding the right page.
|
|
120
|
+
|
|
121
|
+
**Versus plain YAML in the repo.** You could keep this structure by hand. prodc adds the schema, the proof resolution against real test suites, the evidence ladder, the lint, and the write-back commands that keep the files consistent.
|
|
122
|
+
|
|
123
|
+
**What it does not do.** It does not run your tests, call an LLM, or ping a tracker on its own. It does not model the full discovery loop (opportunity-solution-tree restructuring, parallel solution testing); it records needs, the evidence under them, and the decisions taken on them.
|
|
124
|
+
|
|
125
|
+
## Key features
|
|
126
|
+
|
|
127
|
+
- **Evidence ladder.** A source is `primary` (the user's own words), `secondary` (a report, a
|
|
128
|
+
ticket, an internal doc), or `assumption` (a guess). With neither primary nor secondary, the need
|
|
129
|
+
is an opinion: `lint` fails a must-have opinion, `status --strict` fails on any.
|
|
130
|
+
- **Proofs resolve against the repo.** Adapters for Gherkin scenarios and tags, pytest node ids,
|
|
131
|
+
file existence, human sign-off, prototypes, citations, eval metric gates, external results files,
|
|
132
|
+
and a generic `command` hook. Feed it cucumber-json or junit XML and declared states become real.
|
|
133
|
+
- **Five states, honest semantics.** DELIVERED, PENDING, HOLE, BROKEN, UNRESOLVED. A HOLE is never
|
|
134
|
+
a failure; BROKEN always is; a proof in an absent sibling repo is UNRESOLVED, not BROKEN.
|
|
135
|
+
- **Flows.** A need with `children` rolls them up, with `seam` proofs for the behaviour between
|
|
136
|
+
steps. Any BROKEN child breaks the flow; a flow is DELIVERED only when every child and seam is
|
|
137
|
+
green.
|
|
138
|
+
- **The roll-up is machine-checked.** `tests/test_models.py` builds a
|
|
139
|
+
[musil](https://pypi.org/project/musil/) model of the lifecycle and explores every reachable
|
|
140
|
+
state against the real resolver, so the documented invariants cannot drift from the code.
|
|
141
|
+
- **Write-back that respects your files.** `set`, `capture`, `accept`, `attest`, `withdraw`, `add`
|
|
142
|
+
splice one need's block back into the original YAML, keeping other comments. All take `--dry-run`
|
|
143
|
+
and `--expect-sha`.
|
|
144
|
+
- **Boards for humans.** `status`, `show`, `brief`, `plan`, `report --format html` (a self-contained
|
|
145
|
+
interactive viewer), `portfolio`, and `diff` against a snapshot. The read commands (`status`,
|
|
146
|
+
`lint`, `show`, `brief`, `plan`, `diff`, `portfolio`, `dupes`, `recurrence`, `suggest`, `review`)
|
|
147
|
+
take `--json`; exit codes are fixed (0 ok, 1 gate failed, 2 config or load error).
|
|
148
|
+
|
|
149
|
+
prodc also models goals, releases, roles, principles, decisions, attestations, withdrawals and
|
|
150
|
+
settled answers, and ships hygiene commands (`dupes`, `recurrence`, `suggest`); the
|
|
151
|
+
[docs](https://cardona-oss.gitlab.io/prodc/) have the full surface.
|
|
152
|
+
|
|
153
|
+
## Use it with a coding agent
|
|
154
|
+
|
|
155
|
+
prodc ships a playbook that teaches an agent to act as the project's product manager: read the board, capture evidence with honest provenance, find proofs, gate before a handoff. An agent may record `assumption` or `secondary` sources; `primary` is reserved for a real user's own words.
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
prodc agent >> AGENTS.md # or: prodc agent --write AGENTS.md
|
|
159
|
+
prodc guide # the short human onboarding guide
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Links
|
|
163
|
+
|
|
164
|
+
- Repository: <https://gitlab.com/cardona-oss/prodc>
|
|
165
|
+
- PyPI: <https://pypi.org/project/prodc/>
|
|
166
|
+
- Docs: <https://cardona-oss.gitlab.io/prodc/> — [Quickstart](https://cardona-oss.gitlab.io/prodc/quickstart/), [Commands](https://cardona-oss.gitlab.io/prodc/commands/), [Model](https://cardona-oss.gitlab.io/prodc/model/), [Config](https://cardona-oss.gitlab.io/prodc/config/), [Lifecycle](https://cardona-oss.gitlab.io/prodc/lifecycle/)
|
|
167
|
+
- Changelog: [CHANGELOG.md](https://gitlab.com/cardona-oss/prodc/-/blob/main/CHANGELOG.md)
|
|
168
|
+
|
|
169
|
+
`AGENTS.md` holds build and test instructions for coding agents working on prodc itself.
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
MIT. See [LICENSE](https://gitlab.com/cardona-oss/prodc/-/blob/main/LICENSE).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "prodc"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.7"
|
|
4
4
|
description = "Product management as code: roles, flows, and features traced to evidence."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -32,9 +32,10 @@ docs = [
|
|
|
32
32
|
]
|
|
33
33
|
|
|
34
34
|
[project.urls]
|
|
35
|
-
Homepage = "https://gitlab.com/
|
|
36
|
-
Repository = "https://gitlab.com/
|
|
37
|
-
Changelog = "https://gitlab.com/
|
|
35
|
+
Homepage = "https://gitlab.com/cardona-oss/prodc"
|
|
36
|
+
Repository = "https://gitlab.com/cardona-oss/prodc"
|
|
37
|
+
Changelog = "https://gitlab.com/cardona-oss/prodc/-/blob/main/CHANGELOG.md"
|
|
38
|
+
Documentation = "https://cardona-oss.gitlab.io/prodc/"
|
|
38
39
|
|
|
39
40
|
[build-system]
|
|
40
41
|
requires = ["hatchling"]
|
|
@@ -39,6 +39,7 @@ from ..status import (
|
|
|
39
39
|
contradictions,
|
|
40
40
|
decisions_for,
|
|
41
41
|
evidence_age_days,
|
|
42
|
+
flow_step_warnings,
|
|
42
43
|
importance_revisit_due,
|
|
43
44
|
orphaned_decisions,
|
|
44
45
|
principle_report,
|
|
@@ -298,6 +299,9 @@ def cmd_lint(args: argparse.Namespace) -> int:
|
|
|
298
299
|
| {n.client for n in needs if n.client is not None}
|
|
299
300
|
)
|
|
300
301
|
warnings += tag_warnings(needs, structural_ids, cfg.duplicate_threshold)
|
|
302
|
+
# Flow step hygiene (#90): once [flows].steps declares a vocabulary, grade each journey's
|
|
303
|
+
# shape — never-terminal, out-of-order, mixed bare/stepped. Silent when no vocab is declared.
|
|
304
|
+
warnings += flow_step_warnings(needs, cfg.flow_steps)
|
|
301
305
|
if args.json:
|
|
302
306
|
print(
|
|
303
307
|
json.dumps(
|
|
@@ -120,6 +120,16 @@ class Config:
|
|
|
120
120
|
callbacks: tuple[Callback, ...] = () # [[callbacks.hooks]] — lifecycle callbacks (#71)
|
|
121
121
|
callback_state_file: str = ".prodc/state.json" # [callbacks].state_file — the state ledger
|
|
122
122
|
principle_scopes: tuple[str, ...] = ("product",) # [principles].scopes — allowed scopes (#88)
|
|
123
|
+
flow_steps: tuple[str, ...] = () # [flows].steps — ordered step vocab; () = undeclared (#90)
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def terminal_step(self) -> str | None:
|
|
127
|
+
"""The step that ends a journey: the last declared step, or None when no vocab (#90).
|
|
128
|
+
|
|
129
|
+
The single definition of 'terminal' — status/lint read it here, never recompute
|
|
130
|
+
``flow_steps[-1]`` elsewhere. Order in ``[flows].steps`` *is* the journey.
|
|
131
|
+
"""
|
|
132
|
+
return self.flow_steps[-1] if self.flow_steps else None
|
|
123
133
|
|
|
124
134
|
def importance_label(self, level: int | None) -> str | None:
|
|
125
135
|
"""The declared label for an importance level, or None if unset/undeclared (#59)."""
|
|
@@ -312,6 +322,17 @@ class Config:
|
|
|
312
322
|
else:
|
|
313
323
|
principle_scopes = ("product",)
|
|
314
324
|
|
|
325
|
+
flows_tbl = _as_dict(raw.get("flows", {}), "[flows]")
|
|
326
|
+
if "steps" in flows_tbl:
|
|
327
|
+
steps = _as_str_list(flows_tbl["steps"], "[flows].steps")
|
|
328
|
+
if not steps:
|
|
329
|
+
raise ConfigError("[flows].steps must list at least one step")
|
|
330
|
+
if len(set(steps)) != len(steps):
|
|
331
|
+
raise ConfigError("[flows].steps must not repeat a step (order is its rank)")
|
|
332
|
+
flow_steps = tuple(steps)
|
|
333
|
+
else:
|
|
334
|
+
flow_steps = ()
|
|
335
|
+
|
|
315
336
|
return cls(
|
|
316
337
|
root=root,
|
|
317
338
|
need_globs=need_globs,
|
|
@@ -326,4 +347,5 @@ class Config:
|
|
|
326
347
|
callbacks=callbacks,
|
|
327
348
|
callback_state_file=callback_state_file,
|
|
328
349
|
principle_scopes=principle_scopes,
|
|
350
|
+
flow_steps=flow_steps,
|
|
329
351
|
)
|
|
@@ -554,6 +554,7 @@ def load(config: Config) -> Model:
|
|
|
554
554
|
_check_source_roles(acc.needs, acc.principles, {p.id for p in acc.roles})
|
|
555
555
|
_check_decisions(acc.decisions, {n.id for n in acc.needs}, {p.id for p in acc.principles})
|
|
556
556
|
_check_principle_scopes(acc.principles, config.principle_scopes)
|
|
557
|
+
_check_flow_steps(acc.needs, config.flow_steps)
|
|
557
558
|
acc.needs = _symmetrize_contradicts(acc.needs)
|
|
558
559
|
return Model(
|
|
559
560
|
needs=tuple(acc.needs),
|
|
@@ -618,6 +619,27 @@ def _check_principle_scopes(principles: list[Principle], allowed: tuple[str, ...
|
|
|
618
619
|
)
|
|
619
620
|
|
|
620
621
|
|
|
622
|
+
def _check_flow_steps(needs: list[Need], allowed: tuple[str, ...]) -> None:
|
|
623
|
+
"""Every flow edge's ``step`` must be one declared in ``[flows].steps`` (#90).
|
|
624
|
+
|
|
625
|
+
Membership is integrity, not judgment — a mis-spelled step (``conlcude``) is a typo, the
|
|
626
|
+
same severity as an undeclared principle scope, so it is a hard load error. Because the
|
|
627
|
+
writer re-loads and reverts on ``LoaderError``, this also refuses to *write* a bad step.
|
|
628
|
+
Skipped entirely when no vocabulary is declared: a bare step stays free text, as it was
|
|
629
|
+
before this. Order and completeness are the lint's job, not the loader's.
|
|
630
|
+
"""
|
|
631
|
+
if not allowed:
|
|
632
|
+
return
|
|
633
|
+
allowed_set = set(allowed)
|
|
634
|
+
for need in needs:
|
|
635
|
+
for c in need.children:
|
|
636
|
+
if c.step is not None and c.step not in allowed_set:
|
|
637
|
+
raise LoaderError(
|
|
638
|
+
f"need {need.id!r}: child {c.id!r} step {c.step!r} is not declared in "
|
|
639
|
+
f"[flows].steps ({', '.join(allowed)})"
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
|
|
621
643
|
def _check_withdrawal_of(needs: list[Need]) -> None:
|
|
622
644
|
"""A withdrawal's ``of`` must name a known need (never itself); required when kind is duplicate.
|
|
623
645
|
|
|
@@ -911,6 +911,51 @@ def flow_orphan_warnings(needs: list[Need]) -> set[str]:
|
|
|
911
911
|
return {n.id for n in needs if not n.children and n.id not in in_a_flow}
|
|
912
912
|
|
|
913
913
|
|
|
914
|
+
def flow_step_warnings(needs: list[Need], steps: tuple[str, ...]) -> list[tuple[str, str]]:
|
|
915
|
+
"""Judgment-level hygiene on a flow's steps, once a ``[flows].steps`` vocabulary exists (#90).
|
|
916
|
+
|
|
917
|
+
Opt-in: silent when no vocabulary is declared — a board with no steps has no journey to
|
|
918
|
+
grade. Membership (a step word that isn't in the vocab) is the loader's hard error; this
|
|
919
|
+
catches the *shape* of the journey, keyed by flow id, as warnings:
|
|
920
|
+
|
|
921
|
+
- **never terminal**: a flow with stepped edges but none carrying the terminal step — the
|
|
922
|
+
journey never ends (Patton: the story must end). The message names the configured word.
|
|
923
|
+
- **out of order**: stepped edges whose rank decreases along ``children`` — the map runs
|
|
924
|
+
backwards. Warned once per flow.
|
|
925
|
+
- **mixed bare/stepped**: a flow where some edges carry a step and some don't — a use case
|
|
926
|
+
with a hole, which would otherwise pass clean.
|
|
927
|
+
|
|
928
|
+
A flow with zero stepped edges is exempt: it's a plain flow, not a use case (#90), and
|
|
929
|
+
warning it would punish every existing board the moment a vocabulary is declared.
|
|
930
|
+
"""
|
|
931
|
+
if not steps:
|
|
932
|
+
return []
|
|
933
|
+
terminal = steps[-1]
|
|
934
|
+
rank = {s: i for i, s in enumerate(steps)}
|
|
935
|
+
out: list[tuple[str, str]] = []
|
|
936
|
+
for flow in needs:
|
|
937
|
+
if not flow.children:
|
|
938
|
+
continue
|
|
939
|
+
stepped = [c for c in flow.children if c.step is not None]
|
|
940
|
+
if not stepped:
|
|
941
|
+
continue # a plain flow, not a use case — exempt
|
|
942
|
+
if len(stepped) != len(flow.children):
|
|
943
|
+
bare = next(c.id for c in flow.children if c.step is None)
|
|
944
|
+
out.append((flow.id, f"edge {bare!r} has no step while siblings do — a use case with a hole"))
|
|
945
|
+
if not any(c.step == terminal for c in stepped):
|
|
946
|
+
out.append(
|
|
947
|
+
(
|
|
948
|
+
flow.id,
|
|
949
|
+
f"no edge carries the terminal step {terminal!r} — the journey never ends; "
|
|
950
|
+
"add it or drop the steps",
|
|
951
|
+
)
|
|
952
|
+
)
|
|
953
|
+
ranks = [rank[c.step] for c in stepped if c.step in rank]
|
|
954
|
+
if any(b < a for a, b in pairwise(ranks)):
|
|
955
|
+
out.append((flow.id, "steps run out of order — reorder children or re-step the edges"))
|
|
956
|
+
return out
|
|
957
|
+
|
|
958
|
+
|
|
914
959
|
def shared_proof_warnings(results: list[NeedResult]) -> dict[str, str]:
|
|
915
960
|
"""Flag a need whose *every* proof is also claimed by another need (#45).
|
|
916
961
|
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Flow step vocabulary (#90 follow-up): [flows].steps config, the load-time membership
|
|
2
|
+
error, and the flow_step_warnings hygiene lint (never-terminal / out-of-order / mixed)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from prodc.config import Config, ConfigError
|
|
11
|
+
from prodc.loader import LoaderError, load
|
|
12
|
+
from prodc.model import Child, Need
|
|
13
|
+
from prodc.status import flow_step_warnings
|
|
14
|
+
|
|
15
|
+
_TOML = """
|
|
16
|
+
[prodc]
|
|
17
|
+
needs = ["needs/*.yaml"]
|
|
18
|
+
|
|
19
|
+
[flows]
|
|
20
|
+
steps = ["trigger", "act", "conclude"]
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _project(tmp_path: Path, toml: str = _TOML, needs: str = "needs: []\n") -> Path:
|
|
25
|
+
(tmp_path / "needs").mkdir(parents=True, exist_ok=True)
|
|
26
|
+
(tmp_path / "prodc.toml").write_text(toml, encoding="utf-8")
|
|
27
|
+
(tmp_path / "needs" / "a.yaml").write_text(needs, encoding="utf-8")
|
|
28
|
+
return tmp_path / "prodc.toml"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# --- config (#90) -----------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_flow_steps_default_is_empty_and_terminal_none(tmp_path: Path) -> None:
|
|
35
|
+
cfg = Config.load(_project(tmp_path, toml="[prodc]\nneeds = [\"needs/*.yaml\"]\n"))
|
|
36
|
+
assert cfg.flow_steps == ()
|
|
37
|
+
assert cfg.terminal_step is None # no vocab → nothing is terminal
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_flow_steps_parsed_ordered_and_terminal_is_last(tmp_path: Path) -> None:
|
|
41
|
+
cfg = Config.load(_project(tmp_path))
|
|
42
|
+
assert cfg.flow_steps == ("trigger", "act", "conclude")
|
|
43
|
+
assert cfg.terminal_step == "conclude" # last entry, the single definition of terminal
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_flow_steps_empty_list_rejected(tmp_path: Path) -> None:
|
|
47
|
+
toml = "[prodc]\nneeds = [\"needs/*.yaml\"]\n\n[flows]\nsteps = []\n"
|
|
48
|
+
with pytest.raises(ConfigError, match=r"\[flows\].steps must list at least one step"):
|
|
49
|
+
Config.load(_project(tmp_path, toml=toml))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_flow_steps_duplicate_rejected(tmp_path: Path) -> None:
|
|
53
|
+
toml = "[prodc]\nneeds = [\"needs/*.yaml\"]\n\n[flows]\nsteps = [\"act\", \"act\"]\n"
|
|
54
|
+
with pytest.raises(ConfigError, match="must not repeat a step"):
|
|
55
|
+
Config.load(_project(tmp_path, toml=toml))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# --- loader membership = hard error (#90) -----------------------------------
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_undeclared_step_is_load_error(tmp_path: Path) -> None:
|
|
62
|
+
needs = """
|
|
63
|
+
needs:
|
|
64
|
+
- id: F-1
|
|
65
|
+
text: a flow
|
|
66
|
+
children: [{id: US-1, step: conlcude}]
|
|
67
|
+
- id: US-1
|
|
68
|
+
text: a step
|
|
69
|
+
"""
|
|
70
|
+
with pytest.raises(LoaderError, match="step 'conlcude' is not declared in"):
|
|
71
|
+
load(Config.load(_project(tmp_path, needs=needs)))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_declared_step_loads(tmp_path: Path) -> None:
|
|
75
|
+
needs = """
|
|
76
|
+
needs:
|
|
77
|
+
- id: F-1
|
|
78
|
+
text: a flow
|
|
79
|
+
children: [{id: US-1, step: trigger}, {id: US-2, step: conclude}]
|
|
80
|
+
- id: US-1
|
|
81
|
+
text: a
|
|
82
|
+
- id: US-2
|
|
83
|
+
text: b
|
|
84
|
+
"""
|
|
85
|
+
model = load(Config.load(_project(tmp_path, needs=needs)))
|
|
86
|
+
flow = next(n for n in model.needs if n.id == "F-1")
|
|
87
|
+
assert [c.step for c in flow.children] == ["trigger", "conclude"]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_no_vocab_lets_any_step_pass(tmp_path: Path) -> None:
|
|
91
|
+
toml = "[prodc]\nneeds = [\"needs/*.yaml\"]\n"
|
|
92
|
+
needs = """
|
|
93
|
+
needs:
|
|
94
|
+
- id: F-1
|
|
95
|
+
text: a flow
|
|
96
|
+
children: [{id: US-1, step: whatever}]
|
|
97
|
+
- id: US-1
|
|
98
|
+
text: a
|
|
99
|
+
"""
|
|
100
|
+
model = load(Config.load(_project(tmp_path, toml=toml, needs=needs)))
|
|
101
|
+
assert model.needs[0].children[0].step == "whatever" # free text, unchecked
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# --- lint: shape of the journey (#90) ---------------------------------------
|
|
105
|
+
|
|
106
|
+
_STEPS = ("trigger", "act", "conclude")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_lint_silent_without_vocabulary() -> None:
|
|
110
|
+
needs = [Need(id="F-1", text="f", children=(Child("A", "trigger"),)), Need(id="A", text="a")]
|
|
111
|
+
assert flow_step_warnings(needs, ()) == []
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_lint_exempts_flow_with_no_stepped_edges() -> None:
|
|
115
|
+
needs = [Need(id="F-1", text="f", children=(Child("A"), Child("B")))]
|
|
116
|
+
assert flow_step_warnings(needs, _STEPS) == [] # a plain flow, not a use case
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_lint_warns_when_no_terminal_step() -> None:
|
|
120
|
+
needs = [Need(id="F-1", text="f", children=(Child("A", "trigger"), Child("B", "act")))]
|
|
121
|
+
warns = flow_step_warnings(needs, _STEPS)
|
|
122
|
+
assert any(nid == "F-1" and "terminal step 'conclude'" in m for nid, m in warns)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_lint_terminal_step_names_the_configured_word() -> None:
|
|
126
|
+
needs = [Need(id="F-1", text="f", children=(Child("A", "start"),))]
|
|
127
|
+
warns = flow_step_warnings(needs, ("start", "done"))
|
|
128
|
+
assert any("terminal step 'done'" in m for _, m in warns) # not hardcoded "conclude"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_lint_warns_on_out_of_order_steps() -> None:
|
|
132
|
+
needs = [
|
|
133
|
+
Need(id="F-1", text="f", children=(Child("A", "act"), Child("B", "trigger"), Child("C", "conclude")))
|
|
134
|
+
]
|
|
135
|
+
warns = flow_step_warnings(needs, _STEPS)
|
|
136
|
+
assert any("out of order" in m for _, m in warns)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_lint_warns_on_mixed_bare_and_stepped() -> None:
|
|
140
|
+
needs = [
|
|
141
|
+
Need(id="F-1", text="f", children=(Child("A", "trigger"), Child("B"), Child("C", "conclude")))
|
|
142
|
+
]
|
|
143
|
+
warns = flow_step_warnings(needs, _STEPS)
|
|
144
|
+
assert any("has no step while siblings do" in m for _, m in warns)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_lint_clean_journey_is_silent() -> None:
|
|
148
|
+
needs = [
|
|
149
|
+
Need(id="F-1", text="f", children=(Child("A", "trigger"), Child("B", "act"), Child("C", "conclude")))
|
|
150
|
+
]
|
|
151
|
+
assert flow_step_warnings(needs, _STEPS) == []
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_lint_command_surfaces_flow_step_warning(
|
|
155
|
+
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
156
|
+
) -> None:
|
|
157
|
+
from prodc.cli import main
|
|
158
|
+
|
|
159
|
+
needs = """
|
|
160
|
+
needs:
|
|
161
|
+
- id: F-1
|
|
162
|
+
text: a flow that never ends
|
|
163
|
+
children: [{id: US-1, step: trigger}, {id: US-2, step: act}]
|
|
164
|
+
- id: US-1
|
|
165
|
+
text: a
|
|
166
|
+
- id: US-2
|
|
167
|
+
text: b
|
|
168
|
+
"""
|
|
169
|
+
code = main(["lint", "--config", str(_project(tmp_path, needs=needs))])
|
|
170
|
+
out = capsys.readouterr().out
|
|
171
|
+
assert code == 0 # a shape warning is not a hard error
|
|
172
|
+
assert "terminal step 'conclude'" in out
|
prodc-0.4.6/PKG-INFO
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: prodc
|
|
3
|
-
Version: 0.4.6
|
|
4
|
-
Summary: Product management as code: roles, 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: roles, flows, and features, each traced to the evidence behind it and the test that proves it. The aim is to make product work something a whole team can share — kept in the repo, next to the code, read by the tools you already use — rather than a specialty locked in one person's head and one tracker.
|
|
30
|
-
|
|
31
|
-
prodc records that structure and checks it holds together; it does not call a model or ping a tracker. Two ideas carry it, both about not fooling yourself:
|
|
32
|
-
|
|
33
|
-
1. **A need with no evidence is an opinion.** Write down who asked for something — an interview, a ticket, an email — or prodc flags it as an opinion, because that is what it is until someone outside the room has asked.
|
|
34
|
-
2. **"Done" is a claim your tests back.** You attach a proof (a locator into your test suite), and `prodc status` reports each need as DELIVERED, PENDING, HOLE (an honest gap), or BROKEN. The status is read from the repo, not remembered from a standup.
|
|
35
|
-
|
|
36
|
-
## Install
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
pip install prodc # or: uv add prodc
|
|
40
|
-
uv sync && make check # contributors: deps + lint + typecheck + test
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## Use
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
prodc status
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
```text
|
|
50
|
-
prodc status — quickstart
|
|
51
|
-
gaps: 2 HOLE · 1 opinion coverage 1/3 (33%)
|
|
52
|
-
id GROUNDS need WARRANT
|
|
53
|
-
|
|
54
|
-
jordan (design partner) 1 DELIVERED · 2 HOLE
|
|
55
|
-
!! N-1 DELIVERED* 1/1 see which orders are past due at a glance primary:Jordan
|
|
56
|
-
N-2 HOLE export the day's delivery run sheet secondary
|
|
57
|
-
N-3 HOLE implement a settings page opinion
|
|
58
|
-
⚠ text may be a solution in disguise (starts with 'implement')
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
GROUNDS = does a test prove it. WARRANT = did a user ask for it. The gap between them is the point.
|
|
62
|
-
|
|
63
|
-
## Use it with an agent
|
|
64
|
-
|
|
65
|
-
prodc ships a playbook that teaches a coding agent to run it as the project's product manager —
|
|
66
|
-
read the board, capture evidence, record needs with honest provenance, find proofs, gate before a
|
|
67
|
-
handoff. Drop it where your agent reads project guidance:
|
|
68
|
-
|
|
69
|
-
```bash
|
|
70
|
-
prodc agent >> AGENTS.md # or: prodc agent --write AGENTS.md
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
This is how an engineer vibe-codes their way into product work: the agent does the product
|
|
74
|
-
management from inside the repo, with prodc keeping it honest.
|
|
75
|
-
|
|
76
|
-
## Docs
|
|
77
|
-
|
|
78
|
-
- [Quickstart](docs/quickstart.md) — 60 seconds to a board
|
|
79
|
-
- [Commands](docs/commands.md) — every verb, one line each
|
|
80
|
-
- [Model](docs/model.md) — the nouns (Need, Role, Goal, Release) and the Need fields
|
|
81
|
-
|
|
82
|
-
`docs/issues/` holds follow-ups. `AGENTS.md` is for coding agents.
|
prodc-0.4.6/README.md
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
# prodc
|
|
2
|
-
|
|
3
|
-
Product management as code: roles, flows, and features, each traced to the evidence behind it and the test that proves it. The aim is to make product work something a whole team can share — kept in the repo, next to the code, read by the tools you already use — rather than a specialty locked in one person's head and one tracker.
|
|
4
|
-
|
|
5
|
-
prodc records that structure and checks it holds together; it does not call a model or ping a tracker. Two ideas carry it, both about not fooling yourself:
|
|
6
|
-
|
|
7
|
-
1. **A need with no evidence is an opinion.** Write down who asked for something — an interview, a ticket, an email — or prodc flags it as an opinion, because that is what it is until someone outside the room has asked.
|
|
8
|
-
2. **"Done" is a claim your tests back.** You attach a proof (a locator into your test suite), and `prodc status` reports each need as DELIVERED, PENDING, HOLE (an honest gap), or BROKEN. The status is read from the repo, not remembered from a standup.
|
|
9
|
-
|
|
10
|
-
## Install
|
|
11
|
-
|
|
12
|
-
```bash
|
|
13
|
-
pip install prodc # or: uv add prodc
|
|
14
|
-
uv sync && make check # contributors: deps + lint + typecheck + test
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
## Use
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
prodc status
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
```text
|
|
24
|
-
prodc status — quickstart
|
|
25
|
-
gaps: 2 HOLE · 1 opinion coverage 1/3 (33%)
|
|
26
|
-
id GROUNDS need WARRANT
|
|
27
|
-
|
|
28
|
-
jordan (design partner) 1 DELIVERED · 2 HOLE
|
|
29
|
-
!! N-1 DELIVERED* 1/1 see which orders are past due at a glance primary:Jordan
|
|
30
|
-
N-2 HOLE export the day's delivery run sheet secondary
|
|
31
|
-
N-3 HOLE implement a settings page opinion
|
|
32
|
-
⚠ text may be a solution in disguise (starts with 'implement')
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
GROUNDS = does a test prove it. WARRANT = did a user ask for it. The gap between them is the point.
|
|
36
|
-
|
|
37
|
-
## Use it with an agent
|
|
38
|
-
|
|
39
|
-
prodc ships a playbook that teaches a coding agent to run it as the project's product manager —
|
|
40
|
-
read the board, capture evidence, record needs with honest provenance, find proofs, gate before a
|
|
41
|
-
handoff. Drop it where your agent reads project guidance:
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
prodc agent >> AGENTS.md # or: prodc agent --write AGENTS.md
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
This is how an engineer vibe-codes their way into product work: the agent does the product
|
|
48
|
-
management from inside the repo, with prodc keeping it honest.
|
|
49
|
-
|
|
50
|
-
## Docs
|
|
51
|
-
|
|
52
|
-
- [Quickstart](docs/quickstart.md) — 60 seconds to a board
|
|
53
|
-
- [Commands](docs/commands.md) — every verb, one line each
|
|
54
|
-
- [Model](docs/model.md) — the nouns (Need, Role, Goal, Release) and the Need fields
|
|
55
|
-
|
|
56
|
-
`docs/issues/` holds follow-ups. `AGENTS.md` is for coding agents.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|