All files / packages/sds-customization-compliance/src/checks/helpers regressionFlagFix.ts

26.08% Statements 6/23
6.52% Branches 3/46
20% Functions 4/20
22.72% Lines 5/22

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78                                                      2x       4x       4x                                                           2x                   2x    
/**
 * Per-flag remediation hints for regression-audit flags. Used by the
 * regression check (`no-unintended-visual-changes`) so each offender row
 * carries an actionable next step instead of just the bare flag id.
 *
 * Hints intentionally name the *decision* the human reviewer needs to
 * make, not a mechanical rewrite — most of these flags are
 * classification questions ("is this rename intentional?"), not
 * autopilot fixes.
 */
 
import type { Flag, FlagSide } from '../../audit/types.js';
 
/**
 * `file:line` string the regression checks prepend to offender notes so
 * each row points at the exact CSS location that triggered the flag.
 *
 * Source positions are populated by the audit's PostCSS parse pass and
 * threaded through every `FlagSide`; if neither side carries a source
 * (e.g. browser path / synthesized fixtures), this returns `null` and
 * the offender note degrades gracefully.
 *
 * Prefers the legacy side because regressions are described from the
 * legacy CSS's perspective: the question the reviewer is answering is
 * "what changed *from* the legacy file?".
 */
export function flagLocation(flag: Flag): string | null {
  return formatSideLocation(flag.legacy) ?? formatSideLocation(flag.next);
}
 
function formatSideLocation(side: FlagSide | null | undefined): string | null {
  Eif (!side?.source) return null;
  return side.line ? `${side.source}:${side.line}` : side.source;
}
 
const HINTS: Record<string, (flag: Flag) => string> = {
  'hook-renamed': (f) =>
    `Hook renamed${
      f.legacy?.hook && f.next?.hook ? ` (\`${f.legacy.hook}\` → \`${f.next.hook}\`)` : ''
    }. Confirm the rename is intentional and document it in the customer migration notes; consider keeping the legacy name as an alias for one release if the surface is broadly customized.`,
  'hook-removed-from-public-api': (f) =>
    `\`${f.legacy?.hook ?? 'hook'}\` is no longer a customer-reachable API. Confirm the removal is intentional and call it out in the migration notes; if customers may still rely on it, restore the public read or provide a replacement hook.`,
  'hook-selector-moved': (f) =>
    `\`${f.legacy?.hook ?? 'hook'}\` moved from \`${f.legacy?.selector ?? 'legacy selector'}\` to \`${f.next?.selector ?? 'new selector'}\`. Confirm customers writing overrides on the legacy selector still pick up the new wiring; update the override-surface docs.`,
  'fallback-removed': (f) =>
    `Fallback \`${f.legacy?.terminal ?? 'value'}\` removed from \`${f.legacy?.prop ?? 'property'}\` on \`${f.selector ?? 'selector'}\`. Restore the fallback in the \`var()\` chain or accept the new unset behavior and document the change.`,
  'fallback-moved-to-theme': () =>
    `Fallback moved from base CSS into a theme file. Verify every theme provides the value (or that the fallback genuinely no longer needs to exist on the base path).`,
  'cascade-shortened': () =>
    `Cascade shortened. Confirm the customer override path remains reachable; if the reduction was unintentional, restore the intermediate hook/selector.`,
  'property-value-changed': (f) =>
    `\`${f.legacy?.prop ?? 'property'}\` value changed on \`${f.selector ?? 'selector'}\`. Confirm the new paint is intentional and update visual regression baselines; if not, restore the prior value.`,
  'property-replaced': (f) =>
    `\`${f.legacy?.prop ?? 'property'}\` replaced on \`${f.selector ?? 'selector'}\`. Confirm the replacement preserves the intended paint; if customers are likely to override the legacy property, keep both for one release.`,
  'property-removed': (f) =>
    `\`${f.legacy?.prop ?? 'property'}\` removed from \`${f.selector ?? 'selector'}\`. Confirm the removal is intentional; if customers override this property, restore it or document the migration.`,
  'selector-scope-narrowed': (f) =>
    `Selector scope narrowed on \`${f.selector ?? 'selector'}\`. Confirm the narrower scope still covers every place the legacy rule painted.`,
  'selector-specificity-changed': (f) =>
    `Specificity changed on \`${f.selector ?? 'selector'}\`. Under Rule 5 (root-anchored modifiers and elements) this is the intended new shape; informational only — no action needed unless the new specificity also drops paint coverage that should be restored on the canonical selector.`,
  'selector-moved-to-theme': (f) =>
    `\`${f.selector ?? 'selector'}\` moved from base CSS into a theme file. Verify every theme re-states the rule, or move the rule to \`themes/base.css\` so theme inheritance covers it.`,
  'selector-removed': (f) =>
    `\`${f.selector ?? 'selector'}\` is no longer present in \`themes/base.css\` or any tracked theme. Confirm this selector's painting is now covered by the theme architecture (or that the rule is intentionally retired); customer hook overrides on the component root continue to work either way.`,
  'pseudo-element-content-hookified': () =>
    `Pseudo-element \`content\` is now driven by a hook. Confirm every theme assigns the new hook, otherwise the pseudo-element will render empty.`,
  'kinetics-or-motion-removed': () =>
    `Kinetics/motion rule removed. Confirm reduced-motion behavior is still correct in the new CSS; restore the rule if the removal was unintentional.`,
  'hook-opened-but-unassigned-in-all-themes': (f) =>
    `\`${f.next?.hook ?? 'hook'}\` is opened by base CSS but unassigned in every tracked theme. Either assign it in at least one theme or remove it from the public API.`,
  'audit-incomplete': () =>
    `Audit was incomplete on this component. Re-run \`sds-compliance build\` and re-evaluate.`,
};
 
export function regressionFlagFix(flag: Flag): string | undefined {
  return HINTS[flag.id]?.(flag);
}