Status: Approved in brainstorming session 2026-07-07
Parent spec: docs/superpowers/specs/2026-07-07-pink-pulsar-plan-of-plans-design.md (§7 MP-0a, §3 architecture, §5 UI/UX stack)
Scope: One PR-sized increment — turn the fresh Astro 7 starter into a production-ready skeleton with strict TS, Tailwind v4, real shadcn React components (server-rendered = zero JS), SEO head, native fonts, ESLint isolation rules, Prettier, and Vitest. No business logic, no pages beyond a smoke-test home page.
Turn the fresh Astro 7 starter (src/ has default Welcome.astro, Layout.astro, index.astro only) into a production-ready skeleton that every subsequent mini-plan builds on. MP-0a establishes: strict TypeScript, the design-token system, the component approach (real shadcn React via @astrojs/react, server-rendered by default), the SEO head, native font optimization, lint/format/test tooling, and the layer-isolation rule enforcement.
tsconfig.json: strict mode +noUncheckedIndexedAccess+exactOptionalPropertyTypes+@/*path alias (shadcn convention)@astrojs/reactintegration + React 19- Tailwind v4 via
@tailwindcss/vite+src/styles/global.css - Astro native Fonts API (stable in Astro 7): Inter via Fontsource,
--font-sans - shadcn CLI init (
components.json) + 4 base components:Button,Input,Card,Labelas.tsxinsrc/components/ui/ - shadcn CSS-variable design tokens (pink primary, light-only) in
src/styles/tokens.css cn()helper insrc/lib/utils.ts(clsx + tailwind-merge)MetaHead.astroSEO component (Astro — server-only, no interactivity)Image.astrothin wrapper around Astro<Image>fromastro:assetsLayout.astrowith<head>(Font + MetaHead + global CSS) + slot- ESLint 10 flat config with
no-restricted-imports(§3.5 isolation) +eslint-plugin-astro - Prettier with
prettier-plugin-astro+prettier-plugin-tailwindcss - Vitest via
getViteConfig()fromastro/config+astro/containerContainer API .dev.vars(gitignored) +.dev.vars.example(committed) skeletonastro.config.mjs:siteset,fontsconfig,@astrojs/react+ Tailwind Vite pluginspackage.json: bump Node engine>=22.13.0, add scripts (lint,lint:fix,format,format:check,test,test:watch,typecheck)- Smoke-test home page at
src/pages/index.astro(Layout + Card + Button, zero JS) - Removal of starter cruft:
Welcome.astro,src/assets/astro.svg,src/assets/background.svg, starter README content
- D1/KV/R2/Analytics Engine bindings (MP-0b)
- CI/CD pipeline (MP-0c)
- Middleware, env accessor, logger, error taxonomy, rate-limit,
/health,/rum(MP-0d) - Any feature pages (home/product/catalog/legal/admin — MP-2 onward)
- Drizzle ORM (MP-1)
- Interactive React islands with
client:*(MP-3 onward — MP-0a uses zeroclient:*) - JSON-LD structured data (MP-2, MP-12)
- Image responsive layout config (MP-2, MP-13)
- Lighthouse CI thresholds (MP-0c configures CI; MP-0a just keeps the path clean)
shadcn React components in src/components/ui/ are used without client:* directives on prerendered pages. Astro renders them to static HTML at build time and ships zero React JS. The @astrojs/react integration enables server-side rendering of React components; React runtime only ships when a client:* directive is added (later MPs: cart, checkout, admin). This preserves the Lighthouse 100 target on the storefront while giving a consistent shadcn API everywhere.
| Decision | Choice | Rationale |
|---|---|---|
| Site URL | https://pink-pulsar.workers.dev |
Cloudflare Workers default domain; update when custom domain is purchased |
| Brand name | "Pink Pulsar" | Drives <title>, footer, OG tags |
| Font | Inter via Fontsource (variable, weights 400–700, latin, normal) | Privacy-friendly (downloaded at build, served from own site), Lighthouse-friendly, single family |
| Color theme | Pink primary (hsl(330 81% 60%)) on neutral (zinc) base, light-only |
Brand-aligned; dark mode deferred to a future MP |
| Base components | Button, Input, Card, Label (4) | Proves the token system end-to-end; covers common immediate needs; later MPs add more via shadcn CLI |
| Component approach | Real shadcn React via @astrojs/react + shadcn CLI |
Per §5 amendment — server-rendered (zero JS) on prerendered pages, islands where interactive |
| Path alias | @/* → ./src/* |
shadcn CLI convention — generated imports use @/components/ui/*, @/lib/utils |
| Linter | ESLint 10 flat config + eslint-plugin-astro + typescript-eslint + no-restricted-imports (§3.5) |
Latest ESLint; Astro-recommended plugin; enforces layer isolation |
| Formatter | Prettier + prettier-plugin-astro + prettier-plugin-tailwindcss (last in plugins) |
Official Astro Prettier plugin; Tailwind class sorting |
| Test runner | Vitest via getViteConfig() from astro/config + astro/container Container API |
Per §4 amendment — no astro test CLI exists in Astro 7 |
| Node engine | >=22.13.0 |
ESLint 10 requirement |
| Starter cleanup | Remove Welcome.astro, starter SVGs, starter README |
Fresh foundation |
.
├── astro.config.mjs # modify: site, fonts, @astrojs/react, vite.tailwind plugin
├── package.json # modify: engines, scripts, deps (react), devDeps
├── tsconfig.json # modify: paths (@/*), strict flags, jsx: react-jsx
├── pnpm-workspace.yaml # modify: allowBuilds (esbuild, sharp, workerd = true)
├── components.json # new — shadcn config (created by `shadcn init`)
├── .dev.vars # new (gitignored) — placeholder secrets
├── .dev.vars.example # new (committed) — template
├── eslint.config.mjs # new — flat config + §3.5 isolation rules
├── .prettierrc # new — astro + tailwind plugins
├── .prettierignore # new
├── vitest.config.ts # new — getViteConfig() from astro/config
├── src/
│ ├── styles/
│ │ ├── global.css # @import "tailwindcss"; @import tokens; @theme inline; base layer
│ │ └── tokens.css # shadcn CSS vars (:root, pink primary, light-only)
│ ├── lib/
│ │ └── utils.ts # cn() helper (clsx + tailwind-merge) — created by shadcn init
│ ├── layouts/
│ │ └── Layout.astro # modify: head + Font + MetaHead + slot
│ ├── components/
│ │ ├── ui/ # shadcn React components (.tsx, created by `shadcn add`)
│ │ │ ├── button.tsx
│ │ │ ├── input.tsx
│ │ │ ├── card.tsx
│ │ │ └── label.tsx
│ │ ├── seo/
│ │ │ └── MetaHead.astro # Astro — server-only SEO head
│ │ └── Image.astro # Astro wrapper around astro:assets <Image>
│ ├── env.d.ts # Astro env type definitions (extend if needed)
│ └── pages/
│ └── index.astro # modify: smoke-test home (Layout + Card + Button, zero JS)
├── tests/
│ ├── smoke.test.ts # Layout renders, components render, no astro-island, meta tags present
│ └── lint.test.ts # shells out to pnpm lint + pnpm format:check, asserts exit 0
└── (delete: src/components/Welcome.astro, src/assets/astro.svg, src/assets/background.svg)
import { defineConfig, fontProviders } from "astro/config";
import cloudflare from "@astrojs/cloudflare";
import react from "@astrojs/react";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
site: "https://pink-pulsar.workers.dev",
adapter: cloudflare(),
integrations: [react()],
vite: { plugins: [tailwindcss()] },
fonts: [
{
provider: fontProviders.fontsource(),
name: "Inter",
cssVariable: "--font-sans",
weights: ["400 700"],
styles: ["normal"],
subsets: ["latin"],
},
],
});imageresponsive defaults deferred to MP-2/MP-13.- No
outputoverride — Astro 7 default isstatic(SSG), which is the Lighthouse-100 path. Later MPs addprerender = falseon dynamic routes.
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*", "./worker-configuration.d.ts"],
"exclude": ["dist"],
"compilerOptions": {
"baseUrl": ".",
"paths": { "@/*": ["./src/*"] },
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"jsx": "react-jsx"
}
}@/*alias matches shadcn CLI convention (generated imports use@/components/ui/*).jsx: "react-jsx"required for.tsxshadcn components.
Engines: "node": ">=22.13.0" (ESLint 10 requirement).
Add deps:
react,react-dom@astrojs/react
Add devDeps:
tailwindcss,@tailwindcss/viteeslint,@eslint/js,typescript-eslint,eslint-plugin-astro,eslint-config-prettierprettier,prettier-plugin-astro,prettier-plugin-tailwindcssvitestclsx,tailwind-merge,class-variance-authority,lucide-react(shadcn deps — installed byshadcn init)@types/react,@types/react-dom
Add scripts:
{
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "astro check"
}allowBuilds:
esbuild: true
sharp: true
workerd: true(Required for Astro build with Cloudflare adapter + image optimization.)
{
"tailwind": {
"config": "",
"css": "src/styles/global.css",
"baseColor": "zinc",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui"
},
"iconLibrary": "lucide"
}# Razorpay (MP-5)
RAZORPAY_KEY=
RAZORPAY_SECRET=
# SMTP (MP-6b)
SMTP_URL=
# Admin shared secret (MP-7)
ADMIN_SECRET=
# HMAC signing key for signed links (MP-4, MP-8)
SIGNING_KEY=
.dev.vars is the same file with real placeholder values, gitignored (already covered by .gitignore .env rule — but MP-0a adds explicit .dev.vars entry to .gitignore).
{
"plugins": ["prettier-plugin-astro", "prettier-plugin-tailwindcss"],
"overrides": [{ "files": "*.astro", "options": { "parser": "astro" } }]
}prettier-plugin-tailwindcss must be the last entry in the plugins array (per Tailwind docs).
dist/
.astro/
.wrangler/
node_modules/
pnpm-lock.yaml
/// <reference types="vitest/config" />
import { getViteConfig } from "astro/config";
export default getViteConfig({
test: {
environment: "node",
},
});- Uses Astro's
getViteConfig()helper so Vitest inherits the Astro project config (paths, integrations, Vite plugins). - Component rendering tests use
experimental_AstroContainerfromastro/container(imported in test files, not configured here). environment: 'node'— Astro Container API renders to strings server-side; no DOM needed for MP-0a smoke tests.
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 330 81% 60%;
--primary-foreground: 0 0% 100%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 330 81% 60%;
--radius: 0.5rem;
}- No
.darkblock — light-only for v1. A future dark-mode MP adds.dark { ... }overrides. --ringmatches--primary(pink) for focus rings.- shadcn components consume these via
hsl(var(--token))in their Tailwind classes.
@import "tailwindcss";
@import "./tokens.css";
@theme inline {
--font-sans: var(--font-sans);
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--radius-lg: var(--radius);
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
font-family: var(--font-sans);
}
}@import "tailwindcss"activates Tailwind v4.@theme inlinemaps shadcn HSL tokens to Tailwind v4 theme tokens sobg-primary,text-foreground,border-borderetc. work.--font-sansis wired by the Astro Fonts API (the<Font cssVariable="--font-sans" />component in Layout emits the@font-face+ CSS variable).
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}Created by pnpm dlx shadcn@latest add button input card label. Standard shadcn output:
button.tsx—cvavariants (default / destructive / outline / secondary / ghost / link) + sizes (default / sm / lg / icon), usescn().input.tsx— styled<input>.card.tsx— exportsCard,CardHeader,CardTitle,CardDescription,CardContent,CardFooter.label.tsx— wraps@radix-ui/react-label.
No manual authoring — CLI generates them. Imported via @/components/ui/button, @/components/ui/card, etc.
Server-rendered by default: in .astro pages, import and render directly in frontmatter. Astro renders these to static HTML at build time. Zero React JS shipped unless a client:* directive is added (which MP-0a does not do).
---
interface Props {
title: string;
description: string;
canonical?: string;
image?: string;
noindex?: boolean;
}
const { title, description, canonical, image, noindex } = Astro.props;
const site = Astro.site ?? new URL(Astro.url.origin);
const canonicalURL = canonical
? new URL(canonical, site)
: new URL(Astro.url.pathname, site);
const ogImage = image ? new URL(image, site) : undefined;
---
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonicalURL.href} />
<meta name="robots" content={noindex ? "noindex, nofollow" : "index, follow"} />
<meta property="og:type" content="website" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonicalURL.href} />
{ogImage && <meta property="og:image" content={ogImage.href} />}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
{ogImage && <meta name="twitter:image" content={ogImage.href} />}- No JSON-LD here — per-page JSON-LD (
Product,Offer,BreadcrumbList) is added in MP-2. - MP-0a establishes the base meta component; later MPs extend it as needed (e.g.
hreflangstub in MP-12).
---
import { Image as AstroImage } from "astro:assets";
interface Props {
src: ImageMetadata;
alt: string;
width?: number;
height?: number;
loading?: "eager" | "lazy";
class?: string;
}
const {
src,
alt,
width,
height,
loading = "lazy",
class: className,
} = Astro.props;
---
<AstroImage
src={src}
alt={alt}
width={width}
height={height}
loading={loading}
class={className}
/>- Astro-native (not shadcn) — for storefront product images in MP-1/MP-2.
- Wraps
astro:assets<Image>with project defaults (loading="lazy").
---
import { Font } from "astro:assets";
import "../styles/global.css";
import MetaHead from "@/components/seo/MetaHead.astro";
interface Props {
title: string;
description: string;
canonical?: string;
image?: string;
noindex?: boolean;
}
const props = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<Font cssVariable="--font-sans" preload />
<MetaHead {...props} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
</head>
<body>
<slot />
</body>
</html>- Imports
global.css(which importstokens.css+ Tailwind). <Font>emits the@font-facefor Inter + the--font-sansCSS variable + preload link.<MetaHead>handles all SEO meta. Props passed through from the page.- No header/footer/nav yet — those come in MP-2 (storefront layout) and MP-9 (footer with legal links).
---
import Layout from "@/layouts/Layout.astro";
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
---
<Layout title="Pink Pulsar" description="Pink Pulsar storefront.">
<main class="mx-auto max-w-2xl p-8">
<Card>
<CardHeader>
<CardTitle>Pink Pulsar</CardTitle>
<CardDescription
>Skeleton ready. Storefront coming in MP-2.</CardDescription
>
</CardHeader>
<CardContent>
<Button>Get started</Button>
</CardContent>
</Card>
</main>
</Layout>- Zero JS shipped — no
client:*directive. Astro renders the ReactCardandButtonto static HTML at build. - Replaces the starter
Welcome.astrocontent. - This page will be replaced by the real home page in MP-2.
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import astro from "eslint-plugin-astro";
import prettierConfig from "eslint-config-prettier";
export default tseslint.config(
{ ignores: ["dist/", ".astro/", ".wrangler/", "node_modules/"] },
eslint.configs.recommended,
...tseslint.configs.recommended,
...astro.configs.recommended,
// per-file no-restricted-imports blocks (§7.2)
prettierConfig, // must be last — disables conflicting style rules
);Enforced on .ts files (and .astro frontmatter imports via typescript-eslint). eslint-plugin-astro does not support no-restricted-imports cleanly on .astro template body, but frontmatter imports are TS and covered.
src/actions/**/*.ts — may import services + core only:
{
files: ["src/actions/**/*.ts"],
rules: {
"no-restricted-imports": ["error", {
patterns: [
{ group: ["src/lib/bindings/*", "@/lib/bindings/*"], message: "Actions may import services + core only, never concrete adapters." },
{ group: ["src/features/*/repository"], message: "Actions import services, not repositories directly." },
],
}],
},
}src/features/**/service.ts — may import own repository (interface), binding types (interfaces only), db schemas, core; blocked from concrete adapters + other features' internals:
{
files: ["src/features/**/service.ts"],
rules: {
"no-restricted-imports": ["error", {
patterns: [
{ group: ["src/lib/bindings/*/!(types)*", "@/lib/bindings/*/!(types)*"], message: "Services import binding *types.ts* (interfaces) only — never concrete adapters. Wire adapters in src/lib/app/ instead." },
{ group: ["src/features/*/!(service|repository|types)"], message: "A feature service may not import another feature's internals." },
{ group: ["src/pages/*"], message: "Services must not import pages." },
{ group: ["src/actions/*"], message: "Services must not import actions." },
],
}],
},
}src/lib/bindings/**/*.ts + src/lib/db/**/*.ts — must not import features/pages/actions (reverse dependency):
{
files: ["src/lib/bindings/**/*.ts", "src/lib/db/**/*.ts"],
rules: {
"no-restricted-imports": ["error", {
patterns: [
{ group: ["src/features/*", "@/features/*"], message: "Bindings/DB must not import features (reverse dependency)." },
{ group: ["src/pages/*", "src/actions/*"], message: "Bindings/DB must not import pages or actions." },
],
}],
},
}src/lib/app/prod.ts + src/lib/app/test.ts — exception, these ARE allowed to import concrete adapters:
{
files: ["src/lib/app/prod.ts", "src/lib/app/test.ts"],
rules: { "no-restricted-imports": "off" },
}src/pages/**/*.astro — pages import services + components + layouts only:
{
files: ["src/pages/**/*.astro"],
rules: {
"no-restricted-imports": ["error", {
patterns: [
{ group: ["src/lib/bindings/*", "@/lib/bindings/*"], message: "Pages import services + components + layouts only — never bindings." },
{ group: ["src/lib/db/*", "@/lib/db/*"], message: "Pages must not import DB layer directly." },
{ group: ["src/features/*/repository"], message: "Pages import services, not repositories." },
],
}],
},
}The negation glob !(types) is a pattern approximation. The exact syntax may need tuning during implementation. The implementation plan includes a verification test that imports a forbidden path and asserts ESLint errors. If a pattern proves unenforceable via globs, fall back to a custom ESLint rule (~20 lines) that checks the import path against the §3.5 matrix. This is an implementation detail, not a spec change.
MP-0a creates the rule blocks. Most target directories (src/features/*, src/actions/*, src/lib/bindings/*, src/lib/db/*, src/lib/app/*) don't exist yet — they're created in MP-0b/MP-0d/MP-1+. The rules are still written now so that when those directories appear, isolation is enforced from day one. MP-0a's own files (src/components/ui/*, src/lib/utils.ts, src/components/seo/*, src/layouts/*, src/pages/*) are not blocked by any rule.
Uses Vitest + astro/container Container API:
import { experimental_AstroContainer as AstroContainer } from "astro/container";
import { describe, it, expect } from "vitest";
import Layout from "@/layouts/Layout.astro";
describe("MP-0a smoke", () => {
it("Layout renders with title, description, canonical, OG tags", async () => {
const container = await AstroContainer.create();
const result = await container.renderToString(Layout, {
slots: { default: "<p>smoke</p>" },
props: { title: "Test", description: "Test desc" },
});
expect(result).toContain("<title>Test</title>");
expect(result).toContain('<meta name="description" content="Test desc"');
expect(result).toContain('rel="canonical"');
expect(result).toContain("og:title");
expect(result).toContain("twitter:card");
expect(result).toContain("smoke");
});
it("Layout emits Font CSS variable and preload", async () => {
const container = await AstroContainer.create();
const result = await container.renderToString(Layout, {
props: { title: "T", description: "D" },
});
expect(result).toContain("--font-sans");
});
it("index page renders shadcn Card + Button as static HTML (no astro-island)", async () => {
const container = await AstroContainer.create();
// Render index.astro — assert Card title "Pink Pulsar" present
// and NO <astro-island> wrapper in output (zero-JS guarantee)
// (Implementation detail: import index.astro and renderToString it)
});
});Key assertion: shadcn components rendered without client:* produce plain HTML — no <astro-island> custom element, no hydration script. This is the Lighthouse-100 guarantee for the storefront path.
Thin test that shells out to pnpm lint and pnpm format:check and asserts exit 0. Keeps the "lint passes" + "format passes" gates enforceable from vitest run.
import { describe, it, expect } from "vitest";
import { spawnSync } from "node:child_process";
describe("MP-0a lint + format", () => {
it("pnpm lint exits 0", () => {
const result = spawnSync("pnpm", ["lint"], { stdio: "pipe", shell: true });
expect(result.status).toBe(0);
});
it("pnpm format:check exits 0", () => {
const result = spawnSync("pnpm", ["format:check"], {
stdio: "pipe",
shell: true,
});
expect(result.status).toBe(0);
});
});(spawnSync returns an object with .status; execSync throws on non-zero exit and returns a Buffer, so spawnSync is the correct choice for exit-code assertions.)
A test (or a manual verification step in the implementation plan) that creates a temporary file violating each no-restricted-imports rule and asserts ESLint errors. This proves the §3.5 enforcement is wired correctly. If done as a test, it lives in tests/isolation.test.ts and uses execSync('pnpm lint -- ...') on a fixture file.
# 1. Install all new deps
pnpm install
# 2. Initialize shadcn (generates components.json + src/lib/utils.ts + src/styles/global.css)
pnpm dlx shadcn@latest init
# 3. Add the 4 base components (generates src/components/ui/*.tsx)
pnpm dlx shadcn@latest add button input card label
# 4. Typecheck (astro check — strict passes)
pnpm typecheck
# 5. Lint (ESLint 10 flat config + isolation rules pass)
pnpm lint
# 6. Format check (Prettier passes)
pnpm format:check
# 7. Tests (vitest run — smoke + lint tests green)
pnpm test
# 8. Build (astro build — SSG build succeeds, dist/ contains static HTML for /)
pnpm build
# 9. Dev server boots
pnpm dev --background
# visit http://localhost:4321 — see Card + Button rendered as static HTML
# view source — no React JS shipped, no <astro-island> for Card/Button
astro dev stop-
pnpm typecheckexits 0 (strict TS passes, includingnoUncheckedIndexedAccess+exactOptionalPropertyTypes) -
pnpm lintexits 0 (ESLint 10 flat config + §3.5 isolation rules pass) -
pnpm format:checkexits 0 (Prettier passes) -
pnpm testexits 0 (all smoke + lint tests green) -
pnpm buildsucceeds —dist/contains static HTML for/ - Built
/page source has NO<astro-island>tags (zero JS shipped) — preserves Lighthouse 100 path - Built
/page source contains<title>Pink Pulsar</title>,<meta name="description">,<link rel="canonical">, OG tags, Twitter card tags - Built
/page source contains Inter font preload +--font-sansCSS variable - ESLint isolation rule verified: a test import of
@/lib/bindings/*from a feature service triggers an error (or the rule pattern is documented as enforceable) -
.dev.vars.examplecommitted;.dev.varsgitignored -
Welcome.astro,src/assets/astro.svg,src/assets/background.svgremoved -
package.jsonengines.nodeis>=22.13.0 -
@/*path alias resolves in both TS and Astro imports
Depends on: None. MP-0a is the first mini-plan. The plan-of-plans §4 amendments (real shadcn via @astrojs/react, Vitest via getViteConfig(), astro/zod naming, stable Fonts API) are already applied to the parent spec.
Unblocks: Every subsequent MP. Specifically:
- MP-0b (storage plumbing) — uses the
@/*alias, ESLint isolation rules, Vitest setup - MP-0c (CI/CD) — uses
pnpm typecheck,pnpm lint,pnpm test,pnpm buildscripts - MP-0d (cross-cutting runtime) — uses the Layout, ESLint rules, Vitest setup
- MP-1/MP-2 (catalog) — uses
Image.astro, shadcn components,MetaHead.astro, design tokens - MP-3+ (cart/checkout/admin) — use shadcn components as islands with
client:*
All decisions locked during the brainstorming session. No TBDs.