onevcs 0.37.0

Version control and remote-host abstraction for agent workflows: host-neutral change requests, sessions, and a rules system.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
//! Version control and its remote host behind one host-neutral vocabulary.
//!
//! The review unit is a [`ChangeRequest`] — GitHub maps it to a pull request,
//! and a later host maps it to whatever it calls the same thing. [`Vcs`] owns the
//! repository side (identities, sessions, preserved work) and [`RemoteHost`] owns
//! the host side (opening a change, reading its checks, merging it). A
//! [`rules`] file decides, per repository, how a change is published and whether
//! it needs an approval. Everything a process does along the way is emitted as an
//! [`Envelope`].
//!
//! # The shape of one change
//!
//! ```text
//! session open  →  a --shared clone and one worktree, occupancy-leased: a warm
//!                   pool slot where the host keeps one, else cut for this run
//!    →  work happens in the worktree
//!    →  publish   →  fetch and merge the current base  (bounded resolve-and-requeue)
//!                 →  the repository's own merge path verifies it: its pre-push
//!                    hook, or the host's required checks on the change request
//!                 →  local-direct squash, or a change request the host lands
//!    →  session close  →  the branch is copied out and stays; a slot is returned
//!                         warm, a per-run worktree goes
//! ```
//!
//! Everything durable lives under one state root (`ONEVCS_HOME`, otherwise
//! `~/.onevcs`): the registry document, the advisory locks and merge-queue state,
//! the per-session workspaces, the event streams, and their artifacts.
//!
//! # Two surfaces over one decision
//!
//! [`run`] is the command line: it answers a process, with an exit code and a line
//! of prose. A caller embedding this crate wants the decision itself, so the same
//! operations answer values — [`publish`] hands back a [`Publication`],
//! [`close_session`] the session it released, [`session`] what the repository side
//! recorded, [`preserve`] what putting a branch on its origin did, [`recoverable`] the
//! branches that still hold unpublished work, and [`EventStream`] the envelopes one
//! session wrote. The command line is a rendering of those rather than a second path
//! through them.
//!
//! **Every** command is: [`registered_identities`] and [`repositories`] are what
//! `onevcs repos` prints, [`register_checkout`], [`resolve_repository`],
//! [`publish_branch`], [`recover`], [`work_status`], [`import_branch`],
//! [`integrate`], [`sync`], [`sweep`], [`rules_check`], [`read_artifact`] and
//! [`EventLines`] are the rest, and a command with no such operation fails this
//! crate's own contract suite.

#![warn(missing_docs)]

use std::path::Path;

mod app;
mod branch;
pub mod branches;
mod change;
pub mod cli;
mod closing;
pub mod declaration;
mod error;
mod event;
mod gh;
mod git;
mod guidance;
mod home;
mod host;
mod ids;
mod import;
mod integrate;
mod label;
mod landed;
mod lock;
mod merge_path;
mod ops;
mod policy;
mod pool;
mod preserve;
mod probe;
mod processes;
pub mod provenance;
mod providers;
mod publish;
mod publish_branch;
mod queue;
mod recover;
pub mod registry;
mod release;
pub mod releases;
mod remainder;
mod retire;
pub mod rules;
mod session;
mod status;
mod store;
mod stream;
mod sweep;
mod vcs;
mod verdict;
mod vocabulary;
mod workspace;
pub mod workspaces;

pub use change::{ChangeDescription, SessionChange};
pub use declaration::{
    Declaration, DeclaredTarget, InstructionTemplate, RegistryId, RetiredArtifact,
};
pub use error::{Error, Result};
pub use event::{
    ArtifactId, ArtifactRef, Dimensions, Envelope, EventFilter, EventKind, EventMatcher, Labels,
    MatchFields, Phase, PhaseOf, Source, VcsEvents, DIMENSIONS, RESERVED_LABELS, SOURCE_WORD,
};
pub use host::{
    ChangeChecks, ChangeId, ChangeRequest, ChangeSpec, Check, CheckSource, CheckState, Description,
    GitHub, Hosting, MergeOutcome, ProtectionSource, RemoteHost, RequiredChecks, Sha,
};
pub use import::{Imported, Source as ImportSource, Wrote};
pub use integrate::{
    BranchOutcome, Ending as IntegrationEnding, Outcome as Integration, Status as IntegrationStatus,
};
pub use landed::{Landed, LandingEvidence};
pub use ops::{
    import_branch, integrate, publish_branch, read_artifact, recover, register_checkout,
    registered_identities, repositories, resolve_repository, rules_check, sweep, sync, work_status,
    BasePush, BranchPublishRequest, CheckoutAudit, GateAudit, ImportRequest, IntegrateRequest,
    MatchedRule, MergePathCoverage, RecoverRequest, RegisteredCheckout, RegisteredRepository,
    Registration, RequiredChecksAnswer, ResolvedPolicy, ResolvedRepository, RulesCheck,
    StatusReport, Sweeping, Synced, TrailerPrefixSource,
};
pub use pool::{
    pool_maintain, pool_prune, pool_status, workspace_capacity, IdentityMaintenance,
    IdentityOutcome, MaintainReport, MaintenanceOutcome, PoolStatus, PruneReport, SlotMaintenance,
    SlotOutcome, SlotState, SlotStatus, WorkspaceCapacity,
};
pub use preserve::{Preservation, PreserveRequest, Preserved};
pub use providers::Providers;
pub use publish::{
    DraftReason, FailureKind, Publication, PublishOutcome, PublishRequest, Retention, Subject,
    HOST_PREREQUISITE_MARKER,
};
pub use registry::Identity;
pub use releases::{
    Acknowledgement, Adoption, Baseline, BaselineRecord, DeclarationPolicy, DeclarationSource,
    Discovery, Probe, ReleaseAnswer, ReleaseDefault, ReleaseMethod, ReleaseRule, ReleaseSource,
    ReleaseStatus, ReleaseStyle, ReleaseTarget, ReleasesFile, RepositoryReleases,
    SupersededRelease, TargetName, TargetRelease, TargetSource,
};
pub use retire::{
    BranchHolder, BranchHolderKind, BranchRef, Derivation, FailedHolder, KeepReason, RetireMode,
    RetireOutcome, RetirePass, RetireRequest, Retired, Retirement, RetirementClass,
    RetirementPassReport, RetirementProof, RetirementQuery, SupersededBy, Supersession,
};
pub use rules::MergePolicy;
pub use session::{
    HeldBy, Holding, Lifecycle, LineChange, Liveness, NetNegative, OnOrigin, OpenConflict,
    PreservedBranch, Provenance, Recoverable, Scope, Selection, Session, SessionHolder,
    SessionRecord, SessionRequest, SessionToken,
};
pub use stream::{EventLine, EventLines, EventStream};
pub use sweep::Report as SweepReport;
pub use vcs::{Git, Vcs};
pub use workspaces::{first_matching, Bound, Span};

/// A parsed absolute URL, re-exported so a caller needs no direct dependency on
/// the parser this crate validates change-request URLs with.
pub use url::Url;

/// Run one parsed command line, returning the process exit code.
///
/// The binary is a thin shell over this, so a journey that drives `onevcs` and a
/// caller that embeds it take the same path and cannot disagree about an exit code.
pub fn run(cli: &cli::Cli) -> u8 {
    run_with(cli, Providers::real())
}

/// Run one parsed command line against supplied implementations of the two
/// interfaces, returning the process exit code.
///
/// [`run`] is this with [`Providers::real`], so nothing about the command's own
/// behaviour changes with the implementations behind it: one code path, reached
/// through [`Vcs`] and [`Hosting`] rather than through the types that satisfy them
/// by default.
pub fn run_with(cli: &cli::Cli, providers: Providers<'_>) -> u8 {
    app::run(&cli.command, &providers)
}

/// Verify a session's work and publish it, returning what the publication did.
///
/// The library form of `onevcs publish`, and the reason it exists: a run answers
/// with an exit code and prose, and a caller that has to branch on *what happened*
/// can only parse the prose. [`Publication`] is that answer as a value — the policy
/// it was taken under, whether it merged, opened a change request, queued one, or
/// had nothing to publish, and the failure and what became of the branch when it
/// did not land.
///
/// It runs through the seam, so a session a supplied [`Vcs`] opened publishes
/// against a supplied [`Hosting`] with no git, no host, and no process.
pub fn publish(
    providers: &Providers<'_>,
    token: &SessionToken,
    request: &PublishRequest,
) -> Result<Publication> {
    providers.vcs.publish(token, request, providers.hosting)
}

/// Release a session's worktree and its occupancy lease, keeping its branch.
///
/// The library form of `onevcs session close`.
pub fn close_session(providers: &Providers<'_>, token: &SessionToken) -> Result<Session> {
    providers.vcs.close_session(token)
}

/// The session's own change request — the one [`publish`] would open or adopt for
/// it, from its branch into the base its publication resolves — as the host holds
/// it, or `None` when the host holds none.
///
/// The library form of `onevcs change show`. It never invents one: a session that
/// has not published has no change request, and [`publish`] is what opens one.
pub fn session_change(
    providers: &Providers<'_>,
    token: &SessionToken,
) -> Result<Option<SessionChange>> {
    change::session_change(providers, token)
}

/// Replace the description of the session's own change request — its body, and its
/// title when one is given — and report the change as it stands after the write.
///
/// The library form of `onevcs change describe`. It refuses a session with no change
/// request rather than opening one. A title is a [`Subject`] and is held to the
/// repository's own `commit-msg` hook exactly as a publication's is, because it is
/// the squash subject a `change-auto` merge lands under. The body is written
/// verbatim and recorded as an artifact the `change-described` event names.
pub fn describe_change(
    providers: &Providers<'_>,
    token: &SessionToken,
    description: &ChangeDescription,
) -> Result<SessionChange> {
    change::describe_change(providers, token, description)
}

/// Mark the session's own change request ready for review, and report it as it
/// stands.
///
/// The library form of `onevcs change ready`: the lift a publication carrying no
/// [`DraftReason`] performs, asked for on its own. A change request that is not a
/// draft is asked for nothing and reported as it stands.
pub fn ready_change(providers: &Providers<'_>, token: &SessionToken) -> Result<SessionChange> {
    change::ready_change(providers, token)
}

/// Every session recorded for one repository, live or not, in token order.
///
/// The library form of `onevcs session holders`, and the question a caller asks
/// *before* it has a token: which sessions hold this repository's workspaces, which
/// of them still have an owner, and which are the remains of a run that stopped.
/// [`SessionHolder::token`] is what the rest of this surface takes, so a holder is
/// a session to act on rather than a line to read.
///
/// It takes no [`Providers`] because there is nothing here for an implementation to
/// answer: the holders are the records under this host's state root, which is where
/// `Git` writes them and where the command reads them. A `Vcs` that keeps its
/// sessions elsewhere therefore does not appear in this list — the same limit the
/// command has, since the two are one path.
///
/// A record whose owner process has exited *and* whose run root no live process is
/// working inside is forgotten as this answers rather than reported: nobody is left
/// to be asked about it, and a list of those is what buries the one holder a caller
/// had to act on. A session somebody is still working in is reported however long
/// ago the command that opened it exited.
pub fn session_holders(repo: &str) -> Result<Vec<SessionHolder>> {
    workspace::holders(repo)
}

/// Put an unpublished branch on its identity's origin, under its own name, without
/// publishing it.
///
/// The library form of `onevcs preserve`, and the half of a soft shutdown that makes
/// the commits a host was about to lose survive it. **Nothing about it is a
/// publication**: no change request is opened, no merge path is run, no base branch is
/// touched, nothing is force-pushed, and no provenance marker or attestation is
/// written — so a branch carrying an unattested incomplete-step marker is preserved
/// exactly as it stands and still needs [`recoverable`]'s command afterwards.
///
/// The branch is looked for everywhere the landing verbs look, run clones included,
/// so work a live dispatch committed a moment ago is found. A branch the origin
/// already carries at that commit is [`Preservation::AlreadyOnOrigin`] and nothing is
/// pushed; a location with no origin is [`Preservation::NoRemote`] and nothing is
/// attempted. A refusal — non-fast-forward, credentials, a local-path origin that
/// declines the ref — is an `Err` naming the identity, the branch, and git's own
/// per-ref summary, so a caller preserving many branches reports that one and carries
/// on.
///
/// It takes no [`Providers`] deliberately. Adding a method to the public [`Vcs`] trait
/// would break every outside implementor of it, and this reaches git and nothing else
/// — no host, no change request — so it sits beside [`workspace_capacity`] and
/// [`pool_maintain`], which are free functions for the same reason.
pub fn preserve(request: &PreserveRequest) -> Result<Preserved> {
    preserve::run(&store::load()?, request)
}

/// Every preserved-but-unpublished branch in scope, and what would land each.
///
/// The library form of `onevcs recoverable`, which had none: the enumeration was a
/// private function over types this surface already exports, so the one consumer that
/// wanted it had to spawn the binary and parse what it printed.
///
/// It answers exactly what the command answers without `--all`: a branch whose work
/// reached the base is not one of these, because the row is read to be pasted and
/// pasting one for finished work re-opens a change request for what the base already
/// carries. [`Vcs::preserved`] is the wider question — every preserved branch,
/// whatever became of its work — and it is on the seam because a supplied
/// implementation answers it.
///
/// It takes no [`Providers`] for the reason [`session_holders`] does not: the branches
/// are the ones under this host's own state root and in the checkouts its registry
/// records, which is where `Git` keeps them and where the command reads them.
pub fn recoverable(scope: &Scope) -> Result<Vec<Recoverable>> {
    vcs::collect(scope, vcs::Reporting::UnpublishedOnly)
}

/// What the repository side recorded about a session.
///
/// The library form of the record every command that takes a token reads: which
/// repository it belongs to, whether it is still open, and whether its branch
/// carries an incomplete-step marker.
pub fn session(providers: &Providers<'_>, token: &SessionToken) -> Result<SessionRecord> {
    providers.vcs.session(token)
}

/// What one branch is, for the question of whether it may be deleted: `retirable`,
/// `superseded-with-changes`, or `keep` with the one reason it is kept.
///
/// The library form of the classification `onevcs retire`, `onevcs reclaim` and
/// `onevcs retire-finished` act on and `onevcs recoverable` reports per row. It
/// deletes nothing and writes nothing: it reads every place this host keeps a copy of
/// the branch, asks the origin where its copy is, and asks the host about a change
/// request only where this host's own records do not decide it.
///
/// With [`RetirementQuery::repo`] `None` the branch is resolved across every
/// registered identity, and refused, naming the candidates, where more than one holds
/// it. A branch nothing holds any more is answered with the retirement recorded for
/// it, and refused where none is.
pub fn classify_retirement(
    providers: &Providers<'_>,
    query: &RetirementQuery,
) -> Result<Retirement> {
    retire::classify(providers.hosting, query)
}

/// Delete one branch everywhere this host holds it, where its class permits.
///
/// The library form of `onevcs retire` ([`RetireMode::Lossless`]) and `onevcs
/// reclaim` ([`RetireMode::Reclaim`]). A class the mode does not permit deletes
/// nothing and answers [`RetireOutcome::Kept`]; a branch nothing holds and whose
/// retirement is recorded answers [`RetireOutcome::AlreadyRetired`]; a copy that could
/// not be deleted answers [`RetireOutcome::Incomplete`], naming it, and a re-run
/// finishes the job. A pool slot is returned and never removed.
pub fn retire(providers: &Providers<'_>, request: &RetireRequest) -> Result<Retired> {
    retire::retire_named(providers.hosting, request)
}

/// Retire every branch in scope that provably holds no work beyond its base.
///
/// The automatic pass: `onevcs retire-finished`, and what an engine calls from its
/// idle maintenance. A change request opened from a branch whose landing is not
/// recorded is asked about once first, through the same reconciliation a `status`
/// read that meets a late merge makes. It acts on [`RetirementClass::Retirable`]
/// alone — never on a branch superseded with changes — and the report lists every
/// branch examined with its classification and what became of it.
pub fn retire_finished(
    providers: &Providers<'_>,
    pass: &RetirePass,
) -> Result<RetirementPassReport> {
    retire::pass(Some(providers.hosting), pass, retire::Trigger::Pass)
}

/// Record that a branch was superseded by a retry that landed.
///
/// The library form of `onevcs supersede`. Idempotent: the same identity, branch,
/// superseding branch and landing are recorded once. The record changes nothing
/// about the branch itself; it is what makes one that still differs from its base
/// `superseded-with-changes`, which `onevcs reclaim` acts on and nothing automatic
/// does.
///
/// It takes no [`Providers`] for the reason [`preserve`] does not: it writes this
/// host's own record, and there is nothing here for an implementation of either
/// interface to answer.
pub fn record_supersession(supersession: &Supersession) -> Result<()> {
    retire::supersede(supersession)
}

/// What one repository releases, and what it adopts.
///
/// The library form of `onevcs release targets`. `repo` is the identity key, a
/// registered alias, an origin URL, or a path, exactly as every other command takes
/// one.
///
/// # Three layers, in one stated order
///
/// The answer is resolved from the repository's own declaration and this host's
/// document together, and which of them decides is fixed rather than a consequence
/// of read order:
///
/// 1. the **producer's** `release-targets.toml`, read from its publication
///    checkout, contributes its targets in its own publication order;
/// 2. a target this host's `releases.yml` names and the producer does not is
///    **added** — a consumer standing in where nobody declared one;
/// 3. a target both name is the **host's**, in the producer's position — an
///    override, which is how a host runs a probe differently from the way the
///    repository publishes it.
///
/// A rule saying [`DeclarationPolicy::Ignore`] drops layer 1 entirely, which is how
/// a host says it does not consume what a repository declares. A host with no rules
/// and repositories declaring nothing answers exactly what it always did.
///
/// [`RepositoryReleases::sources`] says which layer answered for each target, and
/// [`RepositoryReleases::declaration`] says what the producer's half contributed —
/// including the state that is neither a declaration nor its absence, a declaration
/// this build could **not read**, which is never "this repository publishes
/// nothing".
///
/// It takes no [`Providers`] for the reason [`session_holders`] does not: what a
/// repository releases is this host's own configuration, the repository's own
/// declaration, and this host's own record, and there is nothing here for an
/// implementation of either interface to answer.
pub fn release_targets(repo: &str) -> Result<RepositoryReleases> {
    release::targets(&store::load()?, repo)
}

/// Every target one repository has, and what each of them has released right now.
///
/// The library form of `onevcs release discover`, and the answer a consumer that
/// waits on a release actually needs: [`release_targets`] says what there is to wait
/// for and this says what each of those has done, over the same three layers and in
/// one resolution rather than one per target.
///
/// Each target is answered exactly as [`release_latest`] answers it, so the two
/// cannot disagree — and [`ReleaseAnswer::NotAnswered`] stays distinct from
/// [`ReleaseAnswer::NoRelease`] here as everywhere: a consumer holds on the first
/// and acts on the second.
///
/// A repository with no targets answers with none rather than refusing, because
/// "there is nothing to wait for" is a thing a consumer acts on — but what it is
/// worth depends on [`RepositoryReleases::declaration`], which travels with it.
pub fn release_discovery(repo: &str) -> Result<Discovery> {
    release::discover(&store::load()?, repo)
}

/// What version of one target is released right now.
///
/// The library form of `onevcs release latest`. An **automated** target is answered
/// by running its probe; a **human-step** target executes nothing at all and is
/// answered from the newest acknowledgement across its landings, or
/// [`ReleaseAnswer::NoRelease`] where nobody has recorded one.
///
/// [`ReleaseAnswer::NotAnswered`] is never [`ReleaseAnswer::NoRelease`]: a consumer
/// holds on the first and acts on the second.
pub fn release_latest(repo: &str, target: Option<&TargetName>) -> Result<ReleaseAnswer> {
    release::latest(&store::load()?, repo, target)
}

/// Whether the release that carries one landed change has happened yet.
///
/// The library form of `onevcs release status`. `reference` is the four-spelling
/// reference `onevcs status` takes: a change request's URL, a session token, a
/// branch name, or a commit.
pub fn release_status(reference: &str, target: Option<&TargetName>) -> Result<ReleaseStatus> {
    release::status(
        &store::load()?,
        reference,
        target,
        Providers::real().hosting,
    )
}

/// [`release_status`] against supplied implementations, which is what the command
/// line runs.
///
/// The signature above is the contract's and takes no [`Providers`], so it answers
/// through the real host. The command line was handed implementations and asks the
/// same question of those — the one question a release read puts to a host, which is
/// whether a change request a `checks-unsettled` close stopped watching has merged
/// since.
pub(crate) fn release_status_with(
    providers: &Providers<'_>,
    reference: &str,
    target: Option<&TargetName>,
) -> Result<ReleaseStatus> {
    release::status(&store::load()?, reference, target, providers.hosting)
}

/// Whether one piece of work reached its base, and what says so.
///
/// The landing decision on its own, which is what [`release_status`] answers *before*
/// it selects a release target — and the reason this exists beside it. A repository
/// that declares no release target has no target to select, so `release_status`
/// refuses it; a refusal is undecided rather than "not landed", and a caller that
/// only wanted to know whether the work is on the base was left with no answer at
/// all. This takes no target, reads no release configuration, and answers every
/// repository the same way.
///
/// `reference` is the four-spelling reference [`release_status`] takes: a change
/// request's URL, a session token, a branch name, or a commit. `repo` is the
/// repository the question is narrowed to — an identity key, a registered alias, an
/// origin URL, or a path, exactly as every other repository-taking read here takes one
/// — and `None` resolves the repository from the reference alone. Naming one is how a
/// branch name two identities both hold is asked about; a reference belonging to
/// another identity is refused rather than answered under the name of the one asked
/// about.
///
/// The tiers are decided in one private place — `landed::decide` — and nowhere else,
/// so this, `onevcs status` and `onevcs recoverable` cannot come to disagree about one
/// branch. Which tier decided travels inside the [`Landed`] value, so a caller acting
/// on a `yes` can say what it acted on.
///
/// It takes no [`Providers`] for the reason [`release_targets`] does not: a landing is
/// decided from the history of the repositories under this host's own state root, and
/// there is nothing here for an implementation of either interface to answer.
pub fn landing_status(reference: &str, repo: Option<&str>) -> Result<Landed> {
    Ok(status::landing_of_within(&store::load()?, reference, repo, None)?.landed)
}

/// Record the release somebody says carries a landing.
///
/// The library form of `onevcs release acknowledge`. It accepts an automated target
/// whose landing has no established probed baseline, and refuses one whose baseline
/// is established. Human-step targets behave the same way regardless of baselines.
/// It also refuses a reference that has not landed, a version
/// that is not a semantic version, and a target the repository does not declare.
///
/// Recording the same version for the same landing again succeeds and changes
/// nothing, re-reporting the existing record with its original timestamp and actor.
/// Recording a *different* version is refused unless `supersede` is set, which
/// writes the new version and keeps the previous one in the record's own history.
pub fn acknowledge_release(
    reference: &str,
    target: &TargetName,
    version: &str,
    supersede: bool,
) -> Result<Acknowledgement> {
    release::acknowledge(&store::load()?, reference, target, version, supersede)
}

/// What one repository declares that it publishes.
///
/// The library form of `onevcs release declaration`, and the producer half of the
/// release contract: [`release_targets`] answers what *this host* waits on for a
/// repository, and this answers what the repository itself says it publishes. The
/// two are different documents on purpose — see [`declaration`] — and deciding
/// between them is nobody's job here.
///
/// `path` is either a repository's root or the `release-targets.toml` in it. A
/// repository carrying no declaration is refused rather than answered with an empty
/// one: "this repository publishes nothing" and "nobody has said what this
/// repository publishes" are different answers, and a consumer that waits on a
/// release acts differently on each.
///
/// It takes no [`Providers`] for the reason [`release_targets`] does not: a
/// declaration is a file in a checkout, and there is nothing here for an
/// implementation of either interface to answer.
pub fn read_release_declaration(path: &Path) -> Result<Declaration> {
    declaration::read(path)
}

/// Validate one release declaration's text, and answer what it declares.
///
/// The half of [`read_release_declaration`] that touches no filesystem: a caller
/// that fetched a declaration from a host, or is about to write one, holds it to
/// exactly the checks a repository's own file gets. `origin` is what the refusals
/// name the document by — a path, a URL, or whatever the caller knows it as.
pub fn validate_release_declaration(document: &str, origin: &str) -> Result<Declaration> {
    declaration::parse(document, origin)
}

/// Render a declaration back as the TOML document it declares.
///
/// **A producer's comments are not preserved, because they were never read.** A
/// declaration is mostly prose — the reasoning about what is a target and what is
/// not — and this answers with the declaration alone, so writing the result over a
/// producer's own file deletes that reasoning. It is for *producing* a declaration;
/// editing one is a job for a person.
///
/// What it does promise is that the result reads as the same declaration, and it
/// keeps that promise by holding what it was handed to a document's own checks
/// first: a [`Declaration`] a caller *built* rather than read is refused here if it
/// is one no repository could mean, rather than written out as a document nothing
/// can read back.
pub fn render_release_declaration(declared: &Declaration) -> Result<String> {
    declaration::render(declared)
}

/// Which rung of the adoption chain one repository resolves to.
///
/// The repository rung where a rule sets one and the global rung otherwise. It
/// never answers the node rung and never defaults to [`Adoption::Fast`] itself:
/// those two rungs belong to the consumer, and a crate that answered all four would
/// make the chain unreadable from either side.
pub fn adoption_for(repo: &str) -> Result<Adoption> {
    release::adoption(&store::load()?, repo)
}