Changes touching this path

  • day 0: loot hosts loot f4c30e75 · dbf3dbe6…
  • evidence: crew minted and verified (#86) 1fada823 · dbf3dbe6…diff
  • normalize working tree to LF: byte-stable co-located bridge (.gitattributes -text) e58fdda6 · dbf3dbe6…diff
  • Catch up to git main 809ddfe: #260 smoke default, #271 arm64 build, loot diff (#1), ADR 0034 dock retirement (#253) 587684dd · dbf3dbe6…diff
  • Add loot verify: object-store integrity check (#19) - rehash every loose object against its address, report corrupt and missing by address, exit 1 on problems; load-free so a corrupt store can still be diagnosed aeb147b3 · dbf3dbe6…diff
  • loot verify: name each missing object's referencing change+path, and add --accept-loss lost-ledger (#335) - missing report carries provenance (MissingObject/MissingRef); .loot/lost records operator-acknowledged unrecoverable losses so verify gates CI again while new damage still fails; primary-only, store-locked 57350aa4 · dbf3dbe6…diff
  • the forge's metadata pool caps every connection it opens, so a burst queues for one instead of being refused by Postgres (#611) f2d8ee80 · dbf3dbe6…diff
  • the land-change skill reads the branch= verdict field, so a failed branch reap is no longer reported as a clean land (#650) d4753206 · 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
  • the README stops describing a third of the CLI: What works today carries the full 61-verb list regenerated from loot --help, dock/docks are gone with ADR 0034, and a guard test pins the block to the dispatch table (#1107) 0d284658 · dbf3dbe6…diff
  • the trust matrix says what each reader can actually read, and /why stops claiming the relay cannot read your code (#1130) Publishes /trust as the one canonical answer to who reads what: six readers against four tiers, with a regardless-of-tier block for path names and DAG metadata, and footnotes on the two cells whose plain answer misleads — Restricted vs the git mirror rests on ADR 0028 projection omission rather than keys, and an embargo reveal is enforced by the relay clock rather than by maths (ADR 0027, written down nowhere until now). #1089 falls out of the matrix as its ticket predicted: the /why headline was false over ordinary code, since Internal is the default and a relay reads it by design. Every other surface now cites /trust instead of answering independently, which is what let /why and /docs drift into contradicting each other. ADR 0069 records that Internal stays the default deliberately — Restricted by default would make a clone readable by nobody and empty the git projection. ADR 0068 records the at-rest design for #1138: .loot/keyring is raw oid-key pairs beside the ciphertext it opens, so the unit of exposure is the whole .loot/ directory. Carries ADR 0067 from #1132, which shared this tree uncaptured. 361b2026 · dbf3dbe6…diff
  • custody at rest seals to your own key behind an optional passphrase, unlock is a session file or an env var, and no non-interactive caller starts prompting (#1138) c8ffee3b · dbf3dbe6…diff
  • a no-remote repo converges its diverged heads with loot converge, and the diverged hint names the verb the repo can actually run (#1183) 2d9a2d31 · dbf3dbe6…diff
  • loot clone probes the host and speaks the signed forge client, so a forge URL reaches the door instead of dying at the transport, and the sealed grants arrive with it (#1162) 98f5b662 · dbf3dbe6…diff
  • content whose embargo has already lifted stops reading as sealed, because promoting due keys out of escrow is now the construction of the reader every content read hangs off instead of a doc-comment obligation six callers hoisted by hand, and deleting four of those hoists left all 2438 tests green while loot surface told an author to request a grant from herself (#1464) e59e46b3 · 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
  • an unexpected head stops being something the operator can only fold on faith, because `loot heads` lists every live head with what introduced it and how many changes it holds of its own — and since a head is a tip no other head reaches, a count of zero can only mean the head is not live, so #1477s empty head is superseded and the converge it asks for would merge nothing (#1496) 955ec9ff · dbf3dbe6…diff
  • losing one bad file stops costing you every other uncaptured edit, because `loot restore <path>...` discards per path against the same delta seam `status` reads — and it never decrypts to decide, so a path whose recorded side is sealed, embargoed, burned or merely past its grant expiry refuses the whole run rather than clobbering the readable half beside it (#1498) cd463799 · dbf3dbe6…diff
  • a named landmark stops needing a format major to carry its message, because a landmark IS an attestation under a reserved role envelope the signature already covers — and that is also what keeps a retirement from eating the thing it retires, since the attestation log is keyed by (change, attester, role) and writing both under one role made the retirement silently REPLACE the attestation rather than record beside it (#1508) 2d06014f · dbf3dbe6…diff
  • the mis-seal question gets a verb, and the reason it could not have one turns out to be that the decision existed three times: visibility_for, is_published and public_by_fallthrough were three near-identical loops over the attribute rules, so there was nowhere to hang an explanation off and the rule against writing a second matcher was impossible to honour until they collapsed into one Attributes::first_match. That collapse, not the line numbers, is this change - the line numbers were already tracked since #1329 for MalformedRule, including the convention of counting blanks and comments so a number matches what an editor shows. loot explain now names the .lootattributes line that decided a path, lists what it passed over so first-match-wins is visible rather than asserted, reports which .lootignore line covered it, and answers for a path with no file on disk, which is the mis-seal question asked before the mistake rather than after. Because explain and the real decision are projections of one traversal they cannot drift, and that is pinned twice rather than assumed: one test deletes the line explain named and asserts the real visibility moves, then deletes each line explain called skipped and asserts it does not, hardcoding nothing about which rule should win; the other runs a real capture and asserts that for every recorded path the sealed tier equals explain's token, with the ignored file absent and a positive control that the sealed set is non-empty. Eight breakages were each proved red before the tests were trusted, including giving explain its own matcher while the decision kept the old one. And loot explain . would have printed a fully furnished answer about a path that is not one, because ** matches the empty string in this glob dialect and the door rebases a bare dot at the repo root to the empty relative path, so in any repo carrying the catch-all ADR 0038 says every real repo wants it would have named a tier and a line for nothing at all - refused at the door now, with a control showing the rules really would have answered. Found by running it rather than reading it. Five censuses needed updating where the plan named two, and the two nobody would predict are the README verb block, which is a docs file failing in the bin suite, and the derived impl-width sentence in verbs/mod.rs, because adding a single inherent method to Workspace reddens a doc comment (#1521) ebd3285f · dbf3dbe6…diff
  • loot show becomes the fourth rendering over the one delta seam and grows no differ of its own: it calls Endpoint::base, ws.content_delta and render_delta, produces no hunk, counts no line and matches no content, and the pin is byte equality rather than shape - show ends with exactly the bytes loot diff --content <parent> <sel> produces in both content and stat modes, with the leftover prefix asserted to name no path, hold no hunk header and hold no disclosure line, so a second opinion about the parent or a renderer choosing its own words for the seam's tally both go red. Four breakages proved it, and a fifth attempt was a false negative caught by its author: truncating to the last newline is a no-op, so it passed, which is exactly as misleading as a vacuous test and is why it is named here rather than quietly replaced. Three of this ticket's premises were wrong. The two commands it says reading a change takes are really one command plus reading a parent id out of loot log by eye, because <version-prefix>~1 is not a form the #305 grammar spells and only HEAD~<n> is. Subject does not mean what the criteria assume: the tree defines it as the first line of the message and this repo writes single-line paragraphs, so the first run printed a two-thousand-character subject row - nothing truncates anywhere in the CLI, so rather than invent a rule the subject moves to the last header row, above the diff where git show puts the body, instead of pushing the lineage off screen. And the no-selector arm was ambiguous in a way that made the first message false: loot status prints a live working-change row computed off the disk under ADR 0030 while working_id and the @ selector both say there is none, so saying nothing is in progress contradicted the verb next door about the same tree - the message now names which absence it is, explains the live row, and points at diff and status, and show is deliberately not widened to render anchor against disk, because that delta is bare loot diff's by construction and a second verb answering it under a header claiming a non-durable version id is the duplication #1541 deleted. A merge names which parent it is taken against and prints the command for the other side rather than pretending the diff is whole. PROSE_ONLY_CEILING rises 50 to 51 deliberately with its reason written beside it, which is the mechanism landed one change ago doing what it was built for, and a pre-existing comment claiming the arity gate is a no-op for all but one verb is repointed at the property since it has been four for some time (#1492) 7ff52d8e · dbf3dbe6…diff
  • user-declared gates arrive as data rather than as hooks, and the decision the ticket owed splits rather than picking one of its three options, because the gate vocabulary is two halves with different properties. The mechanism moves down into loot-cli, since its value is concentrated in the fake - a FakeSpawner that panics on an unstubbed step is what turns no process spawned from a claim into a failure, and two fakes drift with a fake that has quietly stopped intercepting being indistinguishable from one that works; loot-first keeps a re-export shim so no orchestrator caller changed. The phase axis is duplicated deliberately because it is not the same axis: loot-first asks about two trees, one of which only a land produces, while the CLI asks about two commitments, and a shared enum would carry four variants of which each consumer must refuse two - a rule nothing checks, where two small enums are checked by the type system. A new crate was rejected on arithmetic rather than taste, since loot-first already depends on loot-cli and there is no pair needing a bridge; loot-core was rejected because only half of #1682's criterion transfers, and that half is now a census asserting the engine spawns nothing rather than a sentence. A gate runs after the capture and can never prevent it: it does not decide whether the tree is recorded, only whether it is signed or sent, so a refusal costs the signature and nothing else, which is the ordering #174 already chose. On the confidentiality boundary the absolute form of the criterion is unachievable by any mechanism that runs a command in the working tree, and saying so is better than a strong claim that will not hold - loot surface writes and never deletes, so a path whose grant lapsed stays on disk as stale plaintext readable by cat, grep, the editor and every process running as the operator. The guarantee is therefore the narrow checkable one, that a gate is granted no read the operator does not already have, held up by four facts each of which a regression would have to break first: the runner never sees a Workspace, nothing is materialized, a Step carries program args and cwd with no environment channel, and there is no shell. A fifth fact answers a different attack - the declaration is untracked, so a pull cannot write one, which is git's own reason for never transporting hooks and matters more here since ADR 0075 has the forge holding proposals. This ticket also understated the surface that already exists: loot resolve --tool hands a user command decrypted plaintext today via LOOT_BASE and its siblings, so gates are not the first such door but they are the one that opens none (#1538) 44d4869a · dbf3dbe6…diff
  • a contributor can offer work to a repo they do not own: loot propose over a three-path family - posture, propose and withdraw - and none of them is the ref-moving endpoint, which stays single because ADR 0075 refused putting the contribution path one flag away from the path that advances someone's tip. The bytes ride /stow unchanged, since the forge's stow literally calls the same push, so the batches are byte-identical and there is no new transport. One premise had to be WIDENED and the reason is a real replay: the ticket says sign the claim exactly as the head declaration does, but a head declaration names no repo and does not have to, because require_pusher catches a replay at another namespace - while a proposal is BY DEFINITION a write by a non-owner, so every repo with an open door admits the same signer, and a tip-and-base-only envelope is replayable by any observer at every one of them, in the contributor's name. The claim therefore carries the repo path and the server compares it against the URL's namespace, proved by RUNNING the replay and watching the second namespace admit it rather than by arguing it. The disclosure is an ORDERING rule and the posture route exists only to make it one: a client that learned metadata_public from the act's own answer could only ever speak afterwards, so posture comes first, the disclosure is printed second, and only then the ref read, the stow and the claim. It is pinned twice - once by a transport that records every call into the SAME trace as the output lines, so the assertion compares positions rather than presence, and once by byte offsets in real CLI stdout against a real forge - and a shut door still discloses first and sends nothing at all. Withdrawal is the tip change's AUTHOR and only them, where visibility admits three readers, because seeing is not acting and the carrier's remedies are the author's withdrawal or the owner's decline; it does not consult the door, since a shut door must not strand what is already offered, and absent and concealed refuse in byte-identical sentences. The lane refusal keeps every one of its old assertions, including that nothing reaches the forge before it fires, and gains the sentence naming the alternative. Twelve breaks were each run and read before being restored byte-identical, and the ones worth naming are the replay above, deleting the author check so a presenter can withdraw work they did not write, adding the door to the withdraw path so a shut door strands an offer already disclosed, and declaring the tip as a head so the CAS counter moves. The ref-moving path is asserted untouched rather than described as untouched: a proposal leaves generation and head set alone, and the SAME fixture then lands a change the ordinary way and the generation does move, which is the control (#1651) 83c5afa5 · dbf3dbe6…diff
  • loot merge <head> is converge restricted to one head rather than a second fold: merge_head and converge_heads both reach the graph through one private fold_onto, so the three-way content merge is the ADR 0044 seam unchanged, the conflict record is the one loot conflicts and loot resolve already read, the merge node carries a minted mechanical subject naming the head it folded, and loot undo steps it back through an op recorded off the merge node rather than off an outcome map that can be empty. What it deliberately does not do is retire anything: converge drops superseded heads on the way past, and a retirement is a change to a head the operator did not name, which is the whole of what separates this verb from the one it restricts. A flat head is refused rather than silently skipped, because naming a divergent co-version does not make divergence content-mergeable. Two premises of the ticket were wrong. The un-described-change refusal does not apply as it does to the other merges: that refusal belongs to adopt, raised inside fold_line_in, which captures and signs its own merge parent, while converge never reaches it at all because capture-first defers on any working change one branch earlier - so this verb inherits the deferral, and the refusal is unreachable by construction rather than reimplemented. And loot heads resolves no selector, it only lists, so the spelling merge takes comes from loot abandon --head, which routes through resolve_selector. A third premise is narrower: conflicts are recorded and nothing proceeds, but a single named head has no loop to stop, so that criterion is merge_tips behaviour unchanged rather than anything this change makes true. One inherited shape is named rather than altered - a deferred merge --porcelain prints nothing and exits 0, exactly as a deferred converge does, because a deferral rides the human field while the machine contract is verdict rows (#1505) 6107561a · dbf3dbe6…diff
  • loot format-patch lands as the fourth rendering over the one delta seam rather than a second differ: a header block plus Rendering::Patch, which drops the four-space body indent every other rendering files its content under and prints git's extended header, so what comes out is a real unified diff that git apply --check accepts and a real git apply reconstructs byte for byte, proved by applying it in a scratch repo and by a strict applier in the tests that checks every context and deletion line against the base at the line the @@ header declares, since a shape assertion cannot see a wrong range. Two of the ticket's premises did not survive contact. AC3 asks to omit AND NAME, and a sealed row has no name to give: the #306 rule already withholds it, so what is named is the row, its class and content address and tier and the seam's own four-way reason, and the wording of the decision promised a path this export is not allowed to print. And a rendering over seam ROWS alone is not quite sufficient for a faithful patch: the hunks come off blame::lcs_match's line space, which ate both the trailing newline and the carriage return, so the no-newline marker is recovered from the row's raw bytes instead, and a CRLF file's patch is LF, named as a limit rather than closed with the second differ this ticket was rescoped to avoid. One neighbouring behaviour is deliberately not inherited, the sanitizer: a patch exists to reconstruct a file, so caging a control character would emit a patch that rebuilds different bytes than the change recorded, silently, and content therefore leaves verbatim with the usage line saying so. Everything else is read rather than re-derived, the path set and each row's rung and the closing in-the-clear count all being the seam's, and the header is loot show's Header widened by the author pubkey and the whole message rather than a second read of the graph. That header is designed as loot apply-patch's input: its own format number on the first line, full-width ids because a patch outlives the store a prefix would resolve against, absence spelled none rather than the dash every human header uses, and the message folded RFC-822 style so that no body can collide with a delimiter. (#1512) 9b32538e · dbf3dbe6…diff
  • loot count-objects lands, and the constraint it is built under is that its number has to be the number gc acts on rather than a second opinion about it: the unreachable count and the reclaim figure are gc OWN pruner and sweeper called with dry_run, over a keep set extracted out of gc as DagRepo::gc_keep_set the way 1505 pulled fold_onto out of converge, so a census that disagrees with a collection is not a thing that can be written, and the pin measures a store, really collects it, then asserts the two halves and the sum all match. One premise of the ticket was wrong in the direction that saved work: nothing reported how large a store is as a VERB, but the totals half was already factored, since RepoStore::objects_size has been the perf record load-bearing counter since 632, so this adds no third walk of the object directory at all, it adds a caller. The extraction surfaced something gc had been getting away with: its keep set was position-dependent, because the primary own working change is the one root in neither the shared graph file nor the lane registry and verify has been adding it by hand since 1309. gc could not notice, since it refuses from anywhere but the primary, where those oids ride in the loaded graph anyway; a read-only census can run from a lane, so the root moved into the shared function and the answer stopped depending on who asks. Machine output is this verb product rather than a garnish, which is why the porcelain is one row per metric keyed by a leading mark instead of one wide row: ADR 0023 makes a column unaddable, and a census is the shape most likely to be asked for one more number. The anti-vacuity pin is the same fixture reading 1 and then 0 with nothing changed but a change naming the orphan, and the read-only pin fingerprints every file under .loot rather than trusting the op log, which would never have shown a pruned object. On this lane own store it reads 7910 objects at 146.7 MiB with 2330 of them unreachable at 36.9 MiB, which is the 1524 baseline (#1523) f10ba1d9 · dbf3dbe6…diff
  • loot config grows a second scope and the local one is untracked because of where it sits rather than because a rule says so: .loot/config.local is skipped by the tree walk at name == DOT, ahead of every ignore call, which is a stronger guarantee than the obvious alternative - a root-level file plus a .lootignore line could not have held, since .lootignore is itself tracked and a fresh loot init writes none, so the first status in every new repo would capture a per-operator default identity as internal, and the dialect has no negation to walk it back with. verified rather than argued: the same content under .loot/ is invisible to status while at the repo root it lands as internal on the very next command. precedence is one function and the merged listing is defined through it, so the file a row names and the value that --get prints cannot disagree; --get exits non-zero on an unset key, pinned through the shipped binary because a library Result proves nothing about what a shell branches on, and a flag-shaped --get value is refused rather than looked up, since the door hands a valued flag whatever follows it. bare set and unset write GLOBAL, inverting git deliberately: loot config set identity is the route init own refusal advertises as one name across every repo on this machine. the settings are read where they matter - remote is consulted by resolve_remote, so a bare push, pull, grants, pull-grants or propose now contacts what the repo names, with origin still the answer when nothing says otherwise. two of the ticket premises were wrong. identity cannot do what it asks: a repo identity is already repo-local in .loot/identity, a config key that overrode it would manufacture the exact name-to-key drift doctor checks for, and loot init can never read a local answer at all because the file lives inside the .loot it is about to create - so the live case is clone run from inside a repo, which is what the pin asserts against a repo configuring nothing as its control. and the local file is not a store artifact: ON_MALFORMED has nothing to choose between, because the key = value dialect cannot produce an undecodable value - the real hazard was an unreadable file reading as absent and falling through to the other scope, which Layer::load now refuses outright. one argv finding rode along: config is the first family whose flags may precede its subcommand word, and args[0] was wrong in both directions, so the leaf now resolves from the first token that is neither a flag nor a flag value (#1507) 2c12d0f2 · dbf3dbe6…diff
  • loot apply-patch lands as the inbound half of patch interchange, and the limit 1512 deliberately left open is CLOSED BY REFUSING: CRLF is refused on both sides, because the two halves are not the same kind of problem. A patch declaring lossy: crlf-normalized cannot reconstruct the bytes the change it names recorded - that is fidelity, read off the exporters own measurement rather than re-derived, since by the time the content is in the file it is LF. The half that actually bites is the LOCAL one: a hunks line space is LF, so writing an applied result back over a CRLF working file rewrites EVERY line ending in it, a whole-file change no hunk describes, that no report would show as more than the lines the patch touched, and that the next capture would sign. Reconstruction was rejected as a guess - the line space carries no endings at all, so an inserted line in a mixed file has nothing to decide from - and documentation as too weak for a silent whole-file rewrite. The cost is named rather than hidden: a CRLF repo cannot use patch interchange in either direction, and the remedy is a bundle, which carries content verbatim because it carries objects. Refusing is also what makes the round-trip pin true rather than approximately true. This is a separate verb from loot apply and not a mode of it, since a bundle carries objects, keys and signed change nodes while a patch carries none of those; dispatch is an exact-name lookup so nothing can collide there, and the place that DOES match loosely is verb_usage, which compares the token after loot rather than a prefix - pinned, because a prefix match would have dragged the whole apply-patch usage line into loot apply --help. Hunks apply strictly at the lines they declare, and what does not falls back to the ADR 0044 three-way against the change the patch names in its own parent row, which is the git am -3 shape and the reason that row is full width; loot-core gains one narrow door, converge::compose, because the theirs side of a patch is text at NO address while three_way_merge exists to pick between two addresses. A contended path is recorded and the run stops with the tree untouched, which makes this the sixth member of the 1506 resume family, and the census is what said so. Three of the ticket premises moved under it. The AC3 re-seal under the local .lootattributes is a SENTENCE and not a step, exactly as 1499 found for restore: the verb writes plaintext and then captures, so the tier is todays local policy by construction, and the export side already carries no tier for it to have inherited - the work was pinning it and saying it in the report. The AC4 no-partial-application rule forced the capture-first snapshot to run AFTER the plan rather than before, found by a fixture rather than reasoned out: a repo holding a path it cannot open has that paths plaintext on disk, capture_uncaptured_edits refuses to re-seal content it cannot read, and with the capture first the verbs own refusal was never reached at all. And a hunk body must be consumed by the counts in its @@ header and never by scanning for the closing separator, because a deleted line whose content is exactly a dash renders as two dashes, byte-identical to it. ADR 0082 freezes the header grammar now that a reader exists, which is why the 1786 fidelity rows went in before this ticket rather than after; the machine shape 1512 expected this ticket to name is deferred, with the reason written into ADR 0066 and PROSE_ONLY_CEILING, because it is one shape shared by both verbs and freezing the import side alone is the same half-contract 1512 declined on its own side (#1513) d5fba81e · dbf3dbe6…diff
  • loot fetch lands as the first half of pull by call rather than by imitation: the negotiate-fetch-apply loop comes out of pull_via_declaring as Workspace::fetch_via, and pull now runs capture-first, that call, then the fold, so the two verbs cannot drift about what arriving means - the same extraction merge made of fold_onto. the custody routing comes out of pull_routed the same way, so a locked fetch takes the exact fork pull takes, relay unsigned and forge refused by name, and fetch becomes the second caller of the sealing door, which the custody gate now enumerates by name instead of counting to one. safe on a dirty tree is structural rather than guarded, because capture-first sits above the extracted half, and it is pinned by byte-comparing status porcelain across the call with a modified tracked file in the fixture - the live version id is what moves, and a capture-first mutation reddens it while the path list stays identical. no re-fetch is measured rather than asserted: the test relay now records the raw wants count per round, a later pull is required to ask for zero addresses, and the non-zero ask the fetch itself made is the positive control - an ingest-free mutation makes that number read 2. what integrating would involve is read off the head partition converge executes, through a shared converge_partition, so fast-forward means no merge node rather than one head. two premises of the ticket are wrong. a subsequent converge or pull completes the GRAPH integration but not the working tree when the repo was strictly behind, because converge_heads returns before materializing on a single head and the auto-surface of pull fires only on a non-empty apply, so the content lands in the graph and never on disk while the pull prints nothing new - the same un-materialized state a no-surface pull and a locked pull leave, and fetch now prints that and names loot rehome first. and --remote is the first valued flag of six verbs rather than of this one, so loot fetch --remote --json resolved a remote named --json; the refusal went into remote_sel, where push, pull, propose, grants and pull-grants all reach it (#1514) 577ae796 · dbf3dbe6…diff
  • loot clean lands, and the two nevers the ticket asks for are properties of how the candidate set is BUILT rather than filters over one that already exists: the protected set is the union over every change tree in the graph, consulted inside the keep predicate of the tree walk itself, so a recorded path never enters a list at all, and nothing under .loot can be reached because that same walk already skips it at name == DOT ahead of every rule, which is why this file adds no second check free to drift from the first. the walk grew a WalkFilter trait so that skip stayed one line with two implementations rather than a second walk beside it. AC6 is the one that eats work and it has its own fixture: a delta compares path-and-address pairs, so a file the graph records which currently holds an uncaptured edit reads as absent and gets deleted - which is why the set is keyed by PATH and drawn from ANY change rather than the current one. a second fixture pins the other half of that word: a path only the FIRST of two changes records, which a set read off the tip or off surface_target calls unrecorded. both were run red by named mutations before being left green, and deleting the DOT skip put twenty three .loot/objects entries in the candidate list, so the pin is not vacuous. the default is a listing and --force is the only thing that deletes; -n is accepted as the explicit spelling of that default rather than a fourth spelling of the preview idea 1809 is open about, and -n --force is refused because one of the two readings deletes files. -x and -X choose which side of .lootignore the run acts on, and the rules are read by the same parser a capture uses so first-match-wins and the absence of negation hold; -e is repeatable through a new FlagSpec::values that value now delegates to, is parsed by that same parser, holds under -x the way git keeps its own, refuses a leading bang because unlike an inherited ignore file the operator is authoring it right now, and refuses a value spelled like a flag because a valued flag swallowing the bare flag after it has been the defect five times this run. the verb is prose-only and the ceiling moved to 55 with its reason beside it: the shape worth freezing is shared with loot view --prune, which has none, and the column that matters is the per-path spared-reason neither remover computes yet (#1515) c0b301ed · dbf3dbe6…diff
  • loot verify --unreachable lands the dangling report and closes the packing family, and the thing it does NOT do is walk: it answers from gc_keep_set, so the whole new computation is a SET DIFFERENCE over the scan the integrity check was already doing - no third reachability walk and no second directory pass either. 1523 refused to write the second one and this refuses to write the third, which matters because a report that could disagree with the collection it names as the remedy is worse than no report. the two directions deliberately take DIFFERENT root sets and that is the subtle half: verify own node list stays the roots for MISSING, because it carries the referencing change and path that a bare oid set cannot, while the keep set is the roots for UNREACHABLE, because it additionally holds the loaded graph and it DROPS burned oids under ADR 0038 - so using it for missing would hide deliberate destruction, and reporting a burned object as missing damage is exactly the inversion burn exists to avoid. the primary own working change is seeded once in each, by the code that already seeded it. the exit rule is structural rather than a renderer discipline: is_clean does not read unreachable at all, so garbage alone cannot fail a verify however the report is rendered, and the process pin deletes an object the listing did NOT name, chosen by subtraction, so its missing half is a positive control on its unreachable half rather than a second assertion of the same thing. size is the SPAN length for a packed object and the file length for a loose one, never plaintext, which loot cannot know without keys - and the two halves are reported APART rather than summed, because that is what keeps each one checkable against its own collector: the packed half is GcReport packed_orphans by construction and the loose half is the census unreachable figure, and the census must go on excluding packed garbage to keep the promise it is pinned to. ObjectScan now carries how each present object is stored, which costs ZERO extra syscalls because both arms already held both facts at the instant they graded an address. one residual is documented rather than hidden: an address stored both loose and packed, the window inside a repack, is attributed once to its loose copy. and the empty case prints that every stored object is referenced by a change - the exact sentence 1525 had to qualify at gc as scope drift - where here it is NOT drift, because the enumeration behind it is scan_objects and that sees packed spans too; the asymmetry is pinned as a test rather than asserted in prose. the verbs census moved to 345 and its sentence moved with it, the first run in four where that did not go stale (#1526) 061af7f3 · dbf3dbe6…diff
  • loot move lands as the sixth member of the REFIT FAMILY rather than as machinery of its own, and the whole re-anchor is ONE DELTA over tree entries: the shift, the new parent tree against the old parent, applied to every node of the moved line - so node N tree becomes old_parent then delta N, which is why DESCENDANTS NEED NO SEPARATE RULE and why the clash set is decidable from trees BEFORE anything is minted. the naming decision is the ticket own proposal taken rather than a third option invented on taste: loot move <selector> --onto <selector>, with no collision to find - loot has no mv, and a rename is something a capture RECORDS since v12 - and it is recorded in CONTEXT.md as a new Move entry that states the decision against the word rebase, which the glossary deliberately avoids. AC6 follows an EXISTING mechanism rather than a new one: split moves an entry whole and duplicate reuses entries entry-for-entry under ADR 0004, so move reuses that shape, and the structural proof is that there is NO put_sealed and NO ObjectStore::get on the move path at all - a re-seal would require acquiring a key the code never asks for. its pin carries a POSITIVE CONTROL, open_object failing before AND after, because without one an entry comparing equal proves nothing. the clash arm is the squash clash copied in shape - record conflicts, persist, op, mint nothing - and move joins STOPPING so it carries the abort, continue and force trio. two things are reported rather than papered over: the ticket parent line calls this the start of the jj rebase -r item but AC2 asks for the OPPOSITE of -r, descendants travelling WITH the change rather than re-parenting onto the old parent, and the AC as WRITTEN is what shipped. and the working change is itself a descendant, so it travels, re-recorded as a superseding version but deliberately UNSIGNED, because signing it would mint permanent history from work nobody named - which also means a clean move reports two descendants where the operator sees one, and that is pinned as true rather than hidden. eleven pins, each reddened by a named mutation with the count read rather than the word ok (#1517) 7ff4ef4d · dbf3dbe6…diff
  • loot notes lands as a SECOND WRITER ON THE ATTESTATION LANE rather than as a record type of its own, so AC3 and AC6 arrive FREE and the wire does not move: a note is an attestation carrying a reserved role, the sibling of #1508 landmark record in the namespace the TAG field was shaped to admit, so every shipped reader parses the bytes, FORMAT_MAJOR does not move and NO RELAY OR FORGE REDEPLOY IS OWED. the visibility rule is the decision and it is DERIVED rather than chosen: the attestation lane carries no key and no grant list and rides the bundle beside the change it names, so a note audience is EVERY HOLDER OF THE CHANGE including a relay holding no content key - on a wholly-Internal change that audience can already open every byte, so no text ABOUT it can tell them anything new, and that implication FAILS THE INSTANT one path is Restricted or Embargoed. so the verb REFUSES on a change recording any path sealed narrower than Internal and names the paths, while Published PASSES because it is Internal plus world, a WIDER audience. inherit was rejected on its own terms - a Change HAS NO SINGLE VISIBILITY, that is the founding sentence of the glossary, so inheriting means a key granted to the INTERSECTION of holders which goes stale on every grant and maroon - and own-visibility was rejected because it is the only option that can OVER-disclose, making the safe answer an operator choice every time on an append-only already-delivered lane. an allow-leak hatch was declined because it would consent to nothing enforceable. the gate is the unconditional FIRST STATEMENT of the write path above a match exhaustive over the three writing ops, so a fourth does not COMPILE until it has an arm, and the limit is stated rather than implied: the check is at WRITE time, a path demoted afterwards leaves the note standing, and burn is the verb for that. seventeen mutations, each red with the count read, and both risky pins carry POSITIVE CONTROLS - the version-id pin mints a real version three lines below the assertion that notes did not, and the visibility pin has a peer who provably CANNOT open the sealed object reading the note in the clear beside it, each half the control for the other. a smoke run caught a defect this change had introduced rather than inherited: a multi-line note broke the attested-by line at column zero, so the cut is made AT THE DOOR, which covers a multi-line tag message that was latent and never exercised (#1519) 0ec3069f · dbf3dbe6…diff
  • range-diff pairs by the DURABLE CHANGE ID and by nothing else, which resolves a real tension between two of its own acceptance criteria rather than papering over it: AC1 asks for content similarity and AC6 forbids comparing content across a visibility boundary, and a similarity scorer is EXACTLY the construction AC6 rules out. git scores patches because a git commit has no durable identity - loot RECORDS what git infers, and the handle survives re-snapshot, amend and refit, which is precisely what a rework is. so the pairing is exact, O(n+m) instead of O(n times m) content deltas, and STRUCTURALLY INCAPABLE of comparing content: pair takes two slices of identifier pairs, no Workspace, no keyring, no clock, no tree, and its only production builder reads GraphView, which loot-core documents as excluding anything that touches stored bytes. the evidence that the property is held by the TYPES is that the pairing tests need no repo at all. the verb is the FIFTH RENDERING over the delta seam and builds no differ: each pair body is the seam spec, the seam delta and the seam renderer, byte for byte. what the rule cannot pair it SAYS SO ABOUT rather than guessing - no change id from a legacy or git-bridge ingest, copies, which mint a fresh handle by design, and divergent handles, paired in order with the surplus named. the skip count is argued rather than waved past: its three inputs are change ids that are cleartext on every node, recorded addresses the engine already compares without decrypting, and a fact about THIS READER keys, so the most it can say is that two versions differ in paths you cannot open, which any holder computes from the recorded addresses alone. fifteen mutations, all red with counts read, and AC6 positive control pairs an EMBARGOED change and a readable one in the SAME RUN by the SAME RULE, with only the rendering differing. two findings came from looking at real output rather than reasoning: an amend records the superseded version as a PARENT as well as a predecessor, so a different-bases warning fired on every amended pair until same_base learned it, and a superseded version CANNOT BE NAMED BY A SELECTOR at all, so the revset door is the only way to aim at the left-hand side of an amend - a thing this verb can do that diff cannot (#1533) c4b0cd67 · dbf3dbe6…diff
  • the verb that reads two books stops returning after the first: manifest returned no grants recorded BEFORE it ever called all_attestations, so every repo that tagged a release without sharing a sealed path read as though it held no attestations at all - confirmed by a real run rather than by reading, a scratch repo with one tag and no grant printing nothing from manifest while tag list showed the landmark and its note. the attestation read is hoisted ABOVE the empty-grants branch, so that branch OWN WORDING now depends on it, and the grant table moves into the else. three states where there were two: no grants and no attestations says exactly that in one sentence, no grants but attestations present prints the block under the same first line, and grants present prints both. the landmark naming from #1508 becomes reachable in BOTH states that have attestations, so a record renders as a named landmark carrying its note rather than as a raw envelope. grant-table bytes and attestation-block bytes are unchanged - what changed is REACHABILITY. the neighbouring class was checked rather than assumed: this verb had exactly ONE return, the other five in the file are refusals with nothing downstream to skip, and the sibling empty-early-returns across the CLI - grants peek, purges, shortlog, oplog, lane sweep and view - are each single-book verbs whose early return skips only the rendering of the same empty thing. manifest was the only verb reading two independent books. and the run caught a VACUOUS PIN OF ITS OWN, one level inside the defect it was fixing: the first empty-case test asserted a constant AGAINST ITSELF, green for any wording whatsoever, so the mutation collapsing the two empty states came back GREEN at 8 passed 0 failed and proved nothing - both tests now assert the LITERAL strings, that mutation is red at 7 passed 1 failed, and the reason is recorded beside them so the next reader does not rewrite it back. four mutations with counts read, and the three surfaces that describe this verb - the usage line, the README and the docs page - now agree with what it actually does (#1610) 21edfeb8 · dbf3dbe6…diff
  • the opt-in telemetry channel is built, and off means no call because a paired test says so rather than a sentence loot#1658, building what ADR 0074 specified and loot#1626 shipped only the guard for. loot telemetry on | off | status | show; an append-only accumulator that writes one line per dispatched verb, the dispatch-table name, with burn and maroon collapsed to other AT WRITE TIME so no local trace of an incident exists either; a sender that runs in main after the verb has printed and decided its exit code, at most once per 24 hours, behind a 3-second hard timeout, that can never print, never change the code, and never delay a verb; a spool that IS the outbox, so show reads the same entry the sender transmits; and a 16-byte id minted on opt-in and deleted on opt-out, never machine-id. The guarantee is the pairing. One local listener, the real binary, a sandboxed config home: init, status, doctor and show with telemetry off make ZERO connections; on, the same verbs make EXACTLY ONE, and the same verbs again in the same period make no more; off again makes none. A lone assert-zero passes when the listener is broken, the binary crashed early, or it pointed at the wrong port - the exact shape of the logrotate -f check that could never fail. The endpoint override is https only with the loopback LITERAL the sole exception, enforced twice, and localhost is refused because what it resolves to is the resolver business; the refusal happens BEFORE an id is minted, so a broken override never leaves an identifier that can never be used. Clause 5 of the field rule is a property of the wire bytes, not a promise: a test parses every string out of the JSON and requires each to be a compile-time constant, a member of a fixed array, a dispatch-table name, or the hex id. The platform is a build-script constant rather than a runtime assembly from std consts, which is what the rule forbids spelled from constants. The verb emits machine output - show --json IS the report - so it joins the emitting tier rather than the prose-only debt list the ceiling forbids growing. Every count that moved was moved in the documents that state it, because the censuses read them: 129 verb lines in three sites, 82 dispatched and 80 in the table and 25 emitting in ADR 0066 with telemetry in the named list, all 82 verbs and the block in the README, the CLI page, and ADR 0074 eight-entry allowlist - because the endpoint is now the binary ONE destination it contacts rather than prints, and its ALLOWED row says under what condition. The destination census flagged three lines that dial nothing: a printed hint naming http://127.0.0.1 and the two scheme-prefix tests in check_endpoint. They are respelled rather than exempted - the check splits on the separator and compares the scheme WORD, same rule, no allowlist row - because a row is a sentence someone has to be able to defend and these had nothing to defend. The site half: POST /api/telemetry validates a report against the SAME closed sets the binary declares, pinned cross-repo by reading the Rust source, and refuses WHOLE on any field outside them. A valid report becomes source=cli rows, one per verb invocation under /cli/<verb> plus two bucket rows, all stamped with the arrival time so no ordering exists to reconstruct; the telemetry_id rides the row and expires with the 30-day partitions. It sits behind the same /api/ limit_req the beacon needed, which is why the ADR put it there. doctor prints on/off, the EFFECTIVE endpoint, last send and the spool. The notice gains the CLI tier, as ADR 0074 §11 requires: sends nothing unless you run on, the id links reports and off deletes it, what is never in a report, show prints it before you decide, and the erasure route - quote your id within 30 days, you hold the key. The test that asserted the page does NOT collect telemetry is inverted, as it was for the visitor key. 1214 lib and 118 bin tests, 51 suites, 634 site tests, budget and font gates green, rendered and read. 8a919b01 · dbf3dbe6…diff
  • loot apply-patch no longer records a conflict base equal to its ours: the header-disagreement stop took the parent content as the base, which is the ours object when the path is unchanged since the parent, and the loot resolve guard from #1993 reads a record whose ours is its base as a deletion, so a file on disk holding those bytes could be refused over. the stop now leaves such a base out. a new verb-tier test went red before the change (0 passed, 1 failed, the recorded base equal to ours) and with the filter disabled (12 passed, 1 failed), and the resolve guard doc and ADR 0080 now say the deletion reading is only as true as the records it is handed. the sweep 11 prose fixes ride along: hygiene.rs states that tests already check docs and skills files instead of naming them, workflow.md uses <jobs> for the build job cap and gives the relay and forge redeploy commands a Set-Location <scripts> prefix with the placeholder defined, afk-loop points at the brief for its placeholders instead of listing them, the rewrap leftovers in afk-loop and diagnose-push-rejected are fixed, and the README apply-patch line no longer says CRLF on either side is refused. runbook_portability and the workspace suite are green (#1999) f221956f · dbf3dbe6…diff
  • loot seek asks a repo one of three questions without materializing it: list the paths a pathspec admits, search them with --text, or read one file verbatim with --read, in the ambient repo or in another loot repo on disk named by --in and opened with its own identity and keyring; a remote name, a URL or a git directory refuse naming the map ticket that builds them (#2063 tickets 3 to 5). the question is explicit, never inferred, so a glob matching one file does not turn a listing into a read; --read refuses a machine format rather than render bytes lossily. the verb computes no content of its own: the listing and the search are Workspace::readable_tree_at, the read is path_content_at, a hit is grep::scan_file, and the spawned-binary pin holds the listing to surface --porcelain, the hits to grep -n and the bytes to cat on one fixture. the frozen shape is P path rows, T hit rows under the nearest P, one S summary row carrying the sealed-skip count and a truncated flag, and --json with contract first and target.kind drawn from the Target enum; zero rows exits 0 and a refusal alone is non-zero. the usage line, the README verb block, ADR 0066 four counts and its amendment log, the MACHINE_OUTPUT and ATTACHED censuses and the revset_everywhere lists all learned the verb, and ADR 0023 gains the #2068 amendment; PROSE_ONLY_CEILING does not move. red under mutation: the sealed count dropped (0 passed, 1 failed), truncated reported for every bound (0 passed, 1 failed), --read under --json allowed (0 passed, 1 failed), the S row omitted (0 passed, 1 failed), and a sealed path name pushed into a row (0 passed, 1 failed). no migration, no wire or format byte moves, and no forge or relay byte moves, so this owes no deploy. the workspace suite is green (3942 passed over 123 binaries, 7 ignored) (#2068) a733d45c · dbf3dbe6…diff
  • loot pipeline (#2127, ADR 0091, map #2099 ticket 3): the tracked .lootpipeline at the repo root, in the .lootattributes grain with no dependency column, one step per line as name, trigger, kind and what, where the trigger names the tree a step judges, change or main, never the moment, and the kind is run, argv through the spawn seam with {message} and {paths} as gates reads them through one shared builder gates::spawnable, or check, a loot-native gate deferred naming #2128 until the checks live where the verb can reach them, and a deferred step is a refusal exiting 1, never a pass. loot pipeline lists and spawns nothing; run judges the working tree by default or, under --version, a scratch tree of a held version written by Workspace::scratch_tree from readable_tree_at so the readability decision precedes any file, holding the version own .lootpipeline, and removed on drop, with the hand-off from Workspace::version_handoff naming the paths whose readable content differs from the first parent; --only narrows to one step and an unknown name is refused naming the declared set; status reads the pipeline/TRIGGER/KIND passes and pipeline/approve admissions trusted keys signed on a version through attester_trust, passes only. A tracked file never runs implicitly: loot new with a run step declared and no .loot/gates line reaches no spawner, pinned. Frozen shapes in ADR 0023 amendment, exit code on the shape. Measured on the release binary: a two-step run 74 ms, run --version 47 ms and a listing 18 ms on a three-file repo; on this repo of 1452 tracked files, run --version HEAD materializes the whole readable tree in 1.2 s against a 0.15 s listing. Found by that measurement, not by thought: under load a Windows scanner held a freshly written scratch file, the one-shot remove_dir_all failed and its swallowed error left plaintext in the temp dir, so ScratchTree drop now retries over half a second and names the path on stderr if it still cannot remove it. Seven censuses moved, each read off its own refusal: the verb tier method count to 387, MACHINE_OUTPUT gains pipeline in sorted position, the usage spells the list leaf as an invocation line, README block and its all 84 verbs sentence, 133 verb lines in three places, the revset placeholder lists take <change|main>, <step> and <id> and SELECTOR_ONLY takes pipeline status, and the site CLI page names the verb; ADR 0066 counts 84 dispatched, 82 in the table, 32 emit machine output with an amendment. CONTEXT.md Pipeline entry says what is built. Nine unit pins plus two in change.rs, five broken once by a named mutation and confirmed red at 0 passed 1 failed then green at 1 passed: a check made a pass, the trigger filter widened, the scratch removal removed, the approval role made a job, and the tracked file run at a finalize. loot-cli and loot-first green across 64 binaries, 2162 passed; the site verb census green. No wire, format or store byte moves, no migration (#2127) fe693773 · dbf3dbe6…diff
  • loot runners lists the runner rows a forge holds for this repo, part 2 of 2 of this ticket: name, key, the step kinds each may claim, live or retired, and when the forge last heard from it, owner-only because it is the owner configuration, retired rows included because who was allowed to run a version is what an audit asks. last seen needed a column, migration 0021 runner.last_seen_at, written by the runner door on every admitted request and throttled in its WHERE clause to one write per runner per 60 seconds, so a runner polling every second costs one write a minute; a registration never sets or clears it, so a runner registered and not started reads never seen, and deriving it from job rows was rejected because an idle runner finding no work leaves none. the first spelling of that throttle, last_seen_at <= $3 - $4, failed on Postgres 18 at the first touch, which would have made every runner request a 500 on the live forge while every in-memory test passed; the edge is computed in Rust now, and landed part 1 never wrote the column. the shape is born frozen under ADR 0023: porcelain pubkey state capabilities last_seen and the name last because it may hold a space, JSON with contract, remote and one object per runner, absent times as null, both from one value and pinned byte for byte. the verb census cost, counted: MACHINE_OUTPUT gains runners, the README block and its sentence move to 85 verbs, verb lines 133 to 134 at three main.rs sites and one ADR 0066 claim, dispatched verbs 84 to 85, dispatch table 83 to 84, machine output 32 to 33 with its named list, and two ADR 0066 lines that are dated records were left alone as the census skips them; the site verb list gains runners and its gate is green. the verb runs end to end through the spawned binary against a real forge. red under mutation, counts read each time: the touch unthrottled (1 passed and 1 failed), the door never touching (0 passed and 1 failed), the name not last (0 and 1), an absent time spelled as a dash in JSON (0 and 1), and a registration allowed to set last seen (1 and 1) once its pin moved into the store, where the defence lives, after a first attempt at the route proved unobservable; on Postgres 18 the throttle removed from the SQL went red at 0 passed and 1 failed after a green unmutated arm, each restored green. bash ci/local.sh is green against Postgres 18 (4258 passed over 136 binaries, 10 ignored). migration 0021 rides the forge binary, so the forge owes a deploy (#2158) 6dd6ae69 · dbf3dbe6…diff
  • every compiled-in and current-tense loot host moves to loot.build (map #2412, ADR 0099): loot --help names security@loot.build and https://loot.build/trust, ALPHA_PROMISE_URL is https://loot.build/install and the telemetry DEFAULT_ENDPOINT https://loot.build/api/telemetry, so the next release carries them; the site install one-liners, R2_BASE and HOP_BASE (https://dl.loot.build, https://loot.build/dl), the metrics SITE_SCOPE and own-host referrer, the disclosure links on trust, terms, privacy and known-issues, and the install, quickstart and guides pages follow, with the privacy and terms markers moved to 25 September 2026 and re-pinned since readers see the new address; README, CONTEXT.md (the forge door now forge.loot.build), the release checklist, site CI and test fixtures follow, and the two specs carry a note instead of a partial rewrite. the destination census gains build in WEB_TLDS, without which security@loot.build would be invisible to its bare-host clause, and exempts the two shipped lines that tld makes read as hosts (state.build, r.build()) by their exact text; ADR 0074 section 9 says twelve-entry to match. 37 lines still name millerbyte.com outside ADR bodies, evidence, research and scratch, each kept on purpose: dated history, millerbyte.com the site and its @millerbyte/ui tokens, the two noted specs, and relay-era tooling with no successor host, filed as #2438. census red with build dropped from WEB_TLDS, restored; site gate 819 passed and within budget; workspace suite 4559 passed over 142 binaries, 13 ignored (#2418) c5663115 · dbf3dbe6…diff
  • loot ticket new, loot ticket show and loot tickets ship as slice 1 of the native tracker (spec §12, map #2422): a new ticket module in loot-cli holds the layout (tickets/<id>, tickets/public/<id>, tickets/<group>/<id>, where an 8-letter k-z segment is always an id and never a space), the one file format of header lines, a blank line and a verbatim body, ids of 16 random bytes shown as 8 letters and minted again on a collision with any ticket at the read position, and causal order keyed on the space-stripped path, found by walking the change graph from the read position in descending generation to the change where each key first appeared, with a key only the working tree holds sorting after every recorded one. a read folds the tree at the harbor tip, this position and the working tree, so a lane sees a ticket a sibling landed without adopt and its own uncaptured one. new writes meta, the body and one waits-on edge per --on as plain files in the working tree and prints the id alone on stdout; the body comes only through --body-file, a file or - for stdin, with a leading byte-order mark dropped and CRLF made LF, and --title refuses a tab or a line break. show --json and tickets --porcelain are the spec §7 shapes, recorded in an ADR 0023 amendment; the JSON leads with contract as ADR 0023 has every --json do, the open form and the sealed {contract, id, sealed: true} alike, the spec field list naming the payload after it. a ticket this identity cannot open is an S row, and a field filter keeps it. --restricted-to and --published are left to #2428. red under eleven named mutations over sixteen runs, each 0 passed and 1 failed and restored: position keyed on the spaced path, the harbor tip skipped, a colliding id kept, the byte-order mark kept (once through the binary), a tab allowed in a header, an unopenable meta dropped, an id-shaped segment read as a space, labels folded add-only, a field filter dropping a sealed ticket, and contract left off the sealed form and off the open form. the verb census moves: README 87 verbs, 137 usage lines, ADR 0066 at 87 dispatched and 35 with machine output, MACHINE_OUTPUT, the placeholder census and the site verb list. cargo test green in two runs, 4576 passed with 13 ignored, and the site gate green at 819 passed (#2424) 1a26ff0a · 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.