@mrciphersmith/keryx 0.3.0 → 0.3.2

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.
Files changed (93) hide show
  1. package/README.md +4 -1
  2. package/dist/cli.js +13362 -7078
  3. package/dist/core.js +11706 -11330
  4. package/package.json +1 -1
  5. package/src/gdskills/bundled/agents/go-code-auditor.md +1 -1
  6. package/src/gdskills/bundled/agents/python-code-auditor.md +1 -1
  7. package/src/gdskills/bundled/install-manifest.json +271 -4
  8. package/src/gdskills/bundled/rules/core/model-selection.mdc +51 -0
  9. package/src/gdskills/bundled/skills/review/review-jev-comments/SKILL.md +184 -0
  10. package/src/gdskills/bundled/skills/review/review-jev-docs/SKILL.md +189 -0
  11. package/src/gdskills/bundled/skills/review/review-jev-risk/SKILL.md +190 -0
  12. package/src/gdskills/bundled/skills/review/review-jev-rules/SKILL.md +267 -0
  13. package/src/gdskills/bundled/skills/review/review-jev-scenarios/SKILL.md +187 -0
  14. package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.detail.md +39 -0
  15. package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.md +1 -1
  16. package/src/gdskills/bundled/stacks/angular/agent-refs.json +4 -0
  17. package/src/gdskills/bundled/stacks/angular/governance/eval.json +1751 -0
  18. package/src/gdskills/bundled/stacks/angular/governance/scout.json +32 -0
  19. package/src/gdskills/bundled/stacks/angular/pack.json +55 -0
  20. package/src/gdskills/bundled/stacks/angular/rules/coding-style.mdc +82 -0
  21. package/src/gdskills/bundled/stacks/angular/rules/patterns.mdc +84 -0
  22. package/src/gdskills/bundled/stacks/angular/rules/security.mdc +70 -0
  23. package/src/gdskills/bundled/stacks/angular/rules/testing.mdc +73 -0
  24. package/src/gdskills/bundled/stacks/angular/skills/angular-build-fix/SKILL.md +127 -0
  25. package/src/gdskills/bundled/stacks/angular/skills/angular-build-fix/evals.json +72 -0
  26. package/src/gdskills/bundled/stacks/angular/skills/angular-code-review/SKILL.md +98 -0
  27. package/src/gdskills/bundled/stacks/angular/skills/angular-code-review/evals.json +73 -0
  28. package/src/gdskills/bundled/stacks/angular/skills/angular-implementation/SKILL.md +112 -0
  29. package/src/gdskills/bundled/stacks/angular/skills/angular-implementation/evals.json +74 -0
  30. package/src/gdskills/bundled/stacks/angular/skills/angular-testing/SKILL.md +102 -0
  31. package/src/gdskills/bundled/stacks/angular/skills/angular-testing/evals.json +71 -0
  32. package/src/gdskills/bundled/stacks/mobx/agent-refs.json +4 -0
  33. package/src/gdskills/bundled/stacks/mobx/governance/eval.json +904 -0
  34. package/src/gdskills/bundled/stacks/mobx/governance/scout.json +18 -0
  35. package/src/gdskills/bundled/stacks/mobx/pack.json +28 -0
  36. package/src/gdskills/bundled/stacks/mobx/rules/coding-style.mdc +91 -0
  37. package/src/gdskills/bundled/stacks/mobx/rules/patterns.mdc +122 -0
  38. package/src/gdskills/bundled/stacks/mobx/rules/security.mdc +56 -0
  39. package/src/gdskills/bundled/stacks/mobx/rules/testing.mdc +63 -0
  40. package/src/gdskills/bundled/stacks/mobx/skills/mobx-observable-testing/SKILL.md +124 -0
  41. package/src/gdskills/bundled/stacks/mobx/skills/mobx-observable-testing/evals.json +73 -0
  42. package/src/gdskills/bundled/stacks/mobx/skills/mobx-store-implementation/SKILL.md +149 -0
  43. package/src/gdskills/bundled/stacks/mobx/skills/mobx-store-implementation/evals.json +74 -0
  44. package/src/gdskills/bundled/stacks/nestjs/agent-refs.json +4 -0
  45. package/src/gdskills/bundled/stacks/nestjs/governance/eval.json +1308 -0
  46. package/src/gdskills/bundled/stacks/nestjs/governance/scout.json +34 -0
  47. package/src/gdskills/bundled/stacks/nestjs/pack.json +53 -0
  48. package/src/gdskills/bundled/stacks/nestjs/rules/coding-style.mdc +70 -0
  49. package/src/gdskills/bundled/stacks/nestjs/rules/patterns.mdc +83 -0
  50. package/src/gdskills/bundled/stacks/nestjs/rules/security.mdc +73 -0
  51. package/src/gdskills/bundled/stacks/nestjs/rules/testing.mdc +69 -0
  52. package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-build-fix/SKILL.md +157 -0
  53. package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-build-fix/evals.json +70 -0
  54. package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-implementation/SKILL.md +129 -0
  55. package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-implementation/evals.json +71 -0
  56. package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-testing/SKILL.md +143 -0
  57. package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-testing/evals.json +69 -0
  58. package/src/gdskills/bundled/stacks/nextjs-nuxt/agent-refs.json +4 -0
  59. package/src/gdskills/bundled/stacks/nextjs-nuxt/governance/eval.json +2413 -0
  60. package/src/gdskills/bundled/stacks/nextjs-nuxt/governance/scout.json +42 -0
  61. package/src/gdskills/bundled/stacks/nextjs-nuxt/pack.json +42 -0
  62. package/src/gdskills/bundled/stacks/nextjs-nuxt/rules/coding-style.mdc +69 -0
  63. package/src/gdskills/bundled/stacks/nextjs-nuxt/rules/patterns.mdc +88 -0
  64. package/src/gdskills/bundled/stacks/nextjs-nuxt/rules/security.mdc +72 -0
  65. package/src/gdskills/bundled/stacks/nextjs-nuxt/rules/testing.mdc +64 -0
  66. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-build-fix/SKILL.md +147 -0
  67. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-build-fix/evals.json +75 -0
  68. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-code-review/SKILL.md +118 -0
  69. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-code-review/evals.json +76 -0
  70. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-implementation/SKILL.md +135 -0
  71. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-implementation/evals.json +78 -0
  72. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-testing/SKILL.md +116 -0
  73. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-testing/evals.json +75 -0
  74. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-upgrade-migration/SKILL.md +134 -0
  75. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-upgrade-migration/evals.json +76 -0
  76. package/src/gdskills/bundled/stacks/vue/agent-refs.json +4 -0
  77. package/src/gdskills/bundled/stacks/vue/governance/eval.json +2215 -0
  78. package/src/gdskills/bundled/stacks/vue/governance/scout.json +42 -0
  79. package/src/gdskills/bundled/stacks/vue/pack.json +42 -0
  80. package/src/gdskills/bundled/stacks/vue/rules/coding-style.mdc +73 -0
  81. package/src/gdskills/bundled/stacks/vue/rules/patterns.mdc +84 -0
  82. package/src/gdskills/bundled/stacks/vue/rules/security.mdc +60 -0
  83. package/src/gdskills/bundled/stacks/vue/rules/testing.mdc +69 -0
  84. package/src/gdskills/bundled/stacks/vue/skills/vue-build-fix/SKILL.md +137 -0
  85. package/src/gdskills/bundled/stacks/vue/skills/vue-build-fix/evals.json +72 -0
  86. package/src/gdskills/bundled/stacks/vue/skills/vue-code-review/SKILL.md +120 -0
  87. package/src/gdskills/bundled/stacks/vue/skills/vue-code-review/evals.json +71 -0
  88. package/src/gdskills/bundled/stacks/vue/skills/vue-implementation/SKILL.md +122 -0
  89. package/src/gdskills/bundled/stacks/vue/skills/vue-implementation/evals.json +72 -0
  90. package/src/gdskills/bundled/stacks/vue/skills/vue-testing/SKILL.md +115 -0
  91. package/src/gdskills/bundled/stacks/vue/skills/vue-testing/evals.json +72 -0
  92. package/src/gdskills/bundled/stacks/vue/skills/vue2-to-vue3-migration/SKILL.md +135 -0
  93. package/src/gdskills/bundled/stacks/vue/skills/vue2-to-vue3-migration/evals.json +71 -0
@@ -0,0 +1,2215 @@
1
+ {
2
+ "schemaVersion": "1.0.0",
3
+ "reports": [
4
+ {
5
+ "schemaVersion": "1.0.0",
6
+ "skillId": "vue/vue-implementation",
7
+ "strictness": "high",
8
+ "trials": 10,
9
+ "triggerAccuracy": {
10
+ "truePositive": 6,
11
+ "falsePositive": 0,
12
+ "positives": 6,
13
+ "negatives": 6
14
+ },
15
+ "evidence": "authored",
16
+ "scenarios": [
17
+ {
18
+ "id": "trigger-positive-1",
19
+ "kind": "trigger-positive",
20
+ "prompt": "I need a new vue single file component that takes some typed props from its parent and emits an event back up when something happens",
21
+ "strictness": "high",
22
+ "trials": 1,
23
+ "passes": 1,
24
+ "passRate": 1,
25
+ "passAtK": 1,
26
+ "grader": "trigger-rank-fork-family",
27
+ "status": "ran",
28
+ "deterministic": true
29
+ },
30
+ {
31
+ "id": "trigger-positive-2",
32
+ "kind": "trigger-positive",
33
+ "prompt": "add a pinia store action to update the cart",
34
+ "strictness": "high",
35
+ "trials": 1,
36
+ "passes": 1,
37
+ "passRate": 1,
38
+ "passAtK": 1,
39
+ "grader": "trigger-rank-fork-family",
40
+ "status": "ran",
41
+ "deterministic": true
42
+ },
43
+ {
44
+ "id": "trigger-positive-3",
45
+ "kind": "trigger-positive",
46
+ "prompt": "for this piece of local state in a vue component, is ref() or reactive() the better call here",
47
+ "strictness": "high",
48
+ "trials": 1,
49
+ "passes": 1,
50
+ "passRate": 1,
51
+ "passAtK": 1,
52
+ "grader": "trigger-rank-fork-family",
53
+ "status": "ran",
54
+ "deterministic": true
55
+ },
56
+ {
57
+ "id": "trigger-positive-4",
58
+ "kind": "trigger-positive",
59
+ "prompt": "two of my vue components need the same piece of reactive logic -- how do I pull it out into a composable so they can both use it",
60
+ "strictness": "high",
61
+ "trials": 1,
62
+ "passes": 1,
63
+ "passRate": 1,
64
+ "passAtK": 1,
65
+ "grader": "trigger-rank-fork-family",
66
+ "status": "ran",
67
+ "deterministic": true
68
+ },
69
+ {
70
+ "id": "trigger-positive-5",
71
+ "kind": "trigger-positive",
72
+ "prompt": "I want typing into this vue form's input to update the parent component's data automatically through v-model -- how do I wire that up",
73
+ "strictness": "high",
74
+ "trials": 1,
75
+ "passes": 1,
76
+ "passRate": 1,
77
+ "passAtK": 1,
78
+ "grader": "trigger-rank-fork-family",
79
+ "status": "ran",
80
+ "deterministic": true
81
+ },
82
+ {
83
+ "id": "trigger-positive-6",
84
+ "kind": "trigger-positive",
85
+ "prompt": "implement this vue 3 script setup component",
86
+ "strictness": "high",
87
+ "trials": 1,
88
+ "passes": 1,
89
+ "passRate": 1,
90
+ "passAtK": 1,
91
+ "grader": "trigger-rank-fork-family",
92
+ "status": "ran",
93
+ "deterministic": true
94
+ },
95
+ {
96
+ "id": "trigger-negative-1",
97
+ "kind": "trigger-negative",
98
+ "prompt": "write a react component with props and state",
99
+ "strictness": "high",
100
+ "trials": 1,
101
+ "passes": 1,
102
+ "passRate": 1,
103
+ "passAtK": 1,
104
+ "grader": "trigger-rank-fork-family",
105
+ "status": "ran",
106
+ "deterministic": true
107
+ },
108
+ {
109
+ "id": "trigger-negative-2",
110
+ "kind": "trigger-negative",
111
+ "prompt": "fix this vue-tsc type error blocking the build",
112
+ "strictness": "high",
113
+ "trials": 1,
114
+ "passes": 1,
115
+ "passRate": 1,
116
+ "passAtK": 1,
117
+ "grader": "trigger-rank-fork-family",
118
+ "status": "ran",
119
+ "deterministic": true
120
+ },
121
+ {
122
+ "id": "trigger-negative-3",
123
+ "kind": "trigger-negative",
124
+ "prompt": "review this vue component diff for reactivity bugs",
125
+ "strictness": "high",
126
+ "trials": 1,
127
+ "passes": 1,
128
+ "passRate": 1,
129
+ "passAtK": 1,
130
+ "grader": "trigger-rank-fork-family",
131
+ "status": "ran",
132
+ "deterministic": true
133
+ },
134
+ {
135
+ "id": "trigger-negative-4",
136
+ "kind": "trigger-negative",
137
+ "prompt": "write a vitest test for this vue component",
138
+ "strictness": "high",
139
+ "trials": 1,
140
+ "passes": 1,
141
+ "passRate": 1,
142
+ "passAtK": 1,
143
+ "grader": "trigger-rank-fork-family",
144
+ "status": "ran",
145
+ "deterministic": true
146
+ },
147
+ {
148
+ "id": "trigger-negative-5",
149
+ "kind": "trigger-negative",
150
+ "prompt": "migrate this vue 2 options api component to vue 3",
151
+ "strictness": "high",
152
+ "trials": 1,
153
+ "passes": 1,
154
+ "passRate": 1,
155
+ "passAtK": 1,
156
+ "grader": "trigger-rank-fork-family",
157
+ "status": "ran",
158
+ "deterministic": true
159
+ },
160
+ {
161
+ "id": "trigger-negative-6",
162
+ "kind": "trigger-negative",
163
+ "prompt": "implement this node.js CLI command",
164
+ "strictness": "high",
165
+ "trials": 1,
166
+ "passes": 1,
167
+ "passRate": 1,
168
+ "passAtK": 1,
169
+ "grader": "trigger-rank-fork-family",
170
+ "status": "ran",
171
+ "deterministic": true
172
+ },
173
+ {
174
+ "id": "prop-mutation-vs-emit",
175
+ "kind": "behavior",
176
+ "prompt": "A Vue child component has an 'items' prop for a todo list. A button handler currently does `props.items.push(newItem)` to add an item. What should this do instead so the parent's data stays in sync?",
177
+ "strictness": "high",
178
+ "trials": 10,
179
+ "passes": 10,
180
+ "passRate": 1,
181
+ "passAtK": 1,
182
+ "grader": "judge",
183
+ "status": "ran",
184
+ "trialRecords": [
185
+ {
186
+ "output": "The child should never mutate `props.items` in place. Emit an event with the new item and let the parent — which owns the array — do the mutation.\n\n**Child component:**\n\n```vue\n<script setup lang=\"ts\">\nconst props = defineProps<{ items: Todo[] }>();\n\nconst emit = defineEmits<{\n add: [item: Todo];\n}>();\n\nfunction handleAdd() {\n emit('add', { id: crypto.randomUUID(), text: 'New todo' });\n}\n</script>\n\n<template>\n <button @click=\"handleAdd\">Add</button>\n</template>\n```\n\n**Parent component:**\n\n```vue\n<script setup lang=\"ts\">\nimport { ref } from 'vue';\n\nconst items = ref<Todo[]>([]);\n\nfunction addItem(item: Todo) {\n items.value.push(item);\n}\n</script>\n\n<template>\n <TodoList :items=\"items\" @add=\"addItem\" />\n</template>\n```\n\nWhy the original line is a problem: Vue warns at runtime about mutating a prop, and more importantly the parent's own `items` state is now being changed by a child it doesn't control. The parent's data silently drifts, any parent-side logic that should have run on an add (persisting to a store, re-deriving a count, triggering a fetch) is skipped, and the mutation is invisible to anyone reading the parent. The change also won't survive a parent re-render that resets the array from its own source of truth.\n\nA few adjacent notes:\n\n- If the todo list is shared across unrelated parts of the tree, or needs to persist, move the array into a Pinia store and have both components call a store action instead of passing events up through intermediate components (\"prop drilling\" through layers that don't care about the data).\n- For a form-like child that owns an input value, use `v-model` semantics: declare `modelValue` as a prop and emit `update:modelValue`, rather than a bespoke `add` event, when the pattern is genuinely two-way binding.\n- Keep the emit type-only (`defineEmits<{ add: [item: Todo] }>()`) to match the project's `<script setup lang=\"ts\">` conventions, and run `vue-tsc --noEmit` and `eslint .` afterward to confirm the event payload type lines up on both sides.",
187
+ "outputSha256": "e97752fe28fb989eb46c61fbda6b834ffe8ccd226dcd84b88bec202b267b369b",
188
+ "promptSha256": "cd0871ecaccef3176ce14d159e364c4efca26c473281f2c19ccf06a97a41620b",
189
+ "deterministic": [],
190
+ "judge": {
191
+ "verdict": "pass",
192
+ "reason": "Answer states 'The child should never mutate `props.items` in place' and explains props are parent-owned/one-way. Shows concrete code: `const emit = defineEmits<{ add: [item: Todo] }>();` and `emit('add', { id: crypto.randomUUID(), text: 'New todo' });` passing the new item, plus parent handler. No fail criterion met."
193
+ },
194
+ "passed": true
195
+ },
196
+ {
197
+ "output": "The child should not mutate `props.items` at all. `props` is a one-way binding from parent to child, and mutating it directly (even the array contents via `.push()`) triggers Vue's \"Avoid mutating a prop directly\" warning and, more importantly, breaks the contract that the parent owns its own data.\n\nInstead, the child should emit an event and let the parent perform the mutation.\n\n**Pattern: emit a generic \"add\" event**\n\nChild component:\n\n```vue\n<script setup lang=\"ts\">\nconst props = defineProps<{\n items: TodoItem[]\n}>()\n\nconst emit = defineEmits<{\n (e: 'add', item: TodoItem): void\n}>()\n\nfunction handleAdd() {\n const newItem: TodoItem = { id: crypto.randomUUID(), text: '...' }\n emit('add', newItem)\n}\n</script>\n\n<template>\n <button @click=\"handleAdd\">Add</button>\n</template>\n```\n\nParent component:\n\n```vue\n<script setup lang=\"ts\">\nimport { ref } from 'vue'\n\nconst items = ref<TodoItem[]>([])\n\nfunction onAdd(item: TodoItem) {\n items.value.push(item)\n}\n</script>\n\n<template>\n <TodoList :items=\"items\" @add=\"onAdd\" />\n</template>\n```\n\n**Pattern: `v-model` / `update:modelValue`**\n\nIf the prop is really the model value (as `items` here often is), use the `v-model` convention so the parent gets two-way binding sugar:\n\nChild:\n\n```vue\n<script setup lang=\"ts\">\nconst props = defineProps<{ modelValue: TodoItem[] }>()\nconst emit = defineEmits<{ (e: 'update:modelValue', value: TodoItem[]): void }>()\n\nfunction handleAdd(newItem: TodoItem) {\n emit('update:modelValue', [...props.modelValue, newItem])\n}\n</script>\n```\n\nParent:\n\n```vue\n<TodoList v-model=\"items\" />\n```\n\nKey points:\n\n- The parent is the single source of truth for `items`; the child only reports intent through an event.\n- When you do need a new array, emit a new array (spread/`concat`) rather than pushing into the prop's array, so you never touch the parent's reference.\n- If the list is shared across unrelated parts of the tree (not just this parent/child pair), put it in a Pinia store and have the child call a store action instead of emitting. That action performs the mutation, and every consumer of the store stays in sync automatically.\n- Never keep a local `const { items } = props` copy and mutate that either; destructuring a prop (pre-3.5) or a `reactive()` object severs the reactive link and the child silently stops updating.",
198
+ "outputSha256": "9fe2ae9d94547b958726f68d25c0af98e222025de10fb8ed65c4f71ab6928a52",
199
+ "promptSha256": "cd0871ecaccef3176ce14d159e364c4efca26c473281f2c19ccf06a97a41620b",
200
+ "deterministic": [],
201
+ "judge": {
202
+ "verdict": "pass",
203
+ "reason": "Answer explicitly says not to mutate props.items (one-way binding, parent owns data) and shows code: defineEmits<{(e:'add', item:TodoItem):void}>() with emit('add', newItem), plus update:modelValue variant emitting a new array. No fail criterion holds."
204
+ },
205
+ "passed": true
206
+ },
207
+ {
208
+ "output": "The child should not mutate the prop at all. Instead, it emits an event carrying the new item, and the parent — which owns `items` — performs the mutation in its handler.\n\nChild component:\n\n```vue\n<script setup lang=\"ts\">\nimport type { Todo } from './types'\n\nconst props = defineProps<{ items: Todo[] }>()\nconst emit = defineEmits<{\n addItem: [item: Todo]\n 'update:items': [items: Todo[]]\n}>()\n\nfunction handleAdd() {\n const newItem = { id: crypto.randomUUID(), text: 'New todo', done: false }\n // Either emit the new item and let the parent push...\n emit('addItem', newItem)\n // ...or emit a brand-new array for the parent to adopt.\n emit('update:items', [...props.items, newItem])\n}\n</script>\n```\n\nParent:\n\n```vue\n<script setup lang=\"ts\">\nimport { ref } from 'vue'\n\nconst items = ref<Todo[]>([/* ... */])\n\nfunction onAddItem(item: Todo) {\n items.value = [...items.value, item]\n}\n</script>\n\n<template>\n <TodoList :items=\"items\" @add-item=\"onAddItem\" />\n</template>\n```\n\nKey points:\n\n- `props.items.push(...)` triggers Vue's \"Avoid mutating a prop directly\" warning at runtime. Even when it appears to work (because the parent passed a reference to an array it also holds), the mutation is invisible to anyone reading the parent: the parent's own code never sees an explicit state change, devtools can't trace who changed it, and any read-only/`shallowReadonly` caller breaks outright.\n- Two idiomatic shapes: emit a discrete event (`add-item` with the new item) so the parent appends, or emit the whole replacement array via `update:items` so `v-model:items` works on the parent side.\n- Inside the parent's handler, do the append immutably (`items.value = [...items.value, item]`) rather than in-place `push`. This keeps the change trackable and avoids surprises if the array is later passed somewhere that compares by reference (`watch`, `computed`, `shallowRef`, memoized child props).\n- If several distant components need to add to this same list, that is the signal to move the list into a Pinia store and call a store action (`todoStore.addItem(newItem)`) instead of routing events through intermediate parents. Either way, the component rendering the list is never the one mutating it.",
209
+ "outputSha256": "b056a4ea83ad1329a6a4377b23a90447b9f329df592800a667fb262eae6d2eee",
210
+ "promptSha256": "cd0871ecaccef3176ce14d159e364c4efca26c473281f2c19ccf06a97a41620b",
211
+ "deterministic": [],
212
+ "judge": {
213
+ "verdict": "pass",
214
+ "reason": "Answer explicitly says the child should not mutate the prop and that props.items.push triggers Vue's warning (one-way flow). It shows concrete code: defineEmits<{ addItem: [item: Todo]; 'update:items': [items: Todo[]] }>() and emit('addItem', newItem) / emit('update:items', [...props.items, newItem]) passing the new item, plus parent handler. No fail criterion met."
215
+ },
216
+ "passed": true
217
+ },
218
+ {
219
+ "output": "Instead of mutating the prop in place, the child should treat `items` as read-only and emit an event that asks the parent to perform the addition. The parent owns the array, so it (or the store the parent is wired to) is the only place that should call `push`.\n\nConcretely, the child declares an emit for the add operation and the handler emits the new item rather than pushing it:\n\n```vue\n<script setup lang=\"ts\">\nconst props = defineProps<{ items: Todo[] }>()\nconst emit = defineEmits<{ add: [item: Todo] }>()\n\nfunction addItem(newItem: Todo) {\n emit('add', newItem)\n}\n</script>\n```\n\nThe parent then listens and mutates its own data:\n\n```vue\n<template>\n <TodoList :items=\"todos\" @add=\"onAdd\" />\n</template>\n\n<script setup lang=\"ts\">\nconst todos = ref<Todo[]>([])\nfunction onAdd(item: Todo) {\n todos.value.push(item)\n}\n</script>\n```\n\nWhy the current code is wrong: mutating `props.items.push(...)` triggers Vue's runtime prop-mutation warning, and while the array's contents technically change (arrays are passed by reference, so the parent's array object is the same one), the write bypasses the parent's ownership of its own state. Anyone reading the parent never sees where the item came from, and if the parent later switches the prop to a store getter, a computed, or a frozen/readonly value, the push either has no effect or throws — a silent, delayed break.\n\nIf the item being added is meant to drive a form control instead of a plain list append, the canonical shape is `v-model` with the `update:modelValue` emit:\n\n```ts\nconst emit = defineEmits<{ 'update:modelValue': [value: Todo[]] }>()\nemit('update:modelValue', [...props.items, newItem])\n```\n\nNote this last form creates a new array rather than mutating, which is also the safer habit for any `ref` holding an array, since it sidesteps the question of whether some other reference to the old array is still in play.\n\nIf the todo list is app-wide rather than scoped to one parent, the better move is neither emit nor prop mutation: put `items` and an `addItem` action in a Pinia store, and have the child call `todoStore.addItem(newItem)`. The store owns the mutation; the child just requests it.\n\nEither way, the rule is the same: a child never writes to a prop, it emits an event or calls a store action, and the owner of the state performs the change.",
220
+ "outputSha256": "384b4e687b16f43e044a0ccba56b17cbb9961f414fd53a7f933f156ca38d3678",
221
+ "promptSha256": "cd0871ecaccef3176ce14d159e364c4efca26c473281f2c19ccf06a97a41620b",
222
+ "deterministic": [],
223
+ "judge": {
224
+ "verdict": "pass",
225
+ "reason": "Answer explicitly says not to mutate the prop (\"Instead of mutating the prop in place, the child should treat `items` as read-only\") and explains one-way ownership. It shows concrete code: `const emit = defineEmits<{ add: [item: Todo] }>()` and `emit('add', newItem)`, plus parent handler. No fail criterion holds."
226
+ },
227
+ "passed": true
228
+ },
229
+ {
230
+ "output": "The child should not mutate the prop at all — it should emit an event and let the parent perform the mutation.\n\n**Change in the child:**\n\n```vue\n<script setup lang=\"ts\">\nconst props = defineProps<{ items: Todo[] }>()\nconst emit = defineEmits<{ add: [item: Todo] }>()\n\nfunction onAdd() {\n emit('add', newItem)\n}\n</script>\n```\n\n**Change in the parent:**\n\n```vue\n<script setup lang=\"ts\">\nimport { ref } from 'vue'\nconst items = ref<Todo[]>([])\n\nfunction addItem(item: Todo) {\n items.value.push(item)\n}\n</script>\n\n<template>\n <TodoList :items=\"items\" @add=\"addItem\" />\n</template>\n```\n\nWhy `props.items.push(...)` is wrong:\n\n- Vue emits a runtime warning (\"Unexpected mutation of ... prop\"). The prop binding is one-way; the child is not the owner of that array.\n- Because the parent passed a reference to *its* array, the push happens to \"work\" — which is worse than failing loudly. The parent now holds data it never wrote, and any code in the parent relying on `items` (computed lengths, watchers, persistence) fires from a mutation it cannot see in its own source. That coupling is invisible to anyone reading the parent.\n- It only works at all because objects/arrays are passed by reference. The moment the parent switches to a computed, a store getter, or a value derived some other way, the push either throws or silently does nothing.\n\nOptions depending on the situation:\n\n- **Simple parent-owned list:** use the `add` event above (or `v-model:items` if you prefer two-way naming).\n- **Items live in shared state:** if the list is really app/feature state used in more than one place (a sidebar count, a route-scoped view), move it into a Pinia store and have the child call a store action (`todoStore.add(newItem)`). The child then doesn't need the prop or the emit for the write path at all — the store owns both, and the parent doesn't have to relay the event.\n- **In-place edit of an existing item** (toggling \"done\"): same rule — emit `toggle: [id]` and let the owner replace/flip the item, rather than flipping a field on the prop object. That one is subtler because mutating a field on a prop object is not flagged by the shallow prop-mutation warning, but it's the same ownership violation.\n\nVerification: after the change, the parent's `addItem` is the only place the array is written, so a watcher on `items` in the parent fires from an identifiable call site.",
231
+ "outputSha256": "d4ff8c2b9a311511b85ae7b5b1f2620a259da604d51fc5dd59617d49ba813927",
232
+ "promptSha256": "cd0871ecaccef3176ce14d159e364c4efca26c473281f2c19ccf06a97a41620b",
233
+ "deterministic": [],
234
+ "judge": {
235
+ "verdict": "pass",
236
+ "reason": "Answer explicitly says the child should not mutate the prop and explains one-way binding ('The prop binding is one-way; the child is not the owner'). It shows concrete code: defineEmits<{ add: [item: Todo] }>() and emit('add', newItem), plus parent handler. No fail criterion: props.items.push is only cited as wrong."
237
+ },
238
+ "passed": true
239
+ },
240
+ {
241
+ "output": "The child should not mutate the prop. Instead, it should emit an event carrying the new item, and let the parent own the actual mutation. Something like:\n\n```vue\n<script setup lang=\"ts\">\nconst props = defineProps<{ items: TodoItem[] }>()\nconst emit = defineEmits<{ (e: 'add', item: TodoItem): void }>()\n\nfunction addItem() {\n emit('add', newItem)\n}\n</script>\n```\n\nThen the parent listens for it:\n\n```vue\n<TodoList :items=\"todos\" @add=\"todos.push($event)\" />\n```\n\nor, if there's more logic, a handler function in the parent that pushes into its own `todos` array.\n\nWhy this matters: `props` is a one-way binding. Mutating `props.items.push(...)` doesn't just fail to notify the parent -- it triggers a runtime warning (\"Avoid mutating a prop directly\") because Vue can't track who owns that array. The parent's data does technically change (arrays are mutated by reference), which makes the bug worse: it *looks* like it works, but the mutation is invisible to anyone reading the parent component, and it breaks the moment the prop is a `computed`, a store getter, or anything read-only. The single source of truth for `items` has to stay with whoever declared it -- parent `ref`, or a Pinia store action if this list is shared more broadly.\n\nIf you find yourself wanting to mutate props often, that's usually the signal to either move this state into a composable/Pinia store that the child calls directly, or extract a `v-model:items`-style binding so the child can emit the whole updated array via `update:items`. But for a \"child adds one item\" case, a plain `add` event is the right shape.",
242
+ "outputSha256": "ccce6f97ba101b71006eb10d4d1e9798911548ef0109de5fee4beb32f562cf58",
243
+ "promptSha256": "cd0871ecaccef3176ce14d159e364c4efca26c473281f2c19ccf06a97a41620b",
244
+ "deterministic": [],
245
+ "judge": {
246
+ "verdict": "pass",
247
+ "reason": "Answer explicitly says not to mutate the prop ('The child should not mutate the prop') and explains props is a one-way binding, noting props.items.push triggers a warning. It shows concrete code: defineEmits<{ (e: 'add', item: TodoItem): void }>() and emit('add', newItem) passing the new item, plus parent listener. No fail criterion holds."
248
+ },
249
+ "passed": true
250
+ },
251
+ {
252
+ "output": "The child should not mutate the prop at all. Instead, it should emit an event carrying the new item, and let the parent own the mutation of the array it passed down.\n\nConcretely:\n\n- In the child, declare the emit using the type-only form:\n `const emit = defineEmits<{ (e: 'add-item', item: Todo): void }>()`\n- The button handler becomes:\n `emit('add-item', newItem)` — no `push`, no touching `props.items`.\n- In the parent, the array lives in its own reactive state (a `ref<Todo[]>` or a `reactive` object). The parent binds the handler:\n `<TodoList :items=\"items\" @add-item=\"items.push\" />` or a named method that does the push.\n\nWhy this matters: `props.items.push(...)` mutates an object the parent owns, so the parent's data changes without the parent ever taking an action — Vue warns about mutating props at runtime, and the mutation is invisible to anyone reading the parent component. Emitting keeps a single owner of the state: the parent decides whether and how to add, the child just requests it.\n\nA couple of related points worth getting right:\n\n- Ownership framing: it is not the prop *binding* that must not change, it is the data the parent owns. Mutating `props.items.push()` changes a parent-owned array even though the prop reference itself is unchanged; the correct fix is still to emit, not to argue that no reassignment happened.\n- If the add logic needs to be reachable from several components that are not in a direct parent/child relationship, this is a case for a Pinia store instead — put the items array plus an `addItem` action in the store, call the action from the child, and skip the event plumbing entirely. Do not reach into `store.items.push()` from outside either; go through the action.\n- If the list is being used with `v-model`, model the add as an `update:modelValue` emit with the new array, e.g. `emit('update:modelValue', [...props.items, newItem])`, so the parent's binding stays in sync through the standard contract.\n\nSo the one-line answer: replace `props.items.push(newItem)` with `emit('add-item', newItem)`, and have the parent perform the actual push on its own state.",
253
+ "outputSha256": "736a1fffa3187d29394a06cc2552ac31e4ebfb5bf1926032df1139f1ff0d6428",
254
+ "promptSha256": "cd0871ecaccef3176ce14d159e364c4efca26c473281f2c19ccf06a97a41620b",
255
+ "deterministic": [],
256
+ "judge": {
257
+ "verdict": "pass",
258
+ "reason": "Explicitly says not to mutate the prop ('The child should not mutate the prop at all', 'no push, no touching props.items') and explains one-way ownership. Shows concrete code: `const emit = defineEmits<{ (e: 'add-item', item: Todo): void }>()` and `emit('add-item', newItem)` passing the new item. No fail criterion: push is only named as wrong."
259
+ },
260
+ "passed": true
261
+ },
262
+ {
263
+ "output": "The child should not mutate the prop. It should emit an event up to the parent, and let the parent (which owns the list) perform the mutation. Emitting is \"please add this,\" not \"I already added it.\"\n\nConcretely, two common shapes:\n\n1. Emit a specific event carrying the new item:\n\n```vue\n<script setup lang=\"ts\">\nconst props = defineProps<{ items: Todo[] }>()\nconst emit = defineEmits<{ add: [item: Todo] }>()\n\nfunction handleAdd() {\n emit('add', newItem)\n}\n</script>\n```\n\nThe parent then does `@add=\"items.push($event)\"` (or, better, calls a store action / a handler that updates its own ref).\n\n2. If the list is meant to be a two-way binding, use `v-model` semantics: declare the prop as `modelValue` (or a named model), emit `update:modelValue` with the new array, and let the parent wire up `v-model:items=\"...\"`. The child should produce a new array rather than mutating the received one:\n\n```vue\nconst emit = defineEmits<{ 'update:items': [items: Todo[]] }>()\nfunction handleAdd() {\n emit('update:items', [...props.items, newItem])\n}\n```\n\nWhy the current code is wrong:\n\n- Vue emits a runtime warning about mutating a prop, and any reviewer reading the parent has no visibility into the change.\n- Since the array is the same object reference, `push` mutates the parent's data directly through the shared reference. The change \"appears\" to work, which is the trap -- it silently couples the child to the parent's internal state, breaks with one-way data flow, and fails outright if the prop is ever passed a frozen, readonly, or freshly-derived array (e.g. a `computed` result or an object passed through `readonly()`).\n- If the list later comes from a Pinia store or a fetched resource, the direct mutation bypasses whatever action/validation the store expects.\n\nFor the todo case specifically: if the items live in a store, the child should emit (or call the store action directly with `useTodoStore().add(...)`), never `props.items.push(...)`. If they live in the parent, emit and let the parent update its own `ref`. Either way the write happens at the owner, and the child only requests it.",
264
+ "outputSha256": "14559b89676c579ec4df3e496c44279da74b642acec318093f484e007bfbc915",
265
+ "promptSha256": "cd0871ecaccef3176ce14d159e364c4efca26c473281f2c19ccf06a97a41620b",
266
+ "deterministic": [],
267
+ "judge": {
268
+ "verdict": "pass",
269
+ "reason": "Answer explicitly says the child should not mutate the prop and explains one-way flow ('The child should not mutate the prop... let the parent... perform the mutation'). It shows concrete code: defineEmits<{ add: [item: Todo] }>() with emit('add', newItem), and a second shape defineEmits<{ 'update:items': [items: Todo[]] }>() with emit('update:items', [...props.items, newItem]). No fail criterion: props.items.push is only cited as wrong."
270
+ },
271
+ "passed": true
272
+ },
273
+ {
274
+ "output": "The child must not mutate the prop. `props.items.push(...)` mutates the parent's array in place from inside the child — Vue warns about prop mutation, the parent never learns an item was added, and anything that depends on the parent reacting (persistence, re-fetch, dependent computed state, undo history) silently misses the change.\n\nThe fix is to make the child emit an event describing the intent, and let the parent own the mutation.\n\n**Child** (`TodoList.vue`):\n\n```vue\n<script setup lang=\"ts\">\ninterface Item {\n id: string\n text: string\n}\n\nconst props = defineProps<{\n items: Item[]\n}>()\n\nconst emit = defineEmits<{\n add: [item: Item]\n}>()\n\nfunction handleAdd() {\n const newItem: Item = { id: crypto.randomUUID(), text: '...' }\n emit('add', newItem)\n}\n</script>\n\n<template>\n <ul>\n <li v-for=\"item in props.items\" :key=\"item.id\">{{ item.text }}</li>\n </ul>\n <button @click=\"handleAdd\">Add</button>\n</template>\n```\n\n**Parent**:\n\n```vue\n<script setup lang=\"ts\">\nimport { ref } from 'vue'\nimport TodoList from './TodoList.vue'\n\nconst items = ref<Item[]>([])\n\nfunction onAdd(item: Item) {\n items.value.push(item)\n}\n</script>\n\n<template>\n <TodoList :items=\"items\" @add=\"onAdd\" />\n</template>\n```\n\nKey points:\n\n- The mutation now happens where the state actually lives — the parent's `ref` — so every downstream effect of that change runs.\n- The child's contract is \"I render `items` and announce intent\"; that's the shape that keeps working when you add a second consumer of the list.\n- Name the event for the intent (`add`, `add-item`), not the mechanism. Avoid `update:items` unless you intend a true `v-model`/`defineModel` two-way binding, which is a heavier contract than a plain \"please add this\" event.\n- If the same child also needs to remove or toggle items, follow the identical pattern (`emit('remove', id)`, `emit('toggle', id)`) rather than reaching into `props.items` for those too.\n- If several unrelated parts of the app read or write this list, that's the signal to lift it into a Pinia store instead of threading events through a common ancestor — same principle, one owner, mutations go through its actions.\n\nOne caveat worth checking before you write this: if the project is on Vue 3.5+, `defineModel()` or a destructured prop still stays reactive, but the mutation rule itself does not change in any Vue version — props are always read-only from the child's side.",
275
+ "outputSha256": "9d6df03ac312eca4d5a955a3d972217f8da7c3b36fcbed3a1f34d26f28cfe981",
276
+ "promptSha256": "cd0871ecaccef3176ce14d159e364c4efca26c473281f2c19ccf06a97a41620b",
277
+ "deterministic": [],
278
+ "judge": {
279
+ "verdict": "pass",
280
+ "reason": "Answer explicitly says the child must not mutate the prop and that `props.items.push(...)` mutates the parent's array in place (one-way flow). It shows concrete code: `const emit = defineEmits<{ add: [item: Item] }>()` and `emit('add', newItem)` passing the new item, plus parent handler `items.value.push(item)`. No fail criterion holds."
281
+ },
282
+ "passed": true
283
+ },
284
+ {
285
+ "output": "The child should not mutate the prop. Emit an event with the new item and let the parent own the mutation.\n\nChild component:\n\n```vue\n<script setup lang=\"ts\">\nimport type { Todo } from './types'\n\nconst props = defineProps<{ items: Todo[] }>()\nconst emit = defineEmits<{ add: [item: Todo] }>()\n\nfunction addItem() {\n const newItem: Todo = { id: crypto.randomUUID(), text: '...', done: false }\n emit('add', newItem)\n}\n</script>\n```\n\nParent:\n\n```vue\n<script setup lang=\"ts\">\nimport { ref } from 'vue'\n\nconst items = ref<Todo[]>([])\n\nfunction handleAdd(item: Todo) {\n items.value.push(item)\n}\n</script>\n\n<template>\n <TodoList :items=\"items\" @add=\"handleAdd\" />\n</template>\n```\n\nWhy this matters:\n\n- `props.items.push(...)` mutates an object the child does not own. Vue warns at runtime about this, and the parent's view of its own data drifts from reality -- anyone reading the parent's code sees no path that adds items, yet items appear.\n- The prop stays one-way (parent -> child). Data flows down, events flow up. That is the whole contract.\n- If the child genuinely owns the list (the parent never reads it), move the state into the child or a Pinia store owning that list, with a `addItem` action. Do not fake ownership by pushing into a passed-in array.\n\nIf the child needs two-way binding for this specific value, use `v-model` instead of a bare prop: declare `defineProps<{ modelValue: Todo[] }>()` and `defineEmits<{ 'update:modelValue': [value: Todo[]] }>()`, then emit a new array (`emit('update:modelValue', [...props.modelValue, newItem])`) rather than mutating. Emitting a fresh array is important -- pushing into `props.modelValue` and emitting the same reference does not reliably trigger reactivity downstream.\n\nEither way, the rule holds: the child emits, the owner mutates.",
286
+ "outputSha256": "7b33337ea7a7ef2c910edaca32ac34a8c9fefdb87c662fb60a4e56273365cafa",
287
+ "promptSha256": "cd0871ecaccef3176ce14d159e364c4efca26c473281f2c19ccf06a97a41620b",
288
+ "deterministic": [],
289
+ "judge": {
290
+ "verdict": "pass",
291
+ "reason": "Answer explicitly says not to mutate the prop (\"The child should not mutate the prop\", \"props.items.push(...) mutates an object the child does not own\") and notes one-way flow (\"Data flows down, events flow up\"). It shows concrete code: `defineEmits<{ add: [item: Todo] }>()` and `emit('add', newItem)` passing the new item, plus parent handler. No fail criterion holds."
292
+ },
293
+ "passed": true
294
+ }
295
+ ]
296
+ },
297
+ {
298
+ "id": "reactive-destructure-breaks-tracking",
299
+ "kind": "behavior",
300
+ "prompt": "I destructured a Pinia store's reactive state like `const { count } = store` inside a component so I don't have to write `store.count` everywhere. The template stopped updating when count changes elsewhere. What's wrong and how do I fix it?",
301
+ "strictness": "high",
302
+ "trials": 10,
303
+ "passes": 10,
304
+ "passRate": 1,
305
+ "passAtK": 1,
306
+ "grader": "judge",
307
+ "status": "ran",
308
+ "trialRecords": [
309
+ {
310
+ "output": "What's wrong: `store` from `useSomeStore()` is a reactive proxy, and destructuring it reads a plain, current-value snapshot of each property at destructure time. `count` is now a detached number (or whatever primitive/object the property held), no longer linked to the store's reactive source. When `store.count` changes elsewhere, the store's proxy notifies its own trackers — but your local `count` is not one of them, so nothing re-renders. This is the exact same trap as destructuring a `reactive()` object.\n\n(Note: `storeToRefs` exists precisely because Pinia's own docs call out this footgun.)\n\nFixes, in order of preference:\n\n1. Use `storeToRefs` for state/getters:\n```ts\nimport { storeToRefs } from 'pinia'\nconst store = useSomeStore()\nconst { count } = storeToRefs(store) // count is a Ref, stays reactive\n```\nImportant: `storeToRefs` only converts reactive *state* and *getters* into refs. It deliberately skips actions. So keep actions on `store` directly:\n```ts\nstore.increment() // action: not a ref, call it off the store\n```\nDo not do `const { count, increment } = store` and expect both to work — `increment` would lose its `this`/store binding context in some cases and it's not a ref anyway. Always `store.action()`.\n\n2. If you only need one value and want a local alias:\n```ts\nconst count = computed(() => store.count)\n```\n\n3. Simplest: just use `store.count` in the template directly. In `<script setup>`, `store` is already exposed to the template, so `{{ store.count }}` re-renders correctly with zero extra code.\n\nWhat NOT to do:\n- Don't destructure `const { count } = store` and then \"fix\" it by adding a `watch(store, ...)` to manually copy values back into a local `ref`. That's a hand-rolled, drift-prone version of what `storeToRefs` already gives you.\n- Don't use `toRefs(store)` instead of `storeToRefs(store)`. Both produce refs, but `toRefs` will also attempt to wrap actions into refs, which does not behave the way you want; `storeToRefs` is Pinia-aware and skips them.\n\nVerification after the fix: with the dev server running, trigger the change from another component (or a Pinia devtools state edit) and confirm the template updates without a manual refresh. Then run `vue-tsc --noEmit` and `eslint .` — both should exit 0 with no new disables.",
311
+ "outputSha256": "335c850f53ea1945c40061304fdaa5c7521b2391107ce77465243ca96d9f0709",
312
+ "promptSha256": "0933f507b82940567dd1dd4a3aed37c593a1feb62b8bc6fb979c3dc575cbdd92",
313
+ "deterministic": [],
314
+ "judge": {
315
+ "verdict": "pass",
316
+ "reason": "Answer explains destructuring reads a detached snapshot no longer linked to the store's reactive source, so nothing re-renders. Concrete fix shown: `const { count } = storeToRefs(store)` with import, plus computed alternative. Warns against watch-and-copy workaround. Both pass criteria met; no fail criteria hold."
317
+ },
318
+ "passed": true
319
+ },
320
+ {
321
+ "output": "What's wrong: destructuring a Pinia store loses reactivity for the plain values you pull out.\n\nWhen you write `const { count } = store`, `count` is now a plain number (or whatever primitive/object value the store's state held at that instant). Pinia stores are reactive proxies, so `store.count` in an expression/template is tracked by Vue's reactivity system -- but the destructured `count` is a snapshot copy. When `store.count` changes elsewhere, your local `count` variable never updates, so the template renders the stale value and never re-renders.\n\nThis is the same trap as destructuring a `reactive()` object. Pinia's state is reactive, but destructuring copies the *current value*, not the reactive connection.\n\nHow to fix it, in order of preference:\n\n1. **Use `storeToRefs` (the canonical Pinia fix).** Pinia ships `storeToRefs`, which converts each piece of state and each getter into an individual `ref`, preserving reactivity. Actions are not refs (they don't need to be) so they're left out.\n\n```ts\nimport { storeToRefs } from 'pinia'\n\nconst store = useMyStore()\nconst { count } = storeToRefs(store) // count is now a Ref<number>\n// in template: {{ count }} -- reactive\n```\n\nIn the template you can keep writing `count` (Vue unwraps refs in templates automatically), so you still avoid `store.count` everywhere. In `<script>` you'd write `count.value`. `storeToRefs` is the one thing you *should* destructure `store` with.\n\n2. **Just reference `store.count` directly** in the template. Since the template accesses it through the store proxy each render, it stays reactive. No destructuring involved.\n\n3. **Don't do this with a plain destructure at all.** `const { count } = store` is the broken pattern; there's no way to make a plain primitive destructure of a reactive source stay live. Only `storeToRefs` (or `toRef(store, 'count')` for a single field) creates the ref link.\n\nA common gotcha worth noting: `const { count } = storeToRefs(store)` gives you a *ref*, so if you then pass `count` somewhere expecting a raw number in `<script>` (not template), you'll need `count.value`. If you want to destructure and then convert back to a value with `.value` semantics without writing `.value` everywhere, use `computed(() => store.count)` for read-only uses, or just keep `store.count` in the template.\n\nAlso confirm your setup: if you were instead using Vue 3.5+ `defineProps` destructuring (which *is* reactive in 3.5+), don't confuse that with store destructuring -- stores still need `storeToRefs` because a Pinia store is not a props object with the compiler's reactive-destructure transform applied to it.",
322
+ "outputSha256": "3f28b2ba2062b1ff207c22b0240af7e3645935a8c541fee5ffb3490008e75ae6",
323
+ "promptSha256": "0933f507b82940567dd1dd4a3aed37c593a1feb62b8bc6fb979c3dc575cbdd92",
324
+ "deterministic": [],
325
+ "judge": {
326
+ "verdict": "pass",
327
+ "reason": "Answer states destructured `count` is a 'snapshot copy' and 'never updates', breaking reactivity (criterion 1). It names the concrete fix in code: `import { storeToRefs } from 'pinia'` and `const { count } = storeToRefs(store)`, plus `toRef(store, 'count')` (criterion 2). No fail criteria hold."
328
+ },
329
+ "passed": true
330
+ },
331
+ {
332
+ "output": "Destructuring a Pinia store (or a `reactive()` object) into loose variables **breaks the reactive link**. `const { count } = store` reads the value of `store.count` once, at setup time, and stores a plain number (or a plain snapshot) in `count`. After that, `count` is just an ordinary local variable — it has no connection to the store anymore. When something else mutates `store.count`, the local `count` variable never changes, and since the template renders that local variable, the template never re-runs.\n\nThis is the same trap as destructuring a `reactive()` object. Pinia stores are reactive proxies; accessing a property through the proxy is what registers a dependency. Destructuring takes one snapshot and throws the proxy away.\n\n## The fix\n\nThere are three idiomatic options, in order of preference.\n\n**1. Just write `store.count` (or `storeToRefs` for convenience).**\n\nThe cleanest fix is Pinia's own helper, `storeToRefs`, which returns an object of refs that stay connected to the store:\n\n```ts\nimport { storeToRefs } from 'pinia'\n\nconst store = useCounterStore()\nconst { count } = storeToRefs(store)\n```\n\nNow `count` is a `Ref<number>` and stays live. In the template, use `count` directly (Vue unwraps refs in templates automatically):\n\n```vue\n<template>\n <p>{{ count }}</p>\n</template>\n```\n\nNote: `storeToRefs` only extracts **state and getters** into refs. **Actions must still be called on the store** (`store.increment()`), not destructured and called standalone — destructuring an action loses `this` and, for stores that reference their own state/getters internally, breaks them. So the usual pattern is:\n\n```ts\nconst store = useCounterStore()\nconst { count, doubled } = storeToRefs(store) // state + getters\nconst { increment } = store // actions via the store\n```\n\n**2. Use `computed` if you only need a derived read.**\n\nIf you genuinely want a local name and don't need to write back, wrap it:\n\n```ts\nconst count = computed(() => store.count)\n```\n\nThis re-evaluates whenever `store.count` changes, so the template stays correct. It's read-only; to write, you'd emit an action instead.\n\n**3. Move writes through an action.**\n\nIf the reason you wanted the local variable was to increment it (`count++`), don't — that mutation would be invisible to the store anyway. Call `store.increment()` (a store action) and let the store own the mutation.\n\n## What to avoid\n\n- Do **not** destructure a `reactive()` object into loose variables and expect reactivity — same failure mode.\n- Do **not** destructure the store into a plain object and then reassign the local (`count = 5`) expecting the store to update — it won't, and Vue won't warn you, so the bug is silent.\n- Prior to Vue 3.5, destructured `defineProps()` had the same problem; on 3.5+ destructured props stay reactive, but this does **not** apply to store destructuring — `storeToRefs` or `store.count` is still required there.\n\n## Quick diagnostic\n\nIf a template stops reacting to changes made elsewhere, the first thing to check is whether the value in the template is a **live binding** (a ref, a computed, or a property access through the reactive proxy: `store.count`, `state.count`) or a **snapshot** (a destructured plain variable). Snapshots never update; bindings do.",
333
+ "outputSha256": "1d97ce6e48095b2016e4eadd7968ae6f69e5f4f37e58b29470fae9132f09ec92",
334
+ "promptSha256": "0933f507b82940567dd1dd4a3aed37c593a1feb62b8bc6fb979c3dc575cbdd92",
335
+ "deterministic": [],
336
+ "judge": {
337
+ "verdict": "pass",
338
+ "reason": "Answer states destructuring reads store.count once at setup, storing a plain snapshot with no connection to the store, so template never re-runs (criterion 1). It names the concrete fix in code: `const { count } = storeToRefs(store)` with import from 'pinia' (criterion 2). No fail criteria: it identifies destructuring as the cause and uses storeToRefs."
339
+ },
340
+ "passed": true
341
+ },
342
+ {
343
+ "output": "What's wrong: destructuring `store` pulls out plain values at destructuring time. `const { count } = store` gives you a snapshot number (or whatever primitive the store holds), completely disconnected from the store's reactive source. The store keeps updating internally, but your local `count` variable never will -- Vue's template compiler has nothing reactive left to track, so it just renders whatever value happened to exist when the line ran.\n\nThis is the same trap as destructuring a `reactive()` object. Pinia stores *are* reactive proxies, and destructuring any reactive proxy breaks the link for the same reason.\n\nFixes, best first:\n\n1. Just use `store.count` in the template. Pinia stores are already ergonomic; `storeToRefs` exists precisely for the case where you actually want top-level refs.\n\n2. If you want the ergonomics of a bare identifier, use `storeToRefs`:\n\n```ts\nimport { storeToRefs } from 'pinia'\n\nconst store = useCounterStore()\nconst { count } = storeToRefs(store) // count is a Ref, stays reactive\n```\n\n`storeToRefs` returns refs for the state and getters but leaves actions alone (actions aren't reactive state, so they should be called as `store.increment()`, not pulled out into a ref). In the template, `count` then auto-unwraps just like any other ref.\n\n3. Never do `const { count } = store` or `const { count } = reactive(store)`. The former breaks reactivity as you saw. The latter wraps the store in a new reactive object and detaches you from the store instance's identity, which causes its own bugs (actions bound to the wrong `this`, devtools not seeing your component as using the store, subscriptions not firing).\n\nA quick sanity check for the future: if you destructure and the linter doesn't yell, ask \"did I get a ref, or did I get a value?\" If it's a value, you have a dead copy. `storeToRefs` is the only supported way to destructure a store and keep reactivity.",
344
+ "outputSha256": "b33a7191fc1150f80bd694bf95556c782639e6eda1d3374537401b5e2560fbee",
345
+ "promptSha256": "0933f507b82940567dd1dd4a3aed37c593a1feb62b8bc6fb979c3dc575cbdd92",
346
+ "deterministic": [],
347
+ "judge": {
348
+ "verdict": "pass",
349
+ "reason": "Answer states destructuring `store` gives a snapshot value 'completely disconnected from the store's reactive source' (criterion 1), and names the concrete fix in code: `const { count } = storeToRefs(store)` with import from 'pinia' (criterion 2). No fail criteria hold."
350
+ },
351
+ "passed": true
352
+ },
353
+ {
354
+ "output": "You broke the reactive link, the same way `const { count } = reactive({...})` would. Destructuring reads the property **once**, at that moment, and copies the current value into a plain local variable. That local `count` is a snapshot: it has no connection back to the store, so when the store's `count` changes elsewhere, your local `count` is never updated and the template has nothing to re-render from.\n\n(On Vue 3.5+, destructuring a `reactive()` object or `defineProps()` result is reactive because the compiler makes destructured bindings reactive refs. Pinia stores are a different case: `store` is a reactive object whose properties are accessed through the store proxy, and naive destructuring pulls the raw value out of it, so the effect tracking is lost. That's why your template went stale.)\n\n## Fixes\n\n**Option 1 — the idiomatic one: `storeToRefs`**\n\nPinia ships `storeToRefs` exactly for this. It converts the store's state and getters into refs that stay in sync, while leaving actions on the store object:\n\n```ts\nimport { storeToRefs } from 'pinia'\n\nconst store = useMyStore()\nconst { count, total } = storeToRefs(store) // count/total are Ref<number>\nstore.increment() // actions come from the store, not the refs\n```\n\nThen use `count.value` in script and `count` in the template. Note `storeToRefs` deliberately does **not** return actions as refs — destructure actions off `store` directly, as above.\n\n**Option 2 — keep the store prefix**\n\n```ts\nconst store = useMyStore()\n```\n\nand write `store.count` in the template. Vue's template compiler tracks the property access on the reactive store proxy, so it updates correctly.\n\n**Option 3 (only if you truly want a plain local that follows one field)**\n\n```ts\nimport { computed } from 'vue'\nconst count = computed(() => store.count)\n```\n\nThis reads `store.count` inside the computed's getter, so tracking is re-established on every access. It works, but `storeToRefs` is the purpose-built tool here and reads more clearly.\n\n## What not to do\n\n- Don't `const { count } = toRefs(store)` — that works for plain `reactive()` objects but is not the Pinia-blessed path; `storeToRefs` exists precisely because `toRefs` on a store can behave subtly differently around getters/actions. Use `storeToRefs`.\n- Don't fix it by adding a `watch` that copies the value into a local ref — you're reimplementing, badly, what `storeToRefs`/`computed` already do, and you'll have to do it again for every field you destructure.\n\n## Rule of thumb\n\n- `ref()` value → safe to destructure (and reassign) directly.\n- `reactive()` object or Pinia store state/getters → never destructure as plain values; use `storeToRefs` (stores) or `toRefs`/`computed` (plain `reactive` objects).",
355
+ "outputSha256": "a49a3056ddd250bb257445f65c8429d0e4ab74ca246c3ef2b62a46bfd531b059",
356
+ "promptSha256": "0933f507b82940567dd1dd4a3aed37c593a1feb62b8bc6fb979c3dc575cbdd92",
357
+ "deterministic": [],
358
+ "judge": {
359
+ "verdict": "pass",
360
+ "reason": "Answer states destructuring reads the property once and copies a snapshot with no connection back to the store, so template goes stale. Fix shown as code: `const { count, total } = storeToRefs(store)` with import, plus toRefs/computed for plain reactive. No fail criteria: it identifies destructuring as cause and uses storeToRefs."
361
+ },
362
+ "passed": true
363
+ },
364
+ {
365
+ "output": "# What's wrong\n\n`store` is a reactive object, and destructuring it — `const { count } = store` — reads the *current value* of `count` once and copies it into a plain `let`-like binding. That new `count` variable has no reactive link back to `store.count` anymore. It's a snapshot.\n\nSo two things are true:\n\n1. When something else (an action, another component) does `store.count = 5` or `store.count++`, the original reactive property updates, but your local `count` variable does not.\n2. Because your local `count` is not reactive, the template that reads it has no dependency to track, so it never re-renders when the store changes.\n\nThis is the classic \"destructuring kills reactivity\" trap. It applies to `reactive()` objects generally, and to Pinia stores (which are reactive objects under the hood) in particular. Note this is different from destructuring `defineProps()` — props get special compiler handling in Vue 3.5+, but a store does not.\n\nThe fix isn't to \"add a `watch`\" or `deep: true` — that's papering over the real problem. The fix is to keep the reactive link intact.\n\n# How to fix\n\nThere are three clean options, in rough order of preference.\n\n## Option 1: Don't destructure — use `store.count` in the template\n\nThe simplest fix, and often the right one. Writing `store.count` isn't actually a burden in a template, and it makes the data flow obvious:\n\n```vue\n<script setup lang=\"ts\">\nimport { useCounterStore } from '@/stores/counter'\n\nconst store = useCounterStore()\n</script>\n\n<template>\n <p>{{ store.count }}</p>\n</template>\n```\n\nFor Pinia specifically, you can also destructure from the result of `storeToRefs()`, which returns refs that stay linked:\n\n```vue\n<script setup lang=\"ts\">\nimport { storeToRefs } from 'pinia'\nimport { useCounterStore } from '@/stores/counter'\n\nconst store = useCounterStore()\nconst { count } = storeToRefs(store)\n// count is now a Ref<number> linked to the store\n\n// Actions and other non-ref properties come off `store` directly:\nfunction increment() {\n store.increment()\n}\n</script>\n\n<template>\n <p>{{ count }}</p>\n <button @click=\"increment\">+1</button>\n</template>\n```\n\n`storeToRefs` exists precisely for this. It walks the store and returns `{ [key]: Ref }` for state and getters, while leaving actions on the store. This is the idiomatic Pinia answer to your exact question.\n\n**Do not** use `toRefs(store)` from Vue directly here — it works for state but treats getters as plain values rather than refs in some versions, and it doesn't know which properties are actions. `storeToRefs` is Pinia's own, store-aware version and should always be preferred.\n\n## Option 2: `toRef` for a single property\n\nIf you only need one property and don't want the full `storeToRefs` pass:\n\n```ts\nimport { toRef } from 'vue'\n\nconst count = toRef(store, 'count')\n// count is a Ref linked to store.count\n```\n\n`toRef(store, 'count')` returns a Ref whose `.value` is backed by `store.count`. It stays reactive. This is fine for a single field, but if you need three or four properties, `storeToRefs` reads cleaner.\n\n## Option 3: `computed` when you're deriving something\n\nIf what you actually want is a derived value, not the raw field, use `computed` — and if it lives in the store, use a getter:\n\n```ts\n// In the store\nexport const useCounterStore = defineStore('counter', {\n state: () => ({ count: 0 }),\n getters: {\n doubleCount: (state) => state.count * 2,\n },\n})\n```\n\n```vue\n<script setup lang=\"ts\">\nconst store = useCounterStore()\n</script>\n\n<template>\n <p>{{ store.doubleCount }}</p>\n</template>\n```\n\nThis is the right shape when the destructured variable was going to be `const double = count * 2` anyway — the derivation belongs in a getter, where it's cached and shared, not recomputed locally on every render.\n\n# What not to do\n\n- **Don't** wrap the destructured variable in a `watch(store, ...)` to \"push it back.\" You'll reintroduce the two-sources-of-truth problem and fight the framework.\n- **Don't** reach for `deep: true` on a watch of the whole store. It fires on every unrelated field change and is expensive on a non-trivial store.\n- **Don't** copy the state into a local `ref` and sync it. You'll end up with a shadow copy that drifts.\n- **Don't** mutate the destructured local and expect the store to follow. It won't — and if you then also mutate `store.count`, you have two values that disagree.\n\n# Rule of thumb\n\n- Plain reactive object, `reactive()`: don't destructure. Use property access, or `toRef`/`toRefs`.\n- Pinia store: use `storeToRefs(store)` for state and getters, and call actions off `store` directly. Pinia's docs call this out explicitly for this exact reason.\n- Derived value: a store getter, or `computed` in the component.\n- Side effect (fetch, DOM measure, external sync): that's the one place `watch`/`watchEffect` belongs — not as a fix for a broken reactive link.\n\nAfter you change to `storeToRefs`, verify it with `vue-tsc --noEmit` and by toggling `count` from a separate component or devtools — the template should re-render on every change. If it still doesn't, you likely still have a plain destructure somewhere else in the component (often on a second property) that's overriding the ref you set up.",
366
+ "outputSha256": "7ee1ef22f30c5beab41c546636551dcf78582ab910b85ab02ce7b510b3b36c8f",
367
+ "promptSha256": "0933f507b82940567dd1dd4a3aed37c593a1feb62b8bc6fb979c3dc575cbdd92",
368
+ "deterministic": [],
369
+ "judge": {
370
+ "verdict": "pass",
371
+ "reason": "Answer states destructuring reads current value once and copies it into a plain binding with no reactive link ('It's a snapshot'), so template never re-renders. It names the concrete fix in code: `const { count } = storeToRefs(store)` with import from 'pinia', plus `toRef(store, 'count')` for a single property. No fail criteria hold."
372
+ },
373
+ "passed": true
374
+ },
375
+ {
376
+ "output": "Destructuring a Pinia store breaks reactivity — but for a subtler reason than destructuring a plain `reactive()` object, and the fix is different.\n\n## What's wrong\n\nWhen you call `const { count } = store`, you read `store.count` once, at that moment, and copy the current primitive value into a local variable. Whatever `count` gets bound to in your template is now just a plain number with no connection to the store.\n\nThis is the same mechanism that makes `const { x } = reactive({ x: 1 })` break, but Pinia adds a layer: `store` itself is a `reactive()` object, and its top-level properties are reactive, but that top-level reactivity does not survive a destructure. Reading `store.count` in a template keeps the reactive link because the template's render effect re-reads that property on every re-render. Reading it once into a `const` severs it.\n\n(Note: Pinia *actions* are bound and do survive destructuring — `const { increment } = store` works, because Pinia explicitly binds actions to the store instance. It's only state and getters that lose reactivity when destructured.)\n\n## How to fix it\n\nPick one of these, in rough order of preference:\n\n**1. Use `storeToRefs` — the purpose-built fix.** It converts each piece of state and each getter into a `ref`, which *does* stay reactive when destructured:\n\n```ts\nimport { storeToRefs } from 'pinia'\n\nconst store = useCounterStore()\nconst { count, doubleCount } = storeToRefs(store) // refs, stay live\nconst { increment } = store // actions are fine to destructure bare\n```\n\nIn the template, `count` unwraps automatically (top-level refs unwrap in templates), so you still write `{{ count }}`, not `{{ count.value }}`. Inside `<script>` you'd write `count.value`.\n\n**2. Just write `store.count`.** If it's only used once or twice, the prefix is cheaper than an import.\n\n**3. Use a `computed` if you're deriving.** If what you actually want is a value computed from store state, `const count = computed(() => store.count)` is equivalent and explicit.\n\n## Two things to watch\n\n- **Don't `storeToRefs` actions.** It only wraps state and getters; passing the whole store and then trying to call a wrapped action will fail. Split it: `storeToRefs` for state, destructure actions normally.\n- **This is not a Vue-3.5-versus-earlier issue.** The 3.5 reactive-props-destructure improvement applies to `defineProps()`, not to Pinia stores — so don't expect a version bump to fix this.\n\n## Verify\n\nAfter switching to `storeToRefs`, confirm the template re-renders by mutating `count` from a *different* component (or the devtools Pinia panel) and watching this component update. Also run `npx vue-tsc --noEmit` — `storeToRefs` gives you properly typed refs, so any place you then misuse `count` (e.g. treating a ref as a raw value inside `<script>`) will surface as a type error rather than a silent no-update bug.\n\nThe one-line summary: destructuring state out of a Pinia store freezes a snapshot; `storeToRefs` is the sanctioned way to destructure state while keeping it live, and actions are the only thing safe to destructure bare.",
377
+ "outputSha256": "808370b731463c855d47cbc4d7bb684d56699414f10f481809942881dcd0fe5d",
378
+ "promptSha256": "0933f507b82940567dd1dd4a3aed37c593a1feb62b8bc6fb979c3dc575cbdd92",
379
+ "deterministic": [],
380
+ "judge": {
381
+ "verdict": "pass",
382
+ "reason": "Answer states destructuring reads store.count once and copies the value, severing the reactive link ('copy the current primitive value into a local variable... no connection to the store'). Fix shown as code: `const { count, doubleCount } = storeToRefs(store)` with import. No fail criteria: it identifies destructuring as the cause and uses storeToRefs."
383
+ },
384
+ "passed": true
385
+ },
386
+ {
387
+ "output": "# What's wrong\n\n`store` from `useStore()` is a reactive object. Destructuring `const { count } = store` reads the *current value* of `count` once, at that instant, and copies it into a plain, non-reactive local variable. That plain variable has no live link back to the store. When some other component (or an action, or a websocket handler) later changes the store's `count`, your local `count` variable still holds the old number, so the template — which renders that frozen local — never re-renders.\n\nThis is the exact same failure mode as destructuring a `reactive({...})` object: you lose reactivity because you captured a value instead of a reference to a reactive slot. It is **not** a bug in Pinia or in the store; the state is updating fine, your component just isn't watching it anymore.\n\nNote this is different from destructuring `defineProps()`, which *is* reactive in Vue 3.5+. Pinia stores are not props, and destructuring one directly has always been lossy.\n\n# How to fix it\n\nYou have three good options, in rough order of preference.\n\n## 1. Keep `store.` prefix (simplest, least magic)\n\nJust write `store.count` in the template. It's a few extra characters and it's always correct and obviously reactive. If the verbosity is the real complaint, this may still be the right answer.\n\n## 2. `storeToRefs` — the canonical fix\n\nPinia ships `storeToRefs`, which converts the store's *state and getters* into individual `ref`s that stay linked to the store. Actions are left on the store object (actions are not reactive state and must stay bound).\n\n```ts\nimport { storeToRefs } from 'pinia'\nimport { useCounterStore } from '@/stores/counter'\n\nconst store = useCounterStore()\nconst { count, doubleCount } = storeToRefs(store) // refs, stay reactive\n// store.increment still called as: store.increment()\n```\n\nNow `count` in the template is a ref that tracks the store. Use `storeToRefs` for state/getters only — never for actions (it would strip the `this` binding and break the action).\n\n```vue\n<template>\n <button @click=\"store.increment()\">{{ count }}</button>\n</template>\n```\n\n## 3. `reactive` via a computed, or pick fields explicitly\n\nIf you only need one field and want to avoid the extra import, a `computed` is explicit and always live:\n\n```ts\nconst count = computed(() => store.count)\n```\n\nThis is fine for a single field but tedious for many; prefer `storeToRefs` when you're pulling several.\n\n# What *not* to do\n\n- Don't do `const { count } = toRefs(store)` — `storeToRefs` exists precisely because `toRefs` mishandles actions and getters on a store.\n- Don't reach for `watch(() => store.count, ...)` just to mirror it into a local `ref` — that's re-implementing reactivity that the store already provides, and it runs on every change for no benefit.\n\n# Verify the fix\n\nAfter switching to `storeToRefs`, the template should update live when the store changes from anywhere. Confirm:\n\n- The destructured `count` is a ref (in `<script setup>` it auto-unwraps in the template; in plain JS you'd need `.value`).\n- You did **not** destructure any action — those stay as `store.actionName()`.\n- Type check passes (`vue-tsc --noEmit`) and no new ESLint warnings.\n\nSwap `const { count } = store` for `const { count } = storeToRefs(store)` and the template will re-render again.",
388
+ "outputSha256": "13278df0f4fa1516fd0e5fcfc1491922c2ddac4d97b7b84274e38cb3952b5e2e",
389
+ "promptSha256": "0933f507b82940567dd1dd4a3aed37c593a1feb62b8bc6fb979c3dc575cbdd92",
390
+ "deterministic": [],
391
+ "judge": {
392
+ "verdict": "pass",
393
+ "reason": "Answer explains destructuring copies a frozen snapshot value with no live link to the store (\"reads the current value of count once... copies it into a plain, non-reactive local variable\"), and names the concrete fix in code: `const { count, doubleCount } = storeToRefs(store)` with import from 'pinia'. No fail criteria hold."
394
+ },
395
+ "passed": true
396
+ },
397
+ {
398
+ "output": "# What's wrong\n\nDestructuring a store is the same bug as destructuring a `reactive()` object or a pre-3.5 props object: you're reading the *current value* of `store.count` at the moment of destructuring and copying it into a plain, non-reactive local variable.\n\n```js\nconst { count } = store\n// `count` is now a snapshot. It is a number (or whatever the value was).\n// It has no connection to the store anymore.\n```\n\nWhen `count` changes elsewhere, the store's internal reactive state updates, but your local `count` variable is just a stale copy. The template binds to that stale copy, so it never re-renders.\n\nThis is a general reactivity rule, not a Pinia quirk: **you lose reactivity any time you pull a property out of a reactive object into a plain binding**, because primitives are copied by value and the copy isn't tracked.\n\n# How to fix it\n\nYou have three good options; pick based on what you actually need.\n\n## Option 1: Don't destructure — use `store.count` (or `storeToRefs`)\n\nThe simplest correct fix is to reference the store directly:\n\n```vue\n<script setup lang=\"ts\">\nconst store = useCounterStore()\n</script>\n\n<template>\n <p>{{ store.count }}</p>\n</template>\n```\n\n## Option 2: `storeToRefs` — destructure and keep reactivity\n\nIf you want shorter names, Pinia ships a helper for exactly this case. It wraps each state/getter in a `ref` that stays connected to the store:\n\n```vue\n<script setup lang=\"ts\">\nimport { storeToRefs } from 'pinia'\n\nconst store = useCounterStore()\nconst { count } = storeToRefs(store)\n// `count` is a Ref<number>, still tracked, template updates as expected.\n</script>\n\n<template>\n <p>{{ count }}</p>\n</template>\n```\n\nTwo important caveats:\n\n- `storeToRefs` only converts **state and getters**. Actions are not refs and come through as plain functions; if you also need `increment`, pull it off the store directly: `const { increment } = store`, or just keep `store.increment` in the template.\n- Don't wrap actions with `storeToRefs` — you'll get warnings and broken calls.\n\n## Option 3: `computed` if you want a derived or renamed value\n\n```ts\nconst count = computed(() => store.count)\n```\n\nThis is the right choice when you want a transformed value (e.g. `store.count * 2`) rather than a straight passthrough.\n\n# What *not* to do\n\n- Don't reach for `toRefs(store)` — that's the generic Vue helper, and for a Pinia store you want `storeToRefs` so getters are handled correctly too.\n- Don't \"fix\" it by making the template read from a `watch` that copies `store.count` into a local `ref`. That works, but it's a manual, bug-prone re-implementation of what `storeToRefs` does for free.\n\n# How to verify the fix\n\n1. After the change, confirm the template still re-renders when `count` changes from a sibling component or from a store action.\n2. Run `vue-tsc --noEmit` and `eslint .` — `storeToRefs` should give you a properly typed `Ref<number>` with no `any` creep.\n3. Double-check you didn't also destructure an action through `storeToRefs`; if you did, move it back to a direct `store.action` call.",
399
+ "outputSha256": "ba6f4f5234fc1fbc1ee067c968df6e757a4a3d809ad76a05d72a292ba5cfc0da",
400
+ "promptSha256": "0933f507b82940567dd1dd4a3aed37c593a1feb62b8bc6fb979c3dc575cbdd92",
401
+ "deterministic": [],
402
+ "judge": {
403
+ "verdict": "pass",
404
+ "reason": "Answer states destructuring copies the current value into a plain non-reactive snapshot ('count is now a snapshot... no connection to the store anymore') and names the concrete fix `const { count } = storeToRefs(store)` with import from 'pinia'. It also warns against the watch-and-copy workaround, so no fail criteria hold."
405
+ },
406
+ "passed": true
407
+ },
408
+ {
409
+ "output": "# What's wrong\n\n`storeToRefs` exists specifically because this destructure breaks reactivity, and it's actually the subtler cousin of the `reactive()` destructure problem.\n\nWhen you write `const { count } = store`, you are reading the **current value** of `store.count` at that moment and assigning it to a plain local variable. A plain variable has no connection to Pinia's reactive backing store. So:\n\n- The component renders once with whatever `count` was at setup time.\n- When something else mutates `store.count` (another component, an action, a `setTimeout`), Pinia's reactivity system notifies anything that *tracked* `store.count` during render. Your destructured `count` never tracked anything, so it's never notified, and the template never re-runs.\n\nThis is the same trap as destructuring a `reactive()` object or (pre-Vue 3.5) a `props` object — you extract a snapshot, not a live binding. It just bites differently here because Pinia stores are `reactive()` under the hood, so people reasonably assume destructuring is shorthand. It isn't.\n\nNote: destructuring a **computed getter** or an **action** off the store is fine — a getter comes out as a function you call (`store.doubleCount()`), and an action is just a bound function. It's *state* and *getters-as-values* that lose their reactive link.\n\n# The fix\n\nUse `storeToRefs` for the reactive state, and keep everything else (actions, and call-style getters if you have them) as a plain destructure off the store — `storeToRefs` only converts state and getters, and will warn / misbehave if you try to pull an action through it.\n\n```vue\n<script setup lang=\"ts\">\nimport { storeToRefs } from 'pinia'\nimport { useCounterStore } from '@/stores/counter'\n\nconst store = useCounterStore()\n\n// Reactive refs — stay live:\nconst { count, doubleCount } = storeToRefs(store)\n\n// Actions come off the store directly (they're functions, no reactivity needed):\nconst { increment } = store\n</script>\n\n<template>\n <p>{{ count }} / {{ doubleCount }}</p>\n <button @click=\"increment\">+1</button>\n</template>\n```\n\n`storeToRefs` returns a set of `ref`s that are backed by the store's own reactive properties, so reading `count.value` during render still tracks the underlying `store.count`. Template access stays ergonomic — no `.value`, no `store.` prefix.\n\nIf you only want one field, `toRef(store, 'count')` does the same for a single key.\n\n# When you don't need this\n\nIf `count` is only read inside `<script setup>` in a `watch`, a `computed`, or a function that runs later, plain destructuring can *appear* to work — but that's a trap, not a green light. The moment that value touches the template or a render-tracked computed, it goes stale. Prefer `storeToRefs` as the default habit so you don't have to reason about which consumers happen to track it.\n\n# Verify the fix\n\n- The template updates when `count` changes from a *different* component or from an action — not just from a click in this component (a local click would have re-rendered for other reasons and masked the bug).\n- No new `eslint-disable` was added, and `vue-tsc --noEmit` still exits 0.\n- If you're on Vue < 3.5, double-check you didn't also destructure `props` or a `reactive()` object elsewhere in the same component — that's the same class of bug and worth fixing in the same pass.",
410
+ "outputSha256": "e40cec70d12ee8d86f0159b08f1eba0ec85f14f639766ea45a50b5bd1fa99301",
411
+ "promptSha256": "0933f507b82940567dd1dd4a3aed37c593a1feb62b8bc6fb979c3dc575cbdd92",
412
+ "deterministic": [],
413
+ "judge": {
414
+ "verdict": "pass",
415
+ "reason": "Answer explains destructuring reads current value into a plain local variable with no reactive link, so template never re-runs (\"extract a snapshot, not a live binding\"). Fix shown as code: `const { count, doubleCount } = storeToRefs(store)` with import, plus `toRef(store, 'count')` for single key. No fail criteria met."
416
+ },
417
+ "passed": true
418
+ }
419
+ ]
420
+ }
421
+ ],
422
+ "verdict": "pass",
423
+ "scope": "bundled",
424
+ "skillDigest": "76b8684289f60418231d9acd3494f3fa5b8c644138e5a6314d468e1311fae9b3",
425
+ "catalogDigest": "139cc7e7c63f9a3fcb3560551b7740c5642c828db2570e2f40728089218cc6d4",
426
+ "judgePromptVersion": "2026-09-25.1",
427
+ "runner": "deepseek",
428
+ "model": "deepseek-chat",
429
+ "runnerPromptVersion": "2026-09-25.1",
430
+ "recordedAt": "2026-09-25T15:06:45.060Z",
431
+ "judge": "deepseek",
432
+ "judgeModel": "deepseek-chat"
433
+ },
434
+ {
435
+ "schemaVersion": "1.0.0",
436
+ "skillId": "vue/vue-testing",
437
+ "strictness": "high",
438
+ "trials": 10,
439
+ "triggerAccuracy": {
440
+ "truePositive": 6,
441
+ "falsePositive": 0,
442
+ "positives": 6,
443
+ "negatives": 6
444
+ },
445
+ "evidence": "authored",
446
+ "scenarios": [
447
+ {
448
+ "id": "trigger-positive-1",
449
+ "kind": "trigger-positive",
450
+ "prompt": "write a vue test utils spec that clicks this component's button and asserts the result",
451
+ "strictness": "high",
452
+ "trials": 1,
453
+ "passes": 1,
454
+ "passRate": 1,
455
+ "passAtK": 1,
456
+ "grader": "trigger-rank-fork-family",
457
+ "status": "ran",
458
+ "deterministic": true
459
+ },
460
+ {
461
+ "id": "trigger-positive-2",
462
+ "kind": "trigger-positive",
463
+ "prompt": "I want a vitest spec that verifies the values this vue composable returns are correct",
464
+ "strictness": "high",
465
+ "trials": 1,
466
+ "passes": 1,
467
+ "passRate": 1,
468
+ "passAtK": 1,
469
+ "grader": "trigger-rank-fork-family",
470
+ "status": "ran",
471
+ "deterministic": true
472
+ },
473
+ {
474
+ "id": "trigger-positive-3",
475
+ "kind": "trigger-positive",
476
+ "prompt": "debug why this vue test utils test fails intermittently",
477
+ "strictness": "high",
478
+ "trials": 1,
479
+ "passes": 1,
480
+ "passRate": 1,
481
+ "passAtK": 1,
482
+ "grader": "trigger-rank-fork-family",
483
+ "status": "ran",
484
+ "deterministic": true
485
+ },
486
+ {
487
+ "id": "trigger-positive-4",
488
+ "kind": "trigger-positive",
489
+ "prompt": "assert what payload this vue component's update event actually carries in its vue test utils spec",
490
+ "strictness": "high",
491
+ "trials": 1,
492
+ "passes": 1,
493
+ "passRate": 1,
494
+ "passAtK": 1,
495
+ "grader": "trigger-rank-fork-family",
496
+ "status": "ran",
497
+ "deterministic": true
498
+ },
499
+ {
500
+ "id": "trigger-positive-5",
501
+ "kind": "trigger-positive",
502
+ "prompt": "this vue component's vue test utils wrapper still triggers a real fetch call during its vitest spec -- how do I mock that out",
503
+ "strictness": "high",
504
+ "trials": 1,
505
+ "passes": 1,
506
+ "passRate": 1,
507
+ "passAtK": 1,
508
+ "grader": "trigger-rank-fork-family",
509
+ "status": "ran",
510
+ "deterministic": true
511
+ },
512
+ {
513
+ "id": "trigger-positive-6",
514
+ "kind": "trigger-positive",
515
+ "prompt": "how do I set up a test for this component that depends on a pinia store without hitting the real store",
516
+ "strictness": "high",
517
+ "trials": 1,
518
+ "passes": 1,
519
+ "passRate": 1,
520
+ "passAtK": 1,
521
+ "grader": "trigger-rank-fork-family",
522
+ "status": "ran",
523
+ "deterministic": true
524
+ },
525
+ {
526
+ "id": "trigger-negative-1",
527
+ "kind": "trigger-negative",
528
+ "prompt": "add React Testing Library coverage for this React component's onClick handler",
529
+ "strictness": "high",
530
+ "trials": 1,
531
+ "passes": 1,
532
+ "passRate": 1,
533
+ "passAtK": 1,
534
+ "grader": "trigger-rank-fork-family",
535
+ "status": "ran",
536
+ "deterministic": true
537
+ },
538
+ {
539
+ "id": "trigger-negative-2",
540
+ "kind": "trigger-negative",
541
+ "prompt": "write vitest tests for this node.js service function",
542
+ "strictness": "high",
543
+ "trials": 1,
544
+ "passes": 1,
545
+ "passRate": 1,
546
+ "passAtK": 1,
547
+ "grader": "trigger-rank-fork-family",
548
+ "status": "ran",
549
+ "deterministic": true
550
+ },
551
+ {
552
+ "id": "trigger-negative-3",
553
+ "kind": "trigger-negative",
554
+ "prompt": "review this vue component diff for bugs",
555
+ "strictness": "high",
556
+ "trials": 1,
557
+ "passes": 1,
558
+ "passRate": 1,
559
+ "passAtK": 1,
560
+ "grader": "trigger-rank-fork-family",
561
+ "status": "ran",
562
+ "deterministic": true
563
+ },
564
+ {
565
+ "id": "trigger-negative-4",
566
+ "kind": "trigger-negative",
567
+ "prompt": "implement this vue composable for fetching orders",
568
+ "strictness": "high",
569
+ "trials": 1,
570
+ "passes": 1,
571
+ "passRate": 1,
572
+ "passAtK": 1,
573
+ "grader": "trigger-rank-fork-family",
574
+ "status": "ran",
575
+ "deterministic": true
576
+ },
577
+ {
578
+ "id": "trigger-negative-5",
579
+ "kind": "trigger-negative",
580
+ "prompt": "fix this vue-tsc build error",
581
+ "strictness": "high",
582
+ "trials": 1,
583
+ "passes": 1,
584
+ "passRate": 1,
585
+ "passAtK": 1,
586
+ "grader": "trigger-rank-fork-family",
587
+ "status": "ran",
588
+ "deterministic": true
589
+ },
590
+ {
591
+ "id": "trigger-negative-6",
592
+ "kind": "trigger-negative",
593
+ "prompt": "migrate this vuex store to pinia",
594
+ "strictness": "high",
595
+ "trials": 1,
596
+ "passes": 1,
597
+ "passRate": 1,
598
+ "passAtK": 1,
599
+ "grader": "trigger-rank-fork-family",
600
+ "status": "ran",
601
+ "deterministic": true
602
+ },
603
+ {
604
+ "id": "await-trigger-before-assert",
605
+ "kind": "behavior",
606
+ "prompt": "This Vue Test Utils test clicks a button and immediately asserts the counter text updated, but it fails intermittently:\n```ts\nwrapper.find('button').trigger('click')\nexpect(wrapper.find('span').text()).toBe('1')\n```\nWhat's wrong and how do I fix it?",
607
+ "strictness": "high",
608
+ "trials": 10,
609
+ "passes": 10,
610
+ "passRate": 1,
611
+ "passAtK": 1,
612
+ "grader": "regex+judge",
613
+ "status": "ran",
614
+ "trialRecords": [
615
+ {
616
+ "output": "The `trigger('click')` call is not awaited. Vue's DOM updates are asynchronous by default — `trigger()` returns a promise that resolves after Vue has flushed the update queue, but the test asserts on `wrapper.find('span').text()` synchronously in the next line, before that flush has happened. Depending on scheduling (and other microtasks in flight), the assertion sometimes reads the already-updated DOM and sometimes reads the stale DOM. That's the intermittency.\n\nFix:\n\n```ts\nawait wrapper.find('button').trigger('click')\nexpect(wrapper.find('span').text()).toBe('1')\n```\n\nPer the workflow's Step 3: always `await` a `trigger()`, and if the click kicks off an async composable/store call whose result feeds the DOM you're asserting on, also flush that work before asserting:\n\n```ts\nawait wrapper.find('button').trigger('click')\nawait flushPromises() // and/or await nextTick()\nexpect(wrapper.find('span').text()).toBe('1')\n```\n\nDo not paper over this with a `setTimeout(..., 0)` or by loosening the assertion. The arbitrary-timeout approach reintroduces the same flakiness (it just moves the race) and couples the test to timing rather than to Vue's own reactivity signal.\n\nOne more thing worth checking while you're in this file: per Step 4, prefer asserting on the rendered `<span>` text as you're doing rather than reaching into `wrapper.vm` — that part of the test is already correct, so the only change needed is the missing `await` (plus a `flushPromises()`/`nextTick()` only if an async call actually sits between the click and the updated text). Confirm with `npx vitest run <path-to-spec>` that it exits 0, ideally a few times in a row to be confident the flake is actually gone.",
617
+ "outputSha256": "60d30071effc0eba301fbfbefe6590c0d747086d15e210d30ce84d3df36b7165",
618
+ "promptSha256": "afc7cf644452fe2686cec974f37e270458b4dc7a466512440aa1392d42eac7fc",
619
+ "deterministic": [
620
+ true
621
+ ],
622
+ "judge": {
623
+ "verdict": "pass",
624
+ "reason": "Explains trigger() returns a promise resolving after Vue flushes its async update queue, so the synchronous assertion reads stale DOM (\"Vue's DOM updates are asynchronous by default... before that flush has happened\"). Shows the fix as code: `await wrapper.find('button').trigger('click')` before the assertion, plus optional flushPromises/nextTick. No fail criterion: it explicitly rejects setTimeout/retries."
625
+ },
626
+ "passed": true
627
+ },
628
+ {
629
+ "output": "# The problem: missing `await`\n\nVue batches DOM updates asynchronously. `trigger('click')` returns a promise (a `nextTick`-based flush), and the DOM is not patched until that promise resolves. Your assertion runs synchronously on the same tick, before Vue has flushed the update, so `wrapper.find('span').text()` still reads the *previous* text.\n\nThe reason it fails only *intermittently* is that whether the DOM has been patched by the time the assertion runs depends on timing/event-loop scheduling (jsdom vs happy-dom, microtask ordering, whether other awaited work happened earlier in the test). One day the microtask resolves before your line; the next it doesn't. That non-determinism is the signature of an un-awaited flush.\n\n## The fix\n\n```ts\nawait wrapper.find('button').trigger('click')\nexpect(wrapper.find('span').text()).toBe('1')\n```\n\n`trigger()`'s returned promise resolves after Vue's next DOM flush. Awaiting it guarantees the patch has happened before you read the DOM.\n\n## If the click kicks off an async call\n\nWhen the click handler triggers an async composable/store action (e.g. a `fetch`), awaiting `trigger()` alone is not enough — it only waits for the synchronous part of the handler plus one flush. Also await a helper that drains pending promises *and* flushes:\n\n```ts\nimport flushPromises from 'flush-promises'\n\nawait wrapper.find('button').trigger('click')\nawait flushPromises()\nawait nextTick()\nexpect(wrapper.find('span').text()).toBe('1')\n```\n\nUse the project's existing flush helper if it has one (check a sibling test for the convention) rather than introducing a new dependency.\n\n## What *not* to do\n\n- Do **not** bandage it with `await new Promise(r => setTimeout(r, 50))`. That reintroduces timing-dependent flakiness under load and slows the suite for no guarantee.\n- Do **not** force a synchronous update via `wrapper.vm.$forceUpdate()` — it sidesteps the very reactivity path you're meant to be testing.\n- Do **not** switch the assertion to `wrapper.vm.count` to \"make it pass faster.\" That trades a real (asynchronous-behavior) assertion for an implementation-detail one; a refactor that renames the ref breaks the test even when behavior is unchanged.\n\n## Quick confirmation\n\nTo prove the diagnosis before changing anything, insert `await nextTick()` (or `await flushPromises()`) between the two lines and re-run a handful of times. If it goes green consistently, it was the missing await, not a genuine counter bug. Then apply the fix above (`await` on the `trigger()` itself is the cleanest, most idiomatic form).\n\n## Verify\n\n```\nnpx vitest run <path-to-spec>\nnpx vitest run\n```\n\nConfirm the target spec passes repeatedly (run it a few times to be sure it's deterministically green, not just lucky) and that the full suite still passes. Ensure `git status` shows changes confined to the test file.",
630
+ "outputSha256": "29538d45170b5ea5f40b21493c1efeeafba4a48cd74d77d03385460cdaa39ae1",
631
+ "promptSha256": "afc7cf644452fe2686cec974f37e270458b4dc7a466512440aa1392d42eac7fc",
632
+ "deterministic": [
633
+ true
634
+ ],
635
+ "judge": {
636
+ "verdict": "pass",
637
+ "reason": "Explains Vue batches DOM updates asynchronously and trigger() returns a nextTick-based promise, so the assertion reads stale DOM (\"DOM is not patched until that promise resolves\"). Shows the fix as code: `await wrapper.find('button').trigger('click')` before the assertion. It also warns against setTimeout delays rather than using them."
638
+ },
639
+ "passed": true
640
+ },
641
+ {
642
+ "output": "The `trigger('click')` call isn't awaited. Vue 3 batches DOM updates and flushes them asynchronously (in a microtask, `flush: 'pre'` by default), so the assertion on the very next line runs against DOM that may or may not have been re-rendered yet — hence the intermittent failure. It sometimes passes because the microtask happens to have flushed by the time the assertion runs; it sometimes fails because it hasn't.\n\n**Fix:**\n\n```ts\nawait wrapper.find('button').trigger('click')\nexpect(wrapper.find('span').text()).toBe('1')\n```\n\n`trigger()` (and `@testing-library/vue`'s `fireEvent`) returns a promise that resolves after Vue has flushed the pending update, so awaiting it guarantees the DOM is settled before you read it.\n\n**Two related gotchas to watch for:**\n\n1. If the click kicks off an *async* operation whose result drives the DOM (an API/composable/store call), awaiting `trigger()` alone may not be enough — the promise chain started by the handler can still be in flight. In that case await the flush explicitly:\n\n ```ts\n await wrapper.find('button').trigger('click')\n await flushPromises()\n await nextTick()\n expect(wrapper.find('span').text()).toBe('1')\n ```\n\n Never paper over this with a `setTimeout`/sleep — that's flaky by construction and by a different mechanism than the bug you're fixing.\n\n2. The same rule applies to any assertion that depends on a reactive change, not just clicks — `setProps`, `setValue` on an input, `setData`, etc. They're all asynchronous and all need an `await`.\n\nOne thing not to \"fix\": if awaiting the trigger still leaves the text at the wrong value, that's a real behavior bug in the component (or a stale expectation), not a timing issue. Fix the component or correct the assertion, and say which — don't weaken the assertion to make it green.",
643
+ "outputSha256": "2ee5f3b69842e19b64b3343afbb830b78e38519ebcb4a82c28ce32e644850ad1",
644
+ "promptSha256": "afc7cf644452fe2686cec974f37e270458b4dc7a466512440aa1392d42eac7fc",
645
+ "deterministic": [
646
+ true
647
+ ],
648
+ "judge": {
649
+ "verdict": "pass",
650
+ "reason": "Explains Vue 3 batches DOM updates and flushes asynchronously in a microtask, so the assertion runs against possibly-stale DOM. Shows the fix as code: `await wrapper.find('button').trigger('click')` before the assertion. No fail criterion: it explicitly rejects setTimeout/sleep as flaky."
651
+ },
652
+ "passed": true
653
+ },
654
+ {
655
+ "output": "The bug is the missing `await` on `trigger()`. Vue's DOM updates are asynchronous by default (the scheduler flushes them in a microtask after the interaction), so the `expect` on the very next line reads the DOM *before* Vue has re-rendered the `span`. It sometimes passes by accident when something else in the event loop happens to flush the update first — that's why it's intermittent rather than a hard failure.\n\nThe fix:\n\n```ts\nawait wrapper.find('button').trigger('click')\nexpect(wrapper.find('span').text()).toBe('1')\n```\n\n`trigger()` returns the `nextTick` promise, so awaiting it resolves after Vue has flushed the queued render and the `span` reflects the new value. That's why the test needs no separate `nextTick()` here — the awaited trigger already covers the synchronous state change.\n\nA few things to check that often accompany this exact pattern:\n\n- **If the click kicks off an async call** (a composable, a store action, a fetch), awaiting the trigger is *not* enough — it only flushes Vue's render, not the pending promise. Await the promise resolution too:\n ```ts\n await wrapper.find('button').trigger('click')\n await flushPromises()\n // or: await nextTick() if the state settles synchronously inside the handler\n expect(wrapper.find('span').text()).toBe('1')\n ```\n Never substitute an arbitrary `setTimeout` for this; it reintroduces the flakiness you're trying to remove.\n\n- **Do not** \"fix\" it by loosening the assertion (e.g. checking the counter exists rather than its value) or by hard-coding a delay. Both hide the real ordering problem and the flake will resurface elsewhere.\n\n- The same rule applies to `@testing-library/vue`'s `fireEvent` and to any `setProps`/`setValue` call — always `await` it before asserting on the resulting DOM.\n\nFor the underlying counter, also make sure the state change is actually synchronous with respect to the click handler; if the component updates the counter inside a `watch` with a `{ flush: 'post' }` option, or debounces the update, the awaited trigger alone won't be sufficient and you'll need the `flushPromises()` / `nextTick()` follow-up above. But for the plain \"click increments a ref\" case, adding the single `await` is the complete fix.",
656
+ "outputSha256": "0ba07447662b52c3043216c52033e350abfeef6ef6d5d6c029c1efea2ee2c834",
657
+ "promptSha256": "afc7cf644452fe2686cec974f37e270458b4dc7a466512440aa1392d42eac7fc",
658
+ "deterministic": [
659
+ true
660
+ ],
661
+ "judge": {
662
+ "verdict": "pass",
663
+ "reason": "Explains Vue DOM updates are async/batched (\"scheduler flushes them in a microtask\"), so the expect reads DOM before re-render. Shows fix code: `await wrapper.find('button').trigger('click')` before the assertion. No fail criterion: it explicitly rejects setTimeout/delay and loosening assertions."
664
+ },
665
+ "passed": true
666
+ },
667
+ {
668
+ "output": "The problem is that `trigger('click')` is asynchronous. Vue batches DOM updates and flushes them on the next microtask tick, so the `expect` right after runs against the DOM *before* Vue has re-rendered. Whether the assertion passes depends on timing — hence the intermittent failure.\n\nTwo things are wrong:\n\n1. The `trigger()` isn't awaited.\n2. Even after awaiting, if the click handler kicks off any async work (a composable, a store action, a fetch), you may need to let that settle too before the DOM reflects it.\n\n**Fix — minimum:**\n\n```ts\nawait wrapper.find('button').trigger('click')\nexpect(wrapper.find('span').text()).toBe('1')\n```\n\n**Fix — when the handler does async work:**\n\n```ts\nimport { flushPromises } from '@vue/test-utils'\n\nawait wrapper.find('button').trigger('click')\nawait flushPromises() // let store actions / promises resolve\nawait nextTick() // ensure the resulting re-render has flushed\nexpect(wrapper.find('span').text()).toBe('1')\n```\n\nNote on that second form: `await trigger(...)` already awaits `nextTick()` internally, so the trailing `nextTick()` is only needed if your own `flushPromises()` step re-triggered further reactive updates. Don't add it reflexively.\n\n**What not to do:** don't \"fix\" this with `setTimeout(() => expect(...), 0)` or `vi.waitFor` with an arbitrary delay. Those make the test slow and still timing-dependent. Awaiting the trigger (plus `flushPromises` only if there's genuine async work) is the correct, deterministic fix.\n\nA quick sanity check that you've actually fixed the root cause: the test should pass reliably when run repeatedly (`npx vitest run <file>` several times, or `--repeat`), not just once. If it still flakes, your click handler is doing real async work you're not waiting on — add the `flushPromises()` step and mock the network boundary it hits.",
669
+ "outputSha256": "d29de6bb3f40bd0eaac98ae7bf33a0e64a51b269e0bf94282874f74a7142e9ba",
670
+ "promptSha256": "afc7cf644452fe2686cec974f37e270458b4dc7a466512440aa1392d42eac7fc",
671
+ "deterministic": [
672
+ true
673
+ ],
674
+ "judge": {
675
+ "verdict": "pass",
676
+ "reason": "Explains trigger() is async because Vue batches DOM updates flushed on next microtask, so the expect reads stale DOM (\"Vue batches DOM updates and flushes them on the next microtask tick\"). Shows the fix as code: `await wrapper.find('button').trigger('click')` before the assertion. No fail criterion: it explicitly warns against setTimeout/arbitrary delays."
677
+ },
678
+ "passed": true
679
+ },
680
+ {
681
+ "output": "The bug is the missing `await` on `trigger('click')`.\n\n## Why it fails intermittently\n\nVue 3 batches DOM updates and flushes them **asynchronously**, not synchronously with the click handler. When you call `trigger('click')` without `await`, the click handler (and whatever updates the reactive count ref) may run, but the re-render that writes the new text into the `<span>` is scheduled on Vue's next microtask flush rather than happening inline.\n\nSo at the moment `expect(...)` runs, the DOM is often still showing the old text. It's \"intermittent\" because whether the flush has landed by the time the assertion executes depends on the exact microtask/timing interleaving — sometimes the update wins the race, sometimes it doesn't. That nondeterminism is the tell that you're reading un-settled DOM, not that the component is flaky.\n\n`trigger()` returns a promise (it calls `nextTick()` internally after dispatching the event), so not awaiting it throws away the very signal you need.\n\n## The fix\n\n```ts\nawait wrapper.find('button').trigger('click')\nexpect(wrapper.find('span').text()).toBe('1')\n```\n\nAwaiting the trigger waits for Vue to flush the pending update before the assertion runs, so the `<span>` reliably reflects the new count.\n\n## If the click kicks off async work (store action, fetched data, composable)\n\nAwaiting `trigger()` only settles Vue's *render* flush — it does **not** await an in-flight promise your handler started (e.g. a Pinia action that awaits a mocked API call). In that case add a flush before asserting:\n\n```ts\nawait wrapper.find('button').trigger('click')\nawait flushPromises() // or the project's existing helper\nexpect(wrapper.find('span').text()).toBe('1')\n```\n\n`flushPromises()` from `@vue/test-utils` drains pending microtasks; follow with `await nextTick()` only if you still observe stale DOM. Never substitute a `setTimeout(...)` — it's slow and still racy.\n\n## Two things not to do\n\n- Don't \"fix\" it by asserting on `wrapper.vm.count` instead. That reads internal reactive state and couples the test to a ref name; if the DOM isn't updating, that's a real component bug you'd be masking, not a test problem.\n- Don't retry/poll the assertion to paper over the race. The un-awaited trigger is the actual defect.\n\n## Verify\n\nAfter the change, run the target file and then the full suite:\n\n```bash\nnpx vitest run <path-to-spec>\nnpx vitest run\n```\n\nConfirm every `trigger()`/`fireEvent` in the file is `await`ed and no arbitrary `setTimeout` was introduced waiting for the DOM.",
682
+ "outputSha256": "4b6dc7500dea070cd612888ed44589b0b719c8e8b59bbc2576334de2083f67cc",
683
+ "promptSha256": "afc7cf644452fe2686cec974f37e270458b4dc7a466512440aa1392d42eac7fc",
684
+ "deterministic": [
685
+ true
686
+ ],
687
+ "judge": {
688
+ "verdict": "pass",
689
+ "reason": "Explains Vue batches DOM updates and flushes asynchronously, so un-awaited trigger() reads stale DOM ('re-render ... scheduled on Vue's next microtask flush'). Shows the fix as code: `await wrapper.find('button').trigger('click')` before the assertion. No fail criterion: it explicitly rejects setTimeout/retries."
690
+ },
691
+ "passed": true
692
+ },
693
+ {
694
+ "output": "The bug: `trigger('click')` returns a promise, and Vue 3 batches DOM updates asynchronously (on the microtask queue, `flush: 'pre'` by default). You're asserting on `wrapper.find('span').text()` before that update has flushed, so you read the *old* DOM. It fails intermittently rather than always because whether the microtask has drained before your synchronous assertion runs depends on how much other async work happens to be queued around it — sometimes the tick lands in time, sometimes it doesn't. That flakiness is the tell.\n\nThe fix is to `await` the trigger, which forces the returned promise to resolve *after* the component's DOM has updated:\n\n```ts\nawait wrapper.find('button').trigger('click')\nexpect(wrapper.find('span').text()).toBe('1')\n```\n\nThat's the whole fix for the synchronous-count case. `trigger()` resolves after Vue's reactivity flush completes, so anything rendered as a direct result of the click — in the same tick — is settled by the time the assertion runs.\n\nTwo related cases worth handling at the same time, since they're the usual next failure:\n\n**If the click kicks off an async operation** (a store action awaiting a fetch, a debounce, etc.), awaiting the trigger is *not* enough — it only waits for the synchronous reactivity flush, not for the promise your handler started. In that case also flush the pending promises:\n\n```ts\nawait wrapper.find('button').trigger('click')\nawait flushPromises() // from @vue/test-utils\n// optionally: await nextTick() if a further reactive cascade happens after\nexpect(wrapper.find('span').text()).toBe('1')\n```\n\nNever substitute an arbitrary `setTimeout(..., 0)` or `setTimeout(..., 50)` for either of these — it \"fixes\" the flake by buying an unbounded amount of time and will still fail under load or CI slowness.\n\nIf this is genuinely a one-off render rather than an async handler, `await nextTick()` after the trigger also works, but awaiting `trigger()` itself is the idiomatic and sufficient form — prefer it.\n\nLast thing: after you make it pass, run `npx vitest run` a few times in a row (or `vitest run --repeat`) to confirm the flake is actually gone rather than just having landed the other way on this particular run.",
695
+ "outputSha256": "b92ace5589e4bd817229befdc7cb2f590c0962b39d4b23dc745232b5b4d996f1",
696
+ "promptSha256": "afc7cf644452fe2686cec974f37e270458b4dc7a466512440aa1392d42eac7fc",
697
+ "deterministic": [
698
+ true
699
+ ],
700
+ "judge": {
701
+ "verdict": "pass",
702
+ "reason": "Explains Vue batches DOM updates asynchronously on the microtask queue (flush:'pre'), so the assertion reads stale DOM before the flush. Shows the fix as code: `await wrapper.find('button').trigger('click')` before the expect. Does not attribute flakiness to CI/retries; explicitly warns against setTimeout substitutes."
703
+ },
704
+ "passed": true
705
+ },
706
+ {
707
+ "output": "The bug is the missing `await` on `trigger()`. Vue schedules DOM updates asynchronously (the default `flush: 'pre'` update queue), so the line right after an un-awaited `trigger()` reads the DOM *before* the re-render has happened. Depending on microtask timing — and on whether anything else in the test happens to yield to the event loop first — the assertion sometimes sees the updated text and sometimes sees the stale text. That's exactly the intermittent failure you're seeing.\n\nFix:\n\n```ts\nawait wrapper.find('button').trigger('click')\nexpect(wrapper.find('span').text()).toBe('1')\n```\n\n`trigger()` returns `Vue.nextTick()`, so awaiting it flushes the component's queued re-render before control returns to your assertion.\n\nIf the click kicks off an *async* handler (a store action, a composable fetch, a mocked API call), awaiting `trigger()` alone isn't enough — it only flushes the update caused by the click itself, not the update that lands after the promise resolves. In that case add:\n\n```ts\nimport { flushPromises } from '@vue/test-utils'\n\nawait wrapper.find('button').trigger('click')\nawait flushPromises() // drain pending promise callbacks\nawait nextTick() // flush the re-render they queued\nexpect(wrapper.find('span').text()).toBe('1')\n```\n\n(or the project's existing equivalent helper — check a neighbouring spec for the established convention before introducing a new one).\n\nWhat *not* to do: replacing the assertion timing with a `setTimeout(..., 0)` or `await new Promise(r => setTimeout(r))`. That papers over the race with a timing guess instead of synchronizing on Vue's actual update queue, and it will resurface as a flake on a slower CI machine.\n\nTwo related things worth checking while you're in this file:\n\n- If the whole `it(...)` body has several interactions and DOM assertions interleaved, every `trigger()` in it needs the same `await` — an un-awaited one earlier in the test can leave queued work that surfaces as a failure on a *later* assertion, which reads as \"the wrong line is flaky.\"\n- If the counter lives in a Pinia store and the component reads it, confirm the test gives the component a fresh store instance (`createTestingPinia()` or `createPinia()` + `setActivePinia()` in `beforeEach`). Shared store state across tests produces a different flavour of intermittent failure — a count that starts at 1 instead of 0 because a previous test already incremented it — and it's easy to misattribute to the missing `await` once you've fixed that one.",
708
+ "outputSha256": "13b5dafe73872bd23b06737afe23fe3b4388aab46f0a4b6d866989ceb8db4cc8",
709
+ "promptSha256": "afc7cf644452fe2686cec974f37e270458b4dc7a466512440aa1392d42eac7fc",
710
+ "deterministic": [
711
+ true
712
+ ],
713
+ "judge": {
714
+ "verdict": "pass",
715
+ "reason": "Explains Vue schedules DOM updates asynchronously (flush:'pre' queue) so un-awaited trigger() reads stale DOM, causing intermittent failure. Shows fix code: `await wrapper.find('button').trigger('click')` before the assertion. No fail criterion: it explicitly warns against setTimeout/retry timing guesses."
716
+ },
717
+ "passed": true
718
+ },
719
+ {
720
+ "output": "The root cause is in Step 3's rule (and the first Red Flag): Vue defers DOM updates. `trigger('click')` returns a promise that resolves after Vue flushes its update queue, but that promise isn't `await`ed here, so the assertion runs against stale DOM — the `<span>` still shows the pre-click value. The intermittency comes from whether the microtask flush happens to race ahead of the assertion; under load or with extra async work in the handler it loses the race, otherwise it wins. It's a race, not a real logic bug in the component.\n\nThe fix has two parts.\n\n1. Await the trigger so Vue flushes before you assert:\n\n```ts\nawait wrapper.find('button').trigger('click')\nexpect(wrapper.find('span').text()).toBe('1')\n```\n\nVue's DOM updates are asynchronous (`flush: 'pre'` by default), and `await`ing `trigger()` waits for that flush to complete. This alone resolves the plain synchronous-click case.\n\n2. If the click handler kicks off an async operation (an API call, a `await`-ed composable, a store action that resolves later), awaiting the trigger is not enough — the trigger's flush completes before the async result lands. You also need to let the pending promise settle before asserting:\n\n```ts\nawait wrapper.find('button').trigger('click')\nawait flushPromises() // drain the handler's pending async work\nawait nextTick() // then let the resulting DOM update flush\nexpect(wrapper.find('span').text()).toBe('1')\n```\n\n`flushPromises()` is from `@vue/test-utils`. Check Step 1: if the project already has a shared \"settle\" helper next to its other specs, reuse that instead of introducing a second convention. Never substitute an arbitrary `setTimeout` — it's the red-flag pattern of guessing at a delay rather than waiting for the actual flush, and it re-introduces the same race under different timing.\n\nTwo notes so the fix is actually correct and not just green:\n\n- Don't \"fix\" this by changing the assertion to match the stale value or by loosening it (e.g. asserting the text is `'0'` or `'1'`). That deletes the coverage the test was supposed to provide. The assertion is right; the wait is wrong.\n- Once awaited, confirm the test fails for the right reason when the component is broken — temporarily revert the counter increment and check the test goes red, then restore it. A test that passes both before and after the fix isn't testing anything.\n\nThen verify per the skill's Verification section:\n\n```bash\nnpx vitest run <path-to-spec>\nnpx vitest run\n```\n\nRun the full suite too, in case a shared Pinia instance or module mock leaks state into this test (that's a separate failure mode, but worth ruling out if it still flakes). Changes should be confined to the test file; `git status` should show nothing else touched.",
721
+ "outputSha256": "9ef603aab88780707262181973f28150757b6441e5d3ab6fe4dc24575e0e7144",
722
+ "promptSha256": "afc7cf644452fe2686cec974f37e270458b4dc7a466512440aa1392d42eac7fc",
723
+ "deterministic": [
724
+ true
725
+ ],
726
+ "judge": {
727
+ "verdict": "pass",
728
+ "reason": "Explains Vue defers/batches DOM updates and trigger() returns a promise resolving after flush, so un-awaited assertion reads stale DOM (\"Vue defers DOM updates... assertion runs against stale DOM\"). Shows fix code: `await wrapper.find('button').trigger('click')` before the expect. No fail criterion: intermittency attributed to the missing await race, not slow CI/retries/setTimeout."
729
+ },
730
+ "passed": true
731
+ },
732
+ {
733
+ "output": "The bug is that the click's DOM update is asynchronous and you never awaited it. In Vue 3, DOM updates triggered by a reactive state change are batched and flushed on the next microtask tick, not synchronously during `trigger()`. `trigger()` itself returns a promise (`nextTick`) that resolves after that flush — so the assertion right after an un-awaited `trigger()` can run before the DOM has actually re-rendered. When the microtask queue happens to drain before your assertion runs (e.g. something else awaited in between), you get `'1'`; otherwise you still see the old text `'0'`. That's exactly the intermittent behavior.\n\nFix: await the trigger.\n\n```ts\nawait wrapper.find('button').trigger('click')\nexpect(wrapper.find('span').text()).toBe('1')\n```\n\n`await`ing `trigger()` waits for `nextTick()`, which is when the queued DOM update from the click's state change is flushed. This is deterministic — no more race.\n\nA few related notes so the fix actually holds:\n\n- **If the click kicks off an async call** (a `fetch`, a store action, an async composable), awaiting `trigger()` is not enough by itself. The reactivity update happens after the async work resolves, so add `await flushPromises()` (from `@vue/test-utils`) and, if needed, one more `await nextTick()` before asserting:\n\n ```ts\n await wrapper.find('button').trigger('click')\n await flushPromises()\n expect(wrapper.find('span').text()).toBe('1')\n ```\n\n- **Never paper over this with `setTimeout`.** An arbitrary delay is the same race with a bigger window; it makes the test slower and still flaky under load.\n\n- **Same rule applies to any interaction** — `fireEvent` from `@testing-library/vue`, form `setValue`, etc. Await them all before asserting on DOM that depends on them.\n\n- **Prefer asserting rendered output over internals.** `wrapper.find('span').text()` is the right kind of assertion here. Don't replace it with `wrapper.vm.count` — that couples the test to an internal ref name and will break on a behavior-preserving refactor.\n\nIf it still fails intermittently after adding the `await`, the async-`flushPromises` path above is the next thing to check — the assertion is probably reading the DOM before the component's async source has settled, not before Vue's render flush.",
734
+ "outputSha256": "c05c29549a463f4c34be696b54350b5b607b2a787ca61f8598841810e0ab9bfe",
735
+ "promptSha256": "afc7cf644452fe2686cec974f37e270458b4dc7a466512440aa1392d42eac7fc",
736
+ "deterministic": [
737
+ true
738
+ ],
739
+ "judge": {
740
+ "verdict": "pass",
741
+ "reason": "Explains DOM updates are batched/flushed on next microtask tick, so un-awaited trigger() reads stale DOM (\"batched and flushed on the next microtask tick\"). Shows fix code: `await wrapper.find('button').trigger('click')` before the assertion. No fail criterion: it explicitly rejects setTimeout delays."
742
+ },
743
+ "passed": true
744
+ }
745
+ ]
746
+ },
747
+ {
748
+ "id": "assert-emitted-payload-not-presence",
749
+ "kind": "behavior",
750
+ "prompt": "My component test checks that clicking save fires the 'submit' event: `expect(wrapper.emitted('submit')).toBeTruthy()`. The test passes, but a teammate says it wouldn't catch a bug where the wrong data is submitted. What should the assertion actually check?",
751
+ "strictness": "high",
752
+ "trials": 10,
753
+ "passes": 10,
754
+ "passRate": 1,
755
+ "passAtK": 1,
756
+ "grader": "judge",
757
+ "status": "ran",
758
+ "trialRecords": [
759
+ {
760
+ "output": "Your teammate is right. `toBeTruthy()` only confirms the event fired at least once — it says nothing about what was passed to it. A component that emits `submit` with an empty object, a stale snapshot, or the wrong field entirely would still pass this assertion, so the test would stay green while the actual save behavior is broken.\n\n**What to assert instead**\n\n`wrapper.emitted('submit')` returns an array of calls, where each call is an array of the arguments emitted for that call. So for a single save click you want to check the payload of the first (and only) call:\n\n```ts\nconst events = wrapper.emitted('submit')\nexpect(events).toHaveLength(1) // fired exactly once\nexpect(events![0]).toEqual([expectedPayload])\n```\n\n`events![0]` is the argument list for the first emission. If your component emits a single object argument (`emit('submit', formData)`), the assertion is `expect(events![0][0]).toEqual({ id: 1, name: '...' })`. If it emits multiple arguments (`emit('submit', id, payload)`), compare the whole array `[id, payload]` so a swapped argument order is caught too.\n\n**Tips that make it actually catch the bug**\n\n- Derive `expectedPayload` from the same props/inputs the test set up, not by copying whatever the component currently produces — otherwise the test just re-encodes the current (possibly wrong) behavior.\n- Prefer `toEqual` over `toBe`, and avoid `toMatchObject` unless you deliberately want to allow extra fields; `toMatchObject` will let a payload with wrong extra keys slip through.\n- If the component is supposed to pull values from specific inputs, assert those values explicitly, e.g. fill the form with a value only that bug would mangle, then assert the emitted payload contains it.\n- Also assert the call count (`toHaveLength(1)`) so a double-emit bug is caught, and consider asserting DOM/state that should change as a result (e.g. a \"saving\" flag or a disabled button), since the event alone doesn't prove the click path fully worked.\n\nOne caveat: don't switch to asserting `wrapper.vm.someInternalState` to \"see the data\" — the emitted payload is the public contract and is the right thing to pin down. Keep the assertion on `wrapper.emitted('submit')` and just make it check the payload, not merely presence.",
761
+ "outputSha256": "6b3d63748f179d7c984802fef290dc9176bf67d721d2c383064af7e7f217e18e",
762
+ "promptSha256": "9c3ec07300523cf7feadb2b771d5b51798047aed4b05335ebf6f6f0731594dc5",
763
+ "deterministic": [],
764
+ "judge": {
765
+ "verdict": "pass",
766
+ "reason": "Answer states toBeTruthy only confirms the event fired, not what was passed (empty object/stale snapshot/wrong field). Concrete fix shown: `expect(events![0]).toEqual([expectedPayload])`, plus `expect(events![0][0]).toEqual({...})`. No fail criterion: it fixes the assertion rather than deferring to another test."
767
+ },
768
+ "passed": true
769
+ },
770
+ {
771
+ "output": "Your teammate is right. `expect(wrapper.emitted('submit')).toBeTruthy()` only proves the event fired at least once — it says nothing about what was passed with it. A component that emits `submit` with `undefined`, an empty object, or the wrong form values still passes this assertion.\n\n`wrapper.emitted()` returns an object whose values are arrays (one entry per emission), where each entry is itself the array of arguments that emission was called with. So the assertion should reach into that payload and check the actual submitted data:\n\n```ts\nconst emitted = wrapper.emitted('submit')\nexpect(emitted).toHaveLength(1)\n\n// emitted![0] is the args array for the first emission, i.e. [payload]\nconst [payload] = emitted![0] as [SubmitPayload]\nexpect(payload).toEqual({ id: 42, name: 'Ada', active: true })\n```\n\nA few points worth getting right:\n\n- **Use `toEqual` (or `toStrictEqual`), not `toBe`.** `toBe` compares identity, so it would only pass if the component emitted the exact same object reference your test constructed — which it almost never will. `toEqual` does a structural comparison, which is what you actually mean by \"the right data.\"\n- **Assert the emission count too.** If save is wired up correctly, one click should produce exactly one emission. `toHaveLength(1)` catches double-emit regressions that a `[0]`-only assertion would silently allow.\n- **Drive the payload from the component's actual inputs, not a literal you hand-wrote to match.** Set up the component with test data (fill an input, set a prop), then assert the emitted payload equals what that input should produce. Otherwise you've just hardcoded the same wrong value on both sides.\n- **Prefer this over `wrapper.vm`.** Checking the emitted payload keeps the test at the component's public boundary; reading `wrapper.vm.formData` couples the test to an internal ref that can be renamed without any behavior change.\n\nSo the concrete replacement for that one line is roughly:\n\n```ts\nawait wrapper.find('[data-testid=\"save\"]').trigger('click')\n\nconst emitted = wrapper.emitted('submit')\nexpect(emitted).toHaveLength(1)\nexpect(emitted![0][0]).toEqual({ id: 42, name: 'Ada', active: true })\n```\n\nNote the `await` on `trigger()` as well — without it you can be reading `emitted()` before the handler has run.\n\nOne caveat: this only catches a *wrong payload* bug for the fields you actually assert on. If the form has ten fields and you check three, a bug in the other seven still slips through. Assert on the full payload object you expect, not a hand-picked subset, unless there's a specific reason to ignore a field.",
772
+ "outputSha256": "770ce43d2f9966a435481b697fc45a128eab567337d0e86e8a9573d7212c957e",
773
+ "promptSha256": "9c3ec07300523cf7feadb2b771d5b51798047aed4b05335ebf6f6f0731594dc5",
774
+ "deterministic": [],
775
+ "judge": {
776
+ "verdict": "pass",
777
+ "reason": "Answer states toBeTruthy only proves the event fired, not what was passed (\"says nothing about what was passed with it\"), and shows concrete payload assertions: expect(emitted![0][0]).toEqual({id:42,...}) and expect(payload).toEqual(...). No fail criterion; it fixes the assertion rather than deferring to another test."
778
+ },
779
+ "passed": true
780
+ },
781
+ {
782
+ "output": "Your teammate is right. `wrapper.emitted('submit')` only tells you the event fired — it says nothing about the argument passed to `emit('submit', payload)`. A component that emits `submit` with a stale, partial, or wrongly-shaped payload passes that assertion while being completely broken.\n\n## What to assert instead\n\nCheck the payload itself. `wrapper.emitted(name)` returns an array of per-call argument arrays, so index into it:\n\n```js\nconst emitted = wrapper.emitted('submit')\nexpect(emitted).toHaveLength(1) // fired exactly once, not zero or twice\nexpect(emitted![0]).toEqual([expectedPayload]) // the args of the FIRST emit call\n```\n\nTwo details that trip people up:\n\n- **`emitted('submit')` is an array of calls, and each call is an array of args.** So `emitted[0]` is the argument list of the first emission, not the payload directly. If the component does `emit('submit', form)`, the payload is `emitted[0][0]`.\n- **Assert on the actual expected value, not just its shape.** `expect(emitted[0][0]).toMatchObject({ id: 1 })` is stronger than checking `typeof payload === 'object'`, but `toEqual`/`toMatchObject` against the concrete expected payload is what actually catches \"submitted the wrong data.\"\n\n## Stronger still: drive it through a real interaction\n\nThe assertion above only proves the component emits the right thing for whatever state it's in. To catch \"wrong data,\" set up the state the test is about and let the click read it:\n\n```js\nconst wrapper = mount(SaveForm, { props: { initial: { name: '', email: '' } } })\n\nawait wrapper.find('[data-testid=\"name\"]').setValue('Ada')\nawait wrapper.find('[data-testid=\"email\"]').setValue('ada@example.com')\nawait wrapper.find('[data-testid=\"save\"]').trigger('click')\n\nconst emitted = wrapper.emitted('submit')\nexpect(emitted).toHaveLength(1)\nexpect(emitted![0][0]).toEqual({ name: 'Ada', email: 'ada@example.com' })\n```\n\nNow the test fails if the component submits a stale name, drops the email, or submits the initial empty values — exactly the bug class your teammate described.\n\n## Things to avoid while you're here\n\n- Don't swap the assertion for `expect(wrapper.vm.someState)` — that couples the test to an internal ref name and breaks on harmless refactors while still not proving the payload is right.\n- Keep the `await` on `trigger()` (and any `setValue`) — without it you may be reading the DOM/emitted state before Vue flushes, which can make the payload assertion flaky or falsely green.\n- If the component validates before emitting, add a case where the payload is invalid and assert `emitted('submit')` is `undefined` (event did **not** fire) — that's a separate, real behavior worth pinning down.\n\nAsserting the payload — the concrete value from `emitted(name)[0][0]` — is what turns this from \"an event happened\" into \"the right thing was submitted.\"",
783
+ "outputSha256": "e9bbf030eb64e4ecd701bf9ef9e835ed509c2e3e0f942be2735b5ec2b5f5c562",
784
+ "promptSha256": "9c3ec07300523cf7feadb2b771d5b51798047aed4b05335ebf6f6f0731594dc5",
785
+ "deterministic": [],
786
+ "judge": {
787
+ "verdict": "pass",
788
+ "reason": "Answer states emitted('submit') only proves the event fired, not the payload ('says nothing about the argument passed'). Concrete fix shown: expect(emitted![0]).toEqual([expectedPayload]) and expect(emitted![0][0]).toEqual({name:'Ada',email:'ada@example.com'}). No fail criterion: it never recommends keeping toBeTruthy() and relying on another test."
789
+ },
790
+ "passed": true
791
+ },
792
+ {
793
+ "output": "Your teammate is right: `expect(wrapper.emitted('submit')).toBeTruthy()` only proves the event fired at least once. It says nothing about what was passed to it, so a component that emits `submit` with the wrong payload — stale form state, an unmapped field, a missing id — still passes.\n\n`wrapper.emitted('submit')` returns an array of emitted calls, where each entry is that call's argument list. So check the argument, e.g.:\n\n```ts\nexpect(wrapper.emitted('submit')).toHaveLength(1)\nexpect(wrapper.emitted('submit')![0]).toEqual([expectedPayload])\n```\n\nA few things to get right:\n\n- `emitted('submit')` is `undefined` until the event actually fires, hence the non-null assertion (or a guard) once you index into it. Asserting the length first keeps the failure message readable: \"expected 1 emit, got 0\" instead of a confusing \"cannot read [0] of undefined.\"\n- Note the double nesting: each entry is the argument list for that call, so if the handler was invoked as `emit('submit', payload)`, the entry is `[payload]`, and you compare against `[expectedPayload]`, not `expectedPayload`. Getting this wrong is a common source of a test that passes for the wrong reason.\n- If the component emits multiple times (e.g. an optimistic emit plus a confirmed one), assert on the specific call you mean — `emitted('submit')![1]` — rather than `toHaveLength(1)`, which would then be wrong.\n\nAnd since you're already fixing this: make sure the assertion is `await`ed from the trigger, i.e. `await wrapper.find('[data-testid=\"save\"]').trigger('click')`. An un-awaited trigger reads stale DOM/environment and can pass or fail depending on batching, which is its own latent flakiness.\n\nOne caveat on payload choice: assert against the actual data the component is responsible for submitting — the values you set on the form/input in this test — not a fabricated literal unrelated to what the test drove. If you type `\"Acme\"` into the name field and click save, the expected payload should contain `\"Acme\"`. Otherwise the assertion is decoupled from the setup and you're back to testing nothing meaningful.",
794
+ "outputSha256": "8d4e3b5b0fbd5e5fe5cdbd421479be68075c09d233d3b1567fed0af6ed8d3438",
795
+ "promptSha256": "9c3ec07300523cf7feadb2b771d5b51798047aed4b05335ebf6f6f0731594dc5",
796
+ "deterministic": [],
797
+ "judge": {
798
+ "verdict": "pass",
799
+ "reason": "Answer states toBeTruthy only proves the event fired, not the payload ('says nothing about what was passed to it'). Shows concrete fix: expect(wrapper.emitted('submit')![0]).toEqual([expectedPayload]), plus length check and double-nesting explanation. No fail criterion met."
800
+ },
801
+ "passed": true
802
+ },
803
+ {
804
+ "output": "Your teammate is right. `expect(wrapper.emitted('submit')).toBeTruthy()` only proves the event was emitted at least once — it says nothing about the argument the component handed to `emitter`. A component that calls `emit('submit', { userId: undefined })` or emits the wrong form field would still pass that assertion. The bug your teammate is describing — \"the wrong data is submitted\" — is exactly the class of bug this assertion is blind to.\n\n**What it should check:** the payload of the emitted event, not just its presence.\n\n```ts\nconst emitted = wrapper.emitted('submit')\nexpect(emitted).toHaveLength(1)\nexpect(emitted![0]).toEqual([{ name: 'Ada', email: 'ada@example.com' }])\n```\n\nA few specifics worth getting right:\n\n- **`wrapper.emitted('submit')` is an array of call-argument-arrays.** Each entry is the argument list for one emission, so `emitted![0]` is the args of the first call and `emitted![0][0]` is the first argument. If the component emits a single object, `emitted![0]` is `[{ ...payload }]`, so assert on `emitted![0][0]` (or `toEqual([expectedPayload])` on `emitted![0]`).\n- **Also assert the count.** `toHaveLength(1)` catches a double-fire (e.g. a click handler that also triggers on blur) that a payload-only check or `toBeTruthy()` would miss.\n- **Drive the payload through real input first.** The assertion is only meaningful if the test actually typed/selected the values the component is supposed to submit — use `await wrapper.find('[data-testid=\"name\"]').setValue('Ada')` before the save click, so the test proves the component reads its own state correctly, not just that it forwards some constant.\n- **Make the assertion tight enough to fail on the wrong data.** `toEqual` on the full payload is usually what you want. `expect.objectContaining({ ... })` is reasonable when some fields are genuinely irrelevant or non-deterministic, but don't reach for it just to avoid pinning down the fields the bug is most likely to corrupt.\n\nOne addition that often pays off here: if the payload is produced by a composable or store action rather than built inline, also assert the side effect that consumes it (the mocked API call received the same argument), since a component can emit the correct payload while the handler wired to it does the wrong thing.\n\nSo the fix is: keep the emission-presence check only as a precondition, and add a payload assertion (and usually a count assertion) so a regression in what gets submitted fails the test.",
805
+ "outputSha256": "88fbafdb0d509eb45ab8afa41d8cf0772ccb5113b7d443407f0ea59af3ac524b",
806
+ "promptSha256": "9c3ec07300523cf7feadb2b771d5b51798047aed4b05335ebf6f6f0731594dc5",
807
+ "deterministic": [],
808
+ "judge": {
809
+ "verdict": "pass",
810
+ "reason": "Answer states toBeTruthy only proves the event was emitted, not the argument (\"says nothing about the argument the component handed to emitter\"), and shows the concrete payload fix: expect(emitted![0]).toEqual([{ name: 'Ada', email: 'ada@example.com' }]). No fail criterion; it does not recommend presence-only."
811
+ },
812
+ "passed": true
813
+ },
814
+ {
815
+ "output": "Your teammate is right: `expect(wrapper.emitted('submit')).toBeTruthy()` only proves the event fired at least once — it says nothing about *what* was submitted. A component that emits `submit` with `{ name: '' }` instead of `{ name: 'Alice' }`, or with a stale/wrong id, or with `undefined`, all pass that assertion. That's exactly the class of bug the test is supposed to catch.\n\n## What the assertion should check\n\nAssert on the payload, not just the event's existence:\n\n```ts\nawait wrapper.find('[data-test=\"save\"]').trigger('click')\n\nconst submitted = wrapper.emitted('submit')\nexpect(submitted).toHaveLength(1) // fired exactly once\nexpect(submitted![0]).toEqual([{ // Vue wraps each emit's args in an array\n name: 'Alice',\n email: 'alice@example.com',\n}])\n```\n\nKey points:\n\n- **`wrapper.emitted('submit')` returns an array of emit calls**, and each element is the array of arguments passed to that emit. So a single-argument emit `emit('submit', payload)` shows up as `[[payload]]` — hence `submitted![0]` is `[payload]` and you compare against `[payload]`, or index again with `submitted![0][0]` if you want the payload object directly.\n- **Check the call count** when it matters (`toHaveLength(1)`), because a double-emit is also a real bug that a presence check misses.\n- **Use `toEqual` for object payloads**, matching the exact expected shape/values. Be deliberate about whether you spy on the whole object or use `expect.objectContaining(...)` if only a subset is stable.\n- **Drive the assertion from behavior**, not internals: populate the form the way a user would (set the input, trigger the change) and then assert the emitted payload reflects what was typed. That's what catches \"submits the wrong data.\"\n\n## Full shape of a corrected test\n\n```ts\nit('submits the entered form data when save is clicked', async () => {\n const wrapper = mount(SignupForm)\n\n await wrapper.find('[data-test=\"name-input\"]').setValue('Alice')\n await wrapper.find('[data-test=\"email-input\"]').setValue('alice@example.com')\n\n await wrapper.find('[data-test=\"save\"]').trigger('click')\n\n const submitted = wrapper.emitted('submit')\n expect(submitted).toHaveLength(1)\n expect(submitted![0]).toEqual([\n { name: 'Alice', email: 'alice@example.com' },\n ])\n})\n```\n\n## Things to keep in mind\n\n- **`await` the `trigger()`** (and the `setValue()` calls) — Vue's DOM updates are async, and an un-awaited trigger can let the test pass against stale state.\n- **Don't fall back to `wrapper.vm.someRef`** to grab the data — that couples the test to an internal ref name and reintroduces the \"passes for the wrong reason\" problem from a different angle.\n- **If you want to keep a lightweight presence test**, that's fine as a separate case, but the payload assertion above is the one that actually protects the submit behavior.\n\nSo the short answer: replace the presence check with an assertion on `wrapper.emitted('submit')![0]` (the emit's argument array), comparing the payload with `toEqual`, and assert the call count too.",
816
+ "outputSha256": "6eace59031914e32a4538005b004db37df78677e75ef5b17783e531bd2233147",
817
+ "promptSha256": "9c3ec07300523cf7feadb2b771d5b51798047aed4b05335ebf6f6f0731594dc5",
818
+ "deterministic": [],
819
+ "judge": {
820
+ "verdict": "pass",
821
+ "reason": "Answer states toBeTruthy only proves the event fired, not what was submitted (\"says nothing about *what* was submitted\"), and shows the concrete fix: expect(submitted![0]).toEqual([{ name: 'Alice', email: 'alice@example.com' }]). No fail criterion: it does not recommend relying on an unrelated test; the presence test is only optional alongside the payload assertion."
822
+ },
823
+ "passed": true
824
+ },
825
+ {
826
+ "output": "Your teammate is right. `wrapper.emitted('submit')` only returns an array of payload arrays (one entry per emit), so `toBeTruthy()` just confirms the event fired at least once — it says nothing about what was passed to the parent. A component that emits `submit` with the wrong form data, stale state, or `undefined` would still pass.\n\nWhat you actually want is to assert on the payload:\n\n```ts\nconst emitted = wrapper.emitted('submit')\nexpect(emitted).toHaveLength(1)\nexpect(emitted![0]).toEqual([{ name: 'Alice', email: 'alice@example.com' }])\n```\n\nKey points:\n\n- `emitted('submit')` is an array of calls; each call is an array of arguments. So `emitted![0]` is the first call's argument list. If the component emits a single object, you destructure it: `const [payload] = emitted![0]`.\n- Assert the concrete expected value, not just \"an argument exists.\" Checking `expect(emitted![0]).toBeDefined()` has the same weakness as `toBeTruthy()` on the event itself.\n- Ideally drive the expected value from the same inputs you set up: fill the fields (or set the props/store state) to known values, click save, then assert the emitted payload equals exactly those values. That way a bug that submits the initial/default state instead of the edited state, or that drops a field, fails the test.\n- If the component's job is \"submit normalized/validated data,\" also cover the transformation you care about (trimming, coercion, omitted empty fields) with a second case, so the assertion pins down behavior rather than just echoing whatever the component happens to pass through.\n\nOne caveat: don't over-specify to the point of asserting internal object identity or extra fields that aren't part of the component's contract. Assert the payload the parent actually consumes — that is the behavior the test should lock in.\n\nAlso make sure the `trigger('click')` that precedes this is awaited (or followed by `await nextTick()`), otherwise the assertion can read a stale emit list; since it's currently passing, that's probably already handled, but confirm it before rewriting the assertion so you don't chase a flake.",
827
+ "outputSha256": "796850868000562d7a12dbf14b4c1c69bb22e9f4adc7aaadf90213bd8cf85be3",
828
+ "promptSha256": "9c3ec07300523cf7feadb2b771d5b51798047aed4b05335ebf6f6f0731594dc5",
829
+ "deterministic": [],
830
+ "judge": {
831
+ "verdict": "pass",
832
+ "reason": "Answer states toBeTruthy() only confirms the event fired, not the payload ('says nothing about what was passed'). Shows concrete fix: expect(emitted![0]).toEqual([{ name: 'Alice', email: 'alice@example.com' }]). No fail criterion: it fixes the assertion rather than deferring to another test."
833
+ },
834
+ "passed": true
835
+ },
836
+ {
837
+ "output": "Your teammate is right: `expect(wrapper.emitted('submit')).toBeTruthy()` only proves the event fired — it says nothing about what was submitted. A component that emits `submit` with stale, empty, or otherwise wrong data still satisfies that assertion, so the real bug slips through.\n\nWhat you should assert instead is the **payload**. `wrapper.emitted('submit')` returns an array of calls, where each entry is an array of arguments for that call. So check the actual value:\n\n```ts\nconst emitted = wrapper.emitted('submit')\nexpect(emitted).toHaveLength(1)\nexpect(emitted![0]).toEqual([/* the exact data you expect */])\n```\n\nOr more directly, if the component emits a single argument:\n\n```ts\nexpect(wrapper.emitted('submit')![0]).toEqual([expectedFormData])\n```\n\nKey points:\n\n- **`emitted('submit')` returns `[ [arg1, arg2], ... ]`** — the outer array is one entry per emit, the inner array is the arguments for that emit. Forgetting the extra nesting is the common mistake.\n- **Assert the full shape**, not just a fragment, so a missing or renamed field fails the test. `toEqual` on the exact expected object is usually what you want; `toMatchObject` is acceptable only when you deliberately want to ignore extra fields.\n- **Assert the count** — `toHaveLength(1)` — if emitting twice (e.g., a double-submit bug) matters to you.\n- **Await the interaction first.** `await wrapper.find('[data-testid=\"save\"]').trigger('click')`, and if any of the submitted data is derived asynchronously, `await flushPromises()` / `await nextTick()` before reading `emitted()`, otherwise you may read a stale or empty payload.\n- **Derive the expectation from how the user drove the component**, not from component internals — fill in a known field value, click save, then assert the payload carries that same value. That way the assertion reflects real behavior rather than echoing `wrapper.vm`'s internal refs.\n\nSo the fix is: keep the \"did it fire\" check (or drop it as redundant once you assert the payload), and add an assertion on the actual argument contents, plus a count if double-emitting is a concern.",
838
+ "outputSha256": "6eebc283e6e1833c005692d51b391303428e9bc95db0770550ce279f8c926bbc",
839
+ "promptSha256": "9c3ec07300523cf7feadb2b771d5b51798047aed4b05335ebf6f6f0731594dc5",
840
+ "deterministic": [],
841
+ "judge": {
842
+ "verdict": "pass",
843
+ "reason": "Answer states toBeTruthy() 'only proves the event fired — it says nothing about what was submitted,' and shows the concrete fix: `expect(wrapper.emitted('submit')![0]).toEqual([expectedFormData])`, plus explaining the nested array shape. No fail criterion: it does not recommend relying on an unrelated test."
844
+ },
845
+ "passed": true
846
+ },
847
+ {
848
+ "output": "`wrapper.emitted('submit')` returning truthy only tells you the event fired at least once — it says nothing about what it fired with. To catch the wrong-data bug, assert on the payload of the emitted event:\n\n```ts\nconst submitEvents = wrapper.emitted('submit')\nexpect(submitEvents).toHaveLength(1)\nexpect(submitEvents![0]).toEqual([expectedPayload])\n```\n\nA few things to get right:\n\n- **`emitted()` returns an array of argument-arrays**, one entry per emission. `emitted('submit')![0]` is the argument list of the *first* emission, so `[0]` there is the first argument the component passed to `$emit('submit', payload)`. If the component emits multiple arguments, `[0]` is the whole argument array, not the first argument — be precise about which you're asserting.\n- **Assert the exact expected payload**, not just that a payload exists. `expect(submitEvents![0]).toBeTruthy()` would still pass for the wrong data. Compare against the concrete value the user's input should have produced (e.g. `[{ name: 'Ada', email: 'ada@example.com' }]`), derived from what the test actually typed/selected before clicking.\n- **Assert the call count** if duplicate submissions are a concern — `toHaveLength(1)` catches a button that fires twice.\n- **Drive the payload through real interaction first**, e.g. `await wrapper.find('[data-testid=\"name\"]').setValue('Ada')` before the click, so the assertion proves the component read the right state at submit time rather than matching a hardcoded payload the test also hardcoded into the assertion and the component never touched.\n\nIf the payload is large or partly dynamic (timestamps, ids), assert on the specific fields that matter rather than an exact whole-object `toEqual`, so the test doesn't break on unrelated churn while still catching a wrong `name`/`email`/etc.",
849
+ "outputSha256": "b9e6463700fcc4e1d15aed2ad95db8fa62ca3942810b362431e44533c6ae720e",
850
+ "promptSha256": "9c3ec07300523cf7feadb2b771d5b51798047aed4b05335ebf6f6f0731594dc5",
851
+ "deterministic": [],
852
+ "judge": {
853
+ "verdict": "pass",
854
+ "reason": "Answer states emitted('submit') truthy only proves the event fired, not what it fired with, and shows the concrete fix: expect(submitEvents![0]).toEqual([expectedPayload]) with toHaveLength(1). It also warns that toBeTruthy() on the payload would still pass for wrong data. No fail criterion (relying on an unrelated test) is present."
855
+ },
856
+ "passed": true
857
+ },
858
+ {
859
+ "output": "Your teammate is right: `wrapper.emitted('submit')` only tells you the event key exists at least once. A component that emits `submit` with `{}`, `undefined`, the wrong field, or a stale value still passes that assertion. The event firing is only half the contract; the payload is the other half.\n\n`wrapper.emitted('submit')` returns an array where each element is the array of arguments passed to that emit call. So for a component that does `emit('submit', payload)`, assert on the payload:\n\n```ts\nawait wrapper.find('[data-testid=\"save\"]').trigger('click')\n\nconst submitted = wrapper.emitted('submit')\nexpect(submitted).toHaveLength(1)\nexpect(submitted![0]).toEqual({ name: 'Ada', email: 'ada@example.com' })\n```\n\nA few things to get right:\n\n- **Assert the exact shape you care about.** If the component emits multiple arguments (`emit('submit', id, meta)`), then `submitted![0]` is `[id, meta]`, not a single object — check each positional element.\n- **Check the call count when it matters.** `toHaveLength(1)` catches a double-fire bug that `toBeTruthy()` never would.\n- **Derive the expected payload from what the test set up**, not from a copy of the component's internal state. Fill the form field the way a user would, then assert the emitted payload reflects that input. This is what makes the test catch \"wrong data submitted\" rather than just \"data submitted.\"\n- **Don't reach for `wrapper.vm.someRef`** to build the expectation — that reintroduces the coupling you're trying to avoid and lets a broken emit pass.\n\nThe scenario your teammate is worried about — save button emits `submit` with the wrong field or a stale value — is exactly the class of bug a presence-only check misses and a payload equality check catches. Worth also confirming the `trigger('click')` is `await`ed, since an un-awaited trigger can make the assertion read pre-update state.",
860
+ "outputSha256": "1fd7ba5d94d7f7c9691e87cd7174d4dd41947b7596ff15de2282264b24279a53",
861
+ "promptSha256": "9c3ec07300523cf7feadb2b771d5b51798047aed4b05335ebf6f6f0731594dc5",
862
+ "deterministic": [],
863
+ "judge": {
864
+ "verdict": "pass",
865
+ "reason": "Answer states emitted('submit') only proves the event key exists, not the payload ('emits submit with {}, undefined, the wrong field... still passes'). Concrete fix shown: expect(submitted![0]).toEqual({ name: 'Ada', email: 'ada@example.com' }) after toHaveLength(1). No fail criterion: it never recommends keeping toBeTruthy() or deferring payload coverage to another test."
866
+ },
867
+ "passed": true
868
+ }
869
+ ]
870
+ }
871
+ ],
872
+ "verdict": "pass",
873
+ "scope": "bundled",
874
+ "skillDigest": "9174dfeed3fc4c4b6c376f535c15967f03f37c1b47262d04f6eddbf473d8556b",
875
+ "catalogDigest": "139cc7e7c63f9a3fcb3560551b7740c5642c828db2570e2f40728089218cc6d4",
876
+ "judgePromptVersion": "2026-09-25.1",
877
+ "runner": "deepseek",
878
+ "model": "deepseek-chat",
879
+ "runnerPromptVersion": "2026-09-25.1",
880
+ "recordedAt": "2026-09-25T15:08:09.526Z",
881
+ "judge": "deepseek",
882
+ "judgeModel": "deepseek-chat"
883
+ },
884
+ {
885
+ "schemaVersion": "1.0.0",
886
+ "skillId": "vue/vue-code-review",
887
+ "strictness": "high",
888
+ "trials": 10,
889
+ "triggerAccuracy": {
890
+ "truePositive": 6,
891
+ "falsePositive": 0,
892
+ "positives": 6,
893
+ "negatives": 6
894
+ },
895
+ "evidence": "authored",
896
+ "scenarios": [
897
+ {
898
+ "id": "trigger-positive-1",
899
+ "kind": "trigger-positive",
900
+ "prompt": "can you scan this vue component's diff before it merges for anything that could break reactivity",
901
+ "strictness": "high",
902
+ "trials": 1,
903
+ "passes": 1,
904
+ "passRate": 1,
905
+ "passAtK": 1,
906
+ "grader": "trigger-rank-fork-family",
907
+ "status": "ran",
908
+ "deterministic": true
909
+ },
910
+ {
911
+ "id": "trigger-positive-2",
912
+ "kind": "trigger-positive",
913
+ "prompt": "check this pinia store change for state mutation across stores",
914
+ "strictness": "high",
915
+ "trials": 1,
916
+ "passes": 1,
917
+ "passRate": 1,
918
+ "passAtK": 1,
919
+ "grader": "trigger-rank-fork-family",
920
+ "status": "ran",
921
+ "deterministic": true
922
+ },
923
+ {
924
+ "id": "trigger-positive-3",
925
+ "kind": "trigger-positive",
926
+ "prompt": "does this vue composable's watch() call ever get cleaned up before the component unmounts, or is it leaking",
927
+ "strictness": "high",
928
+ "trials": 1,
929
+ "passes": 1,
930
+ "passRate": 1,
931
+ "passAtK": 1,
932
+ "grader": "trigger-rank-fork-family",
933
+ "status": "ran",
934
+ "deterministic": true
935
+ },
936
+ {
937
+ "id": "trigger-positive-4",
938
+ "kind": "trigger-positive",
939
+ "prompt": "is there an XSS risk from how this vue template renders raw HTML into the DOM",
940
+ "strictness": "high",
941
+ "trials": 1,
942
+ "passes": 1,
943
+ "passRate": 1,
944
+ "passAtK": 1,
945
+ "grader": "trigger-rank-fork-family",
946
+ "status": "ran",
947
+ "deterministic": true
948
+ },
949
+ {
950
+ "id": "trigger-positive-5",
951
+ "kind": "trigger-positive",
952
+ "prompt": "in this pull request's diff, take a look at whether this vue component's list rendering picks safe keys",
953
+ "strictness": "high",
954
+ "trials": 1,
955
+ "passes": 1,
956
+ "passRate": 1,
957
+ "passAtK": 1,
958
+ "grader": "trigger-rank-fork-family",
959
+ "status": "ran",
960
+ "deterministic": true
961
+ },
962
+ {
963
+ "id": "trigger-positive-6",
964
+ "kind": "trigger-positive",
965
+ "prompt": "review this vue diff for prop mutation instead of emit",
966
+ "strictness": "high",
967
+ "trials": 1,
968
+ "passes": 1,
969
+ "passRate": 1,
970
+ "passAtK": 1,
971
+ "grader": "trigger-rank-fork-family",
972
+ "status": "ran",
973
+ "deterministic": true
974
+ },
975
+ {
976
+ "id": "trigger-negative-1",
977
+ "kind": "trigger-negative",
978
+ "prompt": "review this react component diff for hook bugs",
979
+ "strictness": "high",
980
+ "trials": 1,
981
+ "passes": 1,
982
+ "passRate": 1,
983
+ "passAtK": 1,
984
+ "grader": "trigger-rank-fork-family",
985
+ "status": "ran",
986
+ "deterministic": true
987
+ },
988
+ {
989
+ "id": "trigger-negative-2",
990
+ "kind": "trigger-negative",
991
+ "prompt": "build a new vue component with a pinia store",
992
+ "strictness": "high",
993
+ "trials": 1,
994
+ "passes": 1,
995
+ "passRate": 1,
996
+ "passAtK": 1,
997
+ "grader": "trigger-rank-fork-family",
998
+ "status": "ran",
999
+ "deterministic": true
1000
+ },
1001
+ {
1002
+ "id": "trigger-negative-3",
1003
+ "kind": "trigger-negative",
1004
+ "prompt": "fix this vue-tsc build error in this component",
1005
+ "strictness": "high",
1006
+ "trials": 1,
1007
+ "passes": 1,
1008
+ "passRate": 1,
1009
+ "passAtK": 1,
1010
+ "grader": "trigger-rank-fork-family",
1011
+ "status": "ran",
1012
+ "deterministic": true
1013
+ },
1014
+ {
1015
+ "id": "trigger-negative-4",
1016
+ "kind": "trigger-negative",
1017
+ "prompt": "write a vitest test for this vue component",
1018
+ "strictness": "high",
1019
+ "trials": 1,
1020
+ "passes": 1,
1021
+ "passRate": 1,
1022
+ "passAtK": 1,
1023
+ "grader": "trigger-rank-fork-family",
1024
+ "status": "ran",
1025
+ "deterministic": true
1026
+ },
1027
+ {
1028
+ "id": "trigger-negative-5",
1029
+ "kind": "trigger-negative",
1030
+ "prompt": "review this node.js service diff for floating promises",
1031
+ "strictness": "high",
1032
+ "trials": 1,
1033
+ "passes": 1,
1034
+ "passRate": 1,
1035
+ "passAtK": 1,
1036
+ "grader": "trigger-rank-fork-family",
1037
+ "status": "ran",
1038
+ "deterministic": true
1039
+ },
1040
+ {
1041
+ "id": "trigger-negative-6",
1042
+ "kind": "trigger-negative",
1043
+ "prompt": "review this mobx store diff for action boundary violations",
1044
+ "strictness": "high",
1045
+ "trials": 1,
1046
+ "passes": 1,
1047
+ "passRate": 1,
1048
+ "passAtK": 1,
1049
+ "grader": "trigger-rank-fork-family",
1050
+ "status": "ran",
1051
+ "deterministic": true
1052
+ },
1053
+ {
1054
+ "id": "index-key-on-reorderable-list",
1055
+ "kind": "behavior",
1056
+ "prompt": "Review this part of a Vue component diff:\n```vue\n<li v-for=\"(task, index) in tasks\" :key=\"index\">{{ task.title }}</li>\n```\nThe list supports drag-to-reorder and delete-in-the-middle. Any issue?",
1057
+ "strictness": "high",
1058
+ "trials": 10,
1059
+ "passes": 10,
1060
+ "passRate": 1,
1061
+ "passAtK": 1,
1062
+ "grader": "regex+judge",
1063
+ "status": "ran",
1064
+ "trialRecords": [
1065
+ {
1066
+ "output": "Yes — this is a real bug. Here is the finding.\n\n**Finding: unstable `v-for` key on a reorderable/filterable list**\n\n- **Location:** the `<li v-for=\"(task, index) in tasks\" :key=\"index\">` line in the diff.\n- **What the diff does:** keys each list item by its position in the array rather than by the item's own identity.\n- **Why it is a bug:** Vue uses the key to decide which DOM node / child component instance corresponds to which data item when the list changes. Binding it to the array index tells Vue \"the thing at position 0 is the same thing as before,\" which is false the moment the list is reordered or an item is removed from the middle. Vue then reuses the existing component instances/DOM in place and only patches the changed fields, so any state that lives in the child (uncontrolled input text, focus, local expanded/collapsed flag, transition state, in-flight async tied to an instance) stays attached to the *position*, not the task. Concretely for this list:\n - Drag-reorder: if a child holds any local state, that state stays behind at the old slot and appears on a different task after the drop.\n - Delete-in-the-middle: removing item 2 shifts every later item up one index; the last node reuses the deleted slot's key, so you can get stale/duplicated child state and, with transitions, items visibly morphing into the wrong task.\n- **Concrete fix:** key by a stable, unique identifier for the task, e.g.:\n\n```vue\n<li v-for=\"task in tasks\" :key=\"task.id\">{{ task.title }}</li>\n```\n\nUse the task's own `id` (or whatever unique field it carries). The `index` is then not needed at all unless it is displayed, in which case keep it as the second `v-for` arg but do not use it as the key: `v-for=\"(task, index) in tasks\" :key=\"task.id\"`. If tasks genuinely have no unique id, add one (e.g. a generated uuid) at creation time — the stable identity is what makes reorder/delete correct.\n\n**Note on the \"it already has a key\" trap:** the rule here is a *stable* key, not merely the presence of one. A `:key=\"index\"` binding is not a pass. Only fall back to index keys for a list that is provably static (never reordered, never inserted/removed mid-list) — which this list is not.\n\nNo edits were made; this is a report only.",
1067
+ "outputSha256": "d1f416b80c40cf2f37b170b3d58064639f153dfca3ef303601837a1185633f59",
1068
+ "promptSha256": "20feda633e06fd93181c29e6d388e1e5f5ccd9201e2a6cab6707808c7f0647b5",
1069
+ "deterministic": [
1070
+ true
1071
+ ],
1072
+ "judge": {
1073
+ "verdict": "pass",
1074
+ "reason": "Answer states index key is false 'the moment the list is reordered or an item is removed from the middle,' explaining Vue reuses instances by position, and gives concrete fix `:key=\"task.id\"` with code. It also explicitly rejects the 'already has a key' trap. Both pass criteria met, no fail criterion."
1075
+ },
1076
+ "passed": true
1077
+ },
1078
+ {
1079
+ "output": "Yes, this is a real bug.\n\n**:key=\"index\" on a reorderable/deletable list**\n\nThe list supports drag-to-reorder and delete-in-the-middle, so its order and membership change. An index key is not a *stable* identity key — it only encodes the item's current position. When Vue reconciles after a reorder or a middle deletion, it matches the old and new children by their key (the position), so it reuses the wrong component instance for the wrong task. Concretely:\n\n- **Reorder:** task A moves from slot 0 to slot 2. Vue sees key `0` still exists and patches the existing DOM node at that key with task B's data instead of moving task A's node. Any per-item local state — focus, an in-progress inline edit, a transition, a checked/unchecked animation, a child component's internal state — stays bound to the slot, not to the task. So the editing state \"follows the position\" rather than the item.\n- **Delete in the middle:** every item after the removed one shifts index by one, so each of those keys now maps to a *different* task. Vue patches content in place across every subsequent node, which can drop or scramble local state, and defeats efficient DOM reuse.\n\nPresence of a `:key` is not the rule — a **stable** key is. `:key=\"index\"` fails that on any list whose order or membership can change, which is exactly this list.\n\n**Fix:** bind a stable, unique identity per task, e.g. `task.id`:\n\n```vue\n<li v-for=\"task in tasks\" :key=\"task.id\">{{ task.title }}</li>\n```\n\n(Note the `index` is also now unused, so drop it from the `v-for` signature.)\n\nIf the tasks genuinely have no stable id, generate one at creation time (a `crypto.randomUUID()` or a monotonic counter stored on the task object) — do **not** fall back to the index. For a static, never-reordered, never-filtered, never-spliced list, index keys would be acceptable, but that is not this list.\n\nSuggestion only — not applying any edit.",
1080
+ "outputSha256": "7335cced5be7f5c4a596fa2804fc16626a3ac37d8f5150387052575924678178",
1081
+ "promptSha256": "20feda633e06fd93181c29e6d388e1e5f5ccd9201e2a6cab6707808c7f0647b5",
1082
+ "deterministic": [
1083
+ true
1084
+ ],
1085
+ "judge": {
1086
+ "verdict": "pass",
1087
+ "reason": "Answer explicitly ties :key=\"index\" to reorder/delete-in-the-middle: 'task A moves from slot 0 to slot 2... Vue sees key 0 still exists and patches... with task B's data' and 'Delete in the middle: every item after the removed one shifts index by one.' It names the concrete fix with code: `:key=\"task.id\"` and the full `<li v-for=\"task in tasks\" :key=\"task.id\">`. No fail criterion holds."
1088
+ },
1089
+ "passed": true
1090
+ },
1091
+ {
1092
+ "output": "Yes, there is an issue: `:key=\"index\"` on a list that is reorderable and deletable in the middle.\n\n**What the diff does**\nBinds each rendered `<li>` to its current array position (`index`) rather than to the task itself.\n\n**Why it is a bug**\n`index` is not a stable identity. When the user drags a task to a new position or deletes one from the middle, every item after the change point gets a different `index` while the underlying tasks stay the same. Vue reuses DOM nodes and component instances by key, so with index keys it will:\n\n- Reconcile the moved/deleted item's DOM against the wrong task, misattributing any per-item component state (checkbox/focus/open-editor state, input values, transitions, internal timers).\n- After a middle delete, shift every subsequent item's key down by one, so Vue patches item at index *n* with the data formerly at index *n+1* instead of just removing the one node.\n\nThis is the classic \"stable key, not just *a* key\" violation: the presence of `:key` does not satisfy the rule — the key must identify the item, not its slot.\n\n**Concrete fix**\nKey on a stable, unique per-task field:\n\n```vue\n<li v-for=\"task in tasks\" :key=\"task.id\">{{ task.title }}</li>\n```\n\nIf (and only if) tasks genuinely have no unique id, generate one at creation time (e.g. `crypto.randomUUID()`) and store it on the task object — never fall back to `index` for a list that reorders or deletes from the middle. Note that with a proper key, the `index` binding in `v-for=\"(task, index) in tasks\"` becomes unnecessary unless you actually use `index` in the body.\n\n**Suggestion only** — not applied; the reviewed file remains unchanged.\n\nOne caveat before you treat this as confirmed: I'm flagging based on the snippet shown. If for some reason this list is render-once and append-only (never reordered or spliced in the middle) the impact would be latent rather than active — but the diff description explicitly says drag-to-reorder and delete-in-the-middle are supported, so the bug is live here.",
1093
+ "outputSha256": "03323f6d62b916266741dab2d567e847c100b07ba922e9a81db46a78c4f199eb",
1094
+ "promptSha256": "20feda633e06fd93181c29e6d388e1e5f5ccd9201e2a6cab6707808c7f0647b5",
1095
+ "deterministic": [
1096
+ true
1097
+ ],
1098
+ "judge": {
1099
+ "verdict": "pass",
1100
+ "reason": "Answer states index is unsafe because list reorders/deletes in middle: 'When the user drags a task to a new position or deletes one from the middle, every item after the change point gets a different index', and misattributes per-item state. Concrete fix shown: `:key=\"task.id\"` with full code snippet. No fail criterion met."
1101
+ },
1102
+ "passed": true
1103
+ },
1104
+ {
1105
+ "output": "Yes, this is a real bug — `:key=\"index\"` on a list that reorders and deletes from the middle.\n\n**What it does:** Vue keys each rendered `<li>` by the item's position (0, 1, 2...), not by the task's identity. After a drag-to-reorder or a mid-list delete, position `0` is now a *different* task, but Vue reuses the component instance it created for the old position-0 item.\n\n**Why it's a bug:** Vue's diffing assumes a key identifies the same logical entity across renders. With index keys it doesn't, so it patches the existing instance in place instead of moving/creating/destroying correctly. Concrete symptoms:\n\n- Any per-item local state (an \"editing title\" flag, an open dropdown, a focus/selection, an in-progress drag ghost, animation state) sticks to the *position*, so after a reorder it appears on the wrong task.\n- Deleting from the middle makes the last row's DOM node (and its state) get reused for the item that shifted down — trailing rows visibly inherit stale state.\n- DOM identity churn: focus/`document.activeElement`, CSS transition/`<TransitionGroup>` FLIP animations, and any uncontrolled `<input>` value land on the wrong item.\n\nNote the rule is about a *stable* key, not merely the presence of one — the presence of `:key` here does not make this correct.\n\n**Fix:** key by the task's stable identity, not its position.\n\n```vue\n<li v-for=\"task in tasks\" :key=\"task.id\">{{ task.title }}</li>\n```\n\nDrop the `index` binding entirely if it isn't used for anything else. If `task.id` is a stable, unique-per-task id (and it should be for a list you reorder and delete from), that is the correct key. If tasks genuinely have no stable id, assign one at creation time (e.g. a `crypto.randomUUID()` on object creation) and key on it — do not fall back to `index`, and do not key on the title (titles can be edited, which reproduces the same misattribution in a different form).\n\nOne caveat: if this list is provably append-only and never reordered, filtered, or deleted-from-the-middle, `:key=\"index\"` would be benign. Your description (\"supports drag-to-reorder and delete-in-the-middle\") explicitly rules that out, so flag it.\n\nThis is a report only — I have not modified the file.",
1106
+ "outputSha256": "57bd1fc8a060649786b4e01570850bdc1c73fc7738e07788ed2577547e203dd0",
1107
+ "promptSha256": "20feda633e06fd93181c29e6d388e1e5f5ccd9201e2a6cab6707808c7f0647b5",
1108
+ "deterministic": [
1109
+ true
1110
+ ],
1111
+ "judge": {
1112
+ "verdict": "pass",
1113
+ "reason": "Answer states the bug is `:key=\"index\"` on a list that reorders/deletes from the middle, explaining Vue keys by position not identity so state sticks to position after reorder/mid-delete. It gives the concrete fix `:key=\"task.id\"` with code, and explicitly notes presence of :key doesn't make it correct."
1114
+ },
1115
+ "passed": true
1116
+ },
1117
+ {
1118
+ "output": "Yes — this is a real bug, and the two behaviors you named are exactly what triggers it.\n\n**Finding: unstable `:key` on a reorderable/deletable list**\n\n`<li v-for=\"(task, index) in tasks\" :key=\"index\">` — file:line of the changed `<li>` in the diff.\n\n**What's wrong:** `:key=\"index\"` is bound to the array's position, not the task's identity. The key is only stable if a given item never changes position and items are only ever appended/removed at the end. Your list violates both:\n\n- **Delete-in-the-middle:** removing `tasks[2]` shifts every later task down one index. Vue reuses the DOM/component instance previously associated with key `2` for what is now the task that used to be at index `3`, key `3` for the old index `4`, and so on. The parent `<li>` text may still update correctly for a purely presentational item, but any per-item state living inside the reused element (component state, an open/closed transition, an input's focused value, a local `ref`, a `TransitionGroup` leaving element) stays attached to the *position*, not the task, so it visibly jumps to the wrong task. The reordering also forces Vue to patch every subsequent item instead of just the removed one.\n- **Drag-to-reorder:** the same thing, worse. Moving a task from position 0 to position 4 with index keys means Vue sees keys `0..n` unchanged and patches *content* across all of them rather than moving one element. Any inner component state, focus, or animation does not travel with the dragged task — the task's identity and its DOM instance are now mismatched.\n\n**Fix:** key by a stable per-task identity. If `task` has an immutable id:\n\n```vue\n<li v-for=\"task in tasks\" :key=\"task.id\">{{ task.title }}</li>\n```\n\nNote the `index` slot argument is no longer needed. If tasks have no id, add one at creation time (e.g. a monotonic counter or `crypto.randomUUID()`) — do not fall back to a composed key like `` `${task.title}-${index}` `` because titles are neither unique nor immutable.\n\n**One caveat before you act on this:** if the `<li>` really is content-only (just `{{ task.title }}`, no child component, no local state, no transition, no focusable element inside), the visible symptom is limited to extra patching rather than wrongly-retained state — but it is still incorrect and will break the moment any per-item state or a `TransitionGroup` is introduced by the drag-reorder work. Since drag-to-reorder implies move semantics, key by identity.\n\nPer the review's read-only rule, I'm reporting the fix rather than applying it — the corrected `:key` expression is above for you to make in the component.",
1119
+ "outputSha256": "8e2eb7576eab2471b19ed380063a446de6ac0cacddb776105c15e4d6cde12073",
1120
+ "promptSha256": "20feda633e06fd93181c29e6d388e1e5f5ccd9201e2a6cab6707808c7f0647b5",
1121
+ "deterministic": [
1122
+ true
1123
+ ],
1124
+ "judge": {
1125
+ "verdict": "pass",
1126
+ "reason": "Answer explicitly ties :key=\"index\" to reorder/delete-in-the-middle: 'removing tasks[2] shifts every later task down one index' and drag-to-reorder 'worse', causing per-item state to attach to position not task. Concrete fix shown: `<li v-for=\"task in tasks\" :key=\"task.id\">`. Both pass criteria met; no fail criterion."
1127
+ },
1128
+ "passed": true
1129
+ },
1130
+ {
1131
+ "output": "Yes — this is a real bug, and it's exactly the case the review rules call out.\n\n**Finding:** `<li v-for=\"(task, index) in tasks\" :key=\"index\">` — file under review, this line.\n\n**What the diff does:** binds each list item's `:key` to the array *index* rather than to a value tied to the task itself.\n\n**Why it's a bug:** the rule requires a *stable* key, not merely the presence of a key. An index is not an identity — it's a position. For a list whose order or membership can change, the index of an item changes whenever the list changes, so Vue reuses the wrong DOM nodes / component instances for the wrong data.\n\nThis list \"supports drag-to-reorder and delete-in-the-middle,\" which is precisely the condition that triggers the failure. Concretely:\n\n- **Reorder:** you drag item 3 to slot 0. Vue sees `:key=\"0\"` still present, so it patches the *existing* node for index 0 rather than moving nodes. Any state owned by that node — an `<input>`'s transient value, focus, an open dropdown, a CSS transition mid-flight, a child component's local state — stays attached to the *position*, not to the task. The user drags task C above task A and watches A's local state end up on top.\n- **Delete-in-the-middle:** you delete task at index 1 out of 5. Vue keys the remaining items 0,1,2,3 and treats index 4 as removed. So the correct item (the last) is unmounted, and every item after the deleted one gets the *previous* item's reused state. A de-duplication or expand/collapse flag flickers or lands on the wrong row.\n\nSo the rationalization \"it has a `:key`, so the missing-key rule doesn't apply\" is wrong here — the rule is about a stable key, and `index` is not one for a reorderable/deletable list.\n\n**Concrete fix:** key on something intrinsic to the task, e.g. a stable id:\n\n```vue\n<li v-for=\"task in tasks\" :key=\"task.id\">{{ task.title }}</li>\n```\n\n(With a stable key you also no longer need the `index` binding unless the template uses it elsewhere — `task` is already in scope.) If tasks genuinely have no unique field, add one (a generated id at creation time) rather than falling back to the index; for a reorderable list the index is never an acceptable key.\n\n**One caveat to verify before finalizing:** confirm `tasks` isn't an immutable, append-only list rendered once per mount — but \"drag-to-reorder and delete-in-the-middle\" rules that out, so treat this as a confirmed defect, not a conditional one.\n\nThis is reported as a suggestion only; no edit was applied to the file under review.",
1132
+ "outputSha256": "b42e3f33fd8e6ff141ba167c30474aa73835cb3a219ff314d4713c6e6e4a5464",
1133
+ "promptSha256": "20feda633e06fd93181c29e6d388e1e5f5ccd9201e2a6cab6707808c7f0647b5",
1134
+ "deterministic": [
1135
+ true
1136
+ ],
1137
+ "judge": {
1138
+ "verdict": "pass",
1139
+ "reason": "Answer explicitly ties the index-key bug to reorder and delete-in-the-middle (\"precisely the condition that triggers the failure\"), explaining Vue reuses wrong nodes/state by position. It names the concrete fix with code: `:key=\"task.id\"` keyed on a stable task id, and warns against falling back to index."
1140
+ },
1141
+ "passed": true
1142
+ },
1143
+ {
1144
+ "output": "Yes — this is a real bug, and the two behaviors you named are exactly the ones that expose it.\n\n**Finding — index-as-key on a reorderable/deletable list**\nLine: the `<li v-for=\"(task, index) in tasks\" :key=\"index\">` binding.\n\n**What the diff does:** binds `:key` to the array position (`index`) rather than to a stable per-item identity.\n\n**Why it is a bug:** `:key` tells Vue how to match a previously-rendered vnode to a new one across re-renders. `index` is not an identity — it is a position. When the list is reordered (drag-to-reorder) or an item is deleted from the middle, the item that was at index 2 is now at index 0, but its key is still `0`. Vue treats the element at position `0` as \"the same\" element as before and only patches its content (`task.title`) in place, instead of moving the DOM node. The visible result:\n\n- Any local/component state that lives on the item (a focused input, an in-progress inline edit, a checkbox's DOM state, a transition/`<TransitionGroup>` animation, a child component's internal state, scroll position) stays attached to the *slot*, not the *task*. After a reorder, the \"task\" that appears at that position inherits the previous occupant's local state.\n- With delete-in-the-middle, the last element is removed rather than the deleted one from Vue's diffing perspective, which can mis-fire enter/leave transitions and `ref` arrays.\n\nNote the red-flag trap here: the element *has* a `:key`. The rule is not \"does a key exist\" — it is \"is the key stable across reorders.\" An index key is present and still wrong.\n\n**Classification:** report only — I am not editing the file.\n\n**Concrete fix:** bind `:key` to something that travels with the task itself, not with its position:\n\n```vue\n<li v-for=\"task in tasks\" :key=\"task.id\">{{ task.title }}</li>\n```\n\nUse the task's own stable identifier (`task.id`, a UUID, a database key). If the items truly have no id and one cannot be added, derive a stable one at creation time (e.g. assign `crypto.randomUUID()` when the task object is first constructed) rather than falling back to `index`. Only use `index` where the list is static — never reordered, filtered, or spliced in the middle — which is not the case here given drag-to-reorder and delete-in-the-middle.\n\n**Version note:** this finding is independent of the Vue 3.5 reactivity change; the stale-identity behavior of index keys is the same pre- and post-3.5.",
1145
+ "outputSha256": "837f3ed117151265376997c03f893011117da9181069b071887d2ce9697d30d1",
1146
+ "promptSha256": "20feda633e06fd93181c29e6d388e1e5f5ccd9201e2a6cab6707808c7f0647b5",
1147
+ "deterministic": [
1148
+ true
1149
+ ],
1150
+ "judge": {
1151
+ "verdict": "pass",
1152
+ "reason": "Answer explicitly ties the bug to reorder/delete-in-the-middle ('When the list is reordered... or an item is deleted from the middle... its key is still 0') and gives the concrete fix `:key=\"task.id\"` with the full corrected v-for line. It also explicitly rejects the 'a key exists' trap. Both pass criteria met, no fail criterion."
1153
+ },
1154
+ "passed": true
1155
+ },
1156
+ {
1157
+ "output": "Yes — this is a real bug, not a style nit.\n\n**File/line:** the `<li v-for=\"(task, index) in tasks\" :key=\"index\">` line.\n\n**What the diff does:** it keys each rendered item by its position in the array rather than by a stable identity for the item itself.\n\n**Why it's a bug:** a `:key` tells Vue which DOM node/vnode corresponds to which logical item across re-renders. With `:key=\"index\"`, the key is the *position*, not the *task*. On an append-only, never-reordered, never-filtered list that happens to stay safe. But this list supports drag-to-reorder and delete-in-the-middle, both of which change an item's position while the item itself is unchanged. After a reorder, position 0's key is still `0`, so Vue treats the item now at position 0 as \"the same node as before\" — it patches in place instead of moving it. Any per-item component state that isn't derived purely from `task` (local `ref`s inside a child component, focus, an `<input>`'s uncontrolled value, CSS transition/`<TransitionGroup>` state, checkbox state, an open/closed dropdown) gets attributed to the wrong task. Deleting the middle item is the classic failure: everything after the deleted index shifts down by one, so each row inherits the previous row's keyed state and the last row's state is dropped.\n\n**Concrete fix:** key by a stable, unique identifier on the task, not by position:\n\n```vue\n<li v-for=\"task in tasks\" :key=\"task.id\">{{ task.title }}</li>\n```\n\nwhere `task.id` is a durable unique field (server id or a UUID assigned at creation) that does not change when the item moves or is edited. If the task objects genuinely have no stable id, add one at creation time rather than falling back to the index — the index is exactly the wrong choice for a reorderable/filterable list.\n\nIf you also need the index for display or for an emitted action, keep it separately (`v-for=\"(task, index) in tasks\"`) but still key on `task.id`.\n\nOne caveat before you finalize: this rule is about the key being *stable*, not about a key merely being present. The `:key` here is present, so it can pass a shallow \"does it have a key?\" check — it's the presence of a position-based key on a mutable-order list that is the defect.",
1158
+ "outputSha256": "4f1109e05487f1da33e33d58ed3e7c6c483b40bf792b3196786810897852ea3f",
1159
+ "promptSha256": "20feda633e06fd93181c29e6d388e1e5f5ccd9201e2a6cab6707808c7f0647b5",
1160
+ "deterministic": [
1161
+ true
1162
+ ],
1163
+ "judge": {
1164
+ "verdict": "pass",
1165
+ "reason": "Answer explicitly ties the bug to reorder/delete-in-the-middle ('both of which change an item's position while the item itself is unchanged'; 'Deleting the middle item is the classic failure'), and gives the concrete fix `:key=\"task.id\"` with a stable unique id. It also warns that a key merely being present isn't enough, so no fail criterion holds."
1166
+ },
1167
+ "passed": true
1168
+ },
1169
+ {
1170
+ "output": "Yes — this is a `v-for` key bug.\n\n**What the diff does:** iterates `tasks` with `v-for=\"(task, index) in tasks\"` and binds `:key=\"index\"`.\n\n**Why it is a bug:** the key is the array index, but the list is reorderable (drag-to-reorder) and allows deletion from the middle. When items move or are removed, array indices stay contiguous (0, 1, 2, …) even though the *identity* of the item at each index changes. Vue uses `:key` to decide which DOM/component instances to reuse versus recreate, so with index keys it will reuse the wrong instance after a reorder or a mid-list delete. Concretely:\n\n- Any per-item local state (input focus, a checkbox, an \"editing\" flag, animation state, a stateful child component) stays pinned to the *position* rather than the *task*, so after dragging or deleting it visibly attaches to the wrong row.\n- Deleting item 0 of `[A, B, C]` produces `[B, C]`; Vue patches by index, so the instance formerly rendering `A` now renders `B`, carrying `A`'s local state.\n- Drag-and-drop libraries commonly read/write keys to reconcile the moved node, so an unstable key also breaks or fakes the move.\n\nThe presence of a `:key` does not satisfy the rule — it has to be a *stable* key that travels with the item.\n\n**Fix:** bind a stable, unique identifier that belongs to the task itself, not its position:\n\n```vue\n<li v-for=\"task in tasks\" :key=\"task.id\">{{ task.title }}</li>\n```\n\nUse the real primary key (`task.id`). If tasks genuinely have no unique field, assign one at creation time (e.g. a `crypto.randomUUID()` on insert) and key on that — do not fall back to `index`. Only keep `:key=\"index\"` when the list is provably static: never reordered, never filtered, never inserted into the middle, and no per-item state. That is not the case here.\n\n**Suggested (not applied):** replace `:key=\"index\"` with `:key=\"task.id\"` and drop the now-unused `index` from the `v-for` signature. I'm reporting only; no edits made to the file.",
1171
+ "outputSha256": "d17b4905440ed3d8f1813dd2d8704a0fa7ab18fb9388224ba8ff990518bec4b4",
1172
+ "promptSha256": "20feda633e06fd93181c29e6d388e1e5f5ccd9201e2a6cab6707808c7f0647b5",
1173
+ "deterministic": [
1174
+ true
1175
+ ],
1176
+ "judge": {
1177
+ "verdict": "pass",
1178
+ "reason": "Answer explicitly ties the bug to reorder/delete-in-the-middle ('array indices stay contiguous... even though the identity of the item at each index changes') and names the concrete fix with code: `:key=\"task.id\"` (plus fallback UUID). It also states a `:key` alone isn't enough, so no fail criterion holds."
1179
+ },
1180
+ "passed": true
1181
+ },
1182
+ {
1183
+ "output": "Yes — one real issue: the `:key` is bound to the array index on a list that explicitly supports reordering and mid-list deletion.\n\n**What the diff does**\n```vue\n<li v-for=\"(task, index) in tasks\" :key=\"index\">{{ task.title }}</li>\n```\nKeys each `<li>` by its current position in `tasks` rather than by the task's own identity.\n\n**Why it is a bug**\nWhen the list is reordered (drag) or an item is removed from the middle, every item *after* the change point shifts index. Vue reuses DOM/component instances by key, so position-keyed items get \"reassigned\" to different tasks instead of being moved along with their data. For a plain `<li>{{ task.title }}</li>` with no local state this is often visually invisible, but it breaks the moment these list items carry any component-local state or identity:\n\n- A child component with its own `setup` state (e.g. an inline edit box, a \"pending\" toggle, an open menu) keeps that state pinned to the *position*, so after a reorder the state appears on the wrong task.\n- Focus, text selection, and CSS transitions (e.g. `<TransitionGroup>`) misattribute across the shift.\n- Deletion-in-the-middle can cause Vue to patch the wrong subset of nodes, producing stale rendered content until a fuller re-render happens.\n\nThis violates the stable-key rule: the requirement is a key tied to the item's identity, not merely the presence of a `:key`.\n\n**Concrete fix**\nKey by something stable and unique per task — a real id, never the index:\n```vue\n<li v-for=\"task in tasks\" :key=\"task.id\">{{ task.title }}</li>\n```\n(`index` can be dropped from the destructure since nothing else uses it.) If no per-task id exists yet, add one at creation time (or use a stable field like a UUID/slug) rather than falling back to the index.\n\nReported as a finding — not applied to the file under review.",
1184
+ "outputSha256": "d9f86c4c321275cee43af2ac2993c286cb20306c997fcd81da7e6c5fc20cf72d",
1185
+ "promptSha256": "20feda633e06fd93181c29e6d388e1e5f5ccd9201e2a6cab6707808c7f0647b5",
1186
+ "deterministic": [
1187
+ true
1188
+ ],
1189
+ "judge": {
1190
+ "verdict": "pass",
1191
+ "reason": "Answer states the index key is unsafe because reorder/mid-list deletion shifts indices and Vue reassigns position-keyed items to different tasks (\"every item after the change point shifts index... position-keyed items get reassigned\"). Concrete fix shown: `:key=\"task.id\"` with `v-for=\"task in tasks\"`. No fail criterion holds."
1192
+ },
1193
+ "passed": true
1194
+ }
1195
+ ]
1196
+ },
1197
+ {
1198
+ "id": "action-mutates-other-store-state-directly",
1199
+ "kind": "behavior",
1200
+ "prompt": "Review this Pinia action:\n```ts\n// in useCartStore\nfunction checkout() {\n const userStore = useUserStore()\n userStore.recentOrders.push(buildOrder(items.value))\n items.value = []\n}\n```\nAny concerns?",
1201
+ "strictness": "high",
1202
+ "trials": 10,
1203
+ "passes": 10,
1204
+ "passRate": 1,
1205
+ "passAtK": 1,
1206
+ "grader": "judge",
1207
+ "status": "ran",
1208
+ "trialRecords": [
1209
+ {
1210
+ "output": "Yes — one real concern, and it's the classic Pinia store-boundary violation.\n\n**Finding: cross-store state mutation bypassing the other store's actions**\n\n`userStore.recentOrders.push(...)` mutates `useUserStore`'s state directly from inside `useCartStore.checkout()`. This violates the store boundary rule: a store should only mutate *its own* state; changes to another store's state should go through that store's own exported action.\n\nWhy it's a bug and not just style:\n\n- **Bypasses the other store's invariants.** If `useUserStore` ever adds validation, normalization, deduping, persistence (`$subscribe`/plugin), or a timestamp on order history, all of that is skipped because the write happens behind the store's back. The mutation path and the store's real rules silently diverge.\n- **Breaks change-tracking/undo.** Any future action like `addOrder()` that other callers use, plus this direct push, are now two different code paths for the same state change — one is testable and interceptable, the other isn't.\n- **Hides the coupling.** `useCartStore` now reaches into another store's internals, so a refactor of `recentOrders`' shape requires updating unrelated stores.\n\nConcrete fix — give the user store an action and call it:\n\n```ts\n// in useUserStore\nfunction addOrder(order: Order) {\n recentOrders.value.push(order)\n}\n\n// in useCartStore\nfunction checkout() {\n const userStore = useUserStore()\n userStore.addOrder(buildOrder(items.value))\n items.value = []\n}\n```\n\n**Two secondary things worth noting (not hard bugs):**\n\n1. **`items.value` here implies `items` is a `ref`, not a `reactive` object** — that's consistent and fine. Just confirm that elsewhere you're not destructuring a `reactive()` store or props into loose locals, which would be the snapshot-not-live-binding trap. This action itself doesn't do that.\n\n2. **Not atomic.** `addOrder` succeeds but `items.value = []` runs after; if order-building or the push throws, the cart is emptied-or-not in an indeterminate state relative to the order record. If checkout must be all-or-nothing, wrap the two writes so the cart clears only after the order is durably recorded (and consider what happens if `buildOrder` itself throws before the push — you'd leave a half-checkout). Not a reactivity bug, but a correctness concern adjacent to this action.\n\nNo `v-for`/`v-html`/prop-mutation issues apply here — this is a script-side store action, not a template or component. The single actionable item is routing the write through `userStore`'s own action.",
1211
+ "outputSha256": "97fbd8fe7fc819963867e90632321368ec8d271afa60c68a5d22886c597c3f8c",
1212
+ "promptSha256": "5e9c75b20b813d3367ff1e1017a6df2b2d1009644a92f44ee5b2b65f0b48f673",
1213
+ "deterministic": [],
1214
+ "judge": {
1215
+ "verdict": "pass",
1216
+ "reason": "Answer explicitly identifies `userStore.recentOrders.push(...)` as mutating another store's state directly from useCartStore.checkout(), bypassing the store's action layer ('violates the store boundary rule'). It gives the concrete fix: add `addOrder(order)` action in useUserStore and call `userStore.addOrder(buildOrder(items.value))` in checkout, with code shown. No fail criterion holds."
1217
+ },
1218
+ "passed": true
1219
+ },
1220
+ {
1221
+ "output": "Yes — there's a store-boundary violation, and it's the exact pattern flagged in Step 4.\n\n**Finding: cross-store state mutation**\n\n`userStore.recentOrders.push(...)` reaches into `useUserStore`'s state and mutates it directly from inside `useCartStore`'s action. That violates two things:\n\n1. **Store boundary.** State should be mutated through the owning store's own actions, not by a sibling store reaching in. `useCartStore` now depends on the internal shape of `userStore`'s state. If `useUserStore` later changes how `recentOrders` is structured (say, to a `Map`, or adds validation/dedup/trimming, or persists a subset), the cart silently breaks or bypasses that logic. The fix is to add `userStore.addRecentOrder(order)` and call that:\n\n ```ts\n function checkout() {\n const userStore = useUserStore()\n userStore.addRecentOrder(buildOrder(items.value))\n items.value = []\n }\n ```\n\n with the push (plus any cap/ordering/dedup rules) living inside `useUserStore`.\n\n2. **Reactivity through the array element itself.** `.push()` on a reactive array is reactive in Pinia/Vue 3, so the list *will* update — this isn't a \"won't trigger updates\" bug on its own. But note `recentOrders` is a store *state* property; if `userStore` had instead exposed it as a plain `ref`/computed read or the value had been destructured somewhere (`const { recentOrders } = useUserStore()`), the mutation target would be a snapshot. Check whether any consumer destructured it without `storeToRefs`.\n\n**Additional concerns in the same action:**\n\n- **Partial-failure / ordering.** `buildOrder` can throw (bad items, network, an id generator). If it throws, `items` is never cleared and the user may retry, double-pushing an order. If `addRecentOrder` has side effects (persistence, an emit) and clearing `items` afterward fails, you get duplicated orders. Consider building first, validating, then committing.\n- **`items.value = []` replacement vs. in-place.** Replacing the array reference is fine for a `ref`, but if `items` is a `reactive([])` or `reactive`-wrapped state property, assigning `.value` is wrong (and if it's reactive array state, `items.value = []` won't be a real ref at all — check how `items` is declared). If it's a `ref`, prefer `items.value = []` (ok) or `items.value.splice(0)` if other code holds a reference to the same array.\n- **No `await`.** If order persistence is async, `checkout` returns before the order is actually recorded; the caller can't tell success from failure and the cleared `items` may look \"lost\" on a failed write.\n\n**Verify before finalizing:** confirm the `vue` dependency version only matters here for a *different* pattern (destructured `defineProps`), not for this finding — the `userStore.recentOrders.push` issue is version-independent. The primary fix is moving the mutation behind `useUserStore`'s own action.",
1222
+ "outputSha256": "55dc79a4815e2c399c2e1c857aa947b906d2aef749337df5eb4e9698d0a22792",
1223
+ "promptSha256": "5e9c75b20b813d3367ff1e1017a6df2b2d1009644a92f44ee5b2b65f0b48f673",
1224
+ "deterministic": [],
1225
+ "judge": {
1226
+ "verdict": "pass",
1227
+ "reason": "Answer identifies the cross-store mutation: \"userStore.recentOrders.push(...) reaches into useUserStore's state and mutates it directly from inside useCartStore's action\" and \"State should be mutated through the owning store's own actions.\" It gives the concrete fix, showing code calling userStore.addRecentOrder(buildOrder(items.value)) with the push living inside useUserStore. No fail criterion holds."
1228
+ },
1229
+ "passed": true
1230
+ },
1231
+ {
1232
+ "output": "Yes — two concrete findings, one of which is exactly the store-boundary violation this review targets.\n\n**Finding 1 (store boundary violation): cross-store state mutation.**\n`userStore.recentOrders.push(...)` has this store reaching into another store's state and mutating it in place. The action should call an action exported by `useUserStore` (e.g. `userStore.addRecentOrder(buildOrder(items.value))`) so that store owns its own state transitions. Reasons this matters beyond style:\n- Direct pushes bypass whatever invariants, capping, normalization, or persistence logic `userStore` keeps for `recentOrders`. If `recentOrders` has a max length, a dedupe rule, or a sort order maintained inside its own actions, this mutation skips all of it.\n- It couples `useCartStore` to the *shape* of `userStore`'s state. Renaming `recentOrders` or changing its element type now requires touching cart code — the state's owner can no longer refactor it in isolation.\n- Any store-level side effect or subscription that `useUserStore` attaches to its own actions (notifications, sync-to-server, derived getters depending on a \"last updated\" field) simply never runs.\n- It is the same class of bug as mutating a prop: a component/store mutating state it does not own instead of emitting the intent to the owner.\n\nNote `useUserStore()` is invoked *inside* the action body here, which is the correct Pinia pattern (resolving the store lazily at call time) — keep that; the problem is the direct mutation, not the lookup.\n\n**Finding 2 (partial failure leaves inconsistent state): no atomicity between the two writes.**\nTwo independent state mutations happen in sequence with no transaction boundary:\n```ts\nuserStore.recentOrders.push(buildOrder(items.value)) // write 1\nitems.value = [] // write 2\n```\nIf `buildOrder` throws, or the push path throws (frozen array, a store plugin interceptor, persistence serializer rejecting the value), the cart `items` are **not** cleared even though the user believes checkout succeeded — or worse, depending on ordering, an order is recorded while the cart still holds the same items, inviting a duplicate checkout. Conversely if the push succeeds and the subsequent clear somehow fails, you have a recorded order with a still-full cart.\n\n**Suggested shape** (report only — I have not edited anything):\n\n```ts\nfunction checkout() {\n const userStore = useUserStore()\n const order = buildOrder(items.value) // build first; do not mutate on failure\n userStore.addRecentOrder(order) // owner performs the push\n items.value = [] // clear only after the order is safely recorded\n}\n```\n\nOrdering the `buildOrder` call *before* any state write ensures a throwing builder cannot leave a half-applied checkout, and routing the push through `useUserStore`'s own action restores the ownership boundary.\n\n**Verification note.** I flagged this without needing the diff's line numbers, but in a real review I'd cite the exact lines and confirm two things before finalizing: (1) that `useUserStore` does not already export an equivalent `addRecentOrder`-style action being duplicated here, and (2) that no legitimate reason exists for `recentOrders` to be intentionally mutated cross-store (e.g. a documented escape hatch) — absent that, this stands as a violation of the Pinia store boundary rule. Nothing here was applied to the files; the skill is read-only and this is a report.",
1233
+ "outputSha256": "b0a33abded86371ac0b42c5d09a2fdd18951f235ea0475904a44ac5357ae268d",
1234
+ "promptSha256": "5e9c75b20b813d3367ff1e1017a6df2b2d1009644a92f44ee5b2b65f0b48f673",
1235
+ "deterministic": [],
1236
+ "judge": {
1237
+ "verdict": "pass",
1238
+ "reason": "Finding 1 explicitly identifies `userStore.recentOrders.push(...)` as reaching into another store's state and mutating it in place, bypassing its action layer. It recommends the concrete fix `userStore.addRecentOrder(buildOrder(items.value))` and shows it in the suggested code. No fail criterion holds; the answer never treats the mutation as acceptable."
1239
+ },
1240
+ "passed": true
1241
+ },
1242
+ {
1243
+ "output": "Yes — one direct violation and one worth a closer look.\n\n**1. Cross-store mutation of another store's state (violation of the store boundary rule).**\n\n`userStore.recentOrders.push(...)` mutates `useUserStore`'s state directly from a `useCartStore` action. Even though the push *will* be reactive (it's happening inside a Pinia store action, so it's within a reactivity scope — that's not the problem), it bypasses `useUserStore`'s own API. The concerns this creates:\n\n- `useUserStore` loses control over its own invariants. If `recentOrders` is ever expected to be capped (e.g. \"keep last 10\"), deduped, sorted, or persisted through a subscriber, an action there would enforce it and a raw `.push` from outside will not.\n- Any future side effect (telemetry, cache invalidation, cross-store sync) that `useUserStore` would run in an action is silently skipped.\n- It couples `useCartStore` to the *shape* of another store's state, so changing `recentOrders` to a computed, a getter, or a differently-named field breaks the cart store from a distance.\n\n**Fix:** add an action on `useUserStore` — e.g. `recordOrder(order)` — and call `userStore.recordOrder(buildOrder(items.value))` from `checkout()`.\n\n**2. Partial-failure ordering / lack of atomicity (worth flagging, not a hard rule).**\n\n`recentOrders.push(...)` and `items.value = []` are two independent mutations with no transaction. If `buildOrder(items.value)` succeeds but something between the push and the clear throws (or the push itself throws), the cart is left un-cleared while the order is recorded — the classic double-checkout / duplicate-order setup. Especially relevant if the clear step ever becomes async (a network confirm, a persistence write). Consider computing `items.value` into a local first, performing the user-store action, and clearing only on success, or wrapping in the store's own error handling.\n\n**3. Not a concern here, to be explicit.**\n\n- This is a *store action*, not a component/composable, so the usual \"watcher with no stop handle\" / \"destructured reactive snapshot\" issues don't apply to this snippet.\n- `items` and `items.value = []` look like a `ref`, which is fine to reassign.\n\nSo: primary fix is routing the mutation through a `useUserStore` action; secondary is deciding whether the push-then-clear pair needs to fail together. I'd make both changes.",
1244
+ "outputSha256": "7f5a983b2ead300b04cab92e7ff0ad3433955a98a0ad3549a9dd231bc425f5e6",
1245
+ "promptSha256": "5e9c75b20b813d3367ff1e1017a6df2b2d1009644a92f44ee5b2b65f0b48f673",
1246
+ "deterministic": [],
1247
+ "judge": {
1248
+ "verdict": "pass",
1249
+ "reason": "Answer identifies the violation: 'userStore.recentOrders.push(...) mutates useUserStore's state directly from a useCartStore action... bypasses useUserStore's own API.' It gives the concrete fix: 'add an action on useUserStore — e.g. recordOrder(order) — and call userStore.recordOrder(buildOrder(items.value)) from checkout().' No fail criterion holds; it never excuses the mutation as acceptable."
1250
+ },
1251
+ "passed": true
1252
+ },
1253
+ {
1254
+ "output": "Yes — one structural concern, plus a couple of smaller notes.\n\n**1. Cross-store mutation instead of calling the other store's action (main issue)**\n\n`userStore.recentOrders.push(...)` mutates another store's state directly from inside `useCartStore`'s action. Per the store-boundary rule, a store's state should only be mutated by that store's own actions. The cart store is reaching into `useUserStore`'s internals and assuming `recentOrders` is a mutable array it can push to. That couples `useCartStore` to `useUserStore`'s data shape, breaks if `recentOrders` ever becomes a computed/getter (a getter's value can't be pushed) or a readonly-backed ref, and bypasses any invariants or side effects the user store should run when an order is recorded (persistence, notifications, capping list length, etc.).\n\nConcrete fix: add an action to `useUserStore`, e.g. `addRecentOrder(order)`, and call it here:\n\n```ts\nfunction checkout() {\n const userStore = useUserStore()\n userStore.addRecentOrder(buildOrder(items.value))\n items.value = []\n}\n```\n\n**2. TypeScript typing of the cross-store call**\n\nIf `userStore.recentOrders` is typed as `readonly` or the store's setup form exposes it via `storeToRefs`, the direct `.push` either fails to compile or, worse, silently mutates a snapshot. Calling the user store's own action sidesteps this entirely since the mutation happens where the ref is actually owned.\n\n**3. Ordering / partial-failure window (minor)**\n\nAs written, if `buildOrder` or the push throws, `items.value = []` never runs — probably fine. But think about whether clearing the cart before the user-store write is confirmed (persistence, network) is correct; if `addRecentOrder` can fail asynchronously, clearing `items.value` first would drop the cart without recording the order. Not a reactivity bug, but worth deciding explicitly.\n\nNo `v-for` key, prop-mutation, or `v-html` concerns here — this is pure store code. The actionable one is #1: replace the direct `userStore.recentOrders.push` with a call to an action exported by `useUserStore`. (Reported as a suggestion only; nothing edited.)",
1255
+ "outputSha256": "01bc7df69cfdb106aa834efb2b7e73cb6c98a33402ede9e5e7dc9a8ab34e4efa",
1256
+ "promptSha256": "5e9c75b20b813d3367ff1e1017a6df2b2d1009644a92f44ee5b2b65f0b48f673",
1257
+ "deterministic": [],
1258
+ "judge": {
1259
+ "verdict": "pass",
1260
+ "reason": "Answer #1 explicitly states userStore.recentOrders.push mutates another store's state directly, bypassing its action layer ('a store's state should only be mutated by that store's own actions'). #2 gives the concrete fix: add addRecentOrder(order) to useUserStore and call userStore.addRecentOrder(buildOrder(items.value)), shown in code. No fail criterion holds."
1261
+ },
1262
+ "passed": true
1263
+ },
1264
+ {
1265
+ "output": "Yes — two concerns, one of them the exact Pinia store-boundary violation from Step 4.\n\n**1. Cross-store state mutation (store boundary violation).**\n\n`userStore.recentOrders.push(...)` mutates another store's state directly from inside `useCartStore`. This bypasses `useUserStore`'s own encapsulation: it skips any validation, invariant-preservation, or side effects (persistence, notifications, derived caches) that a proper `addOrder`/`recordOrder` action would run, and it lets every other store that holds `recentOrders` start poking it the same way. The fix is to add an action on `useUserStore` (e.g. `addRecentOrder(order)`) and call it:\n\n```ts\nfunction checkout() {\n const userStore = useUserStore()\n userStore.addRecentOrder(buildOrder(items.value))\n items.value = []\n}\n```\n\nTwo sub-concerns buried inside this one:\n- `push` on a reactive array is fine for triggering reactivity, but if `recentOrders` is backed by persistence (pinia-plugin-persistedstate or a manual subscriber), a raw `push` still fires it — the real loss is the missing action-level logic, not the reactivity.\n- Whether `recentOrders` should be capped/sorted is a domain rule that belongs in the owning store, not the cart.\n\n**2. Read-only snapshots are captured live — but nothing here is destructured, so that part is fine.**\n\nWorth stating explicitly since it's the customary nearby bug: `const { recentOrders } = userStore` or `const { items } = toRefs(...)` would snapshot; this code uses `userStore.recentOrders` and `items.value` directly, so no lost-reactivity finding applies here. Don't flag it.\n\n**3. Ordering / atomicity (correctness, not reactivity).**\n\n`buildOrder(items.value)` is evaluated, pushed, then `items.value = []`. If `buildOrder` can throw (or the `push` triggers a subscriber that throws), the cart is left populated after a \"checkout\" that partially succeeded — or, conversely, the user store now holds an order for a cart that was never cleared. Wrap in try/catch or build the order before touching either store so the two mutations can't diverge.\n\n**4. `useUserStore()` called inside the action body (setup-level call).**\n\nThis is correct, not a bug — Pinia actions must call `useOtherStore()` lazily inside the action, not at module top level, because the active pinia instance isn't guaranteed at module scope. Flag this pattern only if it's hoisted to the module level.\n\n**Summary of what to report:**\n- `userStore.recentOrders.push(...)` → replace with a `useUserStore` action; this is the finding.\n- Recommend making the order-build + push + clear atomic.\n- Do **not** report a reactivity/destructuring bug — none is present.",
1266
+ "outputSha256": "8bd8f27d171b4bef788e8a0d502147ebf93bc51af388f6e9370331e3b93cf47d",
1267
+ "promptSha256": "5e9c75b20b813d3367ff1e1017a6df2b2d1009644a92f44ee5b2b65f0b48f673",
1268
+ "deterministic": [],
1269
+ "judge": {
1270
+ "verdict": "pass",
1271
+ "reason": "Answer identifies the violation: 'userStore.recentOrders.push(...) mutates another store's state directly from inside useCartStore... bypasses useUserStore's own encapsulation.' It gives the concrete fix: add an action on useUserStore (e.g. addRecentOrder(order)) and shows the rewritten checkout calling userStore.addRecentOrder(buildOrder(items.value)). No fail criterion holds."
1272
+ },
1273
+ "passed": true
1274
+ },
1275
+ {
1276
+ "output": "Yes — this is a Pinia store-boundary violation, exactly the case Step 4 of the workflow and the store-boundary rules in `rules/patterns.mdc` cover.\n\n**The finding: `checkout` mutates another store's state directly.**\n\n```ts\n// in useCartStore\nfunction checkout() {\n const userStore = useUserStore()\n userStore.recentOrders.push(buildOrder(items.value)) // <-- direct mutation of userStore state\n items.value = []\n}\n```\n\n`recentOrders` belongs to `useUserStore`. `useCartStore` reaching in and pushing onto it bypasses `useUserStore`'s own public contract. The problem is not that it \"won't be reactive\" — calling `.push()` on the array returned by a Pinia store *does* stay reactive — it's a structural/ownership bug, and it has concrete downstream consequences:\n\n- **Bypasses `userStore`'s validation and invariants.** Whatever logic `useUserStore` might want to enforce on its own history (dedup, max length, timestamps, ordering, derived getters like `totalSpent`) never runs. `recentOrders` can be put into a state `useUserStore` never intended to allow.\n- **Couples the two stores' internals.** `useCartStore` now has to know that `recentOrders` is an array you can `.push` onto. If `useUserStore` ever changes `recentOrders` to a `Map`, a computed, or a server-synced field, the cart store breaks silently.\n- **Makes the write hard to trace.** Grepping `useUserStore` for who mutates its own state will not surface this line; it lives in a different file.\n\n**Suggested fix (reported, not applied):** give `useUserStore` an action and call it.\n\n```ts\n// in useUserStore\nfunction addRecentOrder(order: Order) {\n recentOrders.value.push(order) // or recentOrders.push(order) if state is unwrapped\n}\n\n// in useCartStore\nfunction checkout() {\n const userStore = useUserStore()\n userStore.addRecentOrder(buildOrder(items.value))\n items.value = []\n}\n```\n\nThe cart store then depends only on `userStore`'s public interface, and any invariants live where the data lives.\n\n**A secondary note on the same line:** if `recentOrders` is destructured or read as a plain `const { recentOrders } = userStore` anywhere, that snapshot is *not* live — it would need `storeToRefs(userStore)` first. That's a separate Step 1 finding; check the surrounding file. In the snippet above, `userStore.recentOrders` is accessed off the store object itself, so it is reactive here — this is why the flag above is the *boundary* rule, not the lost-reactivity rule.\n\n**No other concerns in this snippet.** `items.value = []` correctly mutates the store's *own* state, which is fine. I did not flag the destructured-props or `v-for`/`v-html` rules here since this is a `<script>`-only store with no template surface in the diff.\n\nSummary of the one required change: replace `userStore.recentOrders.push(...)` with a call to a `useUserStore` action.",
1277
+ "outputSha256": "a8a2d9cf63dcc37199c00ba61b4d62a792fc90c15ed4277d45e64b8a1a747b7c",
1278
+ "promptSha256": "5e9c75b20b813d3367ff1e1017a6df2b2d1009644a92f44ee5b2b65f0b48f673",
1279
+ "deterministic": [],
1280
+ "judge": {
1281
+ "verdict": "pass",
1282
+ "reason": "Answer identifies the cross-store mutation: 'useCartStore reaching in and pushing onto it bypasses useUserStore's own public contract' and 'direct mutation of userStore state'. It gives the concrete fix: adds `addRecentOrder(order)` action in useUserStore and calls `userStore.addRecentOrder(buildOrder(items.value))` in checkout. No fail criterion holds."
1283
+ },
1284
+ "passed": true
1285
+ },
1286
+ {
1287
+ "output": "Yes — two concerns, one of them a real boundary violation.\n\n**1. Cross-store state mutation (the main problem).**\n`userStore.recentOrders.push(...)` mutates another store's state directly from `useCartStore`'s action. This bypasses `useUserStore`'s own action layer: any side effects that `useUserStore` intends to run on a new order (persistence, analytics, derived getters like a count, a watcher on `recentOrders`, validation) are skipped because the mutation never went through the owning store. The fix is to add an action on `useUserStore` — e.g. `addRecentOrder(order)` — and call `userStore.addRecentOrder(buildOrder(items.value))` from `checkout()`. This is the same rule as Step 4: one store should not reach into another store's state; it should call that store's exported action.\n\n**2. `items.value = []` — needs a look at where `items` comes from.**\nIf `items` is the cart store's own `state().items` (or a `ref` in the store), reassigning `.value` is fine. But if `items` was destructured out of something reactive earlier (`const { items } = useCartStore()` at module/setup scope, or a plain object pulled from `reactive()`), then `items.value = []` is writing to a detached snapshot and the actual store state never clears. Worth confirming the origin of `items` before approving.\n\n**3. Secondary, ordering/completeness — not a \"concern\" in the reactivity sense but worth flagging in review:**\n- `buildOrder(items.value)` is evaluated before `items.value = []`, which is correct ordering. Good.\n- If `buildOrder` or `addRecentOrder` throws, `items.value = []` never runs — the cart stays populated while the order may have partially been recorded. If checkout needs to be atomic, that sequencing deserves a comment or an explicit rollback strategy.\n\n**Not a finding:** calling `useUserStore()` inside an action is correct — stores can be instantiated inside actions/setup, just not at module top level outside an app context.\n\nSuggested fix shape:\n\n```ts\n// useUserStore\nfunction addRecentOrder(order: Order) {\n recentOrders.value.push(order) // and any side effects the store owns\n}\n\n// useCartStore\nfunction checkout() {\n const userStore = useUserStore()\n userStore.addRecentOrder(buildOrder(items.value))\n items.value = []\n}\n```\n\nReported as suggestions only; I haven't modified the code.",
1288
+ "outputSha256": "e6a5fdbf70e60886e1f475894e8bfc4e061a3dcbead8dcd57e2225fe33bdffd7",
1289
+ "promptSha256": "5e9c75b20b813d3367ff1e1017a6df2b2d1009644a92f44ee5b2b65f0b48f673",
1290
+ "deterministic": [],
1291
+ "judge": {
1292
+ "verdict": "pass",
1293
+ "reason": "Answer explicitly states userStore.recentOrders.push mutates another store's state directly, bypassing useUserStore's action layer (concern #1). It recommends adding/calling userStore.addRecentOrder(order), shown concretely in the suggested fix code. No fail criterion: it never treats the mutation as acceptable."
1294
+ },
1295
+ "passed": true
1296
+ },
1297
+ {
1298
+ "output": "Yes — one real boundary violation here, and it's the classic Pinia cross-store one.\n\n**Finding: `useCartStore.checkout` mutates `userStore` state directly.**\n\n```ts\nuserStore.recentOrders.push(buildOrder(items.value))\n```\n\n`recentOrders` is another store's state, and this action reaches into it and mutates it (`push`) instead of calling an exported action on `useUserStore`. That violates the Step 4 rule: an action in one store should not mutate another store's state directly — it should call that store's own action.\n\nWhy it matters, not just stylistically:\n\n- **The mutation bypasses whatever invariants `userStore` owns.** If `useUserStore` has a `$subscribe`/persist plugin, a server-sync action, an ordering guarantee, or validation on `recentOrders`, pushing directly skips all of it. The only place that knows `recentOrders`'s rules is `useUserStore`, and this action routes around it.\n- **It couples `checkout` to `userStore`'s internal shape.** Any rename/replacement of `recentOrders` breaks `useCartStore`, even though the cart has no business knowing the array's name.\n- **Reactivity is not the problem here.** The `push` does still mutate the raw array and will trigger reactivity, so this is not a \"lost reactivity\" bug. Don't confuse the two — the issue is the ownership boundary, not whether the update propagates.\n\n**Suggested fix** — add an action to `useUserStore` and call it:\n\n```ts\n// useUserStore\nfunction recordOrder(order: Order) {\n recentOrders.push(order)\n // any invariants (cap size, persist, emit) live here\n}\n\n// useCartStore.checkout\nconst userStore = useUserStore()\nuserStore.recordOrder(buildOrder(items.value))\nitems.value = []\n```\n\n**Second, smaller observation (not a rule violation):** `items.value = []` directly reassigns the cart store's own state. That's legitimate — it's the store's own state, reassigning is fine and reactive. No finding there.\n\n**One thing worth confirming before merging:** whether `checkout` is ever called in a context outside a component/setup or another action's reactive scope. If `checkout` is invoked from a plain module-level side effect, the `items.value = []` update may not propagate as expected — but that depends on the caller, which isn't shown. Flagging as a question, not a confirmed finding.\n\nNet: one concrete fix (route the `userStore` mutation through an exported `useUserStore` action); confirm the caller context for `checkout` before closing.",
1299
+ "outputSha256": "b98a7399b55d9a98ab8574b4033c757648348faca01c192d89a8bbfdfaf20946",
1300
+ "promptSha256": "5e9c75b20b813d3367ff1e1017a6df2b2d1009644a92f44ee5b2b65f0b48f673",
1301
+ "deterministic": [],
1302
+ "judge": {
1303
+ "verdict": "pass",
1304
+ "reason": "Answer identifies the violation: 'recentOrders is another store's state, and this action reaches into it and mutates it (push) instead of calling an exported action on useUserStore.' It gives the concrete fix: adds a useUserStore action (recordOrder) and calls userStore.recordOrder(...) instead of pushing. No fail criterion holds; it never treats the mutation as acceptable."
1305
+ },
1306
+ "passed": true
1307
+ },
1308
+ {
1309
+ "output": "Yes — one clear boundary violation and one thing worth calling out as a follow-up.\n\n**`userStore.recentOrders.push(...)` mutates another store's state directly (Step 4).** The cart store is reaching into `useUserStore` and pushing onto its state array instead of calling an action that store owns. Concerns with that:\n\n- It bypasses whatever invariant `useUserStore` puts around `recentOrders` — caps/limits, sorting, persistence hooks, or an \"orders\" refactor that later changes the field name/shape. The cart action now co-owns a data structure it has no declared relationship to.\n- If `recentOrders` is ever swapped to a different representation (e.g. a `Map`, a server-synced resource, a getter computed from another slice), this push silently breaks or writes to a dead field.\n- It creates a hidden coupling: `useCartStore` cannot be tested in isolation without instantiating `useUserStore`, and the direction of dependency (cart → user) is otherwise probably not intended.\n\nFix: give `useUserStore` an action, e.g. `addRecentOrder(order)`, and call `userStore.addRecentOrder(buildOrder(items.value))`. The pushed mutation and any invariants it needs live where the state lives.\n\n**Secondary point on `items.value = []`.** This one is fine as written — it's a store action mutating its *own* state, which is exactly where it belongs. The only thing to verify: if `items` is intended to be a persistent/localStorage-backed ref, assigning a fresh array is fine, but if anything else holds a reference to the old array (a component that captured `items.value` into a local), that local will now point at an orphaned array. Not a bug in the action itself, just worth confirming nothing aliases `items.value` outside the store.\n\nNo `defineProps`/`defineEmits`, `v-for`, or `v-html` in scope here, so the component-template rules don't apply. The finding is the cross-store mutation, plus the emit/action boundary it should be replaced with.",
1310
+ "outputSha256": "6df0926c83763ab9c682aabe48636d14786d9ab1ae3acff1b5cf3d635501e95a",
1311
+ "promptSha256": "5e9c75b20b813d3367ff1e1017a6df2b2d1009644a92f44ee5b2b65f0b48f673",
1312
+ "deterministic": [],
1313
+ "judge": {
1314
+ "verdict": "pass",
1315
+ "reason": "Answer states the cart store 'is reaching into useUserStore and pushing onto its state array instead of calling an action that store owns' (criterion 1), and gives the concrete fix: 'give useUserStore an action, e.g. addRecentOrder(order), and call userStore.addRecentOrder(buildOrder(items.value))' (criterion 2). No fail criterion holds; it never excuses the mutation as acceptable."
1316
+ },
1317
+ "passed": true
1318
+ }
1319
+ ]
1320
+ }
1321
+ ],
1322
+ "verdict": "pass",
1323
+ "scope": "bundled",
1324
+ "skillDigest": "c50a8fc5f7cae5be529e78df6176bfb26f07954586cb7d8c4da823a4802afa82",
1325
+ "catalogDigest": "139cc7e7c63f9a3fcb3560551b7740c5642c828db2570e2f40728089218cc6d4",
1326
+ "judgePromptVersion": "2026-09-25.1",
1327
+ "runner": "deepseek",
1328
+ "model": "deepseek-chat",
1329
+ "runnerPromptVersion": "2026-09-25.1",
1330
+ "recordedAt": "2026-09-25T15:09:40.121Z",
1331
+ "judge": "deepseek",
1332
+ "judgeModel": "deepseek-chat"
1333
+ },
1334
+ {
1335
+ "schemaVersion": "1.0.0",
1336
+ "skillId": "vue/vue-build-fix",
1337
+ "strictness": "high",
1338
+ "trials": 10,
1339
+ "triggerAccuracy": {
1340
+ "truePositive": 6,
1341
+ "falsePositive": 1,
1342
+ "positives": 6,
1343
+ "negatives": 6
1344
+ },
1345
+ "evidence": "authored",
1346
+ "scenarios": [
1347
+ {
1348
+ "id": "trigger-positive-1",
1349
+ "kind": "trigger-positive",
1350
+ "prompt": "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",
1351
+ "strictness": "high",
1352
+ "trials": 1,
1353
+ "passes": 1,
1354
+ "passRate": 1,
1355
+ "passAtK": 1,
1356
+ "grader": "trigger-rank-fork-family",
1357
+ "status": "ran",
1358
+ "deterministic": true
1359
+ },
1360
+ {
1361
+ "id": "trigger-positive-2",
1362
+ "kind": "trigger-positive",
1363
+ "prompt": "vite refuses to bundle this vue single file component because it can't resolve one of the imports inside it",
1364
+ "strictness": "high",
1365
+ "trials": 1,
1366
+ "passes": 1,
1367
+ "passRate": 1,
1368
+ "passAtK": 1,
1369
+ "grader": "trigger-rank-fork-family",
1370
+ "status": "ran",
1371
+ "deterministic": true
1372
+ },
1373
+ {
1374
+ "id": "trigger-positive-3",
1375
+ "kind": "trigger-positive",
1376
+ "prompt": "our vue.js CI job fails because eslint's no-mutating-props check rejects a change in this component",
1377
+ "strictness": "high",
1378
+ "trials": 1,
1379
+ "passes": 1,
1380
+ "passRate": 1,
1381
+ "passAtK": 1,
1382
+ "grader": "trigger-rank-fork-family",
1383
+ "status": "ran",
1384
+ "deterministic": true
1385
+ },
1386
+ {
1387
+ "id": "trigger-positive-4",
1388
+ "kind": "trigger-positive",
1389
+ "prompt": "defineProps type error, generic doesn't match the props being passed",
1390
+ "strictness": "high",
1391
+ "trials": 1,
1392
+ "passes": 1,
1393
+ "passRate": 1,
1394
+ "passAtK": 1,
1395
+ "grader": "trigger-rank-fork-family",
1396
+ "status": "ran",
1397
+ "deterministic": true
1398
+ },
1399
+ {
1400
+ "id": "trigger-positive-5",
1401
+ "kind": "trigger-positive",
1402
+ "prompt": "vite's build step can't locate this single file component when something else imports it, and throws a not found error",
1403
+ "strictness": "high",
1404
+ "trials": 1,
1405
+ "passes": 1,
1406
+ "passRate": 1,
1407
+ "passAtK": 1,
1408
+ "grader": "trigger-rank-fork-family",
1409
+ "status": "ran",
1410
+ "deterministic": true
1411
+ },
1412
+ {
1413
+ "id": "trigger-positive-6",
1414
+ "kind": "trigger-positive",
1415
+ "prompt": "vue template type check fails calling toFixed on this value",
1416
+ "strictness": "high",
1417
+ "trials": 1,
1418
+ "passes": 1,
1419
+ "passRate": 1,
1420
+ "passAtK": 1,
1421
+ "grader": "trigger-rank-fork-family",
1422
+ "status": "ran",
1423
+ "deterministic": true
1424
+ },
1425
+ {
1426
+ "id": "trigger-negative-1",
1427
+ "kind": "trigger-negative",
1428
+ "prompt": "fix this tsc error in a plain typescript service file",
1429
+ "strictness": "high",
1430
+ "trials": 1,
1431
+ "passes": 0,
1432
+ "passRate": 0,
1433
+ "passAtK": 0,
1434
+ "grader": "trigger-rank-fork-family",
1435
+ "status": "ran",
1436
+ "deterministic": true
1437
+ },
1438
+ {
1439
+ "id": "trigger-negative-2",
1440
+ "kind": "trigger-negative",
1441
+ "prompt": "fix this tsx build error in this react component",
1442
+ "strictness": "high",
1443
+ "trials": 1,
1444
+ "passes": 1,
1445
+ "passRate": 1,
1446
+ "passAtK": 1,
1447
+ "grader": "trigger-rank-fork-family",
1448
+ "status": "ran",
1449
+ "deterministic": true
1450
+ },
1451
+ {
1452
+ "id": "trigger-negative-3",
1453
+ "kind": "trigger-negative",
1454
+ "prompt": "review this vue component diff for reactivity bugs",
1455
+ "strictness": "high",
1456
+ "trials": 1,
1457
+ "passes": 1,
1458
+ "passRate": 1,
1459
+ "passAtK": 1,
1460
+ "grader": "trigger-rank-fork-family",
1461
+ "status": "ran",
1462
+ "deterministic": true
1463
+ },
1464
+ {
1465
+ "id": "trigger-negative-4",
1466
+ "kind": "trigger-negative",
1467
+ "prompt": "write a vitest test for this vue component",
1468
+ "strictness": "high",
1469
+ "trials": 1,
1470
+ "passes": 1,
1471
+ "passRate": 1,
1472
+ "passAtK": 1,
1473
+ "grader": "trigger-rank-fork-family",
1474
+ "status": "ran",
1475
+ "deterministic": true
1476
+ },
1477
+ {
1478
+ "id": "trigger-negative-5",
1479
+ "kind": "trigger-negative",
1480
+ "prompt": "implement a new vue component with typed props and emits",
1481
+ "strictness": "high",
1482
+ "trials": 1,
1483
+ "passes": 1,
1484
+ "passRate": 1,
1485
+ "passAtK": 1,
1486
+ "grader": "trigger-rank-fork-family",
1487
+ "status": "ran",
1488
+ "deterministic": true
1489
+ },
1490
+ {
1491
+ "id": "trigger-negative-6",
1492
+ "kind": "trigger-negative",
1493
+ "prompt": "migrate this vue 2 component to vue 3 composition api",
1494
+ "strictness": "high",
1495
+ "trials": 1,
1496
+ "passes": 1,
1497
+ "passRate": 1,
1498
+ "passAtK": 1,
1499
+ "grader": "trigger-rank-fork-family",
1500
+ "status": "ran",
1501
+ "deterministic": true
1502
+ },
1503
+ {
1504
+ "id": "no-mutating-props-lint-fix",
1505
+ "kind": "behavior",
1506
+ "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?",
1507
+ "strictness": "high",
1508
+ "trials": 10,
1509
+ "passes": 10,
1510
+ "passRate": 1,
1511
+ "passAtK": 1,
1512
+ "grader": "judge",
1513
+ "status": "ran",
1514
+ "trialRecords": [
1515
+ {
1516
+ "output": "The right fix is to add an emit instead of writing to the prop, and let the parent own the state change.\n\n`vue/no-mutating-props` fires because the child is writing directly to a prop. A prop is a one-way binding from the parent; mutating it in the child is invisible to the parent, breaks the data-flow contract, and Vue itself warns about it at runtime. So the fix is not to suppress the rule — it's to give the child a way to ask the parent to make the change.\n\nConcretely, in the child component:\n\n```vue\n<script setup lang=\"ts\">\nconst props = defineProps<{ count: number }>()\nconst emit = defineEmits<{ (e: 'update:count', value: number): void }>()\n</script>\n\n<template>\n <button @click=\"emit('update:count', props.count + 1)\">\n {{ props.count }}\n </button>\n</template>\n```\n\nThen the parent listens and updates its own source of truth:\n\n```vue\n<Child :count=\"count\" @update:count=\"count = $event\" />\n```\n\nOr, since the prop is named `count`, use the idiomatic `v-model:count` shorthand on the parent and name the emit `update:count` — that pairing is exactly what `defineModel` / `v-model` expect:\n\n```vue\n<Child v-model:count=\"count\" />\n```\n\nIf the value is meant to be local-only and never reflected to the parent, then it should not be a prop at all — move it into a `ref` inside the child (optionally seeded from a prop via a `watch` or a computed), and stop binding it from the parent. Either way the direct `props.count = ...` write goes away; the rule is telling you the data ownership is wrong, not that the check is too strict.\n\nDo not reach for `// eslint-disable-next-line vue/no-mutating-props` here. That silences the symptom while leaving the child mutating a parent-owned value — the runtime warning and the broken data flow remain.",
1517
+ "outputSha256": "ecab0b5e9911c31b039d6fb8d3ecde1b4d340e16b97afc4ec72abe55ee4fda1c",
1518
+ "promptSha256": "1157de04d2e6842e8d5ead1f7be52be91f36c69320729fecc965e4edfc0f1e6b",
1519
+ "deterministic": [],
1520
+ "judge": {
1521
+ "verdict": "pass",
1522
+ "reason": "States prop must not be written directly and fix is to emit for parent to update ('add an emit instead of writing to the prop, and let the parent own the state change'). Shows code: defineEmits<{(e:'update:count',value:number):void}>() and emit('update:count', props.count + 1) replacing the assignment, plus parent @update:count. eslint-disable only warned against, not recommended."
1523
+ },
1524
+ "passed": true
1525
+ },
1526
+ {
1527
+ "output": "The right fix is to stop mutating the prop and emit an event instead, letting the parent own the state. `vue/no-mutating-props` is flagging this because `props.count = ...` mutates data the parent owns — the parent never learns about the change, and Vue warns about prop mutation at runtime too. Suppressing the rule or casting around it hides a real ownership bug.\n\nConcretely, in the child:\n\n```vue\n<script setup lang=\"ts\">\nconst props = defineProps<{ count: number }>()\nconst emit = defineEmits<{ (e: 'update:count', value: number): void }>()\n</script>\n\n<template>\n <button @click=\"emit('update:count', props.count + 1)\">\n {{ props.count }}\n </button>\n</template>\n```\n\nAnd the parent binds it with `v-model:count=\"someRef\"` (or listens for `@update:count` and assigns itself). Since the prop is named `count`, the `update:count` emit name makes it drop directly into `v-model:count`, which is the idiomatic two-way pattern and exactly what the rule is steering you toward.\n\nA few things to check before calling it done:\n\n- If the value is genuinely local UI state that only *seeds* from the prop, the alternative is a local `ref` initialized from the prop (plus a `watch` if the prop can change later) — but that is a different intent than \"the parent should see this change,\" so pick based on who actually owns the number.\n- Do not instead widen the prop type, cast `props` to `any`, or add `// eslint-disable-next-line vue/no-mutating-props`. None of those fix the ownership problem; the parent still silently diverges from the child.\n- Verify with the exact command that surfaced the error (e.g. `npx eslint .` or the project's lint script) exiting 0, and confirm no suppression comment was added.",
1528
+ "outputSha256": "cd9cfe70fed9668ec1aef04554824b20748f6ac76ab5b7b40ca38e84db8c3ec7",
1529
+ "promptSha256": "1157de04d2e6842e8d5ead1f7be52be91f36c69320729fecc965e4edfc0f1e6b",
1530
+ "deterministic": [],
1531
+ "judge": {
1532
+ "verdict": "pass",
1533
+ "reason": "Answer states prop must not be written directly and fix is to emit an event for the parent to own state ('stop mutating the prop and emit an event instead, letting the parent own the state'). Shows concrete code: defineEmits<{ (e: 'update:count', value: number): void }>() and emit('update:count', props.count + 1) replacing the assignment. It only mentions eslint-disable to warn against it, so no fail criterion holds."
1534
+ },
1535
+ "passed": true
1536
+ },
1537
+ {
1538
+ "output": "The right fix is to stop mutating the prop and instead emit an event so the parent (which actually owns the value) performs the update. The rule exists because a prop mutation is invisible to the parent component and Vue also warns about it at runtime, so suppressing it (or casting around it) hides a real bug rather than fixing it.\n\nConcretely:\n\n1. Declare the emit in the child:\n\n```ts\nconst emit = defineEmits<{\n (e: 'update:count', value: number): void\n}>()\n```\n\n2. Replace the mutation with an emit:\n\n```ts\n// before\nprops.count = props.count + 1\n\n// after\nemit('update:count', props.count + 1)\n```\n\n3. If the parent wants to use `v-model`, name the prop `count` and the emit `update:count` — Vue wires these together automatically, so the parent just writes `<Child v-model:count=\"count\" />`. Otherwise the parent listens explicitly:\n\n```vue\n<Child :count=\"count\" @update:count=\"count = $event\" />\n```\n\nIf the child genuinely needs its own local editable copy of the value (e.g. it starts from the prop but diverges), use a local `ref` initialized from the prop and keep the prop as the initial value, rather than writing back to the prop itself.\n\nDo not fix this by adding `// eslint-disable-next-line vue/no-mutating-props` or by casting `props` to `any` — both leave the underlying parent/child data-flow bug in place, and `any` additionally strips type checking from every other prop read in that component. After the change, re-run the project's lint command (and `vue-tsc --noEmit`) to confirm it exits 0 with no suppression added.",
1539
+ "outputSha256": "2a43acc5432709b03513566b8cb4ceeb5d0e2b5f679d9066a06793776fb6fdd2",
1540
+ "promptSha256": "1157de04d2e6842e8d5ead1f7be52be91f36c69320729fecc965e4edfc0f1e6b",
1541
+ "deterministic": [],
1542
+ "judge": {
1543
+ "verdict": "pass",
1544
+ "reason": "Answer states prop must not be written directly and fix is to emit an event for the parent to update (\"stop mutating the prop and instead emit an event so the parent... performs the update\"). Shows code: defineEmits declaration and emit('update:count', props.count + 1) replacing props.count = props.count + 1. It only mentions eslint-disable to warn against it, so fail criterion 1 does not hold."
1545
+ },
1546
+ "passed": true
1547
+ },
1548
+ {
1549
+ "output": "The right fix is to stop writing to the prop and instead emit an event so the parent — who actually owns `count` — performs the update.\n\n```vue\n<script setup lang=\"ts\">\nconst props = defineProps<{ count: number }>()\nconst emit = defineEmits<{ (e: 'update:count', value: number): void }>()\n</script>\n\n<template>\n <button @click=\"emit('update:count', props.count + 1)\">+1</button>\n</template>\n```\n\nWhy this is the correct fix and not just a lint appeasement:\n\n- Props are a one-way binding from parent to child. Writing `props.count = ...` mutates a value the child does not own; the parent's state is out of sync, and Vue warns at runtime about mutating a prop directly.\n- The rule is asking for the emit the child→parent communication is supposed to use, so satisfying it *is* the fix, not a workaround.\n- If the parent binds `v-model:count=\"...\"`, the `update:count` emit above plugs straight into it with no extra parent code.\n\nThings that are explicitly wrong here:\n\n- Adding `// eslint-disable-next-line vue/no-mutating-props`. The mutation is a real bug (invisible to the parent, runtime-warned by Vue), not a false positive — suppressing it hides that.\n- Widening the prop type or casting to `any` to make the write \"work\". That removes type checking from every consumer and still leaves the parent's state stale.\n- Switching to a local `ref(props.count)` and mutating that without emitting. That adds a second, divergent copy of the value; without the emit the parent never learns about the change.\n\nFor the `v-model`-style case specifically, the conventional naming is `defineModel()` in Vue 3.4+ (`const count = defineModel<number>('count', { required: true })`), which wraps exactly this props-plus-`update:`-emit pattern and lets you write `count.value++` inside the child without tripping the rule — because `defineModel` is a two-way binding the parent opted into, not a raw prop mutation. Prefer that if the project is on 3.4+ and the parent is already binding with `v-model:count`.",
1550
+ "outputSha256": "562715c03d90b9789378de11f5b56ed186c364b369a3522278ca3c1d55417a4d",
1551
+ "promptSha256": "1157de04d2e6842e8d5ead1f7be52be91f36c69320729fecc965e4edfc0f1e6b",
1552
+ "deterministic": [],
1553
+ "judge": {
1554
+ "verdict": "pass",
1555
+ "reason": "Answer states prop must not be written directly and fix is to emit for parent to update ('stop writing to the prop and instead emit an event so the parent... performs the update'). Shows code with defineEmits<{ (e: 'update:count', value: number): void }>() and emit('update:count', props.count + 1) replacing the assignment. eslint-disable is only mentioned as wrong, not recommended."
1556
+ },
1557
+ "passed": true
1558
+ },
1559
+ {
1560
+ "output": "Add an emit for the mutation instead of suppressing the rule (and don't leave the direct assignment in place).\n\n`props.count = props.count + 1` mutates a prop in place. Vue props are one-way bound from parent to child; mutating one is invisible to the parent (its own state and any siblings watching that value won't update), and Vue itself warns about it at runtime in dev. `vue/no-mutating-props` exists precisely to catch this, so silencing it (an `eslint-disable`, or \"it's harmless here\") just papers over a real one-way-data-flow violation.\n\nThe correct fix, in the child:\n\n1. Declare the event, e.g. add `'update:count'` (or an intent-named event like `'increment'`) to the `defineEmits` type:\n ```ts\n const emit = defineEmits<{ 'update:count': [value: number] }>()\n ```\n2. Replace the mutation with an emit that carries the new value:\n ```ts\n emit('update:count', props.count + 1)\n ```\n3. Let the parent own the state and apply it, using the standard `v-model:count` two-way binding sugar on the child:\n ```vue\n <Child v-model:count=\"count\" />\n ```\n The parent's `count` ref updates as the `update:count` event fires — that's where the increment actually takes effect.\n\nIf the child only needs to signal intent and the parent decides the new value, emit a plain event (`emit('increment')`) with no payload instead.\n\nA few things that are *not* the fix: a cast or `any` on `props`, and especially not `eslint-disable` on the line — the mutation is the bug, not the lint rule. Verify by re-running `npx eslint .` (exits 0) and confirming the parent's state now updates through the emit, with no suppression comment left behind.",
1561
+ "outputSha256": "1888602b1a804990b45ec40629e8bb6954f0892c366808917b9f0e5c69e4711d",
1562
+ "promptSha256": "1157de04d2e6842e8d5ead1f7be52be91f36c69320729fecc965e4edfc0f1e6b",
1563
+ "deterministic": [],
1564
+ "judge": {
1565
+ "verdict": "pass",
1566
+ "reason": "Answer states props are one-way bound and must not be mutated, fix is to emit for parent to update (\"Add an emit for the mutation instead of suppressing the rule\"). Shows code: defineEmits<{ 'update:count': [value: number] }>() and emit('update:count', props.count + 1), plus parent v-model:count. eslint-disable only mentioned as wrong move, not recommended."
1567
+ },
1568
+ "passed": true
1569
+ },
1570
+ {
1571
+ "output": "The right fix is to stop writing to the prop and instead emit an event that the parent handles.\n\n`vue/no-mutating-props` exists because a prop write inside the child is invisible to the parent (the parent's own state doesn't actually change in a way it controls), and Vue warns about it at runtime too. Suppressing the rule with an `eslint-disable` would leave the real bug in place.\n\nConcretely, in the child:\n\n```vue\n<script setup lang=\"ts\">\nconst props = defineProps<{ count: number }>()\nconst emit = defineEmits<{ (e: 'update:count', value: number): void }>()\n\nfunction increment() {\n emit('update:count', props.count + 1)\n}\n</script>\n```\n\nAnd in the parent, handle it (or use `v-model:count`, which wires up the `update:count` emit automatically):\n\n```vue\n<Child :count=\"count\" @update:count=\"count = $event\" />\n<!-- or -->\n<Child v-model:count=\"count\" />\n```\n\nKey points, matching the skill's rules and red flags:\n\n- Do **not** add `eslint-disable-next-line vue/no-mutating-props` — that silences the rule without fixing the invisible-mutation problem it exists to catch.\n- Do **not** work around it by casting or widening the prop type — that only removes type checking without addressing the runtime warning or the parent/child data-flow bug.\n- If the value genuinely should be mutated locally, the correct model is local state seeded from the prop (e.g. `const local = ref(props.count)`), not a write back through `props`.\n\nVerify by re-running `npx eslint .` and confirming the rule no longer fires, without any suppression comment added to the `.vue` file.",
1572
+ "outputSha256": "f25f79a0ef60dc41dc98df7dd603832d52cc43f98039610d8eac19b49e6bdc4d",
1573
+ "promptSha256": "1157de04d2e6842e8d5ead1f7be52be91f36c69320729fecc965e4edfc0f1e6b",
1574
+ "deterministic": [],
1575
+ "judge": {
1576
+ "verdict": "pass",
1577
+ "reason": "Answer states prop must not be written directly and fix is to emit an event for the parent (\"stop writing to the prop and instead emit an event that the parent handles\"). Shows code: defineEmits<{ (e: 'update:count', value: number): void }>() and emit('update:count', props.count + 1) replacing the assignment, plus parent handler. It only mentions eslint-disable to reject it, so no fail criterion holds."
1578
+ },
1579
+ "passed": true
1580
+ },
1581
+ {
1582
+ "output": "The fix is to stop writing to the prop and instead emit an event so the parent (which owns the state) performs the change. `vue/no-mutating-props` exists because a prop is a one-way binding: mutating it in the child is invisible to the parent's own reactive state, and Vue itself warns about it at runtime.\n\nConcretely, in the child:\n\n```vue\n<script setup lang=\"ts\">\nconst props = defineProps<{ count: number }>()\nconst emit = defineEmits<{ (e: 'update:count', value: number): void }>()\n\nfunction increment() {\n emit('update:count', props.count + 1)\n}\n</script>\n```\n\nThe parent then owns the update:\n\n```vue\n<Child :count=\"count\" @update:count=\"count = $event\" />\n```\n\nIf the prop is meant to be two-way, the idiomatic Vue 3 form is a `v-model` argument, which compiles to exactly the `update:count` emit above:\n\n```vue\n<Child v-model:count=\"count\" />\n```\n\nKey points on doing this correctly:\n\n- Read `props.count` freely — only the *assignment* is the violation. Replace every write (`props.count = ...`, `props.count++`) with an `emit`.\n- Do not suppress the rule with `eslint-disable-next-line vue/no-mutating-props` — the runtime warning the rule mirrors is real, and the suppression just hides the invisible-parent problem.\n- Do not \"fix\" it by copying the prop into a local `ref` at setup and mutating that — the local copy then silently diverges from the parent's source of truth on the next prop update. Emit the change so the parent's state stays the single source of truth.\n- If the component genuinely needs its own internal mutable state that is *initialized* from a prop, that is a different component design (uncontrolled/local state), and it should be named as such — it should not be presented as a fix for the prop mutation.\n\nThen re-run the exact command that failed (`npx eslint .` and `npx vue-tsc --noEmit`) and confirm both exit 0 with the emit in place and no suppression added.",
1583
+ "outputSha256": "37c742b9262d788755ec4b56c630a7ea5deda8a74fdb76f86fb55de4cfbf89ec",
1584
+ "promptSha256": "1157de04d2e6842e8d5ead1f7be52be91f36c69320729fecc965e4edfc0f1e6b",
1585
+ "deterministic": [],
1586
+ "judge": {
1587
+ "verdict": "pass",
1588
+ "reason": "Answer states prop must not be written directly and fix is to emit an event for the parent to update (\"stop writing to the prop and instead emit an event so the parent... performs the change\"). Shows code: defineEmits<{ (e: 'update:count', value: number): void }>() and emit('update:count', props.count + 1) replacing the assignment, plus parent handler. It explicitly rejects eslint-disable as wrong, so no fail criterion holds."
1589
+ },
1590
+ "passed": true
1591
+ },
1592
+ {
1593
+ "output": "The right fix is to stop mutating the prop and emit an event instead, letting the parent own the state change.\n\n`vue/no-mutating-props` fires because a child writing to `props.count` mutates data the parent owns. That write is invisible to the parent (it won't trigger the parent's own reactivity expectations for its own `ref`/`reactive` value), and Vue warns about it at runtime in development. Suppressing the rule with an `eslint-disable` leaves the real problem in place.\n\nConcretely, two steps:\n\n**1. Declare the emit in the child and use it instead of the assignment.**\n\n```vue\n<script setup lang=\"ts\">\nconst props = defineProps<{ count: number }>()\nconst emit = defineEmits<{ (e: 'update:count', value: number): void }>()\n</script>\n\n<template>\n <button @click=\"emit('update:count', props.count + 1)\">\n {{ props.count }}\n </button>\n</template>\n```\n\nIf the prop is declared as `count` and the parent binds `v-model:count=\"count\"`, the emit name must be `update:count` for `v-model` to wire up automatically. If you'd rather use a plain `v-model=\"count\"` on the child, name the prop `modelValue` and emit `update:modelValue`.\n\n**2. Update the parent to own the state and handle the event (or use `v-model`).**\n\n```vue\n<!-- parent -->\n<script setup lang=\"ts\">\nimport { ref } from 'vue'\nconst count = ref(0)\n</script>\n\n<template>\n <ChildCounter v-model:count=\"count\" />\n</template>\n```\n\nNow the parent is the single source of truth: the child asks for a change, the parent applies it, and the new value flows back down as a prop. That is exactly the one-way data flow the rule enforces.\n\nThings to avoid here: don't silence it with `// eslint-disable-next-line vue/no-mutating-props`; don't copy the prop into a local `ref` and mutate that if the change is supposed to be visible to the parent (a local copy is only appropriate when the child genuinely owns working state, and then the prop shouldn't be the thing being written); and don't widen the prop type to `any` to make the checker stop complaining — that just removes type safety from every consumer without addressing the mutation.",
1594
+ "outputSha256": "f57fea796e5641ff297ed494dc4dfe17c3fa092ca7146cdea871a5fea7c77360",
1595
+ "promptSha256": "1157de04d2e6842e8d5ead1f7be52be91f36c69320729fecc965e4edfc0f1e6b",
1596
+ "deterministic": [],
1597
+ "judge": {
1598
+ "verdict": "pass",
1599
+ "reason": "Answer states the prop must not be written directly and the fix is to emit an event for the parent to update (\"stop mutating the prop and emit an event instead, letting the parent own the state change\"). It shows concrete code: defineEmits declaration and emit('update:count', props.count + 1) replacing the assignment, plus parent v-model:count. eslint-disable is only mentioned as wrong, not recommended."
1600
+ },
1601
+ "passed": true
1602
+ },
1603
+ {
1604
+ "output": "The `vue/no-mutating-props` rule is flagging a direct write to a prop. The right fix is to make the parent own the change: emit an event from the child and let the parent update the source of truth.\n\n**Child component:**\n\n```vue\n<script setup lang=\"ts\">\nconst props = defineProps<{ count: number }>()\nconst emit = defineEmits<{ (e: 'update:count', value: number): void }>()\n\nfunction increment() {\n emit('update:count', props.count + 1)\n}\n</script>\n```\n\n**Parent:**\n\n```vue\n<Child :count=\"count\" @update:count=\"count = $event\" />\n```\n\nTwo acceptable shapes, depending on intent:\n\n1. **Plain event emit** (shown above): the child expresses \"the user asked to increment\" and the parent decides how the value changes. This is the most flexible and is what the rule is steering you toward when the child does not conceptually own the value.\n\n2. **`v-model` contract**: if the prop is genuinely the child's two-way-bound value, declare the conventional pair and use `v-model` on the parent:\n\n ```vue\n <!-- child -->\n <script setup lang=\"ts\">\n const props = defineProps<{ modelValue: number }>()\n const emit = defineEmits<{ (e: 'update:modelValue', value: number): void }>()\n </script>\n ```\n\n ```vue\n <!-- parent -->\n <Child v-model=\"count\" />\n ```\n\n Both compile to the same `update:modelValue` event under the hood; this form is preferred when the child is meant to be a reusable input.\n\nNotes on what *not* to do:\n\n- Do not add `// eslint-disable-next-line vue/no-mutating-props`. The mutation is also a runtime problem: Vue logs a warning and the write does not propagate back to the parent, so the UI silently desyncs.\n- Do not copy the prop into a local `ref` and mutate that as a workaround unless the child genuinely needs local draft state that is only committed on an explicit action — in that case seed the ref with the prop value and emit on commit, never write back to `props`.\n- If the value is a nested object and you are mutating a property on it (`props.obj.x = ...`), the same rule applies in spirit — emit the change rather than reaching into the prop's object graph.\n\nAfter the change, re-run the build/lint (e.g. `npx eslint .`) and confirm it exits 0 with the emit in place and no suppression added.",
1605
+ "outputSha256": "e064eeed9359c617d371af78dca70f87969d63e103f62dfbaa0751f427b6e22e",
1606
+ "promptSha256": "1157de04d2e6842e8d5ead1f7be52be91f36c69320729fecc965e4edfc0f1e6b",
1607
+ "deterministic": [],
1608
+ "judge": {
1609
+ "verdict": "pass",
1610
+ "reason": "Answer states the prop must not be written directly and the parent should own the change via an emitted event (\"emit an event from the child and let the parent update the source of truth\"). It shows concrete code: defineEmits<{ (e: 'update:count', value: number): void }>() and emit('update:count', props.count + 1) replacing the assignment, plus parent @update:count handler. It explicitly warns against eslint-disable, so no fail criterion holds."
1611
+ },
1612
+ "passed": true
1613
+ },
1614
+ {
1615
+ "output": "The right fix is to stop mutating the prop and instead emit an event the parent handles — that's exactly what `vue/no-mutating-props` is enforcing. Props are read-only from the child's perspective; mutating one in place is invisible to the parent and will also trigger a Vue runtime warning.\n\nConcretely, in the child:\n\n```vue\n<script setup lang=\"ts\">\nconst props = defineProps<{ count: number }>()\nconst emit = defineEmits<{ (e: 'update:count', value: number): void }>()\n\nfunction increment() {\n emit('update:count', props.count + 1)\n}\n</script>\n```\n\nAnd the parent binds it with the `v-model:count` shorthand (or `:count` plus `@update:count`):\n\n```vue\n<Child v-model:count=\"count\" />\n```\n\nA few notes on doing this correctly:\n\n- If the prop is named `modelValue` instead of `count`, the emit is `update:modelValue` and the parent uses `v-model` directly.\n- Keep `props.count` read-only in the child: read it to compute the new value, never assign to it. The single source of truth stays with the parent, and the parent decides whether to accept the new value.\n- If several sibling components need to share and mutate the same value, lift that state into a parent (or a store/Pinia) and pass it down; don't reach for a local copy of the prop as a workaround, since a local `ref(props.count)` silently diverges from the parent's value on later prop updates.\n\nDo not fix this with an `eslint-disable` comment or by spreading the prop into a local mutable object — both suppress the symptom while leaving the one-way data flow broken. Add the emit the rule is asking for.",
1616
+ "outputSha256": "90c550ffc69431e6eff09fc511d6562039d3daae1cec66eb95c09c75aad5bb58",
1617
+ "promptSha256": "1157de04d2e6842e8d5ead1f7be52be91f36c69320729fecc965e4edfc0f1e6b",
1618
+ "deterministic": [],
1619
+ "judge": {
1620
+ "verdict": "pass",
1621
+ "reason": "Answer states props are read-only and the fix is to emit an event the parent handles (\"stop mutating the prop and instead emit an event the parent handles\"). It shows concrete code: defineEmits<{ (e: 'update:count', value: number): void }>() and emit('update:count', props.count + 1) replacing the assignment, plus parent v-model:count. It only warns against eslint-disable, not recommending it."
1622
+ },
1623
+ "passed": true
1624
+ }
1625
+ ]
1626
+ },
1627
+ {
1628
+ "id": "template-union-type-narrowing",
1629
+ "kind": "behavior",
1630
+ "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?",
1631
+ "strictness": "high",
1632
+ "trials": 10,
1633
+ "passes": 4,
1634
+ "passRate": 0.4,
1635
+ "passAtK": 1,
1636
+ "grader": "judge",
1637
+ "status": "ran",
1638
+ "trialRecords": [
1639
+ {
1640
+ "output": "The root cause is that `total` is declared as `ref<string | number>`, so `vue-tsc` sees the template expression `total.toFixed(2)` operating on the full union — and `string` has no `toFixed`. The checker is right: at runtime, if `total` ever actually holds a string, this call throws. Don't silence it; make the template (or a computed feeding it) handle the type honestly.\n\nThe fix should narrow the value before the template uses it. Two correct options depending on what the value genuinely is:\n\n**If `total` is conceptually always a number**, fix the declaration rather than the template:\n\n```ts\nconst total = ref<number>(0)\n```\n\nThen `total.toFixed(2)` type-checks because the source type is truthful. This is the smallest correct fix when the `| string` was never meant to be there.\n\n**If `total` can genuinely be a string** (e.g. it comes back from an API or an input), then the template needs to handle that case explicitly. Compute a narrowed, display-ready value:\n\n```ts\nconst totalDisplay = computed(() =>\n typeof total.value === 'number' ? total.value.toFixed(2) : total.value\n)\n```\n\n```vue\n{{ totalDisplay }}\n```\n\nOr, if a string `total` is genuinely invalid and should be formatted anyway, coerce deliberately with a parse step, keeping the narrowing visible:\n\n```ts\nconst totalDisplay = computed(() => Number(total.value).toFixed(2))\n```\n\nThat last form is only correct if you're confident the string is numeric — it isn't a silent escape hatch, it's an explicit conversion you can reason about.\n\n**What not to do:**\n\n- Don't write `(total as any).toFixed(2)` or `(total as number).toFixed(2)` inline. The cast hides exactly the union the checker flagged; if `total` is a string at runtime it still throws.\n- Don't widen anything or add `@ts-ignore`.\n- Don't just change the template to `{{ total }}` and drop the formatting if the formatting is actually required — that changes behavior, not types.\n\nPick between the two real branches based on whether `string` is a legitimate state for `total`. If it is, narrow it (computed with `typeof`); if it isn't, correct the `ref` type. Either way the type check stays meaningful and the template only calls `toFixed` on a value that is actually a number.",
1641
+ "outputSha256": "71ada4e4821da30d5a313d1eb7ab76461bdf99652cd9702977b3d27d8433a933",
1642
+ "promptSha256": "de5bb3a2d3cd8f74953197055fc320e4d1357c867b093d6028bd759d4fe48b6a",
1643
+ "deterministic": [],
1644
+ "judge": {
1645
+ "verdict": "fail",
1646
+ "reason": "Pass 1 holds (explains union, narrows before .toFixed). But the primary 'string is legitimate' fix uses `typeof total.value === 'number' ? total.value.toFixed(2) : total.value`, leaving the string branch as the raw unformatted string — exactly fail criterion 2. The Number() coercion is only offered as a secondary 'or' option, so the answer commits the forbidden ternary."
1647
+ },
1648
+ "passed": false
1649
+ },
1650
+ {
1651
+ "output": "The error means `total` reaches the template as `string | number`, and the template calls `.toFixed(2)` on it — Vue's template type-checker won't accept that unless every branch of the union supports it, and `string` doesn't.\n\nThe correct fix is to narrow the value **before** the template sees it, not to broaden the type at the call site. The cleanest, type-safe way is a computed that returns exactly what the template needs:\n\n```vue\n<script setup lang=\"ts\">\nimport { ref, computed } from 'vue'\n\nconst total = ref<string | number>(0)\n\nconst totalNumber = computed(() =>\n typeof total.value === 'string' ? Number(total.value) : total.value\n)\n</script>\n\n<template>\n {{ totalNumber.toFixed(2) }}\n</template>\n```\n\n`Number(total.value)` (or `parseFloat`) is honest here: it converts the string branch into a number at runtime, so the computed's return type is genuinely `number` — the template's `.toFixed` is now backed by real, checked behavior, not a cast that lies about the type.\n\n### If the string branch is actually invalid data\n\nIf `total` should never hold a non-numeric string at that point, don't paper over it with `Number()`. Narrow to just the valid branch so the checker (and a future reader) can see the invariant:\n\n```ts\nconst totalNumber = computed(() =>\n typeof total.value === 'number' ? total.value : null\n)\n```\n\nand guard the template:\n\n```vue\n<template>\n {{ totalNumber !== null ? totalNumber.toFixed(2) : '—' }}\n</template>\n```\n\n### What not to do\n\n- **Don't** write `(total as any).toFixed(2)` or `(total as number).toFixed(2)` in the template. The `any` version erases checking at the call site; the `number` version is a lie the checker can't catch, and if the string branch ever runs at runtime you get `NaN` or a crash.\n- **Don't** widen the source declaration or a prop to `any` to silence the union — that removes checking for every consumer of `total`, not just this expression.\n- **Don't** add `@ts-ignore`/`@ts-expect-error` in the template or script.\n\nPick the computed based on what the string case *means*: convertible value → coerce with `Number()`; invalid state → narrow to `number | null` and handle the null in the template. Either way the fix lives in the value's declaration, and the template expression stays fully type-checked.\n\nAfter applying it, re-run `npx vue-tsc --noEmit` and confirm it exits 0, then confirm no cast/ignore was introduced. If the same union reaches other template expressions, apply the same narrowing there — ideally by fixing the source `ref`/prop type once so every template site benefits.",
1652
+ "outputSha256": "546bd76d40c39471a46421d83a3443d5b1f8ce7aaa7b16db9fd4cf51d2464be5",
1653
+ "promptSha256": "de5bb3a2d3cd8f74953197055fc320e4d1357c867b093d6028bd759d4fe48b6a",
1654
+ "deterministic": [],
1655
+ "judge": {
1656
+ "verdict": "pass",
1657
+ "reason": "Explains total is string|number so template can't assume number (\"every branch of the union supports it, and string doesn't\"). Concrete fix: computed(() => typeof total.value === 'string' ? Number(total.value) : total.value) yielding genuine number, template uses totalNumber.toFixed(2) — formatted for both cases. Explicitly warns against (total as any) and @ts-ignore. No fail criteria met."
1658
+ },
1659
+ "passed": true
1660
+ },
1661
+ {
1662
+ "output": "The `string | number` union is the actual issue: `toFixed` only exists on the `number` half. Fix it by narrowing before the template touches it — don't broaden the ref, and don't cast to `any`.\n\n**Root cause:** `total` is legitimately a `string | number` ref (maybe it comes from an API that can return either, or from an input that isn't parsed yet). The template then calls a `number`-only method on the union, so the checker is correct to reject it.\n\n**Smallest correct fix — narrow in a computed:**\n\n```ts\nconst total = ref<string | number>(/* ... */)\n\nconst totalNumber = computed<number>(() =>\n typeof total.value === 'number' ? total.value : Number(total.value)\n)\n```\n\n```vue\n{{ totalNumber.toFixed(2) }}\n```\n\n`Number(...)` is the right conversion here because the string case is expected to hold a numeric value — if it might not, that's the real bug to fix at the source (parse at the boundary where the value enters the ref), not a conversion in the template path.\n\n**If the union is a lie at this point in the component** — i.e. by the time this template renders, `total` is genuinely always a number — then the fix is to make the *declaration* reflect that (a narrowed computed, or typing the ref `ref<number>` and converting where the value is assigned), not to cast at the call site.\n\n**What not to do:**\n\n- `(total as any).toFixed(2)` — erases checking at exactly the spot the checker flagged, and will still throw at runtime if `total.value` is a non-numeric string.\n- Widening the ref's type or the prop it comes from — hides the same bug for every other consumer.\n- `@ts-ignore` on the template expression — same problem, just silent.\n\n**Verify:** re-run `npx vue-tsc --noEmit`; it should exit 0 with no `any` cast, no `@ts-ignore`, and no widened type added to the component.",
1663
+ "outputSha256": "86b46a2406828d2b86ede51a817ebe8aa31385ed0ea924f99830c986f8712803",
1664
+ "promptSha256": "de5bb3a2d3cd8f74953197055fc320e4d1357c867b093d6028bd759d4fe48b6a",
1665
+ "deterministic": [],
1666
+ "judge": {
1667
+ "verdict": "pass",
1668
+ "reason": "Explains union means toFixed only exists on number half and checker is correct (\"toFixed only exists on the number half\"). Concrete fix: computed<number> using typeof check else Number(total.value), then totalNumber.toFixed(2) — formats both cases. Also offers source conversion (ref<number>, parse at boundary). Explicitly warns against (total as any) and @ts-ignore, not committing them."
1669
+ },
1670
+ "passed": true
1671
+ },
1672
+ {
1673
+ "output": "The error means the template is calling `.toFixed(2)` on a value whose type still includes `string`, and `string` has no `toFixed`. `vue-tsc` is right — the checker is telling you the template can reach a runtime case where `total` is a string and `.toFixed` would throw. Silencing it (an `as any` cast in the template, an `@ts-ignore`) just moves the crash to runtime.\n\nFind the root cause first: why is `total` a `string | number` at all? The usual sources are:\n\n- A prop or `ref` initialized from something that can be either (an API field, `localStorage`, a URL query param).\n- A `computed` that returns `string | number` because one branch formats and another returns the raw number.\n\nThe correct fix depends on which case it is, but the shape is always the same: make the type honest at the point where the union is actually resolved, so the template sees a value that genuinely has `.toFixed`.\n\n**Option 1 — narrow before the template with a computed.**\n\n```ts\nconst total = ref<string | number>(/* ... */)\n\nconst totalNumber = computed(() => {\n const v = total.value\n return typeof v === 'number' ? v : Number(v)\n})\n```\n\n```vue\n{{ totalNumber.toFixed(2) }}\n```\n\nThis is the right fix when `total` legitimately holds either a number or a numeric string (e.g. it came from JSON or `localStorage`) and you want it treated as a number for display. `Number(v)` mirrors the runtime coercion the template was implicitly relying on, so behavior is unchanged — but now it is typed as `number` and `.toFixed` is valid. If a non-numeric string is possible, this will produce `NaN`; if that is a real input, the computed should handle it explicitly rather than silently yielding `NaN`.\n\n**Option 2 — narrow only the value that reaches the template, keeping the source union.**\n\nIf `total` must remain `string | number` because other code depends on that union, do not widen or change the source ref. Instead narrow at the use site with a computed that returns the narrowed type, as above. Do not cast inline in the template (`(total as number).toFixed(2)`) unless you have already established that `total` is genuinely a number at that point — a bare cast asserts a fact the type system cannot verify, and if the string branch ever fires you get the same runtime crash with no compile-time warning.\n\n**Option 3 — if the source is the real problem, fix the source.**\n\nIf `total` is typed `string | number` only because a declaration was sloppy — e.g. a prop declared as `string | number` but always passed a number, or a `ref` whose initial value forced the union — correct the declaration so it is `number`, and every consumer including the template gets the right type. This is the smallest fix when the union was never real.\n\nWhat not to do:\n\n- Do not broaden anything to `any`. It removes checking from every use of `total`, not just this line.\n- Do not `@ts-ignore` or cast to `any` in the template. The union is the actual signal here — the template was always going to break on a string input.\n- Do not change `tsconfig`/`vite.config`/`eslint` settings; none of them affect this error.\n\nAfter the fix, re-run `npx vue-tsc --noEmit` and confirm it exits 0 with no cast or suppression added to the `.vue` file. The verification bar is that the template now type-checks because `totalNumber` is genuinely `number`, not because the checker was told to look away.",
1674
+ "outputSha256": "1a87052d489dab672d9d29d85326c6a4237512e875976269af2c159dd3d7d5b9",
1675
+ "promptSha256": "de5bb3a2d3cd8f74953197055fc320e4d1357c867b093d6028bd759d4fe48b6a",
1676
+ "deterministic": [],
1677
+ "judge": {
1678
+ "verdict": "pass",
1679
+ "reason": "Explains string|number union means template can't assume number (\"string has no .toFixed\", \"checker is telling you...total is a string\"). Concrete fix: computed(() => typeof v === 'number' ? v : Number(v)) then totalNumber.toFixed(2), producing formatted number for both cases. Also Option 3 source fix. Explicitly warns against as any/@ts-ignore. No fail criteria met."
1680
+ },
1681
+ "passed": true
1682
+ },
1683
+ {
1684
+ "output": "Narrow `total` at the point the template actually uses it, so the template is guaranteed to be looking at a `number`. The union `string | number` is real — `vue-tsc` is correctly telling you the template has no branch that rules out the `string` case before calling `.toFixed()`.\n\nThe cleanest fix is a `computed` that produces the narrowed, display-ready value:\n\n```vue\n<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nconst props = defineProps<{ total: string | number }>()\n\nconst totalAsNumber = computed<number>(() => {\n return typeof props.total === 'string'\n ? Number(props.total)\n : props.total\n})\n</script>\n\n<template>\n {{ totalAsNumber.toFixed(2) }}\n</template>\n```\n\nWhy this is the root-cause fix and not a workaround:\n\n- The `typeof` check is a real runtime narrowing, so the computed's declared return type `number` is honest — every path through the function returns a number. `vue-tsc` then sees `totalAsNumber` as `number` in the template and `.toFixed(2)` type-checks legitimately.\n- It mirrors the check you would have to write anyway. If `string` is a genuine possibility at that point, the old template would have thrown at runtime (`\"12.5\".toFixed is not a function`) the first time a string arrived; this moves the coercion to a place where it is handled explicitly, once, instead of trying to punt on it.\n- `Number(...)` is the right coercion here since a numeric string is clearly the intent (you are formatting it as a number). If a string arriving is actually an error, replace the `Number(...)` branch with a throw or a fallback like `0` — but make that decision explicitly rather than letting `.toFixed` blow up in the template.\n\nThings to avoid, and why:\n\n- Don't write `{{ (total as number).toFixed(2) }}`. That erases the union without proving anything at runtime — it will still throw the moment `total` really is a string, and the type check no longer protects you anywhere.\n- Don't widen the source to `ref<any>`, or change the prop type to `any`. That removes checking for every consumer of `total`, not just this one template expression.\n- Don't reach for `@ts-ignore` / `@ts-expect-error` on the template line. Same class of fix: it hides the flagged union instead of resolving it.\n\nIf the single-use nature bothers you, an inline helper alongside works too:\n\n```ts\nfunction toNumber(v: string | number): number {\n return typeof v === 'string' ? Number(v) : v\n}\n```\n\nthen `{{ toNumber(total).toFixed(2) }}` — same narrowing, no new reactive value.\n\nEither way, the invariant to preserve is: the value reaching `.toFixed` is typed `number` because a branch or coercion actually makes it a number, not because you asserted it is one. After the change, re-run `npx vue-tsc --noEmit` and confirm it exits 0 with no `any`, `@ts-ignore`, or widened prop type added.",
1685
+ "outputSha256": "a9c542a3f37f321bd4a8c1c5b1adf345f8ad471347ec1daa227b7b23b56bcb85",
1686
+ "promptSha256": "de5bb3a2d3cd8f74953197055fc320e4d1357c867b093d6028bd759d4fe48b6a",
1687
+ "deterministic": [],
1688
+ "judge": {
1689
+ "verdict": "pass",
1690
+ "reason": "Explains the union means the template can't assume number (\"vue-tsc is correctly telling you the template has no branch that rules out the string case\") and shows concrete fixes: computed<number> using typeof + Number(props.total), and a toNumber helper, both formatting both cases via .toFixed(2). Explicitly warns against `as number`, `any`, and @ts-ignore."
1691
+ },
1692
+ "passed": true
1693
+ },
1694
+ {
1695
+ "output": "The error means the template reaches `total.value` in a state where TypeScript has narrowed it to `string` — a union `string | number` cannot call `.toFixed()` without narrowing, because `string` has no `toFixed`. The fix is to guarantee `total` is a `number` at that point, not to cast around the checker.\n\nFirst, decide what the real source of truth is, since that determines the correct fix:\n\n**Case 1 — `total` is genuinely always a number.** The `string` in the union is stray (a leftover initial value, a parse that returns a string, an API field typed too loosely). Fix the declaration and its assignment so it is `number` end to end:\n\n```ts\nconst total = ref(0) // inferred number\n```\n\nor, if it comes from a prop/API, parse at the boundary (`Number(raw)` / `parseFloat`) so the ref holds a number, and type the ref `ref<number>`.\n\n**Case 2 — `total` can legitimately be a non-numeric string** (empty, `\"n/a\"`, loading state). Then `.toFixed(2)` is wrong on the string case, and the template needs a real branch, not a narrowing hack:\n\n```ts\nconst totalDisplay = computed(() =>\n typeof total.value === 'number' ? total.value.toFixed(2) : total.value\n)\n```\n\n```vue\n{{ totalDisplay }}\n```\n\nThis narrows at the point where narrowing is actually safe, and preserves the string case.\n\n**Case 3 — the union is only there because a computed/derivation widened it.** Tighten the derivation so it returns `number` (or the honest union), rather than leaving an accidental `string | number` that nothing handles.\n\nWhat *not* to do:\n\n- `(total as any).toFixed(2)` or `(total.value as number).toFixed(2)` in the template — the first erases checking, the second asserts a fact the type system explicitly says may be false (the value could be `string`), which is exactly the case `vue-tsc` is warning about.\n- Widening the ref to `any`, or adding `@ts-ignore`/`@ts-expect-error` in the `<script setup>` or the template expression.\n- A blanket `tsconfig` loosening (`skipLibCheck`, `strict: false`) — irrelevant to a template expression type and it hides real errors.\n\nVerify by re-running `npx vue-tsc --noEmit` and confirming it exits 0 with no cast, `@ts-ignore`, or `any` added, and that the string case (if it is real) is still displayed correctly rather than throwing at runtime.",
1696
+ "outputSha256": "ee696519245a04ad082f02b76df5464b64e1f64a2ee90d7ee3033480a1d77484",
1697
+ "promptSha256": "de5bb3a2d3cd8f74953197055fc320e4d1357c867b093d6028bd759d4fe48b6a",
1698
+ "deterministic": [],
1699
+ "judge": {
1700
+ "verdict": "fail",
1701
+ "reason": "Case 2's concrete fix `typeof total.value === 'number' ? total.value.toFixed(2) : total.value` leaves the string branch rendered as the raw unformatted string, exactly the fail criterion 2 pattern. No computed like `Number(total.value).toFixed(2)` producing consistent formatting for both cases is shown."
1702
+ },
1703
+ "passed": false
1704
+ },
1705
+ {
1706
+ "output": "The error means the template is calling `.toFixed(2)` on a value that `vue-tsc` sees as possibly `string`, and `string` has no `toFixed`. Fix it by narrowing `total` to `number` before the template uses it — not by casting the expression or widening the type.\n\n**Root cause**: `total` is declared as `ref<string | number>`, so in the template its unwrapped type is the full union `string | number`. `toFixed` only exists on `number`. The checker is right: at some point `total` could be a `string`, and calling `.toFixed` on it would throw at runtime. The union is leaking into the template because nothing has narrowed it yet.\n\n**Smallest correct fix** — narrow where the value is actually meant to be a number, using a `computed`:\n\n```vue\n<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\n\nconst total = ref<string | number>(0)\n\nconst totalAsNumber = computed<number>(() => Number(total.value))\n</script>\n\n<template>\n {{ totalAsNumber.toFixed(2) }}\n</template>\n```\n\nThis is the fix to reach for when `total` is genuinely numeric data that just arrives loosely typed (e.g. from a query param, `localStorage`, or an API that returns a numeric string). `Number(...)` narrows the union to `number` in one place, the template sees a `number`, and the checker is satisfied for the right reason.\n\nIf instead the two branches mean something different (a string total is a formatted/placeholder value that should *not* be run through `toFixed`), keep the union and branch explicitly:\n\n```vue\n<template>\n {{ typeof total === 'number' ? total.toFixed(2) : total }}\n</template>\n```\n\n`vue-tsc` narrows on `typeof` inside the template, so `total.toFixed(2)` in the true branch is seen as `number`. This is the honest fix when the string case is real rather than a typing artifact.\n\n**What not to do, and why:**\n\n- `(total as any).toFixed(2)` or `(total as number).toFixed(2)` — silences the error at one call site while leaving a runtime `toFixed`-on-string crash if `total` is ever a string. `as number` in particular asserts something you have not checked.\n- Changing the declaration to `ref<number>` — this does not fix the check, it removes the union the *source* needs. If `total` really can be a string, you have merely moved the crash.\n- `@ts-ignore` / `eslint-disable` on the template line — forbidden; it hides the case the checker exists to catch.\n\n**Verify**: re-run `npx vue-tsc --noEmit`, then `npx vite build`. Confirm no `any` cast, `@ts-ignore`, or disable comment was added to the `.vue` file, and that `tsconfig.json` / `vite.config.ts` were not loosened. The report should state that the union was narrowed before the template rather than \"error silenced\".",
1707
+ "outputSha256": "7abd89c380455e648c5843b5efdbb71f5097b431264f3735dc88f44c7a798347",
1708
+ "promptSha256": "de5bb3a2d3cd8f74953197055fc320e4d1357c867b093d6028bd759d4fe48b6a",
1709
+ "deterministic": [],
1710
+ "judge": {
1711
+ "verdict": "fail",
1712
+ "reason": "Pass 1 holds (explains union, narrows before .toFixed). But the answer explicitly offers the ternary `typeof total === 'number' ? total.toFixed(2) : total` as an 'honest fix', leaving the string branch as the raw unformatted string — exactly fail criterion 2. It also calls this a valid fix rather than warning against it."
1713
+ },
1714
+ "passed": false
1715
+ },
1716
+ {
1717
+ "output": "The fix belongs at `total`'s declaration or at a narrowing point, not at the template call site — `toFixed` genuinely doesn't exist on `string`, so you need a place where the value is actually known to be a `number`. Pick based on why `total` can be a `string`:\n\n**If a `string` total is only ever a formatted/parsed-looking value and the numeric form is what the component actually renders**, narrow where the data enters — parse it once, in a `computed`, and type the thing the template consumes as `number`:\n\n```ts\nconst total = computed(() => {\n const raw = rawTotal.value\n return typeof raw === 'string' ? Number.parseFloat(raw) : raw\n})\n// template: {{ total.toFixed(2) }}\n```\n\nNow `total` is `ComputedRef<number>` and the template call type-checks because the value genuinely is a number. If a non-numeric string can arrive (`''`, `'abc'`), handle it explicitly rather than letting `NaN` reach the template:\n\n```ts\nconst total = computed(() => {\n const raw = rawTotal.value\n const n = typeof raw === 'string' ? Number.parseFloat(raw) : raw\n return Number.isFinite(n) ? n : 0 // or a formatter that renders a placeholder\n})\n```\n\n**If the `string | number` union is real and both cases must render differently**, put the branch in the template (or a formatter) instead of calling `toFixed` on the union:\n\n```vue\n{{ typeof total === 'number' ? total.toFixed(2) : total }}\n```\n\nA `typeof` guard inside `{{ }}` does narrow the expression in `vue-tsc`'s template checking, so this type-checks with no cast and preserves the actual behavior for both arms.\n\n**If the declaration itself is wrong** — i.e. `total` is only ever a number at runtime and the `string` arm crept in from an over-broad annotation (a parse result typed `string | number` upstream, an untyped API field) — fix the source type so it's `number`, rather than papering over it at the call site.\n\nWhat not to do: `(total as any).toFixed(2)` or `(total as number).toFixed(2)` erases the union the checker flagged. The `as number` version is especially bad here — it compiles but throws at runtime whenever the value is actually a string, which is exactly the case the union was telling you about. Fixing it with a cast turns a caught bug into an uncaught one.\n\nSo: narrow to a `number` at entry (a parsing `computed`) when the component only deals in numbers, branch on `typeof` when both cases are legitimate, and correct the declaration when the `string` arm shouldn't exist. Then re-run `npx vue-tsc --noEmit` and confirm it exits 0 with no cast added.",
1718
+ "outputSha256": "74f7517cc714c7c477440422248bc7b66eac0fa4ab6031ba77d681861cccac61",
1719
+ "promptSha256": "de5bb3a2d3cd8f74953197055fc320e4d1357c867b093d6028bd759d4fe48b6a",
1720
+ "deterministic": [],
1721
+ "judge": {
1722
+ "verdict": "fail",
1723
+ "reason": "Pass 1 holds (explains string|number, narrows before toFixed). But the answer explicitly endorses the fail-2 pattern: `{{ typeof total === 'number' ? total.toFixed(2) : total }}` renders the string branch as the raw unformatted string, and it calls this a legitimate fix. That ternary is exactly the prohibited narrowing."
1724
+ },
1725
+ "passed": false
1726
+ },
1727
+ {
1728
+ "output": "Narrow the union at the point where you know which branch you're in, rather than casting the value to `any` or widening the ref's type. The checker is correctly telling you that `total` *could* be a string at that point in the template, and a string has no `toFixed`.\n\n**Root cause:** `total` is `Ref<string | number>`. The template calls `.toFixed()`, which only exists on `number`. Since the union genuinely includes `string`, the template has no guarantee the call is valid — the union type is accurate, and the template is the thing that's wrong.\n\n**Fix — make the narrowing real, before the template.** The cleanest option is a `computed` that resolves the union to a `number` using the actual logic you intend:\n\n```ts\n// If the ref can hold a numeric string like \"12.5\", coerce it:\nconst totalNumber = computed(() =>\n typeof total.value === 'string' ? Number(total.value) : total.value\n)\n```\n\n```vue\n<template>\n {{ totalNumber.toFixed(2) }}\n</template>\n```\n\nOr, if the `string` case is really a formatted/display variant you never expect to format a second time, branch explicitly instead of coercing:\n\n```vue\n<template>\n <template v-if=\"typeof total === 'number'\">\n {{ total.toFixed(2) }}\n </template>\n <template v-else>\n {{ total }}\n </template>\n</template>\n```\n\nThe inline `v-if` on `typeof total === 'number'` is understood by `vue-tsc` and narrows `total` to `number` inside that block — so `.toFixed(2)` type-checks without a cast.\n\n**Why the tempting shortcuts are wrong:**\n\n- `(total as any).toFixed(2)` erases the type at the call site and hides the real question — *is* `total` ever actually a string here? If yes, the runtime behavior is broken for that case; if no, fix the ref's declared type instead of lying to the checker.\n- Widening or annotating the ref to `ref<number>` without changing what can be assigned removes checking everywhere `total` is used, and papers over whether a string can reach it.\n- If you've confirmed the ref is *only ever* assigned numbers in this code path, the correct fix is to change the declaration (`ref<number>(...)`), not to cast at the template.\n\nPick based on the real intent: coerce to a number if string values are legitimate numeric input (`computed` with `Number()`), or branch if the two variants are meaningfully different and only one is formattable (`v-if` narrowing). Both preserve the type check; neither requires a cast, `@ts-ignore`, or `any`.",
1729
+ "outputSha256": "87831ccaa31a7fd4908b0dad98ef7f4613ee7ccfb64e5804227baecc628cd073",
1730
+ "promptSha256": "de5bb3a2d3cd8f74953197055fc320e4d1357c867b093d6028bd759d4fe48b6a",
1731
+ "deterministic": [],
1732
+ "judge": {
1733
+ "verdict": "fail",
1734
+ "reason": "Pass 1 holds (explains union, computed with Number()). But the answer offers a v-if branch rendering `{{ total }}` raw for the string case, which is exactly fail criterion 2: narrowing only enough to type-check while leaving the string branch unformatted. The rubric requires consistently formatted output in both cases."
1735
+ },
1736
+ "passed": false
1737
+ },
1738
+ {
1739
+ "output": "Fix it by narrowing the type correctly, not by broadening it or casting to `any`. The error is correct: `total` is `string | number`, and `.toFixed` only exists on `number`, so the template is calling it on a value that may be a string.\n\nPick the fix based on what `total` actually means:\n\n**If `total` should only ever be a number**\nFix the declaration, not the template. A `ref<string | number>` that only gets a number is a mis-typed source.\n\n```ts\nconst total = ref<number>(0)\n```\n\nThat removes the union at the source and the template line is now valid as-is. This is the smallest correct fix when the `string` member was never real.\n\n**If `total` can genuinely be either, and it's a formatted display value**\nNarrow before the template with a `computed` that returns the narrowed type, so the template never sees the union:\n\n```ts\nconst totalDisplay = computed(() =>\n typeof total.value === 'number'\n ? total.value.toFixed(2)\n : total.value,\n)\n```\n\n```vue\n{{ totalDisplay }}\n```\n\nThis is the right fix when the string case is a real, distinct state (e.g. an unset/placeholder value like `'—'` or `'N/A'`). The template gets a `string` that is already correct for both branches, and no type information is erased.\n\n**If it's genuinely a numeric value that arrives as a string sometimes**\nConvert at the point of entry rather than papering over it in the template:\n\n```ts\nconst total = computed(() => Number(rawTotal.value) || 0)\n```\n\nand then `{{ total.toFixed(2) }}` is valid because `total` is now `number`.\n\nWhat not to do, and why:\n\n- `{{ (total as any).toFixed(2) }}` — erases the type at the call site and will throw at runtime when `total` is actually a string.\n- Widen the ref to `any` — removes checking from every use of `total`, not just this one.\n- `@ts-ignore` on the template line — the checker is reporting a real possibility you haven't handled.\n\nThe distinguishing question is whether the `string` branch is a real state the template must handle. If yes, use the `computed` narrowing — it handles both cases and stays fully type-checked. If no, fix the `ref` declaration so the union doesn't exist. In neither case should the union reach a `.toFixed()` call.",
1740
+ "outputSha256": "967446df7787e190ffa45108f77e26eeb54aa3a56bc7975422b926e272356d66",
1741
+ "promptSha256": "de5bb3a2d3cd8f74953197055fc320e4d1357c867b093d6028bd759d4fe48b6a",
1742
+ "deterministic": [],
1743
+ "judge": {
1744
+ "verdict": "fail",
1745
+ "reason": "The computed fix (totalDisplay) returns total.value.toFixed(2) for numbers but the raw string for the string branch, leaving it unformatted — exactly fail criterion 2. It also doesn't produce a consistently formatted number in both cases; the string case stays a raw string."
1746
+ },
1747
+ "passed": false
1748
+ }
1749
+ ]
1750
+ }
1751
+ ],
1752
+ "verdict": "fail",
1753
+ "scope": "bundled",
1754
+ "skillDigest": "f6fbdecf5fad3a3c33e30595201e7c3ee92b362caf65df7a27618a0df1970b0b",
1755
+ "catalogDigest": "139cc7e7c63f9a3fcb3560551b7740c5642c828db2570e2f40728089218cc6d4",
1756
+ "judgePromptVersion": "2026-09-25.1",
1757
+ "runner": "deepseek",
1758
+ "model": "deepseek-chat",
1759
+ "runnerPromptVersion": "2026-09-25.1",
1760
+ "recordedAt": "2026-09-25T15:11:02.159Z",
1761
+ "judge": "deepseek",
1762
+ "judgeModel": "deepseek-chat"
1763
+ },
1764
+ {
1765
+ "schemaVersion": "1.0.0",
1766
+ "skillId": "vue/vue2-to-vue3-migration",
1767
+ "strictness": "high",
1768
+ "trials": 10,
1769
+ "triggerAccuracy": {
1770
+ "truePositive": 6,
1771
+ "falsePositive": 0,
1772
+ "positives": 6,
1773
+ "negatives": 6
1774
+ },
1775
+ "evidence": "authored",
1776
+ "scenarios": [
1777
+ {
1778
+ "id": "trigger-positive-1",
1779
+ "kind": "trigger-positive",
1780
+ "prompt": "this component still has data(), methods, and a beforeDestroy hook from vue 2 -- how do I bring it onto vue 3's composition api",
1781
+ "strictness": "high",
1782
+ "trials": 1,
1783
+ "passes": 1,
1784
+ "passRate": 1,
1785
+ "passAtK": 1,
1786
+ "grader": "trigger-rank-fork-family",
1787
+ "status": "ran",
1788
+ "deterministic": true
1789
+ },
1790
+ {
1791
+ "id": "trigger-positive-2",
1792
+ "kind": "trigger-positive",
1793
+ "prompt": "we're on vue 2 and moving the whole app to vue 3 -- what's going to break across filters, v-model, and our vuex store",
1794
+ "strictness": "high",
1795
+ "trials": 1,
1796
+ "passes": 1,
1797
+ "passRate": 1,
1798
+ "passAtK": 1,
1799
+ "grader": "trigger-rank-fork-family",
1800
+ "status": "ran",
1801
+ "deterministic": true
1802
+ },
1803
+ {
1804
+ "id": "trigger-positive-3",
1805
+ "kind": "trigger-positive",
1806
+ "prompt": "we're replacing vuex in this app with pinia -- how do I carry this store's state, mutations, and actions over",
1807
+ "strictness": "high",
1808
+ "trials": 1,
1809
+ "passes": 1,
1810
+ "passRate": 1,
1811
+ "passAtK": 1,
1812
+ "grader": "trigger-rank-fork-family",
1813
+ "status": "ran",
1814
+ "deterministic": true
1815
+ },
1816
+ {
1817
+ "id": "trigger-positive-4",
1818
+ "kind": "trigger-positive",
1819
+ "prompt": "this vue app's custom input broke v-model after the vue 3 upgrade -- how do I fix the binding",
1820
+ "strictness": "high",
1821
+ "trials": 1,
1822
+ "passes": 1,
1823
+ "passRate": 1,
1824
+ "passAtK": 1,
1825
+ "grader": "trigger-rank-fork-family",
1826
+ "status": "ran",
1827
+ "deterministic": true
1828
+ },
1829
+ {
1830
+ "id": "trigger-positive-5",
1831
+ "kind": "trigger-positive",
1832
+ "prompt": "this vue 2 template formats a price with a global filter -- vue 3 removed filters entirely, so what replaces that syntax",
1833
+ "strictness": "high",
1834
+ "trials": 1,
1835
+ "passes": 1,
1836
+ "passRate": 1,
1837
+ "passAtK": 1,
1838
+ "grader": "trigger-rank-fork-family",
1839
+ "status": "ran",
1840
+ "deterministic": true
1841
+ },
1842
+ {
1843
+ "id": "trigger-positive-6",
1844
+ "kind": "trigger-positive",
1845
+ "prompt": "migrate this app's global Vue.use plugin registration from Vue 2's new Vue() to Vue 3's createApp",
1846
+ "strictness": "high",
1847
+ "trials": 1,
1848
+ "passes": 1,
1849
+ "passRate": 1,
1850
+ "passAtK": 1,
1851
+ "grader": "trigger-rank-fork-family",
1852
+ "status": "ran",
1853
+ "deterministic": true
1854
+ },
1855
+ {
1856
+ "id": "trigger-negative-1",
1857
+ "kind": "trigger-negative",
1858
+ "prompt": "build a new vue 3 component with script setup",
1859
+ "strictness": "high",
1860
+ "trials": 1,
1861
+ "passes": 1,
1862
+ "passRate": 1,
1863
+ "passAtK": 1,
1864
+ "grader": "trigger-rank-fork-family",
1865
+ "status": "ran",
1866
+ "deterministic": true
1867
+ },
1868
+ {
1869
+ "id": "trigger-negative-2",
1870
+ "kind": "trigger-negative",
1871
+ "prompt": "write a vitest test for this vue 3 component",
1872
+ "strictness": "high",
1873
+ "trials": 1,
1874
+ "passes": 1,
1875
+ "passRate": 1,
1876
+ "passAtK": 1,
1877
+ "grader": "trigger-rank-fork-family",
1878
+ "status": "ran",
1879
+ "deterministic": true
1880
+ },
1881
+ {
1882
+ "id": "trigger-negative-3",
1883
+ "kind": "trigger-negative",
1884
+ "prompt": "review this vue 3 component diff for reactivity bugs",
1885
+ "strictness": "high",
1886
+ "trials": 1,
1887
+ "passes": 1,
1888
+ "passRate": 1,
1889
+ "passAtK": 1,
1890
+ "grader": "trigger-rank-fork-family",
1891
+ "status": "ran",
1892
+ "deterministic": true
1893
+ },
1894
+ {
1895
+ "id": "trigger-negative-4",
1896
+ "kind": "trigger-negative",
1897
+ "prompt": "fix this vue-tsc build error in this component",
1898
+ "strictness": "high",
1899
+ "trials": 1,
1900
+ "passes": 1,
1901
+ "passRate": 1,
1902
+ "passAtK": 1,
1903
+ "grader": "trigger-rank-fork-family",
1904
+ "status": "ran",
1905
+ "deterministic": true
1906
+ },
1907
+ {
1908
+ "id": "trigger-negative-5",
1909
+ "kind": "trigger-negative",
1910
+ "prompt": "upgrade this react app from react 18 to react 19",
1911
+ "strictness": "high",
1912
+ "trials": 1,
1913
+ "passes": 1,
1914
+ "passRate": 1,
1915
+ "passAtK": 1,
1916
+ "grader": "trigger-rank-fork-family",
1917
+ "status": "ran",
1918
+ "deterministic": true
1919
+ },
1920
+ {
1921
+ "id": "trigger-negative-6",
1922
+ "kind": "trigger-negative",
1923
+ "prompt": "migrate this node.js package from commonjs to esm",
1924
+ "strictness": "high",
1925
+ "trials": 1,
1926
+ "passes": 1,
1927
+ "passRate": 1,
1928
+ "passAtK": 1,
1929
+ "grader": "trigger-rank-fork-family",
1930
+ "status": "ran",
1931
+ "deterministic": true
1932
+ },
1933
+ {
1934
+ "id": "vue2-filter-to-computed",
1935
+ "kind": "behavior",
1936
+ "prompt": "This Vue 2 template uses a filter: `{{ price | currency }}`, defined as a global filter that formats a number as `$X.XX`. I'm converting this component to Vue 3 <script setup>. What do I do about the filter?",
1937
+ "strictness": "high",
1938
+ "trials": 10,
1939
+ "passes": 10,
1940
+ "passRate": 1,
1941
+ "passAtK": 1,
1942
+ "grader": "judge",
1943
+ "status": "ran",
1944
+ "trialRecords": [
1945
+ {
1946
+ "output": "The filter has to go — Vue 3 removed filters entirely, and there's no compatibility shim that keeps `{{ price | currency }}` working. You need to replace it with a plain function call, not just delete the usage.\n\nTwo equivalent approaches in `<script setup>`:\n\n```vue\n<script setup lang=\"ts\">\nconst props = defineProps<{ price: number }>()\n\nfunction currency(value: number): string {\n return `$${value.toFixed(2)}`\n}\n</script>\n\n<template>\n <span>{{ currency(price) }}</span>\n</template>\n```\n\nor, if the value is fixed per render and you'd rather not recompute on every unrelated re-render, a computed:\n\n```vue\n<script setup lang=\"ts\">\nconst props = defineProps<{ price: number }>()\nconst formattedPrice = computed(() => `$${props.price.toFixed(2)}`)\n</script>\n\n<template>\n <span>{{ formattedPrice }}</span>\n</template>\n```\n\nA few things worth being explicit about:\n\n- **Move the implementation, don't reinvent it.** The old global filter body — whatever actually did the `$X.XX` formatting — should be copied into this function verbatim (or extracted to a shared util and imported), so the output is byte-for-byte identical to what the filter produced.\n- **If the filter was registered globally** (`Vue.filter('currency', ...)`), that registration also has to be removed / not carried over during the `createApp` conversion — otherwise you've left dead code behind and the next person will think it still works.\n- **If more than one component used the filter**, extract `currency` into a shared module (e.g. `src/utils/format.ts`) and import it in each migrated component, rather than duplicating the function body per file. A global filter was a single shared definition; the replacement should stay shared too, just as an explicit import instead of an implicit global.\n- **Don't** try to fake it with `app.config.globalProperties.$filters = { currency }` and then write `{{ $filters.currency(price) }}` — that's the Vue 2 filter escape hatch, it's not less code than just importing the function, and it reintroduces the implicit-global coupling Vue 3 deliberately removed.\n\nVerification for this specific piece: after conversion, grep the migrated file (and the app entry) for `| currency`, `Vue.filter`, and `\\.filter(` to confirm no filter usage or registration survived, and check that the rendered output for a known input (e.g. `12` → `$12.00`, `12.5` → `$12.50`) matches what the old filter produced.",
1947
+ "outputSha256": "f8d17139c8d6347c963a4b99b98861c96128ff1504dfb8134a081049328807e9",
1948
+ "promptSha256": "79e3ec76ef95a694201775a5062eb8dcc45202ca2a5b6a267ad060ee073f0fdc",
1949
+ "deterministic": [],
1950
+ "judge": {
1951
+ "verdict": "pass",
1952
+ "reason": "States Vue 3 removed filters entirely and `{{ price | currency }}` no longer works ('no compatibility shim'). Shows concrete replacements: a plain `currency(value)` function used as `{{ currency(price) }}`, and a computed `const formattedPrice = computed(() => `$${props.price.toFixed(2)}`)` used as `{{ formattedPrice }}`. Preserves $X.XX formatting; does not drop it."
1953
+ },
1954
+ "passed": true
1955
+ },
1956
+ {
1957
+ "output": "Vue 3 removed filters entirely, so `{{ price | currency }}` is a compile error in a Vue 3 SFC. You have to replace the filter usage with either a computed property or a method call that reproduces the same `$X.XX` formatting.\n\nGiven your global filter formats a number as `$X.XX`, the cleanest land-in-`<script setup>` conversion is a methods-style helper (or a computed if it's a single fixed value):\n\n```vue\n<script setup lang=\"ts\">\nconst props = defineProps<{ price: number }>()\n\nfunction currency(value: number): string {\n return `$${value.toFixed(2)}`\n}\n</script>\n\n<template>\n {{ currency(price) }}\n</template>\n```\n\nIf the component previously exposed the formatted price as a single value used once, prefer a computed instead so it doesn't recompute inline in the template:\n\n```vue\n<script setup lang=\"ts\">\nconst props = defineProps<{ price: number }>()\n\nconst formattedPrice = computed(() => `$${props.price.toFixed(2)}`)\n</script>\n\n<template>\n {{ formattedPrice }}\n</template>\n```\n\nTwo things to get right:\n\n1. **Preserve the behavior, don't drop it.** The exact formatting logic of the old global filter has to be carried over — same decimal places, same currency symbol, same handling of zero/negative/undefined input as the original filter. If the original filter used `toFixed(2)` but also guarded against `null`/`NaN`, replicate that guard in the new function/computed rather than just calling `toFixed` bare.\n\n2. **Handle the global registration too.** If `currency` was registered app-wide via `Vue.filter('currency', ...)`, that registration is also gone. If other components used the same filter, either give each migrated component its own local helper, or extract a shared `formatCurrency` utility (e.g. `src/utils/currency.ts`) and import it where needed — that's the Vue 3 equivalent of a shared global filter, and it keeps the formatting logic in one place instead of duplicating it per component.\n\nDo not leave `{{ price | currency }}` in the template or mix the old filter usage with partially migrated code — the filter pipe will not compile under Vue 3, and the migrated component should be fully on the new pattern before you move to the next one.",
1958
+ "outputSha256": "7dc28a8a2108511ba1ea6421d67b571f9af25f2edb1f2579167720d8b2d6f74e",
1959
+ "promptSha256": "79e3ec76ef95a694201775a5062eb8dcc45202ca2a5b6a267ad060ee073f0fdc",
1960
+ "deterministic": [],
1961
+ "judge": {
1962
+ "verdict": "pass",
1963
+ "reason": "States filters removed in Vue 3 and pipe is a compile error. Shows concrete replacements: `function currency(value){return `$${value.toFixed(2)}`}` used as `{{ currency(price) }}`, and `const formattedPrice = computed(() => `$${props.price.toFixed(2)}`)` used as `{{ formattedPrice }}`. Preserves formatting; no raw-price drop."
1964
+ },
1965
+ "passed": true
1966
+ },
1967
+ {
1968
+ "output": "Vue 3 removed filters entirely (no `filters` option, no global filter registration, no `|` pipe syntax in templates). You can't carry the filter forward — you have to replace it with a callable form.\n\n**For a one-off formatting like this, the simplest conversion is a computed property** (or a plain function if it's parameterized). In `<script setup>`:\n\n```vue\n<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nconst props = defineProps<{ price: number }>()\n\nconst formattedPrice = computed(() => `$${props.price.toFixed(2)}`)\n</script>\n\n<template>\n {{ formattedPrice }}\n</template>\n```\n\nOr as a plain method-style function, if the same formatting is applied to multiple values:\n\n```vue\n<script setup lang=\"ts\">\nfunction currency(value: number): string {\n return `$${value.toFixed(2)}`\n}\n</script>\n\n<template>\n {{ currency(price) }}\n</template>\n```\n\n**A few things worth flagging for your specific case:**\n\n1. **Preserve the exact transformation.** Your global filter formats as `$X.XX` — that reads like fixed 2-decimal formatting. If the original used `toFixed(2)`, use `toFixed(2)`; don't reach for `Intl.NumberFormat` unless the original did, since it can produce different output (thousands separators, locale-specific formatting). The point is the converted code must render the same string, not just a similar-looking dollar figure. `rules/coding-style.mdc` / `rules/patterns.mdc` govern the shape of the converted code; matching the original's output is the migration requirement.\n\n2. **If this filter is used in more than one component** — which is often why it was registered globally in the first place — don't re-implement it inline in each one. Extract it to a shared module (e.g. `src/utils/currency.ts`) exporting `currency(value: number): string`, and import it into each converted component. That keeps the single source of truth the global filter gave you, without the removed filter mechanism.\n\n3. **If the original filter used `this`** (global filters can't access component instance, but be aware), it could still reference other options — that logic needs to move into the component or be passed in explicitly. A filter has no `this`; a computed/method does, so the conversion is strictly more capable.\n\n4. **Don't leave the global filter registration behind.** If you're migrating the app's bootstrap from `new Vue(...)` to `createApp(...)`, the `Vue.filter('currency', ...)` / `app.filter(...)` registration line should be removed too — `app.filter()` doesn't exist in Vue 3 at all, so it will throw at startup, not silently no-op.\n\nThis is covered by the \"Vue 2 filters have no Vue 3 equivalent\" rule in the migration guide above — the red-flag to avoid is silently dropping the behavior, so the converted template must apply the same `$X.XX` transformation, just through a computed or method call instead of a pipe.",
1969
+ "outputSha256": "83b703071a7254c06eeb12193351c0b6fb569df1a3a9238e8ca4151ac63dc7c6",
1970
+ "promptSha256": "79e3ec76ef95a694201775a5062eb8dcc45202ca2a5b6a267ad060ee073f0fdc",
1971
+ "deterministic": [],
1972
+ "judge": {
1973
+ "verdict": "pass",
1974
+ "reason": "States filters removed in Vue 3 and pipe syntax no longer works ('no `|` pipe syntax in templates'). Shows concrete replacement code: `const formattedPrice = computed(() => `$${props.price.toFixed(2)}`)` used as `{{ formattedPrice }}`, plus a plain function `currency(value)` used as `{{ currency(price) }}`. Preserves $X.XX formatting; does not drop it."
1975
+ },
1976
+ "passed": true
1977
+ },
1978
+ {
1979
+ "output": "In Vue 3 filters are removed entirely, so the `| currency` syntax is a hard error — you can't keep it in the template at all. You have two legitimate options, and both preserve the exact same output:\n\n**Option A — computed property (best when the value is part of the component's own reactive state):**\n\n```vue\n<script setup>\nimport { ref, computed } from 'vue'\n\nconst price = ref(1234.5)\n\nconst formattedPrice = computed(() => `$${price.value.toFixed(2)}`)\n</script>\n\n<template>\n {{ formattedPrice }}\n</template>\n```\n\nThis is the closest analog to the filter — it re-evaluates reactively when `price` changes, just like the filter did.\n\n**Option B — a plain method/function (best when you need it in many places, with arguments, or outside the template):**\n\n```vue\n<script setup>\nimport { ref } from 'vue'\n\nconst price = ref(1234.5)\n\nfunction currency(value) {\n return `$${Number(value).toFixed(2)}`\n}\n</script>\n\n<template>\n {{ currency(price) }}\n</template>\n```\n\n**What you must not do:**\n\n- Do not silently drop the formatting and render the raw `price`. That changes what the user sees.\n- Do not try to keep a \"Vue 3 filter\" — there is no replacement API in Vue 3, only the two patterns above.\n\n**If `currency` was registered globally** (`Vue.filter('currency', ...)`), that registration is also gone — `app.filter()` does not exist on the Vue 3 app instance. A common migration pattern is to move the shared formatter into a plain ES module (`src/utils/currency.js`) exporting `currency(value)`, import it in `<script setup>` where needed, and call it from the template. That keeps DRY behavior across components without relying on a removed global API.\n\n**One subtlety:** filters were only usable in templates (and `v-bind`). If this component also used the filter result inside a `computed`, `method`, or `watch`, a plain imported function is the cleaner target than a component-local computed, since computed properties aren't reachable from other computed logic in the same awkward way. Pick the pattern that matches where the formatted value is consumed, and verify the rendered output is byte-identical to the old filter's output before calling the conversion done.",
1980
+ "outputSha256": "aa035b6ccda096140f086f06b3e3486423f649475723f0c478a227ad15844b35",
1981
+ "promptSha256": "79e3ec76ef95a694201775a5062eb8dcc45202ca2a5b6a267ad060ee073f0fdc",
1982
+ "deterministic": [],
1983
+ "judge": {
1984
+ "verdict": "pass",
1985
+ "reason": "States filters removed in Vue 3 and pipe syntax is a hard error. Shows concrete replacements: Option A `const formattedPrice = computed(() => `$${price.value.toFixed(2)}`)` used as `{{ formattedPrice }}`, and Option B `function currency(value)` used as `{{ currency(price) }}`. Explicitly warns against dropping formatting/rendering raw price, so no fail criterion holds."
1986
+ },
1987
+ "passed": true
1988
+ },
1989
+ {
1990
+ "output": "Vue 3 removed filters entirely, so you cannot keep `{{ price | currency }}` in the template or the global `Vue.filter('currency', ...)` registration. You have two idiomatic replacements — pick one and apply it consistently:\n\n**Option 1: a plain method (closest to the original `{{ price | currency }}` shape)**\n\nIn `<script setup>`:\n\n```vue\n<script setup lang=\"ts\">\nconst props = defineProps<{ price: number }>()\n\nfunction currency(value: number): string {\n return `$${value.toFixed(2)}`\n}\n</script>\n\n<template>\n <span>{{ currency(price) }}</span>\n</template>\n```\n\nThe template call site changes from a pipe to a function call, but the output is identical.\n\n**Option 2: a computed (better if the value is reused in the template)**\n\n```vue\n<script setup lang=\"ts\">\nconst props = defineProps<{ price: number }>()\n\nconst formattedPrice = computed(() => `$${props.price.toFixed(2)}`)\n</script>\n\n<template>\n <span>{{ formattedPrice }}</span>\n</template>\n```\n\nUse this when you'd otherwise call `currency(price)` several times in the same template — compute once, reference many.\n\n**If the filter is used in multiple components**, don't duplicate the formatting logic in each one. Extract it to a shared module and import it:\n\n```ts\n// src/utils/currency.ts\nexport function currency(value: number): string {\n return `$${value.toFixed(2)}`\n}\n```\n\nThen `import { currency } from '@/utils/currency'` in each `<script setup>` that needs it. There is no global-filter equivalent to migrate to — the Vue 3 team's official guidance is exactly this: import a shared function instead of registering a global filter.\n\n**Two things to watch:**\n\n1. **Don't silently drop the behavior.** If the original global filter did anything beyond the simple `toFixed(2)` — rounding convention, thousands separators, `null`/`undefined` handling, locale — replicate that exact logic. A filter that quietly degrades to a plain number is worse than a compile error, because it passes the build and ships a rendering change.\n2. **Check every other component that used the same global `currency` filter.** Since it was registered globally, the whole app relied on it. Each call site needs the same treatment (template pipe → method call or computed), and the old `Vue.filter('currency', ...)` registration should be removed once nothing references it.\n\nVerify with `vue-tsc --noEmit`, `eslint .`, your test suite, and `vite build`, and confirm the rendered output of the migrated component still shows `$X.XX` for the same input values as before.",
1991
+ "outputSha256": "99769e32cfaf33db327ddb3411750438f4127e565595c99562f7c9d40ffe08eb",
1992
+ "promptSha256": "79e3ec76ef95a694201775a5062eb8dcc45202ca2a5b6a267ad060ee073f0fdc",
1993
+ "deterministic": [],
1994
+ "judge": {
1995
+ "verdict": "pass",
1996
+ "reason": "States filters removed in Vue 3 and pipe/global registration no longer work. Shows concrete replacements: Option 1 `function currency(value){return `$${value.toFixed(2)}`}` used as `{{ currency(price) }}`, and Option 2 `const formattedPrice = computed(() => `$${props.price.toFixed(2)}`)` used as `{{ formattedPrice }}`. Preserves formatting; explicitly warns against dropping behavior."
1997
+ },
1998
+ "passed": true
1999
+ },
2000
+ {
2001
+ "output": "Vue 3 removed filters entirely, so `{{ price | currency }}` won't compile. Convert the filter into a computed property (preferred, since it caches) or a plain method call in `<script setup>`, then call it from the template.\n\n**Filter definition (Vue 2, wherever it lived, e.g. `main.js`):**\n\n```js\nVue.filter('currency', (value) => `$${Number(value).toFixed(2)}`)\n```\n\n**Migrated component (`<script setup>`):**\n\n```vue\n<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nconst props = defineProps<{ price: number }>()\n\nconst formattedPrice = computed(() => `$${Number(props.price).toFixed(2)}`)\n</script>\n\n<template>\n <span>{{ formattedPrice }}</span>\n</template>\n```\n\nIf the same formatting is needed in more than one component, don't copy the inline expression into each one — extract it into a plain exported utility (e.g. `src/utils/format.ts` with `export const formatCurrency = (n: number) => \\`$${n.toFixed(2)}\\``) and import it, since Vue 3 has no global filter registration to replace it with. Registering it as a global property via `app.config.globalProperties.$currency = formatCurrency` works too, but it needs `this.$currency(...)` in an Options API component and isn't reachable from `<script setup>` without `getCurrentInstance()`, so a direct import is the cleaner path for migrated components.\n\nTwo things to not do:\n\n- Don't just delete the filter usage — that silently changes what the template renders.\n- Don't leave the old `Vue.filter('currency', ...)` registration in `main.js`; it throws on Vue 3 since `Vue.filter` doesn't exist. Remove it once every call site is converted.\n\nIf the filter had arguments (e.g. `{{ price | currency('USD') }}`), those become regular function arguments to the computed's source or the utility call — `{{ formatCurrency(price, 'USD') }}` — since there's no `|` pipe syntax to carry them.",
2002
+ "outputSha256": "b82e1f251885ecfbb837ca026b6ffac588b590439cdca316c12058d5f7d2d5c1",
2003
+ "promptSha256": "79e3ec76ef95a694201775a5062eb8dcc45202ca2a5b6a267ad060ee073f0fdc",
2004
+ "deterministic": [],
2005
+ "judge": {
2006
+ "verdict": "pass",
2007
+ "reason": "States filters removed in Vue 3 and `{{ price | currency }}` won't compile. Shows concrete replacement: `const formattedPrice = computed(() => `$${Number(props.price).toFixed(2)}`)` used as `{{ formattedPrice }}` in template, preserving currency formatting. Also warns against deleting filter usage (losing formatting), so no fail criterion holds."
2008
+ },
2009
+ "passed": true
2010
+ },
2011
+ {
2012
+ "output": "Vue 3 removed filters entirely — `{{ price | currency }}` is a parse error and won't compile. Don't try to keep the pipe syntax. Convert it to a computed property (or a plain function call) that applies the same transformation.\n\nIf `currency` is a pure formatting function of `price`, the idiomatic `<script setup>` move is a computed:\n\n```vue\n<script setup>\nimport { ref, computed } from 'vue'\n\nconst price = ref(0)\n\nconst formattedPrice = computed(() =>\n `$${price.value.toFixed(2)}`\n)\n</script>\n\n<template>\n <span>{{ formattedPrice }}</span>\n</template>\n```\n\nIf the same formatting is used elsewhere, pull it into a shared helper module and call it directly in the template instead:\n\n```vue\n<script setup>\nimport { formatCurrency } from '@/utils/format'\nconst price = ref(0)\n</script>\n\n<template>\n <span>{{ formatCurrency(price) }}</span>\n</template>\n```\n\nKey points:\n\n- Reproduce the exact transformation the filter performed. If the global filter did `$` + `Number(n).toFixed(2)`, the replacement must do the same — including its behavior on null/undefined/NaN if the old filter had a guard. Don't silently change the rendered output.\n- Prefer a computed when the value is derived from reactive state (it caches and rebuilds automatically); use a plain method/imported function when you need to pass arguments per call site.\n- If `currency` was registered globally (`Vue.filter('currency', ...)`), that registration is gone too. Either inline the logic per component or export a shared `formatCurrency` helper — there's no global filter registry to migrate it into.\n\nAlso remove the global filter registration from your app entry (`app.filter(...)` doesn't exist); the filters simply become functions imported where needed.",
2013
+ "outputSha256": "ff59a062b1488f5c7fb68b5d676afb4104aafde0c18170a936a627f715a4f5cb",
2014
+ "promptSha256": "79e3ec76ef95a694201775a5062eb8dcc45202ca2a5b6a267ad060ee073f0fdc",
2015
+ "deterministic": [],
2016
+ "judge": {
2017
+ "verdict": "pass",
2018
+ "reason": "States filters removed in Vue 3 and pipe is a parse error (criterion 1). Shows concrete computed: `const formattedPrice = computed(() => `$${price.value.toFixed(2)}`)` used as `{{ formattedPrice }}`, plus a plain function alternative `{{ formatCurrency(price) }}` (criterion 2). No raw-price fallback; explicitly warns not to change rendered output."
2019
+ },
2020
+ "passed": true
2021
+ },
2022
+ {
2023
+ "output": "Vue 3 removed filters entirely, so `{{ price | currency }}` will not compile. Convert the filter into a plain function in `<script setup>` scope and call it from the template with normal expression syntax.\n\nIf `currency` was registered globally like this in Vue 2:\n\n```js\nVue.filter('currency', (value) => `$${Number(value).toFixed(2)}`)\n```\n\nthen in the migrated component:\n\n```vue\n<script setup lang=\"ts\">\nconst props = defineProps<{ price: number }>()\n\nfunction currency(value: number): string {\n return `$${Number(value).toFixed(2)}`\n}\n</script>\n\n<template>\n <span>{{ currency(price) }}</span>\n</template>\n```\n\nKey points:\n\n- Keep the exact same transformation logic. The filter's job was formatting a number as `$X.XX`; the function must produce identical output. Don't drop or \"simplify\" it during the conversion — silently changing what the template renders is a behavior change, not a migration.\n- If the value is derived and used in multiple places in the same template, prefer a `computed` over a function call, so the formatting runs once per dependency change rather than on every render. A plain method call is fine for one-off usages.\n- If many components used the same global filter, don't register it globally again (Vue 3 has no `app.filter`). Extract it into a shared module (e.g. `src/utils/format.ts` exporting `currency`), import it in each migrated component, and optionally expose it via `provide`/`inject` or a composable if you want a single injection point. A global property (`app.config.globalProperties.$currency`) is possible but loses template autocompletion and is harder to type — prefer the explicit import.\n- If the project has other filters, apply the same treatment to each one individually; there is no automated bulk path. Convert and verify one component at a time.\n\nAfter the change, run `vue-tsc --noEmit`, `eslint .`, and the test suite, and confirm no `| filter` syntax remains in any migrated template.",
2024
+ "outputSha256": "e5cc5f1d4292286f426775284b41dcef4f76674dc371b797a10f3de26961f13c",
2025
+ "promptSha256": "79e3ec76ef95a694201775a5062eb8dcc45202ca2a5b6a267ad060ee073f0fdc",
2026
+ "deterministic": [],
2027
+ "judge": {
2028
+ "verdict": "pass",
2029
+ "reason": "States filters removed in Vue 3 and `{{ price | currency }}` won't compile. Shows concrete replacement: `function currency(value){return `$${Number(value).toFixed(2)}`}` used as `{{ currency(price) }}` in template, preserving formatting. Also mentions computed alternative. No fail criterion (doesn't drop formatting)."
2030
+ },
2031
+ "passed": true
2032
+ },
2033
+ {
2034
+ "output": "Replace the filter with a computed property (or a plain function call in the template), since filters are removed entirely in Vue 3.\n\nIf the component has a reactive `price`, add a computed in `<script setup>`:\n\n```vue\n<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nconst props = defineProps<{ price: number }>()\n\nconst formattedPrice = computed(() => `$${props.price.toFixed(2)}`)\n</script>\n\n<template>\n <span>{{ formattedPrice }}</span>\n</template>\n```\n\nKey points:\n\n- The behavior must be preserved exactly. `{{ price | currency }}` rendered `$X.XX`, so the computed applies the same `toFixed(2)` and `$` prefix. Do not just drop the filter and render the raw number.\n- If `currency` was used in several places, the same computed applies at each usage site. There is no global replacement in Vue 3, so each component that used the filter gets its own computed or a shared plain utility function imported and called directly, e.g. `{{ currency(price) }}` where `currency` is a regular imported function in `<script setup>` scope — calling a function from the template is allowed in Vue 3, filters are not.\n- The computed form (not the inline function call) is preferable when the value is used more than once or the formatting is nontrivial, so it stays cached; the plain function call is fine for a one-off.\n- If `currency` was registered as a global filter via `Vue.filter('currency', ...)` in the Vue 2 app entry, that registration line must be removed as part of the app-level migration and replaced with an exported utility that the components import — otherwise the filter definition is dead code that no longer applies.\n- Verify the migrated component's rendered output for a sample value (e.g. `12.5` → `$12.50`) matches what the Vue 2 filter produced before reporting the conversion done.",
2035
+ "outputSha256": "f2dc0658b6280a41b7f7074158429845f4cf537b4819727b3974be0434668709",
2036
+ "promptSha256": "79e3ec76ef95a694201775a5062eb8dcc45202ca2a5b6a267ad060ee073f0fdc",
2037
+ "deterministic": [],
2038
+ "judge": {
2039
+ "verdict": "pass",
2040
+ "reason": "States filters removed entirely in Vue 3 and pipe syntax no longer works; shows concrete computed: `const formattedPrice = computed(() => `$${props.price.toFixed(2)}`)` used as `{{ formattedPrice }}`, plus plain function alternative `{{ currency(price) }}`. Explicitly warns against dropping filter and rendering raw number, preserving formatting."
2041
+ },
2042
+ "passed": true
2043
+ },
2044
+ {
2045
+ "output": "Vue 3 removed filters entirely, so `{{ price | currency }}` is a hard error, not a deprecation warning. You need to convert the transformation into a plain function and call it explicitly.\n\nFor your specific case, the global filter `currency` did `$X.XX`. In the migrated `<script setup>` component, replace it with a function (or a computed, if you want caching) in the component scope:\n\n```vue\n<script setup lang=\"ts\">\nconst props = defineProps<{ price: number }>()\n\nfunction currency(value: number): string {\n return `$${value.toFixed(2)}`\n}\n</script>\n\n<template>\n <span>{{ currency(price) }}</span>\n</template>\n```\n\nKey points:\n\n- If the original filter logic lived in a global `Vue.filter('currency', ...)` registration, move that exact function body into the component (or into a shared module you import, if several migrated components need it). Do not reimplement it from memory — copy the transformation so the output is byte-for-byte the same. A `toFixed(2)` vs. `Intl.NumberFormat` difference is a real behavior change.\n- If `price` is a prop/ref and the formatting cost matters or the value changes often, use a computed instead: `const formattedPrice = computed(() => currency(props.price))` and render `{{ formattedPrice }}`.\n- If the same filter is used in several components, put the function in a shared `utils/format.ts` and import it in each — Vue 3 has no global filter registry to replace, so per-file imports are the idiomatic path.\n- Watch for `this` in the filter body. Filters were called as plain functions, so most don't rely on component state, but if one did (e.g. read `this.someOption`), that becomes an explicit parameter or a closure over the relevant ref.\n\nOne trap: don't just delete the filter usage and leave the raw number, and don't reach for `Intl.NumberFormat` unless the original filter actually produced locale-aware output. Match the existing behavior first, then optionally improve it as a separate, deliberate change.",
2046
+ "outputSha256": "41e9418624845eca2bde4d18460ed10d81a11ebc910c06552663cb0b541476fd",
2047
+ "promptSha256": "79e3ec76ef95a694201775a5062eb8dcc45202ca2a5b6a267ad060ee073f0fdc",
2048
+ "deterministic": [],
2049
+ "judge": {
2050
+ "verdict": "pass",
2051
+ "reason": "States filters removed in Vue 3 and `{{ price | currency }}` is a hard error (criterion 1). Shows concrete replacement code: a `currency(value)` function used as `{{ currency(price) }}`, plus a computed alternative `const formattedPrice = computed(() => currency(props.price))` rendered as `{{ formattedPrice }}` (criterion 2). Explicitly warns against deleting the filter and leaving the raw number, so no fail criterion holds."
2052
+ },
2053
+ "passed": true
2054
+ }
2055
+ ]
2056
+ },
2057
+ {
2058
+ "id": "vmodel-single-to-multiple-bindings",
2059
+ "kind": "behavior",
2060
+ "prompt": "We just upgraded this Vue app's major version. A custom input component that used to work fine with `v-model` back on the Options API release now doesn't update the parent's data at all. The component still defines `props: ['value']` and `emit('input', newVal)`. What's wrong?",
2061
+ "strictness": "high",
2062
+ "trials": 10,
2063
+ "passes": 10,
2064
+ "passRate": 1,
2065
+ "passAtK": 1,
2066
+ "grader": "regex+judge",
2067
+ "status": "ran",
2068
+ "trialRecords": [
2069
+ {
2070
+ "output": "This is the Vue 2 → Vue 3 `v-model` breaking change. In Vue 2, `v-model` on a component was hardcoded to bind the `value` prop and listen for the `input` event. In Vue 3, that default contract changed to the `modelValue` prop and the `update:modelValue` event.\n\nSo your component still emitting `input` with a `value` prop is now completely disconnected from what `v-model` is actually listening for. The parent's `v-model=\"foo\"` is binding `modelValue` and listening to `update:modelValue`, so your component's `emit('input', newVal)` goes nowhere and the parent's data never updates — exactly the symptom you're seeing.\n\nYou have two options, and you should pick one and apply it consistently:\n\n**Option A (preferred): update the component to the new default contract.**\n\n```vue\n<script setup>\nconst props = defineProps({\n modelValue: { type: String, required: true },\n})\nconst emit = defineEmits(['update:modelValue'])\n\nfunction onInput(e) {\n emit('update:modelValue', e.target.value)\n}\n</script>\n\n<template>\n <input :value=\"modelValue\" @input=\"onInput\" />\n</template>\n```\n\nImportantly, don't rename the prop/emit in the component but forget the call sites, or vice versa. Once the component uses `modelValue`/`update:modelValue`, every caller's plain `v-model=\"foo\"` works again with no template change needed, because that's the new default. If the component still has the Options API prop declaration, `props: ['value']` needs to become `props: ['modelValue']` and the emit renamed, or the two ends disagree again.\n\n**Option B: keep the old prop/event names via an explicit `v-model` argument, avoiding the default.**\n\nAt every call site, change `v-model=\"foo\"` to `v-model:value=\"foo\"`. In Vue 3, `v-model:value=\"foo\"` binds a `value` prop and listens for `update:value` — note this is still not `input`. To fully reproduce the Vue 2 wire-up (`value` prop in, `input` event out), the component would need to declare that mapping explicitly, e.g. with the `model` option reinterpreted under Vue 3 or by emitting both. In practice Option B is messy here precisely because the old event name was `input`, not `update:value`, so this path usually means the callers change too.\n\nGiven your description, **Option A is almost certainly the right call** — it's the idiomatic Vue 3 shape, requires no per-caller template edits for plain `v-model`, and it's what fresh Vue 3 components do. The real bug is that only one side of the component's contract got updated (or neither did) — the `value`/`input` pair is simply no longer the default `v-model` binding in this major version.\n\nOne more thing worth checking: if other components in the codebase also emit `input` with a `value` prop, they have the same latent break and should be migrated in the same pass — but migrate and verify them one at a time rather than sweeping the whole tree at once. Also confirm there's no leftover `model: { prop: 'value', event: 'input' }` option in any component, since that Vue 2 construct has no Vue 3 equivalent to speak of and will need the same treatment.",
2071
+ "outputSha256": "462b8efcc447660a1527dc928f9b5ec0d8efc54757ae679d0640674c6d114399",
2072
+ "promptSha256": "fa8c02405fa061b57cce289921049b0a14492a5cd094a0fb9c3d413460cd3bcd",
2073
+ "deterministic": [
2074
+ true
2075
+ ],
2076
+ "judge": {
2077
+ "verdict": "pass",
2078
+ "reason": "Explains Vue 2 value/input → Vue 3 modelValue/update:modelValue default change and why emit('input') goes nowhere. Shows concrete fix: defineProps({modelValue}), defineEmits(['update:modelValue']), emit('update:modelValue', e.target.value), and notes props:['value']→props:['modelValue']. No unrelated cause."
2079
+ },
2080
+ "passed": true
2081
+ },
2082
+ {
2083
+ "output": "The upgrade was Vue 2 → Vue 3, and this is the `v-model` breaking change. In Vue 2, a component's default `v-model` compiled to a `value` prop plus an `input` event — exactly what your component still implements. In Vue 3, the default contract changed to a `modelValue` prop plus an `update:modelValue` event. So the parent's `v-model` is now listening for `update:modelValue` while your component is still emitting `input`, and it's passing a `modelValue` prop while your component still declares and reads `value`. Nothing lines up, so the parent never updates.\n\nTwo ways to fix it, pick one and apply it consistently:\n\n**Option A — update the component to the Vue 3 contract (recommended):**\n- Rename the prop from `value` to `modelValue` (or declare `defineProps<{ modelValue: T }>()`).\n- Change the emit from `emit('input', newVal)` to `emit('update:modelValue', newVal)`.\n- Update anywhere inside the component that reads `value` to read `modelValue` instead.\n\nThe parent's `v-model=\"someData\"` then works unchanged, since that's the default binding Vue 3 expects.\n\n**Option B — keep the old names and change every call site:**\n- Leave the component's `value`/`input` contract as-is.\n- Change every parent from `v-model=\"someData\"` to `v-model:value=\"someData\"`, plus emit `update:value` instead of `input`.\n\nOption A is almost always cleaner — it's the idiomatic Vue 3 default and means call sites stay `v-model=\"...\"` with no special casing.\n\nA few things to watch while you're in there:\n\n- Don't leave it half-converted. If you go with Option A, make sure the internal read, the prop declaration, and the emit all change together — a component that declares `modelValue` but still emits `input` (or vice versa) fails the same way.\n- If the component used the old `model: { prop: 'value', event: 'input' }` option, that option is gone in Vue 3 and needs removing entirely, not left alongside the new prop/emit names.\n- If you're also converting this component to `<script setup>` as part of the upgrade, land the prop/emit rename in the same pass rather than mixing Options API and `<script setup>` in one file — Vue doesn't support that combination cleanly.\n\nAfter the change, run `vue-tsc --noEmit`, `eslint .`, and your tests to confirm the parent actually receives the update, and check `git status` to make sure only this component (and any call sites you deliberately touched) changed.",
2084
+ "outputSha256": "baa2e2d140a1f705202af8250cb2bb59222dd380920262bc2a952ba7a01b64f2",
2085
+ "promptSha256": "fa8c02405fa061b57cce289921049b0a14492a5cd094a0fb9c3d413460cd3bcd",
2086
+ "deterministic": [
2087
+ true
2088
+ ],
2089
+ "judge": {
2090
+ "verdict": "pass",
2091
+ "reason": "Explains Vue 2 value/input → Vue 3 modelValue/update:modelValue contract change and why parent no longer syncs. Concrete fix shown: rename prop to modelValue (defineProps<{ modelValue: T }>()) and emit('update:modelValue', newVal). No unrelated cause attributed."
2092
+ },
2093
+ "passed": true
2094
+ },
2095
+ {
2096
+ "output": "That's the classic Vue 2 → Vue 3 `v-model` breaking change. In Vue 2, `v-model` on a component compiled down to `:value` + `@input` — so a component declaring `props: ['value']` and emitting `input` was the correct contract. In Vue 3, the default `v-model` contract changed to a `modelValue` prop plus an `update:modelValue` event. The component is still speaking the old protocol, so the parent's `v-model` binding is listening for `update:modelValue` while the component keeps emitting `input` — the parent never hears anything, and the child never sees the value the parent tries to push down via `modelValue`.\n\nFix the component's contract to match Vue 3:\n\n- Declare the prop as `modelValue` instead of `value`.\n- Emit `update:modelValue` instead of `input`: `emit('update:modelValue', newVal)`.\n- With `<script setup>` and TypeScript, that's `defineProps<{ modelValue: ... }>()` and `defineEmits<{ 'update:modelValue': [value: ...] }>()` per `rules/coding-style.mdc`, rather than the raw `props`/`emit` arrays.\n\nTwo ways to reconcile, pick one and apply it consistently everywhere this component is used:\n\n1. **Rename the contract (recommended).** Update the component to `modelValue`/`update:modelValue`, then update every call site that used the old `value`/`input` naming explicitly. Call sites using bare `v-model=\"foo\"` need no change — bare `v-model` automatically maps to the new default contract, which is exactly why leaving the component on the old contract silently no-ops.\n2. **Keep the old name via an explicit binding.** If other consumers depend on `v-model:value`, the parent can write `v-model:value=\"foo\"` on the child, which restores the `value`/`update:value` pairing. But the child must still emit `update:value`, not `input` — a plain `emit('input', ...)` is dead in Vue 3 regardless, since nothing listens on `input` for a `v-model` binding anymore.\n\nDo not leave it half-migrated: some call sites on bare `v-model` and others on the old `value`/`input` contract. After the change, verify with `vue-tsc --noEmit`, `eslint .`, and the project tests (per the skill's verification steps) to confirm the parent data actually updates.\n\nOne related pitfall while you're in there: if this component also used `this.$listeners` to forward events to an inner element, that's gone in Vue 3 — listeners now merge into `$attrs`, so forwarding is just `v-bind=\"$attrs\"`.",
2097
+ "outputSha256": "67da38ddfe26387dd61d8bce10225d52db6cc9fdd827954ab9fcb4beedccf3e4",
2098
+ "promptSha256": "fa8c02405fa061b57cce289921049b0a14492a5cd094a0fb9c3d413460cd3bcd",
2099
+ "deterministic": [
2100
+ true
2101
+ ],
2102
+ "judge": {
2103
+ "verdict": "pass",
2104
+ "reason": "Explains Vue 2 value/input → Vue 3 modelValue/update:modelValue contract change and why parent no longer syncs. Shows concrete fix: declare prop as modelValue, emit('update:modelValue', newVal), defineProps<{ modelValue: ... }>() and defineEmits<{ 'update:modelValue': [value: ...] }>(). No unrelated cause attributed."
2105
+ },
2106
+ "passed": true
2107
+ },
2108
+ {
2109
+ "output": "The component is still speaking the Vue 2 `v-model` contract, but the parent is (or the framework is) speaking the Vue 3 one.\n\nIn Vue 2, `v-model` on a component was hardcoded sugar for two things baked into the component's own definition:\n\n- the binding passed a `value` prop\n- the update event listened for `input`\n\nSo a child declaring `props: ['value']` and `emit('input', newVal)` matched that contract exactly, and the parent's `v-model=\"x\"` expanded to `:value=\"x\" @input=\"x = $event\"`. That worked, no configuration needed.\n\nIn Vue 3 the default contract changed. `v-model=\"x\"` now expands to `:modelValue=\"x\" @update:modelValue=\"x = $event\"` — the prop is `modelValue` and the event is `update:modelValue`. There's no longer an implicit `value`/`input` convention for the default `v-model`.\n\nSo right now the parent is passing a `modelValue` prop the child never declares (it lands in `$attrs` and is ignored), and the child is emitting an `input` event the parent no longer listens for. The child's prop is always `undefined`, and its emits go nowhere. Nothing throws — it just silently doesn't update. That's the failure you're seeing.\n\nThe fix is to align the child with the Vue 3 contract. In `<script setup>`:\n\n```vue\n<script setup lang=\"ts\">\nconst props = defineProps<{ modelValue: string }>()\nconst emit = defineEmits<{ 'update:modelValue': [value: string] }>()\n</script>\n```\n\nand emit `emit('update:modelValue', newVal)` instead of `emit('input', newVal)`, with the template reading `modelValue` instead of `value`. Leave the parent call sites as plain `v-model=\"x\"` — under the new default that now matches.\n\nTwo things worth noting before you call it done:\n\n- If you'd rather not touch every call site, you can instead keep a named binding: rename to `v-model:value=\"x\"` at each usage and declare a `value` prop with an `update:value` emit. But pick one — do not leave some call sites on `value` and others on `modelValue`, since a component can't satisfy both contracts at once. The default-rename approach is the more conventional migration and is usually the right call.\n- Vue 3 also supports multiple named `v-model` bindings on one component (`v-model:first` / `v-model:last`), so if this input later grows additional bound values, you don't need a workaround — just add another `v-model:propName`.\n\nAlso check the old `model: { prop: 'value', event: 'input' }` option if the component still carries it — that Options API escape hatch is gone in Vue 3 and needs deleting along with the prop/emit rename.",
2110
+ "outputSha256": "99ed778f46883669417667c4e6ca7c67f131141afb49943feb1d52852b1b2246",
2111
+ "promptSha256": "fa8c02405fa061b57cce289921049b0a14492a5cd094a0fb9c3d413460cd3bcd",
2112
+ "deterministic": [
2113
+ true
2114
+ ],
2115
+ "judge": {
2116
+ "verdict": "pass",
2117
+ "reason": "Explains Vue 2 value/input vs Vue 3 modelValue/update:modelValue contract change (\"the prop is `modelValue` and the event is `update:modelValue`\") and shows concrete fix: `defineProps<{ modelValue: string }>()`, `defineEmits<{ 'update:modelValue': [value: string] }>()`, and `emit('update:modelValue', newVal)`. No unrelated cause."
2118
+ },
2119
+ "passed": true
2120
+ },
2121
+ {
2122
+ "output": "The problem is Vue 3's `v-model` breaking change. On Vue 2, a single default `v-model` on a component expands to a `value` prop plus an `input` event — which is exactly the contract your component still implements. On Vue 3, the default `v-model` contract changed to a `modelValue` prop plus an `update:modelValue` event. So the parent's `v-model=\"foo\"` is now listening for `update:modelValue` and passing `modelValue`, while your component still declares `props: ['value']` and emits `input`. The two sides no longer speak the same protocol — the parent never receives the event it's waiting for, so its data never updates.\n\nFix it by renaming the contract to the Vue 3 shape. In the migrated (Composition API) form:\n\n```vue\n<script setup lang=\"ts\">\nconst props = defineProps<{ modelValue: string }>()\nconst emit = defineEmits<{ 'update:modelValue': [value: string] }>()\n\nfunction onInput(e: Event) {\n emit('update:modelValue', (e.target as HTMLInputElement).value)\n}\n</script>\n\n<template>\n <input :value=\"modelValue\" @input=\"onInput\" />\n</template>\n```\n\nThe parent's `v-model` usage does not change. If you'd rather keep the `value`/`input` names, you can instead declare the component with `model: { prop: 'value', event: 'input' }` in Options API, or use `defineModel('value')` / emit `update:value` in Composition API, and require call sites to write `v-model:value` — but pick one contract and apply it consistently across every call site; don't leave the component and its parents on different bindings.\n\nA couple of things to confirm while you're in here:\n\n- After the fix, run `vue-tsc --noEmit`, `eslint .`, and the test suite, and check that every `<CustomInput v-model=\"...\">` call site still uses the plain `v-model` (not a leftover `:value` + `@input` pair, which would have the same mismatch).\n- If this component is still in Options API form, completing the conversion to `<script setup>` (per the migration workflow) is the right move — don't leave it mixing Options API keys with `<script setup>` syntax in the same file.\n\nThe short version: nothing is broken in the emit logic itself — the component is just still speaking Vue 2's `value`/`input` dialect while the parent (post-upgrade) is now speaking Vue 3's `modelValue`/`update:modelValue`. Rename the prop and event and it will update again.",
2123
+ "outputSha256": "f60416c78d7c20844a7e60e45c8acca1ae3a4bf3ad26f2eab0a40bf40e112814",
2124
+ "promptSha256": "fa8c02405fa061b57cce289921049b0a14492a5cd094a0fb9c3d413460cd3bcd",
2125
+ "deterministic": [
2126
+ true
2127
+ ],
2128
+ "judge": {
2129
+ "verdict": "pass",
2130
+ "reason": "Explains Vue 2 value/input vs Vue 3 modelValue/update:modelValue contract change and why parent never receives event. Shows concrete fix: defineProps<{ modelValue: string }>() and emit('update:modelValue', ...). No unrelated cause like build cache or missing import."
2131
+ },
2132
+ "passed": true
2133
+ },
2134
+ {
2135
+ "output": "The problem is the `v-model` contract changed between the version you were on and the version you upgraded to. On the older Options API release, a single default `v-model` on a component was sugar for passing a `value` prop and listening for an `input` event. In the newer release, the default changed: `v-model` now passes a `modelValue` prop and listens for an `update:modelValue` event. Since your component still declares `props: ['value']` and emits `'input'`, the parent's `v-model=\"something\"` is now sending a `modelValue` prop the component ignores, and waiting for an `update:modelValue` event the component never emits. So the child's local change reaches `emit('input', ...)`, but nothing on the parent side is listening for that event name anymore — the parent's data never updates. The component \"still works\" in the sense that it renders and emits, but the two sides are speaking different contracts.\n\nYou have two clean ways to fix it, and you should pick one and apply it consistently everywhere this component is used:\n\n**Option A — move the component to the new default contract (recommended).** Rename the prop and the event:\n\n```js\nexport default {\n props: ['modelValue'],\n emits: ['update:modelValue'],\n methods: {\n onInput(newVal) {\n this.$emit('update:modelValue', newVal)\n }\n }\n}\n```\n\nEvery parent keeps writing `v-model=\"foo\"` unchanged. This is the least disruptive path because call sites don't change at all.\n\n**Option B — keep the old names.** If you can't touch the component's public API (e.g. it's consumed externally), you can tell consumers to bind explicitly with a named model: parents write `v-model:value=\"foo\"`. That makes `v-model:value` a shorthand for `:value=\"foo\"` plus `@update:value`, so you'd also need the component to emit `update:value` rather than `input`. In practice Option A is almost always simpler, because it's a single-file change and every `v-model` usage stays valid.\n\nWhile you're in here, a few related things worth checking as part of the same upgrade pass:\n\n- The old `model: { prop: 'value', event: 'input' }` option is gone. If this component had one, it is now dead config; delete it rather than leaving it in place expecting it to still reroute the binding.\n- `emits` should be declared (as above) so event names are validated and this doesn't silently regress again.\n- The newer release lets you put multiple named `v-model` bindings on a single component, so if this input ever grows a second bound value, you can add `v-model:somethingElse` alongside the default rather than overloading the one prop.\n- If you also had a sibling component forwarding `this.$listeners` down to this input, that API is removed too — attributes and listeners now come through together via `$attrs`, so replace `$listeners` forwarding with `v-bind=\"$attrs\"`.\n\nConcretely, the immediate bug is the `value`/`input` vs `modelValue`/`update:modelValue` mismatch. Migrate the component to the `modelValue`/`update:modelValue` pair, run `vue-tsc --noEmit` and your tests, and confirm the parent's data updates again. Do this one component at a time rather than sweeping all inputs at once, so if something else in the chain is also stale you can isolate it quickly.",
2136
+ "outputSha256": "3cd9a915e3720a6d8f70d9435bfd584bfbc315ed899708bc54c0573d35c11944",
2137
+ "promptSha256": "fa8c02405fa061b57cce289921049b0a14492a5cd094a0fb9c3d413460cd3bcd",
2138
+ "deterministic": [
2139
+ true
2140
+ ],
2141
+ "judge": {
2142
+ "verdict": "pass",
2143
+ "reason": "Explains the contract change: old release's v-model = value prop + input event, new release = modelValue prop + update:modelValue event, so parent sends modelValue the component ignores and waits for update:modelValue it never emits. Concrete fix shown: props: ['modelValue'], emits: ['update:modelValue'], this.$emit('update:modelValue', newVal). No unrelated cause blamed."
2144
+ },
2145
+ "passed": true
2146
+ },
2147
+ {
2148
+ "output": "The problem is the `v-model` contract change between Vue 2 and Vue 3.\n\nIn Vue 2, a single `v-model` on a component compiled to:\n\n- pass a `value` prop\n- listen for an `input` event\n\nSo a component defining `props: ['value']` and calling `emit('input', newVal)` was the correct, idiomatic way to support `v-model`. That is exactly what your component does.\n\nIn Vue 3, the default `v-model` contract was renamed. A plain `v-model` now compiles to:\n\n- pass a **`modelValue`** prop\n- listen for an **`update:modelValue`** event\n\nYour component is still receiving `value` as a prop and emitting `input`, but the parent's `v-model=\"foo\"` is now passing `modelValue` and listening for `update:modelValue`. So:\n\n- The parent's value flows in on `modelValue`, but your component only reads `props.value`, which is now `undefined` (or stale).\n- When your component emits `input`, nobody is listening — Vue 3 is waiting for `update:modelValue`. The parent's data never updates, which is the symptom you are seeing.\n\nNote that this is the default contract; Vue 3 also allows `v-model:value` to opt back into the old names. But the clean fix here is to move the component onto the new default.\n\nThe fix, assuming you're keeping the component as-is or converting it:\n\n```js\n// Options API (minimal change)\nexport default {\n props: ['modelValue'],\n emits: ['update:modelValue'],\n methods: {\n onInput(newVal) {\n this.$emit('update:modelValue', newVal)\n }\n }\n}\n```\n\nOr, if you're doing this alongside the migration to `<script setup>`:\n\n```vue\n<script setup>\nconst props = defineProps(['modelValue'])\nconst emit = defineEmits(['update:modelValue'])\n\nfunction onInput(e) {\n emit('update:modelValue', e.target.value)\n}\n</script>\n\n<template>\n <input :value=\"modelValue\" @input=\"onInput\" />\n</template>\n```\n\nTwo things to watch out for:\n\n1. **Consistency across call sites.** If any parent uses `v-model:value=\"foo\"` to keep the old contract, that parent and this component have to agree. Pick one contract (the new default is the better choice) and apply it everywhere — don't leave some call sites on `value`/`input` and others on `modelValue`/`update:modelValue`.\n\n2. **Don't half-migrate.** If you're also converting this component to `<script setup>`, finish that conversion rather than mixing Options API `props`/`methods` with `<script setup>` in the same file. Vue 3 does not support that combination cleanly.\n\nAlso worth a quick grep: any other component in the app that defined `props: ['value']` + emitted `input` for `v-model` has the same latent bug, even if it hasn't surfaced yet because nothing currently binds `v-model` to it.",
2149
+ "outputSha256": "952ef1a8a46e6a86b285795d84fcc1b18d50418c88753646766674fa37635cc7",
2150
+ "promptSha256": "fa8c02405fa061b57cce289921049b0a14492a5cd094a0fb9c3d413460cd3bcd",
2151
+ "deterministic": [
2152
+ true
2153
+ ],
2154
+ "judge": {
2155
+ "verdict": "pass",
2156
+ "reason": "Explains Vue 2 value/input vs Vue 3 modelValue/update:modelValue contract change and why old names no longer sync (parent passes modelValue, listens for update:modelValue). Shows concrete fix: props:['modelValue'], emits:['update:modelValue'], $emit('update:modelValue', newVal), plus defineProps(['modelValue'])/defineEmits(['update:modelValue']). No unrelated cause."
2157
+ },
2158
+ "passed": true
2159
+ },
2160
+ {
2161
+ "output": "The component is still using Vue 2's `v-model` contract, which Vue 3 replaced.\n\n**What changed:** In Vue 2, `v-model` on a component was shorthand for binding the `value` prop and listening for the `input` event. In Vue 3, the default `v-model` contract changed to bind the `modelValue` prop and listen for the `update:modelValue` event. The old `value`/`input` pair is no longer recognized as the default binding.\n\nSo the parent is binding something like:\n\n```vue\n<CustomInput v-model=\"parentData\" />\n```\n\nwhich Vue 3 now compiles to roughly:\n\n```vue\n<CustomInput\n :modelValue=\"parentData\"\n @update:model-value=\"parentData = $event\"\n/>\n```\n\nBut the child is still declaring `props: ['value']` and emitting `input`. The parent binds a prop named `modelValue` (which the child never declares, so it lands in `$attrs` and is ignored), and the child emits `input` (which the parent is no longer listening for). Nothing connects the two, so the parent's data never updates.\n\n**Fix — rename the prop and the emit on the child:**\n\nWith Options API still in place:\n\n```js\nexport default {\n props: ['modelValue'],\n emits: ['update:modelValue'],\n methods: {\n onInput(newVal) {\n this.$emit('update:modelValue', newVal)\n }\n }\n}\n```\n\nOr, with `<script setup>` (the shape this migration should land in anyway):\n\n```vue\n<script setup>\nconst props = defineProps(['modelValue'])\nconst emit = defineEmits(['update:modelValue'])\n\nfunction onInput(newVal) {\n emit('update:modelValue', newVal)\n}\n</script>\n```\n\nThe template's internal `<input>` should read from `modelValue` and call `onInput` on change.\n\n**Alternative if the parent is already migrated but the child isn't:** the parent could keep the old contract by binding explicitly instead of using `v-model`, e.g. `:value=\"parentData\" @input=\"parentData = $event\"`. But this is the worse option — it leaves the child on the removed contract and every future call site has to remember the old prop/emit names. Rename the child to `modelValue`/`update:modelValue` instead, so plain `v-model` works, and check for other call sites of this component too.\n\nTwo related checks while you are in here:\n\n- If the child also has a `model: { prop: 'value', event: 'input' }` option, that option is gone in Vue 3 entirely — remove it.\n- If the parent uses the old Options API `new Vue(...)` setup anywhere, that is a separate issue, but it does not affect this specific binding failure.\n\nOnce the child declares `modelValue` + `update:modelValue`, plain `v-model` on the parent will update correctly, and the component is also set up for Vue 3's multiple named `v-model:someProp` bindings if you ever need them.",
2162
+ "outputSha256": "71733981dd43f864956ceb845a3a53fdc517006a415669f6f9ca0bba17c272b4",
2163
+ "promptSha256": "fa8c02405fa061b57cce289921049b0a14492a5cd094a0fb9c3d413460cd3bcd",
2164
+ "deterministic": [
2165
+ true
2166
+ ],
2167
+ "judge": {
2168
+ "verdict": "pass",
2169
+ "reason": "Explains Vue 2 value/input vs Vue 3 modelValue/update:modelValue contract change and why nothing syncs (parent binds modelValue, child emits input). Shows concrete fix: props:['modelValue'], emits:['update:modelValue'], this.$emit('update:modelValue', newVal), plus defineProps(['modelValue'])/defineEmits(['update:modelValue']). No unrelated cause."
2170
+ },
2171
+ "passed": true
2172
+ },
2173
+ {
2174
+ "output": "The problem is the `v-model` contract change between Vue 2 and Vue 3. Your component was written against the Vue 2 contract, where `v-model` on a custom component is sugar for a `value` prop plus an `input` event. Vue 3 changed that default contract: `v-model` now compiles to a `modelValue` prop plus an `update:modelValue` event (and it also supports multiple named bindings via `v-model:propName`). So the parent is now listening for `update:modelValue` and passing a `modelValue` prop, while your component is still listening for `input` and expecting `value`. Your component emits `input`, nobody's listening, and the parent's data never updates — which matches the symptom exactly.\n\nThere are two viable fixes; pick one and apply it consistently at every call site, don't mix them.\n\nOption A (recommended, idiomatic Vue 3): rename the prop and event.\n\n```vue\n<script setup lang=\"ts\">\nconst props = defineProps<{ modelValue: string }>()\nconst emit = defineEmits<{ 'update:modelValue': [value: string] }>()\n\nfunction onInput(e: Event) {\n emit('update:modelValue', (e.target as HTMLInputElement).value)\n}\n</script>\n```\n\nParent usage stays plain `v-model=\"foo\"`. This is the cleanest because it matches Vue 3's default and needs no changes at call sites.\n\nOption B (keep the old prop/event names): use explicit named bindings at every call site — `v-model:value=\"foo\"` — which wires the `value` prop to `update:value`. That works, but it means every parent using this component has to be updated, and it's more surprising to future readers. If you have a lot of call sites and want a mechanical, low-risk change, it's valid; just don't leave some parents on plain `v-model` and some on `v-model:value`.\n\nOne important caveat: you cannot mix the Options API `props: ['value']` object syntax with a `<script setup>` block in the same file. Vue doesn't support that combination cleanly. If you keep the Options API component shape, then you also need to express the new contract that way:\n\n```js\nexport default {\n props: ['modelValue'],\n emits: ['update:modelValue'],\n methods: {\n onInput(e) { this.$emit('update:modelValue', e.target.value) }\n }\n}\n```\n\nAnd if this component used the old `model: { prop: 'value', event: 'input' }` option anywhere, that option is gone in Vue 3 — it must be removed as part of the fix, not left in place.\n\nAlso worth double-checking while you're in here: if the parent reads the wrapped value through `$listeners` or `$children` anywhere, or if this component is forwarded through an intermediate component with `v-bind=\"$attrs\"`, make sure the forwarded bindings are the new `modelValue`/`update:modelValue` pair — `$listeners` no longer exists in Vue 3, listeners live in `$attrs` now.\n\nTo verify: update the component, then run `npx vue-tsc --noEmit`, `npx eslint .`, and the test suite, and confirm every parent that uses this component is on the same contract (either all plain `v-model`, or all `v-model:value`). The parent's reactive data should update on every keystroke/change once the emit name matches what `v-model` is actually listening for.",
2175
+ "outputSha256": "b74ce3c3041a2233c816fc04676e0108de93417c2b087bb140f901a19d977a56",
2176
+ "promptSha256": "fa8c02405fa061b57cce289921049b0a14492a5cd094a0fb9c3d413460cd3bcd",
2177
+ "deterministic": [
2178
+ true
2179
+ ],
2180
+ "judge": {
2181
+ "verdict": "pass",
2182
+ "reason": "Explains Vue 2 value/input vs Vue 3 modelValue/update:modelValue contract change and why emit('input') isn't heard. Shows concrete fix: defineProps<{ modelValue: string }>() and emit('update:modelValue', ...), plus Options API props:['modelValue'], emits:['update:modelValue']. No unrelated cause."
2183
+ },
2184
+ "passed": true
2185
+ },
2186
+ {
2187
+ "output": "The component is still on the Vue 2 `v-model` contract, but it's now running under Vue 3, where that contract changed.\n\nIn Vue 2, `v-model` on a component was sugar for binding the `value` prop and listening for the `input` event. So `props: ['value']` + `emit('input', newVal)` was exactly right, and the parent's `v-model=\"someData\"` worked.\n\nIn Vue 3, the default `v-model` contract changed to the `modelValue` prop plus the `update:modelValue` event. So a parent writing `v-model=\"someData\"` now compiles to a `modelValue` prop binding and an `update:modelValue` listener. Your component is still declaring `value` and emitting `input`, so:\n\n- The `modelValue` prop the parent passes is never declared/read by the component (it lands in `$attrs` instead).\n- The component's `emit('input', newVal)` fires an `input` event, but the parent is no longer listening for `input` -- it's listening for `update:modelValue`.\n\nNet effect: the child never sees the current value, and the parent never hears the update. Nothing in the DOM throws, so it looks like \"it just doesn't update.\"\n\n## Fix\n\nRename the prop and event to the Vue 3 defaults:\n\n```vue\n<script setup>\nconst props = defineProps(['modelValue'])\nconst emit = defineEmits(['update:modelValue'])\n</script>\n\n<template>\n <input\n :value=\"modelValue\"\n @input=\"emit('update:modelValue', $event.target.value)\"\n />\n</template>\n```\n\nThen the parent's `v-model=\"someData\"` works unchanged, since that's already the default Vue 3 contract.\n\n## The alternative (and why to pick one, not both)\n\nIf renaming would touch a lot of call sites at once, you can instead keep the old names and make the parent explicit:\n\n```vue\n<CustomInput v-model:value=\"someData\" />\n```\n\nBut be aware this changes the meaning: `v-model:value` binds a prop named `value` and listens for `update:value` -- not `input`. So even this path requires switching the component's emit from `input` to `update:value`. You cannot fully preserve the Vue 2 `value`/`input` contract through `v-model` in Vue 3; the event name always has to become `update:<propName>`.\n\nThe cleanest and most idiomatic option is renaming to `modelValue`/`update:modelValue`. Whatever you choose, apply it consistently across every call site -- don't leave some parents on `v-model:value` and others on plain `v-model`, since they now mean different things.\n\n## Also check\n\n- Any other components in the app that declared the old `model: { prop: 'value', event: 'input' }` option need the same treatment; that option no longer has the effect it did in Vue 2.\n- If you're mid-migration on other components too, don't half-convert this one -- finish it out of Options API into `<script setup>` per the migration workflow rather than leaving `props` as an Options API key alongside new code.\n- Run `vue-tsc --noEmit` and `eslint .` after the change to confirm the prop/emit types line up with every parent binding.",
2188
+ "outputSha256": "5cd51c8226a082ae6734009b49111b5dd4de35eed57bdf34bb5ac4d8c390e800",
2189
+ "promptSha256": "fa8c02405fa061b57cce289921049b0a14492a5cd094a0fb9c3d413460cd3bcd",
2190
+ "deterministic": [
2191
+ true
2192
+ ],
2193
+ "judge": {
2194
+ "verdict": "pass",
2195
+ "reason": "Explains Vue 2 value/input vs Vue 3 modelValue/update:modelValue contract change and why sync breaks (modelValue lands in $attrs, parent listens for update:modelValue). Shows concrete fix: defineProps(['modelValue']), defineEmits(['update:modelValue']), emit('update:modelValue', $event.target.value). No unrelated cause."
2196
+ },
2197
+ "passed": true
2198
+ }
2199
+ ]
2200
+ }
2201
+ ],
2202
+ "verdict": "pass",
2203
+ "scope": "bundled",
2204
+ "skillDigest": "d4cf575617be80ecdeaad64a35d255a66d7e930aa5ed392e8a69764e743772c3",
2205
+ "catalogDigest": "139cc7e7c63f9a3fcb3560551b7740c5642c828db2570e2f40728089218cc6d4",
2206
+ "judgePromptVersion": "2026-09-25.1",
2207
+ "runner": "deepseek",
2208
+ "model": "deepseek-chat",
2209
+ "runnerPromptVersion": "2026-09-25.1",
2210
+ "recordedAt": "2026-09-25T15:12:35.844Z",
2211
+ "judge": "deepseek",
2212
+ "judgeModel": "deepseek-chat"
2213
+ }
2214
+ ]
2215
+ }