@mrciphersmith/keryx 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/cli.js +8451 -4817
- package/dist/core.js +11700 -11374
- package/package.json +1 -1
- package/src/gdskills/bundled/agents/go-code-auditor.md +1 -1
- package/src/gdskills/bundled/agents/python-code-auditor.md +1 -1
- package/src/gdskills/bundled/install-manifest.json +271 -4
- package/src/gdskills/bundled/rules/core/model-selection.mdc +51 -0
- package/src/gdskills/bundled/skills/review/review-jev-rules/SKILL.md +267 -0
- package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.detail.md +26 -0
- package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.md +1 -1
- package/src/gdskills/bundled/stacks/angular/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/angular/governance/eval.json +1751 -0
- package/src/gdskills/bundled/stacks/angular/governance/scout.json +32 -0
- package/src/gdskills/bundled/stacks/angular/pack.json +55 -0
- package/src/gdskills/bundled/stacks/angular/rules/coding-style.mdc +82 -0
- package/src/gdskills/bundled/stacks/angular/rules/patterns.mdc +84 -0
- package/src/gdskills/bundled/stacks/angular/rules/security.mdc +70 -0
- package/src/gdskills/bundled/stacks/angular/rules/testing.mdc +73 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-build-fix/SKILL.md +127 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-build-fix/evals.json +72 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-code-review/SKILL.md +98 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-code-review/evals.json +73 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-implementation/SKILL.md +112 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-implementation/evals.json +74 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-testing/SKILL.md +102 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-testing/evals.json +71 -0
- package/src/gdskills/bundled/stacks/mobx/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/mobx/governance/eval.json +904 -0
- package/src/gdskills/bundled/stacks/mobx/governance/scout.json +18 -0
- package/src/gdskills/bundled/stacks/mobx/pack.json +28 -0
- package/src/gdskills/bundled/stacks/mobx/rules/coding-style.mdc +91 -0
- package/src/gdskills/bundled/stacks/mobx/rules/patterns.mdc +122 -0
- package/src/gdskills/bundled/stacks/mobx/rules/security.mdc +56 -0
- package/src/gdskills/bundled/stacks/mobx/rules/testing.mdc +63 -0
- package/src/gdskills/bundled/stacks/mobx/skills/mobx-observable-testing/SKILL.md +124 -0
- package/src/gdskills/bundled/stacks/mobx/skills/mobx-observable-testing/evals.json +73 -0
- package/src/gdskills/bundled/stacks/mobx/skills/mobx-store-implementation/SKILL.md +149 -0
- package/src/gdskills/bundled/stacks/mobx/skills/mobx-store-implementation/evals.json +74 -0
- package/src/gdskills/bundled/stacks/nestjs/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/nestjs/governance/eval.json +1308 -0
- package/src/gdskills/bundled/stacks/nestjs/governance/scout.json +34 -0
- package/src/gdskills/bundled/stacks/nestjs/pack.json +53 -0
- package/src/gdskills/bundled/stacks/nestjs/rules/coding-style.mdc +70 -0
- package/src/gdskills/bundled/stacks/nestjs/rules/patterns.mdc +83 -0
- package/src/gdskills/bundled/stacks/nestjs/rules/security.mdc +73 -0
- package/src/gdskills/bundled/stacks/nestjs/rules/testing.mdc +69 -0
- package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-build-fix/SKILL.md +157 -0
- package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-build-fix/evals.json +70 -0
- package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-implementation/SKILL.md +129 -0
- package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-implementation/evals.json +71 -0
- package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-testing/SKILL.md +143 -0
- package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-testing/evals.json +69 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/governance/eval.json +2413 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/governance/scout.json +42 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/pack.json +42 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/rules/coding-style.mdc +69 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/rules/patterns.mdc +88 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/rules/security.mdc +72 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/rules/testing.mdc +64 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-build-fix/SKILL.md +147 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-build-fix/evals.json +75 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-code-review/SKILL.md +118 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-code-review/evals.json +76 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-implementation/SKILL.md +135 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-implementation/evals.json +78 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-testing/SKILL.md +116 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-testing/evals.json +75 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-upgrade-migration/SKILL.md +134 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-upgrade-migration/evals.json +76 -0
- package/src/gdskills/bundled/stacks/vue/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/vue/governance/eval.json +2215 -0
- package/src/gdskills/bundled/stacks/vue/governance/scout.json +42 -0
- package/src/gdskills/bundled/stacks/vue/pack.json +42 -0
- package/src/gdskills/bundled/stacks/vue/rules/coding-style.mdc +73 -0
- package/src/gdskills/bundled/stacks/vue/rules/patterns.mdc +84 -0
- package/src/gdskills/bundled/stacks/vue/rules/security.mdc +60 -0
- package/src/gdskills/bundled/stacks/vue/rules/testing.mdc +69 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-build-fix/SKILL.md +137 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-build-fix/evals.json +72 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-code-review/SKILL.md +120 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-code-review/evals.json +71 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-implementation/SKILL.md +122 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-implementation/evals.json +72 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-testing/SKILL.md +115 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-testing/evals.json +72 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue2-to-vue3-migration/SKILL.md +135 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue2-to-vue3-migration/evals.json +71 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"query": "Use when building or changing a Vue 3 Single File Component, composable, or Pinia store -- covers <script setup> with type-based defineProps/defineEmits, choosing ref vs reactive, computed vs watch/watchEffect, composable extraction, and Pinia store/action design. Not for Options API legacy components (use vue2-to-vue3-migration) or Node.js service/CLI code with no .vue file (use nodejs-implementation).",
|
|
4
|
+
"decision": "fork",
|
|
5
|
+
"topMatch": "vue/vue2-to-vue3-migration",
|
|
6
|
+
"recordedAt": "2026-09-25T06:25:21.007Z",
|
|
7
|
+
"skillName": "vue-implementation",
|
|
8
|
+
"justification": "Top match vue2-to-vue3-migration (0.417) shares vue/pinia/component vocabulary inherent to any two skills in the same framework pack, but is scoped to converting legacy Options API code to Composition API, not authoring new Composition API code -- distinct trigger surface (upgrade/convert/migrate prompts vs build/implement prompts), confirmed by clean trigger-accuracy eval. Second match vue-code-review (0.350) is read-only review, not authoring."
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"query": "Use when a Vue 3 component or composable's test suite needs writing, extending, or fixing with Vue Test Utils and Vitest -- covers mount vs. shallowMount, querying by test id/role, awaiting trigger()/nextTick for async DOM updates, asserting wrapper.emitted() payloads, and Pinia test-store isolation with createTestingPinia. Not for plain Node.js/TypeScript unit tests with no .vue mount (use nodejs-testing) or React Testing Library tests (use react-testing).",
|
|
12
|
+
"decision": "fork",
|
|
13
|
+
"topMatch": "nextjs-nuxt/nextjs-nuxt-testing",
|
|
14
|
+
"recordedAt": "2026-09-25T06:25:27.390Z",
|
|
15
|
+
"skillName": "vue-testing",
|
|
16
|
+
"justification": "Top matches are sibling vue/* skills sharing pack-level vocabulary (component, pinia, script); nodejs-testing and react-testing (named explicitly as exclusions in the description) target a different mount surface (no .vue SFC / React JSX respectively). Confirmed distinct by clean trigger-accuracy eval against both a react-testing-library prompt and a plain node vitest prompt."
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"query": "Use when reviewing changed Vue 3 Single File Component or composable code (.vue) for reactivity bugs -- lost reactivity from destructuring reactive()/props, missing watch cleanup, prop mutation instead of emit, v-for key misuse, v-html injection risk, and Pinia store boundary violations. Read-only, no repository convention-doc lookup and no MobX/React store review. Not for authoring or fixing the component (use vue-implementation or vue-build-fix).",
|
|
20
|
+
"decision": "fork",
|
|
21
|
+
"topMatch": "react/react-code-review",
|
|
22
|
+
"recordedAt": "2026-09-25T06:25:34.017Z",
|
|
23
|
+
"skillName": "vue-code-review",
|
|
24
|
+
"justification": "Overlap is with sibling vue/* skills (shared pack vocabulary) and existing review skills (review-frontend, code-mobx-store-review) that this description explicitly excludes (no MobX/React store review). This skill's scope -- Vue reactivity/prop-emit/Pinia boundary bugs in .vue diffs -- has no existing catalog equivalent. Confirmed distinct by clean trigger-accuracy eval."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"query": "Use when resolving a vue-tsc template type error, an eslint-plugin-vue lint failure, or a Vite build error in a Vue 3 project -- covers template expression type mismatches, defineProps/defineEmits type errors, unresolved SFC/component imports, and Vite plugin/HMR build failures. Applies the smallest root-cause fix and never silences the checker with an any cast or an eslint-disable. Not for a generic tsc error in a .ts file with no .vue involved (use nodejs-build-fix) or a JSX/React build failure (use react-build-fix).",
|
|
28
|
+
"decision": "fork",
|
|
29
|
+
"topMatch": "ts-js-node/nodejs-build-fix",
|
|
30
|
+
"recordedAt": "2026-09-25T06:25:40.079Z",
|
|
31
|
+
"skillName": "vue-build-fix",
|
|
32
|
+
"justification": "Explicitly excludes and is distinct from nodejs-build-fix (plain .ts, no .vue) and react-build-fix (JSX/React) which the description names outright; overlap with sibling vue/* skills is pack-level vocabulary. Confirmed distinct by clean trigger-accuracy eval against both a plain-tsc negative and a react-build negative."
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"query": "Use when migrating a Vue 2 (Options API) codebase or component to Vue 3 -- covers converting Options API to <script setup> Composition API, replacing the removed filters and $listeners/$children APIs, Vuex-to-Pinia store migration, v-model breaking changes (single default to multiple named bindings), global API changes (createApp vs new Vue), and Vue 2 lifecycle hook renames (beforeDestroy -> beforeUnmount). Not for a fresh Vue 3 component with no legacy code (use vue-implementation).",
|
|
36
|
+
"decision": "fork",
|
|
37
|
+
"topMatch": "nextjs-nuxt/nextjs-nuxt-upgrade-migration",
|
|
38
|
+
"recordedAt": "2026-09-25T06:25:46.482Z",
|
|
39
|
+
"skillName": "vue2-to-vue3-migration",
|
|
40
|
+
"justification": "Top match vue-implementation (same pack) shares Composition API/script-setup vocabulary but is explicitly excluded (fresh components, no legacy code) and scoped oppositely -- authoring new vs converting legacy. No existing catalog skill covers Vue 2->3 migration specifics (filters, $listeners/$children, Vuex->Pinia, v-model contract change). Confirmed distinct by clean trigger-accuracy eval."
|
|
41
|
+
}
|
|
42
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "vue",
|
|
3
|
+
"family": "framework",
|
|
4
|
+
"extends": "ts-js-node",
|
|
5
|
+
"modules": ["vue-rules", "vue-skills"],
|
|
6
|
+
"detectionMarkers": ["vue"],
|
|
7
|
+
"provenance": {
|
|
8
|
+
"origin": "authored",
|
|
9
|
+
"sourceRef": "flow 318, Wave 4 batch 2"
|
|
10
|
+
},
|
|
11
|
+
"stability": "experimental",
|
|
12
|
+
"skills": {
|
|
13
|
+
"implement": ["vue-implementation"],
|
|
14
|
+
"test": ["vue-testing"],
|
|
15
|
+
"review": ["vue-code-review"],
|
|
16
|
+
"build-fix": ["vue-build-fix"],
|
|
17
|
+
"migrate": ["vue2-to-vue3-migration"]
|
|
18
|
+
},
|
|
19
|
+
"agentProfile": {
|
|
20
|
+
"displayName": "Vue",
|
|
21
|
+
"auditFocus": [
|
|
22
|
+
"reactivity lost by destructuring a reactive()/props object instead of using refs, toRefs, or (Vue 3.5+) defineProps destructuring",
|
|
23
|
+
"a watch/watchEffect with a missing or wrong dependency, or one left running past the component's lifecycle with no cleanup",
|
|
24
|
+
"props or emitted events with no type-based defineProps<T>()/defineEmits<T>() declaration, or a mutated prop instead of emitting a change",
|
|
25
|
+
"v-html rendering unsanitized or user-controlled content",
|
|
26
|
+
"a Pinia store action mutating another store's state directly instead of calling its own actions, or state read outside a component/setup reactivity scope",
|
|
27
|
+
"a v-for list with no stable :key, or :key bound to the array index where the list can reorder"
|
|
28
|
+
],
|
|
29
|
+
"buildCommands": [
|
|
30
|
+
"the project's type-check script (vue-tsc --noEmit or its package.json equivalent)",
|
|
31
|
+
"the project's lint script (eslint with eslint-plugin-vue enabled)",
|
|
32
|
+
"the project's test script (Vitest run of Vue Test Utils suites)",
|
|
33
|
+
"the project's build (vite build, or the framework's own production build)"
|
|
34
|
+
],
|
|
35
|
+
"fixGuardrails": [
|
|
36
|
+
"never disable or downgrade an eslint-plugin-vue rule to silence a warning",
|
|
37
|
+
"never cast a component's props, emits, or template expression to any to satisfy vue-tsc",
|
|
38
|
+
"never delete or loosen a failing assertion in a Vue Test Utils test to make it pass",
|
|
39
|
+
"never mutate a prop in place as a build-fix instead of emitting an update event"
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
extends: common
|
|
3
|
+
paths: ["**/*.vue"]
|
|
4
|
+
metadata:
|
|
5
|
+
origin: authored
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Vue coding style
|
|
9
|
+
|
|
10
|
+
Narrows `core-common-rules`' stack-agnostic style rules to Vue Single File
|
|
11
|
+
Component conventions. Applies only to `*.vue` files -- the `<script>`
|
|
12
|
+
block's own TypeScript style still comes from `ts-js-node`'s coding-style
|
|
13
|
+
rule, which this file `extends` transitively through the pack's `extends`.
|
|
14
|
+
|
|
15
|
+
## Script setup and typing
|
|
16
|
+
|
|
17
|
+
- Use `<script setup lang="ts">` for new components; do not introduce the
|
|
18
|
+
Options API or a bare (non-setup) `<script>` block in new files unless the
|
|
19
|
+
project has no `<script setup>` components at all yet.
|
|
20
|
+
- Declare props with the type-only form, `defineProps<{ ... }>()`, not the
|
|
21
|
+
runtime-object form (`defineProps({ foo: String })`), so prop types are
|
|
22
|
+
checked against actual usage instead of a separate runtime shape. Use
|
|
23
|
+
`withDefaults(defineProps<Props>(), { ... })` for default values rather
|
|
24
|
+
than mutating the returned props object.
|
|
25
|
+
- Declare emits with the type-only form: `defineEmits<{ change: [id: number]
|
|
26
|
+
}>()` (named-tuple syntax) rather than the untyped string-array form
|
|
27
|
+
(`defineEmits(['change'])`).
|
|
28
|
+
- On Vue 3.5+ projects, destructuring `defineProps()` directly
|
|
29
|
+
(`const { title } = defineProps<Props>()`) is reactive and preferred over
|
|
30
|
+
a separate `props.title` reference; on pre-3.5 projects, keep the props
|
|
31
|
+
object intact (`props.title`, or `toRefs(props)`) since a destructured
|
|
32
|
+
prop is a frozen snapshot, not a live binding.
|
|
33
|
+
- Type `ref<T>()` explicitly when the initial value's inferred type is
|
|
34
|
+
narrower than the eventual type (`ref<string | null>(null)`, not a bare
|
|
35
|
+
`ref(null)` that infers `Ref<null>`).
|
|
36
|
+
|
|
37
|
+
## Naming and structure
|
|
38
|
+
|
|
39
|
+
- Component file and `name` (when set) in `PascalCase`
|
|
40
|
+
(`OrderSummary.vue`); composable files and exported function names in
|
|
41
|
+
`camelCase` prefixed with `use` (`useOrderTotals.ts`).
|
|
42
|
+
- One component per `.vue` file; a template that needs its own local
|
|
43
|
+
sub-markup repeated more than once becomes its own child component, not a
|
|
44
|
+
`v-for` over inline duplicated markup.
|
|
45
|
+
- Order SFC blocks `<script setup>`, `<template>`, `<style>` (or the
|
|
46
|
+
project's existing established order) consistently across the codebase;
|
|
47
|
+
do not invent a third ordering in new files.
|
|
48
|
+
- Multi-word component names (`UserCard`, not `Card`) to avoid clashing
|
|
49
|
+
with current and future HTML elements, matching `eslint-plugin-vue`'s
|
|
50
|
+
`multi-word-component-names` rule.
|
|
51
|
+
|
|
52
|
+
## Formatting and imports
|
|
53
|
+
|
|
54
|
+
- Format with the project's configured formatter (Prettier or the
|
|
55
|
+
project's own config); do not hand-format around a formatter that is
|
|
56
|
+
already configured.
|
|
57
|
+
- Run the project's configured `eslint` (with `eslint-plugin-vue`) instead
|
|
58
|
+
of hand-checking template directive order, unused template refs, or
|
|
59
|
+
v-for/v-if precedence; fix what it flags rather than adding a blanket
|
|
60
|
+
disable comment.
|
|
61
|
+
- Prefer auto-imported composables/components only when the project has
|
|
62
|
+
`unplugin-auto-import`/`unplugin-vue-components` (or Nuxt) configured;
|
|
63
|
+
otherwise import explicitly -- do not assume auto-import is active.
|
|
64
|
+
|
|
65
|
+
## Errors
|
|
66
|
+
|
|
67
|
+
- Surface a composable or store action's failure to the caller (thrown
|
|
68
|
+
error, or a returned error/result shape the caller checks) instead of
|
|
69
|
+
swallowing it in a bare `catch` with no rethrow and no user-visible
|
|
70
|
+
state change.
|
|
71
|
+
- Use Vue's `onErrorCaptured` (component-tree scope) or a top-level
|
|
72
|
+
`app.config.errorHandler` (app-wide scope) for error boundaries, not an
|
|
73
|
+
ad hoc `try/catch` wrapped around every template-triggered call site.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
extends: common
|
|
3
|
+
paths: ["**/*.vue", "**/*.ts"]
|
|
4
|
+
metadata:
|
|
5
|
+
origin: authored
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Vue patterns
|
|
9
|
+
|
|
10
|
+
Narrows `core-common-rules`' stack-agnostic design-pattern guidance to
|
|
11
|
+
idiomatic Vue 3 Composition API design and its common anti-patterns.
|
|
12
|
+
Applies to `*.vue` files and plain `*.ts` files alike (R1 review, PR #719,
|
|
13
|
+
M4): a composable (`useX()`) is routinely a standalone `.ts` file, not a
|
|
14
|
+
`.vue` SFC, and the reactivity/composable guidance below (`ref()` vs.
|
|
15
|
+
`reactive()`, lifecycle-hook-in-composables rules, the return-shape rule)
|
|
16
|
+
is exactly what a `.ts` composable needs — `ts-js-node`'s own generic
|
|
17
|
+
patterns rule has no notion of Vue's reactivity primitives at all.
|
|
18
|
+
|
|
19
|
+
## Reactivity
|
|
20
|
+
|
|
21
|
+
- Use `ref()` for a single primitive or a value that is often reassigned
|
|
22
|
+
wholesale; use `reactive()` for a fixed-shape object whose properties are
|
|
23
|
+
mutated in place. Do not wrap a `reactive()` object in a `ref()` "for
|
|
24
|
+
safety" -- pick one.
|
|
25
|
+
- Never destructure a `reactive()` object or a raw (non-`defineProps`)
|
|
26
|
+
props object into loose variables (`const { count } = state`) -- that
|
|
27
|
+
copies the current value and breaks the reactive link. Use `toRefs()`
|
|
28
|
+
(whole object) or `toRef()` (one property) when a template or composable
|
|
29
|
+
needs individual reactive references out of a `reactive()` object.
|
|
30
|
+
- Prefer a `computed()` over a `watch` that just re-derives and assigns a
|
|
31
|
+
value on every dependency change; reach for `watch`/`watchEffect` only
|
|
32
|
+
for a genuine side effect (a fetch, a DOM measurement, a log, syncing to
|
|
33
|
+
an external system).
|
|
34
|
+
- Give every `watch` an explicit, minimal source (`watch(() => props.id,
|
|
35
|
+
...)`, not `watch(props, ...)` with `{ deep: true }` when only one field
|
|
36
|
+
actually needs to be observed) -- a whole-object deep watch fires on every
|
|
37
|
+
unrelated field change and is expensive on large objects.
|
|
38
|
+
- Stop a `watch`/`watchEffect` created outside a component's own setup
|
|
39
|
+
scope (e.g. inside a manually managed subscription) using the stop handle
|
|
40
|
+
it returns; one created directly inside `setup`/`<script setup>` is
|
|
41
|
+
auto-stopped on unmount and needs no manual cleanup.
|
|
42
|
+
|
|
43
|
+
## Composables
|
|
44
|
+
|
|
45
|
+
- Extract a composable (`useX()`) when the same stateful logic (a fetch +
|
|
46
|
+
loading/error state, a subscription, a computed derivation) is needed in
|
|
47
|
+
more than one component, instead of copy-pasting the `ref`/`watch` block.
|
|
48
|
+
- Call composables that use lifecycle hooks (`onMounted`, `onUnmounted`,
|
|
49
|
+
`provide`/`inject`) only from another composable or directly inside
|
|
50
|
+
`setup`/`<script setup>` -- never from inside a callback, `watch`
|
|
51
|
+
handler, or `async` continuation after an `await`, where the active
|
|
52
|
+
component instance is no longer bound.
|
|
53
|
+
- A composable returns refs/computed values and functions, not a plain
|
|
54
|
+
object of dereferenced values -- return `{ count, increment }` (where
|
|
55
|
+
`count` is the `ref` itself), not `{ count: count.value, increment }`,
|
|
56
|
+
or the caller loses reactivity.
|
|
57
|
+
|
|
58
|
+
## Component design
|
|
59
|
+
|
|
60
|
+
- Emit an event (`emit('update:modelValue', v)`) instead of mutating a
|
|
61
|
+
prop in place; a prop is one-way data flow from the parent, and Vue warns
|
|
62
|
+
at runtime when a child writes to it directly.
|
|
63
|
+
- Use `v-model` (or a named `v-model:propName`) for two-way bindable state
|
|
64
|
+
instead of hand-wiring a prop plus a separately-named update event when
|
|
65
|
+
the semantics are genuinely two-way.
|
|
66
|
+
- Keep template `v-if`/`v-for` on the same element simple; when both are
|
|
67
|
+
needed, wrap the `v-for` in a `<template>` with the `v-if` on each item,
|
|
68
|
+
or filter the source list in a `computed()`, rather than relying on
|
|
69
|
+
`eslint-plugin-vue`'s default precedence to make an implicit filter
|
|
70
|
+
correct.
|
|
71
|
+
- Prefer `provide`/`inject` for state genuinely shared down an arbitrary
|
|
72
|
+
depth of the component tree (theme, current user); prop-drilling two or
|
|
73
|
+
three levels does not need it, and a store (Pinia) is the better fit for
|
|
74
|
+
state shared across unrelated branches of the tree.
|
|
75
|
+
|
|
76
|
+
## Anti-patterns
|
|
77
|
+
|
|
78
|
+
- A `ref`/`reactive` object stored in module scope and mutated from
|
|
79
|
+
multiple unrelated components as an ad hoc global store, instead of a
|
|
80
|
+
Pinia store with named actions -- it works but loses devtools tracking,
|
|
81
|
+
action naming, and testability.
|
|
82
|
+
- A deeply nested `reactive()` object mutated from many call sites with no
|
|
83
|
+
action layer -- prefer a Pinia store (or a composable with named mutator
|
|
84
|
+
functions) so state changes have one discoverable entry point.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
extends: common
|
|
3
|
+
paths: ["**/*.vue"]
|
|
4
|
+
metadata:
|
|
5
|
+
origin: authored
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Vue security
|
|
9
|
+
|
|
10
|
+
Narrows `core-common-rules`' stack-agnostic security rules to Vue-specific
|
|
11
|
+
render-time and routing risks. Applies only to `*.vue` files; server-side
|
|
12
|
+
Node.js concerns (request validation, dependency risk) still come from
|
|
13
|
+
`ts-js-node`'s security rule.
|
|
14
|
+
|
|
15
|
+
## Template injection (XSS)
|
|
16
|
+
|
|
17
|
+
- Never bind `v-html` to a value that includes user-controlled or
|
|
18
|
+
unsanitized content -- `v-html` renders raw HTML with no escaping, so a
|
|
19
|
+
string containing `<script>`/`onerror=`/etc. executes. Sanitize with a
|
|
20
|
+
library such as DOMPurify immediately before the `v-html` binding if raw
|
|
21
|
+
HTML rendering is genuinely required, and keep the sanitize call next to
|
|
22
|
+
the binding so it cannot be bypassed by a later edit.
|
|
23
|
+
- Prefer interpolation (`{{ value }}`) or bound attributes (`:title="value"`)
|
|
24
|
+
over `v-html` whenever the content is plain text -- both are
|
|
25
|
+
auto-escaped by Vue's compiler, `v-html` is not.
|
|
26
|
+
- Treat a dynamic `:href`/`:src` bound to user-controlled input as
|
|
27
|
+
untrusted: reject or allowlist the scheme (`http:`/`https:`/`mailto:`)
|
|
28
|
+
before binding it, since a `javascript:` URI in an anchor's `href` still
|
|
29
|
+
executes on click.
|
|
30
|
+
- Never pass a user-controlled string into `render()`'s `h()` calls as a
|
|
31
|
+
raw HTML string; `h()` treats string children as text, but a manually
|
|
32
|
+
constructed VNode tree that re-injects raw markup reintroduces the same
|
|
33
|
+
risk `v-html` has.
|
|
34
|
+
|
|
35
|
+
## Routing and SSR
|
|
36
|
+
|
|
37
|
+
- Validate and narrow a Vue Router dynamic segment (`route.params.id`)
|
|
38
|
+
before using it in a query, redirect target, or `v-html`-rendered value
|
|
39
|
+
-- a route param is attacker-controlled input, not a trusted internal
|
|
40
|
+
value.
|
|
41
|
+
- In a server-rendered (Nuxt or custom SSR) app, never read a secret,
|
|
42
|
+
session token, or unsanitized request header directly into a `ref`/
|
|
43
|
+
`reactive()` value that gets serialized into the client-hydrated state --
|
|
44
|
+
anything in the initial state payload is visible in the page source.
|
|
45
|
+
- Guard a navigation guard's redirect target (`next('/somewhere')` or
|
|
46
|
+
`router.push()`) built from a query parameter against open-redirect: only
|
|
47
|
+
redirect to a same-origin, allowlisted path, never an unvalidated
|
|
48
|
+
`redirect` query value passed straight through.
|
|
49
|
+
|
|
50
|
+
## Store and state exposure
|
|
51
|
+
|
|
52
|
+
- Do not store a raw auth token or other secret in a Pinia store field
|
|
53
|
+
that is included in a `persist`/localStorage plugin without explicit
|
|
54
|
+
review -- persisted store state survives in the browser's storage past
|
|
55
|
+
the session and is readable by any script with DOM access on that
|
|
56
|
+
origin.
|
|
57
|
+
- Scope a Pinia store's `$reset()` (or manual reset) to run on logout for
|
|
58
|
+
any store holding user-specific data, so a shared browser session (or a
|
|
59
|
+
test suite reusing app state) does not leak the previous user's data
|
|
60
|
+
into the next session.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
extends: common
|
|
3
|
+
paths: ["**/*.vue"]
|
|
4
|
+
metadata:
|
|
5
|
+
origin: authored
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Vue testing
|
|
9
|
+
|
|
10
|
+
Narrows `core-common-rules`' stack-agnostic testing rules to Vue component
|
|
11
|
+
and composable testing with Vitest and Vue Test Utils. Applies only to
|
|
12
|
+
`*.vue` files' own tests; a plain `.ts` composable's unit tests follow
|
|
13
|
+
`ts-js-node`'s testing rule unless they mount a component.
|
|
14
|
+
|
|
15
|
+
## Layout and runner
|
|
16
|
+
|
|
17
|
+
- Co-locate a component's test next to it (`OrderSummary.vue` /
|
|
18
|
+
`OrderSummary.spec.ts` or the project's existing `__tests__` convention)
|
|
19
|
+
-- match whatever the project already does rather than introducing a
|
|
20
|
+
second layout.
|
|
21
|
+
- Use the project's configured runner (Vitest is standard for Vue 3 /
|
|
22
|
+
Vite projects) and its already-configured DOM environment (`happy-dom`
|
|
23
|
+
or `jsdom`); do not add a second test runner or DOM environment for new
|
|
24
|
+
tests.
|
|
25
|
+
- Use `@vue/test-utils`'s `mount()` for a component that needs its full
|
|
26
|
+
render tree and child components resolved, and `shallowMount()` only
|
|
27
|
+
when child components are irrelevant to the behavior under test and
|
|
28
|
+
stubbing them meaningfully reduces noise.
|
|
29
|
+
|
|
30
|
+
## Querying and interaction
|
|
31
|
+
|
|
32
|
+
- Query by test id, role, or visible text (`wrapper.find('[data-testid=...]')`,
|
|
33
|
+
or `@testing-library/vue`'s `getByRole`/`getByText` when the project uses
|
|
34
|
+
it) instead of a CSS class or DOM structure selector that changes with
|
|
35
|
+
styling.
|
|
36
|
+
- Trigger user interaction through Test Utils' `trigger()` (`await
|
|
37
|
+
wrapper.find(selector).trigger('click')`) or `@testing-library/vue`'s
|
|
38
|
+
`fireEvent`, and `await` it -- Vue's DOM updates are asynchronous, so an
|
|
39
|
+
un-awaited trigger asserts against stale DOM.
|
|
40
|
+
- Assert on rendered output and emitted events (`wrapper.emitted('update')`),
|
|
41
|
+
not on a component's internal reactive state reached through
|
|
42
|
+
`wrapper.vm` -- `vm` access ties the test to implementation details that
|
|
43
|
+
change without changing behavior.
|
|
44
|
+
|
|
45
|
+
## Props, emits, and stores
|
|
46
|
+
|
|
47
|
+
- Pass required props explicitly through `mount(Component, { props: {...}
|
|
48
|
+
})`; do not rely on defaults silently covering a prop the test never
|
|
49
|
+
sets when its value actually matters to the assertion.
|
|
50
|
+
- Assert an emitted event with `wrapper.emitted('eventName')` and check
|
|
51
|
+
the payload array, not just that the event fired -- a wrong payload is a
|
|
52
|
+
real bug an emitted-only check misses.
|
|
53
|
+
- For a component that uses Pinia, create a fresh test Pinia instance per
|
|
54
|
+
test (`createTestingPinia()` from `@pinia/testing`, or `createPinia()` +
|
|
55
|
+
`setActivePinia()`) rather than reusing one instance across tests, so
|
|
56
|
+
store state from one test cannot leak into the next.
|
|
57
|
+
|
|
58
|
+
## Mocking and determinism
|
|
59
|
+
|
|
60
|
+
- Mock network calls at the request boundary (the composable/service
|
|
61
|
+
function, or MSW at the fetch layer) the project already uses, not deep
|
|
62
|
+
inside a component's internals.
|
|
63
|
+
- Use Vitest's fake timers (`vi.useFakeTimers()`) for a component that
|
|
64
|
+
debounces, polls, or delays, and restore real timers afterward
|
|
65
|
+
(`vi.useRealTimers()`) so later tests are not affected.
|
|
66
|
+
- Flush pending reactivity updates with `await nextTick()` (or the
|
|
67
|
+
awaited `trigger()`/`flushPromises()` the project already imports)
|
|
68
|
+
before asserting on DOM that changes after an async composable call
|
|
69
|
+
resolves, instead of adding an arbitrary `setTimeout` wait.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vue-build-fix
|
|
3
|
+
description: "Use when resolving a vue-tsc template type error, an eslint-plugin-vue lint failure, or a Vite build error in a Vue 3 project -- covers template expression type mismatches, defineProps/defineEmits type errors, unresolved SFC/component imports, and Vite plugin/HMR build failures. Applies the smallest root-cause fix and never silences the checker with an any cast or an eslint-disable. Not for a generic tsc error in a .ts file with no .vue involved (use nodejs-build-fix) or a JSX/React build failure (use react-build-fix)."
|
|
4
|
+
triggers:
|
|
5
|
+
- "fix this vue-tsc template type error"
|
|
6
|
+
- "vite build is failing on this vue component"
|
|
7
|
+
- "eslint-plugin-vue is failing on this component"
|
|
8
|
+
- "defineProps type error in this vue component"
|
|
9
|
+
- "cannot find module for this .vue import"
|
|
10
|
+
- "this vue component's template type check is failing"
|
|
11
|
+
metadata:
|
|
12
|
+
origin: authored
|
|
13
|
+
category: build-fix
|
|
14
|
+
version: "1.0.0"
|
|
15
|
+
compatible_harnesses: "claude,codex,cursor,zed,opencode"
|
|
16
|
+
license: "MIT"
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Vue build-fix (vue-tsc / eslint-plugin-vue / Vite)
|
|
20
|
+
|
|
21
|
+
Resolve a `vue-tsc` template type error, an `eslint-plugin-vue` lint
|
|
22
|
+
failure, or a Vite build failure blocking a Vue 3 build. Applies the
|
|
23
|
+
smallest change that fixes the actual root cause. See
|
|
24
|
+
`rules/coding-style.mdc` for the prop/emit typing conventions a correct
|
|
25
|
+
fix should restore.
|
|
26
|
+
|
|
27
|
+
## Workflow
|
|
28
|
+
|
|
29
|
+
### Step 1: Reproduce the failure
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx vue-tsc --noEmit
|
|
33
|
+
npx eslint .
|
|
34
|
+
npx vite build
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Run the project's own `package.json` scripts if they wrap these with
|
|
38
|
+
extra flags (path aliases, multiple tsconfig projects). Capture the
|
|
39
|
+
exact error, including the `.vue` file, line, and (for a template error)
|
|
40
|
+
whether it points inside `<template>` or `<script setup>`.
|
|
41
|
+
|
|
42
|
+
### Step 2: Classify the failure
|
|
43
|
+
|
|
44
|
+
- **Template type error**: `vue-tsc` reports a type mismatch inside
|
|
45
|
+
`<template>` -- a prop/expression type that does not match what the
|
|
46
|
+
template does with it (e.g. calling `.toFixed()` on a value typed
|
|
47
|
+
`string | number`), or a bound event handler with the wrong signature.
|
|
48
|
+
- **`defineProps`/`defineEmits` type error**: the type-only declaration
|
|
49
|
+
itself does not compile (an invalid generic, a prop default that does
|
|
50
|
+
not satisfy `withDefaults`' inferred type).
|
|
51
|
+
- **Import/resolution error**: `Cannot find module '...vue'`, an
|
|
52
|
+
unresolved `@/` alias, or a component registered but never imported.
|
|
53
|
+
- **Lint error**: an `eslint-plugin-vue` rule violation -- read the rule
|
|
54
|
+
name, not just the message.
|
|
55
|
+
- **Vite build error**: a plugin failure, an asset that cannot be
|
|
56
|
+
resolved, or an HMR-only issue that does not reproduce in `vite build`.
|
|
57
|
+
|
|
58
|
+
### Step 3: Find the root cause
|
|
59
|
+
|
|
60
|
+
**Template type errors**: trace the expression's value back to its
|
|
61
|
+
`ref`/`computed`/prop declaration; a union type reaching the template
|
|
62
|
+
usually means the narrowing needs to happen before the template (a
|
|
63
|
+
computed that narrows, or an inline type cast per Vue's documented
|
|
64
|
+
`(x as T)` pattern only when the narrowing is genuinely safe at that
|
|
65
|
+
point) -- not a broadened type on the source declaration that hides a
|
|
66
|
+
real case the template does not otherwise handle.
|
|
67
|
+
|
|
68
|
+
**`defineProps`/`defineEmits` errors**: check the interface/type literal
|
|
69
|
+
passed to the generic; a `withDefaults` default that does not structurally
|
|
70
|
+
satisfy the prop's declared type is usually the type declaration being
|
|
71
|
+
wrong about optionality, not a checker bug.
|
|
72
|
+
|
|
73
|
+
**Import/resolution errors**: check `vite.config.ts`'s `resolve.alias`
|
|
74
|
+
matches `tsconfig.json`'s `paths`, and that the `.vue` extension is
|
|
75
|
+
covered by `moduleFileExtensions`/`vueCompilerOptions` where the runner
|
|
76
|
+
needs it explicitly (Vitest, not Vite itself, which resolves `.vue`
|
|
77
|
+
via its Vue plugin).
|
|
78
|
+
|
|
79
|
+
**Lint errors**: read what the specific `eslint-plugin-vue` rule
|
|
80
|
+
enforces (e.g. `vue/no-mutating-props` wants an emit instead of a prop
|
|
81
|
+
write, not a suppression) before changing the code to satisfy it.
|
|
82
|
+
|
|
83
|
+
### Step 4: Apply the smallest correct fix
|
|
84
|
+
|
|
85
|
+
- A template type error: narrow the value correctly (a `computed()` that
|
|
86
|
+
returns the narrowed type, or a scoped inline cast at the point the
|
|
87
|
+
narrowing is actually safe) -- not a broadened prop/ref type that
|
|
88
|
+
silently accepts a case the template cannot actually handle.
|
|
89
|
+
- A `defineProps`/`defineEmits` error: correct the type literal or the
|
|
90
|
+
`withDefaults` default so it genuinely matches -- not a cast to `any`
|
|
91
|
+
on the destructured value.
|
|
92
|
+
- An import/resolution error: fix the alias/extension config that
|
|
93
|
+
genuinely matches the project's build target -- not a blanket
|
|
94
|
+
`skipLibCheck`/`moduleResolution` change that papers over the real
|
|
95
|
+
mismatch.
|
|
96
|
+
- A lint error: change the code to satisfy the rule's actual intent
|
|
97
|
+
(e.g. replace a direct prop mutation with an emit for
|
|
98
|
+
`vue/no-mutating-props`).
|
|
99
|
+
|
|
100
|
+
### Step 5: Verify and report
|
|
101
|
+
|
|
102
|
+
Re-run the exact command from Step 1; confirm it exits 0. Report the
|
|
103
|
+
root cause and the fix, not just "error resolved".
|
|
104
|
+
|
|
105
|
+
## Rules
|
|
106
|
+
|
|
107
|
+
- Follow `rules/coding-style.mdc` for the prop/emit typing conventions a
|
|
108
|
+
fix should restore, not just silence.
|
|
109
|
+
- ALWAYS fix the root cause with the smallest change; never widen the
|
|
110
|
+
fix beyond the file(s) the failure actually touches.
|
|
111
|
+
- NEVER use `@ts-ignore`, a cast to `any`, or an `eslint-disable` comment
|
|
112
|
+
on a `.vue` file's `<script>` or template expression to make an error
|
|
113
|
+
disappear.
|
|
114
|
+
- NEVER mutate a prop in place as a "quick fix" for a `vue/no-mutating-
|
|
115
|
+
props` lint failure -- add the emit the rule is asking for.
|
|
116
|
+
- NEVER delete or skip a failing test to turn the build green.
|
|
117
|
+
|
|
118
|
+
## Red Flags
|
|
119
|
+
|
|
120
|
+
| Rationalization | Why it is wrong |
|
|
121
|
+
|---|---|
|
|
122
|
+
| "I'll cast this template expression `(x as any).toFixed(2)` to unblock vue-tsc" | Erases the type entirely at that call site; the union the checker flagged usually needs an actual narrowing branch, not a blanket escape hatch |
|
|
123
|
+
| "eslint-disable this `vue/no-mutating-props` line, the mutation is harmless here" | The rule exists because a prop mutation is invisible to the parent and Vue warns about it at runtime too; add the emit the rule is asking for instead of suppressing it |
|
|
124
|
+
| "I'll widen the prop's type to `any` so `defineProps` stops complaining" | Removes type checking from every consumer of that prop, not just this one call site |
|
|
125
|
+
| "This `.vue` import fails to resolve, I'll just add `skipLibCheck: true` and move on" | `skipLibCheck` skips checking `.d.ts` files, it does not fix a genuinely broken alias or missing extension resolution -- the import still fails at runtime |
|
|
126
|
+
|
|
127
|
+
## Verification
|
|
128
|
+
|
|
129
|
+
Do not report the fix done until all of the following hold:
|
|
130
|
+
|
|
131
|
+
- The exact command that reproduced the failure in Step 1 now exits 0.
|
|
132
|
+
- No `@ts-ignore`, `any` cast, or `eslint-disable` was added to the
|
|
133
|
+
`.vue` file.
|
|
134
|
+
- No unrelated `tsconfig.json`/`vite.config.ts`/`eslint` config was
|
|
135
|
+
loosened.
|
|
136
|
+
- A `vue/no-mutating-props` fix added an emit, not a suppression.
|
|
137
|
+
- The report states the actual root cause, not just "fixed the error".
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"triggers": {
|
|
3
|
+
"positive": [
|
|
4
|
+
"our CI pipeline breaks on a type mismatch coming from vue-tsc, pointing at an expression inside this component's template block -- how do I resolve it",
|
|
5
|
+
"vite refuses to bundle this vue single file component because it can't resolve one of the imports inside it",
|
|
6
|
+
"our vue.js CI job fails because eslint's no-mutating-props check rejects a change in this component",
|
|
7
|
+
"defineProps type error, generic doesn't match the props being passed",
|
|
8
|
+
"vite's build step can't locate this single file component when something else imports it, and throws a not found error",
|
|
9
|
+
"vue template type check fails calling toFixed on this value"
|
|
10
|
+
],
|
|
11
|
+
"negative": [
|
|
12
|
+
"fix this tsc error in a plain typescript service file",
|
|
13
|
+
"fix this tsx build error in this react component",
|
|
14
|
+
"review this vue component diff for reactivity bugs",
|
|
15
|
+
"write a vitest test for this vue component",
|
|
16
|
+
"implement a new vue component with typed props and emits",
|
|
17
|
+
"migrate this vue 2 component to vue 3 composition api"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"scenarios": [
|
|
21
|
+
{
|
|
22
|
+
"id": "no-mutating-props-lint-fix",
|
|
23
|
+
"prompt": "eslint-plugin-vue reports `vue/no-mutating-props` on this line inside a child component: `props.count = props.count + 1`. What's the right fix?",
|
|
24
|
+
"strictness": "high",
|
|
25
|
+
"expected_behavior": [
|
|
26
|
+
{
|
|
27
|
+
"grader": "judge",
|
|
28
|
+
"rubric": "A correct answer fixes the vue/no-mutating-props violation by replacing the direct prop write with an emitted event so the parent updates its own state, rather than suppressing the rule.",
|
|
29
|
+
"pass_criteria": [
|
|
30
|
+
"States that the prop must not be written to directly and the fix is to emit an event for the parent to update the value instead",
|
|
31
|
+
"Shows the fix as code: a defineEmits declaration and the emit() call (e.g. emit('update:count', props.count + 1)) replacing the direct assignment"
|
|
32
|
+
],
|
|
33
|
+
"fail_criteria": [
|
|
34
|
+
"Recommends adding an eslint-disable comment (line or file level) for vue/no-mutating-props instead of removing the direct prop write. Mentioning eslint-disable only to say it is the wrong move does not count as this failure."
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"calibration": {
|
|
39
|
+
"known_right": "`vue/no-mutating-props` is correct here: `props.count = props.count + 1` writes directly to a prop, which is one-way data from the parent and Vue warns about at runtime too. Fix it by emitting an event and letting the parent own the update:\n```ts\nconst emit = defineEmits<{ 'update:count': [value: number] }>()\nfunction increment() {\n emit('update:count', props.count + 1)\n}\n```\nIn the parent, bind `v-model:count=\"count\"` (or listen for `@update:count`) so the actual state change happens where `count` is owned. Re-run eslint to confirm the rule is now satisfied without a suppression.",
|
|
40
|
+
"known_wrong": "Easiest way to unblock this: add `// eslint-disable-next-line vue/no-mutating-props` above the line. The mutation is harmless in this component since nothing else reads `count` from the parent side right now.",
|
|
41
|
+
"vague": "Don't mutate the prop, emit an event instead so the parent handles the update.",
|
|
42
|
+
"subtle_wrong": "I'd copy the prop into local component state on mount and increment that local copy instead: `const localCount = ref(props.count); function increment() { localCount.value++ }`, then render `localCount` in the template. That way nothing writes to `props.count` anymore and the lint rule passes."
|
|
43
|
+
},
|
|
44
|
+
"anti_patterns": ["eslint-disable"]
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"id": "template-union-type-narrowing",
|
|
48
|
+
"prompt": "vue-tsc reports a template type error: `Property 'toFixed' does not exist on type 'string'` on `{{ total.toFixed(2) }}`, where `total` is typed `ref<string | number>`. How do I fix this without breaking the type check?",
|
|
49
|
+
"strictness": "high",
|
|
50
|
+
"expected_behavior": [
|
|
51
|
+
{
|
|
52
|
+
"grader": "judge",
|
|
53
|
+
"rubric": "A correct answer explains that the template calls a number-only method on a value that can also be a string, and fixes it so the rendered result is consistently a formatted number in both cases -- either via a computed that converts the value to a number before formatting, or converting the value at its source -- rather than casting the expression to bypass the checker or narrowing only enough to satisfy the type checker while leaving the string case unformatted.",
|
|
54
|
+
"pass_criteria": [
|
|
55
|
+
"Explains that `total` being typed `string | number` means the template cannot assume it's a number, and the fix converts/narrows it before the template uses `.toFixed`",
|
|
56
|
+
"Shows a concrete fix as code that produces a consistently formatted number for BOTH the string and number cases, e.g. `computed(() => Number(total.value).toFixed(2))`, or converting the value at its source so it is genuinely a `number` at that point"
|
|
57
|
+
],
|
|
58
|
+
"fail_criteria": [
|
|
59
|
+
"Recommends bypassing the check with a template cast to `any` (e.g. `(total as any).toFixed(2)`) or a `@ts-ignore`-equivalent instead of narrowing the type",
|
|
60
|
+
"Narrows only enough to satisfy the type checker (e.g. a template `typeof total === 'number' ? total.toFixed(2) : total` ternary) while leaving the string branch rendered as the raw unformatted string instead of a consistently formatted number"
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"calibration": {
|
|
65
|
+
"known_right": "The template error is real: `total` is typed `ref<string | number>`, and `.toFixed()` only exists on `number`, so calling it directly on a value that could be a string is a genuine mismatch vue-tsc is right to catch. Rather than casting past it, narrow the value before the template touches it -- a computed that guarantees a number:\n```ts\nconst formattedTotal = computed(() => Number(total.value).toFixed(2))\n```\n```vue\n<template>{{ formattedTotal }}</template>\n```\nIf `total` should genuinely always be a number by the time it reaches the template, the better fix is at the source: change wherever it's assigned a string to convert it there instead, and narrow the ref's type to `ref<number>`.",
|
|
66
|
+
"known_wrong": "Quickest fix: change the template to `{{ (total as any).toFixed(2) }}`. That satisfies vue-tsc immediately and the runtime behavior doesn't change either way.",
|
|
67
|
+
"vague": "You need to make sure total is a number before calling toFixed on it in the template.",
|
|
68
|
+
"subtle_wrong": "I'd add a template-level check instead: `{{ typeof total === 'number' ? total.toFixed(2) : total }}`. This satisfies the type checker's narrowing inside the ternary and avoids a cast, without needing to add a computed property."
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|