Documentation
¶
Overview ¶
Package catalog is the CLI's consumer of the page-builder catalog (mio-page-catalog — the cross-repo source of truth for the page-builder vocabulary: author templates, compiled section types, page types, and each template's declarative starter recipe). It carries a digest-pinned VENDORED copy of catalog.json (embedded below) as the offline/air-gapped fallback, a Go port of the reference applier (applier.go) that scaffolds real node-trees from template recipes, and typed accessors the CLI commands use instead of hardcoded lists: the writable section-type allow-list (imperative door), template-id + variant validation (tree door), and recommended templates per page type.
Live fetching of the freshest catalog over HTTP (with the vendored copy as the fail-safe) lives in the client layer; this package owns the vendored artifact, the applier, and the parse/lookup logic that runs over either source.
Index ¶
- Constants
- func ApplicationID(hubID, hubTemplateID string) string
- func BindPlaylistDataSources(node Node, idsByKey map[string]string) (bound int, unresolved []string)
- func CacheDirForOrigin(origin string) string
- func CacheDirUnder(base, origin string) string
- func CanonicalJSON(v any) ([]byte, error)
- func CreateApplicationID(teamID, hubTemplateID, name, slug string) string
- func DefaultCacheDir() string
- func Digest(cat Node) (string, error)
- func HubPolicyFieldKey(f string) bool
- func HubPolicyFieldKeys() []string
- func InterpolateNavigation(nav map[string]any, hubName, hubSlug string) error
- func InterpolateTitle(title, hubName, hubSlug string) (string, error)
- func InterpolateTreeValues(node Node, hubName, hubSlug string) error
- func NormalizeIDs(node any) any
- func PinnedRef() string
- func PlaylistDataSourceKeys(node Node) []string
- func Resolve(ctx context.Context, opts ResolveOptions) (*Catalog, Source, error)
- func TreeDigest(tree map[string]any) (string, error)
- type Catalog
- func (c *Catalog) DigestPinned() string
- func (c *Catalog) HubTemplateByID(id string) (HubTemplate, bool)
- func (c *Catalog) HubTemplateIDs() []string
- func (c *Catalog) IsWritableSectionType(id string) bool
- func (c *Catalog) PageTemplateForType(pageType string) (Template, bool)
- func (c *Catalog) Raw() Node
- func (c *Catalog) RecommendedTemplates(pageType string) []Template
- func (c *Catalog) SectionType(id string) (SectionType, bool)
- func (c *Catalog) TemplateByID(id string) (Template, bool)
- func (c *Catalog) TemplateIDs() []string
- func (c *Catalog) WritableSectionTypes() []string
- type FetchResult
- type Fetcher
- type HubTemplate
- type IDGen
- type InterpolationError
- type Meta
- type Node
- type PageRef
- type Recommendation
- type ResolveOptions
- type SectionType
- type Source
- type Template
- type TemplateAttrDef
- type TemplateDocument
- type TemplatePlaylist
- type TemplateSpace
- type TemplateWelcomePost
Constants ¶
const ( CapLeafValue = 5000 CapPageTitle = 200 MaxHubNameCP = 255 )
Post-substitution caps in Unicode code points (§4.3), plus the --name preflight bound for a hub name itself: the hub title DB column is VARCHAR(255), so the CLI preflights MaxHubNameCP and a bad --name fails before any write.
const ( CodeUnknownToken = "UNKNOWN_TOKEN" CodeValueTooLong = "VALUE_TOO_LONG" CodeTitleTooLong = "TITLE_TOO_LONG" CodeLabelTooLong = "LABEL_TOO_LONG" )
Machine-readable error codes — the exact strings shared with the TS and Python implementations (corpus vocabulary).
const DiscussionTitleMaxCP = 280
DiscussionTitleMaxCP mirrors mio-backend's DISCUSSION_TITLE_MAX_LENGTH (app/community/discussion_text.py) — the cap the create endpoint enforces twice over, via Field(max_length=280) and normalize_discussion_title. Both count CODE POINTS (Python len() over a str), never bytes.
The backend applies it to the RAW value because that is what it receives; HubTemplate.Validate applies it to the STRIPPED one because that is what the scaffold sends. See the reject conditions there for why the two differ.
Variables ¶
This section is empty.
Functions ¶
func ApplicationID ¶ added in v0.12.0
ApplicationID is the deterministic provenance id shared with the backend op: sha256hex(hub_id + "\x1f" + hub_template_id). A re-run recomputes it without any server-side record and can locate this application's pages.
func BindPlaylistDataSources ¶ added in v0.17.0
func BindPlaylistDataSources(node Node, idsByKey map[string]string) (bound int, unresolved []string)
BindPlaylistDataSources writes the created playlist ids into an instantiated tree IN PLACE: for every playlist dataSource carrying a `key`, `id` becomes idsByKey[key]. It returns how many nodes it bound and the keys it could not resolve (first-seen order, deduplicated).
An unresolved key leaves the node exactly as the catalog shipped it — id "" — rather than deleting the dataSource or the node: the tree is a catalog artifact this code is filling in, not authoring, and a half-removed binding is harder to diagnose than an unfilled one. The caller reports the unresolved keys; this function never errors, because "no playlist for this key" is a runtime condition (the playlists step skipped on a hub that already had playlists), not a malformed tree — the malformed-tree case is caught write-free by Validate.
func CacheDirForOrigin ¶ added in v0.12.0
CacheDirForOrigin is CacheDirUnder over the default OS cache dir.
func CacheDirUnder ¶ added in v0.12.0
CacheDirUnder returns base scoped to a backend origin, so a cache populated from one origin is never validated/read against another. Empty origin or base falls back to base unchanged (legacy unscoped layout). The segment is host[:port] reduced to filesystem-safe characters. cmd wires base from MIO_CATALOG_CACHE_DIR / DefaultCacheDir (a later task).
func CanonicalJSON ¶
CanonicalJSON returns deterministic JSON byte-identical to the cross-repo TS canonicalizer (canonical.ts: JSON.stringify(sortKeys(value))): object keys recursively sorted by UTF-16 code-unit order (matching JS String sort), arrays in order, numbers emitted verbatim (json.Number), and strings escaped exactly as JS JSON.stringify. Matching TS byte-for-byte is what lets the Go digest equal the digest shipped in the catalog's meta.digest / HTTP ETag.
Go's encoding/json is NOT usable here: it escapes U+2028/U+2029 (and, unless disabled, <>&) where JS emits them raw, and it sorts map keys by UTF-8 bytes rather than UTF-16 code units — both would diverge from the TS digest for a catalog containing those characters.
func CreateApplicationID ¶ added in v0.16.0
CreateApplicationID is ApplicationID's CREATE-mode counterpart (MIO-2976): the deterministic Idempotency-Key for the whole-hub op, which runs before any hub id exists and so cannot key off one.
It covers exactly the identity the operator typed — team, template, name, slug — so re-running the SAME command converges on the backend's stored application instead of creating a second hub (MIO-2565), while two different hubs from one template stay distinct keys. Both name and slug are folded in, not slug alone: the backend puts both in its request fingerprint, so a key that ignored name would turn `--name Other --slug same` into a fingerprint mismatch (409) rather than the distinct application it is.
Note what is deliberately NOT in the key but IS in the backend's fingerprint: the catalog digest and the overrides. A re-run after the backend's catalog pin moves, or with a different --publish, therefore reuses this key with a changed body and gets 409 `idempotency_fingerprint_mismatch` — refusing to half-apply a different request under an old key. That is the safe direction, and the caller turns it into actionable guidance rather than retrying.
func DefaultCacheDir ¶
func DefaultCacheDir() string
DefaultCacheDir returns the per-user catalog cache directory, or "" if the OS cache dir cannot be determined (caching then simply degrades to off).
func Digest ¶
Digest computes "sha256:<hex>" over the canonical catalog with meta.digest removed (charter §5.2.1). Ports mio-page-catalog src/canonical.ts.
func HubPolicyFieldKey ¶ added in v0.14.0
HubPolicyFieldKey reports whether f is an accepted field inside a hubTemplate policies value. Exported so the CONSUMER (cmd templateHubPolicy) enforces THIS allow-list rather than keeping a second copy of it — two lists that must be edited together are two lists that will eventually disagree.
func HubPolicyFieldKeys ¶ added in v0.14.0
func HubPolicyFieldKeys() []string
HubPolicyFieldKeys returns the accepted field set for a hubTemplate policies value, sorted.
Exported for the CONSUMER-COVERAGE guard (MIO-2567): accepting a field at preflight only matters if something downstream ACTS on it, and "enabled" sat on this allow-list — shipped in the community template, waved through by Validate — while the scaffold's policy step read only content/ require_acceptance/required. The result was a hub whose ToS was written and whose gate was never switched on. The cmd-side guard drives a template declaring each key here through the real step and asserts the REQUESTS change: membership in a second hand-maintained list proves nothing, because adding the key to that list is also the cheapest way to make such a test go green while the drop survives.
func InterpolateNavigation ¶ added in v0.12.0
InterpolateNavigation interpolates, in place, the `label` of every header[]/footer[] item in a hub navigation blob (§4.3 allowed location (c)), capped at CapNavLabel code points. Nothing else on a nav item is scanned.
func InterpolateTitle ¶ added in v0.12.0
InterpolateTitle interpolates a page title (§4.3 allowed location (b)), capped at CapPageTitle code points.
func InterpolateTreeValues ¶ added in v0.12.0
InterpolateTreeValues walks an instantiated node tree in place, interpolating the string `value` of every headline/text/button node (§4.3 allowed location (a)), capped at CapLeafValue code points. No other node field is scanned — not settings, href, icon, or slug.
func NormalizeIDs ¶
NormalizeIDs returns a deep copy of node with every string node id replaced by a deterministic pre-order (DFS) placeholder (#0, #1, …) so two structurally identical trees compare equal regardless of the concrete UUIDv7 ids each applier minted. Ports mio-page-catalog src/normalize-ids.ts; used by the golden parity test. Only nodes that already carry a string id are renumbered (matches the reference), and the input is left untouched.
func PinnedRef ¶ added in v0.13.0
func PinnedRef() string
PinnedRef returns the upstream mio-page-catalog commit SHA that vendoredCatalogJSON was vendored from. The catalog-pin-staleness workflow compares this against mio-page-catalog's HEAD to decide whether to open a bump PR.
func PlaylistDataSourceKeys ¶ added in v0.17.0
PlaylistDataSourceKeys returns every distinct playlist-dataSource `key` declared in a node tree, in first-seen (depth-first) order. Read-only.
Used by HubTemplate.Validate to check, write-free, that every key a page template declares names a playlist the SAME hub template creates.
func Resolve ¶
Resolve loads the active catalog per the precedence documented above and reports which source was used.
func TreeDigest ¶ added in v0.12.0
TreeDigest returns "sha256:<hex>" over the canonical tree — the provenance marker's appliedTreeDigest (§5.1). Written and read back only by the CLI's client-side path in v1 (§10.9 cross-language digest reconciliation pending).
Types ¶
type Catalog ¶
type Catalog struct {
Meta Meta
PageTypes []string
SectionTypes []SectionType
Templates []Template // section templates (templates[])
PageTemplates []Template // page templates (pageTemplates[])
HubTemplates []HubTemplate // hub templates (hubTemplates[], schema ≥2.1; empty otherwise)
// contains filtered or unexported fields
}
Catalog is a parsed page-builder catalog.
func Load ¶
Load parses the embedded, digest-pinned vendored catalog. Use Parse to load a live-fetched or overridden catalog body.
func Parse ¶
Parse decodes a raw catalog.json body into a Catalog, preserving numeric literals (UseNumber) so Digest matches the cross-repo canonicalizer.
func (*Catalog) DigestPinned ¶
DigestPinned returns the vendored catalog's declared meta.digest.
func (*Catalog) HubTemplateByID ¶ added in v0.12.0
func (c *Catalog) HubTemplateByID(id string) (HubTemplate, bool)
HubTemplateByID returns the hub template with the given id, if any.
func (*Catalog) HubTemplateIDs ¶ added in v0.12.0
HubTemplateIDs returns every hub template id, sorted (stable help output).
func (*Catalog) IsWritableSectionType ¶
IsWritableSectionType reports whether id is an allowed imperative-door type.
func (*Catalog) PageTemplateForType ¶
PageTemplateForType returns the page template whose pageType matches, if any.
func (*Catalog) RecommendedTemplates ¶
RecommendedTemplates returns the section templates applicable to pageType (pageType ∈ applicablePageTypes), ordered by recommendation.order ascending.
func (*Catalog) SectionType ¶
func (c *Catalog) SectionType(id string) (SectionType, bool)
SectionType returns the section type with the given id, and whether it exists in the catalog. Used to distinguish a KNOWN non-writable type (reject) from an UNKNOWN type (defer to the backend) on the imperative door.
func (*Catalog) TemplateByID ¶
TemplateByID looks a template up by id across BOTH registries — section templates (tree/section vocabulary) and page templates. Page templates win ties only in theory; ids are unique across both per the catalog invariants. Resolves templateIDAliases first, so a caller may pass either the real id or a known alias.
func (*Catalog) TemplateIDs ¶
TemplateIDs returns every scaffoldable template id (section + page), sorted.
func (*Catalog) WritableSectionTypes ¶
WritableSectionTypes returns the sorted ids of section types that accept a direct imperative write (`sections create --type`) — the catalog-derived replacement for the hardcoded 9-type list.
type FetchResult ¶
FetchResult is the client-agnostic outcome of a raw catalog fetch. cmd adapts the client's CatalogResult into this so internal/catalog carries no dependency on the HTTP layer.
type Fetcher ¶
type Fetcher interface {
FetchCatalog(ctx context.Context, ifNoneMatch string) (FetchResult, error)
}
Fetcher fetches the raw catalog over HTTP, honoring If-None-Match.
type HubTemplate ¶ added in v0.12.0
type HubTemplate struct {
ID, Label, Lifecycle string
HubTemplate is one hubTemplates[] entry: a declarative full-experience hub definition. The four map[string]any blobs mirror the hub's untyped JSONB blobs; the typed slices carry the per-resource inputs each pipeline step consumes.
func (HubTemplate) HomepagePage ¶ added in v0.12.0
func (h HubTemplate) HomepagePage() *PageRef
HomepagePage returns a pointer to the pages[] entry marked isHomepage (into h.Pages), or nil if none is marked.
func (HubTemplate) Validate ¶ added in v0.12.0
func (h HubTemplate) Validate(c *Catalog) error
Validate enforces the hub-template invariants both scaffold apply paths depend on: pages non-empty with unique non-empty slugs (the backend reserves "home"), a valid privacy on every page, every pageTemplate resolving to a page template in c, and exactly one homepage; space/onboarding slugs unique and non-empty with enum-valid attributes; an optional welcomePost whose title clears the endpoint's own reject conditions (non-blank, NUL-free, and ≤280 code points measured on the STRIPPED title — what the scaffold actually posts), whose body is NUL-free, and whose space slug names one of this template's own spaces; every policies value an object whose fields are within hubPolicyFieldKeys (a typo must fail preflight, not silently reset policy content); playlist titles non-empty and keys unique and non-empty with enum-valid visibility, and every declared document carrying a title; and every playlist dataSource `key` a referenced page template declares naming a playlist THIS template creates. Slug/key uniqueness matters because each pipeline step snapshots existing server slugs ONCE and skip-if-exists against that snapshot — a duplicate would issue a duplicate create mid-pipeline.
type IDGen ¶
IDGen mints node ids. The error return exists because the production generator draws from crypto/rand, which can fail; deterministic test generators never error.
func NewUUIDv7Gen ¶
func NewUUIDv7Gen() IDGen
NewUUIDv7Gen returns an IDGen minting RFC 9562 (§5.7) UUIDv7 strings — a 48-bit Unix-millis timestamp, version nibble 7, variant bits 10, and 74 bits of cryptographic randomness. Uniqueness within a tree is guaranteed by the random bits (and enforced by reIDInPlace). mio-cli carries no uuid dependency, so this is implemented directly.
type InterpolationError ¶ added in v0.12.0
type InterpolationError struct {
Code string
// contains filtered or unexported fields
}
InterpolationError carries the machine code the cross-language corpus keys on alongside a human-readable message.
func (*InterpolationError) Error ¶ added in v0.12.0
func (e *InterpolationError) Error() string
type Meta ¶
type Meta struct {
SchemaVersion string
CatalogVersion string
Revision int
Digest string
CreatedAt string
}
Meta mirrors catalog.json's meta block (the fields the CLI surfaces).
type Node ¶
Node is a page-builder recipe/tree node. Unknown fields round-trip untouched; numbers are decoded as json.Number so canonicalization is byte-faithful to the TS reference.
func CloneNode ¶ added in v0.12.0
CloneNode returns a deep copy of a node tree (maps/slices/scalars). A nil input stays nil — Node is a map alias, so without the guard a typed nil map would match deepClone's map case and come back as an allocated EMPTY map, turning callers' "was there a blob at all?" nil checks always-true (the scaffold once PATCHed navigation:{} — a whole-blob wipe — because of this).
func CloneWithFreshIDs ¶
CloneWithFreshIDs deep-clones node (so mutating an instance never mutates the catalog recipe), then walks it once re-IDing every node in the children-tree. Ports mio-hub's cloneWithFreshIds.
func InstantiateTemplate ¶
InstantiateTemplate deep-clones a template into a fresh authored subtree, selecting variants[variant] when a matching key exists, else falling back to the base starter — a graceful fallback that never errors for a missing or omitted variant (matches mio-hub's applyTemplate(name, {variant}), MIO-2248). Every node gets a fresh id from gen.
type PageRef ¶ added in v0.12.0
PageRef is one hubTemplate pages[] entry: a page to instantiate from a page template. Slug/Title/Privacy feed the page create; IsHomepage marks the one entry the scaffold publishes as the hub's homepage.
type Recommendation ¶
type Recommendation struct {
Tier string // baseline | addition | forbidden
Order int // picker sort key (ascending)
}
Recommendation is a template's picker placement (charter §5.2.3).
type ResolveOptions ¶
type ResolveOptions struct {
Offline bool // force the vendored copy (no network, no cache)
OverrideFile string // --catalog: use this file exclusively
CacheDir string // on-disk cache dir ("" disables caching)
Fetcher Fetcher // live fetch source (nil skips live fetch)
Warnf func(format string, a ...any) // non-fatal diagnostics ("" → stderr; nil → silent)
// Mutating marks a resolve whose catalog must be CURRENT — one that will
// drive writes, or a live listing that must not silently degrade to a
// stale copy. It fails closed where a read-only resolve degrades: a
// digest-mismatched OverrideFile is rejected; a failed live fetch is an
// error (no stale-cache or vendored fallback); absent both a Fetcher and
// an OverrideFile it errors immediately. A 304-validated cache read is
// still allowed (the server confirmed it current).
Mutating bool
}
ResolveOptions configures catalog resolution.
type SectionType ¶
type SectionType struct {
ID string
Lifecycle string
AnonSafe bool
Writable bool
CompiledFrom []string
}
SectionType is a compiled section.type registry entry (charter §5.0). Writable marks the imperative-door (`sections create --type`) allow-list.
type Template ¶
type Template struct {
ID string
Label string
Category string // "section" for templates[]; "" for pageTemplates[]
Lifecycle string
CompiledSectionType string // section templates only
PageType string // page templates only
ApplicablePageTypes []string // section templates only
Recommendation *Recommendation
Starter Node
Variants map[string]Node
IsPage bool // true for pageTemplates[] entries
}
Template is a catalog author template — either a section template (Category "section", carries CompiledSectionType) or a page template (from pageTemplates[], carries PageType). Starter is the base recipe subtree; Variants are keyed alternative subtrees (data-source type or layout preset).
func (Template) VariantKeys ¶
VariantKeys returns a template's variant keys, sorted (stable help output).
type TemplateAttrDef ¶ added in v0.12.0
TemplateAttrDef is a contact-attribute definition, optionally surfaced in onboarding.
type TemplateDocument ¶ added in v0.17.0
type TemplateDocument struct{ Title, Description string }
TemplateDocument is one `playlists[].documents[]` entry: a placeholder text document the scaffold registers as a SYNTHETIC file (MIO-2285 — READY on creation, no upload/finalize/transcode) and attaches to the playlist.
The two fields ARE the vocabulary the catalog declares (its own pin asserts each entry has exactly title + description). They map onto the synthetic-file register's `title` and `description`; asset_kind is always "document".
type TemplatePlaylist ¶ added in v0.12.0
type TemplatePlaylist struct {
Title, Key, Visibility string
FileIDs []string
Documents []TemplateDocument
}
TemplatePlaylist is a media playlist published onto the hub (mirrors the retired internal/hubtemplate Playlist shape).
Visibility is the PLAYLIST's own visibility (public|unlisted|private) — see hubPlaylistVisibilityValues. The per-hub publication row's visibility is a different enum and is NOT template-expressible today; stepPlaylists documents the hardcode.
FileIDs and Documents are two ways to fill the same playlist and compose: FileIDs attaches media that already exists (a team-scoped id the template author knows), Documents CREATES placeholder text files first. A template that ships neither gets an empty playlist, which is a legitimate day-one "add your content here" state.
type TemplateSpace ¶ added in v0.12.0
type TemplateSpace struct{ Name, Slug, Description, AccessLevel, PostingPermission, Icon string }
TemplateSpace is a community discussion space to create.
Icon is a SPRITE NAME, not an emoji (MIO-2802): the value is whatever the hub frontend's icon registry accepts (mio-hub components/ui/icon.tsx ICON_NAMES), and it passes through to the space's `icon` attribute untouched. The CLI does not police the registry — that list lives in another repo and moves independently, so an unknown name is the API's rejection to make, not ours.
type TemplateWelcomePost ¶ added in v0.14.0
type TemplateWelcomePost struct {
Space, Title, Body string
// Published mirrors the endpoint's is_published, whose server-side default is
// TRUE. A template that omits the key therefore means "publish it" — hence the
// explicit default at parse time rather than the bool zero value, which would
// silently scaffold every welcome post as an invisible draft. A present but
// NON-BOOL value (null, "true", 1) is treated as absent for the same reason:
// coercing it through a comma-ok bool assertion yields false and lands exactly
// the invisible draft the default exists to prevent. Type-policing a
// schema-validated artifact belongs to the catalog's own validator, so this
// stays a tolerant parse (the package-wide convention) that fails SAFE.
Published bool
}
TemplateWelcomePost is the optional `welcomePost` block: the first discussion a scaffolded community lands in one of its own spaces (MIO-2558), authored via the admin welcome-post endpoint (MIO-2262). Space is a SLUG referencing this template's own spaces[] — the scaffold resolves it to the hub's real space id at apply time, so the manifest stays id-free and reusable.
The four fields ARE the endpoint's whole attribute set (its request schema is extra="forbid"); notably there is no author field, because the author is derived server-side from the caller's credentials.
NOT RATIFIED CATALOG VOCABULARY — MIO-2812 (read this before relying on the key name or the field spellings). `welcomePost` does not appear in mio-page-catalog's catalog.schema.json — $defs/hubTemplate declares no such property, nothing in that repo mentions it, and neither the TS reference applier nor the backend's W2b scaffold-from-template op implements it. It parses today only because $defs/hubTemplate is additionalProperties:true, i.e. the CLI defined this vocabulary unilaterally and the schema is not in a position to disagree. MIO-2812 asks the catalog owners to ratify it.
Both failure modes are SILENT, which is the whole reason this is worth a ticket rather than a TODO:
- if the ratified spec lands with a different name or shape (`welcome_post`, the block nested under spaces[], `body` renamed), this parser sees no `welcomePost` key, the step takes its no-declaration branch, and the post simply never appears — nothing errors, nothing warns;
- additionalProperties:true also means the schema cannot catch a TYPO, so a template author who writes `welcomepost` gets neither a validation error from the catalog nor a post from the scaffold.
The one mitigation available here is that the no-declaration branch records a plan-VISIBLE entry ("no welcome post in template") rather than skipping silently, so `--dry-run` at least shows an operator that the step ran and found nothing to do. When MIO-2812 lands, BOTH this parser and stepWelcomePost move with it.
Title/Body are LITERAL: {{hub_name}}/{{hub_slug}} interpolation is a closed contract whose scanned locations are exhaustively specified (MIO-2573 §4.3: leaf values on headline/text/button nodes, page titles, nav labels — "nothing else"), and it is implemented three times over in Go/TS/Python against a shared corpus. A welcome post is not one of those locations, so a token here would be stored verbatim — exactly like the equally literal spaces[].name, playlists[].title and policies[].content. Widening §4.3 is a catalog-spec change, not something the CLI may do unilaterally — which is why "should title/body join the §4.3 set?" is an open question ON MIO-2812 with this reasoning recorded as the CLI's position, not a decision taken here. If the catalog owners say yes, interpolating them is a follow-up on this repo.