──────
Build and Push Docker Images / detect-changes (push) Successful in 11s
Build and Push Docker Images / build-auth-api (push) Successful in 1m0s
Build and Push Docker Images / build-employee-api (push) Successful in 1m2s
Build and Push Docker Images / build-transit-api (push) Successful in 1m1s
Build and Push Docker Images / build-error-log-api (push) Successful in 1m5s
Build and Push Docker Images / build-admin-ui (push) Successful in 1m13s
Build and Push Docker Images / build-monitoring-ui (push) Successful in 1m15s
Build and Push Docker Images / deploy-test-node (push) Successful in 3s
Build and Push Docker Images / detect-changes (push) Successful in 11s
Build and Push Docker Images / build-auth-api (push) Successful in 1m0s
Build and Push Docker Images / build-employee-api (push) Successful in 1m2s
Build and Push Docker Images / build-transit-api (push) Successful in 1m1s
Build and Push Docker Images / build-error-log-api (push) Successful in 1m5s
Build and Push Docker Images / build-admin-ui (push) Successful in 1m13s
Build and Push Docker Images / build-monitoring-ui (push) Successful in 1m15s
Build and Push Docker Images / deploy-test-node (push) Successful in 3s
### Summary of Changes
1. Documentation Consolidation:
• Relocated the docs/ folder and all repository .md files (README.md, DEPLOYMENT.md, DOCKER_README.md,
DOMAINS_README.md, GETTING_STARTED.md, CI_CD_PORTAINER_GUIDE.md, ADMIN_DEPLOYMENT_SUMMARY.md,
SECURITY_PRIVATE_FILES.md, handoff.md, azure-project-plan.md, plus service-specific READMEs) into .
• Updated document links in README.md.
2. AI Agent Project Map:
• Created PROJECT_MAP.md in the project root containing complete architecture diagrams, service specs, ports, DB
schemas, environment variables, and developer workflows.
• Added PROJECT_MAP.md to .gitignore.
3. Monitoring UI & Infrastructure Integration:
• Added monitoring_ui service to docker-compose.yml (port 5175).
• Configured Gitea workflow docker-build.yml for monitoring_ui builds.
• Added CORS middleware headers in index.js.
• Added nginx.conf copy step in Dockerfile.
──────
### Recommended Commit Message
docs: consolidate project documentation into monitoring_ui and add agent project map
- Move all markdown documentation files and docs/ folder into monitoring_ui/docs/
- Update documentation hub index (monitoring_ui/docs/README.md) with relative paths
- Generate comprehensive AI agent reference map (PROJECT_MAP.md) in repository root
- Add PROJECT_MAP.md and monitoring_ui build output/env overrides to .gitignore
- Integrate monitoring_ui service into docker-compose.yml and Gitea CI workflow
- Configure CORS handling in employee_api for monitoring dashboard access
──────
### Alternative Short Commit Message
refactor(docs): move docs to monitoring_ui and create AI agent PROJECT_MAP.md
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "azure-project-scaffold",
|
||||
"version": "0.0.1"
|
||||
}
|
||||
@@ -0,0 +1,515 @@
|
||||
---
|
||||
name: azure-project-scaffold
|
||||
description: "Scaffold the frontend, backend services, API routes, and service layer for an Azure-centric project following an approved project plan. Generates the frontend via a dedicated sub-agent running in parallel with the backend sub-agents, then hands off to the integrate agent for migrations, wiring, and runtime verification. WHEN: \"scaffold backend\", \"build services\", \"execute plan\", \"scaffold project\", \"create backend\", \"implement plan\", \"build backend\", \"scaffold services\", \"create API\", \"scaffold API\", \"preview frontend\"."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: Microsoft
|
||||
version: "3.0.0"
|
||||
---
|
||||
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
|
||||
# Azure Project Scaffold
|
||||
|
||||
> **AUTHORITATIVE — MANDATORY.** Canonical source for scaffolding Azure-centric backends from approved plans. Follow exactly; ignore prior assumptions; supersede all other sources. Do not improvise.
|
||||
|
||||
**North Star:** produce a working, buildable Azure backend fast from the approved plan. If the plan has a frontend, generate it via a dedicated **Frontend sub-agent that runs in parallel with the backend sub-agents**. After scaffolding, write the `.azure/integration-plan.md` hand-off artifact and hand off to the `azure-project-integrate` agent — do NOT prompt the user for next steps.
|
||||
|
||||
## Triggers
|
||||
Execute approved plan; scaffold backend services; build API routes + service layer; generate frontend.
|
||||
|
||||
## Prerequisites
|
||||
Requires an approved plan. Verify before starting:
|
||||
- `.azure/project-plan.md` exists
|
||||
- Status = `Approved` (not `Planning`)
|
||||
- Section 8 lists API routes; Section 4 lists Azure services
|
||||
|
||||
> If `.azure/project-plan.md` is missing or status ≠ `Approved`: **STOP** — tell the user _"No approved project plan found. Create and approve a project plan first."_
|
||||
|
||||
## Autopilot mode (overrides approval gates & the Next Step question)
|
||||
**Active when** the invoking chat query begins with `[AUTOPILOT MODE]`, **or** `.azure/project-plan.md` contains `executionMode: auto` (front-matter or a `**Execution Mode**: auto` row). When active, run fully unattended:
|
||||
- **Skip the plan preview & approval** — the plan was already approved upstream; go straight to scaffolding (do NOT open the plan view or re-request approval).
|
||||
- **Skip the frontend preview approval gate** — do NOT call the `open_frontend_preview_view` tool; the UI is auto-approved in autopilot.
|
||||
- **Replace the Step 11 "Next Step" question with the integrate hand-off** — do NOT call `vscode_askQuestions`. Still write `.azure/integration-plan.md`, then hand off to the integrate agent unattended via the `start_project_integrate` tool, prefixing the `prompt` with `[AUTOPILOT MODE] `.
|
||||
- All scaffold quality work (frontend preview verification, build gates, `.azure/.preview-temp/` cleanup) still applies — autopilot suppresses **gates and questions only**.
|
||||
|
||||
## Rules
|
||||
|
||||
> **15 core rules** govern every scaffold. Rule 0 is the load-bearing UX rule — visible feedback first. Rules 1–14 govern correctness. Details in referenced docs, consumed at relevant step.
|
||||
|
||||
> **📁 Paths are examples, not assumptions.** Every directory shown in these instructions (`services/web/`, `services/functions/`, `services/shared/`, `services/functions/src/utils/`, …) is an **illustrative default for a fresh project**. When the workspace already has a structure, follow it. Read the actual layout first and map these roles (frontend folder, Functions project, shared types, etc.) onto the user's real folders — never assume or impose a specific path. The plan's Project Structure section, when present, is the source of truth for where things go. **If the plan names the deployable apps after the product** (e.g. `services/office-compliance-api`, `services/office-compliance-portal`), honor those names exactly — including in `workspaces`, `cd` commands, imports, and the computed `main`/`rootDir` (`dist/<project>-api/src/functions/*.js`). The shared package stays generic (`services/shared`).
|
||||
|
||||
0. **Frontend-first generation (load-bearing UX rule)** — If the plan includes a frontend, the orchestrator launches the **Frontend Sub-Agent** (Step 1) at the same point it kicks off the backend track, so frontend generation runs **concurrently** with backend Phase A/B rather than blocking it. The sub-agent generates `services/web/` (mock data, pages, components), builds it, and reports back. The user already approved the design during planning (the `.azure/.preview-temp/` mock-up), so **do NOT ask the user to approve the UX during scaffolding.** If the plan has no frontend, this rule is satisfied trivially. See [sub-agent-strategy.md](.github/agents/azure-project-scaffold/references/sub-agent-strategy.md).
|
||||
1. **Plan is source of truth** — Read `.azure/project-plan.md` at start. Follow route definitions, service list, types, architecture exactly. Do NOT re-ask user for plan requirements.
|
||||
2. **Track progress** — Update plan status as you go: Approved → In Progress → Awaiting Integration. Do not defer status updates. (The `azure-project-integrate` agent advances it to `Integrated`.)
|
||||
3. **Build-gate enforcement** — Every phase ends with build check (`tsc` / `npm run build`). If fails, iterate until clean. **Do NOT proceed until code compiles.** Most important rule.
|
||||
4. **Azure Functions v4** — Always v4 programming model (Node.js v4, Python v2, .NET isolated). Prioritize Azure services. Runtimes: TypeScript, Python, C#.
|
||||
5. **API auth and Azure client boundaries** — Read `API Login` from the plan. When it is `Yes`, scaffold user-facing login across the frontend and API behind a small application auth interface. Separately, put every Azure SDK client behind a client-provider interface. Handlers and domain code NEVER import Azure SDKs, inspect the environment, or choose client implementations. See [service-abstraction.md](.github/agents/shared-references/service-abstraction.md).
|
||||
6. **Modular, one function per file** — Each Function own file. Each service own module. Extract shared utilities to `services/functions/src/utils/` — no duplication, no unused stubs. Prefix unused params with `_`. **DRY**: Same helper in 2+ files → extract to `services/functions/src/utils/` and import. **Proactive**: Before writing handlers, identify common patterns (password hashing, entity sanitization, response formatting) and pre-create shared utils. See [architecture.md](.github/agents/shared-references/architecture.md).
|
||||
7. **Managed identity in production** — Every production client used for backend-to-Azure communication MUST authenticate with managed identity (`DefaultAzureCredential` or the runtime equivalent) and a resource endpoint. Select an emulator-backed local client only when the runtime environment is explicitly `Development`. Every other value, including missing or misspelled values, selects the managed-identity client and fails startup if its endpoint configuration is invalid. Never use account keys, connection-string secrets, API keys, or a local emulator as a production fallback. Keep the environment check in the composition root. See [service-abstraction.md](.github/agents/shared-references/service-abstraction.md).
|
||||
8. **Input validation & standardized errors** — Every endpoint has validation schema (Zod/Pydantic/FluentValidation). Every route returns `{ error: { code, message, details? } }`. Error codes typed union, not strings. See [error-handling.md](.github/agents/shared-references/error-handling.md).
|
||||
9. **Resilience classification** — Follow plan's Essential/Enhancement classification. Enhancement services wrapped in try/catch with fallback. **Enhancement constructors MUST NOT throw** — defer config validation to method calls or wrap in try/catch in registry. Constructor throws crash ALL handlers via `getServices()`. See [resilience.md](.github/agents/shared-references/resilience.md).
|
||||
10. **Database write integrity** — Handlers performing multi-table writes MUST use `database.transaction()`. Document the collection-to-table mapping so the integrate agent can build matching schema migrations. (The integrate agent owns schema migrations and seed data; the scaffold does not create them.) See [database-integrity.md](.github/agents/shared-references/database-integrity.md).
|
||||
11. **Auto-initialization** — Registry `getServices()` MUST auto-initialize with concrete implementations when nothing pre-registered. (The integrate agent's runtime smoke test confirms this — but the code must be correct here.) See [service-abstraction.md](.github/agents/shared-references/service-abstraction.md).
|
||||
12. **Cross-workspace build safety** — When Functions imports `../shared/`, set `rootDir` to `".."` and **compute `main` field from actual `dist/` output after `tsc`** — never hardcode. With `rootDir: ".."`, handlers compile to `dist/functions/src/functions/X.js`. After build, list `dist/`, verify `main` matches. **#1 cause of "build passes but app won't start"**. See [architecture.md](.github/agents/shared-references/architecture.md).
|
||||
13. **Deploy-ready artifact (dependency split)** — The deployed unit is the **compiled `dist/`**, not TS source; the server installs **production deps only** (`npm install --omit=dev`) and must not re-build. So every package imported by runtime code MUST be in `dependencies`, and build-only tooling (`typescript`, `@types/*`, test runners, bundlers) MUST be in `devDependencies` — a `--omit=dev` install has to satisfy every `import` in `dist/`. `dist/` must never import from `src/` or need a rebuild. Native-addon packages (`bcrypt`, `sharp`, `better-sqlite3`, Prisma engines) stay in `dependencies` (installed on the Linux host), never bundled from a local build. This is what lets the deploy agent ship a prebuilt artifact with the platform build **disabled** instead of relying on an Oryx rebuild (which runs without devDependencies and fails on missing `tsc`). **Monorepo:** keep each service self-contained — import `shared` via **relative paths** so `tsc` compiles it into the service's `dist/` (no workspace-package dependency to resolve in a per-service deploy), and resolve hoisted third-party deps with a service-level production install; single-file bundling (esbuild `--packages=external`) is an optional optimization, not the default. See [typescript.md](.github/agents/shared-references/runtimes/typescript.md) → Deployment build contract.
|
||||
14. **Frontend quality contract** — If the plan has a frontend, **Section 5 (Design System & UI) of the plan is load-bearing**. Treat each region token in Section 5's Pages table (`header`, `hero`, `grid`, `form`, ...) as layout **intent** — render it using real primitives from the library named in `Component Library:`, themed by the Color Palette. **Reproduce the records from Section 5's Sample Content block** — the scaffolded page MUST show the same entities, names, values, and states the planning preview showed (the preview and the scaffold share this one content contract, so they stay in parity). Seed your mock data from Sample Content, then extend it. **Never produce raw `<div className="card">` placeholders that just mimic the wireframe** — but bespoke, domain-specific components (polaroid frames, ticket stubs, gallery tiles, chat bubbles) that wrap a real library primitive and carry real content + imagery are encouraged, not banned. **Every media-bearing entity MUST render a real image from the mock data, never an empty tinted surface or solid-color block.** **The HTML preview at `.azure/.preview-temp/*.html` is a presentation-quality visual spec — reproduce it faithfully with real primitives and add the production-only layer (real photos, motion, dark mode, webfont, library elevation); never regress below it.** If a generated page looks **less** polished than the static preview (flatter surfaces, missing icons, no motion, no dark mode, no polished hero, no library elevation), you have failed the bar — go back and apply the Polish floor before claiming the page is complete. See [frontend-quality-bar.md](.github/agents/azure-project-scaffold/references/frontend-quality-bar.md) for the per-library region-token → primitive mapping, theming contract, icon contract, state-coverage contract, **Polish floor**, and **Polish self-review checklist**. If Section 5 is missing or `Component Library:` is blank, STOP — the plan must be completed before scaffolding.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Context Management — read this first
|
||||
|
||||
> Do NOT read all reference files upfront (~250KB total) — it wastes context needed for code, test output, and fixes. Read lazily: only when a step needs them.
|
||||
|
||||
### Step-to-Reference Mapping
|
||||
|
||||
| Step | Read ONLY these files | Skip |
|
||||
|------|----------------------|------|
|
||||
| **Step 0** (Read Plan) | `.azure/project-plan.md` | All reference files |
|
||||
| **Step 1** (Frontend) | `../shared-references/frontend-patterns.md`, `references/frontend-preview-steps.md`, `references/frontend-quality-bar.md` | All other reference files |
|
||||
| **Sub-Agent Strategy** | `references/sub-agent-strategy.md` | |
|
||||
| **Step 2** (Foundation) | `../shared-references/architecture.md` | |
|
||||
| **Step 3** (Config) | `../shared-references/service-abstraction.md` — read only the Config Module section | |
|
||||
| **Step 4** (Services) | `../shared-references/service-abstraction.md` (full), selected runtime file | |
|
||||
| **Step 5** (Types/Validation) | `../shared-references/error-handling.md` — read only the Error Code Type Safety section | |
|
||||
| **Step 6** (Routes) | `../shared-references/resilience.md`, selected runtime file | |
|
||||
| **Step 7** (Errors) | `../shared-references/error-handling.md` (full) | |
|
||||
| **Step 8–10** (Health/OpenAPI/Logging) | _(instructions are in this skill, below)_ | |
|
||||
| **Step 11** (Wrap Up) | _(instructions are in this skill, below)_ | |
|
||||
|
||||
### Runtime-Specific Files — Load ONLY ONE
|
||||
|
||||
Select based on the **backend service's Language** (its stack section, e.g. `## 2. Backend`) — not the `Runtime` row (Node / Bun / etc.). When the frontend is a different language than the backend, load the runtime reference for each language a service uses.
|
||||
|
||||
| Selected Language | Orchestration | Load | Do NOT load |
|
||||
|-------------------|---------------|------|-------------|
|
||||
| TypeScript | docker-compose | `../shared-references/runtimes/typescript.md` | `python.md`, `dotnet.md` |
|
||||
| Python | docker-compose | `../shared-references/runtimes/python.md` | `typescript.md`, `dotnet.md` |
|
||||
| C# (.NET) | docker-compose / Functions | `../shared-references/runtimes/dotnet.md` | `typescript.md`, `python.md` |
|
||||
|
||||
### Context Release
|
||||
|
||||
> After step checkpoint passes, that step's reference no longer needed. Under context pressure, prioritize current step reference + project source over completed step references.
|
||||
|
||||
---
|
||||
|
||||
## 🔁 Cross-platform command discipline — read once, apply everywhere
|
||||
|
||||
> **Every shell command in this skill MUST work on Windows (PowerShell + cmd) AND macOS / Linux (bash / zsh) unchanged.** Each `run_in_terminal` call lands in a fresh shell whose default differs by OS — assume nothing.
|
||||
|
||||
| ❌ Non-portable pattern | ✅ Portable replacement |
|
||||
|------------------------|------------------------|
|
||||
| `cd services/web && npx vite build` | `run_in_terminal` with `cwd: "services/web"` and command `npx vite build`. If no `cwd` param, use `npm --prefix services/web run build` (or `npm --prefix services/web exec -- vite build`). |
|
||||
| `mkdir -p services/web/src/components` | `node -e "require('fs').mkdirSync('services/web/src/components', {recursive: true})"` |
|
||||
| `rm -rf .azure/.preview-temp` | `node -e "require('fs').rmSync('.azure/.preview-temp', {recursive: true, force: true})"` |
|
||||
| `cp -r services/shared/types services/web/src/types` | `node -e "require('fs').cpSync('services/shared/types', 'services/web/src/types', {recursive: true})"` |
|
||||
| `touch .env` | `node -e "require('fs').closeSync(require('fs').openSync('.env', 'a'))"` or just write the file via the file-creation tool. |
|
||||
| `cat .env >> .env.local` | Read with the file-read tool, write with the file-write tool. |
|
||||
| `export FOO=bar` followed by another call | Pass via the command line on the same call: `npx cross-env FOO=bar npm run build` (or set in `.env`). PowerShell uses `$env:FOO`, bash uses `export FOO` — they don't share. |
|
||||
| `ls`, `pwd`, `which X` | Don't invoke shell utilities. Use the workspace tools (`list_dir`, etc.) for read-only inspection. |
|
||||
|
||||
**Cardinal rules:**
|
||||
|
||||
1. **Prefer the tool's `cwd` parameter** over `cd X && …` chains. `cd` doesn't survive across `run_in_terminal` calls and isn't equally portable.
|
||||
2. **For Node-based operations**, prefer `node -e "…"` — Node is already a dependency of any frontend scaffold and is on PATH for backend scaffolds too.
|
||||
3. **For npm operations in subfolders**, prefer `npm --prefix <folder> run <script>` over chained `cd`.
|
||||
4. **Never use `&&` or `||` or `;` with shell built-ins** (`cd`, `export`, `set`) — those built-ins behave differently between PowerShell and POSIX shells. Multiple commands joined with `&&` are fine if every command is a real binary (`node`, `npm`, `npx`, `func`).
|
||||
5. **Path separators**: use forward slashes (`/`) in all command-line paths — both Node and modern Windows tooling accept them; backslashes break in bash and break inside JSON strings in `node -e`.
|
||||
|
||||
If you find yourself writing a command that wouldn't run on the other OS, stop and rewrite it using one of the portable patterns above.
|
||||
|
||||
---
|
||||
|
||||
## STEP 0: Read Plan & Validate — MANDATORY FIRST ACTION
|
||||
|
||||
**BEFORE starting execution**, read and validate plan:
|
||||
|
||||
| Task | Details |
|
||||
|------|---------|
|
||||
| Read `.azure/project-plan.md` | Load complete plan |
|
||||
| Validate status | Must be `Approved`. If not, STOP — instruct user to run `azure-project-plan`. |
|
||||
| Extract plan details | Routes, services, entity types, language, runtime, framework, **API Login**, and **orchestration** for each service's stack section (`## 2. Backend`, `## 3. Frontend`, …), structure |
|
||||
| Extract design contract (if frontend) | If a frontend is planned, read Section 5 (Design System & UI). Extract `Component Library:`, `Style Direction:`, `Typography:`, the Color Palette table, and the Pages table (page → layout regions). **If Section 5 is missing or `Component Library:` is blank, STOP — the plan's design section must be completed before scaffolding. Section 5 is load-bearing for Rule 13 / Step 1 quality bar.** |
|
||||
| Read the approved HTML preview (if frontend) | List `.azure/.preview-temp/` if it exists. Read `manifest.json` to get the page list, then read each `<slug>.html` plus `theme.css`. **Treat these files as the visual mock-up that the user already approved during planning.** They are the source of truth for layout, palette translation, and per-page region composition. The scaffolded app must reproduce this look using the framework + library named in the Frontend stack section / Section 5 — NOT by serving the preview HTML itself. If `.azure/.preview-temp/` is missing for a plan that has a frontend, do not fail — just rely on Section 5 alone. |
|
||||
| Determine frontend needed | Check if plan includes frontend (SPA + API, Full-stack SSR, Static + API). If yes, Step 1 generates the frontend. |
|
||||
| Update plan status | Set to `In Progress` |
|
||||
|
||||
> **✅ Checkpoint**: Plan loaded, status valid, status `In Progress`. If frontend planned, `.azure/.preview-temp/` contents loaded into context as visual reference.
|
||||
|
||||
---
|
||||
|
||||
## Execution Steps
|
||||
|
||||
> **Execution chronology** (frontend-first, backend in parallel):
|
||||
>
|
||||
> ```
|
||||
> t=0 Step 0 read plan, validate
|
||||
> t=10s Step 1 frontend SUB-AGENT ─┐
|
||||
> t=10s Phase A contracts (sequential) ─┼─ concurrent
|
||||
> t=10s Phase B backend SUB-AGENT ─┘
|
||||
> t=Nm Step 11 wrap up: frontend built AND Phase B done → write hand-off (sequential)
|
||||
> ```
|
||||
>
|
||||
> The chronology is load-bearing. **Launch the Frontend sub-agent and the backend track together** right after Step 0 so they run in parallel. For API-only projects (no frontend), Step 1 is skipped and Phase A/B begin immediately after Step 0.
|
||||
|
||||
### Step 1: Frontend (If Applicable)
|
||||
|
||||
> **Skip** if plan has no frontend ("API only" or "Background worker").
|
||||
|
||||
> **Run as a sub-agent (parallel with backend).** The orchestrator delegates frontend generation (sub-steps **F1–F4**) to a dedicated **Frontend Sub-Agent** so it runs concurrently with backend Phase A/B. The sub-agent generates + builds `services/web/` and returns a single report. See [sub-agent-strategy.md](.github/agents/azure-project-scaffold/references/sub-agent-strategy.md) for the sub-agent brief and hand-back contract.
|
||||
|
||||
**Goal**: Standalone frontend with mock data, generated and built. The integrate agent later wires it to the real backend. When `API Login` is `Yes`, include the login UI and auth state, but seed local identity so the preview lands on the main view (dashboard, feed), NOT the login page.
|
||||
|
||||
> ⚠️ **WORKING DIRECTORY (most-common scaffold failure)**: Every frontend command — `npm install`, `npx vite build`, `npm run build` — MUST run against the **frontend folder** (typically `services/web/`), never the workspace root. **Prefer the working-directory-independent form `npm --prefix services/web run <script>`** — `--prefix` loads the frontend's `package.json` no matter where the shell starts, so it can't accidentally run from the root. When using a binary directly (e.g. `npx vite build`), pass `cwd: "services/web"` on the same terminal call.
|
||||
|
||||
**References**:
|
||||
- [frontend-quality-bar.md](.github/agents/azure-project-scaffold/references/frontend-quality-bar.md) for the per-library region-token → primitive mapping, theming contract, icon contract, and state-coverage contract. **READ THIS FIRST — it is the contract between the plan's Section 6 and the JSX you ship.**
|
||||
- [frontend-patterns.md](.github/agents/shared-references/frontend-patterns.md) for patterns and quality bar.
|
||||
- [frontend-preview-steps.md](.github/agents/azure-project-scaffold/references/frontend-preview-steps.md) for sub-steps (F1–F4), working directory rules, approval loop.
|
||||
|
||||
> **✅ Checkpoint**:
|
||||
> 1. Frontend builds zero errors (`npm --prefix services/web run build` — cwd-independent; **never** a bare `npx vite build` from the workspace root)
|
||||
> 2. No `any` types in `.ts`/`.tsx`
|
||||
> 3. When `API Login` is `Yes`, local identity state is seeded so the app lands on main content on first load, with the signed-in user read from the seam (`api.getCurrentUser()`), not imported from `src/mocks/`
|
||||
> 4. **API seam intact** — `src/api/` declares an `ApiClient` interface (never `type ApiClient = typeof mockClient`), the mock is declared as `ApiClient`, `src/api/index.ts` is the one-line swap point, and **no file outside `src/api/` imports `src/mocks/` or `mockClient`**. Reference data with no plan route (assignee names, the signed-in user) gets an `ApiClient` method backed by the mock — it is never imported directly. See [frontend-preview-steps.md](.github/agents/azure-project-scaffold/references/frontend-preview-steps.md) → Load-bearing seam rule.
|
||||
> 5. **No UX approval prompt** — the design was already approved during planning via `.azure/.preview-temp/`. Do NOT call `ask_user` for "do you approve this UI?".
|
||||
> 6. **Preview-embeddable dev server** — `vite.config`'s `server` sets `host: true`, `allowedHosts: true`, `strictPort: false` (Angular: `--host 0.0.0.0 --disable-host-check`; Next.js: `-H 0.0.0.0`); the `dev`/`start` script serves and prints a `http://localhost:<port>/` URL (never `build --watch`); no `X-Frame-Options`/`frame-ancestors` meta CSP in `index.html`; and you did NOT start your own dev server or an auto-start dev task. The `open_frontend_preview_view` tool starts and owns the dev server for the **Approve UI** preview — these keep its webview iframe from hanging or rendering blank (which would leave the user unable to approve). See [frontend-preview-steps.md](.github/agents/azure-project-scaffold/references/frontend-preview-steps.md) → Preview compatibility.
|
||||
> 7. **Quality bar (Rule 13)**: Every page imports primitives from the library named in plan Section 5's `Component Library:`; the app shell is wrapped in that library's theme provider with a brand ramp derived from Section 5's palette; every icon is a real library icon (no emoji, no SVG placeholders); every `form` region has a visible validation state; every data-bearing page exposes all four states (loading / error / empty / data) via a dev-only toggle. **Use the approved HTML mock-up at `.azure/.preview-temp/<slug>.html` as the layout/visual reference per page** — reproduce the same regions and tonal feel using the real library primitives, not by embedding the HTML. See [frontend-quality-bar.md](.github/agents/azure-project-scaffold/references/frontend-quality-bar.md).
|
||||
|
||||
---
|
||||
|
||||
### Sub-Agent Strategy for Backend Scaffolding
|
||||
|
||||
**Reference**: Read [sub-agent-strategy.md](.github/agents/azure-project-scaffold/references/sub-agent-strategy.md) for execution model, Frontend/Phase A/Phase B details, coordination rules.
|
||||
|
||||
> Sub-agents parallelize work. The **Frontend sub-agent** (Step 1, F1–F4) and the backend track launch together right after Step 0. Phase A (Contracts) is sequential/blocking; Phase B (Backend) launches when Phase A completes. The Frontend sub-agent runs concurrently with both.
|
||||
|
||||
> **Completion gate**: Step 11 (Wrap Up) writes the hand-off artifact only after BOTH: (a) the frontend is generated and built (sub-agent returned) AND (b) Phase B completed.
|
||||
|
||||
### Step 2: Foundation
|
||||
|
||||
**Goal**: Project skeleton compiles/builds with zero errors.
|
||||
|
||||
| Task | Details |
|
||||
|------|---------|
|
||||
| Initialize project | `package.json` + `tsconfig.json` (Node.js) / `pyproject.toml` (Python) / `*.csproj` + `*.sln` (.NET) |
|
||||
| Configure linter/formatter | ESLint + Prettier (Node.js) / Ruff (Python) / dotnet format (.NET) |
|
||||
| Create `.gitignore` | Runtime-appropriate ignores (node_modules, data/, etc.). **Write it before any file that holds credentials** — `.env` and `local.settings.json` must be ignored from the first commit, never added and removed later. See architecture.md § .gitignore Additions |
|
||||
| Create directory structure | `services/functions/`, `services/functions/src/utils/`, `services/shared/` (do NOT create `services/web/` — may exist from frontend generation) |
|
||||
|
||||
**Reference**: [architecture.md](.github/agents/shared-references/architecture.md)
|
||||
|
||||
> **✅ Checkpoint**:
|
||||
> 1. **Build gate**: `npm run build` / `python -m py_compile` / `dotnet build`. Zero errors.
|
||||
> 2. **Workspace build scripts**: If monorepo, verify every workspace has `build` script. Run in each. If produces `dist/`, verify non-empty.
|
||||
> 3. **Shared package**: If `services/shared/` exists, verify: (a) `package.json` has `"exports"` or `"main"` pointing to compiled output, (b) `npm run build` produces `dist/` with `.js` and `.d.ts`, (c) other workspaces import without errors.
|
||||
> 4. **Cross-workspace imports (CRITICAL)**: Run `tsc --noEmit` in every workspace importing shared. If `TS2307: Cannot find module` → exports broken. **Fix before proceeding.**
|
||||
> 5. **rootDir and main field (CRITICAL)**: After `tsc`, **list actual dist/ contents**, verify `main` glob matches compiled handlers. If `rootDir: ".."`, output nests deeper. Fix `main`.
|
||||
> 6. **Dependency split (deploy-readiness, Rule 13)**: Every package imported by runtime code lives in `dependencies`; build-only tooling (`typescript`, `@types/*`, test runners, bundlers) lives in `devDependencies`. A production install (`npm install --omit=dev`) must satisfy every `import` in `dist/` — this is what lets the deploy ship the prebuilt artifact without an Oryx rebuild.
|
||||
> 7. **Emitted specifiers resolve (CRITICAL — after build, not `--noEmit`)**: `tsc` does **not** rewrite module specifiers on emit. Any non-relative import that only resolved through a tsconfig `paths` alias is still in `dist/` verbatim and dies at runtime with `Cannot find module`, even though checkpoints 1–6 all pass. **Do NOT use `paths` aliases for cross-workspace imports** — import the shared package by its `package.json` `name`, or use the relative `../shared/...` form. After building, verify every non-relative specifier in `dist/` actually resolves from that workspace (run from the workspace that emitted `dist/`):
|
||||
>
|
||||
> ```bash
|
||||
> node --input-type=module -e "
|
||||
> import fs from 'node:fs';
|
||||
> import path from 'node:path';
|
||||
> import {builtinModules, createRequire} from 'node:module';
|
||||
> const bad=[];
|
||||
> (function walk(d){for(const e of fs.readdirSync(d,{withFileTypes:true})){
|
||||
> const p=path.join(d,e.name);
|
||||
> if(e.isDirectory()) walk(p);
|
||||
> else if(e.name.endsWith('.js')) for(const m of fs.readFileSync(p,'utf8').matchAll(/(?:require\(|from\s*)['\x22]([^'\x22]+)['\x22]/g)){
|
||||
> const s=m[1];
|
||||
> if(s.startsWith('.')||s.startsWith('node:')||builtinModules.includes(s)) continue;
|
||||
> let ok=false;
|
||||
> try { createRequire(path.resolve(p)).resolve(s); ok=true; } catch {}
|
||||
> if(ok===false){ try { import.meta.resolve(s); ok=true; } catch {} }
|
||||
> if(ok===false) bad.push(p+' -> '+s);
|
||||
> }}})('dist');
|
||||
> if(bad.length){ console.error('unresolvable specifiers in emitted output:'); bad.forEach(b=>console.error(' '+b)); process.exit(1); }
|
||||
> console.log('all emitted specifiers resolve');
|
||||
> "
|
||||
> ```
|
||||
>
|
||||
> Three details in it are load-bearing — do not "simplify" them away:
|
||||
> - **`path.resolve(p)`** — the walk builds paths relative to `dist/`, and `createRequire()` rejects a relative path with `ERR_INVALID_ARG_VALUE`. Swallowed by the `catch`, that reports **every** bare specifier as unresolvable, including real dependencies like `@azure/functions` — a check that reds on a healthy project gets ignored.
|
||||
> - **The second `import.meta.resolve` attempt** — `createRequire().resolve()` resolves under the `require` condition only. A `"type": "module"` shared package whose `exports` declare `import` and not `require` resolves fine at runtime but not that way, so a single CJS attempt would red the very fix this checkpoint recommends. A specifier is only reported when **both** conditions fail, which still catches a genuinely unexported subpath.
|
||||
> - **`['\x22]` rather than a literal `"`** — keeps the body free of double quotes so `node -e "…"` parses in PowerShell as well as bash (see the cross-platform cardinal rules above).
|
||||
>
|
||||
> ⚠️ **Pitfalls**: (1) Shared packages without build → `ERR_MODULE_NOT_FOUND`. (2) Wildcard exports fail TS resolution. (3) `rootDir: "."` blocks cross-workspace imports — use `".."` and update `main`. (4) tsconfig `paths` aliases (e.g. `@shared/*`) resolve at compile time only — `tsc --noEmit` passes and the emitted `dist/` still says `Cannot find module` at runtime.
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Configuration & Environment
|
||||
|
||||
**Goal**: Config module that loads env vars with validation and safe defaults.
|
||||
|
||||
| Task | Details |
|
||||
|------|---------|
|
||||
| Create `config` module | `services/config.ts` / `services/config.py` / `Services/Config.cs` |
|
||||
| Create `.env.example` | All required env vars with placeholders and comments |
|
||||
| Create `local.settings.json` | Azure Functions local settings with emulator defaults and `"AZURE_FUNCTIONS_ENVIRONMENT": "Development"` so local provider selection is explicit |
|
||||
| Implement env validation | Validate the variables required by the selected providers at startup. Missing or unknown environment means production, so missing production configuration fails fast. |
|
||||
| Keep selection centralized | Read the environment once in the startup composition root. Application code receives interfaces and contains no environment checks. |
|
||||
|
||||
**Reference**: [service-abstraction.md](.github/agents/shared-references/service-abstraction.md)
|
||||
|
||||
> **✅ Checkpoint**: Config module loads env vars. `.env.example` documents all variables.
|
||||
|
||||
---
|
||||
|
||||
### Step 4: Service Abstraction Layer
|
||||
|
||||
**Goal**: Implement user-facing API authentication when selected, and isolate every Azure client behind local and managed-identity providers.
|
||||
|
||||
`API Login` is authoritative. Do not infer or override it. It controls application user login only; it never changes how backend services authenticate to Azure.
|
||||
|
||||
#### API login
|
||||
|
||||
When `API Login` is `Yes`, scaffold the complete frontend-to-API flow. When it is `No`, do not add login pages, auth endpoints, token middleware, mock users, or protected-route wrappers.
|
||||
|
||||
| Task | Details |
|
||||
|------|---------|
|
||||
| Define the auth interface | Model application behavior such as `createAccount(input)`, `login(credentials)`, `authenticate(request)`, and `getCurrentUser()`. Keep token and credential logic out of handlers and UI components. |
|
||||
| Add the API flow | Add registration, login, and current-user endpoints even though the plan intentionally omits auth routes. Registration validates input, rejects duplicate accounts, and stores only a strong password hash. Protect every non-health, non-auth API route by default unless its product behavior is explicitly public. |
|
||||
| Choose a stack-appropriate implementation | Reuse an existing auth system when present. Otherwise default to a REST login flow that verifies stored password hashes and issues a short-lived signed JWT. Verify signature, algorithm, issuer, audience, and expiry on every protected request. Require production signing configuration; never ship a hard-coded or generated-at-startup production secret. |
|
||||
| Add the frontend flow | Add login, logout, current-user state, protected navigation, and authenticated API calls. The login page MUST contain a visible **Create account** button that opens a dedicated create-account page. The create-account page MUST submit to the registration flow, show field and duplicate-account errors, and return the user to login or establish the new session after success. Keep the token out of source code and fixture imports. Prefer an HttpOnly, Secure, SameSite cookie when the chosen stack supports it; otherwise keep a bearer token in memory rather than long-lived browser storage. |
|
||||
| Test the boundary | Cover successful and failed account creation, duplicate accounts, successful and failed login, invalid/expired tokens, logout, current-user lookup, protected-route rejection, and authorization between users. |
|
||||
|
||||
Managed identity is not an end-user login mechanism. Never use a managed identity token as the application's user session.
|
||||
|
||||
#### Azure client providers
|
||||
|
||||
> ⚠️ **CRITICAL — DO NOT SKIP EITHER AZURE CLIENT IMPLEMENTATION**
|
||||
>
|
||||
> Every Azure client interface MUST have both an emulator-backed local implementation and a managed-identity production implementation. Select once at startup and inject the selected interface. Do not scatter `isDevelopment` checks through handlers or services.
|
||||
|
||||
| Task | Details |
|
||||
|------|---------|
|
||||
| Define the client-provider interface | Model only the Azure client behavior the application needs. Handlers and domain services depend on this interface, never an environment variable or credential. |
|
||||
| Implement local behavior | Point the Azure SDK client at Azurite or the planned emulator. Never embed local selection inside the implementation. |
|
||||
| Implement production behavior | Use the Azure resource endpoint with managed identity (`DefaultAzureCredential` or the runtime equivalent). Do not accept account keys, SAS tokens, or connection-string secrets for production Azure communication. |
|
||||
| Select once at startup | A factory/composition root checks for the exact development marker, constructs the matching providers, validates endpoint configuration, and injects the interfaces. Missing or invalid production configuration fails startup. |
|
||||
| Create service registry | `getServices()` MUST auto-initialize selected clients when nothing was pre-registered. ESM uses static imports or `await import()`, NOT `require()`. Enhancement construction follows Rule 9 without substituting a local provider. |
|
||||
| Leave breadcrumbs | Use explicit names such as `AzuriteBlobClientProvider` and `ManagedIdentityBlobClientProvider`. Keep the interface, both implementations, and factory together; document settings in `.env.example`; set the development marker in `local.settings.json`. |
|
||||
|
||||
**Reference**: [service-abstraction.md](.github/agents/shared-references/service-abstraction.md)
|
||||
|
||||
> **📋 File Verification** — Before checkpoint, verify on disk:
|
||||
>
|
||||
> When `API Login` is `Yes`:
|
||||
> - [ ] auth interface and implementation
|
||||
> - [ ] registration, login, and current-user endpoints
|
||||
> - [ ] frontend login/session flow, including a visible **Create account** button and dedicated create-account page
|
||||
> - [ ] authentication and authorization tests
|
||||
>
|
||||
> For each Azure service in the plan:
|
||||
> - [ ] `src/services/interfaces/I{Service}Service.ts` — interface
|
||||
> - [ ] local client provider — emulator behavior
|
||||
> - [ ] production client provider — Azure endpoint plus managed identity
|
||||
>
|
||||
> Additionally:
|
||||
> - [ ] `src/services/registry.ts` — `initializeServices()` selects Azure client providers once and constructs them
|
||||
> - [ ] `getServices()` calls `initializeServices()` when `services === null` (lazy auto-init)
|
||||
> - [ ] application code depends only on auth/client interfaces and contains no Azure-client environment checks
|
||||
> - [ ] client selection tests prove explicit Development uses emulators, missing/other values use managed identity, and invalid production config fails
|
||||
>
|
||||
> **If an API Login flow is incomplete or either Azure client implementation is missing, DO NOT proceed.**
|
||||
|
||||
> **✅ Checkpoint**: All interfaces, concrete implementations, and registry exist. `getServices()` auto-initializes. `tsc` zero errors.
|
||||
|
||||
---
|
||||
|
||||
### Step 5: Shared Types & Validation Schemas
|
||||
|
||||
**Goal**: Type-safe contracts between frontend and backend, with validation covering every endpoint.
|
||||
|
||||
| Task | Details |
|
||||
|------|---------|
|
||||
| Create shared types | Entity types, API request/response contracts in `services/shared/` |
|
||||
| Create auth contracts when selected | When `API Login` is `Yes`, add credential, session/current-user, and login response types without exposing password hashes or signing secrets |
|
||||
| Create validation schemas | Zod (Node.js) / Pydantic (Python) / FluentValidation (.NET) — **one per endpoint accepting input** |
|
||||
| Create path param schemas | UUID format validation for path params (e.g., `:id`) |
|
||||
| Create file upload validation | Size limit and MIME type validation for uploads |
|
||||
| Define error code enum | Typed union of all valid error codes (not plain `string`) |
|
||||
| Wire validation into handlers | Validate request body/params before processing |
|
||||
|
||||
**Reference**: [error-handling.md](.github/agents/shared-references/error-handling.md)
|
||||
|
||||
> ⚠️ **Schema Completeness Check** (MANDATORY)
|
||||
>
|
||||
> Before marking complete, verify **every route** has:
|
||||
> - Request body schema (if accepts body)
|
||||
> - Query param schema (if has query params)
|
||||
> - Path param schema (if has path params like `:id`)
|
||||
> - Response type in shared package
|
||||
>
|
||||
> Count schemas vs routes. Coverage < 100% = NOT complete.
|
||||
|
||||
> **✅ Checkpoint**: Every route has a corresponding validation schema. Types build cleanly.
|
||||
|
||||
---
|
||||
|
||||
### Step 6: API Routes / Functions (Per Feature)
|
||||
|
||||
**Goal**: Implement each route one at a time. Each compiles and matches API contract before starting next.
|
||||
|
||||
> ❌ **CRITICAL**: Implement ONE route at a time. Verify compiles. THEN start next.
|
||||
|
||||
When `API Login` is `Yes`, implement the derived auth endpoints and middleware before the plan's feature routes. The plan omits auth route details by design; this is the one allowed addition to its route inventory. Protect feature routes according to the Step 4 API login rule.
|
||||
|
||||
For **each** route in plan:
|
||||
|
||||
| Task | Details |
|
||||
|------|---------|
|
||||
| Create function handler | One file per function. **All async calls MUST include `await`**. **`handleError` calls MUST match standardized signature**. |
|
||||
| Use transactions for multi-table writes | Any handler writing 2+ tables MUST use `database.transaction()` |
|
||||
| Wrap Enhancement services | External services classified Enhancement MUST have try/catch with fallback (see [resilience.md](.github/agents/shared-references/resilience.md)) |
|
||||
| Validate file uploads server-side | Check file size and MIME type before processing |
|
||||
| Validate path params before DB queries | When the auth service supplies a user ID, **validate its format** (e.g., UUID) before a DB query. A malformed ID on a typed column causes 500 instead of 401. |
|
||||
| Verify response shape | `jsonBody` must match Route Definitions |
|
||||
| Verify collection names | Must map to the documented collection-to-table mapping (Rule 10) |
|
||||
| Extract shared utilities | Duplicated helpers → `services/functions/src/utils/` (Rule 6). **After each handler**, grep for helpers in 2+ files, extract immediately. Consider handler wrapper if >8 handlers share try/catch boilerplate. Prefix unused params with `_`. |
|
||||
|
||||
**Reference**: [service-abstraction.md](.github/agents/shared-references/service-abstraction.md), [resilience.md](.github/agents/shared-references/resilience.md)
|
||||
|
||||
> **✅ Checkpoint (per feature)**: Handler compiles. Response shape matches plan contract.
|
||||
|
||||
> **✅ Post-Step 6 Build Check (MANDATORY — after ALL routes)**:
|
||||
>
|
||||
> 1. Build functions: `npm run build` (or `tsc`). **Zero errors.** If `TS2307` import errors → shared package exports broken, fix first.
|
||||
> 2. **Verify `main` field** — List `dist/`, confirm `main` glob matches compiled handlers. If `rootDir` set to parent, output nests deeper (Rule 12). Fix before proceeding.
|
||||
>
|
||||
> ⚠️ The integrate agent runs the `func start` smoke test, endpoint checks, and migrations after scaffolding — the scaffold's job ends at a clean build.
|
||||
|
||||
---
|
||||
|
||||
### Step 7: Error Handling Middleware
|
||||
|
||||
**Goal**: Global error handler for consistent error responses.
|
||||
|
||||
| Task | Details |
|
||||
|------|---------|
|
||||
| Create error types | Custom classes (NotFoundError, ValidationError, etc.) |
|
||||
| Create error middleware | Catches errors, maps to standardized response |
|
||||
| Create error response shape | `{ error: { code: string, message: string, details?: any } }` |
|
||||
|
||||
**Reference**: [error-handling.md](.github/agents/shared-references/error-handling.md)
|
||||
|
||||
> **✅ Checkpoint**: Error types and middleware exist. Response shape consistent. `tsc` zero errors.
|
||||
|
||||
---
|
||||
|
||||
### Step 8: Health Check Endpoint
|
||||
|
||||
**Goal**: `/api/health` endpoint that reports status of all configured services.
|
||||
|
||||
| Task | Details |
|
||||
|------|---------|
|
||||
| Create health check function | Calls each service's health method, aggregates results |
|
||||
| Return structured response | `{ status: "healthy" | "degraded" | "unhealthy", services: { ... } }` |
|
||||
|
||||
**Status → HTTP code mapping** (must match tests):
|
||||
|
||||
| Status | HTTP Code | Condition |
|
||||
|--------|:---------:|-----------|
|
||||
| `healthy` | 200 | All services healthy |
|
||||
| `degraded` | 200 | Some services down but app functional |
|
||||
| `unhealthy` | 503 | All services down or all Essential down |
|
||||
|
||||
> ⚠️ **`degraded` returns 200, NOT 503.** App still serving — reduced functionality. Only `unhealthy` returns 503. Tests must match.
|
||||
|
||||
> **✅ Checkpoint**: Health endpoint exists, returns structured response. Status-to-HTTP mapping correct.
|
||||
|
||||
---
|
||||
|
||||
### Step 9: OpenAPI / API Contract
|
||||
|
||||
**Goal**: Auto-generated or manually defined OpenAPI 3.x spec from route definitions.
|
||||
|
||||
| Task | Details |
|
||||
|------|---------|
|
||||
| Generate OpenAPI spec | From plan route definitions, produce `openapi.yaml` or `.json`. **Prefer inlining as TypeScript object** in handler to avoid dist/ path issues. |
|
||||
| Add spec endpoint | Serve at `/api/docs` or `/api/openapi.json`. If file-based, verify path resolves from compiled output. |
|
||||
| Validate responses | Test actual responses match spec shapes |
|
||||
|
||||
> **✅ Checkpoint**: OpenAPI spec exists and valid. Endpoint wired.
|
||||
|
||||
---
|
||||
|
||||
### Step 10: Structured Logging
|
||||
|
||||
**Goal**: Consistent, machine-readable logging across handlers and services.
|
||||
|
||||
| Task | Details |
|
||||
|------|---------|
|
||||
| Configure logger | pino (Node.js) / structlog (Python) / **`ILogger<T>` + OpenTelemetry → App Insights (.NET — no Serilog)** |
|
||||
| Add request logging | Log method, path, status, duration per request |
|
||||
| Add operation logging | Log key operations (create, update, delete) |
|
||||
|
||||
**Reference**: [runtimes/](.github/agents/shared-references/runtimes//)
|
||||
|
||||
> **✅ Checkpoint**: Logger configured, wired into handlers. Request logging in place. `tsc` zero errors.
|
||||
|
||||
---
|
||||
|
||||
### Step 11: Wrap Up
|
||||
|
||||
**Goal**: All code compiles and builds cleanly. Scaffold complete; hand off to verify.
|
||||
|
||||
| Task | Details |
|
||||
|------|---------|
|
||||
| Build all workspaces | `npm run build` in every workspace — zero errors |
|
||||
| Clean up the HTML preview | If `.azure/.preview-temp/` exists, delete the whole folder — its contents were a transient mock-up consumed during scaffolding and should not ship in the repo. Use a portable command (see Cross-platform command discipline): `node -e "require('fs').rmSync('.azure/.preview-temp', {recursive: true, force: true})"`. Do **NOT** use `rm -rf` or `Remove-Item -Recurse -Force` directly — those are not cross-platform. |
|
||||
| Update plan status | Set to `Awaiting Integration` — signals the scaffold built clean but the frontend still uses mock data and migrations/live wiring are pending (the `azure-project-integrate` agent's job) |
|
||||
| Print completion | List created files, announce: **"Scaffolding complete!"** |
|
||||
| **Write the integration artifact** | Write `.azure/integration-plan.md` — the hand-off brief the `azure-project-integrate` agent consumes. Include: backend folder + run command + port + health path; frontend folder + build/dev commands + the **API seam to swap** (`services/web/src/api/index.ts` — repoint from `mockClient` to the live client) plus the **mock files to delete** (`src/api/mockClient.ts`, `src/mocks/*`, local mock types, and the dev-only Mock State Switcher `src/api/previewState.ts` + its corner-switcher component); the full API route inventory (method + path) so the live client mirrors the `ApiClient` interface method-for-method; when `API Login` is `Yes`, the registration, login, and current-user endpoints, auth middleware, create-account and session files, and required signing configuration; the database type + migration tool + migration directory + connection env vars (state explicitly that **NO seed data** is to be created); the shared-types package + import alias; the service list (Essential vs Enhancement). Keep it concise — paths and commands, not prose. |
|
||||
| **Open the frontend preview & UI-approval gate** | **Only when the plan has a frontend AND not in autopilot.** Call the `open_frontend_preview_view` tool with `{ "frontendFolder": "services/web" }`. Set `frontendFolder` only when it isn't the default `services/web` (e.g. a product-named app); otherwise call with `{}`. This opens a webview that starts the frontend dev server and renders the **running app (mock data)** in an iframe, with an **Approve UI** header and a feedback box — mirroring the plan-approval UX. **The webview owns the hand-off**: clicking **Approve UI** triggers `copilotOnRails.startProjectIntegrate` itself, and the feedback box re-opens this scaffold agent with the user's UI change requests (the dev server hot-reloads as you edit). After opening the gate, **STOP** — do NOT also call `start_project_integrate`, do NOT call `vscode_askQuestions`. If the plan has **no frontend**, skip this row and use the direct hand-off row below. |
|
||||
| **Hand off to the Integrate agent** | **Use this row only when there is NO frontend, or in autopilot mode** (the preview gate is skipped). Call the `start_project_integrate` tool with no arguments (`{}`). This starts a **new chat session** running `azure-project-integrate`, which reads the artifact and its instruction file to wire the frontend to live data, smoke-test the backend, create the migrations, and verify end-to-end. Do **NOT** call `vscode_askQuestions` — the hand-off is the next step. |
|
||||
|
||||
> **✅ Final Checkpoint**:
|
||||
> 1. **Build**: `npm run build` every workspace. `dist/` has output. Zero errors.
|
||||
> 2. **Preview cleanup**: `.azure/.preview-temp/` no longer exists.
|
||||
> 3. **Status**: `.azure/project-plan.md` = `Awaiting Integration`.
|
||||
> 4. **Integration artifact**: `.azure/integration-plan.md` written with the integrate agent's brief.
|
||||
> 5. **Hand-off**: For a project **with a frontend** (interactive mode), opened the UI-approval gate via the `open_frontend_preview_view` tool and stopped — the gate's **Approve UI** button performs the hand-off. For a **no-frontend** project (or autopilot), started the `azure-project-integrate` session via the `start_project_integrate` tool. Either way, did NOT call `vscode_askQuestions` or print next-step suggestions.
|
||||
|
||||
---
|
||||
|
||||
## Outputs
|
||||
|
||||
> Locations below are **example conventions** for a new project. Where the workspace already has a structure, the actual paths follow it — do not assume these exact directories.
|
||||
|
||||
| Artifact | Location |
|
||||
|----------|----------|
|
||||
| Frontend (if applicable) | `services/web/` (with the `src/api/` seam — `ApiClient` interface + mock impl + one-line swap point — mock data, local types, pages, components — from Step 1) |
|
||||
| Backend (Functions) | `services/functions/` or user-specified path |
|
||||
| Shared types | `services/shared/` |
|
||||
| Service abstractions | `services/functions/src/services/` (or equivalent) |
|
||||
| Function handlers | `services/functions/src/functions/` (or equivalent) |
|
||||
| Validation schemas | `services/shared/schemas/` or `services/shared/validation/` |
|
||||
| Error types | `services/functions/src/errors/` (or equivalent) |
|
||||
| OpenAPI spec | `services/functions/openapi.yaml` or `openapi.json` |
|
||||
| Environment template | `.env.example` (project root) |
|
||||
| Functions config | `services/functions/local.settings.json` |
|
||||
| **Integration artifact** | `.azure/integration-plan.md` (hand-off brief for `azure-project-integrate`) |
|
||||
| **Next step** | If the plan has a frontend: open the UI-approval gate via the `open_frontend_preview_view` tool (its **Approve UI** button hands off). Otherwise: hand off directly via the `start_project_integrate` tool |
|
||||
|
||||
---
|
||||
|
||||
## Runtime Quick Reference
|
||||
|
||||
| Language | Orchestration | Init | Hosting Model | Package Manager |
|
||||
|----------|---------------|------|---------------|-----------------|
|
||||
| TypeScript | docker-compose | `func init --typescript --model V4` | Functions v4 | npm / pnpm |
|
||||
| Python | docker-compose | `func init --python --model V2` | Functions v2 | pip / poetry |
|
||||
| C# (.NET) | docker-compose | `func init --dotnet --isolated` | Functions isolated worker | dotnet |
|
||||
|
||||
For runtime-specific implementation patterns, see [runtimes/](.github/agents/shared-references/runtimes//).
|
||||
|
||||
---
|
||||
|
||||
## Next
|
||||
|
||||
> After scaffolding completes, write `.azure/integration-plan.md`. **If the plan includes a frontend (and you are not in autopilot), open the UI-approval gate via the `open_frontend_preview_view` tool** — it shows the running frontend (mock data) in a webview with an **Approve UI** header + feedback box, and its Approve button performs the integrate hand-off for you, so do NOT also call the `start_project_integrate` tool. For a no-frontend project (or autopilot), hand off directly to the `azure-project-integrate` agent via the `start_project_integrate` tool (Step 11). Do NOT ask the user what to do next — do NOT call `vscode_askQuestions` (or any chat question API), and do NOT print plain-text follow-up suggestions. The integrate agent takes over from here.
|
||||
@@ -0,0 +1,130 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# Frontend Steps
|
||||
|
||||
> Detailed sub-steps for generating the standalone frontend. Read during **Step 1** (Frontend).
|
||||
|
||||
> **Companion contract**: Before writing any JSX, also read [frontend-quality-bar.md](.github/agents/azure-project-scaffold/references/frontend-quality-bar.md). It defines the load-bearing contract between the plan's Section 5 (Design System & UI) and the JSX you ship — per-library region-token → primitive mapping, theming via the library's brand ramp, real icons, and the four-state coverage gate. The sub-steps below cover *how* to generate and build the frontend (working directory, build, verify); the quality bar covers *what* the frontend must contain. Design approval already happened during planning via `.azure/.preview-temp/` — Step 1 does **not** re-prompt the user for UX approval.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ ️ WORKING DIRECTORY — READ BEFORE RUNNING ANY COMMAND
|
||||
|
||||
**Every frontend command (scaffolder, `npm install`, `npx vite build`, `npm run build`, etc.) MUST be invoked with `cwd` set to the frontend project folder — the directory that contains the frontend's `package.json` and `index.html` (typically `services/web/`). Running these commands from the workspace root is the #1 cause of build failures.**
|
||||
|
||||
| ✅ Correct | ❌ Wrong |
|
||||
|----------|---------|
|
||||
| `run_in_terminal` with `cwd: "services/web"` and command `npx vite build` | `run_in_terminal` from workspace root running `npx vite build` |
|
||||
| `run_in_terminal` with `cwd: "services/web"` and command `npm install` | `cd services/web && npx vite build` chained from another shell that was launched at root, then forgotten on the next command |
|
||||
| Pass the folder explicitly every time (don't rely on a previous `cd`) | Assume the terminal is still in `services/web/` from an earlier command — the agent runs each command in a fresh shell |
|
||||
|
||||
**Rules:**
|
||||
|
||||
1. **Prefer a working-directory-independent command** for the frontend's own npm scripts: `npm --prefix <frontend-folder> run <script>` (e.g. `npm --prefix services/web run build`). `--prefix` loads the frontend's `package.json` from that folder no matter where the shell starts, so it cannot accidentally run from the workspace root.
|
||||
2. When you must use a tool/binary directly (e.g. `npx vite build`), pass the frontend folder via the `cwd` parameter of `run_in_terminal` on the **same** call. Do **not** rely on a previous `cd` — each terminal invocation may start at the workspace root.
|
||||
3. If the tool doesn't take a `cwd` parameter, prefix every command with `cd <frontend-folder> &&` and put it on the *same* shell call as the build command. Never split `cd` and `npx vite build` across two separate `run_in_terminal` calls.
|
||||
4. The frontend folder is whatever the plan specifies — usually `services/web/`. Confirm by checking that the folder contains both `package.json` and `index.html` (or `vite.config.*`).
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Preview compatibility — the "Approve UI" iframe MUST be able to load your dev server
|
||||
|
||||
After scaffolding, the flow opens an **Approve UI** preview (the `open_frontend_preview_view` tool). That tool **starts your frontend's dev server for you** and renders it inside a **VS Code webview iframe**; the user clicks **Approve UI** there to continue to integration. If that iframe can't load the running app, the user is **stuck** — the app can be "live" in a normal browser window yet the preview shows a blank surface or never leaves "Starting…", and **Approve UI never enables**. Scaffold the frontend so the preview can always load it:
|
||||
|
||||
- **Make the dev server embeddable + reachable.** In `vite.config` set `server: { host: true, allowedHosts: true, strictPort: false }` (Angular: `ng serve --host 0.0.0.0 --disable-host-check`; Next.js: `next dev -H 0.0.0.0`). `host: true` lets the webview / forwarded port (remote, Codespaces, Dev Container, SSH) reach it; `allowedHosts: true` stops the dev server 403-blocking the forwarded/webview origin; `strictPort: false` lets the preview bind a free port instead of erroring when the default is taken. See [architecture.md](.github/agents/shared-references/architecture.md) → Frontend Dev Server Configuration.
|
||||
- **Keep the `dev`/`start` script a real server.** It must launch a server that prints `http://localhost:<port>/` (plain `vite`, `next dev`, `ng serve`) — never `vite build --watch` or a build-only script. The preview waits for that URL before it enables **Approve UI**; a script that never serves leaves it on "Starting…" until it times out.
|
||||
- **Never frame-bust the dev server.** No `X-Frame-Options` header from the dev server and no `<meta http-equiv="Content-Security-Policy" content="… frame-ancestors …">` in `index.html`. Those let a top-level browser tab load the app but block embedding in the webview iframe — the exact "works in my browser, blank in the preview" trap.
|
||||
- **Do not run a competing dev server.** The `open_frontend_preview_view` tool owns the single dev server; the scaffold only *builds* (`npm run build`, per F4). Do **not** start your own `npm run dev`, and do **not** scaffold a VS Code task that auto-starts the dev server on folder open — a second server contends for the port, so the preview's own server can fail to bind and error out while the other server is what shows "live" in the browser.
|
||||
|
||||
---
|
||||
|
||||
## Sub-step F1: Initialize Frontend Project
|
||||
|
||||
| Task | Details |
|
||||
|------|---------|
|
||||
| Initialize frontend project | React + Vite / Vue + Vite / Angular / Svelte (per plan) |
|
||||
| **Make the dev server preview-embeddable** | In `vite.config` set `server: { host: true, allowedHosts: true, strictPort: false }` (Angular: `ng serve --host 0.0.0.0 --disable-host-check`; Next.js: `next dev -H 0.0.0.0`). Keep the `dev` script a plain server (`vite`), and keep `X-Frame-Options` / `frame-ancestors` CSP out of `index.html`. This is what lets the **Approve UI** preview load your app in its webview iframe (the `/api` proxy is added later by the integrate agent). See the **Preview compatibility** callout above. |
|
||||
| Create the frontend folder | Use the path the plan specifies (e.g. `services/web/`); follow the user's existing structure when one exists. Standard structure matching plan's frontend framework |
|
||||
| Create local type definitions | Define entity types locally in the frontend's types folder (e.g. `services/web/src/types/`) — standalone mock types for now |
|
||||
| **Define the `ApiClient` interface** | In the seam folder `services/web/src/api/types.ts`, declare an `ApiClient` interface with **one method per endpoint in the plan's API route inventory (Section 7)**, named and typed from the route contract (e.g. `listItems(): Promise<Item[]>`, `getItem(id: string): Promise<Item>`, `createItem(input: CreateItemRequest): Promise<Item>`). **Then add a method for every *other* entity any page, component, or provider renders, even when the plan has no route for it** — assignee/author lookups, the signed-in user, category or status reference lists (e.g. `listUsers(): Promise<User[]>`, `getCurrentUser(): Promise<User>`). When `API Login` is `Yes`, include the auth methods the UI needs, including `createAccount(input)`, even though the plan omits auth routes. Reference data still goes through the seam; the mock backs it at scaffold time and the integrate agent maps it to a real route or a static list later. **The interface must cover everything the UI renders** — a page that needs data the interface does not expose has no legal way to get it and will be forced to reach around the seam. This interface is the **stable seam** both the mock and the future live client implement — it is what makes integration a one-file swap. |
|
||||
|
||||
---
|
||||
|
||||
## Sub-step F2: Create the Mock Data Layer Behind the `src/api/` Seam
|
||||
|
||||
> **Load-bearing seam rule.** **No file outside `services/web/src/api/` imports the mock** — not pages, not hooks, not shared components, not the auth provider. They all import a single `api` object from `services/web/src/api/` (the seam). At scaffold time that `api` is backed by the mock implementation; at integrate time the integrate agent swaps **one file** (`src/api/index.ts`) to point at the live client and deletes the mock impl — **with no edits anywhere outside `src/api/`**. Anything that imported `src/mocks/` directly stops compiling the moment the mock is deleted, which is exactly what turns the one-file swap into a rewrite.
|
||||
>
|
||||
> **If a file needs data the `ApiClient` interface does not expose — including reference data the plan has no route for — add a method to the interface (F1) and back it with the mock. Never import the fixture.** This case is not an exemption from the rule and it is not a judgement call: extend the seam. Wire the seam exactly as below so the swap stays a single-file edit.
|
||||
|
||||
| Task | Details |
|
||||
|------|---------|
|
||||
| Create mock data files | `services/web/src/mocks/data.ts` — realistic sample data matching plan entities. **Any entity field that represents an image — `image`/`photo`/`avatar`/`cover`/`thumbnail`/`banner`/`url` on a media entity — MUST be populated with a real, loadable image URL**, never left blank, `null`, or pointing at a solid-color placeholder. Use `https://picsum.photos/seed/<stable-id>/<w>/<h>` for generic media, curated `https://images.unsplash.com/...` URLs for domain-specific imagery, and `https://i.pravatar.cc/<size>?u=<id>` for avatars. Empty media surfaces render as flat color blocks and fail the quality bar. |
|
||||
| Create the mock client (an `ApiClient` impl) | `services/web/src/api/mockClient.ts` — `export const mockClient: ApiClient = { … }` implementing **every** method of the `ApiClient` interface from F1 (including the reference-data methods), returning data from `src/mocks/data.ts` with small simulated delays. It MUST satisfy the interface so it is type-interchangeable with the future live client. **Declare the mock as `ApiClient`; never derive the interface from the mock.** `export type ApiClient = typeof mockClient` inverts the seam — integrate deletes the mock, which deletes the interface with it and leaves the live client nothing to implement. |
|
||||
| **Create the seam entry (the one file that swaps)** | `services/web/src/api/index.ts` — the single swap point. It contains exactly:<br>`import type { ApiClient } from './types';`<br>`import { mockClient } from './mockClient';`<br>`export const api: ApiClient = mockClient;`<br>`export type { ApiClient } from './types';`<br>At integrate time only this file changes (mock → live). Keep it this small — no logic, just the wiring line. |
|
||||
| **Auto-seed auth state** | When the plan says `API Login: Yes`, the local auth state MUST auto-login with local credentials on first load (no token in storage). Preview boots directly into the main authenticated view, not a login page. Login/logout MUST still work if the user manually logs out. **Get the signed-in user from the seam (`api.getCurrentUser()`, added to the interface in F1). The auth provider follows the seam rule exactly like a page, so `import { currentUser } from '../mocks/data'` is a violation.** When `API Login: No`, do not scaffold auth state or login UI. |
|
||||
| **Build the Mock State Switcher (STANDARD — always)** | Create `services/web/src/api/previewState.ts` exposing the forced `PreviewDataState` (`'data' \| 'loading' \| 'empty' \| 'error'`), initialized from the `?previewState=` query param → `localStorage['previewState']` → `'data'`. The **mock client must honor it on every method**: `loading` → never/slowly resolves; `error` → rejects with a realistic `Error`; `empty` → returns `[]` / `null`; `data` → normal fixtures. Render a small fixed-corner Data/Loading/Empty/Error switcher gated by `import.meta.env.DEV` (forced to `'data'`, no UI, in PROD). This is a fixed contract — scaffold it the same way every time. See the Mock State Switcher standard in [frontend-quality-bar.md](.github/agents/azure-project-scaffold/references/frontend-quality-bar.md). |
|
||||
| Handle all 4 data states | Loading (skeleton/spinner), Error (retry button), Empty (call-to-action), Data (populated) — all four reachable live via the Mock State Switcher above |
|
||||
|
||||
---
|
||||
|
||||
## Sub-step F3: Create Pages & Components
|
||||
|
||||
| Task | Details |
|
||||
|------|---------|
|
||||
| Create pages | One page per major feature, wired to the `api` object from the seam — `import { api } from '@/api'` (or the relative `../api`). **Never import from `src/mocks/` or `src/api/mockClient.ts` in ANY file outside `src/api/`** — not a page, not a hook, not a shared component, not the auth provider. Need an assignee name, the signed-in user, or any other reference data? Add an `ApiClient` method for it (F1) and call it through the seam. This is what keeps integration a one-file swap. |
|
||||
| Create shared components | Reusable UI components (layout, nav, forms, cards) |
|
||||
| Error handling in hooks | Every async hook catches errors, handles loading/error states |
|
||||
| Destructive action confirmations | Delete and irreversible actions require user confirmation |
|
||||
| Auth context auto-login | When `API Login` is `Yes`, AuthProvider/auth context MUST auto-login on mount when no token exists, so preview opens to main authenticated content |
|
||||
| Account creation | When `API Login` is `Yes`, the login page MUST show a visible **Create account** button that routes to a dedicated create-account page. The page calls `api.createAccount(...)` through the seam and displays validation, duplicate-account, loading, and success states. This is required even though local auto-login makes the main authenticated content the initial preview. |
|
||||
| Use correct file extensions | `.tsx` for JSX, `.ts` for pure TypeScript |
|
||||
|
||||
---
|
||||
|
||||
## Sub-step F4: Build & Verify the Frontend
|
||||
|
||||
> ⚠️ **PARALLEL STEP**: Frontend generation + build (F1–F4, sub-agent) runs **concurrently** with Phase A (Contracts) and Phase B (Backend). Backend derives from **plan's route definitions and entity types**, not the frontend — independent work streams. Phase A and Phase B may begin immediately after Step 0 (plan validation) while the Frontend sub-agent generates and builds `services/web/`.
|
||||
>
|
||||
> The Frontend sub-agent only needs to **generate and build** `services/web/` with mock data — it does **not** wire to the real backend. The verify agent, in a later session, swaps the seam (`src/api/index.ts`) from the mock client to the live client and replaces the local mock types with shared imports — a one-file swap at the seam, no page or hook edits.
|
||||
>
|
||||
> **Why safe**: Entity types, route definitions, service interfaces all come from approved plan. Frontend uses standalone mock types (`services/web/src/types/`) independent of `services/shared/`, behind the `ApiClient` seam (`src/api/`). Frontend UI changes (layout, styling, components) don't affect backend contracts. The verify agent merges both streams later by repointing the seam at the live client and replacing mock types with shared imports.
|
||||
|
||||
> ⚠️ ️ **WORKING DIRECTORY** (see also the top of this file): every `npx vite build`, `npm run build`, `npm install`, etc. **MUST run against the frontend folder** (e.g. `services/web/`), never the workspace root. **Prefer the working-directory-independent form `npm --prefix services/web run <script>`** (e.g. `npm --prefix services/web run build`) — it loads the frontend's `package.json` regardless of where the shell starts. When invoking a binary directly (`npx vite build`), pass `cwd: services/web` on the same terminal call; do **not** assume a previous `cd` carried over.
|
||||
|
||||
> ⚠️ **NO UX APPROVAL PROMPT.** The user already approved the design during planning via the HTML/CSS mock-up at `.azure/.preview-temp/`. **Do NOT call `ask_user` for "do you approve this UI?"** during scaffolding. The only legitimate user prompt during Step 1 is a hard build failure that requires their input to resolve.
|
||||
|
||||
### Procedure
|
||||
|
||||
1. **Frontend builds with zero errors.** Build with a **working-directory-independent** command so it can't accidentally run from the workspace root: `npm --prefix <frontend-folder> run build` (e.g. `npm --prefix services/web run build`). `--prefix` resolves the frontend's `package.json` regardless of where the shell starts, so it's immune to the root-launch bug. Only fall back to `npx vite build` with `cwd: <frontend-folder>` if there is no `build` script. **Never run a bare `npx vite build` from the project root.**
|
||||
2. No `any` types in `.ts`/`.tsx` files.
|
||||
3. When `API Login` is `Yes`, local identity state is seeded so the app lands on main content, not a login page, on first load. Logging out exposes a login page with a visible **Create account** button and a working dedicated create-account page. When it is `No`, the frontend contains no auth UI or state.
|
||||
4. **Briefly note** that the frontend was generated and builds cleanly, and that backend work continues in parallel — one short sentence. **Then keep working** — no approval question, no waiting loop.
|
||||
|
||||
> **CRITICAL**: Do NOT prompt "Would you like to preview?" or "Do you approve this UI?" during scaffolding — design approval already happened during planning via the HTML mock-up.
|
||||
|
||||
### Translating the planning mock-up into real framework code
|
||||
|
||||
The `.azure/.preview-temp/*.html` files are a **layout + tonal reference**, not source code to ship. For each page:
|
||||
|
||||
- The plan's Pages table tells you which **regions** belong on the page (`header + hero + grid + footer` etc.).
|
||||
- The HTML mock-up shows the **approved arrangement, density, and palette** of those regions.
|
||||
- Your job in Step 1 is to reproduce that visual feel using the **real `Component Library:` primitives** from Section 6 (see [frontend-quality-bar.md](.github/agents/azure-project-scaffold/references/frontend-quality-bar.md) for the per-library token → primitive mapping).
|
||||
|
||||
Do not import the HTML mock-up, embed it via `<iframe>`, or copy CSS class names from it into your JSX — the mock-up is throwaway and `.azure/.preview-temp/` is deleted in Step 11. Only the visual intent (regions, palette, density) carries forward into real components.
|
||||
|
||||
---
|
||||
|
||||
## Frontend Quality Bar
|
||||
|
||||
Even before it's wired to the backend, the frontend MUST meet these standards. The full per-library contract lives in [frontend-quality-bar.md](.github/agents/azure-project-scaffold/references/frontend-quality-bar.md) — read it before writing any JSX. Baseline rules enforced here:
|
||||
|
||||
- No `any` types (use local type definitions in `services/web/src/types/`)
|
||||
- Hooks catch errors and handle loading/error states
|
||||
- Destructive actions (delete, etc.) require `window.confirm()` before executing
|
||||
- `.tsx` for files containing JSX, `.ts` for pure TypeScript
|
||||
- All 4 data states handled: loading, error, empty, data (see quality-bar's State Coverage Contract for per-library primitives — `<Skeleton>` / `<MessageBar intent="error">` / empty illustration + CTA / real data), and all four reachable live via the **Mock State Switcher** (dev-only `?previewState=` override — see quality-bar's Mock State Switcher standard)
|
||||
- **Auto-authenticated preview**: When `API Login` is `Yes`, local auth state MUST auto-login on first load so the user sees main content immediately, not a login page
|
||||
- **Render layout tokens with real library primitives** — never raw `<div className="card">` placeholders. See [frontend-quality-bar.md](.github/agents/azure-project-scaffold/references/frontend-quality-bar.md) for the region-token → primitive mapping per library.
|
||||
- **Wrap the app shell in the library's theme provider** with a brand ramp derived from plan Section 6's `primary` color. See [frontend-quality-bar.md](.github/agents/azure-project-scaffold/references/frontend-quality-bar.md) → Theming contract.
|
||||
- **Use real icons** from the library's icon set (Fluent: `@fluentui/react-icons` Regular variants; Vuetify: `mdi-*`; Material: `<mat-icon>` real names; Skeleton/Pico: Lucide/Tabler). No emoji, no SVG placeholders.
|
||||
@@ -0,0 +1,344 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# Frontend Quality Bar — Render Layout Tokens with Real Library Primitives
|
||||
|
||||
> **Load this BEFORE writing any frontend page or component.** Read during **Step 1** (Frontend). This is the contract between the plan's Section 5 (Design System & UI) and the scaffolded JSX.
|
||||
|
||||
> **Read first:** the fidelity-agnostic **Shared design-quality principles** in [`../shared-references/frontend-quality-bar.md`](.github/agents/shared-references/frontend-quality-bar.md) — the single design contract shared by the planning preview and this scaffold. This file adds the **framework rendering layer** (per-library primitive mapping, theming, motion, dark mode) on top of those principles.
|
||||
|
||||
---
|
||||
|
||||
## Core principle
|
||||
|
||||
> **Layout tokens are layout INTENT, not implementation.** When the plan's Section 6 says a page's layout is `header + hero + grid + footer`, that does NOT mean produce four `<div>`s with placeholder text. It means **render the equivalent of those regions using the real `Component Library` named in Section 6, themed by the Section 6 Color Palette.**
|
||||
|
||||
If you ever emit JSX like this:
|
||||
|
||||
```tsx
|
||||
// ❌ ZERO-EFFORT WIREFRAME REPRODUCTION — DO NOT SHIP
|
||||
<div className="header">Header</div>
|
||||
<div className="hero">Hero</div>
|
||||
<div className="grid">
|
||||
<div className="card">Card 1</div>
|
||||
<div className="card">Card 2</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
…you have failed the quality bar. That output **looks worse than the presentation-quality plan preview** the user already approved, because it strips the theme, the icons, the elevation, and the state coverage the preview shipped. The preview is a promise; this contract is how you keep it.
|
||||
|
||||
---
|
||||
|
||||
## Section 6 inputs you MUST consume
|
||||
|
||||
| Field in plan §5 | What it controls |
|
||||
|--------------------------|--------------------------------------------------------------------------|
|
||||
| `Component Library:` | Which library's primitives you import and render (mandatory). |
|
||||
| `Style Direction:` | Density, corner radius, elevation, list-vs-hero bias. |
|
||||
| `Typography:` | Font family applied at the app shell level. |
|
||||
| Color Palette table | Brand ramp / theme tokens — wire through the library's theme provider. |
|
||||
| Pages table (`Layout`) | Which **library primitives** to compose per page (see mapping below). |
|
||||
| `.azure/.preview-temp/*.html` + `theme.css` | The **presentation-quality static HTML/CSS preview** the user approved during planning. It is a faithful visual spec: page regions and order, the themed palette, real inline-SVG icons, populated content, and all four data states. What it deliberately omits (raw-static limits): real webfonts, motion, working dark mode, and real photos. **Reproduce it faithfully with real library primitives and add only those production-only capabilities** (see "Polish floor" below) — never regress below it. Do not import, embed, or `<iframe>` it. Folder is deleted in Step 11. |
|
||||
|
||||
> If Section 6 is missing or `Component Library:` is blank, **STOP**. The plan is incomplete — re-run `azure-project-plan` instead of guessing.
|
||||
|
||||
---
|
||||
|
||||
## Per-library region-token → primitive mapping
|
||||
|
||||
Pick the row that matches the plan's `Component Library:` value. Every region token below MUST resolve to a real, themed library primitive — not a bare `<div>`. Compound tokens (`split(a|b)`, `two-column(a+b)`) compose two of these in a CSS Grid.
|
||||
|
||||
### Fluent UI v9 (`@fluentui/react-components`) — React default
|
||||
|
||||
| Region token | Primitive(s) to render |
|
||||
|--------------|---------------------------------------------------------------------------------------------------------------------|
|
||||
| `header` | `<Toolbar>` + `<Subtitle1>` (app title) + `<Link>`s + `<Avatar name=… />` on the right |
|
||||
| `nav` | Horizontal `<TabList>` + `<Tab icon={<HomeRegular />}>` per route |
|
||||
| `sidebar` | Vertical `<TabList vertical>` + `<Tab icon=…>` per route, with `<Divider>` between groups |
|
||||
| `hero` | `<Card appearance="filled-alternative">` + `<Title3>` + `<Body1>` + `<Button appearance="primary">` |
|
||||
| `main` | `<Card>` wrapper with `<CardHeader>` + body content driven by page intent |
|
||||
| `list` | List of `<Card>` rows with `<CardHeader image={<Avatar />} header=… description=… />` |
|
||||
| `card-list` | CSS Grid of `<Card>`s with `<CardPreview>` + `<CardHeader>` + `<CardFooter>` (real ratings / metadata, not lorem) |
|
||||
| `grid` | Same as `card-list` but tighter (e.g. `repeat(auto-fill, minmax(220px, 1fr))`) |
|
||||
| `form` | `<Field label=… validationState=…>` wrapping `<Input>` / `<Textarea>` / `<Combobox>` / `<Switch>` — at least one field shows `validationState="warning"` with a `validationMessage` so the four-state contract is visible |
|
||||
| `table` | `<Table>` + `<TableHeader>` + `<TableHeaderCell>`s + `<TableBody>` + `<TableRow>` / `<TableCell>` (real columns) |
|
||||
| `tabs` | `<TabList>` + `<Tab>`s with the first tab `selected`, second tab content as the visible body |
|
||||
| `actions` / `action-bar` | `<Toolbar>` + `<ToolbarButton icon=…>`s; primary action uses `appearance="primary"` |
|
||||
| `modal` | Inline `<Card>` mock (do NOT mount a real `<Dialog>` in the preview — it steals focus and breaks the screenshot) |
|
||||
| `footer` | `<Divider>` + horizontal `<Caption1>` row with copyright + links |
|
||||
| unknown | `<MessageBar intent="info">` saying `Unknown layout token "{token}" — will be rendered with {Component Library} in scaffold` |
|
||||
|
||||
**Theming**: Wrap the app shell in `<FluentProvider theme={appTheme}>` where `appTheme = createLightTheme(brandRamp)` and `brandRamp` is a 16-step `BrandVariants` derived from Section 6's `primary` color. Body font comes from Section 6's `Typography`.
|
||||
|
||||
**Icons**: Use real icons from `@fluentui/react-icons` (Regular variants) — never emoji and never inline SVG hand-drawn placeholders. Reasonable defaults: `HomeRegular`, `SearchRegular`, `SettingsRegular`, `PersonRegular`, `GridRegular`, `DocumentRegular`, `BookmarkRegular`, `MailRegular`, `CalendarRegular`, `ChevronDownRegular`, `AppsRegular`, `TableSimpleRegular`.
|
||||
|
||||
### Vuetify 3 — Vue default
|
||||
|
||||
| Region token | Primitive(s) to render |
|
||||
|--------------|--------------------------------------------------------------------------------------------------|
|
||||
| `header` | `<v-app-bar>` + `<v-app-bar-title>` + `<v-spacer>` + `<v-avatar>` |
|
||||
| `nav` | `<v-tabs>` + `<v-tab prepend-icon="…">` |
|
||||
| `sidebar` | `<v-navigation-drawer permanent>` + `<v-list>` + `<v-list-item prepend-icon=…>` |
|
||||
| `hero` | `<v-card variant="tonal">` + `<v-card-title>` + `<v-card-text>` + `<v-btn color="primary">` |
|
||||
| `main` | `<v-card>` + `<v-card-title>` + body slot |
|
||||
| `list` | `<v-list>` of `<v-list-item>` with `prepend-avatar` + `title` + `subtitle` |
|
||||
| `card-list` / `grid` | `<v-row>` of `<v-col cols="12" sm="6" md="4">` wrapping `<v-card>`s with `<v-img>` + `<v-card-title>` + `<v-card-actions>` |
|
||||
| `form` | `<v-form>` + `<v-text-field>` / `<v-textarea>` / `<v-select>` / `<v-switch>` (at least one with `:error-messages` set) |
|
||||
| `table` | `<v-data-table>` with real `headers` + `items` |
|
||||
| `tabs` | `<v-tabs>` + `<v-tab>` + `<v-window>` + `<v-window-item>` |
|
||||
| `actions` | `<v-toolbar density="compact">` + `<v-btn color="primary">` |
|
||||
| `modal` | Inline `<v-card>` (NOT `<v-dialog>` in preview) |
|
||||
| `footer` | `<v-divider />` + `<v-footer>` row |
|
||||
| unknown | `<v-alert type="info">Unknown layout token "{token}"</v-alert>` |
|
||||
|
||||
**Theming**: Configure `vuetify({ theme: { themes: { light: { colors: { primary: '…', surface: '…', … } } } } })` from Section 6's palette.
|
||||
|
||||
### Angular Material — Angular default
|
||||
|
||||
| Region token | Primitive(s) to render |
|
||||
|--------------|------------------------------------------------------------------------------|
|
||||
| `header` | `<mat-toolbar color="primary">` + title + spacer + `mat-icon-button` |
|
||||
| `nav` / `sidebar` | `<mat-sidenav-container>` + `<mat-sidenav mode="side" opened>` + `<mat-nav-list>` |
|
||||
| `hero` | `<mat-card appearance="outlined">` + `<mat-card-title>` + `<button mat-raised-button color="primary">` |
|
||||
| `list` | `<mat-list>` + `<mat-list-item>` with `<mat-icon matListItemIcon>` |
|
||||
| `card-list` / `grid` | `<mat-grid-list cols="3">` + `<mat-grid-tile>` + `<mat-card>` |
|
||||
| `form` | `<mat-form-field appearance="outline">` + `<input matInput>` / `<mat-select>` / `<mat-slide-toggle>` (at least one with `<mat-error>`) |
|
||||
| `table` | `<table mat-table [dataSource]=…>` with `<th mat-header-cell>` + `<td mat-cell>` |
|
||||
| `tabs` | `<mat-tab-group>` + `<mat-tab label=…>` |
|
||||
| `actions` | `<mat-toolbar>` + `<button mat-raised-button color="primary">` |
|
||||
| `modal` | Inline `<mat-card>` (NOT `<mat-dialog>` in preview) |
|
||||
| `footer` | `<mat-divider>` + footer row |
|
||||
| unknown | `<mat-card>` with warning icon + `Unknown layout token "{token}"` |
|
||||
|
||||
**Theming**: Define a Material 3 theme via `mat.define-theme(...)` using Section 6's primary as the seed color.
|
||||
|
||||
### Skeleton UI (Svelte default)
|
||||
|
||||
| Region token | Primitive(s) to render |
|
||||
|--------------|--------------------------------------------------------------------------------------------------|
|
||||
| `header` | `<AppBar>` with `lead`, `headline`, `trail` slots |
|
||||
| `nav` | `<TabGroup>` + `<Tab>`s with `<svelte:fragment slot="lead">` icon |
|
||||
| `sidebar` | `<AppRail>` + `<AppRailTile>`s, or `<AppShell sidebarLeft>` + `<nav>` + `<a class="list-item">` |
|
||||
| `hero` | `<div class="card variant-glass-primary p-8">` + `<h2 class="h2">` + `<button class="btn variant-filled-primary">` |
|
||||
| `list` | `<ul class="list">` + `<li>` with avatar `<Avatar>` + title + subtitle |
|
||||
| `card-list` / `grid` | CSS Grid of `<div class="card">` blocks with `<header class="card-header">` + body + footer |
|
||||
| `form` | `<label class="label">` + `<input class="input">` / `<textarea class="textarea">` / `<select class="select">` / `<SlideToggle>` (at least one with `input-error`) |
|
||||
| `table` | `<Table source={…} />` (Skeleton) or `<table class="table table-hover">` |
|
||||
| `tabs` | `<TabGroup>` + `<Tab>`s |
|
||||
| `actions` | `<div class="card-footer">` + `<button class="btn variant-filled-primary">` |
|
||||
| `modal` | Inline `<div class="card">` (NOT `<Modal>` in preview) |
|
||||
| `footer` | `<hr class="hr">` + footer `<div>` |
|
||||
| unknown | `<aside class="alert variant-ghost-surface">Unknown layout token "{token}"</aside>` |
|
||||
|
||||
**Theming**: Build a Skeleton theme module from Section 6's palette and set `data-theme="…"` on the root.
|
||||
|
||||
### Pico.css / plain HTML
|
||||
|
||||
| Region token | Primitive(s) to render |
|
||||
|--------------|------------------------------------------------------------------------------|
|
||||
| `header` | `<header><nav><ul>…</ul></nav></header>` (Pico styles `<nav>` automatically) |
|
||||
| `nav` | `<nav><ul>` of `<li><a>` |
|
||||
| `sidebar` | `<aside><nav>` of `<li><a>` |
|
||||
| `hero` | `<article>` + `<hgroup>` + `<button>` |
|
||||
| `list` / `card-list` / `grid` | `<div class="grid">` of `<article>` cards with `<hgroup>` |
|
||||
| `form` | `<form>` + `<label>` + `<input>` / `<select>` / `<textarea>` (one with `aria-invalid="true"`) |
|
||||
| `table` | `<figure><table>` + `<thead>` / `<tbody>` |
|
||||
| `tabs` | `<nav>` of `<a role="button">` (Pico has no native tabs — flatten to nav) |
|
||||
| `actions` | `<footer><button class="primary">` |
|
||||
| `modal` | Inline `<dialog open>` (rendered, not toggled) |
|
||||
| `footer` | `<footer>` with small text |
|
||||
| unknown | `<mark>Unknown layout token "{token}"</mark>` |
|
||||
|
||||
**Theming**: Set CSS custom properties (`--pico-primary`, `--pico-background-color`, `--pico-color`, `--pico-muted-color`) from Section 6's palette on `:root` or `[data-theme=light]`.
|
||||
|
||||
---
|
||||
|
||||
## State coverage contract (every data-bearing page)
|
||||
|
||||
Every page that displays data MUST cover all four states with real library primitives — not text-only fallbacks:
|
||||
|
||||
| State | Fluent UI v9 | Vuetify 3 | Angular Material | Skeleton | Pico |
|
||||
|----------|-----------------------------------------------|--------------------------------------|-------------------------------------------------|-----------------------------------------|------------------------------------|
|
||||
| loading | `<Skeleton>` + `<SkeletonItem>` rows | `<v-skeleton-loader type="card">` | `<ngx-skeleton-loader>` or `<mat-progress-bar>` | `<div class="placeholder animate-pulse">` | `<progress indeterminate>` |
|
||||
| error | `<MessageBar intent="error">` + retry `<Button>` | `<v-alert type="error">` + retry `<v-btn>` | `<mat-card>` + `<mat-icon>error</mat-icon>` + retry button | `<aside class="alert variant-filled-error">` + retry | `<article><mark>` + retry `<button>` |
|
||||
| empty | `<Card>` with illustration + `<Body1>` + primary `<Button>` CTA | `<v-empty-state>` or `<v-card>` with `<v-icon>` + `<v-btn>` CTA | `<mat-card>` + `<mat-icon>` + primary action | `<div class="card">` + icon + primary CTA `<button>` | `<article>` + `<p>` + primary `<button>` |
|
||||
| data | Real list/grid/table from mock fixtures | Real list/grid/table | Real list/grid/table | Real list/grid/table | Real list/grid/table |
|
||||
|
||||
> The four states MUST be reachable in the running app — wire a small dev-only toggle (URL hash, query param, or a corner button gated by `import.meta.env.DEV`) so `loading`, `error`, `empty`, and `data` can each be exercised. The toggle is the verification path for the four-state contract.
|
||||
|
||||
---
|
||||
|
||||
## Theming contract
|
||||
|
||||
1. **Build a brand ramp from Section 6's `primary`** and pass it to the library's theme provider — do **not** ship the library's default brand color.
|
||||
- Fluent UI v9: `createLightTheme(brandRamp)` where `brandRamp: BrandVariants` is a 16-step ramp from HSL lightening/darkening of `primary`.
|
||||
- Vuetify: `theme.themes.light.colors.primary`.
|
||||
- Angular Material: `mat.define-theme({ color: { primary: $palette } })`.
|
||||
- Skeleton: custom theme module with `--color-primary-*` CSS variables.
|
||||
- Pico: `--pico-primary` CSS variable.
|
||||
2. **Map `surface` / `text` / `muted` / `border`** onto the library's neutral tokens — do not hard-code colors in component JSX. Semantic states (success / warning / error) come from the library's built-in semantic tokens, not from the plan palette.
|
||||
3. **Apply `Typography`** at the app shell level (Fluent: `FluentProvider` style override; Vuetify: `<v-app>` font-family; Angular: `--mat-sys-body-large-font`; Skeleton: theme module; Pico: `:root { font-family: … }`).
|
||||
4. The plan-preview webview renders a sandboxed **HTML/CSS** preview — purely presentational, no JavaScript, no real component library involved. Each page lives at `.azure/.preview-temp/<slug>.html` and shares a single `.azure/.preview-temp/theme.css`. Treat those files as a **presentation-quality visual spec** — approved regions, themed palette, real icons, populated content, and all four states. Reproduce that look with real library primitives and **add the production-only capabilities the static files could not include**: real webfont, motion, dark mode, and real imagery. Do not import, embed, or `<iframe>` the preview. `.azure/.preview-temp/` is deleted in Step 11.
|
||||
|
||||
---
|
||||
|
||||
## Quality gate (run this checklist before claiming the preview is ready)
|
||||
|
||||
- [ ] Every page imports primitives from the library named in Section 6 — **zero raw `<div className="card">` / `<div className="header">` placeholders** outside the layout grid wrappers.
|
||||
- [ ] App shell is wrapped in the library's theme provider; brand ramp is derived from Section 6 `primary`.
|
||||
- [ ] Every icon is a real library icon (Fluent: `*Regular` from `@fluentui/react-icons`; Material: `<mat-icon>name</mat-icon>` with real names; Vuetify: `mdi-*`; Skeleton/Pico: native SVG icons via a real icon set such as Lucide or Tabler). **No emoji, no `<svg viewBox="0 0 1 1">` placeholders.**
|
||||
- [ ] Every `form` region has at least one field with a visible validation state (warning/error) and an inline message.
|
||||
- [ ] Every data-bearing page exposes all four states (loading / error / empty / data) via a dev-only toggle.
|
||||
- [ ] `Style Direction:` is reflected in density and corner radius (e.g. "data-dense" → compact toolbars, tight list rows; "calm and spacious" → generous padding, larger cards).
|
||||
- [ ] No `any` types; the four-state contract still holds; locally seeded identity works when `API Login` is `Yes`, and no auth UI exists when it is `No`.
|
||||
- [ ] When `API Login` is `Yes`, logging out reveals a visible **Create account** button on the login page, and that button opens a complete create-account page with validation and error states.
|
||||
- [ ] The scaffolded UI **reproduces the approved preview at `.azure/.preview-temp/<slug>.html` and adds the production-only layer** — same regions, same brand color, same density and populated content, now with real library primitives, real webfont, motion, dark mode, and real imagery the static preview could not include. If a generated page looks **less** polished than the preview, the page has failed the bar.
|
||||
|
||||
---
|
||||
|
||||
## Polish floor — every scaffolded app, regardless of library
|
||||
|
||||
These eight requirements are **non-negotiable**. They are the gap between "the sketch with components swapped in" and "an app the user wants to ship". A page that misses any of them fails the bar.
|
||||
|
||||
### 1. Hero treatment (every landing / dashboard / list-index page)
|
||||
|
||||
A flat colored panel is **not** a hero. Every hero region MUST have:
|
||||
|
||||
- A **brand-gradient card** (linear or radial) using two stops from Section 6's palette (typically `primary` → `accent`, or `primary` → a 12-step-lighter `primary`).
|
||||
- An **eyebrow line** above the headline: uppercased, letter-spaced ~0.12em, ~11–12px, with a 6px dot prefix — distinct from the headline.
|
||||
- A **headline** at the library's `display`/`Title1`/`h1` token, max-width ~24ch, line-height ~1.1, sub-tight letter-spacing.
|
||||
- A **subtitle** at the body token, max-width ~60ch, opacity 0.85–0.95.
|
||||
- **Two CTAs**: a primary library button (named action, see microcopy below) + a secondary/ghost variant.
|
||||
- An **ambient SVG mesh backdrop** layered behind the gradient. Concrete pattern:
|
||||
|
||||
```tsx
|
||||
<svg className="hero-mesh" aria-hidden="true" viewBox="0 0 800 400" preserveAspectRatio="none">
|
||||
<defs>
|
||||
<radialGradient id="m1" cx="0%" cy="0%" r="80%">
|
||||
<stop offset="0%" stopColor="var(--brand-accent)" stopOpacity="0.55" />
|
||||
<stop offset="100%" stopColor="var(--brand-accent)" stopOpacity="0" />
|
||||
</radialGradient>
|
||||
<radialGradient id="m2" cx="100%" cy="100%" r="80%">
|
||||
<stop offset="0%" stopColor="var(--brand-primary)" stopOpacity="0.55" />
|
||||
<stop offset="100%" stopColor="var(--brand-primary)" stopOpacity="0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<rect width="800" height="400" fill="url(#m1)" />
|
||||
<rect width="800" height="400" fill="url(#m2)" />
|
||||
</svg>
|
||||
```
|
||||
|
||||
Position the SVG `position: absolute; inset: 0; pointer-events: none; opacity: 0.7;`. Layer the gradient panel above it. The `aria-hidden` is required.
|
||||
|
||||
### 2. Real icons — everywhere (no exceptions)
|
||||
|
||||
Every navigation item, sidebar item, KPI tile, empty state, primary CTA, and section title row MUST carry a real, named icon from the library's official icon set:
|
||||
|
||||
| Library | Icon source | Concrete sample imports |
|
||||
|------------------|-----------------------------------|--------------------------------------------------------------------------------------------------|
|
||||
| Fluent UI v9 | `@fluentui/react-icons` Regular | `HomeRegular`, `SearchRegular`, `SettingsRegular`, `PersonRegular`, `GridRegular`, `BookmarkRegular`, `ChevronRightRegular`, `AddRegular`, `DocumentRegular`, `InboxRegular`, `CalendarRegular`, `ChartMultipleRegular` |
|
||||
| Vuetify 3 | Material Design Icons (`mdi-*`) | `mdi-home`, `mdi-magnify`, `mdi-cog`, `mdi-account`, `mdi-view-dashboard`, `mdi-bookmark`, `mdi-chevron-right`, `mdi-plus`, `mdi-file-document`, `mdi-inbox`, `mdi-calendar`, `mdi-chart-line` |
|
||||
| Angular Material | Material Symbols / `mat-icon` | `home`, `search`, `settings`, `person`, `dashboard`, `bookmark`, `chevron_right`, `add`, `description`, `inbox`, `calendar_today`, `show_chart` |
|
||||
| Skeleton (Svelte)| Lucide-Svelte | `Home`, `Search`, `Settings`, `User`, `LayoutGrid`, `Bookmark`, `ChevronRight`, `Plus`, `FileText`, `Inbox`, `Calendar`, `LineChart` |
|
||||
| Pico | Lucide (`lucide-static` or `lucide` web) | Same Lucide names above; render via inline SVG or `<i data-lucide="home">` |
|
||||
|
||||
**Hard fail**: emoji (🏠, 📊), Unicode glyphs (▲, ★), or hand-drawn `<svg>` shapes used as nav/section/CTA iconography.
|
||||
|
||||
### 3. Motion (lightweight, library-aligned)
|
||||
|
||||
Every app MUST add motion in three places, with `prefers-reduced-motion` respected:
|
||||
|
||||
| Library | Motion tool | Where it must appear |
|
||||
|------------------|---------------------------------------|-------------------------------------------------------------------------------|
|
||||
| Fluent UI v9 | `framer-motion` or `motion/react` | Route change (fade+12px slide), card hover (1–2px lift), dialog/popover open |
|
||||
| Vuetify 3 | `@vueuse/motion` or built-in `<v-fade-transition>` / `<v-slide-y-transition>` | Route change, card hover, dialog open |
|
||||
| Angular Material | Angular Animations (`@angular/animations`) | Route change (`fadeInUp`), card hover, dialog open |
|
||||
| Skeleton | Svelte built-in `transition:fade` / `crossfade` | Route change, card hover, drawer open |
|
||||
|
||||
Duration: 150–300ms. Easing: `cubic-bezier(0.4, 0, 0.2, 1)` or library default. Wrap all motion in a `prefers-reduced-motion: reduce` media query / `useReducedMotion()` hook — no exceptions.
|
||||
|
||||
### 4. All four states — visibly, with library illustrations
|
||||
|
||||
The state contract in the table above is the **minimum**. Every empty state MUST also include a real visual element (library illustration component, Lucide/Tabler icon at 64–96px, or a domain-specific SVG), not just text:
|
||||
|
||||
- Fluent: empty card with a 64px `*Regular` icon centered above the `<Title3>` + body + primary CTA.
|
||||
- Vuetify: `<v-empty-state>` (built-in, has illustration slot) or `<v-card>` with `<v-icon size="64">`.
|
||||
- Angular Material: `<mat-card>` with a `<mat-icon style="font-size: 64px; width: 64px; height: 64px;">`.
|
||||
- Skeleton: `<div class="card">` with `<Inbox size={64} />` (Lucide) above text.
|
||||
|
||||
### 5. Density + radius derived from Style Direction
|
||||
|
||||
Translate the `Style Direction:` literal from Section 6 into concrete library tokens:
|
||||
|
||||
| Style Direction keyword | Density / radius / weight choices |
|
||||
|---------------------------------|--------------------------------------------------------------------------------|
|
||||
| `playful`, `friendly`, `consumer` | Large radii (12–20px on cards, full-pill on buttons), comfortable padding, heavier headlines (700+), saturated palette |
|
||||
| `professional`, `enterprise`, `serious` | Small radii (4–8px), compact padding, semibold headlines (600), muted palette |
|
||||
| `editorial`, `magazine`, `content-led` | Mixed radii (cards 4px, hero 0–4px), generous whitespace, large display type, serif option for headings |
|
||||
| `minimal`, `calm`, `data-dense` | Tight radii (2–6px), compressed padding, regular weight body, restrained palette, hairline 1px borders |
|
||||
|
||||
Wire these through the library's density/spacing/radius tokens (Fluent: `tokens.borderRadiusMedium`; Vuetify: `density="compact"` + theme `defaults`; Material: M3 density CSS vars; Skeleton: theme module radius variables).
|
||||
|
||||
### 6. Dark mode (required, with persistence)
|
||||
|
||||
Every app ships with light + dark themes from day one:
|
||||
|
||||
- A theme toggle in the header (uses the library's icon button).
|
||||
- Persistence via `localStorage` key `app-theme` (`'light' | 'dark' | 'system'`).
|
||||
- Initial value reads `prefers-color-scheme` when key is missing or set to `'system'`.
|
||||
- Live update on `matchMedia('(prefers-color-scheme: dark)').addEventListener('change', …)` while in `'system'` mode.
|
||||
- Both themes derive from the **same** Section 6 brand palette — only neutrals and surfaces flip.
|
||||
|
||||
### 7. Real webfont, mapped from Style Direction
|
||||
|
||||
Apply a webfont via the `<head>` (CSS `@import` or `<link>`) and wire it through the library's font token. Choose by Style Direction:
|
||||
|
||||
| Style Direction keyword | Webfont |
|
||||
|---------------------------------|------------------------------------|
|
||||
| `playful`, `friendly` | `Inter` (variable) or `Geist Sans` |
|
||||
| `professional`, `enterprise` | `Inter` or `IBM Plex Sans` |
|
||||
| `editorial`, `magazine` | `Source Sans 3` body + `Source Serif 4` headings |
|
||||
| `minimal`, `calm`, `data-dense` | `Geist Sans` or `IBM Plex Sans` |
|
||||
|
||||
Load via `<link rel="preconnect">` + `<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=…">` or self-host. Fall back to `system-ui, -apple-system, "Segoe UI", sans-serif`.
|
||||
|
||||
### 8. Microcopy — primary CTAs name the action
|
||||
|
||||
Primary buttons MUST name the action being committed. **Hard fail**: generic verbs.
|
||||
|
||||
| ❌ Generic (fails) | ✅ Named (passes) |
|
||||
|----------------------|-------------------------------------------------------------------|
|
||||
| `Submit` | `Save changes`, `Create project`, `Send invite`, `Publish post` |
|
||||
| `OK` | `Got it`, `Acknowledge`, `Mark as read` |
|
||||
| `Continue` | `Continue to billing`, `Review and confirm` |
|
||||
| `Delete` | `Delete project permanently`, `Remove from team` |
|
||||
|
||||
Secondary/ghost buttons may be generic (`Cancel`, `Back`). Modal confirm buttons follow the same rule (`Delete 3 items` not `Confirm`).
|
||||
|
||||
### 9. Imagery & art direction (domain fit beats stock chrome)
|
||||
|
||||
A page can pass items 1–8 and still look templated if it renders empty media surfaces and identical generic cards for every app. Every app MUST:
|
||||
|
||||
- **Render real images for every media-bearing entity.** Bind the mock data's image URL (see Sub-step F2) into a real `<img>` / Fluent `<CardPreview image>` / Vuetify `<v-img>` / Angular `mat-card-image` / Skeleton-Pico `<img>` — **never** an empty tinted `<CardPreview>` or a solid-color `<div>` standing in for a photo. A media card with no visible photo is a hard fail.
|
||||
- **Match the domain's visual idiom.** A photo-sharing app reads as a gallery (edge-to-edge imagery, scrapbook/polaroid framing, captions), a finance app as data-dense tables, an editorial app as a magazine, a chat app as message bubbles. Do not flatten every app into the same generic SaaS card grid.
|
||||
- **Use bespoke, domain-specific component treatments** — tilted polaroid frames, ticket stubs, chat bubbles, kanban cards, gallery tiles, etc. — **layered on top of** the library primitives from the region-token mapping. This is encouraged, not forbidden (see self-review item 12). The ban is only on empty placeholder `<div>`s that re-skin the wireframe, never on art direction that fits the domain.
|
||||
|
||||
---
|
||||
|
||||
## Polish self-review checklist (per page, before marking complete)
|
||||
|
||||
Run through this 12-item yes/no list for **each page** generated. A "no" on any item means the page is not done. Do not move on.
|
||||
|
||||
1. Does the hero use a brand gradient (not flat color) with eyebrow + headline + subtitle + 2 CTAs + ambient SVG mesh backdrop?
|
||||
2. Does every nav/sidebar item carry a real named icon from the library's icon set (not emoji, not glyph)?
|
||||
3. Does every KPI tile / section-title row / empty state / primary CTA carry a real icon?
|
||||
4. Is there at least one motion: route change, card hover, OR dialog/popover open, wired through the library's motion tool, with `prefers-reduced-motion` respected?
|
||||
5. For data-bearing pages: are all four states (loading / error / empty / data) reachable via a dev toggle, and does the empty state include a 64–96px icon or illustration (not just text)?
|
||||
6. Is dark mode wired through a header toggle, persisted in `localStorage`, with a `prefers-color-scheme` initial read?
|
||||
7. Is a real webfont loaded via `<link>` / `@import` and applied through the library's font token?
|
||||
8. Do density + corner radius tokens reflect Section 6's `Style Direction:` (playful → larger radii; professional → tighter)?
|
||||
9. Do all primary CTAs name the action (`Save changes`, `Create project`) — no generic `Submit` / `OK` / `Continue`?
|
||||
10. Is the brand ramp derived from Section 6's `primary` (16-step / theme-provider-driven), with both themes sharing it?
|
||||
11. Does the page **match or exceed** the approved static preview — same regions and content, now with real library elevation, motion, real webfont, and real imagery — so a reviewer comparing them would say "yes, this is the preview, brought to life" (never "this looks worse than the mock")?
|
||||
12. Does every region **resolve to a real library primitive** (no zero-effort `<div className="card">Card 1</div>` wireframe stubs)? Bespoke, domain-styled components (polaroid frames, ticket stubs, gallery tiles, chat bubbles) are **encouraged** as long as they wrap or extend a real library primitive and carry real content + imagery — the ban is on empty placeholder `<div>`s that merely re-skin the wireframe, not on domain art direction.
|
||||
13. Does every media-bearing entity render a **real image** (from the mock data's image URL), not an empty tinted surface or solid-color block?
|
||||
@@ -0,0 +1,87 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# Sub-Agent Strategy for Backend Scaffolding
|
||||
|
||||
> Parallelization strategy for backend scaffold execution. Read when transitioning from **Step 1** to backend implementation.
|
||||
|
||||
---
|
||||
|
||||
## Execution Model
|
||||
|
||||
> ⚠️ **PIPELINING**: The **Frontend sub-agent** (Step 1) and the backend track both begin **immediately after Step 0** (plan validation) and run **concurrently**. Phase A (Contracts) and Phase B (Backend) derive from the plan, not the frontend, so neither track blocks the other. For API-only projects (no frontend), the Frontend sub-agent is skipped and backend scaffolding proceeds immediately after Step 0.
|
||||
>
|
||||
> **Execution timeline for SPA + API projects:**
|
||||
> ```
|
||||
> Step 0 (Plan Validated)
|
||||
> ├── Frontend Sub-Agent (F1–F4: generate + build) ──> returns ──────────────────────────┐
|
||||
> └── Phase A: Contracts (sequential) ──> Phase B: Backend Sub-Agent ────────────────────┤
|
||||
> ▼
|
||||
> Step 11: Wrap Up
|
||||
> ```
|
||||
>
|
||||
> The Frontend sub-agent generates + builds + verifies `services/web/` (F1–F4) and hands back.
|
||||
|
||||
---
|
||||
|
||||
## Frontend Sub-Agent (parallel with backend)
|
||||
|
||||
Launched right after Step 0 (concurrently with Phase A/B). Owns frontend generation and build.
|
||||
|
||||
| Sub-Agent | Responsibility | Scope |
|
||||
|-----------|---------------|-------|
|
||||
| **Frontend Agent** (general-purpose) | Generate `services/web/`: the `ApiClient` seam (`src/api/` — interface + mock impl + one-line `index.ts` swap point) backed by a mock data layer with real images (F1–F2), pages + shared components that import only the seam `api` object (F3), login UI plus locally seeded identity when `API Login` is `Yes`, and all four data states. Login-enabled frontends MUST include a visible **Create account** button on the login page and a dedicated create-account page wired through `api.createAccount(...)`. When `API Login` is `No`, generate no auth UI or state. **The `ApiClient` interface must cover every entity the UI renders, including reference data the plan has no route for (assignee lookups, the signed-in user) — nothing outside `src/api/` may import `src/mocks/`.** Apply the Rule 13 quality bar + Polish floor. Run the frontend build gate (`npm --prefix services/web run build`, zero errors, no `any`) (F4). | Step 1 sub-steps **F1–F4** |
|
||||
|
||||
**Brief handed to the sub-agent** (full context it receives):
|
||||
- The approved plan, especially **Section 6 (Design System & UI)**: `Component Library:`, `Style Direction:`, `Typography:`, Color Palette, Pages table.
|
||||
- The approved HTML preview under `.azure/.preview-temp/` (manifest + per-page `<slug>.html` + `theme.css`) as the presentation-quality visual spec.
|
||||
- The three frontend reference docs: `frontend-quality-bar.md`, `frontend-patterns.md`, `frontend-preview-steps.md`.
|
||||
|
||||
**Hand-back contract** (what the sub-agent returns):
|
||||
- `services/web/` generated and **building cleanly** (it ran the F1–F4 checkpoints: build passes, no `any`, API Login behavior matches the plan, login-enabled apps have the required create-account button and page, four states present, Rule 13 satisfied).
|
||||
- A short report listing the pages generated and any caveats.
|
||||
- It MUST NOT call `ask_user` for UX approval — the design was already approved during planning.
|
||||
|
||||
---
|
||||
|
||||
## Phase A: Contracts First (BLOCKING — Sequential, No Parallelism)
|
||||
|
||||
Create sequentially — dependencies for everything else:
|
||||
1. Shared types (`services/shared/types/`)
|
||||
2. Validation schemas (`services/shared/schemas/`)
|
||||
3. Service interfaces (`services/functions/src/services/interfaces/`)
|
||||
4. Error types (`services/functions/src/errors/`)
|
||||
5. Config module (`services/functions/src/services/config.ts`)
|
||||
|
||||
Build shared package to produce `dist/`. Verify cross-workspace imports resolve.
|
||||
|
||||
---
|
||||
|
||||
## Phase B: Parallel Implementation via Sub-Agents
|
||||
|
||||
Once contracts exist on disk, launch backend sub-agent:
|
||||
|
||||
| Sub-Agent | Responsibility | Scope |
|
||||
|-----------|---------------|-------|
|
||||
| **Backend API Agent** (general-purpose) | Concrete service implementations, service registry, function handlers, OpenAPI spec, structured logging | Steps 3–10 implementation files |
|
||||
|
||||
> **NOTE**: Testing is NOT part of the scaffold phase. Test infrastructure, mocks, fixtures, and unit tests are out of scope — the scaffold produces correct, buildable production code only. Keeping tests out of scaffold ensures the production code stays focused and is not buried under test scaffolding.
|
||||
|
||||
---
|
||||
|
||||
## Coordination Rules
|
||||
|
||||
- The **Frontend Agent** and the backend track (Phase A → Phase B) launch together after Step 0 and run concurrently.
|
||||
- The **Backend API Agent** receives the full project plan and the contracts created in Phase A as context.
|
||||
- After the Backend agent completes, run the final build gate (`npm run build` in all workspaces).
|
||||
- The scaffold does **not** start a dev server or open Simple Browser — the frontend is generated and built only; running it locally is out of scope for scaffolding.
|
||||
- **Completion gate**: Step 11 (Wrap Up) writes the hand-off artifact only after BOTH: (a) frontend generated and building cleanly — the Frontend sub-agent returned — AND (b) Phase B backend agent completed. If one track finishes first, wait for the other.
|
||||
- Then proceed to Step 11 (Wrap Up)
|
||||
|
||||
---
|
||||
|
||||
## Key Contract Rules
|
||||
|
||||
- Agent MUST use same `AppConfig` shape (flat structure — see [../../shared-references/service-abstraction.md](.github/agents/shared-references/service-abstraction.md))
|
||||
- Agent MUST use same collection names (`'user'`, `'couple'`, etc.) mapping to SQL table names
|
||||
- Agent MUST use same validation schema names exported from `services/shared/schemas/validation.ts`
|
||||
@@ -0,0 +1,576 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# Testing Patterns
|
||||
|
||||
> Core reference for self-testable north star. Every module ships with tests. Every phase has test gate.
|
||||
|
||||
---
|
||||
|
||||
## Core Principle
|
||||
|
||||
**Project is not done until tests say it is.**
|
||||
|
||||
Agent runs tests after every module. If tests fail, agent iterates until they pass. No module complete until tests green. Not a suggestion — it's workflow.
|
||||
|
||||
---
|
||||
|
||||
## Test Pyramid
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ E2E / │ ← Full request-response cycle
|
||||
│ Integration │ with real/mock services
|
||||
├──────────────┤
|
||||
│ │
|
||||
│ Unit │ ← Fast, isolated, mocked deps
|
||||
│ │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
| Layer | What It Tests | Dependencies | Speed | When |
|
||||
|-------|---------------|-------------|-------|------|
|
||||
| **Unit** | Single function/class in isolation | All deps mocked | Fast (ms) | Every module, always |
|
||||
| **Integration** | Request → handler → service → response cycle | Mock services injected | Fast (ms) | Every route, always |
|
||||
| **E2E** | Full stack with real emulators | Running emulators (via azure-debug-plan) | Slower (s) | When emulators are available |
|
||||
|
||||
### What Each Layer Covers
|
||||
|
||||
**Unit Tests** (mandatory for every module):
|
||||
- Service abstraction methods (with mock storage/DB/cache)
|
||||
- Config loading (env vars present, missing, defaults)
|
||||
- Validation schemas (valid input, invalid input, edge cases)
|
||||
- Error types and error handler (mapping to HTTP status codes)
|
||||
- Individual function handler logic (with injected mock services)
|
||||
- Utility functions and helpers
|
||||
|
||||
**Integration Tests** (mandatory for every route):
|
||||
- HTTP request → function handler → mock service → HTTP response
|
||||
- Correct status codes (200, 201, 400, 404, 422, 500)
|
||||
- Correct response body shapes
|
||||
- Request validation (bad input rejected)
|
||||
- Error handling (service failures produce correct error responses)
|
||||
|
||||
**E2E Tests** (when emulators available via azure-debug-plan):
|
||||
- Full DB round-trip (create → read → verify data)
|
||||
- File upload → storage → retrieval
|
||||
- Cache set → get → verify
|
||||
- Health check with live services
|
||||
|
||||
---
|
||||
|
||||
## Test Runner Quick Reference
|
||||
|
||||
### Node.js (TypeScript)
|
||||
|
||||
| Runner | Setup | Config File | Test Command | Mock Library | Assertion Library |
|
||||
|--------|-------|-------------|-------------|-------------|------------------|
|
||||
| **vitest** | `npm i -D vitest` | `vitest.config.ts` | `npx vitest run` | Built-in `vi.mock()` | Built-in `expect` |
|
||||
| **jest** | `npm i -D jest ts-jest @types/jest` | `jest.config.ts` | `npx jest` | Built-in `jest.mock()` | Built-in `expect` |
|
||||
| **mocha+chai+sinon** | `npm i -D mocha chai sinon @types/mocha @types/chai @types/sinon tsx` | `.mocharc.yml` | `npx mocha` | sinon | chai `expect` |
|
||||
|
||||
#### vitest config example
|
||||
```typescript
|
||||
// vitest.config.ts
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['tests/**/*.test.ts'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
include: ['src/**/*.ts'],
|
||||
exclude: ['src/**/interfaces/**']
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### jest config example
|
||||
```typescript
|
||||
// jest.config.ts
|
||||
export default {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
roots: ['<rootDir>/tests'],
|
||||
testMatch: ['**/*.test.ts'],
|
||||
collectCoverageFrom: ['src/**/*.ts', '!src/**/interfaces/**']
|
||||
};
|
||||
```
|
||||
|
||||
#### mocha config example
|
||||
```yaml
|
||||
# .mocharc.yml
|
||||
require:
|
||||
- tsx
|
||||
spec: 'tests/**/*.test.ts'
|
||||
recursive: true
|
||||
timeout: 5000
|
||||
```
|
||||
|
||||
### .NET
|
||||
|
||||
| Runner | Setup | Config | Test Command | Mock Library | Assertion |
|
||||
|--------|-------|--------|-------------|-------------|-----------|
|
||||
| **xUnit** | NuGet: `xunit`, `xunit.runner.visualstudio`, `Microsoft.NET.Test.Sdk` | `.csproj` | `dotnet test` | Moq or NSubstitute | xUnit `Assert` or FluentAssertions |
|
||||
| **NUnit** | NuGet: `NUnit`, `NUnit3TestAdapter`, `Microsoft.NET.Test.Sdk` | `.csproj` | `dotnet test` | Moq or NSubstitute | NUnit `Assert` or FluentAssertions |
|
||||
|
||||
---
|
||||
|
||||
## Mock Data Patterns
|
||||
|
||||
### Fixture Files (JSON)
|
||||
|
||||
Store realistic mock data in `tests/fixtures/`:
|
||||
```json
|
||||
// tests/fixtures/items.json
|
||||
{
|
||||
"validItems": [
|
||||
{
|
||||
"id": "item-001",
|
||||
"name": "Widget Alpha",
|
||||
"description": "A high-quality widget for testing",
|
||||
"price": 29.99,
|
||||
"category": "widgets",
|
||||
"createdAt": "2026-01-15T10:30:00.000Z",
|
||||
"updatedAt": "2026-01-15T10:30:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "item-002",
|
||||
"name": "Gadget Beta",
|
||||
"description": "An innovative gadget",
|
||||
"price": 49.99,
|
||||
"category": "gadgets",
|
||||
"createdAt": "2026-02-20T14:00:00.000Z",
|
||||
"updatedAt": "2026-03-01T09:15:00.000Z"
|
||||
}
|
||||
],
|
||||
"invalidItems": [
|
||||
{ "name": "", "description": "Missing name" },
|
||||
{ "name": "X", "price": -5, "description": "Invalid price" },
|
||||
{ "description": "No name field at all" }
|
||||
],
|
||||
"createItemRequest": {
|
||||
"name": "New Widget",
|
||||
"description": "A brand new widget",
|
||||
"price": 19.99,
|
||||
"category": "widgets"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Factory Functions
|
||||
|
||||
For dynamic mock data, use factory functions:
|
||||
```typescript
|
||||
// tests/fixtures/itemFactory.ts
|
||||
import { Item, CreateItemRequest } from '../../services/shared/types/entities';
|
||||
|
||||
let counter = 0;
|
||||
|
||||
export function createMockItem(overrides?: Partial<Item>): Item {
|
||||
counter++;
|
||||
return {
|
||||
id: `item-${counter.toString().padStart(3, '0')}`,
|
||||
name: `Test Item ${counter}`,
|
||||
description: `Description for test item ${counter}`,
|
||||
price: 9.99 + counter,
|
||||
category: 'test',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
export function createMockCreateRequest(overrides?: Partial<CreateItemRequest>): CreateItemRequest {
|
||||
counter++;
|
||||
return {
|
||||
name: `New Item ${counter}`,
|
||||
description: `New item description ${counter}`,
|
||||
price: 19.99,
|
||||
category: 'test',
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### C# Fixtures
|
||||
|
||||
```csharp
|
||||
// Fixtures/ItemFixtures.cs
|
||||
public static class ItemFixtures
|
||||
{
|
||||
public static Item CreateValidItem(string? id = null) => new()
|
||||
{
|
||||
Id = id ?? Guid.NewGuid().ToString(),
|
||||
Name = "Test Widget",
|
||||
Description = "A test widget",
|
||||
Price = 29.99m,
|
||||
Category = "widgets",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
public static CreateItemRequest CreateValidRequest() => new()
|
||||
{
|
||||
Name = "New Widget",
|
||||
Description = "A brand new widget",
|
||||
Price = 19.99m,
|
||||
Category = "widgets"
|
||||
};
|
||||
|
||||
public static List<Item> CreateItemList(int count = 5) =>
|
||||
Enumerable.Range(1, count)
|
||||
.Select(i => CreateValidItem($"item-{i:D3}"))
|
||||
.ToList();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Service Mocking Patterns
|
||||
|
||||
### TypeScript — vitest
|
||||
|
||||
```typescript
|
||||
// tests/mocks/mockDatabase.ts
|
||||
import { IDatabaseService } from '../../src/services/interfaces/IDatabaseService';
|
||||
import { Item } from '../../services/shared/types/entities';
|
||||
|
||||
export function createMockDatabase(initialData: Item[] = []): IDatabaseService {
|
||||
const store = new Map<string, Item>();
|
||||
initialData.forEach(item => store.set(item.id, item));
|
||||
|
||||
return {
|
||||
findAll: async () => Array.from(store.values()),
|
||||
findById: async (id: string) => store.get(id) ?? null,
|
||||
create: async (item: Item) => { store.set(item.id, item); return item; },
|
||||
update: async (id: string, data: Partial<Item>) => {
|
||||
const existing = store.get(id);
|
||||
if (!existing) return null;
|
||||
const updated = { ...existing, ...data, updatedAt: new Date().toISOString() };
|
||||
store.set(id, updated);
|
||||
return updated;
|
||||
},
|
||||
delete: async (id: string) => { return store.delete(id); },
|
||||
healthCheck: async () => true
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// tests/functions/getItems.test.ts
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { createMockDatabase } from '../mocks/mockDatabase';
|
||||
import { createMockItem } from '../fixtures/itemFactory';
|
||||
|
||||
describe('getItems', () => {
|
||||
let mockDb: IDatabaseService;
|
||||
const testItems = [createMockItem(), createMockItem()];
|
||||
|
||||
beforeEach(() => {
|
||||
mockDb = createMockDatabase(testItems);
|
||||
});
|
||||
|
||||
it('should return all items', async () => {
|
||||
const items = await mockDb.findAll();
|
||||
expect(items).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should return empty array when no items', async () => {
|
||||
mockDb = createMockDatabase([]);
|
||||
const items = await mockDb.findAll();
|
||||
expect(items).toEqual([]);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### TypeScript — mocha + chai + sinon
|
||||
|
||||
```typescript
|
||||
// tests/functions/getItems.test.ts
|
||||
import { expect } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
import { createMockDatabase } from '../mocks/mockDatabase';
|
||||
import { createMockItem } from '../fixtures/itemFactory';
|
||||
|
||||
describe('getItems', () => {
|
||||
let mockDb: IDatabaseService;
|
||||
const testItems = [createMockItem(), createMockItem()];
|
||||
|
||||
beforeEach(() => {
|
||||
mockDb = createMockDatabase(testItems);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('should return all items', async () => {
|
||||
const items = await mockDb.findAll();
|
||||
expect(items).to.have.lengthOf(2);
|
||||
});
|
||||
|
||||
it('should return empty array when no items', async () => {
|
||||
mockDb = createMockDatabase([]);
|
||||
const items = await mockDb.findAll();
|
||||
expect(items).to.deep.equal([]);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### C# — xUnit + Moq
|
||||
|
||||
```csharp
|
||||
// Functions/GetItemsTests.cs
|
||||
public class GetItemsTests
|
||||
{
|
||||
private readonly Mock<IDatabaseService> _mockDb;
|
||||
|
||||
public GetItemsTests()
|
||||
{
|
||||
_mockDb = new Mock<IDatabaseService>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetItems_ReturnsAllItems()
|
||||
{
|
||||
var items = ItemFixtures.CreateItemList(3);
|
||||
_mockDb.Setup(db => db.FindAllAsync()).ReturnsAsync(items);
|
||||
|
||||
var result = await _mockDb.Object.FindAllAsync();
|
||||
|
||||
Assert.Equal(3, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetItems_ReturnsEmptyList()
|
||||
{
|
||||
_mockDb.Setup(db => db.FindAllAsync()).ReturnsAsync(new List<Item>());
|
||||
|
||||
var result = await _mockDb.Object.FindAllAsync();
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test Gate Enforcement
|
||||
|
||||
Agent MUST follow this workflow at every test gate:
|
||||
|
||||
### 1. Run Tests
|
||||
|
||||
```bash
|
||||
# TypeScript
|
||||
npm test
|
||||
# or: npx vitest run
|
||||
# or: npx jest
|
||||
# or: npx mocha
|
||||
|
||||
# .NET
|
||||
dotnet test
|
||||
```
|
||||
|
||||
### 2. Parse Output
|
||||
|
||||
Look for:
|
||||
- **Pass**: All tests passed, zero failures → proceed to next phase
|
||||
- **Fail**: One or more tests failed → DO NOT proceed
|
||||
### 3. If Tests Fail
|
||||
|
||||
1. Read failure output — identify which test failed and why
|
||||
2. Determine if issue is in **code** or **test**
|
||||
3. Fix issue
|
||||
4. Re-run tests
|
||||
5. Repeat until ALL tests pass
|
||||
|
||||
### 4. If Tests Pass
|
||||
|
||||
1. Update the plan's `Status:` field in `.azure/project-plan.md` if the phase advances it
|
||||
2. Proceed to next phase
|
||||
|
||||
### Decision Tree
|
||||
|
||||
```
|
||||
Run tests
|
||||
│
|
||||
├── ALL PASS ──→ Mark phase complete → Proceed to next phase
|
||||
│
|
||||
└── ANY FAIL ──→ Read failure output
|
||||
│
|
||||
├── Code bug ──→ Fix code → Re-run tests
|
||||
│
|
||||
├── Test bug ──→ Fix test → Re-run tests
|
||||
│
|
||||
└── Missing dep ──→ Install dep → Re-run tests
|
||||
```
|
||||
|
||||
> **NEVER skip a test gate.** If you cannot get tests to pass after reasonable effort, report failure to user rather than silently proceeding.
|
||||
|
||||
---
|
||||
|
||||
## Coverage Guidance
|
||||
|
||||
**Do not set hard coverage thresholds.** Instead, ensure:
|
||||
|
||||
- Every function handler has at least one happy-path test and one error-path test
|
||||
- Every service method is tested via mock implementations
|
||||
- Every validation schema has valid/invalid input tests
|
||||
- Every error type is tested for correct HTTP status mapping
|
||||
- Edge cases are covered (empty arrays, null values, boundary numbers, special characters)
|
||||
|
||||
The goal is **meaningful coverage**, not a percentage target.
|
||||
|
||||
---
|
||||
|
||||
## Test Naming Conventions
|
||||
|
||||
Use descriptive test names that document behavior:
|
||||
|
||||
```
|
||||
✅ "should return 200 with list of items when items exist"
|
||||
✅ "should return 404 when item ID does not exist"
|
||||
✅ "should return 422 when name is empty string"
|
||||
✅ "should return 500 when database connection fails"
|
||||
|
||||
❌ "test1"
|
||||
❌ "getItems test"
|
||||
❌ "works"
|
||||
```
|
||||
|
||||
### Convention by Runtime
|
||||
|
||||
| Runtime | Pattern | Example |
|
||||
|---------|---------|---------|
|
||||
| TypeScript | `it('should {behavior} when {condition}')` | `it('should return 404 when item not found')` |
|
||||
| C# | `{Method}_{Condition}_{Expected}` | `GetItemById_ItemNotFound_Returns404()` |
|
||||
|
||||
---
|
||||
|
||||
## Frontend Testing
|
||||
|
||||
> The frontend has its own build gate at Step 1 (F4). These patterns ensure the frontend is tested as rigorously as the backend.
|
||||
|
||||
### Minimum Test Coverage
|
||||
|
||||
Every frontend generated by this skill must include tests for:
|
||||
|
||||
| Category | What to Test | Why |
|
||||
|----------|-------------|-----|
|
||||
| **Auth flow** | Login success, login failure, logout, token expiry redirect | Auth is the security boundary |
|
||||
| **Protected routes** | Unauthenticated user redirected to /login | Ensures route protection works |
|
||||
| **Data display** | List renders items from mock API data | Core feature verification |
|
||||
| **Error states** | Error message shown when API returns error | Users must see failures |
|
||||
| **Form validation** | Invalid input shows feedback before submit | UX quality |
|
||||
| **Destructive actions** | Delete shows confirmation before executing | Data safety |
|
||||
|
||||
### Test Setup Pattern (React + Vitest)
|
||||
|
||||
```typescript
|
||||
// tests/setup.ts
|
||||
import '@testing-library/jest-dom';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
// Mock fetch globally for all frontend tests
|
||||
global.fetch = vi.fn();
|
||||
|
||||
// Helper to mock a successful API response
|
||||
export function mockFetchSuccess(body: unknown, status = 200) {
|
||||
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status,
|
||||
json: async () => body,
|
||||
});
|
||||
}
|
||||
|
||||
// Helper to mock an API error response
|
||||
export function mockFetchError(status: number, error: { code: string; message: string }) {
|
||||
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status,
|
||||
json: async () => ({ error: { ...error, details: null } }),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Component Test Pattern
|
||||
|
||||
```typescript
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { mockFetchSuccess, mockFetchError } from '../setup';
|
||||
|
||||
describe('ItemListPage', () => {
|
||||
it('renders items from API', async () => {
|
||||
const mockItems = [
|
||||
{ id: '1', name: 'Widget', price: 9.99 },
|
||||
{ id: '2', name: 'Gadget', price: 19.99 },
|
||||
];
|
||||
mockFetchSuccess({ items: mockItems, total: 2 });
|
||||
|
||||
render(<MemoryRouter><ItemListPage /></MemoryRouter>);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Widget')).toBeInTheDocument();
|
||||
expect(screen.getByText('Gadget')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows error when API fails', async () => {
|
||||
mockFetchError(500, { code: 'INTERNAL_ERROR', message: 'Server error' });
|
||||
|
||||
render(<MemoryRouter><ItemListPage /></MemoryRouter>);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/something went wrong/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows empty state when no items', async () => {
|
||||
mockFetchSuccess({ items: [], total: 0 });
|
||||
|
||||
render(<MemoryRouter><ItemListPage /></MemoryRouter>);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/no items/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Auth Flow Test Pattern
|
||||
|
||||
```typescript
|
||||
describe('useAuth', () => {
|
||||
it('sets token and user on successful login', async () => {
|
||||
const mockUser = { id: '1', email: 'test@example.com', displayName: 'Test' };
|
||||
mockFetchSuccess({ user: mockUser, token: 'jwt-token-123' });
|
||||
|
||||
// Render hook and trigger login
|
||||
// Assert token stored and user state updated
|
||||
});
|
||||
|
||||
it('clears state on logout', () => {
|
||||
// Set initial auth state
|
||||
// Call logout
|
||||
// Assert token removed and user null
|
||||
});
|
||||
|
||||
it('redirects to login when token expires', async () => {
|
||||
mockFetchError(401, { code: 'UNAUTHORIZED', message: 'Token expired' });
|
||||
|
||||
// Trigger authenticated request
|
||||
// Assert redirect to /login
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Reference
|
||||
|
||||
See [frontend-patterns.md](.github/agents/shared-references/frontend-patterns.md) for complete frontend architecture guidance.
|
||||
Reference in New Issue
Block a user