WORK-611
ID:WORK-611Status:done

Add the registers declaration and its core registration hook

SPEC-144's mechanism. A rune declares that it registers a named entity or an edge, and one core pipeline participant performs the registration in the existing Phase 2 (register) / Phase 3 (aggregate) slots. The registry, the relationship graph and the query runes (collection, relationships, aggregate) are unchanged. This only gives them a declarative way in.

Priority:highComplexity:complexMilestone:v0.39.0Source:SPEC-144

Criteria completion

Criteria completion: 9 of 9 (100%) checked; history from Oct 7 to Oct 70%25%50%75%100%Oct 7Oct 7
Branches 4
History 6
  1. cc9aaad
    • ☑ A rune can declare `registers.entity` with `type`, `idFrom`, `scope`, `data` and `aliases`
    • ☑ A rune can declare `registers.edge` with `from`, `to` and `kind`
    • ☑ The declaration contains no function values and round-trips through `JSON.parse(JSON.stringify(…))`
    • ☑ One core hook performs the registration for every rune carrying the block, in the existing Phase 2 / Phase 3 slots, beside — not replacing — `PluginPipelineHooks`
    • ☑ A declaration naming a field that no emitted node, field-bag entry or attribute provides is reported at validate time, with file and line — the same treatment a schema-table source already gets
    • ☑ `refrakt inspect` shows a rune's registration declaration
    • ☑ The generated reference documents `registers` for every rune that carries one
    • ☑ The plan plugin's pipeline is unchanged, and the authoring guide states SPEC-144's reach table — storytelling and design are reachable, plan is not, and why (D5)
    • ☑ D3 (channel explicitness) is decided and the decision recorded in the resolution
    by bjornolofandersson
  2. 49df934
    Content editedby Claude
    docs(plan): close WORK-611/612/613 with resolutions and PR
  3. 39aa947
    Content editedby Claude
    docs(plan): WORK-611/612/613 criteria checked
  4. 4867d2c
    Content editedby Claude
    test(storytelling,design): capture the registry snapshot before the regi
  5. 50daca8
    Created (ready)by bjornolofandersson
  6. 1046f41
    Content editedby Claude
    docs(plan): add v0.39.0 milestone, Declarations part two

Scope: registration only

Declarable bounded sentinel resolution (SPEC-144's revised D2, and its two corresponding criteria) is not in this item or this milestone. Registration is the independently valuable half, and the sentinel half migrates five core postProcess instances, including buildAutoBreadcrumb, which BUG-018 is changing in the same milestone.

Open question to settle here

SPEC-144 D3, channel explicitness: whether idFrom: name must say ref: / field:, or the implicit ref-then-bag fallback stands. The spec says to decide with a migration in hand. WORK-612 is that migration, so record the decision in this item's resolution, informed by it.

Acceptance Criteria

  • A rune can declare registers.entity with type, idFrom, scope, data and aliases
  • A rune can declare registers.edge with from, to and kind
  • The declaration contains no function values and round-trips through JSON.parse(JSON.stringify(…))
  • One core hook performs the registration for every rune carrying the block, in the existing Phase 2 / Phase 3 slots, beside — not replacing — PluginPipelineHooks
  • A declaration naming a field that no emitted node, field-bag entry or attribute provides is reported at validate time, with file and line — the same treatment a schema-table source already gets
  • refrakt inspect shows a rune's registration declaration
  • The generated reference documents registers for every rune that carries one
  • The plan plugin's pipeline is unchanged, and the authoring guide states SPEC-144's reach table — storytelling and design are reachable, plan is not, and why (D5)
  • D3 (channel explicitness) is decided and the decision recorded in the resolution

References

  • SPEC-144 — mechanism, D1, D3, D5
  • SPEC-072 — the relationship graph this declares into
  • ADR-036 — why the vocabulary is closed

Resolution

Completed: 2026-10-07

Branch: claude/v039-registers PR: refrakt-md/refrakt#669

What was done

  • packages/runes/src/lib/registers.ts: the RegistersDeclaration vocabulary (entity: type/idFrom/scope/data/aliases; edge: type/from/to/kind/bidirectional/data), validateRegistersDeclaration (closed keys, inert values only, JSON round-trip; aliases.from and kind/bidirectional {field} must name bag keys), schemaRegisters WeakMap + registersFor, readRegistersSource, auditRegistersSources, describeRegisters.
  • packages/runes/src/registers-pipeline.ts: createRegistersHooks (Phase 2 register; Phase 3 name index with first-alias-wins, unknown-endpoint warnings, registry.relate for edges with alias endpoints canonicalised to ids) and composeRegistersHooks (runs the participant inside the declaring package's hook set, beside its own hooks; a package with no aggregate gets { entityByName } in its slot).
  • createContentModelSchema({ registers }) checks and records the block, and wraps the schema's validate with the source audit (registers-source-unresolved, warning, Markdoc attaches file/line). One extra transform per rune per process: sources seen resolved are cached per schema; the transform uses a copy of the page variables.
  • packages/content/src/site.ts: core catalog and every plugin are composed with their declarations in buildPreprocessHookSets. validate.ts: id added to DEFAULT_VALIDATION_IDS.
  • refrakt inspect (text "Registers" section with audit; --json registers field), refrakt reference (markdown + JSON registers).
  • Docs: new site/content/extend/rune-authoring/registration.md (vocabulary, source rule, aggregate slot, SPEC-144 reach table incl. why plan is not reachable); pointers from plugin-authoring/pipeline, authoring-overview, configuration reference (validation id), relationships and bond pages.
  • Tests: packages/runes/test/registers.test.ts (24), packages/content/test/validate-content.test.ts (file + line through the fast tier, build agrees).

D3 — decided: the implicit ref-then-bag fallback stays

A source is a bare name: first a data-name node inside the rune (bounded at nested runes), then the data-rune-fields bag. createComponentRenderable throws on a property/ref name collision (ADR-008), so the two namespaces are disjoint per rune and a bare name has one meaning. The risk explicitness would catch — a typo registering an empty id — is caught instead by registers-source-unresolved at validate time, with file and line. The storytelling and design migrations needed no channel prefix anywhere: every data field resolved from the bag, every id/endpoint from a ref or the bag, with registries identical to the hand-written hooks.

Notes

  • "Beside, not replacing": one implementation, composed into each declaring plugin's hook set rather than run as a separate __registers__ slot. That is what makes registration order, getTypes() order and warning attribution identical to the hooks it replaces.
  • The ref lookup is bounded at nested runes (like findAllByName); storytelling's old findByName was not. They differ only when a rune lacks its own ref and a nested rune has one.
  • Contracts and SEO baseline: no diff.
  • Spec correction for SPEC-144: the "same treatment a schema-table source already gets" was inspect-time only (inspect --audit); registers sources now get both that and a validate-time finding.