Acceptance Criteria
extractText for AST nodes is defined once, exported from @refrakt-md/runes, and imported by all six former copiesplan, storytelling and media either import textContent or carry a comment stating why they keep a local variantnpm test passes unchangedrefrakt contracts --check and npm run seo:baseline:check report no drift
Approach
The AST side is a pure move — the six bodies are byte-identical, so a single export and six imports.
The renderable side is a behaviour change at each site, not a rename. The exported textContent ends with .trim(); the three local copies do not. Two places where that matters and must be checked individually:
plugins/plan/src/pipeline.ts — countCheckboxes regexes the result for [ ] / [x]. Trimming cannot change a count, so this is safe.plugins/storytelling/src/pipeline.ts — feeds entity indexing; a trimmed key differs from an untrimmed one.plugins/media/src/tags/track.ts — check what consumes the value before swapping.
Where trimming is wrong, keep the local variant and say why in a comment. A blanket swap is the failure mode here.
Resolution
Completed: 2026-10-06
Branch: claude/v0-37-post-release-plan-dc3va1
What was done
extractText(node: Node) added to packages/runes/src/lib/node.ts, exported via lib/index.ts and the package index; the six byte-identical copies (form, map, palette, typography, spacing, comparison) now import it.plugins/plan/src/pipeline.ts, plugins/storytelling/src/pipeline.ts, plugins/media/src/tags/track.ts: local untrimmed helpers replaced by the exported textContent (storytelling's extractTextContent(node).trim() becomes textContent(node)).
Notes
- Measured rather than reasoned: temporarily wrapped each local helper to log any call whose result differed from its trimmed form, then ran
refrakt validate --deep on the main site (1,520 calls), on the real plan/ entities via a temporary contentDir swap (2,375 calls), and the plugin tests. The only differences were 4 whole-entity texts feeding countCheckboxes / extractIdReferences — trimming cannot change a count or a matched ID. No title, storytelling entity name or track name differed.