diff --git a/Sources/SpecGraph/Documentation.docc/ProductWorkspacePilots.md b/Sources/SpecGraph/Documentation.docc/ProductWorkspacePilots.md index 480483ef..cc128abc 100644 --- a/Sources/SpecGraph/Documentation.docc/ProductWorkspacePilots.md +++ b/Sources/SpecGraph/Documentation.docc/ProductWorkspacePilots.md @@ -720,6 +720,13 @@ 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. +Proposal `0209` adds the post-rerun visibility layer for this loop: +`structural_depth_delta` records before/after structural counts, added +event-storming refs, added workflow relation evidence, remaining shallow +dimensions, and whether the repair effect was `improved`, `resolved`, +`still_shallow`, `unchanged`, or `not_measured`. The delta is review-only +evidence for downstream product surfaces, not a Metrics field, gate, score, or +authority expansion. The next slices are Platform awareness and a `local-subscription-control` demo pass. Platform may use the report as an explanatory preflight signal before diff --git a/docs/archive/proposal_sources/0209_depth_repair_effect_visibility.md b/docs/archive/proposal_sources/0209_depth_repair_effect_visibility.md new file mode 100644 index 00000000..479a00cd --- /dev/null +++ b/docs/archive/proposal_sources/0209_depth_repair_effect_visibility.md @@ -0,0 +1,20 @@ +# 0209 Depth Repair Effect Visibility + +Source draft for proposal `0209`. + +Structural-depth repair became actionable in proposals `0207` and `0208`, but +downstream product surfaces still need durable evidence of what changed after a +rerun. Add a review-only `structural_depth_delta` to rerun preview and +materialization artifacts: + +- before/after candidate structure counts; +- added event-storming entry refs; +- added workflow relation evidence; +- remaining shallow dimensions; +- compact status: `resolved`, `improved`, `still_shallow`, `unchanged`, or + `not_measured`. + +The delta is visibility only. It must not change Metrics schemas, add a score, +become a gate, mutate canonical specs or intake sources, write Ontology +packages, accept ontology terms, execute prompt agents, create Git artifacts, or +publish read models. diff --git a/docs/product_workspace_graph_versioning_roadmap.md b/docs/product_workspace_graph_versioning_roadmap.md index 108851bd..2b50f7eb 100644 --- a/docs/product_workspace_graph_versioning_roadmap.md +++ b/docs/product_workspace_graph_versioning_roadmap.md @@ -1614,6 +1614,23 @@ workflow topology remains review evidence and never becomes canonical spec mutation, Ontology acceptance, Git authority, or YAML implementation dependency. +## Depth Repair Effect Visibility + +Status: implemented producer-side in proposal `0209`; SpecSpace can layer a +read-only "Depth impact" surface over the existing rerun materialization +artifact. + +Proposal `0209` closes the visibility gap after depth-driven repair. Rerun +preview and rerun materialization now expose +`structural_depth_delta`: before/after structural counts, added event-storming +entry refs, added workflow relation evidence, remaining shallow dimensions, and +a compact status such as `improved`, `resolved`, `still_shallow`, `unchanged`, +or `not_measured`. + +The delta is not a Metrics schema extension, score, gate, approval signal, or +promotion condition. It is review-only evidence that helps product surfaces +explain what depth repair changed and what still needs clarification. + ## Real Idea Entry Request Import Status: implemented producer-side in proposal `0202`; downstream UI/Platform diff --git a/docs/proposals/0209_depth_repair_effect_visibility.md b/docs/proposals/0209_depth_repair_effect_visibility.md new file mode 100644 index 00000000..67015a0b --- /dev/null +++ b/docs/proposals/0209_depth_repair_effect_visibility.md @@ -0,0 +1,108 @@ +# 0209 Depth Repair Effect Visibility + +## Status + +Draft / producer-side repair-effect visibility slice. + +## Summary + +Proposals `0205` through `0208` made structural depth observable, interpretable, +actionable through clarification requests, and repairable through safe +event-storming and workflow-topology hints. The remaining gap is visibility: +after a rerun, downstream surfaces can see final `candidate_structure_depth` +counts, but they cannot easily explain what changed because of the accepted +depth repair answers. + +This slice adds a review-only `structural_depth_delta` surface to the existing +rerun preview/materialization artifacts. It records before/after structural +counts, added event-storming entries, added workflow relations, remaining +shallow dimensions, and a compact effect status. + +## Decision + +`tools/idea_to_spec_rerun_preview.py` emits: + +```json +{ + "rerun_preview": { + "structural_depth_delta": { + "proposal_id": "0209", + "status": "improved", + "before": { + "actor_count": 0, + "workflow_edge_count": 0 + }, + "after": { + "actor_count": 1, + "workflow_edge_count": 3 + }, + "delta": { + "actor_count": 1, + "workflow_edge_count": 3 + }, + "added_event_storming_entry_refs": { + "actors": ["actor.household-member"] + }, + "added_workflow_relation_count": 3, + "remaining_shallow_dimensions": ["constraint_count"], + "review_only": true, + "canonical_mutations_allowed": false, + "materialization_dependency": false + } + } +} +``` + +`tools/idea_to_spec_rerun_materialization.py` preserves the effect in +`materialization_preview.delta.structural_depth_delta` and recomputes graph-side +after counts from the actual `candidate_graph_preview`. + +Valid statuses are: + +- `resolved`: previously shallow structural dimensions are now non-zero; +- `improved`: at least one structural dimension increased, but some dimensions + may still be shallow; +- `still_shallow`: no measured improvement and at least one dimension is still + zero; +- `unchanged`: no measured improvement and no shallow dimensions remain; +- `not_measured`: the rerun/materialization was not ready enough to produce a + trustworthy delta. + +`tools/idea_maturity_metrics_report.py` may reference this delta from +producer-side readiness explainers, but it does not add a new Metrics field, +score, status, or gate. + +## Authority Boundary + +This slice is report-only and review-only. It does not: + +- change Metrics schemas or validator semantics; +- add a structural-depth score; +- change Idea Maturity status semantics; +- change repair, Pre-SIB, candidate approval, promotion, Git, or publication + gates; +- 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 remains product-review evidence. Structural depth deltas must +not become YAML `depends_on`, implementation dependencies, or promotion +authority. + +## Acceptance Criteria + +- Rerun preview emits `structural_depth_delta` for event-storming and workflow + topology repair answers. +- Rerun materialization preserves `structural_depth_delta` in its existing + review-only delta and recomputes graph-side after counts from the materialized + candidate graph preview. +- Failed or unready materialization reports `status: "not_measured"` instead of + implying depth was unchanged. +- Idea Maturity readiness explainers can cite the structural depth delta as + evidence without changing Metrics counts, status, or gate semantics. +- Downstream consumers can show what improved, what was added, and which shallow + dimensions remain without gaining execution or mutation authority. diff --git a/tests/test_idea_maturity_metrics_report.py b/tests/test_idea_maturity_metrics_report.py index c456d5bc..cc0a13ab 100644 --- a/tests/test_idea_maturity_metrics_report.py +++ b/tests/test_idea_maturity_metrics_report.py @@ -646,6 +646,44 @@ def test_idea_maturity_metrics_report_interprets_shallow_structure_without_gatin candidate_graph["nodes"][0]["acceptance_criteria"] = [] candidate_graph["edges"] = [] write_json(paths["candidate_graph"], candidate_graph) + materialization = load_json(paths["rerun_materialization"]) + materialization["materialization_preview"]["delta"]["structural_depth_delta"] = { + "proposal_id": "0209", + "status": "improved", + "before": { + "actor_count": 0, + "command_count": 0, + "domain_event_count": 0, + "policy_count": 0, + "constraint_count": 0, + "topology_edge_count": 0, + "workflow_edge_count": 0, + "requirement_count": 0, + "acceptance_criteria_count": 0, + }, + "after": { + "actor_count": 0, + "command_count": 0, + "domain_event_count": 0, + "policy_count": 0, + "constraint_count": 0, + "topology_edge_count": 1, + "workflow_edge_count": 1, + "requirement_count": 0, + "acceptance_criteria_count": 0, + }, + "delta": {"workflow_edge_count": 1, "topology_edge_count": 1}, + "remaining_shallow_dimensions": [ + "actor_count", + "command_count", + "domain_event_count", + "policy_count", + "constraint_count", + "requirement_count", + "acceptance_criteria_count", + ], + } + write_json(paths["rerun_materialization"], materialization) report = build_report(paths) @@ -667,6 +705,18 @@ def test_idea_maturity_metrics_report_interprets_shallow_structure_without_gatin } assert all("candidate_approval" not in item["blocks"] for item in structure_explainers) assert all("platform_promotion" not in item["blocks"] for item in structure_explainers) + workflow_explainer = next( + item + for item in structure_explainers + if item["kind"] == "candidate_structure_workflow_topology_flat" + ) + assert ( + f"{ref(paths['rerun_materialization'])}" + "#materialization_preview.delta.structural_depth_delta" + in workflow_explainer["evidence_refs"] + ) + assert workflow_explainer["evidence"]["repair_effect_status"] == "improved" + assert workflow_explainer["evidence"]["repair_effect_delta"] == 1 def test_idea_maturity_metrics_report_prioritizes_blockers_before_structure( diff --git a/tests/test_idea_to_spec_rerun_materialization.py b/tests/test_idea_to_spec_rerun_materialization.py index 754f7e90..b939b575 100644 --- a/tests/test_idea_to_spec_rerun_materialization.py +++ b/tests/test_idea_to_spec_rerun_materialization.py @@ -238,6 +238,51 @@ def rerun_preview_with_workflow_topology() -> dict[str, object]: "review_only": True, "materialization_dependency": False, } + preview["rerun_preview"]["structural_depth_delta"] = { + "proposal_id": "0209", + "status": "improved", + "before": { + "actor_count": 0, + "command_count": 0, + "domain_event_count": 0, + "policy_count": 0, + "constraint_count": 0, + "topology_edge_count": 0, + "workflow_edge_count": 0, + "requirement_count": 0, + "acceptance_criteria_count": 0, + }, + "after": { + "actor_count": 0, + "command_count": 0, + "domain_event_count": 0, + "policy_count": 0, + "constraint_count": 0, + "topology_edge_count": 2, + "workflow_edge_count": 2, + "requirement_count": 0, + "acceptance_criteria_count": 0, + }, + "delta": { + "topology_edge_count": 2, + "workflow_edge_count": 2, + }, + "added_event_storming_entry_refs": {}, + "added_workflow_relation_count": 0, + "added_workflow_relations": [], + "remaining_shallow_dimensions": [ + "actor_count", + "command_count", + "domain_event_count", + "policy_count", + "constraint_count", + "requirement_count", + "acceptance_criteria_count", + ], + "review_only": True, + "canonical_mutations_allowed": False, + "materialization_dependency": False, + } preview["rerun_preview"]["ontology_gap_preview"]["resolved_ontology_gaps"] = [] preview["rerun_preview"]["ontology_gap_preview"]["unresolved_ontology_gaps"] = [] return preview @@ -365,9 +410,39 @@ def test_rerun_materialization_merges_review_only_workflow_topology_edges() -> N "event.pantry-item-recorded", "event.pantry-item-reviewed", } + depth_delta = delta["structural_depth_delta"] + assert depth_delta["proposal_id"] == "0209" + assert depth_delta["status"] == "improved" + assert depth_delta["before"]["workflow_edge_count"] == 0 + assert depth_delta["after"]["workflow_edge_count"] == 2 + assert depth_delta["delta"]["workflow_edge_count"] == 2 + assert depth_delta["added_workflow_relation_count"] == 0 + assert report["summary"]["structural_depth_delta_status"] == "improved" assert report["summary"]["removed_gap_count"] == 0 +def test_rerun_materialization_treats_missing_preview_depth_delta_as_not_measured() -> None: + module = load_module() + rerun_preview = rerun_preview_with_workflow_topology() + del rerun_preview["rerun_preview"]["structural_depth_delta"] + + report = module.build_idea_to_spec_rerun_materialization( + rerun_preview=rerun_preview, + candidate_graph=candidate_graph_for_workflow_topology(), + ) + + assert report["readiness"]["ready"] is True + delta = report["materialization_preview"]["delta"] + assert delta["added_workflow_topology_edge_count"] == 2 + depth_delta = delta["structural_depth_delta"] + assert depth_delta["status"] == "not_measured" + assert depth_delta["delta"] == {} + assert depth_delta["remaining_shallow_dimensions"] == [] + assert "actor_count" not in depth_delta["before"] + assert report["summary"]["structural_depth_delta_status"] == "not_measured" + assert report["summary"]["structural_depth_remaining_shallow_dimension_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()) diff --git a/tests/test_idea_to_spec_rerun_preview.py b/tests/test_idea_to_spec_rerun_preview.py index 4b064f45..b3733d28 100644 --- a/tests/test_idea_to_spec_rerun_preview.py +++ b/tests/test_idea_to_spec_rerun_preview.py @@ -178,6 +178,31 @@ def rerun_input_with_workflow_relations(relations: list[dict[str, object]]) -> d } +def rerun_input_with_event_storming_entries() -> dict[str, object]: + payload = rerun_input_with_workflow_relations([]) + hints = payload["rerun_input_overlay"]["intake_overlay"]["event_storming_hints"] + hints.append( + { + "request_id": "clarification.depth.event-storming", + "request_kind": "candidate_structure_gap", + "answer_kind": "answer_question", + "target_artifact": "runs/idea_event_storming_intake.json", + "target_ref": "event_storming_hints.domain_events", + "value": { + "actors": [{"id": "actor.household-member", "name": "Household member"}], + "domain_events": [ + { + "id": "event.pantry-item-recorded", + "statement": "Pantry item recorded", + } + ], + "policies": [{"id": "policy.expiration-reminder", "name": "Expiration reminder"}], + }, + } + ) + return payload + + def candidate_graph_with_candidate_gaps() -> dict[str, object]: graph = candidate_graph_artifact() graph["nodes"] = [ @@ -300,6 +325,7 @@ def test_rerun_preview_blocks_unready_rerun_input() -> None: assert report["readiness"]["ready"] is False assert "rerun_input_not_ready" in finding_ids(report) + assert report["rerun_preview"]["structural_depth_delta"]["status"] == "not_measured" gap_preview = report["rerun_preview"]["ontology_gap_preview"] assert gap_preview["decision_count"] == 0 assert gap_preview["resolved_ontology_gap_count"] == 0 @@ -838,6 +864,41 @@ def test_rerun_preview_applies_typed_workflow_relation_hints() -> None: assert topology["topology_relation_counts"]["event_informs_policy"] == 1 assert all(edge["review_only"] is True for edge in topology["workflow_edges"]) assert report["summary"]["workflow_relation_hint_count"] == 3 + depth_delta = report["rerun_preview"]["structural_depth_delta"] + assert depth_delta["proposal_id"] == "0209" + assert depth_delta["status"] == "improved" + assert depth_delta["before"]["workflow_edge_count"] == 0 + assert depth_delta["after"]["workflow_edge_count"] == 3 + assert depth_delta["delta"]["workflow_edge_count"] == 3 + assert depth_delta["added_workflow_relation_count"] == 3 + assert "constraint_count" in depth_delta["remaining_shallow_dimensions"] + assert report["summary"]["structural_depth_delta_status"] == "improved" + + +def test_rerun_preview_structural_depth_delta_tracks_added_event_storming_entries() -> None: + module = load_module( + PREVIEW_TOOL_PATH, + "idea_to_spec_rerun_preview_depth_delta_event_entries_test", + ) + + report = module.build_idea_to_spec_rerun_preview( + rerun_input=rerun_input_with_event_storming_entries(), + intake=intake_artifact(), + candidate_graph=workflow_topology_candidate_graph(), + ) + + assert report["readiness"]["ready"] is True + depth_delta = report["rerun_preview"]["structural_depth_delta"] + assert depth_delta["status"] == "improved" + assert depth_delta["before"]["actor_count"] == 0 + assert depth_delta["after"]["actor_count"] == 1 + assert depth_delta["after"]["domain_event_count"] == 1 + assert depth_delta["after"]["policy_count"] == 1 + assert depth_delta["added_event_storming_entry_refs"] == { + "actors": ["actor.household-member"], + "domain_events": ["event.pantry-item-recorded"], + "policies": ["policy.expiration-reminder"], + } def test_rerun_preview_preserves_distinct_workflow_edges_with_same_endpoints() -> None: diff --git a/tools/README.md b/tools/README.md index 253099b9..db3f4e78 100644 --- a/tools/README.md +++ b/tools/README.md @@ -413,6 +413,12 @@ Supervisor modes: Rerun preview validates source/target refs and relation kinds before emitting review-only workflow topology edges; materialization copies only `review_only` / non-dependency edges into candidate graph preview. +- Proposal 0209 adds `structural_depth_delta` to rerun preview and + materialization so downstream surfaces can show depth repair impact: + before/after counts, added event-storming refs, added workflow relation + evidence, remaining shallow dimensions, and an effect status. This is + report-only visibility, not a Metrics schema change, score, gate, or authority + expansion. - `tools/user_idea_intake_source.py`: deterministic generic user-idea source builder introduced by proposal 0158. Use `make user-idea-intake-source USER_IDEA_INTAKE_SOURCE=` to normalize product workspace identity, diff --git a/tools/idea_maturity_metrics_report.py b/tools/idea_maturity_metrics_report.py index 1de9c406..f5bec470 100644 --- a/tools/idea_maturity_metrics_report.py +++ b/tools/idea_maturity_metrics_report.py @@ -16,6 +16,7 @@ READINESS_EXPLAINERS_PROPOSAL_ID = "0180" METRICS_CONTRACT_PROPOSAL_ID = "0181" STRUCTURE_DEPTH_EXPLAINERS_PROPOSAL_ID = "0206" +DEPTH_REPAIR_EFFECT_PROPOSAL_ID = "0209" SCHEMA_VERSION = 1 CONTRACT_REF = "specgraph.idea-to-spec.maturity-metrics-report.v0.1" METRIC_PACK_ID = "idea_to_spec_maturity" @@ -2279,11 +2280,28 @@ def _project_local_ontology_readiness_explainers( def _candidate_structure_depth_readiness_explainers( artifacts: dict[str, dict[str, Any]], + paths: dict[str, Path], metrics: dict[str, Any], ) -> list[dict[str, Any]]: if not artifacts.get("candidate_graph") and not artifacts.get("repaired_candidate_graph"): return [] depth = _dict(metrics.get("candidate_structure_depth")) + repair_effect = _dict( + _dict(_dict(artifacts.get("rerun_materialization")).get("materialization_preview")).get( + "delta" + ) + ).get("structural_depth_delta") + repair_effect = _dict(repair_effect) + repair_effect_delta = _dict(repair_effect.get("delta")) + repair_effect_refs = [] + if repair_effect: + repair_effect_refs.append( + _path_evidence_ref( + paths, + "rerun_materialization", + "materialization_preview.delta.structural_depth_delta", + ) + ) has_intake = bool(artifacts.get("intake")) explainers: list[dict[str, Any]] = [] for ( @@ -2300,6 +2318,15 @@ def _candidate_structure_depth_readiness_explainers( value = _int(depth.get(field)) if value > 0: continue + evidence = {"metric_id": field, "observed_count": value} + if repair_effect: + evidence.update( + { + "repair_effect_status": _text(repair_effect.get("status")), + "repair_effect_delta": _int(repair_effect_delta.get(field)), + "repair_effect_proposal_id": DEPTH_REPAIR_EFFECT_PROPOSAL_ID, + } + ) explainers.append( _readiness_explainer( explainer_id=explainer_id, @@ -2313,9 +2340,10 @@ def _candidate_structure_depth_readiness_explainers( ( "runs/idea_maturity_metrics_report.json" f"#groups.candidate_structure_depth.{field}" - ) + ), + *repair_effect_refs, ], - evidence={"metric_id": field, "observed_count": value}, + evidence=evidence, proposal_id=STRUCTURE_DEPTH_EXPLAINERS_PROPOSAL_ID, ) ) @@ -2386,7 +2414,7 @@ def _readiness_explainers( *_project_local_ontology_readiness_explainers(artifacts, paths), *_finding_readiness_explainers(policy_findings, collection="policy_findings"), *_finding_readiness_explainers(invariant_findings, collection="invariant_findings"), - *_candidate_structure_depth_readiness_explainers(artifacts, metrics), + *_candidate_structure_depth_readiness_explainers(artifacts, paths, metrics), ] return _dedupe_explainers(explainers) diff --git a/tools/idea_to_spec_rerun_materialization.py b/tools/idea_to_spec_rerun_materialization.py index 564dbcce..9ecc762e 100644 --- a/tools/idea_to_spec_rerun_materialization.py +++ b/tools/idea_to_spec_rerun_materialization.py @@ -11,6 +11,7 @@ ROOT = Path(__file__).resolve().parents[1] PROPOSAL_ID = "0167" +DEPTH_REPAIR_EFFECT_PROPOSAL_ID = "0209" SCHEMA_VERSION = 1 CONTRACT_REF = "specgraph.idea-to-spec.rerun-materialization.v0.1" RERUN_PREVIEW_CONTRACT_REF = "specgraph.idea-to-spec.rerun-preview.v0.1" @@ -31,6 +32,25 @@ "raw_response", "raw_text", } +WORKFLOW_RELATIONS = { + "actor_triggers_command", + "command_emits_event", + "event_informs_policy", + "event_informs_constraint", + "constraint_applies_to_command", + "policy_applies_to_command", +} +STRUCTURAL_DEPTH_FIELDS = ( + "actor_count", + "command_count", + "domain_event_count", + "policy_count", + "constraint_count", + "topology_edge_count", + "workflow_edge_count", + "requirement_count", + "acceptance_criteria_count", +) def _now_iso() -> str: @@ -49,6 +69,10 @@ def _text(value: Any, default: str = "") -> str: return value.strip() if isinstance(value, str) and value.strip() else default +def _int(value: Any) -> int: + return value if isinstance(value, int) and not isinstance(value, bool) else 0 + + def _text_list(value: Any) -> list[str]: return [_text(item) for item in _list(value) if _text(item)] @@ -304,6 +328,105 @@ def _candidate_graph_counts(candidate_graph: dict[str, Any]) -> dict[str, int]: } +def _candidate_graph_depth(candidate_graph: dict[str, Any]) -> dict[str, int]: + nodes = [_dict(item) for item in _list(candidate_graph.get("nodes"))] + edges = [_dict(item) for item in _list(candidate_graph.get("edges"))] + return { + "topology_edge_count": len(edges), + "workflow_edge_count": sum( + 1 for edge in edges if _text(edge.get("relation")) in WORKFLOW_RELATIONS + ), + "requirement_count": sum(len(_list(node.get("requirements"))) for node in nodes), + "acceptance_criteria_count": sum( + len(_list(node.get("acceptance_criteria"))) for node in nodes + ), + } + + +def _depth_status(before: dict[str, int], after: dict[str, int]) -> str: + if not before or not after: + return "not_measured" + remaining_shallow = [field for field in STRUCTURAL_DEPTH_FIELDS if after.get(field, 0) <= 0] + was_shallow = [field for field in STRUCTURAL_DEPTH_FIELDS if before.get(field, 0) <= 0] + improved = any(after.get(field, 0) > before.get(field, 0) for field in STRUCTURAL_DEPTH_FIELDS) + if was_shallow and not remaining_shallow: + return "resolved" + if improved: + return "improved" + if remaining_shallow: + return "still_shallow" + return "unchanged" + + +def _has_complete_depth_counts(value: dict[str, Any]) -> bool: + return all( + isinstance(value.get(field), int) and not isinstance(value.get(field), bool) + for field in STRUCTURAL_DEPTH_FIELDS + ) + + +def _not_measured_structural_depth_delta( + *, + before: dict[str, int] | None = None, + after: dict[str, int] | None = None, +) -> dict[str, Any]: + return { + "proposal_id": DEPTH_REPAIR_EFFECT_PROPOSAL_ID, + "status": "not_measured", + "before": before or {}, + "after": after or {}, + "delta": {}, + "added_event_storming_entry_refs": {}, + "added_workflow_relation_count": 0, + "added_workflow_relations": [], + "remaining_shallow_dimensions": [], + "review_only": True, + "canonical_mutations_allowed": False, + "materialization_dependency": False, + } + + +def _structural_depth_delta( + *, + rerun_preview: dict[str, Any], + candidate_graph: dict[str, Any], + candidate_graph_preview: dict[str, Any], +) -> dict[str, Any]: + preview_delta = _dict(_dict(rerun_preview.get("rerun_preview")).get("structural_depth_delta")) + before = _dict(preview_delta.get("before")) + after = dict(_dict(preview_delta.get("after"))) + if ( + not preview_delta + or _text(preview_delta.get("status")) == "not_measured" + or not _has_complete_depth_counts(before) + or not _has_complete_depth_counts(after) + ): + return _not_measured_structural_depth_delta() + after.update(_candidate_graph_depth(candidate_graph_preview)) + remaining = [field for field in STRUCTURAL_DEPTH_FIELDS if after.get(field, 0) <= 0] + return { + "proposal_id": DEPTH_REPAIR_EFFECT_PROPOSAL_ID, + "status": _depth_status(before, after), + "before": before, + "after": after, + "delta": { + field: after.get(field, 0) - before.get(field, 0) + for field in sorted(set(before) | set(after)) + }, + "added_event_storming_entry_refs": _public_safe( + _dict(preview_delta.get("added_event_storming_entry_refs")) + ), + "added_workflow_relation_count": _int(preview_delta.get("added_workflow_relation_count")), + "added_workflow_relations": _public_safe( + _list(preview_delta.get("added_workflow_relations")) + ), + "remaining_shallow_dimensions": remaining, + "review_only": True, + "canonical_mutations_allowed": False, + "materialization_dependency": False, + } + + def _refresh_candidate_graph_summary(candidate_graph: dict[str, Any]) -> None: summary = dict(_dict(candidate_graph.get("summary"))) summary.update(_candidate_graph_counts(candidate_graph)) @@ -378,6 +501,10 @@ def _empty_delta(candidate_graph: dict[str, Any]) -> dict[str, Any]: "removed_gap_ids": [], "added_workflow_topology_edges": [], "added_workflow_topology_edge_count": 0, + "structural_depth_delta": _not_measured_structural_depth_delta( + before=_candidate_graph_depth(candidate_graph), + after=_candidate_graph_depth(candidate_graph), + ), "unresolved_ontology_gap_ids": unresolved_ontology_gap_ids, "unresolved_candidate_gap_ids": unresolved_candidate_gap_ids, "resolved_ontology_gap_count": 0, @@ -500,6 +627,11 @@ def _materialize_candidate_graph_preview( added_workflow_edges = _merge_workflow_topology_edges(preview, rerun_preview) preview["source_ref"] = candidate_graph_preview_ref _refresh_candidate_graph_summary(preview) + structural_depth_delta = _structural_depth_delta( + rerun_preview=rerun_preview, + candidate_graph=candidate_graph, + candidate_graph_preview=preview, + ) preview["rerun_materialization"] = { "proposal_id": PROPOSAL_ID, "contract_ref": CONTRACT_REF, @@ -509,11 +641,13 @@ def _materialize_candidate_graph_preview( "ontology_resolution_count": len(ontology_resolution_records), "candidate_resolution_count": len(candidate_resolution_records), "added_workflow_topology_edge_count": len(added_workflow_edges), + "structural_depth_delta_status": structural_depth_delta["status"], } delta = { "removed_gap_ids": removed_gap_ids, "added_workflow_topology_edges": added_workflow_edges, "added_workflow_topology_edge_count": len(added_workflow_edges), + "structural_depth_delta": structural_depth_delta, "unresolved_ontology_gap_ids": unresolved_ontology_gap_ids, "unresolved_candidate_gap_ids": unresolved_candidate_gap_ids, "resolved_ontology_gap_count": len(ontology_resolution_records), @@ -614,6 +748,13 @@ def build_idea_to_spec_rerun_materialization( "resolved_candidate_gap_count": delta["resolved_candidate_gap_count"], "unresolved_candidate_gap_count": delta["unresolved_candidate_gap_count"], "removed_gap_count": len(delta["removed_gap_ids"]), + "structural_depth_delta_status": delta["structural_depth_delta"]["status"], + "structural_depth_added_workflow_relation_count": delta["structural_depth_delta"][ + "added_workflow_relation_count" + ], + "structural_depth_remaining_shallow_dimension_count": len( + delta["structural_depth_delta"]["remaining_shallow_dimensions"] + ), "finding_count": len(findings), }, } diff --git a/tools/idea_to_spec_rerun_preview.py b/tools/idea_to_spec_rerun_preview.py index e3b85ae0..3a50cae8 100644 --- a/tools/idea_to_spec_rerun_preview.py +++ b/tools/idea_to_spec_rerun_preview.py @@ -11,6 +11,7 @@ ROOT = Path(__file__).resolve().parents[1] PROPOSAL_ID = "0166" +DEPTH_REPAIR_EFFECT_PROPOSAL_ID = "0209" SCHEMA_VERSION = 1 CONTRACT_REF = "specgraph.idea-to-spec.rerun-preview.v0.1" RERUN_INPUT_CONTRACT_REF = "specgraph.idea-to-spec.answer-rerun-input.v0.1" @@ -85,6 +86,17 @@ }, } WORKFLOW_RELATION_NAMES = set(WORKFLOW_RELATION_SPECS) +STRUCTURAL_DEPTH_FIELDS = ( + "actor_count", + "command_count", + "domain_event_count", + "policy_count", + "constraint_count", + "topology_edge_count", + "workflow_edge_count", + "requirement_count", + "acceptance_criteria_count", +) RAW_TRACE_FIELDS = { "operator_note", "operator_notes", @@ -859,6 +871,155 @@ def _workflow_topology_preview( } +def _event_storming_depth(event_storming: dict[str, Any]) -> dict[str, int]: + return { + "actor_count": len(_list(event_storming.get("actors"))), + "command_count": len(_list(event_storming.get("commands"))), + "domain_event_count": len(_list(event_storming.get("domain_events"))), + "policy_count": len(_list(event_storming.get("policies"))), + "constraint_count": len(_list(event_storming.get("constraints"))), + } + + +def _candidate_graph_depth(candidate_graph: dict[str, Any]) -> dict[str, int]: + nodes = [_dict(node) for node in _list(candidate_graph.get("nodes"))] + edges = [_dict(edge) for edge in _list(candidate_graph.get("edges"))] + return { + "topology_edge_count": len(edges), + "workflow_edge_count": sum( + 1 for edge in edges if _text(edge.get("relation")) in WORKFLOW_RELATION_NAMES + ), + "requirement_count": sum(len(_list(node.get("requirements"))) for node in nodes), + "acceptance_criteria_count": sum( + len(_list(node.get("acceptance_criteria"))) for node in nodes + ), + } + + +def _event_storming_entry_ids(event_storming: dict[str, Any], category: str) -> set[str]: + return { + _text(_dict(entry).get("id")) + for entry in _list(event_storming.get(category)) + if _text(_dict(entry).get("id")) + } + + +def _edge_key(edge: dict[str, Any]) -> tuple[str, str, str, tuple[str, ...]]: + return ( + _text(edge.get("from")), + _text(edge.get("to")), + _text(edge.get("relation")), + tuple(_text_list(edge.get("source_event_refs"))), + ) + + +def _new_workflow_edges( + *, + candidate_graph: dict[str, Any], + workflow_topology_preview: dict[str, Any], +) -> list[dict[str, Any]]: + existing = {_edge_key(_dict(edge)) for edge in _list(candidate_graph.get("edges"))} + added: list[dict[str, Any]] = [] + seen = set(existing) + for raw_edge in _list(workflow_topology_preview.get("workflow_edges")): + edge = _dict(raw_edge) + if edge.get("review_only") is not True: + continue + if edge.get("materialization_dependency") is not False: + continue + key = _edge_key(edge) + if not key[0] or not key[1] or not key[2] or key in seen: + continue + seen.add(key) + added.append(_public_safe(edge)) + return added + + +def _depth_status(before: dict[str, int], after: dict[str, int]) -> str: + if not before or not after: + return "not_measured" + remaining_shallow = [field for field in STRUCTURAL_DEPTH_FIELDS if after.get(field, 0) <= 0] + was_shallow = [field for field in STRUCTURAL_DEPTH_FIELDS if before.get(field, 0) <= 0] + improved = any(after.get(field, 0) > before.get(field, 0) for field in STRUCTURAL_DEPTH_FIELDS) + if was_shallow and not remaining_shallow: + return "resolved" + if improved: + return "improved" + if remaining_shallow: + return "still_shallow" + return "unchanged" + + +def _not_measured_structural_depth_delta() -> dict[str, Any]: + return { + "proposal_id": DEPTH_REPAIR_EFFECT_PROPOSAL_ID, + "status": "not_measured", + "before": {}, + "after": {}, + "delta": {}, + "added_event_storming_entry_refs": {}, + "added_workflow_relation_count": 0, + "added_workflow_relations": [], + "remaining_shallow_dimensions": [], + "review_only": True, + "canonical_mutations_allowed": False, + "materialization_dependency": False, + } + + +def _structural_depth_delta( + *, + intake: dict[str, Any], + candidate_graph: dict[str, Any], + event_storming_preview: dict[str, Any], + workflow_topology_preview: dict[str, Any], + ready: bool, +) -> dict[str, Any]: + if not ready: + return _not_measured_structural_depth_delta() + before_event_storming = _dict(intake.get("event_storming")) + after_event_storming = _dict(event_storming_preview.get("event_storming")) + added_edges = _new_workflow_edges( + candidate_graph=candidate_graph, + workflow_topology_preview=workflow_topology_preview, + ) + before = { + **_event_storming_depth(before_event_storming), + **_candidate_graph_depth(candidate_graph), + } + after_graph_depth = _candidate_graph_depth( + {**candidate_graph, "edges": _list(candidate_graph.get("edges")) + added_edges} + ) + after = { + **_event_storming_depth(after_event_storming), + **after_graph_depth, + } + added_entries = { + category: sorted( + _event_storming_entry_ids(after_event_storming, category) + - _event_storming_entry_ids(before_event_storming, category) + ) + for category in ("actors", "commands", "domain_events", "policies", "constraints") + } + added_entries = {category: refs for category, refs in added_entries.items() if refs} + return { + "proposal_id": DEPTH_REPAIR_EFFECT_PROPOSAL_ID, + "status": _depth_status(before, after), + "before": before, + "after": after, + "delta": {field: after.get(field, 0) - before.get(field, 0) for field in before}, + "added_event_storming_entry_refs": added_entries, + "added_workflow_relation_count": len(added_edges), + "added_workflow_relations": added_edges, + "remaining_shallow_dimensions": [ + field for field in STRUCTURAL_DEPTH_FIELDS if after.get(field, 0) <= 0 + ], + "review_only": True, + "canonical_mutations_allowed": False, + "materialization_dependency": False, + } + + def _decision_records( *, overlay: dict[str, Any], @@ -1387,6 +1548,13 @@ def build_idea_to_spec_rerun_preview( candidate_gap_preview, ) ready = not findings + structural_depth_delta = _structural_depth_delta( + intake=intake, + candidate_graph=candidate_graph, + event_storming_preview=event_storming_preview, + workflow_topology_preview=workflow_topology_preview, + ready=ready, + ) return { "artifact_kind": "idea_to_spec_rerun_preview", "schema_version": SCHEMA_VERSION, @@ -1428,6 +1596,7 @@ def build_idea_to_spec_rerun_preview( "active_frame_preview": active_frame_preview, "event_storming_preview": event_storming_preview, "workflow_topology_preview": workflow_topology_preview, + "structural_depth_delta": structural_depth_delta, "ontology_gap_preview": ontology_gap_preview, "candidate_gap_preview": candidate_gap_preview, "candidate_review_preview": candidate_review_preview, @@ -1455,6 +1624,13 @@ def build_idea_to_spec_rerun_preview( "applied_workflow_relation_count" ], "workflow_edge_preview_count": workflow_topology_preview["workflow_edge_count"], + "structural_depth_delta_status": structural_depth_delta["status"], + "structural_depth_added_workflow_relation_count": structural_depth_delta[ + "added_workflow_relation_count" + ], + "structural_depth_remaining_shallow_dimension_count": len( + structural_depth_delta["remaining_shallow_dimensions"] + ), "ontology_decision_count": ontology_gap_preview["decision_count"], "resolved_ontology_gap_count": ontology_gap_preview["resolved_ontology_gap_count"], "unresolved_ontology_gap_count": ontology_gap_preview["unresolved_ontology_gap_count"], diff --git a/tools/proposal_promotion_registry.json b/tools/proposal_promotion_registry.json index 624e39f5..bc37e724 100644 --- a/tools/proposal_promotion_registry.json +++ b/tools/proposal_promotion_registry.json @@ -2689,5 +2689,18 @@ "required_provenance_links": [ "source_draft_ref" ] + }, + { + "proposal_id": "0209", + "source_artifact_class": "working_draft", + "source_refs": [ + "docs/archive/proposal_sources/0209_depth_repair_effect_visibility.md" + ], + "motivating_concern": "Depth-driven repair can now add event-storming entries and workflow relations, but downstream product surfaces need durable review-only evidence showing what changed and which shallow dimensions remain after rerun materialization.", + "normalized_title": "Depth Repair Effect Visibility", + "bounded_scope": "Emit review-only structural_depth_delta evidence from rerun preview and rerun materialization, and let Idea Maturity readiness explainers cite that evidence, without changing Metrics schemas, adding scores, changing gates, approving candidates, mutating source intake or canonical specs, writing Ontology packages or lockfiles, accepting ontology terms, creating Git branches or commits, opening pull requests, publishing read models, or turning workflow topology into YAML dependencies.", + "required_provenance_links": [ + "source_draft_ref" + ] } ] diff --git a/tools/proposal_runtime_registry.json b/tools/proposal_runtime_registry.json index ee5d2d09..948a6e5e 100644 --- a/tools/proposal_runtime_registry.json +++ b/tools/proposal_runtime_registry.json @@ -11908,5 +11908,65 @@ } ], "notes": "SpecGraph can now turn flat workflow_edge_count observations into review-required typed workflow relation requests and safely materialize validated review-only topology edges in candidate graph preview." + }, + { + "proposal_id": "0209", + "posture": "bounded_runtime_followup", + "runtime_surfaces": [ + "docs/proposals/0209_depth_repair_effect_visibility.md", + "docs/archive/proposal_sources/0209_depth_repair_effect_visibility.md", + "tools/idea_to_spec_rerun_preview.py", + "tools/idea_to_spec_rerun_materialization.py", + "tools/idea_maturity_metrics_report.py", + "tests/test_idea_to_spec_rerun_preview.py", + "tests/test_idea_to_spec_rerun_materialization.py", + "tests/test_idea_maturity_metrics_report.py", + "docs/product_workspace_graph_versioning_roadmap.md", + "Sources/SpecGraph/Documentation.docc/ProductWorkspacePilots.md", + "tools/README.md" + ], + "runtime_markers": [ + { + "path": "tools/idea_to_spec_rerun_preview.py", + "pattern": "DEPTH_REPAIR_EFFECT_PROPOSAL_ID = \"0209\"" + }, + { + "path": "tools/idea_to_spec_rerun_preview.py", + "pattern": "def _structural_depth_delta(" + }, + { + "path": "tools/idea_to_spec_rerun_materialization.py", + "pattern": "\"structural_depth_delta\"" + } + ], + "validation_markers": [ + { + "path": "tests/test_idea_to_spec_rerun_preview.py", + "pattern": "test_rerun_preview_structural_depth_delta_tracks_added_event_storming_entries" + }, + { + "path": "tests/test_idea_to_spec_rerun_materialization.py", + "pattern": "structural_depth_delta_status" + }, + { + "path": "tests/test_idea_maturity_metrics_report.py", + "pattern": "repair_effect_status" + } + ], + "observation_markers": [ + { + "path": "docs/product_workspace_graph_versioning_roadmap.md", + "pattern": "Depth Repair Effect Visibility" + }, + { + "path": "Sources/SpecGraph/Documentation.docc/ProductWorkspacePilots.md", + "pattern": "Proposal `0209` adds the post-rerun visibility layer" + }, + { + "path": "tools/README.md", + "pattern": "Proposal 0209 adds `structural_depth_delta`" + } + ], + "notes": "SpecGraph now emits review-only structural_depth_delta evidence from depth repair reruns so downstream surfaces can show what changed, what was added, and which shallow dimensions remain without adding metrics, scores, gates, or authority." } ]