diff --git a/src/v3/compiler/src/dag/effects.rs b/src/v3/compiler/src/dag/effects.rs index fba5f35968a..969e827d8dd 100644 --- a/src/v3/compiler/src/dag/effects.rs +++ b/src/v3/compiler/src/dag/effects.rs @@ -1,19 +1,19 @@ -//! `std.effects` mirror (DB-18 / Lane 2 Stage 2b). +//! `std.effects` substrate carriers + Stage 2b native-`Dag` projection (DB-18). //! -//! Structural carriers aligned with `src/v3/std/effects.dag` — the -//! compiler-side authority for `compose_effects`, [`WorkflowEffect`], and -//! [`BranchArm`] until the self-hosted pipeline consumes the `.dag` forms -//! directly. +//! Structural carriers align with `src/v3/std/effects.dag`. Executable Stage 2b +//! helpers (`analyze_workflow`, [`lane2_workflow_idempotency_report`], …) live +//! here as the typed Rust witness for unparsed std arrow bodies in bootstrap +//! (`ArrowBody::Unparsed`) — sole compiler-local implementation surface for this algebra +//! (R3 gate `tier3_effect_carrier_mirror_dissolved`: retired standalone +//! `workflow_idempotency.rs`). //! //! Each coproduct / boundary carrier below carries its own 🟢/🟡 //! dissolution stamp (modeling-discipline principle 4); do not rely on //! the module banner alone. 🔴 does not appear in this block — there is //! no intentionally-wrong deferred carrier here; unsupported control //! flow is modeled via explicit sums, not silent placeholders. -//! -//! Extracted from `dag.rs` (L4b). No behavior change. -use super::{BoolPortRef, ElementRef, NonSingletonList}; +use super::{BoolPortRef, Dag, ElementRef, NodeId, NonSingletonList}; /// 🟢 **TERMINAL.** HTTP verb literals — 1:1 with `std.effects` `HttpMethod`; /// naming authority is `effects.dag`. @@ -84,7 +84,7 @@ pub struct OperationEffect { /// record" hole by replacing the copied payload with a validated index, /// but it does not by itself preserve the owner list identity or prove /// the pointed operation is breaking. Those facts are still established -/// by `workflow_idempotency::compose_operation_effects` and by callers +/// by [`compose_operation_effects`] and by callers /// resolving against the matching workflow evidence chain, and are /// tracked as the same constructor-validation asymmetry class as other /// reflected handles until the substrate grows an owner-bound, @@ -214,3 +214,94 @@ pub enum WorkflowParallelismReport { ParallelCompositionVerdict(CompositionVerdict), ParallelismUnsupported(ParallelismUnsupportedDetail), } + +// ── Stage 2b projection (native `Dag` / rustc round-trip exports) ─────────── + +pub(crate) fn compose_operation_effects(effects: &[OperationEffect]) -> CompositionVerdict { + for (index, effect) in effects.iter().enumerate() { + if matches!(effect.shape, EffectShape::IsBreaking(_)) { + // `ElementRef` preserves the validated in-bounds position of the + // breaker without copying a second breaker record; the breaking + // subset fact and the owner-list identity still come from this + // partition check plus callers resolving against the same slice. + let first_breaker = ElementRef::from_slice(effects, index) + .expect("enumerated workflow effect index must stay in-bounds"); + return CompositionVerdict::BrokenBy { first_breaker }; + } + } + CompositionVerdict::IdempotentComposition +} + +/// Pure projection used by Stage 2b — kept aligned with +/// `std.effects::report_unsupported_workflow_variant`. Exported for +/// `emit_rust_module` output from `src/v3/lenses/idempotency.dag` (rustc +/// round-trip in `m2_lens_idempotency_migration_test`). +pub fn report_unsupported_workflow_variant( + variant_name: &str, + downstream_stage: &str, + reason: &str, +) -> WorkflowIdempotencyReport { + WorkflowIdempotencyReport::IdempotencyUnsupported(IdempotencyUnsupportedDetail { + variant_name: variant_name.to_string(), + downstream_stage: downstream_stage.to_string(), + reason: reason.to_string(), + }) +} + +/// Keeps parity with `std.effects::lane2_workflow_idempotency_report`. +pub fn lane2_workflow_idempotency_report(workflow: &WorkflowEffect) -> WorkflowIdempotencyReport { + project_workflow_idempotency_report(workflow) +} + +pub(crate) fn project_workflow_idempotency_report( + workflow: &WorkflowEffect, +) -> WorkflowIdempotencyReport { + match workflow { + WorkflowEffect::LinearEffect { ops } => WorkflowIdempotencyReport::WorkflowCompositionVerdict( + compose_operation_effects(ops.as_slice()), + ), + WorkflowEffect::BranchEffect { .. } => WorkflowIdempotencyReport::IdempotencyUnsupported( + IdempotencyUnsupportedDetail { + variant_name: "BranchEffect".to_string(), + downstream_stage: "lane2_stage2b_idempotency_lens".to_string(), + reason: "non-linear workflow; branch-wise idempotency composition is not in the Stage 2b algebra" + .to_string(), + }, + ), + WorkflowEffect::LoopEffect { .. } => WorkflowIdempotencyReport::IdempotencyUnsupported( + IdempotencyUnsupportedDetail { + variant_name: "LoopEffect".to_string(), + downstream_stage: "lane2_stage2b_idempotency_lens".to_string(), + reason: "non-linear workflow; loop-carried idempotency composition is not in the Stage 2b algebra" + .to_string(), + }, + ), + WorkflowEffect::ParallelEffect { .. } => WorkflowIdempotencyReport::IdempotencyUnsupported( + IdempotencyUnsupportedDetail { + variant_name: "ParallelEffect".to_string(), + downstream_stage: "lane2_stage2b_idempotency_lens".to_string(), + reason: "non-linear workflow; parallel idempotency composition is not in the Stage 2b algebra" + .to_string(), + }, + ), + } +} + +/// Native-Dag convenience entry for the declared `lenses.idempotency` surface. +/// +/// The `.dag` entry reads the reflected substrate with `lane2_workflow_at`; this +/// Rust entry reads the same projection from the native `Dag` fields used before +/// emission. +pub fn analyze_workflow(d: &Dag, workflow_root: NodeId) -> WorkflowIdempotencyReport { + let Some(workflow) = d.lane2_workflow_effect_at(&workflow_root) else { + return WorkflowIdempotencyReport::IdempotencyUnsupported( + IdempotencyUnsupportedDetail { + variant_name: "Lane2WorkflowRoot".to_string(), + downstream_stage: "lane2_stage2b_idempotency_lens".to_string(), + reason: "no WorkflowEffect at this substrate root - populate `lane2_workflow` on `Value`/`Bind` via lowering or `try_register_lane2_workflow_effect`" + .to_string(), + }, + ); + }; + project_workflow_idempotency_report(workflow) +}