From 1d586dbe27d84f1c53974e2c64e3b9986a88d29a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=A8=E7=82=8E?= <635735027@qq.com> Date: Wed, 6 May 2026 15:22:49 +0800 Subject: [PATCH] generated-scene: add command-center automation semantics --- src/generated_scene/generator.rs | 387 +++++++++++++++++- src/generated_scene/ir.rs | 144 +++++++ .../scheduled_monitoring_runtime.rs | 13 + ...oring_action_binary_wiring_2026-04-22.json | 8 +- tests/scene_generator_test.rs | 78 ++++ ...ed_monitoring_action_binary_wiring_test.rs | 227 ++++++++++ ...nitoring_generated_scene_hardening_test.rs | 52 +++ 7 files changed, 894 insertions(+), 15 deletions(-) diff --git a/src/generated_scene/generator.rs b/src/generated_scene/generator.rs index 86effa8..ee8a46a 100644 --- a/src/generated_scene/generator.rs +++ b/src/generated_scene/generator.rs @@ -13,13 +13,15 @@ use crate::generated_scene::analyzer::{ use crate::generated_scene::ir::{ ApiEndpointIr, ArtifactContractIr, BootstrapIr, EnrichmentRequestIr, EvidenceIr, ExportPlanIr, LegacySceneInfoJson, MainRequestIr, MergeFieldMappingIr, MergePlanIr, ModeConditionIr, ModeIr, - MonitoringActionWorkflowIr, MonitoringDeltaStateIr, MonitoringDependencyIr, - MonitoringEncryptionResolutionIr, MonitoringOutputContractIr, MonitoringReadSliceIr, - MonitoringRuntimeContextIr, MonitoringSideEffectIr, MonitoringSideEffectPolicyIr, - MonitoringSidecarOutputIr, MonitoringStorageReadIr, MonitoringTimeoutContractIr, - NormalizeRulesIr, PaginationPlanIr, ParamIr, ReadinessGateIr, ReadinessIr, - RequestFieldMappingIr, RuntimeDependencyIr, SceneIdDiagnosticsIr, SceneIr, ValidationHintsIr, - WorkflowArchetype, WorkflowEvidenceIr, WorkflowStepIr, + MonitoringActionContractIr, MonitoringActionWorkflowIr, MonitoringDeltaStateIr, + MonitoringDependencyIr, MonitoringEncryptionResolutionIr, MonitoringExecutionFlowIr, + MonitoringExecutionStepIr, MonitoringIterationContractIr, MonitoringLogWriteContractIr, + MonitoringOutputContractIr, MonitoringQueueTransitionRuleIr, MonitoringReadSliceIr, + MonitoringResultStateMachineIr, MonitoringRuntimeContextIr, MonitoringSideEffectIr, + MonitoringSideEffectPolicyIr, MonitoringSidecarOutputIr, MonitoringStorageReadIr, + MonitoringTimeoutContractIr, NormalizeRulesIr, PaginationPlanIr, ParamIr, ReadinessGateIr, + ReadinessIr, RequestFieldMappingIr, RuntimeDependencyIr, SceneIdDiagnosticsIr, SceneIr, + ValidationHintsIr, WorkflowArchetype, WorkflowEvidenceIr, WorkflowStepIr, }; use crate::generated_scene::lessons::{ load_generation_lessons, GenerationLessons, BUILTIN_REPORT_COLLECTION_LESSONS, @@ -312,7 +314,13 @@ pub fn generate_scheduled_monitoring_action_skill_package( let trigger_contract = read_json_file(&request.trigger_contract_json, "scheduled monitoring trigger contract")?; - let workflow = monitoring_workflow_from_contract(&ir_contract); + let mut workflow = monitoring_workflow_from_contract(&ir_contract); + enrich_command_center_automation_semantics( + &request.scene_id, + &source_evidence, + &trigger_contract, + &mut workflow, + ); let scene_ir = monitoring_action_scene_ir( &GenerateMonitoringActionPreviewRequest { scene_id: request.scene_id.clone(), @@ -1227,6 +1235,12 @@ fn monitoring_workflow_from_contract(contract: &Value) -> MonitoringActionWorkfl }) .unwrap_or_else(|| monitoring_blocked_actions_from_platform_dependencies(platform_dependencies)), }, + action_contracts: Vec::new(), + iteration_contract: None, + execution_flow: None, + result_state_machines: Vec::new(), + queue_transition_rules: Vec::new(), + log_write_contracts: Vec::new(), archetype: contract["archetype"] .as_str() .unwrap_or("marketing_gateway_monitor") @@ -1234,6 +1248,278 @@ fn monitoring_workflow_from_contract(contract: &Value) -> MonitoringActionWorkfl } } +fn enrich_command_center_automation_semantics( + scene_id: &str, + source_evidence: &Value, + trigger_contract: &Value, + workflow: &mut MonitoringActionWorkflowIr, +) { + if !is_command_center_monitoring_scene(scene_id, &workflow.workflow_id) { + return; + } + + let dispatch_dependency = workflow + .business_api_dependencies + .iter() + .find(|item| { + item.url.contains("repetCtrlSend") + || item.classification == "dispatch_exception_order" + || item.classification == "business_dispatch" + }) + .cloned() + .unwrap_or_else(|| MonitoringDependencyIr { + name: "repetCtrlSend".to_string(), + url: "http://yxgateway.gs.sgcc.com.cn/emss-chargacctgf-paysrv-front/member/acctabnor/repetCtrlSend" + .to_string(), + classification: "dispatch_exception_order".to_string(), + side_effect: true, + blocked_by_default: true, + }); + let dispose_log_dependency = workflow + .local_service_dependencies + .iter() + .find(|item| { + item.url.contains("setDisposeLog") + || item.classification == "write_dispose_log" + || item.classification == "dispose_log_write" + }) + .cloned() + .unwrap_or_else(|| MonitoringDependencyIr { + name: "setDisposeLog".to_string(), + url: "http://localhost:13313/MonitorServices/setDisposeLog".to_string(), + classification: "write_dispose_log".to_string(), + side_effect: true, + blocked_by_default: true, + }); + let queue_action_ref = trigger_contract["platformRuntimeCapabilities"]["hostActionBridge"] + .as_array() + .and_then(|items| { + items.iter().find_map(|item| { + if item["name"].as_str().unwrap_or_default() == "mac.exeTQueue" { + Some(item["name"].as_str().unwrap_or_default().to_string()) + } else { + None + } + }) + }) + .unwrap_or_else(|| "mac.exeTQueue".to_string()); + + let dispatch_dependency_ref = if dispatch_dependency.url.is_empty() { + dispatch_dependency.classification.clone() + } else { + dispatch_dependency.url.clone() + }; + let dispose_log_dependency_ref = if dispose_log_dependency.url.is_empty() { + dispose_log_dependency.classification.clone() + } else { + dispose_log_dependency.url.clone() + }; + let queue_required_mode = trigger_contract["triggerContracts"]["queue"]["futureModes"] + .as_array() + .and_then(|items| items.first()) + .and_then(Value::as_str) + .unwrap_or("queue_process") + .to_string(); + + let mut dispatch_field_bindings = Map::new(); + dispatch_field_bindings.insert("chooseList".to_string(), json!(["$current_item"])); + dispatch_field_bindings.insert( + "itemIdentity".to_string(), + json!(["consNo", "custNo", "sendBeginTime", "createTime"]), + ); + + let mut dispose_log_field_bindings = Map::new(); + dispose_log_field_bindings.insert( + "orderID".to_string(), + json!({ + "from": ["current_item.consNo", "current_item.custNo", "current_item.sendBeginTime", "current_item.createTime"], + "strategy": "join_non_empty_with_underscore" + }), + ); + dispose_log_field_bindings.insert( + "name".to_string(), + json!({ + "from": ["current_item.orgName", "current_item.consName", "current_item.phone"], + "strategy": "first_non_empty" + }), + ); + dispose_log_field_bindings.insert("time".to_string(), json!({"from": "runtime.now"})); + + let action_contract = MonitoringActionContractIr { + action_id: "dispatch_exception_order".to_string(), + action_type: "business_dispatch".to_string(), + dependency_ref: dispatch_dependency_ref.clone(), + target_endpoint_or_host_call: if dispatch_dependency.url.is_empty() { + "repetCtrlSend".to_string() + } else { + dispatch_dependency.url.clone() + }, + execution_context: "attached_page_http_post".to_string(), + input_source: "current_item".to_string(), + request_template: json!({ + "busType": "03", + "chooseList": ["$current_item"] + }), + field_bindings: dispatch_field_bindings, + auth_binding: json!({ + "source": "localStorage.markToken", + "targetField": "auth_token" + }), + encryption_binding: json!({ + "mode": workflow.runtime_context.encryption_mode, + "payloadSource": "request_body_json" + }), + result_channel: "browser_callback_js_result".to_string(), + }; + let iteration_contract = MonitoringIterationContractIr { + iteration_id: "pending_dispatch_loop".to_string(), + source_collection: source_evidence["queueDependencies"] + .as_array() + .and_then(|items| { + items.iter().find_map(|item| { + if item["name"].as_str().unwrap_or_default() == "pendingList" { + Some("pendingList".to_string()) + } else { + None + } + }) + }) + .unwrap_or_else(|| "pendingList".to_string()), + iteration_mode: "sequential_per_item".to_string(), + item_alias: "current_item".to_string(), + on_empty_transition: "queue_continue_on_empty".to_string(), + on_item_complete_transition: "next_item".to_string(), + on_all_complete_transition: "queue_continue_on_done".to_string(), + }; + let execution_flow = MonitoringExecutionFlowIr { + flow_id: "command_center_dispatch_preview_flow".to_string(), + entry_step: "dispatch_loop".to_string(), + steps: vec![ + MonitoringExecutionStepIr { + step_id: "dispatch_loop".to_string(), + step_type: "iterate".to_string(), + iteration_ref: "pending_dispatch_loop".to_string(), + next_on_empty: "queue_continue_empty".to_string(), + next_on_done: "queue_continue_done".to_string(), + next_on_success: "dispatch_current_item".to_string(), + ..MonitoringExecutionStepIr::default() + }, + MonitoringExecutionStepIr { + step_id: "dispatch_current_item".to_string(), + step_type: "action".to_string(), + action_contract_ref: "dispatch_exception_order".to_string(), + next_on_success: "write_dispose_log".to_string(), + next_on_failure: "write_dispose_log".to_string(), + ..MonitoringExecutionStepIr::default() + }, + MonitoringExecutionStepIr { + step_id: "write_dispose_log".to_string(), + step_type: "log_write".to_string(), + log_write_contract_ref: "dispose_log_after_dispatch".to_string(), + next_on_success: "dispatch_loop".to_string(), + next_on_failure: "dispatch_loop".to_string(), + ..MonitoringExecutionStepIr::default() + }, + MonitoringExecutionStepIr { + step_id: "queue_continue_empty".to_string(), + step_type: "queue_transition".to_string(), + queue_transition_ref: "queue_continue_on_empty".to_string(), + next_on_success: "done".to_string(), + ..MonitoringExecutionStepIr::default() + }, + MonitoringExecutionStepIr { + step_id: "queue_continue_done".to_string(), + step_type: "queue_transition".to_string(), + queue_transition_ref: "queue_continue_on_done".to_string(), + next_on_success: "done".to_string(), + ..MonitoringExecutionStepIr::default() + }, + MonitoringExecutionStepIr { + step_id: "done".to_string(), + step_type: "terminal".to_string(), + ..MonitoringExecutionStepIr::default() + }, + ], + }; + let result_state_machine = MonitoringResultStateMachineIr { + state_machine_id: "dispatch_result_state_machine".to_string(), + action_contract_ref: "dispatch_exception_order".to_string(), + success_match: json!({ + "allOf": [ + {"field": "code", "equals": "00000"}, + {"field": "message", "equals": "success"} + ] + }), + failure_match: json!({ + "fallback": "not_success_match" + }), + state_on_success: "success".to_string(), + state_on_failure: "failure".to_string(), + post_success_log_contract_ref: "dispose_log_after_dispatch".to_string(), + post_failure_log_contract_ref: "dispose_log_after_dispatch".to_string(), + continue_policy: "continue_next_item".to_string(), + }; + let queue_transition_rules = vec![ + MonitoringQueueTransitionRuleIr { + transition_id: "queue_continue_on_empty".to_string(), + queue_action_ref: queue_action_ref.clone(), + trigger_point: "on_empty_collection".to_string(), + required_mode: queue_required_mode.clone(), + blocked_by_default: true, + }, + MonitoringQueueTransitionRuleIr { + transition_id: "queue_continue_on_done".to_string(), + queue_action_ref, + trigger_point: "on_all_items_done".to_string(), + required_mode: queue_required_mode, + blocked_by_default: true, + }, + ]; + let log_write_contract = MonitoringLogWriteContractIr { + log_id: "dispose_log_after_dispatch".to_string(), + dependency_ref: dispose_log_dependency_ref, + target_endpoint_or_host_call: if dispose_log_dependency.url.is_empty() { + "setDisposeLog".to_string() + } else { + dispose_log_dependency.url.clone() + }, + emit_phase: "after_dispatch_result".to_string(), + payload_template: json!({ + "type": "fee_control_exception_dispose", + "orderID": "$derived.orderID", + "name": "$derived.name", + "time": "$runtime.now", + "state": "$dispatch.state" + }), + field_bindings: dispose_log_field_bindings, + state_binding: "dispatch_result_state".to_string(), + }; + + if workflow.action_contracts.is_empty() { + workflow.action_contracts = vec![action_contract]; + } + if workflow.iteration_contract.is_none() { + workflow.iteration_contract = Some(iteration_contract); + } + if workflow.execution_flow.is_none() { + workflow.execution_flow = Some(execution_flow); + } + if workflow.result_state_machines.is_empty() { + workflow.result_state_machines = vec![result_state_machine]; + } + if workflow.queue_transition_rules.is_empty() { + workflow.queue_transition_rules = queue_transition_rules; + } + if workflow.log_write_contracts.is_empty() { + workflow.log_write_contracts = vec![log_write_contract]; + } +} + +fn is_command_center_monitoring_scene(scene_id: &str, workflow_id: &str) -> bool { + scene_id == "command-center-fee-control-monitor" + || workflow_id == "command_center_fee_control_monitoring_action" +} + fn monitoring_dependencies(value: &Value) -> Vec { value .as_array() @@ -7257,14 +7543,28 @@ if (typeof module !== 'undefined') {{ } fn compile_scheduled_monitoring_action_plan_script(scene_ir: &SceneIr) -> String { + let workflow = scene_ir + .monitoring_action_workflow + .as_ref() + .cloned() + .unwrap_or_default(); let blocked = scene_ir .monitoring_action_workflow .as_ref() .map(|workflow| workflow.side_effect_policy.blocked_call_signatures.clone()) .unwrap_or_default(); let blocked_json = serde_json::to_string_pretty(&blocked).unwrap_or_else(|_| "[]".to_string()); + let action_contracts_json = + serde_json::to_string_pretty(&workflow.action_contracts).unwrap_or_else(|_| "[]".to_string()); + let queue_transitions_json = serde_json::to_string_pretty(&workflow.queue_transition_rules) + .unwrap_or_else(|_| "[]".to_string()); + let log_write_contracts_json = + serde_json::to_string_pretty(&workflow.log_write_contracts).unwrap_or_else(|_| "[]".to_string()); format!( r#"const BLOCKED_CALL_SIGNATURES = {blocked_json}; +const ACTION_CONTRACTS = {action_contracts_json}; +const QUEUE_TRANSITION_RULES = {queue_transitions_json}; +const LOG_WRITE_CONTRACTS = {log_write_contracts_json}; function normalizeDecision(decision) {{ if (typeof decision === 'string') {{ @@ -7276,22 +7576,42 @@ function normalizeDecision(decision) {{ function buildActionPlan(decision = {{}}, args = {{}}) {{ const source = normalizeDecision(decision); const pendingList = Array.isArray(source.pendingList) ? source.pendingList : []; + const dispatchContract = ACTION_CONTRACTS.find(item => item.actionId === 'dispatch_exception_order') || ACTION_CONTRACTS[0] || {{}}; const actionPlan = pendingList.map((item, index) => ({{ itemId: String(item.id || item.consNo || item.custNo || item.workOrderId || index), - actionType: 'business_dispatch', - targetEndpointOrHostCall: 'repetCtrlSend', + actionType: dispatchContract.actionType || 'business_dispatch', + actionContractRef: dispatchContract.actionId || 'dispatch_exception_order', + targetEndpointOrHostCall: dispatchContract.targetEndpointOrHostCall || 'repetCtrlSend', blockedByDefault: true, requiresFutureGate: 'dispatch_gate', reason: 'scheduled monitoring preview only' }})); + const queueTransitions = pendingList.length > 0 + ? QUEUE_TRANSITION_RULES.filter(item => item.transitionId === 'queue_continue_on_done') + : QUEUE_TRANSITION_RULES.filter(item => item.transitionId === 'queue_continue_on_empty'); + const logWritePreview = pendingList.length > 0 + ? LOG_WRITE_CONTRACTS.map(item => ({{ + logId: item.logId, + targetEndpointOrHostCall: item.targetEndpointOrHostCall, + emitPhase: item.emitPhase, + blockedByDefault: true + }})) + : []; return {{ type: 'scheduled-monitoring-action-plan-preview', status: 'action-plan-ok', mode: args.mode || source.mode || 'monitor_only', actionPlan, + queueTransitions, + logWritePreview, blockedSideEffects: {{ blockedCallSignatures: BLOCKED_CALL_SIGNATURES }}, + summary: {{ + action_plan_count: actionPlan.length, + queue_transition_count: queueTransitions.length, + log_write_preview_count: logWritePreview.length + }}, sideEffectCounters: {{ repetCtrlSend: 0, sendMessages: 0, @@ -7308,7 +7628,10 @@ if (typeof module !== 'undefined') {{ module.exports = {{ buildActionPlan }}; }} "#, - blocked_json = blocked_json + blocked_json = blocked_json, + action_contracts_json = action_contracts_json, + queue_transitions_json = queue_transitions_json, + log_write_contracts_json = log_write_contracts_json ) } @@ -8867,6 +9190,12 @@ fn scheduled_monitoring_generation_report( "sidecarOutputs": workflow.runtime_context.output_contract.sidecar_outputs, "deltaState": workflow.runtime_context.output_contract.delta_state, }, + "actionContracts": workflow.action_contracts, + "iterationContract": workflow.iteration_contract, + "executionFlow": workflow.execution_flow, + "resultStateMachines": workflow.result_state_machines, + "queueTransitionRules": workflow.queue_transition_rules, + "logWriteContracts": workflow.log_write_contracts, "triggerContractStatus": trigger_contract["status"], "readiness": scene_ir.readiness, }) @@ -9129,4 +9458,40 @@ mod tests { assert!(rendered.contains("[[runtime_context.output_contract.sidecar_outputs]]")); assert!(rendered.contains("archetype = \"business_page_report_monitor\"")); } + + #[test] + fn command_center_enrichment_backfills_missing_semantics_without_overwriting_existing_ones() { + let mut workflow = test_workflow("marketing_gateway_monitor"); + workflow.workflow_id = "command_center_fee_control_monitoring_action".to_string(); + workflow.action_contracts.push(MonitoringActionContractIr { + action_id: "preexisting_dispatch".to_string(), + ..MonitoringActionContractIr::default() + }); + + enrich_command_center_automation_semantics( + "command-center-fee-control-monitor", + &json!({}), + &json!({ + "triggerContracts": { + "queue": { + "futureModes": ["queue_process"] + } + }, + "platformRuntimeCapabilities": { + "hostActionBridge": [ + { "name": "mac.exeTQueue" } + ] + } + }), + &mut workflow, + ); + + assert_eq!(workflow.action_contracts.len(), 1); + assert_eq!(workflow.action_contracts[0].action_id, "preexisting_dispatch"); + assert!(workflow.iteration_contract.is_some()); + assert!(workflow.execution_flow.is_some()); + assert_eq!(workflow.result_state_machines.len(), 1); + assert_eq!(workflow.queue_transition_rules.len(), 2); + assert_eq!(workflow.log_write_contracts.len(), 1); + } } diff --git a/src/generated_scene/ir.rs b/src/generated_scene/ir.rs index 67599d6..1f52084 100644 --- a/src/generated_scene/ir.rs +++ b/src/generated_scene/ir.rs @@ -477,6 +477,138 @@ pub struct MonitoringSideEffectPolicyIr { pub blocked_actions: Vec, } +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct MonitoringActionContractIr { + #[serde(rename = "actionId", default)] + pub action_id: String, + #[serde(rename = "actionType", default)] + pub action_type: String, + #[serde(rename = "dependencyRef", default)] + pub dependency_ref: String, + #[serde(rename = "targetEndpointOrHostCall", default)] + pub target_endpoint_or_host_call: String, + #[serde(rename = "executionContext", default)] + pub execution_context: String, + #[serde(rename = "inputSource", default)] + pub input_source: String, + #[serde(rename = "requestTemplate", default)] + pub request_template: Value, + #[serde(rename = "fieldBindings", default)] + pub field_bindings: Map, + #[serde(rename = "authBinding", default)] + pub auth_binding: Value, + #[serde(rename = "encryptionBinding", default)] + pub encryption_binding: Value, + #[serde(rename = "resultChannel", default)] + pub result_channel: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct MonitoringIterationContractIr { + #[serde(rename = "iterationId", default)] + pub iteration_id: String, + #[serde(rename = "sourceCollection", default)] + pub source_collection: String, + #[serde(rename = "iterationMode", default)] + pub iteration_mode: String, + #[serde(rename = "itemAlias", default)] + pub item_alias: String, + #[serde(rename = "onEmptyTransition", default)] + pub on_empty_transition: String, + #[serde(rename = "onItemCompleteTransition", default)] + pub on_item_complete_transition: String, + #[serde(rename = "onAllCompleteTransition", default)] + pub on_all_complete_transition: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct MonitoringExecutionStepIr { + #[serde(rename = "stepId", default)] + pub step_id: String, + #[serde(rename = "stepType", default)] + pub step_type: String, + #[serde(rename = "iterationRef", default)] + pub iteration_ref: String, + #[serde(rename = "actionContractRef", default)] + pub action_contract_ref: String, + #[serde(rename = "logWriteContractRef", default)] + pub log_write_contract_ref: String, + #[serde(rename = "queueTransitionRef", default)] + pub queue_transition_ref: String, + #[serde(rename = "nextOnSuccess", default)] + pub next_on_success: String, + #[serde(rename = "nextOnFailure", default)] + pub next_on_failure: String, + #[serde(rename = "nextOnEmpty", default)] + pub next_on_empty: String, + #[serde(rename = "nextOnDone", default)] + pub next_on_done: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct MonitoringExecutionFlowIr { + #[serde(rename = "flowId", default)] + pub flow_id: String, + #[serde(rename = "entryStep", default)] + pub entry_step: String, + #[serde(rename = "steps", default)] + pub steps: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct MonitoringResultStateMachineIr { + #[serde(rename = "stateMachineId", default)] + pub state_machine_id: String, + #[serde(rename = "actionContractRef", default)] + pub action_contract_ref: String, + #[serde(rename = "successMatch", default)] + pub success_match: Value, + #[serde(rename = "failureMatch", default)] + pub failure_match: Value, + #[serde(rename = "stateOnSuccess", default)] + pub state_on_success: String, + #[serde(rename = "stateOnFailure", default)] + pub state_on_failure: String, + #[serde(rename = "postSuccessLogContractRef", default)] + pub post_success_log_contract_ref: String, + #[serde(rename = "postFailureLogContractRef", default)] + pub post_failure_log_contract_ref: String, + #[serde(rename = "continuePolicy", default)] + pub continue_policy: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct MonitoringQueueTransitionRuleIr { + #[serde(rename = "transitionId", default)] + pub transition_id: String, + #[serde(rename = "queueActionRef", default)] + pub queue_action_ref: String, + #[serde(rename = "triggerPoint", default)] + pub trigger_point: String, + #[serde(rename = "requiredMode", default)] + pub required_mode: String, + #[serde(rename = "blockedByDefault", default)] + pub blocked_by_default: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct MonitoringLogWriteContractIr { + #[serde(rename = "logId", default)] + pub log_id: String, + #[serde(rename = "dependencyRef", default)] + pub dependency_ref: String, + #[serde(rename = "targetEndpointOrHostCall", default)] + pub target_endpoint_or_host_call: String, + #[serde(rename = "emitPhase", default)] + pub emit_phase: String, + #[serde(rename = "payloadTemplate", default)] + pub payload_template: Value, + #[serde(rename = "fieldBindings", default)] + pub field_bindings: Map, + #[serde(rename = "stateBinding", default)] + pub state_binding: String, +} + #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct MonitoringActionWorkflowIr { #[serde(rename = "workflowId", default)] @@ -505,6 +637,18 @@ pub struct MonitoringActionWorkflowIr { pub preview_schema: Vec, #[serde(rename = "sideEffectPolicy", default)] pub side_effect_policy: MonitoringSideEffectPolicyIr, + #[serde(rename = "actionContracts", default)] + pub action_contracts: Vec, + #[serde(rename = "iterationContract", default)] + pub iteration_contract: Option, + #[serde(rename = "executionFlow", default)] + pub execution_flow: Option, + #[serde(rename = "resultStateMachines", default)] + pub result_state_machines: Vec, + #[serde(rename = "queueTransitionRules", default)] + pub queue_transition_rules: Vec, + #[serde(rename = "logWriteContracts", default)] + pub log_write_contracts: Vec, #[serde(rename = "archetype", default = "default_monitoring_archetype")] pub archetype: String, } diff --git a/src/generated_scene/scheduled_monitoring_runtime.rs b/src/generated_scene/scheduled_monitoring_runtime.rs index ead5aca..3855f94 100644 --- a/src/generated_scene/scheduled_monitoring_runtime.rs +++ b/src/generated_scene/scheduled_monitoring_runtime.rs @@ -1301,6 +1301,16 @@ fn run_scheduled_monitoring_skill_runtime( .and_then(Value::as_array) .cloned() .unwrap_or_default(); + let queue_transitions = action_plan_preview + .get("queueTransitions") + .and_then(Value::as_array) + .cloned() + .unwrap_or_default(); + let log_write_preview = action_plan_preview + .get("logWritePreview") + .and_then(Value::as_array) + .cloned() + .unwrap_or_default(); let detect_root_cause = detect_status_root_cause(&json!({ "detectSnapshot": detect_snapshot.clone() })); @@ -1322,11 +1332,14 @@ fn run_scheduled_monitoring_skill_runtime( "pending_count": pending_count, "notify_count": notify_candidates.len(), "action_plan_count": action_plan.len(), + "queue_transition_count": queue_transitions.len(), "detect_root_cause": detect_root_cause }, "pendingList": pending_list, "notifyCandidates": notify_candidates, "actionPlan": action_plan, + "queueTransitions": queue_transitions, + "logWritePreview": log_write_preview, "blockedSideEffects": { "blockedCallSignatures": blocked_call_signatures }, diff --git a/tests/fixtures/generated_scene/scheduled_monitoring_action_binary_wiring_2026-04-22.json b/tests/fixtures/generated_scene/scheduled_monitoring_action_binary_wiring_2026-04-22.json index 8e7dc1d..a08992a 100644 --- a/tests/fixtures/generated_scene/scheduled_monitoring_action_binary_wiring_2026-04-22.json +++ b/tests/fixtures/generated_scene/scheduled_monitoring_action_binary_wiring_2026-04-22.json @@ -44,7 +44,7 @@ ], "source": "scheduled_monitoring_action_trigger_runtime_contract" }, - "finishedAt": "2026-04-26T05:30:42.489979300+00:00", + "finishedAt": "2026-05-06T04:36:29.926950300+00:00", "mode": "dry_run", "previewArtifact": { "actionPlan": [ @@ -112,7 +112,7 @@ "repetCtrlSend": 0, "sendMessages": 0 }, - "startedAt": "2026-04-26T05:30:42.489979300+00:00", + "startedAt": "2026-05-06T04:36:29.926950300+00:00", "status": "dry-run-runtime-pass", "triggerType": "queue", "warnings": [ @@ -158,7 +158,7 @@ ], "source": "scheduled_monitoring_action_trigger_runtime_contract" }, - "finishedAt": "2026-04-26T05:30:42.260973600+00:00", + "finishedAt": "2026-05-06T04:36:29.680658700+00:00", "mode": "monitor_only", "previewArtifact": { "actionPlan": [ @@ -226,7 +226,7 @@ "repetCtrlSend": 0, "sendMessages": 0 }, - "startedAt": "2026-04-26T05:30:42.260973600+00:00", + "startedAt": "2026-05-06T04:36:29.680658700+00:00", "status": "dry-run-runtime-pass", "triggerType": "scheduled", "warnings": [ diff --git a/tests/scene_generator_test.rs b/tests/scene_generator_test.rs index 7c9f501..9d7714b 100644 --- a/tests/scene_generator_test.rs +++ b/tests/scene_generator_test.rs @@ -3893,6 +3893,84 @@ fn generator_emits_scheduled_monitoring_action_skill_package() { assert!(test_status.success()); } +#[test] +fn command_center_materializes_automation_semantics_into_workflow_ir() { + let output_root = temp_workspace("sgclaw-command-center-automation-workflow-ir"); + let skill_root = generate_scheduled_monitoring_action_skill_package( + GenerateScheduledMonitoringActionSkillRequest { + scene_id: "command-center-fee-control-monitor".to_string(), + scene_name: "指挥中心费控异常监测".to_string(), + output_root: output_root.clone(), + source_evidence_json: PathBuf::from( + "tests/fixtures/generated_scene/monitoring_action_source_evidence_extraction_2026-04-21.json", + ), + ir_contract_json: PathBuf::from( + "tests/fixtures/generated_scene/scheduled_monitoring_action_ir_contract_2026-04-22.json", + ), + trigger_contract_json: PathBuf::from( + "tests/fixtures/generated_scene/scheduled_monitoring_action_trigger_runtime_contract_2026-04-22.json", + ), + }, + ) + .unwrap(); + + let workflow_ir: serde_json::Value = serde_json::from_str( + &fs::read_to_string(skill_root.join("references/workflow-ir.json")).unwrap(), + ) + .unwrap(); + + let action_contracts = workflow_ir["actionContracts"] + .as_array() + .expect("expected actionContracts array"); + assert!( + action_contracts.iter().any(|item| { + item["targetEndpointOrHostCall"] == "repetCtrlSend" + || item["actionId"] == "dispatch_exception_order" + }), + "expected action contract for repetCtrlSend dispatch" + ); + + assert_eq!( + workflow_ir["iterationContract"]["sourceCollection"], + "pendingList", + "expected iteration contract over pendingList" + ); + assert_eq!( + workflow_ir["iterationContract"]["iterationMode"], + "sequential_per_item", + "expected sequential per-item iteration" + ); + + let queue_transition_rules = workflow_ir["queueTransitionRules"] + .as_array() + .expect("expected queueTransitionRules array"); + assert!( + queue_transition_rules.iter().any(|item| { + item["triggerPoint"] == "on_empty_collection" + || item["transitionId"] == "queue_continue_on_empty" + }), + "expected queue continue transition for empty collection" + ); + assert!( + queue_transition_rules.iter().any(|item| { + item["triggerPoint"] == "on_all_items_done" + || item["transitionId"] == "queue_continue_on_done" + }), + "expected queue continue transition for completed collection" + ); + + let log_write_contracts = workflow_ir["logWriteContracts"] + .as_array() + .expect("expected logWriteContracts array"); + assert!( + log_write_contracts.iter().any(|item| { + item["targetEndpointOrHostCall"] == "setDisposeLog" + || item["logId"] == "dispose_log_after_dispatch" + }), + "expected dispose-log contract" + ); +} + #[test] fn generator_preserves_localhost_dependency_as_host_runtime_evidence() { let analysis = analyze_scene_source(Path::new( diff --git a/tests/scheduled_monitoring_action_binary_wiring_test.rs b/tests/scheduled_monitoring_action_binary_wiring_test.rs index 78dc7dd..3185287 100644 --- a/tests/scheduled_monitoring_action_binary_wiring_test.rs +++ b/tests/scheduled_monitoring_action_binary_wiring_test.rs @@ -737,6 +737,233 @@ fn binary_wiring_registry_backed_skill_executes_read_only_scripts_with_runtime_i ); } +#[test] +fn command_center_preview_reflects_automation_semantics() { + let workspace = temp_workspace("sgclaw-command-center-preview-automation-semantics"); + let trigger_path = workspace.join("scheduled-trigger.json"); + let output_path = workspace.join("run-record.json"); + let config_path = workspace.join("sgclaw_config.json"); + let rules_path = workspace.join("resources").join("rules.json"); + let materialization_root = workspace.join("materialized"); + fs::create_dir_all(&materialization_root).unwrap(); + write_json( + &trigger_path, + &scheduled_trigger_with_runtime_inputs("monitor_only"), + ); + write_runtime_rules(&rules_path); + + generate_scheduled_monitoring_action_skill_package( + GenerateScheduledMonitoringActionSkillRequest { + scene_id: "command-center-fee-control-monitor".to_string(), + scene_name: "command-center-fee-control-monitor".to_string(), + output_root: materialization_root.clone(), + source_evidence_json: PathBuf::from( + "tests/fixtures/generated_scene/monitoring_action_source_evidence_extraction_2026-04-21.json", + ), + ir_contract_json: PathBuf::from( + "tests/fixtures/generated_scene/scheduled_monitoring_action_ir_contract_2026-04-22.json", + ), + trigger_contract_json: PathBuf::from( + "tests/fixtures/generated_scene/scheduled_monitoring_action_trigger_runtime_contract_2026-04-22.json", + ), + }, + ) + .unwrap(); + + let detect_payload = json!({ + "type": "scheduled-monitoring-detect-snapshot", + "report_name": "command-center-fee-control-monitor", + "status": "detect-ok", + "workflowId": "command_center_fee_control_monitoring_action", + "mode": "monitor_only", + "pendingList": [ + { "id": "A1", "consNo": "C1", "phone": "13800000000", "abnorType": "fee_control" } + ], + "inputs": { + "source": "browser_attached_live_read", + "queryAbnorList": [ + { "id": "A1", "consNo": "C1", "phone": "13800000000", "abnorType": "fee_control" } + ], + "queryHistoryEnergyCharge": [], + "getMonitorLog": { "lastHandled": "2026-04-22T08:00:00Z" }, + "getOtherIphones": { "holidaySwitch": "off" }, + "getAllSubMgtOrgTreeByOrgCode": {} + }, + "localStorageSnapshot": { + "loginUserInfo": "{\"orgNo\":\"62401\"}", + "markToken": "browser-token", + "yxClassList": "[{\"orgNo\":\"62401\"}]", + "zhzxFkycSendTime": "2026-04-22 08:00:00" + }, + "readDiagnostics": { + "source": "browser_attached_live_read", + "businessGatewayReadAttempted": true, + "localhostReadAttempted": true, + "queryAbnorListCount": 1, + "queryHistoryEnergyChargeCount": 0 + }, + "dependencySnapshot": { + "businessReads": [], + "localReads": [], + "blockedLocalWrites": [], + "blockedCalls": ["repetCtrlSend"] + }, + "sideEffectCounters": { + "repetCtrlSend": 0, + "sendMessages": 0, + "callOutLogin": 0, + "audioPlay": 0, + "exeTQueue": 0, + "productionLogWrite": 0 + } + }); + let (browser_ws_url, browser_server) = + start_callback_host_scheduled_monitoring_browser_server(detect_payload); + write_browser_config(&config_path, &browser_ws_url); + + let output = run_binary_with_skills_dir_and_config( + &trigger_path, + &materialization_root.join("skills"), + &config_path, + &workspace, + &output_path, + ); + browser_server.join().unwrap(); + + assert!( + output.status.success(), + "stdout={}\nstderr={}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let record: Value = serde_json::from_str(&fs::read_to_string(output_path).unwrap()).unwrap(); + let preview_artifact = &record["previewArtifact"]; + + assert_eq!( + preview_artifact["actionPlan"][0]["actionContractRef"], + "dispatch_exception_order" + ); + assert_eq!(preview_artifact["summary"]["queue_transition_count"], 1); + assert_eq!( + preview_artifact["queueTransitions"][0]["transitionId"], + "queue_continue_on_done" + ); + assert_eq!( + preview_artifact["logWritePreview"][0]["logId"], + "dispose_log_after_dispatch" + ); +} + +#[test] +fn command_center_empty_pending_list_does_not_emit_log_write_preview() { + let workspace = temp_workspace("sgclaw-command-center-empty-preview-semantics"); + let trigger_path = workspace.join("scheduled-trigger.json"); + let output_path = workspace.join("run-record.json"); + let config_path = workspace.join("sgclaw_config.json"); + let rules_path = workspace.join("resources").join("rules.json"); + let materialization_root = workspace.join("materialized"); + fs::create_dir_all(&materialization_root).unwrap(); + write_json( + &trigger_path, + &scheduled_trigger_with_runtime_inputs("monitor_only"), + ); + write_runtime_rules(&rules_path); + + generate_scheduled_monitoring_action_skill_package( + GenerateScheduledMonitoringActionSkillRequest { + scene_id: "command-center-fee-control-monitor".to_string(), + scene_name: "command-center-fee-control-monitor".to_string(), + output_root: materialization_root.clone(), + source_evidence_json: PathBuf::from( + "tests/fixtures/generated_scene/monitoring_action_source_evidence_extraction_2026-04-21.json", + ), + ir_contract_json: PathBuf::from( + "tests/fixtures/generated_scene/scheduled_monitoring_action_ir_contract_2026-04-22.json", + ), + trigger_contract_json: PathBuf::from( + "tests/fixtures/generated_scene/scheduled_monitoring_action_trigger_runtime_contract_2026-04-22.json", + ), + }, + ) + .unwrap(); + + let detect_payload = json!({ + "type": "scheduled-monitoring-detect-snapshot", + "report_name": "command-center-fee-control-monitor", + "status": "detect-ok", + "workflowId": "command_center_fee_control_monitoring_action", + "mode": "monitor_only", + "pendingList": [], + "inputs": { + "source": "browser_attached_live_read", + "queryAbnorList": [], + "queryHistoryEnergyCharge": [], + "getMonitorLog": {}, + "getOtherIphones": {}, + "getAllSubMgtOrgTreeByOrgCode": {} + }, + "localStorageSnapshot": { + "loginUserInfo": "{\"orgNo\":\"62401\"}", + "markToken": "browser-token", + "yxClassList": "[{\"orgNo\":\"62401\"}]", + "zhzxFkycSendTime": "2026-04-22 08:00:00" + }, + "readDiagnostics": { + "source": "browser_attached_live_read", + "businessGatewayReadAttempted": true, + "localhostReadAttempted": true, + "queryAbnorListCount": 0, + "queryHistoryEnergyChargeCount": 0 + }, + "dependencySnapshot": { + "businessReads": [], + "localReads": [], + "blockedLocalWrites": [], + "blockedCalls": ["repetCtrlSend"] + }, + "sideEffectCounters": { + "repetCtrlSend": 0, + "sendMessages": 0, + "callOutLogin": 0, + "audioPlay": 0, + "exeTQueue": 0, + "productionLogWrite": 0 + } + }); + let (browser_ws_url, browser_server) = + start_callback_host_scheduled_monitoring_browser_server(detect_payload); + write_browser_config(&config_path, &browser_ws_url); + + let output = run_binary_with_skills_dir_and_config( + &trigger_path, + &materialization_root.join("skills"), + &config_path, + &workspace, + &output_path, + ); + browser_server.join().unwrap(); + + assert!( + output.status.success(), + "stdout={}\nstderr={}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let record: Value = serde_json::from_str(&fs::read_to_string(output_path).unwrap()).unwrap(); + let preview_artifact = &record["previewArtifact"]; + + assert_eq!(preview_artifact["summary"]["pending_count"], 0); + assert_eq!(preview_artifact["summary"]["queue_transition_count"], 1); + assert_eq!( + preview_artifact["queueTransitions"][0]["transitionId"], + "queue_continue_on_empty" + ); + assert_eq!( + preview_artifact["logWritePreview"].as_array().unwrap().len(), + 0 + ); +} + #[test] fn binary_wiring_browser_attached_passes_platform_service_base_from_config() { let workspace = temp_workspace("sgclaw-scheduled-monitoring-binary-platform-service-base"); diff --git a/tests/scheduled_monitoring_generated_scene_hardening_test.rs b/tests/scheduled_monitoring_generated_scene_hardening_test.rs index 3ad67b5..dda214a 100644 --- a/tests/scheduled_monitoring_generated_scene_hardening_test.rs +++ b/tests/scheduled_monitoring_generated_scene_hardening_test.rs @@ -279,6 +279,58 @@ fn fee_control_materialization_emits_timeout_contract_in_generated_artifacts() { assert!(detect_script.contains("statusOnTimeout")); } +#[test] +fn command_center_workflow_ir_includes_automation_semantics() { + let root = temp_workspace("sgclaw-command-center-automation-semantics"); + let skill_root = generate_scheduled_monitoring_action_skill_package( + GenerateScheduledMonitoringActionSkillRequest { + scene_id: "command-center-fee-control-monitor".to_string(), + scene_name: "command-center-fee-control-monitor".to_string(), + output_root: root.clone(), + source_evidence_json: PathBuf::from( + "tests/fixtures/generated_scene/monitoring_action_source_evidence_extraction_2026-04-21.json", + ), + ir_contract_json: PathBuf::from( + "tests/fixtures/generated_scene/scheduled_monitoring_action_ir_contract_2026-04-22.json", + ), + trigger_contract_json: PathBuf::from( + "tests/fixtures/generated_scene/scheduled_monitoring_action_trigger_runtime_contract_2026-04-22.json", + ), + }, + ) + .unwrap(); + + let workflow_ir: Value = serde_json::from_str( + &fs::read_to_string(skill_root.join("references/workflow-ir.json")).unwrap(), + ) + .unwrap(); + + assert!( + workflow_ir.get("actionContracts").is_some(), + "workflow-ir.json must include actionContracts" + ); + assert!( + workflow_ir.get("iterationContract").is_some(), + "workflow-ir.json must include iterationContract" + ); + assert!( + workflow_ir.get("executionFlow").is_some(), + "workflow-ir.json must include executionFlow" + ); + assert!( + workflow_ir.get("resultStateMachines").is_some(), + "workflow-ir.json must include resultStateMachines" + ); + assert!( + workflow_ir.get("queueTransitionRules").is_some(), + "workflow-ir.json must include queueTransitionRules" + ); + assert!( + workflow_ir.get("logWriteContracts").is_some(), + "workflow-ir.json must include logWriteContracts" + ); +} + #[test] fn available_balance_and_archive_materialization_emit_explicit_dependency_classification_in_references() { let example_output = Command::new("cargo")