Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions Sources/SpecGraph/Documentation.docc/ProductWorkspacePilots.md
Original file line number Diff line number Diff line change
Expand Up @@ -713,9 +713,13 @@ structural-depth observations can now become ordinary
`idea_to_spec_clarification_requests` with `event_storming_hints.*` targets.
Accepted typed `entries[]` answers flow through the existing review-only rerun
overlay and can increase actor, command, event, policy, or constraint depth.
Workflow-topology depth remains diagnostic until a patch-capable topology
overlay exists. The loop does not introduce a score, prompt-agent execution,
canonical spec mutation, Ontology writes, or promotion authority.
Proposal `0208` adds the patch-capable workflow-topology overlay for the
previously diagnostic `workflow_edge_count` case. Flat workflow topology can
now ask for typed `event_storming_hints.workflow_relations` answers. Rerun
preview validates relation names and source/target event-storming refs before
materialization copies only review-only, non-dependency workflow edges into the
candidate graph preview. The loop does not introduce a score, prompt-agent
execution, canonical spec mutation, Ontology writes, or promotion authority.

The next slices are Platform awareness and a `local-subscription-control` demo
pass. Platform may use the report as an explanatory preflight signal before
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# 0208 Safe Workflow Topology Repair

Source draft for proposal `0208`.

See `docs/proposals/0208_safe_workflow_topology_repair.md` for the realized
proposal text and acceptance criteria.
21 changes: 21 additions & 0 deletions docs/product_workspace_graph_versioning_roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -1593,6 +1593,27 @@ event-storming entries rather than patching existing command/event relations.
The requests are `review_required`, not blocking, and accepted typed answers
feed the existing review-only rerun overlay as event-storming hints.

## Safe Workflow Topology Repair

Status: implemented producer-side in proposal `0208`; SpecSpace can layer typed
relation controls over the same request/answer/rerun contracts.

Proposal `0208` closes the deferred workflow-topology gap from proposal `0207`.
When `workflow_edge_count` is zero and the current event-storming intake already
has linkable entries, SpecGraph can emit one `workflow_topology_gap`
clarification request targeting `event_storming_hints.workflow_relations`.
Accepted answers contain typed `relations[]` such as command-event,
actor-command, event-policy, event-constraint, policy-command, or
constraint-command links.

The rerun preview validates relation names and source/target kinds against the
current event-storming preview before patching relation fields. Rerun
materialization copies only validated `review_only` edges with
`materialization_dependency: false` into the candidate graph preview, so
workflow topology remains review evidence and never becomes canonical spec
mutation, Ontology acceptance, Git authority, or YAML implementation
dependency.

## Real Idea Entry Request Import

Status: implemented producer-side in proposal `0202`; downstream UI/Platform
Expand Down
104 changes: 104 additions & 0 deletions docs/proposals/0208_safe_workflow_topology_repair.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# 0208 Safe Workflow Topology Repair

## Status

Draft / producer-side topology repair slice.

## Summary

Proposal `0207` lets shallow `candidate_structure_depth` observations ask for
missing event-storming entries, but it intentionally leaves
`workflow_edge_count = 0` diagnostic-only because the rerun overlay only
appended entries.

This slice adds a bounded, typed relation overlay for workflow topology repair.
It lets accepted clarification answers connect already-published
event-storming entries through review-only relations such as actor-command,
command-event, event-policy, event-constraint, policy-command, and
constraint-command links.

## Decision

When `groups.candidate_structure_depth.workflow_edge_count = 0` and the
maturity report has a loaded event-storming intake source with enough existing
entries to link, `tools/idea_to_spec_clarification_requests.py` may emit:

- `kind: workflow_topology_gap`;
- `target_ref: event_storming_hints.workflow_relations`;
- `suggested_answer_shape: event_storming_relation[]`;
- `suggested_actions: ["answer_question", "defer_candidate"]`.

Accepted answers feed `tools/idea_to_spec_answer_rerun_input.py` as:

```json
{
"intake_overlay": {
"event_storming_hints": [
{
"target_ref": "event_storming_hints.workflow_relations",
"value": {
"workflow_relations": [
{
"relation": "command_emits_event",
"source_ref": "command.record-item",
"target_ref": "event.item-recorded"
}
]
}
}
]
}
}
```

`tools/idea_to_spec_rerun_preview.py` validates every relation against the
current event-storming preview before applying it:

- `actor_triggers_command`: actor -> command;
- `command_emits_event`: command -> domain event;
- `event_informs_policy`: domain event -> policy;
- `event_informs_constraint`: domain event -> constraint;
- `constraint_applies_to_command`: constraint -> command;
- `policy_applies_to_command`: policy -> command.

Unknown, missing, stale, or wrong-kind refs become `review_required` findings.
Valid relation hints patch only event-storming relation fields
(`actor_refs`, `produces_event_refs`, `trigger_event_refs`, `command_refs`) and
emit `workflow_topology_preview.workflow_edges`.

`tools/idea_to_spec_rerun_materialization.py` may copy validated workflow edges
into the review-only `candidate_graph_preview` when every edge has
`review_only: true` and `materialization_dependency: false`.

## Authority Boundary

This slice is review-only. It does not:

- change Metrics schemas or validator semantics;
- create a structural-depth score;
- turn workflow topology into a promotion, approval, or Pre-SIB gate;
- infer fuzzy topology links;
- mutate event-storming intake source artifacts;
- mutate candidate or canonical specs;
- write Ontology packages or lockfiles;
- accept ontology terms;
- execute prompt agents;
- create Git branches, commits, or pull requests;
- publish read models.

Workflow topology edges are product-review evidence. They must not become YAML
`depends_on` or implementation dependencies.

## Acceptance Criteria

- Zero workflow-edge count can create one review-required topology
clarification request only when existing entries can be linked.
- Accepted `relations[]` answers are preserved in the rerun input overlay.
- Rerun preview rejects unknown relations, missing refs, and wrong source/target
kinds.
- Rerun preview patches relation fields only after validation.
- Rerun preview emits review-only workflow topology edges.
- Rerun materialization copies only review-only, non-dependency workflow edges
into the candidate graph preview.
- Metrics remains objective telemetry; topology repair is producer/consumer
behavior.
46 changes: 46 additions & 0 deletions tests/test_idea_to_spec_answer_rerun_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,52 @@ def test_answer_rerun_input_accepts_array_valued_event_storming_answer() -> None
assert report["summary"]["intake_overlay_count"] == 1


def test_answer_rerun_input_preserves_workflow_relation_hints() -> None:
module = load_module(
RERUN_TOOL_PATH,
"idea_to_spec_answer_rerun_input_workflow_relations_test",
)
answer_report = ready_answer_report_with_answer(
answer_kind="answer_question",
value={
"relations": [
{
"relation": "command_emits_event",
"source_ref": "command.record-pantry-item",
"target_ref": "event.pantry-item-recorded",
}
]
},
request_kind="workflow_topology_gap",
target_ref="event_storming_hints.workflow_relations",
)

report = module.build_idea_to_spec_answer_rerun_input(
answers_report=answer_report,
)

hints = report["rerun_input_overlay"]["intake_overlay"]["event_storming_hints"]
assert hints == [
{
"answer_kind": "answer_question",
"request_id": "clarification.repair.repair-review-unresolved-gaps",
"request_kind": "workflow_topology_gap",
"target_artifact": "runs/candidate_spec_graph.json",
"target_ref": "event_storming_hints.workflow_relations",
"value": {
"workflow_relations": [
{
"relation": "command_emits_event",
"source_ref": "command.record-pantry-item",
"target_ref": "event.pantry-item-recorded",
}
]
},
}
]
assert report["summary"]["intake_overlay_count"] == 1


def test_answer_rerun_input_routes_non_ontology_reject_to_candidate_hints() -> None:
module = load_module(
RERUN_TOOL_PATH,
Expand Down
7 changes: 6 additions & 1 deletion tests/test_idea_to_spec_clarification_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ def test_clarification_requests_adds_depth_driven_event_storming_requests() -> N
assert report["readiness"]["ready"] is False
assert report["readiness"]["review_state"] == "clarification_review_required"
assert report["summary"]["blocking_request_count"] == 0
assert report["summary"]["review_required_request_count"] == 2
assert report["summary"]["review_required_request_count"] == 3
actor_request = request_by_id(report, "clarification.depth.actors")
assert actor_request["kind"] == "event_storming_gap"
assert actor_request["severity"] == "review_required"
Expand All @@ -359,6 +359,11 @@ def test_clarification_requests_adds_depth_driven_event_storming_requests() -> N
assert actor_request["source_findings"][0]["evidence"]["source_ref"] == (
"runs/custom/idea_maturity_metrics_report.json#groups.candidate_structure_depth.actor_count"
)
topology_request = request_by_id(report, "clarification.depth.workflow-topology")
assert topology_request["kind"] == "workflow_topology_gap"
assert topology_request["target_ref"] == "event_storming_hints.workflow_relations"
assert topology_request["suggested_answer_shape"] == "event_storming_relation[]"
assert topology_request["source_findings"][0]["evidence"]["proposal_id"] == "0208"


def test_clarification_requests_missing_depth_group_is_not_faked() -> None:
Expand Down
88 changes: 88 additions & 0 deletions tests/test_idea_to_spec_rerun_materialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,68 @@ def candidate_graph_artifact() -> dict[str, object]:
}


def candidate_graph_for_workflow_topology() -> dict[str, object]:
graph = candidate_graph_artifact()
graph["nodes"] = [
{
"id": "candidate-spec.product-boundary",
"kind": "product_boundary",
"source_event_refs": [],
"gaps": [],
},
{
"id": "candidate-spec.record-pantry-item",
"kind": "behavior_requirement",
"source_event_refs": ["command.record-pantry-item"],
"gaps": [],
},
]
graph["edges"] = []
return graph


def rerun_preview_with_workflow_topology() -> dict[str, object]:
preview = rerun_preview_artifact()
preview["rerun_preview"]["workflow_topology_preview"] = {
"workflow_edges": [
{
"id": "edge.command.record-pantry-item.pantry-item-recorded",
"from": "candidate-spec.record-pantry-item",
"to": "candidate-spec.product-boundary",
"relation": "command_emits_event",
"source_event_refs": [
"command.record-pantry-item",
"event.pantry-item-recorded",
],
"command_ref": "command.record-pantry-item",
"event_ref": "event.pantry-item-recorded",
"review_only": True,
"materialization_dependency": False,
},
{
"id": "edge.command.record-pantry-item.pantry-item-reviewed",
"from": "candidate-spec.record-pantry-item",
"to": "candidate-spec.product-boundary",
"relation": "command_emits_event",
"source_event_refs": [
"command.record-pantry-item",
"event.pantry-item-reviewed",
],
"command_ref": "command.record-pantry-item",
"event_ref": "event.pantry-item-reviewed",
"review_only": True,
"materialization_dependency": False,
},
],
"workflow_edge_count": 2,
"review_only": True,
"materialization_dependency": False,
}
preview["rerun_preview"]["ontology_gap_preview"]["resolved_ontology_gaps"] = []
preview["rerun_preview"]["ontology_gap_preview"]["unresolved_ontology_gaps"] = []
return preview


def candidate_graph_with_candidate_gaps() -> dict[str, object]:
graph = candidate_graph_artifact()
graph["nodes"] = [
Expand Down Expand Up @@ -280,6 +342,32 @@ def test_rerun_materialization_removes_resolved_candidate_gap_in_preview() -> No
assert "private prompt trace" not in json.dumps(report)


def test_rerun_materialization_merges_review_only_workflow_topology_edges() -> None:
module = load_module()

report = module.build_idea_to_spec_rerun_materialization(
rerun_preview=rerun_preview_with_workflow_topology(),
candidate_graph=candidate_graph_for_workflow_topology(),
)

assert report["readiness"]["ready"] is True
preview = report["materialization_preview"]["candidate_graph_preview"]
edges = preview["edges"]
assert len(edges) == 2
edge = edges[0]
assert edge["relation"] == "command_emits_event"
assert edge["review_only"] is True
assert edge["materialization_dependency"] is False
assert preview["summary"]["edge_count"] == 2
delta = report["materialization_preview"]["delta"]
assert delta["added_workflow_topology_edge_count"] == 2
assert {edge["event_ref"] for edge in delta["added_workflow_topology_edges"]} == {
"event.pantry-item-recorded",
"event.pantry-item-reviewed",
}
assert report["summary"]["removed_gap_count"] == 0


def test_rerun_materialization_preserves_node_scope_for_duplicate_candidate_gap_ids() -> None:
module = load_module()
candidate_graph = copy.deepcopy(candidate_graph_artifact())
Expand Down
Loading