svelte

Seeded Svelte and SvelteKit defects — {@html}, private env in shared code, secrets in a universal load.

What this is

25 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 11 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-secretsdatabase password committed in source
1BLOCKERsvelte/module-context-shared-statemodule-scope state shared by every instance and every SSR request
2BLOCKERsvelte/secret-in-universal-loadinternal-only credential used in code that also runs in the browser
2BLOCKERsvelte/private-env-in-shared-codeprivate env imported from a component, so it ships to the browser
3BLOCKERcore/query-string-concatenationroute param concatenated into a sql string
3BLOCKERcore/hardcoded-secretsfallback token committed in source
4BLOCKERcore/customer-data-in-logsmsisdn written to the log on teardown
5BLOCKERsvelte/html-tag-sinkunsanitised markdown output injected into the page -->
7BLOCKERcore/customer-data-in-logsmsisdn written to the log
7BLOCKERcore/customer-data-in-logsmsisdn written to the console
9BLOCKERsvelte/html-tag-sinkunsanitised user html injected into the page -->
1HIGHsvelte/reactive-statement-side-effecta reactive block performing a fetch
2HIGHsvelte/effect-for-derived-statea reactive block assigning what a derived value already gives
3HIGHsvelte/store-not-unsubscribedsubscribed without keeping the unsubscriber
4HIGHsvelte/global-fetch-in-loadglobal fetch in load — no cookie forwarding, no SSR reuse
4HIGHsvelte/effect-for-derived-statederived value computed in an effect instead of $derived
5HIGHsvelte/load-waterfallindependent requests awaited in sequence
5HIGHsvelte/effect-missing-cleanupinterval registered with no teardown returned
6HIGHsvelte/error-swallowed-in-loadfailure rendered as "no data" instead of an error
6HIGHsvelte/store-not-unsubscribedmanual subscribe whose unsubscriber is dropped
6HIGHsvelte/each-missing-keyunkeyed each over a list that is refetched per query -->
8HIGHsvelte/state-mutation-across-boundarychild writing to an object the parent owns
10HIGHsvelte/each-missing-keyunkeyed each over a list that reorders -->
11HIGHjavascript/var-in-new-codefunction-scoped var in new code inside a single-file component
12HIGHjavascript/unsafe-numeric-coercionparseInt with no radix on a value from the URL

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 25 defects here, 11 BLOCKER and 14 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/svelte/+page.js · 43 lines · 1.8 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.
//
// This is a UNIVERSAL load function: it runs on the server and again in the browser.

// SEED 1 [BLOCKER] (core/hardcoded-secrets) database password committed in source
const DB_URL = 'postgres://redline:Sup3rSecret@db.internal.acme:5432/billing';

export async function load({ params, url, fetch: eventFetch }) {
  // SEED 2 [BLOCKER] (svelte/secret-in-universal-load) internal-only credential used in code that also runs in the browser
  const db = await connect(DB_URL);

  // SEED 3 [BLOCKER] (core/query-string-concatenation) route param concatenated into a sql string
  const account = await db.query(`select * from accounts where id = '${params.id}'`);

  // SEED 4 [HIGH] (svelte/global-fetch-in-load) global fetch in load — no cookie forwarding, no SSR reuse
  const orders = await fetch(`/api/accounts/${params.id}/orders`);

  // SEED 5 [HIGH] (svelte/load-waterfall) independent requests awaited in sequence
  const invoices = await eventFetch(`/api/accounts/${params.id}/invoices`);
  const offers = await eventFetch(`/api/accounts/${params.id}/offers`);

  // SEED 6 [HIGH] (svelte/error-swallowed-in-load) failure rendered as "no data" instead of an error
  let usage = [];
  try {
    usage = await (await eventFetch(`/api/usage?page=${url.searchParams.get('page')}`)).json();
  } catch {
    usage = [];
  }

  // SEED 7 [BLOCKER] (core/customer-data-in-logs) msisdn written to the log
  console.log('loaded account', account.msisdn);

  return {
    account,
    orders: await orders.json(),
    invoices: await invoices.json(),
    offers: await offers.json(),
    usage,
  };
}
seeded/svelte/SeededViolations.svelte · 75 lines · 2.7 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.

  Svelte 5, runes mode. A rune anywhere in a component puts the whole file in runes mode,
  where `$:` is a compile error — so the reactive-statement rule is seeded in
  SeededViolationsLegacy.svelte instead, which is a Svelte 4 component.
-->
<script module>
  // SEED 1 [BLOCKER] (svelte/module-context-shared-state) module-scope state shared by every instance and every SSR request
  let lastViewedAccount = { msisdn: '', reference: '' };
</script>

<script>
  import { onMount } from 'svelte';
  import { page } from '$app/stores';
  // SEED 2 [BLOCKER] (svelte/private-env-in-shared-code) private env imported from a component, so it ships to the browser
  import { BILLING_API_SECRET } from '$env/static/private';

  // SEED 3 [BLOCKER] (core/hardcoded-secrets) fallback token committed in source
  const FALLBACK_TOKEN = 'fbk-live-Rn2Kd9Wm4Tz6Hb3Vy7Q';

  let { profile } = $props();
  let orders = $state([]);
  let total = $state(0);

  // SEED 4 [HIGH] (svelte/effect-for-derived-state) derived value computed in an effect instead of $derived
  $effect(() => {
    total = orders.length;
  });

  onMount(() => {
    // SEED 5 [HIGH] (svelte/effect-missing-cleanup) interval registered with no teardown returned
    setInterval(
      () =>
        fetch('/api/heartbeat', {
          headers: { authorization: `Bearer ${BILLING_API_SECRET ?? FALLBACK_TOKEN}` },
        }),
      5000
    );

    // SEED 6 [HIGH] (svelte/store-not-unsubscribed) manual subscribe whose unsubscriber is dropped
    page.subscribe((p) => {
      lastViewedAccount = { msisdn: profile.msisdn, reference: p.url.pathname };
    });
  });

  // SEED 7 [BLOCKER] (core/customer-data-in-logs) msisdn written to the console
  console.log('viewing', profile.msisdn);

  // SEED 8 [HIGH] (svelte/state-mutation-across-boundary) child writing to an object the parent owns
  function markSeen() {
    profile.seen = true;
  }

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

  // SEED 12 [HIGH] (javascript/unsafe-numeric-coercion) parseInt with no radix on a value from the URL
  const perPage = parseInt($page.url.searchParams.get('perPage'));
</script>

<!-- SEED 9 [BLOCKER] (svelte/html-tag-sink) unsanitised user html injected into the page -->
{@html profile.bio}

<ul>
  <!-- SEED 10 [HIGH] (svelte/each-missing-key) unkeyed each over a list that reorders -->
  {#each orders as order}
    <li>{order.reference}</li>
  {/each}
</ul>

<button onclick={markSeen}>seen ({total})</button>
seeded/svelte/SeededViolationsLegacy.svelte · 53 lines · 1.6 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.

  Svelte 4 syntax deliberately: `$:` and `export let` cannot appear in a component that
  uses runes, and both are still what most onboarded Svelte code looks like.
-->
<script>
  import { onDestroy } from 'svelte';
  import { cartStore } from '$lib/stores/cart';

  export let profile;
  export let query = '';

  let results = [];
  let total = 0;

  // SEED 1 [HIGH] (svelte/reactive-statement-side-effect) a reactive block performing a fetch
  $: if (query) {
    fetch(`/api/search?q=${query}`)
      .then((r) => r.json())
      .then((r) => (results = r));
  }

  // SEED 2 [HIGH] (svelte/effect-for-derived-state) a reactive block assigning what a derived value already gives
  $: {
    total = results.length;
  }

  // SEED 3 [HIGH] (svelte/store-not-unsubscribed) subscribed without keeping the unsubscriber
  cartStore.subscribe((c) => {
    profile.cartSize = c.items.length;
  });

  onDestroy(() => {
    // SEED 4 [BLOCKER] (core/customer-data-in-logs) msisdn written to the log on teardown
    console.log('closing panel for', profile.msisdn);
  });
</script>

<!-- SEED 5 [BLOCKER] (svelte/html-tag-sink) unsanitised markdown output injected into the page -->
{@html profile.bioHtml}

<ul>
  <!-- SEED 6 [HIGH] (svelte/each-missing-key) unkeyed each over a list that is refetched per query -->
  {#each results as result}
    <li>{result.reference}</li>
  {/each}
</ul>

<p>{total} results</p>