1use crate::branches::DEFAULT_BRANCH;
10use crate::model::*;
11use lex_ast::{sig_id, stage_id, Stage};
12use serde::de::DeserializeOwned;
13use serde::Serialize;
14use std::fs;
15use std::path::{Path, PathBuf};
16use std::time::{SystemTime, UNIX_EPOCH};
17
18#[derive(Debug, thiserror::Error)]
19pub enum StoreError {
20 #[error("io error: {0}")]
21 Io(#[from] std::io::Error),
22 #[error("serialization error: {0}")]
23 Serde(#[from] serde_json::Error),
24 #[error("imports cannot be published as stages")]
25 CannotPublishImport,
26 #[error("unknown stage_id `{0}`")]
27 UnknownStage(String),
28 #[error("unknown sig_id `{0}`")]
29 UnknownSig(String),
30 #[error("invalid lifecycle transition: {0}")]
31 InvalidTransition(String),
32 #[error("unknown branch `{0}`")]
33 UnknownBranch(String),
34 #[error(transparent)]
35 Apply(#[from] lex_vcs::ApplyError),
36 #[error("type errors in published program: {} error(s)", .0.len())]
42 TypeError(Vec<lex_types::TypeError>),
43 #[error(
50 "branch advance blocked: op {} missing attestations: {}",
51 .0.op_id, .0.missing.join(", ")
52 )]
53 BranchAdvanceBlocked(crate::policy::BranchAdvanceBlocked),
54 #[error(
61 "branch advance blocked: op {} touches stage {} with an attestation from \
62 quarantined producer `{}` (blocked at {}, attestation at {})",
63 .0.op_id, .0.stage_id, .0.tool_id, .0.blocked_at, .0.attestation_at
64 )]
65 ProducerBlocked(crate::policy::ProducerBlocked),
66}
67
68#[derive(Debug, Clone, serde::Serialize)]
70pub struct PublishOutcome {
71 pub ops: Vec<PublishOp>,
72 pub head_op: Option<lex_vcs::OpId>,
73}
74
75#[derive(Debug, Clone, serde::Serialize)]
77pub struct PublishOp {
78 pub op_id: lex_vcs::OpId,
79 pub kind: serde_json::Value,
80}
81
82#[derive(Debug, Clone, serde::Serialize, PartialEq)]
86pub struct StageHistoryEntry {
87 pub stage_id: String,
88 pub status: StageStatus,
89 pub last_at: u64,
91 #[serde(skip_serializing_if = "Option::is_none")]
97 pub published_at: Option<u64>,
98}
99
100pub struct Store {
101 root: PathBuf,
102}
103
104impl Store {
105 pub fn open(root: impl AsRef<Path>) -> Result<Self, StoreError> {
107 let root = root.as_ref().to_path_buf();
108 fs::create_dir_all(root.join("stages"))?;
109 fs::create_dir_all(root.join("traces"))?;
110 Ok(Self { root })
111 }
112
113 pub fn root(&self) -> &Path { &self.root }
114
115 fn now() -> u64 {
116 SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
117 }
118
119 fn sig_dir(&self, sig: &str) -> PathBuf { self.root.join("stages").join(sig) }
120 fn impl_dir(&self, sig: &str) -> PathBuf { self.sig_dir(sig).join("implementations") }
121 fn tests_dir(&self, sig: &str) -> PathBuf { self.sig_dir(sig).join("tests") }
122 fn specs_dir(&self, sig: &str) -> PathBuf { self.sig_dir(sig).join("specs") }
123 fn lifecycle_path(&self, sig: &str) -> PathBuf { self.sig_dir(sig).join("lifecycle.json") }
124
125 pub fn publish(&self, stage: &Stage) -> Result<String, StoreError> {
131 self.publish_signed(stage, None)
132 }
133
134 pub fn publish_signed(
146 &self,
147 stage: &Stage,
148 signer: Option<&lex_vcs::Keypair>,
149 ) -> Result<String, StoreError> {
150 let sig = sig_id(stage).ok_or(StoreError::CannotPublishImport)?;
151 let stage_id = stage_id(stage).ok_or(StoreError::CannotPublishImport)?;
152 let name = stage_name(stage).to_string();
153
154 fs::create_dir_all(self.impl_dir(&sig))?;
155 fs::create_dir_all(self.tests_dir(&sig))?;
156 fs::create_dir_all(self.specs_dir(&sig))?;
157
158 let ast_path = self.impl_dir(&sig).join(format!("{}.ast.json", stage_id));
159 let meta_path = self.impl_dir(&sig).join(format!("{}.metadata.json", stage_id));
160
161 if !ast_path.exists() {
162 write_canonical_json(&ast_path, stage)?;
163 }
164 if !meta_path.exists() {
165 let signature = signer.map(|kp| kp.sign_stage_id(&stage_id));
166 let metadata = Metadata {
167 stage_id: stage_id.clone(),
168 sig_id: sig.clone(),
169 name,
170 published_at: Self::now(),
171 note: None,
172 signature,
173 };
174 write_canonical_json(&meta_path, &metadata)?;
175 }
176
177 let mut life = self.read_lifecycle(&sig).unwrap_or_else(|_| Lifecycle {
179 sig_id: sig.clone(),
180 ..Default::default()
181 });
182 if !life.transitions.iter().any(|t| t.stage_id == stage_id) {
183 life.transitions.push(Transition {
184 stage_id: stage_id.clone(),
185 from: StageStatus::Draft, to: StageStatus::Draft,
187 at: Self::now(),
188 reason: None,
189 });
190 self.write_lifecycle(&sig, &life)?;
191 }
192 Ok(stage_id)
193 }
194
195 pub fn activate(&self, stage_id: &str) -> Result<(), StoreError> {
198 let (sig, mut life) = self.lookup_lifecycle(stage_id)?;
199 let active = life.current_active().map(|s| s.to_string());
201 if let Some(prev) = active {
202 if prev != stage_id {
203 life.transitions.push(Transition {
204 stage_id: prev,
205 from: StageStatus::Active,
206 to: StageStatus::Deprecated,
207 at: Self::now(),
208 reason: Some("superseded".into()),
209 });
210 }
211 }
212 let cur = life.status_of(stage_id);
213 if cur == Some(StageStatus::Tombstone) {
214 return Err(StoreError::InvalidTransition("tombstoned cannot be activated".into()));
215 }
216 life.transitions.push(Transition {
217 stage_id: stage_id.into(),
218 from: cur.unwrap_or(StageStatus::Draft),
219 to: StageStatus::Active,
220 at: Self::now(),
221 reason: None,
222 });
223 self.write_lifecycle(&sig, &life)
224 }
225
226 pub fn deprecate(&self, stage_id: &str, reason: impl Into<String>) -> Result<(), StoreError> {
227 let (sig, mut life) = self.lookup_lifecycle(stage_id)?;
228 let cur = life.status_of(stage_id).ok_or_else(|| StoreError::UnknownStage(stage_id.into()))?;
229 if cur != StageStatus::Active {
230 return Err(StoreError::InvalidTransition(format!("{cur:?} ⇒ Deprecated")));
231 }
232 life.transitions.push(Transition {
233 stage_id: stage_id.into(),
234 from: cur,
235 to: StageStatus::Deprecated,
236 at: Self::now(),
237 reason: Some(reason.into()),
238 });
239 self.write_lifecycle(&sig, &life)
240 }
241
242 pub fn tombstone(&self, stage_id: &str) -> Result<(), StoreError> {
243 let (sig, mut life) = self.lookup_lifecycle(stage_id)?;
244 let cur = life.status_of(stage_id).ok_or_else(|| StoreError::UnknownStage(stage_id.into()))?;
245 if cur != StageStatus::Deprecated {
246 return Err(StoreError::InvalidTransition(format!("{cur:?} ⇒ Tombstone")));
247 }
248 life.transitions.push(Transition {
249 stage_id: stage_id.into(),
250 from: cur,
251 to: StageStatus::Tombstone,
252 at: Self::now(),
253 reason: None,
254 });
255 self.write_lifecycle(&sig, &life)
256 }
257
258 pub fn resolve_sig(&self, sig: &str) -> Result<Option<String>, StoreError> {
262 let life = match self.read_lifecycle(sig) {
263 Ok(l) => l,
264 Err(_) => return Ok(None),
265 };
266 Ok(life.current_active().map(|s| s.to_string()))
267 }
268
269 pub fn sig_history(&self, sig: &str) -> Result<Vec<StageHistoryEntry>, StoreError> {
276 let life = match self.read_lifecycle(sig) {
277 Ok(l) => l,
278 Err(_) => return Ok(Vec::new()),
279 };
280 let mut by_stage: indexmap::IndexMap<String, StageHistoryEntry> =
284 indexmap::IndexMap::new();
285 for t in &life.transitions {
286 let entry = by_stage.entry(t.stage_id.clone()).or_insert(StageHistoryEntry {
287 stage_id: t.stage_id.clone(),
288 status: t.to,
289 last_at: t.at,
290 published_at: None,
291 });
292 entry.status = t.to;
293 entry.last_at = t.at;
294 if t.from == StageStatus::Draft && entry.published_at.is_none() {
295 entry.published_at = Some(t.at);
296 }
297 if t.to == StageStatus::Draft && entry.published_at.is_none() {
298 entry.published_at = Some(t.at);
300 }
301 }
302 let mut out: Vec<StageHistoryEntry> = by_stage.into_values().collect();
303 out.sort_by_key(|e| std::cmp::Reverse(e.last_at));
305 Ok(out)
306 }
307
308 pub fn get_ast(&self, stage_id: &str) -> Result<Stage, StoreError> {
309 let (sig, _) = self.lookup_lifecycle(stage_id)?;
310 let path = self.impl_dir(&sig).join(format!("{}.ast.json", stage_id));
311 let bytes = fs::read(&path)?;
312 Ok(serde_json::from_slice(&bytes)?)
313 }
314
315 pub fn get_metadata(&self, stage_id: &str) -> Result<Metadata, StoreError> {
316 let (sig, _) = self.lookup_lifecycle(stage_id)?;
317 let path = self.impl_dir(&sig).join(format!("{}.metadata.json", stage_id));
318 let bytes = fs::read(&path)?;
319 Ok(serde_json::from_slice(&bytes)?)
320 }
321
322 pub fn get_status(&self, stage_id: &str) -> Result<StageStatus, StoreError> {
323 let (_sig, life) = self.lookup_lifecycle(stage_id)?;
324 life.status_of(stage_id).ok_or_else(|| StoreError::UnknownStage(stage_id.into()))
325 }
326
327 pub fn list_stages_by_name(&self, name: &str) -> Result<Vec<String>, StoreError> {
328 let mut out = Vec::new();
331 let stages_dir = self.root.join("stages");
332 if !stages_dir.exists() { return Ok(out); }
333 for entry in fs::read_dir(&stages_dir)? {
334 let entry = entry?;
335 let sig_dir = entry.path();
336 if !sig_dir.is_dir() { continue; }
337 let sig = entry.file_name().to_string_lossy().to_string();
338 let impls = self.impl_dir(&sig);
340 if !impls.exists() { continue; }
341 for f in fs::read_dir(impls)? {
342 let f = f?;
343 let p = f.path();
344 if p.extension().is_some_and(|e| e == "json")
345 && p.file_name().is_some_and(|n| n.to_string_lossy().ends_with(".metadata.json"))
346 {
347 if let Ok(bytes) = fs::read(&p) {
348 if let Ok(m) = serde_json::from_slice::<Metadata>(&bytes) {
349 if m.name == name {
350 if !out.contains(&sig) { out.push(sig.clone()); }
351 break;
352 }
353 }
354 }
355 }
356 }
357 }
358 out.sort();
359 Ok(out)
360 }
361
362 pub fn list_sigs(&self) -> Result<Vec<String>, StoreError> {
363 let stages_dir = self.root.join("stages");
364 let mut out = Vec::new();
365 if !stages_dir.exists() { return Ok(out); }
366 for entry in fs::read_dir(stages_dir)? {
367 let entry = entry?;
368 if entry.file_type()?.is_dir() {
369 out.push(entry.file_name().to_string_lossy().to_string());
370 }
371 }
372 out.sort();
373 Ok(out)
374 }
375
376 pub fn attach_test(&self, sig: &str, test: &Test) -> Result<String, StoreError> {
379 if !self.sig_dir(sig).exists() {
380 return Err(StoreError::UnknownSig(sig.into()));
381 }
382 fs::create_dir_all(self.tests_dir(sig))?;
383 let path = self.tests_dir(sig).join(format!("{}.json", test.id));
384 write_canonical_json(&path, test)?;
385 Ok(test.id.clone())
386 }
387
388 pub fn list_tests(&self, sig: &str) -> Result<Vec<Test>, StoreError> {
389 let dir = self.tests_dir(sig);
390 if !dir.exists() { return Ok(Vec::new()); }
391 let mut out = Vec::new();
392 for f in fs::read_dir(dir)? {
393 let f = f?;
394 if f.path().extension().is_some_and(|e| e == "json") {
395 let bytes = fs::read(f.path())?;
396 out.push(serde_json::from_slice(&bytes)?);
397 }
398 }
399 Ok(out)
400 }
401
402 pub fn attach_spec(&self, sig: &str, spec: &Spec) -> Result<String, StoreError> {
403 if !self.sig_dir(sig).exists() {
404 return Err(StoreError::UnknownSig(sig.into()));
405 }
406 fs::create_dir_all(self.specs_dir(sig))?;
407 let path = self.specs_dir(sig).join(format!("{}.json", spec.id));
408 write_canonical_json(&path, spec)?;
409 Ok(spec.id.clone())
410 }
411
412 pub fn list_specs(&self, sig: &str) -> Result<Vec<Spec>, StoreError> {
413 let dir = self.specs_dir(sig);
414 if !dir.exists() { return Ok(Vec::new()); }
415 let mut out = Vec::new();
416 for f in fs::read_dir(dir)? {
417 let f = f?;
418 if f.path().extension().is_some_and(|e| e == "json") {
419 let bytes = fs::read(f.path())?;
420 out.push(serde_json::from_slice(&bytes)?);
421 }
422 }
423 Ok(out)
424 }
425
426 fn trace_path(&self, run_id: &str) -> PathBuf {
429 self.root.join("traces").join(run_id).join("trace.json")
430 }
431
432 pub fn save_trace(&self, tree: &lex_trace::TraceTree) -> Result<String, StoreError> {
433 let path = self.trace_path(&tree.run_id);
434 write_canonical_json(&path, tree)?;
435 Ok(tree.run_id.clone())
436 }
437
438 pub fn load_trace(&self, run_id: &str) -> Result<lex_trace::TraceTree, StoreError> {
439 let bytes = fs::read(self.trace_path(run_id))?;
440 Ok(serde_json::from_slice(&bytes)?)
441 }
442
443 pub fn list_traces(&self) -> Result<Vec<String>, StoreError> {
444 let dir = self.root.join("traces");
445 if !dir.exists() { return Ok(Vec::new()); }
446 let mut out = Vec::new();
447 for entry in fs::read_dir(dir)? {
448 let entry = entry?;
449 if entry.file_type()?.is_dir() {
450 out.push(entry.file_name().to_string_lossy().to_string());
451 }
452 }
453 out.sort();
454 Ok(out)
455 }
456
457 fn lookup_lifecycle(&self, stage_id: &str) -> Result<(String, Lifecycle), StoreError> {
460 for sig in self.list_sigs()? {
462 if let Ok(life) = self.read_lifecycle(&sig) {
463 if life.transitions.iter().any(|t| t.stage_id == stage_id) {
464 return Ok((sig, life));
465 }
466 }
467 }
468 Err(StoreError::UnknownStage(stage_id.into()))
469 }
470
471 fn read_lifecycle(&self, sig: &str) -> Result<Lifecycle, StoreError> {
472 let path = self.lifecycle_path(sig);
473 if !path.exists() {
474 return Ok(Lifecycle { sig_id: sig.into(), transitions: Vec::new() });
475 }
476 let bytes = fs::read(&path)?;
477 Ok(serde_json::from_slice(&bytes)?)
478 }
479
480 fn write_lifecycle(&self, sig: &str, life: &Lifecycle) -> Result<(), StoreError> {
481 write_canonical_json(&self.lifecycle_path(sig), life)
482 }
483
484 pub fn publish_program(
497 &self,
498 branch: &str,
499 stages: &[lex_ast::Stage],
500 diff: &lex_vcs::DiffReport,
501 new_imports: &lex_vcs::ImportMap,
502 activate: bool,
503 ) -> Result<PublishOutcome, StoreError> {
504 self.publish_program_signed(branch, stages, diff, new_imports, activate, None)
505 }
506
507 pub fn publish_program_signed(
512 &self,
513 branch: &str,
514 stages: &[lex_ast::Stage],
515 diff: &lex_vcs::DiffReport,
516 new_imports: &lex_vcs::ImportMap,
517 activate: bool,
518 signer: Option<&lex_vcs::Keypair>,
519 ) -> Result<PublishOutcome, StoreError> {
520 use std::collections::{BTreeMap, BTreeSet};
521
522 if let Err(errors) = lex_types::check_program(stages) {
531 return Err(StoreError::TypeError(errors));
532 }
533
534 let old_head = self.branch_head(branch)?;
536 let old_name_to_sig: BTreeMap<String, String> = old_head.iter()
537 .filter_map(|(sig, stg)| {
538 self.get_metadata(stg).ok().map(|m| (m.name, sig.clone()))
539 })
540 .collect();
541 let old_effects: BTreeMap<String, BTreeSet<String>> = old_head.iter()
542 .filter_map(|(sig, stg)| {
543 let ast = self.get_ast(stg).ok()?;
544 match ast {
545 lex_ast::Stage::FnDecl(fd) => {
546 let s: BTreeSet<String> = fd.effects.iter()
547 .map(|e| e.name.clone()).collect();
548 Some((sig.clone(), s))
549 }
550 _ => None,
551 }
552 })
553 .collect();
554 let old_imports = self.derive_imports_from_oplog(branch)?;
555
556 let op_kinds = lex_vcs::diff_to_ops(lex_vcs::DiffInputs {
557 old_head: &old_head,
558 old_name_to_sig: &old_name_to_sig,
559 old_effects: &old_effects,
560 old_imports: &old_imports,
561 new_stages: stages,
562 new_imports,
563 diff,
564 }).map_err(|e| StoreError::InvalidTransition(format!("diff_to_ops: {e}")))?;
565
566 let mut ops_out: Vec<PublishOp> = Vec::new();
567 let mut last_op_id: Option<lex_vcs::OpId> = None;
568 for kind in op_kinds {
569 if let Some(stg) = stage_for_kind(&kind, stages) {
572 if !matches!(stg, lex_ast::Stage::Import(_)) {
573 self.publish_signed(stg, signer)?;
574 if activate {
575 if let Some(stage_id_str) = stage_id(stg) {
576 let _ = self.activate(&stage_id_str);
577 }
578 }
579 }
580 }
581 let transition = transition_for_kind(&kind);
582 let attestable = attestable_stage_ids(&transition);
583 let head_now = self.get_branch(branch)?.and_then(|b| b.head_op);
584 let op = lex_vcs::Operation::new(
585 kind.clone(),
586 head_now.into_iter().collect::<Vec<_>>(),
587 );
588 let op_id = self.apply_operation(branch, op, transition)?;
589 self.record_typecheck_passed(&attestable, &op_id)?;
590 ops_out.push(PublishOp {
591 op_id: op_id.clone(),
592 kind: serde_json::to_value(&kind)
593 .map_err(StoreError::Serde)?,
594 });
595 last_op_id = Some(op_id);
596 }
597
598 let head_op = match last_op_id {
599 Some(id) => Some(id),
600 None => self.get_branch(branch)?.and_then(|b| b.head_op),
602 };
603
604 Ok(PublishOutcome {
605 ops: ops_out,
606 head_op,
607 })
608 }
609
610 pub fn derive_imports_from_oplog(
611 &self,
612 branch: &str,
613 ) -> Result<lex_vcs::ImportMap, StoreError> {
614 use lex_vcs::OperationKind::*;
615 let log = lex_vcs::OpLog::open(self.root())?;
616 let head = match self.get_branch(branch)?.and_then(|b| b.head_op) {
617 Some(h) => h,
618 None => return Ok(Default::default()),
619 };
620 let mut out: lex_vcs::ImportMap = Default::default();
621 for r in log.walk_forward(&head, None)? {
622 match r.op.kind {
623 AddImport { in_file, module } => {
624 out.entry(in_file).or_default().insert(module);
625 }
626 RemoveImport { in_file, module } => {
627 if let Some(set) = out.get_mut(&in_file) { set.remove(&module); }
628 }
629 _ => {}
630 }
631 }
632 Ok(out)
633 }
634
635 pub fn apply_operation_checked(
688 &self,
689 branch: &str,
690 op: lex_vcs::Operation,
691 transition: lex_vcs::StageTransition,
692 candidate: &[lex_ast::Stage],
693 ) -> Result<lex_vcs::OpId, StoreError> {
694 if let Err(errors) = lex_types::check_program(candidate) {
695 return Err(StoreError::TypeError(errors));
696 }
697 let attestable = attestable_stage_ids(&transition);
702 let op_effects = op_declared_effects(&op.kind);
703 let new_head = self.persist_op_only(branch, op, transition)?;
704 self.record_typecheck_passed(&attestable, &new_head.op_id)?;
705 self.run_required_attestations_gate(&new_head.op_id, &attestable, &op_effects)?;
706 self.set_branch_head_op(branch, new_head.op_id.clone())?;
707 Ok(new_head.op_id)
708 }
709
710 pub fn attestation_log(&self) -> Result<lex_vcs::AttestationLog, StoreError> {
717 Ok(lex_vcs::AttestationLog::open(self.root())?)
718 }
719
720 fn record_typecheck_passed(
733 &self,
734 stage_ids: &[String],
735 op_id: &lex_vcs::OpId,
736 ) -> Result<(), StoreError> {
737 if stage_ids.is_empty() {
738 return Ok(());
739 }
740 let log = self.attestation_log()?;
741 for stage_id in stage_ids {
742 let attestation = lex_vcs::Attestation::new(
743 stage_id.clone(),
744 Some(op_id.clone()),
745 None,
746 lex_vcs::AttestationKind::TypeCheck,
747 lex_vcs::AttestationResult::Passed,
748 typecheck_producer(),
749 None,
750 );
751 log.put(&attestation)?;
752 }
753 Ok(())
754 }
755
756 pub fn apply_operation(
759 &self,
760 branch: &str,
761 op: lex_vcs::Operation,
762 transition: lex_vcs::StageTransition,
763 ) -> Result<lex_vcs::OpId, StoreError> {
764 let attestable = attestable_stage_ids(&transition);
765 let op_effects = op_declared_effects(&op.kind);
766 let new_head = self.persist_op_only(branch, op, transition)?;
767 self.run_required_attestations_gate(&new_head.op_id, &attestable, &op_effects)?;
775 self.set_branch_head_op(branch, new_head.op_id.clone())?;
776 Ok(new_head.op_id)
777 }
778
779 fn persist_op_only(
787 &self,
788 branch: &str,
789 op: lex_vcs::Operation,
790 transition: lex_vcs::StageTransition,
791 ) -> Result<lex_vcs::NewHead, StoreError> {
792 if branch != DEFAULT_BRANCH && self.get_branch(branch)?.is_none() {
799 return Err(StoreError::UnknownBranch(branch.into()));
800 }
801 let log = lex_vcs::OpLog::open(self.root())?;
802 let head_op = self.get_branch(branch)?.and_then(|b| b.head_op);
803 lex_vcs::apply(&log, head_op.as_ref(), op, transition).map_err(|e| match e {
804 lex_vcs::ApplyError::Persist(io) => StoreError::Io(io),
805 other => StoreError::Apply(other),
806 })
807 }
808
809 fn run_required_attestations_gate(
828 &self,
829 op_id: &lex_vcs::OpId,
830 stage_ids: &[String],
831 op_effects: &std::collections::BTreeSet<String>,
832 ) -> Result<(), StoreError> {
833 let candidate: Vec<(lex_vcs::OpId, Option<String>, std::collections::BTreeSet<String>)> =
838 if stage_ids.is_empty() {
839 vec![(op_id.clone(), None, op_effects.clone())]
840 } else {
841 stage_ids
842 .iter()
843 .map(|sid| (op_id.clone(), Some(sid.clone()), op_effects.clone()))
844 .collect()
845 };
846 let attest_log = self.attestation_log()?;
847
848 crate::policy::check_producer_block(&attest_log, &candidate)
852 .map_err(StoreError::ProducerBlocked)?;
853
854 let policy = match crate::policy::load(self.root())? {
857 Some(p) if !p.required_attestations.is_empty() => p,
858 _ => return Ok(()),
859 };
860 crate::policy::check_required_attestations(&attest_log, &candidate, &policy)
861 .map_err(StoreError::BranchAdvanceBlocked)
862 }
863}
864
865fn stage_name(stage: &Stage) -> &str {
866 match stage {
867 Stage::FnDecl(fd) => &fd.name,
868 Stage::TypeDecl(td) => &td.name,
869 Stage::Import(i) => &i.alias,
870 }
871}
872
873fn stage_for_kind<'a>(
874 kind: &lex_vcs::OperationKind,
875 stages: &'a [lex_ast::Stage],
876) -> Option<&'a lex_ast::Stage> {
877 use lex_vcs::OperationKind::*;
878 let target_sig = match kind {
879 AddFunction { sig_id, .. } | ModifyBody { sig_id, .. }
880 | ChangeEffectSig { sig_id, .. } | AddType { sig_id, .. }
881 | ModifyType { sig_id, .. } => Some(sig_id.clone()),
882 RenameSymbol { to, .. } => Some(to.clone()),
883 _ => None,
884 };
885 let target_sig = target_sig?;
886 stages.iter().find(|s| sig_id(s).as_deref() == Some(target_sig.as_str()))
887}
888
889fn transition_for_kind(kind: &lex_vcs::OperationKind) -> lex_vcs::StageTransition {
890 use lex_vcs::OperationKind::*;
891 use lex_vcs::StageTransition;
892 match kind {
893 AddFunction { sig_id, stage_id, .. }
894 | AddType { sig_id, stage_id } => StageTransition::Create {
895 sig_id: sig_id.clone(), stage_id: stage_id.clone(),
896 },
897 RemoveFunction { sig_id, last_stage_id }
898 | RemoveType { sig_id, last_stage_id } => StageTransition::Remove {
899 sig_id: sig_id.clone(), last: last_stage_id.clone(),
900 },
901 ModifyBody { sig_id, from_stage_id, to_stage_id, .. }
902 | ChangeEffectSig { sig_id, from_stage_id, to_stage_id, .. }
903 | ModifyType { sig_id, from_stage_id, to_stage_id } => StageTransition::Replace {
904 sig_id: sig_id.clone(),
905 from: from_stage_id.clone(),
906 to: to_stage_id.clone(),
907 },
908 RenameSymbol { from, to, body_stage_id } => StageTransition::Rename {
909 from: from.clone(), to: to.clone(),
910 body_stage_id: body_stage_id.clone(),
911 },
912 AddImport { .. } | RemoveImport { .. } => StageTransition::ImportOnly,
913 Merge { .. } => StageTransition::Merge { entries: Default::default() },
914 }
915}
916
917fn typecheck_producer() -> lex_vcs::ProducerDescriptor {
922 lex_vcs::ProducerDescriptor {
923 tool: "lex-store".into(),
924 version: env!("CARGO_PKG_VERSION").into(),
925 model: None,
926 }
927}
928
929fn op_declared_effects(kind: &lex_vcs::OperationKind) -> std::collections::BTreeSet<String> {
946 use lex_vcs::OperationKind::*;
947 match kind {
948 AddFunction { effects, .. } => effects.clone(),
949 ChangeEffectSig { to_effects, .. } => to_effects.clone(),
950 _ => std::collections::BTreeSet::new(),
951 }
952}
953
954fn attestable_stage_ids(transition: &lex_vcs::StageTransition) -> Vec<String> {
955 use lex_vcs::StageTransition::*;
956 match transition {
957 Create { stage_id, .. } => vec![stage_id.clone()],
958 Replace { to, .. } => vec![to.clone()],
959 Rename { body_stage_id, .. } => vec![body_stage_id.clone()],
960 Merge { entries } => entries
961 .values()
962 .filter_map(|opt| opt.clone())
963 .collect(),
964 Remove { .. } | ImportOnly => Vec::new(),
965 }
966}
967
968fn write_canonical_json<T: Serialize>(path: &Path, value: &T) -> Result<(), StoreError> {
969 let v = serde_json::to_value(value)?;
970 let s = lex_ast::canon_json::to_canonical_string(&v);
971 if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; }
972 fs::write(path, s)?;
973 Ok(())
974}
975
976#[allow(dead_code)]
977fn read_json<T: DeserializeOwned>(path: &Path) -> Result<T, StoreError> {
978 let bytes = fs::read(path)?;
979 Ok(serde_json::from_slice(&bytes)?)
980}