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.
- package/CHANGELOG.md +22 -0
- package/bin/install.js +159 -9
- package/bin/state.js +70 -5
- package/docs/EMPLOYEE-QUICKSTART.md +162 -0
- package/package.json +2 -1
- package/skills/qualia-doctor/SKILL.md +62 -0
- package/skills/qualia-new/REFERENCE.md +7 -0
- package/skills/qualia-new/SKILL.md +42 -0
- package/skills/qualia-report/SKILL.md +13 -0
- package/templates/stacks/README.md +110 -0
- package/templates/stacks/ai-agent/env.required.json +44 -0
- package/templates/stacks/ai-agent/phases.md +53 -0
- package/templates/stacks/ai-agent/scaffold/.env.example +12 -0
- package/templates/stacks/ai-agent/scaffold/README.md +31 -0
- package/templates/stacks/ai-agent/scaffold/app/api/chat/route.ts +33 -0
- package/templates/stacks/ai-agent/scaffold/app/globals.css +13 -0
- package/templates/stacks/ai-agent/scaffold/app/layout.tsx +19 -0
- package/templates/stacks/ai-agent/scaffold/app/page.tsx +12 -0
- package/templates/stacks/ai-agent/scaffold/evals/cases.json +23 -0
- package/templates/stacks/ai-agent/scaffold/lib/openrouter/client.ts +51 -0
- package/templates/stacks/ai-agent/scaffold/lib/prompts/system.ts +6 -0
- package/templates/stacks/ai-agent/scaffold/lib/supabase/client.ts +10 -0
- package/templates/stacks/ai-agent/scaffold/lib/supabase/server.ts +28 -0
- package/templates/stacks/ai-agent/scaffold/next.config.mjs +7 -0
- package/templates/stacks/ai-agent/scaffold/package.json +29 -0
- package/templates/stacks/ai-agent/scaffold/postcss.config.mjs +7 -0
- package/templates/stacks/ai-agent/scaffold/supabase/migrations/0001_init.sql +41 -0
- package/templates/stacks/ai-agent/scaffold/tsconfig.json +21 -0
- package/templates/stacks/ai-agent/stack.json +9 -0
- package/templates/stacks/ai-agent/verify-checklist.md +31 -0
- package/templates/stacks/full-app/env.required.json +20 -0
- package/templates/stacks/full-app/phases.md +45 -0
- package/templates/stacks/full-app/scaffold/.env.example +7 -0
- package/templates/stacks/full-app/scaffold/README.md +28 -0
- package/templates/stacks/full-app/scaffold/app/globals.css +14 -0
- package/templates/stacks/full-app/scaffold/app/layout.tsx +19 -0
- package/templates/stacks/full-app/scaffold/app/page.tsx +20 -0
- package/templates/stacks/full-app/scaffold/lib/supabase/client.ts +10 -0
- package/templates/stacks/full-app/scaffold/lib/supabase/server.ts +31 -0
- package/templates/stacks/full-app/scaffold/next.config.mjs +7 -0
- package/templates/stacks/full-app/scaffold/package.json +29 -0
- package/templates/stacks/full-app/scaffold/postcss.config.mjs +7 -0
- package/templates/stacks/full-app/scaffold/supabase/migrations/0001_init.sql +27 -0
- package/templates/stacks/full-app/scaffold/tsconfig.json +21 -0
- package/templates/stacks/full-app/stack.json +9 -0
- package/templates/stacks/full-app/verify-checklist.md +32 -0
- package/templates/stacks/internal-tool/env.required.json +20 -0
- package/templates/stacks/internal-tool/phases.md +45 -0
- package/templates/stacks/internal-tool/scaffold/.env.example +7 -0
- package/templates/stacks/internal-tool/scaffold/README.md +29 -0
- package/templates/stacks/internal-tool/scaffold/app/globals.css +13 -0
- package/templates/stacks/internal-tool/scaffold/app/layout.tsx +20 -0
- package/templates/stacks/internal-tool/scaffold/app/page.tsx +22 -0
- package/templates/stacks/internal-tool/scaffold/lib/supabase/client.ts +9 -0
- package/templates/stacks/internal-tool/scaffold/lib/supabase/server.ts +28 -0
- package/templates/stacks/internal-tool/scaffold/next.config.mjs +6 -0
- package/templates/stacks/internal-tool/scaffold/package.json +29 -0
- package/templates/stacks/internal-tool/scaffold/postcss.config.mjs +7 -0
- package/templates/stacks/internal-tool/scaffold/supabase/migrations/0001_init.sql +28 -0
- package/templates/stacks/internal-tool/scaffold/tsconfig.json +21 -0
- package/templates/stacks/internal-tool/stack.json +9 -0
- package/templates/stacks/internal-tool/verify-checklist.md +31 -0
- package/templates/stacks/landing-page/env.required.json +8 -0
- package/templates/stacks/landing-page/phases.md +42 -0
- package/templates/stacks/landing-page/scaffold/.env.example +3 -0
- package/templates/stacks/landing-page/scaffold/README.md +25 -0
- package/templates/stacks/landing-page/scaffold/app/globals.css +14 -0
- package/templates/stacks/landing-page/scaffold/app/layout.tsx +19 -0
- package/templates/stacks/landing-page/scaffold/app/page.tsx +21 -0
- package/templates/stacks/landing-page/scaffold/next.config.mjs +7 -0
- package/templates/stacks/landing-page/scaffold/package.json +26 -0
- package/templates/stacks/landing-page/scaffold/postcss.config.mjs +7 -0
- package/templates/stacks/landing-page/scaffold/tsconfig.json +21 -0
- package/templates/stacks/landing-page/stack.json +9 -0
- package/templates/stacks/landing-page/verify-checklist.md +28 -0
- package/tests/bin.test.sh +3 -3
- 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,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,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,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,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,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
|
package/tests/state.test.sh
CHANGED
|
@@ -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:"
|