Changes touching this path
- the forge stops holding lanes and holds proposals instead: #504 decision 3 recorded two objections and this ticket carried only the privacy one, but a hosted lane tip would also enter ref_head and reintroduce the multi-head ambiguity v1 exists without, so a hosted lane was the wrong noun rather than a deferred feature and both objections dissolve at once rather than trading off. ADR 0075 records the eight decisions - a proposal and not a lane, its own verb over its own route with bytes riding stow so ingest stays the one ref-moving endpoint, a stack named by its tip's durable change id with extent derived to the landed frontier and the base declared as a checked assertion, existence riding metadata_public paired with a propose-time disclosure because the owner's flag cannot consent for the contributor, three declared terminal states because deriving liveness from silence is exactly what #483 forbade, may_propose beside may_push rather than a widening of it, and provenance as two records that never merge - and the forge spec's contribution section, its swimlanes paragraph, ADR 0041 and CONTEXT.md are amended to match, with the approval flip that arrives when the author is not the lander handed to map #1014 rather than answered here (#1638)
6968491d · dbf3dbe6… - 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…diff - 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 sync surface learns membership, which is ADR 0077's other half: AccessClass::of stops being a byte compare and becomes owner-or-member, with the owner compare staying FIRST because a repo can have an owner with no account at all - repo carries no tenant_id and a push creates no tenant - and the membership arm resolving through a LIVE account_key row, since a rotation retires a key precisely so a leaked one stops working and reading a collaborator's sealed metadata is what a leaked key would be used for. The read is a new trait method with no writing sibling, and liveness stays in the CALLER so the two stores cannot disagree about #927's rule. Two breaks prove it and the interesting one is the small break: resolving membership before the owner compare moves exactly ONE cell of twenty-one, which is the point rather than a weakness, because a matrix without an accountless owner in it would not have moved at all. The dangerous shape is the other one - resolving from account_key alone with no repo_member join, which makes every account a member of every repo - and it turns eight of twenty-one green including two whole columns; the same break is red in SQL against a live cluster, and the unjoined query is run BESIDE the shipped one inside the test and asserted to admit the repo it should not. The write rules are pinned by #1650's method rather than described: the whole matrix of five signers across both door positions, run with membership rows planted for every account and again with none, demanding identical rendered answers including the message text, with a control that those same rows really do move the read class - and folding membership into require_pusher flips the contributor cell from a 403 that names another account to ok, which IS a stranger moving the owner's ref. The may_ship_key pin found something that strengthens ADR 0077's sixth decision: widening the class ships no key that was not already world-readable, and a member gets the Internal object's BYTES with the key set asserted as exactly the published oid rather than as an absence - but making may_ship_key answer true left the case GREEN, because verify_publication independently refuses a non-world object, so the lane is closed THREE times and not twice and the pin cannot be broken by widening Member in any spelling. A gap the ticket and the ADR both missed is pinned rather than fixed: a rotated owner's OTHER live key reads Anon on their own repo while require_pusher admits it, which this change did not cause since a byte compare answered the same way, but the new matrix makes it visible - the remedy needs no new predicate, only a repo_member row for the owner's own account. The claim that there is exactly one read method here was a live count in nine places and staled the moment a second arrived, so all nine become names and no number is written anywhere (#1728)
1fc1f6a2 · 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 - 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 - the owner side of a proposal is decided, and only one of ADR 0075 three terminal states had a producer: close_proposal accepts withdrawn, declined and landed and both stores implement it, but its sole non-test caller is propose::withdraw and every other call site is a test or a conformance case, while propose::list had no caller outside its own tests and propose::read only handle_propose_withdraw, so an owner could not see, decline or land anything and the half of ADR 0075 naming their acts was unreachable rather than undecided. landing stays LOCAL and the forge only observes, foreclosed three independent ways rather than by preference: the forge would have to mint a HeadDeclaration carrying generation_expected, which is the one artifact this design has a client sign; ADR 0091 decides the forge runs nothing because it holds ciphertext and cannot run a step over a sealed path, and a land is a converge plus a gate; and a runner cannot stand in because ADR 0091 puts it under no account, so both write doors refuse it by absence. ingest closes the row pre-computed OUTSIDE its transaction, because IngestTxn is assembled and validated outside so the transaction is pure writes after the CAS, and that same CAS is what makes the pre-computation sound rather than racy, generation_expected pinning the very head set it assumed. the decline binds the repo, the change id AND the tip, the tip because re-proposal after a decline is permitted and a captured envelope must not end a revised one. the read surface is two transports sharing no path, the CLI over HTTP and the site over Postgres barrier views, so both are owed and a conformance pin demands that visible_to and the views answer with identical row sets, or the web discloses what the CLI conceals and nothing fails. the presenting key is world-visible and the propose-time disclosure now says so rather than naming existence alone. and one consequence of this ADR was WRONG: landing a proposal does not take policy::approval off its SelfAuthoredFastPath arm, because that function compares a GitHub PR author login with the GitHub account running the land and reads the loot change author nowhere, so an owner opening the PR keeps the fast path and a stranger code lands on the weakest approval signal with nothing in the gate knowing it was not theirs, which is why a distinct owner-signed review/approve role is required rather than the question handed to map #1014. ADR 0091 gains the landing policy home and the second human role, CONTEXT.md and spec section 7 gain the owner acts, graduated as #2174 through #2178 with #2180, #2181 and #2182 filed beside them. docs only: no code, no migration, no wire or format byte moves, so this owes no deploy (#2162)
31ecdf50 · dbf3dbe6…diff - the land gate gets the predicate it was specified without, and proposal-derived turns out to be the wrong concept: ADR 0075's #2162 amendment said the local land enforces attestations for a proposal-derived change and never defined it, which could not be built at all because loot-first reads no loot change author anywhere and the only author it sees is the GitHub PR login, the very field that amendment had to correct. ADR 0092 replaces the concept rather than implementing it, firing on a change whose author is not in the lander's own key set, the active signing key plus the rotation archive, because what a gate should care about is code the lander did not write and the route it arrived by is incidental, which also catches a colleague's change carried in with no proposal at all, the case ADR 0075 calls a feature. two predicates stay separate on purpose and each names its own question: carry_line asks may I re-author this and must compare the current key since a rotated key cannot sign, while the gate asks did I write this and spans every key the identity has used since a rotation does not make earlier work somebody else's, so a rotated operator's own change is Foreign to the carry and theirs to the gate, and the natural repair of unifying them breaks one caller in either direction. that same Foreign arm is what makes a verdict usable at a land at all, since an attestation binds a version and a foreign suffix is merged rather than replayed, so version ids survive where a replay would have detached every pass in silence and read as not yet verified. the judged set is the ancestor closure of the lane tip minus what main covers rather than carry_line first-parent chain, because a stack containing a merge of its own would otherwise smuggle unapproved work in on a second parent and report green. an absent author counts as foreign and that was already shipped. gating is the repo policy, declared per requirement in the LANDED .lootpipeline and defaulting to advisory, which is what makes the #2162 rule about reading the landed copy load-bearing rather than prudent, while detection is always on and the verdict line states foreign authorship so that advisory never becomes invisible. the predicate itself is not configurable, authorship being a fact rather than a preference. ADR 0091 section 6 is corrected in passing: its sentence that a change step judges the merged tree at a land is an intention, loot-first naming .lootpipeline nowhere and a land running the Land gate phase off the untracked .loot/gates instead. CONTEXT.md gains Foreign authorship, and #2178 is re-specified and shrinks. docs only: no code, no migration, no wire or format byte moves, so this owes no deploy (#2216)
3c7ce4b1 · 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 - 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 - Manifest::addresses reads the frames only after a step over them shows the stored keys strictly ascend in Path order, so a manifest whose frames are not its map, two keys naming one path or keys out of the map order, answers from the map, and a pull over a corrupt local graph file no longer asks for an address the map dropped. tier_counts keeps its backslash test, now documented as not a proof, and the a.txt and a/b ordering it cites is corrected to component order in both places. what the check costs, paired --missing readings, one loot-perf-gate --features count per arm, eight interleaved rounds, load idle at 2 to 10 percent: 0.875 to 0.924 ms without it against 2.09 to 2.18 ms with it at width 200, and 5.76 to 5.88 against 10.50 to 10.63 ms at width 800, where a decode arm read 18.75 to 19.13 and 100.0 to 100.8 ms, so about 9x against the decode where it was about 21x and 17x; the Path comparison is most of it. red under mutation, counts read each time: the check removed, compared by byte, not strict, byte equality only and path equality only (1 passed and 1 failed each), restored to 2 passed. riding along: store.rs and ADR 0075 say the ingest transaction still reads for checks of its own and that the landing decision is what reads nothing inside it, that a push with no proposal open pays 0.28 to 0.43 ms where #2177 asked for nothing measurable, and that the fallback decision rests on one repo size; finish_stage says why any refusal is answered by the address on disk; the dated test counts in format_skew_gate.rs and workflow.md say at the time; two rewraps; the calls.rs runner stubs use the file imports. no migration and no format byte moves, so this owes no deploy. the workspace suite is green (4274 passed over 135 binaries, 10 ignored) (#2273)
0a64d280 · 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 - ADR 0075 re-decides the proposal ingest fallback across history sizes on a throwaway Postgres 18: a push with ten proposals open grows from about 5 ms at 1,000 changes to about 520 ms at 300,000 while a push with none open stays flat at 2 to 3 ms, and the growth is the repo graph read, which past a planner step, between about 1,450 and 1,550 changes on a forge of about 288,000 edges, scans the whole global change_parent table and so follows the forge edge count as well as the repo history (55 to 123 ms at 30,000 changes as the forge grew to about 893,000 edges). the knowingly-wrong arm, a graph read per proposal, read about ten times the ten-open arm at each size from 3,000 up. the fallback is retired: offer, fetch and propose read make the same read, and since #2175 the web reads state from the column the fallback would leave open. the no-proposal cost, 0.11 to 0.48 ms and flat in history, is recorded and not closed, since skipping the read needs a new column or a cache. store.rs notes the plan step at repo_change_graph. docs only: no code, no migration, no deploy owed. the workspace suite is green (4400 passed over 139 binaries, 13 ignored) (#2312)
223b87d4 · dbf3dbe6…diff - the review-sweep fix-up over #2325, #2207 and #2208. a proposal landed close is bound to the tip it was decided for, on both stores: ingest decided the landing outside the transaction and each store closed the row on state open alone, so a revision through record_proposal between the decision and the transaction, which leaves the row open and moves no ref and so is invisible to generation_expected, was closed landed at a tip nobody landed. the rows proposals_this_push_lands hands the store already carry the tip decided on, and the Postgres close now adds tip_version = $4 and the memory close compares the tip, the binding a decline has; no other writer closes a row this way. a new conformance case, a revision between the landing decision and the ingest stays open, runs on each store: red first on the memory store (1 passed and 1 failed, the pg half skipping without a database) and on Postgres 18 with the tip term defeated (106 passed and 1 failed), green with it (107 passed). CONTEXT.md, store.rs, pg/meta.rs, memory.rs, ingest.rs and ADR 0075 stop calling the CAS the whole of the soundness. false sentences fixed: ADR 0012 eleventh amendment now says the ledger ran after the lanes until #2208, and the fifteenth names the plan pin as the fixture where the timed walk reads one seal, measured by hand; the plan pin now deposits every row it plans at another forge as well as the Restricted row at a relay, so dropping the remote from the dedupe empties the plan rather than dropping pitch.md alone (20 passed and 3 failed, all six rows gone), restored green (23 passed); Route::ALL is written by a routes macro from the enum variant list, so a variant added to Route is in it with no second edit, checked by adding a probe variant, and the http.rs sentence states that. the --decline refusal says the handle reads as a change id, and the Judged census doc says it sees only the Spawner. no migration, format byte or wire byte moves; the landed close is live once the forge is redeployed. the workspace suite is green under bash ci/local.sh against Postgres 18 (4404 passed over 138 binaries, 13 ignored; site live suites 7 files passed) (#2347)
facefe81 · dbf3dbe6…diff - the review-sweep fix-up over #2308, #2310, #2311, #2312 and #2313. the lock note beside a failed test is said only on a Windows land: loot-hygiene failed_tests now takes whether the run was on Windows, since os error 5 and 32 are an I/O error and a broken pipe on macOS and Linux, and loot-first passes cfg!(windows); both answers are pinned on this host, red under mutation with counts read each time (recognised everywhere 2 passed and 1 failed, nowhere 0 and 3, loot-first passing false 1 and 4). the plain pre-land test finding now prints the re-run-first recovery the merged pass prints, for each gate row that runs programs and rides the merged pass, derived from the row and pinned whole (no triage 3 and 2, ignoring the merged pass or the row judgement 4 and 1 each, the order swapped 4 and 1). the timing line noun is a ServerKind enum whose relay and forge lines are pinned byte for byte (a misspelled noun 7 and 1), and the loot-s3 put refusal calls the idle duration the idle budget and keeps floor for the rate (the old word 18 and 1). tutorial.rs qualifies the #2311 halt as run on Windows and reasoned elsewhere, and a reap failure no longer prints could not run; ADR 0075 names the graph read by MetadataStore::repo_change_graph and its callers rather than a list that missed propose::admit; sync.rs states the store property the scoped forge offer rests on and what would break it, and its round-zero comments speak of the remote; land-change SKILL.md, workflow.md and ADR 0055 carry the re-run-first order and the Windows-only note. the (idle, uplink_floor) data clump in loot-s3 is left as it is. no migration, format byte or wire byte moves; the loot-first change is live once the primary release binaries are rebuilt. the workspace suite is green (4406 passed over 139 binaries, 13 ignored) (#2348)
77d9c94c · 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.