catalog

package
v0.18.0 Latest Latest
Warning

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

Go to latest
Published: Aug 20, 2026 License: MIT Imports: 19 Imported by: 0

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

View Source
const (
	CapLeafValue = 5000
	CapPageTitle = 200
	CapNavLabel  = 80
	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.

View Source
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).

View Source
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

func ApplicationID(hubID, hubTemplateID string) string

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

func CacheDirForOrigin(origin string) string

CacheDirForOrigin is CacheDirUnder over the default OS cache dir.

func CacheDirUnder added in v0.12.0

func CacheDirUnder(base, origin string) string

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

func CanonicalJSON(v any) ([]byte, error)

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

func CreateApplicationID(teamID, hubTemplateID, name, slug string) string

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

func Digest(cat Node) (string, error)

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

func HubPolicyFieldKey(f string) bool

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

func InterpolateNavigation(nav map[string]any, hubName, hubSlug string) error

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

func InterpolateTitle(title, hubName, hubSlug string) (string, error)

InterpolateTitle interpolates a page title (§4.3 allowed location (b)), capped at CapPageTitle code points.

func InterpolateTreeValues added in v0.12.0

func InterpolateTreeValues(node Node, hubName, hubSlug string) error

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

func NormalizeIDs(node any) any

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

func PlaylistDataSourceKeys(node Node) []string

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

func Resolve(ctx context.Context, opts ResolveOptions) (*Catalog, Source, error)

Resolve loads the active catalog per the precedence documented above and reports which source was used.

func TreeDigest added in v0.12.0

func TreeDigest(tree map[string]any) (string, error)

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

func Load() (*Catalog, error)

Load parses the embedded, digest-pinned vendored catalog. Use Parse to load a live-fetched or overridden catalog body.

func Parse

func Parse(data []byte) (*Catalog, error)

Parse decodes a raw catalog.json body into a Catalog, preserving numeric literals (UseNumber) so Digest matches the cross-repo canonicalizer.

func (*Catalog) DigestPinned

func (c *Catalog) DigestPinned() string

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

func (c *Catalog) HubTemplateIDs() []string

HubTemplateIDs returns every hub template id, sorted (stable help output).

func (*Catalog) IsWritableSectionType

func (c *Catalog) IsWritableSectionType(id string) bool

IsWritableSectionType reports whether id is an allowed imperative-door type.

func (*Catalog) PageTemplateForType

func (c *Catalog) PageTemplateForType(pageType string) (Template, bool)

PageTemplateForType returns the page template whose pageType matches, if any.

func (*Catalog) Raw

func (c *Catalog) Raw() Node

Raw returns the full parsed catalog (for Digest verification).

func (*Catalog) RecommendedTemplates

func (c *Catalog) RecommendedTemplates(pageType string) []Template

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

func (c *Catalog) TemplateByID(id string) (Template, bool)

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

func (c *Catalog) TemplateIDs() []string

TemplateIDs returns every scaffoldable template id (section + page), sorted.

func (*Catalog) WritableSectionTypes

func (c *Catalog) WritableSectionTypes() []string

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

type FetchResult struct {
	Body        []byte
	ETag        string
	NotModified bool
}

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
	Branding, Navigation, Settings, Policies map[string]any
	Spaces                                   []TemplateSpace
	Onboarding                               []TemplateAttrDef
	Playlists                                []TemplatePlaylist
	Pages                                    []PageRef
	// WelcomePost is nil when the template declares no `welcomePost` — which is
	// the case for every catalog shipped so far, including `community` at catalog
	// 0.14.1 (its keys are branding/id/label/lifecycle/navigation/onboarding/
	// pages/playlists/policies/settings/spaces). The scaffold step is then a
	// clean no-op: the CLI holds no templates and must not invent post copy.
	WelcomePost *TemplateWelcomePost
}

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

type IDGen func() (string, error)

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

type Node = map[string]any

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

func CloneNode(n Node) Node

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

func CloneWithFreshIDs(node Node, gen IDGen) (Node, error)

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

func InstantiateTemplate(t Template, variant string, gen IDGen) (Node, error)

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

type PageRef struct {
	Role, PageTemplate, Slug, Title, Privacy string
	IsHomepage                               bool
}

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 Source

type Source string

Source identifies which catalog a Resolve call ended up using.

const (
	SourceOverride Source = "override" // --catalog <file>
	SourceLive     Source = "live"     // fresh HTTP 200, digest-verified
	SourceCache    Source = "cache"    // last-good on-disk copy (304 or fetch failure)
	SourceVendored Source = "vendored" // embedded digest-pinned fallback
)

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

func (t Template) VariantKeys() []string

VariantKeys returns a template's variant keys, sorted (stable help output).

type TemplateAttrDef added in v0.12.0

type TemplateAttrDef struct {
	Name, Slug, FieldType  string
	InOnboarding, Required bool
}

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.

Jump to

Keyboard shortcuts

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