diff --git a/scripts/generate_spreadsheet_programme_workflow_seed.py b/scripts/generate_spreadsheet_programme_workflow_seed.py index 8a3fc5ede..3bfcfe3b3 100644 --- a/scripts/generate_spreadsheet_programme_workflow_seed.py +++ b/scripts/generate_spreadsheet_programme_workflow_seed.py @@ -1594,7 +1594,7 @@ def build_bundle() -> dict[str, object]: return { "family_id": "spreadsheet_programme_representation_workflow_seed_bundle", "schema_version": "repo_seed_workflow_bundle.v1", - "seed_version": "19", + "seed_version": "20", "source_tag": "JVNAUTOSCI-2592", "managed_by": "spreadsheet_programme_workflow_vontology_service", "processing_authority_fingerprint": authority_fingerprint, diff --git a/src/backend/services/workflow_repo_seed_bootstrap.py b/src/backend/services/workflow_repo_seed_bootstrap.py index 0e7da40c7..7f42ed95a 100644 --- a/src/backend/services/workflow_repo_seed_bootstrap.py +++ b/src/backend/services/workflow_repo_seed_bootstrap.py @@ -685,6 +685,7 @@ def _stable_state_metadata_subset(state: Any) -> dict[str, Any]: "writes_context_keys", "tool_output_context_mappings", "subworkflow_contract", + "retry_policy", "mutation_authority", ): value = metadata.get(key) diff --git a/src/backend/workflows/repo_seed_bundles/kr_materialisation_workflow_seed_bundle.json b/src/backend/workflows/repo_seed_bundles/kr_materialisation_workflow_seed_bundle.json index 80a89fcdd..fd14ba356 100644 --- a/src/backend/workflows/repo_seed_bundles/kr_materialisation_workflow_seed_bundle.json +++ b/src/backend/workflows/repo_seed_bundles/kr_materialisation_workflow_seed_bundle.json @@ -2,7 +2,14 @@ "family_id": "#V#kr_design_materialisation_workflow_family", "managed_by": "kr_materialisation_workflow_vontology_service", "schema_version": "repo_seed_workflow_bundle.v1", - "seed_version": "6", + "seed_version": "7", + "known_legacy_authority_payload_sha256_by_seed_version": { + "#V#kr_design_concept_materialisation_item_workflow": { + "6": [ + "c027e848fe7860016bdc91fd1efa5809c3cfb57e667eb021bc638d1cfb34d8d8" + ] + } + }, "source_tag": "JVNAUTOSCI-2271", "supported_action_ids": [ "llm.action", @@ -746,8 +753,9 @@ "on_failure_state": "summarise_blocker", "retry_policy": { "backoff_policy": "fixed", - "delay_ms": 1000, + "initial_delay_ms": 1000, "max_attempts": 2, + "max_delay_ms": 1000, "retry_on_outcomes": [ "failure" ], @@ -755,18 +763,6 @@ }, "state_id": "read_back_concept", "static_input_bindings": [ - [ - "include_relations_any_arg", - true - ], - [ - "include_text_relations_arg1", - true - ], - [ - "limit", - 40 - ], [ "tool_name", "fetch_concept" @@ -804,8 +800,9 @@ "on_failure_state": "summarise_blocker", "retry_policy": { "backoff_policy": "fixed", - "delay_ms": 1000, + "initial_delay_ms": 1000, "max_attempts": 2, + "max_delay_ms": 1000, "retry_on_outcomes": [ "failure" ], @@ -1211,16 +1208,18 @@ "execution_mode": "deterministic", "next_state": "read_back_target", "on_failure_state": "summarise_blocker", + "retry_policy": { + "backoff_policy": "fixed", + "initial_delay_ms": 1000, + "max_attempts": 2, + "max_delay_ms": 1000, + "retry_on_outcomes": [ + "failure" + ], + "schema_version": "workflow_step_retry_policy.v1" + }, "state_id": "read_back_source", "static_input_bindings": [ - [ - "include_relations_any_arg", - true - ], - [ - "limit", - 40 - ], [ "tool_name", "fetch_concept" @@ -1256,16 +1255,18 @@ "execution_mode": "deterministic", "next_state": "emit_relationship_payload", "on_failure_state": "summarise_blocker", + "retry_policy": { + "backoff_policy": "fixed", + "initial_delay_ms": 1000, + "max_attempts": 2, + "max_delay_ms": 1000, + "retry_on_outcomes": [ + "failure" + ], + "schema_version": "workflow_step_retry_policy.v1" + }, "state_id": "read_back_target", "static_input_bindings": [ - [ - "include_relations_any_arg", - true - ], - [ - "limit", - 40 - ], [ "tool_name", "fetch_concept" diff --git a/src/backend/workflows/repo_seed_bundles/spreadsheet_programme_representation_workflow_seed_bundle.json b/src/backend/workflows/repo_seed_bundles/spreadsheet_programme_representation_workflow_seed_bundle.json index 365edcdfd..30997417d 100644 --- a/src/backend/workflows/repo_seed_bundles/spreadsheet_programme_representation_workflow_seed_bundle.json +++ b/src/backend/workflows/repo_seed_bundles/spreadsheet_programme_representation_workflow_seed_bundle.json @@ -3,14 +3,14 @@ "managed_by": "spreadsheet_programme_workflow_vontology_service", "processing_authority_dependencies": { "kr_materialisation_workflow_seed_bundle": { - "canonical_payload_sha256": "sha256:330476eb8e85bc0ea4aeca0c7bf844968495c8c1018f52b5097a96cd68fd8c0a", + "canonical_payload_sha256": "sha256:9b476fe39cd6595890adb83ab80480ada00329bbd2461b693b453cdf10e90536", "family_id": "#V#kr_design_materialisation_workflow_family", - "seed_version": "6" + "seed_version": "7" } }, - "processing_authority_fingerprint": "sha256:7ecf13bbdfa9a9a95f28eefc3634fad91cc5c7498aa0aa84b6763ab965c46267", + "processing_authority_fingerprint": "sha256:3f7d250eec0d520b2658d63493285776c21c123ccec3676e4eb561217a1c4f60", "schema_version": "repo_seed_workflow_bundle.v1", - "seed_version": "19", + "seed_version": "20", "source_tag": "JVNAUTOSCI-2592", "support_concepts": [ { @@ -306,7 +306,7 @@ }, { "key": "processing_authority_fingerprint", - "value": "sha256:7ecf13bbdfa9a9a95f28eefc3634fad91cc5c7498aa0aa84b6763ab965c46267" + "value": "sha256:3f7d250eec0d520b2658d63493285776c21c123ccec3676e4eb561217a1c4f60" } ], "tool_output_mapping_specs": [ @@ -702,7 +702,7 @@ }, { "key": "processing_authority_fingerprint", - "value": "sha256:7ecf13bbdfa9a9a95f28eefc3634fad91cc5c7498aa0aa84b6763ab965c46267" + "value": "sha256:3f7d250eec0d520b2658d63493285776c21c123ccec3676e4eb561217a1c4f60" } ], "tool_output_mapping_specs": [ @@ -776,7 +776,7 @@ }, { "key": "processing_authority_fingerprint", - "value": "sha256:7ecf13bbdfa9a9a95f28eefc3634fad91cc5c7498aa0aa84b6763ab965c46267" + "value": "sha256:3f7d250eec0d520b2658d63493285776c21c123ccec3676e4eb561217a1c4f60" } ], "tool_output_mapping_specs": [ @@ -1234,7 +1234,7 @@ }, { "key": "processing_authority_fingerprint", - "value": "sha256:7ecf13bbdfa9a9a95f28eefc3634fad91cc5c7498aa0aa84b6763ab965c46267" + "value": "sha256:3f7d250eec0d520b2658d63493285776c21c123ccec3676e4eb561217a1c4f60" } ], "tool_output_mapping_specs": [ @@ -1557,7 +1557,7 @@ }, { "key": "processing_authority_fingerprint", - "value": "sha256:7ecf13bbdfa9a9a95f28eefc3634fad91cc5c7498aa0aa84b6763ab965c46267" + "value": "sha256:3f7d250eec0d520b2658d63493285776c21c123ccec3676e4eb561217a1c4f60" } ], "tool_output_mapping_specs": [ @@ -1625,7 +1625,7 @@ }, { "key": "processing_authority_fingerprint", - "value": "sha256:7ecf13bbdfa9a9a95f28eefc3634fad91cc5c7498aa0aa84b6763ab965c46267" + "value": "sha256:3f7d250eec0d520b2658d63493285776c21c123ccec3676e4eb561217a1c4f60" } ], "tool_output_mapping_specs": [ diff --git a/src/backend/workflows/workflow_repo_seed_export_service.py b/src/backend/workflows/workflow_repo_seed_export_service.py index d405b6ecb..3f6620253 100644 --- a/src/backend/workflows/workflow_repo_seed_export_service.py +++ b/src/backend/workflows/workflow_repo_seed_export_service.py @@ -493,6 +493,9 @@ def _build_publication_spec_payload_from_definition( "on_true_state": on_true_state, "on_unknown_state": on_unknown_state, "prompt_concept_ids": prompt_concept_ids, + "retry_policy": _stable_json_like( + runtime_details.retry_policy + ), "state_id": state_id, "static_input_bindings": static_input_bindings, "tool_output_context_mappings": [], diff --git a/tests/backend/test_kr_materialisation_workflow_vontology_service.py b/tests/backend/test_kr_materialisation_workflow_vontology_service.py index 2f2aaef86..7708c6c0a 100644 --- a/tests/backend/test_kr_materialisation_workflow_vontology_service.py +++ b/tests/backend/test_kr_materialisation_workflow_vontology_service.py @@ -14,12 +14,17 @@ from src.backend.workflows.durable.control_flow_actions import ( register_control_flow_actions, ) +from src.backend.workflows.engine import _normalise_retry_policy_spec from src.backend.workflows.execution_contracts import ( WORKFLOW_CONTROL_ACTION_CONTEXT_SET_ID, ) from src.backend.workflows.metadata_validation import ( validate_state_metadata_post_action, ) +from src.backend.workflows import ( + workflow_concept_authority_service as authority_service, +) +from src.backend.workflows import workflow_repo_seed_export_service as export_service _PROJECT_ROOT = Path(__file__).resolve().parents[2] _SEED_BUNDLE_PATH = ( @@ -161,7 +166,14 @@ def test_kr_prompt_seed_assets_hold_operational_prompt_content() -> None: def test_kr_seed_applies_optional_guard_before_each_write_phase() -> None: bundle = json.loads(_SEED_BUNDLE_PATH.read_text(encoding="utf-8")) - assert bundle["seed_version"] == "6" + assert bundle["seed_version"] == "7" + assert bundle["known_legacy_authority_payload_sha256_by_seed_version"] == { + mod.KR_DESIGN_CONCEPT_MATERIALISATION_ITEM_WORKFLOW_ID: { + "6": [ + "c027e848fe7860016bdc91fd1efa5809c3cfb57e667eb021bc638d1cfb34d8d8" + ] + } + } workflow = next( row for row in bundle["workflows"] @@ -241,8 +253,9 @@ def test_kr_seed_applies_optional_guard_before_each_write_phase() -> None: assert description_outputs["kr_description_text_value_id"] == "text_value_id" expected_readback_retry = { "backoff_policy": "fixed", - "delay_ms": 1000, + "initial_delay_ms": 1000, "max_attempts": 2, + "max_delay_ms": 1000, "retry_on_outcomes": ["failure"], "schema_version": "workflow_step_retry_policy.v1", } @@ -254,6 +267,23 @@ def test_kr_seed_applies_optional_guard_before_each_write_phase() -> None: concept_item_states["read_back_text_relations"]["retry_policy"] == expected_readback_retry ) + relationship_item = next( + row + for row in bundle["workflows"] + if row["workflow_id"] == mod.KR_DESIGN_RELATIONSHIP_ASSERTION_ITEM_WORKFLOW_ID + ) + relationship_item_states = { + row["state_id"]: row + for row in relationship_item["publication_spec"]["steps"] + } + assert ( + relationship_item_states["read_back_source"]["retry_policy"] + == expected_readback_retry + ) + assert ( + relationship_item_states["read_back_target"]["retry_policy"] + == expected_readback_retry + ) concept_iteration_bindings = dict( states["materialise_concepts"]["static_input_bindings"] ) @@ -307,6 +337,87 @@ def test_kr_seed_suppresses_event_fan_out_only_for_owned_mutations() -> None: } +def test_kr_readbacks_avoid_expensive_relation_expansion() -> None: + bundle = json.loads(_SEED_BUNDLE_PATH.read_text(encoding="utf-8")) + concept_readback = _publication_step( + bundle, + workflow_id=mod.KR_DESIGN_CONCEPT_MATERIALISATION_ITEM_WORKFLOW_ID, + state_id="read_back_concept", + ) + text_readback = _publication_step( + bundle, + workflow_id=mod.KR_DESIGN_CONCEPT_MATERIALISATION_ITEM_WORKFLOW_ID, + state_id="read_back_text_relations", + ) + + assert dict(concept_readback["static_input_bindings"]) == { + "tool_name": "fetch_concept" + } + assert { + mapping["tool_output_field"] + for mapping in concept_readback["tool_output_mapping_specs"] + } == {"concept_id", "relationships"} + assert concept_readback["next_state"] == "read_back_text_relations" + assert dict(text_readback["static_input_bindings"]) == { + "language": "en-NZ", + "limit": 10, + "predicate": "hasDescription", + "tool_name": "get_text_relations", + } + for state_id in ("read_back_source", "read_back_target"): + relationship_readback = _publication_step( + bundle, + workflow_id=mod.KR_DESIGN_RELATIONSHIP_ASSERTION_ITEM_WORKFLOW_ID, + state_id=state_id, + ) + assert dict(relationship_readback["static_input_bindings"]) == { + "tool_name": "fetch_concept" + } + assert { + mapping["tool_output_field"] + for mapping in relationship_readback["tool_output_mapping_specs"] + } == {"concept_id", "relationships"} + + +def test_kr_retry_policy_round_trips_in_canonical_loader_shape() -> None: + raw_bundle = json.loads(_SEED_BUNDLE_PATH.read_text(encoding="utf-8")) + bundle = authority_service.load_repo_seed_workflow_bundle(_SEED_BUNDLE_PATH) + readback_states_by_workflow = { + mod.KR_DESIGN_CONCEPT_MATERIALISATION_ITEM_WORKFLOW_ID: ( + "read_back_concept", + "read_back_text_relations", + ), + mod.KR_DESIGN_RELATIONSHIP_ASSERTION_ITEM_WORKFLOW_ID: ( + "read_back_source", + "read_back_target", + ), + } + + for workflow_id, state_ids in readback_states_by_workflow.items(): + publication_spec = bundle["publication_specs"][workflow_id] + definition = authority_service._build_definition_from_publication_spec( + workflow_id=workflow_id, + spec=publication_spec, + ) + exported = export_service._build_publication_spec_payload_from_definition( + workflow_id=workflow_id, + definition=definition, + ) + exported_steps = {step["state_id"]: step for step in exported["steps"]} + + for state_id in state_ids: + raw_policy = _publication_step( + raw_bundle, + workflow_id=workflow_id, + state_id=state_id, + )["retry_policy"] + parsed_policy = definition.states[state_id].metadata["retry_policy"] + canonical_policy = _normalise_retry_policy_spec(parsed_policy) + assert raw_policy == canonical_policy + assert parsed_policy == canonical_policy + assert exported_steps[state_id]["retry_policy"] == canonical_policy + + def test_kr_seed_bundle_validates_as_workflow_contracts() -> None: report = mod.validate_kr_materialisation_seed_bundle() diff --git a/tests/backend/test_spreadsheet_programme_workflow_vontology_service.py b/tests/backend/test_spreadsheet_programme_workflow_vontology_service.py index 846d86cb0..bd9dff5d1 100644 --- a/tests/backend/test_spreadsheet_programme_workflow_vontology_service.py +++ b/tests/backend/test_spreadsheet_programme_workflow_vontology_service.py @@ -343,7 +343,7 @@ def test_processing_authority_fingerprint_tracks_exact_kr_dependency( from scripts import generate_spreadsheet_programme_workflow_seed as generator baseline = generator.build_bundle() - assert baseline["seed_version"] == "19" + assert baseline["seed_version"] == "20" baseline_dependency = baseline["processing_authority_dependencies"][ "kr_materialisation_workflow_seed_bundle" ] @@ -352,7 +352,7 @@ def test_processing_authority_fingerprint_tracks_exact_kr_dependency( ) assert baseline_dependency["family_id"] == source_payload["family_id"] assert baseline_dependency["seed_version"] == source_payload["seed_version"] - assert baseline_dependency["seed_version"] == "6" + assert baseline_dependency["seed_version"] == "7" assert baseline_dependency["canonical_payload_sha256"].startswith("sha256:") changed_payload = dict(source_payload) diff --git a/tests/backend/test_workflow_repo_seed_affordances.py b/tests/backend/test_workflow_repo_seed_affordances.py index d56128282..d23d8eb25 100644 --- a/tests/backend/test_workflow_repo_seed_affordances.py +++ b/tests/backend/test_workflow_repo_seed_affordances.py @@ -50,6 +50,44 @@ def test_repo_workflow_seed_bundles_declare_seed_version() -> None: assert missing == [] +def test_materialisation_comparison_detects_missing_step_retry_policy() -> None: + retry_policy = { + "schema_version": "workflow_step_retry_policy.v1", + "max_attempts": 2, + "backoff_policy": "fixed", + "initial_delay_ms": 1000, + "max_delay_ms": 1000, + "retry_on_outcomes": ["failure"], + } + expected_step = authority_service._CanonicalStepPublicationSpec( + state_id="read_back", + action_id="workflow_mcp.invoke_tool", + retry_policy=retry_policy, + ) + expected_spec = authority_service._CanonicalWorkflowPublicationSpec( + initial_state="read_back", + steps=(expected_step,), + ) + live_without_retry = authority_service._build_definition_from_publication_spec( + workflow_id="#V#retry_drift_workflow", + spec=authority_service._CanonicalWorkflowPublicationSpec( + initial_state="read_back", + steps=( + authority_service._CanonicalStepPublicationSpec( + state_id="read_back", + action_id="workflow_mcp.invoke_tool", + ), + ), + ), + ) + + assert not seed_bootstrap._materialisation_matches_publication_spec( + loaded_definition=live_without_retry, + workflow_id="#V#retry_drift_workflow", + publication_spec=expected_spec, + ) + + def test_support_concepts_materialise_relationships_and_text_relations( monkeypatch, ) -> None: @@ -721,7 +759,7 @@ def test_equal_repo_seed_version_preserves_altered_live_authority( assert invalidations == [] -def test_repo_seed_newer_version_refreshes_current_materialisation_metadata( +def test_repo_seed_newer_version_accepts_known_legacy_digest_with_stale_marker_sha( monkeypatch, ) -> None: bundle = { @@ -746,9 +784,14 @@ def test_repo_seed_newer_version_refreshes_current_materialisation_metadata( } legacy_payload = {"authority": "reviewed-v1"} target_payload = {"authority": "target-v2"} + legacy_payload_sha256 = seed_bootstrap._stable_payload_sha256(legacy_payload) + stale_marker_sha256 = seed_bootstrap._stable_payload_sha256( + {"authority": "old-exporter-projection-of-reviewed-v1"} + ) + assert stale_marker_sha256 != legacy_payload_sha256 bundle["known_legacy_authority_payload_sha256_by_seed_version"] = { "#V#test_workflow": { - "1": [seed_bootstrap._stable_payload_sha256(legacy_payload)] + "1": [legacy_payload_sha256] } } live_state = {"payload": legacy_payload} @@ -765,7 +808,10 @@ def test_repo_seed_newer_version_refreshes_current_materialisation_metadata( monkeypatch.setattr( seed_bootstrap, "_load_workflow_repo_seed_version_marker", - lambda _workflow_id: {"seed_version": "1"}, + lambda _workflow_id: { + "seed_version": "1", + "authority_payload_sha256": stale_marker_sha256, + }, ) monkeypatch.setattr( seed_bootstrap, @@ -888,6 +934,13 @@ def _publish_with_context_capture(**kwargs): assert result["repo_seed_version_gate"]["refresh_workflow_ids"] == [ "#V#test_workflow" ] + adjudication = result["repo_seed_version_gate"][ + "authority_adjudication_by_workflow" + ]["#V#test_workflow"] + assert adjudication["reason"] == "exact_reviewed_legacy_migration" + assert adjudication["publication_authorised"] is True + assert adjudication["exact_reviewed_source"] is True + assert adjudication["observed_authority_payload_sha256"] == legacy_payload_sha256 assert publications assert publications[0]["validate_after_publish"] is False assert publications[0]["event_workflow_integration"] == "0" diff --git a/tests/backend/test_workflow_repo_seed_export_service.py b/tests/backend/test_workflow_repo_seed_export_service.py index d845212ca..2064f0493 100644 --- a/tests/backend/test_workflow_repo_seed_export_service.py +++ b/tests/backend/test_workflow_repo_seed_export_service.py @@ -2,6 +2,7 @@ import json from pathlib import Path +from types import SimpleNamespace from src.backend.workflows import workflow_repo_seed_export_service as export_service @@ -83,3 +84,40 @@ def _unexpected_loader(_asset_path): } ], } + + +def test_publication_spec_export_preserves_step_retry_policy() -> None: + retry_policy = { + "schema_version": "workflow_step_retry_policy.v1", + "max_attempts": 2, + "backoff_policy": "fixed", + "initial_delay_ms": 1000, + "max_delay_ms": 1000, + "retry_on_outcomes": ["failure"], + } + definition = SimpleNamespace( + initial_state="read_back", + states={ + "read_back": SimpleNamespace( + actions=( + SimpleNamespace( + action_id="workflow_mcp.invoke_tool", + contract_concept_id=None, + execution_mode="deterministic", + llm_policy=None, + validation_policy=None, + inputs={"tool_name": "fetch_concept"}, + ), + ), + transitions=(), + metadata={"retry_policy": retry_policy}, + ) + }, + ) + + payload = export_service._build_publication_spec_payload_from_definition( + workflow_id="#V#retry_export_workflow", + definition=definition, + ) + + assert payload["steps"][0]["retry_policy"] == retry_policy