Skip to main content

dpcs/binding/
framework.rs

1//! Binding framework: capability gate and adapter dispatch (SPEC Ch 17).
2
3use std::fs;
4use std::path::Path;
5use std::str::FromStr;
6
7use crate::capabilities::{evaluate, CapabilityProfile, CapabilityReport, CapabilityResult};
8use crate::diagnostics::ValidationReport;
9use crate::model::PipelineContract;
10use crate::paths::join_under_root;
11use crate::plan::{self, PipelinePlan, PlanResult};
12
13use super::adapters::{adapter_for, validate_relative_path};
14use super::artifact::{BindingBundle, BindingTarget};
15use super::diagnostics::{self, write_failed};
16
17/// Context passed to orchestrator adapters during translation.
18#[derive(Debug, Clone)]
19pub struct BindContext<'a> {
20    /// Capability profile identity.
21    pub profile_identity: &'a str,
22    /// Successful capability evaluation report.
23    pub capability: &'a CapabilityReport,
24}
25
26/// Result of attempting to bind a Pipeline Plan to an orchestrator target.
27#[derive(Debug, Clone, PartialEq)]
28pub enum BindingResult {
29    /// Binding succeeded and produced a platform artifact bundle.
30    Ok(Box<BindingBundle>),
31    /// Binding refused (capability gate, planning, or translation failure).
32    Err {
33        /// Binding-stage (and related) diagnostics.
34        diagnostics: ValidationReport,
35        /// Structured capability report when refusal is due to capability matching.
36        capability: Option<Box<CapabilityReport>>,
37    },
38}
39
40impl BindingResult {
41    /// Returns the bundle when binding succeeded.
42    pub fn bundle(self) -> Option<BindingBundle> {
43        match self {
44            Self::Ok(bundle) => Some(*bundle),
45            Self::Err { .. } => None,
46        }
47    }
48
49    /// Returns a reference to the bundle when binding succeeded.
50    pub fn as_bundle(&self) -> Option<&BindingBundle> {
51        match self {
52            Self::Ok(bundle) => Some(bundle),
53            Self::Err { .. } => None,
54        }
55    }
56
57    /// Returns whether binding succeeded.
58    pub fn is_ok(&self) -> bool {
59        matches!(self, Self::Ok(_))
60    }
61
62    /// Returns diagnostics when binding failed.
63    pub fn report(&self) -> Option<&ValidationReport> {
64        match self {
65            Self::Ok(_) => None,
66            Self::Err { diagnostics, .. } => Some(diagnostics),
67        }
68    }
69
70    /// Returns the capability report retained on capability-gate refusal.
71    pub fn capability_report(&self) -> Option<&CapabilityReport> {
72        match self {
73            Self::Ok(_) => None,
74            Self::Err { capability, .. } => capability.as_deref(),
75        }
76    }
77
78    fn err(diagnostics: ValidationReport) -> Self {
79        Self::Err {
80            diagnostics,
81            capability: None,
82        }
83    }
84
85    fn err_with_capability(diagnostics: ValidationReport, report: CapabilityReport) -> Self {
86        Self::Err {
87            diagnostics,
88            capability: Some(Box::new(report)),
89        }
90    }
91}
92
93/// Parse a binding target name into [`BindingTarget`].
94///
95/// On failure returns a binding-stage diagnostic report with `DPCS-BIND-002`.
96pub fn parse_target(name: &str) -> Result<BindingTarget, ValidationReport> {
97    BindingTarget::from_str(name)
98        .map_err(|_| diagnostics::report_error(diagnostics::unknown_target(name)))
99}
100
101/// Bind a validated Pipeline Plan to a target orchestrator.
102///
103/// Runs capability evaluation against `profile` first. Missing mandatory
104/// capabilities refuse binding with `DPCS-BIND-001`. On success, translates the
105/// plan into platform-specific scaffold artifacts.
106pub fn bind(
107    plan: &PipelinePlan,
108    profile: &CapabilityProfile,
109    target: BindingTarget,
110) -> BindingResult {
111    let capability = match evaluate(plan, profile) {
112        CapabilityResult::Ok(report) => report,
113        CapabilityResult::Err {
114            report,
115            diagnostics,
116        } => {
117            return BindingResult::err_with_capability(
118                diagnostics::report_capability_gate(diagnostics),
119                *report,
120            );
121        }
122    };
123
124    let ctx = BindContext {
125        profile_identity: &profile.identity,
126        capability: &capability,
127    };
128
129    let adapter = adapter_for(target);
130    match adapter.translate(plan, &ctx) {
131        Ok(files) => {
132            if files.is_empty() {
133                return BindingResult::err(diagnostics::report_error(
134                    diagnostics::translation_incomplete("binding adapter produced no artifacts"),
135                ));
136            }
137            for file in &files {
138                if let Err(report) = validate_relative_path(&file.relative_path) {
139                    return BindingResult::err(report);
140                }
141            }
142            BindingResult::Ok(Box::new(BindingBundle {
143                target,
144                contract_id: plan.contract_id.clone(),
145                contract_version: plan.contract_version.clone(),
146                profile_identity: profile.identity.clone(),
147                files,
148                capability: *capability,
149            }))
150        }
151        Err(report) => BindingResult::err(report),
152    }
153}
154
155/// Plan a contract, then bind it to a target orchestrator.
156///
157/// Planning failures are returned as [`BindingResult::Err`] with the planning
158/// diagnostics (including `DPCS-PLN-001` when validation failed).
159pub fn bind_contract(
160    contract: &PipelineContract,
161    profile: &CapabilityProfile,
162    target: BindingTarget,
163) -> BindingResult {
164    // Deep-resolve with default planning options (CWD); prefer
165    // `bind_contract_with_resolve` + `ResolveOptions::from_document_path` for
166    // document-relative nested locations.
167    bind_contract_with_resolve(contract, profile, target, None)
168}
169
170/// Plan (with reference resolution) then bind.
171///
172/// When `resolve` is `None`, uses [`crate::ResolveOptions::default_for_planning`].
173pub fn bind_contract_with_resolve(
174    contract: &PipelineContract,
175    profile: &CapabilityProfile,
176    target: BindingTarget,
177    resolve: Option<&crate::resolve::ResolveOptions>,
178) -> BindingResult {
179    match plan::plan_with_resolve(contract, resolve) {
180        PlanResult::Ok(planned) => bind(&planned, profile, target),
181        PlanResult::Err(report) => BindingResult::err(report),
182    }
183}
184
185/// Write a binding bundle's files under `out_dir`.
186///
187/// Creates parent directories as needed. Rejects absolute paths, `..` segments,
188/// and empty relative paths (`DPCS-BIND-004`). Returns a binding-stage
189/// diagnostic report on filesystem errors.
190pub fn write_bundle(bundle: &BindingBundle, out_dir: &Path) -> Result<(), ValidationReport> {
191    if let Err(err) = fs::create_dir_all(out_dir) {
192        return Err(diagnostics::report_error(write_failed(format!(
193            "failed to create directory {}: {err}",
194            out_dir.display()
195        ))));
196    }
197    for file in &bundle.files {
198        validate_relative_path(&file.relative_path)?;
199        let path = join_under_root(out_dir, &file.relative_path).map_err(|err| {
200            diagnostics::report_error(write_failed(format!(
201                "unsafe binding path {}: {err}",
202                file.relative_path
203            )))
204        })?;
205        if let Some(parent) = path.parent() {
206            if let Err(err) = fs::create_dir_all(parent) {
207                return Err(diagnostics::report_error(write_failed(format!(
208                    "failed to create directory {}: {err}",
209                    parent.display()
210                ))));
211            }
212        }
213        if let Err(err) = fs::write(&path, &file.content) {
214            return Err(diagnostics::report_error(write_failed(format!(
215                "failed to write {}: {err}",
216                path.display()
217            ))));
218        }
219    }
220    Ok(())
221}