WORK-601
ID:WORK-601Status:done

Consolidate extractText and adopt the exported textContent

Two families of duplication, with different fixes.

AST side — six byte-identical copies. extractText(node: Node) is defined identically in packages/runes/src/tags/form.ts, plugins/places/src/tags/map.ts, plugins/design/src/tags/{palette,typography,spacing}.ts and plugins/marketing/src/tags/comparison.ts.

Renderable side — textContent is already exported and reimplemented anyway. @refrakt-md/runes exports it (seo.ts:23), and plugins/plan/src/pipeline.ts + plugins/storytelling/src/pipeline.ts define extractTextContent, while plugins/media/src/tags/track.ts defines a Tag-flavoured extractText.

Priority:mediumComplexity:simpleMilestone:v0.38.0Source:SPEC-140
claude/v0-37-post-release-plan-dc3va1 View source

Criteria completion

Criteria completion: 4 of 4 (100%) checked; history from Oct 6 to Oct 60%25%50%75%100%Oct 6Oct 6
Branches 3
claude/v0-37-post-release-plan-dc3va1 current done
main doneclaude/nifty-ptolemy-80epot readyclaude/v0-37-0-review-vqpl41 ready
History 2
  1. afc1e37
    • ☑ `extractText` for AST nodes is defined once, exported from `@refrakt-md/runes`, and imported by all six former copies
    • ☑ `plan`, `storytelling` and `media` either import `textContent` or carry a comment stating why they keep a local variant
    • ☑ `npm test` passes unchanged
    • ☑ `refrakt contracts --check` and `npm run seo:baseline:check` report no drift
    by bjornolofandersson
  2. 66dae35
    Created (in-progress)by bjornolofandersson

Acceptance Criteria

  • extractText for AST nodes is defined once, exported from @refrakt-md/runes, and imported by all six former copies
  • plan, storytelling and media either import textContent or carry a comment stating why they keep a local variant
  • npm test passes unchanged
  • refrakt 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.

References

  • SPEC-140 — Tier 2, D6

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.