Skip to main content

onevcs/
event.rs

1//! The NDJSON event envelope this crate writes, and the filter a consumer reads it
2//! through.
3//!
4//! The *shape* of an envelope — a version, a stamp, a stream, a sequence number, a
5//! source, a kind, the labels, the payload, the artifacts — and the filter grammar
6//! over it are `onemessagebus`'s, and `onemessagebus`'s `docs/contract.md` is the
7//! one source of that shape. The **words** are this crate's: `src/vocabulary.rs`
8//! declares the source, the phases, the reserved labels and what a matcher may ask
9//! of them, and the types re-exported below are the bus core's generic ones over
10//! that vocabulary. This crate holds its own bytes to the copy in
11//! `docs/contract.md`, so an envelope that stopped being what this crate always
12//! wrote fails here rather than downstream.
13//!
14//! What stays in this module is the rest of that vocabulary — the closed
15//! [`EventKind`], and the phase each kind belongs to — and how a line of a stream
16//! this crate wrote is read back.
17
18use onemessagebus::Kind;
19use serde::{Deserialize, Serialize};
20
21pub use crate::vocabulary::{
22    ArtifactRef, Dimensions, Envelope, EventFilter, EventMatcher, Labels, MatchFields, Phase,
23    Source, VcsEvents, DIMENSIONS, RESERVED_LABELS, SOURCE_WORD,
24};
25
26/// What an event says happened.
27///
28/// These are the kinds `onevcs` produces. A consumer merging several sources
29/// reads each source's own kinds; nothing here promises to name another
30/// library's.
31///
32/// **A kind is retired by keeping it recognised and inert, never by deleting it.**
33/// A stream is a record, and this enum is what says whether a line of one can be
34/// read at all: delete a variant and every line any earlier build wrote with it
35/// stops parsing, in a build that has no way to know what it lost. That is not
36/// hypothetical — `gate-started` and `gate-verdict` went with the host-run gate in
37/// 0.11.0, and 30% of the streams on the host that consumes this crate turned into
38/// one refusal per line, most expensively in a status read that walks every stream
39/// there is. So a kind nothing emits any more keeps its variant, documented as
40/// retired and produced by nothing, and a reader goes on being able to say what
41/// that line recorded.
42///
43/// The permissive read this crate's stream readers go through is the other half of
44/// that rule rather than a substitute for it. It makes a kind this build has *never
45/// had* — one a later build wrote — cheap to pass over, which is the case no
46/// vocabulary here can cover. It cannot give back the meaning of a kind that was
47/// deleted, which is why the two retired above are a loss this crate carries rather
48/// than one it recovered.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(rename_all = "kebab-case")]
51pub enum EventKind {
52    /// A session was opened over a clone and worktree, on a pool slot or a run root.
53    SessionOpened,
54    /// Objects were fetched, deliberately outside any exclusive section.
55    Fetch,
56    /// Waiting on an identity's lock; carries the identity, elapsed, and queue
57    /// position.
58    LockWait,
59    /// The identity's lock was acquired.
60    LockAcquired,
61    /// Work was committed onto a preserved branch; carries the provenance kind.
62    CommitPreserved,
63    /// A branch was put on its identity's origin under its own name, without being
64    /// published; carries the branch, the identity, the origin URL, the commit, and
65    /// which of the three things `onevcs preserve` found to do.
66    ///
67    /// Deliberately **not** [`Push`](EventKind::Push), and that is the whole reason it
68    /// is its own kind. The one producer of `push` is a publication, so a reader
69    /// counting pushes to find publications must never meet a preservation — and a
70    /// preservation is the opposite of one: no change request is opened, no base is
71    /// touched, and the branch is exactly as recoverable afterwards as it was before.
72    BranchPreserved,
73    /// A branch was pushed.
74    Push,
75    /// A change request was opened; carries its URL and the host kind.
76    ChangeOpened,
77    /// A change request was opened as a **draft**, and this is why it is not ready;
78    /// carries its URL, the host's identifier for it, its base, and the reason — its
79    /// `kind`, and every field of that kind: for `awaiting-release` the repository
80    /// whose release is awaited, which target of it, the reference the change is
81    /// pinned to, and the one line a person reads; for `held` that one line alone.
82    ///
83    /// Beside [`ChangeOpened`](EventKind::ChangeOpened) rather than instead of it: the
84    /// change request *was* opened, and the link from its URL back to the branch is
85    /// what that kind records. This one records the state it was opened in.
86    ChangeDrafted,
87    /// The draft was lifted and the change request is open for review; carries its
88    /// URL and the host's identifier for it.
89    ///
90    /// The reason is not repeated here: it is on the
91    /// [`ChangeDrafted`](EventKind::ChangeDrafted) this answers, and the publication
92    /// that lifts a draft is one carrying none — on green checks, early when they
93    /// never ran on the draft, or at once with the lifecycle off. `onevcs change ready`
94    /// emits the same kind, because it is the same lift asked for as a verb.
95    DraftLifted,
96    /// A draft the publication opened while its required checks ran was lifted
97    /// **before** any of them had run on it, because the grace window elapsed and
98    /// the repository's checks skip drafts; carries its URL, identifier and base,
99    /// the required checks that had not run, the grace window in seconds, and whether
100    /// the warning line was printed. Emitted beside the
101    /// [`DraftLifted`](EventKind::DraftLifted) every lift emits.
102    DraftLiftedEarly,
103    /// Green required checks left a `change-open` change whose approvals are required
104    /// a draft, for its own user's review rather than the team's; carries its URL,
105    /// identifier and base.
106    DraftKeptForReview,
107    /// A publication's watch saw every required check stop blocking; carries the
108    /// change request's URL and identifier, the commit watched, the verdict —
109    /// `passed`, or `passed-with-skipped` where a required check concluded skipped —
110    /// and the skipped required checks by name.
111    ChecksSettled,
112    /// The change request's description was replaced after it was opened; carries
113    /// its URL, the host's identifier for it, its base, the title where the
114    /// description replaced it, and the artifact the body was stored as.
115    ///
116    /// The body is not inlined: it is prose of unbounded size, and the stream bounds
117    /// payload text. The artifact is the record — `onevcs artifact cat ID` reads back
118    /// exactly what was written — and the event references it as every other large
119    /// piece of evidence is referenced.
120    ChangeDescribed,
121    /// A check moved; carries its name, whether it is required, the status
122    /// transition, the conclusion, and its log as an artifact once complete.
123    ChangeCheck,
124    /// The change request merged; carries its URL, the commit it landed at as `sha`
125    /// and as `landing`, and `landed_at`, when the base received it.
126    ChangeMerged,
127    /// One local invocation reached its actual base, carrying base, commit and ordered branches.
128    BaseArrived,
129    /// The change request entered the host's merge queue.
130    MergeQueued,
131    /// The merge the host had queued completed; carries the commit it landed at as
132    /// `sha` and as `landing`, and `landed_at`, when the base received it.
133    MergeCompleted,
134    /// Preserved work that carried an incomplete-step marker was verified and
135    /// attested.
136    RecoveryAttested,
137    /// The base moved under a publication and the bounded resolve-and-requeue did
138    /// not converge.
139    SyncConflict,
140    /// The session's worktree and lease were released.
141    SessionClosed,
142    /// An **automated** release target's probe was run; carries the identity, the
143    /// target, which form of probe it was, what it answered, the version where it
144    /// answered one, and how long it took. A human-step target never produces one,
145    /// and that absence is the observable proof that no probe ran for it.
146    ReleaseProbed,
147    /// Somebody recorded a release of a human-step target; carries the identity,
148    /// the target, the version, the landing commit, the actor, and the version it
149    /// superseded where it replaced one.
150    ReleaseAcknowledged,
151    /// A landing was released, the first time it was: its baseline passed for an
152    /// automated target, its acknowledgement recorded for a human-step one.
153    ReleaseObserved,
154    /// A branch was recorded as superseded by a retry whose landing reached the base;
155    /// carries the identity, the branch, the branch that superseded it, that
156    /// branch's landing — a commit or a change request's URL — and the labels its
157    /// caller stamped.
158    ///
159    /// A record and nothing else: the superseded branch is left exactly where it is.
160    /// What it changes is how the branch is *classified* — `superseded-with-changes`
161    /// where it still differs from the base, which only `onevcs reclaim` acts on.
162    BranchSuperseded,
163    /// A finished branch was deleted everywhere this host holds it; carries the
164    /// identity, the branch, the tip it was deleted at, the class and proof that
165    /// permitted it, which verb or moment acted, and what was deleted and what was
166    /// not — so a partial retirement names the holder a re-run still has to reach.
167    BranchRetired,
168    /// One completed gate run of a publication attempt; carries which gate it was —
169    /// `pre-push` or `required-checks` — the attempt it belongs to, when it started
170    /// and ended, how long that was, its verdict, and for required checks every
171    /// required check with the times and conclusion the host reported.
172    ///
173    /// Recorded from the run the publication makes anyway, never from a second one:
174    /// the push that executed the repository's `pre-push` hook, and the watch of a
175    /// change request's required checks. Its phase is the gate's, so the producer
176    /// stamps it — `integrate` for `pre-push`, `review` for `required-checks`.
177    GateRun,
178    /// A change request's review feedback was read; carries its URL, how many
179    /// comments the read returned, whether nothing was new since the marker it was
180    /// read from, the digest of the marker to read from next (never the marker, which
181    /// grows with the change request), and what the host charged for it —
182    /// `{graphql_points, rest_requests}`, from the responses' own figures.
183    ReviewCommentsRead,
184    /// A reply to one review comment was posted, or found already posted under its
185    /// key; carries the change request's URL, the comment answered, the reply's id
186    /// and URL, whether it sits in the comment's thread, the key, and whether it was
187    /// already there so nothing was posted.
188    ReviewReplyPosted,
189    /// A stack base was assembled: a branch cut from a root and carrying a merge of
190    /// several parent branches, put on its origin with no change request; carries
191    /// the branch, its root, each parent with the head it was merged at or already
192    /// carried, the branch's head, and whether it was `created`, `advanced`,
193    /// `unchanged` or `conflicted`.
194    ///
195    /// Not [`Push`](EventKind::Push), for the reason
196    /// [`BranchPreserved`](EventKind::BranchPreserved) is not: the one producer of
197    /// `push` is a publication, and a stack base publishes nothing.
198    StackBaseAssembled,
199    /// Where a batch of one repository's change requests stand was read; carries the
200    /// repository, how many were read, the change requests whose landing the read
201    /// recorded, and what the host charged, `{graphql_points, rest_requests}`.
202    ChangeStandingsRead,
203    /// A change request was moved onto another base, or found on it already; carries
204    /// its URL, the base it left and the one it targets, whether it moved
205    /// (`retargeted` or `unchanged`), and whether a description was written with it.
206    ChangeRetargeted,
207    /// A closed change request was reopened over its restored base; carries its URL,
208    /// the base branch restored on the origin, and the commit it was restored at.
209    ChangeReopened,
210    /// A publication decided whether to merge its moved base into the branch before
211    /// pushing it; carries the branch, the base, the `decision` (`merged` or
212    /// `skipped`) and `because`.
213    BaseMerge,
214    /// A publication whose wait for the checks elapsed with none of them red left the
215    /// host's own merge armed on its change request, so it lands once the checks pass;
216    /// carries the change request's URL and the head commit armed.
217    MergeArmed,
218    /// A `publish-branch` found its change request open at the boundary its
219    /// verification passed and resumed it at its hosted checks, pushing nothing;
220    /// carries the `branch`, the verified `head`, the `base` and the `change_url`.
221    ///
222    /// Emitted before that publication's [`ChangeOpened`](EventKind::ChangeOpened),
223    /// whether or not its base moved. A first publication's `push` is what names its
224    /// branch and head on a stream, and a resumed one has none, so this is what
225    /// resolves its change request's URL and its landing commit back to the work.
226    PublicationResumed,
227}
228
229/// How much attention an event asks of its consumer, stamped by its producer.
230#[derive(
231    Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
232)]
233#[non_exhaustive]
234#[serde(rename_all = "kebab-case")]
235pub enum Significance {
236    /// A change opened, its checks passed, or its landing reached its base.
237    Milestone,
238    /// An attempted action failed, was refused, or needs a person.
239    Blocker,
240    /// Ordinary running or successful detail.
241    Progress,
242}
243
244pub(crate) use crate::significance::{significance, EmissionResult};
245
246impl EventKind {
247    /// The kebab-case string this kind travels as, which is what a filter's
248    /// `kind` glob is matched against and what an envelope's `kind` carries.
249    ///
250    /// The spelling is serde's, and `the_wire_spelling_of_every_kind_is_the_one_a_filter_matches`
251    /// in `tests/contract.rs` holds the two together — a match rather than a
252    /// serialization so that stamping an event cannot fail, and exhaustive so that a
253    /// kind added to the enum cannot reach a stream unnamed.
254    fn wire(self) -> &'static str {
255        match self {
256            EventKind::SessionOpened => "session-opened",
257            EventKind::Fetch => "fetch",
258            EventKind::LockWait => "lock-wait",
259            EventKind::LockAcquired => "lock-acquired",
260            EventKind::CommitPreserved => "commit-preserved",
261            EventKind::BranchPreserved => "branch-preserved",
262            EventKind::Push => "push",
263            EventKind::ChangeOpened => "change-opened",
264            EventKind::ChangeDrafted => "change-drafted",
265            EventKind::DraftLifted => "draft-lifted",
266            EventKind::DraftLiftedEarly => "draft-lifted-early",
267            EventKind::DraftKeptForReview => "draft-kept-for-review",
268            EventKind::ChecksSettled => "checks-settled",
269            EventKind::ChangeDescribed => "change-described",
270            EventKind::ChangeCheck => "change-check",
271            EventKind::ChangeMerged => "change-merged",
272            EventKind::BaseArrived => "base-arrived",
273            EventKind::MergeQueued => "merge-queued",
274            EventKind::MergeCompleted => "merge-completed",
275            EventKind::RecoveryAttested => "recovery-attested",
276            EventKind::SyncConflict => "sync-conflict",
277            EventKind::SessionClosed => "session-closed",
278            EventKind::ReleaseProbed => "release-probed",
279            EventKind::ReleaseAcknowledged => "release-acknowledged",
280            EventKind::ReleaseObserved => "release-observed",
281            EventKind::BranchSuperseded => "branch-superseded",
282            EventKind::BranchRetired => "branch-retired",
283            EventKind::GateRun => "gate-run",
284            EventKind::ReviewCommentsRead => "review-comments-read",
285            EventKind::ReviewReplyPosted => "review-reply-posted",
286            EventKind::StackBaseAssembled => "stack-base-assembled",
287            EventKind::ChangeStandingsRead => "change-standings-read",
288            EventKind::ChangeRetargeted => "change-retargeted",
289            EventKind::ChangeReopened => "change-reopened",
290            EventKind::BaseMerge => "base-merge",
291            EventKind::MergeArmed => "merge-armed",
292            EventKind::PublicationResumed => "publication-resumed",
293        }
294    }
295
296    /// The kind an envelope's wire `kind` names, where this build has a word for it.
297    ///
298    /// Read through serde's own spelling rather than a second table, so the kinds
299    /// this build can read back are exactly the kinds it can write.
300    fn named(kind: &Kind) -> Option<Self> {
301        Self::deserialize(
302            serde::de::value::StrDeserializer::<serde::de::value::Error>::new(kind.as_str()),
303        )
304        .ok()
305    }
306}
307
308/// An envelope's `kind` is open on the wire, and this crate's closed vocabulary is
309/// what it writes into it.
310impl From<EventKind> for Kind {
311    fn from(kind: EventKind) -> Self {
312        Kind::from(kind.wire())
313    }
314}
315
316/// The phase an event of one of this crate's kinds belongs to, where the kind alone
317/// decides it.
318///
319/// A trait rather than an inherent method because the four phases are declared in
320/// `src/vocabulary.rs`, beside the rest of the wire words, and which of them an
321/// `onevcs` kind is in is a fact about the kinds declared here. With it in scope
322/// the call is spelled `Phase::of(kind)`, as it always was.
323pub trait PhaseOf {
324    /// The phase an event of this kind belongs to, where the kind alone decides it.
325    ///
326    /// `None` for exactly two kinds, and neither is an omission: a
327    /// [`Push`](EventKind::Push) of the session's own branch is
328    /// [`Development`](Phase::Development) and a push of anything else — the base a
329    /// `local-direct` squash lands on, the base a merge train advanced — is
330    /// [`Integrate`](Phase::Integrate). Which of the two it was is a fact about the
331    /// push rather than about the kind, so the producer stamps it and this answers
332    /// that it cannot. A [`GateRun`](EventKind::GateRun) is the same: a `pre-push`
333    /// gate is [`Integrate`](Phase::Integrate) and a change request's required checks
334    /// are [`Review`](Phase::Review), which is a fact about the gate.
335    fn of(kind: EventKind) -> Option<Phase>;
336}
337
338impl PhaseOf for Phase {
339    fn of(kind: EventKind) -> Option<Phase> {
340        Some(match kind {
341            EventKind::SessionOpened
342            | EventKind::Fetch
343            | EventKind::LockWait
344            | EventKind::LockAcquired
345            | EventKind::CommitPreserved
346            // The work being made, kept: putting a branch on its origin under its own
347            // name proposes nothing and integrates nothing, and a `local-direct`
348            // identity has no Review phase for it to be in.
349            | EventKind::BranchPreserved
350            // The repair of a preserved branch, and therefore the work being made:
351            // it puts the branch back into a state its merge path can rule on, and
352            // it happens before that branch may enter one at all.
353            | EventKind::RecoveryAttested
354            | EventKind::SessionClosed => Phase::Development,
355            EventKind::MergeQueued | EventKind::MergeCompleted | EventKind::SyncConflict | EventKind::BaseArrived => {
356                Phase::Integrate
357            }
358            // Bringing the base into the branch, or deciding not to, is the step a sync
359            // conflict is met at: the work being brought level with what it lands on.
360            EventKind::BaseMerge => Phase::Integrate,
361            // What became of a branch once its work reached the base, or once a retry
362            // of it did: the base is what decides both, so both are that work being
363            // integrated rather than made or proposed.
364            EventKind::BranchSuperseded | EventKind::BranchRetired => Phase::Integrate,
365            // Several branches brought together onto a root, for the changes based on
366            // them: integration of work that is already made, and proposing nothing.
367            EventKind::StackBaseAssembled => Phase::Integrate,
368            EventKind::ChangeOpened
369            | EventKind::ChangeDrafted
370            | EventKind::DraftLifted
371            | EventKind::DraftLiftedEarly
372            | EventKind::DraftKeptForReview
373            | EventKind::ChecksSettled
374            | EventKind::ChangeDescribed
375            | EventKind::ChangeCheck
376            | EventKind::ChangeMerged
377            // The host's merge of the change request, armed to happen once its checks
378            // pass: the change being ruled on, as `change-merged` is.
379            | EventKind::MergeArmed
380            // Taking an open change request up again at its hosted checks: the change
381            // being ruled on, at the point its first publication's watch would start.
382            | EventKind::PublicationResumed
383            // Reading what reviewers said and answering it is the change being ruled on.
384            | EventKind::ReviewCommentsRead
385            | EventKind::ReviewReplyPosted
386            // Where each change request under review stands, and the moves that keep a
387            // stack of them true to the host — a base changed, a closed draft reopened:
388            // the change being ruled on, as the host reads and writes it.
389            | EventKind::ChangeStandingsRead
390            | EventKind::ChangeRetargeted
391            | EventKind::ChangeReopened => Phase::Review,
392            EventKind::ReleaseProbed
393            | EventKind::ReleaseAcknowledged
394            | EventKind::ReleaseObserved => Phase::Release,
395            EventKind::Push | EventKind::GateRun => return None,
396        })
397    }
398}
399
400/// The phase an event of this kind is stamped at when its producer names none.
401///
402/// The kinds whose phase their producer decides are a push and a gate run, and
403/// [`Stream::push_result`](crate::stream::Stream::push_result) and
404/// [`Stream::emit_gate_run`](crate::stream::Stream::emit_gate_run) are where every
405/// one in this crate decides it. A push reaching here instead — or a line an older
406/// build wrote before there was a phase — is read at the phase a session's own
407/// stream is in, which is what all but one producer of a push stamps. A gate run
408/// never reaches here unstamped: no build wrote one before there was a phase.
409pub(crate) fn phase_of(kind: EventKind) -> Phase {
410    Phase::of(kind).unwrap_or(Phase::Development)
411}
412
413/// The id of a stored artifact.
414///
415/// This crate's own rather than the bus's: `onemessagebus` 0.4.0 carries an
416/// artifact's id as a plain string on [`ArtifactRef`], and the host seam's
417/// [`check_log`](crate::RemoteHost::check_log) answers a typed one.
418#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
419#[serde(transparent)]
420pub struct ArtifactId(pub String);
421
422/// One line of a stream, read as far as this build's vocabulary reaches.
423///
424/// The distinction the readers of a stream are held to. A line that is *not what a
425/// writer left* — torn, of an envelope version this build does not read, stamped in
426/// a way nothing can order — is a gap, and saying so is not negotiable: reporting
427/// "could not look" as "there is none" is how a report about half a record reads as
428/// a report about all of it. A line that is a perfectly good envelope recording
429/// something this build has no word for is not that. There is nothing to conclude
430/// from it and nothing missing from the read either, so it is passed over rather
431/// than announced — which is what a reader of streams a *later* build wrote needs,
432/// and what a reader of streams an earlier one wrote needed and did not have.
433pub(crate) enum Line {
434    /// An envelope of a kind this build knows, and can therefore act on.
435    ///
436    /// Boxed: an envelope carries three strings, a map and a vector, and the other
437    /// variant is a header — so an unboxed enum would make every line of every
438    /// stream cost the larger of the two.
439    Known(Box<Known>),
440    /// A well-formed envelope of a kind this build has no word for.
441    Unknown(UnknownKind),
442}
443
444/// An envelope whose kind this build has a word for, with that word beside it.
445pub(crate) struct Known {
446    /// The kind the envelope's wire `kind` names.
447    pub kind: EventKind,
448    /// The envelope itself, at the phase its producer stamped — or, for a line an
449    /// older build wrote before there was a phase, the phase its kind decides.
450    pub envelope: Envelope,
451}
452
453/// A line whose kind this build cannot name, as much of it as such a reader can use.
454///
455/// Its header and nothing else, deliberately. Whether the line is a *gap* is
456/// decided by the envelope's own fields — the version it declares, the stamp it is
457/// ordered by, the stream it belongs to — and those mean the same thing whatever
458/// happened, so every check a known kind gets this one gets too. The payload is the
459/// part the kind is the key to, so there is nothing here to offer of it.
460pub(crate) struct UnknownKind {
461    /// The envelope schema version the line declares.
462    pub v: u32,
463    /// When it was stamped, unparsed: ordering it is the reader's own check.
464    pub ts: String,
465    /// The stream it names, so attribution is asked of it as of any other line.
466    pub stream: String,
467    /// The kind it carries, as it was spelled.
468    pub kind: Kind,
469}
470
471impl Line {
472    /// One line of a stream, tolerant of its kind and of nothing else.
473    // llmlint: ignore[boundary_inputs_validated] the envelope's *shape* is validated here,
474    // by the agent profile's envelope: an unknown top-level key, a `seq` that is not a
475    // u64, a source outside the three families, and a missing required field are each
476    // refused. The two semantic checks — that `v` is the version this build reads, and
477    // that `ts` is a stamp it can order — are deliberately not here, for the reason the
478    // envelope has never made them either: what to do about one is the *reader's*, and
479    // the readers disagree. `status` reports each as a gap in its notes rather than
480    // refusing, because it is asked what became of a piece of work and must not answer
481    // "there is none" for "could not look" — and it applies both to a line whose kind has
482    // no word here exactly as to one it knows, which is what `UnknownKind` carries the
483    // header for. Deciding either here would take that choice away from the one caller
484    // that has to report rather than refuse.
485    pub(crate) fn read(line: &str) -> serde_json::Result<Self> {
486        serde_json::from_str(line).map(Self::of)
487    }
488
489    /// An envelope a reader has already parsed, read as far as this build's
490    /// vocabulary reaches.
491    pub(crate) fn of(mut envelope: Envelope) -> Self {
492        let Some(kind) = EventKind::named(&envelope.kind) else {
493            return Line::Unknown(UnknownKind {
494                v: envelope.v,
495                ts: envelope.ts,
496                stream: envelope.stream,
497                kind: envelope.kind,
498            });
499        };
500        // A line an older build wrote carries no phase, and the phase is additive
501        // inside `v: 1`, so it reads at the phase its kind decides. A `push` among
502        // them named no target, so there is nothing to recover the producer's
503        // classification from — it reads as the phase a push of the session's own
504        // branch is, which is a reading of a record rather than a claim about the
505        // push, and why the phase is stamped from here on.
506        envelope
507            .dimensions
508            .phase
509            .get_or_insert_with(|| phase_of(kind));
510        Line::Known(Box::new(Known { kind, envelope }))
511    }
512
513    /// The stream this line names, whether or not its kind has a word here.
514    pub(crate) fn stream(&self) -> &str {
515        match self {
516            Line::Known(known) => &known.envelope.stream,
517            Line::Unknown(header) => &header.stream,
518        }
519    }
520}