WORK-619
ID:WORK-619Status:done

The identity guard is path-granular

Implement SPEC-158. IDENTITY_FIELDS (packages/transform/src/identity-fields.ts) becomes a list of paths, and findReservedFields resolves a wildcard segment. This adds three protections:

Priority:highComplexity:simpleMilestone:v0.40.0Source:SPEC-158
claude/v040-identity-guard View source

Criteria completion

Criteria completion: 10 of 10 (100%) checked; history from Oct 8 to Oct 80%25%50%75%100%Oct 8Oct 8
Branches 2
claude/v040-identity-guard current done
claude/post-v0.38-roadmap-jb7pie donemain done
History 2
  1. e0e5cca
    • ☑ `IDENTITY_FIELDS` holds paths, and `findReservedFields` resolves a wildcard segment, with the existing eight keys behaving exactly as before — asserted by the current tests passing unchanged
    • ☑ A theme override setting `sequence` is dropped and reported, with the rune and field named, and a test cites {% ref "ADR-030" /%} rule 3 as the reason so the history is not lost
    • ☑ `sequenceDirection` remains overridable, asserted alongside, so the split is visible in one place
    • ☑ A theme override setting `metaFields.status.metaType` is dropped while that field's `label` and `sentimentMap` merge normally, asserted on one override carrying all three
    • ☑ An `attrs` map setting `data-section`, `typeof` or `property` is refused with the path and attribute named; the wrapper's other attributes still apply
    • ☑ `attrs` setting `data-zone-layout` is *not* refused (D4)
    • ☑ The refusal is enforced at config normalisation, so it fires once per config rather than per render
    • ☑ `npm run seo:baseline:check` and `refrakt contracts --check` report no drift on either contract copy
    • ☑ `packages/lumina` merges unchanged, asserted rather than assumed
    • ☑ The theme-authoring guide states which paths are guarded and why, with the track-number example as the case that makes it concrete
    by bjornolofandersson
  2. cfbab94
    Created (in-progress)by bjornolofandersson

Acceptance Criteria

  • IDENTITY_FIELDS holds paths, and findReservedFields resolves a wildcard segment, with the existing eight keys behaving exactly as before — asserted by the current tests passing unchanged
  • A theme override setting sequence is dropped and reported, with the rune and field named, and a test cites ADR-030 rule 3 as the reason so the history is not lost
  • sequenceDirection remains overridable, asserted alongside, so the split is visible in one place
  • A theme override setting metaFields.status.metaType is dropped while that field's label and sentimentMap merge normally, asserted on one override carrying all three
  • An attrs map setting data-section, typeof or property is refused with the path and attribute named; the wrapper's other attributes still apply
  • attrs setting data-zone-layout is not refused (D4)
  • The refusal is enforced at config normalisation, so it fires once per config rather than per render
  • npm run seo:baseline:check and refrakt contracts --check report no drift on either contract copy
  • packages/lumina merges unchanged, asserted rather than assumed
  • The theme-authoring guide states which paths are guarded and why, with the track-number example as the case that makes it concrete

Resolution

Completed: 2026-10-08

Branch: claude/v040-identity-guard PR: refrakt-md/refrakt#679

What was done

  • packages/transform/src/identity-fields.ts — IDENTITY_FIELDS holds paths (sequence, metaFields.*.metaType added after the original eight); findReservedFields resolves a * segment to concrete paths with per-segment presence semantics. New IDENTITY_FIELD_ATTRIBUTES / derivedAttributeOwner / derivedAttributeMessage state D4 by inversion (field → emitted attributes, plus data-{kebab} for declared modifiers).
  • packages/transform/src/merge.ts — dotted-path stripping (copy-on-write); the declared metaType is reasserted on replaced metaFields entries; layout attrs (rune + variant-delta layouts) cleaned and reported under guardIdentity at config normalisation. IdentityViolation.attribute added; mergeRuneConfig exported.
  • packages/transform/src/validate.ts — validateThemeConfig errors on refused layout attrs (surfaces in refrakt plugin validate).
  • Tests: packages/transform/test/identity-paths.test.ts (new); identity-fields.test.ts list/loops adjusted; packages/lumina/test/identity-guard.test.ts asserts guarded == unguarded merge for Lumina and the full assembly.
  • site/content/extend/theme-authoring/config-api.md — "Guarded paths" table with the track-number example; cross-links from sequence/metaFields/layout/variants/merge sections.
  • Changeset: @refrakt-md/transform minor.

Notes (spec findings)

  • "Current tests pass unchanged" cannot hold literally: identity-fields.test.ts asserted the exact eight-key list and looped over IDENTITY_FIELDS as top-level keys. The list gained the two entries and the loops iterate whole-key paths; every assertion about the original eight is unchanged.
  • Stripping metaType alone does not preserve it: metaFields merges per entry, so a replaced entry would lose the declared type (as would an entry that omits it). The guard reasserts the base metaType — slightly beyond D1's presence-only wording, required for "the rune's own declaration stands".
  • The attrs refusal also applies to a rune's first declaration (no base), since attrs bypasses the owning field whoever writes it. Core runes' own layouts are checked only by validateThemeConfig; none use attrs.
  • Inert: SEO baseline and both contract copies unchanged; Lumina and all nine plugins trip no violations.