Changes touching this path
- One rendering seam for the Verdict contract (#321)
bea1221c · dbf3dbe6… - Candidate 1a: verbs return an Emit shape, not stdout (in-process seam)
Every cmd_* verb now returns Result<Box<dyn Emit>, String> instead of
printing to stdout itself; main.rs's dispatcher renders the shape once with
the resolved OutFmt and prints it. This is the deepening the architecture
review's top recommendation called for: the verb interface becomes an
in-process test surface, so a test can assert on a verb's output as a value
without spawning CARGO_BIN_EXE_loot (the friction tests/emit_snapshot.rs
documents: "there is no in-process seam to call").
- emit.rs: add a Message shape (prose/silent verbs — one rendering across
every OutFmt) and make Status own its data so a shape can outlive the
verb's Workspace.
- main.rs: Verb.run signature + dispatcher; ~50 verbs converted to build and
return their shape. Machine verbs return their existing emit shape; prose
verbs accumulate their former stdout into a Message. print_step /
print_surface_listing now return String so verbs can compose them. Streaming
verbs (push, serve) keep direct progress prints and return an empty Message.
- clone folds init_repo's now-returned summary into its own output; pull folds
its post-reconcile surface listing into the shape's Human field.
- New test exercises the seam directly; the emit_snapshot characterization
tests confirm byte-identical output.
Keystone for candidates 1b (CliError) and 5 (prose-into-Shape).
1301bafb · dbf3dbe6…diff - Publish: surface the `published` visibility token across CLI/porcelain/JSON/WASM/SDK (#481 refinement 1)
082ad335 · dbf3dbe6…diff - a view step refuses over work no change records, and a primary finalize says so on stderr (#436)
a3248ede · dbf3dbe6…diff - the reader leaving mid-pipe becomes a quiet end: one stdout boundary tells EPIPE from a failed write (#870)
08caedf2 · dbf3dbe6…diff - a verb that could only publish to main unreviewed is retired: lane merge goes, its fold machinery stays where adopt needs it, and the mirror-free fold that genuinely leaves with it is named rather than glossed (#1048)
3f7635a9 · dbf3dbe6…diff - loot stops having no way to read one recorded file, because `loot cat <path> [<selector>]` resolves one path at one change through the same endpoint, key oracle and burn lookup the delta seam already resolves a whole side with — and it names which of the four absences it hit rather than printing an empty file, while binary content passes through verbatim because the third option nobody names, a lossy render, writes a corrupt file and exits 0 (#1490)
cc1384f9 · dbf3dbe6…diff - a verb that takes no positionals stops being able to say so by claiming the opposite, because `NO_ARGS` now sits beside `NO_PATHS` and a census reads every verb invocation shape out of USAGE to find the ones still spelling nothing as open-ended — which is how `loot heads junk` was accepted 88 minutes after the same hole was closed at `loot cat`, and beside it `HEAD~n` ends where the revset lexer already ended it so a range can be typed at all (#1562)
fb140e48 · dbf3dbe6…diff - a flags value stops being read as a flag, because `FlagSpec::given` asks in flag position the way `positionals` already does and every verb-local scan in both binaries is deleted behind a census that refuses the shape — which is what had `loot tag -m "-d"` signing a retirement, `loot describe -m "--no-snapshot"` silently skipping the capture, and `loot-first land --allow-publish "--skip-tests"` skipping the gate that is its whole job (#1612)
b40e1843 · dbf3dbe6…diff - tag and buoy stop disagreeing about whether a landmark is live, and the disagreement turns out to have been built by a sentence that was true of the resolver and read as true of the verb: both module docs said buoy is untouched by construction, which holds for loot_core buoy resolve and is false of the verb the operator runs, so the fix is entirely in what buoy consults and resolve is not changed by one line. a retirement annotates and never withdraws - that was already decided rather than open, since ADR 0025's #1508 amendment calls a retirement advisory, and four recorded facts each independently forbid withdrawal: there is no trustworthy order to withdraw by because newest is topological precisely since authored_at is advisory and never an ordering input; the attestation log is keyed by change, key and name so re-attesting after a retirement is a no-op, which would make retire the one permanently destructive verb on an append-only lane by a route that deletes no byte; buoy trusts peer-registry union self, so under withdrawal any registered peer could silently un-name somebody else's landmark; and the result would be the mutable ref that ADRs 0022 and 0025 exist to reject. so buoy now reports the retirement it already resolved through, in all three formats, additively - the B row and the human answer line are byte-identical to their pre-fix selves and the porcelain contract pin stayed green through a revert that reddened three others, which is the additive claim proving itself. an un-retired control runs in the same test so both-mention-retirement cannot pass on a verb that always mentions it, and four independent reverts were each proved red including one dropping the trust gate, which surfaced a stranger's validly signed retirement. no record is deleted or rewritten, so #1508's AC3 stands and its pin passes unchanged; the false sentence is narrowed at both copies rather than deleted, naming resolve as its subject (#1611)
ac8ee01f · dbf3dbe6…diff - loot log gets a machine shape, and the seam it needed turns out to have existed all along: every verb already renders through Emit, so nothing had to be plumbed - what was missing is that the prose-only verbs return Message, which collapses their structure into a String at the cmd_ site, before the format is known. So adding machine output is not teaching the renderer a second format, it is stopping a verb destroying its own data on the way out, which is why it cannot be done to forty-seven verbs mechanically and why this ticket does one. Log, LogChange and LogWorking hold the structure and render all three formats from it, consuming topology.rs by making the same two calls --graph makes and reading row, edges, elided and merge straight off the nodes - no second walk, no ancestry query, and the module never sees a Workspace, so the manifests d6be741 stopped materializing stay unmaterialized. The load-bearing subtlety is that edges are indices into the shipped array, so --reverse has to happen before Topology::of rather than after, and the pin reads edges back as version-byte pairs so a wrong index cannot pass as a right one. The empty head is why head rows exist at all: b0cffc66 records no changes of its own, so it has an H row and no change row anywhere, and without the head rows a two-head repo would reach a machine reader looking unforked. What stops the other verbs being forgotten is a census that names rather than counts, two set-equal lists over derived vectors plus a sum, reusing the existing machine_output_verbs so it cannot disagree with the USAGE census - and the debt list may only shrink. Two ADRs are amended rather than edited past: 0023 quotes its own sentence that nothing needs machine output from log or manifest and says log graduates while manifest does not, and narrows its porcelain-first framing as written for a homogeneous per-path verdict rather than a shape with nested arrays; 0066's three counts had to move in place because a test asserts them against the roster. And both numbers in this ticket's scoping were wrong the same way source_walk was wrong in 9559f0a, a grep for one spelling of a thing with two: the table holds 66 verbs and not 54 because twelve rows are verb_of, sixteen took a machine selector and not seven, and ADR 0066 had recorded the right figures three weeks earlier. Criterion four had nothing to satisfy, because the SDK never calls loot log at all (#1540)
915db92c · dbf3dbe6…diff - the fix-up a review sweep asked for, and the sweep's own premise was wrong twice: explain's porcelain row stops being two frozen positional booleans and becomes the comma-joined open set heads and log already document, with policy-file added as a genuine third flag so the growth is demonstrated rather than promised - and that flag makes a policy file distinguishable from an ordinary not-ignored path for the first time, since false true said nothing, so the #62 answer reaches porcelain at all. The pin asserts one row width AND at least three distinct flag cardinalities, which is what makes it discriminating: reverting to the two booleans leaves the width clause green because the row is still constant and fails only the cardinality clause, where a literal-string pin would have passed the revert entirely. ADR 0023 gains the explain amendment its own rule required, describing the corrected shape rather than the one being replaced, since #1540 wrote one verb at a time each with an amendment here and #1521 then cited this ADR while amending 0066 only. The stale verb_of figures turn out to sit in three places rather than the one the ticket named, and the third is inside the doc comment of the census whose whole subject is that a count is not a census, saying the table holds 66 rows where it holds 67 - so the number is deleted and the property asserted instead: a test derives both spellings, pins that 54 plus 13 accounts for the table, and reads emit.rs's warning back to assert it still exists exactly once and carries no count at all, which fired on its author's own first draft over the word two. PROSE_ONLY gains a ceiling so appending a name goes red rather than being the cheapest green, and the claim narrows to exactly that rather than inventing fifty reasons no assertion can read - the sentence is kept because it carries the argument for the list existing instead of forty-six tickets. source_walk.rs moved because the new census is a sixth consumer of the one walker and its header is generated by the test that counts them, which is the mechanism #1676 built after that header said four for too long. And this ticket argued urgency from a contract that froze on landing: explain is after the v0.4.19 tag, so no released binary ever emitted the old row and no consumer could have read one - the shape is still better, and it was cheaper to change than the ticket claimed (#1696)
0dc35b55 · dbf3dbe6…diff - ahead and behind against a remote without pushing to find out, and the thesis constraint is met as a property of the REQUEST rather than as advice: the question carries an EMPTY PAYLOAD and every comparison is local. a relay is asked POST /haves with a zero length body, a forge POST /ref with a signed envelope over zero bytes, so a repo of one path emits BYTE IDENTICAL bytes to a repo of ten thousand and the question has no room to encode paths, object addresses, or even our own heads. the two endpoints that could are forbidden on this path and ADR 0021 now records why: /wants sends our object addresses, which are per-content identifiers, so a status in a loop hands the relay a per-path edit-frequency profile of ciphertext it cannot open - ADR 0083 refusal at a higher call rate - and /offer sends our head ids, which a push may do because a push is a CHOSEN act and status is not. the pin asserts the recorded path-and-body list SET EQUAL to exactly /info and /haves both empty, asserts the two recordings equal across two very different positions, and carries a positive control that a real push through the same stub records a NON-EMPTY body; mutated into the rejected design it goes red with /wants carrying a literal 32 byte address. offline is split POSITIVELY, the site gate SKIPPED-OFFLINE rule: only reqwest own is_connect and is_timeout may be called offline, and anything that ARRIVED - a refusal, a 404, a proxy page, a truncated body - is unusable, because those are different facts and collapsing them is how a guess gets reported as a measurement. the relay answered and holds nothing is a THIRD thing and reads as declared with a count. every unknown count renders dash or null and NEVER zero, so a machine that never reached the network cannot emit a number, and log --unpushed REFUSES rather than printing an empty listing, because a listing has no row meaning I could not ask and an empty one reads as everything is pushed. the asymmetry is stated rather than faked: unpushed is exact, being the same change lane a push would send through the same ancestor_closure, while unpulled is zero exactly when every declared head is held here and UNKNOWN otherwise, since a head declaration says what the tips are and not how deep they run. no FORMAT_MAJOR bump and the reason is recorded: ADR 0019 marker exists to prevent misparse of a durable or on-wire artifact, an unasked status emits byte identical porcelain, and the R row lives behind a flag that did not exist when the shape froze. no revset predicate either, because loot-revset is handed a GraphView and a KeyOracle and nothing else (#868), so a predicate answering over HTTP would put a network round trip inside revset::select and therefore inside grep and format-patch too (#1522)
2237a331 · dbf3dbe6…diff - the delta reaches the TypeScript SDK as a SHAPE rather than as prose, and it is the FIRST INSTANCE of #1763 rule rather than a bespoke design: DeltaShape::of is a PROJECTION of the seam and nothing else - the mark is the #306 gutter rather than a new alphabet, the rung is the LineDelta variant, the counts are its own counts and the tally is the disclosure verbatim - so nothing here was hand-designed and the shape cannot drift from what the human rendering shows. three decisions carry the weight. added and deleted are NEVER ZERO where the count is unknown, they are absent, exactly where --stat calls a row uncounted, and ONE function now feeds both channels so the two cannot disagree. a sealed row WITHHOLDS the path, the from-path and the recipient list in both channels, because a path name is CONTENT under #306 - the shape refuses to leak through the encoding what the prose refuses to print. and the machine channel is ONE SHAPE REGARDLESS of --content and --stat, verified byte-identical, because those flags pick WORDS over a delta while this picks an ENCODING of it. the contract number is the shared VERDICT_CONTRACT and not a per-verb one, which ADR 0023 already answered for every shape and #1516 declined explicitly for porcelain, and an SDK test asserts diff and status report the SAME number so a per-verb version would go RED. a defect was caught BEFORE the freeze, which is the only time that is cheap: the first encoder used to_string_lossy, so on Windows a FROZEN contract would have shipped a backslash path beside a human line printing a forward slash - caught by the pin that compares the two RENDERINGS rather than asserting each is non-empty, and its unit pin uses an EMBEDDED backslash rather than a nested path, because a nested-path fixture is vacuous on POSIX. wire names stay snake_case deliberately, since a camelCase mirror would be a second vocabulary for one frozen contract and the only thing it could do is drift. nine mutations with counts read, and the ceiling comes DOWN 57 to 56 - the first entry on that list to PAY rather than be excepted (#1554)
ac5700af · dbf3dbe6…diff - loot gates --run now exits with the verdict its rows state: 0 when every gate in the phase cleared, 2 when a gate ran and refused, and 1 when loot could not run one, a program that never started or a malformed declaration, which outranks a refusal. a listing still runs nothing, and the porcelain and json bytes are unchanged, while the human sentence that said the verb exits 0 either way now names the code. the code rides the shape through a new Emit exit_code hook read at the same output boundary as bytes and notice, so gates stays in the dispatch table and no third early dispatch was added, and ADR 0076 gains a dated amendment recording the codes, superseding the #1538 exit 0 call, and noting that buoy could now move into the table while bisect dispatches early for another reason. CONTEXT.md follows. a new spawned-binary test pins both arms in each output format plus the listing, the malformed file and the codes the help states; with the dispatcher ignoring the shape code it went red (4 passed, 2 failed) while the unit pin stayed green (1 passed), with the precedence swapped both went red (5 passed, 1 failed; 0 passed, 1 failed), and with the refusal code moved the help pin went red (5 passed, 1 failed). the workspace suite is green (#1764)
b1126183 · dbf3dbe6…diff - buoy joins the dispatch table and bisect is left alone on the early path with the only reason that is still its own: #1764 made an output shape able to carry its own exit code, which was the whole of why buoy was dispatched ahead of COMMANDS, so the arm is gone and cmd_buoy returns a shape like every other verb. emit::Buoy reads ADR 0025 exit codes off the BuoyVerdict it already holds, one arm per row of that table, so the rendered answer and the code are one decision and the empty porcelain of the none row cannot come apart from its 2; buoy --nearest carries its code as a value on the message instead, having collapsed to one token with no structure to read an outcome back off; and buoy_write is deleted rather than moved, because the dispatcher outcome was already byte-for-byte the same #870 rule about a reader that left early. everything that walked the table and the buoy spec separately now walks the table alone — the telemetry note_dispatched call, the machine-output census, the verb census, the documented-flag census, the template-flag census, help_for and the completion list — and nothing is counted twice: the telemetry report is asserted to say buoy once, the table is asserted to declare no name twice, the offered completion names are asserted to hold each name once, and the census sum is now an equality with the table rather than the table plus one. going through the argv door means declaring an arity, so exactly one invocation moved: loot buoy reviewed junk refuses by name and exits 1 where an open claim used to drop the word and resolve for reviewed, which is #1562 reaching a verb that had been standing outside the gate enforcing it. every code is pinned through the spawned binary in tests/buoy_exit.rs, each outcome in every rendering it accepts. red under mutation, counts read each time: the verdict codes flattened to zero (buoy_exit 4 passed and 2 failed, emit_snapshot 12 passed and 1 failed), Message::coded discarding the code it is handed (loot-cli lib 1334 passed and 1 failed, buoy_exit 5 passed and 1 failed), the early telemetry call put back beside the one in the table (telemetry 2 passed and 1 failed, reporting buoy three times for two runs), buoy pushed onto the completion list beside its own row (loot-cli bin 125 passed and 1 failed, buoy_exit 5 passed and 1 failed), the row declared twice (loot-cli bin 121 passed and 5 failed), the row declining its arity again (loot-cli bin 123 passed and 3 failed, buoy_exit 5 passed and 1 failed), and the census sum left at the table plus one (loot-cli bin 125 passed and 1 failed). ADR 0025 records where each code now comes from and the one that moved, ADR 0076 closes the open work it had recorded and narrows the early-dispatch path to bisect, ADR 0066 moves the split inside its dispatched total, and CONTEXT.md stops naming buoy beside bisect. no migration, no wire or format byte moves and no host behaviour moves, so this owes no deploy. the workspace suite is green (4058 passed over 129 binaries, 8 ignored) (#1976)
fa68a7d3 · dbf3dbe6…diff - the filter in front of the import resolver becomes part of what it refuses, and a dated amendment gets back the figure it landed with: #2148 made the branches of imported_names refuse by default and left a filter in front of them that kept only a line starting use, and a default a filter feeds is a default over what the filter let through, so a module named on a continuation line and a pub use re-export reached no branch at all and were skipped in silence, which is the #1946 under-count surviving the ticket that closed it and then the ticket that closed that, a third time. use_items reads items and not lines, a statement being the run between one semicolon and the next and a use item being the statement the keyword stands in, so a visibility, an attribute and where the lines break decide nothing, and what would make that wrong is a statement spelling the keyword without being one, which costs a refusal naming the line rather than a reach passed over. the call walk stops deciding the same question a second way and reads the code with those items blanked by without_use_items, so an import can never also be read as a call and the use and mod line prefixes it skipped by are gone, the declaration it does look for being found by what it declares. the re-export and the wrapped item are planted as outcomes, one resolving and one refusing, beside the alias #2148 planted, and the blanking is planted with them. the ASCII identifier reading in temp_root_census is gone for census_text is_ident_char, which whole_word_matches is now bounded by too, so where a name begins has one home and the wider letter a narrow reading lets answer as a whole word is planted where that census can see it. ADR 0066 #2127 amendment is restored to 82 in the dispatch table, which is what was true on its date, and the cause is named rather than the number: the_verb_counts_stated_in_prose_are_the_ones_usage_holds held every occurrence of its phrases to the count the code holds today, records included, so the cheapest green was to rewrite the record and #1976 rewrote it; claims_only takes the amendment entries out before that census reads, an entry stating what was true on the date it carries. emit.rs stops saying buoy is dispatched ahead of the table, ADR 0088 stops walking a buoy flags constant that has no references left, three plurals about verbs dispatched ahead of the table go singular where one verb is left, the telemetry pin reads the count instead of a substring of it, since buoy 1 stands inside buoy 10, and the glossary stops spelling the secret-shaped set with two env positions where #1930 made it three. red under mutation, counts read each time: the line shape deciding again which items are read (loot-cli lib 6 passed and 2 failed), an item ending where its line does (7 passed and 1 failed), the shared boundary widened to admit every character (loot-cli lib 4 passed and 4 failed, temp_root_census 1 passed and 2 failed) and narrowed back to ASCII, which nothing caught before this (temp_root_census 2 passed and 1 failed), the ADR records read as claims again (loot-cli bin 0 passed and 1 failed at the control, and with the control stood down 0 passed and 1 failed naming 82 where the code says 83), the cut widened to take every line (0 passed and 1 failed, the phrase it guards no longer stated), and the count reader stopping at the first digit (telemetry 3 passed and 1 failed). item 6 is a correction to two landed commit records and is reported rather than made. no migration, no wire or format byte moves and no host behaviour moves, so this owes no deploy. the workspace suite is green (4062 passed over 129 binaries, 8 ignored) (#2156)
54167f6e · dbf3dbe6…diff
Renames are not followed. loot's tree maps a path to an address, so a rename is a delete and an add. This list is the history of the name, not of the bytes.