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 with_dep_resolver(self, resolver: Arc<dyn DepResolver>) -> Self
pub fn with_dep_resolver(self, resolver: Arc<dyn DepResolver>) -> Self
Install the dependency resolver the write-time gate uses to type-check
heads that keep external import edges (#930). Builder-style so a
caller can write Store::open(p)?.with_dep_resolver(r).
Sourcepub fn set_dep_resolver(&mut self, resolver: Arc<dyn DepResolver>)
pub fn set_dep_resolver(&mut self, resolver: Arc<dyn DepResolver>)
Set the dependency resolver in place (for a Store already owned, e.g.
behind a Mutex in the HTTP State).
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. Text wrapper over Self::put_blob_bytes; the id is
the sha of the UTF-8 bytes, so it is unchanged from before #1007.
Sourcepub fn put_blob_bytes(&self, bytes: &[u8]) -> Result<String, StoreError>
pub fn put_blob_bytes(&self, bytes: &[u8]) -> Result<String, StoreError>
Content-address arbitrary bytes (#1007: files beside the op-log may
be binary) and persist them under <root>/blobs/<sha>. Returns the
lowercase hex SHA-256 — the same value sha256sum prints. 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 as text. UnknownBlob if absent; an
InvalidData I/O error if the blob is not UTF-8 (use
Self::get_blob_bytes for binary content).
Sourcepub fn get_blob_bytes(&self, sha: &str) -> Result<Vec<u8>, StoreError>
pub fn get_blob_bytes(&self, sha: &str) -> Result<Vec<u8>, StoreError>
Read a blob’s exact bytes by its sha. UnknownBlob if absent.
Sourcepub fn blob_len(&self, sha: &str) -> Option<u64>
pub fn blob_len(&self, sha: &str) -> Option<u64>
Byte length of a blob without reading it, or None if absent.
Sourcepub fn blob_bytes_used(&self) -> Result<u64, StoreError>
pub fn blob_bytes_used(&self) -> Result<u64, StoreError>
Total bytes held in this store’s blob space (#1007): the sum of every stored blob’s length, ignoring in-flight temp files. What a per-store blob quota is measured against. O(number of blobs).
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 set_committed_lock(
&self,
head_op: &str,
lock_toml: &str,
) -> Result<(), StoreError>
pub fn set_committed_lock( &self, head_op: &str, lock_toml: &str, ) -> Result<(), StoreError>
Record the lex.lock committed with the package head head_op — the
exact dependency versions and op-log heads that head was built and
type-checks against (#930 phase 2b-1: “HEAD + its committed lex.lock
always type-checks”). Content-addressed via Self::put_blob and
bound under the lock namespace keyed by the head op, so it is
idempotent (a re-push converges) and travels with the package through
the same object-sync path as stages and intents. Keyed by head op
rather than by branch so re-verifying a historical head resolves it
against the lock that head actually committed, not whatever the branch
points at now.
Sourcepub fn committed_lock(
&self,
head_op: &str,
) -> Result<Option<String>, StoreError>
pub fn committed_lock( &self, head_op: &str, ) -> Result<Option<String>, StoreError>
The lex.lock committed with head_op, or None when the head
carries no committed lock — a dependency-free package, or one
published before locks were committed (the write-time gate then has no
registry/git dependencies to resolve, exactly as today).
Sourcepub fn committed_lock_inherited(
&self,
head_op: &str,
) -> Result<Option<String>, StoreError>
pub fn committed_lock_inherited( &self, head_op: &str, ) -> Result<Option<String>, StoreError>
The lock governing head_op: its own committed lock if it has one,
otherwise the nearest ancestor’s (#975).
Self::committed_lock is an exact-key lookup, and a lock is only ever
committed for a head a client pushes — or, since #977, for a merge op,
which commits the union of its parents’ locks
(Self::merged_lock_for_parents). A head landed through /v1/patch
has none of its own, so the exact lookup returned None, the dependency
resolver got no pins, and a non-inlined head was rejected as
unknown_identifier "<alias>" even though every dependency was
resolvable. A head inherits its ancestors’ pins until a new lock is
committed: the correct model for a patch head, which genuinely has no
new pins to record.
Breadth-first, so the nearest ancestor wins. That returns one lock whole, which is only right along a single line of history — which is why a merge op does not rely on it and commits its own merged lock.
Sourcepub fn merged_lock_for_parents(
&self,
parents: &[OpId],
) -> Result<Option<String>, StoreError>
pub fn merged_lock_for_parents( &self, parents: &[OpId], ) -> Result<Option<String>, StoreError>
The lock a merge op with parents commits as its own (#977): the
union of every parent’s governing lock
(Self::committed_lock_inherited).
Inheriting one parent’s lock whole is wrong for a merge: if the source
branch introduced a dependency, only the source’s lock pins it, and a
merge that inherited the destination’s lock failed its gate as
unknown_identifier "<alias>". Unioning fixes that.
parents must be in merge order — the first is the branch being merged
INTO (dst), later ones are merged in. Note an Operation’s own
parents are sorted (so the op id is order-independent) and do not
carry that order; the caller supplies it. It only matters for naming
the sides of a conflict and for which entry is kept on a tie. When two parents pin the same package at
different versions the merge is refused with
StoreError::DependencyConflict rather than silently picking one:
either choice changes what one side’s code was tested against. When the
versions agree, the first parent’s entry is kept (filling in a
head_op it lacks from a later parent).
Ok(None) when no parent is governed by any lock (a dependency-free
package), and also when a parent’s lock can’t be parsed — the merge then
carries no lock of its own and falls back to inheritance exactly as
before #977, rather than refusing a merge over an unreadable lock it
did not write. Read-only: nothing is written.
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).
Sourcepub fn get_metadata_for_sig(
&self,
sig_id: &str,
stage_id: &str,
) -> Result<Metadata, StoreError>
pub fn get_metadata_for_sig( &self, sig_id: &str, stage_id: &str, ) -> Result<Metadata, StoreError>
The metadata for a stage under a named sig.
Self::get_metadata resolves the sig through the stage index, which
is a stage-id-only lookup — and a StageId does not encode the name
(#826), so two sigs can share one. Resolving by id then hands both
declarations whichever record the index happens to point at. Harmless
for a hash, not for content: it gave two different declarations the
same documentation, duplicating a module header into a package that
had it once.
When the caller knows the sig — a head map is keyed by it — this is the honest lookup.
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.
Sourcepub fn publish_program_with_intent(
&self,
branch: &str,
stages: &[Stage],
diff: &DiffReport,
new_imports: &ImportMap,
activate: bool,
signer: Option<&Keypair>,
intent_id: Option<IntentId>,
module_prefixes: &BTreeMap<String, String>,
) -> Result<PublishOutcome, StoreError>
pub fn publish_program_with_intent( &self, branch: &str, stages: &[Stage], diff: &DiffReport, new_imports: &ImportMap, activate: bool, signer: Option<&Keypair>, intent_id: Option<IntentId>, module_prefixes: &BTreeMap<String, String>, ) -> Result<PublishOutcome, StoreError>
Self::publish_program_signed plus an optional intent_id
(#131 / #839): when given, every op this publish emits is stamped
with it, so the op log records why the change happened — the
prompt / model / session an agent was acting under — not only
what it was. lex recall --intent <id> and lex op replay read
it back. The caller records the lex_vcs::Intent in the
lex_vcs::IntentLog beforehand; this only links ops to it.
None is the existing (intent-less) behavior, so op ids for
intent-less publishes are unchanged.
Sourcepub fn unsatisfiable_owner(&self, sig: &str, stage: &str) -> Option<String>
pub fn unsatisfiable_owner(&self, sig: &str, stage: &str) -> Option<String>
The sig stage is actually filed under, when binding it to sig is
provably unsatisfiable (#992): (sig, stage) cannot be read, yet
the store holds that very stage under a different sig. None when the
pair reads fine, and also when the stage is simply absent — an absent
blob (mid-pull, partial clone) proves nothing about satisfiability.
Presence is judged by the stage file existing under the sig (a full snapshot or a delta), not by decoding it: this runs over every head entry on a ref advance, and a stat is all the question needs.
Sourcepub fn check_pairs_satisfiable<'a>(
&self,
pairs: impl IntoIterator<Item = (&'a String, &'a String)>,
) -> Result<(), StoreError>
pub fn check_pairs_satisfiable<'a>( &self, pairs: impl IntoIterator<Item = (&'a String, &'a String)>, ) -> Result<(), StoreError>
Refuse the first provably unsatisfiable pair in pairs with
StoreError::UnsatisfiablePair — the write-time half of #992’s
“always-valid HEAD”. See Self::unsatisfiable_owner for what counts.
Sourcepub fn stranded_head_entries(
&self,
head_pairs: &[(String, String)],
) -> Vec<OperationKind>
pub fn stranded_head_entries( &self, head_pairs: &[(String, String)], ) -> Vec<OperationKind>
Head entries naming a (sig, stage) no store can hold, where that same
stage is readable under a different sig at the head (#992).
This is the fingerprint a pre-#992 ChangeEffectSig leaves behind. It
bound the old sig to the new stage, while the store files an
implementation under the sig its own AST hashes to — and that AST
declares the new effects. The head then names one declaration twice and
one of the two can never be resolved, so every render of that head
fails and any release cut from it is born broken (lex-web@0.4.0).
Fixing the op stops new ones appearing but cannot repair a head that already has one: the publish diff is keyed by declaration name and reads the old side through the ASTs it can resolve, so a stranded entry is invisible to it and no op is ever emitted.
The second condition is the whole reason this is safe to do without asking. Requiring the stage to be resolvable under another sig proves the content is present and merely filed elsewhere, so retiring the entry discards nothing. A store that is simply missing blobs — mid-pull, a partial clone, a GC’d object — fails that test, because there neither sig resolves, and is left strictly alone.
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 apply_operation_checked_with_intent(
&self,
branch: &str,
op: Operation,
transition: StageTransition,
candidate: &[Stage],
intent: Option<&Intent>,
) -> Result<OpId, StoreError>
pub fn apply_operation_checked_with_intent( &self, branch: &str, op: Operation, transition: StageTransition, candidate: &[Stage], intent: Option<&Intent>, ) -> Result<OpId, StoreError>
Self::apply_operation_checked that also records intent in the
lex_vcs::IntentLog (#837 piece A). The caller stamps the intent’s id on
op (Operation::with_intent); this only makes sure the intent record
exists, and does so after the type-check and session-budget gates and
before the head moves — so a rejected write leaves no op and no
intent behind, the same “no footprint” guarantee the gate already
gives the op record. None is exactly apply_operation_checked.
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 pins only the sigs the
merge decided, 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_operation_gated_with_intent(
&self,
branch: &str,
op: Operation,
transition: StageTransition,
intent: Option<&Intent>,
) -> Result<OpId, StoreError>
pub fn apply_operation_gated_with_intent( &self, branch: &str, op: Operation, transition: StageTransition, intent: Option<&Intent>, ) -> Result<OpId, StoreError>
Self::apply_operation_gated that also records intent (see
Self::apply_operation_checked_with_intent) — the write path for an
intent-carrying /v1/patch (#837 piece A).
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 pins only the sigs the merge decided
(#1062); 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 entries never mention. 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 apply_merge_op_gated_with_manifest(
&self,
branch: &str,
op: Operation,
transition: StageTransition,
manifest: Option<&str>,
intent_id: Option<&IntentId>,
) -> Result<OpId, StoreError>
pub fn apply_merge_op_gated_with_manifest( &self, branch: &str, op: Operation, transition: StageTransition, manifest: Option<&str>, intent_id: Option<&IntentId>, ) -> Result<OpId, StoreError>
Like Self::apply_merge_op_gated, but also appends the
SetFiles a disagreeing-manifest merge needs (#1007 §1 / PR 7).
manifest is the already-computed, already-stored merged manifest
blob id — see [crate::files::manifest_merge] plus a merge
session’s file-conflict resolutions for how the caller builds it.
None when dst’s and src’s manifests already agreed (the common
case): nothing to record, behaves exactly like
apply_merge_op_gated.
On any failure — the sig-level type-check gate (as before), or the
follow-up SetFiles (which should essentially never fail here
since the caller already validated the manifest before calling
this, but a store can be modified concurrently) — the branch head
is rolled all the way back to where it was before this call. A
merge op is never left standing as a head with an Ambiguous
manifest; that would violate the always-valid-HEAD invariant
check_head_files polices for every other path onto a head.
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 replay_request(&self, op_id: &str) -> Result<ReplayRequest, StoreError>
pub fn replay_request(&self, op_id: &str) -> Result<ReplayRequest, StoreError>
#836 G3: assemble everything a regenerator needs to replay an
op — re-derive the change from its recorded cause. Returns the
op’s recorded intent (prompt / model / session), the target sig
and the stage id it produced, and the program the change was
made against (the parent state, rendered to source). An external
harness feeds the prompt + parent program to the recorded model,
then hands the regenerated stage back to Self::replay_compare
(lex owns the deterministic comparison; the model call is the
harness’s, matching the rest of the architecture).
Errors with UnknownOp if the op_id is unknown, or
InvalidTransition if the op didn’t produce a stage (a removal /
import / merge has nothing to regenerate). A SetFiles op (#1007)
is refused by kind with NotReplayable::Files — typed, so replay
coverage can leave it out instead of counting it as a miss.
Sourcepub fn replay_compare(
&self,
op_id: &str,
candidate: &Stage,
) -> Result<ReplayOutcome, StoreError>
pub fn replay_compare( &self, op_id: &str, candidate: &Stage, ) -> Result<ReplayOutcome, StoreError>
#836 G3: compare a regenerated candidate against what the op
recorded producing, and emit the Replay attestation. The
reproducibility claim made concrete — a faithful regeneration of
the same function from the same cause yields the same
content-addressed stage id.
reproduced is true iff the candidate is the same sig and the
same stage id the op recorded. A candidate for a different sig
counts as “not reproduced” (produced_stage_id: None) rather
than an error — it’s a legitimate, if negative, replay result.
The attestation is addressed to the op’s recorded stage, so
list_for_stage surfaces it alongside the TypeCheck/Examples
evidence.
Sourcepub fn replay_record_miss(
&self,
op_id: &str,
reason: &str,
) -> Result<ReplayOutcome, StoreError>
pub fn replay_record_miss( &self, op_id: &str, reason: &str, ) -> Result<ReplayOutcome, StoreError>
Record a negative replay result for a regeneration that never
yielded a comparable stage — the output didn’t parse, or didn’t
define the target sig (#836 G3). Emits a Replay { reproduced: false, produced_stage_id: None } attestation with reason in
its Failed detail, so an automated lex op replay run always
records a verdict rather than aborting. reason is caller-supplied
(e.g. “regenerated source did not parse”).
Sourcepub fn replay_record(
&self,
op_id: &str,
produced_stage_id: Option<String>,
reproduced: bool,
behavioral_samples: Option<usize>,
fail_detail: Option<String>,
) -> Result<ReplayOutcome, StoreError>
pub fn replay_record( &self, op_id: &str, produced_stage_id: Option<String>, reproduced: bool, behavioral_samples: Option<usize>, fail_detail: Option<String>, ) -> Result<ReplayOutcome, StoreError>
Record a replay verdict the caller has already decided — used by
the CLI’s behavioral tier, which does the (VM-backed) equivalence
check the store deliberately can’t. expected_stage_id is looked
up from the op. Set behavioral_samples to Some(n) when the
candidate reproduced behaviorally over n sampled inputs rather
than by exact stage-id match; the attestation then records that
weaker-but-real claim distinctly.
Sourcepub fn replay_stage_of(
&self,
op_id: &str,
candidate: &Stage,
) -> Result<(String, Option<String>, bool), StoreError>
pub fn replay_stage_of( &self, op_id: &str, candidate: &Stage, ) -> Result<(String, Option<String>, bool), StoreError>
Compute the exact-match verdict for a candidate without emitting
an attestation — (expected_stage_id, produced_stage_id, exact).
Lets a caller (the CLI) fall back to a behavioral check on a valid
but non-identical candidate and emit a single verdict, instead of
Self::replay_compare’s emit-immediately shape.
Sourcepub fn program_stages_at_op(
&self,
op_id: &str,
) -> Result<Vec<Stage>, StoreError>
pub fn program_stages_at_op( &self, op_id: &str, ) -> Result<Vec<Stage>, StoreError>
The program at an op (that op and all its ancestors applied), as
canonical stages. The behavioral replay tier needs the whole
program — a regenerated function may call helpers from its parent
state, so it can only be run in context. Exposed for the CLI’s
equivalence check; op_id may be any op in the log.
Sourcepub fn program_stages_at_op_skipping(
&self,
op_id: &str,
) -> Result<ReconstructedProgram, StoreError>
pub fn program_stages_at_op_skipping( &self, op_id: &str, ) -> Result<ReconstructedProgram, StoreError>
Self::program_stages_at_op, but head declarations whose stage
can’t be loaded are skipped and reported instead of failing the
reconstruction (#868).
Sourcepub fn bare_declaration_name(name: &str) -> &str
pub fn bare_declaration_name(name: &str) -> &str
See [demangled_name]; method form for call sites that already hold a
Store.
Sourcepub fn demangled_program_at_op(
&self,
op_id: &str,
) -> Result<Vec<Stage>, StoreError>
pub fn demangled_program_at_op( &self, op_id: &str, ) -> Result<Vec<Stage>, StoreError>
The program at an op as an author would write it — declarations
under bare names (twice, not lib_<hash>.twice), with the head’s
imports (#980).
A package publish mangles every declaration with a path-derived prefix.
That prefix is a storage detail: a dotted name cannot be written as
Lex source at all (fn lib_abc.twice(..) is a parse error), so anything
that shows a program to an author — or asks one to regenerate a
function from it — has to de-mangle first.
Falls back to the mangled reconstruction when de-mangling isn’t available (a multi-module head, pending #942), so callers degrade to the previous behaviour rather than failing outright.
Sourcepub fn demangled_program_at_op_skipping(
&self,
op_id: &str,
) -> Result<ReconstructedProgram, StoreError>
pub fn demangled_program_at_op_skipping( &self, op_id: &str, ) -> Result<ReconstructedProgram, StoreError>
Self::demangled_program_at_op, but head declarations whose stage
can’t be loaded (GC’d, superseded, never persisted) are skipped and
reported instead of failing the reconstruction (#868). This is what
replay uses: a partial context is still worth replaying against, and
the skips travel with the request and verdict.
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 verify_head_and_attest(
&self,
branch: &str,
from_head: Option<&str>,
to_head: &str,
) -> Result<HubCiVerdict, StoreError>
pub fn verify_head_and_attest( &self, branch: &str, from_head: Option<&str>, to_head: &str, ) -> Result<HubCiVerdict, StoreError>
The hosted CI runner (#93): independently re-run the write-time
type-check gate on a branch head and record the verdict as a
lex-hub-ci-produced TypeCheck attestation for the stages the
advance introduced. Called after an op push fast-forwards the
head, so require-attestation type_check gates are backed by a
producer that actually verified the code server-side, not by
whatever attestation a client chose to attach. Does NOT move or
roll back the head — the client’s own always-valid-HEAD gate is
what refuses a bad publish; this produces the trusted verdict on
top of an already-committed advance (so a client that bypassed
its gate is caught by a TypeCheck::Failed from lex-hub-ci).
from_head is the branch head before the advance; the ops
between it and to_head are the ones whose stages get attested.
Idempotent: attestations are content-addressed, so re-verifying
the same head is a no-op.
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 arrival order in the attestation
log — a server-assigned sequence number — NOT the
attestation’s timestamp, which the writer chooses and which
is excluded from the attestation id. So a verdict pushed with a
far-future timestamp does not outrank one that actually arrived
later. Legacy attestations with no arrival stamp order by
timestamp among themselves and below every stamped one; see
lex_vcs::AttestationLog::sort_by_arrival. Used by
promote_candidate to honor a standing Reject.
This fixes ordering only. It does not authenticate who wrote a verdict: any writer that can reach the log can still append a later one.
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_replace_match_arm_with_intent(
&self,
branch: &str,
from_stage_id: &str,
match_node: &NodeId,
arm_index: usize,
new_body: CExpr,
intent: Option<&Intent>,
) -> Result<OpId, StoreError>
pub fn apply_replace_match_arm_with_intent( &self, branch: &str, from_stage_id: &str, match_node: &NodeId, arm_index: usize, new_body: CExpr, intent: Option<&Intent>, ) -> Result<OpId, StoreError>
Self::apply_replace_match_arm with the write attributed to intent
(#837 piece A): the op is stamped with the intent’s id and the intent
is recorded once the gate has passed. None is the intent-less form.
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_rename_local_with_intent(
&self,
branch: &str,
from_stage_id: &str,
let_node: &NodeId,
new_name: &str,
intent: Option<&Intent>,
) -> Result<OpId, StoreError>
pub fn apply_rename_local_with_intent( &self, branch: &str, from_stage_id: &str, let_node: &NodeId, new_name: &str, intent: Option<&Intent>, ) -> Result<OpId, StoreError>
Self::apply_rename_local with the write attributed to intent
(#837 piece A); see Self::apply_replace_match_arm_with_intent.
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_inline_let_with_intent(
&self,
branch: &str,
from_stage_id: &str,
let_node: &NodeId,
intent: Option<&Intent>,
) -> Result<OpId, StoreError>
pub fn apply_inline_let_with_intent( &self, branch: &str, from_stage_id: &str, let_node: &NodeId, intent: Option<&Intent>, ) -> Result<OpId, StoreError>
Self::apply_inline_let with the write attributed to intent
(#837 piece A); see Self::apply_replace_match_arm_with_intent.
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 apply_extract_function_with_intent(
&self,
branch: &str,
from_stage_id: &str,
expr_node: &NodeId,
spec: ExtractFnSpec,
supplied_intent: Option<&Intent>,
) -> Result<(OpId, OpId), StoreError>
pub fn apply_extract_function_with_intent( &self, branch: &str, from_stage_id: &str, expr_node: &NodeId, spec: ExtractFnSpec, supplied_intent: Option<&Intent>, ) -> Result<(OpId, OpId), StoreError>
Self::apply_extract_function with the write attributed to a
caller-supplied intent (#837 piece A). Both ops carry that intent
instead of the synthetic [lex.transform.extract_function] one — the
typed shape is still recoverable from the op pair itself
(AddFunction + ModifyBody under one intent). None keeps the
synthetic intent.
Two ops means two gate passes, so this method makes the pair atomic itself: the final program (source rewritten + new fn) is checked before anything lands, and if the second op is refused anyway (a non-type gate) the head is rolled back to where it started. A rejected extraction therefore never leaves the new fn on the head without the call that uses it.
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.
Sourcepub fn branch_manifest(&self, name: &str) -> Result<ManifestAt, StoreError>
pub fn branch_manifest(&self, name: &str) -> Result<ManifestAt, StoreError>
The files manifest at name’s head (#1007) — see
Store::manifest_at. Cached in the head snapshot alongside the
sig→stage map and extended the same way, so steady state costs
O(ops since the last call).
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 advance_branch_head_ff(
&self,
name: &str,
new_head: &OpId,
) -> Result<BranchAdvance, StoreError>
pub fn advance_branch_head_ff( &self, name: &str, new_head: &OpId, ) -> Result<BranchAdvance, StoreError>
Atomically set a branch’s head_op. Used by apply_operation
after a successful op apply. Materializes main’s branch file
on first call (creates branches/main.json).
Crash safety: the tempfile’s data is fsync’d before rename
(see write_branch_atomic), so a successful return implies a
durable branch file at the final path. The containing directory
is not fsync’d; on a crash between rename and the directory’s
metadata flush, the rename can be lost — the prior head (or
missing branch file for a fresh main) survives. The op record
itself is content-addressed and is independently durable in the
op log.
Concurrency: single-writer per store. Two writers calling this
for the same branch race on read-modify-write of the JSON file
(each reads, mutates head_op, renames its tempfile in). Last
writer wins; the loser’s head update is silently dropped, even
though both their op records survive in the op log. Tier-1
merge / lex publish callers run sequentially; multi-writer
safety (file locking) is on the table once lex serve becomes
a real concurrent producer (#130 territory).
Advance branch to new_head, fast-forward only — the ref half
of op push (the op objects are transferred separately via the
ops batch). Semantics mirror git push to a branch:
- branch absent / no head yet → create it at
new_head; new_headalready the head → no-op (UpToDate);- current head is an ancestor of
new_head→ fast-forward; - otherwise →
StoreError::NonFastForward, so a disjoint or diverged history can’t silently clobber a shared branch.
new_head must already exist in the op log (the batch landed it);
an unknown op is a NonFastForward against a head it can’t reach.
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>
Sourcepub fn sig_map_at_op(
&self,
op_id: &str,
) -> Result<BTreeMap<String, String>, StoreError>
pub fn sig_map_at_op( &self, op_id: &str, ) -> Result<BTreeMap<String, String>, StoreError>
The SigId → StageId map at op_id: the full replay of that op’s
ancestry in OpLog::walk_forward’s canonical order, computed fresh
(never through a branch’s incremental snapshot). This is the single
definition of “the head at an op”; branch_head must agree with it.
Sourcepub fn merge_pins(
&self,
dst_head: Option<&OpId>,
src_head: Option<&OpId>,
auto_resolved: &[MergeOutcome],
resolved: &[(ConflictId, Resolution)],
) -> Result<BTreeMap<String, Option<String>>, StoreError>
pub fn merge_pins( &self, dst_head: Option<&OpId>, src_head: Option<&OpId>, auto_resolved: &[MergeOutcome], resolved: &[(ConflictId, Resolution)], ) -> Result<BTreeMap<String, Option<String>>, StoreError>
The Merge entries that pin every sig a merge decided to the
value it decided, so the merged head is a function of the resolved
merge and not of the order the two parallel histories are replayed in
(#1062).
A merge op is replayed by re-applying both parents’ ancestries and
then its own entries. Both sides’ ops on a sig the merge decided are
concurrent and do not commute (a rename retires the sig a modify
rebinds), so whichever the replay happens to apply last used to win —
and a resolution that leaves the sig as dst already has it (take_ours,
or a sig only dst touched) changes nothing relative to dst, so it was
never listed and nothing outranked the replay. Listing it here does.
Srcoutcomes: src’s stage (None: src removed it).Dst/Bothoutcomes andTakeOurs: the sig as dst’s head has it (None: dst lacks it, so the merge keeps it absent).TakeTheirs: the sig as src’s head has it.
Custom resolutions carry their target in the op itself; callers add
them on top (and own the error for an op that has none).
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).
Source§impl Store
impl Store
Sourcepub fn plan_blob_gc(&self, grace: Duration) -> Result<BlobGcPlan, StoreError>
pub fn plan_blob_gc(&self, grace: Duration) -> Result<BlobGcPlan, StoreError>
Build a BlobGcPlan: mark every blob reachable from a retained
SetFiles op’s manifest plus every blobrefs/** binding, then
sweep everything else older than grace. See the module docs
above for why the grace period exists and why “retained” mirrors
Self::plan_gc rather than recomputing branch reachability
independently.
Sourcepub fn apply_blob_gc(&self, plan: &BlobGcPlan) -> Result<usize, StoreError>
pub fn apply_blob_gc(&self, plan: &BlobGcPlan) -> Result<usize, StoreError>
Apply a BlobGcPlan — delete every blob in plan.to_delete.
Idempotent: a blob already gone (deleted by a concurrent GC pass,
e.g. another replica) is not an error. Returns the number of
blobs actually removed.
Source§impl Store
impl Store
Sourcepub fn put_manifest(&self, manifest: &Manifest) -> Result<BlobId, StoreError>
pub fn put_manifest(&self, manifest: &Manifest) -> Result<BlobId, StoreError>
Validate manifest and store it as a blob. Returns its id.
Sourcepub fn get_manifest(&self, id: &str) -> Result<Manifest, StoreError>
pub fn get_manifest(&self, id: &str) -> Result<Manifest, StoreError>
Load and validate the manifest stored under id.
Source§impl Store
impl Store
Sourcepub fn manifest_at(&self, op_id: &str) -> Result<ManifestAt, StoreError>
pub fn manifest_at(&self, op_id: &str) -> Result<ManifestAt, StoreError>
The files manifest in force at op_id (#1007): its own if it is a
SetFiles, else inherited through its parents — see ManifestAt.
UnknownOp if op_id is not in the log.
Sourcepub fn validate_set_files(&self, manifest: &str) -> Result<Manifest, StoreError>
pub fn validate_set_files(&self, manifest: &str) -> Result<Manifest, StoreError>
Check that manifest may become the file set of a head: it decodes
as a canonical, valid manifest (no reserved src/**/*.lex paths),
every entry blob is present, and each blob’s length matches its
entry’s size. Writes nothing.
Sourcepub fn apply_set_files(
&self,
branch: &str,
manifest: &str,
intent_id: Option<&IntentId>,
) -> Result<OpId, StoreError>
pub fn apply_set_files( &self, branch: &str, manifest: &str, intent_id: Option<&IntentId>, ) -> Result<OpId, StoreError>
Append a SetFiles { manifest } op to branch (#1007): the
repository’s non-op-log files become exactly the snapshot manifest
names. The sig→stage map is untouched.
Validated first (Self::validate_set_files); on any failure
nothing is persisted and the head is unchanged. Goes through the
same CAS advance (and branch-advance policy gate) as every other op.
Always appends: a caller that wants “no op when unchanged” compares
against Self::branch_manifest first.
Source§impl Store
impl Store
Sourcepub fn manifest_merge(
&self,
base_head: Option<&str>,
ours_head: Option<&str>,
theirs_head: Option<&str>,
) -> Result<ManifestMergeOutcome, StoreError>
pub fn manifest_merge( &self, base_head: Option<&str>, ours_head: Option<&str>, theirs_head: Option<&str>, ) -> Result<ManifestMergeOutcome, StoreError>
3-way-merge the files manifests of a merge’s ours (dst) and
theirs (src) heads against their base (the merge’s LCA) — the
files-dimension counterpart of [crate::merge::merge] for sigs.
Ok(NoChange) when ours and theirs already carry the exact
same ManifestAt (including both Absent — a package with no
files at all) — the merge needs no SetFiles, matching #1007 §1’s
“files agree ⇒ no SetFiles” case. Otherwise Ok(Needed { .. }):
auto_entries holds every path that resolved without a conflict
(already reflecting the winning side, or absent if both sides
agree the path is gone); conflicts lists the paths that need an
explicit lex_vcs::FileResolution.
Err(AmbiguousManifest) if ours or theirs is itself
Ambiguous — an earlier merge in that side’s history landed
without the SetFiles §1 requires. There is no well-defined
manifest to diff against on that side, so this merge refuses too;
see StoreError::AmbiguousManifest.
Sourcepub fn build_merged_manifest(
&self,
auto_entries: BTreeMap<String, Entry>,
conflicts: &[FileConflict],
resolutions: &BTreeMap<FilePath, FileResolution>,
merge_op_id: &str,
) -> Result<BlobId, StoreError>
pub fn build_merged_manifest( &self, auto_entries: BTreeMap<String, Entry>, conflicts: &[FileConflict], resolutions: &BTreeMap<FilePath, FileResolution>, merge_op_id: &str, ) -> Result<BlobId, StoreError>
Build the final merged Manifest once every conflict
Self::manifest_merge surfaced has a resolution (#1007 PR 7):
auto_entries (unconditionally included) plus, per conflict, the
resolved side’s entry (omitted entirely if that side had none —
i.e. the resolution keeps a removal). Stores the manifest as a
blob and returns its id, ready for
Store::apply_merge_op_gated_with_manifest.
Callers are expected to have already rejected any
lex_vcs::FileResolution::Defer (the same contract
MergeSession::commit enforces before this is ever called) —
Defer here is treated the same as no resolution: the conflict’s
path is simply left out of the merged manifest, which would
silently drop content, so this returns
StoreError::AmbiguousManifest instead to fail loud.