Skip to main content

arete_interpreter/
public_artifacts.rs

1//! Compatibility bridge between explicit public artifacts and the current
2//! interpreter generators. The composite AST is constructed only in memory and
3//! remains a legacy input shape, not a published artifact.
4
5use std::collections::{BTreeMap, BTreeSet};
6
7use arete_artifacts::{
8    decompose_legacy_stack, resolve_stack_composition_v2, LegacyDecomposition, LiveSpecArtifact,
9    LiveSpecArtifactV2, ProgramSpecArtifact, ResolvedLiveSpecV2, StackManifestArtifact,
10    StackManifestArtifactV2,
11};
12use serde::de::DeserializeOwned;
13use serde::Serialize;
14
15use crate::ast::{InstructionDef, PdaDefinition, SerializableStackSpec, CURRENT_AST_VERSION};
16
17#[derive(Debug, Clone)]
18pub struct AliasedStackSpecV2 {
19    pub alias: String,
20    pub live_spec_hash: String,
21    pub stack_spec: SerializableStackSpec,
22}
23
24#[derive(Debug, Clone)]
25pub struct ComposedStackSpecsV2 {
26    pub name: String,
27    pub live_specs: Vec<AliasedStackSpecV2>,
28}
29
30pub fn decompose_stack_spec(
31    stack_spec: &SerializableStackSpec,
32) -> Result<LegacyDecomposition, String> {
33    let bytes = serde_json::to_vec(stack_spec).map_err(|error| error.to_string())?;
34    decompose_legacy_stack(&bytes).map_err(|error| error.to_string())
35}
36
37pub fn stack_spec_from_program_artifacts(
38    name: impl Into<String>,
39    programs: &[ProgramSpecArtifact],
40) -> Result<SerializableStackSpec, String> {
41    for program in programs {
42        program.validate().map_err(|error| error.to_string())?;
43    }
44    let (program_ids, idls, program_specs, pdas, instructions) = program_inputs(programs)?;
45    Ok(SerializableStackSpec {
46        ast_version: CURRENT_AST_VERSION.to_string(),
47        stack_name: name.into(),
48        program_ids,
49        idls,
50        program_specs,
51        entities: Vec::new(),
52        pdas,
53        instructions,
54        content_hash: None,
55    })
56}
57
58pub fn stack_spec_from_artifacts(
59    programs: &[ProgramSpecArtifact],
60    live_spec: &LiveSpecArtifact,
61    manifest: &StackManifestArtifact,
62) -> Result<SerializableStackSpec, String> {
63    live_spec.validate().map_err(|error| error.to_string())?;
64    manifest.validate().map_err(|error| error.to_string())?;
65    let mut stack = stack_spec_from_program_artifacts(&manifest.payload.name, programs)?;
66
67    let program_hashes = programs
68        .iter()
69        .map(|program| program.artifact_hash)
70        .collect::<Vec<_>>();
71    let live_program_hashes = live_spec
72        .payload
73        .programs
74        .iter()
75        .map(|program| program.program_spec_hash)
76        .collect::<Vec<_>>();
77    let manifest_program_hashes = manifest
78        .payload
79        .programs
80        .iter()
81        .map(|program| program.artifact_hash)
82        .collect::<Vec<_>>();
83    if program_hashes != live_program_hashes || program_hashes != manifest_program_hashes {
84        return Err(
85            "ProgramSpec order must match LiveSpec and StackManifest requirements".to_string(),
86        );
87    }
88    if manifest.payload.live_specs.len() != 1
89        || manifest.payload.live_specs[0].artifact_hash != live_spec.artifact_hash
90    {
91        return Err("StackManifest must reference the supplied LiveSpec exactly once".to_string());
92    }
93
94    stack.entities = transcode(&live_spec.payload.entities)?;
95    if let Some(extensions) = &live_spec.payload.legacy_program_extensions {
96        stack.pdas = transcode(&extensions.pdas)?;
97        stack.instructions = transcode(&extensions.instructions)?;
98    }
99    Ok(stack)
100}
101
102/// Reconstruct the single-live generator input from typed V2 artifacts.
103/// This compatibility wrapper deliberately rejects zero- and multi-live inputs.
104pub fn stack_spec_from_artifacts_v2(
105    programs: &[ProgramSpecArtifact],
106    live_spec: &LiveSpecArtifactV2,
107    manifest: &StackManifestArtifactV2,
108) -> Result<SerializableStackSpec, String> {
109    if manifest.payload.live_specs.len() != 1 {
110        return Err("single-live compatibility requires exactly one aliased LiveSpec".to_string());
111    }
112    let alias = manifest.payload.live_specs[0].alias.clone();
113    let lives = vec![(alias, live_spec.clone())];
114    let mut composed = stack_specs_from_artifacts_v2(programs, &lives, manifest)?;
115    Ok(composed.live_specs.remove(0).stack_spec)
116}
117
118/// Build one fresh generator model per manifest alias. ProgramSpec lookup is
119/// hash-keyed, adapters are applied only to that live's dependency subset, and
120/// selected views are projected before language generation.
121pub fn stack_specs_from_artifacts_v2(
122    programs: &[ProgramSpecArtifact],
123    live_specs: &[(String, LiveSpecArtifactV2)],
124    manifest: &StackManifestArtifactV2,
125) -> Result<ComposedStackSpecsV2, String> {
126    let resolved = resolve_stack_composition_v2(manifest, live_specs, programs)
127        .map_err(|error| error.to_string())?;
128    let multiple = resolved.live_specs.len() > 1;
129    let live_specs = resolved
130        .live_specs
131        .into_iter()
132        .map(|live| {
133            let stack_name = if multiple {
134                format!(
135                    "{}{}",
136                    identifier_pascal_case(&manifest.payload.name),
137                    identifier_pascal_case(&live.alias)
138                )
139            } else {
140                manifest.payload.name.clone()
141            };
142            stack_spec_for_live(stack_name, live)
143        })
144        .collect::<Result<Vec<_>, String>>()?;
145    Ok(ComposedStackSpecsV2 {
146        name: manifest.payload.name.clone(),
147        live_specs,
148    })
149}
150
151fn stack_spec_for_live(
152    stack_name: String,
153    live: ResolvedLiveSpecV2<'_>,
154) -> Result<AliasedStackSpecV2, String> {
155    let programs = live
156        .program_specs
157        .iter()
158        .map(|program| (*program).clone())
159        .collect::<Vec<_>>();
160    let mut stack = stack_spec_from_program_artifacts(stack_name, &programs)?;
161    stack.entities = transcode(&live.artifact.payload.entities)?;
162    let selected = live.selected_views.iter().collect::<BTreeSet<_>>();
163    for entity in &mut stack.entities {
164        entity.views.retain(|view| selected.contains(&view.id));
165    }
166
167    for adapter in &live.artifact.payload.program_adapters {
168        let program = programs
169            .iter()
170            .find(|program| program.artifact_hash == adapter.program_spec_hash)
171            .ok_or_else(|| "program adapter references an unknown ProgramSpec".to_string())?;
172        let program_name = &program.payload.idl_snapshot.snapshot.name;
173        let pdas = stack.pdas.entry(program_name.clone()).or_default();
174        for (name, pda) in &adapter.pdas {
175            pdas.insert(name.clone(), transcode(pda)?);
176        }
177        for resolution in &adapter.instruction_resolutions {
178            let instruction = stack
179                .instructions
180                .iter_mut()
181                .find(|instruction| {
182                    instruction.name == resolution.instruction
183                        && instruction.program_id.as_deref()
184                            == Some(program.payload.program_id.as_str())
185                })
186                .ok_or_else(|| {
187                    format!(
188                        "program adapter references unknown instruction '{}.{}'",
189                        program_name, resolution.instruction
190                    )
191                })?;
192            for (account_name, account_resolution) in &resolution.accounts {
193                let account = instruction
194                    .accounts
195                    .iter_mut()
196                    .find(|account| account.name == *account_name)
197                    .ok_or_else(|| {
198                        format!(
199                            "program adapter references unknown account '{}.{}.{}'",
200                            program_name, resolution.instruction, account_name
201                        )
202                    })?;
203                account.resolution = transcode(account_resolution)?;
204            }
205        }
206    }
207    Ok(AliasedStackSpecV2 {
208        alias: live.alias,
209        live_spec_hash: live.artifact.artifact_hash.to_string(),
210        stack_spec: stack,
211    })
212}
213
214type ProgramInputs = (
215    Vec<String>,
216    Vec<arete_idl::snapshot::IdlSnapshot>,
217    Vec<arete_hash::ProgramSpecV1>,
218    BTreeMap<String, BTreeMap<String, PdaDefinition>>,
219    Vec<InstructionDef>,
220);
221
222fn program_inputs(programs: &[ProgramSpecArtifact]) -> Result<ProgramInputs, String> {
223    let mut program_ids = Vec::with_capacity(programs.len());
224    let mut idls = Vec::with_capacity(programs.len());
225    let mut program_specs = Vec::with_capacity(programs.len());
226    let mut pdas = BTreeMap::new();
227    let mut instructions = Vec::new();
228    for program in programs {
229        let payload = &program.payload;
230        program_ids.push(payload.program_id.clone());
231        idls.push(payload.idl_snapshot.snapshot.clone());
232        program_specs.push(payload.clone());
233        let program_pdas = transcode(&payload.pdas)?;
234        pdas.insert(payload.idl_snapshot.snapshot.name.clone(), program_pdas);
235        instructions.extend(transcode::<_, Vec<InstructionDef>>(&payload.instructions)?);
236    }
237    Ok((program_ids, idls, program_specs, pdas, instructions))
238}
239
240fn transcode<T: Serialize, U: DeserializeOwned>(value: &T) -> Result<U, String> {
241    serde_json::from_value(serde_json::to_value(value).map_err(|error| error.to_string())?)
242        .map_err(|error| error.to_string())
243}
244
245fn identifier_pascal_case(value: &str) -> String {
246    let mut output = value
247        .split(|character: char| !character.is_ascii_alphanumeric())
248        .filter(|segment| !segment.is_empty())
249        .map(|segment| {
250            let mut characters = segment.chars();
251            characters
252                .next()
253                .map(|first| first.to_ascii_uppercase().to_string() + characters.as_str())
254                .unwrap_or_default()
255        })
256        .collect::<String>();
257    if output
258        .chars()
259        .next()
260        .is_some_and(|character| character.is_ascii_digit())
261    {
262        output.insert(0, 'A');
263    }
264    output
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270    use arete_hash::{CanonicalIdlDocument, ProgramSpecV1};
271
272    fn program() -> ProgramSpecArtifact {
273        let idl = br#"{
274          "address":"11111111111111111111111111111111",
275          "metadata":{"name":"system","version":"1.0.0","spec":"0.1.0"},
276          "instructions":[],"accounts":[],"types":[],"events":[],"errors":[]
277        }"#;
278        let document = CanonicalIdlDocument::parse(idl, None).unwrap();
279        ProgramSpecArtifact::new(ProgramSpecV1::from_document(&document)).unwrap()
280    }
281
282    #[test]
283    fn program_artifacts_reconstruct_generator_input_without_hosted_release_state() {
284        let program = program();
285        let stack = stack_spec_from_program_artifacts("SystemProgram", &[program.clone()]).unwrap();
286        assert_eq!(stack.program_ids, vec![program.payload.program_id]);
287        assert_eq!(
288            stack.program_specs[0].hash().unwrap(),
289            program.artifact_hash
290        );
291        assert!(stack.entities.is_empty());
292    }
293}