prodc 0.4.1__tar.gz → 0.4.2__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.1 → prodc-0.4.2}/PKG-INFO +1 -1
- {prodc-0.4.1 → prodc-0.4.2}/pyproject.toml +1 -1
- {prodc-0.4.1 → prodc-0.4.2}/src/prodc/cli/render.py +4 -1
- {prodc-0.4.1 → prodc-0.4.2}/src/prodc/cli/serialize.py +1 -1
- {prodc-0.4.1 → prodc-0.4.2}/src/prodc/loader.py +20 -4
- {prodc-0.4.1 → prodc-0.4.2}/src/prodc/model.py +16 -1
- {prodc-0.4.1 → prodc-0.4.2}/src/prodc/status.py +6 -6
- {prodc-0.4.1 → prodc-0.4.2}/src/prodc/writer.py +14 -1
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_loader.py +20 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_models.py +2 -2
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_status.py +7 -7
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_writer.py +27 -0
- {prodc-0.4.1 → prodc-0.4.2}/.gitignore +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/CHANGELOG.md +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/LICENSE +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/README.md +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/examples/quickstart/features/orders.feature +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/examples/quickstart/needs/jordan.yaml +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/examples/quickstart/prodc.toml +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/src/prodc/__init__.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/src/prodc/adapters.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/src/prodc/agent.md +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/src/prodc/board.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/src/prodc/callbacks.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/src/prodc/cli/__init__.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/src/prodc/cli/app.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/src/prodc/cli/commands.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/src/prodc/cli/common.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/src/prodc/config.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/src/prodc/dedup.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/src/prodc/guide.md +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/src/prodc/py.typed +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/src/prodc/report.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/src/prodc/report_html.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_adapters.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_answer.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_callbacks.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_citation.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_claim_gating.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_cli.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_command_hook.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_contradicts.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_decisions.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_dedup.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_description.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_drafts.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_evalgate.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_external_adapter.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_gitlab_source.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_importance.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_needref.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_principles.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_relink.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_report.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_report_html.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_review.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_roles.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_source_role.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_sources.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_staleness.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_suggest.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_variants.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_who_variants.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_withdrawn.py +0 -0
- {prodc-0.4.1 → prodc-0.4.2}/tests/test_writer_comments.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: prodc
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
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
|
|
@@ -382,7 +382,10 @@ def render_show(
|
|
|
382
382
|
else:
|
|
383
383
|
lines.append(" sources: (none — opinion)")
|
|
384
384
|
if need.children:
|
|
385
|
-
|
|
385
|
+
rendered = ", ".join(
|
|
386
|
+
c.id if c.step is None else f"{c.id}@{c.step}" for c in need.children
|
|
387
|
+
)
|
|
388
|
+
lines.append(f" children: {rendered}")
|
|
386
389
|
if need.contradicts:
|
|
387
390
|
lines.append(f" contradicts: {', '.join(need.contradicts)}")
|
|
388
391
|
if need.seam:
|
|
@@ -160,7 +160,7 @@ def show_json(r: NeedResult, decisions: list[Decision] | None = None) -> str:
|
|
|
160
160
|
for s in need.sources
|
|
161
161
|
],
|
|
162
162
|
"proofs": list(need.proofs),
|
|
163
|
-
"children":
|
|
163
|
+
"children": [{"id": c.id, "step": c.step} for c in need.children],
|
|
164
164
|
"seam": list(need.seam),
|
|
165
165
|
}
|
|
166
166
|
)
|
|
@@ -21,6 +21,7 @@ from .model import (
|
|
|
21
21
|
Answer,
|
|
22
22
|
Attestation,
|
|
23
23
|
AttestationKind,
|
|
24
|
+
Child,
|
|
24
25
|
Decision,
|
|
25
26
|
Goal,
|
|
26
27
|
Model,
|
|
@@ -203,6 +204,21 @@ def _source(raw: object, where: str) -> Source:
|
|
|
203
204
|
)
|
|
204
205
|
|
|
205
206
|
|
|
207
|
+
def _child(raw: object, where: str) -> Child:
|
|
208
|
+
"""Parse one flow edge: a bare id string, or a ``{id, step}`` mapping (#90).
|
|
209
|
+
|
|
210
|
+
A bare ``US-050`` stays valid (``Child("US-050")`` with no step), so existing flows load
|
|
211
|
+
unchanged; a mapping adds the optional ``step`` the edge plays in the journey.
|
|
212
|
+
"""
|
|
213
|
+
if isinstance(raw, str):
|
|
214
|
+
return Child(id=raw)
|
|
215
|
+
tbl = _as_mapping(raw, where)
|
|
216
|
+
return Child(
|
|
217
|
+
id=_as_str(tbl.get("id"), f"{where}.id"),
|
|
218
|
+
step=_opt_str(tbl.get("step"), f"{where}.step"),
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
206
222
|
def _need(
|
|
207
223
|
raw: object,
|
|
208
224
|
where: str,
|
|
@@ -243,7 +259,7 @@ def _need(
|
|
|
243
259
|
for i, p in enumerate(_as_list(tbl.get("proofs"), f"{where}.proofs"))
|
|
244
260
|
)
|
|
245
261
|
children = tuple(
|
|
246
|
-
|
|
262
|
+
_child(c, f"{where}.children[{i}]")
|
|
247
263
|
for i, c in enumerate(_as_list(tbl.get("children"), f"{where}.children"))
|
|
248
264
|
)
|
|
249
265
|
seam = tuple(
|
|
@@ -564,9 +580,9 @@ def _check_children(needs: list[Need]) -> None:
|
|
|
564
580
|
"""Every id in a need's ``children`` must name a known need."""
|
|
565
581
|
known = {n.id for n in needs}
|
|
566
582
|
for need in needs:
|
|
567
|
-
for
|
|
568
|
-
if
|
|
569
|
-
raise LoaderError(f"need {need.id!r}: child {
|
|
583
|
+
for c in need.children:
|
|
584
|
+
if c.id not in known:
|
|
585
|
+
raise LoaderError(f"need {need.id!r}: child {c.id!r} is not a known need id")
|
|
570
586
|
|
|
571
587
|
|
|
572
588
|
def _check_withdrawal_of(needs: list[Need]) -> None:
|
|
@@ -257,6 +257,21 @@ class Withdrawal:
|
|
|
257
257
|
of: str | None = None # id of the need this one duplicates (required when kind is duplicate)
|
|
258
258
|
|
|
259
259
|
|
|
260
|
+
@dataclass(frozen=True)
|
|
261
|
+
class Child:
|
|
262
|
+
"""One edge of a flow: the sub-need and, optionally, the ``step`` it plays in the journey (#90).
|
|
263
|
+
|
|
264
|
+
A flow's ``children`` are ordered, and a use case ("job") is just a flow with a ``role`` set and
|
|
265
|
+
a ``step`` on each edge (locate / execute / conclude). The step annotates the *edge*, not the
|
|
266
|
+
sub-need — the same need can be the ``execute`` step of one flow and the ``locate`` of another —
|
|
267
|
+
so it lives here, not on :class:`Need`. A bare ``US-050`` in YAML loads as ``Child("US-050")``
|
|
268
|
+
with no step, so every existing flow keeps working unchanged.
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
id: str
|
|
272
|
+
step: str | None = None
|
|
273
|
+
|
|
274
|
+
|
|
260
275
|
@dataclass(frozen=True)
|
|
261
276
|
class Need:
|
|
262
277
|
"""What a named person wants, with its provenance and its proofs."""
|
|
@@ -269,7 +284,7 @@ class Need:
|
|
|
269
284
|
must: bool = False
|
|
270
285
|
sources: tuple[Source, ...] = ()
|
|
271
286
|
proofs: tuple[str, ...] = () # locators, "<adapter>:<payload>" (leaf needs)
|
|
272
|
-
children: tuple[
|
|
287
|
+
children: tuple[Child, ...] = () # ordered sub-need edges (a flow is a Need with children)
|
|
273
288
|
seam: tuple[str, ...] = () # end-to-end proof locators for a flow's between-steps behaviour
|
|
274
289
|
client: str | None = None # which client this instance is for; None = shared baseline
|
|
275
290
|
variant_of: str | None = None # id of the baseline need this specialises; None = standalone
|
|
@@ -293,11 +293,11 @@ class Resolver:
|
|
|
293
293
|
warnings: list[str] = []
|
|
294
294
|
child_states: list[NeedState] = []
|
|
295
295
|
declared = False
|
|
296
|
-
for
|
|
297
|
-
if
|
|
298
|
-
warnings.append(f"unknown child id '{
|
|
296
|
+
for c in need.children:
|
|
297
|
+
if c.id not in self._by_id:
|
|
298
|
+
warnings.append(f"unknown child id '{c.id}'")
|
|
299
299
|
continue
|
|
300
|
-
cr = self.resolve(
|
|
300
|
+
cr = self.resolve(c.id)
|
|
301
301
|
child_states.append(cr.state)
|
|
302
302
|
declared = declared or cr.declared
|
|
303
303
|
delivered = sum(1 for s in child_states if s is NeedState.DELIVERED)
|
|
@@ -806,7 +806,7 @@ def handoffs(needs: list[Need]) -> list[Handoff]:
|
|
|
806
806
|
for flow in needs:
|
|
807
807
|
if not flow.children:
|
|
808
808
|
continue
|
|
809
|
-
steps = [by_id[c] for c in flow.children if c in by_id]
|
|
809
|
+
steps = [by_id[c.id] for c in flow.children if c.id in by_id]
|
|
810
810
|
for a, b in pairwise(steps):
|
|
811
811
|
if a.role and b.role and a.role != b.role:
|
|
812
812
|
out.append(Handoff(flow, a, b, a.role, b.role))
|
|
@@ -844,7 +844,7 @@ def flow_orphan_warnings(needs: list[Need]) -> set[str]:
|
|
|
844
844
|
return set()
|
|
845
845
|
in_a_flow: set[str] = set()
|
|
846
846
|
for flow in flows:
|
|
847
|
-
in_a_flow.update(flow.children)
|
|
847
|
+
in_a_flow.update(c.id for c in flow.children)
|
|
848
848
|
return {n.id for n in needs if not n.children and n.id not in in_a_flow}
|
|
849
849
|
|
|
850
850
|
|
|
@@ -365,6 +365,19 @@ def _coerce(value: str) -> object:
|
|
|
365
365
|
return value
|
|
366
366
|
|
|
367
367
|
|
|
368
|
+
def _child_item(raw: str) -> object:
|
|
369
|
+
"""Parse a ``children+=`` value into a flow edge: ``id`` stays a bare string, ``id@step`` a map.
|
|
370
|
+
|
|
371
|
+
Mirrors the loader's dual shape (#90): ``children+=US-050`` appends the bare id (unstepped),
|
|
372
|
+
``children+=US-052@execute`` appends ``{id: US-052, step: execute}``. A bare id keeps the file's
|
|
373
|
+
existing plain-string style; the ``@`` suffix mirrors the ``need:A@yes`` proof-locator idiom.
|
|
374
|
+
"""
|
|
375
|
+
head, sep, step = raw.partition("@")
|
|
376
|
+
if not sep:
|
|
377
|
+
return raw
|
|
378
|
+
return {"id": head.strip(), "step": step.strip()}
|
|
379
|
+
|
|
380
|
+
|
|
368
381
|
def _apply_assignment(need: dict[str, object], assignment: str, path: Path) -> None:
|
|
369
382
|
if "+=" in assignment:
|
|
370
383
|
key, _, raw = assignment.partition("+=")
|
|
@@ -377,7 +390,7 @@ def _apply_assignment(need: dict[str, object], assignment: str, path: Path) -> N
|
|
|
377
390
|
if not isinstance(current, list):
|
|
378
391
|
raise WriteError(f"{path.name}: {key!r} is not a list")
|
|
379
392
|
items = cast("list[object]", current)
|
|
380
|
-
items.append(raw.strip())
|
|
393
|
+
items.append(_child_item(raw.strip()) if key == "children" else raw.strip())
|
|
381
394
|
need[key] = items
|
|
382
395
|
return
|
|
383
396
|
key, sep, raw = assignment.partition("=")
|
|
@@ -109,6 +109,26 @@ def test_flow_with_proofs_raises(tmp_path: Path) -> None:
|
|
|
109
109
|
load_needs(cfg)
|
|
110
110
|
|
|
111
111
|
|
|
112
|
+
def test_children_load_as_bare_id_or_stepped_mapping(tmp_path: Path) -> None:
|
|
113
|
+
# #90: a flow's children take either a bare id (unstepped) or a {id, step} mapping, mixed.
|
|
114
|
+
cfg = _config(
|
|
115
|
+
tmp_path,
|
|
116
|
+
(
|
|
117
|
+
"a.yaml",
|
|
118
|
+
"needs:\n"
|
|
119
|
+
" - id: F-1\n"
|
|
120
|
+
" text: a flow\n"
|
|
121
|
+
" children:\n"
|
|
122
|
+
" - C-1\n"
|
|
123
|
+
" - {id: C-2, step: execute}\n"
|
|
124
|
+
" - id: C-1\n text: one\n"
|
|
125
|
+
" - id: C-2\n text: two\n",
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
flow = next(n for n in load_needs(cfg) if n.id == "F-1")
|
|
129
|
+
assert [(c.id, c.step) for c in flow.children] == [("C-1", None), ("C-2", "execute")]
|
|
130
|
+
|
|
131
|
+
|
|
112
132
|
def test_variant_scope_shared_baseline_across_clients(tmp_path: Path) -> None:
|
|
113
133
|
cfg = _config(
|
|
114
134
|
tmp_path,
|
|
@@ -14,7 +14,7 @@ from dataclasses import dataclass, replace
|
|
|
14
14
|
from musil import Action, Model, check, explore, reaches
|
|
15
15
|
|
|
16
16
|
from prodc.adapters import Adapter, ProofResult
|
|
17
|
-
from prodc.model import Decision, Need, NeedState, ProofState, Withdrawal, board_needs
|
|
17
|
+
from prodc.model import Child, Decision, Need, NeedState, ProofState, Withdrawal, board_needs
|
|
18
18
|
from prodc.status import Resolver, active_decisions
|
|
19
19
|
|
|
20
20
|
|
|
@@ -141,7 +141,7 @@ def flow_state(f: Flow) -> NeedState:
|
|
|
141
141
|
"""The NeedState prodc's real aggregate gives this set of children + seam."""
|
|
142
142
|
children = [_child_need(f"c{i}", s) for i, s in enumerate(f.children)]
|
|
143
143
|
seam = tuple(f"stub:{s}" for s in f.seam if s != "none")
|
|
144
|
-
flow = Need(id="flow", text="f", children=tuple(c.id for c in children), seam=seam)
|
|
144
|
+
flow = Need(id="flow", text="f", children=tuple(Child(c.id) for c in children), seam=seam)
|
|
145
145
|
return Resolver([flow, *children], _ADAPTERS).resolve("flow").state
|
|
146
146
|
|
|
147
147
|
|
|
@@ -6,7 +6,7 @@ from dataclasses import dataclass
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
8
|
from prodc.adapters import FrontmatterAdapter, ProofResult, SourceAdapter
|
|
9
|
-
from prodc.model import Need, NeedState, ProofState, Source, SourceKind
|
|
9
|
+
from prodc.model import Child, Need, NeedState, ProofState, Source, SourceKind
|
|
10
10
|
from prodc.status import resolve
|
|
11
11
|
|
|
12
12
|
|
|
@@ -52,7 +52,7 @@ def test_flow_aggregates_two_of_three() -> None:
|
|
|
52
52
|
Need(id="C-2", text="two", proofs=("web:2",)),
|
|
53
53
|
Need(id="C-3", text="three", proofs=()), # a HOLE
|
|
54
54
|
)
|
|
55
|
-
flow = Need(id="F-1", text="a flow", children=("C-1", "C-2", "C-3"))
|
|
55
|
+
flow = Need(id="F-1", text="a flow", children=(Child("C-1"), Child("C-2"), Child("C-3")))
|
|
56
56
|
results = {r.need.id: r for r in resolve([flow, *children], {"web": _delivered()})}
|
|
57
57
|
flow_result = results["F-1"]
|
|
58
58
|
assert flow_result.state is NeedState.PENDING # not all delivered → pending
|
|
@@ -234,7 +234,7 @@ def test_coverage_counts_leaves_not_flow_parents() -> None:
|
|
|
234
234
|
from prodc.status import summarize
|
|
235
235
|
|
|
236
236
|
needs = [
|
|
237
|
-
Need(id="F-1", text="roast day flow", children=("L-1", "L-2")),
|
|
237
|
+
Need(id="F-1", text="roast day flow", children=(Child("L-1"), Child("L-2"))),
|
|
238
238
|
Need(id="L-1", text="step one", proofs=("x:1",)),
|
|
239
239
|
Need(id="L-2", text="step two", proofs=("x:1",)),
|
|
240
240
|
]
|
|
@@ -296,7 +296,7 @@ def test_flow_orphan_warning() -> None:
|
|
|
296
296
|
from prodc.status import flow_orphan_warnings
|
|
297
297
|
|
|
298
298
|
needs = [
|
|
299
|
-
Need(id="F-1", text="a flow", children=("L-1",)),
|
|
299
|
+
Need(id="F-1", text="a flow", children=(Child("L-1"),)),
|
|
300
300
|
Need(id="L-1", text="in the flow"),
|
|
301
301
|
Need(id="L-2", text="in no flow"),
|
|
302
302
|
]
|
|
@@ -354,7 +354,7 @@ def test_flow_seam_proof_gates_delivered() -> None:
|
|
|
354
354
|
# all children delivered but the seam proof pending → flow is PENDING, not DELIVERED (#44)
|
|
355
355
|
from prodc.adapters import Adapter
|
|
356
356
|
|
|
357
|
-
flow = Need(id="F-1", text="roast day", children=("L-1", "L-2"), seam=("web:seam#E2E",))
|
|
357
|
+
flow = Need(id="F-1", text="roast day", children=(Child("L-1"), Child("L-2")), seam=("web:seam#E2E",))
|
|
358
358
|
children = [
|
|
359
359
|
Need(id="L-1", text="a", proofs=("x:1",)),
|
|
360
360
|
Need(id="L-2", text="b", proofs=("x:1",)),
|
|
@@ -368,14 +368,14 @@ def test_flow_seam_proof_gates_delivered() -> None:
|
|
|
368
368
|
|
|
369
369
|
|
|
370
370
|
def test_flow_seam_green_delivers() -> None:
|
|
371
|
-
flow = Need(id="F-1", text="roast day", children=("L-1",), seam=("web:seam#E2E",))
|
|
371
|
+
flow = Need(id="F-1", text="roast day", children=(Child("L-1"),), seam=("web:seam#E2E",))
|
|
372
372
|
child = Need(id="L-1", text="a", proofs=("x:1",))
|
|
373
373
|
by_id = {r.need.id: r for r in resolve([flow, child], {"x": _delivered(), "web": _delivered()})}
|
|
374
374
|
assert by_id["F-1"].state is NeedState.DELIVERED
|
|
375
375
|
|
|
376
376
|
|
|
377
377
|
def test_flow_seam_broken_breaks_flow() -> None:
|
|
378
|
-
flow = Need(id="F-1", text="roast day", children=("L-1",), seam=("nope:x",))
|
|
378
|
+
flow = Need(id="F-1", text="roast day", children=(Child("L-1"),), seam=("nope:x",))
|
|
379
379
|
child = Need(id="L-1", text="a", proofs=("x:1",))
|
|
380
380
|
by_id = {r.need.id: r for r in resolve([flow, child], {"x": _delivered()})}
|
|
381
381
|
assert by_id["F-1"].state is NeedState.BROKEN # no 'nope' adapter → seam BROKEN
|
|
@@ -275,6 +275,33 @@ def test_delete_need_removes_the_block_keeping_others(tmp_path: Path) -> None:
|
|
|
275
275
|
assert "# keep me" in text # header comment survives the splice
|
|
276
276
|
|
|
277
277
|
|
|
278
|
+
def test_children_append_stepped_edge(tmp_path: Path) -> None:
|
|
279
|
+
# #90: `children+=id@step` appends a {id, step} edge; a bare id stays a plain string.
|
|
280
|
+
body = (
|
|
281
|
+
"needs:\n"
|
|
282
|
+
" - id: F-1\n text: a flow\n children: [N-2]\n"
|
|
283
|
+
" - id: N-2\n text: step two\n proofs: ['x:1']\n"
|
|
284
|
+
" - id: N-3\n text: step three\n proofs: ['x:2']\n"
|
|
285
|
+
)
|
|
286
|
+
cfg = _proj(tmp_path, body)
|
|
287
|
+
set_field(cfg, "F-1", "children+=N-3@conclude", expect_sha=None, dry_run=False)
|
|
288
|
+
flow = next(n for n in load(cfg).needs if n.id == "F-1")
|
|
289
|
+
assert [(c.id, c.step) for c in flow.children] == [("N-2", None), ("N-3", "conclude")]
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def test_children_append_bare_id(tmp_path: Path) -> None:
|
|
293
|
+
body = (
|
|
294
|
+
"needs:\n"
|
|
295
|
+
" - id: F-1\n text: a flow\n children: [N-2]\n"
|
|
296
|
+
" - id: N-2\n text: step two\n proofs: ['x:1']\n"
|
|
297
|
+
" - id: N-3\n text: step three\n proofs: ['x:2']\n"
|
|
298
|
+
)
|
|
299
|
+
cfg = _proj(tmp_path, body)
|
|
300
|
+
set_field(cfg, "F-1", "children+=N-3", expect_sha=None, dry_run=False)
|
|
301
|
+
flow = next(n for n in load(cfg).needs if n.id == "F-1")
|
|
302
|
+
assert [(c.id, c.step) for c in flow.children] == [("N-2", None), ("N-3", None)]
|
|
303
|
+
|
|
304
|
+
|
|
278
305
|
def test_delete_need_unknown_id_rejected(tmp_path: Path) -> None:
|
|
279
306
|
cfg = _proj(tmp_path, "needs:\n - id: N-1\n text: t\n")
|
|
280
307
|
with pytest.raises(WriteError, match="no need 'N-9'"):
|
|
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
|