qualia-framework 6.8.1 → 6.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/install.js +159 -9
  3. package/bin/state.js +70 -5
  4. package/docs/EMPLOYEE-QUICKSTART.md +162 -0
  5. package/package.json +2 -1
  6. package/skills/qualia-doctor/SKILL.md +62 -0
  7. package/skills/qualia-new/REFERENCE.md +7 -0
  8. package/skills/qualia-new/SKILL.md +42 -0
  9. package/skills/qualia-report/SKILL.md +13 -0
  10. package/templates/stacks/README.md +110 -0
  11. package/templates/stacks/ai-agent/env.required.json +44 -0
  12. package/templates/stacks/ai-agent/phases.md +53 -0
  13. package/templates/stacks/ai-agent/scaffold/.env.example +12 -0
  14. package/templates/stacks/ai-agent/scaffold/README.md +31 -0
  15. package/templates/stacks/ai-agent/scaffold/app/api/chat/route.ts +33 -0
  16. package/templates/stacks/ai-agent/scaffold/app/globals.css +13 -0
  17. package/templates/stacks/ai-agent/scaffold/app/layout.tsx +19 -0
  18. package/templates/stacks/ai-agent/scaffold/app/page.tsx +12 -0
  19. package/templates/stacks/ai-agent/scaffold/evals/cases.json +23 -0
  20. package/templates/stacks/ai-agent/scaffold/lib/openrouter/client.ts +51 -0
  21. package/templates/stacks/ai-agent/scaffold/lib/prompts/system.ts +6 -0
  22. package/templates/stacks/ai-agent/scaffold/lib/supabase/client.ts +10 -0
  23. package/templates/stacks/ai-agent/scaffold/lib/supabase/server.ts +28 -0
  24. package/templates/stacks/ai-agent/scaffold/next.config.mjs +7 -0
  25. package/templates/stacks/ai-agent/scaffold/package.json +29 -0
  26. package/templates/stacks/ai-agent/scaffold/postcss.config.mjs +7 -0
  27. package/templates/stacks/ai-agent/scaffold/supabase/migrations/0001_init.sql +41 -0
  28. package/templates/stacks/ai-agent/scaffold/tsconfig.json +21 -0
  29. package/templates/stacks/ai-agent/stack.json +9 -0
  30. package/templates/stacks/ai-agent/verify-checklist.md +31 -0
  31. package/templates/stacks/full-app/env.required.json +20 -0
  32. package/templates/stacks/full-app/phases.md +45 -0
  33. package/templates/stacks/full-app/scaffold/.env.example +7 -0
  34. package/templates/stacks/full-app/scaffold/README.md +28 -0
  35. package/templates/stacks/full-app/scaffold/app/globals.css +14 -0
  36. package/templates/stacks/full-app/scaffold/app/layout.tsx +19 -0
  37. package/templates/stacks/full-app/scaffold/app/page.tsx +20 -0
  38. package/templates/stacks/full-app/scaffold/lib/supabase/client.ts +10 -0
  39. package/templates/stacks/full-app/scaffold/lib/supabase/server.ts +31 -0
  40. package/templates/stacks/full-app/scaffold/next.config.mjs +7 -0
  41. package/templates/stacks/full-app/scaffold/package.json +29 -0
  42. package/templates/stacks/full-app/scaffold/postcss.config.mjs +7 -0
  43. package/templates/stacks/full-app/scaffold/supabase/migrations/0001_init.sql +27 -0
  44. package/templates/stacks/full-app/scaffold/tsconfig.json +21 -0
  45. package/templates/stacks/full-app/stack.json +9 -0
  46. package/templates/stacks/full-app/verify-checklist.md +32 -0
  47. package/templates/stacks/internal-tool/env.required.json +20 -0
  48. package/templates/stacks/internal-tool/phases.md +45 -0
  49. package/templates/stacks/internal-tool/scaffold/.env.example +7 -0
  50. package/templates/stacks/internal-tool/scaffold/README.md +29 -0
  51. package/templates/stacks/internal-tool/scaffold/app/globals.css +13 -0
  52. package/templates/stacks/internal-tool/scaffold/app/layout.tsx +20 -0
  53. package/templates/stacks/internal-tool/scaffold/app/page.tsx +22 -0
  54. package/templates/stacks/internal-tool/scaffold/lib/supabase/client.ts +9 -0
  55. package/templates/stacks/internal-tool/scaffold/lib/supabase/server.ts +28 -0
  56. package/templates/stacks/internal-tool/scaffold/next.config.mjs +6 -0
  57. package/templates/stacks/internal-tool/scaffold/package.json +29 -0
  58. package/templates/stacks/internal-tool/scaffold/postcss.config.mjs +7 -0
  59. package/templates/stacks/internal-tool/scaffold/supabase/migrations/0001_init.sql +28 -0
  60. package/templates/stacks/internal-tool/scaffold/tsconfig.json +21 -0
  61. package/templates/stacks/internal-tool/stack.json +9 -0
  62. package/templates/stacks/internal-tool/verify-checklist.md +31 -0
  63. package/templates/stacks/landing-page/env.required.json +8 -0
  64. package/templates/stacks/landing-page/phases.md +42 -0
  65. package/templates/stacks/landing-page/scaffold/.env.example +3 -0
  66. package/templates/stacks/landing-page/scaffold/README.md +25 -0
  67. package/templates/stacks/landing-page/scaffold/app/globals.css +14 -0
  68. package/templates/stacks/landing-page/scaffold/app/layout.tsx +19 -0
  69. package/templates/stacks/landing-page/scaffold/app/page.tsx +21 -0
  70. package/templates/stacks/landing-page/scaffold/next.config.mjs +7 -0
  71. package/templates/stacks/landing-page/scaffold/package.json +26 -0
  72. package/templates/stacks/landing-page/scaffold/postcss.config.mjs +7 -0
  73. package/templates/stacks/landing-page/scaffold/tsconfig.json +21 -0
  74. package/templates/stacks/landing-page/stack.json +9 -0
  75. package/templates/stacks/landing-page/verify-checklist.md +28 -0
  76. package/tests/bin.test.sh +3 -3
  77. package/tests/state.test.sh +83 -0
@@ -0,0 +1,20 @@
1
+ import type { Metadata } from "next";
2
+ import "./globals.css";
3
+
4
+ export const metadata: Metadata = {
5
+ title: "Internal Tool",
6
+ // Internal portal — keep it out of search indexes.
7
+ robots: { index: false, follow: false },
8
+ };
9
+
10
+ export default function RootLayout({
11
+ children,
12
+ }: {
13
+ children: React.ReactNode;
14
+ }) {
15
+ return (
16
+ <html lang="en">
17
+ <body>{children}</body>
18
+ </html>
19
+ );
20
+ }
@@ -0,0 +1,22 @@
1
+ import { createClient } from "@/lib/supabase/server";
2
+
3
+ export default async function Home() {
4
+ const supabase = await createClient();
5
+ const {
6
+ data: { user },
7
+ } = await supabase.auth.getUser();
8
+
9
+ // Role lives in app_metadata (never user_metadata).
10
+ const role = (user?.app_metadata?.role as string | undefined) ?? "none";
11
+
12
+ return (
13
+ <main className="mx-auto flex min-h-dvh max-w-3xl flex-col justify-center gap-6 px-6">
14
+ <h1 className="text-4xl font-semibold tracking-tight">Internal Tool</h1>
15
+ <p className="text-lg text-neutral-600">
16
+ {user
17
+ ? `Signed in as ${user.email} (role: ${role}).`
18
+ : "Staff only — invite required. Wire portal auth + role gating in Phase 1."}
19
+ </p>
20
+ </main>
21
+ );
22
+ }
@@ -0,0 +1,9 @@
1
+ import { createBrowserClient } from "@supabase/ssr";
2
+
3
+ // Client-side Supabase adapter — publishable key only. RLS protects data.
4
+ export function createClient() {
5
+ return createBrowserClient(
6
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
7
+ process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!
8
+ );
9
+ }
@@ -0,0 +1,28 @@
1
+ import { createServerClient } from "@supabase/ssr";
2
+ import { cookies } from "next/headers";
3
+
4
+ // Server-side Supabase adapter — all mutations and RLS-protected reads.
5
+ export async function createClient() {
6
+ const cookieStore = await cookies();
7
+
8
+ return createServerClient(
9
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
10
+ process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
11
+ {
12
+ cookies: {
13
+ getAll() {
14
+ return cookieStore.getAll();
15
+ },
16
+ setAll(cookiesToSet) {
17
+ try {
18
+ cookiesToSet.forEach(({ name, value, options }) =>
19
+ cookieStore.set(name, value, options)
20
+ );
21
+ } catch {
22
+ // Server Component context — middleware refreshes the session.
23
+ }
24
+ },
25
+ },
26
+ }
27
+ );
28
+ }
@@ -0,0 +1,6 @@
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ // Internal portal — no public SEO. The root layout sets robots: noindex.
4
+ };
5
+
6
+ export default nextConfig;
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "internal-tool",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "next": "^16.0.0",
14
+ "react": "^19.0.0",
15
+ "react-dom": "^19.0.0",
16
+ "@supabase/supabase-js": "^2.45.0",
17
+ "@supabase/ssr": "^0.5.0",
18
+ "zod": "^3.23.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^22.0.0",
22
+ "@types/react": "^19.0.0",
23
+ "@types/react-dom": "^19.0.0",
24
+ "tailwindcss": "^4.0.0",
25
+ "@tailwindcss/postcss": "^4.0.0",
26
+ "postcss": "^8.4.0",
27
+ "typescript": "^5.6.0"
28
+ }
29
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
@@ -0,0 +1,28 @@
1
+ -- Initial migration. Internal portal: roles gate everything; RLS on every
2
+ -- table from the first migration. Replace `records` with your real domain.
3
+
4
+ create table if not exists public.records (
5
+ id uuid primary key default gen_random_uuid(),
6
+ owner_id uuid references auth.users (id) on delete set null,
7
+ title text not null,
8
+ status text not null default 'open',
9
+ deleted_at timestamptz, -- soft-delete (constitution: where flagged)
10
+ created_at timestamptz not null default now()
11
+ );
12
+
13
+ alter table public.records enable row level security;
14
+
15
+ -- Authenticated staff may read non-deleted records.
16
+ create policy "records_select_staff"
17
+ on public.records for select
18
+ using (auth.role() = 'authenticated' and deleted_at is null);
19
+
20
+ -- Admins (role in app_metadata) may write. Matching SELECT exists above.
21
+ create policy "records_write_admin"
22
+ on public.records for all
23
+ using (
24
+ coalesce(auth.jwt() -> 'app_metadata' ->> 'role', '') = 'admin'
25
+ )
26
+ with check (
27
+ coalesce(auth.jwt() -> 'app_metadata' ->> 'role', '') = 'admin'
28
+ );
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["dom", "dom.iterable", "ES2022"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [{ "name": "next" }],
17
+ "paths": { "@/*": ["./*"] }
18
+ },
19
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
20
+ "exclude": ["node_modules"]
21
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "internal-tool",
3
+ "title": "Internal Tool",
4
+ "description": "Staff-facing portal — Supabase auth (invite/SSO, no public signup), role-gated dashboard, Tailwind, Vercel. Built for a known set of internal users.",
5
+ "archetype": "web-app",
6
+ "defaultStack": "Next.js 16 (App Router) · Supabase (portal auth + RLS) · Tailwind · Vercel",
7
+ "scaffoldEntry": "app/page.tsx",
8
+ "whenToUse": "Admin panel, ops dashboard, internal CRM — a known set of staff users, no public signup. Pick this over full-app when the audience is employees, access is invite-only, and roles gate everything."
9
+ }
@@ -0,0 +1,31 @@
1
+ # Internal Tool — Definition of Done
2
+
3
+ Per-stack verify checklist. Inherits `rules/constitution.md` and the `web-app` archetype DoD.
4
+ Internal/living product → runs as a **rolling release**; each increment clears this DoD, no terminal Handoff.
5
+
6
+ ## Portal auth & access (constitution)
7
+ - [ ] Public signup disabled — staff join by invite / SSO only.
8
+ - [ ] Roles stored in `app_metadata` (never `user_metadata`); role-based routing in middleware AND data layer.
9
+ - [ ] **RLS enabled on every table**; verified as two roles — each sees only its scope.
10
+ - [ ] Every UPDATE policy has a matching SELECT policy.
11
+ - [ ] Postgres views set `security_invoker = true`.
12
+ - [ ] Sessions revoked before a user is deleted/deactivated.
13
+
14
+ ## Security
15
+ - [ ] `SUPABASE_SERVICE_ROLE_KEY` server-only; used only in server actions (e.g. inviting users).
16
+ - [ ] Mutations use `lib/supabase/server.ts`; inputs validated with Zod.
17
+ - [ ] Security headers (HSTS); MFA on Supabase/Vercel accounts.
18
+
19
+ ## App quality
20
+ - [ ] Loading / empty / error states on every async surface.
21
+ - [ ] Destructive actions confirm; soft-delete + audit where flagged.
22
+ - [ ] Rate limiting on mutating endpoints; no N+1 on list views.
23
+
24
+ ## Schema flow
25
+ - [ ] All schema changes are migrations in `supabase/migrations/`.
26
+ - [ ] `npx supabase gen types` run after schema changes.
27
+
28
+ ## Build & Deploy
29
+ - [ ] `npx tsc --noEmit` exits 0; `next build` succeeds.
30
+ - [ ] Deploys to Vercel; HTTP 200; auth callback + role isolation verified.
31
+ - [ ] Design anti-slop bar met; a11y AA.
@@ -0,0 +1,8 @@
1
+ [
2
+ {
3
+ "name": "NEXT_PUBLIC_ANALYTICS_ID",
4
+ "purpose": "Optional analytics/measurement id for the marketing site (e.g. Plausible domain, GA4 id). The landing page runs without it.",
5
+ "howToObtain": "Create a property in your analytics provider and copy the site/measurement id. Skip if you are not tracking conversions yet.",
6
+ "ownerIssued": false
7
+ }
8
+ ]
@@ -0,0 +1,42 @@
1
+ # Landing Page — Phase Plan
2
+
3
+ Stack: Next.js 16 (App Router, SSG) · Tailwind · Vercel. No database, no auth.
4
+ Archetype: `website`. This is the seed plan `/qualia-new` copies into ROADMAP.md.
5
+
6
+ ## Phase 1: Foundation & Design System
7
+
8
+ **Goal:** The site renders on a Vercel preview URL with the design system wired (tokens, fonts, OKLCH palette) and a base layout (nav + footer).
9
+
10
+ **Success criteria:**
11
+ 1. `npm run dev` renders the homepage with no console errors.
12
+ 2. DESIGN.md tokens are live (CSS variables, distinctive type pair — never Inter/Arial/system-ui).
13
+ 3. Nav + footer render and are responsive at 375px and 1440px.
14
+ 4. Deploys to a Vercel preview URL.
15
+
16
+ ## Phase 2: Hero & Core Sections
17
+
18
+ **Goal:** The above-the-fold hero plus the primary value sections (features, social proof) are built with real copy.
19
+
20
+ **Success criteria:**
21
+ 1. Hero communicates the offer in one glance; primary CTA is present and reachable.
22
+ 2. Feature/benefit sections render with real copy — no lorem ipsum.
23
+ 3. All sections responsive, with loading-free SSG output.
24
+
25
+ ## Phase 3: Conversion & Contact
26
+
27
+ **Goal:** The conversion path works — CTA → contact form or external link, with success/error states.
28
+
29
+ **Success criteria:**
30
+ 1. Contact form (or CTA link) submits and shows a success state; errors are handled.
31
+ 2. If a form posts anywhere, it validates client-side (Zod or equivalent) — no DB needed.
32
+ 3. Analytics event fires on the primary CTA.
33
+
34
+ ## Phase 4: Polish & Launch
35
+
36
+ **Goal:** Production-ready — SEO meta, a11y, performance, custom domain.
37
+
38
+ **Success criteria:**
39
+ 1. SEO: title/description/OG tags per page; sitemap; favicon.
40
+ 2. a11y: alt text, labels, keyboard nav, AA contrast.
41
+ 3. Lighthouse performance/SEO green on the homepage.
42
+ 4. Custom domain wired; production deploy + smoke (HTTP 200).
@@ -0,0 +1,3 @@
1
+ # Landing Page — no database, no auth. Nothing required to run locally.
2
+ # Optional analytics (uncomment if you wire it):
3
+ # NEXT_PUBLIC_ANALYTICS_ID=
@@ -0,0 +1,25 @@
1
+ # Landing Page
2
+
3
+ Static/SSG marketing site — Next.js 16 + Tailwind + Vercel. No database, no auth.
4
+
5
+ ## Run
6
+
7
+ ```bash
8
+ npm install
9
+ npm run dev # http://localhost:3000
10
+ ```
11
+
12
+ ## Ship
13
+
14
+ ```bash
15
+ npm run build
16
+ vercel --prod
17
+ ```
18
+
19
+ ## Where things live
20
+
21
+ - `app/page.tsx` — the landing page itself (replace the hero copy).
22
+ - `app/globals.css` — OKLCH design tokens. Source of truth is `.planning/DESIGN.md`.
23
+ - `.env.example` — nothing required; copy to `.env.local` only if you add analytics.
24
+
25
+ This is an MVP skeleton. Build the real sections per `.planning/ROADMAP.md`.
@@ -0,0 +1,14 @@
1
+ @import "tailwindcss";
2
+
3
+ /* Define your OKLCH palette here. Neutrals tinted toward the brand hue.
4
+ NEVER raw #000 / #fff. See .planning/DESIGN.md for the committed tokens. */
5
+ :root {
6
+ --brand: oklch(0.62 0.18 264);
7
+ --ink: oklch(0.21 0.01 264);
8
+ --paper: oklch(0.99 0.004 264);
9
+ }
10
+
11
+ body {
12
+ background: var(--paper);
13
+ color: var(--ink);
14
+ }
@@ -0,0 +1,19 @@
1
+ import type { Metadata } from "next";
2
+ import "./globals.css";
3
+
4
+ export const metadata: Metadata = {
5
+ title: "Landing Page",
6
+ description: "Replace with your product's one-sentence pitch.",
7
+ };
8
+
9
+ export default function RootLayout({
10
+ children,
11
+ }: {
12
+ children: React.ReactNode;
13
+ }) {
14
+ return (
15
+ <html lang="en">
16
+ <body>{children}</body>
17
+ </html>
18
+ );
19
+ }
@@ -0,0 +1,21 @@
1
+ export default function Home() {
2
+ return (
3
+ <main className="mx-auto flex min-h-dvh max-w-3xl flex-col justify-center gap-6 px-6">
4
+ <h1 className="text-balance text-4xl font-semibold tracking-tight sm:text-6xl">
5
+ Your one-sentence pitch goes here.
6
+ </h1>
7
+ <p className="text-lg text-neutral-600">
8
+ Replace this hero with real copy. No lorem ipsum — the page is the
9
+ product.
10
+ </p>
11
+ <div>
12
+ <a
13
+ href="#contact"
14
+ className="inline-flex h-11 items-center rounded-md bg-neutral-900 px-6 font-medium text-white"
15
+ >
16
+ Primary CTA
17
+ </a>
18
+ </div>
19
+ </main>
20
+ );
21
+ }
@@ -0,0 +1,7 @@
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ // Marketing site: keep it static where possible. Add `output: "export"`
4
+ // if the host is purely static (no Vercel server runtime needed).
5
+ };
6
+
7
+ export default nextConfig;
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "landing-page",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "next": "^16.0.0",
14
+ "react": "^19.0.0",
15
+ "react-dom": "^19.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^22.0.0",
19
+ "@types/react": "^19.0.0",
20
+ "@types/react-dom": "^19.0.0",
21
+ "tailwindcss": "^4.0.0",
22
+ "@tailwindcss/postcss": "^4.0.0",
23
+ "postcss": "^8.4.0",
24
+ "typescript": "^5.6.0"
25
+ }
26
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["dom", "dom.iterable", "ES2022"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [{ "name": "next" }],
17
+ "paths": { "@/*": ["./*"] }
18
+ },
19
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
20
+ "exclude": ["node_modules"]
21
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "landing-page",
3
+ "title": "Landing Page",
4
+ "description": "A static/SSG marketing site — hero, features, CTA, contact. No database, no auth. Built to convert, fast to ship.",
5
+ "archetype": "website",
6
+ "defaultStack": "Next.js 16 (App Router, SSG) · Tailwind · Vercel",
7
+ "scaffoldEntry": "app/page.tsx",
8
+ "whenToUse": "Marketing site, product landing, portfolio, throwaway prototype. Pick this when there is no logged-in user and no persistent data — the page IS the product."
9
+ }
@@ -0,0 +1,28 @@
1
+ # Landing Page — Definition of Done
2
+
3
+ Per-stack verify checklist. Inherits `rules/constitution.md` and the `website` archetype DoD.
4
+
5
+ ## Build & Deploy
6
+ - [ ] `npx tsc --noEmit` exits 0.
7
+ - [ ] `next build` succeeds; output is static/SSG (no accidental server-only data fetch on a marketing page).
8
+ - [ ] Deploys to Vercel; homepage returns HTTP 200.
9
+ - [ ] Custom domain wired (production).
10
+
11
+ ## Design (anti-slop)
12
+ - [ ] DESIGN.md committed; distinctive type pair (NOT Inter/Roboto/Arial/Helvetica/system-ui).
13
+ - [ ] OKLCH palette; neutrals tinted toward brand hue; no raw `#000`/`#fff`.
14
+ - [ ] 8px spacing grid + fluid `clamp()` page padding.
15
+ - [ ] Responsive at 375px and 1440px; touch targets ≥44px.
16
+
17
+ ## Content & Conversion
18
+ - [ ] Real copy — zero "lorem ipsum", zero placeholder text.
19
+ - [ ] Primary CTA present, reachable, and instrumented with an analytics event.
20
+ - [ ] Contact form (if any) validates client-side and has success + error states.
21
+
22
+ ## SEO & a11y
23
+ - [ ] Per-page title + meta description + OG tags; sitemap + favicon present.
24
+ - [ ] Alt text on every image; labels on every input; keyboard navigable.
25
+ - [ ] AA contrast verified.
26
+
27
+ ## Out of scope (by design)
28
+ - No Supabase, no auth, no `SUPABASE_SERVICE_ROLE_KEY`. If the project needs accounts or persisted data, it is a `full-app`, not a `landing-page`.
package/tests/bin.test.sh CHANGED
@@ -1466,10 +1466,10 @@ mkdir -p "$TMP/.codex"
1466
1466
  echo "OLD USER CONTENT" > "$TMP/.codex/AGENTS.md"
1467
1467
  printf 'QS-FAWZI-11\n2\n' | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/log.txt" 2>&1
1468
1468
  EXIT=$?
1469
- BAK_COUNT=$(ls "$TMP/.codex/"AGENTS.md.bak.* 2>/dev/null | wc -l)
1469
+ BAK_COUNT=$(ls "$TMP/.codex/backups/"AGENTS.md.bak.* 2>/dev/null | wc -l)
1470
1470
  if [ "$EXIT" -eq 0 ] \
1471
1471
  && [ "$BAK_COUNT" -ge 1 ] \
1472
- && grep -q "OLD USER CONTENT" "$TMP/.codex/"AGENTS.md.bak.* \
1472
+ && grep -q "OLD USER CONTENT" "$TMP/.codex/backups/"AGENTS.md.bak.* \
1473
1473
  && ! grep -q "OLD USER CONTENT" "$TMP/.codex/AGENTS.md"; then
1474
1474
  pass "Codex AGENTS.md backup-before-overwrite preserves prior content"
1475
1475
  else
@@ -1482,7 +1482,7 @@ TMP=$(mktmp)
1482
1482
  printf 'QS-FAWZI-11\n2\n' | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/log.txt" 2>&1
1483
1483
  # Re-run with same input — content should be identical, no new backup.
1484
1484
  printf 'QS-FAWZI-11\n2\n' | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/log2.txt" 2>&1
1485
- BAK_COUNT=$(ls "$TMP/.codex/"AGENTS.md.bak.* 2>/dev/null | wc -l)
1485
+ BAK_COUNT=$(ls "$TMP/.codex/backups/"AGENTS.md.bak.* 2>/dev/null | wc -l)
1486
1486
  if [ "$BAK_COUNT" -eq 0 ]; then
1487
1487
  pass "Codex re-install with identical content → no redundant .bak"
1488
1488
  else
@@ -1011,6 +1011,89 @@ else
1011
1011
  fail_case "close-milestone + init" "got=$RESULT expected=2,4,1,1,1,0"
1012
1012
  fi
1013
1013
 
1014
+ # 46b. close-milestone resolves the REAL milestone name from JOURNEY.md
1015
+ # instead of emitting a "Milestone N" placeholder when tracking.json has
1016
+ # no (or a placeholder) milestone_name.
1017
+ TMP=$(make_project)
1018
+ cat > "$TMP/.planning/JOURNEY.md" <<'JOURNEY'
1019
+ # Journey
1020
+
1021
+ ## Milestone 1 · Foundation Arc
1022
+
1023
+ Some prose.
1024
+
1025
+ ## Milestone 2 · Core Features
1026
+ JOURNEY
1027
+ $NODE -e "
1028
+ const fs = require('fs');
1029
+ const t = JSON.parse(fs.readFileSync('$TMP/.planning/tracking.json','utf8'));
1030
+ t.milestone_name = 'Milestone 1'; // placeholder — must lose to JOURNEY.md
1031
+ fs.writeFileSync('$TMP/.planning/tracking.json', JSON.stringify(t, null, 2));
1032
+ "
1033
+ (cd "$TMP" && $NODE "$STATE_JS" close-milestone --force >/dev/null 2>&1)
1034
+ RESULT=$($NODE -e "
1035
+ const t = JSON.parse(require('fs').readFileSync('$TMP/.planning/tracking.json','utf8'));
1036
+ console.log(t.milestones[0].name + '|' + t.milestone_name);
1037
+ ")
1038
+ if [ "$RESULT" = "Foundation Arc|Core Features" ]; then
1039
+ pass "close-milestone resolves real name from JOURNEY.md (no placeholder)"
1040
+ else
1041
+ fail_case "close-milestone JOURNEY name resolution" "got=$RESULT expected='Foundation Arc|Core Features'"
1042
+ fi
1043
+
1044
+ # 46c. close-milestone dedupes: same real name already logged under a
1045
+ # different (legacy string) num updates that entry instead of pushing a
1046
+ # duplicate.
1047
+ TMP=$(make_project)
1048
+ cat > "$TMP/.planning/JOURNEY.md" <<'JOURNEY'
1049
+ ## Milestone 1 · Foundation Arc
1050
+ JOURNEY
1051
+ $NODE -e "
1052
+ const fs = require('fs');
1053
+ const t = JSON.parse(fs.readFileSync('$TMP/.planning/tracking.json','utf8'));
1054
+ t.milestone_name = '';
1055
+ t.milestones = [{ num: '9', name: 'Foundation Arc', total_phases: 2, phases_completed: 2, tasks_completed: 0, shipped_url: '', closed_at: '' }];
1056
+ fs.writeFileSync('$TMP/.planning/tracking.json', JSON.stringify(t, null, 2));
1057
+ "
1058
+ (cd "$TMP" && $NODE "$STATE_JS" close-milestone --force >/dev/null 2>&1)
1059
+ RESULT=$($NODE -e "
1060
+ const t = JSON.parse(require('fs').readFileSync('$TMP/.planning/tracking.json','utf8'));
1061
+ console.log(t.milestones.length + '|' + t.milestones[0].num + '|' + t.milestones[0].name);
1062
+ ")
1063
+ if [ "$RESULT" = "1|1|Foundation Arc" ]; then
1064
+ pass "close-milestone dedupes same-name entry under different num"
1065
+ else
1066
+ fail_case "close-milestone name dedupe" "got=$RESULT expected='1|1|Foundation Arc'"
1067
+ fi
1068
+
1069
+ # 46d. backfill-milestones repairs a placeholder-named real-close entry in
1070
+ # place (fixes the name, keeps the richer close data like tasks_completed).
1071
+ TMP=$(make_project)
1072
+ cat > "$TMP/.planning/JOURNEY.md" <<'JOURNEY'
1073
+ # Journey
1074
+
1075
+ | # | Milestone | Status | Phases | Closed |
1076
+ |---|-----------|--------|--------|--------|
1077
+ | 1 | Foundation Arc | CLOSED | 1–2 | 2026-01-01 |
1078
+ | 2 | Core Features | OPEN | rolling | — |
1079
+ JOURNEY
1080
+ $NODE -e "
1081
+ const fs = require('fs');
1082
+ const t = JSON.parse(fs.readFileSync('$TMP/.planning/tracking.json','utf8'));
1083
+ t.milestones = [{ num: '1', name: 'Milestone 1', total_phases: 2, phases_completed: 2, tasks_completed: 7, shipped_url: 'https://x.test', closed_at: '2026-01-01T00:00:00.000Z' }];
1084
+ fs.writeFileSync('$TMP/.planning/tracking.json', JSON.stringify(t, null, 2));
1085
+ "
1086
+ (cd "$TMP" && $NODE "$STATE_JS" backfill-milestones >/dev/null 2>&1)
1087
+ RESULT=$($NODE -e "
1088
+ const t = JSON.parse(require('fs').readFileSync('$TMP/.planning/tracking.json','utf8'));
1089
+ console.log(t.milestones.length + '|' + t.milestones[0].name + '|' + t.milestones[0].tasks_completed + '|' + t.milestone_name);
1090
+ ")
1091
+ if [ "$RESULT" = "1|Foundation Arc|7|Core Features" ]; then
1092
+ pass "backfill-milestones repairs placeholder name, keeps real close data"
1093
+ else
1094
+ fail_case "backfill-milestones placeholder repair" "got=$RESULT expected='1|Foundation Arc|7|Core Features'"
1095
+ fi
1096
+
1014
1097
  # ─── Backward compatibility ──────────────────────────────
1015
1098
  echo ""
1016
1099
  echo "backward compatibility:"