Day 02–03 — Tailwind, Fonts, and the Schema That Lied to the Plan
Date: 2026-05-23
Status at end of day: Plans 1 and 0 complete. Tailwind v4, Google Fonts, all 5 Payload collections, 2 globals, Docker full stack, CI/CD, 18/18 tests passing. 130 recipes, 16 categories, 130 images migrated from WordPress. ESLint fixed, 8 TypeScript errors cleaned up. Ready for Plan 2.
TLDR
- Built: Tailwind CSS v4 with design tokens, Google Fonts, five Payload collections, two globals, Docker full-stack, GitHub Actions CI/CD — 18 tests passing. Site is still visually empty but the entire infrastructure is in place.
- Built: 130 recipes, 16 categories, and 130 images migrated from WordPress into Payload via a single TypeScript script.
- Flop: TypeScript errors on Payload relationship fields —
CollectionSlugtypes don't exist untilnpm run generate:typesruns. Fixed with temporaryas anycasts; documented as a known Payload v3 friction point. - Flop: A subagent reported a commit that never landed (ghost commit to a detached state). Caught by checking
git log, recreated manually in two minutes. - Flop:
tsxsilently ignores.env.local— Payload init failed with "missing secret key." Fixed with--env-file=.env.local. - Flop: Wrong import path in the migration script — plan said two levels up, actual file is one level up. Fixed by checking the filesystem.
- Flop: R2 credentials not set locally caused every image upload to fail. Fixed by making the S3 plugin conditional on env vars being present.
- Flop: Category slugs (
cheesecake) don't match display names (cheesecakes) — caused silent "no category found" misses across three debug runs. Caught once both values were printed side by side. - Audit: Pre-Plan 2 check found ESLint silently broken and 8 TypeScript errors in migration scripts — both fixed before moving on.
What we built today
Two plans in one session. The first half was Plan 1, Tasks 2 and 3: Tailwind CSS v4 with design tokens, Google Fonts wired in via next/font. No visible pages, but the design system is in place — every color, every font, every token is defined and usable from here on.
But equally as much of the first half was spent on something that doesn't show up in git diff: prompt and context management.
Between tasks I was running slash commands explicitly:
/claude-md-management:claude-md-improver— structured audit of CLAUDE.md against a quality rubric. Scores each section, lists specific issues, proposes targeted diffs. Not "update the docs" but "here are three concrete things wrong and here's exactly what to change."- Explicit prompts after each task: "have you updated memory and documentation?" — because Claude won't do this unprompted. It finishes the task and moves on. You have to ask.
- "tell me what you're going to do first" before every task — forcing a plan summary before any code is written. This catches misunderstandings before they become wrong commits, not after.
The audit caught real things. After Task 3, CLAUDE.md had npx vitest as the session-start test command — already stale because we'd just changed the test script to npm test. It had two missing test directories. It had a filename discrepancy between the plan (.ts) and the actual file on disk (.mts). None of those would have caused immediate failures. All of them would have quietly confused a future session.
The pattern I'm running each task:
- Ask what Claude is going to do — get confirmation before any code
- Let it execute
- Verify in the browser or terminal
- Ask to update memory and CLAUDE.md
- Periodically run
/claude-md-management:claude-md-improverfor a formal audit
It's overhead. It's also the difference between a codebase where AI sessions build on each other cleanly and one where each session starts by re-discovering what the last session did.
The second half was Plan 0: the WordPress content migration. The job was to read the WXR export sitting in resources/, scrape recipe JSON-LD from the live site (because the Zip Recipes plugin stores ingredients and steps in custom DB tables that didn't export), download all the featured images, and bulk-import everything into Payload.
Eight tasks. A single disposable TypeScript script at src/scripts/migrate-wordpress.ts. The plan was written in the previous session and handed to Claude at the start of this one.
The script ended up being harder than the plan made it look. Not because the architecture was wrong — the architecture was right. But because the plan was written against an imagined schema, and the actual schema that got built in Plan 1 was different in half a dozen small but breaking ways.
Task 2: Tailwind CSS v4 with design tokens
What changed in v4
Tailwind v4 drops tailwind.config.js. Design tokens now live in a @theme block inside globals.css. The PostCSS plugin changed too — it's @tailwindcss/postcss now, not tailwindcss. Simpler, fewer files.
postcss.config.mjs:
export default {
plugins: {
'@tailwindcss/postcss': {},
},
}
src/app/globals.css has all eight design colors, four font family vars, and a card border radius in the @theme block:
@import "tailwindcss";
@theme {
--color-background: #FDFAF5;
--color-accent: #C0622A;
/* ... all eight colors ... */
--font-display: var(--font-playfair);
/* ... four font vars ... */
--radius-card: 8px;
}
The font vars reference --font-playfair etc. — CSS custom properties that next/font will inject on the <html> element. They're empty until Task 3 wires up the root layout.
Flop: the CSS import I didn't audit
After updating (frontend)/layout.tsx to import ../globals.css and deleting styles.css, the dev server returned:
Module not found: Can't resolve './styles.css'
I'd updated the layout. What I hadn't checked was page.tsx. The scaffold's (frontend)/page.tsx had its own import './styles.css' — a completely separate reference that wasn't in scope when I was thinking about the layout.
Fix: remove the import from page.tsx. One grep -r "styles.css" src/ would have caught it before the server started.
Root cause: Thought about the layout file, didn't think to audit all files that imported the old CSS. Obvious in hindsight.
Task 3: Google Fonts and root layout
The root layout
The scaffold doesn't create a root src/app/layout.tsx — only the route-group layouts. This task adds the real root layout, which loads all four Google Fonts and attaches their CSS variables to <html>:
const playfair = Playfair_Display({
subsets: ['latin'],
variable: '--font-playfair',
display: 'swap',
})
// ... lora, sourceSans, dmSans same pattern ...
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html
lang="en"
className={`${playfair.variable} ${lora.variable} ${sourceSans.variable} ${dmSans.variable}`}
>
<body>{children}</body>
</html>
)
}
next/font downloads the font files at build time and serves them from the same origin — no Google DNS at runtime, no layout shift, no GDPR exposure from third-party font requests. The CSS variable mechanism is what connects the font load to the design tokens in globals.css.
With the root layout owning <html>, <body>, and globals.css, the (frontend)/layout.tsx becomes a one-liner passthrough:
export default function FrontendLayout({ children }: { children: React.ReactNode }) {
return <>{children}</>
}
Vitest config fix
The scaffold's vitest.config.mts was pointed at tests/int/**/*.int.spec.ts with a jsdom environment — a placeholder that matched none of the test files we're about to write. Replaced with:
export default defineConfig({
plugins: [tsconfigPaths()],
test: {
environment: 'node',
globals: true,
include: ['src/__tests__/**/*.test.ts'],
},
})
environment: 'node' is correct for collection config tests — they don't touch the DOM. Component tests will override this per-file with // @vitest-environment jsdom when needed.
Also replaced the scaffold's test script (which called pnpm) with:
"test": "vitest run",
"test:watch": "vitest"
Verification
Dev server loaded. Network tab showed four .woff2 requests — Playfair Display, Lora, Source Sans 3, DM Sans. TypeScript clean. Committed.
Tasks 4–14: Payload collections, globals, and Docker infrastructure
After the design system work, we kept going. The rest of Plan 1 — all of it — in one session. Here's what that looked like.
The workflow: subagent-driven development
For Tasks 4–14 I used a different execution pattern than the first half of the day. Instead of Claude writing code in the main conversation, each task was dispatched to a fresh subagent:
- Implementer subagent gets the full task spec, context, and instructions. Follows TDD exactly: write failing test → verify it fails → implement → verify it passes →
tsc --noEmit→ commit. - Spec compliance reviewer reads the actual files and compares them line-by-line against the spec. Does not trust the implementer's report.
- Code quality reviewer reviews the diff for bugs, missing validations, and consistency issues.
If either reviewer finds problems, the implementer (or me directly for simple fixes) patches them and the reviewer runs again.
Fresh subagent per task means no context bleed — the Workshops implementer doesn't remember anything from the Recipes implementer. Each one reads exactly the context it needs, nothing more. Eleven tasks, zero accumulated confusion.
What got built
Collections (Tasks 4–8):
Recipes— the core collection. 17 fields: title, slug, status, publishedAt, category relationship, tags, featuredImage, intro, prepTime, cookTime, servings, ingredients array, steps array, equipment array with affiliate URLs, seoTitle, seoDescription.Categories— simple taxonomy: name, slug, description, image upload.Workshops— culinary classes. Title, slug, status (upcoming/past), date, location, price, description, bookingUrl, image.Comments— moderated via admin. Recipe relationship, authorName, authorEmail, body,approved: falseby default.Media— replaces the scaffold placeholder. Four image sizes: thumbnail (400×300), card (800×533), hero (1440×600), mobile (390×260). Cloudflare R2 storage adapter wired in.
Globals (Tasks 9–10):
SiteSettings— siteName, tagline, socialUrls group (facebook/pinterest/instagram/youtube), newsletter heading/subheading.Navigation— primaryNav and footerNav arrays of label+url pairs. SharednavLinkFieldsconst keeps them DRY.
Infrastructure (Tasks 11–14):
- Full test suite: 7 test files, 18 tests, all green.
- Payload types generated (
src/payload-types.ts, 773 lines) — Payload's type generator ran against the config without needing the dev server or database. Dockerfile— 3-stage multi-stage build (deps → builder → runner), non-rootnextjsuser.docker-compose.yml— expanded from the minimal db-only version to the full stack: app + db + caddy.Caddyfile— one-liner:myloveofbaking.com { reverse_proxy app:3000 }..github/workflows/deploy.yml— build Docker image → push to GHCR → SSH to server →docker compose pull && up -d..env.example— all 20+ env vars documented.
Ten commits. All green.
AI flops (Plan 1)
Forward references in TypeScript
Payload's CollectionConfig types require relationTo to be a key from the CollectionSlug union — which is auto-generated and empty until npm run generate:types runs. So when Recipes references 'categories' before Categories exists, TypeScript throws:
Type '"categories"' is not assignable to type 'CollectionSlug'
The fix the implementer used: relationTo: 'categories' as any. Fine, temporary, documented with an eslint-disable comment. After Task 11 generates types, the cast is technically still needed because CollectionSlug won't update again until the next build. The casts stay in place until someone runs npm run generate:types and commits the result again.
This is a known Payload v3 TDD friction point: you can't write TypeScript-clean relationship fields until all collections exist and types are generated. The plan didn't mention it. The implementer handled it correctly, but it should be in the gotchas list.
The .env.example commit that went nowhere
Task 14's subagent reported: "Committed with: docs: add .env.example". Git log showed no such commit. ls .env.example returned nothing.
The agent had probably run in the wrong directory or committed to a detached state. No error, no complaint, just a ghost commit that never landed.
I created the file directly and committed it myself. Two minutes to fix, but the pattern is worth noting: when an agent reports a commit that doesn't appear in git log, the commit didn't happen. Always verify with git log before moving on, not after.
Pre-existing TypeScript noise
Every tsc --noEmit run in every task reported three errors about src/app/(payload)/admin/importMap.js:
error TS2307: Cannot find module '../importMap'
This is Payload's auto-generated file — it exists at runtime but not in the repo (gitignored). It produces the same three errors whether the project is empty or fully implemented, and it has nothing to do with anything we're building. Every implementer spent cycles confirming these errors were pre-existing before reporting DONE. That's the right behavior, but it added noise to every single task report.
AI wins (Plan 1)
URL validation catch — affiliateUrl in Recipes equipment
The plan spec said affiliateUrl was a text field. The code quality reviewer flagged that with no validation, a content editor could save a plain product name or a bare ASIN and it would silently produce a broken <a href> later. The reviewer suggested a validate function using new URL(). Implemented. Took 10 minutes. This is exactly the kind of thing a thorough code review catches that a fast implementation skips.
index: true on slug fields
Same reviewer flagged that Recipes' slug field had unique: true but no index: true. PostgreSQL creates an implicit index for UNIQUE constraints, but declaring index: true explicitly signals query intent and is the Payload v3 pattern for slug-based lookups. Caught in Task 4, applied as the project pattern in Tasks 5–6.
Media collection: scaffold replacement without ceremony
The scaffold ships a Media collection with upload: true and nothing else — no image sizes, no storage config. The implementer correctly identified this as a placeholder, replaced it entirely with the spec-defined version (four image sizes, alt text, R2 adapter), and handled the scaffold's access: { read: () => true } drop correctly: R2 serves images directly from a public URL, bypassing Payload's access control layer.
18/18 tests on first full run
After all collections and globals were in place, npm test ran clean on the first try. Every collection test, every global test, no failures. That's TDD doing what it's supposed to do — you don't find out if the collection config is wrong at page-render time, you find out immediately.
The schema divergence problem (Plan 0)
The migration plan was written before Plan 1 executed. When Plan 1 built the Recipes collection, it made sensible field naming decisions that didn't match what the plan had assumed:
| Plan spec assumed | Actual schema |
|---|---|
featured_image |
featuredImage |
prep_time (text "20 min") |
prepTime (number, minutes) |
cook_time (text) |
cookTime (number, minutes) |
servings (string) |
servings (number) |
seo_title |
seoTitle |
seo_description |
seoDescription |
steps[].text |
steps[].instruction |
tags: string[] |
tags: { tag: string }[] |
The first attempt at Task 7 failed immediately with TypeError: Cannot create property 'tag' on string 'baking'. That was the tags mismatch. Fix deployed. Then it ran fine for two recipes before hitting a wall on ingredient.quantity — required field, empty string, validation error. Turns out a lot of recipe ingredients don't have a measurable quantity. "Salt to taste" doesn't have a quantity. The schema said otherwise.
Then 60 recipes failed with category mismatches. The WXR <category> element has two representations of the category — a nicename attribute (the slug) and CDATA text (the display name). The plan said to use c['__cdata'], which is the display name "Dessert" — but the ID map was built from the slug dessert. All lookup attempts returned undefined.
Then JSON-LD instructions. Schema.org recipeInstructions can be a string, an array of HowToStep, or an array of HowToSection containing nested HowToStep entries. The plan assumed plain array. Several recipes use HowToSection. The ?? [] fallback doesn't help when the value is a non-null non-array.
In total: five debug-fix cycles to get from first attempt to clean run. None of these were catastrophic — the script is idempotent, so each re-run picked up where it left off, creating only the recipes that hadn't been created yet. 68 created, then 50 more, then 10 more, until 0 created and 130 skipped.
AI flops (Plan 0)
Ghost env var. First Payload initialization attempt:
Error: missing secret key. A secret key is needed to secure Payload.
The .env.local file exists and has PAYLOAD_SECRET, but tsx doesn't load it by default. Fix is npx tsx --env-file=.env.local. This is documented in CLAUDE.md now. But the plan said npx tsx src/scripts/migrate-wordpress.ts with no mention of env loading — Claude generated the command from the plan without checking whether tsx actually picks up .env.local by default (it doesn't).
Wrong import path. payload.config lives at src/payload.config.ts. The migration script is at src/scripts/migrate-wordpress.ts. The plan said import config from '../../payload.config' — two levels up, which puts you at the project root. The actual file is one level up: ../payload.config. Plan was wrong; Claude followed the plan without checking the filesystem.
R2 blocking uploads. The Payload Media collection is wired to S3/R2 via @payloadcms/storage-s3. Running locally without R2 credentials, every image upload failed:
Error: No value provided for input HTTP label: Bucket.
The plan said "R2 must be configured before running the migration." It's not configured yet. The right fix: make the S3 plugin conditional on the env vars being present. One change to payload.config.ts — spread the plugin into the array only when all four R2 vars exist. Local dev now falls back to the uploads/ directory. This should have been in the plan from the start; it's a standard pattern for dev/prod parity.
Category display name vs. slug. The worst debugging session of the day. Recipes were being skipped with "no category found" despite having categories in WXR. The category ID map keys are slugs (dessert). The post category extraction used c['__cdata'] which gives the display name (Dessert). These look similar enough that you'd need to actually print both to spot the mismatch. It took three runs before I looked at what was actually in the map vs. what was being looked up.
AI wins (Plan 0)
Idempotency worked perfectly. The script checked for existing slugs before creating. Every failed recipe in one run could be retried in the next without duplicating anything. Over five runs, it accumulated: 2 → 68 → 118 → 120 → 130. No manual cleanup needed between any of them.
Schema violations surfaced fast. Payload's validation errors were precise: The following field is invalid: Ingredients 7 > Name. Not just "create failed" — it told me exactly which field, which row, and what was wrong. Claude correctly parsed that into "the ingredient name is empty" and traced it back to the parseIngredient helper returning { quantity: '', name: '' } when the raw string was just whitespace.
The plan's structure held. Even though field names were wrong and several edge cases weren't covered, the overall architecture — parse WXR, scrape JSON-LD, upload images, create categories first, create recipes last — was exactly right. The bugs were in the details, not the design.
Pre-Plan 2 audit: things that had been broken the whole time
Before starting Plan 2, I asked Claude to run a full verification pass — tests, linter, TypeScript — and check every infrastructure file against the Plan 1 spec. Good thing we did.
ESLint was completely broken. Not "a few warnings" broken. It crashed on startup with a circular structure error and produced zero output:
TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
| property 'configs' -> object with constructor 'Object'
| ...
--- property 'react' closes the circle
The root cause: eslint.config.mjs was using FlatCompat to wrap next/core-web-vitals and next/typescript. This was the Next.js-recommended pattern for ESLint 9 flat config — but eslint-config-next v16 now exports native flat config arrays. Running a native flat config through FlatCompat creates circular references when the validator tries to JSON-stringify the config for inspection. ESLint crashes before it reads a single file.
The fix was removing FlatCompat entirely and importing the configs directly:
// Before (broken)
import { FlatCompat } from '@eslint/eslintrc'
const compat = new FlatCompat({ baseDirectory: __dirname })
const eslintConfig = [...compat.extends('next/core-web-vitals', 'next/typescript'), ...]
// After (working)
import coreWebVitals from 'eslint-config-next/core-web-vitals'
import typescript from 'eslint-config-next/typescript'
const eslintConfig = [...coreWebVitals, ...typescript, ...]
Also had to add design/ to the ESLint ignores — the design canvas JSX files aren't application code and were producing hundreds of react/jsx-no-undef errors once the linter actually ran.
Eight TypeScript errors in the migration script. The script had run successfully and created 130 recipes — tsx doesn't enforce types at runtime, so these were invisible during execution:
doc.id as stringon numeric PostgreSQL IDs — you can't assertnumber as string. Should beString(doc.id).category: categoryPayloadId— the map stored IDs as strings, but Payload's relation field expects a number. Should beNumber(categoryPayloadId).buildLexicalParagraphreturneddirection: 'ltr'typed asstring, but Payload's Lexical type wants"ltr" | "rtl" | null. Fix:direction: 'ltr' as const.payload.db.destroy()— TypeScript correctly flagged thatdestroymight not exist on the db adapter. Fix:payload.db?.destroy?.().
None of these affected the data already in Payload — the migration ran fine despite the type errors. But they would have silently misled anyone trying to understand the script, and the category-as-string error would have caused real issues if the script were ever re-run after Payload's types changed.
Tests: 18/18 passing. Infrastructure: all present. The actual functionality from Plans 0 and 1 checks out. The issues were all in the tooling layer.
End of day
Done:
- Tailwind CSS v4 installed with PostCSS config
- All design tokens defined in
globals.css(@themeblock) - Root layout created with four Google Fonts via
next/font - Font CSS variables connected to design tokens
(frontend)/layout.tsxthinned to a passthrough- Vitest pointed at the correct test directory with node environment
- Recipes, Categories, Workshops, Comments, Media collections
- SiteSettings, Navigation globals
- 18/18 tests passing
- Payload types generated (773 lines)
- Dockerfile + docker-compose.yml (full stack) + Caddyfile
- GitHub Actions CI/CD workflow
- .env.example documenting all 20+ env vars
- 130 recipes in Payload with ingredients, steps, categories, featured images, SEO fields
- 16 categories
- 130 media items (all featured images)
- Migration script is idempotent, documented, and re-runnable
- 7 informational posts have placeholder
"See original recipe"ingredients — flagged inMIGRATION-NOTES.mdfor manual follow-up payload.config.tsupdated to gate R2 on env vars — local dev no longer requires R2 credentials- ESLint fixed —
FlatCompatremoved,design/ignored - 8 TypeScript errors in migration scripts fixed — type-safe ID conversions,
as conston Lexical literals, optional chaining on db destroy
Not done:
- Any public-facing pages (Plan 2)
- prepTime is 0 for most recipes — the live site's JSON-LD often omits
prepTime. Expected; content filled in through admin over time. - R2 credentials in actual env files
- GitHub Actions secrets (DEPLOY_HOST, DEPLOY_USER, DEPLOY_SSH_KEY) — manual step
- Docker full-stack test against a real build
Next: Plan 2. Build all 11 pages against the real data.
Context engineering: the meta-layer I'm building as we go
Something I want to document while I'm still doing it consciously, because I think it's the part most people skip when they write about AI-assisted development.
Between tasks today I stopped and asked: "have you updated memory and documentation?"
Claude hadn't. The execution state memory still said Task 2 was in progress. CLAUDE.md still said Task 2 was next. Left unchecked, the next session would have started with stale context and potentially re-done work or made wrong assumptions. I also triggered the /claude-md-management:claude-md-improver skill explicitly — a structured audit that found three real issues: a stale npx vitest command, two missing test directories, and an undocumented filename mismatch between the plan and the actual file on disk.
None of those are catastrophic bugs. But each one is a small drift — a tiny gap between what Claude thinks is true and what's actually true. Drift compounds. By session five or six, a codebase full of small drifts starts producing confusing errors and contradictory suggestions.
What I'm doing — and I'm being deliberate about this — is context engineering. Not prompt engineering in the "magic words to get better output" sense. Actual engineering: designing and maintaining the information environment that the AI operates in.
The setup has several layers:
- Implementation plans in
docs/superpowers/plans/— committed to disk, read at the start of every session. These survive context compaction because they're files, not conversation history. - Memory files in
.claude/projects/.../memory/— structured records of execution state, project decisions, user preferences, and deviations from plan. Written at the end of each session, read at the start of the next. - CLAUDE.md — the always-loaded project context. Commands, gotchas, architecture, conventions. Audited with a dedicated skill when things change.
- Skills — invokable workflows for recurring activities.
/claude-md-management:claude-md-improverfor CLAUDE.md audits.commit-commands:commitfor consistent commit patterns.superpowers:verification-before-completionbefore claiming anything is done.
The key insight is that AI context is a resource, not a given. It degrades. It gets stale. It gets compressed. If you don't actively manage it — writing things down, updating memory, auditing documentation — you're relying on the model to remember things it structurally cannot remember across sessions.
Most people use AI assistants like a smart colleague you can ask anything. That works for a one-off question. For a multi-week project with real architecture decisions and real gotchas, you need to treat context like infrastructure: design it deliberately, maintain it, check it for drift.
The overhead is real. Stopping mid-session to ask "update the memory and CLAUDE.md" takes a few minutes. Triggering a CLAUDE.md audit takes longer. But the alternative is a compounding tax — every future session starting with slightly wrong assumptions, every small drift requiring slightly more correction.
I'm spending maybe 10% of each session on context maintenance. It feels like it's already paying off.
Honest take
Long day. Two plans done in one session.
Plan 1 closed cleanly: every collection, every global, Docker, CI/CD, env documentation, 18 tests. The subagent workflow was the thing that made it possible. For the collections work specifically, it kept the main conversation clean while 11 separate implementers each did exactly one thing. The quality reviews caught real issues (the affiliateUrl validation, the slug index pattern) that would have been missing if I'd just accepted the first implementation. Fresh context per task is not overhead — it's the mechanism that keeps the implementation focused and the code consistent.
Plan 0 was messier. The pattern: plan meets reality, plan loses details, Claude dutifully implements the wrong details, we iterate. What works — the iteration loop is fast. An error message → Claude identifies the root cause → fix is two lines → re-run. The five debug cycles took maybe 30 minutes total. On a human-only implementation, finding the nicename/CDATA mismatch would have meant carefully reading WXR documentation and staring at the object structure. Claude spotted it by looking at what was actually in the variables at runtime — same as you'd do, just faster.
What's still fragile: plans that were written against a spec that later changed. The migration plan was good but it was written in session 1 against a mental model of what the schema would look like. By session 2, the actual schema had slightly different field names. The plan didn't update. Claude implemented the plan, not the schema. This is a documentation problem — keeping plans and code in sync — and I don't have a clean answer for it yet.
The ghost .env.example commit is the flop I want to hold onto from the first half. The agent reported success, there was no error, and I almost moved on. The right discipline: after any commit claim, run git log and verify the SHA actually appears. AI agents are occasionally optimistic about whether an action succeeded. Check the evidence.
The audit added a different lesson. The linter had been broken since the project started — every npm run lint during Plan 1 and Plan 0 would have crashed immediately. Nobody noticed because we weren't running it as a gate. The fix: the pre-phase audit. Before starting Plan 2, we ran everything — npm test, npm run lint, npx tsc --noEmit — and treated any failure as blocking. It caught real problems. Verification should be a gate, not an afterthought.
This is part of an ongoing series documenting the rebuild of a friend's food blog from WordPress to a custom Next.js stack, built with AI assistance.

Comments
Post a Comment