1// Package sources is the seam every capture source is read through.2//3// A source knows one publisher's shape: how its captures are laid out, what its4// numbers mean, what it refuses to translate. Everything it knows stays inside5// its own directory under this one. What comes out is an Atlas interchange6// document, and from there nothing -- not composition, not enrichment, not the7// application -- can tell which source it read.8//9// The interface is small on purpose. A source is handed an opened archive and10// one volume of it, and hands back a document. It does not choose where the11// archive is, does not decide whether the result is written, and never reaches12// the network: capture is a separate, hand-run, network-touching step, and a13// translator is a pure function of bytes already on disk. That is what lets an14// editorial change replay over years of captures instead of re-crawling them.15//16// The interface is spelled in plain arguments rather than a request struct so17// that a source package can satisfy it without importing this one, which is18// what keeps registration a one-way edge: this package knows the sources, and19// no source knows this package.20//21// Registration is in registry.go, the one file outside a source's own directory22// permitted to name a source. docs/generate.md carries the source-authoring23// guide.24package sources2526import (27 "errors"28 "fmt"29 "log/slog"3031 "github.com/FelineStateMachine/atlas/internal/generate/archive"32 "github.com/FelineStateMachine/atlas/internal/generate/doc"33)3435// A Source translates one publisher's archived captures into the interchange36// document.37type Source interface {38 // Describe is the source's own account of itself: the slug a ledger names39 // it by, the label a person reads, what the data is licensed under, and40 // which id space its documents' numbers live in. It is copied verbatim into41 // every document the source emits.42 Describe() doc.Provenance4344 // Translate reads one volume's captures and emits its document. It is45 // deterministic: the same archived bytes give the same document, on any46 // machine, however many times it is run. The logger is never nil.47 //48 // A volume the source cannot read yet -- a half-finished crawl, a world49 // with no capture -- is reported by wrapping ErrNotReady, which a caller50 // skips. Everything else is a failure: a source that cannot honour its own51 // gates says so rather than translating something it does not understand.52 Translate(a *archive.Archive, v archive.VolumeRef, log *slog.Logger) (doc.Document, error)53}5455// ErrNotReady marks a volume the archive holds but cannot yet answer for. It56// wraps the archive's own signal so a caller has one thing to test.57var ErrNotReady = archive.ErrNotReady5859// ErrUnknownSource is reported for a volume whose archive entry names a source60// nothing is registered for.61var ErrUnknownSource = errors.New("no source is registered for this volume")6263// For finds the source registered under a name, or under the name the legacy64// archive files its captures as. Both are asked because the archive is older65// than Atlas's own naming and is read as it stands: a volume filed under a66// publisher's abbreviation still answers to the source Atlas calls by its own67// name.68func For(name string) (Source, error) {69 for _, source := range All() {70 about := source.Describe()71 if about.Name == name || about.ArchivedAs == name {72 return source, nil73 }74 }75 return nil, fmt.Errorf("%w: %q", ErrUnknownSource, name)76}7778// Names lists the registered sources, in registration order.79func Names() []string {80 sources := All()81 out := make([]string, 0, len(sources))82 for _, source := range sources {83 out = append(out, source.Describe().Name)84 }85 return out86}