# Recommendations

How recommendations are shaped, written, sanitized, and graded.

## Table of contents

- [Schema](#schema)
- [Writing rules](#writing-rules)
- [The 12 sanitizers](#the-12-sanitizers)
- [Envelope-unwrap recovery](#envelope-unwrap-recovery)
- [Grading rubric](#grading-rubric)
- [Next.js version awareness](#nextjs-version-awareness)

## Schema

Every recommendation is a JSON object matching this TypeScript shape:

```ts
interface Recommendation {
  // Customer-facing
  what: string; // 1 line, lead with impact. Max 80 chars when feasible.
  why: string; // 1-2 sentences. Root cause. Cites codebase findings + counts.
  fix: string; // Step-by-step. Includes before/after code fences. Specific enough to implement.
  bucket: "cost" | "performance" | "reliability";
  effort: "low" | "medium" | "high";
  affectedFiles: string[]; // Verified file paths, from candidate.files
  currentBehavior: string; // What the code does now (with snippet)
  desiredBehavior: string; // Target state (with snippet)
  risk?: string; // Optional: e.g., "Removing force-dynamic may serve stale data on /admin"
  verify: string; // How to confirm the fix worked. e.g., "Re-run `vercel metrics …` and watch p95"

  // Impact (computed from impact-magnitude.mjs in Step 4)
  impactLabel: {
    performance?: string; // PRECISE: "Reduce /api/products p95 from 850ms toward ~250-400ms"
    costMagnitude?: "negligible" | "small" | "medium" | "large" | "very-large";
    costPhrase?: string; // "hundreds of dollars per month at current traffic"
    billingDimension?: string;
    fractionReduced?: number;
  };
  impactTier: "high" | "medium" | "low";
  billingDimension?:
    | "edge-requests"
    | "function-duration"
    | "image-optimization"
    | "isr-reads"
    | "isr-writes"
    | "bandwidth"
    | "data-cache-reads"
    | "cron-invocations"
    | string;

  // Grounding
  citations: string[]; // From references/docs-library.json allow-list. Required: ≥1 entry.
  candidateRef?: string; // The gate candidate this rec traces to (e.g., "uncached_route:/api/products")
  findingRefs?: string[]; // File:line markers from verifiedFindings.json
  appliesAlsoTo?: Array<{
    // Added by dedup when matching recs collapse into one customer-facing item.
    candidateRef?: string;
    affectedFiles?: string[];
    o11ySignal?: string;
    what?: string;
  }>;
  corroborationCount?: number; // Number of matching verified recs folded into this item, including itself.

  // Verifier output (computed in Step 3.6)
  verification?: {
    passRate: number;
    failed: Array<{ type: string; text: string; reason: string }>;
  };

  // Sanitizer audit trail (computed in Step 3.4)
  sanitizerTrail?: string[]; // ["$-strip:2", "version-mismatch:next@15+:1", ...]
  needsReview?: boolean; // Set when a sanitizer caught a hazard

  // Grading (Step 3.5)
  quality: {
    specificity: number; // 0-1
    actionability: number;
    grounding: number;
    evidence: number;
    overall: number;
    grade: "Excellent" | "Good" | "Fair" | "Poor";
  };
}
```

## Writing rules

The recommender prompt explicitly tells the agent to follow these rules. Sanitizers enforce them after generation.

**Voice and tone** are governed by [`references/voice.md`](./voice.md). Read it before writing recommendation prose; it keeps reports direct, metric-grounded, and free of internal process terms.

### Lead with impact

The `what` field opens with the verb + the change, not the framing. Compare:

- ❌ "Consider enabling caching on the /api/products route" (filler before substance)
- ✅ "Add Cache-Control with s-maxage to /api/products" (verb-first, scope-explicit)

### Cite codebase findings with line numbers

The `why` must reference a verified finding from `verifiedFindings.json`:

- ❌ "The route is uncached" (could apply anywhere)
- ✅ "src/app/api/products/route.ts:22 returns Response without Cache-Control; observability shows 0% cache hit on 1.2M invocations/mo"

### No $ literals in customer fields

The user-mandated rule. `what`/`why`/`fix`/`impact`/`currentBehavior`/`desiredBehavior` must not contain `$N` money literals. Use magnitude framing from `impact-magnitude.mjs`.

- ❌ "Save $340/mo by adding s-maxage"
- ✅ "Hundreds of dollars per month at current traffic"
- ✅ (precise performance) "Move 1.2M monthly invocations to the CDN; expect p95 to drop from 850ms toward ~50ms on cache hits"

The `$-strip` sanitizer enforces this at output time, but the prompt should also instruct the LLM not to emit dollar literals in the first place.

### Before/after code fences required

`currentBehavior` shows the offending snippet. `desiredBehavior` shows the target. Language-tagged code fences. Keep both under ~20 lines.

### Cite at least one URL from the library

`citations[]` must contain at least one entry from `references/docs-library.json`. The `missing-citation` sanitizer drops uncited recs. The `unknown-citation` and `version-mismatch` sanitizers strip invalid citations.

### Match the user's framework version

Don't recommend `'use cache'` (Next 15+) to a Next 13 user. The recommender prompt receives only the citation subset valid for the user's stack — but the LLM can still hallucinate. The `version-mismatch` sanitizer catches stragglers.

## The 12 sanitizers

Each sanitizer records its action in `rec.sanitizerTrail` when it mutates a field. Tag format: `tag:detail`. Tags are lexically stable — downstream consumers grep them.

| #   | Sanitizer                 | Trigger                                                | Action                                             | Trail tag                              |
| --- | ------------------------- | ------------------------------------------------------ | -------------------------------------------------- | -------------------------------------- |
| 1   | `$-strip`                 | Money-literal regex in customer field                  | Replace with "the billed cost"                     | `$-strip:N`                            |
| 2   | `vercel-directive-strip`  | `stale-if-error` / `proxy-revalidate` in cache-control | Strip directive (Vercel CDN doesn't honor)         | `vercel-directive-strip:directive`     |
| 3   | `rate-limit`              | Concurrency × delay > known provider rate limit        | Prepend caveat, set needsReview                    | `rate-limit:provider:prescribed/limit` |
| 4   | `pre-release`             | Fix enables `-rc`/`-beta`/`-canary` feature            | Append "requires pre-release version" caveat       | `pre-release:pkg@version`              |
| 5   | `middleware-conflict`     | Rec targets route covered by middleware matcher        | Append "Middleware {matcher} may intercept" caveat | `middleware-conflict:matcher`          |
| 6   | `undeclared-dep`          | Fix imports a package not in package.json              | Prepend "Add dependency first: npm i {pkg}"        | `undeclared-dep:pkg`                   |
| 7   | `count-correct`           | Cited count > verified count, ground-truth known       | Rewrite to "~N" with verified count                | `count-correct:token:cited→actual`     |
| 8   | `count-strip`             | Cited count > verified count, no ground truth          | Rewrite to "a number of"                           | `count-strip:token`                    |
| 9   | `rendering-mode-mislabel` | Rec blames ISR/SSR on a static page                    | Append warning, set needsReview                    | `rendering-mode-mislabel`              |
| 10  | `unknown-citation`        | URL not in `references/docs-library.json`              | Strip URL, set needsReview if all stripped         | `unknown-citation:url`                 |
| 11  | `version-mismatch`        | URL's `applicableFrameworks` doesn't match stack       | Strip URL, set needsReview if all stripped         | `version-mismatch:url`                 |
| 12  | `missing-citation`        | `citations.length === 0` after other sanitizers        | DROP rec entirely                                  | (rec not emitted; counted at end)      |

The sanitizer order matters: dollar-strip runs first (cheap, deterministic), then content sanitizers, then citation sanitizers last. This guarantees citation count is computed against the final state.

The `recordSanitizer(rec, tag)` helper is the single entry point — sanitizers MUST call it before mutating fields. Otherwise the audit trail rots.

### Provider rate limits

Used by sanitizer #3. These provider limits are public contract values:

| Provider  | Limit   | Doc URL                                                |
| --------- | ------- | ------------------------------------------------------ |
| Notion    | 3 rps   | https://developers.notion.com/reference/request-limits |
| OpenAI    | 30 rps  | https://platform.openai.com/docs/guides/rate-limits    |
| Stripe    | 100 rps | https://docs.stripe.com/rate-limits                    |
| Anthropic | 10 rps  | https://docs.anthropic.com/en/api/rate-limits          |

Tiers/plans differ; these are first-tier defaults. The sanitizer prepends a caveat if the rec prescribes higher concurrency.

## Envelope-unwrap recovery

Not a sanitizer — a recovery step. LLMs occasionally wrap their JSON output in an envelope:

```json
{ "data": { "recommendations": [...] } }
{ "result": { "recommendations": [...] } }
{ "insights": { "recommendations": [...] } }
```

`attemptManualRecovery` peels one wrapping layer before schema validation. Increments `hygieneCounters.envelopeUnwraps`. Logs the unwrap to the run log.

This is the only "creative" parsing the skill does. Anything else that fails schema validation is rejected.

## Grading rubric

Each rec is scored on four axes, 0-1 each. Average → grade:

| Axis          | What it measures                            | Strong (1.0) signal                                                                        |
| ------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Specificity   | Concrete files, line numbers, code snippets | Triple-backtick code fence OR inline code ≥10 chars + verified file path                   |
| Actionability | Clear "do this then that" steps             | Numbered steps; verbs present in each step; no "consider"/"might"                          |
| Grounding     | Claims trace to findings or metric data     | `sourceIndex` matches a finding OR rec has affectedFiles + code fences (presumed evidence) |
| Evidence      | Numeric, observed claims                    | Count words (errors, queries, invocations) + units (% / ms / s / K / M)                    |

Grade thresholds:

- `Excellent` ≥ 0.85
- `Good` 0.70 – 0.85
- `Fair` 0.55 – 0.70
- `Poor` < 0.55 → dropped at quality floor in Step 4

## Next.js version awareness

The recommender's citation library is filtered by `signals.json.stack.framework@frameworkVersion`. The agent should still self-check the version when picking which APIs to recommend:

| Feature                            | Available   | Notes                                          |
| ---------------------------------- | ----------- | ---------------------------------------------- |
| App Router                         | Next ≥ 13.0 | Default since 14                               |
| `generateStaticParams`             | Next ≥ 13.0 | Replaces getStaticPaths for App Router         |
| Fetch `next: { revalidate }`       | Next ≥ 13.0 | Note: default fetch caching flipped in Next 15 |
| `unstable_cache`                   | Next 14-15  | Replaced by 'use cache' in 16                  |
| `'use cache'` directive            | Next ≥ 15.0 | Persistent cache primitive                     |
| `cacheLife()`, `cacheTag()`        | Next ≥ 15.0 | Pairs with 'use cache'                         |
| `after()`                          | Next ≥ 15.0 | Non-blocking post-response work                |
| Partial Prerendering               | Next ≥ 15.0 | Stable target later — verify per release       |
| `revalidateTag` / `revalidatePath` | Next ≥ 13.4 | Tag-based on-demand invalidation               |
| `cookies()` / `headers()` async    | Next ≥ 15.0 | Async pattern in 15+                           |

The skill's curated citation library encodes these constraints via `applicableFrameworks`. If a contributor adds a new Next.js feature URL, they MUST set the right semver range in `references/docs-library.json`.
