352 lines
18 KiB
Markdown
352 lines
18 KiB
Markdown
# Portfolio Phase 3: Media, Accessibility, Resilience, and Performance Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** Replace text-first media slots with cleared authentic project assets and prove that the complete experience meets its accessibility, resilience, responsive, and performance contracts.
|
||
|
||
**Architecture:** A deterministic asset-preparation script re-encodes approved inputs into safe generated filenames and a hash-bound manifest; original files and the private clearance register remain outside version control. Media components load only through the manifest and obey motion/data policies. Browser instrumentation and fault hooks are compiled into test builds only.
|
||
|
||
**Tech Stack:** Node.js, Sharp, ffmpeg, Tesseract.js, ZXing, Vue 3, Vitest, Playwright, axe-core, Lighthouse CLI
|
||
|
||
**Spec:** `docs/superpowers/specs/2026-09-01-portfolio-journey-design.md`
|
||
|
||
## Global Constraints
|
||
|
||
- Only authentic user-supplied project media and code-generated diagrams/world artwork may ship.
|
||
- Original filenames, private names, source paths, metadata, browser chrome, account data, QR/barcodes, and title screens never enter distributable output.
|
||
- Health-related assets use visibly fictional demo values only and require Luca’s manual approval after final re-encoding.
|
||
- Video uses `preload="none"`, no native inaccessible controls, no autoplay in reduced/static/data-saver modes, and an author-owned play/pause control.
|
||
- Every chart has a visible caption and adjacent structured description; exact data tables appear when a claim depends on exact values.
|
||
- Automated accessibility supports but never replaces the recorded WCAG 2.2 A/AA manual checklist.
|
||
- Release budgets and fault/browser matrix are hard gates; ordinary device memory-pressure observations remain advisory.
|
||
|
||
---
|
||
|
||
### Task 1: Build deterministic asset preparation and clearance checks
|
||
|
||
**Files:**
|
||
- Create: `scripts/prepare-assets.mjs`
|
||
- Create: `scripts/lib/asset-recipes.mjs`
|
||
- Create: `scripts/lib/hash-file.mjs`
|
||
- Create: `scripts/lib/privacy-scan.mjs`
|
||
- Create: `scripts/lib/video-frames.mjs`
|
||
- Create: `content/public/asset-recipes.json`
|
||
- Create: `docs/asset-clearance-template.md`
|
||
- Create: `tests/fixtures/assets/`
|
||
- Test: `tests/integration/prepare-assets.test.ts`
|
||
- Modify: `package.json`
|
||
|
||
**Interfaces:**
|
||
- Consumes: `--source-root <private path>`, approved recipe JSON, and a denylist file descriptor/path supplied only to the scanner process.
|
||
- Produces: `inputs/prepared-assets/`, updated `content/public/asset-manifest.json`, and a private scan report outside tracked paths.
|
||
|
||
- [ ] **Step 1: Install asset tooling into the exact lockfile**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
npm install --save-dev sharp tesseract.js @zxing/library file-type lighthouse
|
||
```
|
||
|
||
Add `prepare:assets` and `verify:assets` scripts that pass explicit paths rather than discover user directories.
|
||
|
||
- [ ] **Step 2: Write failing deterministic-preparation tests**
|
||
|
||
Fixtures cover EXIF removal, generated safe filenames, byte-identical output across two runs, unexpected/extra output rejection, OCR denylist match, QR/barcode rejection, video poster/frame sampling, title-frame exclusion, malformed media, and command failure without leaking matched text.
|
||
|
||
```ts
|
||
expect(result.manifest.assets[0].filename).toMatch(/^project-[a-z0-9-]+\.[a-z0-9]+$/);
|
||
expect(result.reportText).not.toContain(secretCanary);
|
||
expect(firstOutputHash).toBe(secondOutputHash);
|
||
```
|
||
|
||
- [ ] **Step 3: Define exact public recipes**
|
||
|
||
The recipe file uses only generic asset IDs and generated outputs:
|
||
|
||
```json
|
||
{
|
||
"schema": "portfolio-asset-recipes/v1",
|
||
"assets": [
|
||
{ "id": "medication-mobile-overview", "kind": "image", "output": "project-medication-overview.webp", "maxWidth": 720 },
|
||
{ "id": "medication-mobile-settings", "kind": "image", "output": "project-medication-settings.webp", "maxWidth": 720 },
|
||
{ "id": "footy-detection-frame", "kind": "image", "output": "project-footy-detection.webp", "maxWidth": 1600 },
|
||
{ "id": "opinion-polarization-chart", "kind": "image", "output": "project-opinion-polarization.webp", "maxWidth": 1000 },
|
||
{ "id": "private-game-loop", "kind": "video", "output": "project-private-game-loop.webm", "startSeconds": 3, "durationSeconds": 6 },
|
||
{ "id": "healthcare-interface", "kind": "image", "output": "project-healthcare-interface.webp", "maxWidth": 1440 },
|
||
{ "id": "currency-converter-interface", "kind": "image", "output": "project-currency-converter.webp", "maxWidth": 1440 }
|
||
]
|
||
}
|
||
```
|
||
|
||
Additional opinion charts follow the same `opinion-<metric>-chart` naming. Source filenames/paths are supplied through an untracked private mapping and never copied into the recipe or manifest.
|
||
|
||
- [ ] **Step 4: Implement safe image/video preparation**
|
||
|
||
Images are decoded, resized without enlargement, flattened where needed, and re-encoded as WebP/PNG with metadata omitted. Video is decoded by ffmpeg in a resource-limited subprocess, cropped to approved gameplay time, stripped of audio/unexpected tracks unless separately cleared, scaled to the approved cap, encoded as VP9 WebM plus H.264 MP4 fallback, and given a cleared poster. Subprocess arguments are arrays, never shell-concatenated strings.
|
||
|
||
- [ ] **Step 5: Implement final-output scanning**
|
||
|
||
Run OCR and QR/barcode detection against every final image/poster and sampled video frames; run byte/text/metadata checks against every output and decompressed test container. Failure output contains only opaque rule ID and coordinate/frame. Unsupported/encrypted/malformed input fails closed.
|
||
|
||
- [ ] **Step 6: Require manual final review**
|
||
|
||
Generate a contact sheet for Luca showing every final derivative at full readable scale plus representative/all-changing video frames. Record approval in an untracked private clearance register; only approved hashes may be copied to `inputs/prepared-assets` and the public manifest.
|
||
|
||
- [ ] **Step 7: Run and commit only safe outputs/code**
|
||
|
||
```bash
|
||
npm run test:unit -- tests/integration/prepare-assets.test.ts
|
||
npm run prepare:assets -- --source-root /absolute/private/source --mapping /absolute/private/mapping.json --denylist /absolute/private/denylist.txt
|
||
git add scripts content/public/asset-recipes.json content/public/asset-manifest.json inputs/prepared-assets docs/asset-clearance-template.md tests package.json package-lock.json
|
||
git commit -m "feat: add deterministic cleared project media"
|
||
```
|
||
|
||
Before committing, `git diff --cached --name-only` must contain no original source name or private path.
|
||
|
||
---
|
||
|
||
### Task 2: Integrate project media, chart descriptions, and controlled video
|
||
|
||
**Files:**
|
||
- Create: `src/components/ProjectMedia.vue`
|
||
- Create: `src/components/AccessibleVideo.vue`
|
||
- Create: `src/components/ChartFigure.vue`
|
||
- Create: `src/composables/useLazyMedia.ts`
|
||
- Modify: `src/components/ProjectCaseStudy.vue`
|
||
- Modify: `src/components/ProjectCard.vue`
|
||
- Modify: `content/public/portfolio-content.json`
|
||
- Test: `tests/unit/ProjectMedia.test.ts`
|
||
- Test: `tests/browser/media-policy.spec.ts`
|
||
|
||
**Interfaces:**
|
||
- Consumes: approved asset IDs/copy, public manifest, motion policy.
|
||
- Produces: intrinsic-size figures, author-controlled video, lazy/detached media lifecycle, accessible fallbacks.
|
||
|
||
- [ ] **Step 1: Write failing component policy tests**
|
||
|
||
Assert manifest-only URLs, contextual alt from content, figure captions, chart descriptions, no native video controls, `preload="none"`, detached sources outside the threshold, and explicit Play behavior in reduced/static/data-saver modes.
|
||
|
||
- [ ] **Step 2: Implement manifest-bound media resolution**
|
||
|
||
```ts
|
||
export interface MediaPresentation {
|
||
assetId: string;
|
||
alt: string;
|
||
caption?: string;
|
||
longDescription?: string;
|
||
}
|
||
```
|
||
|
||
`ProjectMedia` rejects unknown IDs and never reads contextual wording from the manifest.
|
||
|
||
- [ ] **Step 3: Implement author-owned video controls**
|
||
|
||
`AccessibleVideo` uses a real Play/Pause button, visible state, poster, caption/description, time-preserving detach on offscreen pause, and failure text. It attaches sources within 300 CSS pixels only when automatic media policy permits; otherwise explicit Play attaches them.
|
||
|
||
- [ ] **Step 4: Implement chart figures**
|
||
|
||
Each chart renders `<figure>`, visible `<figcaption>`, structured variables/trend/conclusion text, and a table only when exact values support the claim. Color never carries the conclusion alone.
|
||
|
||
- [ ] **Step 5: Verify and commit**
|
||
|
||
```bash
|
||
npm run test:unit -- tests/unit/ProjectMedia.test.ts
|
||
npm run test:browser -- tests/browser/media-policy.spec.ts
|
||
git add src/components src/composables/useLazyMedia.ts content/public/portfolio-content.json tests
|
||
git commit -m "feat: present cleared accessible project media"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 3: Enforce WCAG 2.2 A/AA and screen-reader workflows
|
||
|
||
**Files:**
|
||
- Create: `tests/browser/accessibility-axe.spec.ts`
|
||
- Create: `tests/browser/accessibility-keyboard.spec.ts`
|
||
- Create: `tests/browser/accessibility-reflow.spec.ts`
|
||
- Create: `tests/browser/accessibility-forced-colors.spec.ts`
|
||
- Create: `docs/accessibility/wcag-2.2-aa-checklist.md`
|
||
- Create: `docs/accessibility/screen-reader-scripts.md`
|
||
- Modify: `src/styles/base.css`
|
||
- Modify: `src/styles/components.css`
|
||
- Modify: `src/styles/world.css`
|
||
|
||
**Interfaces:**
|
||
- Consumes: all default/audio/static/reduced/media-failure/missing-social states.
|
||
- Produces: automated reports plus manual NVDA/Firefox and VoiceOver/Safari evidence.
|
||
|
||
- [ ] **Step 1: Write automated axe/state tests**
|
||
|
||
```ts
|
||
for (const state of ['default', 'audio-running', 'static', 'reduced-motion', 'media-failure', 'missing-social']) {
|
||
test(`${state} has no axe violations`, async ({ page }) => {
|
||
await enterState(page, state);
|
||
expect((await new AxeBuilder({ page }).analyze()).violations).toEqual([]);
|
||
});
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Test keyboard, focus, and target geometry**
|
||
|
||
Enumerate every enabled author-owned pointer target and require 44×44 CSS pixels except marked inline sentence links. Verify focus is visible, inside safe bounds, and not overlapped by fixed HUD/controls. Keyboard traversal reaches Contact in logical order.
|
||
|
||
- [ ] **Step 3: Test reflow and user styles**
|
||
|
||
Run at 320 CSS pixels and desktop 400% zoom/effective 320 width. Apply WCAG text-spacing overrides and forced colors; require no lost/clipped content, control, focus ring, or Contact action.
|
||
|
||
- [ ] **Step 4: Execute named screen-reader scripts**
|
||
|
||
Record versions/results for NVDA+Firefox on Windows and VoiceOver+Safari on macOS/iOS. Follow the seven literal tasks in the spec, including scene description, navigation focus, audio controls, Toolkit labels, chart descriptions, video controls, and contact links.
|
||
|
||
- [ ] **Step 5: Resolve every applicable criterion and commit evidence**
|
||
|
||
```bash
|
||
npm run test:browser -- tests/browser/accessibility-axe.spec.ts tests/browser/accessibility-keyboard.spec.ts tests/browser/accessibility-reflow.spec.ts tests/browser/accessibility-forced-colors.spec.ts
|
||
git add tests/browser/accessibility-* docs/accessibility src/styles
|
||
git commit -m "test: enforce accessible portfolio interaction"
|
||
```
|
||
|
||
No automated violation remains open; false positives require evidence in the checklist.
|
||
|
||
---
|
||
|
||
### Task 4: Implement deterministic fault injection and browser coverage
|
||
|
||
**Files:**
|
||
- Create: `src/testing/fault-hooks.ts`
|
||
- Create: `tests/browser/fault-matrix.spec.ts`
|
||
- Create: `tests/browser/context-loss.spec.ts`
|
||
- Create: `tests/browser/audio-interruption.spec.ts`
|
||
- Create: `tests/browser/media-failure.spec.ts`
|
||
- Create: `tests/helpers/test-build-auth.ts`
|
||
- Modify: `vite.config.ts`
|
||
- Modify: `playwright.config.ts`
|
||
|
||
**Interfaces:**
|
||
- Consumes: authenticated test-build secret generated per run.
|
||
- Produces: F1–F13 fault hooks only in test builds and cross-engine failure evidence.
|
||
|
||
- [ ] **Step 1: Write failing absence/authentication tests**
|
||
|
||
Production bundles must not contain `__PORTFOLIO_TEST_HOOKS__`. A test build rejects missing/wrong ephemeral token. Correct token exposes only named deterministic failures and state snapshots.
|
||
|
||
- [ ] **Step 2: Implement compile-time-eliminated hooks**
|
||
|
||
Hooks cover scene chunk, shader, model, texture, poster, video, hydration, pre/post-first-frame exception, context loss/restore, audio start rejection, and audio interruption. They cannot mutate content, URLs, approval data, or production configuration.
|
||
|
||
- [ ] **Step 3: Implement exact failure oracles**
|
||
|
||
Every case asserts H1, sections, anchors, contact, private-link omissions, focus behavior, one static fallback, no duplicate resources, and no unhandled exception/rejection.
|
||
|
||
- [ ] **Step 4: Expand the executable browser matrix**
|
||
|
||
Configure current Chromium/Firefox/WebKit/Edge equivalents and the previous stable majors available in CI containers. Record physical iOS Safari and Pixel Chromium separately; do not silently replace them with emulation.
|
||
|
||
- [ ] **Step 5: Verify and commit**
|
||
|
||
```bash
|
||
npm run build:preview
|
||
npm run test:browser -- tests/browser/fault-matrix.spec.ts tests/browser/context-loss.spec.ts tests/browser/audio-interruption.spec.ts tests/browser/media-failure.spec.ts
|
||
git add src/testing tests playwright.config.ts vite.config.ts
|
||
git commit -m "test: add deterministic resilience matrix"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 5: Add performance instrumentation and enforce budgets
|
||
|
||
**Files:**
|
||
- Create: `src/testing/performance-instrumentation.ts`
|
||
- Create: `tests/performance/drive-trace.spec.ts`
|
||
- Create: `tests/performance/physical-drive.protocol.ts`
|
||
- Create: `tests/performance/budgets.test.ts`
|
||
- Create: `scripts/check-bundle-budgets.mjs`
|
||
- Create: `docs/performance/lab-runbook.md`
|
||
- Modify: `package.json`
|
||
- Modify: `vite.config.ts`
|
||
|
||
**Interfaces:**
|
||
- Consumes: instrumented test build, physical reference devices, pinned network profiles.
|
||
- Produces: five-run lab JSON, bundle report, physical protocol log, and pass/fail summary.
|
||
|
||
- [ ] **Step 1: Write unit tests for metric calculations**
|
||
|
||
Cover nearest-rank p95, LCP cutoff, CLS windows, pre-running TBT, post-running long tasks, event-to-next-paint eight-action oracle, scene continuity, lost-presentation share, and zero unexpected late shifts.
|
||
|
||
- [ ] **Step 2: Implement test-only instrumentation**
|
||
|
||
Use PerformanceObserver and marks around complete scene update+render. Strip instrumentation from production with a compile-time constant. Never collect GPU/audio identity or export telemetry.
|
||
|
||
- [ ] **Step 3: Implement `drive-trace.v1` exactly**
|
||
|
||
Use the offsets/actions in the spec, trusted CDP input, pinned High/Low tiers, five fresh profiles per reference class, precise timeouts, and the deterministic navigation-settlement barrier. Write one result JSON per run plus aggregation.
|
||
|
||
- [ ] **Step 4: Implement `physical-drive.v1` as a protocol module**
|
||
|
||
Generate the exact W3C `driveTouch`/`driveKeys` actions, safe-viewport coordinates, cycle direction, excluded 389/390-second cycles, terminal-Static branches, rotations, backgrounding, interruption, context loss, and Contact finish. The module outputs actions and assertions; platform runners consume it without changing timing.
|
||
|
||
- [ ] **Step 5: Enforce transfer/runtime budgets**
|
||
|
||
Read the exact byte/frame/bundle thresholds from §14 into named constants and fail the build report on any violation. Audit dynamic chunk separation so reduced motion downloads no Three.js/live-scene asset.
|
||
|
||
- [ ] **Step 6: Run lab gates and commit tooling/results**
|
||
|
||
```bash
|
||
npm run build:preview
|
||
node scripts/check-bundle-budgets.mjs dist
|
||
npx playwright test tests/performance/drive-trace.spec.ts
|
||
git add src/testing tests/performance scripts/check-bundle-budgets.mjs docs/performance package.json vite.config.ts
|
||
git commit -m "perf: enforce journey budgets and reference traces"
|
||
```
|
||
|
||
Physical-device result files are committed only after device metadata is reviewed for privacy.
|
||
|
||
---
|
||
|
||
### Task 6: Complete visual QA, flash analysis, and audience validation
|
||
|
||
**Files:**
|
||
- Create: `tests/browser/visual-regression.spec.ts`
|
||
- Create: `docs/accessibility/flash-analysis.md`
|
||
- Create: `docs/reviews/audience-validation.md`
|
||
- Create: `docs/reviews/visual-qa.md`
|
||
- Modify: scene/style/component files identified by the captured diffs
|
||
|
||
**Interfaces:**
|
||
- Consumes: final Phase 3 captures across viewports/states.
|
||
- Produces: approved visual baseline, flash evidence, and three-person audience results.
|
||
|
||
- [ ] **Step 1: Capture every required visual state**
|
||
|
||
Capture desktop widths 1280/1440/1920/2560, tablet 768, mobile portrait/landscape, reduced motion, Static, audio-running, media failure, missing social, and Contact arrival. Use deterministic journey/test state but production rendering code.
|
||
|
||
- [ ] **Step 2: Review composition against the product direction**
|
||
|
||
Check readable near-black panels, restrained cyan/amber, no content scanlines/glow, original car silhouette, stable chase composition, no AI/generated project art, balanced systems/software weight, and restrained mobile world density.
|
||
|
||
- [ ] **Step 3: Run flash analysis**
|
||
|
||
Analyze final scene/video captures in every current engine. Record that no content exceeds the three-flashes threshold and resolve any failure by reducing/removing the effect.
|
||
|
||
- [ ] **Step 4: Run the evidence-first audience check**
|
||
|
||
Three independent hiring/technical reviewers use Static or reduced motion. Record 30-second identity/role/contact and 60-second systems/software proof results plus professionalism/evidence-balance ratings. Revise content hierarchy if thresholds fail.
|
||
|
||
- [ ] **Step 5: Run the complete Phase 3 gate and commit**
|
||
|
||
```bash
|
||
npm run typecheck
|
||
npm run test:unit
|
||
npm run build:preview
|
||
npm run test:browser
|
||
node scripts/check-bundle-budgets.mjs dist
|
||
git add tests/browser/visual-regression.spec.ts docs/accessibility docs/reviews src
|
||
git commit -m "test: approve portfolio quality and audience evidence"
|
||
```
|
||
|
||
## Phase 3 completion checkpoint
|
||
|
||
The build is a launch candidate only after cleared final assets, WCAG records, cross-engine fault results, performance budgets, physical-device evidence, flash analysis, and audience thresholds all pass without exceptions hidden in CI logs.
|
||
|