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("unknown op_id `{0}`")]
35 UnknownOp(lex_vcs::OpId),
36 #[error("transform failed: {0}")]
43 TransformError(lex_ast::TransformError),
44 #[error(transparent)]
45 Apply(#[from] lex_vcs::ApplyError),
46 #[error("type errors in published program: {} error(s)", .0.len())]
52 TypeError(Vec<lex_types::TypeError>),
53 #[error(
60 "branch advance blocked: op {} missing attestations: {}",
61 .0.op_id, .0.missing.join(", ")
62 )]
63 BranchAdvanceBlocked(crate::policy::BranchAdvanceBlocked),
64 #[error("branch advance contention on `{branch}`: {attempts} retries exhausted")]
71 Contention { branch: String, attempts: u32 },
72 #[error(
79 "branch advance blocked: op {} touches stage {} with an attestation from \
80 quarantined producer `{}` (blocked at {}, attestation at {})",
81 .0.op_id, .0.stage_id, .0.tool_id, .0.blocked_at, .0.attestation_at
82 )]
83 ProducerBlocked(crate::policy::ProducerBlocked),
84 #[error(
90 "session `{session_id}` budget exceeded: spent_after={spent_after} > cap={cap}"
91 )]
92 BudgetExceeded {
93 session_id: String,
94 cap: u64,
95 spent_after: u64,
96 },
97}
98
99#[derive(Debug, Clone, serde::Serialize)]
101pub struct PublishOutcome {
102 pub ops: Vec<PublishOp>,
103 pub head_op: Option<lex_vcs::OpId>,
104}
105
106#[derive(Debug, Clone, serde::Serialize)]
108pub struct PublishOp {
109 pub op_id: lex_vcs::OpId,
110 pub kind: serde_json::Value,
111}
112
113#[derive(Debug, Clone, serde::Serialize, PartialEq)]
117pub struct StageHistoryEntry {
118 pub stage_id: String,
119 pub status: StageStatus,
120 pub last_at: u64,
122 #[serde(skip_serializing_if = "Option::is_none")]
128 pub published_at: Option<u64>,
129}
130
131#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
134pub struct CandidateInfo {
135 pub op_id: lex_vcs::OpId,
136 pub stage_id: lex_vcs::StageId,
137 pub intent_id: Option<lex_vcs::IntentId>,
141}
142
143pub struct Store {
144 root: PathBuf,
145}
146
147impl Store {
148 pub fn open(root: impl AsRef<Path>) -> Result<Self, StoreError> {
150 let root = root.as_ref().to_path_buf();
151 fs::create_dir_all(root.join("stages"))?;
152 fs::create_dir_all(root.join("traces"))?;
153 Ok(Self { root })
154 }
155
156 pub fn root(&self) -> &Path { &self.root }
157
158 fn now() -> u64 {
159 SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
160 }
161
162 fn sig_dir(&self, sig: &str) -> PathBuf { self.root.join("stages").join(sig) }
163 fn impl_dir(&self, sig: &str) -> PathBuf { self.sig_dir(sig).join("implementations") }
164 fn tests_dir(&self, sig: &str) -> PathBuf { self.sig_dir(sig).join("tests") }
165 fn specs_dir(&self, sig: &str) -> PathBuf { self.sig_dir(sig).join("specs") }
166 fn lifecycle_path(&self, sig: &str) -> PathBuf { self.sig_dir(sig).join("lifecycle.json") }
167
168 pub fn publish(&self, stage: &Stage) -> Result<String, StoreError> {
174 self.publish_signed(stage, None)
175 }
176
177 pub fn publish_signed(
189 &self,
190 stage: &Stage,
191 signer: Option<&lex_vcs::Keypair>,
192 ) -> Result<String, StoreError> {
193 let sig = sig_id(stage).ok_or(StoreError::CannotPublishImport)?;
194 let stage_id = stage_id(stage).ok_or(StoreError::CannotPublishImport)?;
195 let name = stage_name(stage).to_string();
196
197 fs::create_dir_all(self.impl_dir(&sig))?;
198 fs::create_dir_all(self.tests_dir(&sig))?;
199 fs::create_dir_all(self.specs_dir(&sig))?;
200
201 let ast_path = self.impl_dir(&sig).join(format!("{}.ast.json", stage_id));
202 let delta_path = self.impl_dir(&sig).join(format!("{}.delta.json", stage_id));
203 let meta_path = self.impl_dir(&sig).join(format!("{}.metadata.json", stage_id));
204
205 if !ast_path.exists() && !delta_path.exists() {
212 self.persist_stage_bytes(&sig, &stage_id, stage, &ast_path, &delta_path)?;
213 }
214 if !meta_path.exists() {
215 let signature = signer.map(|kp| kp.sign_stage_id(&stage_id));
216 let metadata = Metadata {
217 stage_id: stage_id.clone(),
218 sig_id: sig.clone(),
219 name,
220 published_at: Self::now(),
221 note: None,
222 signature,
223 };
224 write_canonical_json(&meta_path, &metadata)?;
225 }
226
227 let mut life = self.read_lifecycle(&sig).unwrap_or_else(|_| Lifecycle {
229 sig_id: sig.clone(),
230 ..Default::default()
231 });
232 if !life.transitions.iter().any(|t| t.stage_id == stage_id) {
233 life.transitions.push(Transition {
234 stage_id: stage_id.clone(),
235 from: StageStatus::Draft, to: StageStatus::Draft,
237 at: Self::now(),
238 reason: None,
239 });
240 self.write_lifecycle(&sig, &life)?;
241 }
242 Ok(stage_id)
243 }
244
245 pub fn activate(&self, stage_id: &str) -> Result<(), StoreError> {
248 let (sig, mut life) = self.lookup_lifecycle(stage_id)?;
249 let active = life.current_active().map(|s| s.to_string());
251 if let Some(prev) = active {
252 if prev != stage_id {
253 life.transitions.push(Transition {
254 stage_id: prev,
255 from: StageStatus::Active,
256 to: StageStatus::Deprecated,
257 at: Self::now(),
258 reason: Some("superseded".into()),
259 });
260 }
261 }
262 let cur = life.status_of(stage_id);
263 if cur == Some(StageStatus::Tombstone) {
264 return Err(StoreError::InvalidTransition("tombstoned cannot be activated".into()));
265 }
266 life.transitions.push(Transition {
267 stage_id: stage_id.into(),
268 from: cur.unwrap_or(StageStatus::Draft),
269 to: StageStatus::Active,
270 at: Self::now(),
271 reason: None,
272 });
273 self.write_lifecycle(&sig, &life)
274 }
275
276 pub fn deprecate(&self, stage_id: &str, reason: impl Into<String>) -> Result<(), StoreError> {
277 let (sig, mut life) = self.lookup_lifecycle(stage_id)?;
278 let cur = life.status_of(stage_id).ok_or_else(|| StoreError::UnknownStage(stage_id.into()))?;
279 if cur != StageStatus::Active {
280 return Err(StoreError::InvalidTransition(format!("{cur:?} ⇒ Deprecated")));
281 }
282 life.transitions.push(Transition {
283 stage_id: stage_id.into(),
284 from: cur,
285 to: StageStatus::Deprecated,
286 at: Self::now(),
287 reason: Some(reason.into()),
288 });
289 self.write_lifecycle(&sig, &life)
290 }
291
292 pub fn tombstone(&self, stage_id: &str) -> Result<(), StoreError> {
293 let (sig, mut life) = self.lookup_lifecycle(stage_id)?;
294 let cur = life.status_of(stage_id).ok_or_else(|| StoreError::UnknownStage(stage_id.into()))?;
295 if cur != StageStatus::Deprecated {
296 return Err(StoreError::InvalidTransition(format!("{cur:?} ⇒ Tombstone")));
297 }
298 life.transitions.push(Transition {
299 stage_id: stage_id.into(),
300 from: cur,
301 to: StageStatus::Tombstone,
302 at: Self::now(),
303 reason: None,
304 });
305 self.write_lifecycle(&sig, &life)
306 }
307
308 pub fn resolve_sig(&self, sig: &str) -> Result<Option<String>, StoreError> {
312 let life = match self.read_lifecycle(sig) {
313 Ok(l) => l,
314 Err(_) => return Ok(None),
315 };
316 Ok(life.current_active().map(|s| s.to_string()))
317 }
318
319 pub fn sig_history(&self, sig: &str) -> Result<Vec<StageHistoryEntry>, StoreError> {
326 let life = match self.read_lifecycle(sig) {
327 Ok(l) => l,
328 Err(_) => return Ok(Vec::new()),
329 };
330 let mut by_stage: indexmap::IndexMap<String, StageHistoryEntry> =
334 indexmap::IndexMap::new();
335 for t in &life.transitions {
336 let entry = by_stage.entry(t.stage_id.clone()).or_insert(StageHistoryEntry {
337 stage_id: t.stage_id.clone(),
338 status: t.to,
339 last_at: t.at,
340 published_at: None,
341 });
342 entry.status = t.to;
343 entry.last_at = t.at;
344 if t.from == StageStatus::Draft && entry.published_at.is_none() {
345 entry.published_at = Some(t.at);
346 }
347 if t.to == StageStatus::Draft && entry.published_at.is_none() {
348 entry.published_at = Some(t.at);
350 }
351 }
352 let mut out: Vec<StageHistoryEntry> = by_stage.into_values().collect();
353 out.sort_by_key(|e| std::cmp::Reverse(e.last_at));
355 Ok(out)
356 }
357
358 pub fn get_ast(&self, stage_id: &str) -> Result<Stage, StoreError> {
359 let (sig, _) = self.lookup_lifecycle(stage_id)?;
360 let bytes = self.read_stage_canonical_bytes(&sig, stage_id)?;
361 Ok(serde_json::from_slice(&bytes)?)
362 }
363
364 fn read_stage_canonical_bytes(
369 &self,
370 sig: &str,
371 stage_id: &str,
372 ) -> Result<Vec<u8>, StoreError> {
373 let ast_path = self.impl_dir(sig).join(format!("{}.ast.json", stage_id));
374 if ast_path.exists() {
375 return Ok(fs::read(&ast_path)?);
376 }
377 let delta_path = self.impl_dir(sig).join(format!("{}.delta.json", stage_id));
378 if !delta_path.exists() {
379 return Err(StoreError::UnknownStage(stage_id.into()));
380 }
381 let delta_bytes = fs::read(&delta_path)?;
382 let delta: crate::delta::StageDelta = serde_json::from_slice(&delta_bytes)?;
383 let base_bytes = self.read_stage_canonical_bytes(sig, &delta.base_stage_id)?;
384 crate::delta::apply(&base_bytes, &delta)
385 .map_err(|e| StoreError::Io(std::io::Error::new(
386 std::io::ErrorKind::InvalidData,
387 format!("applying delta for {stage_id}: {e}"),
388 )))
389 }
390
391 fn persist_stage_bytes(
397 &self,
398 sig: &str,
399 stage_id: &str,
400 stage: &Stage,
401 ast_path: &Path,
402 delta_path: &Path,
403 ) -> Result<(), StoreError> {
404 let new_bytes = canonical_bytes(stage)?;
405 if let Some((base_stage_id, base_chain_length)) =
406 self.pick_delta_base(sig, stage_id)?
407 {
408 let base_bytes = self.read_stage_canonical_bytes(sig, &base_stage_id)?;
409 let (prefix, suffix, middle) = crate::delta::splice(&base_bytes, &new_bytes);
410 let chain_length = base_chain_length + 1;
411 if crate::delta::is_worth_encoding(middle.len(), new_bytes.len(), chain_length) {
412 let delta = crate::delta::StageDelta {
413 base_stage_id,
414 chain_length,
415 common_prefix: prefix,
416 common_suffix: suffix,
417 middle_hex: hex::encode(&middle),
418 };
419 write_canonical_json(delta_path, &delta)?;
420 return Ok(());
421 }
422 }
423 if let Some(parent) = ast_path.parent() { fs::create_dir_all(parent)?; }
425 fs::write(ast_path, &new_bytes)?;
426 Ok(())
427 }
428
429 fn pick_delta_base(
435 &self,
436 sig: &str,
437 new_stage_id: &str,
438 ) -> Result<Option<(String, usize)>, StoreError> {
439 let life = self.read_lifecycle(sig).ok();
440 let Some(life) = life else { return Ok(None); };
441 let mut latest_per_stage: indexmap::IndexMap<&str, StageStatus> = indexmap::IndexMap::new();
444 for t in &life.transitions {
445 latest_per_stage.insert(&t.stage_id, t.to);
446 }
447 let mut candidates: Vec<&str> = latest_per_stage
448 .iter()
449 .filter(|(id, status)| {
450 **id != new_stage_id && **status != StageStatus::Tombstone
451 })
452 .map(|(id, _)| *id)
453 .collect();
454 candidates.reverse();
458 let Some(&base) = candidates.first() else { return Ok(None); };
459 let base_chain_length = self.delta_chain_length(sig, base)?;
460 Ok(Some((base.to_string(), base_chain_length)))
461 }
462
463 fn delta_chain_length(&self, sig: &str, stage_id: &str) -> Result<usize, StoreError> {
467 let ast_path = self.impl_dir(sig).join(format!("{}.ast.json", stage_id));
468 if ast_path.exists() {
469 return Ok(0);
470 }
471 let delta_path = self.impl_dir(sig).join(format!("{}.delta.json", stage_id));
472 if !delta_path.exists() {
473 return Ok(0);
474 }
475 let bytes = fs::read(&delta_path)?;
476 let delta: crate::delta::StageDelta = serde_json::from_slice(&bytes)?;
477 Ok(delta.chain_length)
478 }
479
480 pub fn get_metadata(&self, stage_id: &str) -> Result<Metadata, StoreError> {
481 let (sig, _) = self.lookup_lifecycle(stage_id)?;
482 let path = self.impl_dir(&sig).join(format!("{}.metadata.json", stage_id));
483 let bytes = fs::read(&path)?;
484 Ok(serde_json::from_slice(&bytes)?)
485 }
486
487 pub fn get_status(&self, stage_id: &str) -> Result<StageStatus, StoreError> {
488 let (_sig, life) = self.lookup_lifecycle(stage_id)?;
489 life.status_of(stage_id).ok_or_else(|| StoreError::UnknownStage(stage_id.into()))
490 }
491
492 pub fn list_stages_by_name(&self, name: &str) -> Result<Vec<String>, StoreError> {
493 let mut out = Vec::new();
496 let stages_dir = self.root.join("stages");
497 if !stages_dir.exists() { return Ok(out); }
498 for entry in fs::read_dir(&stages_dir)? {
499 let entry = entry?;
500 let sig_dir = entry.path();
501 if !sig_dir.is_dir() { continue; }
502 let sig = entry.file_name().to_string_lossy().to_string();
503 let impls = self.impl_dir(&sig);
505 if !impls.exists() { continue; }
506 for f in fs::read_dir(impls)? {
507 let f = f?;
508 let p = f.path();
509 if p.extension().is_some_and(|e| e == "json")
510 && p.file_name().is_some_and(|n| n.to_string_lossy().ends_with(".metadata.json"))
511 {
512 if let Ok(bytes) = fs::read(&p) {
513 if let Ok(m) = serde_json::from_slice::<Metadata>(&bytes) {
514 if m.name == name {
515 if !out.contains(&sig) { out.push(sig.clone()); }
516 break;
517 }
518 }
519 }
520 }
521 }
522 }
523 out.sort();
524 Ok(out)
525 }
526
527 pub fn list_sigs(&self) -> Result<Vec<String>, StoreError> {
528 let stages_dir = self.root.join("stages");
529 let mut out = Vec::new();
530 if !stages_dir.exists() { return Ok(out); }
531 for entry in fs::read_dir(stages_dir)? {
532 let entry = entry?;
533 if entry.file_type()?.is_dir() {
534 out.push(entry.file_name().to_string_lossy().to_string());
535 }
536 }
537 out.sort();
538 Ok(out)
539 }
540
541 pub fn attach_test(&self, sig: &str, test: &Test) -> Result<String, StoreError> {
544 if !self.sig_dir(sig).exists() {
545 return Err(StoreError::UnknownSig(sig.into()));
546 }
547 fs::create_dir_all(self.tests_dir(sig))?;
548 let path = self.tests_dir(sig).join(format!("{}.json", test.id));
549 write_canonical_json(&path, test)?;
550 Ok(test.id.clone())
551 }
552
553 pub fn list_tests(&self, sig: &str) -> Result<Vec<Test>, StoreError> {
554 let dir = self.tests_dir(sig);
555 if !dir.exists() { return Ok(Vec::new()); }
556 let mut out = Vec::new();
557 for f in fs::read_dir(dir)? {
558 let f = f?;
559 if f.path().extension().is_some_and(|e| e == "json") {
560 let bytes = fs::read(f.path())?;
561 out.push(serde_json::from_slice(&bytes)?);
562 }
563 }
564 Ok(out)
565 }
566
567 pub fn attach_spec(&self, sig: &str, spec: &Spec) -> Result<String, StoreError> {
568 if !self.sig_dir(sig).exists() {
569 return Err(StoreError::UnknownSig(sig.into()));
570 }
571 fs::create_dir_all(self.specs_dir(sig))?;
572 let path = self.specs_dir(sig).join(format!("{}.json", spec.id));
573 write_canonical_json(&path, spec)?;
574 Ok(spec.id.clone())
575 }
576
577 pub fn list_specs(&self, sig: &str) -> Result<Vec<Spec>, StoreError> {
578 let dir = self.specs_dir(sig);
579 if !dir.exists() { return Ok(Vec::new()); }
580 let mut out = Vec::new();
581 for f in fs::read_dir(dir)? {
582 let f = f?;
583 if f.path().extension().is_some_and(|e| e == "json") {
584 let bytes = fs::read(f.path())?;
585 out.push(serde_json::from_slice(&bytes)?);
586 }
587 }
588 Ok(out)
589 }
590
591 fn trace_path(&self, run_id: &str) -> PathBuf {
594 self.root.join("traces").join(run_id).join("trace.json")
595 }
596
597 pub fn save_trace(&self, tree: &lex_trace::TraceTree) -> Result<String, StoreError> {
598 let path = self.trace_path(&tree.run_id);
599 write_canonical_json(&path, tree)?;
600 Ok(tree.run_id.clone())
601 }
602
603 pub fn load_trace(&self, run_id: &str) -> Result<lex_trace::TraceTree, StoreError> {
604 let bytes = fs::read(self.trace_path(run_id))?;
605 Ok(serde_json::from_slice(&bytes)?)
606 }
607
608 pub fn list_traces(&self) -> Result<Vec<String>, StoreError> {
609 let dir = self.root.join("traces");
610 if !dir.exists() { return Ok(Vec::new()); }
611 let mut out = Vec::new();
612 for entry in fs::read_dir(dir)? {
613 let entry = entry?;
614 if entry.file_type()?.is_dir() {
615 out.push(entry.file_name().to_string_lossy().to_string());
616 }
617 }
618 out.sort();
619 Ok(out)
620 }
621
622 fn lookup_lifecycle(&self, stage_id: &str) -> Result<(String, Lifecycle), StoreError> {
625 for sig in self.list_sigs()? {
627 if let Ok(life) = self.read_lifecycle(&sig) {
628 if life.transitions.iter().any(|t| t.stage_id == stage_id) {
629 return Ok((sig, life));
630 }
631 }
632 }
633 Err(StoreError::UnknownStage(stage_id.into()))
634 }
635
636 fn read_lifecycle(&self, sig: &str) -> Result<Lifecycle, StoreError> {
637 let path = self.lifecycle_path(sig);
638 if !path.exists() {
639 return Ok(Lifecycle { sig_id: sig.into(), transitions: Vec::new() });
640 }
641 let bytes = fs::read(&path)?;
642 Ok(serde_json::from_slice(&bytes)?)
643 }
644
645 fn write_lifecycle(&self, sig: &str, life: &Lifecycle) -> Result<(), StoreError> {
646 write_canonical_json(&self.lifecycle_path(sig), life)
647 }
648
649 pub fn publish_program(
662 &self,
663 branch: &str,
664 stages: &[lex_ast::Stage],
665 diff: &lex_vcs::DiffReport,
666 new_imports: &lex_vcs::ImportMap,
667 activate: bool,
668 ) -> Result<PublishOutcome, StoreError> {
669 self.publish_program_signed(branch, stages, diff, new_imports, activate, None)
670 }
671
672 pub fn publish_program_signed(
677 &self,
678 branch: &str,
679 stages: &[lex_ast::Stage],
680 diff: &lex_vcs::DiffReport,
681 new_imports: &lex_vcs::ImportMap,
682 activate: bool,
683 signer: Option<&lex_vcs::Keypair>,
684 ) -> Result<PublishOutcome, StoreError> {
685 use std::collections::{BTreeMap, BTreeSet};
686
687 if let Err(errors) = lex_types::check_program(stages) {
696 return Err(StoreError::TypeError(errors));
697 }
698
699 let old_head = self.branch_head(branch)?;
701 let old_name_to_sig: BTreeMap<String, String> = old_head.iter()
702 .filter_map(|(sig, stg)| {
703 self.get_metadata(stg).ok().map(|m| (m.name, sig.clone()))
704 })
705 .collect();
706 let old_effects: BTreeMap<String, BTreeSet<String>> = old_head.iter()
707 .filter_map(|(sig, stg)| {
708 let ast = self.get_ast(stg).ok()?;
709 match ast {
710 lex_ast::Stage::FnDecl(fd) => {
711 let s: BTreeSet<String> = fd.effects.iter()
712 .map(|e| e.name.clone()).collect();
713 Some((sig.clone(), s))
714 }
715 _ => None,
716 }
717 })
718 .collect();
719 let old_imports = self.derive_imports_from_oplog(branch)?;
720
721 let op_kinds = lex_vcs::diff_to_ops(lex_vcs::DiffInputs {
722 old_head: &old_head,
723 old_name_to_sig: &old_name_to_sig,
724 old_effects: &old_effects,
725 old_imports: &old_imports,
726 new_stages: stages,
727 new_imports,
728 diff,
729 }).map_err(|e| StoreError::InvalidTransition(format!("diff_to_ops: {e}")))?;
730
731 let mut ops_out: Vec<PublishOp> = Vec::new();
732 let mut last_op_id: Option<lex_vcs::OpId> = None;
733 for kind in op_kinds {
734 if let Some(stg) = stage_for_kind(&kind, stages) {
737 if !matches!(stg, lex_ast::Stage::Import(_)) {
738 self.publish_signed(stg, signer)?;
739 if activate {
740 if let Some(stage_id_str) = stage_id(stg) {
741 let _ = self.activate(&stage_id_str);
742 }
743 }
744 }
745 }
746 let transition = transition_for_kind(&kind);
747 let attestable = attestable_stage_ids(&transition);
748 let head_now = self.get_branch(branch)?.and_then(|b| b.head_op);
749 let op = lex_vcs::Operation::new(
750 kind.clone(),
751 head_now.into_iter().collect::<Vec<_>>(),
752 );
753 let op_id = self.apply_operation(branch, op, transition)?;
754 self.record_typecheck_passed(&attestable, &op_id)?;
755 ops_out.push(PublishOp {
756 op_id: op_id.clone(),
757 kind: serde_json::to_value(&kind)
758 .map_err(StoreError::Serde)?,
759 });
760 last_op_id = Some(op_id);
761 }
762
763 let head_op = match last_op_id {
764 Some(id) => Some(id),
765 None => self.get_branch(branch)?.and_then(|b| b.head_op),
767 };
768
769 Ok(PublishOutcome {
770 ops: ops_out,
771 head_op,
772 })
773 }
774
775 pub fn derive_imports_from_oplog(
776 &self,
777 branch: &str,
778 ) -> Result<lex_vcs::ImportMap, StoreError> {
779 use lex_vcs::OperationKind::*;
780 let log = lex_vcs::OpLog::open(self.root())?;
781 let head = match self.get_branch(branch)?.and_then(|b| b.head_op) {
782 Some(h) => h,
783 None => return Ok(Default::default()),
784 };
785 let mut out: lex_vcs::ImportMap = Default::default();
786 for r in log.walk_forward(&head, None)? {
787 match r.op.kind {
788 AddImport { in_file, module } => {
789 out.entry(in_file).or_default().insert(module);
790 }
791 RemoveImport { in_file, module } => {
792 if let Some(set) = out.get_mut(&in_file) { set.remove(&module); }
793 }
794 _ => {}
795 }
796 }
797 Ok(out)
798 }
799
800 pub fn apply_operation_checked(
853 &self,
854 branch: &str,
855 op: lex_vcs::Operation,
856 transition: lex_vcs::StageTransition,
857 candidate: &[lex_ast::Stage],
858 ) -> Result<lex_vcs::OpId, StoreError> {
859 if let Err(errors) = lex_types::check_program(candidate) {
860 let attestable = attestable_stage_ids(&transition);
869 let failed_op_id = op.op_id();
870 let _ = self.record_repair_hint(&attestable, &failed_op_id, &errors);
871 return Err(StoreError::TypeError(errors));
872 }
873 self.check_session_budget(&op)?;
879 let attestable = attestable_stage_ids(&transition);
880 let op_effects = op_declared_effects(&op.kind);
881 self.cas_retry_advance(branch, op, transition, |new_head| {
888 self.record_typecheck_passed(&attestable, &new_head.op_id)?;
889 self.run_required_attestations_gate(
890 branch, &new_head.op_id, &attestable, &op_effects,
891 )
892 })
893 }
894
895 pub fn recompute_producer_trust(
918 &self,
919 tool_id: &str,
920 window: usize,
921 granted_by: &str,
922 ) -> Result<Option<lex_vcs::AttestationId>, StoreError> {
923 let log = self.attestation_log()?;
924 let all = log.list_all()?;
925 if lex_vcs::active_producer_block(&all, tool_id).is_some() {
927 return Err(StoreError::InvalidTransition(format!(
928 "cannot recompute trust for `{tool_id}` — \
929 producer is currently blocked"
930 )));
931 }
932 let mut from_tool: Vec<&lex_vcs::Attestation> = all.iter()
935 .filter(|a| a.produced_by.tool == tool_id)
936 .filter(|a| !matches!(a.kind,
939 lex_vcs::AttestationKind::ProducerTrust { .. }
940 | lex_vcs::AttestationKind::TrustWaived { .. }))
941 .collect();
942 from_tool.sort_by_key(|a| std::cmp::Reverse(a.timestamp));
943 from_tool.truncate(window);
944 if from_tool.is_empty() {
945 return Ok(None);
946 }
947 let (mut passed, mut total) = (0u64, 0u64);
948 for a in &from_tool {
949 total += 1;
950 if matches!(a.result, lex_vcs::AttestationResult::Passed) {
951 passed += 1;
952 }
953 }
954 let score = if total == 0 {
955 0
956 } else {
957 let raw = (passed as f64) * 1000.0 / (total as f64);
958 raw.round().clamp(0.0, 1000.0) as u32
959 };
960 let head_op = self.list_branches()?
961 .into_iter()
962 .find_map(|b| self.get_branch(&b).ok().flatten().and_then(|x| x.head_op))
963 .unwrap_or_else(|| "fresh".into());
964 let evidence = format!("window={window}, sample={}, head_op={head_op:.16}", from_tool.len());
965 let attestation = lex_vcs::Attestation::new(
966 tool_id.to_string(),
967 None,
968 None,
969 lex_vcs::AttestationKind::ProducerTrust {
970 tool_id: tool_id.into(),
971 score_thousandths: score,
972 evidence,
973 granted_by: granted_by.into(),
974 },
975 lex_vcs::AttestationResult::Passed,
976 producer_trust_producer(),
977 None,
978 );
979 let id = attestation.attestation_id.clone();
980 log.put(&attestation)?;
981 Ok(Some(id))
982 }
983
984 pub fn attestation_log(&self) -> Result<lex_vcs::AttestationLog, StoreError> {
985 Ok(lex_vcs::AttestationLog::open(self.root())?)
986 }
987
988 fn record_typecheck_passed(
1001 &self,
1002 stage_ids: &[String],
1003 op_id: &lex_vcs::OpId,
1004 ) -> Result<(), StoreError> {
1005 if stage_ids.is_empty() {
1006 return Ok(());
1007 }
1008 let log = self.attestation_log()?;
1009 for stage_id in stage_ids {
1010 let attestation = lex_vcs::Attestation::new(
1011 stage_id.clone(),
1012 Some(op_id.clone()),
1013 None,
1014 lex_vcs::AttestationKind::TypeCheck,
1015 lex_vcs::AttestationResult::Passed,
1016 typecheck_producer(),
1017 None,
1018 );
1019 log.put(&attestation)?;
1020 }
1021 Ok(())
1022 }
1023
1024 fn check_session_budget(
1032 &self,
1033 op: &lex_vcs::Operation,
1034 ) -> Result<(), StoreError> {
1035 let Some(intent_id) = op.intent_id.as_deref() else { return Ok(()); };
1036 let intent_log = lex_vcs::IntentLog::open(self.root())?;
1037 let Some(intent) = intent_log.get(&intent_id.to_string())? else {
1038 return Ok(());
1042 };
1043 let policy = crate::policy::load(self.root())?.unwrap_or_default();
1044 let Some(cap) = policy.session_budgets.cap_for(&intent.session_id) else {
1045 return Ok(());
1046 };
1047 let current = self.session_budget(&intent.session_id)?;
1052 let increment = crate::budget::monotonic_spend_of(&op.kind);
1053 let spent_after = current.spent.saturating_add(increment);
1054 if spent_after > cap {
1055 return Err(StoreError::BudgetExceeded {
1056 session_id: intent.session_id,
1057 cap,
1058 spent_after,
1059 });
1060 }
1061 Ok(())
1062 }
1063
1064 fn record_repair_hint(
1080 &self,
1081 stage_ids: &[String],
1082 failed_op_id: &lex_vcs::OpId,
1083 errors: &[lex_types::TypeError],
1084 ) -> Result<(), StoreError> {
1085 if stage_ids.is_empty() {
1086 return Ok(());
1087 }
1088 let errors_json = serde_json::to_value(errors)
1089 .map_err(StoreError::Serde)?;
1090 let suggested_transform = errors
1096 .first()
1097 .and_then(|e| lex_types::suggested_transform_for(e.rule_tag()));
1098 let log = self.attestation_log()?;
1099 for stage_id in stage_ids {
1100 let attestation = lex_vcs::Attestation::new(
1101 stage_id.clone(),
1102 None, None,
1106 lex_vcs::AttestationKind::RepairHint {
1107 failed_op_id: failed_op_id.clone(),
1108 errors: errors_json.clone(),
1109 suggested_transform: suggested_transform.clone(),
1110 },
1111 lex_vcs::AttestationResult::Failed {
1112 detail: format!("op {} rejected: {} type error(s)",
1113 failed_op_id, errors.len()),
1114 },
1115 repair_hint_producer(),
1116 None,
1117 );
1118 log.put(&attestation)?;
1119 }
1120 Ok(())
1121 }
1122
1123 pub fn record_op_trace(
1141 &self,
1142 run_id: &str,
1143 root_target: &str,
1144 op_id: &lex_vcs::OpId,
1145 result: lex_vcs::AttestationResult,
1146 producer: lex_vcs::ProducerDescriptor,
1147 ) -> Result<usize, StoreError> {
1148 let log = lex_vcs::OpLog::open(self.root())?;
1149 let rec = log.get(op_id)?
1150 .ok_or_else(|| StoreError::UnknownOp(op_id.clone()))?;
1151 let stage_ids = attestable_stage_ids(&rec.produces);
1152 if stage_ids.is_empty() {
1153 return Ok(0);
1154 }
1155 let attlog = self.attestation_log()?;
1156 let mut emitted = 0;
1157 for stage_id in stage_ids {
1158 let attestation = lex_vcs::Attestation::new(
1159 stage_id,
1160 Some(op_id.clone()),
1161 None,
1162 lex_vcs::AttestationKind::Trace {
1163 run_id: run_id.into(),
1164 root_target: root_target.into(),
1165 },
1166 result.clone(),
1167 producer.clone(),
1168 None,
1169 );
1170 attlog.put(&attestation)?;
1171 emitted += 1;
1172 }
1173 Ok(emitted)
1174 }
1175
1176 pub fn record_run_committed_ops_since(
1192 &self,
1193 run_id: &str,
1194 root_target: &str,
1195 branch: &str,
1196 base: Option<&lex_vcs::OpId>,
1197 result: lex_vcs::AttestationResult,
1198 producer: lex_vcs::ProducerDescriptor,
1199 ) -> Result<usize, StoreError> {
1200 let head = match self.get_branch(branch)?.and_then(|b| b.head_op) {
1201 Some(h) => h,
1202 None => return Ok(0),
1203 };
1204 let log = lex_vcs::OpLog::open(self.root())?;
1205 let new_ops = log.ops_since(&head, base)?;
1206 let mut total = 0;
1207 for rec in new_ops {
1208 total += self.record_op_trace(
1209 run_id, root_target, &rec.op_id,
1210 result.clone(), producer.clone(),
1211 )?;
1212 }
1213 Ok(total)
1214 }
1215
1216 pub fn apply_replace_match_arm(
1241 &self,
1242 branch: &str,
1243 from_stage_id: &str,
1244 match_node: &lex_ast::NodeId,
1245 arm_index: usize,
1246 new_body: lex_ast::CExpr,
1247 ) -> Result<lex_vcs::OpId, StoreError> {
1248 let from_stage = self.get_ast(from_stage_id)?;
1249 let new_stage = lex_ast::replace_match_arm(
1250 &from_stage, match_node, arm_index, new_body,
1251 ).map_err(StoreError::TransformError)?;
1252 let sig = lex_ast::sig_id(&from_stage)
1253 .ok_or(StoreError::CannotPublishImport)?;
1254 let to_stage_id = self.publish(&new_stage)?;
1255 if to_stage_id == from_stage_id {
1256 return Err(StoreError::InvalidTransition(format!(
1260 "replace_match_arm produced the same stage_id `{from_stage_id}`"
1261 )));
1262 }
1263
1264 let head = self.branch_head(branch)?;
1267 let mut candidate: Vec<lex_ast::Stage> = Vec::with_capacity(head.len());
1268 for (other_sig, other_stage_id) in &head {
1269 if other_sig == &sig {
1270 candidate.push(new_stage.clone());
1271 } else {
1272 candidate.push(self.get_ast(other_stage_id)?);
1273 }
1274 }
1275 if !head.contains_key(&sig) {
1280 return Err(StoreError::InvalidTransition(format!(
1281 "sig `{sig}` not on branch `{branch}`'s head"
1282 )));
1283 }
1284
1285 let from_budget = budget_of_stage(&from_stage);
1287 let to_budget = budget_of_stage(&new_stage);
1288
1289 let head_now = self.get_branch(branch)?.and_then(|b| b.head_op);
1290 let kind = lex_vcs::OperationKind::ReplaceMatchArm {
1291 sig_id: sig.clone(),
1292 from_stage_id: from_stage_id.to_string(),
1293 to_stage_id: to_stage_id.clone(),
1294 match_node: match_node.as_str().to_string(),
1295 arm_index,
1296 from_budget,
1297 to_budget,
1298 };
1299 let transition = lex_vcs::StageTransition::Replace {
1300 sig_id: sig.clone(),
1301 from: from_stage_id.to_string(),
1302 to: to_stage_id.clone(),
1303 };
1304 let op = lex_vcs::Operation::new(
1305 kind,
1306 head_now.into_iter().collect::<Vec<_>>(),
1307 );
1308 self.apply_operation_checked(branch, op, transition, &candidate)
1309 }
1310
1311 pub fn apply_rename_local(
1317 &self,
1318 branch: &str,
1319 from_stage_id: &str,
1320 let_node: &lex_ast::NodeId,
1321 new_name: &str,
1322 ) -> Result<lex_vcs::OpId, StoreError> {
1323 let from_stage = self.get_ast(from_stage_id)?;
1324 let old_name = read_let_name(&from_stage, let_node)
1328 .map_err(StoreError::TransformError)?;
1329 let new_stage = lex_ast::rename_local(&from_stage, let_node, new_name)
1330 .map_err(StoreError::TransformError)?;
1331 let sig = lex_ast::sig_id(&from_stage)
1332 .ok_or(StoreError::CannotPublishImport)?;
1333 let to_stage_id = self.publish(&new_stage)?;
1334 if to_stage_id == from_stage_id {
1335 return Err(StoreError::InvalidTransition(format!(
1336 "rename_local produced the same stage_id `{from_stage_id}`"
1337 )));
1338 }
1339 let head = self.branch_head(branch)?;
1340 let mut candidate: Vec<lex_ast::Stage> = Vec::with_capacity(head.len());
1341 for (other_sig, other_stage_id) in &head {
1342 if other_sig == &sig {
1343 candidate.push(new_stage.clone());
1344 } else {
1345 candidate.push(self.get_ast(other_stage_id)?);
1346 }
1347 }
1348 if !head.contains_key(&sig) {
1349 return Err(StoreError::InvalidTransition(format!(
1350 "sig `{sig}` not on branch `{branch}`'s head"
1351 )));
1352 }
1353 let from_budget = budget_of_stage(&from_stage);
1354 let to_budget = budget_of_stage(&new_stage);
1355 let head_now = self.get_branch(branch)?.and_then(|b| b.head_op);
1356 let kind = lex_vcs::OperationKind::RenameLocal {
1357 sig_id: sig.clone(),
1358 from_stage_id: from_stage_id.to_string(),
1359 to_stage_id: to_stage_id.clone(),
1360 let_node: let_node.as_str().to_string(),
1361 old_name,
1362 new_name: new_name.to_string(),
1363 from_budget,
1364 to_budget,
1365 };
1366 let transition = lex_vcs::StageTransition::Replace {
1367 sig_id: sig.clone(),
1368 from: from_stage_id.to_string(),
1369 to: to_stage_id.clone(),
1370 };
1371 let op = lex_vcs::Operation::new(
1372 kind,
1373 head_now.into_iter().collect::<Vec<_>>(),
1374 );
1375 self.apply_operation_checked(branch, op, transition, &candidate)
1376 }
1377
1378 pub fn apply_inline_let(
1384 &self,
1385 branch: &str,
1386 from_stage_id: &str,
1387 let_node: &lex_ast::NodeId,
1388 ) -> Result<lex_vcs::OpId, StoreError> {
1389 let from_stage = self.get_ast(from_stage_id)?;
1390 let binding_name = read_let_name(&from_stage, let_node)
1391 .map_err(StoreError::TransformError)?;
1392 let new_stage = lex_ast::inline_let(&from_stage, let_node)
1393 .map_err(StoreError::TransformError)?;
1394 let sig = lex_ast::sig_id(&from_stage)
1395 .ok_or(StoreError::CannotPublishImport)?;
1396 let to_stage_id = self.publish(&new_stage)?;
1397 if to_stage_id == from_stage_id {
1398 return Err(StoreError::InvalidTransition(format!(
1399 "inline_let produced the same stage_id `{from_stage_id}`"
1400 )));
1401 }
1402 let head = self.branch_head(branch)?;
1403 let mut candidate: Vec<lex_ast::Stage> = Vec::with_capacity(head.len());
1404 for (other_sig, other_stage_id) in &head {
1405 if other_sig == &sig {
1406 candidate.push(new_stage.clone());
1407 } else {
1408 candidate.push(self.get_ast(other_stage_id)?);
1409 }
1410 }
1411 if !head.contains_key(&sig) {
1412 return Err(StoreError::InvalidTransition(format!(
1413 "sig `{sig}` not on branch `{branch}`'s head"
1414 )));
1415 }
1416 let from_budget = budget_of_stage(&from_stage);
1417 let to_budget = budget_of_stage(&new_stage);
1418 let head_now = self.get_branch(branch)?.and_then(|b| b.head_op);
1419 let kind = lex_vcs::OperationKind::InlineLet {
1420 sig_id: sig.clone(),
1421 from_stage_id: from_stage_id.to_string(),
1422 to_stage_id: to_stage_id.clone(),
1423 let_node: let_node.as_str().to_string(),
1424 binding_name,
1425 from_budget,
1426 to_budget,
1427 };
1428 let transition = lex_vcs::StageTransition::Replace {
1429 sig_id: sig.clone(),
1430 from: from_stage_id.to_string(),
1431 to: to_stage_id.clone(),
1432 };
1433 let op = lex_vcs::Operation::new(
1434 kind,
1435 head_now.into_iter().collect::<Vec<_>>(),
1436 );
1437 self.apply_operation_checked(branch, op, transition, &candidate)
1438 }
1439
1440 pub fn apply_extract_function(
1457 &self,
1458 branch: &str,
1459 from_stage_id: &str,
1460 expr_node: &lex_ast::NodeId,
1461 spec: lex_ast::ExtractFnSpec,
1462 ) -> Result<(lex_vcs::OpId, lex_vcs::OpId), StoreError> {
1463 let from_stage = self.get_ast(from_stage_id)?;
1464 let new_fn_name = spec.name.clone();
1465 let (modified_stage, new_fn_stage) =
1466 lex_ast::extract_function(&from_stage, expr_node, spec)
1467 .map_err(StoreError::TransformError)?;
1468
1469 let source_sig = lex_ast::sig_id(&from_stage)
1470 .ok_or(StoreError::CannotPublishImport)?;
1471 let new_fn_sig = lex_ast::sig_id(&new_fn_stage)
1472 .ok_or(StoreError::CannotPublishImport)?;
1473 if source_sig == new_fn_sig {
1474 return Err(StoreError::InvalidTransition(format!(
1475 "extract_function produced a sig matching the source `{source_sig}`"
1476 )));
1477 }
1478 let new_fn_stage_id = self.publish(&new_fn_stage)?;
1479 let modified_stage_id = self.publish(&modified_stage)?;
1480 if modified_stage_id == from_stage_id {
1481 return Err(StoreError::InvalidTransition(format!(
1482 "extract_function produced the same stage_id `{from_stage_id}` for the source"
1483 )));
1484 }
1485
1486 let head = self.branch_head(branch)?;
1487 if !head.contains_key(&source_sig) {
1488 return Err(StoreError::InvalidTransition(format!(
1489 "sig `{source_sig}` not on branch `{branch}`'s head"
1490 )));
1491 }
1492
1493 let intent = lex_vcs::Intent::new(
1498 format!(
1499 "[lex.transform.extract_function]\nnew_fn={new_fn_name}\nsource_sig={source_sig}\nfrom_stage={from_stage_id}\nexpr_node={node}",
1500 node = expr_node.as_str(),
1501 ),
1502 "lex-store::apply_extract_function",
1503 lex_vcs::ModelDescriptor {
1504 provider: "lex-store".into(),
1505 name: env!("CARGO_PKG_VERSION").into(),
1506 version: None,
1507 },
1508 None,
1509 );
1510 let intent_id = intent.intent_id.clone();
1511 lex_vcs::IntentLog::open(self.root())?.put(&intent)?;
1512
1513 let new_fn_effects: std::collections::BTreeSet<String> = match &new_fn_stage {
1517 lex_ast::Stage::FnDecl(fd) => fd.effects.iter()
1518 .map(|e| e.name.clone()).collect(),
1519 _ => Default::default(),
1520 };
1521 let new_fn_budget = budget_of_stage(&new_fn_stage);
1522 let mut candidate_with_new_fn: Vec<lex_ast::Stage> =
1523 Vec::with_capacity(head.len() + 1);
1524 for stage_id in head.values() {
1525 candidate_with_new_fn.push(self.get_ast(stage_id)?);
1526 }
1527 candidate_with_new_fn.push(new_fn_stage.clone());
1528 let head_now = self.get_branch(branch)?.and_then(|b| b.head_op);
1529 let add_op = lex_vcs::Operation::new(
1530 lex_vcs::OperationKind::AddFunction {
1531 sig_id: new_fn_sig.clone(),
1532 stage_id: new_fn_stage_id.clone(),
1533 effects: new_fn_effects,
1534 budget_cost: new_fn_budget,
1535 },
1536 head_now.into_iter().collect::<Vec<_>>(),
1537 ).with_intent(intent_id.clone());
1538 let add_transition = lex_vcs::StageTransition::Create {
1539 sig_id: new_fn_sig.clone(),
1540 stage_id: new_fn_stage_id.clone(),
1541 };
1542 let add_op_id = self.apply_operation_checked(
1543 branch, add_op, add_transition, &candidate_with_new_fn,
1544 )?;
1545
1546 let from_budget = budget_of_stage(&from_stage);
1550 let to_budget = budget_of_stage(&modified_stage);
1551 let mut candidate_with_modified: Vec<lex_ast::Stage> =
1552 Vec::with_capacity(head.len() + 1);
1553 for (other_sig, other_stage_id) in &head {
1554 if other_sig == &source_sig {
1555 candidate_with_modified.push(modified_stage.clone());
1556 } else {
1557 candidate_with_modified.push(self.get_ast(other_stage_id)?);
1558 }
1559 }
1560 candidate_with_modified.push(new_fn_stage.clone());
1561 let head_now = self.get_branch(branch)?.and_then(|b| b.head_op);
1562 let modify_op = lex_vcs::Operation::new(
1563 lex_vcs::OperationKind::ModifyBody {
1564 sig_id: source_sig.clone(),
1565 from_stage_id: from_stage_id.to_string(),
1566 to_stage_id: modified_stage_id.clone(),
1567 from_budget,
1568 to_budget,
1569 },
1570 head_now.into_iter().collect::<Vec<_>>(),
1571 ).with_intent(intent_id);
1572 let modify_transition = lex_vcs::StageTransition::Replace {
1573 sig_id: source_sig,
1574 from: from_stage_id.to_string(),
1575 to: modified_stage_id,
1576 };
1577 let modify_op_id = self.apply_operation_checked(
1578 branch, modify_op, modify_transition, &candidate_with_modified,
1579 )?;
1580
1581 Ok((add_op_id, modify_op_id))
1582 }
1583
1584 pub fn propose_candidate(
1602 &self,
1603 branch: &str,
1604 new_stage: &lex_ast::Stage,
1605 intent_id: &lex_vcs::IntentId,
1606 ) -> Result<lex_vcs::OpId, StoreError> {
1607 let sig = lex_ast::sig_id(new_stage)
1608 .ok_or(StoreError::CannotPublishImport)?;
1609 let stage_id = self.publish(new_stage)?;
1610 let head_now = self.get_branch(branch)?.and_then(|b| b.head_op);
1611 let op = lex_vcs::Operation::new(
1612 lex_vcs::OperationKind::Candidate {
1613 sig_id: sig,
1614 stage_id,
1615 },
1616 head_now.into_iter().collect::<Vec<_>>(),
1617 ).with_intent(intent_id.clone());
1618 let transition = lex_vcs::StageTransition::ImportOnly;
1619 self.apply_operation(branch, op, transition)
1620 }
1621
1622 pub fn list_candidates(&self, sig_id: &str) -> Result<Vec<CandidateInfo>, StoreError> {
1628 let log = lex_vcs::OpLog::open(self.root())?;
1629 let all = log.list_all()?;
1630 let mut referenced: std::collections::BTreeSet<lex_vcs::OpId> = Default::default();
1634 for rec in &all {
1635 if let lex_vcs::OperationKind::Promote { sig_id: s, winner_candidate, supersedes, .. } = &rec.op.kind {
1636 if s != sig_id { continue; }
1637 referenced.insert(winner_candidate.clone());
1638 for sup in supersedes { referenced.insert(sup.clone()); }
1639 }
1640 }
1641 let mut out: Vec<CandidateInfo> = Vec::new();
1642 for rec in all {
1643 let lex_vcs::OperationKind::Candidate { sig_id: s, stage_id } = &rec.op.kind
1644 else { continue };
1645 if s != sig_id { continue; }
1646 if referenced.contains(&rec.op_id) { continue; }
1647 out.push(CandidateInfo {
1648 op_id: rec.op_id.clone(),
1649 stage_id: stage_id.clone(),
1650 intent_id: rec.op.intent_id.clone(),
1651 });
1652 }
1653 out.sort_by(|a, b| a.op_id.cmp(&b.op_id));
1654 Ok(out)
1655 }
1656
1657 pub fn promote_candidate(
1669 &self,
1670 branch: &str,
1671 candidate_op_id: &lex_vcs::OpId,
1672 ) -> Result<lex_vcs::OpId, StoreError> {
1673 let log = lex_vcs::OpLog::open(self.root())?;
1674 let candidate_rec = log.get(candidate_op_id)?
1675 .ok_or_else(|| StoreError::UnknownOp(candidate_op_id.clone()))?;
1676 let (sig, winner_stage_id) = match &candidate_rec.op.kind {
1677 lex_vcs::OperationKind::Candidate { sig_id, stage_id } =>
1678 (sig_id.clone(), stage_id.clone()),
1679 other => return Err(StoreError::InvalidTransition(format!(
1680 "op `{candidate_op_id}` is a `{:?}`, not a Candidate", other
1681 ))),
1682 };
1683
1684 let live = self.list_candidates(&sig)?;
1687 let mut supersedes: Vec<lex_vcs::OpId> = live.iter()
1688 .filter(|c| &c.op_id != candidate_op_id)
1689 .map(|c| c.op_id.clone())
1690 .collect();
1691 supersedes.sort();
1692
1693 let head = self.branch_head(branch)?;
1697 let winner_stage = self.get_ast(&winner_stage_id)?;
1698 let mut candidate_program: Vec<lex_ast::Stage> = Vec::with_capacity(head.len() + 1);
1699 let mut found = false;
1700 for (other_sig, other_stage_id) in &head {
1701 if other_sig == &sig {
1702 candidate_program.push(winner_stage.clone());
1703 found = true;
1704 } else {
1705 candidate_program.push(self.get_ast(other_stage_id)?);
1706 }
1707 }
1708 if !found {
1709 candidate_program.push(winner_stage.clone());
1712 }
1713 let from_stage_id = head.get(&sig).cloned();
1714 let from_budget = from_stage_id.as_deref()
1717 .and_then(|s| self.get_ast(s).ok())
1718 .and_then(|s| budget_of_stage(&s));
1719 let to_budget = budget_of_stage(&winner_stage);
1720
1721 let head_now = self.get_branch(branch)?.and_then(|b| b.head_op);
1722 let op = lex_vcs::Operation::new(
1723 lex_vcs::OperationKind::Promote {
1724 sig_id: sig.clone(),
1725 winner_candidate: candidate_op_id.clone(),
1726 winner_stage_id: winner_stage_id.clone(),
1727 supersedes,
1728 from_stage_id: from_stage_id.clone(),
1729 from_budget,
1730 to_budget,
1731 },
1732 head_now.into_iter().collect::<Vec<_>>(),
1733 );
1734 let transition = match &from_stage_id {
1735 Some(from) => lex_vcs::StageTransition::Replace {
1736 sig_id: sig,
1737 from: from.clone(),
1738 to: winner_stage_id,
1739 },
1740 None => lex_vcs::StageTransition::Create {
1741 sig_id: sig,
1742 stage_id: winner_stage_id,
1743 },
1744 };
1745 self.apply_operation_checked(branch, op, transition, &candidate_program)
1746 }
1747
1748 pub fn apply_operation(
1751 &self,
1752 branch: &str,
1753 op: lex_vcs::Operation,
1754 transition: lex_vcs::StageTransition,
1755 ) -> Result<lex_vcs::OpId, StoreError> {
1756 let attestable = attestable_stage_ids(&transition);
1757 let op_effects = op_declared_effects(&op.kind);
1758 self.cas_retry_advance(branch, op, transition, |new_head| {
1759 self.run_required_attestations_gate(
1760 branch, &new_head.op_id, &attestable, &op_effects,
1761 )
1762 })
1763 }
1764
1765 fn cas_retry_advance<F>(
1773 &self,
1774 branch: &str,
1775 op: lex_vcs::Operation,
1776 transition: lex_vcs::StageTransition,
1777 mut between_persist_and_cas: F,
1778 ) -> Result<lex_vcs::OpId, StoreError>
1779 where
1780 F: FnMut(&lex_vcs::NewHead) -> Result<(), StoreError>,
1781 {
1782 const MAX_ATTEMPTS: u32 = 32;
1786 let is_rebuildable = op.parents.len() <= 1;
1791 let kind = op.kind.clone();
1792 let intent_id = op.intent_id.clone();
1793
1794 let mut last_io_err: Option<StoreError> = None;
1795 let mut current_op = op;
1796 let current_transition = transition;
1797 let mut rebuilt_already = false;
1810 for attempt in 1..=MAX_ATTEMPTS {
1811 let parent = self
1814 .get_branch(branch)?
1815 .and_then(|b| b.head_op);
1816
1817 let should_rebuild = is_rebuildable
1826 && (rebuilt_already
1827 || (current_op.parents.is_empty() && parent.is_some()));
1828 if should_rebuild {
1829 current_op = lex_vcs::Operation {
1830 kind: kind.clone(),
1831 parents: parent.iter().cloned().collect(),
1832 intent_id: intent_id.clone(),
1833 };
1834 }
1835
1836 let new_head = match self.persist_op_only_with_parent(
1842 branch,
1843 parent.as_ref(),
1844 current_op.clone(),
1845 current_transition.clone(),
1846 ) {
1847 Ok(nh) => nh,
1848 Err(StoreError::Apply(lex_vcs::ApplyError::StaleParent { .. }))
1849 if is_rebuildable && rebuilt_already =>
1850 {
1851 rebuilt_already = true;
1852 continue;
1853 }
1854 Err(e) => return Err(e),
1855 };
1856
1857 between_persist_and_cas(&new_head)?;
1862
1863 match self.set_branch_head_op_cas(branch, parent, new_head.op_id.clone()) {
1866 Ok(()) => return Ok(new_head.op_id),
1867 Err(crate::branches::CasFailed::Mismatch { .. }) if is_rebuildable => {
1868 rebuilt_already = true;
1870 continue;
1871 }
1872 Err(crate::branches::CasFailed::Mismatch { .. }) => {
1873 let _ = attempt;
1876 return Err(StoreError::Contention {
1877 branch: branch.into(),
1878 attempts: 1,
1879 });
1880 }
1881 Err(crate::branches::CasFailed::UnknownBranch(b)) => {
1882 return Err(StoreError::UnknownBranch(b));
1883 }
1884 Err(crate::branches::CasFailed::Io(e)) => {
1885 last_io_err = Some(StoreError::Io(std::io::Error::other(e)));
1886 continue;
1887 }
1888 }
1889 }
1890 match last_io_err {
1893 Some(e) => Err(e),
1894 None => Err(StoreError::Contention {
1895 branch: branch.into(),
1896 attempts: MAX_ATTEMPTS,
1897 }),
1898 }
1899 }
1900
1901 fn persist_op_only_with_parent(
1907 &self,
1908 branch: &str,
1909 parent: Option<&lex_vcs::OpId>,
1910 op: lex_vcs::Operation,
1911 transition: lex_vcs::StageTransition,
1912 ) -> Result<lex_vcs::NewHead, StoreError> {
1913 if branch != DEFAULT_BRANCH && self.get_branch(branch)?.is_none() {
1914 return Err(StoreError::UnknownBranch(branch.into()));
1915 }
1916 let log = lex_vcs::OpLog::open(self.root())?;
1917 lex_vcs::apply(&log, parent, op, transition).map_err(|e| match e {
1918 lex_vcs::ApplyError::Persist(io) => StoreError::Io(io),
1919 other => StoreError::Apply(other),
1920 })
1921 }
1922
1923
1924 fn run_required_attestations_gate(
1943 &self,
1944 branch: &str,
1945 op_id: &lex_vcs::OpId,
1946 stage_ids: &[String],
1947 op_effects: &std::collections::BTreeSet<String>,
1948 ) -> Result<(), StoreError> {
1949 let new_op_candidate: Vec<(lex_vcs::OpId, Option<String>, std::collections::BTreeSet<String>)> =
1953 if stage_ids.is_empty() {
1954 vec![(op_id.clone(), None, op_effects.clone())]
1955 } else {
1956 stage_ids
1957 .iter()
1958 .map(|sid| (op_id.clone(), Some(sid.clone()), op_effects.clone()))
1959 .collect()
1960 };
1961 let attest_log = self.attestation_log()?;
1962
1963 let walk_back_candidate = self.collect_ancestor_candidates(branch)?;
1979 let mut producer_block_candidate = walk_back_candidate;
1980 producer_block_candidate.extend(new_op_candidate.iter().cloned());
1981 crate::policy::check_producer_block(&attest_log, &producer_block_candidate)
1982 .map_err(StoreError::ProducerBlocked)?;
1983
1984 let policy = match crate::policy::load(self.root())? {
1989 Some(p) if !p.required_attestations.is_empty() => p,
1990 _ => return Ok(()),
1991 };
1992 let waivers = crate::policy::check_required_attestations(
1993 &attest_log, &new_op_candidate, &policy,
1994 ).map_err(StoreError::BranchAdvanceBlocked)?;
1995 for w in waivers {
2000 let att = lex_vcs::Attestation::new(
2001 w.stage_id,
2002 Some(op_id.clone()),
2003 None,
2004 lex_vcs::AttestationKind::TrustWaived {
2005 producer: w.producer,
2006 score_thousandths: w.score_thousandths,
2007 threshold_thousandths: w.threshold_thousandths,
2008 kind_tag: w.kind_tag,
2009 },
2010 lex_vcs::AttestationResult::Passed,
2011 trust_waived_producer(),
2012 None,
2013 );
2014 attest_log.put(&att)?;
2015 }
2016 Ok(())
2017 }
2018
2019 fn collect_ancestor_candidates(
2025 &self,
2026 branch: &str,
2027 ) -> Result<Vec<GateCandidate>, StoreError> {
2028 let b = match self.get_branch(branch)? {
2029 Some(b) => b,
2030 None => return Ok(Vec::new()),
2031 };
2032 let Some(head) = b.head_op else { return Ok(Vec::new()); };
2033 if Some(&head) == b.last_gate_checkpoint.as_ref() {
2034 return Ok(Vec::new());
2037 }
2038
2039 let log = lex_vcs::OpLog::open(self.root())?;
2040 let walk = log.walk_back(&head, None)?;
2041 let stop_at = b.last_gate_checkpoint.clone();
2042 let mut out = Vec::new();
2043 for rec in walk {
2044 if Some(&rec.op_id) == stop_at.as_ref() {
2045 break;
2046 }
2047 let stages = attestable_stage_ids(&rec.produces);
2048 let effects = op_declared_effects(&rec.op.kind);
2049 if stages.is_empty() {
2050 out.push((rec.op_id.clone(), None, effects));
2051 } else {
2052 for sid in stages {
2053 out.push((rec.op_id.clone(), Some(sid), effects.clone()));
2054 }
2055 }
2056 }
2057 Ok(out)
2058 }
2059}
2060
2061fn stage_name(stage: &Stage) -> &str {
2062 match stage {
2063 Stage::FnDecl(fd) => &fd.name,
2064 Stage::TypeDecl(td) => &td.name,
2065 Stage::Import(i) => &i.alias,
2066 }
2067}
2068
2069fn stage_for_kind<'a>(
2070 kind: &lex_vcs::OperationKind,
2071 stages: &'a [lex_ast::Stage],
2072) -> Option<&'a lex_ast::Stage> {
2073 use lex_vcs::OperationKind::*;
2074 let target_sig = match kind {
2075 AddFunction { sig_id, .. } | ModifyBody { sig_id, .. }
2076 | ChangeEffectSig { sig_id, .. } | AddType { sig_id, .. }
2077 | ModifyType { sig_id, .. } => Some(sig_id.clone()),
2078 RenameSymbol { to, .. } => Some(to.clone()),
2079 _ => None,
2080 };
2081 let target_sig = target_sig?;
2082 stages.iter().find(|s| sig_id(s).as_deref() == Some(target_sig.as_str()))
2083}
2084
2085fn transition_for_kind(kind: &lex_vcs::OperationKind) -> lex_vcs::StageTransition {
2086 use lex_vcs::OperationKind::*;
2087 use lex_vcs::StageTransition;
2088 match kind {
2089 AddFunction { sig_id, stage_id, .. }
2090 | AddType { sig_id, stage_id } => StageTransition::Create {
2091 sig_id: sig_id.clone(), stage_id: stage_id.clone(),
2092 },
2093 RemoveFunction { sig_id, last_stage_id }
2094 | RemoveType { sig_id, last_stage_id } => StageTransition::Remove {
2095 sig_id: sig_id.clone(), last: last_stage_id.clone(),
2096 },
2097 ModifyBody { sig_id, from_stage_id, to_stage_id, .. }
2098 | ChangeEffectSig { sig_id, from_stage_id, to_stage_id, .. }
2099 | ModifyType { sig_id, from_stage_id, to_stage_id }
2100 | ReplaceMatchArm { sig_id, from_stage_id, to_stage_id, .. }
2101 | RenameLocal { sig_id, from_stage_id, to_stage_id, .. }
2102 | InlineLet { sig_id, from_stage_id, to_stage_id, .. } => StageTransition::Replace {
2103 sig_id: sig_id.clone(),
2104 from: from_stage_id.clone(),
2105 to: to_stage_id.clone(),
2106 },
2107 RenameSymbol { from, to, body_stage_id } => StageTransition::Rename {
2108 from: from.clone(), to: to.clone(),
2109 body_stage_id: body_stage_id.clone(),
2110 },
2111 AddImport { .. } | RemoveImport { .. } => StageTransition::ImportOnly,
2112 Merge { .. } => StageTransition::Merge { entries: Default::default() },
2113 Candidate { .. } => StageTransition::ImportOnly,
2118 Promote { sig_id, winner_stage_id, from_stage_id, .. } => match from_stage_id {
2122 Some(from) => StageTransition::Replace {
2123 sig_id: sig_id.clone(),
2124 from: from.clone(),
2125 to: winner_stage_id.clone(),
2126 },
2127 None => StageTransition::Create {
2128 sig_id: sig_id.clone(),
2129 stage_id: winner_stage_id.clone(),
2130 },
2131 },
2132 }
2133}
2134
2135fn typecheck_producer() -> lex_vcs::ProducerDescriptor {
2140 lex_vcs::ProducerDescriptor {
2141 tool: "lex-store".into(),
2142 version: env!("CARGO_PKG_VERSION").into(),
2143 model: None,
2144 }
2145}
2146
2147fn repair_hint_producer() -> lex_vcs::ProducerDescriptor {
2152 lex_vcs::ProducerDescriptor {
2153 tool: "lex-store::repair_hint".into(),
2154 version: env!("CARGO_PKG_VERSION").into(),
2155 model: None,
2156 }
2157}
2158
2159fn trust_waived_producer() -> lex_vcs::ProducerDescriptor {
2165 lex_vcs::ProducerDescriptor {
2166 tool: "lex-store::trust_waived".into(),
2167 version: env!("CARGO_PKG_VERSION").into(),
2168 model: None,
2169 }
2170}
2171
2172fn producer_trust_producer() -> lex_vcs::ProducerDescriptor {
2177 lex_vcs::ProducerDescriptor {
2178 tool: "lex-store::producer_trust".into(),
2179 version: env!("CARGO_PKG_VERSION").into(),
2180 model: None,
2181 }
2182}
2183
2184type GateCandidate = (lex_vcs::OpId, Option<String>, std::collections::BTreeSet<String>);
2195
2196fn op_declared_effects(kind: &lex_vcs::OperationKind) -> std::collections::BTreeSet<String> {
2207 use lex_vcs::OperationKind::*;
2208 match kind {
2209 AddFunction { effects, .. } => effects.clone(),
2210 ChangeEffectSig { to_effects, .. } => to_effects.clone(),
2211 _ => std::collections::BTreeSet::new(),
2212 }
2213}
2214
2215fn attestable_stage_ids(transition: &lex_vcs::StageTransition) -> Vec<String> {
2216 use lex_vcs::StageTransition::*;
2217 match transition {
2218 Create { stage_id, .. } => vec![stage_id.clone()],
2219 Replace { to, .. } => vec![to.clone()],
2220 Rename { body_stage_id, .. } => vec![body_stage_id.clone()],
2221 Merge { entries } => entries
2222 .values()
2223 .filter_map(|opt| opt.clone())
2224 .collect(),
2225 Remove { .. } | ImportOnly => Vec::new(),
2226 }
2227}
2228
2229fn write_canonical_json<T: Serialize>(path: &Path, value: &T) -> Result<(), StoreError> {
2230 let v = serde_json::to_value(value)?;
2231 let s = lex_ast::canon_json::to_canonical_string(&v);
2232 if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; }
2233 fs::write(path, s)?;
2234 Ok(())
2235}
2236
2237fn read_let_name(
2243 stage: &Stage,
2244 let_node: &lex_ast::NodeId,
2245) -> Result<String, lex_ast::TransformError> {
2246 let probed = lex_ast::rename_local(stage, let_node, "__lex_rename_probe__")?;
2256 let Stage::FnDecl(fd) = probed else {
2257 return Err(lex_ast::TransformError::NonFnTarget { stage_kind: "non-FnDecl" });
2258 };
2259 let Stage::FnDecl(orig_fd) = stage else {
2263 return Err(lex_ast::TransformError::NonFnTarget { stage_kind: "non-FnDecl" });
2264 };
2265 let path = parse_let_node_path(let_node.as_str())?;
2267 if path.is_empty() {
2268 return Err(lex_ast::TransformError::NotALet {
2269 at: let_node.as_str().into(),
2270 found_kind: "stage_root",
2271 });
2272 }
2273 if path[0] != orig_fd.params.len() + 1 {
2274 return Err(lex_ast::TransformError::UnknownNode {
2275 at: let_node.as_str().into(),
2276 });
2277 }
2278 let inner = &path[1..];
2279 let target = navigate_to_let(&orig_fd.body, inner, let_node.as_str())?;
2280 let _ = fd; Ok(target.to_string())
2282}
2283
2284fn parse_let_node_path(id: &str) -> Result<Vec<usize>, lex_ast::TransformError> {
2285 let s = id.strip_prefix("n_")
2286 .ok_or_else(|| lex_ast::TransformError::BadNodeId(id.into()))?;
2287 let mut parts = s.split('.');
2288 let head = parts.next()
2289 .ok_or_else(|| lex_ast::TransformError::BadNodeId(id.into()))?;
2290 if head != "0" { return Err(lex_ast::TransformError::BadNodeId(id.into())); }
2291 let mut out = Vec::new();
2292 for p in parts {
2293 out.push(p.parse::<usize>()
2294 .map_err(|_| lex_ast::TransformError::BadNodeId(id.into()))?);
2295 }
2296 Ok(out)
2297}
2298
2299fn navigate_to_let<'a>(
2300 root: &'a lex_ast::CExpr,
2301 path: &[usize],
2302 at: &str,
2303) -> Result<&'a str, lex_ast::TransformError> {
2304 use lex_ast::CExpr::*;
2305 let mut current = root;
2306 for &idx in path {
2307 current = match current {
2308 Call { callee, args } => {
2309 if idx == 0 { callee } else { args.get(idx - 1)
2310 .ok_or_else(|| lex_ast::TransformError::UnknownNode { at: at.into() })? }
2311 }
2312 Let { value, body, .. } => match idx {
2313 0 => value, 1 => body,
2314 _ => return Err(lex_ast::TransformError::UnknownNode { at: at.into() }),
2315 },
2316 Match { scrutinee, arms } => {
2317 if idx == 0 { scrutinee } else {
2318 let arm_off = idx - 1;
2319 if arm_off % 2 != 1 {
2320 return Err(lex_ast::TransformError::UnknownNode { at: at.into() });
2321 }
2322 let arm_index = arm_off / 2;
2323 &arms.get(arm_index)
2324 .ok_or_else(|| lex_ast::TransformError::UnknownNode { at: at.into() })?
2325 .body
2326 }
2327 }
2328 Block { statements, result } => {
2329 if idx < statements.len() { &statements[idx] }
2330 else if idx == statements.len() { result }
2331 else { return Err(lex_ast::TransformError::UnknownNode { at: at.into() }); }
2332 }
2333 Constructor { args, .. } | TupleLit { items: args, .. }
2334 | ListLit { items: args, .. } => args.get(idx)
2335 .ok_or_else(|| lex_ast::TransformError::UnknownNode { at: at.into() })?,
2336 RecordLit { fields } => &fields.get(idx)
2337 .ok_or_else(|| lex_ast::TransformError::UnknownNode { at: at.into() })?
2338 .value,
2339 FieldAccess { value, .. } if idx == 0 => value,
2340 Lambda { body, .. } if idx == 0 => body,
2341 BinOp { lhs, rhs, .. } => match idx {
2342 0 => lhs, 1 => rhs,
2343 _ => return Err(lex_ast::TransformError::UnknownNode { at: at.into() }),
2344 },
2345 UnaryOp { expr, .. } if idx == 0 => expr,
2346 Return { value } if idx == 0 => value,
2347 _ => return Err(lex_ast::TransformError::UnknownNode { at: at.into() }),
2348 };
2349 }
2350 let Let { name, .. } = current else {
2351 return Err(lex_ast::TransformError::NotALet {
2352 at: at.into(),
2353 found_kind: lex_cexpr_kind(current),
2354 });
2355 };
2356 Ok(name)
2357}
2358
2359fn lex_cexpr_kind(e: &lex_ast::CExpr) -> &'static str {
2360 use lex_ast::CExpr::*;
2361 match e {
2362 Literal { .. } => "Literal", Var { .. } => "Var",
2363 Call { .. } => "Call", Let { .. } => "Let",
2364 Match { .. } => "Match", Block { .. } => "Block",
2365 Constructor { .. } => "Constructor", RecordLit { .. } => "RecordLit",
2366 TupleLit { .. } => "TupleLit", ListLit { .. } => "ListLit",
2367 FieldAccess { .. } => "FieldAccess", Lambda { .. } => "Lambda",
2368 BinOp { .. } => "BinOp", UnaryOp { .. } => "UnaryOp",
2369 Return { .. } => "Return",
2370 }
2371}
2372
2373fn budget_of_stage(stage: &Stage) -> Option<u64> {
2378 let fd = match stage {
2379 Stage::FnDecl(fd) => fd,
2380 _ => return None,
2381 };
2382 let mut min_cost: Option<u64> = None;
2383 for eff in &fd.effects {
2384 if eff.name != "budget" { continue }
2385 if let Some(lex_ast::EffectArg::Int { value }) = &eff.arg {
2386 let n = *value as u64;
2387 min_cost = Some(min_cost.map(|c| c.min(n)).unwrap_or(n));
2388 }
2389 }
2390 min_cost
2391}
2392
2393fn canonical_bytes(stage: &Stage) -> Result<Vec<u8>, StoreError> {
2398 let v = serde_json::to_value(stage)?;
2399 Ok(lex_ast::canon_json::to_canonical_string(&v).into_bytes())
2400}
2401
2402#[allow(dead_code)]
2403fn read_json<T: DeserializeOwned>(path: &Path) -> Result<T, StoreError> {
2404 let bytes = fs::read(path)?;
2405 Ok(serde_json::from_slice(&bytes)?)
2406}