Changes touching this path
- the forge learns what a proposal IS, with no verb and no route: migration 0011 adds one table keyed by the tip's durable change id and scoped per repo, with no server-minted id, no base column since the declared base is CHECKED rather than stored in generation_expected's own shape, no extent table because a stack is derived and cannot be declared, and no index because both reads are answered by the primary key. The extent is the walk from the tip along parents to the landed frontier, and landed means the closure of ref_head over parents rather than everything the repo holds - a /stow having written repo_change is deliberately NOT landed, which is the difference between the two admission refusals doing their job and failing open. Refusal one is that the walk must terminate at landed changes, and it is an anti-abuse boundary rather than tidiness: an unrooted stack lets any party donate arbitrary unrelated history into someone else's namespace, unbounded, on a host whose quota producer still has none. Refusal two is that every change in the STACK and not merely the tip carries change_id, author and signature, all three of which are NULLable today because a legacy or unauthored change has none - no durable id is unidentifiable, no author is unattributable, no signature is not a contribution under ADR 0041. Each refusal was proved by deleting it and watching the donated or unsigned row be ADMITTED, and a third breakage is worth recording because it does not fail open: swapping the repo-scoped graph lookup for the global one still refuses eventually, but only after the walk has read another tenant's change_node row and it then blames the wrong thing. The ref_head pin is the decision itself and is asserted at three levels - a before-and-after on the declared head set with controls proving the proposal really is on file and its tip really could have been a head, a check that the migration text never names ref_head guarded by a non-vacuity read of its own prose, and a database-level count over a cluster only the shipped code ever wrote, where proposal tips that are a head of their own repo is zero. The reaper was CHECKED rather than assumed and the answer is the ticket's most important output: #493 is decided and not built, and its live set as specified roots on ref_head plus live publications plus un-acked grant blobs - so an OPEN proposal, which is outside ref_head by construction because that is the decision, is reachable from none of them and would be swept, deleting a contributor's offered work while the maintainer was reading it. ADR 0075's terminal sentence is right and its open half needs proposal.tip as a root, argued once in an amendment and filed as #1720. Visibility is a pure predicate and a concealed proposal reads as ABSENT rather than forbidden; no view was added at all, so the invariant that no forge_read view names grant_log or repo_member survives untouched and the reader holds nothing on the base table. The third viewer, the presenting proposer, is a deliberate widening over ADR 0075's letter of two and is recorded as an amendment rather than a comment: the presenter supplied the whole stack so existence discloses nothing they do not hold, and excluding them makes ADR 0075's own named feature - carrying a colleague's signed change - produce a row its creator cannot see (#1649)
bc73a1dd · dbf3dbe6… - may_propose is born beside may_push and provably does not widen it: the write rule stays exactly as narrow as it was, and the pin is not a sentence but #927's whole matrix - five signers across an accounted and an accountless namespace - run through require_pusher with the contribution door SHUT and again with it OPEN, demanding identical rendered answers including the message text. That covers the rule's entire reachable input space with respect to this change, because require_pusher reads signer, owner_pubkey and account_of and nothing else, and the break that proves it is the conflation a careless implementation would actually write: consulting the door at the top of require_pusher collapses the whole open column to ok, which IS a stranger moving the owner's ref, and it is quoted in the test's failure rather than described. The door is two halves in order, the repo's own accepts_proposals and then a LIVE account_key row, returning Result rather than bool so the refusal sentence is the product; retirement bites for #927's reason, since a rotation retires a key so a leaked one stops writing and a proposal is a write. The account tier is READ and never written - account_tier.rs survives completely untouched, and rather than assert that, its scan was proved to reach the new files by planting an UPDATE of tenant_blob in the migration and watching it name the file. The setting is migration 0012's boolean defaulting to false with no backfill and no wire caller, so it ships CLOSED on every repo, and that is recorded in the ADR amendment so nobody later reads a shut door as a bug. It cannot be derived, proved four ways rather than argued: a behavioural case over both stores that makes the four tempting facts true - the repo has a tip, holds changes, is metadata-public, publishes a path, each asserted by its own control first - and then re-reads the flag and finds it still false; a DDL check that 0012 contains no UPDATE, GENERATED, TRIGGER, SELECT or CASE WHEN and names none of the tables an inference would read, guarded by a vacuity check and the inverse read that its prose still names them; the DEFAULT plus absent backfill; and the upgrade shape run against a REAL database, where a repo that is both metadata-public and carries a live publication comes out of 0012 shut. Two things are recorded because they were caught rather than avoided. An existence-oracle guard initially passed WITH the oracle planted, because the fixture repo had never been ingested to and so was the same state as an absent namespace to every trait method - it needed a real tip plus a control asserting the two states differ before the break went red, and the reason is written beside the helper. And account_of has THREE consumers rather than the two a pre-existing header credited it with, having missed purge::same_live_account, untrue since #927 - so all three sites now NAME the consumers instead of counting them. The Postgres run earned its keep independently: a conformance case omitting put_object before publish is tolerated by the in-memory tables and rejected by a real foreign key, so memory alone would have shipped it (#1650)
8403defb · dbf3dbe6…diff - the third sweep's fix-up, and the worst of what it found was a doc block that had been separated from its subject by an insertion: proposal_of landed BETWEEN changes_of's comment and changes_of, so thirteen lines ending in a cross-tenant isolation warning - that without the repo_change join a named id from another tenant's repo answers with that tenant's whole tree - came to sit on a row decoder that makes no join at all, while the function that actually carries that join had no doc. Nothing was broken and that is the point: a tenancy guarantee attached to code which cannot provide it is a claim nobody can check, and it is now back on changes_of with proposal_of saying explicitly that it joins nothing and that the scope belongs to the query which produced its row. Two authored counts go the way #1650 already sent account_of's consumers, from counting to naming: server.rs said authorization is three rules where require_metadata_access is a fourth and IS a function in that file, while the AccessClass rule listed among the three is not a function there at all but is applied in readable_rows, the seam both /offer and bundle share - and the self-justifying sentence explaining why the count was written down is deleted rather than updated. CONTEXT.md's copy turns out not to be merely authored but WRONG, saying second caller where there are three, one entry away from the account_of entry #1650 had already fixed the same way. Two more sites carry the same defect the ticket located in only one: store.rs calls the four proposal methods writes where two are reads, and its list of the repo-scoped group's non-CAS writers was missing set_accepts_proposals entirely, while a second passage both mis-describes the writes and double-counts the refusals, the walk BEING refusal one. #1650's fourth not-derived proof was written on its issue as if pinned when it had been a manual run, so it is pinned rather than downgraded: a reusable prefix harness applies migrations up to n against a scratch database, plants a repo that is metadata-public and holds a live publication and is old, applies 0012 and finds both repos shut - proved red four ways by appending a real backfill to 0012 each time and reverting, with every migration file verified byte-identical afterwards. That fixture also tripped the repo's own tenancy guard, because planting repo_member to cover the membership derivation made account_tier.rs report a Rust producer of the account tier, which is #753's boundary doing its job - so the arm was backed OUT rather than the tripwire evaded, and the membership derivation is recorded as argued from 0012's text rather than from rows, covered by no row-level test before or after. The DDL guards' comment stripper could be blinded by ordinary SQL, since it cut at a double dash with no awareness of string literals and those guards are what prove 0011 and 0012 do not backfill; it now skips quoted regions using the checksum lexer's own primitives, so there is one answer in the file to where a literal ends, and its five cases each carry a control that runs the OLD parser and REQUIRES it to lose the bytes, so a case discriminating nothing cannot be added (#1723)
1abd431b · dbf3dbe6…diff - the view invariant stops being a name filter and becomes a property of the schema: grant_log_is_refused_in_every_form already enumerated pg_views rather than a literal list, but filtered it to the forge_read name prefix, so the eleven forge_owner views migration 0009 creates sat outside it entirely and 0009's own loudest paragraph - that the owner read surface carries no key material, not published_key, not grant_log, not grant_inbox - was prose with no assertion under it. It now holds over EVERY view in public, and the contrast is COMMITTED rather than transcribed: a new case plants a leaking owner view in a scratch database, asserts the exact violation sentence, runs the pre-1727 query beside it and REQUIRES it to still read zero, then drops the view and requires green - so the case cannot later degrade into one that would have passed before. The ticket was wrong about one table, and implementing it literally would have shipped a permanently red test: published_key is not named by no view at all, since forge_read_published_key has named it since migration 0003 as the anonymous tier's key path, gated two hops back on metadata_public plus a live publication. It becomes an exact set instead, which is strictly stronger than the absence the ticket asked for, because it fires both on an owner or member view GAINING key material and on the read view LOSING its definition. repo_member is expressed the same way and is correct before and after #1726 with no edit, because both sides are computed from the same live catalogue - today both are empty, after #1726 both become the member family - and it is not vacuous today either, since the planted owner view puts a name on the left and none on the right. The vacuity guard runs first and is what makes the rest mean anything, because every assertion here is an absence and an absence is true of an empty result: it asserts the read and owner families are both non-empty and that one view from each is present BY NAME, proved by pointing it at a second database migrated only to 0008, where it answers that the family this ticket exists to cover is the one that is missing. The counts are verified from the catalogue rather than from the ticket - eleven owner views, fifteen read views, zero member views, twenty-six in public - and the opt-in limit is stated rather than fixed, in the register #1734 set hours earlier, naming both tests, the env vars they need, the zero LOOT_FORGE occurrences under the workflows directory, and #1735 for the build that would change it (#1727)
19f9c87a · 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 - a proposal row gains an owner, because it had none: /propose asked whether the signer may propose here and never whether this ROW was theirs to write, so both stores upserted unconditionally with the state reset to open and only landed excluded - which made the ban-and-quota handle rewritable by any account holder who could present the same change id, and made the author's withdrawal reversible by a stranger. It was proved by running the hijack rather than by reading, with a throwaway witness that printed the takeover happening: state Withdrawn with the presenter as proposer before, state Open with the hijacker as proposer after. The rule is the party whose act would be overwritten and nobody else, and it is refusal FIVE because it needs the change id the byte refusals produce. Open lets the PROPOSER amend, which is the carrier re-presenting an updated stack and is the legitimate case the rule cannot simply forbid, so it carries a positive control. Withdrawn lets the AUTHOR alone reopen, because a withdrawal is the one-actor decision withdraw already makes and letting the carrier undo it reaches that decision from behind. Declined lets EITHER party, and that asymmetry is argued rather than assumed: the decline is the OWNER's act and an owner cannot propose, so a stricter rule would leave a declined change id with no exit at all, which is precisely ADR 0075's honourably re-proposed arm. Landed is untouched and stays the store's own invariant rather than being restated. Every party is read from the row ON FILE and never from the presented tip, which the caller chose. The refusal is kept from becoming an existence oracle twice over: concealed and visible-but-not-yours collapse into one sentence that names no term of the row, asserted byte-identical across two keys and both settings of metadata_public with a loop proving it names no proposer, author, tip or state - and the structural half is the stronger one, since both entitled parties are already standing readers, so the write gate is a PROPER SUBSET of the read gate, pinned with the owner as the control because the owner reads and is refused writing. One instruction was subtly wrong and is recorded rather than followed: withdraw pairs ABSENT with concealed, but /propose has no absent refusal at all since an unused change id is ADMITTED, so the assertable pair is concealed against not-yours and the residue - that being refused says the change id is taken - is stated in the amendment instead of being claimed away. The disclosure stops overclaiming: it said nothing has left this machine yet while a SIGNED posture read had already gone and a test welded that sentence, so it now names what went and what it told the forge. And burn stops calling a proposal a push, which is the rule disclosure.rs states two files away, with is_disclosure and the tier token both untouched and a test asserting the prose moved where the token did not (#1746)
db0f3142 · dbf3dbe6…diff - the last sweep's fix-up, and the item with teeth was a doc that told its reader to make the edit its own route file forbids: the component said it was exported so the route could load it LAZILY, where the route records that lazy was measured and REJECTED at 581 bytes against 270 and says in as many words not to fix it back without re-measuring. It now carries the route's true reason - so a test can render either half without a router - and marks explicitly that this is not a lazy split, pointing at the route for the readings rather than copying its numbers. The receipt showed eight hex characters wearing the class the module reserves for a FULL fingerprint, on the one surface whose whole purpose is an out-of-band fingerprint comparison, with adjacent copy inviting a comparison eight characters cannot support; it now shows the full fingerprint, derived in the accept from the bytes the apply itself re-verified rather than re-shortened in the surface, so the receipt cannot be rendered from the offer the apply was meant to re-check. Dropping the class and keeping eight was rejected because it would make the receipt say LESS about the one thing being checked. Rendered bytes did not really move - three gate builds read the same surface at 199873, 199872 and 199873 against 199872 recorded, and the whole sixty-route spread is minus seven to plus four - so nothing was re-recorded and the two bytes the ticket asked about turn out to be the instrument rather than an unaccounted byte. CONTEXT.md's list of account_of's consumers stops being a third copy and POINTS at the header that owns it, naming the one consumer #1746's own restatement dropped - which is the lesson of this fix-up, since that restatement was itself the remedy for a count-drift finding and drifted in the same way. The budget note is corrected as PROSE with no ceiling and no measurement touched: uniformly on every one of these surfaces was authored from a single weighing, and one surface moved the other way and had its ceiling LOWERED after its page lost six entries. The decline receipt moves into the register module that owns the vocabulary, with an absent-phrase list and a RENDERING control, because an absent-phrase list over copy that never renders proves nothing. And a count of refusals is removed rather than corrected: the numbers are labels for rules and do not line up with the code, since the first propagates from the walk and the fifth leaves from more than one site with different sentences (#1751)
571d3533 · dbf3dbe6…diff - a move becomes a recorded fact instead of a later guess, and the premise this ticket rested on was false: #98's object reuse is keyed by PATH, so a moved path is absent from the outgoing tree at its new key, falls through to put_sealed and gets a fresh address - the same object under a different key was not a fact waiting to be read off the tree, it had to be made true by extending the reuse across the move, and everything else follows from that. The rule is that a move is recorded only when the path's sealed object survives it, so a move whose content also changed in the same capture window shares no object, records nothing and is two rows, while a move and then an edit across two captures of one change composes and reads as one row saying the content also changed. Empty content and any ambiguity are refused for the same reason the whole design exists: zero bytes equal all zero bytes, and choosing between two vanished twins would make a signed fact depend on iteration order. On the boundary the render side compares nothing at all - resolve reads keys and never an address, a visibility or a byte, pinned by running it twice over trees that agree on every key and differ on every address at three unopenable tiers, with a positive control beside it so agreement is not evidence it answers nothing. The capture side compares once: it opens a vanished object with this identity's own keys, fail-closed so an unopenable object is never read or hashed, and pairs only against an addition at an identical visibility and publication tier, with the digest living for one call, never an address, never stored and never on the wire. What a relay newly learns is that two keys held equal ciphertext, which recording a move states in plaintext anyway since tree paths are plaintext at every tier, so it is inherent to the feature rather than the ADR 0004 oracle - written into that ADR rather than left in a message. Renames ride the label seam change_id and authored_at already use: covered by the finalize signature so a relay cannot rewrite a move, never folded into the version id so two peers reaching one tree by different routes still agree on its address, which is why every existing id and signature is unchanged and a v11 store reads with no move recorded. That was tested rather than argued, against a real store built by the shipped binary, where the old move still renders as a delete and an add in the same repo the new one renders as a rename, because inferring the old one would be the equality oracle arriving through the compatibility door. Three further things were wrong and are corrected rather than worked around: ADR 0019 says an additive change bumps the minor and has never described this project, since the minor is still zero and all five additive changes took the major; plan_moves own doc comment claimed the tier is checked before the digest, three lines above code that does the lookup first, when the property actually holds by the stronger route that nothing unopenable enters the index at all; and the empty-content refusal was written on both sides of the pairing, where each made the other unprovable and removing either left the test green - a duplicated guard is not belt and braces, it is two guards neither of which can be shown to be doing anything (#1539)
3c7e029b · dbf3dbe6…diff - propose refusal 5 and its write can no longer be separated by another writer: record_proposal takes the row its caller read and writes only if that is still the row on file, compared as a whole row, answering false otherwise, and admit reads again and decides again when it does, so a caller who loses the race meets refusal 5 against the winners row and the rule stays in admit rather than the store. the reference store compares under its lock, and the postgres driver compares and writes in one bound transaction, reading with FOR UPDATE and creating with ON CONFLICT DO NOTHING; no migration. two conformance cases hold a second writer between the authorizing read and the write, one with two concurrent proposers and one with the author withdrawing, and both went red on the old code on both stores, with two proposes admitted where one is right and the withdrawal undone; a third pins the compare itself on both stores, and a postgres case holds the row from another session inside the call and went red with FOR UPDATE removed. removing either store compare or ignoring the answer in admit also turned cases red. ADR 0075 gains a 1747 amendment and CONTEXT.md says the race is closed. the loot-forge suite is green against a throwaway cluster with the CI env block, and the rest of the workspace is green bare (#1747)
14232fd5 · dbf3dbe6…diff - withdraw and its close can no longer be separated by another writer: close_proposal takes the row its caller read and closes only if that is still the row on file, compared as a whole row, answering false otherwise, and withdraw reads again and decides again when it does, as admit has since 1747, so an author whose tip a carrier moved in between meets the author check against the moved row. the reference store compares under its lock and the postgres driver reads the whole row with FOR UPDATE in the transaction that already read its state; no migration. a conformance case holds a carrier amendment between the read and the close, and it went red on the old code on both stores (0 passed, 2 failed) with the withdrawal closing a row whose tip another key wrote; a second pins the compare on both stores, and a postgres case holds the row from another session inside the call and went red with FOR UPDATE removed. ignoring the answer in withdraw, or removing either store compare, also turned cases red. binding_census now opens a fn item only outside another fn body, found by counting braces outside the string and char literals and comments it recognises, and fails a file whose count does not close; a planted pg/planted.rs method holding a nested fn before unbound SQL went red under the new census (2 passed, 1 failed) and stayed green under the old one (1 passed, 0 failed), and a fixture case with unbalanced braces in literals went red on the old census and on the new one with its string or char handling removed. the visibility fixture now covers pub(super), pub(self), pub(in path) and private methods, the stale upsert prose and the statement count in pg/meta.rs are fixed, the misplaced memory.rs comment moves to its arm, CONTEXT.md points at the matrix doc instead of a column count and adds the 1955 caveat, the evidence entry calls the 1956 mechanism suspected, the rewrap leftovers in model.rs, ADR 0078 and conformance.rs are fixed, and ADR 0075 and CONTEXT.md record the withdraw follow-up. the loot-forge suite is green against a throwaway cluster with the CI env block and bare, and the rest of the workspace is green bare (#1959)
e3d4cd50 · dbf3dbe6…diff - a read of a proposal no longer answers with the offering refusal when its tip has stopped reaching the landed history: propose::read runs the same walk as stack_of but reports a failed walk beside the row as propose::Underivable, which names the tip and what the walk found and no key, while admit and stack_of still return ForgeError::Unrooted at WARN with the presenter. the read-back in handle_propose answers an underivable stack as a 400 at INFO instead of Unrooted at WARN, so the status is unchanged and only the body and the log level move on that path; no client parses that body. a new case moves the head set under an admitted proposal, asserts the offering refusal unchanged as the control and the read as a fact about the row, and went red on the old read (0 passed, 1 failed), and again with the offering refusal weakened or the read rendered as Unrooted. ADR 0075 and CONTEXT.md record it. the loot-forge suite is green against a throwaway cluster with the CI env block, and the rest of the workspace is green bare (#1748)
3bd8f756 · dbf3dbe6…diff - sweep 5 fix-up: the lane usage refusal no longer prints a run of spaces, because the rewrapped literal gets its line continuation back, and the subcommand gate test now asserts that no line of that refusal holds two spaces past its leading indent, which went red on the old text (0 passed, 1 failed). the cli smoke deregister case writes its seed with an escaped newline again and spawns on one line. rm and rm --deregister now share one owner-scoped lookup and one entry removal, and deregister drops a redundant exists check; disabling the shared ownership guard turned both the rm and the deregister test red (1 passed, 2 failed), and the verb tier width moves to the derived count. the census lexer reads a C-string raw literal, cr with or without hashes, as raw, with a fixture that failed first on a brace count that did not close (3 passed, 1 failed). ADR 0035 gains a dated amendment for --deregister and for keeping the entry of a held lane, the verb tier doc stops narrating per-ticket widenings as a decision beside the #1922 tripwire one, and the proposal loop argument in propose.rs and ADR 0075 is stated as a rule about any route that calls record_proposal or close_proposal instead of naming the routes. doc fixes: the LANE_BARE doc points at the union test, remove_lane says --reap-others also authorizes --deregister, the Resource Monitor advice is hedged, store.rs loses a method count and corrects its upsert heading, ADR 0075 says the old read would have been refused, and rewrap leftovers are reflowed. the workspace suite is green, and the loot-forge suite is green against a throwaway cluster with the CI env block (#1968)
15bcde31 · dbf3dbe6…diff - a retired namespace key no longer learns that a metadata-private repo holds a proposal: propose::visible_to decided ownership with a byte compare against repo.owner, the shape #1955 took out of AccessClass::of, so the withdraw route, which asks that gate before the author check, refused a retired namespace key as not the author with a 403 where a key that is no party to the offer is told no such proposal with a 404, and the difference between two refusals discloses the fact metadata_public governs. the gate takes AccessClass::may_read_metadata whole now and compares no pubkey of its own, so the two read gates in loot-forge ask ownership in one place, and the answer is a live or accountless namespace key, a live key of the account that holds the namespace (#1744) and not a retired key (#1955); the two standing readers, the tip author and the presenting proposer, are unmoved and still need no account. taking the class whole widens the gate to a live key of a member account, deliberately and pinned: may_read_metadata is the same axis, a member already pulls every message, tree and author in ref_head, and no content moves, where an account holder with no membership row on this repo still sees nothing. the other half the ticket named, a rotated owner treated as a stranger, is latent, as the correction on the ticket established: no route reads or lists proposals, propose::read runs in production only for the presenter, propose::list has no production caller, and on withdraw the successor key only got a different refusal. the class is resolved once per read, per listing and per withdraw attempt, outside the listing row loop and only for a row that exists, and a party to the offer pays it too. red first, on the unchanged predicate: the three new propose.rs cases went red over that selection (24 passed, 3 failed), the read arm handing a retired namespace key the whole proposal view where a stranger reads none and the withdraw arm answering 403 against 404. with the old byte compare put back as the class answer the four in-memory cases go red (400 passed, 4 failed) and the new member_class.rs case against a throwaway cluster goes red (4 passed, 1 failed); with the class dropped so the flag answers alone nine go red, the write-gate and one-sentence pins among them (395 passed, 9 failed; 4 passed, 1 failed). ADR 0075 and ADR 0077 carry the #2002 amendment, CONTEXT.md names the class, the cost and the widening, and AccessClass::of stops claiming every caller reaches it through serve::Reader::new. one flake was found on the way and fixed: pg::tests the_migration_lock_is_one_lock_for_the_whole_cluster probed the advisory key once after dropping its own hold, and hold blocks, so a queued harness takes the lock as it comes back, which was red on both runs of a loaded workspace suite against a cluster and green on three runs of the lib suite alone; the probe waits for the release now, bounded, and still fails on a leaked lock (0 passed, 1 failed after the whole wait, run alone). no migration, and the forge binary changes, so this owes a forge deploy. the forge suite is green against a throwaway cluster with the CI env block and LOOT_FORGE_TEST_REQUIRE_DATABASE (474 passed over nine binaries, no skip note), and the workspace suite is green against the same cluster (3838 passed) (#2002)
eed4d01c · dbf3dbe6…diff - sweep 16 fix-up: the removal census header stops denying a spelling its own fixture holds, and the at-most-one-class claim the no-cache decision rested on is replaced by the worst case a contended withdrawal really pays. the census header said it does not strip block comments or string literals and that there are none of either, while the classifier fixture below it spells six removals in string literals, addresses among them, and the expected set names that fixture because of them; the bullet now says the spellings exist, that the set names the fixture for that reason, and that one arriving in a function the set does not already name arrives red where one inside a function it does name would not. the ticket said five, which is why the corrected sentence carries no count at all. the same header said #1897 gave pack_every_loose_object its address so this census could see it, which it cannot: that site asks the helper, spells no removal and is correctly absent, so what the address buys is that a regression there spelled by address would be visible, and one spelled the old way still would not. ADR 0077 and ADR 0075 both said a request resolves at most one class and rested the no-per-request-cache decision on it, which the comment beside the gate refutes: the existence gate is called inside the withdraw retry loop, so a contended withdrawal resolves one per round. the cache question is re-answered on that: the worst case is rounds times one resolution, the loop carries no round cap so it is unbounded in principle, and what bounds it in practice is who can turn it, since a round is lost only to a write another party committed to this row, the production routes that write over an existing row admit only parties to the offer first, and most such writes end the loop rather than prolong it, an amendment away from this signer refusing at the author check and a close making the row terminal. the resolution stays per round rather than cached, because a cache would pin a class for the life of a request where a round decides against the class as it then stands, and the new conformance case the_withdraw_gate_resolves_a_class_for_every_round_it_turns is the run under that sentence: an uncontended withdrawal as the control, an amendment inside the window, and the contended one resolving twice as many classes as the control. with the resolution hoisted out of the loop it goes red against the memory store and against a live cluster alike (0 passed, 2 failed, left 1 right 2; 2 passed restored). ADR 0075 also stops saying the standing arms answer whatever the class says, which inverted its own rule: they admit independently of the class, the predicate asking the class first and falling through to them. visible_to takes the class and the key as one Viewer now, private fields and one resolver, so the pair cannot be mismatched: a literal outside the module is E0451, measured, and the read-cost claims are unchanged at one resolution per call of read, list and the withdraw gate. the evolog human doc said the cell is blank where the message is empty, the fourth home of the wording #2023 moved, and a blank first line blanks it too, with a spawned pin over the human column and an ordinary subject as its control (5 passed, 1 failed with the whole message back in that column). census mutations: a bare removal by address put back in pack_every_loose_object reddens the census naming that function (2 passed, 1 failed), and the fixture dropped from the expected set reddens it too (2 passed, 1 failed). no migration, and the forge binary changes, so this owes a forge deploy; the live forge suite is green against a throwaway 17.0 cluster with the CI env block and LOOT_FORGE_TEST_REQUIRE_DATABASE (476 passed over nine binaries, no skip note) and the workspace suite is green (3846 passed) (#2028)
3168156f · dbf3dbe6…diff - review sweep 17 fix-up: the separator-rule census checks what its name claims now and reads every caller of a pattern door, two more callers ask policy::unix_separators instead of answering the separator by hand, and the one class of path whose protection depends on that answer is pinned rather than lucky. clean::Sieve::keep spelled the replacement itself and compared the key against the recorded store keys and against an Ignore built from operator-typed -e values, the #1859 class in a third file at a verb that DELETES what it declines to protect; ferry::ingest_commit did the same for the .lootignore and .lootattributes it reads out of a git commit, a fourth home the ticket did not name and one that is inert because git hands over /-separated paths. both ask the normalizer now, whose body is the expression each replaced, so no path moves - measured through the spawned binary before and after on a scratch repo over ten invocations, -x, -X, the default, three backslash-spelled -e values with their slash twins, and a --force run with the tree it left, whose transcripts are byte-identical. what was NOT pinned is the half that matters: the protected set holds /-separated store keys while the walk hands keep a native rel, so only a recorded path with a separator in it can catch a key nobody normalized, and the clean fixture held none - sub/recorded-deep.txt is in it now, and let key = rel.to_string() reddens four cases over that file, the removal matrix among them, having taken a recorded path as a candidate (10 passed, 4 failed), where the whole file was green under that same mutation before. the census is renamed and derived on both halves rather than trusted: the files are every .rs under this crate src, production halves only, and the doors are read off policy.rs, a function there taking rel or path as a &str, so a new door joins by existing; an offence is the pair per function rather than the spelling alone, because a backslash pass on the way to a display line or a map key is the other axis and a spelling-only census would have to name its exceptions. mutations: the hand-spelling put back reddens it naming src/clean.rs fn keep, and put back in ferry naming src/ferry.rs fn ingest_commit (0 passed, 1 failed each); the door reader blinded reddens its anchors; the needle blinded reddens the fixture (left empty, right the one site); and the floor call dropped reddens source_walk own consumer census, whose derived count line this land repastes with the new consumer. #1859 AC4 is measured rather than argued, as #2028 did: from another module of the crate, Patterns and Pattern literals are E0451 on their fields and reaching for .list is E0616, throwaway probes, and the doc says privacy is what holds the pair and that a literal inside the module is not closed by it. a_recorded_inert_pattern_moves_no_paths_tier runs its claim over the paths its own rule list mints, with the inert line and without it, over the tier and over publish-ness, and the separator honoured reddens it naming a derived probe (27 passed, 4 failed); ADR 0038 and CONTEXT.md stop citing a three-path sample for no path changes tier and rest that on the parse. ADR 0038 and view.rs stop rating the .loot/view gap oppositely: closed on the tier axis, and the cost is materialization, which view.rs holds and the ADR now points at. conformance.rs stops resting its counted pair on a comment and points at ClassReads, where every other MetadataStore method is unreachable and the trait defaults none, so a third read in AccessClass::of panics there instead of undercounting here - measured, it does (0 passed, 1 failed). propose::visible_to and Viewer are pub(crate), with the field privacy named as what holds the pair and the in-module literal named as what it does not. seven string literals in cli_smoke.rs that carried a raw newline where the file spells the escape are fixed, five more than the ticket named, from a scan of the whole file. no migration, and no forge behaviour moves - a visibility narrowing, doc prose and one test fixture - so this owes no deploy. the live loot-forge suite is green against a throwaway 17.0 cluster with the CI env block and LOOT_FORGE_TEST_REQUIRE_DATABASE (476 passed over nine binaries, no skip note) and the workspace suite is green (3859 passed over 120 binaries, 7 ignored) (#2033)
4720219d · dbf3dbe6…diff - the owner can finally see what was offered to them, and a read request had to stop being a withdrawal in disguise: propose::list had no caller outside its own tests and propose::read only handle_propose_withdraw, so ADR 0075's store half was built and unreachable and an owner could see nothing. /propose/read and /propose/list join the contribution family with loot propose --show and --list on the CLI side, flags rather than a verb because a new verb trips seven censuses here and a flag does not. the finding the ticket did not anticipate is that a read naming repo and change id would have encoded BYTE-IDENTICALLY to a withdrawal, and the route lives in the URL rather than in the bytes the envelope signs, so a captured read envelope from the tip's author - the one party withdraw admits - could have been re-posted to /propose/withdraw and ended their own offer, a non-destructive act becoming a destructive one with no forgery at all. every new payload in this family now leads with an act tag and the replay is run in both directions rather than argued. the reads dispatch before run and touch ProposeTransport nowhere, because that seam exists to make the propose-time ordering assertable and a read has no ordering to assert, and --show resolves its handle before the remote and before the key so a mistyped id is answered by the refusal for the thing mistyped. terminal rows are listed rather than filtered at the last hop, an empty list says visible to you rather than claiming the repo has none, and an underivable tip is rendered beside its row rather than as a refusal. the propose-time disclosure gains the presenting key, which ADR 0075's #2162 amendment decided is world-visible on a metadata-public repo and which the old sentence omitted while naming the author. two enums share one vocabulary by WORD rather than by numbering, pinned total and injective, because loot-forge depends on loot-net and never the reverse. three censuses refused and each was right: the dispatch table lost a row to a multi-line verb declaration, a source-walk anchor spelling [Route; 16] went stale and is now count-free per the rule its own header already states, and the selector census demanded --show declare its kind, which is evolog's - one proposal by construction, borrowing evolog's resolver rather than growing a fourth. red under mutation, counts read each time: the act tag dropped (118 passed and 1 failed), the handler telling concealed from absent (422 passed and 1 failed), the list filtering terminal rows (422 passed and 1 failed) and the disclosure reverted to naming only the author (11 passed and 1 failed), each restored to 119, 423, 423 and 12. no migration and FORMAT_MAJOR does not move, but two routes are new, so an old forge 404s them and this owes a forge deploy before the flags answer against the live host. the workspace suite is green (4156 passed over 132 binaries, 8 ignored) (#2174)
09b5ad09 · dbf3dbe6…diff - landed becomes a state something actually writes, and the walk that decides it had to see the push's own changes: ADR 0075 declares three terminal proposal states and until now close_proposal's only non-test caller was propose::withdraw, so landed - the one that is nobody's act, the work being in ref_head - was unreachable and a row read open over work the repo had already taken. ingest now closes the proposals its declared head set lands, decided OUTSIDE the transaction because IngestTxn is assembled and validated outside so the transaction stays pure writes after the CAS, and that same CAS is what makes the decision sound rather than racy: generation_expected pins exactly the head set the walk assumed, so an interleaved push fails the swap and takes the closes down with it. the ticket and ADR 0092's predecessor both said this needed nothing new because walk already reports an empty extent for a landed tip, and that is true of the STORED graph and the stored graph is the wrong one: a maintainer lands a proposal by MERGING it, so the change making the tip an ancestor of the new head set rides in this bundle and is not in the store yet, and walking stored edges alone closes nothing on exactly the push this exists for. the walk overlays the prepared changes' parent edges and is red without that overlay. only open rows are candidates and the store applies the decided row only over an open one, so a proposal that went terminal between the decision and the commit keeps its own declaration and its own date - a push cannot overwrite a withdrawal it never saw, asserted in the conformance suite against both stores rather than in one. a repo with no open proposal reads its proposal rows and stops without fetching the graph at all, which is every push to every repo never proposed to. red under mutation, counts read each time: the prepared overlay dropped, so the merge in this bundle lands nothing (421 passed and 1 failed, restored to 422). the SQL is new so it was run rather than read: bash ci/local.sh against a throwaway Postgres, where pg::tests::an_ingest_closes_the_proposals_it_declares_landed passes beside its memory twin and 166 pg tests ran, so the UPDATE was parsed by a real server rather than only by a text test. no migration, no wire or format byte moves, and the forge behaviour moves, so this rides the next deploy (#2177)
fb7c63c8 · dbf3dbe6…diff - a withdrawal binds the tip it was written for, and the format minor deliberately does NOT move: a withdrawal names a proposal by its durable change id, which is stable across revisions on purpose, so a captured envelope replayed after a REVISED re-proposal ended an offer its author never withdrew - and the author is the only party withdraw admits, so the captured envelope is exactly the one that works. the signed bytes now carry the tip, compared inside the retry loop against the row that round actually read rather than once outside it, which is the same window #1959 closed for the author check. the replay is run end to end: the author withdraws at one tip, revises and offers again, and the byte-identical envelope is refused with the proposal left open, while a withdrawal naming the version actually on file still works, so the refusal is the binding and not a wall. ⚠⚠ the ticket instructed a FORMAT_MINOR bump and following it would have been a mistake, which is the finding worth more than the field: the marker's minor is ONE GLOBAL CONSTANT stamped onto every artifact rather than a per-payload version, and persist_codec's is_canonical compares a persisted store against the major and minor pair to decide whether the graph file must be REWRITTEN, so bumping it to announce one optional field on one wire message would force a rewrite of every local store on every machine. every read_version caller discards the minor, so the bump buys no reader anything the trailer's presence does not already give, and a test pins both constants unmoved with that reasoning beside it. the same plan sits on #2159 for its kinds trailer and is flagged there rather than left. an older client sends no tip and is accepted unbound, exactly as safe as before the field existed and no less, since an attacker cannot cause that shape and can only replay one already signed - so the exposure shrinks to withdrawals old clients signed rather than widening. both interop directions are pinned: an old payload decodes here reporting no binding rather than inventing one, and a new payload is byte-identical to an old one up to the trailer, which is what every deployed forge reads. red under mutation, counts read each time: the tip comparison made unreachable so the replay succeeds (430 passed and 1 failed, restored to 431). ⚠ three rounds of missed call sites, each invisible to the scope before it - a package lib run compiles neither integration tests nor other packages, and only the full suite answered. no migration, FORMAT_MAJOR and FORMAT_MINOR both unmoved, and the forge behaviour moves, so this rides the next deploy. the workspace suite is green (4169 passed over 132 binaries) (#2180)
7f2cbeca · dbf3dbe6…diff - the sentence written to replace a deleted list names the definer instead of counting, and the fix-up is read back under the rule it enforces: ADR 0073 said the gated work counters were the object pair and a further one at each amendment since, which is false because the #1903 amendment added none, so the clause now names measure::WORK_COUNTERS and stops. every other count beside a set that can grow is replaced by what decides membership rather than by a corrected number - the first nine and the six artifact counters and the other three Work variants in gate::COUNTERS become every row WORK_COUNTERS does not name, the Tally Display doc shows one label=value per Work::ALL entry and an ellipsis instead of hand-listing the labels, the deposit lane counts point at kind.widens, and the landed proposal state points at store::ProposalState. THE SWEEP MISSED THREE COPIES OF ITS OWN ITEMS and they are corrected here too, because a corrected claim left standing in a second copy is how #1903 shipped one stale: the three lanes sentence in loot-core custody and again in its test, and the ADR 0075 three terminal states sentence duplicated in a forge server test. two code fixes ride along: #2174 landed the wire_state doc inside the propose withdraw doc comment, so the may_propose rationale documented the wrong function and the route documented nothing, and the stale-tip refusal was broken across source lines with no continuation, so an operator read the indentation of the source in the middle of the message. ONE ITEM IS CORRECTED RATHER THAN FOLLOWED: the ticket reads that literal as carrying a newline, and on the tree it is a single line carrying two runs of collapsed indentation, so the rendering defect is real and its shape is runs of spaces rather than a break. red under mutation, counts read each time: the continuations removed so the break rides in the string again failed the strengthened pin (0 passed and 1 failed, the panic printing the refusal across three lines), restored to 1 passed. the CONTEXT rewrap is a pure reflow, identical byte count and identical word stream, so the generated membership sentence and its pin are untouched. no migration, no wire or format byte moves and no host behaviour moves, so this owes no deploy. the workspace suite is green (4174 passed, 0 failed, 8 ignored) (#2234)
f7721acb · dbf3dbe6…diff - the web can now list a repo's proposals, and a pin holds it to the same rule the CLI answers with: migration 0020 adds forge_read_proposal, forge_owner_proposal and forge_member_proposal, each one join onto its family's existing gate view, so none re-derives a gate and each inherits how that gate fails closed, and each family's role is granted its own view and nothing else. propose::visible_to in Rust and these views in SQL are two implementations of one disclosure rule, so tests/proposal_read.rs plants one fixture in a scratch Postgres, pairs each Rust reader with the web reader the site would use for the same person - anonymous, an owner through the account holding a rotated namespace key, an accountless namespace key, a member through its roster row - and demands the whole row set match, terminal rows included, with a vacuity guard that every class but the anonymous reader of a private repo sees rows on both sides, and an executable red that rebuilds the anonymous view without its gate and requires the pin to name that class and repo. THE KEY ARM IS PINNED RATHER THAN SKIPPED: visible_to admits a proposer and a tip author by key and no family names a key, so the web is narrower than the CLI by exactly the rows a key presented or authored and never wider, and the test fails if the difference is ever anything else. the family set is derived in migrate.rs from the earlier grants rather than listed, so a new barrier family meets the text guard and must carry its own proposal view or change it with a reason. a cleared gate answers no rows rather than raising, through the views that inherit it. the plan at 10000 proposals over 100 repos reaches proposal through proposal_pkey on repo_id for the owner and anonymous views, which were measured, with no sequential scan of proposal and no per-row author lookup, since the views carry no author; the member view joins proposal the same way through its gate and its plan was not taken. member_read's row-security set gains proposal, which a member view now reads, and drops the counts beside it; 0011, 0015, ADR 0075 and visible_to's doc now point at the second implementation. red under mutation, counts read each time, against a PG18 throwaway cluster: the owner view dropping terminal rows (0 passed and 1 failed), the member view reaching the owner gate (0 passed and 1 failed), the anonymous view ungated (0 passed and 1 failed, naming the four rows the ungated view served), the member view casting without nullif (0 passed and 1 failed on 22P02), the member grant dropped (0 passed and 1 failed), and the anonymous view reading repo in the text guard (0 passed and 1 failed), each restored to 1 passed. numbered 0020 because #2250 landed 0019 while this was built; the merge that follows resolves migrate.rs against it and carries the suite. the forge owes a deploy for the migration before the site can read these views (#2175)
232d49ae · dbf3dbe6…diff - the forge keeps the attestations a push carries and serves them back with their change, where until now it kept none: ingest never read the bundle attestation lane and every bundle serve built carried an empty one, so a tag, a note or a sign-off pushed to a forge was gone and the push said nothing, while a relay was never affected because its store is a DagRepo. the rules are the engine own two and none is new: an /ingest keeps an attestation that verifies and whose change rides the same bundle and drops the rest without failing the push, and a pull serves one only with its change in the bundle being sent, so a caller already holding a change is not re-sent its attestations and a metadata-private repo needs no new rule. /stow keeps none, because it is also a proposer path and a stranger attestation must not write into the owner repo. migration 0019 adds the attestation table, repo-scoped, keyed on the engine dedupe triple so a re-push is a no-op and the first signature stands, with row security enabled and bound by the 0017 policy, and it joins the rls census, whose doc still said eight tables with ten listed and whose shipped-policy check named migrations by index - it now reads every migration from 0017 on. the driver sorts role COLLATE C, because the reference store orders by bytes and a text column otherwise sorts by the cluster collation. a bundle that sends changes pays one more batched read, so the two offer-cost pins move from 4 to 5 and a no-op pull still asks nothing. a late attestation on an already-pushed change still does not travel - that is #2251, in the engine and on every host - and a pin refuses the forge keeping one outside the bundle so that widening is done on purpose. red under mutation, counts read each time: keeping unverified attestations (0 passed and 1 failed), keeping them outside the bundle (0 passed and 1 failed), serving none (0 passed and 1 failed), a read that ignores the ask and a last-signature-wins upsert (1 passed and 1 failed each, the pg stamp skipping off-cluster), each restored to 1 and 2 passed; and on a throwaway Postgres 18 the COLLATE dropped failed the ordering case (0 passed and 1 failed) where the unmutated arm had passed, restored byte for byte. bash ci/local.sh is green against Postgres 18 (4200 passed over 134 binaries, 9 ignored), after one unrelated loot-cli flake on its first run, filed as #2258. migration 0019 rides the forge binary and nothing on the wire or in a format moves, so this owes a forge deploy (#2250)
97ace3ec · dbf3dbe6…diff - adopt: catch up to landed main 97ace3ec
100c088f · dbf3dbe6…diff - propose::list reads the tip authors its gate needs in one proposal_tip_authors read, taken only when a row cannot be answered without one, where it read change once per row; the read is scoped by the repo proposal rows rather than repo_change, since a tip is held to change_node alone and the Postgres key-arm pin in tests/proposal_read.rs went red on the repo_change join first tried. ingest asks propose::landed_by_declaration, the one landed closure run over the stored graph overlaid with the push, where it carried a second walk of its own, and a new conformance case hands the ingest transaction a decision that disagrees with the graph both ways and holds both stores to applying it and deriving nothing. counted rather than timed through a new call-logging store double, calls.rs: a listing makes the same reads at 1 row as at 40, a push makes the same reads at 1 open proposal as at 40 with the proposal rows and the graph each read once and the transaction last, and a push with no open proposal reads no graph; a push that lands a proposal is answered in the same bytes as the same push to a forge holding none. measured on a throwaway Postgres 18 over a 1,000-change repo with one-change pushes landing nothing, three interleaved passes of 45 pushes per arm: per-pass medians 1.80 to 1.90 ms with the decision removed, 2.18 to 2.23 ms with it and nothing open, 4.52 to 5.94 ms with 1 to 100 open and flat in the count, against a knowingly wrong arm reading the graph per open proposal at 24.7 to 37.1 ms for 10 and 232 to 360 ms for 100, so the recorded fallback is not taken and ADR 0075 now says so. the proposal read still carries no pipeline verdict; that half of this ticket is left. red under mutation, counts read each time: the list reading change per row (0 passed and 1 failed), each store deriving closes from the head set (0 and 1 on memory and on Postgres, while the existing close case stayed at 1 passed), landed following no parent (0 and 1 on the ingest walk pin), the graph read per open proposal, read with none open and a read after the transaction (0 and 1 each), a landing push answered in other bytes (0 and 1), the tip authors answering every held change (0 and 1), and the Postgres read joined through repo_change (0 and 1 in conformance and in proposal_read), restored to 8, 4 and 1 passed. no migration and no format byte moves; the new read is forge SQL, so the list change reaches the live forge with its next deploy. bash ci/local.sh is green against Postgres 18 (4273 passed over 135 binaries, 10 ignored) (#2235)
3cffe4b6 · dbf3dbe6…diff - the kinds trailer on /ingest and /propose and the job creation it drives, with the wake-up sender; the runner-deposit reap is not built. loot_net::forge::HeadDeclaration and ProposalClaim gain kinds: a push declares the kinds of the on main steps of its live heads and a proposal the kinds of the on change steps of its tip, read by pipeline::declared_kinds from the .lootpipeline each version holds, as one trailing byte in Kind::ALL order that is not written when nothing is declared, so a payload declaring nothing is byte-identical to the one an older client signs; a decoder reads the byte iff bytes remain, and a bit this build cannot name is dropped. FORMAT_MINOR is not bumped, per the 2026-09-22 comment on the ticket that overrides step 1 of its body (a minor bump makes persist_codec rewrite every store graph file, the #2180 finding), and adding_the_kinds_moves_neither_format_constant pins both constants; a new client payload decodes on a frozen copy of the old decoder and an old client payload decodes here as declaring nothing. /ingest makes a main job per declared head and declared kind, less those on file, inside the ref-moving transaction (IngestTxn::jobs on both stores, with a conformance case run on memory and on Postgres), and /propose makes a change job per declared kind for the tip once the proposal row stands; member is the author of the version classified by AccessClass::of and approved is a verifying pipeline/approve from a key require_pusher admits, both read once at creation. after the commit each live runner whose row covers a created job kind and carries a URL is woken through jobs::Wake, whose HttpWake calls loot_net::runners::send_wake: the URL checked as registration checks it, the send refused if any resolved address is one registration would refuse (refused_wake_address, which both ask), the checked address pinned, no redirect followed, the request bounded by WAKE_TIMEOUT_SECS. the ticket said the site TypeScript client learns the field, and no code under site or sdk encodes an /ingest or /propose payload, so nothing there changed. the reap is recorded as unbuilt in the ADR 0091 #2159 amendment: grant_inbox is keyed by recipient alone while runner, ref_head, proposal and job are bound to one repo under migration 0017, so a reap run per repo cannot see an address live in another repo the same runner key serves, and how to scope it is a decision left to the reap ticket; nothing writes a runner deposit before #2130. measured: the trailer is one byte when a kind is declared and none otherwise, and ingest::job_cost pins that a push declaring nothing makes no call for jobs and that job creation reads per declared head, not per kind. red under mutation, counts read each time, each 0 passed and 1 failed on a filter selecting its one pin unless said otherwise: the trailer written before the heads, the trailer read unconditionally, the trailer always written, FORMAT_MINOR at 1, every kind made rather than the declared ones (through the push route, the proposal handler and the binary end to end, each), a wake-up sent to every row with a URL, a wake-up sent before the commit, member never set, approved by any attester, the memory ingest dropping jobs, the memory ingest overwriting them, the Postgres ingest dropping them (through bash ci/local.sh, 461 passed and 1 failed in loot-forge), only the first resolved address checked, the binary declaring the kinds of every trigger, the push declaring nothing, approved read once per kind, and the no-kind early return removed; each restored to green. no migration, no format byte and no published wording moves; the forge binary changes, so a forge makes jobs only once it is redeployed, and a forge older than this still accepts a client that declares kinds. the workspace suite is green under bash ci/local.sh against Postgres 18 (4345 passed over 137 binaries, 12 ignored) (#2159)
09cefc35 · dbf3dbe6…diff - the repo owner can decline a proposal: propose::decline beside withdraw, POST /propose/decline, and loot propose --decline <change-id>, the flag named as its siblings on the verb each spell their act. a declination is signed over the repo, the change id and the tip, and tagged Act::ProposalDecline so it can never be read as a bound withdrawal or a read; on the wire it is a bound withdrawal plus the tag byte, and neither format constant moves. the owner is the key require_pusher admits, the rule jobs already asks for an approval; a member reads the proposal and is refused in words naming the author withdrawal as the other route, and a key the read gate conceals the row from gets one sentence whether the row exists or not. may_propose is not asked, so a shut door does not stop a decline; a repeated decline is the same event and is not re-dated, a landed row refuses by name, and a withdrawn row refuses as the withdrawal refuses a declined one. a declination replayed after a revised re-proposal is refused by the tip binding, the same stack re-proposed is declined again, and one signed for another repo of the same owner is refused by the repo binding. the CLI signs the tip the forge reports at the moment of declining and prints it. uncontended, a decline is one close_proposal and no graph read, pinned on the store call log. it touches no job made for the declined tip, which ADR 0091 leaves undecided. ADR 0075 records the build and what a decline does to the bytes, with CONTEXT.md and the forge spec. red under mutation, counts read each time, each 0 passed and 1 failed and then 1 passed restored: the owner check skipped, the visibility gate skipped, the repo binding removed, the tip binding removed, a declined row refusing re-proposal, a landed row declinable, a withdrawn row declinable, the decline walking the stack, the route asking may_propose, the act tag dropped, the tip left out of the signed bytes, and the declination written under its own minor. no migration and no format byte move; the forge binary changes and an older forge answers 404 on /propose/decline, so this owes a forge deploy before the flag works against the live host. the workspace suite is green (4376 passed over 139 binaries, 12 ignored), and green under bash ci/local.sh against Postgres 18 (#2176)
b45cbbf3 · dbf3dbe6…diff - the review-sweep fix-up over #2307, #2176 and #2193. a seek busy marker is believed while its clock is within the window of the reader clock on either side, so the round that claims first with the later clock is no longer taken over, and a marker that does not parse is dated by its mtime against that same now. loot propose --decline takes the version it declines, the full tip --show prints or a selector naming a version held here, following the grammar that already reads a version where a change id is expected; a change id is refused, the CLI finds the row standing at that tip and refuses when none visible does, so a revision made after the owner read the proposal is not declined. two act flags on loot propose refuse through ProposeAct::from_args, where the first one checked ran. the format-constant pin compares the marker of each trailer and act payload with the one a client from before #2159 wrote, recorded in the frozen codec copy, so a bump of either constant reddens it; a push declaring each head its own kinds is pinned through a two-head repo; the decline cost pin asserts the whole call sequence with the gate calls measured alone; the forge door tests ask every route in Route::ALL, now public, that the router mounts. each GATES row declares what its finding is read off (Judged): a finding off files gets a merged remedy that goes to loot edit at once, with no re-run and no file lock, and the pre-land pass rather than each finding says nothing has been signed or published, so the merged pass no longer prints it beside a signed merge; the #2193 census reaches each spawn after passing ones and holds every Judged declaration against the spawner. false sentences fixed in ADR 0055, 0075, 0090 and 0091, workflow.md, npm_not_started and the census doc, including a network-specific NAT64 prefix, which the wake-up rule cannot recognise; the forge refusals of withdraw and decline spell the tip as a version id; the cmd_propose doc sits on cmd_propose; the wake slot test waits for its places to come back. red under mutation, counts read each time, each restored to green: a marker ahead of the clock read as dead (2 passed and 1 failed), the unparseable marker dated by the system clock (2 and 1), the decline signing the tip the forge holds (4 and 1), a change id accepted (4 and 1), two acts not refused (4 and 1), FORMAT_MINOR at 1 (1 and 2), the push unioning kinds (0 and 1), a second read in the decline (0 and 1), /propose/read mounted without the door (0 and 2), the pre-land state line dropped (2 and 1), the files remedy replaced by the run remedy (2 and 1), the second perf spawn failure a finding (2 and 1), the site row declared Files (2 and 1). no migration, no format byte and no wire byte moves; the forge refusal wording changes, so that is live once the forge is redeployed, the decline argument once the CLI is released, and a land runs the primary release loot-first, so the gate changes are live only once those binaries are rebuilt. the workspace suite is green (4392 passed over 139 binaries, 12 ignored), and green under bash ci/local.sh against Postgres 18 (#2325)
ff3b789d · dbf3dbe6…diff - a proposal has a conversation on the forge, stored and written and read, and no CLI flag posts to it yet: migration 0022 adds proposal_comment, keyed under the proposal row so a thread outlives a decline and a re-proposal, holding the envelope its author signed and its body, both set to NULL by a withdrawal that leaves a tombstone saying author or owner, with CHECKs that refuse a withdrawn row holding either, row security bound by the 0017 policy and the rls census widened. POST /propose/comment, /propose/comment/edit and /propose/comment/withdraw are tagged acts over propose::comment, where the rules live: a signer must see the proposal and is otherwise told what an absent one is told; the owner class writes, the accountless namespace key included; anyone else needs a live account key, so a bare keypair or a retired key is refused; members, the proposer and the tip author write through a shut door, anyone else only through an open one; a comment binds the tip on file when it arrives; a path must be in that tree and a reply must name a comment on the proposal; the body is at most 16 KiB of UTF-8, refused and not cut; only the author edits, re-signing the same id, anchors unmoved and bound to the BLAKE3 of the envelope it replaces; the author or the owner class withdraws, and members and proposers do not moderate. the comment id is signed by its author, so the same envelope again is one comment and a withdrawn one is not re-posted back. the thread rides POST /propose/read as a trailer and loot propose --show prints each body only when its envelope verifies against the author and anchors beside it. the flags that post, edit and withdraw a comment, and the disclosure printed before posting, are not built, since ADR 0095 names no flags; ADR 0095 amended and CONTEXT.md updated. red with each piece undone, counts read each time, each restored green: a bare keypair admitted, the door not asked, visibility not asked (each 0 passed and 1 failed on the writer-set pin), a member moderating, the owner editing another comment (0 and 1 each), anchors free to move (0 and 1), a withdrawal that writes nothing (0 and 1), no cap, and the body cut to the cap instead of refused (0 and 1 each), no tip binding (0 and 1), an edit not bound to its prior (0 and 1), an edit past the writer rule (0 and 1), the read sending no thread and an unverified body printed (0 and 1 each); on a throwaway Postgres 18, a withdrawal that kept the words with the CHECKs dropped left the conformance case green (1 passed) and turned the pin that reads the whole row red (0 and 1). bash ci/local.sh is green against Postgres 18 (4476 passed over 139 binaries, 13 ignored). no format constant or codec byte moves; migration 0022 rides the forge binary, so this owes a forge deploy (#2331)
6af9b14f · dbf3dbe6…diff - the web can read a proposal thread with exactly the audience of its proposal: migration 0023 adds forge_read_proposal_comment, forge_owner_proposal_comment and forge_member_proposal_comment, each granted to the role of its own family and each one inner join onto that family proposal view from 0020 on both halves of the proposal key, since one change can be proposed to more than one repo; they serve every comment column but the envelope, which no rendering surface verifies, so a withdrawn comment is its tombstone with no body. tests/proposal_read.rs now compares threads as well: for every reader class the thread the route serves against each view, tombstones, an edit, a path anchor and a reply included, with a vacuity guard, the key arm pinned as the only difference, a cleared gate answering no rows, every view read whole as its own role after a withdrawal through the store, a change proposed to a public and a private repo read with no repo filter, and an executable red that rebuilds the anonymous comment view onto the proposal base table; migrate.rs text guards pin one comment view per family, its grant, its reach, the whole-key join and no envelope. red with each piece undone, counts read each time, each restored green: the anonymous and owner views joined on change_id alone (1 passed and 6 failed), the owner view alone so (6 and 1, caught only by the unfiltered read), the member view reaching the anonymous proposal view (1 and 6, and 2 and 1 on the text guards), a withdrawal that keeps the body with its CHECK dropped (6 and 1), and the envelope served, a cross-family grant and a view without security_barrier (2 and 1 each on the text guards). the timeline and the Markdown renderer are not built: an attestation row carries no time and no reader role has a view of it, the decline reason is #2336, and no site surface renders a thread yet (#2165). ADR 0095 amended and CONTEXT.md updated; setup-forge checks reader views by prefix since scripts@1b2b9ed, so the member view needs no edit there. bash ci/local.sh is green against Postgres 18 (4482 passed over 139 binaries, 13 ignored). no format constant or codec byte moves; migration 0023 rides the forge binary, so this owes a forge deploy (#2332)
38e8b55a · dbf3dbe6…diff - a decline may carry a reason signed with it, kept on the proposal row and shown as the thread closing event: ProposalDeclination gains an optional reason as a trailer on its signed bytes, so a decline with none is the bytes #2176 signs and neither format constant moves, and a reason a carrier swaps or strips no longer verifies. migration 0024 adds proposal.decline_reason with a CHECK holding it to a declined row, never empty and at most COMMENT_BODY_MAX bytes, and replaces the three 0020 proposal views with the column appended, which a text guard holds equal to 0020 once the column is taken out. propose::decline refuses an empty or oversized reason whole and a repeated decline with other words, close_proposal writes the reason only with a decline, and a re-proposal clears it with closed_at. POST /propose/read carries the reason as a trailer after the thread continuation, absent from an older forge, which would drop a reason unread, so loot propose --decline <tip> --reason <text> asks the read first and refuses a reason such a forge would drop, then prints who can read it before signing; --show prints the decline under the thread with its reason caged through render sanitize. frozen before_2336 codecs pin old client against new forge and new client against old forge both ways, and tests/proposal_read.rs compares the column for each reader class with an executable red that blanks it in one view (1 class disagreeing). red with each piece undone, counts read each time, each restored green: the reason left out of the signed bytes (0 passed and 1 failed), the read trailer not decoded (0 and 1 on each of two pins), the route dropping the reason, the read serving none (0 and 1 each), no cap and an empty reason admitted (0 and 1 each), a repeated decline changing the reason (0 and 1), a re-proposal keeping it and a reason written with any terminal state in the memory store (1 and 1 each, the pg stamp of the case green without a database), the CLI signing for a forge that would drop it, the reason printed uncaged (0 and 1 each), --reason admitted beside no decline and no disclosure before signing (0 and 1 each), the migration cap moved and a view joined to another family gate (0 and 1 each). ADR 0095 amended, CONTEXT.md and the forge spec updated. bash ci/local.sh is green against Postgres 18 (4519 passed over 139 binaries, 13 ignored). migration 0024 rides the forge binary and the read and decline routes change, so this owes a forge deploy (#2336)
c8d5a6ea · dbf3dbe6…diff - review sweep 14 over the proposal conversation: the proposer and the tip author are accounts where #2374 left them keys, so a live key of either account is a party at a shut door and sees the proposal on a metadata-private repo, through propose::speaks_for, now beside visible_to, and propose::sees, which read and the comment routes ask one row at a time while list keeps the key arm by key. migration 0025 adds proposal_comment_page on (repo_id, change_id, created_at, comment_id), so a thread page is a range scan the driver statement comment_page reads, where every page sorted the whole thread, and a pg pin reads its plan with the sequential scan and the sort priced out. /propose/decline takes the comment routes body limit, --show says the decline reason prints on the forge word and refuses a thread page that brings nothing, stands still or repeats a comment, and the reason disclosure says a re-proposal clears it. the comment.rs door sentence names may_propose_in and lists no members, PROPOSAL_COLUMNS replaces four copies, and ADR 0095, CONTEXT.md and the code docs say the party rule, the index, the decline limit and that the reason is unverified pending the call ADR 0095 records. red with each piece undone, counts read each time, each restored green: a party by key and seeing by key (0 passed and 1 failed each on the rotated-party pin), the index dropped on a throwaway Postgres 18 (0 and 1), a refusal quoting the envelope printed uncaged (0 and 1), the reason printed without the forge word, the decline route uncapped, a cursor that does not move taken and the re-proposal unsaid (0 and 1 each). bash ci/local.sh is green against Postgres 18 (4525 passed over 139 binaries, 13 ignored). no format constant or codec byte moves; migration 0025 rides the forge binary and the forge rules change, so this owes a forge deploy (#2378)
f168d689 · dbf3dbe6…diff - review sweep 15 over the proposal conversation and the published tier: acting on one proposal decides its parties by account as seeing it does, so the withdrawal and decline gates ask propose::sees, the withdrawal author check and the re-proposal parties in admit ask speaks_for, and a rotated tip author whose original key is retired withdraws their proposal with the account live key on a metadata-private repo, where by key it was told the proposal did not exist, and on a public one, where it was told it did not author the tip; a rotated proposer offers a declined proposal again. the #2378 rotation pin now retires both original keys on a Rotated fixture that files the offer before the rotation. the comment-page EXPLAIN pin asks for Sort anywhere in the plan, where its line-start form could never see a nested Sort node. an unread seal answers not published in rows_of and with_sealed_publish_status, the direction that never overstates, where it fell back to the rule; show renders an unread object by address as sealed, so the listing carries the pin. propose.rs, ADR 0095 and CONTEXT.md state which gates are by account as a property, a read of or act on one proposal, and cite #2006 for list, CONTEXT.md scopes the #2352 tier sentence and points at #2381, and append_page refuses a comment repeated within one page. red with each piece undone, counts read each time, each restored green: the withdrawal seeing by key, the withdrawal author by key, admit parties by key (0 passed and 1 failed each), a retired key speaking for nobody (0 and 1 on the rotation pin), a misshapen page index on a throwaway Postgres 18, comment_id before created_at and created_at descending (0 and 1 each, 1 passed restored), the listing falling back to the rule (0 and 1), a repeat within one page taken (0 and 1); the rows_of fallback mutation stays green, since no verb reaches it. bash ci/local.sh is green against Postgres 18 (4530 passed over 139 binaries, 13 ignored). no format constant, codec byte or migration moves; the forge rules change, so this owes a forge deploy (#2382)
eceef7eb · dbf3dbe6…diff - the forge refuses an /ingest that would drop a current head it neither descends from nor retires by name (ADR 0098 section 3, map #2422): ingest::heads_this_push_drops judges the declared heads against the heads read at the push generation, before the store transaction, so the CAS makes it sound and a push at a stale generation still loses the swap as before; the walk over the push own changes answers an ordinary push with one read, the head set, and only a head that walk does not reach costs the repo graph, through the landed closure propose already has. a refusal is a 412 whose text body names the count, the remedy (loot pull first) and each head, so a client from before this reads a sentence and this client prints the count it read with the same remedy. retirements ride the signed head declaration as a trailer after the kinds, written only when a push names one, so a push retiring nothing is the old bytes and a forge from before this reads past the names and replaces its heads wholesale as it always did. the client names each forge head its declaration does not reach and that this position has seen, holding the change or naming it in its abandoned record, so abandon --head and supersession are covered with no list of verbs, and never names a head it has not seen. four conformance cases run on the memory store and on Postgres through the assembled forge: a fast-forward carried or already held, a dropped unseen head refused and named with the ref unmoved, an explicit retire that lifts only the head it names, and a racing push that still loses the CAS; a runners test that replaced its root with a new root now retires it by name. red under ten named mutations, each 0 passed and 1 failed (2 failed for the trailer) and restored: no refusal, no generation guard, retirements ignored, no overlay fast path (an existing pin saw the graph read), the overlay alone, the client retiring nothing, the client retiring unseen heads, a view-only seen, the kinds left out ahead of the names, and the client not reading the 412. bash ci/local.sh is green against Postgres 18, 4599 passed over 143 binaries with 13 ignored and the site pg suite at 67 passed. no migration; the forge behaviour changes, so this owes a forge deploy (#2426)
e1e5c828 · dbf3dbe6…diff - the forge repo graph read stops scanning the whole forge change_parent table: repo_change_graph on Postgres read the repo ids and then looked their parent edges up by an array, which past a repo size the forge edge count sets (between 1,060 and 1,070 ids on a 297,991-edge throwaway forge, 2,810 and 2,820 on 898,012, 7,500 and 8,000 on 2,698,011) the planner answered with a scan of every edge, so every caller of repo_change_graph slowed as other repos grew. it is now one statement, pg::meta::REPO_GRAPH, a lateral join from repo_change whose OFFSET 0 keeps it a nested loop that reads change_parent one change at a time through its primary key; a plain join was measured beside it and steps the same way. measured on a throwaway Postgres 18 at eight repo sizes on three forge sizes, the new read examined the repo own edges and no others in every cell, 3.47 against 27.9 ms at 1,500 changes on the smallest forge and 21.0 against 98.6 ms at 10,000 on the middle one, while on the largest forge the 30,000 and 100,000-change reads slowed as the index outgrew the 128 MB of shared buffers (202 and 402 ms); ADR 0075 records the table and that. a pg test asks the planner for the statement with nested loops and sequential scans priced out and refuses any change_parent read not keyed by the repo_change row, red with the old array shape and with OFFSET 0 dropped (0 passed and 1 failed each), and the graph conformance case gains a merge whose parents are recorded against address order, red on the pg store with the lateral join made inner, the ordinal dropped from the order and a row per edge left ungrouped (2 passed and 1 failed each over the three graph tests, the memory store green), each restored green. bash ci/local.sh is green against Postgres 18 (4566 passed over 141 binaries, 13 ignored, site live suites 67). no migration; the statement changes, so this owes a forge deploy (#2346)
5dd0e327 · dbf3dbe6…diff - review sweep 16 fix-up over #2382, #2353 and #2358: the test scratch helper hands every path out inside one directory named for the process and claimed once before its first path, so a sibling a fixture derives, the <path>-lanes/ a lane spawns in, is inside what the claim cleared and a reused pid no longer hands a later run the lane an earlier run left, which stacked() in propose.rs took from read_dir; a retired key that wrote a proposal tip is refused the withdrawal in live_account_in words, decided by the retirement rule ADR 0075 already applies to an offer and ADR 0095 to a comment withdrawal, and recorded as an ADR 0075 amendment, with the account live key still withdrawing; the rows_of unread-seal branch is pinned through loot diff --json, which the doc said no verb reached; with_sealed_publish_status refuses a change this position does not hold, sharing held_tree with tiers_as_sealed, where it answered every row unpublished; the speaks_for, sees, rows_of, with_sealed_publish_status and scratch guard docs, CONTEXT.md and ADR 0095 say what the code does, the by-account property scoped to the forge routes. red with each undone, counts read each time and restored: only the handed path cleared (2 passed and 1 failed in scratch_dirs), no retired-key refusal on withdraw (2 passed and 1 failed over the three rotation pins), the decline gate by key through visible_to (0 passed and 1 failed), the rule fallback back in rows_of (0 and 1), and the empty-tree default back in with_sealed_publish_status (0 and 1). bash ci/local.sh is green against Postgres 18, 4606 passed over 142 binaries with 13 ignored and the site live suites at 78 passed. no migration; the withdraw route refuses a retired key now, so this owes a forge deploy (#2386)
6621b15f · 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.