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);
}
|