diff --git a/.claude/hooks/enforce-component-create.mjs b/.claude/hooks/enforce-component-create.mjs index e64498bba..ef5ef751e 100644 --- a/.claude/hooks/enforce-component-create.mjs +++ b/.claude/hooks/enforce-component-create.mjs @@ -88,7 +88,7 @@ async function main() { // edit-via-Write — let it pass (validate-tokens.mjs still applies). if (existsSync(filePath) && !fileIsRecent(filePath)) process.exit(0) - if (skillReferencedInTranscript(input.transcript_path)) process.exit(0) + if (pipelineReferencedInTranscript(input.transcript_path)) process.exit(0) const msg = [ `Component-create gate: blocked Write to ${relPath}`, diff --git a/.claude/hooks/validate-spec-compliance.mjs b/.claude/hooks/validate-spec-compliance.mjs index 70b763737..175cbf2c1 100644 --- a/.claude/hooks/validate-spec-compliance.mjs +++ b/.claude/hooks/validate-spec-compliance.mjs @@ -93,6 +93,29 @@ async function main() { if (slotsDiff.missing.length) violations.push(`Slots missing in .vue: ${slotsDiff.missing.join(', ')}`) if (slotsDiff.extra.length) violations.push(`Slots in .vue not in spec: ${slotsDiff.extra.join(', ')}`) + // ---- Naming conventions (B1): things the spec tables can't catch ---- + // Visual-variant prop must be `kind` (never variant/appearance/intent). `color` is + // left out on purpose — some components legitimately take a color value. + const FORBIDDEN_PROP = { variant: 'kind', appearance: 'kind', intent: 'kind' } + for (const p of sfc.props) { + const alt = FORBIDDEN_PROP[p.name] + if (alt) { + violations.push(`Prop "${p.name}" is not allowed — use "${alt}" (.claude/rules/naming.md).`) + } + if (/^(is|has)[A-Z]/.test(p.name)) { + const suggested = p.name.replace(/^(is|has)/, '').replace(/^./, (c) => c.toLowerCase()) + violations.push(`Boolean prop "${p.name}" must drop the is/has prefix (use "${suggested}").`) + } + } + // Events are kebab-case (update:value, before-close). Flag a camelCase event name, + // but never a v-model `update:*` event (its payload segment can be camelCase). + for (const e of sfc.emits) { + if (!e.name.includes(':') && /^[a-z]+[A-Z]/.test(e.name)) { + const suggested = e.name.replace(/([A-Z])/g, '-$1').toLowerCase() + violations.push(`Event "${e.name}" must be kebab-case (use "${suggested}").`) + } + } + // ---- defineOptions.name ---- const expectedName = toPascal(info.name) if (sfc.defineOptionsName !== expectedName) { diff --git a/.claude/hooks/validate-tokens.mjs b/.claude/hooks/validate-tokens.mjs index 968e2c040..07b719209 100644 --- a/.claude/hooks/validate-tokens.mjs +++ b/.claude/hooks/validate-tokens.mjs @@ -85,6 +85,36 @@ const VIOLATIONS = [ id: 'ts-ignore', regex: /\/\/\s*@ts-(?:ignore|nocheck|expect-error)\b/g, message: '`@ts-ignore`/`@ts-nocheck`/`@ts-expect-error`. Fix the underlying type issue.' + }, + { + id: 'js-class-preset', + regex: /\bconst\s+\w*[Cc]lasses\s*=\s*(?:[{[]|computed\b)/g, + message: + 'JS class preset (const *Classes = {…}/[…]/computed). Put utilities inline on the root class + switch variants with data-* (.claude/rules/styling.md).' + }, + { + id: 'style-block', + regex: /]/g, + message: + 'Component-local