50 ancestors of 481ac23eafe7f9e8
|
2026-10-25
| ||
| 21:00 |
feat(commit): carry a file into a check-in by UUID without its content
FileToCommit gains a UUID field. When set, the F card names that artifact verbatim and nothing is read or stored, reaching the internal manifest.File.UUID path that sparse checkouts already use. A caller that lists a parent's files can now commit with PartialManifest, dropping files, by naming the unchanged part of the tree instead of re-reading and resubmitting every byte of it. Both UUID and Content on one file is refused; so is a UUID the store does not hold outside an eliding cone. leaf check-in: 481ac23eaf user: [email protected] tags: feat/commit-carried-by-uuid, main, trunk | |
|
2026-10-24
| ||
| 19:19 |
Merge pull request #485 from danmestas/feat/horizon-seed-floor
fix(horizon): date a seed no earlier than the origin's minted floor check-in: d47958493b user: [email protected] tags: main, trunk | |
| 19:04 | fix(horizon): read a NULL minted floor as none and replace it leaf check-in: 387b9de549 user: [email protected] tags: feat/horizon-seed-floor, trunk | |
| 19:03 |
fix(horizon): keep the minted floor best effort
A mint wrote the floor on every structural pull and failed the request when it could not, so a read-only mirror or a store whose write lock was held no longer served the structural horizon. The floor is now written only when D moves later, and a failed write is ignored: the mark is a SHOULD, and without it the next seed is dated from the tag rows as before. A stored value that is not a canonical integer, which an overflowing cast would rank above every D, is replaced. check-in: 8356d3d19e user: [email protected] tags: feat/horizon-seed-floor, trunk | |
| 18:56 |
fix(horizon): date a seed no earlier than the origin's minted floor
Shunning and purging the latest tag event removed the tagxref rows a seed's D was dated from, so the next seed was earlier than the one every structural clone held. Each clone's D floor refused it as a replay and it fell back to the content horizon until the repository's tags caught up with the purged tag's time; a far-future tag held it there indefinitely. The draft (zit 39546186f4, §5.2.2) now has an origin date a seed no earlier than the latest D it has minted and keep that D across restarts. Mint reads the mark from the store's horizon-seed-floor config row, dates the seed at the later of it and the latest tag event, and raises the row after minting with a conditional upsert, so concurrent mints never lower it. No config group transfers the row, so it is never sent. Re-pins draft-fossil-horizon-00 at 39546186f4 and adds the conformance case horizon.5.2.2.seed-date-shun, which fails without the floor. check-in: 1fd0dfdbc1 user: [email protected] tags: feat/horizon-seed-floor, trunk | |
| 18:36 |
Merge pull request #484 from danmestas/release/v0.16.1
chore(release): v0.16.1 check-in: 4ac6baf2ea user: [email protected] tags: main, trunk, v0.16.1 | |
| 18:28 | chore(release): v0.16.1 changelog check-in: 08d681d6c3 user: [email protected] tags: main, trunk | |
| 16:47 |
Merge pull request #482 from danmestas/feat/horizon-dfloor-receive
feat(horizon): re-pin to zit caeef421ee: repository-wide seed D and the receive rule check-in: 7b6f43f1dc user: [email protected] tags: main, trunk | |
| 16:39 |
ci: pin zit interop to 0.7.2 and require the seed-date test
zit 0.7.2 (tag version-0.7.2, check-in 1dcb2d4a29) dates a seed at the repository's latest tag event, as go-libfossil now does. Its horizon draft is the caeef421ee text pinned here (artifact ab18fa7f89). - ZIT_VERSION is 0.7.2, and ZIT_SHA256 is the checksum of zit-0.7.2-linux-x86_64.tar.gz, checked against the release's SHA256SUMS. - REQUIRE_ZIT_SEED_DATE=1 sits next to REQUIRE_ZIT_BIN, so TestHorizonSeedDateZit cannot skip in CI. - The README, horizons.md, CHANGELOG and test comments now say interop is pinned to zit 0.7.2. check-in: 0c30e0d32e user: [email protected] tags: feat/horizon-dfloor-receive, main, trunk | |
| 14:45 |
test(conformance): seed byte-identity with zit over a cancelled tag
TestHorizonSeedDateZit has zit mint the seed for a repository whose newest tag event is a cancel, and requires it to match go-libfossil's byte for byte. Both now date a seed at the repository's latest tag event (§5.2.2). No string in the binary tells a zit from before caeef421ee apart: its version still reads 0.7.1. So the test recognises an older zit by an earlier D in an otherwise identical seed, and skips it unless REQUIRE_ZIT_SEED_DATE=1. CI's pinned zit 0.7.1 skips. A zit built at caeef421ee passes with the variable set. leaf check-in: ad82b4a46d user: [email protected] tags: feat/horizon-dfloor-receive, main, trunk | |
| 14:34 |
Merge feat/seed-receive-drop; re-pin horizon draft to zit caeef421ee
Re-pins draft-fossil-horizon-00 to zit trunk check-in caeef421ee (file artifact ab18fa7f89), which changes §5.2.2, §5.2.3 and §6.2.3 and adds two Appendix C bullets. This brings in the local feat/seed-receive-drop branch, which implements the receive rule, and resolves it against main (#477 chain-walk bounds, #478 seed binding). §5.2.2, the seed's D (tracked in #479): - Mint dates a seed at max(tagxref.mtime) over every row in the repository, cancels and propagated rows included, as zit's latestTagEvent does. It was the latest of the rows the seed summarizes. - A cancel of the newest tag, or a reopened closed branch, therefore no longer mints a seed earlier than the one a clone holds, which the clone's D floor refused as a replay. §5.2.3, the receive rule: - A pull, clone or sync drops a seed by this store's label or by a binding card when it can read it, and the link step refuses one completed later. - The draft also requires that a delta kept without its base, pushed or received, is not announced or served while it cannot be expanded. feat/seed-receive-drop only withheld pushed ones. The pending mark moves into storeDeltaContent, so a pull marks too, and a clone marks what is still unreadable when it ends (seedcheck.WithholdUnreadable). That avoids judging every dangling file delta of a clone a second time. - Since #478 minted seeds carry a binding card. The seed-shaped-ordinary cases therefore strip it, and a new test checks that a seed with the card is never linked. Conformance rows: - HZ-5.2-2 takes the new D text. - HZ-5.2-4 loses the old ordinary-artifact sentence. - HZ-6.2-6 says the D is fixed by the repository's tag events. - New rows: HZ-5.2-14 (the receive-path drop; PARTIAL, since a seed completed in a later round is kept inert, and was counted when its delta arrived), HZ-5.2-15 (the dropped-hash record) and HZ-5.2-16 (withholding unexpandable deltas). - New cases: two D cases mirroring zit's "horizon seed date" regression, a cancel on C and a cancel at an ancestor below it, and five receive cases. - The deepen-floor case now replays an older seed, since an honest origin no longer mints one. check-in: 5d0ef566aa user: [email protected] tags: feat/horizon-dfloor-receive, main, trunk | |
| 13:58 |
Merge pull request #481 from danmestas/release/v0.16.0
chore(release): v0.16.0 check-in: 2f1e5f1e3d user: [email protected] tags: main, trunk, v0.16.0 | |
| 13:50 | chore(release): v0.16.0 changelog check-in: 3126bf8e03 user: [email protected] tags: main, trunk | |
| 13:50 |
Merge pull request #480 from danmestas/ci/zit-interop
ci: install pinned zit 0.7.1 so the zit interop tests run check-in: ff2929c770 user: [email protected] tags: main, trunk | |
| 13:42 |
ci: install pinned zit 0.7.1 so the zit interop tests run
The zit interop and horizon-seed tests compared bytes with a real zit only on machines that had one; CI installed none, so every one of them skipped there. The modernc job now installs zit's released 0.7.1 Linux tarball, pinned by its SHA256SUMS entry as fossil is, and runs the unit tests with REQUIRE_ZIT_BIN=1 so a missing or outdated zit fails the run. check-in: 4f2abff4ec user: [email protected] tags: main, trunk | |
| 13:29 |
Merge pull request #478 from danmestas/feat/horizon-seed-binding
feat(horizon): bind each seed to its horizon and sign seeds apart check-in: b35d577a54 user: [email protected] tags: main, trunk | |
| 13:16 | Merge branch 'main' into feat/horizon-seed-binding check-in: 707b42538c user: [email protected] tags: main, trunk | |
| 13:16 |
feat(horizon): bind each seed to its horizon and sign seeds apart
Every seed an origin mints now carries T +horizon-seed <C> <rule> (draft-fossil-horizon-00 §5.2.1), inside the signed bytes. The card is not counted as a tag in force, Mint never derives a horizon-seed row into a seed, and Apply skips the card. Before a clone holds a seed, signed or not, it checks the seed against its store cone (§5.2.4). The seed must carry exactly one binding card naming the store's C and rule, and its D must be no earlier than the held seed's. A seed that fails is refused as a wrongly signed one and the clone falls back to the content horizon. A deepen discards the held seed first, so the floor restarts at the new C. A held seed that is no longer bound is discarded when it is next applied. Seeds are signed with their own horizon-seed-command and are no longer signed by clearsign and pgp-command. SSH seed signatures use the fossil-horizon-seed namespace. clearsign.Key.Verify now takes the namespace, and check-in verification keeps fossilscm. Adds 9 conformance cases and flips HZ-5.2-8, HZ-5.2-11, HZ-5.2-12 and HZ-5.2-13 to covered. The zit interop gate moves to zit 0.7.1, whose seeds are byte-identical with ours. check-in: 451d368554 user: [email protected] tags: main, trunk | |
| 13:16 |
Merge pull request #477 from danmestas/fix/bound-chain-walks
fix(sync): bound basis-chain walks at the read depth check-in: 8d3b1ef28a user: [email protected] tags: main, trunk | |
| 13:08 |
fix(sync): bound basis-chain walks at the read depth
Any pusher could hold the write lock about 30s per push with a two-delta cycle. The receive pass's basis-chain walks, and the horizon seed check's pending-row walk, ran to 1<<24 steps before giving up. All of them now stop at content.MaxDeltaChainDepth, the depth at which content.Expand stops, so no walk outlasts what a read could ever use. What crossing the bound means at each site: - chainEnd (rootOf, for a dangling delta being stored) keeps a visited set. A chain that loops is refused, as it was, but at the revisit rather than after 1<<24 steps. A chain longer than the bound returns errChainTooDeep. storeDeltaContent then stores the delta untracked: the push succeeds, as a push onto a chain between 16k and 16M deep always did, and since no basis can make the delta readable there is nothing to check against its name later. Storing matches the current behaviour, and an honest transfer never fails for a deep chain it did not build. - verifyFrom's cascade carries each row's depth from the basis that arrived, its own chain included. It does not check, or descend past, a row deeper than the bound: no read can produce those bytes, so the pass owes them no check. A delta that does not hash to its name past the bound no longer aborts the pass. - pathRows loses its step counter. Its visited marks already stop it at a row on a path, so it visits each row once, and the cascade's depth check bounds what it verifies. - seedcheck's chainRoot gives a chain that loops or runs past the bound no root, consistent with ErrChainUnresolvable: the row stays pending, withheld, and is never judged. Tests: - Closing a two-delta cycle is refused in under 5s (it took 27s). - A push onto a chain one past the bound is stored. - chainEnd stops exactly at its limit. - A dangling delta with a wrong name one node past the bound does not abort the push, whether the basis that arrives is full text or a delta deep on a held chain, while one within the bound is still checked and readable. - MarkPending gives an over-deep chain no root, so filling its far end examines nothing, and returns at once on a cycle. - TestHorizonOverDeepPendingChainDoesNotBlockFill still passes. - Removing each bound, the cycle check, the untracked store or the root depth fails a test. - chainDepth's own early stop is an equivalent mutant: the cascade's root has just been expanded, so its chain is within the bound unless the store is damaged. check-in: eac16cf7ed user: [email protected] tags: main, trunk | |
| 12:32 |
Merge pull request #476 from danmestas/feat/horizon-names-seed-refusal
feat(horizon): named horizons with depth, and origin seed refusal check-in: 5a7c2ba12e user: [email protected] tags: main, trunk | |
| 12:11 | docs(conformance): note the horizon pin matches zit's version-0.7.1 tag check-in: cc09488fc6 user: [email protected] tags: main, trunk | |
| 11:42 | Merge branch 'feat/471-seed-refusal' into feat/horizon-names-seed-refusal check-in: 928524bd8c user: [email protected] tags: main, trunk | |
| 11:10 | Merge branch 'feat/471-seed-refusal' into feat/seed-receive-drop check-in: b6e785b914 user: [email protected] tags: feat/horizon-dfloor-receive, main, trunk | |
| 11:10 |
fix(sync): an unresolvable pending chain no longer blocks a fill (#471)
Review of a067c6e. JudgeFilled treated content.Expand's chain-bound error as a store fault and returned it, and DeltaStored failed storeDeltaContent with it. A pusher could plant a dangling chain longer than maxDeltaChainDepth under a basis R, building it by re-rooting. After that, every honest push, pull or clone that supplied R as a delta failed for good. Fix: - content now wraps its cycle and depth errors in ErrChainUnresolvable, and exports MaxDeltaChainDepth. - seedcheck counts ErrChainUnresolvable as unreadable, alongside a hash mismatch and a delta that does not apply or fails its checksum. Such a row is skipped and stays pending, so it stays withheld. - Real faults are still returned, such as content that does not decompress. - The classification uses errors.Is on the sentinel, so the expansion JudgeFilled already performs is not repeated. Tests: - TestHorizonOverDeepPendingChainDoesNotBlockFill plants a pending row one node past the bound under R, then pushes R as a delta on a held basis. The push succeeds, R is stored and readable, and the over-deep row stays withheld. - TestUnresolvableChainSentinel pins the sentinel for a cycle and for a chain past the bound. - Dropping the classification, or either wrap, fails a test. A cycle never reaches seedcheck: the receive pass's own basis-chain walk (receive_pass.go chainEnd) refuses it first. check-in: 8b946de415 user: [email protected] tags: main, trunk | |
| 07:10 | Merge branch 'feat/471-seed-refusal' into feat/horizon-names-seed-refusal check-in: 0b3bc67376 user: [email protected] tags: main, trunk | |
| 06:59 | Merge branch 'feat/471-seed-refusal' into feat/seed-receive-drop check-in: 68cf05d9ea user: [email protected] tags: feat/horizon-dfloor-receive, main, trunk | |
| 06:59 |
fix(sync): judge pending rows when a received delta fills their basis (#471)
Two review nits. Liveness. A pushed artifact pending on a basis P stayed withheld for good when a pull supplied P as a delta: - Only MarkPending re-rooted, and only for a pushed delta. - Filling a phantom with a delta runs no cascade, so nothing judged the waiting rows if the delta was readable, or moved them to P's own root if it was not. A later fill of that root never reached them. The fix is one hook at the choke point every received delta passes, storeDeltaContent: seedcheck.DeltaStored. - If the stored delta is readable, the rows pending on it are judged. - If not, they move to its chain root, so whatever finally resolves the chain judges them. - It replaces MarkPending's push-only re-root. Error surfacing. JudgeFilled still skips a row whose own bytes cannot be read (a delta that does not apply or fails its checksum, or bytes that are not the artifact named) and judges the rest of the batch. Any other failure is now returned, a broken chain to the filled root included: check-in and tag return it, and the cascade logs it, as it already did for JudgeFilled errors. The separate availability skip went with it. Tests: - A pushed plain file and control artifact pending on a basis that a pull supplies as a delta, on a held basis or on one that arrives in the next round. Both become readable and stop being withheld, are announced on a push-on, and the control artifact is linked. - A row whose content does not decompress is reported. - A checksum-failing row is skipped like an invalid one. - Each hook branch and each error class fails a test when changed. check-in: 584c55d1fd user: [email protected] tags: main, trunk | |
| 06:49 |
test(conformance): re-pin the horizon draft to the zit 0.7.1 text
Pin draft-fossil-horizon-00 at zit 236b7b4285 (artifact 130b85e386), which adds named horizons (§6.2.7), the origin's own seed label and the refusal of a pushed seed on expanded content (§5.2.2, §5.2.3, §5.7), and the seed's horizon-seed binding card with the clone's checks on it. Re-read horizon.tsv against the new text: the seed-label, push-check and named-horizon rows follow its wording; the client rows of named horizons move from §6.3 to §6.2.7 (HZ-6.2-19 to HZ-6.2-21); the store-local judging of a late seed and the binding-card shape refusal get rows of their own (HZ-5.2-9, HZ-5.2-10). The binding card, its no-derive rule, the clone's binding and D-floor checks and the separate seed-signing namespace (HZ-5.2-8, HZ-5.2-11 to HZ-5.2-13) are not built and show as gaps. check-in: cfa0577b2a user: [email protected] tags: main, trunk | |
| 06:39 |
Merge branch 'feat/471-seed-refusal' into feat/horizon-names-seed-refusal
# Conflicts: # conformance/horizon_test.go # docs/site/data/conformance.json check-in: 81d00b2e3b user: [email protected] tags: main, trunk | |
| 06:29 | Merge remote-tracking branch 'origin/main' into feat/horizon-names-seed-refusal check-in: d24a684bbe user: [email protected] tags: main, trunk | |
| 06:28 |
Merge feat/471-seed-refusal into feat/seed-receive-drop
Takes the client-side announcement fix. A seed refused at the link step without a pending row (one a pull or clone completes in a later round) leaves unclustered through refuse(). A push-on from that store never names it, and a test now pins this. check-in: 9f0b995575 user: [email protected] tags: feat/horizon-dfloor-receive, main, trunk | |
| 05:58 |
fix(sync): never announce a withheld artifact from the client side (#471)
Review of 0e28b6a: when the origin pushed on to a peer, its own client announced artifacts its replies withhold. A pending seed went out from unclustered, and after verify.Rebuild (which marks every blob unclustered and unsent again) a refused seed went out too. The peer kept asking, loadFileCard withheld it, and the push ran out of rounds. Fixed in the tables the client announces from, not in each reader: - MarkPending takes a pending row out of unclustered. - decide puts it back once judged not a seed; a seed stays out. - rebuildBookkeeping leaves withheld rids out of unclustered and unsent. - That also keeps them out of cluster minting, which reads unclustered. - sendPrivate and the private gimme answer, which read the private table, skip withheld rids. - sendUnclustered needs no filter of its own, and sendAllClusters lists only linked clusters, which a withheld row never is. JudgeFilled now skips a row that does not expand (it stays pending) and judges the rest of the batch, instead of returning. Tests: - A pending seed is not announced on a push-on, publicly or privately. - A peer asking for a private pending seed outside a private sync is not told it exists. - After a rebuild, a push-on converges and never names the refused seed. - A pending file, once judged, is announced again on a push-on. - An unreadable row does not stop the next fill in the batch being judged. - Every new mechanism fails a test when disabled. check-in: df78b8aee9 user: [email protected] tags: main, trunk | |
| 05:21 |
Merge feat/471-seed-refusal into feat/seed-receive-drop
Takes feat/471's no-delete model: a refused seed stays stored, refused by its hash, and is never linked or served. Its effect on the receive side: - A seed a pull or clone can read on arrival is still never stored. - One completed in a later round is refused by the link step and kept, inert, rather than swept. The clone's sweep is gone. - Refusing a seed at the link step takes it out of unclustered, as a push-side refusal does. - A dropped seed's phantom row stays inert, since deleting it would free its rid under a receive pass. loadDBPhantoms skips refused hashes, so it is never asked for again. - The client's addPhantom filter is gone: a refused hash always has a row by then. - The handler keeps feat/471's recordStored, which refuses a re-push whose row is already held. A test now pins the client's verified-delta refusal (a seed sent as a delta on a held basis), and seeds dropped on receipt are asserted not stored (assertDropped). check-in: 5b43d4fe38 user: [email protected] tags: feat/horizon-dfloor-receive, main, trunk | |
| 04:58 |
fix(sync): keep a refused seed's bytes; record the refusal only (#471)
Final review of c077fa4. F.1 requires recording a refusal, not deleting the bytes, and every finding came from the deletion: - A delta built on a swept seed lost its basis, was withheld forever, and could not be re-pushed, because it counted as held. - The end-of-sync sweep ran outside a transaction. - A refused hash pulled from an upstream was dropped, which is a receive-side behaviour F.1 does not have. So a refused seed stays stored: - Its hash is recorded in seedrefused. - It leaves unclustered, so no inventory or cluster names it. - It is withheld from every reply, and the link step never links it. - Deltas on it stay readable and are judged like any other pending row. - Pulling the same hash stores nothing new: the row is held, and stays refused. - A re-push of it is refused on its bytes. Every form (full text, delta on a held basis, delta whose own row is held) expands to the seed. Removed: - Sweep, sweepRow and its no-root marking. - Every Sweep call: handler request end, end of sync, check-in, tag and rebuild. - requestPhantoms' refused-hash filter (a refused seed is held, so never a phantom). - ownSeedReadable's refused-hash check and its Clear (the fill's JudgeFilled already clears a non-seed). - JudgeFilled's lazy re-root, which nothing reached: only full text fills a root JudgeFilled hears of. Kept, each pinned by a test: - Pending rows keyed by chain root, with MarkPending moving the rows waiting on a rid when that rid is itself pushed as a dangling delta. Filling a phantom with a delta triggers no cascade, so this is the re-root that matters. - JudgeFilled examining only its subtree, and its hash-mismatch skip. - The link-step judge and the refused-hash check in it. - The push-time atomic refusal. - Withholding pending and refused rows, and Purge forgetting pending rows (rids are reused). Only pushed dangling deltas are marked pending. A dangling delta stored by a pull onto a pending row's basis is not, so rows waiting on it stay withheld until the link step reaches them; a plain file among them stays withheld. Tests: - The reviewer's probe, for a control artifact and a plain file built on a pending seed, pushed after it and before it: after the base arrives both are readable and not withheld, and the control artifact is linked and served. - A plain file after a forged delta is still cleared. - Every judge, filter and mark fails a test when disabled, except two performance mechanisms no behaviour shows: the per-transaction table cache and IsSeedBytes' byte prefilter. check-in: 6083693442 user: [email protected] tags: main, trunk | |
| 04:08 |
feat(sync): drop seeds on every receive path (horizon F.2, F.3) (#471)
A store keeps a seed only through a horizon-seed announcement. This lands with the zit 0.7.2 draft re-pin, after feat/471's F.1. - storeReceived refuses, before storing, a seed it can read: full text, or a delta verified against a held basis (errSeed). The push handler turns it into the atomic refusal. The client and clone drop it: not stored, not counted, and its in-memory phantom forgotten. Its hash is refused. - A hash refused earlier is dropped when sent again, even as a dangling delta, and a client never makes a phantom of one, so it is not asked for again. - The link step refuses any seed-shaped artifact it reaches, whatever stored it. An announced seed is held outside the artifact store, so it never reaches the link step. This catches a seed completed in a later round of a pull or clone, through the dephantomize cascade or the clone's receive linker, from the deck the link step has already parsed. - Received dangling deltas are not marked pending. A clone's history chains arrive newest first, and judging each would expand it a second time. - A clone sweeps refused rows after its linker finalizes. B.2's last sentence follows F.2. A seed-shaped artifact outside an announcement is ordinary only with neither this store's label nor a binding card. hzSeedOrdinary mints under zit's label, and the docs and the HZ-5.2-4 scope note cite F.2 and F.3. Tests: - A pull dropping a seed by label and by card, counted correctly and not asked for again by a second peer. - A seed delta completed in a later round, dropped on pull and on clone. - A clone dropping label, card and a re-sent form, counting none. - A refused seed re-sent to a client as a dangling delta. - Disabling any of these mechanisms fails a test. check-in: 21746b5036 user: [email protected] tags: feat/horizon-dfloor-receive, main, trunk | |
| 03:44 |
fix(sync): refuse pushed seeds by hash; judge only what a fill grounds (#471)
Horizon F.1, push side, and the fourth review's blocking finding. Refusal by hash: - seedcheck keeps two store-local tables, carried by no config group: seedpending(rid, root) and seedrefused(uuid). - A seed judged after the request that pushed it, because a later push, pull, check-in or tag filled its basis, has its hash refused. Its rows are deleted by Sweep, and only where no receive pass holds rids: end of request, end of sync, check-in, tag and rebuild. A deleted rid is reused by the next row, and a pass's caches are keyed by rid. - Until swept, such a row is withheld and never linked. A delta on a refused seed is left unreadable and pending under no root. - requestPhantoms never asks for a refused hash. ownSeedReadable refuses a re-push of one, and a full-text re-push is refused on its bytes. - A purge still forgets pending rows (ForgetDeleted). Refusals are keyed by hash, so rid reuse cannot pass them on. Judging only what a fill grounds: - JudgeReadable rescanned every pending row on each fill. A pusher could plant dangling deltas without bound and make every later fill pay for all of them. - Pending rows now record the root of their basis chain, re-rooted when that root is itself stored as a dangling delta. - JudgeFilled judges only the rows pending on the rids just stored, and reports how many it examined. - Callers: AfterDephantomize and its batch, manifest.Checkin (its files and manifest) and tag.AddTag. linkArtifact still judges a pending row it reaches, caching which tables exist per transaction. - The sweep no longer judges. A seed-shaped artifact received by pull or clone stays ordinary, per the 0.7.1 B.2. Docs and the HZ-5.2-4 scope note cite F.1. Tests: - A seed package test: one fill examines its own row, not 200 planted elsewhere. Run as a rescan mutant, it fails. - A tag whose control artifact is a seed's basis refuses the seed. - The link step refuses a seed made readable by a raw store. - A re-push refused by hash, and an igot of the hash not gimme'd. - A delta on a refused seed withheld. - A purged pending rid reused by an innocent blob. - Disabling any judge call (cascade, check-in, tag, link) or any Sweep call (request, sync, check-in, tag) fails a test. check-in: ed5e3ce5b5 user: [email protected] tags: main, trunk | |
| 03:22 |
Merge remote-tracking branch 'origin/main' into feat/471-seed-refusal
# Conflicts: # CHANGELOG.md check-in: 351bb4f35b user: [email protected] tags: main, trunk | |
| 03:06 |
Merge pull request #475 from danmestas/fix/clone-dangling-delta
fix(sync): clone an origin that holds a dangling delta check-in: 77c6eb043e user: [email protected] tags: main, trunk | |
| 02:58 |
fix(sync): clone an origin that holds a dangling delta
An origin that held a delta whose source it never received could not be cloned at all. walkCloneChain found the source unforwardable (a phantom) and asked buildCloneArtifact to send the delta expanded; expanding it failed, the handler errored, and every client stopped at clone sequence 1. The origin now withholds a chain that ends at a phantom, as it withholds a row outside the clone's cone. Sending the stored delta naming its phantom source, as fossil's send_compressed_file does, is not enough: a fossil client then rejects the whole clone for its unresolved deltas. A later pull fetches the chain once the source arrives. check-in: 67bc700498 user: [email protected] tags: main, trunk | |
| 02:27 |
fix(sync): withhold pending pushed artifacts; judge them when filled (#471)
Third review of the seedcheck design: - Withhold everything recorded, not just refusals. Every serving path (gimme/file, inventory and private igots, both clone paths, the client's file cards) now withholds any rid with a seedcheck row, pending or refused. A seed whose base a local check-in filled was served unjudged before. - Judge promptly. seedcheck.JudgeReadable judges, in one transaction, every pending row that has become readable, of any type: a seed is refused, anything else (files and non-artifacts too) loses its row, so withheld rows do not grow without bound. It runs in the transaction of each path that fills a basis: the dephantomize cascade, the sweep and rebuild (prepareCrosslinkTables), manifest.Checkin and tag.AddTag. Rows are walked in rid order through one cache, so a pending chain is expanded once. A row whose bytes fail their hash stays pending and withheld. - Forget on purge. shun.Purge drops seedcheck rows whose blob is gone (seedcheck.ForgetDeleted), since rids are reused. It is the only place blob rows are deleted. - One existence lookup per transaction. linkArtifact caches whether the table exists in the batch schedule, which is reset on commit and rollback. - Inventories read the withheld set fresh. The per-request cache serves gimme and clone only. Rows a request adds are its own unreadable deltas, which nothing can send, so the store-time and pre-inventory cache resets were unobservable and are gone. - The unsent delete on refusal is removed: only local writes fill unsent, so a pushed artifact is never in it. Tests: - The check-in-fills-base case no longer sweeps explicitly. It shows the seed refused in the check-in's transaction and served to neither a clone nor a pulling peer. - A pending seed is not answered on gimme or announced. - A pending plain file is served once its base arrives. - A purged seed's rid, reused by an innocent blob, is not withheld. - A private inventory withholds a privately pushed seed. - A clone does not send a refused seed as a delta source. - Disabling any serving filter fails a test. A clone of any store holding a dangling delta stalls at clone sequence 1. That is pre-existing and unrelated to seeds, so the pending-seed test checks pull and gimme instead. check-in: c72c1245b8 user: [email protected] tags: main, trunk | |
| 01:44 | Merge branch 'main' into feat/horizon-names-seed-refusal check-in: eb2fc856e5 user: [email protected] tags: main, trunk | |
| 01:44 |
fix(sync): judge pending pushed seeds at the crosslink step (#471)
Re-review found two faults in the shun-based design: - Only the push path judged a dangling seed. A base filled by a pull, a local check-in or a later sweep still crosslinked the seed. - Shunning the seed was wrong. The shun table syncs through the shun config group. A horizon clone that pushed its own current seed back as a dangling delta could get the origin to shun that hash, and every clone pulling the group would then drop its seed. New design, in internal/seedcheck: - A store-local seedcheck(rid, refused) table, created on first use and carried by no configuration group. - The push handler records a delta it cannot expand as pending. Only pushed artifacts are recorded: a seed-shaped artifact that arrives by any other route is ordinary (B.2). - One choke point: manifest.linkArtifact. AfterDephantomize, the sweep, rebuild, the receive linker and required-content linking all reach it. There a pending control artifact is judged: a seed becomes refused, is not linked, and leaves unclustered and unsent. Anything else loses its row and links as usual. A pending non-control row is left, inert. - A seed judged within the request that carried it still refuses that request atomically (ownSeedReadable). It expands through the pass's basis cache, so a dangling chain is expanded once along its length. - Serving honours refused: gimme/file, inventory and private igots, both clone paths, and the client's file cards when it pushes on. judgeGrounded, the shun use and the shun filters added to the sweep, rebuild and cascade are removed. Tests: - The pull-fills-base probe and a check-in-fills-base case. - A refused seed not cloned, and not pushed on even when a peer asks for it by name. - The seedcheck table is in no config group. - B.4.4 also asserts the real minted seed is refused while the shun table stays empty. - Disabling the choke point, or any of the serving filters, fails a test. Clone receive is not covered: a fresh clone has no pending rows, so that path cannot meet one. check-in: 776eae66e9 user: [email protected] tags: main, trunk | |
| 01:34 |
fix: send a clone's horizon by name even as a full hash
A horizon given through CloneOpts.Horizon or --horizon now always goes out as pragma horizon-name, a full hash at depth 0 included. The origin tries a full hash as a check-in and then as a symbolic name (§6.2.7, C.3), so a branch or tag spelled as 40 hex digits is found; a peer that resolves no name is covered by the local fallback. A horizon held on the store cone still goes out as pragma horizon on every pull, sync and deepen. nameAuthority now says why crediting the login card to the whole request is harmless, and TestResolveNameBounded fails within 2s with a message naming the unbounded walk. check-in: 017c7f9393 user: [email protected] tags: main, trunk | |
| 01:18 |
fix(sync): judge a seed when it first becomes readable; shun late seeds (#471)
Review found that a seed pushed as a delta against a missing base, then completed by a later request carrying the base, was crosslinked with no check. Per the amended erratum (spec F), the test now runs on the artifact when it first becomes readable, whatever request carried it: - Full text and a delta on a readable basis are judged as they are stored. A delta left dangling is judged when a phantom grounding it is filled: storeFile runs storeReceived, then judgeGrounded over the filled rid's dependent deltas, before the after-dephantomize cascade. - A seed the current request carried refuses that request atomically with the single error card, as before. - A seed an earlier request left dangling is recorded in the shun table. The request that supplied its base is innocent and is stored and crosslinked normally, with no error. Refusing it would let anyone block a future push by planting a dangling seed delta against a hash the origin will later receive. - A dependent whose bytes do not hash to its name is skipped. Shunning the name would let a forged delta bar the real artifact. Why the shun table: every serving path already withholds shunned names (gimme/file, igot inventories, clone), receipt drops them, and shun.Purge can remove the content. What it does not cover was crosslinking, so candidateQuerySQL (sweep and rebuild) and the dephantomize cascade's delta-child walk now skip shunned blobs. That matches fossil, where shunned content never reaches crosslink. Two consequences: the entry propagates to peers that pull the shun config group, which only shuns an attack artifact, and an admin can lift it with shun.Remove. Also from the review: - handleFile's post-store bookkeeping moves into recordStored. - isSeed and the new helpers assert their inputs. - The end-of-request pushedSeed pass is gone; each delta is judged once when it becomes readable. Tests: the two-request case is now B.4.4, a unit test and conformance case horizon.5.2.3.seed-base-later-request. The later push succeeds and the seed is shunned, never crosslinked (rebuild included), sent or announced. A new unit test judges every dependent, ordinary before seed, and checks a forged dependent is not shunned. Mutations in the judging loop were checked and are killed. check-in: 3b76d8e5f5 user: [email protected] tags: main, trunk | |
| 01:04 |
Merge pull request #474 from danmestas/fix/deterministic-gimme
fix(sync): ask for phantoms in hash order check-in: d40046f706 user: [email protected] tags: main, trunk | |
| 00:57 |
fix(sync): ask for phantoms in hash order
buildGimmeCards ranged over the phantoms map, so each round's gimme cards came out in Go's randomised map order. When a round has more phantoms than mxPhantomReq allows, that also picked a random subset to ask for. A seeded simulation then could not replay the same wire, and the docs generator's engine.json (which records a pull's cards) changed between runs, failing the generated-docs check about one run in three. Sort the phantom names before building the cards. Phantom sets are bounded by what the peer announced, so the sort is cheap next to the round trip. check-in: 82dae665dd user: [email protected] tags: main, trunk | |
| 00:37 |
docs: seeds are never pushed; regenerate conformance data (#471)
Covers what the horizons concept page and the CHANGELOG say about refusing pushed seeds, including two consequences: another implementation's label alone is ordinary, and a user whose login is go-libfossil cannot push control artifacts. check-in: e59ec7547c user: [email protected] tags: main, trunk | |
| 00:37 |
feat(sync): origin refuses a pushed seed by label or shape (#471)
Horizon §5.2.3 as the zit 0.7.1 erratum words it. A pushed control artifact is a seed when its U card is the origin's own label (go-libfossil) or it carries a horizon-seed T card under any label. Full text is judged as it arrives. A delta is judged on its expanded content at the end of the request, so a delta whose base arrives later in the same request is caught. A delta never made readable is not identified and stays a dangling delta. The refusal is atomic: the request rolls back, and the reply is the single error card "seed refused: a seed is private to the clone that holds it; it is never pushed". The horizon-seed name match is kept as an extra check, and now refuses loudly the same way instead of silently dropping the file. Conformance cases for HZ-5.2-4 cover full text, a delta against a held base, a late delta, the shape under a foreign label (full and late), and a foreign label with no binding card, which is stored. An HZ-5.7-2 case covers the stale-seed check on a late delta. An interop case has zit 0.7.0 push such an artifact to a go-libfossil origin and get refused. check-in: 1afe809b01 user: [email protected] tags: main, trunk | |
| 00:37 |
fix(sync): resolve horizon names only for authorized requesters
An origin resolved a horizon-name before the requester's login was read, and served a requester that may neither read nor clone at the horizon the name resolved to, so whether a name exists showed in its reply. The login card is now checked first, and only a requester that may read or clone has the name resolved; anyone else is answered as if the request named no horizon, the same reply for any name. A pull naming an unknown horizon is refused with the one error card, not served densely. The client fails the clone on a second horizon-resolved in a reply, and on one answering a request that sent no horizon-name. A private origin's challenge does not settle the name: the next request resends it with the login, still the clone's first request under §6.1. API: CloneOpts.Horizon replaces CloneOpts.HorizonName and the stand-in check-in a named horizon's StoreCone had to carry. It reuses the Horizon type, whose Checkin may there be a name, beside the existing HorizonDepth, and keeps the rule and the structural level where they were, so no public concept is added. StoreCone gives the path members alone: a horizon on both, or a depth without a Horizon, is an error. A full hash at depth 0 goes on the store cone as itself. Internally a NamedHorizon carries the name, depth, rule and level, not a cone holding a placeholder hash, and only the resolved hash is ever stored. The CLI passes --horizon through the same field. check-in: 54d75fbc78 user: [email protected] tags: main, trunk | |
| 00:37 |
fix(horizon): walk back in one bounded query
ResolveName stepped back one query per check-in, up to 2^32-1 times. A recursive query now walks the primary parents at most depth steps and never more than the check-ins the repository holds, so even a corrupt cycle of primary parents ends. check-in: 6f12912a01 user: [email protected] tags: main, trunk | |