Skip to content

Deploy replaces the record: the lifetime in the store, reconcile's six words, an occupant is what the record wrote, and adopt leaves a record - #940

Merged
David-Noble-at-work merged 5 commits into
developfrom
feature/922-lifetime-fold
Sep 24, 2026
Merged

David-Noble-at-work merged 5 commits into
developfrom
feature/922-lifetime-fold

Conversation

@David-Noble-at-work

@David-Noble-at-work David-Noble-at-work commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

PR A of #916, the writ lifecycle schedule: lanes 1, 2, 3
and 14, one commit each, on one branch, merged with develop before opening.

  • Deploy replaces the record (#922) — a lifetime is
    one writ deploy invocation across every scope it runs, a store document lifetimes/<id>.yaml with a UUIDv7 id
    minted after pre-flight and written with the first trace; lifetimes/current names the current one. upgrade,
    adopt and decommission require a current lifetime and exit 66 without one. readback.Fold reads the current
    lifetime's runs and nothing else: the index read, the trace glob and two helpers are gone. A store written
    before this has no lifetime, so the first plain deploy refuses every occupant; one --conflict=replace deploy
    opens the first lifetime.
  • Reconcile's six words (#923) — the record is the
    desired state. linked, copied, absent, changed, dangling (the source the record names does not resolve,
    for a link or a copy; repair deploy), stale (the source resolves and its content moved; repair upgrade).
    orphan, missing, conflict, modified, modified-or-stale are gone. The classifiers decide from the link and
    the two recorded digests. The ledger records a source under the path the run read it from, the pinned snapshot,
    so the files annotation carries read_from and readback joins the source digest through it; stale had never
    fired on a real deploy.
  • An occupant is what the record wrote (#883) —
    readback.Entry.AsRecorded is the one predicate: a link whose literal endpoint is the recorded source, resolved
    or dangling, or a copy whose digest is the recorded one; the source is never resolved. Pre-flight and reconcile
    share it, so a link left dangling by a file that moved between layers is re-pointed under stop.
  • Adopt carries the platform and leaves a record (#931)
    — --platform <suffix> in the segment vocabulary; one file.move and one file.link per item; the graph carries
    the origin and files annotation a deploy's carries, and is written before the run. Two findings: a graph holding
    a flow.choose does not load back (#939), so the guard
    runs before the graph; and a registered layer is a symlink the confined root refuses to write through, so adopt
    had never worked on a real install — the layer path now resolves through its symlink.

Evidence

Each lane ran on both VMs, snapshotted first; raw logs under build/e2e/9{22,23,83,31}-e2e-*.log in the worktree.

Lane danoble-ud24-1.local linux/arm64 danoble-wd11-3.local windows/arm64
1 first plain deploy refused 166 (no lifetime); replace opened lifetime 1; a plain deploy opened lifetime 2, the first replaced; reconcile 166 linked + 1 copied, the 52 stale rows of 2026-09-22 gone; 0.46 / 0.53 / 0.56 s the same: 71 refused, 71 linked twice, two lifetimes; 18.3 / 20.1 / 12.9 s
2 absent, stale, changed, dangling in turn, nothing else; clean after a deploy absent, changed, dangling through a link (no copy deploys there)
3 a base link's source moved to another suffix directory; a deploy under stop accepted the dangling link and ran; restored the same
14 --platform Ubuntu refused (64); adopted with Linux under noblefactor-ops.Linux; the lifetime's last run adopt; reconcile linked; a deploy under stop accepted it; undone the same with Windows

Locally: make check and the eight scenarios green on every lane; the layer-journey's moved links now redeploy under
the default policy. Coverage total 63.1%; readback 82.4%, reconcile 86.1%, adopt 78.9%, cmd/internal/cli
69.2%. Nothing new over complexity 8.

Test plan

  • make check, make test-scenario, per lane
  • both VMs per lane, above
  • the owner's row after merge: install, then one writ deploy common --conflict=replace

Time

Marker UTC Elapsed
opened (first commit) 2026-09-23T14:35:11Z —
submitted (PR opened) 2026-09-24T06:01:33Z 15.4 h after opened
merged 2026-09-24T06:13:59Z 0.2 h after submitted; 15.6 h in all

Computed by the algorithm of noblefactor-ops#188 at merge.

https://claude.ai/code/session_01SqQVFZz7AdiLZkGoAcUqRW

David Noble added 5 commits September 23, 2026 16:35
…fold reads the current one

A lifetime is one writ deploy invocation across every scope it runs, with the upgrades,
reconciliations and adoptions written into it, until a later deploy replaces it or a
decommission ends it. It is lifetimes/<id>.yaml under the store, a UUIDv7 minted after
pre-flight and reaching the store with the first trace; lifetimes/current names the current
one. upgrade, adopt and decommission require a current lifetime and exit 66 without one.

readback.Fold reads the current lifetime's runs and nothing else: the run index stays a hint,
the trace glob goes, and a trace no lifetime names is not folded. A file the previous lifetime
deployed and this one does not is not in the record; on the Linux VM the 52 rows a layer
reorganization had left as missing are gone after one deploy.

A store written before this has no lifetime, so the first plain deploy refuses every occupant;
one --conflict=replace deploy opens the first lifetime.

Part of #922; lane 1 of #916, PR A.

Claude-Session: https://claude.ai/code/session_01SqQVFZz7AdiLZkGoAcUqRW
…sent, changed, dangling, stale

The record is the desired state; the layer checkout is only deploy's input. reconcile compares
the system to the record and says one of six words per entry. absent: the record says a target
is there and it is not. changed: it is there but is not what the record says. dangling: the
source the record names does not resolve, for a link and for a copy alike, a reference that
outlives its referent, reported and left because only deploy makes a new record. stale: the
source resolves and its content moved, a derivative behind its origin, upgrade's to refresh.
orphan, missing, conflict, modified and modified-or-stale are gone.

The classifiers decide from the link and the two recorded digests; the fresh-render comparison
goes. The ledger records a source under the path the run read it from, the pinned snapshot, so
the graph's files annotation now carries read_from and readback joins the source digest through
it; without that, stale never fired on a real deploy.

Part of #923; lane 2 of #916, PR A.

Claude-Session: https://claude.ai/code/session_01SqQVFZz7AdiLZkGoAcUqRW
…ngling link is re-pointed under stop

readback.Entry.AsRecorded is the one predicate. A link is as recorded when its literal endpoint,
absolutized against the link's own directory, is the recorded source, resolved or dangling; a
copy when its content digest is the recorded as-deployed digest. The source is never resolved.
Deploy's pre-flight under stop and reconcile's changed decide by it, so a link left dangling by a
file that moved between layers is writ's own and a plain deploy re-points it; occupantIsOurs,
which resolved both sides and read a dangling link as foreign, is gone.

The layer-journey scenario's step 3.3 redeploys the moved links under the default policy. The
guide and the reconciliation page say what an occupant of writ's own is.

Part of #883; lane 3 of #916, PR A.

Claude-Session: https://claude.ai/code/session_01SqQVFZz7AdiLZkGoAcUqRW
--platform names the suffix the adopted files carry, in the segment vocabulary the layer tree
matches (Darwin, Unix, Windows, Linux, Linux.Debian, Darwin.arm64); a word the matcher never
reads is a usage error, and the file lands under <project>.<suffix>.

An adoption is a write into the current deployment. Its graph now carries the origin and the
files annotation a deploy's carries, one file.move and one file.link unit per item so every link
has its own unit id, and RunBatches writes the graph before the run, which it never had. The fold
reads an adoption as it reads a deploy, reconcile says linked, and the next deploy recognizes the
link as writ's own.

Two findings on the way. A graph holding a flow.choose does not load back from the document
(#939), so the existing-destination guard runs before the graph and the chain has no subgraph. A
registered layer is a symlink into its repository and the confined run root refuses to write
through it, so adopt had never worked on a real install; the layer path now resolves through its
symlink, which is also what the record must name.

Part of #931; lane 14 of #916, PR A.

Claude-Session: https://claude.ai/code/session_01SqQVFZz7AdiLZkGoAcUqRW
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant