Changes touching this path
- Candidate 3: extract the visibility-policy module from Workspace
CONTEXT.md calls .lootattributes "the user-facing surface of loot's thesis,"
but its whole machinery lived as private helper types at the bottom of the
8,759-line workspace.rs, reachable by tests only through a full Workspace.
Lift it into one cohesive crate::policy module (the codebase-design review's
candidate 3):
- Glob + glob_match (the shared dialect: * stops at /, ** crosses it)
- Ignore (.lootignore parsing + file/dir pruning)
- Attributes (.lootattributes rules + visibility_for + the mis-seal gate's
public_by_fallthrough consent test)
- SECRET_NAMES + is_secret_name + is_catchall (ADR 0038 §1 mis-seal gate)
- parse_visibility, and the ATTRS/IGNORE filenames
Pure move, no behaviour change: Workspace imports these from crate::policy and
consults them at snapshot and the signing seams exactly as before; the engine
still receives already-resolved visibilities. workspace.rs drops ~230 lines of
policy machinery; the thesis surface now has locality and one home. Full
loot-cli suite green (mis-seal gate, glob-dialect, and characterization tests
included).
Follow-up worth doing: migrate the pure glob/is_catchall/is_secret_name unit
tests from workspace.rs's test module into policy.rs so the interface is also
the test surface; they currently reach across via a crate::policy import.
65596200 · dbf3dbe6… - Pass-2 candidate 2: give the pure extracted modules their own test surface
Pass 1's extractions created clean interfaces but left their tests behind in
the god-files ("the interface is the test surface" unmet). Add #[cfg(test)]
modules to the pure/cheap ones:
- policy.rs: the glob dialect (`*` stops at `/`, `**` crosses), Attributes
first-match-wins + public_by_fallthrough (the mis-seal consent test),
is_catchall, is_secret_name, Ignore subtree pruning + policy-file protection,
parse_visibility. This security-relevant dialect was previously exercised
only transitively through workspace.rs.
- reports.rs: DeltaClass::gutter (the frozen #306 +/M/- mapping) and
WorkingRow::version_hex.
- kv.rs already shipped with its own tests (candidate 5).
Surfaced a real finding while writing the secret-name test: SECRET_NAMES's
`.env*` is anchored at the basename start, so the suffix style `prod.env` is
NOT caught (only `.env`, `.env.local`, …). Left as-is and documented in the
test — widening the set is a security-policy call, not a test-backfill.
Stateful modules (negotiation, lanes) stay covered through the engine.rs /
workspace.rs suites: isolated tests there need the same repo/workspace
fixtures those suites already build. loot-cli lib green (237).
9551c8d8 · dbf3dbe6…diff - Rename the Public visibility tier to Internal (ADR 0041 §2, #480)
6969626d · dbf3dbe6…diff - Publish mechanism: the `published` keyword + @world grantee (ADR 0041, #481)
a6758352 · dbf3dbe6…diff - the ignore dialect refuses what it cannot mean: a live ! line stops the snapshot, recorded texts keep their old inertness (#843)
74d9e7be · dbf3dbe6…diff - the agent runtime's lock is not content: ignore it so the catch-up guard stops firing on it (#920)
731b1c18 · dbf3dbe6…diff - a rule you inherited is not a rule you typed: the ignore negation warns instead of wedging every older lane (#921)
a64e595e · dbf3dbe6…diff - the gate proves the tree the position holds, and main receives the merge: a land refuses a lane behind landed main (#939)
main went red at 3597c9d with two lands that each passed their own gate: #920
added a caller (`Ignore::parse`) in one file, #921 deleted the callee in
another. Disjoint edits, no path in common, so loot per-path conflict detection
had nothing to look at — and each land compiled the tree its LANE held while
the ferry projected the MERGE, a tree nothing had ever compiled.
The repair is one line: #920 test takes `parse_recorded`, the sole parse since
#921, which is also what it means (it pins the shipped file behaviour, not a
live-tree strictness that no longer exists).
The guard is the rest. `Workspace::behind_landed_main` is the read-only half of
the `covered` question `adopt` already asks, and `land_gate` refuses on it,
naming what it is behind and pointing at `loot adopt` + a re-review. The land
does not adopt for you: a catch-up merges, a merge can conflict, and burying
that inside the verb whose promise is shipping the reviewed change is the wrong
place for it. The same question is asked again after the gate and before the
finalize, for the sibling land that completes inside a multi-minute cargo test
— there rather than inside the harbor lock, which would be airtight and would
serialize every concurrent land behind one full test suite (the ADR 0036 cost).
The residual window is the git-quiet finalize plus the lock acquisition;
workflow.md names it rather than papering over it.
510a5be7 · dbf3dbe6…diff - the mis-seal gate stops missing the suffix that names a secret: *.env joins .env*, and the tier it has always watched is called Internal in the four places that still said Public (#1108)
72176935 · dbf3dbe6…diff - the mis-seal gate covers keys, cloud credentials, password stores and token-shaped data files, measured to add no wall on this tree (#1139)
59df2a25 · dbf3dbe6…diff - the grant-path skill states how an embargoed key actually reaches a relay, the last stale qualifier drops, and two doc drifts close (#1179)
eee134c9 · dbf3dbe6…diff - a visibility spec loot cannot read stops being dropped in silence: the parser records the malformed line and seal_gate refuses to capture over it, naming the line, the spec and the legal forms, so a typo can no longer ship the path it was written to seal at the tier a relay reads
28ff6a3e · dbf3dbe6…diff - the two dotenv rules stop being root-anchored, so the token vercel leaves under site/ is ignored by the same file that already ignores the one at the root rather than sitting in the primary's working change as internal
389e5cf1 · dbf3dbe6…diff - a second renderer stops being able to disagree with the first about which paths changed, because "what changed between two points" is now one seam taking (from, to, pathspec) whose endpoints may be a recorded change or the live working tree, and the six-rung content ladder and the in-the-clear plaintext count moved into it — so a renderer formats a number it is handed instead of tallying its own, and status and diff stop computing the same delta twice (#1541)
289ddd4e · dbf3dbe6…diff - loot status stops re-reading .lootattributes once per listed path, because the publish column now comes from the one-load derivation #481 built for exactly this, so a 2000-path listing pays one policy read instead of 2001 and that count is pinned as a number that does not grow with the tree, while the two whole-tree decrypts this ticket named measure at 1.5 ms and 4 ms and are left alone with those numbers written at the seams (#1591)
24dd342b · dbf3dbe6…diff - the harness stops measuring only what a run produces and starts measuring what it does, because the three work counters this run built and threw away are now one family in loot-count that a single during region reads whole, with logical object opens and disk reads kept as separate numbers since 2,000 of the first can cost zero of the second, and the family ships compiled out rather than as cfg(test) one-offs because cfg(test) reaches neither an integration test nor loot-perf and would foreclose the gate #1602 exists to add, at a cost measured at zero bytes in the released binary (#1600)
482a72c1 · dbf3dbe6…diff - the mis-seal question gets a verb, and the reason it could not have one turns out to be that the decision existed three times: visibility_for, is_published and public_by_fallthrough were three near-identical loops over the attribute rules, so there was nowhere to hang an explanation off and the rule against writing a second matcher was impossible to honour until they collapsed into one Attributes::first_match. That collapse, not the line numbers, is this change - the line numbers were already tracked since #1329 for MalformedRule, including the convention of counting blanks and comments so a number matches what an editor shows. loot explain now names the .lootattributes line that decided a path, lists what it passed over so first-match-wins is visible rather than asserted, reports which .lootignore line covered it, and answers for a path with no file on disk, which is the mis-seal question asked before the mistake rather than after. Because explain and the real decision are projections of one traversal they cannot drift, and that is pinned twice rather than assumed: one test deletes the line explain named and asserts the real visibility moves, then deletes each line explain called skipped and asserts it does not, hardcoding nothing about which rule should win; the other runs a real capture and asserts that for every recorded path the sealed tier equals explain's token, with the ignored file absent and a positive control that the sealed set is non-empty. Eight breakages were each proved red before the tests were trusted, including giving explain its own matcher while the decision kept the old one. And loot explain . would have printed a fully furnished answer about a path that is not one, because ** matches the empty string in this glob dialect and the door rebases a bare dot at the repo root to the empty relative path, so in any repo carrying the catch-all ADR 0038 says every real repo wants it would have named a tier and a line for nothing at all - refused at the door now, with a control showing the rules really would have answered. Found by running it rather than reading it. Five censuses needed updating where the plan named two, and the two nobody would predict are the README verb block, which is a docs file failing in the bin suite, and the derived impl-width sentence in verbs/mod.rs, because adding a single inherent method to Workspace reddens a doc comment (#1521)
ebd3285f · dbf3dbe6…diff - loot ferry parses both policy files once per commit instead of once per changed path, and the instrument had to be built before the fix because PolicyLoads sat on Attributes::load while both ferry doors call parse, so the counter read a structural zero over this whole path and would have read zero after the fix too - a 0-valued counter being indistinguishable from one watching code that does not run. The tally moves to Attributes::parse and Ignore::parse_recorded rather than being routed through a counted door, because the fix itself removes those door calls: a counter on them would read zero afterwards, which is the same blindness relocated. That changes what the counter MEANS, from policy file reads to policy re-derivations, so all three existing pins were re-read rather than adjusted until green - status moves 2/3 to 3/4 with the +1 being its single Ignore::load now counted, and the point is that the GROWTH half passed untouched (narrow equals wide) while only the constant moved, so the test is re-pinned and not re-decided; the tier-exclusion test is still green because the in-process tier links loot-core and never loot-cli, so ADR 0073's exclusion needs no re-taking; and lane_new_sweep's note that policy_loads is one per read_tree_at is repointed to two, since read_tree_at calls both loads. The instrument was proved non-vacuous against the UNFIXED code first, at 18, 66 and 258 parses over four commits of two, eight and thirty-two changed paths, which is exactly 2 plus 2 times commits times width; after, it is 2 plus 2 times commits, so 258 falls to 10 at width 32 and stays 10 as width grows. The most useful thing learned here is a red proof that inverts an assumption: blinding the instrument by putting the tally back on load makes the counter read a constant 1 everywhere, so the constancy pin passes AND the parses-greater-than-zero guard passes, and only the GROWTH assertion catches it - a positive-value pin does not protect against a blinded instrument, which is what ADR 0072's controls bullet credited it with, and that bullet is corrected rather than left standing. This ticket's own wall clock does not reproduce and is corrected rather than repeated: the removed re-parse is 4.63 microseconds per path in release against this repo's real policy files, not 22.0, so a full-history ferry is about 1.9 seconds rather than 8,819 milliseconds - 22.0 is close to the debug reading of 31.53, so the hunt appears to have measured a debug build, and a figure taken under a different build is not a smaller version of the same number. ignored_under is deleted rather than kept as a pure forward once the parse is hoisted, and ADR 0028 is amended because it argued its delete-arm decision partly on an Ignore::parse per deleted path, a cost that no longer exists - the decision stands on the attribution argument, which was load-bearing anyway. seal_under takes a parsed Attributes and narrows pub to pub(crate) since Attributes is crate-private, and its doc said it keeps the bridge from re-parsing the policy twice per path, which was true about the wrong unit: it halved a cost that should never have been per-path. One honest regression is recorded rather than hidden: a deletions-only commit now costs 2 parses where it cost 0, because the hoist is unconditional (#1704)
1822132f · dbf3dbe6…diff - the REPRESENTATION is extracted and the PREDICATE is not, because the overlap between a pathspec and a view is exactly the storage while the difference is exactly the RULE: a pathspec bare docs also keeps docs/adr/x.md, the git dash-dash affordance, and a view line has only the glob. that difference must STAY, and not because the two live in different places - a view line is STANDING MATERIALIZATION POLICY, so an implicit slash-star-star would silently WIDEN every one-word line already sitting in somebody .loot/view, and the widening would surface as FILES APPEARING ON DISK rather than as an error. the affordance is right where an operator types a spec and reads the answer in one breath and wrong for stored policy. the decisive evidence for the shape is that policy.rs ALREADY SOLVES THIS TWICE - IgnoreLine and Rule each pair a glob with its text as written in one struct - so Pathspec and View were the two OUTLIERS rather than a new abstraction being invented. desynchronisation is now impossible BY THE COMPILER rather than by a comment: the pair fields are private to policy, the list is private, and push is the only place a Pattern is built, taking ONE string and deriving both halves - proven by two mutations that FAIL TO COMPILE, E0616 reaching for the private list and E0451 constructing a mismatched pair. the prose invariant that said the two cannot disagree because new pushes them together - exactly the comment this ticket exists to remove - now points at the structure. one allocation per pattern per path leaves the match loop, a format in a loop that runs once per tree entry becomes a strip_prefix, and NO SPEEDUP IS CLAIMED ANYWHERE because nothing was measured: the machine was building throughout, which is the CPU condition that makes the harness load arm skip SILENTLY. six mutations with counts read, and the oracle was checked rather than assumed - which found TWO UNPINNED BEHAVIOURS the pre-existing suite was green under: that the prefix arm stops at a whole path COMPONENT, where every other negative case in the module missed by more than one character, and the rule difference itself, now one test that reddens if the two are unified in EITHER direction (#1552)
fe21c932 · dbf3dbe6…diff - the five-blind-instruments claim was recorded NOWHERE, and three sites cited an ADR that does not carry it - so the roster is derived from in-tree evidence and recorded ONCE, a five-row table whose rows are ticket, instrument, what it could not see, and the in-tree doc that records it. four of the rows came from a fixture doc that already named them, and the fifth from the counter crate description of a control that blessed a shape reading the tree ZERO times. and ONE NIGHT is wrong too: landing times on main put four of them across 2026-09-05 between 01:37 and 06:16 and the fifth at 14:00 THE SAME DAY, so it is one RUN rather than one night, which is what a sibling comment already called it. the count FIVE now appears in exactly one sentence, directly above its own table, where it is derivable as the row count - every other site states the property and cites the heading, carrying NO number of its own. that is the house answer this run reached two commits earlier: name the instances and stop counting. re-grepping found the same defect well beyond the three sites the ticket named - two more uncited fives, one of them attributing the finding to THIS run, three more citations of an ADR 0072 controls section that does not exist, and two comments reading three shipped instruments where a fourth had landed between the last two they name. the pin is what makes it hold: the citations must RESOLVE, so the heading must exist exactly once in the named ADR, the count word must equal the table row count with each ticket appearing once, and all six citing files must name the heading while NONE carries a count of its own - with the heading and the needle assembled at runtime so the file cannot satisfy the scan with its own constants. six mutations, each at ten passed and one failed, including a CONTROL that removes the whitespace-flattening and goes red, which proves the match is not trivially contiguous since three of the six citations wrap across comment lines. two of the six are each other DISCRIMINATION, one reddening only the count arm and the other only the citer arm. and what the pin does NOT do is stated in its own header rather than found out later: it does not check that the five rows are TRUE, each being a ticket and a code site checkable only by hand, and its citer list is hand-maintained, because deriving it by scanning would invert the check and wrongly demand a citation from the one site this ticket said to leave alone (#1664)
6a9d9cac · dbf3dbe6…diff - the policy counter is renamed PolicyParses and policy_parses because it has counted parses since #1704, and the policy.rs block and the ADR 0073 row now point at the variant doc instead of restating why; no stored perf record carried the old key, because the gate does not record this counter. PUSH_SHAPE_DEPTH, OFFER_GUARD_DEPTH and MISSING_DEPTH are literals rather than aliases because their reasons have diverged, and each is still 128. ride_entry takes one RideState instead of three maps, while the one_pass_bundle oracle keeps its own transcribed walk and memo so the byte-identity test still compares two walks, and still went red when the key arm was disabled. the gated counters read 743, 200 and 24 before and after (#1718)
bbd04ef4 · dbf3dbe6…diff - the two arms of a pathspec compare one spelling now, a .lootignore subtree line spelled with a backslash stops ignoring nothing, and the symptom the ticket reported is measured as unreachable through the CLI. delta::Pathspec::matches read its bare-directory prefix arm off the pattern text as typed while its glob arm read a normalized one, so at the library door --path docs\adr kept docs/adr and nothing beneath it; and Pathspec::new trimmed a trailing / off the raw argument, so docs\ was neither trimmed nor matched by either arm and kept nothing at all. both arms ask policy::unix_separators now, which is where the rule lives, and Pattern::normalized is borrowed off the compiled pattern of the glob rather than stored beside it, so the text a reader compares and the glob that matches it are one string rather than a third value to keep in step. the headline of the ticket is FALSE at the CLI, and that is the main finding: PATHSPEC declares --path a path flag with a Slot::Path tail, the argv door rebases every such token (#1330), and that rebase ends in treepath::store_key, so a pathspec reaching the matcher from a real process is /-separated already on Windows and on POSIX alike, and the one route that skips the rebase, no repo above the cwd, ends in the not-a-loot-repo refusal of the verb itself before any path is matched. measured through the spawned binary on this Windows machine over status, diff, the positional form and a run from a subdirectory: every spelling already selected identically. so no separator hint is added to refuse_unmatched, because a hint naming a cause that cannot be the cause is worse than none; what the refusal does name now is the pattern in the spelling it is compared in, since Pathspec::new normalizes before the trim it always applied. the reachable instance is the one the acceptance criteria sent me to look for: Ignore::parse_recorded read its trailing-slash subtree affordance off the line as typed, so build\ missed strip_suffix and compiled to the glob build/, which matches no path at all, and the line ignored NOTHING where build/ prunes - fail-OPEN, the files staying in the snapshot and sealing internal, the tier a relay reads by design. .loot/view is correct and is pinned rather than claimed: View::matches is a glob match and nothing else, so it has no second arm to disagree with. red first on the tree as it stood: the delta pin fails at the bare-directory arm (0 passed, 1 failed), and reproducing the reported symptom through a process takes the library and the door together - with treepath::store_key dropped from normalize_rel and Pathspec put back, status --path docs\adr refuses where the slash twin narrows, while either half alone leaves the process pin green, which is the measurement that says the CLI was never broken. mutations: normalized returning the text as typed reddens the Patterns pin (26 passed, 1 failed, left docs\adr right docs/adr); the trim taken off the normalized spelling reddens the trailing-separator arm (41 passed, 1 failed); the subtree affordance read off the raw line reddens the ignore pin (25 passed, 1 failed, left false right true at build/a.o); and a second spelling of the rule, in delta.rs or in policy.rs, reddens the locality census (1 failed each). that census asserts at most one spelling rather than exactly one, because rewriting the normalizer without that literal keeps every behaviour pin green (34 passed) and an exactly-once census would tax a refactor instead of guarding against a second home; a not-gone-blind guard sits beside it. CONTEXT.md carries both halves, the store_key spelling of the rebase as the reason the pathspec defect was inert and the .lootignore separator as the fail-open one that was not. no migration, and no forge or site byte moves, so this owes no deploy. the workspace suite is green (3852 passed over 119 binaries, 7 ignored) (#1859)
67f8adf1 · dbf3dbe6…diff - a .lootattributes rule whose pattern ends in a separator refuses the capture instead of sealing its subtree internal, and the decision is recorded with the widening it declines. the trailing-separator subtree affordance is .lootignore dialect - build/ prunes there, and #1859 repaired it there - while an attributes pattern is compared against the whole relative path, which never ends in a separator, so docs/ restricted=connor matched NO path and every path under docs/ fell through to Internal, the ANYONE-granted tier a relay reads by design. reproduced through the spawned binary first, which is what the ticket asked: loot new sealed docs/plan.md internal at exit 0 with one first-seal-summary line as the only signal, the #1108 shape exactly, and loot explain said the rule was tried and none matched. the ticket offered two answers and they are not equally safe, so the rejected one is measured rather than argued: reading the separator as .lootignore does MOVES what a path resolves to, and first-match-wins means not always narrower - with it honoured, docs/ internal above docs/secret/** restricted=connor resolves docs/secret/keys.md internal, and a_recorded_inert_pattern_moves_no_paths_tier goes red (27 passed, 4 failed over the file). silent exactly where the mis-seal gate is blind, a first seal of a basename that is not secret-shaped; an already-recorded path would trip the demotion guard instead, loud and just as unasked-for; either way the operator file would mean something new because the binary changed. so the line is RECORDED, not re-read: Attributes::parse keeps every rule exactly as it always parsed it, so no path changes tier and ferry ingest, which reads the rules and never the faults, is untouched, and the line becomes a MalformedRule that Workspace::malformed_attrs_gate already raises RepoError::MalformedAttrs over - one variant and one slug for both faults, because the taxonomy grows where a driver must act differently (#1582) and the tree one consumer of the slug does not, so each fault renders its own remedy beside its own line rather than one sentence teaching a reader the mistake they did not make. read off the compiled glob, so the docs-backslash spelling a Windows shell completes is the same fault and is quoted in the spelling it is compared in, the one loot explain prints for that line (#1859). scoped to the trailing separator and not to a pattern that matches nothing, since a bare docs internal is a fine rule for a file named docs. VISIBILITY blast radius measured rather than accepted: every .lootattributes in the estate - this repo and its lanes, scripts, millerbyte, the test and perf-scratch repos - plus every recorded revision of this repo own, holds no trailing-separator pattern, so no position next capture refuses and no repo changes tier, and the_shipped_attributes_holds_no_rule_the_gate_refuses keeps the shipped file that way. mutations: the recording dropped reddens the unit pins, the seam pin and the spawned pin (28 passed, 3 failed; 4 passed, 1 failed; 0 passed, 1 failed, the last naming the capture that succeeded); the separator honoured instead reddens four including the no-widening pin; a docs/ line added to the shipped .lootattributes reddens its census (0 passed, 1 failed). the path compared never ending in a separator is measured too, since the argv door rebase strips one and loot explain docs/ asks about docs. ADR 0038 carries the #2030 amendment and CONTEXT.md both halves, the attributes refusal and the .lootignore entry stating its affordance does not reach the other file. no migration, and no forge, relay or site byte moves, so this owes no deploy. the workspace suite is green (3859 passed over 119 binaries, 7 ignored) (#2030)
3e849712 · 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 - sweep 1 fix-up over #1826, #1837 and #1839: a live change only a superseded head reaches is listed in the fork view, in a third section of its own, rather than in no section at all. LogGraph::retain_heads leaves such a change with an empty reachable_from, and history() partitioned the fork on exactly one head and more than one, so after a move --onto the ancestor the old tip alone reaches was printed nowhere - reproduced first through the spawned binary with two live lines (0 passed, 1 failed), the fork and the non-empty superseded head both holding as controls. GraphHistory gains unreached, the renderer prints it under behind a superseded tip after the shared ancestry, drawn_rows leads with it because such a change can only be a child of a lane or shared row, the path filter, retain_versions and all_rows walk it, and both machine shapes carry the row with no new column. HEAD on an untracked position with no working change now counts the heads Liveness::is_superseded rejects and answers the sole counted head by name, because Position::anchor answers the first graph head, which can be the superseded one; measured at the other two sites the ticket named, propose already reads is_live and IntegrationPreview deliberately counts the whole partition with retire for the superseded ones, so the liveness doc now points at the callers of the predicate rather than listing two readers. the move --onto usage line gets its space back; refit_shape spells its path columns through treepath::store_key with a backslash pin, and the #2033 census records the tree-path axis it is blind to by design; the shallow clause has one spelling, workspace::parents_not_held, read by the notice and the push refusal, with the pin the suite lacked; carried is carried_count, Reanchored.change is change_id, the squash and absorb op-log lines say re-anchored, frontier_beyond_the_host is private, the one-flag-member sentences drop one, ADR 0089 names both readers of .loot/shallow and both frontier spellings, and the one raw exit ADR 0089 rests on is held to one by a census in tutorial.rs. own > 0 for the move --onto stale head is pinned off the loot heads row. red first, six mutations in one round over disjoint pins: the unreached filter blinded, drawn_rows reordered, the HEAD filter blinded, store_key undone and a second raw exit added redden five pins with the sync shallow control green (1 passed, 5 failed) and the spawned fork pin (2 passed, 1 failed); the noun-verb swap survived tests/shallow (10 passed, 0 failed), which is why the clause has a pin now (0 passed, 1 failed under the swap). ADR 0066 keeps its gated 28 and points at the gate instead of at tickets. no migration, no wire or format byte moves, and no forge or relay byte moves, so this owes no deploy. the workspace suite is green (3895 passed over 122 binaries, 7 ignored) (#2046)
0b55a20f · dbf3dbe6…diff - a .env in the middle of a basename is secret-shaped too, and the backup written beside a sealed credential file is refused rather than sealed Internal: #1108 widened the mis-seal gate to the .env suffix and recorded one boundary as deliberately left open, that <name>.env.<suffix> answered to neither affix, on the argument that closing it with *.env.* would swallow the committed-template convention; that was an argument and not a measurement, so this measures it first. over every git-tracked basename in every git repo directly under this estate, 25 repos and 12,732 paths on 2026-09-20, the arm newly catches one path, the scripts repo .setup.env.example, and nothing else, which is the template class #1108 feared and also the one shape the arm cannot be narrowed away from by name, since a template and a backup differ only by the suffix the operator typed and a suffix allowlist would be a hand-maintained list of exactly the kind AGENTS.md names, guarding a credential. what bounds even that catch is the gate scoping rather than the pattern, first seal and Internal-by-fallthrough only, so a path already in the anchor or named by a rule of its own never trips and the template costs one .lootattributes line, once, against a false negative that loot burn can only bound. the case is this estate again: scripts seals .setup.env by exact basename, its gitignore covers the whole family since loot#1643, and a .setup.env.bak-v0420 sits in that working tree named by neither of the two loot policy files. reproduced through the spawned binary in a throwaway repo before and after, a .setup.env.bak-v0419 sibling beside a .setup.env restricted=connor rule captured and sealed internal with no refusal on 0.4.22 and refused by name on the lane binary, with one rule reaching the sibling sealing it restricted instead. ADR 0069 carries the measurement table and ADR 0038 section 1 closes the boundary it had recorded as open. red under mutation, counts read each time: the arm removed (0 passed and 2 failed, the gate handing back the old Ok with the sibling Internal), the arm widened to a boundary-free env substring (0 passed and 2 failed, on the negative controls both here and in the suffix pin), the refusal condition stripped of its tier and fallthrough halves so the sealed original is named beside the sibling (0 passed and 1 failed), and the arm narrowed to a suffix allowlist that spares the template (0 passed and 1 failed). the scripts .lootattributes half of the ticket is untouched and stays with the operator, being a visibility rule in another repo. no migration, no wire or format byte moves and no host behaviour moves, so this owes no deploy. the workspace suite is green (4051 passed over 128 binaries, 8 ignored) (#1930)
85479732 · dbf3dbe6…diff - the clean sieve asks the separator rule at the half that descends too, and the census says which direction it reads rather than certifying the other one: #2033 routed Sieve::keep and left Sieve::descend handing the walk native rel straight to extra.ignores_dir and to ignore.descend, inert because each rule it feeds normalizes what it is given, and descend asks policy::unix_separators now so that half stops passing on a spelling it never decided. the reason #2035 gave for the census not seeing that site is refuted, and measured refuted: ignores_dir takes rel as a &str, so it is a door by the census own reading, and a hand-spelling planted in descend reddens the census naming src/clean.rs fn descend, before the routing and again with it undone (0 passed and 1 failed each time). what the census is really blind to is the ABSENCE of an answer rather than a second one, which is the same direction its own fixture had been calling the fixed shape, so that leg says the direction this census does not read instead, and the blindness note states the one shape it reads and calls everything outside it blind by definition rather than carrying a roster that grows. measured on the tree: rel.to_string() in Sieve::keep leaves the census green and reddens the behaviour pins (11 passed and 4 failed, a_recorded_path_with_uncaptured_edits_survives naming sub/recorded-deep.txt, a recorded file the verb would have deleted). the coverage the pair census gave up is stated too: a door is read off a signature naming a path, so Patterns::push taking text is not one and delta::Pathspec::new asks none, and widening the door reader to text: &str is declined on a measurement, since it admits push, whose bare name is Vec::push, and the census then reports its own home unix_separators beside three more functions spelling a store key or a display line, the other axis. the count-nonzero floor over the walked files goes, replaced by the property it stood in for, that a crate-wide census must have read a door caller outside the file its doors come from, which reads red with the walk truncated to policy.rs (0 passed and 1 failed, naming the eight askers left in it). the offence list is computed once rather than twice, for the assertion and its message. the ADR 0038 sentence #2033 rewrote stays: it cites a pin that stopped naming that path in the same land, so restoring it would restore a stale citation, and the paragraph below it already records the move. the record correction owed on #2030 is reported to the spawning session, being a comment. no migration, no wire or format byte moves and no host behaviour moves, so this owes no deploy. the workspace suite is green (4103 passed over 132 binaries, 8 ignored) (#2035)
5cd64d96 · 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.