From f1a0758dbaf7e37ba1b85bcebe9d36c9afce265c Mon Sep 17 00:00:00 2001 From: witbrock Date: Thu, 23 Jul 2026 23:26:17 +0100 Subject: [PATCH] Materialise optional KR workflow outputs --- ...ate_spreadsheet_programme_workflow_seed.py | 2 +- ..._materialisation_workflow_seed_bundle.json | 9 +- ...e_representation_workflow_seed_bundle.json | 20 +-- ...erialisation_workflow_vontology_service.py | 133 ++++++++++++++++++ 4 files changed, 145 insertions(+), 19 deletions(-) diff --git a/scripts/generate_spreadsheet_programme_workflow_seed.py b/scripts/generate_spreadsheet_programme_workflow_seed.py index 053816b4d..0daeee005 100644 --- a/scripts/generate_spreadsheet_programme_workflow_seed.py +++ b/scripts/generate_spreadsheet_programme_workflow_seed.py @@ -1595,7 +1595,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": "14", + "seed_version": "15", "source_tag": "JVNAUTOSCI-2592", "managed_by": "spreadsheet_programme_workflow_vontology_service", "processing_authority_fingerprint": authority_fingerprint, 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 decf49697..39917860a 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,7 @@ "family_id": "#V#kr_design_materialisation_workflow_family", "managed_by": "kr_materialisation_workflow_vontology_service", "schema_version": "repo_seed_workflow_bundle.v1", - "seed_version": "JVNAUTOSCI-2592-write-guard-v1", + "seed_version": "2", "source_tag": "JVNAUTOSCI-2271", "supported_action_ids": [ "llm.action", @@ -201,7 +201,6 @@ }, { "key": "kr_concept_id", - "skip_if_unresolved": true, "value_from_context_options": [ "current_kr_concept_spec.existing_concept_id", "current_kr_concept_spec.concept_id" @@ -209,7 +208,6 @@ }, { "key": "kr_concept_create_concepts", - "skip_if_unresolved": true, "value_from_context": "current_kr_concept_spec.concepts" }, { @@ -221,12 +219,10 @@ }, { "key": "kr_concept_parent_rationale", - "skip_if_unresolved": true, "value_from_context": "current_kr_concept_spec.parent_rationale" }, { "key": "kr_concept_blocking_reason", - "skip_if_unresolved": true, "value_from_context": "current_kr_concept_spec.blocking_reason" } ] @@ -1055,7 +1051,6 @@ [ { "key": "kr_relationship_key", - "skip_if_unresolved": true, "value_from_context_options": [ "current_kr_relationship_spec.key", "current_kr_relationship_spec.relationship_key" @@ -1075,12 +1070,10 @@ }, { "key": "kr_relationship_rationale", - "skip_if_unresolved": true, "value_from_context": "current_kr_relationship_spec.rationale" }, { "key": "kr_relationship_blocking_reason", - "skip_if_unresolved": true, "value_from_context": "current_kr_relationship_spec.blocking_reason" } ] 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 3f9aa9e16..fe33f3f3c 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:25b49e8f2585139ca51a46914f7e34a373320e99a4c5b8e3c7abfde639ea61f4", + "canonical_payload_sha256": "sha256:ce53d8c2ba252fb1b251f778444a797cb4c436fc2bbb94fa50391b5c7a1307f6", "family_id": "#V#kr_design_materialisation_workflow_family", - "seed_version": "JVNAUTOSCI-2592-write-guard-v1" + "seed_version": "2" } }, - "processing_authority_fingerprint": "sha256:a9e37d52c2f4d842cdf15793c08a6f0ddaa25c764fcccb21902a14bf2c6bc1ac", + "processing_authority_fingerprint": "sha256:62aaed4941a0e0dbb16d5c4bd6c2fc8b09ff34f1e905c561c40004c04d840c87", "schema_version": "repo_seed_workflow_bundle.v1", - "seed_version": "14", + "seed_version": "15", "source_tag": "JVNAUTOSCI-2592", "support_concepts": [ { @@ -306,7 +306,7 @@ }, { "key": "processing_authority_fingerprint", - "value": "sha256:a9e37d52c2f4d842cdf15793c08a6f0ddaa25c764fcccb21902a14bf2c6bc1ac" + "value": "sha256:62aaed4941a0e0dbb16d5c4bd6c2fc8b09ff34f1e905c561c40004c04d840c87" } ], "tool_output_mapping_specs": [ @@ -702,7 +702,7 @@ }, { "key": "processing_authority_fingerprint", - "value": "sha256:a9e37d52c2f4d842cdf15793c08a6f0ddaa25c764fcccb21902a14bf2c6bc1ac" + "value": "sha256:62aaed4941a0e0dbb16d5c4bd6c2fc8b09ff34f1e905c561c40004c04d840c87" } ], "tool_output_mapping_specs": [ @@ -776,7 +776,7 @@ }, { "key": "processing_authority_fingerprint", - "value": "sha256:a9e37d52c2f4d842cdf15793c08a6f0ddaa25c764fcccb21902a14bf2c6bc1ac" + "value": "sha256:62aaed4941a0e0dbb16d5c4bd6c2fc8b09ff34f1e905c561c40004c04d840c87" } ], "tool_output_mapping_specs": [ @@ -1234,7 +1234,7 @@ }, { "key": "processing_authority_fingerprint", - "value": "sha256:a9e37d52c2f4d842cdf15793c08a6f0ddaa25c764fcccb21902a14bf2c6bc1ac" + "value": "sha256:62aaed4941a0e0dbb16d5c4bd6c2fc8b09ff34f1e905c561c40004c04d840c87" } ], "tool_output_mapping_specs": [ @@ -1557,7 +1557,7 @@ }, { "key": "processing_authority_fingerprint", - "value": "sha256:a9e37d52c2f4d842cdf15793c08a6f0ddaa25c764fcccb21902a14bf2c6bc1ac" + "value": "sha256:62aaed4941a0e0dbb16d5c4bd6c2fc8b09ff34f1e905c561c40004c04d840c87" } ], "tool_output_mapping_specs": [ @@ -1625,7 +1625,7 @@ }, { "key": "processing_authority_fingerprint", - "value": "sha256:a9e37d52c2f4d842cdf15793c08a6f0ddaa25c764fcccb21902a14bf2c6bc1ac" + "value": "sha256:62aaed4941a0e0dbb16d5c4bd6c2fc8b09ff34f1e905c561c40004c04d840c87" } ], "tool_output_mapping_specs": [ diff --git a/tests/backend/test_kr_materialisation_workflow_vontology_service.py b/tests/backend/test_kr_materialisation_workflow_vontology_service.py index f4ada9a50..e221a0a2a 100644 --- a/tests/backend/test_kr_materialisation_workflow_vontology_service.py +++ b/tests/backend/test_kr_materialisation_workflow_vontology_service.py @@ -7,6 +7,19 @@ from scripts import publish_kr_materialisation_workflows as publish_cli from src.backend.services import kr_materialisation_workflow_vontology_service as mod +from src.backend.workflows.action_registry import ( + ActionRegistry, + WorkflowEnvironment, +) +from src.backend.workflows.durable.control_flow_actions import ( + register_control_flow_actions, +) +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, +) _PROJECT_ROOT = Path(__file__).resolve().parents[2] _SEED_BUNDLE_PATH = ( @@ -47,6 +60,58 @@ def _iter_publication_steps(bundle: Mapping[str, Any]) -> Iterator[Mapping[str, yield step +def _publication_step( + bundle: Mapping[str, Any], + *, + workflow_id: str, + state_id: str, +) -> Mapping[str, Any]: + workflow = next( + row + for row in bundle.get("workflows") or [] + if isinstance(row, Mapping) and row.get("workflow_id") == workflow_id + ) + publication_spec = workflow.get("publication_spec") + assert isinstance(publication_spec, Mapping) + return next( + row + for row in publication_spec.get("steps") or [] + if isinstance(row, Mapping) and row.get("state_id") == state_id + ) + + +def _validate_context_set_writes( + *, + step: Mapping[str, Any], + context: dict[str, Any], +) -> dict[str, Any]: + bindings = dict(step.get("static_input_bindings") or []) + assignments = bindings.get("assignments") + assert isinstance(assignments, list) + registry = ActionRegistry() + register_control_flow_actions( + registry, + definition_loader=lambda _workflow_id: None, + ) + context_before = dict(context) + result = registry.execute( + WORKFLOW_CONTROL_ACTION_CONTEXT_SET_ID, + inputs={"assignments": assignments}, + context=context, + env=WorkflowEnvironment(llm_client=None), + ) + assert result.status == "success" + context.update(result.outputs) + validation = validate_state_metadata_post_action( + state_id=str(step.get("state_id") or ""), + metadata={"writes_context_keys": step.get("writes_context_keys") or []}, + context_before=context_before, + context_after=context, + ) + assert validation.ok is True + return context + + def test_kr_seed_bundle_uses_prompt_concepts_not_inline_prompt_text() -> None: bundle = json.loads(_SEED_BUNDLE_PATH.read_text(encoding="utf-8")) rendered_bundle = json.dumps(bundle, sort_keys=True) @@ -145,6 +210,74 @@ def test_kr_seed_bundle_validates_as_workflow_contracts() -> None: assert validation["valid"] is True +def test_kr_optional_initialiser_fields_satisfy_enforced_write_metadata() -> None: + bundle = json.loads(_SEED_BUNDLE_PATH.read_text(encoding="utf-8")) + concept_step = _publication_step( + bundle, + workflow_id=mod.KR_DESIGN_CONCEPT_MATERIALISATION_ITEM_WORKFLOW_ID, + state_id="initialise_from_concept_spec", + ) + relationship_step = _publication_step( + bundle, + workflow_id=mod.KR_DESIGN_RELATIONSHIP_ASSERTION_ITEM_WORKFLOW_ID, + state_id="initialise_from_relationship_spec", + ) + + reuse_context = _validate_context_set_writes( + step=concept_step, + context={ + "current_kr_concept_spec": { + "key": "candidate", + "decision": "reuse_existing", + "target_name": "Stable candidate", + "target_kind": "instance", + "parent_id": "#V#doctoral_candidate", + "existing_concept_id": "#V#stable_candidate", + "description_text": "Bounded provenance-bearing candidate.", + } + }, + ) + assert reuse_context["kr_concept_create_concepts"] is None + assert reuse_context["kr_concept_parent_rationale"] is None + assert reuse_context["kr_concept_blocking_reason"] is None + + create_context = _validate_context_set_writes( + step=concept_step, + context={ + "current_kr_concept_spec": { + "key": "candidate", + "decision": "create", + "target_name": "Stable candidate", + "target_kind": "instance", + "parent_id": "#V#doctoral_candidate", + "concepts": [ + { + "name": "Stable candidate", + "kind": "instance", + "description": "Bounded provenance-bearing candidate.", + } + ], + "description_text": "Bounded provenance-bearing candidate.", + } + }, + ) + assert create_context["kr_concept_id"] is None + + relationship_context = _validate_context_set_writes( + step=relationship_step, + context={ + "current_kr_relationship_spec": { + "source_id": "#V#stable_candidate", + "predicate": "#V#has_doctoral_programme", + "target_id": "#V#stable_programme", + } + }, + ) + assert relationship_context["kr_relationship_key"] is None + assert relationship_context["kr_relationship_rationale"] is None + assert relationship_context["kr_relationship_blocking_reason"] is None + + def test_publish_script_validate_all_uses_seed_bundle(capsys) -> None: exit_code = publish_cli.main(["validate-all"]) output = capsys.readouterr().out