# Codebase Pattern Extraction — personal-site ("The Construct")

Skill: codebase-pattern-extraction · Repo: personal-site · Date: 2026-07-01

> **Core insight of the skill:** *If you solved it twice, you'll solve it again. Extract once, reuse forever.*
> This is a **single-repo** application of a cross-repo skill: instead of mining patterns across `/data/projects/*`,
> I mine patterns that **recur within `personal-site`**, and flag which ones generalize to Bo's other web
> properties (12factoragentops.com — the amber "Drawing Set" skin — and the ai-partner surface). `cass` session
> mining was optional and is skipped (not available / not needed); every finding below is grounded in real
> `file:line` citations, not history search.

## Method (the skill's pipeline, applied)

For each candidate I ran **Collect (3+ instances) → Diff/Align (common vs varying) → Abstract (invariant core)
→ Parameterize → Package**, then checked it against the skill's **Abstraction Checklist** (3+ instances / clear
invariant / clear variance / sensible defaults / escape hatch). Patterns that only had 1–2 instances were
rejected per the anti-pattern rule ("don't abstract from 1 instance").

## Scope boundary

Analysis is on the **CODE** surface only (`src/`, `middleware.ts`, `scripts/`, `e2e/`, root config) — ~9.4k LOC.
The ~728-file `career/ .agents/ docs/` markdown knowledge layer is architecture context, **not** an audit target.

## Findings at a glance

| # | Pattern | Instances | Package as | Severity |
|---|---------|-----------|------------|----------|
| P1 | OG-image renderer copy-paste (helper exists, unused by 7 routes) | 7 | Extend existing `renderProofOg` lib | **High — DRY, helper already written** |
| P2 | Page-metadata triple (title/desc ×3: metadata+OG+twitter) | 10+ | `buildPageMetadata()` lib | **High** |
| P3 | JSON-LD `@graph` wrapper + `author` Person node | 3 + 5 | `wrapJsonLdGraph()` + `AUTHOR_NODE` const | Medium |
| P4 | JSON-LD injection inconsistency (home raw nonce-less `<script>` vs `JsonLdScript`) | 1 vs 5 | Consolidate on `JsonLdScript`; also a CSP correctness issue | **High — correctness** |
| P5 | Construct color ramp hardcoded inline (grey text + status hexes) | ~100 | `COLORS` token set in `tokens.ts` | **High — highest frequency** |
| P6 | Panel-title-body "mini card" primitives (5 near-clones) + `StatRow`≈`Stats` | 6 | Collapse to 1–2 parameterized primitives | Medium |
| P7 | Case-study page scaffold (CaseHead+HookBox+Stats+Section+ship-it) | 5 | Optional `<CaseSheet>` template / codemod | Low–Medium |
| P8 | `Cache-Control` header string literal | 3 | `CACHE_1H_1D` constant | Low |
| P9 | `.mjs` script harness convention (shebang + GLUE/TRIPWIRE header + node:fs + colocated `.test.mjs` + exit-code contract) | 5+ | Documented template / `scripts/_lib` | Low |

---

## Extracted Pattern: P1 — Open Graph card renderer

**Source Projects (instances):**
- `src/app/about/opengraph-image.tsx:9-56` (inline JSX)
- `src/app/method/opengraph-image.tsx:9-56` (inline JSX)
- `src/app/ai-partner/opengraph-image.tsx:8-55` (inline JSX)
- `src/app/training/opengraph-image.tsx` (inline JSX, 55 LOC)
- `src/app/work/opengraph-image.tsx` (inline JSX, 56 LOC)
- `src/app/writing/opengraph-image.tsx` (inline JSX, 56 LOC)
- `src/app/opengraph-image.tsx` (inline JSX, 141 LOC — richer variant)
- **Contrast — already DRY'd:** `src/lib/proof-og.tsx:10-63` (`renderProofOg`), used by all five `src/app/work/*/opengraph-image.tsx` (e.g. `src/app/work/vibe-check/opengraph-image.tsx:1-15` is a 15-line call).

**Diff/Align:**
```
about / method / ai-partner / training / work / writing        proof-og.tsx (the extraction)
────────────────────────────────────────────────────────       ─────────────────────────────
<div background:#0a0a0a padding:60 fontFamily:monospace>        renderProofOg({ label, statement, subline })
  <span #4ade80 28>boden fuller</span>                          → EXACT same JSX, parameterized
  <div background:#4ade80 …>{LABEL}</div>                          label     ← "About" / "Method" / …
  <div #4ade80 46>{STATEMENT}</div>                                statement ← the green headline
  <div #fff 22 opacity .6>{SUBLINE}</div>                          subline   ← the "// …" line
```
COMMON: the entire card skeleton (colors `#0a0a0a`/`#4ade80`/`#fff`, sizes 28/46/22, layout, `boden fuller`
tag). VARIES: only `label`, `statement`, `subline`, `alt`, `size`/`contentType` (already exported by the lib as
`ogSize`/`ogContentType`).

**Invariant Core:** `renderProofOg` in `src/lib/proof-og.tsx` **already is** the invariant core. The seven
route-level OG images predate (or ignored) it and re-inline the identical tree.

**Variance Points:** `label` / `statement` / `subline` → already the exact params of `renderProofOg`. The root
`src/app/opengraph-image.tsx` (141 LOC) is a genuinely richer card and is the legitimate **escape hatch** — leave
it inline or add a second `renderHomeOg`.

**Packaged As:**
- [x] Library exists: `src/lib/proof-og.tsx` — no new code needed for the six simple ones.
- [ ] Refactor: rewrite `about/method/ai-partner/training/work/writing/opengraph-image.tsx` to the 15-line
      `renderProofOg({...})` form (copy `work/vibe-check/opengraph-image.tsx` as the model). Rename the lib to a
      neutral `og-card.tsx` since it's no longer just "proof" cards.
- Note `writing/[slug]/opengraph-image.tsx:20-57` is a **third shape** (white bold title + description, dynamic
  from `getPostBySlug`) — parameterize `renderProofOg` with an optional `variant: 'accent' | 'title'` rather than
  forcing it.

**Validation (skill's "is it simpler?" test):** LOC(extracted call) ≈ 15 < avg LOC(instance) ≈ 55. Passes.
The `src/app/work/*` pages already prove it works.

**Generalizes to:** 12factoragentops.com uses the same `next/og` OG-card idiom — the lib should take the accent
color (`#4ade80` green vs amber) as a param so one card renderer serves both skins.

---

## Extracted Pattern: P2 — Page metadata builder

**Source Projects (instances):** every non-dynamic route repeats `title` + `description` up to **three times**
(top-level `metadata`, `openGraph`, `twitter`) plus a `canonical`:
- `src/app/work/vibe-check/page.tsx:8-24` (title/desc ×3 + canonical)
- `src/app/work/dify-platform/page.tsx:8-22`
- `src/app/work/openshift-paas/page.tsx:7-19`
- `src/app/work/platform-tooling/page.tsx:7-19`
- `src/app/work/12-factor-agentops/page.tsx:26-…`
- `src/app/page.tsx:6-26` (title/desc ×3)
- `src/app/about/page.tsx:7-20` (metadata+OG, no twitter — an **inconsistency** the builder would fix)
- `src/app/writing/[slug]/page.tsx:33-46` (dynamic, metadata+OG, no twitter)

**Diff/Align:** COMMON: `{ title, description, alternates.canonical, openGraph{title,description,url}, twitter{title,description} }`.
VARIES: only the two strings + the path. `about` and the dynamic writing page **drop** `twitter` — exactly the
drift a single builder eliminates.

**Invariant Core:**
```ts
export function buildPageMetadata(o: {
  title: string; description: string; path: string;
  ogType?: 'website' | 'article' | 'profile';
}): Metadata {
  return {
    title: o.title, description: o.description,
    alternates: { canonical: o.path },
    openGraph: { title: o.title, description: o.description, url: o.path, type: o.ogType ?? 'website' },
    twitter:   { title: o.title, description: o.description },
  }
}
```

**Variance Points:** the string pair + `path` (required); `ogType` (default `'website'`, escape hatch for
`article`/`profile`). Pages with extra fields (`publishedTime`, `tags` on the writing post) spread the builder
result and override.

**Packaged As:**
- [ ] Library: `src/lib/metadata.ts` (colocated `metadata.test.ts`).
- [ ] Apply back to the 8+ pages above; deletes ~10 lines each and removes the "did I remember twitter?" class of
      bug (already present on `about`).

**Validation:** 1 call vs ~15-line object per page. Passes the simpler-than-sum test decisively.

---

## Extracted Pattern: P3 — JSON-LD `@graph` wrapper + shared author node

**Source Projects (instances):**
- `@context/@graph` wrapper hand-written inline at `src/app/work/12-factor-agentops/page.tsx:11`,
  `src/app/writing/page.tsx:81`, `src/app/writing/[slug]/page.tsx:58`, and once inside the lib at
  `src/lib/jsonld.ts:51` (`buildHomePageJsonLd`).
- `author: { '@type': 'Person', name: 'Boden Fuller', url: SITE_URL }` repeated **5×** in `src/lib/jsonld.ts`
  (lines ~55, 74, 93-97, 112-118, 141-145 — grep confirms 5 `name: 'Boden Fuller'` sites).

**Diff/Align:** COMMON: the `{'@context':'https://schema.org','@graph':[…]}` envelope and the identical Person
author sub-object. VARIES: only the node array contents.

**Invariant Core:**
```ts
export const AUTHOR_NODE = { '@type': 'Person', name: 'Boden Fuller', url: SITE_URL } as const
export const wrapJsonLdGraph = (...nodes: unknown[]) =>
  ({ '@context': 'https://schema.org', '@graph': nodes })
```

**Variance Points:** the `nodes` spread. `AUTHOR_NODE` has zero variance → pure constant.

**Packaged As:**
- [ ] Add both to `src/lib/jsonld.ts`; replace the 5 inline author literals + 3 inline envelopes
      (`writing/page.tsx`, `writing/[slug]/page.tsx`, `work/12-factor-agentops/page.tsx`).

**Validation:** collapses the envelope to one call and single-sources the author identity (change the byline once,
not 5×).

---

## Extracted Pattern: P4 — Nonce-aware JSON-LD injection (consistency + CSP correctness)

**Source Projects (instances):**
- **The good pattern:** `src/components/JsonLdScript.tsx:11-19` reads `x-nonce` via `headers()` and passes it to
  `next/script`. Used by `src/app/writing/page.tsx:93`, `src/app/writing/[slug]/page.tsx:71`,
  `src/app/work/12-factor-agentops/page.tsx:44`.
- **The divergence:** `src/app/page.tsx:33-36` injects JSON-LD with a **raw `<script dangerouslySetInnerHTML>`
  and no nonce**.

**Why this is more than DRY:** the strict prod CSP (`src/lib/constants.ts`, `script-src` = nonce +
`strict-dynamic`) authorizes scripts by nonce. A nonce-less inline `<script>` on the home route is the exact class
of failure `JsonLdScript` was built to prevent. This is a **correctness finding**, not just a style nit — verify
against `next build && next start` (per repo memory `feedback-verify-prod-build-not-dev`), not `next dev` whose
relaxed CSP hides it.

**Secondary inconsistency (coverage, not correctness):** of the five `src/app/work/*/page.tsx` case studies, only
`12-factor-agentops` emits `TechArticle` + `Breadcrumb` JSON-LD (`page.tsx:44`); `dify-platform`, `openshift-paas`,
`platform-tooling`, `vibe-check` emit **none**. `buildTechArticleJsonLd` (`src/lib/jsonld.ts:101`) already exists —
the four are missing a one-liner each.

**Packaged As:**
- [ ] Rewrite `src/app/page.tsx:33-36` to `<JsonLdScript id="home-jsonld" data={jsonLd} />`.
- [ ] Add the `TechArticle`+`Breadcrumb` `JsonLdScript` block to the 4 case pages (they already have a canonical
      path in metadata to build the breadcrumb from).

**Abstraction check:** invariant = "every server route injects structured data through the nonce-aware component";
escape hatch = none needed (the component already handles the `nonce ?? undefined` case).

---

## Extracted Pattern: P5 — Construct color ramp → semantic tokens (highest-frequency DRY)

**Source Projects (instances):** the design system already extracts **fonts** (`src/components/construct/tokens.ts:9-24`
— `MONO`/`DISPLAY`/`READ`/`TICK`) and **accent** (`rgb(var(--acc))`/`var(--acc-b)` CSS vars). But the **grey/text
ramp and status colors are hardcoded hex, inline, everywhere.** Counts across `src/components/construct` + `src/app`:

| hex | uses | role |
|-----|------|------|
| `#8a948d` | 29 | dim label / secondary |
| `#9aa39c` | 15 | body-dim |
| `#c2cbc5` | 13 | body-strong |
| `#828c84` | 13 | faint / tertiary |
| `#aab4ad` | 11 | standfirst |
| `#76817a` | 8 | faintest |
| `#cfd6d1`/`#cfd7d1` | 6 | body-bright |
| `#f87171` | 3 | error/red (status) |

175 total inline hex literals across construct+app. Examples: `primitives.tsx:22,49,164,189,222,223,283,324`;
`case.tsx:33,47,58,101,115,118,135,166`; and repeated verbatim in page bodies (`about/page.tsx:25`,
`work/vibe-check/page.tsx:30,79,83`).

**Diff/Align:** COMMON: a fixed ~7-step neutral text ramp + a red status color, reused identically. VARIES:
nothing — the same hex recurs by role. This is textbook "same value 100× → token."

**Invariant Core:** add a semantic color object to `tokens.ts` (matching the existing `CSSProperties` idiom, so it
stays CSP-safe as inline style — do **not** move to classes, per the `style-src 'unsafe-inline'` decision in
`constants.ts:22`):
```ts
export const C = {
  bodyBright: '#cfd6d1', bodyStrong: '#c2cbc5', body: '#9aa39c',
  dim: '#8a948d', faint: '#828c84', faintest: '#76817a',
  standfirst: '#aab4ad', danger: '#f87171',
} as const
```

**Variance Points:** none within the green skin. The **big generalization**: these greys are the neutral ramp
*under* the accent; the amber "Drawing Set" skin (12factoragentops.com) wants the same ramp with an amber-warmed
tint. Promote the ramp to CSS vars (`--c-dim`, …) set per-skin in `layout.tsx` exactly as `--acc` already is, so
`bo-brand`'s "ONE phosphor base, TWO skins" holds for text too, not just accent.

**Packaged As:**
- [ ] `COLORS`/`C` export in `src/components/construct/tokens.ts` (or CSS vars in `globals.css` + thin JS mirror).
- [ ] Codemod the 175 hex sites → tokens. Highest-frequency win in the repo.

**Validation:** LOC-neutral but removes a whole class of "which grey was that?" drift and makes the amber reskin a
config change instead of a find-replace. Passes "document WHY" — the ramp is the neutral scale beneath `--acc`.

---

## Extracted Pattern: P6 — Panel-title-body "mini card" family + Stats near-clone

**Source Projects (instances):** six primitives are all "`<Panel>` + a mono title + a reading-font body," differing
only in font sizes/padding:
- `src/components/construct/primitives.tsx:381-388` `LabeledCard` (title 14 / body 12.5)
- `src/components/construct/primitives.tsx:218-226` `Metric` (value 34 / label / detail)
- `src/components/construct/case.tsx:54-61` `MiniCard` (title 13 / body 11.5)
- `src/components/construct/case.tsx:111-126` `LayerCard` (title + sub + bulleted items)
- `src/components/construct/case.tsx:27-38` `StatRow` and `:40-52` `Stats` — **near-identical**: `StatRow` is
  `Stats` with `cols=4` hardwired and value fontSize 26 vs 24.

**Diff/Align:** COMMON: `Panel pad={…}` wrapping a `{...MONO, color:'rgb(var(--acc-b))'}` heading and a
`{...READ, color:'#8a948d'}` body. VARIES: font sizes, whether there's a sub/detail line, grid vs single.

**Invariant Core:** a single `<StatGrid>` (absorbs `StatRow`=`Stats cols={4}`) and a single `<InfoCard>` with
optional `sub`/`detail`/`size` props absorbing `LabeledCard`/`MiniCard`/`LayerCard`/`Metric`.

**Variance Points:** `cols` (default = `stats.length`), `size` ('sm'|'md'|'lg' mapping the 11.5/13/14/34 ladder),
optional `sub`, optional bullet `items`.

**Packaged As:**
- [ ] Collapse `StatRow` into `Stats` (delete `StatRow`, callers pass `cols={4}`) — zero-risk, they're clones.
- [ ] Optionally unify the four card variants behind one `InfoCard` with a `size` prop. Lower priority (each is
      small and readable) — apply the skill's anti-pattern guard "don't over-parameterize"; the `StatRow`/`Stats`
      merge is the clear win, the 4-card merge is judgment-call.

**Validation:** `StatRow`≈`Stats` is a proven duplicate (same JSX, two constants differ). Merge passes; the wider
card merge is optional.

---

## Extracted Pattern: P7 — Case-study page scaffold

**Source Projects (instances):** all five `src/app/work/*/page.tsx` follow the identical spine:
`PageLayout → CaseHead(route,comment) → HookBox → Stats → Section×N → Section "ship it" with Button`s. See
`src/app/work/vibe-check/page.tsx:35-119` as the archetype; `CaseHead` at `src/components/construct/case.tsx:172-187`
is already the shared header. Each page also defines a **local one-off** Panel-title-body component (`Axiom` in
`about/page.tsx:22`, `VibeLevelRow` in `vibe-check/page.tsx:26`), reinforcing P6.

**Diff/Align:** COMMON: the section ordering + wrapper. VARIES: the middle content (genuinely per-case). This is a
**structure** pattern → per the skill's Pattern-Types table, package as a **template**, not a library (forcing a
`<CaseSheet>` component would fight the free-form middle).

**Packaged As:**
- [ ] Lightweight `<CaseSheet route comment hook stats>…children…</CaseSheet>` wrapper that renders the fixed
      head/hook/stats and slots the variable body — or just a documented scaffold in `docs/`. Low priority; the
      current primitives already carry most of it.

**Abstraction check:** invariant is real but thin; escape hatch (arbitrary children) mandatory. Keep it optional.

---

## Extracted Pattern: P8 — Cache-Control header literal

**Source Projects (instances):** `'public, max-age=3600, s-maxage=86400'` literal at
`src/app/api/posts/route.ts:8`, `src/app/api/posts/[slug]/route.ts:19`, `src/app/llms.txt/route.ts:52` (and
asserted verbatim in all three `*.test.ts`).

**Invariant Core:** `export const CACHE_1H_1D = 'public, max-age=3600, s-maxage=86400'` in `src/lib/constants.ts`
(alongside `SITE_URL`/CSP). Pair with the `no-store` literal used on the 404/health paths → `CACHE_NONE`.

**Packaged As:** [ ] two string constants in `constants.ts`; import in the 3 routes + tests. Trivial, prevents
skew between route and its test.

---

## Extracted Pattern: P9 — `.mjs` tooling-script harness

**Source Projects (instances):** `scripts/slop-lint.mjs`, `x-atomize.mjs`, `x-draft-lint.mjs`, `x-calendar-check.mjs`,
`typegen-clean.mjs` share a convention: `#!/usr/bin/env node` shebang + a top **"this is GLUE / TRIPWIRE, not a
writer/replacement"** doc-comment + `node:fs`/`node:url`/`node:path` imports + a colocated `*.test.mjs` (Vitest
`scripts` project) whose **load-bearing assertion is the process exit code** (`x-draft-lint.test.mjs` uses
`spawnSync` and checks exit ≠ 0 on violation).

**Diff/Align:** COMMON: shebang, intent-header, node-builtin imports, exit-code-as-contract, colocated test.
VARIES: the lint/transform logic itself.

**Packaged As:** [ ] a documented "AgentOps content-script" template (or thin `scripts/_lib/cli.mjs` for arg/exit
helpers). This is the pattern most likely to **generalize across Bo's repos** (the same x-atomize / slop-lint
gates want to live in `ai-partner-workspace` and `12-factor-agentops`) — a real candidate for a shared skill or
npm-linkable `scripts/_lib`, which is exactly the cross-repo case the skill was written for.

---

## What's already well-extracted (don't "fix")

The Construct design system is a **positive** instance of this skill already applied: `tokens.ts` single-sources
fonts, `primitives.tsx`/`case.tsx` single-source the schematic UI, `mission.ts:5-8` single-sources the thesis line
across hero + metadata + OG (with a comment mandating byte-identical copy), `builds.ts` single-sources the project
registry consumed by `/work` + home + `llms.txt`, and `proof-og.tsx` DRYs the `/work/*` OG cards. The findings
above are the **remaining** un-extracted recurrences, not a critique of the architecture.

**Do NOT** "extract" the CSP `style-src 'unsafe-inline'` into a nonce (`constants.ts:22`) — it is deliberate; the
whole system styles via inline `style={{}}` which nonces don't cover. Any color/token work in P5 must **stay inline
style**, not migrate to CSS classes, for that reason.

## Recommended sequencing (highest ROI first)

1. **P4** (correctness: home nonce-less script + missing case-study JSON-LD) — ship first, it's a real prod-CSP risk.
2. **P1** (OG images → existing `renderProofOg`) — biggest LOC deletion, helper already exists, near-zero risk.
3. **P5** (color tokens) — highest frequency (175 sites), unlocks the amber-skin generalization for bo-brand.
4. **P2 / P3** (metadata + JSON-LD builders) — kill the title/desc-×3 and author-×5 duplication and the
   already-present `twitter`-missing drift.
5. **P6 (StatRow→Stats merge) / P8 (cache const)** — trivial, mechanical.
6. **P7 / P9 / P6-cards** — optional; guard against over-parameterization.

## Abstraction-checklist scorecard

| Pattern | 3+ instances | clear invariant | clear variance | sensible default | escape hatch | verdict |
|---------|:---:|:---:|:---:|:---:|:---:|---------|
| P1 | ✅ 7 | ✅ | ✅ | ✅ | ✅ (root OG) | **extract** |
| P2 | ✅ 10+ | ✅ | ✅ | ✅ (`website`) | ✅ (spread+override) | **extract** |
| P3 | ✅ 5+3 | ✅ | ✅ | ✅ | n/a | **extract** |
| P4 | ✅ 5 vs 1 | ✅ | ✅ | ✅ | ✅ | **extract (correctness)** |
| P5 | ✅ ~100 | ✅ | ✅ (per-skin) | ✅ | ✅ | **extract** |
| P6 | ✅ 6 | ✅ | ⚠️ (some over-param risk) | ✅ | ✅ | **partial (StatRow/Stats yes)** |
| P7 | ✅ 5 | ⚠️ thin | ✅ | ✅ | ✅ required | **optional (template)** |
| P8 | ✅ 3 | ✅ | none | ✅ | n/a | **extract (trivial)** |
| P9 | ✅ 5 | ✅ | ✅ | ✅ | ✅ | **document/template** |

## Cross-repo generalization note

Per `bo-brand` ("ONE phosphor base, TWO skins"), the three highest-value extractions — **P1** (OG renderer takes
accent color), **P5** (neutral ramp as per-skin CSS vars), and **P9** (shared content-script harness) — are the
ones that pay off *twice*: once here (green "The Construct") and once on 12factoragentops.com (amber "Drawing Set").
Package those three so the accent/warmth is a parameter, and the amber site inherits them instead of re-forking.
