prodc 0.3.83__tar.gz → 0.4.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.4.0/CHANGELOG.md +78 -0
- {prodc-0.3.83 → prodc-0.4.0}/PKG-INFO +1 -1
- {prodc-0.3.83 → prodc-0.4.0}/pyproject.toml +1 -1
- {prodc-0.3.83 → prodc-0.4.0}/src/prodc/agent.md +1 -1
- {prodc-0.3.83 → prodc-0.4.0}/src/prodc/board.py +2 -1
- {prodc-0.3.83 → prodc-0.4.0}/src/prodc/cli/app.py +20 -10
- {prodc-0.3.83 → prodc-0.4.0}/src/prodc/cli/commands.py +54 -52
- {prodc-0.3.83 → prodc-0.4.0}/src/prodc/cli/render.py +13 -21
- {prodc-0.3.83 → prodc-0.4.0}/src/prodc/cli/serialize.py +1 -22
- {prodc-0.3.83 → prodc-0.4.0}/src/prodc/loader.py +53 -60
- {prodc-0.3.83 → prodc-0.4.0}/src/prodc/model.py +13 -31
- {prodc-0.3.83 → prodc-0.4.0}/src/prodc/report.py +34 -3
- {prodc-0.3.83 → prodc-0.4.0}/src/prodc/report_html.py +52 -1
- {prodc-0.3.83 → prodc-0.4.0}/src/prodc/status.py +9 -29
- {prodc-0.3.83 → prodc-0.4.0}/src/prodc/writer.py +60 -1
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_contradicts.py +11 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_decisions.py +41 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_loader.py +15 -41
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_report.py +33 -2
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_report_html.py +21 -1
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_source_role.py +4 -4
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_status.py +27 -19
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_withdrawn.py +58 -0
- prodc-0.3.83/CHANGELOG.md +0 -7
- {prodc-0.3.83 → prodc-0.4.0}/.gitignore +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/LICENSE +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/README.md +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/examples/quickstart/features/orders.feature +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/examples/quickstart/needs/jordan.yaml +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/examples/quickstart/prodc.toml +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/src/prodc/__init__.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/src/prodc/adapters.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/src/prodc/callbacks.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/src/prodc/cli/__init__.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/src/prodc/cli/common.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/src/prodc/config.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/src/prodc/dedup.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/src/prodc/guide.md +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/src/prodc/py.typed +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_adapters.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_answer.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_callbacks.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_citation.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_claim_gating.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_cli.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_command_hook.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_dedup.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_description.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_drafts.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_evalgate.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_external_adapter.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_gitlab_source.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_importance.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_models.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_needref.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_principles.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_relink.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_review.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_roles.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_sources.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_staleness.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_suggest.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_variants.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_who_variants.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_writer.py +0 -0
- {prodc-0.3.83 → prodc-0.4.0}/tests/test_writer_comments.py +0 -0
prodc-0.4.0/CHANGELOG.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes are documented here. Versioning is [SemVer](https://semver.org/).
|
|
4
|
+
|
|
5
|
+
## [0.4.0] - 2026-10-16
|
|
6
|
+
|
|
7
|
+
The OST is whole, and the board cannot lie.
|
|
8
|
+
|
|
9
|
+
0.4 finishes the Opportunity-Solution Tree — a need now carries the solution
|
|
10
|
+
chosen for it and the vision that solution rests on — and closes the gaps that
|
|
11
|
+
let a board read greener than the evidence warranted.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **Product principles** (`principles:`) — the product vision as a cited, rippling noun.
|
|
16
|
+
`add principle`, `revise principle`, and `add principle --from-need` (promote a need,
|
|
17
|
+
keeping its text and sources). An `out_of_scope` withdrawal must cite the principle it
|
|
18
|
+
rests on (`withdraw --principle`); revising a principle re-opens decisions taken under it.
|
|
19
|
+
- **Decision noun** (`decisions:`) — the solution chosen for a need over the alternatives
|
|
20
|
+
rejected (OST, not an ADR). `add decision --needs … --alternative … --consequences …
|
|
21
|
+
--principles …`; `withdraw decision <id>` / `reinstate decision <id>` supersede and restore.
|
|
22
|
+
Lint flags an orphan decision (built on no need) and a stale one (its principle moved under it).
|
|
23
|
+
- **`contradicts`** — a symmetric need↔need edge for two needs in tension; `lint` flags a pair
|
|
24
|
+
left both-live, because the vision has to decide between them.
|
|
25
|
+
- **`prodc guide`** — a short human CLI onboarding guide, sibling to `prodc agent`;
|
|
26
|
+
`--write PATH` drops it into the repo.
|
|
27
|
+
- **`report` renders Principles and Decisions** — both the markdown document and the
|
|
28
|
+
interactive HTML viewer now carry the two new sections.
|
|
29
|
+
- **`status` shows unresolved contradictions** on the text board.
|
|
30
|
+
|
|
31
|
+
### Changed (breaking)
|
|
32
|
+
|
|
33
|
+
- **`Need.label` removed** (#52). Express it with a declared `role` + `client`:
|
|
34
|
+
John@Sincerely playing the `owner` role, not a free-text label. A board still carrying
|
|
35
|
+
`label:` fails to load with a message naming the replacement.
|
|
36
|
+
*Migration:* declare the role in `roles:`, set `role:` and `client:` on the need, drop `label:`.
|
|
37
|
+
- **The GSN `argument` graph is cut.** Its job is absorbed by `claim` + `proofs` +
|
|
38
|
+
attestation: state the warrant as `need.claim`, prove it with proofs, review it with
|
|
39
|
+
`prodc attest`. A board carrying an `argument:` key fails to load with that guidance.
|
|
40
|
+
*Migration:* move each argument node's warrant onto the need's `claim`; delete `argument:`.
|
|
41
|
+
- **Attestation `verdict` is an enum** — `pass` / `concerns` / `fail`. Only `pass` clears a
|
|
42
|
+
need; `concerns`, `fail`, or no verdict at all now warns, for a human and an AI reviewer
|
|
43
|
+
alike. This was the false-green bug: a recorded-but-not-passing review read as settled.
|
|
44
|
+
*Migration:* an attestation with no `verdict:` now warns — add `verdict: pass` where the
|
|
45
|
+
review did pass.
|
|
46
|
+
- **A `duplicate` withdrawal must name its original** — `Withdrawal.of` is required when
|
|
47
|
+
`kind: duplicate` (`withdraw --of <id>`), so a duplicate always points at what it duplicates.
|
|
48
|
+
|
|
49
|
+
> The `argument:` and `label:` load errors are tombstones for the 0.4 cutover; they are
|
|
50
|
+
> scheduled for removal in 0.5.
|
|
51
|
+
|
|
52
|
+
## [0.3.x]
|
|
53
|
+
|
|
54
|
+
The 0.3 line turned prodc from a need-and-proof tracker into a full product-and-research
|
|
55
|
+
board. Highlights, newest first:
|
|
56
|
+
|
|
57
|
+
- **`prodc report`** — the board as a shareable document: a readable markdown file, or a
|
|
58
|
+
single self-contained, interactive, read-only HTML viewer (filter, search, regroup,
|
|
59
|
+
print to PDF) with no external assets.
|
|
60
|
+
- **Research boards, unified** — no profile fork. An `answer` axis (settled questions with a
|
|
61
|
+
direction, including a settled-NO), a `need:` question→question DAG, and `evalgate` /
|
|
62
|
+
`citation` proof adapters, gated by claim-aware lint so one board holds product bets and
|
|
63
|
+
research questions the same way.
|
|
64
|
+
- **Lifecycle callbacks** — `prodc sync` fires a configured command for each need whose state
|
|
65
|
+
changed since the last sync, ledger-tracked and retried on failure.
|
|
66
|
+
- **Portfolio** — roll many boards into one scoreboard with a totals footer; discover boards
|
|
67
|
+
under a directory.
|
|
68
|
+
- **Drafts and review** — staged needs (`draft: true`), `prodc review` to triage them, an
|
|
69
|
+
age gate, and portfolio exclusion.
|
|
70
|
+
- **Withdrawal kinds** — `wont_do` / `out_of_scope` / `duplicate` on a decision-against a need.
|
|
71
|
+
- **Source kinds** — the academic `primary` / `secondary` / `assumption` ladder; a need with
|
|
72
|
+
no primary or secondary source is an opinion. Old kind names still load, mapped on.
|
|
73
|
+
- **Role noun** — a named function in the product's world; `who` + `client` is an instance of it.
|
|
74
|
+
- **Importance rubric**, **free-form `description`**, and the **cli/ package split**.
|
|
75
|
+
|
|
76
|
+
## [0.1.0]
|
|
77
|
+
|
|
78
|
+
Initial release.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: prodc
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Product management as code: roles, flows, and features traced to evidence.
|
|
5
5
|
Project-URL: Homepage, https://gitlab.com/jorgeecardona/prodc
|
|
6
6
|
Project-URL: Repository, https://gitlab.com/jorgeecardona/prodc
|
|
@@ -89,7 +89,7 @@ Read first, then act, then check.
|
|
|
89
89
|
| act on a need going green (notify a tracker, etc.) | `prodc sync` — runs the `[callbacks]` commands for needs whose state changed since the last sync (`--dry-run` to preview) |
|
|
90
90
|
| gate in CI | `prodc lint --strict` |
|
|
91
91
|
|
|
92
|
-
Less often: `prodc
|
|
92
|
+
Less often: `prodc portfolio <path>…`
|
|
93
93
|
(roll up many boards into one scoreboard + totals; a path is a prodc.toml or a dir to discover
|
|
94
94
|
boards under), and `prodc diff <snapshot.json>` (what changed since a
|
|
95
95
|
saved `status --json`). `prodc status` also takes `--goal <id>`, `--columns id,state,text,warrant`,
|
|
@@ -94,7 +94,6 @@ def need_payload(r: NeedResult, *, flavor: Literal["board", "html"] = "board") -
|
|
|
94
94
|
"state": r.state.value,
|
|
95
95
|
"must": need.must,
|
|
96
96
|
"who": need.who or "(unassigned)",
|
|
97
|
-
"label": need.label,
|
|
98
97
|
"client": need.client,
|
|
99
98
|
"opinion": need.is_opinion,
|
|
100
99
|
"warrant": strongest.value if strongest is not None else "opinion",
|
|
@@ -109,6 +108,7 @@ def need_payload(r: NeedResult, *, flavor: Literal["board", "html"] = "board") -
|
|
|
109
108
|
"date": need.withdrawn.date,
|
|
110
109
|
"kind": need.withdrawn.kind.value,
|
|
111
110
|
"principle": need.withdrawn.principle,
|
|
111
|
+
"of": need.withdrawn.of,
|
|
112
112
|
}
|
|
113
113
|
if need.withdrawn
|
|
114
114
|
else None
|
|
@@ -164,6 +164,7 @@ def need_payload(r: NeedResult, *, flavor: Literal["board", "html"] = "board") -
|
|
|
164
164
|
"date": need.withdrawn.date,
|
|
165
165
|
"kind": need.withdrawn.kind.value,
|
|
166
166
|
"principle": need.withdrawn.principle,
|
|
167
|
+
"of": need.withdrawn.of,
|
|
167
168
|
}
|
|
168
169
|
if need.withdrawn
|
|
169
170
|
else None
|
|
@@ -16,7 +16,6 @@ from .commands import (
|
|
|
16
16
|
cmd_accept,
|
|
17
17
|
cmd_add,
|
|
18
18
|
cmd_agent,
|
|
19
|
-
cmd_argument,
|
|
20
19
|
cmd_attest,
|
|
21
20
|
cmd_brief,
|
|
22
21
|
cmd_capture,
|
|
@@ -144,12 +143,6 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
144
143
|
)
|
|
145
144
|
pl.set_defaults(func=cmd_plan)
|
|
146
145
|
|
|
147
|
-
ar = sub.add_parser("argument", help="show the GSN assurance graph and its defects")
|
|
148
|
-
ar.add_argument("--config", default="prodc.toml", help="path to prodc.toml")
|
|
149
|
-
ar.add_argument("--json", action="store_true", help="machine-readable output")
|
|
150
|
-
ar.add_argument("--strict", action="store_true", help="exit 1 if the graph has any defect")
|
|
151
|
-
ar.set_defaults(func=cmd_argument)
|
|
152
|
-
|
|
153
146
|
st2 = sub.add_parser("set", help="set or append one field on a need (writes YAML)")
|
|
154
147
|
st2.add_argument("need_id", help="the need id to edit, e.g. N-1")
|
|
155
148
|
st2.add_argument("assignment", help="key=value, or key+=value to append (proofs/children)")
|
|
@@ -317,7 +310,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
317
310
|
at.add_argument("--date", required=True, help="when (ISO, e.g. 2026-10-05)")
|
|
318
311
|
at.add_argument("--ai", action="store_true", help="the reviewer is an AI model, not a human")
|
|
319
312
|
at.add_argument(
|
|
320
|
-
"--verdict",
|
|
313
|
+
"--verdict",
|
|
314
|
+
choices=["pass", "concerns", "fail"],
|
|
315
|
+
default=None,
|
|
316
|
+
help="the reviewer's conclusion; only pass clears the warrant (anything else warns)",
|
|
321
317
|
)
|
|
322
318
|
at.add_argument("--comment", default=None, help="the review's feedback, kept on the record")
|
|
323
319
|
at.add_argument("--config", default="prodc.toml", help="path to prodc.toml")
|
|
@@ -354,7 +350,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
354
350
|
wd = sub.add_parser(
|
|
355
351
|
"withdraw", help="record a decision against a need — withdrawn / won't-do (#74)"
|
|
356
352
|
)
|
|
357
|
-
wd.add_argument("
|
|
353
|
+
wd.add_argument("target", help="the need id to withdraw, or 'decision' to withdraw a decision")
|
|
354
|
+
wd.add_argument(
|
|
355
|
+
"decision_id", nargs="?", default=None, help="the decision id (when target is 'decision')"
|
|
356
|
+
)
|
|
358
357
|
wd.add_argument(
|
|
359
358
|
"--reason", required=True, help="why it was decided against (kept on the record)"
|
|
360
359
|
)
|
|
@@ -366,6 +365,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
366
365
|
"or duplicate (#74)",
|
|
367
366
|
)
|
|
368
367
|
wd.add_argument("--date", default=None, help="when (ISO, e.g. 2026-10-10; default: today)")
|
|
368
|
+
wd.add_argument(
|
|
369
|
+
"--of",
|
|
370
|
+
default=None,
|
|
371
|
+
metavar="ID",
|
|
372
|
+
help="the need this one duplicates (validated); required with --kind duplicate",
|
|
373
|
+
)
|
|
369
374
|
wd.add_argument(
|
|
370
375
|
"--principle",
|
|
371
376
|
default=None,
|
|
@@ -389,7 +394,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
389
394
|
ri = sub.add_parser(
|
|
390
395
|
"reinstate", help="remove a need's withdrawal, putting it back on the board (#74)"
|
|
391
396
|
)
|
|
392
|
-
ri.add_argument(
|
|
397
|
+
ri.add_argument(
|
|
398
|
+
"target", help="the need id to reinstate, or 'decision' to reinstate a decision"
|
|
399
|
+
)
|
|
400
|
+
ri.add_argument(
|
|
401
|
+
"decision_id", nargs="?", default=None, help="the decision id (when target is 'decision')"
|
|
402
|
+
)
|
|
393
403
|
ri.add_argument("--config", default="prodc.toml", help="path to prodc.toml")
|
|
394
404
|
ri.add_argument("--json", action="store_true", help="machine-readable output")
|
|
395
405
|
ri.add_argument(
|
|
@@ -11,7 +11,7 @@ import argparse
|
|
|
11
11
|
import importlib.resources
|
|
12
12
|
import json
|
|
13
13
|
import sys
|
|
14
|
-
from dataclasses import asdict
|
|
14
|
+
from dataclasses import asdict, replace
|
|
15
15
|
from datetime import date
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
from typing import cast
|
|
@@ -28,14 +28,13 @@ from ..dedup import (
|
|
|
28
28
|
who_variants,
|
|
29
29
|
)
|
|
30
30
|
from ..loader import LoaderError, load
|
|
31
|
-
from ..model import
|
|
31
|
+
from ..model import Need, NeedResult, NeedState, board_needs
|
|
32
32
|
from ..report import render_markdown
|
|
33
33
|
from ..report_html import render_html
|
|
34
34
|
from ..status import (
|
|
35
35
|
ProofSuggestion,
|
|
36
36
|
RelinkSuggestion,
|
|
37
37
|
applicable_needs,
|
|
38
|
-
argument_defects,
|
|
39
38
|
contradictions,
|
|
40
39
|
decisions_for,
|
|
41
40
|
evidence_age_days,
|
|
@@ -63,11 +62,13 @@ from ..writer import (
|
|
|
63
62
|
delete_need,
|
|
64
63
|
link_variant_group,
|
|
65
64
|
reinstate,
|
|
65
|
+
reinstate_decision,
|
|
66
66
|
relink_proof,
|
|
67
67
|
revise_principle,
|
|
68
68
|
set_field,
|
|
69
69
|
set_importance,
|
|
70
70
|
withdraw,
|
|
71
|
+
withdraw_decision,
|
|
71
72
|
)
|
|
72
73
|
from .common import (
|
|
73
74
|
RepoRow,
|
|
@@ -82,9 +83,9 @@ from .render import (
|
|
|
82
83
|
COLUMNS,
|
|
83
84
|
provenance_counts,
|
|
84
85
|
render,
|
|
85
|
-
render_argument,
|
|
86
86
|
render_brief,
|
|
87
87
|
render_columns,
|
|
88
|
+
render_contradictions,
|
|
88
89
|
render_decisions,
|
|
89
90
|
render_goals,
|
|
90
91
|
render_handoffs,
|
|
@@ -100,7 +101,6 @@ from .render import (
|
|
|
100
101
|
summary_line,
|
|
101
102
|
)
|
|
102
103
|
from .serialize import (
|
|
103
|
-
argument_json,
|
|
104
104
|
brief_json,
|
|
105
105
|
dup_payload,
|
|
106
106
|
plan_json,
|
|
@@ -178,20 +178,6 @@ def _annotate_draft_age_warnings(results: list[NeedResult], cfg: Config) -> None
|
|
|
178
178
|
)
|
|
179
179
|
|
|
180
180
|
|
|
181
|
-
def _annotate_label_deprecation(results: list[NeedResult]) -> None:
|
|
182
|
-
"""Nudge needs still carrying the free-text `label` toward role + client (#52, deprecation).
|
|
183
|
-
|
|
184
|
-
Since Role (#52) carries the archetype and `client` the org, the per-need `label` is redundant —
|
|
185
|
-
"design partner, Sincerely" is `role` + `client`. Soft only: `label` is still parsed and still
|
|
186
|
-
renders; this flags it so a board can migrate off it before it is removed.
|
|
187
|
-
"""
|
|
188
|
-
for r in results:
|
|
189
|
-
if r.need.label:
|
|
190
|
-
r.warnings.append(
|
|
191
|
-
f"'label' ({r.need.label!r}) is deprecated — express it with role + client (#52)"
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
|
|
195
181
|
def _lint_errors(results: list[NeedResult]) -> list[str]:
|
|
196
182
|
"""Hard problems a lint gate fails on: a dangling proof, or a must-have with no evidence."""
|
|
197
183
|
errors: list[str] = []
|
|
@@ -241,7 +227,6 @@ def cmd_lint(args: argparse.Namespace) -> int:
|
|
|
241
227
|
)
|
|
242
228
|
_annotate_importance_warnings(results, cfg)
|
|
243
229
|
_annotate_draft_age_warnings(results, cfg)
|
|
244
|
-
_annotate_label_deprecation(results)
|
|
245
230
|
# The principle ripple: a need withdrawn citing a principle that was revised *after* the decision
|
|
246
231
|
# comes back up — the frame it was judged against has changed since. A warning, never a hard
|
|
247
232
|
# error; prodc flags, the PM re-decides.
|
|
@@ -406,6 +391,9 @@ def cmd_status(args: argparse.Namespace) -> int:
|
|
|
406
391
|
print(render_principles(board.principles))
|
|
407
392
|
if model.decisions:
|
|
408
393
|
print(render_decisions(list(model.decisions), needs=list(model.needs)))
|
|
394
|
+
unresolved = [c for c in contradictions(list(model.needs)) if c.unresolved]
|
|
395
|
+
if unresolved:
|
|
396
|
+
print(render_contradictions(unresolved))
|
|
409
397
|
if orphan_map:
|
|
410
398
|
print(render_orphans(orphan_map))
|
|
411
399
|
|
|
@@ -705,16 +693,32 @@ def cmd_withdraw(args: argparse.Namespace) -> int:
|
|
|
705
693
|
return 2
|
|
706
694
|
try:
|
|
707
695
|
cfg = Config.load(cfg_path)
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
696
|
+
if args.target == "decision":
|
|
697
|
+
# noun-first: ids are unique per noun, so a need and a decision may share one — the
|
|
698
|
+
# noun disambiguates (matches `add decision` / `revise principle`).
|
|
699
|
+
if not args.decision_id:
|
|
700
|
+
print("prodc: withdraw decision needs a decision id", file=sys.stderr)
|
|
701
|
+
return 2
|
|
702
|
+
edit = withdraw_decision(
|
|
703
|
+
cfg,
|
|
704
|
+
args.decision_id,
|
|
705
|
+
reason=args.reason,
|
|
706
|
+
date=args.date or date.today().isoformat(),
|
|
707
|
+
expect_sha=args.expect_sha,
|
|
708
|
+
dry_run=args.dry_run,
|
|
709
|
+
)
|
|
710
|
+
else:
|
|
711
|
+
edit = withdraw(
|
|
712
|
+
cfg,
|
|
713
|
+
args.target,
|
|
714
|
+
reason=args.reason,
|
|
715
|
+
date=args.date or date.today().isoformat(),
|
|
716
|
+
kind=args.kind,
|
|
717
|
+
principle=args.principle,
|
|
718
|
+
of=args.of,
|
|
719
|
+
expect_sha=args.expect_sha,
|
|
720
|
+
dry_run=args.dry_run,
|
|
721
|
+
)
|
|
718
722
|
except (ConfigError, WriteError) as exc:
|
|
719
723
|
print(f"prodc: {exc}", file=sys.stderr)
|
|
720
724
|
return 2
|
|
@@ -729,12 +733,23 @@ def cmd_reinstate(args: argparse.Namespace) -> int:
|
|
|
729
733
|
return 2
|
|
730
734
|
try:
|
|
731
735
|
cfg = Config.load(cfg_path)
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
736
|
+
if args.target == "decision":
|
|
737
|
+
if not args.decision_id:
|
|
738
|
+
print("prodc: reinstate decision needs a decision id", file=sys.stderr)
|
|
739
|
+
return 2
|
|
740
|
+
edit = reinstate_decision(
|
|
741
|
+
cfg,
|
|
742
|
+
args.decision_id,
|
|
743
|
+
expect_sha=args.expect_sha,
|
|
744
|
+
dry_run=args.dry_run,
|
|
745
|
+
)
|
|
746
|
+
else:
|
|
747
|
+
edit = reinstate(
|
|
748
|
+
cfg,
|
|
749
|
+
args.target,
|
|
750
|
+
expect_sha=args.expect_sha,
|
|
751
|
+
dry_run=args.dry_run,
|
|
752
|
+
)
|
|
738
753
|
except (ConfigError, WriteError) as exc:
|
|
739
754
|
print(f"prodc: {exc}", file=sys.stderr)
|
|
740
755
|
return 2
|
|
@@ -921,15 +936,6 @@ def cmd_capture(args: argparse.Namespace) -> int:
|
|
|
921
936
|
return emit_edit(edit, args.dry_run, args.json)
|
|
922
937
|
|
|
923
938
|
|
|
924
|
-
def cmd_argument(args: argparse.Namespace) -> int:
|
|
925
|
-
"""Show the GSN assurance graph and its defects (unsupported claim, dangling assumption)."""
|
|
926
|
-
loaded = load_or_report(args)
|
|
927
|
-
if loaded is None:
|
|
928
|
-
return 2
|
|
929
|
-
_cfg, model = loaded
|
|
930
|
-
defects = argument_defects(list(model.argument))
|
|
931
|
-
print(argument_json(model, defects) if args.json else render_argument(model, defects))
|
|
932
|
-
return 1 if (args.strict and defects) else 0
|
|
933
939
|
|
|
934
940
|
|
|
935
941
|
def _repo_row(config_path: str) -> RepoRow:
|
|
@@ -1041,14 +1047,10 @@ def cmd_report(args: argparse.Namespace) -> int:
|
|
|
1041
1047
|
return 2
|
|
1042
1048
|
cfg, model = loaded
|
|
1043
1049
|
if args.client:
|
|
1050
|
+
# replace keeps every other noun (principles, decisions, …); a hand-rolled Model(...)
|
|
1051
|
+
# silently dropped them, which breaks the moment the report renders them (#0.4).
|
|
1044
1052
|
scoped = applicable_needs(list(model.needs), args.client)
|
|
1045
|
-
model =
|
|
1046
|
-
needs=tuple(scoped),
|
|
1047
|
-
goals=model.goals,
|
|
1048
|
-
releases=model.releases,
|
|
1049
|
-
roles=model.roles,
|
|
1050
|
-
argument=model.argument,
|
|
1051
|
-
)
|
|
1053
|
+
model = replace(model, needs=tuple(scoped))
|
|
1052
1054
|
if args.format == "html":
|
|
1053
1055
|
text = render_html(cfg, model, client=args.client, rev=git_rev(cfg.root))
|
|
1054
1056
|
else:
|
|
@@ -14,7 +14,6 @@ from ..board import warrant as _warrant
|
|
|
14
14
|
from ..model import (
|
|
15
15
|
Decision,
|
|
16
16
|
Goal,
|
|
17
|
-
Model,
|
|
18
17
|
Need,
|
|
19
18
|
NeedResult,
|
|
20
19
|
NeedState,
|
|
@@ -23,6 +22,7 @@ from ..model import (
|
|
|
23
22
|
ears_shape,
|
|
24
23
|
)
|
|
25
24
|
from ..status import (
|
|
25
|
+
Contradiction,
|
|
26
26
|
Handoff,
|
|
27
27
|
PrincipleReport,
|
|
28
28
|
Recurrence,
|
|
@@ -139,11 +139,9 @@ def render(results: list[NeedResult], wide: bool = False) -> str:
|
|
|
139
139
|
]
|
|
140
140
|
for who in sorted(groups):
|
|
141
141
|
rs = sorted(groups[who], key=state_priority)
|
|
142
|
-
label = next((x.need.label for x in rs if x.need.label), None)
|
|
143
142
|
counts = {s: sum(1 for r in rs if r.state is s) for s in NeedState}
|
|
144
143
|
head = " ".join(f"{counts[s]} {_LABEL[s]}" for s in NeedState if counts[s])
|
|
145
|
-
|
|
146
|
-
out.append(f"\n{title} {head}")
|
|
144
|
+
out.append(f"\n{who} {head}")
|
|
147
145
|
out.append(f" {_provenance_line(rs)}\n")
|
|
148
146
|
for r in rs:
|
|
149
147
|
out.append(
|
|
@@ -289,6 +287,14 @@ def render_principles(reports: list[PrincipleReport]) -> str:
|
|
|
289
287
|
return "\n".join(out)
|
|
290
288
|
|
|
291
289
|
|
|
290
|
+
def render_contradictions(contras: list[Contradiction]) -> str:
|
|
291
|
+
"""Unresolved contradictions on the board: two needs in tension the vision has not decided."""
|
|
292
|
+
out = ["\ncontradictions (two needs in tension — the vision has not decided):"]
|
|
293
|
+
for c in contras:
|
|
294
|
+
out.append(f" {c.a.id} vs {c.b.id} {c.a.text[:32]} / {c.b.text[:32]}")
|
|
295
|
+
return "\n".join(out)
|
|
296
|
+
|
|
297
|
+
|
|
292
298
|
def render_decisions(decisions: list[Decision], needs: list[Need]) -> str:
|
|
293
299
|
"""The solution chosen for each need (OST), what it rejected, and the needs it serves.
|
|
294
300
|
|
|
@@ -319,7 +325,7 @@ def render_show(
|
|
|
319
325
|
) -> str:
|
|
320
326
|
"""One need as a citable answer — the artifact you drop into a thread."""
|
|
321
327
|
need = r.need
|
|
322
|
-
who =
|
|
328
|
+
who = need.who or "(unassigned)"
|
|
323
329
|
who += f" — {need.role}" if need.role else ""
|
|
324
330
|
nm = f" {r.delivered}/{r.total}" if r.total else ""
|
|
325
331
|
lines = [
|
|
@@ -332,7 +338,8 @@ def render_show(
|
|
|
332
338
|
lines.append(f" answer: {need.answer.value}")
|
|
333
339
|
if need.withdrawn:
|
|
334
340
|
w = need.withdrawn
|
|
335
|
-
|
|
341
|
+
of = f" of {w.of}" if w.of else ""
|
|
342
|
+
tag = "" if w.kind.value == "wont_do" else f" [{w.kind.value}{of}]"
|
|
336
343
|
per = f" per {w.principle}" if w.principle else ""
|
|
337
344
|
lines.append(f" withdrawn{tag}{per}: {w.reason} ({w.date})")
|
|
338
345
|
if need.description:
|
|
@@ -450,21 +457,6 @@ def render_plan(
|
|
|
450
457
|
return "\n".join(out)
|
|
451
458
|
|
|
452
459
|
|
|
453
|
-
def render_argument(model: Model, defects: list[str]) -> str:
|
|
454
|
-
out = [f"argument — GSN graph: {len(model.argument)} node(s), {len(defects)} defect(s)"]
|
|
455
|
-
for node in model.argument:
|
|
456
|
-
links = ""
|
|
457
|
-
if node.supported_by:
|
|
458
|
-
links += f" ←{','.join(node.supported_by)}"
|
|
459
|
-
if node.in_context_of:
|
|
460
|
-
links += f" ⊙{','.join(node.in_context_of)}"
|
|
461
|
-
about = f" [{node.about}]" if node.about else ""
|
|
462
|
-
out.append(f" {node.id:<12} {node.kind.value:<13} {node.text[:44]}{about}{links}")
|
|
463
|
-
for d in defects:
|
|
464
|
-
out.append(f" ⚠ {d}")
|
|
465
|
-
return "\n".join(out)
|
|
466
|
-
|
|
467
|
-
|
|
468
460
|
def render_portfolio(rows: list[RepoRow], totals: PortfolioTotals | None = None) -> str:
|
|
469
461
|
if not rows:
|
|
470
462
|
return " no boards found"
|
|
@@ -12,7 +12,7 @@ import json
|
|
|
12
12
|
from ..board import need_payload
|
|
13
13
|
from ..board import warrant as _warrant
|
|
14
14
|
from ..dedup import DupPair, VariantGroup
|
|
15
|
-
from ..model import Decision, Goal,
|
|
15
|
+
from ..model import Decision, Goal, NeedResult, NeedState, Release, Role
|
|
16
16
|
from ..status import Handoff, PrincipleReport, Recurrence, Summary, rank
|
|
17
17
|
from .common import brief_open
|
|
18
18
|
|
|
@@ -147,7 +147,6 @@ def show_json(r: NeedResult, decisions: list[Decision] | None = None) -> str:
|
|
|
147
147
|
payload = need_payload(r) # the shared core — never drifts from the board
|
|
148
148
|
payload.update(
|
|
149
149
|
{
|
|
150
|
-
"label": need.label,
|
|
151
150
|
"decisions": [_decision_payload(d) for d in (decisions or [])],
|
|
152
151
|
"sources": [
|
|
153
152
|
{
|
|
@@ -223,26 +222,6 @@ def plan_json(
|
|
|
223
222
|
)
|
|
224
223
|
|
|
225
224
|
|
|
226
|
-
def argument_json(model: Model, defects: list[str]) -> str:
|
|
227
|
-
return json.dumps(
|
|
228
|
-
{
|
|
229
|
-
"nodes": [
|
|
230
|
-
{
|
|
231
|
-
"id": n.id,
|
|
232
|
-
"kind": n.kind.value,
|
|
233
|
-
"text": n.text,
|
|
234
|
-
"supported_by": list(n.supported_by),
|
|
235
|
-
"in_context_of": list(n.in_context_of),
|
|
236
|
-
"about": n.about,
|
|
237
|
-
}
|
|
238
|
-
for n in model.argument
|
|
239
|
-
],
|
|
240
|
-
"defects": defects,
|
|
241
|
-
},
|
|
242
|
-
indent=2,
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
|
|
246
225
|
def variant_payload(g: VariantGroup) -> dict[str, object]:
|
|
247
226
|
return {
|
|
248
227
|
"score": round(g.score, 4),
|