Technical deep dive into Starmap's system design, components, and patterns
Last Updated: 2026-07-09 Status: Production-ready architecture following idiomatic Go patterns
- Overview
- Design Principles
- System Components
- Application Layer
- CLI Architecture
- Core Package Layer
- Root Package (starmap.Client)
- Data Sources
- Sync Pipeline
- Reconciliation System
- Real-Time Event Delivery
- Thread Safety
- Package Organization
- Testing Strategy
- References
Starmap is a unified AI model catalog system that combines data from multiple sources into a single authoritative catalog. The architecture follows idiomatic Go patterns with a focus on:
- Separation of concerns: Clear boundaries between layers
- Dependency injection: Interface-based design for testability
- Thread safety: Value semantics and proper synchronization
- Extensibility: Plugin patterns for sources, strategies, and storage backends
graph TB
subgraph UI["User Interfaces"]
CLI[CLI Tool<br/>cmd/starmap]
GO[Go Package API]
HTTP[HTTP Server<br/>REST API + WebSocket + SSE]
end
subgraph APP["Application Layer - internal/application/"]
APPIF[Application Interface<br/>DI Pattern]
APPIMPL[App Implementation<br/>cmd/starmap/app/]
end
subgraph ROOT["Root Package - starmap.Client"]
SYNC[Sync Adapter<br/>Client.Sync]
HOOKS[Event Hooks<br/>Callbacks]
AUTO[Auto-Updates<br/>Background Sync]
end
subgraph CORE["Core Packages - pkg/"]
CAT[Catalogs<br/>Storage Abstraction]
REC[Reconciler<br/>Multi-Source Merging]
AUTH[Authority<br/>Field-Level Priorities]
SOURCES[Sources<br/>Data Interfaces]
end
subgraph IMPL["Internal Implementations"]
PIPE[Sync Pipeline<br/>internal/catalog/pipeline]
EMBED[Embedded Data<br/>go:embed]
PROVS[Provider Clients<br/>OpenAI, Anthropic, etc.]
MODELS[models.dev<br/>Git & HTTP]
LOCAL[Local Files<br/>User Overrides]
end
CLI --> APPIF
GO --> APPIF
HTTP --> APPIF
APPIF -.implemented by.-> APPIMPL
APPIMPL --> ROOT
ROOT --> PIPE
PIPE --> CORE
PROVS & MODELS & LOCAL & EMBED -.implement.-> SOURCES
style UI fill:#e3f2fd
style APP fill:#fff3e0
style ROOT fill:#f3e5f5
style CORE fill:#e8f5e9
style IMPL fill:#fce4ec
Architecture Layers:
- User Interfaces: Multiple entry points (CLI, Go package, HTTP API)
- Application Layer: Dependency injection pattern with interface/implementation separation
- Root Package: Public API with sync orchestration, hooks, and lifecycle management
- Core Packages: Reusable business logic for catalog management and reconciliation
- Internal Implementations: Provider-specific code and data sources
- Define interfaces where they're used (Go proverb)
- Application interface in
internal/application/(reusable across binaries) - Implementation in
cmd/starmap/app/(concrete types) - Commands depend only on what they need
- Constructor injection via functional options
- Interface-based contracts
- Easy mocking for tests
- Example:
NewCommand(app application.Application)
- Value semantics for collections
- Deep copy once at immutable catalog publication
- Atomic generation reads; caller-owned copies at collection boundaries
- Double-checked locking for singletons
- RWMutex for concurrent access
- See Thread Safety section for details
- Each package has one clear purpose
- Catalog: storage abstraction
- Reconciler: multi-source merging
- Authority: field-level priorities
- Sources: data fetching
- Typed errors in
pkg/errors - No panics in library code
- Errors wrap context
- Examples:
NotFoundError,SyncError,APIError
-
Application Layer (
internal/application/,cmd/starmap/app/)- Dependency injection
- Configuration management
- Lifecycle control (startup/shutdown)
- Singleton management
-
Root Package (
starmap.Client)- Public API surface
- Sync adapter into
internal/catalog/pipeline - Event hooks
- Atomic immutable generation publication
-
Core Packages (
pkg/)- Catalog domain and immutable reads (
pkg/catalogs/) - Transactional generation storage (
pkg/catalogstore/) - Multi-source reconciliation (
pkg/reconciler/) - Field-level authority (
pkg/authority/) - Data source abstractions (
pkg/sources/)
- Catalog domain and immutable reads (
-
Internal Implementations (
internal/)- Embedded catalog data
- Provider API clients
- models.dev integration
- Transport utilities
- Shared catalog query behavior for CLI and HTTP adapters
Location: internal/application/application.go
Design Philosophy:
- "Accept interfaces, return structs" (Go proverb)
- "Define interfaces where they're used" (idiomatic Go)
- Located in
internal/applicationfor internal package organization - Zero import cycles (unidirectional dependency flow)
Interface Definition:
type Application interface {
// Catalog adapts the concrete immutable catalog for command callers
Catalog() (*catalogs.Catalog, error)
// Starmap returns starmap instance with optional configuration
// Without options: returns cached instance (thread-safe singleton)
// With options: creates new instance (no caching)
Starmap(opts ...starmap.Option) (*starmap.Client, error)
// Logger returns the configured logger
Logger() *zerolog.Logger
// OutputFormat returns configured output format
OutputFormat() string
// Version info methods
Version() string
Commit() string
Date() string
BuiltBy() string
}Starmap applies the deletion test to interfaces: retain them at algorithm, transport, or application input boundaries only when there are multiple real adapters, or when an executable alternate adapter proves the extension seam. Constructors return concrete types when a package owns one implementation.
| Interface seam | Count | Adapters exercised by the repository | Disposition |
|---|---|---|---|
catalogs.Reader |
2 | *catalogs.Builder, *catalogs.Catalog |
Retained algorithm input; TestSeamConformanceReaderHasBuilderAndCatalogAdapters executes both |
| Catalog collection readers | 2 each | Mutable Providers/Authors/Endpoints/Models/Provenance and immutable reader wrappers |
Retained read-only collection boundaries with two implementations each |
catalogstore.Store |
4 | memory, filesystem, SQLite, conditional object storage | Retained generation-storage boundary; all adapters run the same TestCatalogStoreConformance suite |
catalogstore.ObjectBackend |
2 | memory reference backend, recording alternate backend | Retained cloud-object input; TestSeamConformanceObjectStoreAcceptsAlternateBackend executes replacement injection |
authority.Authority |
2 | default authorities, custom seamAuthority |
Retained policy input; TestSeamConformanceAuthorityAcceptsCustomAdapter proves replacement policy |
provenance.Tracker |
2 | in-memory tracker, custom seamTracker |
Retained observation input; TestSeamConformancePipelineAcceptsCustomTracker proves replacement tracking |
enhancer.Enhancer |
4 | ModelsDevEnhancer, MetadataEnhancer, ChainEnhancer, test enhancer |
Retained plugin boundary; compile assertions cover all built-ins and pipeline tests execute alternates |
reconciler.Strategy and internal resourceConflictResolver |
2 each | authority and source-order strategies | Retained policy boundaries with two production algorithms |
sources.Source |
5+ | local, provider, models.dev HTTP, models.dev Git, test sources | Retained source/plugin boundary with four production adapters |
| Public and internal provider-client seams | 4+ each | OpenAI-compatible, Anthropic, Google, injected fakes | Retained provider transport boundaries with three production families |
application.Application |
2 | CLI App, application.Mock |
Retained consumer-owned command boundary; compile assertions cover both |
Pipeline Store |
2 | root pipelineStore, pipelineTestStore |
Retained consumer-owned persistence boundary |
Pipeline providerSetter |
2 | *catalogs.Builder, failing test adapter |
Retained failure-injection boundary exercised by pipeline tests |
Update syncClient |
2 | *starmap.Client, recordingSyncClient |
Retained command boundary exercised without network calls |
Attribution Matcher |
2 | compiled matcher, custom seamMatcher |
Retained composite-algorithm input; TestSeamConformanceMultiMatcherAcceptsCustomAdapter proves injection |
CLI hints Provider |
2 | ProviderFunc, named provider |
Retained registry/plugin boundary with two adapters |
CLI Formatter |
4 | JSON, YAML, table, function adapter | Retained output boundary with four adapters |
CLI alerts Writer |
2 | function and structured format writers | Retained output boundary with two adapters |
Transport Authenticator |
5 | no-auth, bearer, header, query, provider auth | Retained transport policy boundary with five adapters |
Event Subscriber |
2 | SSE and WebSocket subscribers | Retained fan-out boundary with two production transports |
Deleted one-adapter or unused abstractions include the exported Snapshot, the
catalog Builder, Writer, Merger, Copier, and Persistence interfaces,
the root Client, Catalog, Updater, AutoUpdater, Hooks, and
Persistence capability interfaces, reconciler Merger and Reconciler,
differ Differ, and provenance Auditor. Builder, Client, Reconciler,
and Differ are now concrete product types; mutation and publication remain
separate.
The root client exposes explicit idempotent Sync/Update operations and owns
no ticker, cadence lifecycle, retry loop, or constructor-started goroutine.
Starport and deployment composition own scheduling, startup policy, jitter,
leases, and HA coordination. Custom candidate construction uses the
context-aware WithUpdateFunc seam; it does not imply cadence.
flowchart BT
APP[cmd/starmap/app/<br/>App implements Application]
CMD[cmd/starmap/cmd/*<br/>Commands use Application]
INT[internal/application/<br/>Application interface]
APP -->|implements| INT
CMD -->|imports| INT
style INT fill:#e3f2fd
style CMD fill:#fff3e0
style APP fill:#f3e5f5
Key Points:
- Commands depend only on the interface, not the implementation
- App is injected into commands at runtime
- Zero import cycles (unidirectional dependencies)
- Easy to test with mock implementations
Location: cmd/starmap/app/app.go
Responsibilities:
- Implements
Applicationinterface - Manages configuration, logger, starmap singleton
- Thread-safe lazy initialization
- Graceful lifecycle management
Key Components:
type App struct {
version string
commit string
date string
builtBy string
config *Config
logger *zerolog.Logger
mu sync.RWMutex
starmap starmap.Client // Lazy-initialized singleton
}Thread-Safe Singleton Pattern:
The App uses double-checked locking for optimal performance:
func (a *App) Starmap(opts ...starmap.Option) (starmap.Client, error) {
// Fast path: read lock check
a.mu.RLock()
if a.starmap != nil && len(opts) == 0 {
sm := a.starmap
a.mu.RUnlock()
return sm, nil
}
a.mu.RUnlock()
// Slow path: write lock initialization
a.mu.Lock()
defer a.mu.Unlock()
// Double-check after acquiring write lock
if a.starmap != nil && len(opts) == 0 {
return a.starmap, nil
}
// Create instance (new if opts provided)
sm, err := starmap.New(...)
if err != nil {
return nil, err
}
// Cache only if no custom options
if len(opts) == 0 {
a.starmap = sm
}
return sm, nil
}Visual Representation of Double-Checked Locking:
sequenceDiagram
participant G1 as Goroutine 1
participant G2 as Goroutine 2
participant Lock as RWMutex
participant SM as Starmap Singleton
Note over G1,G2: Scenario 1: First Call (Uninitialized)
G1->>Lock: RLock()
G1->>SM: Check if nil
SM-->>G1: Yes, is nil
G1->>Lock: RUnlock()
G1->>Lock: Lock() [write lock]
G1->>SM: Double-check if nil
SM-->>G1: Still nil
Note over G1: Initialize starmap<br/>(only once)
G1->>SM: Set instance
G1->>Lock: Unlock()
Note over G1,G2: Scenario 2: Subsequent Calls (Initialized)
G2->>Lock: RLock()
G2->>SM: Check if nil
SM-->>G2: No, exists!
Note over G2: Fast path<br/>(no allocation)
G2->>Lock: RUnlock()
G2-->>G2: Return existing instance
Why This Pattern?
- First Check (Read Lock): Fast path for the common case (already initialized)
- Write Lock Acquisition: Only when initialization needed
- Second Check (Write Lock): Prevent race condition between locks
- Result: Thread-safe singleton with minimal overhead
Starmap's CLI is built on these core principles:
- POSIX Compliance: Standard Unix flag conventions (
-o,--output) - Discoverability: Clear help text, intuitive command names
- Consistency: Same patterns across all commands
- Ergonomics: Short flags for common operations, sensible defaults
Commands follow the VERB-NOUN pattern borrowed from kubectl and other modern CLIs:
starmap <verb> <noun> [arguments] [flags]
↓ ↓ ↓ ↓
action resource identity modifiers
Examples:
starmap models list # resource=models, subcommand=list
starmap providers fetch anthropic # resource=providers, subcommand=fetch, arg=anthropic
starmap update openai # verb=update, arg=openaiCommand Groups:
- Setup Commands: Getting started (auth, deps)
- Catalog Commands: Working with catalog resources (authors, models, providers, update)
- Server Commands: Running the API (serve)
- Development Commands: Debugging and exploration (embed, validate)
- Additional Commands: Utilities (completion, version, help)
These flags are always available and must not be overridden by commands:
| Short | Long | Purpose | Notes |
|---|---|---|---|
-v |
--verbose |
Verbose output | Sets log level to debug |
-q |
--quiet |
Minimal output | Sets log level to warn |
-o |
--output |
Output format | table, json, yaml, wide |
-h |
--help |
Show help | Built-in Cobra flag |
Why -o for output?
- Avoids conflict with embed cat's
--filename(-f) - Matches common tools like
gcc -o output - Frees up
-ffor--forcein commands that need it
Added programmatically via globals.AddResourceFlags():
| Short | Long | Purpose |
|---|---|---|
-p |
--provider |
Filter by provider |
--author |
Filter by author | |
--search |
Search term | |
-l |
--limit |
Limit results |
Commands define their own flags that don't conflict with global flags:
Update Command:
-f/--force- Force fresh update-y/--yes- Auto-approve changes--dry- Preview changes (primary)--dry-run- Preview changes (deprecated alias)
Embed Commands:
- Custom help flag (
-?) frees up-hand-f ls -h- Human-readable sizes (like Unix ls)cat -f- Show filename before content
Decision: Use positional arguments for identity/resource, flags for options/modifiers
Rationale:
# ✅ Good: Resource is positional, options are flags
starmap update openai --dry
# ❌ Avoided: Resource as flag feels less natural
starmap update --provider openai --dryPattern:
- Positional = "What to act on" (which provider, which model)
- Flags = "How to act" (dry run, force, output format)
Decision: Clean breaks acceptable for young projects (<1.0)
Rationale:
- Project is pre-1.0, rapid iteration beneficial
- Clear communication via commit messages
- Deprecation periods add complexity without benefit at this stage
- Post-1.0: Will use proper deprecation (6-12 months)
Example from Phase 2:
# Before (v0.x)
starmap update --provider openai
# After (v0.x+1) - Clean break
starmap update openai
# Commit message included migration guideDecision: Allow command groups to override -h with custom patterns
Rationale:
- Embed commands need Unix-like flags (
ls -hfor human-readable) - Solution: Parent command defines
-?for help - All subcommands inherit this, freeing
-hand-f
Implementation:
// Parent: cmd/starmap/app/commands.go
cmd.PersistentFlags().BoolP("help", "?", false, "help for embed commands")
// Now subcommands can use -h
LsCmd.Flags().BoolVarP(&lsHuman, "human-readable", "h", false, "...")4. Hidden Alias Flags
Decision: Support backward compatibility via hidden aliases
Rationale:
- Users may have scripts depending on old flags
- Hidden flags don't clutter help text
- Smooth migration path
Example:
// Primary flag (shown in help)
rootCmd.PersistentFlags().StringVarP(&a.config.Output, "output", "o", "", "...")
// Hidden aliases (backward compat)
rootCmd.PersistentFlags().StringVar(&a.config.Output, "format", "", "")
_ = rootCmd.PersistentFlags().MarkHidden("format")Framework: Cobra - Industry-standard Go CLI library
Key Files:
cmd/starmap/app/execute.go- Root command and global flagscmd/starmap/app/commands.go- Command registrationinternal/cli/globals/- Shared flag utilitiescmd/starmap/cmd/*/- Individual command implementations
For comprehensive CLI reference and implementation guidelines, see CLI.md.
Location: pkg/catalogs/
Purpose: Immutable catalog product plus a separate advanced construction type
Key Types:
Catalog- Concrete immutable catalog returned to consumersBuilder- Concrete mutable construction type for sources/plugins and update pipelinesReader- Narrow algorithm-input interface implemented by both typesModel,Provider,Author,Endpoint- Core data types- Collections:
Providers,Authors,Models,Endpoints
Storage Backends:
- Memory (testing)
- Filesystem (development)
- Embedded (production)
- Custom FS (S3, GCS, etc.)
Thread Safety: Value semantics, all List() methods return slices of values (not pointers)
See pkg/catalogs/README.md for details.
catalogs.GenerationManifest is the transport-neutral identity and audit record
for one immutable catalog payload. P3.1 defines the contract; P3.2 and later
store work is responsible for committing and activating it atomically.
| Manifest field | Meaning |
|---|---|
manifest_version |
Version of the manifest envelope itself |
schema_version |
Version of the canonical catalog payload schema |
generation_id, generated_at |
Immutable generation identity and UTC creation time |
payload |
SHA-256 checksum, exact byte size, and canonical media type |
validation |
Validator version/time, overall status, counts, and named check results |
sync_run_id |
Correlation ID for the synchronization attempt that built the candidate |
source_observations |
Source/observation IDs and evidence checksums needed for audit and replay |
completeness, degraded, degradation_reasons |
Separate record-coverage and quality/fallback state |
consumer_compatibility |
Inclusive catalog-schema range; never a Starmap or Starport binary range |
Publication eligibility requires a passed validation report, no failed checks,
valid checksums, a non-empty observation set, internally consistent
completeness/degradation state, and a schema version inside the declared
consumer range. The checked-in JSON Schema, example manifest, and exact payload
fixture live in pkg/catalogs/testdata/generation/.
pkg/catalogartifact packages one validated catalogstore.Generation as a
deterministic archive plus detached in-toto statement. The archive contains a
strict descriptor, the complete generation manifest, and the exact canonical
payload. Rebuilds of identical inputs are byte-identical; opening revalidates
the manifest/payload binding, member descriptors, compatibility identity, and
all statement subjects before returning a generation. See
Catalog Artifact Format.
Generation IDs are immutable logical IDs. SHA-256 independently content-addresses the payload and archive; schema compatibility is not coupled to binary versions. Release staging writes archive, statement, and checksum as one fsynced atomic directory; exact retries are idempotent and same-generation byte changes are typed conflicts. The GitHub tag workflow uploads these assets without an overwrite flag.
pkg/catalogremote owns the online Starmap-to-Starmap protocol. A client reads
the current strict manifest from a versioned API base, then fetches the exact
generation-addressed canonical snapshot. Strict media type, body bounds,
catalog-schema compatibility, size, and checksum validation all precede decode
and compare-and-swap publication. The server and root remote-update path share
these route constants; the old ad-hoc unversioned catalog envelope is removed.
See Remote Catalog Protocol.
pkg/catalogdistribution owns the separate hosted protocol. A small
latest-compatible pointer selects immutable archive and attestation URLs under
the same origin; the client verifies pointer compatibility, URL origin, media
type, size, checksum, artifact, statement, and downloaded manifest identity.
The handler reads through a narrow repository boundary. Hosted pointers are
explicit dev, canary, or stable channels, with stable as the consumer
default. Promotion is ordered dev-to-canary-to-stable; stable additionally
requires recent hosted canary evidence for availability, generation freshness,
latency, and exact archive identity. Promotion failures and successes are
queryable telemetry. Reasoned rollback may select only a generation previously
served by that channel and never deletes immutable history. See
Hosted Catalog Distribution.
Channel-specific trust roots and availability/freshness tradeoffs are defined in
Catalog Distribution Trust Model.
The embedded fallback has a separate checked-in budget gate for generation age, canonical uncompressed payload size, deterministic compressed archive size, and minimum provider/model coverage. Runtime readiness and hosted CI report distinct evidence. See Embedded Catalog Budgets.
Repository-owned Scheduled Catalog Generation runs daily or manually above the idempotent sync/generation boundary. It derives new manifest identity only when canonical payload bytes change, validates and attests before immutable payload-digest release publication, and never uses Actions artifacts as runtime distribution.
Deployment-owned Durable Scheduling composes a narrow
Syncer with a named Lease. Contending replicas skip before source work. The
reference lease uses expiry plus fencing, and the filesystem adapter coordinates
independent processes on a shared volume; Starport can supply a distributed
adapter without changing Starmap's core client. Scheduled triggers add bounded
pre-lease jitter; typed retry is fail-closed and bounded while the lease remains
held. An optional RunLedger begins before acquisition, records every attempt,
and completes with the base and published generation identities. Memory and
SQLite reference adapters make lifecycle semantics and crash-visible running
records executable; Starport can bind the same narrow interface to its database.
Successful Sync results also expose validated source-observation projections
even when catalog bytes do not change. An explicit deployment FreshnessPolicy
turns those observations into ready/degraded/unready states and stable alerts;
there are no implicit SLA durations. Out-of-order completion cannot regress the
latest source evidence, and a current generation can seed state after restart.
An InitialRunController requires one of blocking, background, or schedule-only
startup modes. It composes an explicit baseline-readiness probe, performs at
most one startup attempt, and coalesces only scheduled due-times inside a
configured window. Failed startup never suppresses the recovery tick. The
controller remains passive and owns no ticker.
pkg/catalogstore.Store persists a Generation (manifest plus exact payload)
and exposes Current, Get, and Commit. Every commit is compare-and-swap:
the caller supplies the expected current generation ID, with an empty ID meaning
the store must still be empty. Implementations validate the manifest and payload
before storage, retain old immutable generations, return caller-owned bytes, and
make an identical retry idempotent.
| Adapter | Baseline P3.2 mechanism | Later hardening owner |
|---|---|---|
| Memory | Locked immutable map and current ID | Reference semantics/conformance |
| Filesystem | Cross-instance advisory commit lock plus fsynced immutable directory/current rename | P3.3/P3.5 durability and same-base CAS complete |
| SQLite | Serializable database/sql transaction over generation/current tables |
P3.8 rollback, reactivation, deletion, CAS, reopen, and fault matrix complete |
| Object | Immutable manifest/payload objects plus version-conditional current object | P3.9 upload/promotion faults, corruption, rollback, deletion, CAS, and reopen complete |
The shared conformance suite covers empty reads, commit/current/get, immutable ownership, durable reopen, retained history, idempotent retries, stale CAS, checksum rejection, generation-ID collisions, and cancellation. Passing the baseline suite does not substitute for the later adapter-specific fault gates. The concurrent same-base matrix opens independent adapters over one backend and requires exactly one success and one typed conflict. SQLite deployments use immediate transactions with bounded busy waiting; filesystem writers coordinate through a context-aware advisory lock shared across processes.
Builder.Save materializes an optional editable YAML export using replacement
semantics for its managed YAML indexes and provider/author model trees, so
deleted records cannot survive a
save/reload. It deliberately preserves unmanaged neighboring files such as
logos and operator notes. It is a portable materialization, not a second
transactional database: a process failure or rejected durable commit can leave
that directory temporarily ahead of or behind the authoritative generation.
Production readers must consume catalogstore.Store/the immutable distribution
protocol rather than serve the YAML view directly; restart with a durable
current deliberately ignores the export view. Catalog-generation jobs
may still use it as an explicit checked export.
The root client makes that dependency explicit: WithCatalogStore is required
before any non-dry manual, remote, server-triggered, or scheduled mutation. The
preflight runs before source fetch, custom callbacks, remote HTTP, or scheduler
startup and returns a typed errors.ConfigError when the store is absent.
Read-only construction, Catalog, and dry-run synchronization remain usable
without a store. The CLI composition root supplies a passive filesystem store
at catalog_path (default ~/.starmap/catalog); constructing the adapter does
not create storage until its first commit. Optional editable YAML uses
catalog_export_path and defaults to ~/.starmap/exports/catalog for CLI
materialization. Database and export roots must not contain one another, even
through an existing symlink. Cache, source evidence, logs, configuration, YAML
exports, and immutable generations remain separate lifecycle domains.
An explicitly configured catalog export is optional only when its path does not
exist. NewLocal detects the wrapped os.ErrNotExist and uses the embedded
bootstrap; malformed provider, author, provenance, or model YAML and other I/O
or validation failures remain typed errors. When a configured CatalogStore has
a current generation, that validated durable generation is authoritative and
export YAML is not parsed; this prevents a stale or partially materialized
export view from blocking restart. Export YAML is consulted only when no
durable current exists.
The embedded bootstrap has a strict embedded generation.json binding its
generation ID, generation time, catalog schema version, canonical payload
SHA-256, and byte size. starmap.New verifies that manifest entirely offline
before publication. Client.Readiness reports the generation metadata and age;
WithEmbeddedBootstrapMaxAge and WithEmbeddedBootstrapMaxSizeBytes make the
HTTP readiness endpoint fail with stable reason codes while an out-of-budget
bootstrap remains active. A committed generation supersedes bootstrap budgets.
The CLI/server composition root accepts the same policies through
embedded_bootstrap_max_age and embedded_bootstrap_max_size_bytes (or their
uppercase environment-variable forms).
The documented pre-generation directory format is frozen under
pkg/catalogstore/testdata/legacy-v0/. MigrateLegacyDirectory parses that
format without mutation, requires caller-supplied generation/run/observation
identity and UTC time, and deterministically emits schema-v1 CatalogPayload
bytes plus a validated manifest. Provider-specific and author model indexes are
encoded separately because the legacy record structs intentionally exclude
their runtime model maps from JSON/YAML indexes.
Location: pkg/reconciler/
Purpose: Multi-source data reconciliation with conflict resolution
Key Components:
Reconcilerconcrete engineStrategy- Defines how conflicts are resolved- Field rule catalog - Package-internal model/provider/author field inventory
Result- Reconciliation outcome with changeset and metadata
Strategies:
- AuthorityStrategy - Field-level authority priorities
- SourceOrderStrategy - Fixed source precedence order
Pipeline:
- Fetch catalogs from all sources
- Merge using configured strategy
- Detect changes vs baseline
- Generate changeset with provenance
- Return result
Field Rules:
pkg/reconciler/field_rules.go is the canonical inventory for reconciled fields. Each rule carries the resource type, reflection path, authority path, and provenance path. The merger iterates this catalog instead of local string slices, so adding or renaming a catalog field requires updating one rule table and the matching authority entry. Tests verify that every model, provider, and author rule points at a real struct field and resolves through the authority system.
See pkg/reconciler/README.md for details.
Location: pkg/authority/
Purpose: Field-level source authority system
How It Works:
- Each field (e.g., "Pricing", "Limits") has authority configuration
- Sources ranked by priority for that field
- Pattern matching supports wildcards: "Pricing.*"
- Higher priority wins in conflicts
Example Authorities:
// Pricing - a valid provider-offering observation wins atomically
{Path: "Pricing", Source: sources.ProvidersID, Priority: 110}
{Path: "Pricing", Source: sources.ModelsDevHTTPID, Priority: 100}
// Availability - Provider API is truth
{Path: "Features", Source: sources.ProvidersID, Priority: 95}
// Descriptions - prefer manual edits
{Path: "Description", Source: sources.LocalCatalogID, Priority: 90}See pkg/authority/authority.go for legacy field authority configuration.
The canonical definition/offering inventory and merge/empty semantics are
documented in CATALOG_AUTHORITY_POLICY.md and
enforced by pkg/authority.CanonicalPolicies coverage tests.
Source decoding uses scoped strictness rather than a global permissive or
strict mode. Identity and container type drift rejects its source/record scope;
unknown additive members are preserved inside extensions or classified as
reviewable evidence before promotion. The executable inventory and rationale
are documented in SCHEMA_DRIFT_POLICY.md and exposed
by pkg/sources.SchemaDriftPolicies.
Location: pkg/sources/
Purpose: Reentrant observation boundary for external catalog data
Source Interface:
type Source interface {
ID() ID
Name() string
Observe(ctx context.Context, opts ...Option) (Observation, error)
Cleanup() error
Dependencies() []Dependency
IsOptional() bool
}
type Observation struct {
ID string
SourceID ID
ObservedAt time.Time
Revision Revision
Completeness ObservationCompleteness
Status ObservationStatus
EvidenceChecksum string
Catalog *catalogs.Catalog
}Observe returns one immutable result directly. Implementations are safe for
repeated and concurrent calls and never require stateful Fetch then Catalog
ordering. Each call builds its mutable candidate off to the side and publishes
only the resulting immutable catalog. Observation construction deterministically
encodes the normalized catalog, binds its SHA-256 evidence checksum, and derives
an event ID from source, UTC observation time, revision, completeness, status,
and checksum. HTTP ETags and exact Git commits are preferred when available;
until those transport-specific revisions are exposed, adapters explicitly use
the normalized content digest rather than inventing an upstream revision.
models.dev transport loaders execute per observation and honor that source
instance's configured directory; no package-level sync.Once or cached parsed
API survives between scheduled/manual calls. The HTTP transport may still reuse
a validated on-disk response under its explicit cache policy, while parsing it
again for every observation. A versioned sidecar binds cache origin,
validation time, ETag/Last-Modified, and the raw response checksum to the exact
api.json bytes. Only a checksum-matching sidecar can supply conditional
headers or be classified as fresh. After TTL, ETag is preferred and
Last-Modified is the fallback validator; a valid 304 Not Modified refreshes
the sidecar without transferring or rewriting the catalog body, and its exact
validator becomes the observation revision. A missing/mismatched sidecar forces
an unconditional fetch and can only degrade to unverified stale fallback.
Embedded cache origin is retained across reuse and therefore never becomes a
successful fresh HTTP observation. Git verification checks/builds its
configured checkout for every observation. Because Git is an explicit
verification transport, callers must supply an exact 40- or 64-character commit
through sync.WithModelsDevGitCommit (or the corresponding CLI flag); branch
names and empty revisions fail validation before clone/fetch. The Git client
fetches that object, uses a forced detached checkout, verifies HEAD, hashes
bun.lock, runs bun install --frozen-lockfile, and rejects any lockfile
mutation. Its observation revision records the exact Git commit plus lockfile
path and SHA-256, so the build input can be reproduced after the remote branch
moves.
The pipeline validates every observation before reconciliation. Durable generation manifests preserve the exact observation ID, UTC time, revision, completeness, typed status, and evidence checksum; they never substitute the final reconciled catalog checksum for source evidence. A partial observation forces partial/degraded generation state.
pkg/sourceevidence implements the separate evidence-retention boundary. Its
long-term normalized record contains the canonical catalog payload (including
provenance), observation metadata, checksum, and machine-readable issue
scope/code/subject. Diagnostic issue messages are deliberately omitted because
they can contain upstream response text or credentials. Loading a normalized
record verifies its payload checksum and reconstructs the same observation ID,
candidate catalog bytes, and provenance before it can be used for replay.
Raw evidence is response-body-only: request headers, query parameters, and
credentials have no representation in the retained type. Archive encrypts it
with AES-256-GCM using a caller-supplied 32-byte key, binds the ciphertext to
the observation ID and expiry, writes directories/files with 0700/0600
permissions, and uses fsynced atomic replacement. The default raw retention is
24 hours and the enforced maximum is seven days; PurgeExpiredRaw removes
expired envelopes. Archive construction is passive, and normalized evidence
can be retained independently of optional raw retention.
Observation outcomes use one explicit policy:
- a non-nil Go error means the source call failed and produced no policy-usable observation (a diagnostic partial catalog may accompany it, but publication stops);
- a usable incomplete result returns nil error with
partial/degradedstate and typed issues, so valid sibling providers/records remain reconcilable; - issue scope is exactly
record,provider,source, orstale_fallback; record/provider issues name their subject and every issue carries a stable code and message; - missing provider credentials/configuration and provider fetch failures are provider-scoped partial degradation, not successful empty live fetches;
- stale last-known-good fallback is explicitly degraded (and can remain structurally complete), never mislabeled as a fresh success.
Source Types:
- Provider APIs (
sources.ProvidersID) - Real-time model availability - models.dev HTTP (
sources.ModelsDevHTTPID) - Default production input, with validated disk-cache and embedded last-known-good fallback - models.dev Git (
sources.ModelsDevGitID) - Explicit build/verification transport; never runs alongside HTTP in one sync - Local Catalog (
sources.LocalCatalogID) - User overrides - Embedded (
sources.EmbeddedID) - Baseline data shipped with binary
See pkg/sources/README.md for details. See pkg/sourceevidence/README.md for evidence retention and replay.
Location: client.go, sync.go
Purpose: Main public API with sync adapter, catalog access, persistence, and event hooks
type Client struct {
// unexported state
}
func New(opts ...Option) (*Client, error)
func (c *Client) Catalog() *catalogs.Catalog
func (c *Client) Sync(ctx context.Context, opts ...sync.Option) (*sync.Result, error)The root package returns concrete *Client; consumers that need substitution
define the smallest interface at their own use site. After New succeeds,
Catalog is non-failing, non-nil, O(1), and returns a retained immutable
generation.
Used throughout for configuration:
// Creating with options
store, err := catalogstore.NewFilesystem("./catalog")
if err != nil {
return err
}
sm, err := starmap.New(
starmap.WithCatalogStore(store),
starmap.WithCatalogExportPath("./exports/catalog"),
)
// Sync with options
result, err := sm.Sync(ctx,
sync.WithProvider("openai"),
sync.WithDryRun(true),
sync.WithTimeout(5 * time.Minute),
)Location: internal/catalog/query/
CLI commands and HTTP handlers share catalog list behavior through internal/catalog/query. That package owns reusable filtering, stable ID sorting, limiting, and pagination for models, providers, and authors. Command and server packages still own input parsing, authentication, cache keys, transport responses, table formatting, and JSON/YAML output.
The HTTP model query contract validates every declared sort, order, range,
modality, feature, date, and pagination value before execution. Supported model
sorts are id, name, release_date, context_window, created_at, and
updated_at; missing date/numeric values sort last in either direction and ID
is the deterministic tie-breaker. Unsupported or malformed values return a
typed client error rather than silently changing semantics.
Each catalog publication also advances a monotonic process-local sequence tied
to the durable generation_id. Request handlers atomically read the immutable
catalog, generation ID, and sequence together, set X-Starmap-Generation-ID,
and use that pair as the cache namespace. Advancing a sequence flushes the old
namespace; an in-flight request from an older sequence cannot reactivate or
populate it. Only a successful durable commit swaps the catalog and emits the
asynchronous catalog.published event containing the same generation,
sync-run, and sequence identities. Failed commits change neither state nor
events, and an identical remote-generation retry is not republished.
pkg/catalogscheduler.Operations is the deployment composition boundary for
operator telemetry. It owns no ticker or update lifecycle: the deployment
injects its durable RunLedger, FreshnessMonitor, and optional
InitialRunController, then supplies the atomic catalog identity when reading
state. GET /api/v1/operations exposes the evaluated generation, source SLA
report, degraded source IDs, latest actual sync (excluding lease/coalescing
skips), and whether scheduler/startup telemetry is configured. A deployment
that has not wired scheduling reports scheduler.configured=false explicitly
instead of presenting fabricated freshness or success state.
This keeps adapters thin without moving UI-specific behavior into catalog storage:
models := query.Models(cat.Models().List(), query.ModelOptions{
Author: flags.Author,
Capability: capability,
Search: flags.Search,
Limit: flags.Limit,
})
page := query.Paginate(filteredModels, limit, offset)Data flows from multiple sources into the reconciliation engine, with each source having specific authority for different types of data:
graph TD
LOCAL["Local Catalog<br/><b>Priority: 100</b> (API Config)<br/>• API keys & endpoints<br/>• Provider configurations<br/>• User overrides"]
API["Provider APIs<br/><b>Priority: 110</b> (Valid Offering Price)<br/><b>Priority: 95</b> (Model Existence)<br/>• Real-time availability<br/>• Offering-specific pricing<br/>• Concurrent fetching"]
MD["models.dev<br/><b>Priority: 100</b> (Price Fallback / Metadata)<br/>• Community pricing fallback<br/>• Provider logos (SVG)<br/>• HTTP default; Git verification"]
EMB["Embedded Catalog<br/><b>Priority: 80</b> (Baseline)<br/>• Ships with binary (go:embed)<br/>• Fallback data<br/>• Manual corrections"]
REC{Reconciliation<br/>Engine<br/>Authority-Based}
CAT["Unified Catalog<br/>✓ Complete<br/>✓ Accurate<br/>✓ Provenance Tracked"]
LOCAL --> REC
API --> REC
MD --> REC
EMB --> REC
REC --> CAT
style LOCAL fill:#fff3e0
style API fill:#e8f5e9
style MD fill:#e3f2fd
style EMB fill:#f3e5f5
style REC fill:#fff9c4
style CAT fill:#c8e6c9
Authority Resolution:
- Pricing: A semantically valid, currently effective provider observation wins for that provider offering; models.dev and local data are fallbacks
- Limits: models.dev remains the legacy reconciler authority while the canonical provider-offering policy is implemented
- Model Existence: Provider APIs determine what models actually exist
- API Configuration: Local catalog takes precedence (user's environment)
- Baseline Data: Embedded catalog provides defaults when other sources unavailable
Provider Fetching Seam:
Provider API acquisition has one implementation in the public
pkg/sources.ProviderFetcher: context timeouts, credential loading/preflight,
client construction, and ListModels execution. Model and raw fetches share the
same credential preflight. internal/sources/providers composes that concrete
fetcher to add bounded multi-provider concurrency, translate typed fetch errors
into observation issues, and associate models with provider catalog entries; it
does not own a second credential/client/fetch policy. Public/internal
conformance tests cover missing credentials, configuration errors, fetch
failures, and adapter call suppression.
Provider configuration and provider evidence are deliberately separated. The
configuration catalog may contain embedded or last-known-good models needed by
the baseline source, but internal/sources/providers removes those models from
its copied configuration before fetching. Its observation therefore contains
only models returned by that invocation. Missing credentials yield a
provider-scoped partial/degraded observation with zero live models, while a
successful fetch replaces bootstrap models instead of blending them into
current evidence. Reconciliation may still use the separately identified local
baseline observation according to authority policy.
Source and provider libraries never write directly to process stdout/stderr.
They emit context-bound zerolog events through pkg/logging; the pipeline adds
one run_id before source work, every source adds its stable source, and
provider-scoped work adds provider_id. Direct library callers can supply the
same correlation with logging.WithRunID. The operation run_id correlates
pre-publication logs and is intentionally distinct from the durable
sync_run_id assigned to a committed generation. AST and captured-output tests
prevent regressions to fmt.Print*, standard-log printing, or direct
os.Stdout/os.Stderr access in source/provider packages.
Starmap treats source fields as an explicit contract. Every attribute from models.dev, provider APIs, local catalogs, and embedded catalogs must have one of three outcomes:
- mapped into canonical catalog schema when the field is broadly meaningful;
- preserved in a controlled
extensionsbucket when the field is source-specific but still useful; - intentionally ignored with a documented reason and regression coverage when the field is operational noise or not meaningful to the catalog.
Canonical fields cover lifecycle status, lineage, context/input/output limits, generation controls, reasoning controls, tiered pricing, mode-specific pricing/request overrides, and provider/model metadata. Controlled extensions preserve source-specific details without letting them participate in field-authority decisions. Reconciliation merges extension buckets additively by source while the field-rule catalog continues to own canonical precedence.
Source-shape tests in internal/sources/modelsdev and internal/providers/* classify representative response paths so upstream schema drift fails deterministically. Live refreshes are opt-in and must write raw payloads outside the repository, print only normalized path summaries, and never persist secrets.
Every checked-in provider response fixture has an adjacent versioned metadata
record containing provider, capture time, content-digest source revision,
payload path/SHA-256, and an explicit maximum age (currently 365 days for the
legacy capture set). internal/providers/testhelper rejects missing, future,
stale, provider-mismatched, or checksum-mismatched metadata. Refresh helpers
write payload and metadata together; the Make target propagates test/fetch
failures and also fails when an alleged refresh changes neither file, preventing
-update no-ops from silently reporting success.
Provider APIs are fetched concurrently with a bounded worker gate:
// internal/sources/providers/providers.go
resultChan := make(chan providerModels, len(providerConfigs))
semaphore := make(chan struct{}, s.effectiveMaxConcurrency(len(providerConfigs)))
for _, provider := range providerConfigs {
wg.Add(1)
go func(p *catalogs.Provider) {
defer wg.Done()
semaphore <- struct{}{}
defer func() { <-semaphore }()
client, err := s.clientFactory(p)
// Fetch, classify errors, and send provider result...
}(provider)
}Location: internal/catalog/pipeline/ with public entry through client.Sync in sync.go
The sync pipeline is a deep internal module behind the public starmap.Client.Sync method. The root client supplies only a store adapter for reading the current catalog and applying a reconciled catalog after persistence succeeds. internal/catalog/pipeline owns execution ordering, source construction, dependency filtering, observation/cleanup fan-out, reconciliation, dry-run behavior, no-change behavior, and forced-save policy.
The pipeline executes in 13 stages with comprehensive error handling and decision points:
flowchart TD
Start([Sync Called]) --> S1{Context<br/>nil?}
S1 -->|Yes| S1B[Set Background Context]
S1 -->|No| S2
S1B --> S2[Parse Options<br/>with Defaults]
S2 --> S3{Timeout<br/>configured?}
S3 -->|Yes| S3B[Setup WithTimeout]
S3 -->|No| S4
S3B --> S4[Load Local<br/>Catalog]
S4 --> S5[Validate<br/>Options]
S5 --> E1{Valid?}
E1 -->|No| Error1[❌ Return Error]
E1 -->|Yes| S6[Filter Sources<br/>by Options]
S6 --> S7[Resolve Dependencies<br/>Check & Install]
S7 --> S8[Setup Cleanup<br/>defer]
S8 --> S9[Observe Sources<br/>⚡ Concurrent]
S9 --> E2{Observation<br/>Success?}
E2 -->|No| Error2[❌ Return Error]
E2 -->|Yes| S10[Get Existing<br/>Catalog Baseline]
S10 --> S11[Reconcile<br/>All Sources]
S11 --> S12[Log Change<br/>Summary]
S12 --> D1{Has<br/>Changes?}
D1 -->|No| D1B{Force<br/>Save?}
D1B -->|No| End1[✓ Return Result<br/>No Changes]
D1B -->|Yes| D2
D1 -->|Yes| D2{Dry<br/>Run?}
D2 -->|Yes| End2[✓ Return Result<br/>Preview Only]
D2 -->|No| S13[Persist, Publish &<br/>Trigger Hooks]
S13 --> End3([✅ Return Result<br/>Changes Applied])
style Start fill:#e3f2fd
style Error1 fill:#ffcdd2
style Error2 fill:#ffcdd2
style S9 fill:#fff9c4
style S11 fill:#e1bee7
style End1 fill:#c8e6c9
style End2 fill:#c8e6c9
style End3 fill:#c8e6c9
Stage Groups:
- Stages 1-5 (Setup): Context, options, validation
- Stages 6-9 (Preparation): Source filtering, dependency resolution, cleanup, concurrent observation
- Stages 10-11 (Processing): Baseline comparison, reconciliation
- Stages 12-13 (Finalization): Change detection, persistence, hooks
func (c *Client) Sync(ctx context.Context, opts ...sync.Option) (*sync.Result, error) {
return pipeline.New(pipelineStore{client: c}).Sync(ctx, opts...)
}
type pipelineStore struct {
client *Client
}
func (s pipelineStore) Catalog() (*catalogs.Catalog, error) {
return s.client.Catalog(), nil
}
func (s pipelineStore) Apply(ctx context.Context, catalog *catalogs.Builder, options *sync.Options, changeset *differ.Changeset, observations []sources.Observation) (pipeline.Publication, error) {
return s.client.save(ctx, catalog, options, changeset, observations)
}- Deep module boundary:
internal/catalog/pipeline.Pipelineowns orchestration;client.Syncremains a stable public adapter - Staged execution: Each stage has clear purpose
- Error handling: Fail fast with context
- Concurrent observation: Reentrant sources return immutable observations in parallel
- Change detection: Diff against baseline
- Dry-run support: Preview without applying
- Force-save support:
--freshand--reformatpersist even when there are no detected changes - Safe publication: A validated generation commits through
CatalogStorebefore the immutable in-memory swap; failed commits emit no callback - Restart recovery:
Newreads, validates, decodes, and publishes the exact durable current generation before consulting local compatibility YAML; an empty store alone falls back to the verified bootstrap/local baseline, while corrupt or unavailable store state fails initialization - Isolated hooks: Post-commit callbacks run asynchronously through bounded
delivery slots; publication observers within a slot run independently so one
slow callback cannot head-of-line block cache/SSE/WebSocket notification.
Returned errors, panics, drops, and latency are observable through
Client.HookStatsand cannot fail or delay the commit path
The HTTP logging middleware preserves the optional http.Flusher,
http.Hijacker, and http.Pusher capabilities of the underlying response
writer. This is required for SSE flushing and WebSocket upgrades; middleware
must not accidentally turn supported streaming transports into HTTP 500s.
Model definition, provider offering, alias, and Starport routing identities
follow the normative Catalog Identity Contract. In
particular, offering identity is the (provider ID, provider model ID) tuple;
route aliases are policy-layer objects and never source-ingestion aliases.
catalogs.ProviderOffering is the provider-specific schema: its comparable key
is (ProviderID, ProviderModelID), and it owns pricing, limits, availability,
regions, endpoint behavior, provider lifecycle, modes, and typed request
overrides. The legacy Model compatibility record remains in place until the
P4 migration moves intrinsic definition fields into their canonical schema.
catalogs.ModelDefinition is the complementary provider-independent record;
reflection and round-trip tests keep its authorship, lineage, weights, and
capabilities surface disjoint from every offering-owned field.
Legacy embedded and source catalogs pass through MigrateLegacySchema, which
uses deterministic provider ordering and classifies exact/defaulted/missing/
conflicting transformations. Its checked embedded baseline currently contains
516 offerings, 490 definitions, 1,073 defaults, 23 reviewed definition
conflicts, 81 explicit missing-authorship records, and zero unclassified
changes. Multi-author marketplace declarations are candidate sets and are never
copied onto every model as invented joint authorship.
Published catalogs precompute definition and offering indexes. Canonical reads
use Definition, Offering, and ProviderOfferings; the latter two retain the
exact provider-scoped identity and return deep copies of nested mutable values.
Route aliases remain caller-supplied policy-layer identities.
MaterializeRouteAlias resolves their exact offering keys against a retained
catalog generation and reports ineligible targets without storing routing
weights or fallback policy in ingestion.
Public compatibility is versioned through the concrete LegacyCatalogV0
adapter. Canonical Catalog.FindModel returns ModelDefinition; provider facts
come from Offering. Existing callers that require the former flattened
Model use catalog.LegacyV0(). Direct legacy collection methods remain
deprecated during the v1 transition, and schema-v0/v1 constants make the
compatibility boundary executable rather than release-version folklore.
The default reconciliation strategy uses field-level authorities:
How it works:
- Iterate the reconciler field-rule catalog for each resource type
- Use each rule's authority path to find matching authority
- Select value from highest-priority source
- Track provenance using each rule's provenance path
- Generate changeset by comparing with baseline
Example:
Model "gpt-4o" exists in 3 sources:
- Provider API: { Name: "GPT-4o", Features: {...}, Pricing: {...} }
- models.dev: { Pricing: {fallback...}, Limits: {...} }
- Local: { Description: "Custom description" }
Reconciled result:
- Name: "GPT-4o" (Provider API, priority 90)
- Features: {...} (Provider API, priority 95)
- Pricing: {...} (Provider API, validated and atomic)
- Limits: {...} (models.dev, priority 100)
- Description: "Custom desc" (Local, priority 90)
sequenceDiagram
participant Sync as Sync Pipeline
participant Rec as Reconciler
participant Auth as Authority System
participant P as Provider API
participant M as models.dev
participant L as Local
Sync->>Rec: Reconcile(sources)
par Concurrent observation from selected sources
Rec->>P: Fetch()
P-->>Rec: {Name, Features, Pricing}
Rec->>M: Fetch()
M-->>Rec: {Pricing fallback, Limits}
Rec->>L: Fetch()
L-->>Rec: {Description}
end
Note over Rec: Process model field rules
Rec->>Auth: ResolveConflict("Name", values)
Auth-->>Rec: Provider API (priority 90)
Rec->>Auth: ResolveConflict("Features", values)
Auth-->>Rec: Provider API (priority 95)
Rec->>Auth: SelectValidOfferingPricing(values, effectiveAt)
Auth-->>Rec: Provider API, or next valid fallback
Rec->>Auth: ResolveConflict("Limits", values)
Auth-->>Rec: models.dev (priority 100)
Rec->>Auth: ResolveConflict("Description", values)
Auth-->>Rec: Local (priority 90)
Note over Rec: Merge all reconciled fields
Rec-->>Sync: Result with changeset<br/>& provenance tracking
Reconciliation Steps:
- Concurrent Fetch: All sources fetched in parallel
- Field-Level Resolution: Authority system determines winner for each field
- Provenance Tracking: Record which source provided each value
- Changeset Generation: Compare with baseline to detect changes
The reconciler generates a comprehensive changeset:
type Changeset struct {
Models struct {
Added []Model
Updated []ModelUpdate
Removed []Model
}
Summary struct {
TotalChanges int
AddedCount int
UpdatedCount int
RemovedCount int
}
}Change Detection:
- Compare reconciled catalog with baseline
- Track field-level changes
- Preserve attribution for each field
- Generate human-readable diffs
The server exposes catalog update events through WebSocket and Server-Sent Events (SSE). Event delivery is split into lifecycle adapters and one shared fan-out policy module:
internal/server/events.Brokeraccepts catalog events and delivers them to internal subscribers.internal/server/sse.Broadcasterowns SSE client registration, HTTP streaming, and SSE formatting.internal/server/websocket.Hubowns WebSocket client registration, ping/write pumps, and WebSocket message formatting.internal/server/events.Fanoutowns target delivery, cumulative counters, and backpressure policy.
Backpressure behavior is explicit per adapter:
| Adapter | Policy | Behavior |
|---|---|---|
| Broker subscribers | Skip/log failed delivery | Subscribers receive events through one bounded queue and worker per subscriber, so slow subscribers cannot stall the broker event loop and fan-out does not spawn one goroutine per subscriber per event |
| SSE clients | Skip | A full SSE client buffer skips that event and keeps the client connected |
| WebSocket clients | Disconnect | A full WebSocket client buffer removes and closes that client |
Fanout exposes comparable delivery counters: sent, skipped, disconnected, and failed. Admin stats surface those counters for broker, SSE, and WebSocket delivery so production behavior can be monitored consistently.
The broker, SSE broadcaster, and WebSocket hub still use buffered registration/unregistration channels so setup and cleanup do not depend on event-loop startup order.
Browser WebSocket upgrades are same-origin by default and follow an explicit CORS allowlist only when CORS is enabled. The application rate limiter keys on the normalized socket-peer IP and deliberately ignores untrusted forwarding headers; deployments that need end-client limits behind a proxy should enforce them at a trusted ingress boundary. Cleanup is request-driven and owns no background goroutine or shutdown lifecycle.
Starmap's catalog system is designed for thread-safe concurrent access. This section consolidates all thread safety patterns and guidelines.
Value Semantics Over Pointer Semantics
The catalog system uses value semantics to prevent race conditions:
// ✅ CORRECT: Returns values
func (c *Catalog) Models() []Model
// ❌ WRONG: Returns pointers (race condition risk)
func (c *Catalog) Models() []*ModelImmutable Generation Publication
Builders are deep-copied once when published. Readers atomically load the same sealed immutable generation; collection methods return caller-owned copies:
func (c *Client) Catalog() *catalogs.Catalog {
c.mu.RLock()
catalog := c.catalog
c.mu.RUnlock()
return catalog
}Used in App.Starmap() for optimal performance:
func (a *App) Starmap(opts ...starmap.Option) (starmap.Client, error) {
// Fast path: read lock check (common case)
a.mu.RLock()
if a.starmap != nil && len(opts) == 0 {
sm := a.starmap
a.mu.RUnlock()
return sm, nil // No allocation
}
a.mu.RUnlock()
// Slow path: write lock initialization (rare)
a.mu.Lock()
defer a.mu.Unlock()
// Double-check after acquiring write lock
if a.starmap != nil && len(opts) == 0 {
return a.starmap, nil
}
// Initialize exactly once
sm, err := starmap.New(buildOptions()...)
if err != nil {
return nil, err
}
a.starmap = sm // Cache for future calls
return sm, nil
}Why double-checked locking?
- First check (read lock): Fast path for initialized case
- Second check (write lock): Prevent race between read unlock and write lock
- Initialization happens exactly once
- Subsequent calls are fast (read lock only)
Collections return slices of values, not pointers:
// Safe: Returns copies
models := catalog.Models().List() // []Model (values)
// Each model is an independent copy
for _, model := range models {
model.Name = "Modified" // Only affects local copy
}Every type provides deep copy methods:
func (m Model) DeepCopy() Model {
copy := m
// Deep copy nested pointers
if m.Pricing != nil {
pricingCopy := *m.Pricing
copy.Pricing = &pricingCopy
}
// ... copy other pointer fields
return copy
}Catalog collection boundaries are copy-on-read and copy-on-write:
Providers,Authors,Models, andEndpointsstore caller input as owned copies.Get,Resolve,List,Map, and catalog convenience methods return caller-owned values or pointers to copies.- Batch writes (
AddBatch,SetBatch) copy accepted values before storing them. ForEachcallbacks receive copies; callback mutation must not affect catalog internals.Provenancecopies maps and slices onSet,Map,FindByField, andFindByResource. ProvenanceValueandPreviousValueareany, so callers should treat complex values placed there as immutable.Builder.Build()is the deep-copy publication boundary.starmap.Clientstores only a concrete immutable*catalogs.Catalog, swaps it under one lock after persistence, and returns that immutable generation without a full-catalog read copy.- Catalog publication precomputes provider/model indexes keyed by canonical
provider ID and aliases. Provider-specific queries use
ProviderModelsorProviderModel; they never recover membership from the legacy flattenedModels()view, where equal model IDs from different providers are lossy.
// Multiple goroutines can safely read
go func() {
models := catalog.Models().List()
// Process models...
}()
go func() {
providers := catalog.Providers().List()
// Process providers...
}()// Updates are atomic and thread-safe
catalog.SetModel(model1)
catalog.SetModel(model2)
// Concurrent writes are serialized internally
go func() { catalog.SetProvider(p1) }()
go func() { catalog.SetProvider(p2) }()// Don't do this - unnecessary
models := catalog.Models().List()
go func() {
// models already contains values, safe to use
fmt.Println(models[0].Name)
}()
// This is fine because models are values
models[0].Name = "Modified" // Only affects local copygraph LR
subgraph "❌ UNSAFE: Shared Mutable State"
direction TB
G1A[Goroutine 1<br/>Read] -->|direct access| SHARED1[(Shared<br/>Data)]
G2A[Goroutine 2<br/>Write] -->|direct access| SHARED1
SHARED1 -.->|Race Condition| CRASH[💥 Data Race<br/>Undefined Behavior]
style SHARED1 fill:#ffcdd2
style CRASH fill:#f44336,color:#fff
end
subgraph "✅ SAFE: Value Semantics with Deep Copy"
direction TB
G1B[Goroutine 1] -->|DeepCopy| SHARED2[(Shared<br/>Data<br/>+RWMutex)]
SHARED2 -->|independent copy| COPY1[Local<br/>Copy 1]
G2B[Goroutine 2] -->|DeepCopy| SHARED2
SHARED2 -->|independent copy| COPY2[Local<br/>Copy 2]
COPY1 & COPY2 -.->|No Sharing| SAFE[✅ Thread Safe<br/>No Data Races]
style SHARED2 fill:#c8e6c9
style COPY1 fill:#e8f5e9
style COPY2 fill:#e8f5e9
style SAFE fill:#4caf50,color:#fff
end
Key Differences:
- Unsafe: Direct access to shared mutable state causes race conditions
- Safe: Deep copy creates independent instances, preventing data races
- Trade-off: Safety vs. memory efficiency (copies allocate more memory)
- Starmap Choice: Safety first with optimizations (e.g., single copy in App.Catalog)
Collections use RWMutex for concurrent access:
type ProviderCollection struct {
mu sync.RWMutex
providers map[ProviderID]Provider
}
func (c *ProviderCollection) Get(id ProviderID) (Provider, error) {
c.mu.RLock()
defer c.mu.RUnlock()
p, exists := c.providers[id]
if !exists {
return Provider{}, &errors.NotFoundError{...}
}
return p.DeepCopy(), nil // Return copy
}
func (c *ProviderCollection) Set(provider Provider) {
c.mu.Lock()
defer c.mu.Unlock()
c.providers[provider.ID] = provider.DeepCopy()
}Budget scope: BenchmarkClientCatalog measures only loading the current
embedded immutable catalog through Client.Catalog. It does not include
materializing provider/model lists, filtering, serialization, or network I/O.
The executable fast-path budget is zero allocations per call and no more than
10 microseconds per call. The latency ceiling intentionally has broad CI
headroom while still rejecting the former millisecond-scale full-catalog copy.
scripts/verify-catalog-performance.sh enforces both limits across three runs;
race tests remain a separate gate because race instrumentation distorts
allocation measurements.
On 2026-07-09, darwin/arm64 on an Apple M2 Max with Go 1.25.1 measured:
BenchmarkClientCatalog-12 11.09-11.32 ns/op 0 B/op 0 allocs/op
Bytes-per-operation in Go benchmark output can reflect amortized harness or initialization effects even when allocations round to zero. Reproduce with:
go test . -run '^$' -bench BenchmarkClientCatalog -benchmem -count=3Race Detector:
# Run all tests with race detector
go test -race ./...
# Run specific package
go test -race ./pkg/catalogs -v
# Benchmark with race detection
go test -race -bench=. ./pkg/catalogsConcurrent Test Pattern:
func TestConcurrentCatalogAccess(t *testing.T) {
catalog := catalogs.Empty()
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
models := catalog.Models().List()
// Use models...
}()
}
wg.Wait()
}The codebase has been fully migrated to value semantics:
Completed Changes:
- ✅ Collections return values instead of pointers
- ✅ Client interfaces return
[]Modelnot[]*Model - ✅ Filters work with value types
- ✅ Deep copy helpers for all types
- ✅ Double-checked locking for singletons
- ✅ Removed redundant double-copy in App.Catalog()
Performance Improvements:
- Catalog generation reads perform no full-catalog copy
- The fast path is guarded at zero allocations with a 10 microsecond ceiling
- Collection materialization retains caller-owned copies and is outside this budget
For event-driven systems using channels (event brokers, WebSocket hubs, SSE broadcasters), ALWAYS buffer channels used for registration/unregistration:
// ❌ WRONG: Unbuffered channels cause initialization deadlocks
type Broker struct {
register chan Subscriber // Blocks if Run() not started
unregister chan Subscriber // Blocks during cleanup
}
// ✅ CORRECT: Buffered channels prevent blocking
type Broker struct {
register chan Subscriber, 10 // Buffer for setup phase
unregister chan Subscriber, 10 // Buffer for cleanup phase
}Why buffering is critical:
- Initialization Order Independence: Components can be initialized and subscribed before event loops start
- No Deadlocks:
Subscribe()doesn't block waiting forRun()to read from channel - Graceful Cleanup: Unregister operations during shutdown don't block
Buffer sizing guidelines:
- Registration channels: Size based on typical number of subscribers registered during initialization (commonly 5-10)
- Unregistration channels: Same size as registration channels
- Event channels: Size based on burst capacity (commonly 256+ for high-throughput systems)
Real-world example from internal/server/events/broker.go:
func NewBroker(logger *zerolog.Logger) *Broker {
return &Broker{
subscribers: make([]Subscriber, 0),
events: make(chan Event, 256), // High-capacity event buffer
register: make(chan Subscriber, 10), // Prevents blocking during setup
unregister: make(chan Subscriber, 10), // Prevents blocking during shutdown
logger: logger,
}
}Testing for initialization order bugs:
Always write tests that verify subscriptions work before Run() starts:
func TestBroker_SubscribeBeforeRun(t *testing.T) {
b := NewBroker(logger)
// Subscribe BEFORE starting Run() - should NOT block
done := make(chan struct{})
go func() {
sub := newSubscriber()
b.Subscribe(sub) // Would deadlock with unbuffered channels
close(done)
}()
select {
case <-done:
// Success
case <-time.After(2 * time.Second):
t.Fatal("Deadlock detected - channels not buffered!")
}
}See internal/server/events/broker_test.go:TestBroker_SubscribeBeforeRun for a complete example.
When adding new code, ensure:
- Collections return values, not pointers
- Public methods that access shared state use locks
- Deep copy methods handle all pointer fields
- Tests include
-racedetector runs - Singletons use double-checked locking
- No direct pointer returns from getters
- Event-driven channels are buffered (registration/unregistration channels especially)
- Initialization order tests verify Subscribe/Register work before Run()
starmap/
├── cmd/
│ └── starmap/ # CLI binary
│ ├── main.go # Entry point
│ ├── app/ # App implementation
│ │ ├── app.go # App struct and methods
│ │ ├── config.go # Configuration loading
│ │ ├── logger.go # Logger setup
│ │ ├── context.go # Signal handling
│ │ └── execute.go # Command registration
│ └── cmd/ # Command implementations
│ ├── list/ # List command
│ ├── update/ # Update command
│ ├── serve/ # API server command
│ └── ... # Other commands
│
├── pkg/ # Public packages
│ ├── catalogs/ # Catalog domain, builder, and immutable reads
│ ├── catalogstore/ # Generation commit/read/CAS adapters
│ ├── reconciler/ # Multi-source reconciliation
│ ├── authority/ # Field-level authority system
│ ├── sources/ # Source interfaces
│ ├── sync/ # Sync options and results
│ ├── errors/ # Typed errors
│ ├── logging/ # Logging utilities
│ ├── constants/ # Application constants
│ └── convert/ # Format conversion
│
├── internal/ # Internal packages
│ ├── application/ # Application interface used by CLI and server
│ ├── cli/ # CLI support helpers
│ │ ├── format/ # Output formatting
│ │ ├── table/ # Table rendering
│ │ ├── globals/ # Shared flag utilities
│ │ └── ... # Command support packages
│ ├── catalog/
│ │ ├── query/ # Shared CLI/HTTP catalog query behavior
│ │ └── pipeline/ # Sync orchestration behind Client.Sync
│ ├── providers/ # Provider API clients and registry
│ │ ├── clients/ # Provider client registry and raw fetch
│ │ ├── openai/ # OpenAI-compatible client
│ │ ├── anthropic/ # Anthropic client
│ │ ├── google/ # Google AI Studio and Vertex client
│ │ └── ... # Provider-specific test wrappers
│ ├── embedded/ # Embedded catalog data
│ │ ├── catalog/ # Embedded YAML files
│ │ └── openapi/ # OpenAPI 3.1 specs (JSON/YAML)
│ ├── server/ # HTTP server implementation
│ │ ├── server.go # Server struct & lifecycle
│ │ ├── config.go # Configuration management
│ │ ├── router.go # Route registration & middleware
│ │ ├── events/ # Shared event fan-out and broker
│ │ ├── sse/ # Server-Sent Events adapter
│ │ ├── websocket/ # WebSocket adapter
│ │ └── handlers/ # HTTP request handlers
│ │ ├── models.go # Model endpoints
│ │ ├── providers.go # Provider endpoints
│ │ ├── admin.go # Admin operations
│ │ ├── health.go # Health checks
│ │ ├── realtime.go # WebSocket/SSE
│ │ └── openapi.go # OpenAPI spec endpoints
│ ├── sources/ # Source implementations
│ │ ├── providers/ # Provider-backed catalog source
│ │ ├── modelsdev/ # models.dev integration
│ │ └── local/ # Local file source
│ ├── attribution/ # Model author attribution and matcher
│ └── transport/ # HTTP client utilities
│
├── client.go # Client implementation
├── sync.go # Public sync adapter and persistence apply hook
├── hooks.go # Event hooks
├── autoupdate.go # Auto-updates
├── options.go # Functional options
└── persistence.go # Save/load operations
Dependency Flow (Unidirectional):
graph BT
subgraph "Layer 6: Implementations"
INT[internal/*<br/>Embedded, Providers, models.dev]
end
subgraph "Layer 5: Core Packages"
PKG[pkg/*<br/>catalogs, catalogstore, reconciler, sources, authority]
end
subgraph "Layer 4: Root Package"
ROOT[starmap<br/>Concrete Client API]
end
subgraph "Layer 3: App Implementation"
APPIMPL[cmd/starmap/app/<br/>App struct implements Application]
end
subgraph "Layer 2: Commands"
CMDS[cmd/starmap/cmd/*<br/>list, update, serve commands]
end
subgraph "Layer 1: Application Interface"
APPIF[internal/application/<br/>Application interface]
end
INT --> PKG
PKG --> ROOT
ROOT --> APPIMPL
APPIMPL -.implements.-> APPIF
CMDS --> APPIF
style APPIF fill:#e3f2fd
style CMDS fill:#fff3e0
style APPIMPL fill:#f3e5f5
style ROOT fill:#e8f5e9
style PKG fill:#fff9c4
style INT fill:#fce4ec
Architecture Benefits:
- Clean Separation: Each layer has clear responsibilities
- Testability: Commands depend on interfaces, easily mocked
- Flexibility: Implementation can change without affecting commands
- No Cycles: Go enforces unidirectional dependencies
Rules:
- Never import from higher layers
- Commands import
internal/application/interface, notcmd/starmap/app/ - Root package imports pkg packages
- Internal packages can import pkg packages
- Pkg packages are fully independent
The primary deterministic verification gate is:
make verifyThis runs full tests, race-short tests, vet, lint when available, generated-doc checks, whitespace checks, local CLI smoke checks, and critical seam coverage thresholds. See TESTING.md for the maintained verification contract and the current module thresholds.
Use global coverage as an orientation metric only. Production trust comes from coverage at the interfaces where correctness concentrates: catalog ownership, sync pipeline, provider source and client registry, query/params translation, authority and reconciliation, transport, and event fan-out.
Package-Level Tests:
// pkg/catalogs/catalog_test.go
func TestCatalogOperations(t *testing.T) {
catalog := catalogs.Empty()
// Test adding models
err := catalog.SetModel(model)
assert.NoError(t, err)
// Test retrieval
retrieved, err := catalog.Model(model.ID)
assert.NoError(t, err)
assert.Equal(t, model.Name, retrieved.Name)
}Command Tests with Mocks:
func TestListCommand(t *testing.T) {
// Create mock application
mock := &mockApp{
catalog: testCatalog,
logger: testLogger,
}
// Create command with mock
cmd := list.NewCommand(mock)
// Execute and verify
err := cmd.Execute()
assert.NoError(t, err)
}Full Pipeline Tests:
# Tag integration tests
go test -tags=integration ./...
# Run integration tests for specific package
go test -tags=integration ./pkg/reconciler -vExample Integration Test:
//go:build integration
func TestFullSyncPipeline(t *testing.T) {
// Create real starmap with embedded catalog
sm, _ := starmap.New()
// Perform actual sync
result, err := sm.Sync(context.Background(),
sync.WithProvider("openai"),
sync.WithDryRun(true),
)
assert.NoError(t, err)
assert.NotNil(t, result)
}Always test with race detector:
# All tests with race detector
go test -race ./...
# Specific package with race detector
go test -race ./pkg/catalogs -v
# Benchmarks with race detector
go test -race -bench=. ./pkg/catalogs# Generate coverage report
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
# Coverage for specific package
go test -coverprofile=coverage.out ./pkg/catalogs
go tool cover -func=coverage.outProvider API responses are captured as testdata:
# Update testdata for all providers
make testdata
# Update specific provider
make testdata PROVIDER=openai
# Or directly
go test ./internal/providers/openai -updateTestdata Pattern:
var updateFlag = flag.Bool("update", false, "update testdata files")
func TestListModels(t *testing.T) {
if *updateFlag {
// Fetch from real API and save
models, _ := client.ListModels(ctx)
saveTestdata(models)
} else {
// Load from testdata
models := loadTestdata()
// Test with loaded data
}
}| File | Purpose | Lines |
|---|---|---|
client.go |
Concrete public Client API and immutable catalog publication | ~150 |
sync.go |
Public sync adapter and persistence apply hook | ~120 |
internal/catalog/pipeline/pipeline.go |
13-stage catalog sync pipeline | ~150 |
internal/application/application.go |
Application interface | ~97 |
cmd/starmap/app/app.go |
App implementation | ~200 |
pkg/reconciler/reconciler.go |
Reconciliation engine | ~300 |
pkg/authority/authority.go |
Field-level authorities | ~210 |
- pkg/catalogs/README.md - Catalog storage
- pkg/reconciler/README.md - Multi-source reconciliation
- pkg/sources/README.md - Data source abstractions
- pkg/authority/ - Field-level authority system
- pkg/errors/README.md - Error types
- pkg/logging/README.md - Logging utilities
- CLAUDE.md - LLM coding assistant instructions
- README.md - User-facing documentation
- CHANGELOG.md - Version history
Architecture Status: ✅ Production-ready, fully implemented
This architecture has been battle-tested and optimized for:
- Thread safety with race detector validation
- Zero import cycles
- Comprehensive test coverage
- Production use with real provider APIs