diff --git a/.agents/skills/product-demo-seeding/SKILL.md b/.agents/skills/product-demo-seeding/SKILL.md index fb1695a602..e740ea3757 100644 --- a/.agents/skills/product-demo-seeding/SKILL.md +++ b/.agents/skills/product-demo-seeding/SKILL.md @@ -1,34 +1,44 @@ --- name: product-demo-seeding -description: Seed coherent, disposable Kandev demo scenarios for screenshots, product films, landing-page media, and reproducible UI captures. Use whenever media needs believable tasks, workflows, agents, executors, integrations, plans, sessions, diffs, reviews, or native mobile states; invoke before product-video-capture and never use a developer's main instance or data. +description: Seed coherent, disposable Kandev demo scenarios for screenshots, product films, landing-page media, and reproducible UI captures. Use when media needs believable tasks, workflows, agents, executors, integrations, plans, sessions, diffs, reviews, or native-mobile states; invoke before product-video-capture and never use a developer's main instance or data. --- # Product Demo Seeding Build a truthful product state before recording pixels. Treat narrative, data, and UI route as one artifact. -## Prerequisites +## Current-Main Source Gate -Use a Kandev checkout with its web E2E fixtures, Playwright dependencies, pnpm toolchain, Git, and a disposable local backend available. Verify the checkout contains `scripts/dev-isolated` and `apps/web/e2e/` before creating seed state. +Product capture represents current Kandev, so old checkouts and old builds are invalid even when their scripts still run. + +1. Fetch `origin/main` immediately before capture setup. +2. Create a clean detached capture worktree from `origin/main`; do not reuse the task worktree or a landing branch. +3. Record both SHAs and prove `HEAD` equals `origin/main`. Require an empty `git status --porcelain` before building and before capture. +4. Install dependencies and build frontend/backend from that worktree. Verify it contains `scripts/dev-isolated` and `apps/web/e2e/`. +5. Stage specs, raw masters, configs, proofs, logs, and delivery candidates under a fresh artifact root outside production assets. + +Reject stale UI, selector, script, or capture choreography until current source is built and the complete route passes rehearsal. Never substitute an old server, cached bundle, or last month's working selectors for this gate. ## Workflow 1. Read `/e2e` and the relevant existing specs/page objects under `apps/web/e2e/`. 2. Write a one-sentence story: user goal, visible action, visible result. -3. Identify separate desktop and mobile routes. Mobile must use native mobile surfaces, not a desktop crop. +3. Identify separate desktop and native-mobile routes. Mobile must use native mobile surfaces, not a desktop crop. 4. Explore manually with `scripts/dev-isolated --web` when needed. For reproducible capture, use the worker-scoped E2E fixture and `ApiClient`. -5. Create a fresh bogus repository, workspace, workflow, tasks, sessions, and provider state through supported E2E/API methods. +5. Create a fresh fictional repository, workspace, workflow, tasks, sessions, and provider state through supported E2E/API methods. 6. Seed only enough state to make the story legible. Dense believable data beats empty fixtures; excessive data hides the action. 7. Open the intended UI and verify every visible label, control, and transition before recording. -8. Hand the scenario, route, selectors, semantic target bounds, complete intentional pointer journeys, and cleanup command to `/product-video-capture`. +8. Rehearse without a recorder, discard rehearsal mutations, then reseed an identical pristine baseline for each take. +9. Hand the scenario, routes, selectors, semantic target bounds, complete intentional pointer journeys, provenance, and cleanup command to `/product-video-capture`. ## Safety Contract -- Use a fresh temp `HOME`, `KANDEV_HOME_DIR`, SQLite path, repository, worktree root, and non-production ports. -- Use mock external providers and a mock agent. Never load credentials or contact real GitHub/Jira/Linear/Sentry/Slack services. +- Allocate a fresh temp `HOME`, `KANDEV_HOME_DIR`, database, repository/worktree root, unique ports, unique display, unique browser profile, and artifact root. Names and ownership must identify one take. +- Use deterministic mock GitHub and Jira providers plus a controlled agent. Use fixed IDs and timestamps, authors, titles, bodies, states, checks, and ordering. +- No credentials or network access to real provider services is permitted. Unset provider tokens and fail closed if mock routing cannot be proven in backend logs. - Never copy the developer's database for marketing capture. `scripts/dev-isolated --copy-db` is outside this workflow. -- Never query, mutate, stop, or reuse the main Kandev instance. -- Keep capture specs temporary or in a reviewed source bundle. Remove temporary copies after the run. +- Never query, mutate, stop, or reuse the developer's instance, DB, or data. +- Keep temporary capture specs or harness code under `CAPTURE_ROOT`, outside the source worktree. If the runner requires an in-worktree file, write only its exact path to a take-owned excludes file under `CAPTURE_ROOT` and pass it through command-local `core.excludesFile` configuration inherited by the capture process. Prove the clean-worktree status gate still returns an empty `git status --porcelain`, stage a source copy, then remove the file and exclusion during teardown. Never mutate the shared `.git/info/exclude`; linked worktrees and sibling agents may depend on it. - Stop if isolation cannot be proven from process args, ports, paths, and logs. Read [isolation-and-seeding.md](references/isolation-and-seeding.md) before starting an instance. @@ -40,6 +50,17 @@ Read [isolation-and-seeding.md](references/isolation-and-seeding.md) before star - Use current product capabilities. Inspect selectors and API helpers again instead of copying an old capture spec blindly. - If a responsive surface is broken, report the product bug and choose another truthful native route only when it demonstrates the same capability. - Keep local paths, test directives, generic mock responses, fixture names, and host identity out of visible frames. +- Give capture-only mock executors a logical command label such as `mock-agent`; never expose the temporary executable's absolute host path in a visible command, profile, or session label. + +## Per-Take State Contract + +Rehearsal changes product state: integration flows create tasks. A successful rehearsal therefore cannot share recording state. + +- Start from a fresh database for every recorded take. An equivalent reset is acceptable only when it deletes all scenario records and reseeds one canonical baseline with a verified seed hash. +- Use a rehearsal reset or discard its entire temp root before RECORD. Restart backend/browser ownership as needed so cached client state cannot survive. +- Before each take, assert exact workspace/workflow/task/provider counts, unique IDs, unique visible task titles, and expected ordering. Reject duplicate or accumulated tasks, fixtures, sessions, or provider-created records. +- Give desktop GitHub, mobile GitHub, desktop Jira, and mobile Jira independent state ownership. One failed or repeated take must not contaminate another. +- Never clean visible state with DOM deletion, CSS hiding, or capture-time text replacement. Fix the seed or start fresh. ## Story Selection @@ -49,10 +70,12 @@ Use [story-recipes.md](references/story-recipes.md) for Plan, Coordinate, Prepar Before capture, verify: -- Story has a clear initial state, action, and result in 7-11 seconds. +- Story has a clear initial state, action, and result. Treat 7-11 seconds as a target, never a reason to omit an honest route step, rush readability, or shorten the settled loop. - Desktop and mobile each have a native script and safe composition. - Visible data forms one fictional project narrative. - No production endpoint, credential, local path, fixture copy, or unsupported control is visible. +- Current build and all story-critical routes passed a recorder-free rehearsal; rehearsal state was reset or discarded. +- Exact baseline record counts prove no duplicate fixture accumulation before RECORD. - Seed teardown removes temporary profiles, specs, processes, ports, database, and repository. -Report seed name, product story, ports, temp roots, mock providers, real UI route, any sanitation, and teardown result. +Report seed name, story, source SHA, seed hash, fixed provider fixture version, separate desktop/native-mobile routes, process args, ports, display, browser profile, database/temp/artifact roots, mock-routing proof, any sanitation, and teardown result. This provenance must map every delivered take to its isolated baseline and current-main source. diff --git a/.agents/skills/product-demo-seeding/evals/contract.test.mjs b/.agents/skills/product-demo-seeding/evals/contract.test.mjs new file mode 100644 index 0000000000..7af48af5eb --- /dev/null +++ b/.agents/skills/product-demo-seeding/evals/contract.test.mjs @@ -0,0 +1,56 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +const evalDir = path.dirname(fileURLToPath(import.meta.url)); +const skillDir = path.resolve(evalDir, ".."); +const bundle = [ + fs.readFileSync(path.join(skillDir, "SKILL.md"), "utf8"), + ...fs + .readdirSync(path.join(skillDir, "references")) + .filter((name) => name.endsWith(".md")) + .sort() + .map((name) => fs.readFileSync(path.join(skillDir, "references", name), "utf8")), +].join("\n"); + +test("requires a freshly fetched clean origin/main capture checkout", () => { + assert.match(bundle, /fetch(?:ed)?[^\n]{0,80}origin\/main/i); + assert.match(bundle, /(?:detached|clean)[^\n]{0,80}(?:worktree|checkout)/i); + assert.match(bundle, /HEAD[^\n]{0,80}(?:equals|matches|same)[^\n]{0,80}origin\/main/i); +}); + +test("isolates deterministic provider capture resources", () => { + assert.match(bundle, /mock GitHub[^\n]{0,80}(?:and|\/)[^\n]{0,80}Jira/i); + for (const resource of ["HOME", "KANDEV_HOME_DIR", "database", "ports", "display", "browser profile"]) { + assert.match(bundle, new RegExp(resource, "i")); + } + assert.match(bundle, /fixed[^\n]{0,100}(?:IDs?|timestamps?)/i); + assert.match(bundle, /(?:no|never)[^\n]{0,100}(?:credentials?|network)/i); +}); + +test("rejects stale capture state and protects developer data", () => { + assert.match(bundle, /reject[^\n]{0,100}stale (?:UI|selector|script|capture)/i); + assert.match(bundle, /never[^\n]{0,100}developer(?:'s)? (?:instance|database|DB|data)/i); + assert.match(bundle, /outside production assets/i); +}); + +test("hands off native routes, proof, provenance, and teardown", () => { + assert.match(bundle, /separate desktop and (?:native[- ]mobile|mobile)/i); + assert.match(bundle, /provenance/i); + assert.match(bundle, /teardown/i); +}); + +test("resets visible state before every recorded take", () => { + assert.match(bundle, /fresh[^\n]{0,80}(?:database|DB)[^\n]{0,80}(?:every|each)[^\n]{0,40}(?:recorded )?take/i); + assert.match(bundle, /(?:duplicate|accumulated)[^\n]{0,100}(?:task|fixture|record)/i); + assert.match(bundle, /rehearsal[^\n]{0,100}(?:reset|discard|fresh database|fresh DB)/i); +}); + +test("keeps temporary capture harness code out of clean source status", () => { + assert.match(bundle, /temporary capture (?:specs?|harness)[^\n]{0,140}(?:CAPTURE_ROOT|outside (?:the )?(?:source )?worktree)/i); + assert.match(bundle, /(?:locally excluded|explicit exclusion|whitelist)[^\n]{0,120}(?:git status|clean[- ]worktree|status gate)/i); + assert.match(bundle, /command-local[^\n]{0,100}core\.excludesFile/i); + assert.match(bundle, /never[^\n]{0,100}(?:shared )?\.git\/info\/exclude/i); +}); diff --git a/.agents/skills/product-demo-seeding/evals/evals.json b/.agents/skills/product-demo-seeding/evals/evals.json new file mode 100644 index 0000000000..f9433b9dbb --- /dev/null +++ b/.agents/skills/product-demo-seeding/evals/evals.json @@ -0,0 +1,43 @@ +{ + "skill_name": "product-demo-seeding", + "evals": [ + { + "id": 1, + "prompt": "Seed a believable GitHub and Jira product-film scenario from current Kandev main. I need desktop and native-phone stories that begin on a task, open Integrations, inspect provider data, and create a task. Keep my running Kandev instance and data untouched.", + "expected_output": "A current-origin/main, disposable seed plan with deterministic mock GitHub/Jira data, separate desktop/mobile routes, unique runtime resources, isolation proof, coherent visible copy, and explicit teardown ownership.", + "files": [], + "expectations": [ + "Requires a clean detached worktree whose commit is proven equal to freshly fetched origin/main", + "Uses deterministic mock GitHub and Jira providers without loading credentials or contacting either service", + "Allocates fresh HOME, KANDEV_HOME_DIR, database, repository, ports, display, and browser profile", + "Defines separate truthful desktop and native-mobile choreography and verifies visible copy", + "Stages capture inputs outside production assets and reports teardown and provenance" + ] + }, + { + "id": 2, + "prompt": "I found last month's capture script and a copy of my normal Kandev database. Reuse them to make a quick Jira mobile loop, and patch the DOM if the dialog overflows.", + "expected_output": "The skill rejects stale UI, copied developer data, and capture-time product masking; it re-inspects current main, seeds disposable provider state, and blocks or chooses a truthful native route if the current surface is defective.", + "files": [], + "expectations": [ + "Rejects stale capture scripts until selectors, routes, and supported controls are checked on current main", + "Rejects the developer database and any non-disposable instance", + "Rejects capture CSS or DOM changes that hide a responsive product defect", + "Uses actual current mobile UI and records a deterministic cleanup command" + ] + }, + { + "id": 3, + "prompt": "My dark-theme rehearsal worked, but each retry left another copy of the starting task and the GitHub/Jira-created tasks on the board. Prepare the final four desktop/mobile takes without visible fixture accumulation.", + "expected_output": "The skill separates rehearsal from recording state, provisions a fresh deterministic database for each take (or proves an equivalent complete reset), verifies exact task/provider counts before recording, and tears every take down independently.", + "files": [], + "expectations": [ + "Discards rehearsal mutations rather than recording against the rehearsal database", + "Starts each recorded take from a fresh database or a proven complete reset to one canonical baseline", + "Fails the preflight when duplicate titles, IDs, or provider-created task residue are visible", + "Records per-take database, ports, display, browser profile, seed hash, and teardown evidence", + "Keeps temporary capture harness code under the external artifact root or uses an exact take-owned command-local core.excludesFile whitelist while proving clean git status and never mutating shared .git/info/exclude" + ] + } + ] +} diff --git a/.agents/skills/product-demo-seeding/references/isolation-and-seeding.md b/.agents/skills/product-demo-seeding/references/isolation-and-seeding.md index 185c19f240..035752da60 100644 --- a/.agents/skills/product-demo-seeding/references/isolation-and-seeding.md +++ b/.agents/skills/product-demo-seeding/references/isolation-and-seeding.md @@ -1,5 +1,23 @@ # Isolation And Seeding +## Prove Current Source Before Runtime + +Fetch `origin/main` and derive capture source from that remote ref, not from the current branch: + +```bash +git fetch origin main +git worktree add --detach origin/main +git -C rev-parse HEAD +git rev-parse origin/main +git -C status --porcelain +``` + +Keep the two SHAs in provenance. Continue only when HEAD matches `origin/main`, the detached worktree is clean, and dependencies/build outputs come from that worktree. Record the fetch time and build commands. A source checkout alone is insufficient if the browser still serves a stale bundle. + +Create a sibling artifact root, not a directory under landing/public production assets. Put capture source copies, raw masters, metadata, configs, proofs, delivery candidates, logs, and checksums there. Promotion is a later explicit operation. + +Keep temporary capture harness code under that external `CAPTURE_ROOT`. When the E2E runner requires a spec inside the capture worktree, put only that literal path in a take-owned excludes file and pass it to the entire runner with command-local `core.excludesFile` environment configuration. This makes nested Git status checks inherit the same whitelist without changing repository configuration. Verify the clean-worktree status gate remains empty before every take, copy the spec into the staged source bundle, and remove the spec plus exclusion at teardown. Never edit the shared `.git/info/exclude`; linked worktrees share it. A broad ignore pattern or a dirty status hidden only in provenance is not acceptable. + ## Pick The Operating Mode | Need | Mode | Why | @@ -8,7 +26,7 @@ | Produce a repeatable capture | `apps/web/e2e` worker fixture | Owns backend, frontend, database, ports, reset, and teardown | | Exercise Docker or SSH behavior itself | `containers` E2E project | Uses real container-backed executors | -Do not manually point a browser at a running personal instance. A different browser profile does not isolate the backend or database. +Do not manually point a browser at a running personal instance. A different browser profile does not isolate the backend or database. Never inspect or stop a developer's instance as part of capture teardown. ## Manual Exploration @@ -18,7 +36,7 @@ From Kandev repo root: scripts/dev-isolated --web ``` -Use the printed web URL, backend URL, temp home, database, logs, PID file, and teardown command. Confirm none match known production ports or the user's normal Kandev home. +Use the printed web URL, backend URL, temp home, database, logs, PID file, and teardown command. Confirm none match known production ports or the user's normal Kandev home. Manual exploration is not a recordable take because it lacks per-take state ownership. Do not pass `--copy-db` for product media. Fresh data is both safer and easier to make coherent. @@ -42,6 +60,22 @@ Build capture specs on `apps/web/e2e/fixtures/test-base.ts`. It provides: Choose a free `E2E_PORT_OFFSET`; never paste an offset from old notes without checking listeners. Keep each concurrent desktop/mobile run on a different offset. +Allocate and record all take-owned resources together: + +```text +take slug: +source HEAD/origin-main SHA: +HOME / KANDEV_HOME_DIR: +database / repository / worktree root: +backend / agentctl / frontend ports: +X display: +browser profile directory: +artifact root: +PID files and logs: +``` + +Use an unclaimed X display and a new browser profile directory per take. Probe ports and display sockets before launch, then prove the launched process arguments contain only the recorded temp paths and ports. + Use `data-testid`, role locators, and page objects. Snapshot refs and CSS generated by Radix/Dockview drift too easily for reproducible capture. ## Useful Seed APIs @@ -60,6 +94,31 @@ Read current signatures in `apps/web/e2e/helpers/api-client.ts` before use. Comm Use service APIs for normal product state. Use `/_test` seed routes only for state that would otherwise require slow external processes or nondeterministic timing. +## Deterministic Provider State + +Run mock GitHub and Jira providers at the backend boundary. Keep provider credentials unset and prevent live provider network calls; stop when logs cannot prove mock routing. Do not rely on a real account merely because its data is read-only. + +Version one fixture document containing fixed IDs and timestamps plus fixed actors, repositories/projects, titles, descriptions, PR/issue states, checks, comments, labels, and sort order. Use credible but fictional copy. A repeated seed must produce the same canonical record IDs and content. + +Use current provider APIs and actual integration UI. Do not create a fake detail page when the current GitHub UI only supports inspecting a list row. Jira may use its real detail panel when current main exposes one. + +## Rehearsal And Per-Take Reset + +Rehearsal must use the same current build, theme preference, route, fixture version, selectors, and dimensions as RECORD, but its database is disposable. After rehearsal, stop its processes and discard or completely reset its state. + +Prefer a fresh database before every recorded take. If fixture architecture requires reset-in-place, prove the reset removed every workspace-scoped task, session, import, and provider-created record before applying one canonical seed. Reload the browser after reset; a clean database with a stale client store is not clean capture state. + +Pre-record assertions should include: + +1. exact counts for workspace, workflow, stage, starting tasks, sessions, and provider objects; +2. unique canonical task IDs and titles; +3. absence of any task title created by rehearsal or a prior take; +4. exact provider row IDs and deterministic ordering; +5. one expected starting task card, not duplicate fixture accumulation; +6. visible UI scan after a hard reload. + +Fail the take instead of deleting duplicate cards in the DOM. Retry from a new database/temp root when a recording aborts after product mutation. + ## Disposable Repository Create a fresh local Git repository under the fixture temp root. Give it: @@ -105,7 +164,24 @@ Before recording: 2. Assert story-critical backend invariants with `apiClient`. 3. Assert every story-critical control and label through the UI. 4. Scan visible text for `E2E`, `mock`, `fixture`, `/tmp`, `/home`, localhost URLs, slash directives, and host usernames. -5. Exercise the complete route once without a recorder. -6. Record exact teardown ownership: fixture, PID file, temp root, and temporary profiles/specs. +5. Exercise the complete route once without a recorder against rehearsal state. +6. Discard/reset rehearsal state and prove the canonical record counts again. +7. Record exact provenance and teardown ownership: fixture, PID file, temp root, database, ports, display, browser profile, artifact root, and temporary profiles/specs. If any invariant depends on timing luck, redesign the seed. Marketing capture should be deterministic. + +## Provenance And Teardown + +Write a machine-readable manifest before RECORD. Include source/fetched SHAs, clean-status proof, fetch/build commands, fixture/seed hash, fixed clock/timezone, feature/profile environment, process arguments, resource map, route selectors, sanitation, and take slug. Append start/end times and output hashes after capture. + +Teardown only recorded take-owned resources. Stop processes through their PID files; verify each recorded port has no listener and each display/browser process is gone; remove temporary capture specs, browser profiles, databases, repositories, and worktrees; retain only the staged evidence and approved artifacts. Record each check and any intentionally retained path. A cleanup command without post-cleanup verification is not proof. + +Reject these shortcuts: + +- a stale checkout, stale UI bundle, or inherited browser session; +- `--copy-db`, any developer DB/data, or an existing Kandev instance; +- shared ports, display, browser profile, database, or temp root across concurrent takes; +- live provider credentials/network, nondeterministic dates, or provider ordering; +- recording against rehearsal-mutated state or reusing a failed take's DB; +- DOM/CSS patches that hide duplicate data or broken responsive behavior; +- staging raw/proof/delivery files inside production assets. diff --git a/.agents/skills/product-demo-seeding/references/story-recipes.md b/.agents/skills/product-demo-seeding/references/story-recipes.md index 41cfe2bcd5..73f2c587db 100644 --- a/.agents/skills/product-demo-seeding/references/story-recipes.md +++ b/.agents/skills/product-demo-seeding/references/story-recipes.md @@ -54,7 +54,11 @@ Desktop should use the current main Changes/review surface. Mobile should use th ## Integrations -Prefer the real Settings integration index or real task-import surface. Mock provider health/data behind it. Famous logos may carry the visual story, but they must come from actual product assets and current supported entries. +Prefer the real task Integrations sidebar route or current integration index. Mock provider health/data behind it. Famous logos may carry the visual story, but they must come from actual product assets and current supported entries. + +For GitHub/Jira task-creation films, seed one credible starting task and fixed provider rows. Desktop starts on that task, clicks Integrations in the sidebar, opens the provider, inspects the current real row/detail surface, and creates one task through the real form. Native mobile starts on the same semantic state but follows the current responsive home/menu route to Integrations; never crop the desktop sidebar into a phone. + +Use the current surface honestly: inspect a GitHub PR row when no internal PR-detail panel exists; inspect Jira's real issue panel when available. End on the one newly created task. Before each take, prove the board contains exactly the canonical starting tasks and no GitHub/Jira tasks accumulated by rehearsal or previous takes. ## Editor And Terminal diff --git a/.agents/skills/product-video-capture/SKILL.md b/.agents/skills/product-video-capture/SKILL.md index 50630ad895..728395717c 100644 --- a/.agents/skills/product-video-capture/SKILL.md +++ b/.agents/skills/product-video-capture/SKILL.md @@ -1,11 +1,11 @@ --- name: product-video-capture -description: Record, camera-process, encode, integrate, and validate polished Kandev product films, landing-page loops, screenshots, and alternate cuts from isolated demo data. Use whenever the user asks for product videos, GIF-like feature demos, smooth cursor-follow zoom, desktop/mobile captures, recaptures, new landing media, or different framing; invoke product-demo-seeding first for new visible state. +description: Record, camera-process, encode, stage, and validate polished Kandev product films, landing-page loops, screenshots, and alternate framing from isolated demo data. Use when the user asks for product videos, GIF-like feature demos, cursor-follow camera motion, desktop/mobile captures, recaptures, landing media, or different framing; always invoke product-demo-seeding first. --- # Product Video Capture -Produce reusable clean masters first; derive presentation from them later. +Produce reusable clean masters first; derive presentation from them later. Preserve the raw master and camera config so every camera choice is reversible. ## Prerequisites And Repo Discovery @@ -19,24 +19,24 @@ Produce reusable clean masters first; derive presentation from them later. | Request | Path | | --- | --- | | New feature/story | Seed with `/product-demo-seeding`, then capture desktop and mobile masters | -| Different zoom/crop/pacing | Reuse approved raw master; change camera config only | +| Different zoom/crop/pacing | Invoke `/product-demo-seeding` to re-prove source/isolation/provenance, then reuse an approved raw master and change camera config only | | New poster/static image | Extract a settled pointer-free frame from approved master or recapture native screenshot | | Longer walkthrough | Keep continuous 1x source; add a tested delivery profile instead of speed ramps | | Actual GIF required | Derive from approved video last; retain WebM/MP4 as primary web formats | ## Pipeline -1. Resolve and verify `KANDEV_REPO` and `LANDING_REPO` as described above. Do not assume task-specific absolute paths. -2. Create a unique writable `CAPTURE_ROOT`, for example with `mktemp -d "${TMPDIR:-/tmp}/kandev-product-capture.XXXXXX"`. Use it for every raw, proof, config, and staged delivery path. -3. Review the seed handoff and rehearse the full native desktop/mobile story once. -4. Record one continuous, unzoomed, high-resolution master per form factor. Use true physical pixels, not a padded Playwright video canvas. -5. Record semantic action timestamps, target bounds, and dense pointer/touch journeys beside the raw file. Include each intentional movement's start, intermediate samples, arrival, and visibility interval. +1. Always invoke `/product-demo-seeding` before this skill. For alternate framing, it validates provenance and isolation instead of manufacturing new visible state. An approved raw is reusable only when its source SHA still equals freshly fetched `origin/main`; otherwise recapture. Then resolve and verify `KANDEV_REPO` and `LANDING_REPO` as described above. Do not assume task-specific absolute paths. +2. Create a unique writable `CAPTURE_ROOT`, for example with `mktemp -d "${TMPDIR:-/tmp}/kandev-product-capture.XXXXXX"`. Use it for every raw, proof, config, and staged delivery path; do not write candidates into production assets. +3. Review the seed handoff and rehearse the full native desktop/mobile story once. Reject stale UI, fixture accumulation, duplicate tasks, selector drift, or a theme that differs from the requested product surface before starting the recorder. +4. Reset to a fresh per-take database or deterministically clean all rehearsal-created state. Record one continuous take as an unzoomed high-resolution master per form factor. Use true physical pixels, not a padded Playwright video canvas. +5. Record semantic action timestamps, target bounds, target glyph bounds, and dense pointer/touch metadata beside the raw file. Include each intentional movement's start, intermediate samples, arrival, pointer glyph bounds, and visibility interval. 6. Stop recording before capturing the clean poster. 7. Inspect raw frames before post-production. Reject UI bugs, padding, double cursors, fixture text, dead waits, and unreadable states. -8. Build a smooth post camera from semantic events. Ignore micro-jitter, but keep every intentional pointer/touch journey inside the tested safe frame. Widen before long travel and ease toward the destination with the pointer. +8. Build a smooth, center-biased post camera from semantic events. Ignore micro-jitter, but keep every intentional pointer/touch journey inside the tested safe frame. For long travel, widen, pan with the active pointer, then tighten after arrival; complete menus, provider panels, sheets, and dialogs have framing priority. 9. Encode WebM, MP4, and WebP through landing's tested camera/encoder scripts. -10. Review fixed-fraction frames and playback on desktop, native mobile, and reduced motion. -11. Copy only approved delivery assets into the owning production media directory; keep raw/proof files outside production unless requested. +10. Review fixed-fraction frames and playback on desktop, native mobile, and reduced motion. For landing films, also review at the actual theater width; full-resolution frames alone do not prove readable marketing media. +11. Keep all candidates staged outside production. Promotion is a separate, explicit action after approval; a capture request alone does not authorize overwriting landing assets. Read [capture-pipeline.md](references/capture-pipeline.md) before recording and [camera-encoding.md](references/camera-encoding.md) before conforming media. @@ -44,28 +44,41 @@ Read [capture-pipeline.md](references/capture-pipeline.md) before recording and - Fresh isolated data; no main instance, credentials, database, or production ports. - Separate desktop and native-mobile scripts. Never crop desktop footage into a mobile deliverable. -- Raw master is one continuous take at 1x: no body transform, camera, crop, concat, speed ramp, or internal cut. +- Exact profiles: desktop source `3840x2400` at 25 fps delivers `1920x1200` at 25 fps; native mobile source and delivery are `1290x2796` at 25 fps. +- Raw master is one continuous take at 1x with no internal cuts, speed ramps, or audio. It also has no body transform, camera, crop, or concat. - Disable OS cursor in X11 capture; show one intentional high-contrast DOM cursor/touch treatment. -- Use real UI input and retain semantic bounds/timestamps for camera design and audit. +- Use real UI input and retain dense semantic cursor/touch samples, target bounds, independently measured target and pointer glyph bounds, and motion-inclusive visibility intervals for camera design and audit. - Browser chrome absent; product fills the physical frame. - Any responsive/product defect remains visible or blocks capture. Do not hide it with capture CSS. ## Camera And Delivery -Use `$LANDING_REPO/scripts/product-loop-camera.mjs` and `$LANDING_REPO/scripts/product-loop-encoder.mjs`. Their tests define current dimensions, frame rate, maximum zoom, smoothness, loop reset, codecs, and poster quality. Change those contracts test-first when the user requests a genuinely different delivery format. +Use `$LANDING_REPO/scripts/product-loop-camera.mjs` and `$LANDING_REPO/scripts/product-loop-encoder.mjs`. Their tests define dimensions, frame rate, maximum zoom, smoothness, loop reset, codecs, and poster quality. For semantic landing desktop stories, use `formFactor: "landing"` with `cameraProfile: "landing-editorial"` and cap maximum zoom at 2.0x. The `landing-editorial` profile requires both `focusTrack` and `pointerTrack`; configs without either are invalid. Native mobile stays native-size and uses the tested mobile cap, never more than 2.0x. Change these contracts test-first when the user requests a genuinely different delivery format. Do not remove waits with cuts or speed changes. Improve the source choreography or recapture. One trim at the beginning/end is acceptable; time skips are not. +The camera is center-biased, not cursor-naive: follow the active semantic target while retaining the context needed to understand the action. Use one smooth establishing tighten, then keep a stable working depth. Widen-pan-tighten for long journeys. Keep the full dialog or menu visible as a priority, even when that means less zoom. The visible cursor never leaves the frame, including its complete rendered glyph and safety margin. + +Explicit rejection rules: + +- Reject lazy global zoom that holds one crop regardless of the active target. +- Reject zoom breathing: repeated in/out depth changes around one subject or state. +- Reject any zoom or camera move away from the active cursor while it is traveling. +- Reject stale UI, stale builds, accumulated rehearsal data, or captures from a non-current source checkout. +- Reject wide shots that make product text unreadable at actual size. + ## Acceptance Gate Follow [qa-checklist.md](references/qa-checklist.md). Do not ship until: - raw and delivery dimensions are proven by decoded pixels, not only container metadata; -- cadence is constant and timestamps have no gaps; -- 10/25/50/75/90% frames and full playback pass visual review; -- camera motion is smooth, reaches intended depth, and uses the profile's tested loop frame: centered 1x by default, or one identical focused start/end crop for a short docs clip when it removes irrelevant chrome or fixture-only detail; +- cadence is constant 25 fps, timestamps have no gaps, the FFmpeg log proves zero duplicated and dropped frames, and no audio stream exists; +- 10/25/50/75/90% frames, an overview contact sheet, actual-size 100% frame proofs, and full playback pass visual review; +- camera motion is smooth, reaches intended depth, and uses the profile's tested loop frame: centered 1x for a wide loop, or identical first, settled penultimate, and final crops for an approved focused landing/docs loop that improves readability while preserving identifying context; - text, menus, diffs, pointer, and touch targets remain inside frame; frame-by-frame pointer containment passes with a deliberate edge margin; -- WebM, MP4, poster, responsive source selection, lazy loading, and reduced-motion behavior pass; -- all capture processes, ports, temporary specs, and temp data are gone. +- staged WebM, MP4, WebP, responsive source selection, lazy loading, and reduced-motion behavior pass codec probes and real-browser playback; +- SHA-256 hashes cover raw masters, camera configs, semantic metadata, proofs, and every delivery candidate; +- provenance records Kandev and landing commit SHAs, commands, source/delivery profiles, isolated ports/display/browser profile, and deterministic seed identity; +- teardown proves all capture processes, ports, X displays, browser profiles, temporary specs, databases, and temp data are gone. Report story, seed, form factors, raw/delivery dimensions, durations, codecs, camera profile, output sizes/hashes, visual audit, browser checks, teardown, and any unsupported or blocked surface. diff --git a/.agents/skills/product-video-capture/evals/contract.test.mjs b/.agents/skills/product-video-capture/evals/contract.test.mjs new file mode 100644 index 0000000000..6ec032f96f --- /dev/null +++ b/.agents/skills/product-video-capture/evals/contract.test.mjs @@ -0,0 +1,209 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +const evalDir = path.dirname(fileURLToPath(import.meta.url)); +const skillDir = path.resolve(evalDir, ".."); +const skill = fs.readFileSync(path.join(skillDir, "SKILL.md"), "utf8"); +const bundle = [ + skill, + ...fs + .readdirSync(path.join(skillDir, "references")) + .filter((name) => name.endsWith(".md")) + .sort() + .map((name) => fs.readFileSync(path.join(skillDir, "references", name), "utf8")), +].join("\n"); +const routing = fs.readFileSync( + path.resolve(skillDir, "..", "using-agent-skills", "SKILL.md"), + "utf8", +); +const qaChecklist = fs.readFileSync( + path.join(skillDir, "references", "qa-checklist.md"), + "utf8", +); + +test("defines exact source and delivery profiles", () => { + assert.match(bundle, /3840x2400[^\n]{0,100}1920x1200/i); + assert.match(bundle, /1290x2796/i); + assert.match(bundle, /25 fps/i); + assert.match(bundle, /(?:at most|maximum|max(?:imum)? zoom|cap(?:ped)?)\D{0,20}2\.0x/i); +}); + +test("keeps masters continuous, honest, and reversible", () => { + assert.match(bundle, /one continuous take/i); + assert.match(bundle, /no (?:internal )?cuts?[^\n]{0,80}(?:speed|speed ramps?)[^\n]{0,80}audio/i); + assert.match(bundle, /reversible camera/i); +}); + +test("records dense semantic pointer and target geometry", () => { + assert.match(bundle, /dense[^\n]{0,100}(?:pointer|cursor|touch)[^\n]{0,100}(?:samples|waypoints|metadata)/i); + assert.match(bundle, /target (?:glyph )?bounds/i); + assert.match(bundle, /glyph bounds/i); + assert.match(bundle, /visibility interval/i); +}); + +test("keeps the DOM overlay and real browser pointer in lockstep", () => { + assert.match(bundle, /DOM overlay and (?:the )?real browser pointer/i); + assert.match(bundle, /at every sample/i); + assert.match(bundle, /never animate it independently[\s\S]{0,260}move the browser pointer/i); + assert.match(bundle, /zero endpoint displacement/i); + assert.match(bundle, /normal playback speed/i); +}); + +test("uses trusted browser input as the sole pointer authority", () => { + assert.match( + bundle, + /trusted (?:mouse|pointer)(?:move)? event[^\n]{0,120}sole (?:source of truth|authority)/i, + ); + assert.match( + bundle, + /(?:overlay|DOM cursor)[^\n]{0,100}(?:event coordinates|trusted event)[^\n]{0,100}(?:metadata|ledger)/i, + ); + assert.match( + bundle, + /re-?sync[^\n]{0,100}(?:direct|setup)[^\n]{0,100}(?:input|click)[^\n]{0,100}(?:before|prior to)[^\n]{0,40}(?:RECORD|recording)/i, + ); +}); + +test("adapts pointer sampling to recorder load and rejects visible stepping", () => { + assert.match( + bundle, + /adaptive[^\n]{0,100}(?:sample|cadence|wait)[^\n]{0,120}(?:elapsed|recorder load|recording load)/i, + ); + assert.match(bundle, /fixed sleep[^\n]{0,100}(?:reject|insufficient|not)/i); + assert.match(bundle, /p95[^\n]{0,100}(?:56|60|64) ?ms/i); + assert.match(bundle, /maximum[^\n]{0,100}64 ?ms/i); +}); + +test("keeps target and pointer glyph geometry distinct over the full movement", () => { + assert.match( + bundle, + /target glyph bounds[^\n]{0,140}(?:rendered|visual)[^\n]{0,80}(?:content|glyph)[^\n]{0,80}(?:inside|within)[^\n]{0,40}(?:target|control)/i, + ); + assert.match( + bundle, + /never[^\n]{0,120}(?:reuse|alias|copy)[^\n]{0,100}(?:pointer|cursor)[^\n]{0,80}target glyph bounds/i, + ); + assert.match( + bundle, + /visibility interval[^\n]{0,120}(?:begins|starts)[^\n]{0,60}motion start[^\n]{0,80}(?:not|rather than)[^\n]{0,40}arrival/i, + ); +}); + +test("uses center-biased widen-pan-tighten camera choreography", () => { + assert.match(bundle, /center-biased/i); + assert.match(bundle, /widen[^\n]{0,100}pan[^\n]{0,100}tighten/i); + assert.match(bundle, /full (?:dialog|menu)[^\n]{0,80}(?:priority|visible|inside|frame)/i); +}); + +test("requires semantic evidence for the landing editorial profile", () => { + assert.match(bundle, /cameraProfile[^\n]{0,40}landing-editorial/i); + assert.match(bundle, /landing-editorial[^\n]{0,120}requires?[^\n]{0,80}focusTrack/i); + assert.match(bundle, /landing-editorial[^\n]{0,120}requires?[^\n]{0,80}pointerTrack/i); +}); + +test("keeps semantic focus evidence separate from camera keyframes in landing examples", () => { + const reference = fs.readFileSync( + path.join(skillDir, "references", "camera-encoding.md"), + "utf8", + ); + const examples = reference.match(/```jsonc?[\s\S]*?```/g) ?? []; + const landingExamples = examples.filter((example) => + example.includes('"cameraProfile": "landing-editorial"'), + ); + + assert.equal(landingExamples.length, 2); + for (const example of landingExamples) { + assert.match(example, /"focusTrack"\s*:/); + assert.match(example, /"pointerTrack"\s*:/); + assert.match(example, /"keyframes"\s*:/); + } +}); + +test("names the settled penultimate loop frame in the acceptance gate", () => { + assert.match(skill, /first, settled penultimate, and final/i); + assert.doesNotMatch(skill, /first\/settled\/final/i); +}); + +test("explicitly rejects bad editorial shortcuts", () => { + assert.match(bundle, /reject[^\n]{0,100}lazy global zoom/i); + assert.match(bundle, /reject[^\n]{0,120}(?:zoom|camera)[^\n]{0,80}away from[^\n]{0,40}(?:active )?(?:cursor|pointer)/i); + assert.match(bundle, /reject[^\n]{0,100}stale UI/i); + assert.match(bundle, /reject[^\n]{0,120}wide shots?[^\n]{0,100}unreadable/i); +}); + +test("proves pointer containment and actual-size readability", () => { + assert.match(bundle, /(?:cursor|pointer)[^\n]{0,100}(?:never leaves|leaving) (?:the )?frame/i); + assert.match(bundle, /actual-size/i); + assert.match(bundle, /contact sheet/i); +}); + +test("rejects camera breathing and abrupt semantic travel", () => { + assert.match(bundle, /reject[^\n]{0,120}(?:zoom )?breathing/i); + assert.match(bundle, /one smooth[^\n]{0,80}(?:establish|tighten)/i); + assert.match(bundle, /semantic (?:camera )?(?:move|travel)[^\n]{0,100}(?:at least|minimum)[^\n]{0,30}1\.2 seconds/i); + assert.match(bundle, /readable hold[^\n]{0,100}(?:0\.9[^\n]{0,20}1\.5 seconds|900[^\n]{0,20}1,?500 ?ms)/i); + assert.match(bundle, /routine pan[^\n]{0,120}(?:median|p95)[^\n]{0,80}0\.11/i); +}); + +test("keeps the camera timing exceptions explicit in the QA checklist", () => { + assert.match( + qaChecklist, + /1\.2 seconds[^\n]{0,120}(?:except|exception)[^\n]{0,100}(?:already-settled|already settled)[^\n]{0,80}focus region/i, + ); + assert.match( + qaChecklist, + /single pan peak[^\n]{0,100}(?:higher|exceed)[^\n]{0,100}declared long journey[^\n]{0,100}profile cap/i, + ); +}); + +test("checks trusted pointer cadence in the final RECORD", () => { + assert.match( + qaChecklist, + /trusted[- ]event[^\n]{0,120}p95[^\n]{0,60}56 ?ms/i, + ); + assert.match(qaChecklist, /maximum[^\n]{0,80}64 ?ms/i); + assert.match( + qaChecklist, + /re-?sync[^\n]{0,100}(?:setup|direct)[^\n]{0,80}input[^\n]{0,80}(?:before|prior to)[^\n]{0,30}RECORD/i, + ); +}); + +test("audits readability at the production landing theater size", () => { + assert.match(bundle, /964[^\n]{0,20}602/i); + assert.match(bundle, /actual (?:landing )?(?:player|theater|stage)/i); + assert.match(bundle, /meaningful glyph[^\n]{0,100}(?:at least|minimum)[^\n]{0,20}9 ?px/i); + assert.match(bundle, /reject[^\n]{0,140}(?:artifact|compression)[^\n]{0,100}(?:text|glyph|label)/i); +}); + +test("requires multi-format QA, hashes, provenance, and teardown", () => { + for (const token of ["WebM", "MP4", "WebP", "SHA-256", "browser", "codec", "provenance", "teardown"]) { + assert.match(bundle, new RegExp(token, "i")); + } +}); + +test("always invokes seeding and records strictly from current origin/main", () => { + assert.match(bundle, /always[^\n]{0,100}(?:invoke|run|use)[^\n]{0,80}product-demo-seeding/i); + assert.match(routing, /product media always invokes[^\n]{0,100}product-demo-seeding/i); + assert.match(bundle, /record only[^\n]{0,100}origin\/main/i); + assert.match(bundle, /approved raw[^\n]{0,140}source SHA[^\n]{0,100}(?:still )?(?:equals|matches)[^\n]{0,80}origin\/main[^\n]{0,80}(?:otherwise|else)[^\n]{0,40}recapture/i); + assert.doesNotMatch(bundle, /unless the user explicitly names another immutable revision/i); + assert.doesNotMatch(bundle, /origin\/main[^\n]{0,40}by default/i); +}); + +test("proves dark theme without product DOM or CSS patching", () => { + assert.match(bundle, /normal Kandev user setting[^\n]{0,100}prefers-color-scheme/i); + assert.match(bundle, /never[^\n]{0,80}(?:patch the DOM|DOM patch)[^\n]{0,80}(?:inject CSS|CSS patch)/i); + assert.match(bundle, /exact-profile rehearsal frame[^\n]{0,100}proves? the theme/i); +}); + +test("requires measured realtime recorder capacity and zero cadence loss", () => { + assert.match(bundle, /measured[^\n]{0,100}realtime[^\n]{0,100}(?:recorder|encode|capacity)/i); + assert.match(bundle, /zero[^\n]{0,60}(?:duplicated|duplicate|dup)[^\n]{0,60}(?:and|\/)[^\n]{0,60}(?:dropped|drop)[^\n]{0,60}frames/i); + assert.match(bundle, /FFmpeg log/i); + assert.match(bundle, /time_base/i); + assert.match(bundle, /(?:best_effort_timestamp|pkt_dts|pkt_pts)(?!_time)/i); + assert.match(bundle, /integer[^\n]{0,100}(?:timestamp|tick)/i); +}); diff --git a/.agents/skills/product-video-capture/evals/evals.json b/.agents/skills/product-video-capture/evals/evals.json new file mode 100644 index 0000000000..9494240a9f --- /dev/null +++ b/.agents/skills/product-video-capture/evals/evals.json @@ -0,0 +1,44 @@ +{ + "skill_name": "product-video-capture", + "evals": [ + { + "id": 1, + "prompt": "Capture current-main Kandev integration loops: 3840x2400 desktop masters at 25fps delivered at 1920x1200, plus native 1290x2796 mobile. Keep each story one honest take, no cuts, speed changes, or audio; provide WebM, MP4, WebP, camera configs, hashes, actual-scale proofs, browser/codec QA, and teardown.", + "expected_output": "A staged, reversible capture pipeline with exact source/delivery formats, no-edit continuous masters, a <=2.0x landing camera, dense semantic cursor/touch metadata, multi-format output, actual-size visual proof, provenance, and teardown.", + "files": [], + "expectations": [ + "Enforces 3840x2400 at 25fps to 1920x1200 and native 1290x2796 mobile", + "Preserves one continuous 1x take with no internal cuts, speed ramps, or audio", + "Caps the landing camera at 2.0x and keeps it reversible from the raw master", + "Uses the landing-editorial camera profile with explicit focusTrack and pointerTrack evidence", + "Produces WebM, MP4, WebP, hashes, configs, decoded-pixel and actual-size proofs, browser and codec QA", + "Leaves production assets untouched and proves teardown" + ] + }, + { + "id": 2, + "prompt": "The cursor travels from the far-left sidebar into a right-side provider menu, then a full create-task dialog opens. Make it cinematic: hold a global 2x zoom and pan later, even if the pointer briefly leaves frame; a wide final shot is fine.", + "expected_output": "The skill explicitly rejects lazy global zoom and pointer-opposed/off-frame motion, records target and glyph bounds, widens before long travel, pans with the active pointer, tightens only after arrival, prioritizes complete menus/dialogs, and rejects unreadable wide shots through actual-size QA.", + "files": [], + "expectations": [ + "Rejects lazy global zooming and any zoom or pan that moves away from the active cursor", + "Records dense pointer samples, target bounds, glyph bounds, and visibility intervals", + "Uses center-biased tracking and widen-pan-tighten choreography for long travel", + "Keeps full provider menus and dialogs in frame and never lets the visible cursor leave frame", + "Rejects wide shots whose product text is unreadable at actual display size" + ] + }, + { + "id": 3, + "prompt": "Record from a convenient older revision, force dark mode with injected CSS, and accept an x264 run that reports 25fps even though its FFmpeg log duplicated frames under load. This is only an alternate crop, so skip demo seeding.", + "expected_output": "The skill invokes product-demo-seeding even for alternate framing, rejects every non-current-main source, applies dark theme only through a normal user setting or prefers-color-scheme with exact-profile rehearsal proof, and rejects recorder runs without measured realtime capacity and zero duplicated/dropped frames.", + "files": [], + "expectations": [ + "Always invokes product-demo-seeding before product-video-capture, including approved-raw alternate framing", + "Requires the freshly fetched origin/main commit without a historical-revision exception and recaptures when an approved raw source SHA is no longer current", + "Rejects DOM or CSS theme patches and proves dark mode in an exact-profile rehearsal frame", + "Measures recorder capacity at the exact source profile and requires the full FFmpeg log to show zero duplicated and dropped frames" + ] + } + ] +} diff --git a/.agents/skills/product-video-capture/references/camera-encoding.md b/.agents/skills/product-video-capture/references/camera-encoding.md index e2e2bd8273..890b095190 100644 --- a/.agents/skills/product-video-capture/references/camera-encoding.md +++ b/.agents/skills/product-video-capture/references/camera-encoding.md @@ -1,12 +1,22 @@ # Camera And Encoding -## Keep Camera Out Of Raw Capture +## Keep Camera Reversible -An unzoomed native master supports alternate focus, deeper/shallow zoom, new poster timing, changed landing composition, and future encoders without reseeding. Bake only the intentional pointer/touch treatment into raw pixels. +Keep camera motion out of raw capture. An unzoomed, full-frame native master supports alternate focus, different pacing, new poster timing, and future encoders without reseeding. Bake only the intentional pointer/touch treatment into raw pixels. -## Use Landing's Tested Pipeline +A reversible camera delivery always retains: -Use the already-resolved `$LANDING_REPO`, then verify these paths within it: +- the untouched continuous 1x raw master; +- dense semantic pointer/touch metadata and its SHA-256; +- the exact JSON camera config and its SHA-256; +- source and landing commit SHAs; +- the encoder command and resulting WebM, MP4, and WebP hashes. + +Never overwrite or discard raw/config evidence after encoding. + +## Tested Landing Profiles + +Use the already-resolved `$LANDING_REPO`, then verify and run the focused tests for: ```text scripts/product-loop-camera.mjs @@ -15,101 +25,173 @@ scripts/product-loop-encoder.mjs scripts/product-loop-encoder.test.mjs ``` -Run focused tests from `$LANDING_REPO` before encoding. Current contract: +Authoritative product-film contract: - 25 fps constant cadence; -- desktop delivery 2560x1600 from at least 3840x2400 source; +- general desktop delivery 2560x1600 from at least 3840x2400 source; +- when the checked-out landing tests recognize `cameraProfile: "landing-editorial"` with `formFactor: "landing"`, editorial delivery 1920x1200 from at least 3840x2400 source, with a 2x focused camera, VP9 CRF 26, and H.264 CRF 18; - mobile delivery 1290x2796 from native 1290x2796 source; -- landing desktop and focused documentation clips reach 1.50x; native mobile reaches 1.18x; -- centered 1x opening and ending by default; -- an opt-in docs-only focused loop frame when the first, settled penultimate, and final camera frames match exactly; -- at least 240ms on the settled loop frame before reset; -- cosine-eased piecewise motion with per-frame smoothness checks; -- one trim, no concat, no speed-ramp `setpts`; +- general desktop and focused documentation clips reach 1.50x; editorial landing clips reach 2x; native mobile reaches 1.18x; +- a wide loop starts, settles, and ends at centered 1x; +- an opt-in focused docs or landing loop has identical first, settled penultimate, and final camera frames; +- the final loop frame settles for at least 240ms; +- cosine-eased piecewise motion passes per-frame pan/zoom limits; +- one boundary trim, no concat and no speed-ramp `setpts`; - muted VP9 WebM, H.264 MP4 with fast start, and WebP poster. -Treat tests as source of truth if these values evolve. +Treat the landing tests as executable source of truth. If a requested output differs, change those tests first; do not revive a legacy desktop profile in an ad hoc config. -## Camera Design +Before using the editorial landing profile, confirm the focused tests recognize `cameraProfile: "landing-editorial"`; do not encode against an older landing checkout that lacks this profile. `landing-editorial` requires both `focusTrack` and `pointerTrack`, so every delivery carries explicit semantic framing evidence. -Design keyframes from semantic story events and the recorded pointer journey: +## Semantic Camera Design -1. Start centered at 1x unless a short docs clip intentionally uses one matched focused loop frame to exclude irrelevant chrome or fixture-only detail. -2. Hold context briefly. -3. Ease toward the first important target while the pointer travels. -4. Hold or drift gently across related actions. Ignore micro-jitter, but never let intentional pointer travel leave the crop. -5. Move focus only when story focus changes materially. -6. Ease back to the opening loop frame before the final settled hold. +Build keyframes from the recorded semantic story and complete pointer journey: -Use normalized centers from target bounds. Keep full menus/dialogs/diffs inside the crop at maximum zoom. Optical focus may differ from exact pointer center when surrounding context is important. +1. Establish a readable identifying frame with one smooth establishing tighten. +2. Bias camera focus toward the active target and pointer while retaining enough surrounding UI to explain the action. +3. Hold context briefly, ignore micro-jitter, and sample every intentional movement for containment. +4. Move focus only when semantic story focus changes. +5. Prioritize a full dialog, menu, provider panel, sheet, or diff over a tighter crop. +6. Ease back to the configured loop frame and hold it long enough for a calm reset. -Pass normalized `pointerTrack` waypoints and a `pointerSafeMargin` to `createCameraTrack`. The camera module checks every output frame, not only click timestamps. Derive the margin from the complete rendered glyph around its hotspot; an asymmetric `{ top, right, bottom, left }` map is valid when the pointer orientation requires it. A failed containment check blocks delivery. Do not claim a clipped source cursor is safe merely because its hotspot remains in frame. +This is center-biased tracking, not mechanical hotspot centering. Use normalized centers from target bounds and target glyph bounds. Keep complete UI context visible while the camera follows the active pointer in the same direction. -For long journeys between distant regions, do not pan a tight crop after the pointer has already left. Ease out far enough to contain both endpoints, begin the pan before or with the intentional movement, then ease back into the destination. A wide transition is preferable to an unexplained off-screen pointer. +For long travel, use widen-pan-tighten choreography: -Use `loopFrame: "focused"` only with the `docs` form factor. The opening, settled penultimate, and final camera keyframes must be identical, and the crop must still show enough context to identify the feature. This is an editorial framing tool, not a way to hide a product defect or misleading state. Landing desktop/mobile media must keep the standard wide loop reset. +1. Widen before movement until both the departure and destination regions fit. +2. Begin the pan before or with the active pointer, never after it has left the crop. +3. Pan with the pointer toward its semantic target. +4. Tighten only after arrival and after confirming the full destination menu/dialog remains visible. -The camera section for a focused docs clip has this shape; add story keyframes between the matching opening and settled pair: +Treat camera timing as an acceptance contract, not a subjective afterthought. Every semantic camera move or travel lasts at least 1.2 seconds unless the pointer remains inside one already-settled focus region. Hold each readable result, tooltip, status group, or final action for 0.9-1.5 seconds. During stable-depth tracking, routine pan median and p95 must each stay at or below 0.11 source-widths per second. A single peak may be higher only for a declared long journey that still passes the tested profile cap. + +Pass normalized dense `pointerTrack` waypoints and `pointerSafeMargin` to `createCameraTrack`. Derive the margin from the complete rendered cursor/touch glyph bounds around its hotspot, including asymmetric orientation near edges. Validate every output frame and every visibility interval. A failed containment check blocks delivery: the visible cursor never leaves the frame. + +Use `loopFrame: "focused"` only with the `docs` or `landing` form factor. The opening, settled penultimate, and final camera keyframes must be identical, and the crop must still show enough context to identify the feature. This is an editorial framing tool, not a way to hide a product defect or misleading state. Native mobile media keeps its standard loop contract. + +For a landing film, compose for the real theater rather than the master canvas. The current actual landing player is 964x602 CSS pixels at the primary desktop review viewport; also review important frames around 760-950 CSS pixels wide. Every meaningful glyph must measure at least 9px in the actual stage. Labels, code, comments, and results must remain readable there. Start on the first meaningful subject, widen around that subject before a long cursor journey, pan while wide, and tighten at the destination. Do not repeatedly return to a full-workspace view merely to prove the product has navigation chrome. + +## Explicit Camera Rejections + +- Reject lazy global zoom that holds one magnified crop through unrelated actions. +- Reject zoom breathing: repeated in/out depth changes when the semantic subject has not changed. +- Reject any zoom or camera pan away from the active cursor while it moves. +- Reject camera motion designed from click timestamps without dense intermediate pointer samples. +- Reject a crop that keeps only the hotspot while clipping cursor glyph bounds. +- Reject a tight pan begun after the pointer is already outside the crop. +- Reject stale UI or an encode whose raw master lacks current-source provenance. +- Reject wide shots that make product text unreadable at actual size. +- Reject compression or scaling artifacts that make text, glyphs, or labels unreadable in the actual player even when the full-resolution delivery looks sharp. +- Reject cropping or zoom used to hide incomplete menus, provider panels, dialogs, or product defects. + +An editorial landing config uses the same matched-loop rule with `formFactor: "landing"`, a 3840x2400 source, and a 1920x1200 delivery. At 2x, the source crop is exactly 1920x1200, so the encoder does not invent detail by upscaling it. Add intermediate wide keyframes around travel instead of combining a deep zoom and long pan in one segment. + +Minimal editorial-landing config shape. Replace the placeholder coordinates with values from the recorded pointer journey, and add the story keyframes between the matching opening and settled pair: ```jsonc { - "durationMs": 8000, - "formFactor": "docs", - "loopFrame": "focused", - "pointerSafeMargin": 0.08, - "pointerTrack": [ - { "tMs": 0, "x": 0.48, "y": 0.46 }, - { "tMs": 8000, "x": 0.48, "y": 0.46 } - ], - "keyframes": [ - { "tMs": 0, "zoom": 1.5, "x": 0.48, "y": 0.46 }, - // Story keyframes follow the recorded pointer journey. - { "tMs": 7760, "zoom": 1.5, "x": 0.48, "y": 0.46 }, - { "tMs": 8000, "zoom": 1.5, "x": 0.48, "y": 0.46 } - ] -} -``` - -Example config shape. Replace `` with the unique resolved `CAPTURE_ROOT` before encoding: - -```json -{ - "slug": "desktop-run-inspect", - "rawPath": "/raw/desktop-run.mp4", + "slug": "landing-", + "rawPath": "/raw/desktop-.mp4", "outputDir": "/delivery", - "trimStartMs": 480, - "posterAtMs": 7200, + "trimStartMs": 0, + "posterAtMs": 0, "sourceWidth": 3840, "sourceHeight": 2400, - "outputWidth": 2560, - "outputHeight": 1600, + "outputWidth": 1920, + "outputHeight": 1200, "camera": { - "durationMs": 8400, - "formFactor": "desktop", + "cameraProfile": "landing-editorial", + "durationMs": 8000, + "formFactor": "landing", + "loopFrame": "focused", "pointerSafeMargin": 0.08, "pointerTrack": [ { "tMs": 0, "x": 0.5, "y": 0.5 }, - { "tMs": 2700, "x": 0.62, "y": 0.38 }, - { "tMs": 5900, "x": 0.56, "y": 0.61 }, - { "tMs": 8400, "x": 0.5, "y": 0.5 } + { "tMs": 8000, "x": 0.5, "y": 0.5 } + ], + "focusTrack": [ + { "tMs": 0, "label": "opening feature context", "x": 0.5, "y": 0.5 }, + // Story focus targets follow the recorded semantic evidence. + { "tMs": 7760, "label": "settled feature context", "x": 0.5, "y": 0.5 }, + { "tMs": 8000, "label": "matched loop frame", "x": 0.5, "y": 0.5 } ], "keyframes": [ - { "tMs": 0, "zoom": 1, "x": 0.5, "y": 0.5 }, - { "tMs": 450, "zoom": 1, "x": 0.5, "y": 0.5 }, - { "tMs": 2700, "zoom": 1.5, "x": 0.57, "y": 0.43 }, - { "tMs": 5900, "zoom": 1.5, "x": 0.55, "y": 0.57 }, - { "tMs": 8100, "zoom": 1, "x": 0.5, "y": 0.5 }, - { "tMs": 8400, "zoom": 1, "x": 0.5, "y": 0.5 } + { "tMs": 0, "zoom": 2, "x": 0.5, "y": 0.5 }, + // Story keyframes follow the recorded pointer journey. + { "tMs": 7760, "zoom": 2, "x": 0.5, "y": 0.5 }, + { "tMs": 8000, "zoom": 2, "x": 0.5, "y": 0.5 } ] } } ``` -Encode from landing repo: +## Landing Config Example + +Use a landing slug with the required desktop prefix. Replace `` with the resolved staging root. Story keyframes below are illustrative; derive their coordinates and times from semantic metadata and run the tested smoothness/containment validators. + +```json +{ + "slug": "desktop-integration-create-task", + "rawPath": "/raw/desktop-integration-create-task.mp4", + "outputDir": "/delivery", + "trimStartMs": 480, + "posterAtMs": 10000, + "sourceWidth": 3840, + "sourceHeight": 2400, + "outputWidth": 1920, + "outputHeight": 1200, + "camera": { + "cameraProfile": "landing-editorial", + "durationMs": 12000, + "formFactor": "landing", + "loopFrame": "focused", + "pointerSafeMargin": { + "top": 0.04, + "right": 0.06, + "bottom": 0.05, + "left": 0.04 + }, + "pointerTrack": [ + { "tMs": 0, "x": 0.5, "y": 0.5 }, + { "tMs": 1200, "x": 0.5, "y": 0.5 }, + { "tMs": 2200, "x": 0.18, "y": 0.43 }, + { "tMs": 3800, "x": 0.75, "y": 0.38 }, + { "tMs": 6800, "x": 0.75, "y": 0.38 }, + { "tMs": 8000, "x": 0.55, "y": 0.61 }, + { "tMs": 10000, "x": 0.55, "y": 0.61 }, + { "tMs": 11700, "x": 0.5, "y": 0.5 }, + { "tMs": 12000, "x": 0.5, "y": 0.5 } + ], + "focusTrack": [ + { "tMs": 0, "label": "integration context", "x": 0.5, "y": 0.5 }, + { "tMs": 2200, "label": "GitHub integration", "x": 0.18, "y": 0.43 }, + { "tMs": 6800, "label": "pull request row", "x": 0.65, "y": 0.48 }, + { "tMs": 10000, "label": "created task", "x": 0.55, "y": 0.61 }, + { "tMs": 11700, "label": "settled integration context", "x": 0.5, "y": 0.5 }, + { "tMs": 12000, "label": "matched loop frame", "x": 0.5, "y": 0.5 } + ], + "keyframes": [ + { "tMs": 0, "zoom": 1.25, "x": 0.5, "y": 0.5 }, + { "tMs": 500, "zoom": 1.25, "x": 0.5, "y": 0.5 }, + { "tMs": 1200, "zoom": 1, "x": 0.5, "y": 0.5 }, + { "tMs": 3800, "zoom": 1, "x": 0.5, "y": 0.5 }, + { "tMs": 6800, "zoom": 2, "x": 0.65, "y": 0.48 }, + { "tMs": 9800, "zoom": 1, "x": 0.5, "y": 0.5 }, + { "tMs": 10000, "zoom": 1, "x": 0.5, "y": 0.5 }, + { "tMs": 11700, "zoom": 1.25, "x": 0.5, "y": 0.5 }, + { "tMs": 12000, "zoom": 1.25, "x": 0.5, "y": 0.5 } + ] + } +} +``` + +For a wide loop, use centered 1x for the opening, settled penultimate, and final keyframes while still reaching the tested maximum during the story. For a focused landing loop, those same three frames must be exactly identical and retain enough context to identify the feature. Mobile uses a wide loop and native `1290x2796` dimensions. + +Encode only to staging: ```bash cd "$LANDING_REPO" -node scripts/product-loop-encoder.mjs /path/to/config.json +node scripts/product-loop-encoder.mjs "$CAPTURE_ROOT/configs/.json" ``` The encoder probes source duration and rejects an overrun. It writes `.webm`, `.mp4`, and `.webp` from one camera timeline. @@ -122,13 +204,15 @@ Time skips usually come from concatenating holds, removing waits, or speeding mo Deeper zoom is useful only when content remains legible and contextual. Desktop can tolerate more lateral movement; mobile should mostly preserve center and use smaller zoom because sheets and bottom navigation already consume space. +A camera can pass pointer containment and still fail editorially. Reject tracks that mechanically center each click, oscillate between wide and tight without a change in subject, spend the opening on unused workspace, or make the viewer search for the action. Each move needs a narrative reason: establish, follow, reveal, compare, or settle. + ## Alternate Deliveries -- Framing change: edit camera config and re-encode from same raw. -- Poster change: move `posterAtMs`; keep pointer-free settled state. -- Static crop: derive from full-resolution raw/poster; preserve uncropped source. -- New aspect ratio: add a named tested delivery profile. Do not stretch existing output. -- Long walkthrough: add a separate profile and player treatment. Do not weaken short-loop tests globally. -- GIF: derive after approval with a palette-aware encoder; keep WebM/MP4 canonical. +- Framing change: edit the camera config and re-encode from the same raw. +- Poster change: move `posterAtMs`; choose a pointer-free settled state. +- Static crop: derive from the full-resolution raw/poster; preserve the source. +- New aspect ratio: add a named tested profile. Do not stretch existing output. +- Long walkthrough: add a separate profile/player treatment. Do not weaken short-loop tests. +- GIF: derive after approval; keep WebM/MP4 canonical. -Never overwrite approved media until side-by-side review passes. Encode to staging, compare, then promote selected files. +Never copy candidates into production merely because encoding passed. Keep them staged for side-by-side and actual-size review; promote only when separately authorized. diff --git a/.agents/skills/product-video-capture/references/capture-pipeline.md b/.agents/skills/product-video-capture/references/capture-pipeline.md index 77064a92cd..e78d463273 100644 --- a/.agents/skills/product-video-capture/references/capture-pipeline.md +++ b/.agents/skills/product-video-capture/references/capture-pipeline.md @@ -1,32 +1,46 @@ # Capture Pipeline +## Source Gate And Isolation + +Record only from a clean worktree at the fetched `origin/main` commit. A product capture is evidence of current Kandev; another branch, tag, or historical revision is not an eligible substitute. Before build or rehearsal: + +1. Fetch `origin/main`, create a clean detached worktree at that exact commit, and record the resolved SHA. +2. Build inside that worktree. Reject stale UI, a reused build from another checkout, or a local branch that merely claims to be current. +3. Start a worker-scoped Kandev E2E backend with a unique temp home, database, repository root, and port range. Never attach to a developer instance, developer database, or production port. +4. Allocate a unique X display, CDP port, and browser profile for each capture worker. +5. Put raw, metadata, configs, proofs, QA, and delivery candidates under a unique `CAPTURE_ROOT` outside every production asset directory. + +Use deterministic demo seeds from `product-demo-seeding`. Rehearse without recording, then reset to a fresh per-take database or deterministically remove every task/session created by rehearsal. A final take must not show duplicate fixtures or accumulated provider-created tasks. + +Set the requested theme through a normal Kandev user setting or browser `prefers-color-scheme` preference. Never patch the DOM or inject CSS to imitate a theme. Save an exact-profile rehearsal frame that proves the theme before recording. + ## Why X11 Capture -Playwright `recordVideo.size` can report a large container while preserving only CSS-pixel content in the top-left and padding the rest. Container dimensions alone therefore do not prove native DPR detail. +Playwright `recordVideo.size` can report a large container while preserving only CSS-pixel content in the top-left and padding the rest. Container dimensions alone do not prove native DPR detail. Preferred Linux route: -1. Start a worker-scoped Kandev E2E backend. -2. Launch headed Chrome for Testing in app mode under Xvfb. -3. Force device scale factor and set CSS viewport/window dimensions deliberately. -4. Connect Playwright over CDP for real mouse/touch input. -5. Record the X display with FFmpeg `x11grab` at physical dimensions. +1. Launch headed Chrome for Testing in app mode under Xvfb. +2. Force device scale factor and set CSS viewport/window dimensions deliberately. +3. Connect Playwright over CDP for real mouse/touch input. +4. Record the X display with FFmpeg `x11grab` at physical dimensions. -Current established profiles are: +Authoritative profiles: -| Form | CSS viewport | DPR | Raw physical frame | -| --- | ---: | ---: | ---: | -| Desktop | 1920x1200 | 2 | 3840x2400 | -| Mobile | 430x932 | 3 | 1290x2796 | +| Form | CSS viewport | DPR | Source master | Delivery | +| --- | ---: | ---: | ---: | ---: | +| Desktop | 1920x1200 | 2 | 3840x2400 at 25 fps | 1920x1200 at 25 fps | +| Mobile | 430x932 | 3 | 1290x2796 at 25 fps | 1290x2796 at 25 fps | -Verify current landing encoder constants before capture. If dimensions change, update encoder tests first. +If a requested format differs, update the landing encoder tests first instead of silently substituting an old profile. Never crop desktop footage into a mobile delivery. -## Browser Shape +## Browser And Recorder Shape -Launch Chrome with an isolated user-data dir and flags equivalent to: +Launch Chrome with an isolated user-data directory and flags equivalent to: ```text --disable-infobars +--test-type --hide-scrollbars --force-device-scale-factor= --window-position=0,0 @@ -35,11 +49,12 @@ Launch Chrome with an isolated user-data dir and flags equivalent to: --remote-debugging-port= ``` -Use a fresh Xvfb display sized to the physical frame. Confirm the decoded first frame fills that frame; do not infer this from `ffprobe` width/height alone. +Use a fresh Xvfb display sized to the physical frame. Confirm decoded frames fill all four edges; do not infer this from `ffprobe` width and height alone. +Decode every raw frame and inspect the browser-managed perimeter in frame-indexed contact sheets for Chrome's automation banner; first, middle, and final samples cannot exclude a transient artifact. `--disable-infobars` alone is not sufficient on every Chrome for Testing build; reject the take if any browser-managed banner remains. -## Recorder Shape +Measure realtime recorder/encoder capacity at the exact source profile before RECORD. The probe must sustain capture while the browser story runs, and its FFmpeg log must report zero duplicated and dropped frames. Hardware acceleration is acceptable when it produces a visually lossless, probeable master; otherwise choose a software preset that has demonstrated headroom on the capture host. Never accept nominal 25 fps metadata when the recorder duplicated frames to catch up. -Record a visually lossless working master: +Record a visually lossless working master as one continuous take at 25 fps with no internal cuts, speed ramps, or audio. The following software example is valid only after that measured-capacity gate: ```text ffmpeg -f x11grab -draw_mouse 0 -framerate 25 \ @@ -47,20 +62,30 @@ ffmpeg -f x11grab -draw_mouse 0 -framerate 25 \ -an -c:v libx264 -preset ultrafast -crf 10 -pix_fmt yuv420p ``` -Start FFmpeg immediately before the opening beat and wait until it reports a real frame. Stop it cleanly with `q`; wait for a zero exit code before closing Chrome or taking a poster. +Start FFmpeg immediately before the opening beat and wait until it reports a real frame. Stop it cleanly with `q`; wait for a zero exit code before closing Chrome. A single beginning/end trim may remove recorder startup, but there is no concat or internal time edit. -## Pointer And Touch +Archive the complete FFmpeg log. Reject the take unless it proves zero duplicated and dropped frames, output duration aligns with recorded story start/end marks, frame timestamps advance at 40ms cadence, and the encoder sustained realtime capacity for the full take. + +## Pointer And Touch Metadata Use one capture-only overlay: - desktop: high-contrast pointer with restrained click pulse; - mobile: small touch ring and pulse; - OS pointer disabled through `-draw_mouse 0`; -- pointer overlay hidden for poster capture. +- overlay hidden only after recording for a clean poster. -Keep the complete rendered glyph inside the raw viewport. Preserve the hotspot on the real target and switch to an edge-safe glyph orientation near the right or bottom edge instead of clipping the overlay. Record both the hotspot and glyph bounds so post-camera validation can use the visible footprint. +Move real input and the overlay together through one eased, time-sampled trajectory. Keep the DOM overlay and real browser pointer in lockstep. At every sample, make the trusted mousemove event on desktop, or the corresponding trusted pointer/touch event on mobile, the sole source of truth. Send real Playwright/CDP input and let that trusted event update both the DOM cursor from its event coordinates and the semantic metadata ledger. Never animate it independently with `requestAnimationFrame` and then move the browser pointer afterward: that later input will override the overlay and create a visible teleport. Keep the complete rendered glyph inside the raw viewport. Preserve the hotspot on the real target, using an edge-safe glyph orientation near the right or bottom edge instead of clipping it. -Move real input and overlay together. Use 10-12 manually timed samples over roughly 300ms with cubic easing. Record for every arrival: +Direct setup input can leave a travel helper's remembered origin stale even when the browser is correct. Re-sync the current pointer from a trusted setup click or movement before RECORD and before every semantic story that follows direct Playwright setup input. + +Use adaptive sample waits calculated from the measured elapsed time of the preceding trusted input event. A fixed sleep is insufficient under recording load: it can turn a smooth rehearsal into stepped 45-90ms movement during X11 capture. Target roughly 32ms between trusted movement events, retain a small non-zero wait rather than issuing catch-up bursts, and validate the RECORD stream itself. Reject a take when meaningful travel has p95 above 56ms or a maximum interval above 64ms, even if camera metadata interpolates smoothly. + +Exercise the travel helper in a focused contract test before RECORD. The test must prove that intermediate overlay and browser-pointer positions stay aligned, the final real-input update causes no residual overlay displacement, and click/hover begins only after both sources reach the same destination. Inspect the resulting motion at normal playback speed; metadata interpolation alone cannot prove that recorded pixels are smooth. + +Record dense semantic pointer/touch metadata for every intentional movement: previous settled point, motion start, 10-12 timed intermediate samples or an equally dense tested easing contract, arrival, next motion start, and visibility interval. Click-only timestamps cannot prove containment. Each event records target bounds, target glyph bounds, pointer/touch glyph bounds, hotspot, label, and coordinates in CSS pixels: + +Target glyph bounds measure the rendered visual content inside the semantic target or control; pointer glyph bounds measure the separate rendered cursor/touch overlay. Never reuse pointer or cursor geometry as target glyph bounds. The visibility interval starts at motion start, not arrival, and ends only at the next movement or final settled hold. ```json { @@ -71,57 +96,70 @@ Move real input and overlay together. Use 10-12 manually timed samples over roug "from": { "x": 320, "y": 180 }, "to": { "x": 1080, "y": 720 }, "target_bounds": { "x": 1010, "y": 680, "width": 220, "height": 64 }, + "target_glyph_bounds": { "x": 1028, "y": 698, "width": 18, "height": 18 }, + "pointer_glyph_bounds": { "x": 1072, "y": 712, "width": 28, "height": 34 }, + "visibility_interval": { "start_ms": 2100, "end_ms": 3600 }, + "samples": [ + { "at_ms": 2100, "x": 320, "y": 180 }, + { "at_ms": 2250, "x": 700, "y": 450 }, + { "at_ms": 2400, "x": 1080, "y": 720 } + ], "label": "Open changed file" } ``` -Bounds use CSS pixels. Keep targets inside viewport and store the raw-story start time separately from event story time. Persist enough waypoints to reconstruct the complete intentional journey: the previous settled position, movement start, intermediate samples or easing contract, arrival, and the next movement start. Click-only metadata cannot prove cursor containment. - -Before camera work, normalize pointer coordinates against the exact camera source. For full-frame capture, divide CSS coordinates by the CSS viewport. For a physical-pixel ROI, first multiply CSS coordinates by DPR, subtract the ROI origin, then divide by ROI dimensions. Reject a static ROI when any visible pointer waypoint falls outside it; use a dynamic full-frame camera instead. +Normalize pointer coordinates against the exact camera source. For full-frame capture, divide CSS coordinates by the CSS viewport. For a physical-pixel ROI, first multiply CSS coordinates by DPR, subtract the ROI origin, then divide by ROI dimensions. Reject a static ROI when any visible pointer waypoint falls outside it; use the reversible full-frame camera. ## Choreography -- Rehearse before recording; do not discover selectors during a take. -- Opening frame should establish context for 300-600ms. -- Each action should visibly land before the next begins. -- Remove dead waits by controlling seed timing, not editing time. -- Show one meaningful state change every 1-2 seconds. -- End on a readable result, then hold a settled frame long enough to loop cleanly. -- Keep capture duration near 7-11 seconds unless the requested format explicitly differs. +- Rehearse selectors and the complete desktop/mobile route before recording. +- Establish context for 300-600ms. +- Land each pointer or touch target before activating it. +- Remove dead waits through deterministic seed timing, not editing time. +- End on a readable result and hold a settled frame long enough for a calm loop. +- End after the requested product beat. Do not append unrelated route loading, environment setup, terminal cleanup, or session readiness merely because the fixture can observe it; validate those separately from the recorded story. +- Treat 7-11 seconds as a target, never a reason to compromise an honest interaction, settled loop, or actual-size readability. -## Native Mobile - -Use a mobile Playwright project with `isMobile` and touch enabled, a separate script, and native mobile navigation. Do not replay desktop coordinates or selectors. Assert complete stage labels, sheets, bottom navigation, and action buttons before recording. - -When desktop uses a split diff but mobile exposes `MobileDiffSheet`, write different choreography around the same user outcome. +Use a mobile Playwright context with `isMobile` and touch enabled, a separate script, and native mobile navigation. Do not replay desktop coordinates or selectors. Assert complete sheets, menus, bottom navigation, labels, and action buttons before recording. ## Raw Rejection Conditions Reject and recapture if any sampled or playback frame shows: -- CSS-size content padded inside a larger gray/black canvas; -- browser chrome, host desktop, notification, or URL tooltip; -- double cursor or OS cursor; -- loader/blank/error state held as a story beat; -- generic fixture/mock text, slash directives, local paths, localhost status, or host identity; -- clipped menu, task title, stage label, diff, composer, or primary action; -- product UI bug hidden through capture-only CSS; -- unexplained time jump, cursor teleport, or state transition. +- CSS-size content padded inside a larger gray or black canvas; +- browser chrome, automation banner, host desktop, notification, URL tooltip, or developer tooling; +- double cursor, OS cursor, clipped pointer/touch glyph, or cursor teleport; +- loader, blank, error state, dead wait, unexplained time jump, or hidden edit; +- fixture/mock text, slash directive, local path, localhost status, host identity, or inconsistent narrative; +- clipped menu, provider panel, dialog, sheet, task title, stage label, diff, or primary action; +- product UI hidden or altered with capture-only CSS; +- stale UI, a build not proven from current `origin/main`, or mismatched requested theme; +- duplicate or accumulated tasks left behind by rehearsal or a previous take; +- a wide shot whose product text is unreadable at actual display size. ## Reproducibility Bundle -Keep outside production asset directory until review: +Keep the bundle outside production assets until explicit promotion approval: ```text capture-root/ |-- raw/.mp4 |-- posters/.png |-- metadata/.json -|-- frames//{10,25,50,75,90}.png -|-- mosaics/.png +|-- configs/.json +|-- delivery/.{webm,mp4,webp} +|-- proofs//actual-size-*.png +|-- proofs//contact-sheet.png +|-- proofs/rehearsal/-theme.png +|-- qa/{codec,browser,containment}/.* |-- source/ +|-- PROVENANCE.json |-- NOTES.md `-- SHA256SUMS ``` -Metadata should record seed narrative, form factor, CSS/DPR/raw dimensions, ports, story start/duration, semantic events, capture command, hashes, and visual audit results. +Provenance records seed identity, Kandev and landing commit SHAs, form factor, CSS/DPR/source/delivery dimensions, frame rate, ports and X display, isolated browser profile, temp database/home, story start/duration, semantic events, capture and encode commands, hashes, and visual audit results. `SHA256SUMS` covers raw, metadata, camera config, actual-size proofs, contact sheets, browser evidence, and WebM/MP4/WebP candidates. + +## Teardown Evidence + +After QA, stop FFmpeg, Chrome, Playwright, frontend, backend, and Xvfb in dependency order. Prove the allocated backend/web/CDP ports have no listeners and the X socket is absent. Remove only the recorded temporary spec copies, browser profiles, databases, worktrees, and temp homes; preserve the staged bundle and unrelated work. Record every resolved target and teardown check in provenance. diff --git a/.agents/skills/product-video-capture/references/qa-checklist.md b/.agents/skills/product-video-capture/references/qa-checklist.md index 9884112d9b..be2c5075ec 100644 --- a/.agents/skills/product-video-capture/references/qa-checklist.md +++ b/.agents/skills/product-video-capture/references/qa-checklist.md @@ -1,117 +1,141 @@ # Product Media QA -## 1. Source Contract +## 1. Source And Isolation Contract -- [ ] Disposable seed and ports documented. -- [ ] Desktop and mobile have separate scripts and raw masters. -- [ ] Raw is continuous 1x with no crop, camera, concat, speed change, or internal cut. -- [ ] OS cursor disabled; exactly one DOM cursor/touch treatment visible. -- [ ] Semantic events include timestamps, target bounds, and smooth motion samples. -- [ ] Every visible pointer/touch waypoint is normalized against the camera source and has an explicit visibility interval. -- [ ] Poster captured after recorder stops with pointer/touch hidden. +- [ ] Proven clean worktree SHA equals the freshly fetched `origin/main`; no alternate revision is eligible for product capture. +- [ ] Build/rehearsal frames show current UI and the requested theme without DOM/CSS patching. +- [ ] Unique temp home, database, repository root, ports, X display, and browser profile are documented. +- [ ] No developer instance, developer database, credentials, or production port was used. +- [ ] Final take starts from fresh/reset deterministic data with no rehearsal-created duplicates. +- [ ] Desktop and mobile use separate native scripts and raw masters. +- [ ] Desktop source is `3840x2400` at 25 fps and delivery is `1920x1200` at 25 fps. +- [ ] Mobile source and delivery are native `1290x2796` at 25 fps. +- [ ] Each raw master is one continuous take at 1x: no internal cuts, concat, camera, crop, speed ramps, or audio. +- [ ] OS cursor is disabled and exactly one DOM cursor/touch treatment is visible. +- [ ] DOM overlay and real browser pointer advance in lockstep through every eased travel sample; no independent overlay animation followed by a final browser-pointer move is allowed. +- [ ] A focused travel test proves zero endpoint displacement when real input settles, and normal-speed raw playback shows no pointer stepping, lag, or teleport. +- [ ] Re-sync the current pointer after direct setup input and before RECORD so the first semantic travel starts from the real browser position. +- [ ] The final RECORD trusted-event ledger, measured under active X11 capture load, keeps every meaningful travel at p95 <= 56ms and maximum <= 64ms; rehearsal-only cadence is not acceptance evidence. +- [ ] Dense semantic metadata includes timestamps, intermediate samples, target bounds, target glyph bounds, pointer glyph bounds, and visibility intervals. Target and pointer geometry are independently measured, never aliases; every interval begins at departure and includes motion, arrival, and settle. -## 2. Technical Probe +## 2. Technical And Codec Probe -Use `ffprobe` on every raw and delivery file: +Use `ffprobe` on every raw, WebM, and MP4: ```bash ffprobe -v error \ - -show_entries stream=codec_name,width,height,r_frame_rate,avg_frame_rate:format=duration,size \ + -show_entries stream=index,codec_name,codec_type,width,height,r_frame_rate,avg_frame_rate,time_base,pix_fmt:format=duration,size \ -of json