prodc 0.3.7__tar.gz → 0.3.8__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: prodc
3
- Version: 0.3.7
3
+ Version: 0.3.8
4
4
  Summary: Product management as code: personas, 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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "prodc"
3
- version = "0.3.7"
3
+ version = "0.3.8"
4
4
  description = "Product management as code: personas, flows, and features traced to evidence."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -13,6 +13,7 @@ import shutil
13
13
  import sys
14
14
  from dataclasses import asdict, dataclass
15
15
  from pathlib import Path
16
+ from typing import cast
16
17
 
17
18
  from . import __version__
18
19
  from .config import Config, ConfigError
@@ -380,6 +381,51 @@ def _cmd_portfolio(args: argparse.Namespace) -> int:
380
381
  return 2 if any(r.error is not None for r in rows) else 0
381
382
 
382
383
 
384
+ def _cmd_diff(args: argparse.Namespace) -> int:
385
+ """Diff the current board against a prior `status --json` snapshot."""
386
+ loaded = _load_or_report(args)
387
+ if loaded is None:
388
+ return 2
389
+ cfg, needs = loaded
390
+ current = {r.need.id: r.state.value for r in resolve(needs, cfg.adapters, cfg.source_adapters)}
391
+
392
+ snap = Path(args.snapshot)
393
+ try:
394
+ payload: object = json.loads(snap.read_text(encoding="utf-8"))
395
+ except (OSError, ValueError) as exc:
396
+ print(f"prodc: cannot read snapshot {snap}: {exc}", file=sys.stderr)
397
+ return 2
398
+ raw_needs = cast("dict[str, object]", payload).get("needs") if isinstance(payload, dict) else None
399
+ old: dict[str, str] = {}
400
+ for n in cast("list[object]", raw_needs) if isinstance(raw_needs, list) else []:
401
+ if isinstance(n, dict):
402
+ entry = cast("dict[str, object]", n)
403
+ nid, st = entry.get("id"), entry.get("state")
404
+ if isinstance(nid, str) and isinstance(st, str):
405
+ old[nid] = st
406
+
407
+ new = {i: current[i] for i in sorted(current.keys() - old.keys())}
408
+ removed = sorted(old.keys() - current.keys())
409
+ changed = {
410
+ i: [old[i], current[i]] for i in sorted(old.keys() & current.keys()) if old[i] != current[i]
411
+ }
412
+ if args.json:
413
+ print(json.dumps({"new": new, "removed": removed, "changed": changed}, indent=2))
414
+ return 0
415
+ if not (new or removed or changed):
416
+ print("no changes since snapshot")
417
+ return 0
418
+ out: list[str] = []
419
+ for i, st in new.items():
420
+ out.append(f" new {i:<10} ({st})")
421
+ for i in removed:
422
+ out.append(f" removed {i}")
423
+ for i, (was, now) in changed.items():
424
+ out.append(f" changed {i:<10} {was} → {now}")
425
+ print("\n".join(out))
426
+ return 0
427
+
428
+
383
429
  def _git_rev(root: Path) -> str | None:
384
430
  head = root / ".git" / "HEAD"
385
431
  try:
@@ -437,6 +483,12 @@ def main(argv: list[str] | None = None) -> int:
437
483
  pf.add_argument("--json", action="store_true", help="machine-readable output")
438
484
  pf.set_defaults(func=_cmd_portfolio)
439
485
 
486
+ df = sub.add_parser("diff", help="diff the current board against a prior status --json snapshot")
487
+ df.add_argument("snapshot", help="path to a saved `prodc status --json` file")
488
+ df.add_argument("--config", default="prodc.toml", help="path to prodc.toml")
489
+ df.add_argument("--json", action="store_true", help="machine-readable output")
490
+ df.set_defaults(func=_cmd_diff)
491
+
440
492
  args = parser.parse_args(argv)
441
493
  return int(args.func(args))
442
494
 
@@ -179,6 +179,30 @@ def test_portfolio_bad_config_exits_2(tmp_path: Path, capsys: pytest.CaptureFixt
179
179
  assert code == 2
180
180
 
181
181
 
182
+ def test_diff_no_changes(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
183
+ toml = str(_project(tmp_path))
184
+ main(["status", "--config", toml, "--json"])
185
+ snap = tmp_path / "snap.json"
186
+ snap.write_text(capsys.readouterr().out, encoding="utf-8")
187
+ main(["diff", str(snap), "--config", toml])
188
+ assert "no changes" in capsys.readouterr().out
189
+
190
+
191
+ def test_diff_reports_new_need(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
192
+ toml = str(_project(tmp_path))
193
+ main(["status", "--config", toml, "--json"])
194
+ snap = tmp_path / "snap.json"
195
+ snap.write_text(capsys.readouterr().out, encoding="utf-8")
196
+ # add a brand-new need, then diff against the snapshot
197
+ (tmp_path / "needs" / "a.yaml").write_text(
198
+ _NEEDS + " - id: X-9\n text: a newly added need\n", encoding="utf-8"
199
+ )
200
+ code = main(["diff", str(snap), "--config", toml])
201
+ out = capsys.readouterr().out
202
+ assert code == 0
203
+ assert "new" in out and "X-9" in out
204
+
205
+
182
206
  def test_proxy_source_kind_warrant(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
183
207
  import json
184
208
 
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