project

package
v0.103.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 10, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package project turns the canonical BIBFRAME graph into the catalog's derived data: the JSON a static site (the Hugo module's content adapter) and the search index consume (ARCHITECTURE §7). It is a read-only projection -- the graph stays the source of truth -- and it flattens each clustered Work, with its Instances and the union of its feed and editorial statements, into one record.

Index

Constants

View Source
const SchemaVersion = 11

SchemaVersion is the catalog.json / facets.json / redirects.json schema version. The Hugo module and search-index builder read it to detect a projector/consumer mismatch. v2 added the per-Instance identifier scheme (ProviderID.Source) for the availability adapter (tasks/004/008). v3 split controlled subjects (authority URIs + resolved labels) from uncontrolled feed tags (tasks/012). v4 added per-Instance format (from the Instance's RDA media type) and the Work-level formats facet, so a clustered mixed-format Work exposes each format (tasks/011). v5 added subject skos:broader parents (Subject.Broader / SubjectFacet.Broader) so consumers render vocabulary hierarchy without re-reading the graph (tasks/015). v6 added the holdings signal (Instance.Held / Work.Held): physical items or a live-availability identifier whose feed still lists the Work (tasks/078). v7 added Work.Summary from bf:summary -- the description/abstract as first-class bibliographic data rather than an adopter extra (tasks/124). v8 added Subject.Scheme / SubjectFacet.Scheme -- the short vocabulary code derived from the authority-URI namespace -- so a multi-vocabulary corpus can facet and mint term pages per scheme instead of colliding same-label terms from different vocabularies (tasks/141). v9 made classifications {value, label} objects (Work.Classifications / Facets.Classifications): value stays the scheme code (what MARC 084 $a carries), label is the human text riding the classification node's rdfs:label -- the display-only channel -- so a facet can show "Fiction / Romance / Contemporary" while exports keep FIC027000 (tasks/142). v10 added Catalog.Terms -- the vocabulary sideband: every referenced subject term plus its transitive skos:broader ancestors, with whatever labels and broader edges the graph carries -- so a consumer can label hierarchy nodes no Work carries directly instead of minting them label-less (tasks/178). v11 added Work.Relations -- the editorial whole/part links (hasPart/partOf as {id, title}, restricted to works present in this projection) -- and the Instance series statement/enumeration (bf:seriesStatement 490$a, bf:seriesEnumeration 490$v), so the site cross-links parts and shows series lines (tasks/221/222).

Variables

View Source
var SubjectSchemePrefixes = []SchemePrefix{
	{"https://homosaurus.org/", "homosaurus"},
	{"http://homosaurus.org/", "homosaurus"},
	{"http://id.worldcat.org/fast/", "fast"},
	{"https://id.worldcat.org/fast/", "fast"},
	{"http://id.loc.gov/authorities/subjects/", "lcsh"},
	{"https://id.loc.gov/authorities/subjects/", "lcsh"},
	{"http://id.loc.gov/authorities/childrensSubjects/", "lcshac"},
	{"https://id.loc.gov/authorities/childrensSubjects/", "lcshac"},
	{bibframe.LocalAuthorityNS, "local"},
}

SubjectSchemePrefixes is the namespace -> scheme table SchemeForURI consults, first match wins (tasks/141). `lcat project --subject-scheme` prepends deployment-specific entries, so a custom authority (or a different code for a known one) overrides these defaults.

Functions

func Feeds added in v0.99.0

func Feeds(catalogNQ []byte) ([]string, error)

Feeds lists the provider names that appear as feed graphs in a catalog.nq, sorted. It answers "what could this catalog be projected as?", which is the question a caller needs when a requested provider projects nothing: the difference between a catalog that is empty and a catalog whose feeds simply were not asked for (tasks/246).

func SanitizeSources added in v0.30.0

func SanitizeSources(cat *Catalog, public map[string]bool) int

SanitizeSources rewrites every Work's extra "sources" attribution list to the public allowlist, dropping the key when nothing public remains, and returns how many attributions were stripped (tasks/172). Provenance under lcat:extra/sources may name sources whose attribution must not leak on a public surface; the same allowlist governs the nq download (export package). Values are comma-joined by convention and compared trimmed; kept values are re-joined ", " and keep their order.

func SchemeForURI

func SchemeForURI(uri string) string

SchemeForURI returns the short vocabulary code for an authority URI, or "" when the namespace is not in SubjectSchemePrefixes.

func SourceSet added in v0.30.0

func SourceSet(csv string) map[string]bool

SourceSet parses a comma-separated source-name list into the allowlist set SanitizeSources and the export nq filter consume. Names are trimmed; empty entries are ignored, so "" yields an empty (strip-everything) set.

Types

type Catalog

type Catalog struct {
	Version int    `json:"version"`
	Works   []Work `json:"works"`
	// Terms is the vocabulary sideband (tasks/178): one entry per referenced
	// subject term or transitive skos:broader ancestor that the graph
	// describes (labels and/or broader edges; bare URIs with nothing to say
	// are skipped). Sorted by ID.
	Terms []Term `json:"terms,omitempty"`
}

Catalog is the projected corpus: one record per Work, sorted by id.

func Merge added in v0.30.0

func Merge(cats []*Catalog) *Catalog

Merge unions per-feed projections by work id (tasks/172). Project views one provenance graph at a time (feed:<provider> + editorial:), so a multi-feed corpus projects each feed separately and merges here: earlier catalogs win a shared id -- list the primary feed first, its records are richer than a sidecar's -- and the result is sorted by id like Project's output. The input catalog.nq must cover every feed's works; after a multi-feed ingest, `lcat serialize` regenerates it, since each ingest run rewrites catalog.nq with only its own run's works.

The vocabulary sideband (Catalog.Terms, tasks/178) merges by term id, field-wise: labels fill per language (earlier catalogs win a language, like works win an id), broader edges union, the first non-empty scheme sticks -- per-feed projections describe the same authority term from different coverage, so the union is at least as rich as any one input (tasks/180).

func Project

func Project(catalogNQ []byte, provider string) (*Catalog, error)

Project reads a catalog.nq dataset and projects each Work into a Catalog record. Display/facet fields are drawn from the union of the provider's feed graph and the editorial graph, so curated subjects appear alongside feed data. Editorial lcat:overrides markers shadow the feed first (tasks/042): a property a cataloger claimed shows only its editorial values, and deleting the marker resurfaces the feed untouched.

catalogNQ must not be modified while the returned Catalog is in use: the parse is zero-copy (ParseNQuadsShared, tasks/057) -- one input-sized allocation saved at corpus scale -- so projected strings alias the buffer.

func (*Catalog) Facets

func (c *Catalog) Facets() Facets

Facets aggregates the catalog into per-dimension value counts, each value counted once per Work. Values are ordered by descending count then value, so the output is deterministic.

type Classification

type Classification struct {
	Value string `json:"value"`
	Label string `json:"label,omitempty"`
}

Classification is one classification of a Work: the scheme code (bf: classificationPortion -- what MARC 084 $a carries) plus the optional human label riding the classification node's rdfs:label (tasks/142). Facets and term pages key on Value; display prefers Label and falls back to Value, so a corpus without labels renders exactly as before.

type ClassificationFacet

type ClassificationFacet struct {
	Value string `json:"value"`
	Label string `json:"label,omitempty"`
	Count int    `json:"count"`
}

ClassificationFacet is one classification facet value: the scheme code (the key), the optional human label (see Classification), and the number of Works carrying it (tasks/142).

type Contributor

type Contributor struct {
	Name string `json:"name"`
	Role string `json:"role,omitempty"`
}

Contributor is an agent's display name and role.

type FacetValue

type FacetValue struct {
	Value string `json:"value"`
	Count int    `json:"count"`
}

FacetValue is one facet value and the number of Works that carry it.

type Facets

type Facets struct {
	Version         int                   `json:"version"`
	Languages       []FacetValue          `json:"languages,omitempty"`
	Subjects        []SubjectFacet        `json:"subjects,omitempty"`
	Tags            []FacetValue          `json:"tags,omitempty"`
	Formats         []FacetValue          `json:"formats,omitempty"`
	Contributors    []FacetValue          `json:"contributors,omitempty"`
	Classifications []ClassificationFacet `json:"classifications,omitempty"`
}

Facets is the precomputed facet index: for each facetable dimension, the distinct values and how many Works carry each. Emitting it saves the static site from aggregating the whole corpus in templates at build time.

type Instance

type Instance struct {
	ID          string       `json:"id"`
	Format      string       `json:"format,omitempty"`
	ISBNs       []string     `json:"isbns,omitempty"`
	ProviderIDs []ProviderID `json:"providerIds,omitempty"`
	// Items are the Instance's physical holdings (tasks/051): call number,
	// shelving location, barcode, note -- never circulation state, which
	// stays live-only (ARCHITECTURE §5).
	Items []Item `json:"items,omitempty"`
	// Held is true when this Instance has >=1 item (physical) or a
	// live-availability identifier whose feed still lists the Work (digital,
	// unless the reconciliation flagged the Work withdrawn) -- tasks/078.
	Held bool `json:"held,omitempty"`
	// Series carries the Instance's transcribed series statements
	// (bf:seriesStatement, 490$a) and SeriesEnumeration its volume/part
	// within the series (bf:seriesEnumeration, 490$v) -- tasks/221/222.
	Series            []string `json:"series,omitempty"`
	SeriesEnumeration string   `json:"seriesEnumeration,omitempty"`
}

Instance is one edition/format: its id, format (from its RDA media type), ISBNs, and the scheme-tagged provider ids the runtime availability adapter keys on.

type Item

type Item struct {
	CallNumber string `json:"callNumber,omitempty"`
	Location   string `json:"location,omitempty"`
	Barcode    string `json:"barcode,omitempty"`
	Note       string `json:"note,omitempty"`
}

Item is one holding of an Instance (the minimal bf:Item model, tasks/051).

type ProviderID

type ProviderID struct {
	Source string `json:"source,omitempty"`
	Value  string `json:"value"`
}

ProviderID is one non-ISBN identifier with its bf:source scheme, so a client-side availability adapter selects its key by scheme (e.g. OverDrive's "overdrive-reserve" Reserve ID vs the "overdrive" title id) rather than guessing from a flat list (ARCHITECTURE §9, tasks/004). Source is empty for an untagged identifier.

type Redirect

type Redirect struct {
	From string `json:"from"`
	To   string `json:"to"`
}

Redirect is one retired-Work -> surviving-Work URL redirect (ARCHITECTURE §4): after an editorial merge the retired id must still resolve, so shared links and SEO survive.

type RedirectMap

type RedirectMap struct {
	Version   int        `json:"version"`
	Redirects []Redirect `json:"redirects"`
}

RedirectMap is the redirect artifact emitted alongside catalog.json: every retired Work id and the surviving id it now resolves to, chains collapsed to the final survivor and sorted by retired id. The static host turns each into a 301 (per the tasks/001 decision: the projector emits the map, the host serves it).

func Redirects

func Redirects(catalogNQ []byte) (RedirectMap, error)

Redirects builds the redirect map from a catalog.nq dataset by reading the editorial graph's lcat:mergedInto statements and collapsing merge chains (A->B->C yields A->C and B->C) to the final survivor. A merge cycle terminates at the last id reached rather than looping.

catalogNQ must not be modified while the returned map is in use: the parse is zero-copy (ParseNQuadsShared, tasks/057), so ids alias the buffer.

type RelatedWork added in v0.72.0

type RelatedWork struct {
	ID    string `json:"id"`
	Title string `json:"title,omitempty"`
}

RelatedWork is one linked work: its catalog id and display title.

type Relations added in v0.72.0

type Relations struct {
	HasPart []RelatedWork `json:"hasPart,omitempty"`
	PartOf  []RelatedWork `json:"partOf,omitempty"`
}

Relations are a Work's whole/part cross-links (schema v11).

type SchemePrefix

type SchemePrefix struct{ Prefix, Scheme string }

SchemePrefix maps an authority-URI namespace prefix to its short vocabulary code.

type Subject

type Subject struct {
	ID      string            `json:"id"`
	Labels  map[string]string `json:"labels,omitempty"`
	Broader []string          `json:"broader,omitempty"`
	// Scheme is the short vocabulary code derived from the URI's namespace
	// (SchemeForURI, tasks/141) -- "homosaurus", "fast", "lcsh", "local";
	// empty for an unrecognized authority.
	Scheme string `json:"scheme,omitempty"`
}

Subject is a controlled-vocabulary subject: a stable authority URI plus the human labels resolved from the authority's skos:prefLabel / rdfs:label statements in the graph, keyed by language tag (e.g. "en", "es"; "" for an untagged label). Links and facets key on ID; display uses Labels, falling back to ID when the authority provides none (tasks/012). Distinct from an uncontrolled feed Tag.

Broader holds the authority URIs of this term's skos:broader parents (sorted, deduped), so a consumer can render vocabulary hierarchy (breadcrumb trails, broader/narrower drill-down) without re-reading the graph (tasks/015). It is id-only: a parent's label resolves from the parent's own Subject/authority record.

type SubjectFacet

type SubjectFacet struct {
	ID      string            `json:"id"`
	Labels  map[string]string `json:"labels,omitempty"`
	Broader []string          `json:"broader,omitempty"`
	// Scheme is the vocabulary code (see Subject.Scheme) so a consumer
	// renders one facet group per vocabulary (tasks/141).
	Scheme string `json:"scheme,omitempty"`
	Count  int    `json:"count"`
}

SubjectFacet is one controlled-subject facet value: the authority URI (the key), its resolved labels, its skos:broader parents (for hierarchy-aware facet drill-down, tasks/015), and the number of Works carrying it. Facets key on ID so a relabel does not churn the facet; display uses Labels (tasks/012).

type Term added in v0.34.0

type Term struct {
	ID      string            `json:"id"`
	Labels  map[string]string `json:"labels,omitempty"`
	Broader []string          `json:"broader,omitempty"`
	Scheme  string            `json:"scheme,omitempty"`
}

Term is one controlled-vocabulary concept the catalog references -- a Work's subject or one of its skos:broader ancestors -- with the labels and broader edges resolved from the graph. The sideband exists for hierarchy nodes no Work carries directly: the browse-artifact builder unions subtree postings into ancestors (search.BuildBrowse), and without this it can only mint them label-less (tasks/178).

type Work

type Work struct {
	ID       string `json:"id"`
	Title    string `json:"title"`
	Subtitle string `json:"subtitle,omitempty"`
	// Summary is the Work's description/abstract (bf:summary), first label wins
	// when a record carries several (tasks/124).
	Summary         string           `json:"summary,omitempty"`
	Contributors    []Contributor    `json:"contributors,omitempty"`
	Subjects        []Subject        `json:"subjects,omitempty"`
	Tags            []string         `json:"tags,omitempty"`
	Languages       []string         `json:"languages,omitempty"`
	Classifications []Classification `json:"classifications,omitempty"`
	// Formats is the union of the Work's Instances' formats (e.g. ebook, audiobook),
	// so a clustered mixed-format Work is faceted under each format it offers.
	Formats   []string   `json:"formats,omitempty"`
	Instances []Instance `json:"instances,omitempty"`
	// Held is true when any Instance is held (tasks/078): physical items, or
	// a live-availability identifier whose feed still lists the Work. Whether
	// unheld Works are hidden or merely badged is the importing site's call.
	Held bool `json:"held,omitempty"`
	// Extra holds the Work's non-BIBFRAME adopter display fields (e.g. cover, rating,
	// dateRead) a provider carried through the feed graph under bibframe.ExtraPred
	// (tasks/026). The Hugo module forwards it to page params (tasks/022). Omitted (nil)
	// when the corpus carries none, so a catalog without extras is unchanged.
	Extra map[string]string `json:"extra,omitempty"`
	// Relations are the Work's editorial whole/part links (tasks/221/222),
	// restricted to works present in this projection -- a link to a
	// suppressed, tombstoned, or foreign work is omitted rather than
	// rendered as a dead cross-link.
	Relations *Relations `json:"relations,omitempty"`
}

Work is the discovery unit as the static site sees it -- the display and facet fields of a bf:Work plus its Instances (the borrowable editions/formats).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL