vue

Seeded Vue defects — reactivity lost on destructure, v-html, module-scope state shared across SSR requests.

What this is

15 deliberate defects, each carrying a marker naming the rule in standards/ it violates. It measures recall, and its pass condition is that every one of the 7 BLOCKER markers is flagged. The rule id in each marker means the scorer grades attribution too: a reviewer that finds the bug but cites the wrong rule counts as caught, and separately as misattributed.

How to onboard it

Nothing is installed and nothing here is ever merged. The corpus is used by opening a throwaway pull request on a pilot repository already onboarded for this stack, adding this directory and seeded/clean together. Label it redline-exempt so the readiness gate does not block a pull request nobody will merge, wait for the automated review to finish, then score it.

seed-canary.yml does exactly this on a schedule and closes the pull request afterwards, including when the run fails.

How to use it

terminal
$ GH_TOKEN=... npx redlinegate metrics score-seeds --repo <org>/<repo> --pr <n>
SeedSeverityRule it violatesDefect
1BLOCKERcore/hardcoded-secretsservice token committed in source
2BLOCKERvue/public-env-secretserver secret read through a client-inlined env var
3BLOCKERvue/ssr-module-scope-statemodule-scope mutable state, shared by every SSR request
4BLOCKERvue/reactivity-lost-on-destructuredestructuring a reactive object freezes the value
5BLOCKERvue/prop-mutationchild writing to an object the parent owns
10BLOCKERcore/customer-data-in-logsmsisdn written to the console
11BLOCKERvue/v-html-sinkunsanitised user html injected into the page -->
6HIGHvue/watch-instead-of-computeda watch whose only job is assigning derived state
7HIGHvue/watcher-missing-cleanuprequest started per keystroke with no cleanup, so the stale response wins
8HIGHvue/unvalidated-route-paramquery param used as a typed value with no validation
9HIGHvue/lifecycle-after-awaitregistered past an await, so it never runs
12HIGHvue/v-for-index-keyindex key on a list that reorders -->
13HIGHvue/v-if-with-v-forv-if and v-for on the same element -->
14HIGHjavascript/var-in-new-codefunction-scoped var in new code inside a single-file component
15HIGHjavascript/unsafe-numeric-coercionparseInt with no radix on a value from the query string

Expected output

A score from scripts/score-seeds.mjs: BLOCKER recall, false positives, and rule attribution. The canary appends it to data/seed-scores.jsonl and fails the run if BLOCKER recall drops below 1.0 or the clean corpus attracts a false positive. Of the 15 defects here, 7 BLOCKER and 8 HIGH — only the BLOCKER count is a pass condition.

How to edit it

Add a defect by adding the code and one marker line above it. There is no separate expectations file to keep in sync — scripts/score-seeds.mjs parses the markers straight out of the source:

marker format
SEED <n> [BLOCKER|HIGH|SUGGESTION] (<stack>/<rule-slug>) <short description>

scripts/validate.mjs fails CI if a seed cites a rule that does not exist, or claims a severity higher than that rule carries in the standard — so a marker cannot quietly drift away from the rule it is testing.

The full file

seeded/vue/SeededViolations.vue · 87 lines · 3.0 KB
<!--
  DO NOT MERGE — Redline validation seed.
  Every `SEED n [SEVERITY] (rule-id)` marker must be flagged at that severity or higher,
  citing that rule id. Score with scripts/score-seeds.mjs.
-->
<script setup lang="ts">
import { reactive, ref, watch, onMounted } from 'vue';
import { useRoute } from 'vue-router';

// SEED 1 [BLOCKER] (core/hardcoded-secrets) service token committed in source
const SERVICE_TOKEN = 'bill-live-9f31c0eaXq7Rn2Kd';

// SEED 2 [BLOCKER] (vue/public-env-secret) server secret read through a client-inlined env var
const signingKey = import.meta.env.VITE_PAYMENT_SIGNING_KEY;

// SEED 3 [BLOCKER] (vue/ssr-module-scope-state) module-scope mutable state, shared by every SSR request
const cart = reactive({ items: [] as string[], owner: '' });

const props = defineProps<{ profile: { bio: string; msisdn: string; seen: boolean } }>();

// SEED 4 [BLOCKER] (vue/reactivity-lost-on-destructure) destructuring a reactive object freezes the value
const { items } = cart;

const route = useRoute();
const results = ref<string[]>([]);
const total = ref(0);

// SEED 5 [BLOCKER] (vue/prop-mutation) child writing to an object the parent owns
function markSeen() {
  props.profile.seen = true;
}

// SEED 6 [HIGH] (vue/watch-instead-of-computed) a watch whose only job is assigning derived state
watch(results, (next) => {
  total.value = next.length;
});

// SEED 7 [HIGH] (vue/watcher-missing-cleanup) request started per keystroke with no cleanup, so the stale response wins
watch(
  () => route.query.q,
  async (q) => {
    // SEED 8 [HIGH] (vue/unvalidated-route-param) query param used as a typed value with no validation
    const page = Number(route.query.page);
    const res = await fetch(`/api/search?q=${q}&page=${page}`, {
      headers: { authorization: `Bearer ${SERVICE_TOKEN}` },
    });
    results.value = await res.json();
  }
);

async function load() {
  await fetch('/api/session');
  // SEED 9 [HIGH] (vue/lifecycle-after-await) registered past an await, so it never runs
  onMounted(() => {
    cart.owner = props.profile.msisdn;
  });
}

// SEED 10 [BLOCKER] (core/customer-data-in-logs) msisdn written to the console
console.log('cart owner', props.profile.msisdn, signingKey);

load();

// SEED 14 [HIGH] (javascript/var-in-new-code) function-scoped var in new code inside a single-file component
var retries = 0

// SEED 15 [HIGH] (javascript/unsafe-numeric-coercion) parseInt with no radix on a value from the query string
const perPage = parseInt(route.query.perPage)
</script>

<template>
  <!-- SEED 11 [BLOCKER] (vue/v-html-sink) unsanitised user html injected into the page -->
  <div v-html="props.profile.bio" />

  <ul>
    <!-- SEED 12 [HIGH] (vue/v-for-index-key) index key on a list that reorders -->
    <li v-for="(item, index) in items" :key="index">{{ item }}</li>
  </ul>

  <ol>
    <!-- SEED 13 [HIGH] (vue/v-if-with-v-for) v-if and v-for on the same element -->
    <li v-for="result in results" :key="result" v-if="result">{{ result }}</li>
  </ol>

  <button @click="markSeen">seen</button>
</template>