Relationships
Implemented by 1
Informs 1
Branches 2
Context
The pipeline already registers content in the entity registry — core registers every page as a page entity and every heading as a heading (packages/runes/src/config.ts), and the plan plugin registers its domain entities. collection and aggregate (SPEC-070) query the registry through the shared field-match grammar (packages/runes/src/field-match.ts), which already handles arrays, globs, and regex.
But you can't yet aggregate over your own content by arbitrary metadata. Two gaps:
- Only a fixed frontmatter subset is indexed. The
pageentity'sdatacarriestitle,url,parentUrl,draft,description,date,order,icon— nothing else. Sofilter="tags:x"orgroup="category"over pages match nothing, even though the grammar would handle them. - A content page can't declare itself a typed entity. Everything authored as a page is a
page; there's no way to say "this page is a rune / a product / a recipe" socollection type="rune"can find it.
The motivating want: a complete, queryable rune catalogue generated from the docs (and "N runes across M plugins" stats), plus tag-driven page collections — all dogfooding the registry on refrakt's own site.
The obvious shortcut — a build-time hook that registers each rune from the compiled code catalogue — was considered and rejected (see Options).
Decision
Adopt frontmatter-declared registry entities, in two composable layers plus a config convenience:
Index page frontmatter into the
pageentity. Beyond the current fixed subset, merge the page's frontmatter into thepageentity'sdata(excluding a reserved set of layout-control keys —layout,tint-mode,tint-lock, region/frontmatter plumbing — so they don't pollute queries). Any remaining field (tags,category, …) becomes filterable/groupable through the existing grammar with no resolver changes. This alone enables tag-driven collections.A page may declare a registry
type(and optionalid) in frontmatter. Such a page registers as a first-class entity of that type — in addition to itspageregistration — with its (reserved-filtered) frontmatter asdata,iddefaulting to the page URL. Thencollection type="rune"/aggregate type="rune" group="plugin"read semantically. This is the general feature:type: product,type: recipe,type: member— whatever a project models. It is the complement ofentityRoutes(SPEC-069), which maps entity types → page URLs; this maps pages → entities.A config url-pattern → type rule (mirroring
routeRules/entityRoutes) so the type discriminator can be set by convention, not per-page boilerplate: e.g. pages under/runes/**areruneentities. Per-page frontmatter then carries only the metadata (category,plugin,status), nottype:repeated a hundred times.Open-world is the deciding property. Because the catalogue is assembled from pages that self-declare, a third-party plugin's documented runes join automatically — no hook to extend, no PR to refrakt. The knowledge sits with the page (the party that has it), pointing outward, exactly as the composability contract prescribes.
Drift guardrail. Frontmatter can drift from code (add a rune, forget the page → missing from the catalogue). That is arguably the right semantics (the catalogue = documented entities), and it is checkable: a
refrakt inspect/ test assertion that everydefineRune/ plugin rune has a corresponding documented page turns drift into a build signal. Auto-correctness and open-world. (Refrakt-specific use of a general capability.)
Rationale
- Open-world beats closed-world, and it is the house style — the same reason the composability contract has no central nesting registry. A catalogue that third parties can join without our involvement is strictly better than one our build must enumerate.
- Reuse over invention. The query grammar already does the filtering/grouping (including arrays); the only gaps are indexing and a declaration affordance.
- General, not a private path. The same mechanism serves any project's content types — the rune catalogue is merely the dogfood showcase.
- Composes with existing config. It is the inverse of
entityRoutesand sits naturally alongsiderouteRulesas a url-pattern rule.
Consequences
- A companion spec, SPEC-092, details the frontmatter contract (the
type/idkeys, the reserved-key exclusion list, metadata passthrough), thepage-entity enrichment, the config url-pattern→type rule,idderivation, precedence with the always-presentpageregistration, and the drift check — with the rune catalogue as its worked showcase. - Target: next minor (post-v0.20.1). Explicitly not in v0.20.1, which stays a docs/showcase patch — this is a pipeline feature and would be scope creep there.
rune-catalog.mdand the index "N runes" stat can become generated viacollection/aggregateonce rune pages declare their metadata.- Builds on SPEC-070 (the query grammar) and complements SPEC-069 (entityRoutes); touches the core register phase and the config schema.
- The reserved-key exclusion is the main subtlety — getting it wrong pollutes every page query with layout plumbing, so the spec must pin the list.