@polymorphism-tech/morph-spec 9.7.0 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/CLAUDE.md +11 -11
  2. package/README.md +4 -4
  3. package/bin/morph-spec.js +41 -44
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +24 -30
  8. package/framework/MORPH.md +47 -123
  9. package/framework/agents.json +6 -374
  10. package/framework/commands/morph-apply.md +1 -1
  11. package/framework/commands/morph-archive.md +4 -3
  12. package/framework/commands/morph-hotfix.md +8 -30
  13. package/framework/commands/morph-infra.md +2 -2
  14. package/framework/commands/morph-media.md +10 -9
  15. package/framework/commands/morph-review.md +15 -0
  16. package/framework/commands/morph-status.md +1 -1
  17. package/framework/docs/CLI.md +100 -102
  18. package/framework/evals/review-checklist.md +46 -54
  19. package/framework/hooks/README.md +4 -4
  20. package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
  21. package/framework/hooks/claude-code/core/README.md +1 -1
  22. package/framework/hooks/claude-code/core/denylist.json +4 -4
  23. package/framework/hooks/claude-code/core/gate-guard.js +9 -0
  24. package/framework/hooks/claude-code/core/masked-exit.js +49 -0
  25. package/framework/hooks/claude-code/core/state-sync.js +4 -1
  26. package/framework/hooks/claude-code/core/telemetry-log.js +1 -2
  27. package/framework/hooks/claude-code/core/test-budget.js +10 -70
  28. package/framework/hooks/claude-code/core/validate-completion.js +11 -24
  29. package/framework/hooks/claude-code/core/worktree-context.js +5 -22
  30. package/framework/hooks/claude-code/core/worktree-create.js +6 -45
  31. package/framework/hooks/claude-code/core/worktree-env-inject.js +2 -10
  32. package/framework/hooks/shared/e2e-check.js +0 -0
  33. package/framework/hooks/shared/gate-event.js +2 -2
  34. package/framework/hooks/shared/graph-refresh.js +20 -3
  35. package/framework/hooks/shared/heavy-command.js +32 -1
  36. package/framework/hooks/shared/heavy-config.js +1 -1
  37. package/framework/hooks/shared/heavy-slots.js +41 -4
  38. package/framework/hooks/shared/next-step.js +63 -24
  39. package/framework/hooks/shared/phase-utils.js +6 -9
  40. package/framework/hooks/shared/report-progress.js +86 -0
  41. package/framework/hooks/shared/state-reader.js +3 -2
  42. package/framework/hooks/statusline.js +332 -0
  43. package/framework/phases.json +32 -10
  44. package/framework/rules/ai-agents-standards.md +9 -1
  45. package/framework/rules/csharp-standards.md +4 -0
  46. package/framework/rules/ef-standards.md +20 -0
  47. package/framework/rules/frontend-standards.md +1 -1
  48. package/framework/rules/implement-report.md +25 -0
  49. package/framework/rules/infrastructure-standards.md +2 -0
  50. package/framework/rules/nextjs-standards.md +7 -0
  51. package/framework/rules/plan-grammar.md +53 -0
  52. package/framework/rules/ui-design.md +27 -0
  53. package/framework/schemas/config.schema.json +12 -19
  54. package/framework/schemas/feature.schema.json +7 -1
  55. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  56. package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +1 -1
  57. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +2 -2
  58. package/framework/skills/core/morph-apply/SKILL.md +38 -166
  59. package/framework/skills/core/morph-apply/references/debugging.md +8 -0
  60. package/framework/skills/core/morph-apply/references/hotfix.md +42 -0
  61. package/framework/skills/core/morph-apply/references/machine.md +29 -0
  62. package/framework/skills/core/{morph-media/SKILL.md → morph-apply/references/media.md} +11 -74
  63. package/framework/skills/core/morph-apply/references/review.md +65 -0
  64. package/framework/skills/core/morph-plan/SKILL.md +42 -108
  65. package/framework/skills/core/morph-plan/references/critique.md +40 -0
  66. package/framework/skills/core/morph-plan/references/interview.md +25 -0
  67. package/framework/skills/core/morph-uiux/SKILL.md +47 -64
  68. package/framework/skills/core/morph-uiux/surfaces.json +32 -0
  69. package/framework/standards/STANDARDS.json +32 -5
  70. package/framework/standards/STANDARDS.meta.json +5 -1
  71. package/framework/standards/ai-agents/agent-session.md +2 -3
  72. package/framework/standards/ai-agents/context-providers.md +2 -2
  73. package/framework/standards/ai-agents/durable-workflows-hitl.md +2 -2
  74. package/framework/standards/ai-agents/providers/model-registry.md +2 -2
  75. package/framework/standards/ai-agents/rag-custom-pgvector.md +6 -6
  76. package/framework/standards/ai-agents/setup.md +1 -1
  77. package/framework/standards/ai-agents/tool-adapters.md +2 -2
  78. package/framework/standards/architecture/stack-baseline.md +47 -0
  79. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -4
  80. package/framework/standards/frontend/design-system/ai-image-generation.md +51 -12
  81. package/framework/standards/frontend/design-system/ai-video-generation.md +50 -4
  82. package/framework/standards/frontend/design-system/art-direction.md +2 -2
  83. package/framework/standards/frontend/scroll-driven/frame-scrub.md +12 -2
  84. package/framework/standards/frontend/scroll-driven/scroll-components.md +76 -4
  85. package/framework/templates/.idea/morph-templates.xml +92 -0
  86. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  87. package/framework/templates/IDE-SNIPPETS.md +262 -262
  88. package/framework/templates/REGISTRY.json +1 -2
  89. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  90. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  91. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  92. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  93. package/framework/templates/context/ARCHITECTURE.md +1 -1
  94. package/framework/templates/docs/plan.md +12 -12
  95. package/framework/templates/docs/ui-mockups.md +17 -105
  96. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  97. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  98. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  99. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  100. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  101. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  102. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  103. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  104. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  105. package/framework/templates/state.template.json +17 -17
  106. package/framework/templates/ui/design-system.css +226 -226
  107. package/package.json +1 -1
  108. package/src/commands/e2e/e2e.js +88 -23
  109. package/src/commands/graph/diff.js +74 -0
  110. package/src/commands/graph/status.js +1 -1
  111. package/src/commands/observability/cost.js +2 -4
  112. package/src/commands/observability/telemetry.js +1 -1
  113. package/src/commands/project/doctor.js +23 -39
  114. package/src/commands/project/fleet.js +5 -6
  115. package/src/commands/project/init.js +63 -36
  116. package/src/commands/project/stack-scan.js +3 -42
  117. package/src/commands/project/stack-set.js +26 -0
  118. package/src/commands/project/status.js +12 -13
  119. package/src/commands/project/update.js +39 -3
  120. package/src/commands/project/worktree.js +27 -16
  121. package/src/commands/state/advance.js +17 -17
  122. package/src/commands/state/approve.js +0 -6
  123. package/src/commands/state/archive.js +27 -1
  124. package/src/commands/state/auto-approval.js +1 -1
  125. package/src/commands/state/create.js +0 -5
  126. package/src/commands/state/finish.js +43 -1
  127. package/src/commands/state/gate-check.js +37 -30
  128. package/src/commands/state/gate-decision.js +3 -2
  129. package/src/commands/state/next.js +1 -0
  130. package/src/commands/state/recap.js +67 -0
  131. package/src/commands/state/report-check.js +39 -146
  132. package/src/commands/validation/plan-check.js +16 -11
  133. package/src/commands/validation/validate-feature.js +3 -3
  134. package/src/commands/validation/verify.js +6 -6
  135. package/src/core/paths/output-schema.js +3 -3
  136. package/src/core/state/state-manager.js +2 -4
  137. package/src/dashboard/app/components/CommandPalette.jsx +2 -5
  138. package/src/dashboard/app/components/GatePauses.jsx +116 -0
  139. package/src/dashboard/app/components/SessionMap.jsx +77 -0
  140. package/src/dashboard/app/main.jsx +13 -13
  141. package/src/dashboard/app/pages/Activity.jsx +4 -0
  142. package/src/dashboard/app/pages/AgentMap.jsx +1 -2
  143. package/src/dashboard/app/pages/FeatureDetail.jsx +129 -186
  144. package/src/dashboard/app/pages/Features.jsx +7 -7
  145. package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
  146. package/src/dashboard/app/pages/Portfolio.jsx +40 -5
  147. package/src/dashboard/scanner/activity-scanner.js +29 -0
  148. package/src/dashboard/scanner/agents-scanner.js +11 -14
  149. package/src/dashboard/scanner/features-scanner.js +45 -24
  150. package/src/dashboard/scanner/project-api.js +11 -2
  151. package/src/dashboard/scanner/skills-scanner.js +11 -1
  152. package/src/dashboard/vite.config.js +24 -24
  153. package/src/lib/e2e/applicability.js +3 -1
  154. package/src/lib/git/tree-state.js +1 -16
  155. package/src/lib/graph/diff-tree.js +127 -0
  156. package/src/lib/graph/graphify-runtime.js +12 -5
  157. package/src/lib/heavy/gate.js +22 -1
  158. package/src/lib/installers/memory-installer.js +2 -2
  159. package/src/lib/installers/rules-installer.js +57 -0
  160. package/src/lib/installers/stack-assets.js +17 -0
  161. package/src/lib/installers/stack-sync.js +219 -0
  162. package/src/lib/plan/checks.js +55 -110
  163. package/src/lib/plan/critique.js +28 -0
  164. package/src/lib/plan/media.js +42 -0
  165. package/src/lib/plan/plan-html.js +125 -0
  166. package/src/lib/plan/plan-parser.js +34 -45
  167. package/src/lib/recap/piper.js +36 -0
  168. package/src/lib/recap/recap.js +232 -0
  169. package/src/lib/review-reports.js +33 -7
  170. package/src/lib/risk-detector.js +90 -11
  171. package/src/lib/specs/screen-checklist.js +33 -48
  172. package/src/lib/stack/dotnet-build-ignores.js +8 -9
  173. package/src/lib/tasks/test-nodes.js +24 -6
  174. package/src/lib/tasks/test-runner.js +40 -1
  175. package/src/lib/telemetry/metrics.js +27 -29
  176. package/src/lib/validators/reuse/reuse-validator.js +1 -9
  177. package/src/lib/validators/verify-runner.js +50 -19
  178. package/src/lib/worktree/fleet-scanner.js +7 -20
  179. package/src/lib/worktree/worktree-audit.js +0 -37
  180. package/src/scripts/setup-infra.js +15 -50
  181. package/src/utils/agents-installer.js +27 -201
  182. package/src/utils/hooks-installer.js +11 -95
  183. package/framework/hooks/claude-code/core/warn-bash-source-write.js +0 -23
  184. package/framework/hooks/claude-code/statusline.py +0 -699
  185. package/framework/hooks/claude-code/statusline.sh +0 -11
  186. package/framework/hooks/shared/agent-reap.js +0 -74
  187. package/framework/hooks/shared/slice-progress.js +0 -110
  188. package/framework/skills/core/morph-hotfix/SKILL.md +0 -133
  189. package/framework/skills/core/morph-machine/SKILL.md +0 -46
  190. package/framework/skills/core/morph-review/SKILL.md +0 -129
  191. package/framework/skills/core/morph-standards/SKILL.md +0 -52
  192. package/framework/standards/PROVENANCE.json +0 -119
  193. package/src/commands/project/worktree-reap.js +0 -34
  194. package/src/commands/state/handoff.js +0 -65
  195. package/src/commands/validation/mutate.js +0 -263
  196. package/src/commands/validation/scout-check.js +0 -28
  197. package/src/lib/generators/handoff-generator.js +0 -172
  198. package/src/lib/mutation/adapters.js +0 -79
  199. package/src/lib/mutation/commands.js +0 -61
  200. package/src/lib/mutation/dotnet-introspection.js +0 -281
  201. package/src/lib/mutation/manifest.js +0 -144
  202. package/src/lib/mutation/plan.js +0 -44
  203. package/src/lib/mutation/report.js +0 -68
  204. package/src/lib/mutation/runner.js +0 -301
  205. package/src/lib/mutation/schemata.js +0 -60
  206. package/src/lib/mutation/snapshot.js +0 -65
  207. package/src/lib/mutation/subset.js +0 -109
  208. package/src/lib/mutation/trx.js +0 -50
  209. package/src/lib/scout/scout-check.js +0 -293
  210. package/src/lib/validators/traceability-check.js +0 -103
  211. package/src/lib/worktree/agent-reaper.js +0 -247
@@ -1164,9 +1164,12 @@
1164
1164
  "brand-fidelity": "#brand-fidelity-reference-dont-invent",
1165
1165
  "to-video": "#downstream-images-that-become-video",
1166
1166
  "favicon-og": "#the-two-assets-everyone-forgets",
1167
- "variations": "#variations-and-cost"
1167
+ "variations": "#variations-and-cost",
1168
+ "templates": "#category-templates",
1169
+ "principles": "#prompting-principles",
1170
+ "multi-image": "#multi-image-composition"
1168
1171
  },
1169
- "digest": "Guidelines for generating project images with the morph-assets MCP (OpenRouter; default openai/gpt-image-2.5-flare, drafts on mai-image-2.6-flash, gemini-3.1-flash-image for 2K/4K or 6-14 refs; models: [...] compares): when to use AI versus stock versus SVG, prompt patterns, aspect ratios, iterative editing and assets.md format. Core principle is brand fidelity over invention: always pass the client's real assets as references (or @brand with a project) via edit_image rather than generating in a vacuum, never AI-recreate a property developer's architectural render or a physical product (animate or reference the real one), and chain derivatives from an approved hero so the whole set shares light, palette and identity. Frame anything destined to become video WITHOUT a face, because video moderation is stricter (E005), and generate scrub frames at the video's exact ratio with the subject opposite the copy. Always produce the two forgotten assets: a favicon derived from the real logo (never a new one) and a 1200x630 OG share image. Offer two to three genuinely different variations per image (variants returns a contact sheet), varying angle, light or composition since seed is not supported by the GPT Image/Gemini models, and never advance with a weak image because every derivative inherits it. Copy to render goes between double quotes; CONFIRMATION_REQUIRED needs human approval and a confirmToken."
1172
+ "digest": "Guidelines for generating project images with the morph-assets MCP (OpenRouter; default openai/gpt-image-2.5-flare, drafts on mai-image-2.6-flash, gemini-3.1-flash-image for 2K/4K or 6-14 refs; models: [...] compares): when to use AI versus stock versus SVG, prompt patterns, aspect ratios, iterative editing and assets.md format. Core principle is brand fidelity over invention: always pass the client's real assets as references (or @brand with a project) via edit_image rather than generating in a vacuum, never AI-recreate a property developer's architectural render or a physical product (animate or reference the real one), and chain derivatives from an approved hero so the whole set shares light, palette and identity. Frame anything destined to become video WITHOUT a face, because video moderation is stricter (E005), and generate scrub frames at the video's exact ratio with the subject opposite the copy. Always produce the two forgotten assets: a favicon derived from the real logo (never a new one) and a 1200x630 OG share image. Offer two to three genuinely different variations per image (variants returns a contact sheet), varying angle, light or composition since seed is not supported by the GPT Image/Gemini models, and never advance with a weak image because every derivative inherits it. Copy to render goes between double quotes; CONFIRMATION_REQUIRED needs human approval and a confirmToken. Prompts are a layered photo-direction paragraph (subject, action, environment in foreground/mid/background, multi-source lighting, style, mood reference, palette, composition with depth cues, texture), with a skeleton per category (photorealistic, illustration, text rendering, product mockup, minimalist); edits stay one or two sentences and keep the input ratio."
1170
1173
  },
1171
1174
  {
1172
1175
  "id": "frontend-design-system-ai-video-generation",
@@ -1190,9 +1193,11 @@
1190
1193
  "directional": "#section-2-building-directional-footage",
1191
1194
  "moderation": "#section-3-faces-trip-moderation",
1192
1195
  "cost-ladder": "#section-4-the-cost-ladder",
1193
- "prompt-skeleton": "#section-5-prompt-skeleton"
1196
+ "prompt-skeleton": "#section-5-prompt-skeleton",
1197
+ "async": "#section-6-async-contract",
1198
+ "loop-prompt": "#section-7-seamless-loop-prompt-ambient-plane"
1194
1199
  },
1195
- "digest": "The decisive rule: a perfect loop has an end frame identical to its start, which is right for a self-playing ambient background and WRONG for scroll scrubbing, because start and end look the same and the page reads as frozen while the user scrolls. Scroll-driven footage needs directional movement via generate_video with an endImage (push-in/dolly is the most reliable, then reveal, pull-back, slow tilt); avoid orbits and any movement that returns to its origin. Client architectural renders are animated with img2vid, never AI-recreated. Human faces repeatedly trip moderation (E005), so crop the face out of frame (neck down, hands, product) and reserve faces for static image generation. Always climb the cost ladder: seedance-1-5-pro 480p draft to judge framing and movement, then promoteFrom to seedance-2.0 1080p only after approval (same prompt, frames and seed), then re-extract frames with a bumped cache-bust. seedance-2.0-mini does NOT respect endImage and Veo 3.1 Lite/Fast are inconclusive, so scrub only on Seedance 1.5 Pro/2.0 and check the last-frame SSIM QA. Video has no ZDR on OpenRouter: nothing sensitive in the frames. Prompts read like director notes with no hype words, and must reserve a calm dark region for the page copy with the subject on the opposite side. Generation is async via the morph-assets MCP (OpenRouter): a running job is polled with get_video (several ids at once), never by resending generate_video; library reads local history and spends no credits; media_tool extracts scrub frames and posters."
1200
+ "digest": "The decisive rule: a perfect loop has an end frame identical to its start, which is right for a self-playing ambient background and WRONG for scroll scrubbing, because start and end look the same and the page reads as frozen while the user scrolls. Scroll-driven footage needs directional movement via generate_video with an endImage (push-in/dolly is the most reliable, then reveal, pull-back, slow tilt); avoid orbits and any movement that returns to its origin. Client architectural renders are animated with img2vid, never AI-recreated. Human faces repeatedly trip moderation (E005), so crop the face out of frame (neck down, hands, product) and reserve faces for static image generation. Always climb the cost ladder: seedance-1-5-pro 480p draft to judge framing and movement, then promoteFrom to seedance-2.0 1080p only after approval (same prompt, frames and seed), then re-extract frames with a bumped cache-bust. seedance-2.0-mini does NOT respect endImage and Veo 3.1 Lite/Fast are inconclusive, so scrub only on Seedance 1.5 Pro/2.0 and check the last-frame SSIM QA. Video has no ZDR on OpenRouter: nothing sensitive in the frames. Prompts read like director notes with no hype words, and must reserve a calm dark region for the page copy with the subject on the opposite side. Generation is async via the morph-assets MCP (OpenRouter): a running job is polled with get_video (several ids at once), never by resending generate_video; library reads local history and spends no credits; media_tool extracts scrub frames and posters. waitSeconds defaults to 60 with a 110 ceiling while a Seedance job takes about 100-130s, so pass 110 and still expect to poll. An ambient loop prompt has seven mandatory sections (scene, camera returning to its start, action arc with a mid peak, text choreography or 'background plate only', static lighting, loop seal, technical), with the loop direction inferred from the subject type."
1196
1201
  },
1197
1202
  {
1198
1203
  "id": "frontend-design-system-animations",
@@ -1860,7 +1865,7 @@
1860
1865
  "motion": "#section-6-motion-with-intent",
1861
1866
  "prototype": "#section-7-prototype-and-self-review"
1862
1867
  },
1863
- "digest": "Order beats talent: before any component of a new or changed screen, write 1-design/direcao.md with the concept in one sentence, the visual metaphor, what makes it memorable, and what it must NOT look like. New screens and brand moments (login, onboarding, first screen, empty state, success) explore 2-3 DISTINCT directions and recommend one with a product reason. Avoid template tells (raw shadcn look, white card centered on gray, generic icon list, default fonts, flat backgrounds, mobile as stacked desktop, decorative fade-ins, made-up numbers); three or more means raise it. Mobile is composed at 390px on its own (bottom sheet, thumb zone, 44px targets). Motion tells something with the lightest tool (CSS keyframes > scroll-driven CSS > Framer Motion > GSAP), transform/opacity only, and a composed reduced-motion version. Prove it in 1-design/prototipos/{screen}.html with the project REAL tokens, screenshotted at 1280 and 390 and revised until it passes."
1868
+ "digest": "Order beats talent: before any component of a new or changed screen, write the direction (Direção escolhida in 1-design/mockups.md) with the concept in one sentence, the visual metaphor, what makes it memorable, and what it must NOT look like. New screens and brand moments (login, onboarding, first screen, empty state, success) explore 2-3 DISTINCT directions and recommend one with a product reason. Avoid template tells (raw shadcn look, white card centered on gray, generic icon list, default fonts, flat backgrounds, mobile as stacked desktop, decorative fade-ins, made-up numbers); three or more means raise it. Mobile is composed at 390px on its own (bottom sheet, thumb zone, 44px targets). Motion tells something with the lightest tool (CSS keyframes > scroll-driven CSS > Framer Motion > GSAP), transform/opacity only, and a composed reduced-motion version. Prove it in 1-design/prototipos/{screen}.html with the project REAL tokens, screenshotted at 1280 and 390 and revised until it passes."
1864
1869
  },
1865
1870
  {
1866
1871
  "id": "architecture-engineering-lens",
@@ -1930,6 +1935,28 @@
1930
1935
  "not-a-rule": "#o-que-não-é-regra"
1931
1936
  },
1932
1937
  "digest": "Rules with a reason that a machine can see are lint errors, not prose: one eslint.config.mjs at the Next app root, flat config spreading eslint-config-next/core-web-vitals and eslint-config-next/typescript, no new plugin. ESLint 9 (eslint-plugin-import/react peer range; the bundled typescript-eslint wants TypeScript <6.1); Next 16 next build no longer lints; the verify build node runs eslint --quiet on the files the feature changed (legacy lint debt never reddens a feature), from the app root, when eslint.config.* and eslint are present. no-restricted-imports bans framer-motion (one import path, motion/react), zustand/redux/@reduxjs/toolkit (server state is TanStack Query, page and filters in the URL), DB drivers pg/@neondatabase/serverless/@prisma/client/drizzle-orm/postgres (Next never touches the database; the .NET API does), the legacy shadcn @/components/ui/form, and @/features/*/* deep imports outside src/features (use the feature index.ts). import/no-restricted-paths: src/components never imports src/features, src/features never imports src/app, src/hooks never imports the API client (lib/api-client.ts, lib/api). no-restricted-syntax: fetch or apiClient called inside useEffect, and 'use client' in the root app/layout.tsx. import/no-default-export except Next convention files (page, layout, loading, error, not-found, template, default, route, sitemap, robots, icons, OG images) and *.config.*. @typescript-eslint/no-explicit-any error, no-console warn. A new restriction is one line in this file with the reason in its message; a one-off exception is eslint-disable-next-line with the reason. CSS class naming, animation class catalogs, optional className, Zod on props, motion counts and offsets, QueryClient defaults and prop-drilling depth are taste: neither prose nor lint."
1938
+ },
1939
+ {
1940
+ "id": "architecture-stack-baseline",
1941
+ "name": "stack baseline",
1942
+ "path": "architecture/stack-baseline.md",
1943
+ "category": "architecture",
1944
+ "subcategory": null,
1945
+ "tags": [
1946
+ "architecture",
1947
+ "stack",
1948
+ "greenfield"
1949
+ ],
1950
+ "stacks": [
1951
+ "*"
1952
+ ],
1953
+ "anchors": {
1954
+ "stack": "#a-stack",
1955
+ "declare": "#declarar",
1956
+ "forbidden": "#padrões-proibidos",
1957
+ "outside": "#fora-da-stack"
1958
+ },
1959
+ "digest": "Projeto novo começa na stack do Morph INTEIRA (dotnet+nextjs --with neon), mesmo que a primeira feature caiba em menos; recorte ou outra tecnologia só por escolha explícita do usuário:.NET 10 + Vertical Slice (handler + validator + endpoint + errors + tests), EF Core + Neon (Postgres), Result pattern com result.Match(), Microsoft Agent Framework quando houver agentes, Next.js App Router + Tailwind + Motion + Shadcn, Hangfire só com razão real, Docker Compose para as dependências locais. Tags: project.stack dotnet|nextjs|dotnet+nextjs; integrations neon, maf, docker. Declare com `morph-spec stack-set <stack> --with <integrações>`, que grava o config.json e instala as rules de domínio; as rules só carregam no início de uma sessão, então a sessão que implementa começa depois. Proibido: exception como fluxo, Service/Manager entre handler e dados, IRepository/IUnitOfWork, SQL cru sem ADR, redeclarar tipo existente. Outra tecnologia é ADR de projeto no plano, sem rule de domínio."
1933
1960
  }
1934
1961
  ]
1935
1962
  }
@@ -1196,7 +1196,7 @@
1196
1196
  "refactor neutro",
1197
1197
  "migracao de teste"
1198
1198
  ],
1199
- "verifiedAgainst": "Microsoft.Extensions.AI 10.9.0 + Microsoft.Agents.AI 1.20.0 (ai-pin 2026-09-08); os dois blocos C# abaixo foram extraídos com `scripts/extract-doc-csharp.mjs` e compilados contra o pin com `TreatWarningsAsErrors`, e os três testes do segundo bloco foram executados e passam. **A separação em si e a migração de teste são verificação documental:** vêm da feature `tools-finas` do MORPH_GHLBrain e o ai-kit não as traz.",
1199
+ "verifiedAgainst": "Microsoft.Extensions.AI 10.9.0 + Microsoft.Agents.AI 1.20.0 (ai-pin 2026-09-08); os dois blocos C# abaixo foram extraídos com `scripts/extract-doc-csharp.mjs` e compilados contra o pin com `TreatWarningsAsErrors`, e os três testes do segundo bloco foram executados e passam. **A separação em si e a migração de teste são verificação documental:** vêm de um projeto .NET em produção e o ai-kit não as traz.",
1200
1200
  "provedBy": []
1201
1201
  },
1202
1202
  "frontend-design-system-art-direction": {
@@ -1253,6 +1253,10 @@
1253
1253
  ],
1254
1254
  "verifiedAgainst": "eslint 9.39.5, eslint-config-next 16.4.2 (typescript-eslint 8.71.2, eslint-plugin-import 2.32.0), next 16.4.2, typescript 6.0.3 (sonda: um arquivo ruim por regra → erro da regra esperada, `no-console` → aviso, arquivos conformes e o scaffold `morph-spec scaffold nextjs` → 0 problemas).",
1255
1255
  "provedBy": []
1256
+ },
1257
+ "architecture-stack-baseline": {
1258
+ "verifiedAgainst": ".NET 10, EF Core 10, Next.js 16 App Router, Microsoft Agent Framework 1.0.",
1259
+ "provedBy": []
1256
1260
  }
1257
1261
  },
1258
1262
  "sharedAliases": {
@@ -21,12 +21,11 @@
21
21
  | O histórico é **só** a lista de mensagens, sem campo próprio, e o projeto não precisa lê-lo fora do agente | `AgentSession` + um `ChatHistoryProvider` embutido |
22
22
  | Protótipo, CLI, demo, teste | `InMemoryChatHistoryProvider` |
23
23
 
24
- **O veredito da casa: conversa própria como default.** Isso não é preferência — é a aplicação do critério acima aos projetos que existem. GHLBrain, MORPH_OS e ProspectPRO caem todos nas três primeiras linhas.
24
+ **O veredito da casa: conversa própria como default.** Isso não é preferência — é a aplicação do critério acima aos projetos que existem. Os três agentes da casa em produção caem todos nas três primeiras linhas.
25
25
 
26
26
  ### Os três fundamentos, cada um citável
27
27
 
28
- 1. **ADR D3 do GHLBrain.** O estado da conversa vive em `conv_checkpoints`; `ToolAgentRunner.cs:126-130` roda *"sem AgentThread/sessão"* — porque o **retry depende de o histórico ser visível e manipulável**: para repetir uma volta que falhou é preciso remontar as mensagens, e não dá para remontar o que está dentro de um objeto opaco. Registrado em `docs/specs/elevacao-9/brutos/scan-ghlbrain.md:58` e `:599`.
29
- 2. **Doc oficial de self-hosting** (learn.microsoft.com/en-us/agent-framework/hosting/self-hosting/, `ms.date` 2026-08-17), verbatim: *"MAF doesn't include a general-purpose durable session store"*. Produção exige implementação própria. A mesma página recomenda, em conversa longa, um `ChatHistoryProvider` externo **com mensagens por linha** em vez de reescrever a sessão inteira a cada turno.
28
+ 1. **Decisão de arquitetura de um agente .NET em produção.** O estado da conversa vive em `conv_checkpoints`; o runner roda *"sem AgentThread/sessão"* — porque o **retry depende de o histórico ser visível e manipulável**: para repetir uma volta que falhou é preciso remontar as mensagens, e não dá para remontar o que está dentro de um objeto opaco.2. **Doc oficial de self-hosting** (learn.microsoft.com/en-us/agent-framework/hosting/self-hosting/, `ms.date` 2026-08-17), verbatim: *"MAF doesn't include a general-purpose durable session store"*. Produção exige implementação própria. A mesma página recomenda, em conversa longa, um `ChatHistoryProvider` externo **com mensagens por linha** em vez de reescrever a sessão inteira a cada turno.
30
29
  3. **Padrão do canal** (Rasmus, parte 06 — "Conversations"): objeto de conversa próprio do domínio, com **um** mapper para `ChatMessage`. A conversa é entidade de negócio; o formato do provedor é detalhe de transporte.
31
30
 
32
31
  ### Ligação com a Responses API
@@ -23,7 +23,7 @@ Preciso que o agente saiba X.
23
23
  (nem provider, nem tool)
24
24
  ```
25
25
 
26
- **O caso concreto que a regra evita:** RAG passivo injetado como context provider. O usuário digita "oi" e o agente paga uma busca vetorial + o custo dos trechos no prompt — **em todo turno**, inclusive nos que não precisam de conhecimento nenhum. Como tool, o "oi" não dispara busca alguma. Foi medido em produção: o escopo institucional da base do GHLBrain roda sem `LIMIT`, e passivo isso é custo por turno, não por necessidade.
26
+ **O caso concreto que a regra evita:** RAG passivo injetado como context provider. O usuário digita "oi" e o agente paga uma busca vetorial + o custo dos trechos no prompt — **em todo turno**, inclusive nos que não precisam de conhecimento nenhum. Como tool, o "oi" não dispara busca alguma. Foi medido em produção: o escopo institucional de uma base de conhecimento roda sem `LIMIT`, e passivo isso é custo por turno, não por necessidade.
27
27
 
28
28
  ### O compositor determinístico — a alternativa que não é provider
29
29
 
@@ -45,7 +45,7 @@ var plan = PlanFor(conversation.Stage);
45
45
  var options = new ChatOptions { Instructions = plan.Instructions, Tools = [.. plan.Tools] };
46
46
  ```
47
47
 
48
- É a prática do GHLBrain (`OperatorPlan`) e o padrão do canal: **o código decide o modo, o LLM classifica**. Um provider que "sugere" quais tools usar deixa a decisão com o modelo; a allow-list tira a decisão do modelo.
48
+ É a prática de um agente em produção (um plano do operador decidido em código) e o padrão do canal: **o código decide o modo, o LLM classifica**. Um provider que "sugere" quais tools usar deixa a decisão com o modelo; a allow-list tira a decisão do modelo.
49
49
 
50
50
  ---
51
51
 
@@ -71,7 +71,7 @@ await foreach (WorkflowEvent evt in run.WatchStreamAsync())
71
71
  | **Um agente**, uma tool de efeito irreversível (cobrar, enviar proposta, apagar) | `ApprovalRequiredAIFunction` — **sem workflow nenhum** |
72
72
  | Um grafo com etapa humana entre executores, ou espera de dias | `RequestPort` + `CheckpointManager` |
73
73
 
74
- O caso do agente único é o que a casa realmente tem (MORPH_OS: billing, envio de proposta) e é o que faltava neste standard.
74
+ O caso do agente único é o que a casa realmente tem (billing, envio de proposta) e é o que faltava neste standard.
75
75
 
76
76
  ### O ciclo, medido em `Microsoft.Extensions.AI` 10.9.0
77
77
 
@@ -271,7 +271,7 @@ This framework deploys to VPS/Coolify by default and does not adopt Azure as a d
271
271
  - Human-in-the-loop (HITL) | Microsoft Learn: https://learn.microsoft.com/en-us/agent-framework/workflows/human-in-the-loop
272
272
  - Checkpoints | Microsoft Learn: https://learn.microsoft.com/en-us/agent-framework/workflows/checkpoints
273
273
  - Durable Extension | Microsoft Learn: https://learn.microsoft.com/en-us/agent-framework/integrations/durable-extension
274
- - Release notes 1.14 (2026-07-21) e 1.17 do `microsoft/agent-framework` — graduação do `ToolApprovalAgent` e extração do Durable Task (compilados em `docs/specs/elevacao-9/brutos/research-maf.md:312-331`, lidos 2026-09-07)
274
+ - Release notes 1.14 (2026-07-21) e 1.17 do `microsoft/agent-framework` — graduação do `ToolApprovalAgent` e extração do Durable Task (lidos 2026-09-07)
275
275
 
276
276
  ---
277
277
 
@@ -114,7 +114,7 @@ Read it narrowly, because it **is** narrow: the restriction needs **four** facts
114
114
 
115
115
  ### Origin 2 — measured in the field, no primary source
116
116
 
117
- *"Reasoning models reject `temperature != 1` when tools are present."* This is **measured in production**, not documented: GHLBrain's `ToolAgentRunner.cs:35-38` strips temperature for that path, recorded in `docs/specs/elevacao-9/brutos/scan-ghlbrain.md:106`, and corroborated by third parties (issues on `openai-python` and LibreChat). **The 2026-09-08 research did not find a primary OpenAI sentence stating it.**
117
+ *"Reasoning models reject `temperature != 1` when tools are present."* This is **measured in production**, not documented: a production .NET agent strips temperature for that path, and this is corroborated by third parties (issues on `openai-python` and LibreChat). **The 2026-09-08 research did not find a primary OpenAI sentence stating it.**
118
118
 
119
119
  So it lives in the schema as a **per-alias flag with mandatory provenance** (`constraints.temperatureUnsupported` + `constraints.provenance`), never as a universal rule hardcoded in the registry. Whoever turns it on is asserting it for one specific model and writing where they got it from.
120
120
 
@@ -187,7 +187,7 @@ Going through `Microsoft.Extensions.AI` (`IChatClient`) or the MAF (`AIAgent`) k
187
187
 
188
188
  > **Restore trap, measured 2026-09-08:** `Microsoft.Extensions.AI.OpenAI` 10.9.0 declares `OpenAI (>= 2.12.0 && < 2.13.0)`. Pairing it with the pinned `OpenAI` 2.13.0 emits **NU1608** on restore. Either pin `OpenAI` to 2.12.x alongside the bridge, or accept the warning knowingly and write the decision in `plan.md` (`## Decisões`). Do not discover this in CI.
189
189
 
190
- **No floating ranges.** Every `PackageReference` above is an exact version. A floating `2.*` on a weekly-cadence SDK is how ProspectPRO bought a `MissingMethodException` on the Responses API **after paying for the call** (`docs/specs/elevacao-9/brutos/scan-prospectpro.md:55`).
190
+ **No floating ranges.** Every `PackageReference` above is an exact version. A floating `2.*` on a weekly-cadence SDK is how a production project bought a `MissingMethodException` on the Responses API **after paying for the call**.
191
191
 
192
192
  **Adding a provider branch:** the `ModelRegistry.cs` template implements `openai` + `ollama`. To add `google`, `anthropic`, or `azure-openai`:
193
193
  1. Confirm the current NuGet SDK — community SDKs evolve; check NuGet/context7 before coding.
@@ -216,10 +216,10 @@ The agent decides autonomously when to call `VectorSearchTool.SearchAsync`. It r
216
216
 
217
217
  ## Ingestão delta por lookup-key
218
218
 
219
- **O defeito que esta seção existe para matar é o apaga-e-regrava.** `KbStore.IngestAsync` do GHLBrain faz `DELETE … WHERE location_id AND source` e re-insere/re-embeda **tudo** do `source` a cada ingestão, mudou ou não (`docs/specs/elevacao-9/brutos/scan-ghlbrain.md:412`): 288 fichas re-embedadas, uma chamada de embedding por item, a cada reindex em massa do catálogo — e, com id posicional, inserir um trecho no topo de um documento torna "novos" todos os de baixo.
219
+ **O defeito que esta seção existe para matar é o apaga-e-regrava.** O `KbStore.IngestAsync` de um projeto em produção faz `DELETE … WHERE location_id AND source` e re-insere/re-embeda **tudo** do `source` a cada ingestão, mudou ou não: 288 fichas re-embedadas, uma chamada de embedding por item, a cada reindex em massa do catálogo — e, com id posicional, inserir um trecho no topo de um documento torna "novos" todos os de baixo.
220
220
 
221
- > [!warning] Errata (2026-09-15, medida no código e no Neon de produção — Morph.GHLBrain#96)
222
- > A versão anterior deste parágrafo afirmava também uma **janela** entre o apagar e o gravar em que a busca não acha nada. **No GHLBrain ela não existe:** `ConvVectorSql.ExecuteAsync` roda o `DELETE` e os `INSERT`s numa transação só, e o embedding acontece **antes** de a transação abrir — em READ COMMITTED o leitor vê o conteúdo antigo até o commit. O custo medido também é pequeno (324 trechos, ~US$ 0,0005 por re-embed completo, 1 trecho por `source`): lá, o ganho real do delta é **latência** (o reindex em massa) e **prevenção** (documento de até ~70 trechos). A janela é real em quem apaga fora da transação que grava — é o caso que a regra 4 abaixo cobre.
221
+ > [!warning] Errata (2026-09-15, medida no código e no Neon de produção)
222
+ > A versão anterior deste parágrafo afirmava também uma **janela** entre o apagar e o gravar em que a busca não acha nada. **Nesse projeto ela não existe:** `ConvVectorSql.ExecuteAsync` roda o `DELETE` e os `INSERT`s numa transação só, e o embedding acontece **antes** de a transação abrir — em READ COMMITTED o leitor vê o conteúdo antigo até o commit. O custo medido também é pequeno (324 trechos, ~US$ 0,0005 por re-embed completo, 1 trecho por `source`): lá, o ganho real do delta é **latência** (o reindex em massa) e **prevenção** (documento de até ~70 trechos). A janela é real em quem apaga fora da transação que grava — é o caso que a regra 4 abaixo cobre.
223
223
 
224
224
  ### O contrato — uma função pura, sem I/O
225
225
 
@@ -251,7 +251,7 @@ public sealed record DeltaPlan<T>(
251
251
  1. **`Unchanged` não é re-embedado.** É a economia inteira. Num republish típico a lista grande é `Unchanged` e as outras três são curtas — e é exatamente isso que a ingestão anterior escondia.
252
252
  2. **`Removed` são as chaves presentes no acervo e ausentes do `incoming`.** Sem esse balde, conteúdo apagado na origem continua sendo devolvido pela busca.
253
253
  3. **O id continua determinístico** — `DeterministicUuid.V5(namespace, "{loc}:{source}:{idx}")` já é a prática — para que `Updated` seja **upsert**, não delete+insert. Delete+insert quebra referências e troca o id de um chunk que só mudou de texto.
254
- **Ressalva medida (Morph.GHLBrain#96, ADR-2 da `delta-ingestion`):** quando o item é um *trecho* de documento, a posição como lookup-key derrota a regra 1 — inserir um trecho no topo desloca os índices e marca todo trecho abaixo como `Updated`, re-embedando tudo. Ali a chave certa é o **hash do conteúdo** (`lookupKey` e `contentHash` recebem o mesmo delegado): `Updated` fica vazio por construção, editar um trecho vira `Added` + `Removed`, e trechos idênticos são deduplicados antes do `Plan` para a chave duplicada nunca disparar. A chave posicional continua certa quando o item tem identidade própria (uma ficha por produto, um documento inteiro).
254
+ **Ressalva medida em produção:** quando o item é um *trecho* de documento, a posição como lookup-key derrota a regra 1 — inserir um trecho no topo desloca os índices e marca todo trecho abaixo como `Updated`, re-embedando tudo. Ali a chave certa é o **hash do conteúdo** (`lookupKey` e `contentHash` recebem o mesmo delegado): `Updated` fica vazio por construção, editar um trecho vira `Added` + `Removed`, e trechos idênticos são deduplicados antes do `Plan` para a chave duplicada nunca disparar. A chave posicional continua certa quando o item tem identidade própria (uma ficha por produto, um documento inteiro).
255
255
  4. **Sem janela vazia: nunca apague antes de gravar fora da transação que grava.** Apagar e gravar em transações separadas (ou sem transação) é o que faz a busca devolver vazio durante a reingestão. Aplique `Added`/`Updated` primeiro e só então `Removed`, dentro da mesma transação — com a transação a janela já não existe, e a ordem mantém o código correto se um dia ela for partida.
256
256
 
257
257
  ### Chave duplicada no `incoming` é erro de entrada
@@ -273,7 +273,7 @@ Os dois não competem; resolvem problemas diferentes. Adote o writer quando acei
273
273
 
274
274
  ### Item aberto, medido em campo
275
275
 
276
- **Threshold de similaridade e `k` por escopo ainda não têm regra.** Hoje o escopo institucional da base do GHLBrain roda **sem `LIMIT`** (`scan-ghlbrain.md:410`): a busca devolve tudo o que passa do threshold, e o custo do prompt varia com o tamanho da base. Definir `k` e o corte por escopo é trabalho pendente, não recomendação existente — registrado aqui para não ser reinventado como se fosse novo.
276
+ **Threshold de similaridade e `k` por escopo ainda não têm regra.** Hoje o escopo institucional da base desse projeto roda **sem `LIMIT`**: a busca devolve tudo o que passa do threshold, e o custo do prompt varia com o tamanho da base. Definir `k` e o corte por escopo é trabalho pendente, não recomendação existente — registrado aqui para não ser reinventado como se fosse novo.
277
277
 
278
278
  ---
279
279
 
@@ -340,4 +340,4 @@ ALTER TABLE kb_embeddings ENABLE ROW LEVEL SECURITY;
340
340
 
341
341
  ---
342
342
 
343
- *MORPH-SPEC by Polymorphism Tech — ai-agents/rag-custom-pgvector.md v2.1 (2026-09-15 — errata da janela no GHLBrain; ressalva da chave por hash)*
343
+ *MORPH-SPEC by Polymorphism Tech — ai-agents/rag-custom-pgvector.md v2.1 (2026-09-15 — errata da janela; ressalva da chave por hash)*
@@ -35,7 +35,7 @@ Está adicionando um agente AI a um projeto .NET?
35
35
 
36
36
  ## 1. Packages (NuGet)
37
37
 
38
- **Nenhuma faixa flutuante.** Um SDK de cadência semanal com `2.*` é como o ProspectPRO comprou um `MissingMethodException` na Responses API **depois de a chamada ser paga**. Versões abaixo: as do `framework/ai-pin.json` (`pinnedAt` 2026-09-08) ou, quando o pacote **não está no pin**, a versão apurada no nuget.org em **2026-09-08**, dita como tal.
38
+ **Nenhuma faixa flutuante.** Um SDK de cadência semanal com `2.*` é como um projeto em produção comprou um `MissingMethodException` na Responses API **depois de a chamada ser paga**. Versões abaixo: as do `framework/ai-pin.json` (`pinnedAt` 2026-09-08) ou, quando o pacote **não está no pin**, a versão apurada no nuget.org em **2026-09-08**, dita como tal.
39
39
 
40
40
  **Required for any MAF project:**
41
41
 
@@ -5,7 +5,7 @@
5
5
  > **Keywords:** tool, AIFunctionFactory, adaptador de tool, tool fina, Ops, Desfecho, Render, regra de negócio em tool, descrição da tool, golden de tool, refactor neutro, migração de teste
6
6
  > **Read by Claude in:** implement (ao escrever ou migrar uma tool de agente que carrega regra de negócio)
7
7
 
8
- **Verified against:** Microsoft.Extensions.AI 10.9.0 + Microsoft.Agents.AI 1.20.0 (ai-pin 2026-09-08); os dois blocos C# abaixo foram extraídos com `scripts/extract-doc-csharp.mjs` e compilados contra o pin com `TreatWarningsAsErrors`, e os três testes do segundo bloco foram executados e passam. **A separação em si e a migração de teste são verificação documental:** vêm da feature `tools-finas` do MORPH_GHLBrain e o ai-kit não as traz. Last-verified: 2026-09-08.
8
+ **Verified against:** Microsoft.Extensions.AI 10.9.0 + Microsoft.Agents.AI 1.20.0 (ai-pin 2026-09-08); os dois blocos C# abaixo foram extraídos com `scripts/extract-doc-csharp.mjs` e compilados contra o pin com `TreatWarningsAsErrors`, e os três testes do segundo bloco foram executados e passam. **A separação em si e a migração de teste são verificação documental:** vêm de um projeto .NET em produção e o ai-kit não as traz. Last-verified: 2026-09-08.
9
9
 
10
10
  ---
11
11
 
@@ -201,7 +201,7 @@ A migração é um refactor que **não pode mudar o que o modelo lê**. Por isso
201
201
 
202
202
  ## Exemplos reais
203
203
 
204
- MORPH_GHLBrain, `src/Morph.GHLBrain/Features/Conversation/Operator/`:
204
+ Um agente conversacional .NET em produção, na pasta da feature do operador:
205
205
 
206
206
  | Tool | Adaptador | `Ops` | Desfecho |
207
207
  |---|---|---|---|
@@ -0,0 +1,47 @@
1
+ # Stack do Morph — a recomendação para projeto novo
2
+
3
+ > **Scope:** universal
4
+ > **Layer:** 0 (lido pela `morph-plan` quando o projeto ainda não declara `project.stack`, e pelo `init` num diretório vazio)
5
+ > **Keywords:** stack, greenfield, projeto novo, .NET, VSA, EF Core, Neon, Next.js, Shadcn, MAF, Hangfire
6
+ > **Load When:** escolhendo a stack de um projeto novo, ou julgando um desvio de stack no plano
7
+
8
+ **Verified against:** .NET 10, EF Core 10, Next.js 16 App Router, Microsoft Agent Framework 1.0. Last-verified: 2026-10-27.
9
+
10
+ ---
11
+
12
+ ## A stack
13
+
14
+ Projeto novo começa nela **inteira** (`dotnet+nextjs --with neon`), mesmo quando a primeira feature caberia em menos: o projeto cresce, e a stack decidida no primeiro dia é a que carrega as regras certas. Recorte ou outra tecnologia é escolha explícita do usuário, nunca a recomendação. É a que os standards, as rules de domínio e o `stack-scan` conhecem.
15
+
16
+ | Camada | Stack | Tag do `project.stack` |
17
+ | --- | --- | --- |
18
+ | Backend | .NET 10 + Vertical Slice (pasta por feature: handler + validator + endpoint + errors + tests) | `dotnet` |
19
+ | ORM/DB | EF Core + Neon (Postgres) | `neon` em `project.integrations` |
20
+ | Erros | Result pattern com `result.Match()` | — |
21
+ | IA | Microsoft Agent Framework (MAF), quando houver agentes | `maf` em `project.integrations` |
22
+ | Frontend | Next.js App Router + Tailwind + Motion + Shadcn | `nextjs` |
23
+ | Jobs async | Hangfire, só com razão real | — |
24
+ | Local | Docker Compose para as dependências, portas por variável | `docker` em `project.integrations` |
25
+
26
+ Recortes, quando o usuário os escolhe: só API (`dotnet`), só site ou app web (`nextjs`).
27
+
28
+ ## Declarar
29
+
30
+ `morph-spec stack-set dotnet+nextjs --with neon` (mais `maf` quando houver agentes) grava a stack no `config.json` e
31
+ instala tudo o que ela carrega: rules de domínio em `.claude/rules/`, commands e skills filtrados pela stack. As rules só carregam no início de uma sessão do Claude Code: a sessão que implementa
32
+ começa depois do `stack-set`.
33
+
34
+ ## Padrões proibidos
35
+
36
+ Os standards detalham; o build, o lint e o `stack-scan` cobram o mecânico.
37
+
38
+ - Exception como fluxo de controle: o Result põe o erro esperado no tipo.
39
+ - Service/Manager entre o handler e os dados.
40
+ - `IRepository<T>`/`IUnitOfWork`: o `DbSet<T>` já é o repository.
41
+ - SQL cru sem ADR.
42
+ - Redeclarar tipo que já existe: antes, `morph-spec graph explain <nome>` (sem grafo, `grep -rn`); criar exige `CRIAR` com `why` em `## Reuso`.
43
+
44
+ ## Fora da stack
45
+
46
+ Outra tecnologia é decisão do usuário, registrada como ADR no plano (`**Alcance:** projeto`). O harness segue
47
+ funcionando, mas sem rule de domínio para ela: as regras viram critério escrito no plano.
@@ -8,8 +8,7 @@
8
8
  **Verified against:** Neon Auth — @neondatabase/auth + @neondatabase/auth-ui (Better Auth). Last-verified: 2026-07-13.
9
9
 
10
10
  > This standard is the reference implementation, not a troubleshooting log. Every section below
11
- > exists because a real end-to-end run (MORPH_GHLBrain, feature `catalogo-produtos-crud`, task T16,
12
- > 2026-07-13) hit the failure mode it documents. See [Known Failure Modes](#known-failure-modes-postmortem)
11
+ > exists because a real end-to-end run in a production project (2026-07-13) hit the failure mode it documents. See [Known Failure Modes](#known-failure-modes-postmortem)
13
12
  > for the map from bug → section.
14
13
 
15
14
  ---
@@ -733,8 +732,7 @@ Each Neon branch gets its own isolated auth data. This means:
733
732
 
734
733
  ## Known Failure Modes (Postmortem)
735
734
 
736
- Real bugs hit during the `catalogo-produtos-crud` end-to-end run (MORPH_GHLBrain, task T16,
737
- 2026-07-13). Each one now has a permanent fix in this standard — this table exists so a review can
735
+ Real bugs hit during an end-to-end run in a production project (2026-07-13). Each one now has a permanent fix in this standard — this table exists so a review can
738
736
  scan it in one pass instead of rediscovering these by trial and error.
739
737
 
740
738
  | # | Symptom | Root cause | Fixed in |
@@ -5,7 +5,7 @@
5
5
  > **Keywords:** image, gpt-image, gemini, openrouter, morph-assets, asset, ai-generation, placeholder, mcp, next/image
6
6
  > **Load When:** feature has UI components requiring image assets
7
7
 
8
- **Verified against:** morph-assets MCP (OpenRouter; default model `openai/gpt-image-2.5-flare`) + Next.js 15 `next/image`. Last-verified: 2026-09-27.
8
+ **Verified against:** morph-assets MCP (OpenRouter; default model `openai/gpt-image-2.5-flare`) + Next.js 15 `next/image`. Last-verified: 2026-10-27.
9
9
 
10
10
  ---
11
11
 
@@ -88,6 +88,8 @@ mcp__morph-assets__list_models({ kind: "image", needs?: "refs>=5", task?: string
88
88
 
89
89
  **Cost gate:** every call estimates cost first. Above US$2, over the project budget or with an unknown price, the tool returns `CONFIRMATION_REQUIRED` + `confirmToken`; get human approval and repeat the same call with the token. Use `dryRun: true` before large batches or 4K.
90
90
 
91
+ **Fallback:** when a model is unavailable the tool walks a curated fallback chain (same validation, at most 25 percent more expensive). `noFallback: true` turns it off when the exact model matters.
92
+
91
93
  ## Prompt Engineering Patterns
92
94
 
93
95
  ### Design-System-Aware Prompts
@@ -115,20 +117,47 @@ Describe what the image will BE in the UI, not just the subject:
115
117
 
116
118
  ### Narrative Style
117
119
 
118
- GPT Image, Gemini and MAI all respond best to coherent paragraphs. Build prompts by layering:
120
+ GPT Image, Gemini and MAI all respond best to coherent paragraphs. Build prompts by layering, most important first:
121
+
122
+ 1. **Subject** — hyper-specific description. Not "a warrior in armor" but "a tall elven warrior in ornate silver plate armor with falcon-wing pauldrons and a high collar etched with leaf patterns".
123
+ 2. **Action / pose** — what the subject is doing, expression, gesture.
124
+ 3. **Environment** — foreground, mid-ground, background, described separately. A flat description ("neon city at night") gives a flat image; layers give depth.
125
+ 4. **Lighting** — the biggest quality lever. Multiple sources with different colors and directions: "golden hour key light from the left, cool blue fill from the sky on the right, warm rim light from behind catching the hair"; "harsh overhead spotlight with neon signs reflecting off wet surfaces"; "soft window light as primary, practical warm bulbs adding depth behind". A single flat source gives a flat image.
126
+ 5. **Style** — photography terms (camera, lens, film stock: "85mm portrait lens, shallow depth of field, Kodak Portra 400 grain"), a named illustration style ("cel-shaded", "watercolor", "ink wash", "art nouveau"), or a reference artist when useful ("in the style of Studio Ghibli").
127
+ 6. **Mood reference** — 1 or 2 real-world style references to anchor the look (a product film, an editorial).
128
+ 7. **Color palette** — dominant colors and contrast relationships ("warm amber highlights against deep teal shadows").
129
+ 8. **Composition** — camera angle, framing, negative space, and depth cues: overlapping objects, foreground blur, atmospheric perspective (distant objects fading into haze).
130
+ 9. **Texture and detail** — what makes it tactile: "rain droplets on glass", "worn leather with visible stitching", "dust in a light beam", "condensation on cold metal".
131
+
132
+ Write it as a photo-direction paragraph, not as tags.
133
+
134
+ **Never:** keyword spam ("beautiful, stunning, 4k, masterpiece"), JSON prompts (the models have no structured prompt mode), negative prompts ("no cars"). Describe what is there: "an empty, completely still street" rather than "a street without cars".
135
+
136
+ ### Category Templates
137
+
138
+ Each kind of image has a skeleton that works. Fill the brackets with the layers above.
119
139
 
120
- 1. **Subject** — hyper-specific description
121
- 2. **Environment** — foreground, mid-ground, background (layered for depth)
122
- 3. **Lighting** — multiple sources with different colors/directions
123
- 4. **Style** — photography terms ("85mm portrait lens") or art style ("cel-shaded")
124
- 5. **Color palette** — dominant colors and contrast relationships
125
- 6. **Composition** — camera angle, framing, negative space
140
+ | Category | Skeleton |
141
+ |---|---|
142
+ | Photorealistic | `A [shot type] of [subject with details], [action], set in [environment with atmosphere], illuminated by [lighting], captured with [camera/lens], [mood], [orientation].` |
143
+ | Illustration / sticker | `[Art style] [subject with personality], [key details], [line style] outlines, [shading], [palette], [background].` |
144
+ | Text rendering (logo, infographic, menu) | `[Image type] for [brand or concept context], featuring the text "[exact text]" in [font style], [elements around the text], [color scheme and style], [format].` |
145
+ | Product mockup | `High-resolution studio photograph of [product with materials], on [surface], [lighting setup], [camera angle] for [purpose], sharp focus on [detail], [realism], [ratio].` |
146
+ | Minimalist / negative space | `[Single subject] positioned [placement], [vast background], generous negative space [for what], [subtle light direction], [ratio].` |
147
+ | Sequential art (comic, storyboard) | One generation per panel, same character description repeated verbatim in each. |
126
148
 
127
- **Never:** keyword spam ("beautiful, stunning, 4k, masterpiece"), JSON prompts, negative prompts ("no cars").
149
+ Example (product mockup): "High-resolution studio photograph of a minimalist black ceramic mug with a matte finish on a polished concrete surface, three-point softbox lighting with a subtle warm key light, 45-degree elevated angle for an e-commerce listing, sharp focus on the steam rising from the coffee, photorealistic, square format."
150
+
151
+ ### Prompting Principles
152
+
153
+ - **State the context and the intent.** "Logo for a high-end minimalist skincare brand" beats "make a logo": the model uses the context to make style decisions. Always say what the image is for (social post, site, print).
154
+ - **One concept per generation.** Multi-scene compositions are unreliable. Generate the pieces separately and compose with editing.
155
+ - **Character consistency.** If a character's features drift after several edits, start over with a detailed character description up front.
156
+ - **Set the aspect ratio deliberately**, from the final use (see the guide below).
128
157
 
129
158
  ## Iterative Editing Workflows
130
159
 
131
- **Cardinal rule: generation demands depth; editing demands restraint.**
160
+ **Cardinal rule: generation demands depth; editing demands restraint.** A verbose edit prompt makes the model re-imagine the whole image, and the result looks artificial and over-processed. Keep edits to one or two sentences about exactly what changes, and let the model integrate it. Editing keeps the aspect ratio of the input image; say so explicitly if you need a different one.
132
161
 
133
162
  ### Adding / Removing Elements
134
163
  One sentence. Don't describe how to integrate — the model handles that.
@@ -137,10 +166,11 @@ One sentence. Don't describe how to integrate — the model handles that.
137
166
  ```
138
167
 
139
168
  ### Background Replacement
140
- State the new background simply. Don't over-describe.
169
+ State the new background simply. Don't over-describe: the model matches the lighting on its own.
141
170
  ```
142
171
  "Replace the background with a tropical beach at sunset."
143
172
  ```
173
+ Over-described, and therefore more artificial, not less: "Replace the background with a beautiful tropical beach with golden sand, palm trees swaying, turquoise waves, warm sunset colors reflecting on the water, ensuring the subject's lighting matches the sunset glow".
144
174
 
145
175
  ### Inpainting (Targeted Changes)
146
176
  Name what changes and what stays in one line.
@@ -154,6 +184,13 @@ Name the target style.
154
184
  "Transform this into Van Gogh's Starry Night style."
155
185
  ```
156
186
 
187
+ ### Multi-Image Composition
188
+ Pass the source images in `references` (paths, URLs, library ids, `@brand` / `@brand:name` with a `project`) and refer to them **by order** in the prompt:
189
+ ```
190
+ "Put the woman from the second image in the blue dress from the first image."
191
+ ```
192
+ The reference limit depends on the model (MAI Image Flash takes at most 5; 6 to 14 need Gemini 3.1 Flash Image). Check with `list_models { needs: "refs>=N" }` instead of assuming a fixed limit.
193
+
157
194
  ### Iterative Refinement with `continue_editing`
158
195
  One change at a time:
159
196
  - "Make the lighting warmer"
@@ -179,6 +216,8 @@ Put the exact copy between double quotes in the prompt (`the headline "Summer Sa
179
216
  | Story / reel | `9:16` | Mobile wallpaper, social story, vertical banner |
180
217
  | Standard display | `4:3` | Documentation, slides, standard display |
181
218
 
219
+ **Cards follow the grid, not a fixed ratio.** `3:2` is the default for a landscape card in a row of feature cards or blog headers. Use portrait (`3:4` or `2:3`) when the card is a tall tile (a product or garment shown whole, a team member, a masonry gallery), and square (`1:1`) when the grid is uniform tiles or the same asset doubles as a social post. Generate at the ratio the card renders at, so the `object-fit: cover` crop does not cut the subject.
220
+
182
221
  ## File Naming & Directory
183
222
 
184
223
  **Convention:** `public/images/{feature}/{purpose}.png`
@@ -276,7 +315,7 @@ For a frame that will drive a scroll scrub, also generate at the exact ratio the
276
315
 
277
316
  ## The Two Assets Everyone Forgets
278
317
 
279
- - **Favicon / monogram.** Derive a simple mark (one or two letters, or the existing symbol) that reads at 32x32. Use `edit_image` from the real logo so it stays on-brand. Export PNG at 512x512 and let the browser downscale. **Never invent a new logo**, simplify the existing one.
318
+ - **Favicon / monogram.** Derive a simple mark (one or two letters, or the existing symbol) that reads at 32x32. Use `edit_image` from the real logo so it stays on-brand. Export PNG at 512x512 and let the browser downscale, on a transparent background or a solid brand color, whichever the mark reads better on. **Never invent a new logo**, simplify the existing one.
280
319
  - **OG / share image (1200x630).** The image that appears when the link is pasted into a chat or a feed. Use the strongest shot on the page, with breathing room for a possible brand overlay. Generate or crop at that exact ratio. See `frontend/seo/meta-schema-audit.md`.
281
320
 
282
321
  ## Variations and Cost
@@ -5,7 +5,7 @@
5
5
  > **Keywords:** video generation, seedance, morph-assets, openrouter, img2vid, endImage, directional footage, loop video, E005 moderation, cost ladder, promoteFrom
6
6
  > **Load When:** producing footage for a scroll-driven page or animating a client render
7
7
 
8
- **Verified against:** morph-assets MCP (generate_video, generate_loop_video, get_video, library, media_tool) over OpenRouter; models `bytedance/seedance-1-5-pro` and `bytedance/seedance-2.0`. Last-verified: 2026-09-27.
8
+ **Verified against:** morph-assets MCP (generate_video, generate_loop_video, get_video, library, media_tool) over OpenRouter; models `bytedance/seedance-1-5-pro` and `bytedance/seedance-2.0`. Last-verified: 2026-10-27.
9
9
 
10
10
  ---
11
11
 
@@ -38,7 +38,7 @@ Give a start image and an end image, and let the model interpolate a camera trav
38
38
  1. Pick or generate the **start frame**: the wide shot, the resting state.
39
39
  2. Pick or generate the **end frame** (`endImage`): the destination of the movement (a detail in close-up, a revealed facade).
40
40
  3. Call `generate_video { prompt, image: start, endImage: end }`. The prompt describes the camera movement between the two.
41
- 4. Check the **last-frame QA** in the response (SSIM of the real last frame against `endImage`): ✅ respected, ⚠️ does not match, ≈ inconclusive. A ⚠️ means the model ignored `endImage` and the take will not scrub as designed.
41
+ 4. Check the **last-frame QA** in the response (SSIM of the real last frame against `endImage`): ✅ respected, ⚠️ does not match, ≈ inconclusive. A ⚠️ means the model ignored `endImage` and the take will not scrub as designed: adjust the prompt or the frames and regenerate before showing it to anyone. On ≈, decide by eye from the 6-frame preview the response carries.
42
42
 
43
43
  **Models that respect `endImage` (verified 2026-09-27):** Seedance 1.5 Pro and Seedance 2.0. Use them for scrub.
44
44
 
@@ -107,11 +107,55 @@ TECHNICAL: img2vid with image (start) + endImage (end). No audio. No watermark.
107
107
 
108
108
  ## Section 6: Async Contract
109
109
 
110
- `generate_video` and `generate_loop_video` wait up to `waitSeconds` (default 60) and, if the job is still running, return "⏳ generating" with the job id rather than a file. Poll with `get_video { ids? }` (several ids at once; no ids = every pending job), which downloads the `.mp4` once the job completes. **Never resend `generate_video` while a job is running**: an identical request within 15 minutes returns the same job, but a different one is billed again.
110
+ `generate_video` and `generate_loop_video` wait up to `waitSeconds` and, if the job is still running, return "⏳ generating" with the job id rather than a file. **The default is 60s and the ceiling is 110s** (`DEFAULT_WAIT_S` / `MAX_WAIT_S` in the MCP's `src/tools/video.ts`). A Seedance job takes about 100 to 130s, so with the default it almost always comes back pending: pass `waitSeconds: 110` for Seedance, and even then expect to poll. Poll with `get_video { ids? }` (several ids at once; no ids = every pending job), which downloads the `.mp4` once the job completes. **Never resend `generate_video` while a job is running**: an identical request within 15 minutes returns the same job, but a different one is billed again.
111
111
 
112
112
  `library { op: "pending" | "list" | "info" }` reads local history and spends no credits (it replaces the old `list_videos`). `usage { op: "balance" }` checks the OpenRouter key without spending credits, which makes it the safe health check for the MCP.
113
113
 
114
- Post-production runs locally with no cost through `media_tool` (ffmpeg): `scrub_frames` (frames + `frames.json`, each re-extraction creates a new `v{N}` folder, which doubles as the cache-bust), `poster` (static image for mobile), `flip` (hflip when the subject collides with the copy), `concat`, `compress` (web background) and `picker` (the side-by-side approval page for the gate).
114
+ Post-production runs locally with no cost through `media_tool` (ffmpeg): `scrub_frames` (frames + `frames.json`, each re-extraction creates a new `v{N}` folder, which doubles as the cache-bust), `poster` (static image for mobile), `flip` (hflip when the subject collides with the copy), `concat`, `compress` (web background) and `picker` (the side-by-side approval page for the gate). For an ambient loop, `compress { format: "webm", width: 1280 }` with no audio is usually enough: a test loop went from 421 KB to 52 KB (upstream `morph-lp-cinematic` skill; not re-measured here).
115
+
116
+ ---
117
+
118
+ ## Section 7: Seamless Loop Prompt (Ambient Plane)
119
+
120
+ The special case from Section 1: a background video that plays itself on repeat, never the scroll-driven hero. Generate it with `generate_loop_video { prompt, image }`, which sends the same image as first and last frame. The prompt has to describe a complete cycle that returns to its start, and it has to be exhaustive about the details people leave vague: camera, lighting, timing, text and the loop point.
121
+
122
+ **Infer the loop direction from the subject** when the brief does not give one:
123
+
124
+ | Subject type | Default direction | Why |
125
+ |---|---|---|
126
+ | Mechanical, tech, electronics | Deconstruction reveal | shows engineering, internals, precision |
127
+ | Vehicles, transportation | Motion flythrough or speed orbit | movement and power |
128
+ | Fashion, wearables, accessories | Slow orbit focused on material and texture | craftsmanship and detail |
129
+ | Food, beverage | Ingredient explosion or pour sequence | freshness, composition |
130
+ | Abstract, software, SaaS | Particle morph or data visualization | makes the intangible visible |
131
+ | Architecture, real estate | Cinematic flythrough or reveal | space and scale |
132
+ | Beauty, skincare | Liquid or texture flow | sensory qualities |
133
+
134
+ With the subject known, write the prompt; only a missing subject justifies a question, and one question is enough.
135
+
136
+ **Seven sections, none skipped:**
137
+
138
+ 1. **SCENE.** The subject with material, color, form and key details ("matte black over-ear headphones with brushed aluminum hinges", never "headphones"); the environment around it (void, studio, abstract space); 1 or 2 real-world mood references (a product film, a director's style); the palette (background, accents, highlights).
139
+ 2. **CAMERA.** Motion type, speed, elevation, angle, distance, focal or depth-of-field notes. **Start position equals end position**: state the rotation in degrees ("one continuous 360-degree horizontal orbit at a fixed 15-degree elevation").
140
+ 3. **ACTION ARC.** The starting state; the transformation, granular and in order; the **peak** (maximum separation, closest zoom, most dramatic reveal) called out explicitly at roughly 40 to 60 percent of the duration; and the return, described as an intentional resolve ("components drift back and click into place"), not "it reverses". Slow build, peak, satisfying resolution.
141
+ 4. **TEXT CHOREOGRAPHY.** With headlines, for each one: the exact quoted string, the entrance relative to the action arc, position (upper third, center, lower third, aligned left or right), size (large and dominant), contrast (against the darkest or calmest region of the frame, with a darkening gradient or clear zone if the background is busy), exit, and entrance motion. Without headlines write exactly: "No text. No UI. Background plate only." **Readability wins**: if an effect hurts legibility, the effect yields.
142
+ 5. **LIGHTING & ATMOSPHERE.** Source, type (key, rim, ambient), color temperature, which edges the rim catches, atmospheric effects (particles, dust, bokeh). Lighting is **static** for the whole clip, with no shift, flicker or temperature change. Particle density and distribution are identical at the start and the end.
143
+ 6. **LOOP SEAL.** Same reference image for first and last frame; camera back at its exact start; every moving element (components, particles, effects) back in its starting state; no residual motion or lighting drift at the loop point; the return feels intentional (a click, a settle), never abrupt.
144
+ 7. **TECHNICAL.** Duration, seamless loop, image-to-video with the same first and last frame, no audio, no watermark. Resolution follows the cost ladder of Section 4 (the MCP renders 480p or 1080p, never 4K). **The duration in the prompt has to match the `duration` of the call**: the MCP defaults to 5s (`DEFAULT_DURATION` in `src/tools/video.ts`), so a prompt choreographed for 10s and sent without `duration: 10` comes out compressed into 5s, peak and text included. The accepted durations come from the OpenRouter catalog per model (`list_models` shows them; Seedance 2.0 takes 4 to 15s), and an out-of-range value is snapped to the nearest one with a warning.
145
+
146
+ **Craft rules.** Specificity over vagueness: exact angles, speeds, degrees and percentages. Describe what happens on screen, never editing software ("the frame scales inward rapidly", not "apply a keyframed scale in After Effects"). Direct, technical tone, like a director's shot notes, with no hype words ("stunning", "breathtaking", "mesmerizing"). Bullets inside each section for scanability.
147
+
148
+ A short, neutral example of the level of detail expected (a background plate with no text):
149
+
150
+ ```
151
+ SCENE: a brushed-steel wristwatch with a sapphire crystal floats in a deep charcoal void; sparse silver dust drifts behind it. Mood: macro product film, precision-engineering reveal. Palette: charcoal background, cool steel, one warm amber highlight on the crown.
152
+ CAMERA: one continuous 360-degree horizontal orbit at a fixed 10-degree elevation, slow and unhurried, 85mm macro feel, shallow depth of field. Ends at its exact starting angle and distance.
153
+ ACTION ARC: the watch at rest; the case back opens and the movement lifts out in layers; peak at 50% with every layer separated; the layers settle back and the case closes with a soft click.
154
+ TEXT CHOREOGRAPHY: No text. No UI. Background plate only.
155
+ LIGHTING & ATMOSPHERE: cool key light from the upper left, thin warm rim on the right edge, static for the whole clip; dust density identical at start and end.
156
+ LOOP SEAL: same reference image as first and last frame; camera, layers and dust back to their starting state; no residual motion at the loop point.
157
+ TECHNICAL: 10s seamless loop, image-to-video with identical first and last frame, no audio, no watermark.
158
+ ```
115
159
 
116
160
  ---
117
161
 
@@ -126,6 +170,8 @@ Post-production runs locally with no cost through `media_tool` (ffmpeg): `scrub_
126
170
  - [ ] Prompt reserves a calm region for copy, subject opposite the text
127
171
  - [ ] Draft at 480p approved before any 1080p render, and the final made with `promoteFrom`
128
172
  - [ ] Frames re-extracted with a bumped cache-bust after the final render
173
+ - [ ] Seedance calls made with `waitSeconds: 110`; a pending job polled with `get_video`, never resent
174
+ - [ ] Ambient loop prompt carries all seven sections, static lighting and an explicit loop seal, and the call passes the same `duration` the prompt was written for
129
175
 
130
176
  ## Boundary: Runtime Generation Is Another Standard
131
177
 
@@ -15,7 +15,7 @@ A design that follows the tokens, has every state and passes WCAG can still be f
15
15
 
16
16
  ## Section 1: Write the Direction First
17
17
 
18
- Before the first component, answer four questions in `1-design/direcao.md`:
18
+ Before the first component, answer four questions; the chosen direction goes in `**Direção escolhida:**` of `1-design/mockups.md`:
19
19
 
20
20
  1. **Concept in one sentence.** What the screen says, not what it contains. *"The login tells the product cycle in scenes while you sign in."*
21
21
  2. **Visual metaphor.** The image that organizes the layout. *Stories; a control room; a ticket being stamped; a garden that grows.*
@@ -80,7 +80,7 @@ The direction is proved in a rendered prototype, not in prose: `1-design/prototi
80
80
 
81
81
  ## Checklist
82
82
 
83
- - [ ] `direcao.md` answers concept, metaphor, memorable, must-not-look-like
83
+ - [ ] The direction answers concept, metaphor, memorable, must-not-look-like
84
84
  - [ ] Two or three distinct directions for a new screen or brand moment, one recommended with a reason
85
85
  - [ ] Fewer than three template tells on the final screen
86
86
  - [ ] Mobile composed at 390 px on its own terms
@@ -5,7 +5,7 @@
5
5
  > **Keywords:** frame-scrub, canvas, scroll-driven video, scrubbing, ffmpeg, apple-style, poster fallback
6
6
  > **Load When:** ui-designer active, or the brief says scroll controls a video
7
7
 
8
- **Verified against:** GSAP 3.13 ScrollTrigger + Lenis 1.x + ffmpeg 6.x, shipped to production. Last-verified: 2026-08-21.
8
+ **Verified against:** GSAP 3.13 ScrollTrigger + Lenis 1.x + ffmpeg 6.x, shipped to production. Last-verified: 2026-10-27.
9
9
 
10
10
  ---
11
11
 
@@ -51,7 +51,17 @@ const framePath = (i) => `/hero-frames/v${manifest.version}/frame_${String(i).pa
51
51
 
52
52
  Source: the upstream `morph-lp-cinematic` skill (build-stack reference); not re-measured here.
53
53
 
54
- **Treat total sequence weight as a performance budget.** It shows up directly in Lighthouse. If it is too heavy, cut frame count (120 instead of 145) or resolution before cutting quality.
54
+ **Footage from an AI video model is 24 fps, not 30.** A 4s Seedance clip comes out with about 97 frames, which already sits in the useful range: 100 to 145 frames scrubs smoothly without getting heavy. Keep the source frame rate unless the sequence is too light to scrub smoothly. In the upstream test, 97 WebP frames at 1080p weighed about 2 MB in total; `scrub_frames` warns above 25 MB, which is too heavy for a hero. Source: the upstream `morph-lp-cinematic` skill; not re-measured here.
55
+
56
+ **Treat total sequence weight as a performance budget.** It shows up directly in Lighthouse. If it is too heavy, cut frame count (120 instead of 145), width (1280) or quality before cutting the sequence length.
57
+
58
+ **Manual WebP extraction that matches the manifest layout.** When `scrub_frames` is unavailable, extract into the same `v{N}/frame_%04d` structure so the manifest-driven `framePath` above keeps resolving:
59
+
60
+ ```bash
61
+ ffmpeg -i hero.mp4 -vf "fps=24,scale=1920:-1" -c:v libwebp -quality 80 v1/frame_%04d.webp
62
+ ```
63
+
64
+ Prepend `hflip,` to the `-vf` chain to mirror, and bump the folder (`v2/`, `v3/`) on every re-extraction.
55
65
 
56
66
  **Mirror the footage when the subject collides with the copy.** If the subject sits on the side where text goes, flip it during extraction:
57
67