Artifact 8c9dcc47105b0e04c18c9f22d75e4e1a35aea7f6840498ed05c2aa014ccd78b0:
- File
internal/content/sparse_horizon.go
— part of check-in
[2ea6479ba9]
at
2026-10-22 00:04:54
on branch feat/horizon-cones
— feat(content): classify blobs below a content horizon as elided
A store cone may now carry a content horizon (draft-fossil-horizon-00 §3.2, §4.1-§4.3). Classification computes the held set K from the store's own check-in graph and walks K's full trees -- F cards directly, through B baselines and through S sub-manifest nodes, with one visited set over structural names -- because mlink records a file version only where it changed, and a blob introduced below C and unchanged through C is named by C's tree alone. Nothing about K is stored: every classification recomputes it, so moving C or losing the cone is a reload, never a migration.
Blobs no K check-in names are elided; with path members as well a blob is kept only when it is in both keep sets. Under a horizon nothing structural is elided, by the horizon or by path members, and delta bases of held artifacts and shunned names keep their classes. A C the store does not hold elides nothing, as zit's classification does: eliding everything would leave the pull that brings a deepened C nothing to ask for. A structural horizon is refused by SetStoreCone until it is implemented.
internal/content now imports internal/horizon, so the held-set tests, which commit through internal/manifest, move to an external test package to avoid an import cycle. (user: [email protected], size: 3558) [annotate] [blame] [check-ins using] [more...]
package content import ( "context" "database/sql" "errors" "fmt" "github.com/danmestas/go-libfossil/db" "github.com/danmestas/go-libfossil/internal/cone" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/horizon" ) // horizonAbove returns the blobs above the content horizon h // (draft-fossil-horizon-00 §4.2): every F-card target of a check-in in the // held set K, read from the store's own manifests. Nothing is stored: K and // this set are recomputed on every classification (§3.1, §3.2). // // placed is false when C is not a check-in the store holds. Such a horizon // elides nothing, as zit's classification does (src/sparse.zig, // ClassifyState.extend, at horizon-rfc e6ee276c89): reading K as empty // would elide every blob, and after a deepen to a C' not yet pulled the // pull that brings C' would then ask for none of the blobs it must keep. // No blob can be proven below a horizon the store cannot place; the cost // of keeping them wanted is bandwidth, never data. func horizonAbove( ctx context.Context, q db.Querier, facts *ConeReferenceFacts, h cone.Horizon, ) (above map[string]bool, placed bool, err error) { if ctx == nil || q == nil || facts == nil { panic("content.horizonAbove: nil context, querier or facts") } var c libfossil.FslID err = q.QueryRow("SELECT rid FROM blob WHERE uuid=?", h.Hash()).Scan(&c) if errors.Is(err, sql.ErrNoRows) { return nil, false, nil } if err != nil { return nil, false, fmt.Errorf("content.horizonAbove: C: %w", err) } k, err := horizon.HeldSet(ctx, q, c, h.Rule()) if errors.Is(err, horizon.ErrNotHeld) { return nil, false, nil } if err != nil { return nil, false, err } above, err = facts.treeFiles(ctx, k) return above, err == nil, err } // treeFiles returns every F-card target that the check-ins roots name, // directly, through their B baselines, or through S sub-manifest nodes. // mlink cannot answer this: it records a file version only at the check-in // that changed it, while a check-in's tree names every file it carries. // // One visited set over structural names reads a baseline or node shared by // many check-ins once, so the walk is linear in K's distinct structure, and // bounded like the repository's other tree walks. A delta manifest's // deletions and overrides are not applied: a blob its baseline names counts // as named, as in zit. Keeping more than the exact tree is sound, and it // keeps this a plain reachability walk. A tree piece the store does not // hold is skipped; it is itself a transit phantom, and the classification // after it arrives counts the blobs it names. func (facts *ConeReferenceFacts) treeFiles( ctx context.Context, roots []libfossil.FslID, ) (map[string]bool, error) { files := map[string]bool{} visited := map[string]bool{} var work []string push := func(uuid string) { if uuid != "" && !visited[uuid] { visited[uuid] = true work = append(work, uuid) } } for _, rid := range roots { push(facts.names[rid]) } for len(work) > 0 { if err := ctx.Err(); err != nil { return nil, err } if len(visited) > coneReferenceNodes { return nil, fmt.Errorf("horizon: tree walk bound exceeded") } uuid := work[len(work)-1] work = work[:len(work)-1] d := facts.manifests[uuid] if d == nil { d = facts.byUUID[uuid] } if d == nil { continue } for _, f := range d.F { if f.UUID != "" { files[f.UUID] = true } } push(d.B) for _, s := range d.S { push(s.UUID) } } return files, nil }