Acceptance Criteria
fieldMetas' spec is data: a bare string, or { from: [...], default } — no entry accepts a function- The spec round-trips through
JSON.parse(JSON.stringify(...)) unchanged from resolves only the declared roots (attrs.*, file.*); an unknown root is rejected at call time rather than resolving to empty- The five runes reading
config.variables.file express their created / modified fallback without a closure - A property-and-ref name collision is still rejected with the ADR-008 error when the properties object comes from
fieldMetas fieldMetas is adopted where a rune's metas are all plain attrs reads mapped into properties; runes needing a meta outside properties, or conditionally, keep the explicit formgroupByHeading is adopted at the seven loop sites, with each rune's per-item parser left rune-specific- No lint rule or contract assertion makes either utility mandatory
- The utility is named
fieldMetas; nothing in the codebase or docs introduces a second metaFields, and RuneConfig.metaFields is unchanged refrakt contracts --check and npm run seo:baseline:check report no driftnpm test passes unchanged
Approach
fieldMetas writes into the same flat key space as refs (ADR-008), so the collision check has to keep working against a computed object rather than a literal — worth a test, since the current check reads Object.keys of both and a generated object is the case nobody has exercised.
Key order matters for data-rune-fields, which is JSON.stringifyd: iterate the spec in declaration order so the bag's key order stays stable and the contracts diff stays empty.
groupByHeading shares only the traversal. parseColorEntry, parseNameValue, parseFontEntry and parseLocationItem stay where they are — the loop is the duplication, not the parsing.
Blocked by
- WORK-598 —
fieldMetas produces the properties object, so it lands after the children emission is gone rather than having to reproduce it
References
- SPEC-140 — Tier 3, D5
- ADR-008 — the flat namespace
properties and refs share
Resolution
Completed: 2026-10-06
Branch: claude/v0-37-post-release-plan-dc3va1 PR: refrakt-md/refrakt#657
What was done
packages/runes/src/lib/field-metas.ts: fieldMetas(attrs, config, spec) — spec is data (bare-string default, or { from: ['attrs.x' | 'file.x', …], default }); every source is validated before one is chosen, so an unknown root throws even after a hit; keys emerge in declaration order. Exported with FieldMetaSpec / FieldMetaEntry / FieldMetaSource.- Adopted by the five plan runes (work, bug, decision, milestone, spec — the
fileVars closures are gone) and the seven other calls whose properties are all plain attrs.X ?? 'literal' reads: sidenote, pullquote, map-pin (gained its config param), event, organization, comparison-row, api. packages/runes/src/lib/node.ts: groupByHeading(nodes, { initial, heading, item, other? }) — shares only the traversal; the rune's heading callback decides the next group. Adopted at tint (unrecognised heading keeps the section), palette ×2 (same-titled headings stay separate groups), spacing ×2 (unrecognised heading closes the section; one shared sectionOf), map (non-list nodes pass through in order).packages/runes/test/field-metas.test.ts: JSON round-trip, root rejection (incl. a bad source after a hit), declaration order, ADR-008 collision on a computed properties object, traversal incl. other.
Notes
- Bento is left explicit, by decision on #658. Its loop collects the sibling nodes under each heading and never looks at list items — a different walk, and fitting it would grow the helper for one caller. Hence the unchecked "seven loop sites" criterion: six adopted.
- Runes whose property keys differ from their attribute names (hint's
hintType ← type) or whose metas are conditional keep the explicit form; neither utility is mandatory. refrakt inspect --json over every rune × variant on both sites was byte-identical to main.