Artifact e86f07c3539e764458e857f8c49ddd7f4d93335b5d6112d203438a0ac6495c5c:
- File
internal/horizon/seed/seed.go
— part of check-in
[45fb2c7ed4]
at
2026-10-22 14:33:04
on branch feat/horizon-cones
— feat(horizon): mint, hold and apply the structural-horizon seed
A structural-horizon clone lacks C's ancestry, so tag propagation has nowhere to start: every check-in in K would derive no branch, and closed markers and names set below C would vanish. The seed of draft-fossil-horizon-00 Section 5.2 restates the dense tag state at each entry of K so the unmodified derivation comes out exact.
internal/horizon/seed mints it at an origin (T cards per row in force at C and each check-in of K whose primary parent is outside K; D the latest summarized event rounded up to a whole second; U the origin's login), and holds it at a clone in a table of its own, outside the artifact store, so nothing that enumerates or serves artifacts can send it. It is applied when held, by Rebuild, and by seed.Apply, which the sync layer calls after each round. Replacing or discarding a seed re-derives the store, since only a re-derivation separates the old seed's rows from those real artifacts wrote. A Verifier hook carries the Section 5.2.4 provenance policy; no signing is implemented.
The crosslink-table clearing moves from verify into manifest.Rederive so Rebuild and the seed share one owner of re-derivation. The cards match zit's seed byte for byte in T and U, and each implementation derives from the other's seed what it derives from its own. (user: [email protected], size: 13866) [annotate] [blame] [check-ins using]
// Package seed mints, holds and applies the seed of draft-fossil-horizon-00 // §5.2: the control artifact an origin mints so that a structural-horizon // clone, which lacks C's ancestry, derives the branch, tag and closed state // of every check-in in the held set K exactly as a dense repository does. // // An origin mints with Mint. A clone holds the seed with Hold, outside the // artifact store (§5.2.3's second holding): it is in no blob row, so nothing // that enumerates or serves artifacts -- unsent, unclustered, private, a // gimme, a rebuild's re-marking -- can reach it. The derived state is // persisted, so where zit applies its seed at every open this package applies // it whenever the derivation runs: when the seed is held, by Rebuild, and by // Apply, which the sync layer calls after every round that links artifacts so // an entry arriving after its seed is seeded too. package seed import ( "bytes" "database/sql" "encoding/json" "errors" "fmt" "slices" "time" "github.com/danmestas/go-libfossil/db" "github.com/danmestas/go-libfossil/internal/deck" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/hash" "github.com/danmestas/go-libfossil/internal/leaf" "github.com/danmestas/go-libfossil/internal/manifest" "github.com/danmestas/go-libfossil/internal/tag" ) var ( // ErrNotSeed is returned by Hold for bytes that do not hash to the name // they were announced under or do not parse as a control artifact with a // D card and T cards (§6.2.3), and by Apply for a held seed that no // longer parses. ErrNotSeed = errors.New("horizon seed: not a seed") // ErrProvenance is returned by Hold when the origin's key does not verify // the seed (§5.2.4); the clone falls back to the content horizon. ErrProvenance = errors.New("horizon seed: provenance does not verify") // ErrNothingToSeed is returned by Mint when no tag is in force at any // entry, so there is no control artifact to mint; the origin then serves // the request as content (§6.2.5). ErrNothingToSeed = errors.New("horizon seed: no tag in force at any entry") ) // Verifier checks a seed against its origin's public key (§5.2.4): nil when // the seed carries a valid clear-signature by that key. A clone that knows // no key for its origin passes a nil Verifier and accepts an unsigned seed. // This package verifies no signature itself: go-libfossil strips a clear- // signing envelope when it parses (deck.Parse) but implements no signing. type Verifier func(seed []byte) error // heldSeedSchema holds the seed in a table of its own, created when a seed is // held and dropped when it is discarded, like the store cone. It is no // derived table, so a rebuild keeps it and re-applies it. const heldSeedSchema = `CREATE TABLE IF NOT EXISTS horizonseed( id INTEGER PRIMARY KEY CHECK(id=1), seed BLOB NOT NULL )` // Entries returns the entries of K (§5.2.1), ascending by rid: C, and every // check-in of K that names a primary parent outside K. Tag state reaches a // check-in along its primary parent, so these are where K's state enters // from below the horizon. A check-in of K other than C that names no parent // inherits nothing and is no entry, as in zit (src/sparse.zig). func Entries(q db.Querier, c libfossil.FslID, k []libfossil.FslID) ([]libfossil.FslID, error) { if q == nil { panic("seed.Entries: q must not be nil") } if c <= 0 { panic("seed.Entries: c must be positive") } inK := make(map[libfossil.FslID]bool, len(k)) for _, rid := range k { inK[rid] = true } if !inK[c] { return nil, fmt.Errorf("seed.Entries: C rid=%d is not in K", c) } ids, err := json.Marshal(k) if err != nil { return nil, fmt.Errorf("seed.Entries: %w", err) } rows, err := q.Query(`SELECT cid, pid FROM plink WHERE isprim=1 AND cid IN (SELECT value FROM json_each(?))`, string(ids)) if err != nil { return nil, fmt.Errorf("seed.Entries: reading plink: %w", err) } defer rows.Close() entries := []libfossil.FslID{c} for rows.Next() { var cid, pid libfossil.FslID if err := rows.Scan(&cid, &pid); err != nil { return nil, fmt.Errorf("seed.Entries: reading plink: %w", err) } if cid != c && !inK[pid] { entries = append(entries, cid) } } if err := rows.Err(); err != nil { return nil, fmt.Errorf("seed.Entries: reading plink: %w", err) } slices.Sort(entries) return slices.Compact(entries), nil } // Mint returns the seed for C and its held set k (horizon.HeldSet), with the // SHA3-256 name it is announced under (§6.2.3). It carries one T card per // tag in force at each entry in this repository's derivation, applied to the // entry directly: a propagating row as a `*` card, a singleton as `+`, each // with the value tagxref holds. Cancel rows are not in force and are left // out, as zit leaves them out (src/sync.zig composeSeed). // // The D card is the latest mtime among the rows the seed summarizes, not the // time of minting (§6.2.3's SHOULD, which zit follows): two mints over // unchanged state then yield one name. A fractional latest mtime is rounded // up to the next whole second, so D is never earlier than the latest event // (§5.2.2); zit truncates instead. The U card is login, the origin's. func Mint(q db.Querier, c libfossil.FslID, k []libfossil.FslID, login string) (string, []byte, error) { if q == nil { panic("seed.Mint: q must not be nil") } if login == "" { panic("seed.Mint: login must not be empty") } entries, err := Entries(q, c, k) if err != nil { return "", nil, err } var cards []deck.TagCard var latest int64 for _, rid := range entries { rowCards, rowLatest, err := cardsAt(q, rid) if err != nil { return "", nil, err } cards = append(cards, rowCards...) latest = max(latest, rowLatest) } if len(cards) == 0 { return "", nil, ErrNothingToSeed } const second = 1000 d := &deck.Deck{ Type: deck.Control, D: time.UnixMilli((latest + second - 1) / second * second).UTC(), T: cards, U: deck.User(login), } data, err := d.Marshal() if err != nil { return "", nil, fmt.Errorf("seed.Mint: %w", err) } return hash.SHA3(data), data, nil } // cardsAt returns a T card for every tag in force at rid, applied to rid, // and the latest of their mtimes in milliseconds since the Unix epoch. func cardsAt(q db.Querier, rid libfossil.FslID) ([]deck.TagCard, int64, error) { var uuid string if err := q.QueryRow("SELECT uuid FROM blob WHERE rid=?", rid).Scan(&uuid); err != nil { return nil, 0, fmt.Errorf("seed.Mint: entry rid=%d: %w", rid, err) } rows, err := q.Query(`SELECT tag.tagname, tagxref.tagtype, tagxref.value, tagxref.mtime FROM tagxref JOIN tag USING(tagid) WHERE tagxref.rid=? AND tagxref.tagtype>0`, rid) if err != nil { return nil, 0, fmt.Errorf("seed.Mint: tags of rid=%d: %w", rid, err) } defer rows.Close() var cards []deck.TagCard var latest int64 for rows.Next() { var name string var typ int var value sql.NullString var mtimeRaw any if err := rows.Scan(&name, &typ, &value, &mtimeRaw); err != nil { return nil, 0, fmt.Errorf("seed.Mint: tags of rid=%d: %w", rid, err) } mtime, ok := db.ScanJulianDay(mtimeRaw) if !ok { return nil, 0, fmt.Errorf("seed.Mint: tag %q of rid=%d: unreadable mtime %T", name, rid, mtimeRaw) } cardType := deck.TagSingleton if typ == tag.TagPropagating { cardType = deck.TagPropagating } cards = append(cards, deck.TagCard{Type: cardType, Name: name, UUID: uuid, Value: value.String}) latest = max(latest, libfossil.MillisFromJulianDay(mtime)) } if err := rows.Err(); err != nil { return nil, 0, fmt.Errorf("seed.Mint: tags of rid=%d: %w", rid, err) } return cards, latest, nil } // Hold makes data, announced under name, the seed this clone holds, and // applies it. The bytes must hash to name and parse as a control artifact // (§6.2.3), or ErrNotSeed is returned and nothing changes; verify, when the // clone knows its origin's key, must accept them (§5.2.4), or ErrProvenance. // // A seed replacing a different one (a re-seed, §5.6) re-derives the whole // store before applying the new seed: the old seed's rows sit in tagxref // beside rows real artifacts wrote, and only a re-derivation tells them // apart, which is what zit gets by deriving afresh at every open. Holding the // seed already held only applies it again. // // Hold is for the seed of the clone's own structural request. The sync layer // routes a file card to it only when a horizon-seed pragma in the same reply // named it; an origin never calls it (§5.2.3: it refuses a seed offered to // it), and an artifact of the seed's shape arriving any other way is stored // and crosslinked as the ordinary control artifact it is. func Hold(tx *db.Tx, name string, data []byte, verify Verifier) error { if tx == nil { panic("seed.Hold: tx must not be nil") } if len(data) == 0 || name == "" || !hash.IsValidHash(name) || hash.ContentHash(data, name) != name { return fmt.Errorf("%w: the bytes do not hash to %q", ErrNotSeed, name) } if _, err := parse(data); err != nil { return err } if verify != nil { if err := verify(data); err != nil { return fmt.Errorf("%w: %w", ErrProvenance, err) } } old, err := Held(tx) if err != nil { return err } if _, err := tx.Exec(heldSeedSchema); err != nil { return fmt.Errorf("seed.Hold: %w", err) } if _, err := tx.Exec("REPLACE INTO horizonseed(id, seed) VALUES(1, ?)", data); err != nil { return fmt.Errorf("seed.Hold: %w", err) } if old != nil && !bytes.Equal(old, data) { if _, err := manifest.Rederive(tx); err != nil { return fmt.Errorf("seed.Hold: %w", err) } } applied, err := Apply(tx, nil) if err != nil { return err } if !applied { panic("seed.Hold: postcondition: the seed just held was not applied") } return nil } // Held returns the seed this clone holds, or nil when it holds none. func Held(q db.Querier) ([]byte, error) { if q == nil { panic("seed.Held: q must not be nil") } var n int if err := q.QueryRow( "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='horizonseed'", ).Scan(&n); err != nil { return nil, fmt.Errorf("seed.Held: %w", err) } if n == 0 { return nil, nil } var data []byte err := q.QueryRow("SELECT seed FROM horizonseed WHERE id=1").Scan(&data) if errors.Is(err, sql.ErrNoRows) { return nil, nil } if err != nil { return nil, fmt.Errorf("seed.Held: %w", err) } return data, nil } // Apply applies the held seed to the derived state as crosslinking a held // control artifact would (§5.2.3): each T card at the D card's time, on the // check-in it names, propagating from there, with the leaf rechecks that // schedules. It reports whether a seed is held and applied. // // Each row it writes names its target as its source, as a check-in's own // inline card does: the seed is no blob, and a positive source keeps a date // tag in force. A card whose target this store does not hold yet is skipped; // the sync layer calls Apply again after each round that links artifacts, // and a card already applied is superseded by its own row, so a repeat costs // a lookup per card. // // verify, when the clone knows its origin's key, re-checks the held seed // (§5.2.4: every seed it holds); a seed that fails is discarded, the store // re-derived without it, and false returned, so the caller falls back to the // content horizon. func Apply(tx *db.Tx, verify Verifier) (bool, error) { if tx == nil { panic("seed.Apply: tx must not be nil") } data, err := Held(tx) if err != nil || data == nil { return false, err } if verify != nil && verify(data) != nil { return false, Discard(tx) } d, err := parse(data) if err != nil { return false, err } mtime := libfossil.TimeToJulian(d.D) leaves := leaf.NewPending() for _, tc := range d.T { var target libfossil.FslID err := tx.QueryRow("SELECT rid FROM blob WHERE uuid=? AND size>=0", tc.UUID).Scan(&target) if errors.Is(err, sql.ErrNoRows) { continue } if err != nil { return false, fmt.Errorf("seed.Apply: target %s: %w", tc.UUID, err) } if err := tag.ApplyTagWithTx(tx, tag.ApplyOpts{ TargetRID: target, SrcRID: target, TagName: tc.Name, TagType: tagType(tc.Type), Value: tc.Value, MTime: mtime, Leaves: leaves, }); err != nil { return false, fmt.Errorf("seed.Apply: tag %q on %s: %w", tc.Name, tc.UUID, err) } } if err := leaves.Apply(tx); err != nil { return false, fmt.Errorf("seed.Apply: %w", err) } return true, nil } // Discard drops the held seed and re-derives the store without it. A // deepening clone discards the seed for C before it applies the seed for C' // (§5.8); the seed is local, so this has no synchronization consequence. func Discard(tx *db.Tx) error { if tx == nil { panic("seed.Discard: tx must not be nil") } old, err := Held(tx) if err != nil || old == nil { return err } if _, err := tx.Exec("DROP TABLE horizonseed"); err != nil { return fmt.Errorf("seed.Discard: %w", err) } if _, err := manifest.Rederive(tx); err != nil { return fmt.Errorf("seed.Discard: %w", err) } return nil } // parse reads data as a seed: a control artifact with a D card and at least // one T card. deck.Parse strips a clear-signing envelope first. func parse(data []byte) (*deck.Deck, error) { d, err := deck.Parse(data) if err != nil { return nil, fmt.Errorf("%w: %w", ErrNotSeed, err) } if d.Type != deck.Control || d.D.IsZero() || len(d.T) == 0 { return nil, fmt.Errorf("%w: not a control artifact with D and T cards", ErrNotSeed) } return d, nil } func tagType(t deck.TagType) int { switch t { case deck.TagPropagating: return tag.TagPropagating case deck.TagSingleton: return tag.TagSingleton case deck.TagCancel: return tag.TagCancel } panic(fmt.Sprintf("seed.tagType: unknown tag type %q", t)) }