pub struct Store { /* private fields */ }Implementations§
Source§impl Store
impl Store
Sourcepub fn session_budget(
&self,
session_id: &str,
) -> Result<SessionBudget, StoreError>
pub fn session_budget( &self, session_id: &str, ) -> Result<SessionBudget, StoreError>
Compute the budget spent by the given session_id across
every op currently reachable from any branch head. Returns
(spent: 0, op_count: 0) for unknown sessions, with cap
populated from policy.session_budgets.
Sourcepub fn session_budget_cap(
&self,
session_id: &str,
) -> Result<Option<u64>, StoreError>
pub fn session_budget_cap( &self, session_id: &str, ) -> Result<Option<u64>, StoreError>
Resolve the budget cap configured for session_id from
policy.json’s session_budgets (#292 slice 2). Returns
None when no enforcement is configured.
Sourcepub fn all_session_budgets(&self) -> Result<Vec<SessionBudget>, StoreError>
pub fn all_session_budgets(&self) -> Result<Vec<SessionBudget>, StoreError>
Compute per-session budget rollups across every branch.
Returns one entry per distinct session that contributed at
least one budget-bearing op. Sorted by session_id so the
output is deterministic.
Source§impl Store
impl Store
Sourcepub fn open(root: impl AsRef<Path>) -> Result<Self, StoreError>
pub fn open(root: impl AsRef<Path>) -> Result<Self, StoreError>
Open or create a store rooted at root.
Sourcepub fn rebuild_stage_index(&self) -> Result<usize, StoreError>
pub fn rebuild_stage_index(&self) -> Result<usize, StoreError>
Build (or top up) the reverse index in one pass over every
SigId in the store, rather than relying on lookup_lifecycle
to discover entries one at a time. Safe to call at any time,
including on a partially-built index (e.g. one left behind by
an interrupted request that was populating it lazily): already-
indexed stage_ids are skipped, so this only does the work that
remains. Returns the number of newly-added entries.
pub fn root(&self) -> &Path
Sourcepub fn put_blob(&self, content: &str) -> Result<String, StoreError>
pub fn put_blob(&self, content: &str) -> Result<String, StoreError>
Content-address content and persist it under <root>/blobs/<sha>.
Returns the sha. Idempotent: re-putting identical content is a no-op.
Concurrency-safe — writes to a unique temp file then atomically renames
onto the content-addressed path, so parallel writers of the same content
can’t corrupt it.
Sourcepub fn get_blob(&self, sha: &str) -> Result<String, StoreError>
pub fn get_blob(&self, sha: &str) -> Result<String, StoreError>
Read a blob by its sha. UnknownBlob if absent.
Sourcepub fn set_blob_ref(
&self,
namespace: &str,
key: &str,
sha: &str,
) -> Result<(), StoreError>
pub fn set_blob_ref( &self, namespace: &str, key: &str, sha: &str, ) -> Result<(), StoreError>
Bind key to a blob sha within namespace (e.g. namespace
"loom/sprint-abc", key "build-node"). Overwrites an existing
binding. The namespace may contain /; neither namespace nor key may
contain a .. path component.
Sourcepub fn get_blob_ref(
&self,
namespace: &str,
key: &str,
) -> Result<String, StoreError>
pub fn get_blob_ref( &self, namespace: &str, key: &str, ) -> Result<String, StoreError>
Resolve namespace/key to a blob sha. UnknownBlobRef if unbound.
Sourcepub fn list_blob_refs(
&self,
namespace: &str,
) -> Result<BTreeMap<String, String>, StoreError>
pub fn list_blob_refs( &self, namespace: &str, ) -> Result<BTreeMap<String, String>, StoreError>
All key → sha bindings in a namespace (e.g. every artifact in a
sprint). Empty map if the namespace has no bindings yet.
Sourcepub fn publish(&self, stage: &Stage) -> Result<String, StoreError>
pub fn publish(&self, stage: &Stage) -> Result<String, StoreError>
Publish a stage as Draft. Returns the StageId. Idempotent: republishing the same canonical AST returns the same StageId without writing duplicates.
Sourcepub fn publish_signed(
&self,
stage: &Stage,
signer: Option<&Keypair>,
) -> Result<String, StoreError>
pub fn publish_signed( &self, stage: &Stage, signer: Option<&Keypair>, ) -> Result<String, StoreError>
Like Self::publish but optionally attaches an Ed25519
signature over the StageId (#227). When signer is Some,
the persisted metadata gets a signature field that
downstream consumers can verify via
lex_vcs::verify_stage_id.
Idempotency: if a metadata file already exists the signature is not re-written. This preserves “republishing is a no-op” even across different signers — promoting a signed stage requires a fresh stage hash anyway, so a metadata overwrite would be the wrong primitive.
pub fn activate(&self, stage_id: &str) -> Result<(), StoreError>
pub fn deprecate( &self, stage_id: &str, reason: impl Into<String>, ) -> Result<(), StoreError>
pub fn tombstone(&self, stage_id: &str) -> Result<(), StoreError>
Sourcepub fn resolve_sig(&self, sig: &str) -> Result<Option<String>, StoreError>
pub fn resolve_sig(&self, sig: &str) -> Result<Option<String>, StoreError>
The current Active StageId for a signature, or None.
Sourcepub fn sig_history(
&self,
sig: &str,
) -> Result<Vec<StageHistoryEntry>, StoreError>
pub fn sig_history( &self, sig: &str, ) -> Result<Vec<StageHistoryEntry>, StoreError>
Per-stage history for a SigId, ordered chronologically by
the last transition timestamp. Returns one entry per
distinct StageId that has ever been published under sig.
Ok(vec![]) if the SigId doesn’t exist in the store.
Used by lex blame to render “where does this fn come from”.
pub fn get_ast(&self, stage_id: &str) -> Result<Stage, StoreError>
Sourcepub fn get_asts_for_sigs_bulk(
&self,
pairs: &[(String, String)],
) -> Vec<Result<Stage, StoreError>>
pub fn get_asts_for_sigs_bulk( &self, pairs: &[(String, String)], ) -> Vec<Result<Stage, StoreError>>
Bulk AST fetch for callers that already know each stage’s signature — a branch head map, for instance, which is keyed by SigId and whose values are the StageIds it points at.
Prefer this over Self::get_asts_bulk whenever the SigId is in
hand, because resolving a StageId back to a SigId is not
reliable: a StageId hashes the structural signature plus the
implementation, deliberately not the name
(docs/INVARIANTS.md), so two functions that differ only in name
share one StageId while having two distinct SigIds — and two
separate ASTs, one under each sig directory. stage_index maps
each StageId to a single sig, so get_ast/get_asts_bulk return
whichever of those ASTs the index happens to name, i.e. the wrong
name half the time (#826). Reading straight from the sig the
caller already knows removes the ambiguity — and skips loading
the index at all.
Returns results in the same order as pairs, Err for anything
that fails to resolve (mirroring get_ast’s error semantics).
Sourcepub fn get_asts_bulk(
&self,
stage_ids: &[String],
) -> Vec<Result<Stage, StoreError>>
pub fn get_asts_bulk( &self, stage_ids: &[String], ) -> Vec<Result<Stage, StoreError>>
Bulk variant of Self::get_ast for callers resolving many
stage_ids at once (e.g. pkg_publish_handler’s old_head
scan over every live function in a tenant, once per publish
request). get_ast in a loop calls lookup_lifecycle once
per stage_id, and lookup_lifecycle’s index-hit path reads
and re-parses the entire stage_index.jsonl on every single
call — fine for one call, but O(index size × N) for N calls in
a row, which dominates once the index itself is large (#825’s
follow-up: still correct and far better than the pre-index
full-tenant-scan-per-call behavior, but the per-call reparse
is itself a real, measured cost — 87.6s for 3,664 calls against
a ~14k-line index on the alpibrusl tenant).
This loads the index once for the whole batch and keeps it in
memory across all stage_ids, only touching disk again to
append genuinely new entries (a positive backfill or a
negative “not found anywhere” cache, same as the single-call
path) — never to re-read what’s already loaded.
Returns results in the same order as stage_ids, Err for
anything that fails to resolve (mirroring get_ast’s error
semantics per call).
pub fn get_metadata(&self, stage_id: &str) -> Result<Metadata, StoreError>
pub fn get_status(&self, stage_id: &str) -> Result<StageStatus, StoreError>
pub fn list_stages_by_name(&self, name: &str) -> Result<Vec<String>, StoreError>
pub fn list_sigs(&self) -> Result<Vec<String>, StoreError>
pub fn attach_test(&self, sig: &str, test: &Test) -> Result<String, StoreError>
pub fn list_tests(&self, sig: &str) -> Result<Vec<Test>, StoreError>
pub fn attach_spec(&self, sig: &str, spec: &Spec) -> Result<String, StoreError>
pub fn list_specs(&self, sig: &str) -> Result<Vec<Spec>, StoreError>
pub fn save_trace(&self, tree: &TraceTree) -> Result<String, StoreError>
pub fn load_trace(&self, run_id: &str) -> Result<TraceTree, StoreError>
pub fn list_traces(&self) -> Result<Vec<String>, StoreError>
Sourcepub fn publish_program(
&self,
branch: &str,
stages: &[Stage],
diff: &DiffReport,
new_imports: &ImportMap,
activate: bool,
) -> Result<PublishOutcome, StoreError>
pub fn publish_program( &self, branch: &str, stages: &[Stage], diff: &DiffReport, new_imports: &ImportMap, activate: bool, ) -> Result<PublishOutcome, StoreError>
Apply a published program to a branch as a sequence of typed
operations. Returns the ordered list of op_ids + the new
head_op. The caller (lex publish CLI, lex serve’s HTTP
handler) is responsible for computing the DiffReport against
the current branch head — the diff infrastructure lives in
lex-vcs::compute_diff (previously lex-cli) to keep this
layer from owning diffing logic.
On success: every op in the returned list is durable in the
op log and the branch’s head_op points at the last one.
On a no-op (no diff): returns empty ops and the existing
head_op unchanged.
Sourcepub fn publish_program_signed(
&self,
branch: &str,
stages: &[Stage],
diff: &DiffReport,
new_imports: &ImportMap,
activate: bool,
signer: Option<&Keypair>,
) -> Result<PublishOutcome, StoreError>
pub fn publish_program_signed( &self, branch: &str, stages: &[Stage], diff: &DiffReport, new_imports: &ImportMap, activate: bool, signer: Option<&Keypair>, ) -> Result<PublishOutcome, StoreError>
Signed variant of Self::publish_program (#227). Every
stage written under this batch gets the same signer; per-stage
keys aren’t supported because the agent identity model treats
a publish as a single authorial act.
pub fn derive_imports_from_oplog( &self, branch: &str, ) -> Result<ImportMap, StoreError>
Sourcepub fn apply_operation_checked(
&self,
branch: &str,
op: Operation,
transition: StageTransition,
candidate: &[Stage],
) -> Result<OpId, StoreError>
pub fn apply_operation_checked( &self, branch: &str, op: Operation, transition: StageTransition, candidate: &[Stage], ) -> Result<OpId, StoreError>
Apply an operation to a branch and advance its head_op.
The single advance path. Validates parents via lex_vcs::apply,
persists the operation via the op log, then atomically advances
the branch file’s head_op via set_branch_head_op.
Errors:
UnknownBranch: branch does not exist (no op is persisted).Apply(ApplyError::StaleParent): the op’s parents don’t match the branch head — head is unchanged. Callers that want retry-on-stale (e.g.lex publishre-running against a moved head) match on this variant explicitly.Apply(ApplyError::UnknownMergeParent): a merge op’s second parent isn’t in the log.Io: filesystem error during persist or branch advance.
Crash recovery: between op persist and branch advance, a crash
can leave an orphan op record in the log with no branch
pointing at it. The op is content-addressed and cheap to
re-derive from the same source. See
Apply a single op against branch, gated on the candidate
program typechecking. The per-op variant of #130’s
write-time gate — counterpart to Self::publish_program’s
batch-mode check.
candidate is the sequence of Stages that would exist
on this branch after the op is applied. Caller’s
responsibility: today neither lex-store nor lex-vcs
reconstruct the candidate from the op + branch state on
behalf of the caller. The natural callers (HTTP POST /v1/publish for a single op; agent harnesses driving
merges via the future #134 API) already have the candidate
in memory.
On rejection: branch head unchanged, no op record persisted. Same atomicity guarantee as the publish path.
§Why a separate method, not a flag on apply_operation
apply_operation accepting Option<&[Stage]> and silently
skipping the gate on None is exactly the kind of
“secretly opt-out” path #130 is trying to remove. The honest
split: apply_operation for the one caller that already
typechecked its input up front (publish_program),
apply_operation_checked for callers holding the candidate,
Self::apply_operation_gated for single-parent callers
that hold only the transition (/v1/patch), and
Self::apply_merge_op_gated for merge commits (#833).
Sourcepub fn candidate_program_for(
&self,
branch: &str,
transition: &StageTransition,
) -> Result<Vec<Stage>, StoreError>
pub fn candidate_program_for( &self, branch: &str, transition: &StageTransition, ) -> Result<Vec<Stage>, StoreError>
The program that would exist on branch after transition
is applied: the branch head (snapshot-cached) with the
transition replayed over it, every resulting (sig, stage)
bulk-loaded. Exact for a single-parent transition — the
candidate Self::apply_operation_gated wants. Not valid for
a merge: a StageTransition::Merge records only the delta
relative to dst, while the op-DAG replay that computes a
merge’s real head walks both parents (#833).
Sourcepub fn apply_operation_gated(
&self,
branch: &str,
op: Operation,
transition: StageTransition,
) -> Result<OpId, StoreError>
pub fn apply_operation_gated( &self, branch: &str, op: Operation, transition: StageTransition, ) -> Result<OpId, StoreError>
Self::apply_operation_checked for a single-parent op
where the caller holds only the transition: assembles the
candidate via Self::candidate_program_for and runs the
gate. Same rejection semantics — TypeError, a RepairHint
attestation, head unchanged, nothing persisted. This is the
write path for /v1/patch (#833). Merge ops must not use it
(see candidate_program_for); they go through
Self::apply_merge_op_gated.
Sourcepub fn apply_merge_op_gated(
&self,
branch: &str,
op: Operation,
transition: StageTransition,
) -> Result<OpId, StoreError>
pub fn apply_merge_op_gated( &self, branch: &str, op: Operation, transition: StageTransition, ) -> Result<OpId, StoreError>
The gated write path for merge commits (commit_merge,
POST /v1/merge/<id>/commit, lex merge commit).
A StageTransition::Merge records only the delta relative to
dst; the sig->stage map every consumer reads is recomputed by
replaying the op DAG, which for a merge walks both parents
and can surface sigs the delta never mentions. So the only way
to know the true post-merge program is to replay it — land the
op and read branch_head. This lands the merge op,
type-checks the resulting head, and on a failure rolls the
head back and returns TypeError.
Before #833 the merge paths landed through the ungated
apply_operation, so a merge whose result didn’t compose
(e.g. dst still calls helper, an agent-supplied resolution
dropped it) advanced the head with nothing to catch it.
Rollback leaves the rejected merge op as an unreachable record
(reclaimed by lex op gc, the same orphan crash-recovery
already tolerates). A stage the merge names that was never
published surfaces as the underlying StoreError from the
bulk read — the “never advance onto content that can’t be
loaded” invariant from the other side.
Sourcepub fn typecheck_merge_projection(
&self,
branch: &str,
delta: &BTreeMap<String, Option<String>>,
) -> Result<(), StoreError>
pub fn typecheck_merge_projection( &self, branch: &str, delta: &BTreeMap<String, Option<String>>, ) -> Result<(), StoreError>
Type-check the program that would result from overlaying a merge
delta onto branch’s current head — without moving the
head (#834). delta maps sig_id -> Some(stage) to set that
sig to stage, or sig_id -> None to remove it, exactly the
entries a StageTransition::Merge records.
This is the read-only, resolve-time counterpart of
apply_merge_op_gated’s commit-time gate: it lets a merge
session tell an agent which resolution broke type-checking the
moment it is submitted, instead of only after a failed commit.
Ok(()) means the projected program composes; a type failure is
Err(StoreError::TypeError(..)); a read failure is the
corresponding StoreError I/O variant.
Sourcepub fn try_semantic_body_merge(
&self,
dst_branch: &str,
sig_id: &str,
base: &str,
ours: &str,
theirs: &str,
) -> Result<Option<String>, StoreError>
pub fn try_semantic_body_merge( &self, dst_branch: &str, sig_id: &str, base: &str, ours: &str, theirs: &str, ) -> Result<Option<String>, StoreError>
#838: attempt a typed three-way merge of a single sig’s body for
a ModifyModify conflict — the intra-function, better-than-git
case where two agents edited disjoint subtrees of the same
function (different match arms, different let bindings).
base / ours (the dst side) / theirs (the src side) are the
three stage ids the merge engine surfaced for sig_id. Loads
the three FnDecls, structurally merges the bodies
(lex_vcs::merge_bodies), and accepts the result only if the
merged function also type-checks against dst_branch’s head — a
body that composes syntactically but not by type is still a
conflict (#838). On success the merged stage is published
(content-addressed, idempotent; orphaned and GC-reclaimable if
the merge is never committed) and its id returned; None means
“fall back to a whole-function conflict.”
Deliberately narrow for this slice: only pure body divergence is
merged. If the two sides disagree on anything but the body
(examples, type params — the signature is identical by
construction, since all three share sig_id), or either stage
isn’t a function, it falls back to a conflict.
Sourcepub fn recompute_producer_trust(
&self,
tool_id: &str,
window: usize,
granted_by: &str,
) -> Result<Option<AttestationId>, StoreError>
pub fn recompute_producer_trust( &self, tool_id: &str, window: usize, granted_by: &str, ) -> Result<Option<AttestationId>, StoreError>
Open the attestation log rooted at this store. The log lives
under <root>/attestations/; opening is idempotent and cheap
(fs::create_dir_all). Exposed publicly so consumers — lex blame --with-evidence, GET /v1/stage/<id>/attestations —
can read what the store gate emitted without round-tripping
through this crate’s API surface.
Recompute a producer’s trust score from its recent
attestation history and emit a fresh ProducerTrust
attestation (#293). Score = `passed / (passed + failed
- inconclusive)
over the lastwindowattestations produced bytool_id, expressed in thousandths (0..=1000`).
Refuses to grant trust when the tool has an active
ProducerBlock — the block wins as a hard veto. Returns
Ok(None) for “no attestations to score” (a brand-new
producer); the caller can choose how to handle it
(typically: skip the publish until evidence accrues).
granted_by is the identity of the actor running the
recompute (typically the human admin, or “lex-ci-bot”
for an automated nightly).
Sourcepub fn live_producer_trust_scores(
&self,
) -> Result<BTreeMap<String, u32>, StoreError>
pub fn live_producer_trust_scores( &self, ) -> Result<BTreeMap<String, u32>, StoreError>
The latest live ProducerTrust score (thousandths, 0..=1000) for
every producer that currently has trust: the newest score per tool by
timestamp, excluding any tool under an active ProducerBlock (a block
is a hard veto over trust, matching recompute_producer_trust).
Used to export a capsule trusted-keys keyring from earned trust — the
producer id doubles as the publisher’s signing key downstream, so this
turns track record into the allowlist capsule install consumes.
pub fn attestation_log(&self) -> Result<AttestationLog, StoreError>
Sourcepub fn record_examples_passed(
&self,
stage_id: &str,
op_id: &OpId,
count: usize,
) -> Result<(), StoreError>
pub fn record_examples_passed( &self, stage_id: &str, op_id: &OpId, count: usize, ) -> Result<(), StoreError>
Emit an Examples::Passed attestation for a published stage
whose behavioral examples {} block was run and passed (#835,
Tier 1). Mirrors Self::record_typecheck_passed. The
behavioral run itself happens one layer up (lex-api / lex-cli)
because it needs the bytecode compiler + VM, which this crate
deliberately doesn’t depend on; the store only records the
verdict. file_hash uses the stage id — the stage fully
determines its own examples.
Sourcepub fn record_review(
&self,
stage_id: &str,
op_id: Option<OpId>,
reviewer: &str,
verdict: ReviewVerdict,
notes: Option<String>,
) -> Result<AttestationId, StoreError>
pub fn record_review( &self, stage_id: &str, op_id: Option<OpId>, reviewer: &str, verdict: ReviewVerdict, notes: Option<String>, ) -> Result<AttestationId, StoreError>
Record a structured Review verdict on a stage (#836 G4).
The verdict maps onto the attestation result so existing
result-based tooling reads it: Approve->Passed,
Reject->Failed, RequestChanges->Inconclusive.
Sourcepub fn latest_review_verdict(
&self,
stage_id: &str,
) -> Result<Option<ReviewVerdict>, StoreError>
pub fn latest_review_verdict( &self, stage_id: &str, ) -> Result<Option<ReviewVerdict>, StoreError>
The latest Review verdict recorded on a stage, if any
(#836 G4). “Latest” is by attestation timestamp; ties keep the
last one seen. Used by promote_candidate to honor a standing
Reject.
Sourcepub fn record_op_trace(
&self,
run_id: &str,
root_target: &str,
op_id: &OpId,
result: AttestationResult,
producer: ProducerDescriptor,
) -> Result<usize, StoreError>
pub fn record_op_trace( &self, run_id: &str, root_target: &str, op_id: &OpId, result: AttestationResult, producer: ProducerDescriptor, ) -> Result<usize, StoreError>
Emit Trace attestations linking an already-committed op
to the run that produced it (#257). One attestation per
produced stage (matching the TypeCheck emission contract
— see Self::apply_operation_checked) with
op_id: Some(op_id) set, so lex trace --op <op_id>
surfaces the run.
Returns the number of attestations emitted (zero for ops
that produce no attestable stage, e.g. Remove /
ImportOnly).
Idempotent: re-emitting for the same
(run_id, root_target, op_id, stage_id, producer, result)
tuple dedups via content addressing.
op_id must already exist in the op log — an unknown op
surfaces as StoreError::UnknownOp.
Sourcepub fn record_run_committed_ops_since(
&self,
run_id: &str,
root_target: &str,
branch: &str,
base: Option<&OpId>,
result: AttestationResult,
producer: ProducerDescriptor,
) -> Result<usize, StoreError>
pub fn record_run_committed_ops_since( &self, run_id: &str, root_target: &str, branch: &str, base: Option<&OpId>, result: AttestationResult, producer: ProducerDescriptor, ) -> Result<usize, StoreError>
Walk ops_since(branch_head, base) and emit per-stage
Trace attestations for each new op, linking them to the
run that produced them (#257). Used by lex run --trace
after the VM exits: snapshot base = branch_head before
the run, then call this with the post-run head.
base = None means “every op currently reachable from the
branch head” — generally not what you want for a single
run; pass the pre-run head.
Returns the total number of attestations emitted across every new op. Zero is the common case (the run committed no ops).
Idempotent on the per-op level via Self::record_op_trace.
Sourcepub fn apply_replace_match_arm(
&self,
branch: &str,
from_stage_id: &str,
match_node: &NodeId,
arm_index: usize,
new_body: CExpr,
) -> Result<OpId, StoreError>
pub fn apply_replace_match_arm( &self, branch: &str, from_stage_id: &str, match_node: &NodeId, arm_index: usize, new_body: CExpr, ) -> Result<OpId, StoreError>
Apply a typed ReplaceMatchArm transform (#280) and emit a
OperationKind::ReplaceMatchArm op that records the
semantic shape of the edit, not just the byte effect.
Steps:
- Load the source stage’s canonical bytes (delta-aware).
- Run
lex_ast::replace_match_armto produce the newStage. Pure function, no I/O. - Publish the new stage. Idempotent on the
content-addressed
to_stage_id. - Assemble the candidate program (every active stage on
the branch, with the rewritten one swapped in) and call
Self::apply_operation_checked— re-typechecks and runs every existing gate (TypeCheck attestation, required_attestations, producer-block walk-back).
Failure modes:
StoreError::TransformError— transform didn’t apply. The branch is unchanged; no stage published.StoreError::TypeError— transform produced an ill-typed program. The new stage is on disk (idempotent on its content hash) but the branch is unchanged. Same “publish without advance” semantics as #245.- Everything else from
apply_operation_checked.
Sourcepub fn apply_rename_local(
&self,
branch: &str,
from_stage_id: &str,
let_node: &NodeId,
new_name: &str,
) -> Result<OpId, StoreError>
pub fn apply_rename_local( &self, branch: &str, from_stage_id: &str, let_node: &NodeId, new_name: &str, ) -> Result<OpId, StoreError>
Apply a typed RenameLocal transform (#280) — rename a
let-bound local within a fn body and emit a matching
OperationKind::RenameLocal. Same end-to-end shape as
Self::apply_replace_match_arm; see that method for the
failure-mode taxonomy.
Sourcepub fn apply_inline_let(
&self,
branch: &str,
from_stage_id: &str,
let_node: &NodeId,
) -> Result<OpId, StoreError>
pub fn apply_inline_let( &self, branch: &str, from_stage_id: &str, let_node: &NodeId, ) -> Result<OpId, StoreError>
Apply a typed InlineLet transform (#280) — eliminate a
let x := v; body by substituting v for every unshadowed
x in body, then replacing the Let node with the
substituted body. Same end-to-end shape as
Self::apply_replace_match_arm.
Sourcepub fn apply_extract_function(
&self,
branch: &str,
from_stage_id: &str,
expr_node: &NodeId,
spec: ExtractFnSpec,
) -> Result<(OpId, OpId), StoreError>
pub fn apply_extract_function( &self, branch: &str, from_stage_id: &str, expr_node: &NodeId, spec: ExtractFnSpec, ) -> Result<(OpId, OpId), StoreError>
Apply a typed ExtractFunction transform (#280 slice 4) —
extract a sub-expression of from_stage_id’s body into a
new top-level fn defined by spec, and emit two ops tied
together by a shared synthetic Intent so lex op log --intent <id> groups them.
The two ops:
AddFunction { sig_id: <new_fn_sig>, stage_id: <new_fn_stage> }ModifyBody { sig_id: <source_sig>, from_stage_id, to_stage_id: <modified> }
The shared Intent’s prompt is structured (extract_function: <new_fn_name> plus the source identity) so downstream
tooling can recover the typed-transform shape from the
op-log + intent-log join.
Returns (add_fn_op_id, modify_body_op_id).
Sourcepub fn propose_candidate(
&self,
branch: &str,
new_stage: &Stage,
intent_id: &IntentId,
) -> Result<OpId, StoreError>
pub fn propose_candidate( &self, branch: &str, new_stage: &Stage, intent_id: &IntentId, ) -> Result<OpId, StoreError>
Propose a stage for sig_id without advancing the branch
head (#294). Multiple agents can call this concurrently
for the same sig — every call lands a fresh Candidate
op chained off the current head_op. The branch head stays
where it was; a later Self::promote_candidate picks
the winner.
The caller is responsible for typechecking new_stage
against whatever program context they consider valid —
propose_candidate doesn’t run the gate. Type errors
surface at promotion time, where the candidate is
composed back into a candidate program via the standard
apply_operation_checked path.
The stage is published (idempotent on content hash). The
intent_id is required so downstream consumers can
distinguish proposals by author.
Sourcepub fn list_candidates(
&self,
sig_id: &str,
) -> Result<Vec<CandidateInfo>, StoreError>
pub fn list_candidates( &self, sig_id: &str, ) -> Result<Vec<CandidateInfo>, StoreError>
List every live Candidate op for sig_id — i.e. those
not yet referenced by any Promote op (either as the
winner or in the supersedes set). Used by lex stage candidates. Results are sorted by op_id for
reproducibility.
Sourcepub fn promote_candidate(
&self,
branch: &str,
candidate_op_id: &OpId,
) -> Result<OpId, StoreError>
pub fn promote_candidate( &self, branch: &str, candidate_op_id: &OpId, ) -> Result<OpId, StoreError>
Promote a previously-landed Candidate op as the new
branch head for its sig (#294). Emits a Promote op
listing every other live Candidate for the same sig
in its supersedes field. After this lands,
Self::list_candidates returns an empty set for the
sig.
Re-typechecks the candidate program (winner stage + the
rest of the branch) through apply_operation_checked, so
a candidate that doesn’t compose with the current branch
state surfaces as StoreError::TypeError.
Sourcepub fn apply_operation(
&self,
branch: &str,
op: Operation,
transition: StageTransition,
) -> Result<OpId, StoreError>
pub fn apply_operation( &self, branch: &str, op: Operation, transition: StageTransition, ) -> Result<OpId, StoreError>
set_branch_head_op for the durability story on the branch
file itself.
Source§impl Store
impl Store
pub fn current_branch(&self) -> String
pub fn set_current_branch(&self, name: &str) -> Result<(), StoreError>
pub fn list_branches(&self) -> Result<Vec<String>, StoreError>
pub fn get_branch(&self, name: &str) -> Result<Option<Branch>, StoreError>
Sourcepub fn branch_head(
&self,
name: &str,
) -> Result<BTreeMap<String, String>, StoreError>
pub fn branch_head( &self, name: &str, ) -> Result<BTreeMap<String, String>, StoreError>
Computed view: walk the op log from the branch head and replay each transition into a SigId → StageId map.
Backed by a persisted snapshot (<branch>.head_snapshot.json)
keyed on the head it was computed for. Steady state — this
call’s head_op matches the last call’s — replays only the ops
since the snapshot instead of the whole history: O(ops since
the last call) instead of O(total branch history). Falls back
to a full walk (and refreshes the snapshot) whenever there’s no
snapshot yet, or the snapshot’s op isn’t actually an ancestor
of the new head (a branch reset, or history reordered by a
merge) — see OpLog::walk_forward_since’s own doc comment.
This existed as a genuine, measured bottleneck before the snapshot: a single call over a tenant with 110k+ accumulated ops took on the order of an hour, dominated by one disk read per ancestor op in the full BFS walk (alpibrusl/lex-lang#813’s follow-up). Every consumer that used to call this once per file in a multi-file publish (fixed separately, also #813) now calls it once per publish request — but “once” was still a full walk over the entire history every time, since nothing persisted the result between calls.
pub fn branch_log(&self, name: &str) -> Result<Vec<MergeRecord>, StoreError>
Sourcepub fn create_branch(&self, name: &str, from: &str) -> Result<(), StoreError>
pub fn create_branch(&self, name: &str, from: &str) -> Result<(), StoreError>
Snapshot the source branch’s head_op into a new named branch.
Sourcepub fn create_predicate_branch(
&self,
name: &str,
predicate: Value,
) -> Result<(), StoreError>
pub fn create_predicate_branch( &self, name: &str, predicate: Value, ) -> Result<(), StoreError>
Create a predicate-defined branch (#133). The branch’s
content is the set of ops matching predicate; head_op
stays None and is materialized lazily by callers when
they need a single point to apply ops against. Cheap to
create and discard — it’s a saved query, not a snapshot.
pub fn delete_branch(&self, name: &str) -> Result<(), StoreError>
Sourcepub fn invalidate_gate_checkpoints(&self) -> Result<usize, StoreError>
pub fn invalidate_gate_checkpoints(&self) -> Result<usize, StoreError>
Invalidate every branch’s last_gate_checkpoint (#256). Run
when a new ProducerBlock attestation lands so the next
branch advance walks back from genesis once and re-verifies
the full chain. Returns the number of branches whose
checkpoint changed.
Source§impl Store
impl Store
pub fn merge(&self, src: &str, dst: &str) -> Result<MergeReport, StoreError>
pub fn commit_merge( &self, dst: &str, report: &MergeReport, ) -> Result<(), StoreError>
Source§impl Store
impl Store
Sourcepub fn plan_gc(&self, cli_retain: &[Predicate]) -> Result<GcPlan, StoreError>
pub fn plan_gc(&self, cli_retain: &[Predicate]) -> Result<GcPlan, StoreError>
Build a GcPlan from the store’s current state plus an
optional list of additional retention predicates from the
CLI (lex op gc --retain ...). The policy file’s
gc_retention.retain entries are appended to those.
Returns StoreError::Io(InvalidData, ...) if a predicate
in policy.json fails to parse.
Sourcepub fn apply_gc(&self, plan: &GcPlan) -> Result<usize, StoreError>
pub fn apply_gc(&self, plan: &GcPlan) -> Result<usize, StoreError>
Apply a GcPlan — actually delete every op in
plan.to_delete. Idempotent: running again on the same
store after a successful apply yields a plan with an empty
deletion set.
Returns the number of op records actually removed (loose files deleted + packed ops dropped during pack rewrites).