docs: add approved portfolio design and plans
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.superpowers/sdd/
|
||||||
|
.worktrees/
|
||||||
225
docs/reviews/2026-09-01-portfolio-spec-independent-review.md
Normal file
225
docs/reviews/2026-09-01-portfolio-spec-independent-review.md
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
# Independent Review — Portfolio Journey Design Specification
|
||||||
|
|
||||||
|
**Date:** 2026-09-01
|
||||||
|
**Reviewed artifact:** `2026-09-01-portfolio-journey-design.md`
|
||||||
|
**Review method:** Four independent, isolated reviewers with no conversation context and no file-edit permission
|
||||||
|
|
||||||
|
## Review scopes
|
||||||
|
|
||||||
|
1. Architecture, correctness, product coherence, and implementation feasibility
|
||||||
|
2. Security, privacy, media handling, supply chain, and deployment controls
|
||||||
|
3. Performance, resilience, lifecycle behavior, and testability
|
||||||
|
4. Specification quality, audience fit, accessibility, SEO, and content credibility
|
||||||
|
|
||||||
|
## Overall verdict
|
||||||
|
|
||||||
|
**Review closed — approved for implementation planning.**
|
||||||
|
|
||||||
|
No reviewer found a critical conceptual flaw. The original review identified implementation-contract gaps rather than problems with the moving-road concept, car, section signs, restrained content layer, optional audio, no-public-CV decision, or private-project naming policy. Those gaps were remediated through Revision 8 and targeted confirmation passes. Architecture returned a clean Critical/High pass. The remaining High security and performance findings were resolved directly in the final revision, after which the review loop was deliberately closed as disproportionate to a design document.
|
||||||
|
|
||||||
|
## Review closure
|
||||||
|
|
||||||
|
- Architecture: clean independent Critical/High confirmation pass.
|
||||||
|
- Security: final High findings addressed by independent dual-worker/matcher validation, explicit policy/tool-provenance issuer recovery, exact attestation binding, trusted-time expiry checks, and atomic public visibility.
|
||||||
|
- Performance and correctness: final High findings addressed by non-overlapping physical actions, terminal-Static branching, deterministic navigation-cancellation barriers, and an exact `maxScrollY` oracle.
|
||||||
|
- Accessibility, SEO, content credibility, and audience fit: incorporated from the original independent review into the final acceptance gates.
|
||||||
|
- No implementation code was started during review.
|
||||||
|
|
||||||
|
The detailed findings below are retained as the historical audit trail; they no longer describe the current specification's status.
|
||||||
|
|
||||||
|
## Consensus strengths
|
||||||
|
|
||||||
|
- Essential content is independent of WebGL, audio, hydration, and JavaScript failure.
|
||||||
|
- Native scrolling, real anchors, and the absence of a router are appropriate for the document.
|
||||||
|
- The project descriptions avoid fake metrics and honestly label private, modest, experimental, and work-in-progress projects.
|
||||||
|
- The specification treats keyboard use, reduced motion, static fallback, mobile layouts, and screen readers as first-class concerns.
|
||||||
|
- The privacy policy already covers visible copy, alt text, filenames, metadata, source maps, structured data, and private links.
|
||||||
|
- Scene, semantic content, coordination, and audio responsibilities are separated cleanly.
|
||||||
|
- SEO direction correctly prioritizes complete HTML, a canonical identity, crawlable links, and verified profiles rather than promising a ranking.
|
||||||
|
|
||||||
|
## Required revisions
|
||||||
|
|
||||||
|
### 1. Define the exact static-generation contract
|
||||||
|
|
||||||
|
The current combination of Vue, Vite, pre-rendering, and hydration does not name the mechanism that produces complete HTML.
|
||||||
|
|
||||||
|
The revised specification should:
|
||||||
|
|
||||||
|
- choose the exact static-generation or pre-render implementation;
|
||||||
|
- define a typed build-time `siteConfig` containing canonical URL, email, optional verified social profiles, production environment, and metadata;
|
||||||
|
- fail the production build when required values are missing or invalid;
|
||||||
|
- describe how HTML, JSON-LD, sitemap, robots directives, and hydration are generated;
|
||||||
|
- clarify that production artifacts are host-specific even though the application remains portable across static hosts;
|
||||||
|
- rename the product architecture to “a pre-rendered single-page document with progressive Vue enhancement,” avoiding the client-shell implications of “SPA.”
|
||||||
|
|
||||||
|
### 2. Replace screenshot redaction with a closed asset-clearance pipeline
|
||||||
|
|
||||||
|
Removing private product names is insufficient for medication and healthcare media. Screenshots may expose names, emails, medication information, notifications, URLs, tokens, hostnames, QR codes, or environment details. Other footage may raise ownership or public-display concerns.
|
||||||
|
|
||||||
|
Every published asset should have a clearance record containing:
|
||||||
|
|
||||||
|
- safe public asset ID and generated output filename;
|
||||||
|
- source, owner, license or permission, attribution requirement, and public-display rights;
|
||||||
|
- privacy classification and approval owner;
|
||||||
|
- confirmation that health-related interfaces use synthetic/demo data only;
|
||||||
|
- OCR and full-resolution manual review results;
|
||||||
|
- video frame-review and container-track results;
|
||||||
|
- metadata-stripping and safe-format transcoding results;
|
||||||
|
- sanitized output hash and final approval.
|
||||||
|
|
||||||
|
Private identifiers should be pseudonymized before content enters a public repository. Privacy gates must run before preview deployment as well as production. Seeded canary tests should prove the gate catches identifiers in HTML, filenames, source maps, metadata, images, and video.
|
||||||
|
|
||||||
|
### 3. Create an authoritative content and evidence register
|
||||||
|
|
||||||
|
The specification requires factual CV copy and honest project descriptions but does not define the source revision, approved copy, evidence, or verification owner.
|
||||||
|
|
||||||
|
The content model should record, for every role and project:
|
||||||
|
|
||||||
|
- approved title and bounded copy;
|
||||||
|
- source and as-of date;
|
||||||
|
- Luca’s role and contribution;
|
||||||
|
- problem, technical decisions, outcome, and maturity state;
|
||||||
|
- verified public URL or an explicit private/no-link state;
|
||||||
|
- approved asset IDs and accessibility descriptions;
|
||||||
|
- factual, licensing, and privacy approval.
|
||||||
|
|
||||||
|
Experience should be reverse chronological with one consistent date and location format. The on-page profile should define the verified hiring-screening facts that remain public because a CV download is intentionally unavailable.
|
||||||
|
|
||||||
|
### 4. Balance systems and software evidence
|
||||||
|
|
||||||
|
The stated identity is an equal hybrid, but the current content allocation can read as “developer with a homelab.”
|
||||||
|
|
||||||
|
Infrastructure should receive comparable narrative and visual weight through verified evidence of:
|
||||||
|
|
||||||
|
- reverse proxy and certificate lifecycle;
|
||||||
|
- authentication and access-policy design;
|
||||||
|
- service deployment, updates, enrollment, and retirement;
|
||||||
|
- failure investigation or incident-style debugging;
|
||||||
|
- backup/recovery, monitoring, automation, or hardening where facts are available.
|
||||||
|
|
||||||
|
This should not become a full service inventory or disclose operationally sensitive details. The final diagram must exclude real hostnames, addresses, ports, versions, usernames, recovery endpoints, and exact security rules.
|
||||||
|
|
||||||
|
### 5. Define one deterministic journey/navigation state machine
|
||||||
|
|
||||||
|
Intersection observers, measured positions, fragments, history, focus, and browser restoration currently have no authority or precedence rules.
|
||||||
|
|
||||||
|
The revised specification should define:
|
||||||
|
|
||||||
|
- a single active-section algorithm and boundary line;
|
||||||
|
- tie-breaking for tall, short, and overlapping sections;
|
||||||
|
- recalculation after font load, media load, resize, and orientation change;
|
||||||
|
- explicit navigation using one history entry, passive scroll using no new entry or `replaceState`, and `popstate` never pushing;
|
||||||
|
- direct-fragment and bfcache restoration behavior;
|
||||||
|
- loop suppression between scrolling, URL updates, and focus;
|
||||||
|
- a strict rule that scrolling and observers never move keyboard or screen-reader focus;
|
||||||
|
- focus behavior for keyboard activation, pointer activation, direct fragments, and back/forward navigation.
|
||||||
|
|
||||||
|
Sign, HUD, navigation, environment chapter, and semantic section IDs should agree within one animation frame.
|
||||||
|
|
||||||
|
### 6. Add explicit WebGL and Web Audio lifecycle models
|
||||||
|
|
||||||
|
Asynchronous scene and audio initialization can otherwise create duplicate contexts, late attachments, leaked resources, or misleading controls.
|
||||||
|
|
||||||
|
The scene needs named states such as `idle`, `loading`, `running`, `paused`, `lost`, `static`, and `disposed`, with an abort/generation guard for every asynchronous completion. Teardown must cancel animation frames, listeners, observers, loaders, geometries, materials, textures, render targets, and post-processing exactly once.
|
||||||
|
|
||||||
|
On WebGL context loss, rendering stops immediately. One restoration event may rebuild resources; failure within a fixed timeout or any later context loss forces static mode for the session.
|
||||||
|
|
||||||
|
Audio remains an approved feature, but it is non-blocking. It needs idempotent `idle`, `starting`, `running`, `suspended`, `interrupted`, `failed`, and `closed` states. Rapid activation must create one context and one graph. Rejected starts show a quiet “audio unavailable” state. Visibility, mobile interruption, bfcache, mute-during-startup, and teardown behavior must be explicit.
|
||||||
|
|
||||||
|
### 7. Make reduced-motion and adaptive-quality precedence exact
|
||||||
|
|
||||||
|
System reduced motion and manual pause always force static behavior. A saved full-motion preference never silently overrides a current system preference.
|
||||||
|
|
||||||
|
When reduced motion is known at initial load, Three.js and animated project media are not downloaded or initialized. A runtime transition to static cancels rendering and decorative media promptly while leaving audio independently controllable.
|
||||||
|
|
||||||
|
Adaptive quality needs numerical frame-time thresholds, a sampling window, hysteresis, and precedence rules. User-selected quality is a maximum tier; the controller may downgrade but does not automatically upgrade during the session. Repeated context failure and reduced motion always override the selected tier.
|
||||||
|
|
||||||
|
### 8. Separate reproducible lab gates from field performance goals
|
||||||
|
|
||||||
|
Core Web Vitals p75 values are field metrics and cannot be proven from an undefined “production-like” test, especially when the initial release intentionally has no analytics.
|
||||||
|
|
||||||
|
The revised specification should:
|
||||||
|
|
||||||
|
- define exact cold-cache lab devices, browsers, network and CPU throttling, run count, tool, and aggregation method;
|
||||||
|
- set compressed byte budgets for critical HTML/CSS, initial Vue JavaScript, poster, fonts, scene chunk/assets, and video;
|
||||||
|
- cap mobile DPR, texture dimensions, GPU assets, post-initialization long tasks, and per-tier scene frame time;
|
||||||
|
- define downgrade thresholds and cooldown;
|
||||||
|
- treat LCP, INP, and CLS p75 as post-launch goals unless a separately privacy-reviewed, minimal RUM design is approved;
|
||||||
|
- keep the initial release free of analytics SDKs, tracking pixels, cookies, device fingerprinting, and telemetry endpoints.
|
||||||
|
|
||||||
|
### 9. Strengthen media and accessibility behavior
|
||||||
|
|
||||||
|
- Quantitative chart groups need visible captions and adjacent structured descriptions covering purpose, axes, variables, key trends, and conclusions; use a data table when exact values matter.
|
||||||
|
- Automatically moving media longer than five seconds needs a visible, keyboard-operable pause control.
|
||||||
|
- Reduced-motion and data-saver modes use posters until explicit play.
|
||||||
|
- Media uses `preload="none"`, explicit lazy-load thresholds, intrinsic dimensions/aspect ratios, offscreen pause, safe size/duration caps, and accessible failure text.
|
||||||
|
- Final scene and video states must satisfy the three-flashes-or-below-threshold rule.
|
||||||
|
- WCAG acceptance must include 320 CSS-pixel reflow/400% zoom, text-spacing overrides, forced colors, unobscured focus, and named screen-reader/browser pairs.
|
||||||
|
- The 44-by-44 CSS-pixel target applies to touch/pointer controls at every supported viewport, not mobile portrait only.
|
||||||
|
|
||||||
|
### 10. Define the supported-browser and failure-injection matrix
|
||||||
|
|
||||||
|
Required coverage should include current and previous Chromium, Firefox, and Safari/WebKit releases, plus at least one physical iOS Safari and Android Chromium check. Mobile checks cover rotation, dynamic toolbars, safe areas, background/foreground, sustained scrolling, memory pressure, audio interruption, and context loss.
|
||||||
|
|
||||||
|
Fault injection should cover:
|
||||||
|
|
||||||
|
- Three.js chunk failure;
|
||||||
|
- shader compilation failure;
|
||||||
|
- model, texture, poster, and video failure;
|
||||||
|
- hydration-entry failure;
|
||||||
|
- exceptions before and after the first scene frame;
|
||||||
|
- WebGL loss during loading and while hidden;
|
||||||
|
- Web Audio start rejection and interruption.
|
||||||
|
|
||||||
|
Every failure must preserve the H1, all semantic sections, native anchors, contact action, private-link omissions, focus behavior, and one clear static fallback without unhandled rejections.
|
||||||
|
|
||||||
|
### 11. Add a provider-neutral security and hosting contract
|
||||||
|
|
||||||
|
Production responses require tested security controls, including a restrictive Content Security Policy, HSTS, `X-Content-Type-Options`, explicit `Referrer-Policy`, least-privilege `Permissions-Policy`, and anti-framing protection. Hosting must support HTTPS/preferred-host redirects, Brotli/Gzip, immutable caching for hashed assets, HTML revalidation, correct MIME and media range responses, deployment verification, and rollback.
|
||||||
|
|
||||||
|
Preview/staging must be authenticated where possible and additionally use `X-Robots-Tag: noindex, nofollow, noarchive`; it must not publish a production sitemap or enter Search Console.
|
||||||
|
|
||||||
|
Production source maps are not public. External SVGs and media are untrusted, sanitized, and prevented from making external requests. Contact and social deployment values use allowlisted schemes and exact hosts, safe JSON-LD serialization, and `noopener`/referrer protections.
|
||||||
|
|
||||||
|
### 12. Add supply-chain and provenance gates
|
||||||
|
|
||||||
|
- exact lockfile and reproducible clean builds;
|
||||||
|
- dependency and secret scanning with an explicit severity policy;
|
||||||
|
- pinned CI actions;
|
||||||
|
- SBOM generation;
|
||||||
|
- no CDN or hotlinked executable assets;
|
||||||
|
- no unreviewed post-install scripts;
|
||||||
|
- local, licensed fonts and icons recorded in the asset register;
|
||||||
|
- preserved MIT notice if reference code is substantially adapted.
|
||||||
|
|
||||||
|
### 13. Split build-ready and deployed-production acceptance
|
||||||
|
|
||||||
|
Build acceptance can verify semantic HTML, privacy gates, static fallback, validated structured data inputs, accessibility, performance budgets, and browser behavior.
|
||||||
|
|
||||||
|
Deployment acceptance separately verifies production headers, canonical URL, preferred-host redirects, indexability, sitemap, media range requests, and post-deployment Search Console submission. Search Console is not a pre-build gate.
|
||||||
|
|
||||||
|
### 14. Resolve smaller contradictions and define audience validation
|
||||||
|
|
||||||
|
- H1 is exactly **Luca Pîslaru**; the role is an adjacent visible subtitle. The page title contains both name and role.
|
||||||
|
- Domain and contact email are required. Each social profile is independently optional; visible links and JSON-LD `sameAs` entries are emitted only for configured, verified profiles.
|
||||||
|
- The primary contact action is an ordinary `mailto:` link with a visible email fallback and no form backend.
|
||||||
|
- The no-public-CV decision remains. Provide complete on-page experience details and a print-friendly portfolio view without presenting it as a downloadable résumé.
|
||||||
|
- Run a small evidence-first audience check: representative hiring managers and technical interviewers should identify the name, equal hybrid role, contact path, and at least two credible proof points in a short static/reduced-motion review. Animation must not delay or obscure any answer.
|
||||||
|
|
||||||
|
## Recommendations deliberately not adopted
|
||||||
|
|
||||||
|
### Defer audio to a later release
|
||||||
|
|
||||||
|
Not adopted. Interactive audio is an explicitly approved part of the experience. It remains optional, starts only after direct activation, never gates content, and gains a precise lifecycle and failure model.
|
||||||
|
|
||||||
|
### Add a public CV download
|
||||||
|
|
||||||
|
Not adopted. The absence of a public CV is an explicit privacy and contact choice. The specification should instead make the on-page professional record complete and printable, and make the email path frictionless.
|
||||||
|
|
||||||
|
### Add general analytics for Core Web Vitals
|
||||||
|
|
||||||
|
Not adopted for the initial release. Lab budgets become release gates; field p75 values remain goals. Any future RUM design requires a separate privacy review and strict data minimization.
|
||||||
|
|
||||||
|
## Recommended next action
|
||||||
|
|
||||||
|
Proceed to implementation planning from the approved Revision 8 specification. Preserve its product direction and translate the acceptance gates into phased, testable work rather than reopening aesthetic discovery.
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
# Portfolio Journey Implementation Roadmap
|
||||||
|
|
||||||
|
> **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:** Build Luca Pîslaru’s accessible, pre-rendered portfolio and its optional night-drive experience from the approved design specification.
|
||||||
|
|
||||||
|
**Architecture:** Delivery is split into four independently reviewable phases. Phase 1 produces a complete semantic, responsive, pre-rendered portfolio; Phase 2 adds the isolated journey, Three.js, and Web Audio layers; Phase 3 integrates cleared media and enforces accessibility/performance resilience; Phase 4 packages and verifies an atomic Nginx-hosted production release while the source repository remains private.
|
||||||
|
|
||||||
|
**Tech Stack:** Vue 3, TypeScript, Vite, `vue/server-renderer`, Three.js, Web Audio API, Vitest, Vue Test Utils, Playwright, axe-core, Node.js 22, npm 10
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-09-01-portfolio-journey-design.md`
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- Public identity is exactly **Luca Pîslaru** and **System Administrator & Full-Stack Developer**.
|
||||||
|
- Essential content, navigation, and contact links work without JavaScript, WebGL, audio, or video.
|
||||||
|
- The document uses native scrolling and semantic anchors; do not add Vue Router or scroll hijacking.
|
||||||
|
- Audio starts only after **START THE DRIVE** activation and never gates content.
|
||||||
|
- The scene is decorative to assistive technology; one concise text description represents it.
|
||||||
|
- System reduced motion, visitor pause, data saver, and Static mode prevent automatic scene/video loading.
|
||||||
|
- Never publish a portrait, CV download, GitHub heatmap, fake metric, skill percentage, AI-generated project image, private product name, private repository URL, or sensitive infrastructure detail.
|
||||||
|
- Public copy uses **Linux**, never a distribution-specific label.
|
||||||
|
- `PortfolioContent` is the sole owner of subject-matter and rendered accessibility copy.
|
||||||
|
- The four mutable production inputs are `PortfolioContent`, public asset manifest, `PreparedAssetSet`, and `siteConfig`.
|
||||||
|
- Initial source control remains private; the launch artifact is an atomic static release served by Nginx.
|
||||||
|
- Every task follows test-first development and ends with its own reviewable commit after a repository exists.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase order
|
||||||
|
|
||||||
|
1. [Phase 1 — Foundation, content, and semantic SSR](2026-09-01-portfolio-phase-1-foundation.md)
|
||||||
|
2. [Phase 2 — Journey, scene, and audio](2026-09-01-portfolio-phase-2-journey.md)
|
||||||
|
3. [Phase 3 — Media, accessibility, resilience, and performance](2026-09-01-portfolio-phase-3-quality.md)
|
||||||
|
4. [Phase 4 — Security, packaging, and deployment](2026-09-01-portfolio-phase-4-release.md)
|
||||||
|
|
||||||
|
## Specification coverage matrix
|
||||||
|
|
||||||
|
| Specification sections | Owning plan/tasks |
|
||||||
|
|---|---|
|
||||||
|
| §1–3 product, goals, audience, evidence balance | Phase 1 Tasks 3–7; Phase 3 Task 6 |
|
||||||
|
| §4 identity, privacy, approvals, contact | Phase 1 Tasks 2–4; Phase 3 Task 1; Phase 4 Tasks 2–3 |
|
||||||
|
| §5 journey/page content | Phase 1 Tasks 3–5 |
|
||||||
|
| §6 visual direction | Phase 1 Task 5; Phase 2 Tasks 4–5; Phase 3 Task 6 |
|
||||||
|
| §7 motion/navigation/signs | Phase 2 Tasks 1–3 and 7 |
|
||||||
|
| §8 audio | Phase 2 Task 6; Phase 3 Tasks 3–4 |
|
||||||
|
| §9 responsive/reduced/static | Phase 1 Task 5; Phase 2 Tasks 3, 5, and 7; Phase 3 Task 3 |
|
||||||
|
| §10 accessibility | Phase 1 Tasks 4–7; Phase 3 Tasks 2–3 and 6 |
|
||||||
|
| §11 architecture/SSR/lifecycles | Phase 1 Tasks 1–6; Phase 2 Tasks 3–6 |
|
||||||
|
| §12 data flow/formulas | Phase 2 Tasks 1–3 |
|
||||||
|
| §13 loading/resilience/quality | Phase 2 Tasks 3–7; Phase 3 Tasks 2 and 4 |
|
||||||
|
| §14 performance | Phase 3 Task 5 |
|
||||||
|
| §15 SEO | Phase 1 Task 6; Phase 4 Tasks 1 and 5 |
|
||||||
|
| §16 security/hosting/supply chain | Phase 4 Tasks 1–5 |
|
||||||
|
| §17 reference/license | Phase 2 Task 4; Phase 3 Task 1; Phase 4 Task 5 |
|
||||||
|
| §18 test strategy | Phase 1 Task 7; Phase 2 Task 7; Phase 3 Tasks 3–6; Phase 4 Tasks 3–5 |
|
||||||
|
| §19 acceptance gates | Phase 3 completion checkpoint; Phase 4 completion checkpoint |
|
||||||
|
| §20 implementation boundary | This roadmap and its four phase order |
|
||||||
|
|
||||||
|
## Locked file structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
.
|
||||||
|
├── config/
|
||||||
|
│ └── site.preview.json
|
||||||
|
├── content/
|
||||||
|
│ ├── public/portfolio-content.json
|
||||||
|
│ └── public/asset-manifest.json
|
||||||
|
├── docs/
|
||||||
|
│ ├── accessibility/
|
||||||
|
│ ├── deployment/
|
||||||
|
│ ├── performance/
|
||||||
|
│ └── superpowers/{plans,specs}/
|
||||||
|
├── inputs/prepared-assets/
|
||||||
|
├── public/assets/
|
||||||
|
├── scripts/
|
||||||
|
│ ├── prerender.mjs
|
||||||
|
│ ├── prepare-assets.mjs
|
||||||
|
│ ├── verify-artifacts.mjs
|
||||||
|
│ ├── build-release.mjs
|
||||||
|
│ └── smoke-production.mjs
|
||||||
|
├── src/
|
||||||
|
│ ├── App.vue
|
||||||
|
│ ├── app.ts
|
||||||
|
│ ├── entry-client.ts
|
||||||
|
│ ├── entry-server.ts
|
||||||
|
│ ├── audio/
|
||||||
|
│ ├── components/
|
||||||
|
│ ├── composables/
|
||||||
|
│ ├── content/
|
||||||
|
│ ├── journey/
|
||||||
|
│ ├── scene/
|
||||||
|
│ ├── security/
|
||||||
|
│ └── styles/
|
||||||
|
├── tests/
|
||||||
|
│ ├── browser/
|
||||||
|
│ ├── fixtures/
|
||||||
|
│ ├── integration/
|
||||||
|
│ └── unit/
|
||||||
|
├── index.html
|
||||||
|
├── nginx.portfolio.conf.example
|
||||||
|
├── package.json
|
||||||
|
├── playwright.config.ts
|
||||||
|
├── tsconfig.json
|
||||||
|
├── vite.config.ts
|
||||||
|
└── vitest.config.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cross-phase interfaces
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface ProductionInputs {
|
||||||
|
content: PortfolioContent;
|
||||||
|
assets: PublicAssetManifest;
|
||||||
|
preparedAssetRoot: URL;
|
||||||
|
site: SiteConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JourneySnapshot {
|
||||||
|
activeSectionId: SectionId;
|
||||||
|
routeProgress: number;
|
||||||
|
driveSpeed: number;
|
||||||
|
acceleration: number;
|
||||||
|
sign: SignSnapshot | null;
|
||||||
|
chapter: EnvironmentChapter;
|
||||||
|
arrived: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SceneController {
|
||||||
|
start(signal: AbortSignal): Promise<void>;
|
||||||
|
update(snapshot: JourneySnapshot): void;
|
||||||
|
setPaused(paused: boolean): void;
|
||||||
|
snapshot(): SceneLifecycleState;
|
||||||
|
subscribe(listener: (state: SceneLifecycleState) => void): () => void;
|
||||||
|
dispose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AudioController {
|
||||||
|
startFromGesture(): Promise<void>;
|
||||||
|
setMuted(muted: boolean): void;
|
||||||
|
setVolume(volume: number): void;
|
||||||
|
setIntensity(intensity: number): void;
|
||||||
|
suspend(): Promise<void>;
|
||||||
|
dispose(): Promise<void>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Later phases may extend these types with discriminated state unions, but they must not rename or bypass these boundaries.
|
||||||
|
|
||||||
|
## Phase completion rule
|
||||||
|
|
||||||
|
A phase is complete only when its targeted unit, SSR, browser, accessibility, and artifact tests pass; its generated output contains no private names or source paths; and the next phase can consume its documented interfaces without reaching into private module state.
|
||||||
@@ -0,0 +1,576 @@
|
|||||||
|
# Portfolio Phase 1: Foundation and Semantic SSR 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:** Produce a complete, responsive, pre-rendered portfolio whose content, navigation, metadata, and contact path work before any interactive scene code exists.
|
||||||
|
|
||||||
|
**Architecture:** A strict public-input layer validates the four production inputs before Vue receives them. Vue renders one semantic document through the official server renderer; a Node prerender script emits final static HTML, metadata, sitemap, robots rules, and host examples, then the browser hydrates the identical immutable snapshot.
|
||||||
|
|
||||||
|
**Tech Stack:** Node.js 22.22.1, npm 10.9.4, Vue 3, TypeScript, Vite, `vue/server-renderer`, Zod, Vitest, Vue Test Utils, Playwright
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-09-01-portfolio-journey-design.md`
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- The raw HTML contains the canonical H1, hybrid role, eight section headings, project and infrastructure evidence, Toolkit labels, and visible email link.
|
||||||
|
- Vue components contain interface language only; all subject-matter and rendered accessibility copy comes from `PortfolioContent`.
|
||||||
|
- Unknown input keys, extra asset files, invalid URLs, private links, and unmanifested asset IDs fail before rendering.
|
||||||
|
- Preview configuration uses `https://portfolio.localhost`; production configuration is not created until Luca approves the real canonical host.
|
||||||
|
- The private CV/evidence ledger and original media never enter the repository.
|
||||||
|
- No scene, Web Audio, video autoplay, analytics, public source map, or third-party executable asset is introduced in this phase.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Initialize the private repository and testable Vue toolchain
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `.gitignore`
|
||||||
|
- Create: `.nvmrc`
|
||||||
|
- Create: `package.json`
|
||||||
|
- Create: `package-lock.json`
|
||||||
|
- Create: `tsconfig.json`
|
||||||
|
- Create: `vite.config.ts`
|
||||||
|
- Create: `vitest.config.ts`
|
||||||
|
- Create: `playwright.config.ts`
|
||||||
|
- Create: `index.html`
|
||||||
|
- Test: `tests/unit/toolchain.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: empty project folder plus the approved spec.
|
||||||
|
- Produces: npm scripts `dev`, `typecheck`, `test:unit`, `test:browser`, `build:client`, `build:ssr`, `prerender`, and `build:preview`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Initialize local source control without configuring a remote**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git init -b main
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `.git/` exists and `git remote -v` prints nothing.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Write the runtime pins and ignore rules**
|
||||||
|
|
||||||
|
Create `.nvmrc` containing `22.22.1`. Create `.gitignore` with:
|
||||||
|
|
||||||
|
```gitignore
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
dist-ssr/
|
||||||
|
coverage/
|
||||||
|
playwright-report/
|
||||||
|
test-results/
|
||||||
|
.env*
|
||||||
|
config/site.production.json
|
||||||
|
content/private/
|
||||||
|
inputs/original-assets/
|
||||||
|
*.map
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Install the exact toolchain into the lockfile**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm init -y
|
||||||
|
npm install vue three zod
|
||||||
|
npm install --save-dev typescript vite @vitejs/plugin-vue vue-tsc @vue/server-renderer vitest @vue/test-utils happy-dom @playwright/test @axe-core/playwright eslint prettier
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `package-lock.json` records exact resolved versions and `npm audit --omit=dev` completes without a Critical or High production finding.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add strict compiler and test configuration**
|
||||||
|
|
||||||
|
Use strict TypeScript with `noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`, DOM and ES2023 libraries, `moduleResolution: "Bundler"`, and the `@/* → src/*` alias. Configure Vitest for `happy-dom`, global setup disabled, and coverage over `src/**/*.{ts,vue}`. Configure Playwright projects for desktop Chromium and mobile Chromium initially; Phase 3 expands the matrix.
|
||||||
|
|
||||||
|
The `package.json` scripts must be:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --host 127.0.0.1",
|
||||||
|
"typecheck": "vue-tsc --noEmit",
|
||||||
|
"test:unit": "vitest run",
|
||||||
|
"test:browser": "playwright test",
|
||||||
|
"build:client": "vite build --outDir dist/client --manifest",
|
||||||
|
"build:ssr": "vite build --ssr src/entry-server.ts --outDir dist-ssr",
|
||||||
|
"prerender": "node scripts/prerender.mjs",
|
||||||
|
"build:preview": "npm run typecheck && npm run test:unit && npm run build:client && npm run build:ssr && npm run prerender -- --config config/site.preview.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Write and run the first failing toolchain test**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
describe('toolchain', () => {
|
||||||
|
it('runs TypeScript tests', () => {
|
||||||
|
expect(import.meta.env.MODE).toBe('test');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `npm run test:unit`. Expected: PASS after the Vitest configuration is valid.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit the initialized foundation**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .gitignore .nvmrc package.json package-lock.json tsconfig.json vite.config.ts vitest.config.ts playwright.config.ts index.html tests/unit/toolchain.test.ts
|
||||||
|
git commit -m "chore: initialize portfolio toolchain"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Define and enforce the four public production inputs
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/content/types.ts`
|
||||||
|
- Create: `src/content/schemas.ts`
|
||||||
|
- Create: `src/content/load-production-inputs.ts`
|
||||||
|
- Create: `src/content/asset-url.ts`
|
||||||
|
- Create: `tests/fixtures/portfolio-content.valid.json`
|
||||||
|
- Create: `tests/fixtures/asset-manifest.valid.json`
|
||||||
|
- Create: `tests/fixtures/site-config.preview.json`
|
||||||
|
- Test: `tests/unit/content-schemas.test.ts`
|
||||||
|
- Test: `tests/unit/production-inputs.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: four explicit URLs passed to `loadProductionInputs()`.
|
||||||
|
- Produces: `PortfolioContent`, `PublicAssetManifest`, `SiteConfig`, `ProductionInputs`, `parsePortfolioContent()`, `parsePublicAssetManifest()`, `parseSiteConfig()`, `loadProductionInputs()`, and `assetUrl()`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing schema tests for the public boundary**
|
||||||
|
|
||||||
|
Cover exact acceptance and rejection behavior:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
it('rejects a public link on a private project', () => {
|
||||||
|
const input = structuredClone(validContent);
|
||||||
|
input.projects.selected[0].visibility = 'private';
|
||||||
|
input.projects.selected[0].url = 'https://example.test/private';
|
||||||
|
expect(() => parsePortfolioContent(input)).toThrow(/private project.*url/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects unknown site configuration keys', () => {
|
||||||
|
expect(() => parseSiteConfig({
|
||||||
|
environment: 'preview',
|
||||||
|
canonicalUrl: 'https://portfolio.localhost',
|
||||||
|
buildId: 'preview-1',
|
||||||
|
contactEmail: 'forbidden@example.test'
|
||||||
|
})).toThrow(/unrecognized/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects an asset reference absent from the manifest', () => {
|
||||||
|
expect(() => validateProductionInputs(contentWithMissingAsset, manifest, site, rootUrl))
|
||||||
|
.toThrow(/unmanifested asset/i);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `npm run test:unit -- tests/unit/content-schemas.test.ts`. Expected: FAIL because the schemas do not exist.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Define stable public types**
|
||||||
|
|
||||||
|
`src/content/types.ts` must define these roots and discriminants:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export type SectionId = 'start' | 'profile' | 'experience' | 'infrastructure' |
|
||||||
|
'selected-builds' | 'additional-work' | 'toolkit' | 'contact';
|
||||||
|
export type ProjectVisibility = 'public' | 'private';
|
||||||
|
export type ProjectMaturity = 'learning' | 'experimental' | 'prototype' | 'work-in-progress' | 'active';
|
||||||
|
|
||||||
|
export interface PublicProject {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
visibility: ProjectVisibility;
|
||||||
|
maturity: ProjectMaturity;
|
||||||
|
summary: string;
|
||||||
|
problem: string;
|
||||||
|
contribution: string;
|
||||||
|
technicalDecisions: readonly string[];
|
||||||
|
outcome: string;
|
||||||
|
url?: string;
|
||||||
|
assetIds: readonly string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PortfolioContent {
|
||||||
|
identity: { name: 'Luca Pîslaru'; role: 'System Administrator & Full-Stack Developer'; valueProposition: string };
|
||||||
|
navigation: ReadonlyArray<{ id: SectionId; label: string }>;
|
||||||
|
profile: { introduction: string; screeningFacts: ReadonlyArray<{ label: string; value: string }> };
|
||||||
|
experience: readonly ExperienceRecord[];
|
||||||
|
systemsOperations: SystemsCaseStudy;
|
||||||
|
infrastructure: InfrastructureCaseStudy;
|
||||||
|
projects: { selected: readonly PublicProject[]; additional: readonly PublicProject[] };
|
||||||
|
toolkit: readonly ToolkitGroup[];
|
||||||
|
contact: ContactContent;
|
||||||
|
metadata: MetadataContent;
|
||||||
|
sceneDescription: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SiteConfig {
|
||||||
|
environment: 'preview' | 'production';
|
||||||
|
canonicalUrl: string;
|
||||||
|
buildId: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Define the referenced `ExperienceRecord`, `SystemsCaseStudy`, `InfrastructureCaseStudy`, `ToolkitGroup`, `ContactContent`, and `MetadataContent` in the same file with only public fields described by the spec.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement strict Zod schemas and cross-record checks**
|
||||||
|
|
||||||
|
All object schemas use `.strict()`. `parsePortfolioContent()` must assert the exact identity literals, exactly eight unique canonical navigation IDs, two primary selected builds plus the supporting research panel, generic private titles, HTTPS allowlisted social hosts, no URL on private projects, and no empty visible copy. `parseSiteConfig()` accepts only the three declared keys and validates preview or production URL rules. `parsePublicAssetManifest()` enforces unique IDs, generated safe filenames, lowercase SHA-256, media type, and dimensions/duration.
|
||||||
|
|
||||||
|
Run the schema test. Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Write failing closed-set loader tests**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
it('rejects an extra prepared file', async () => {
|
||||||
|
await expect(loadProductionInputs(fixtureUrls.extraPreparedFile)).rejects.toThrow(/extra prepared asset/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns deeply frozen inputs', async () => {
|
||||||
|
const inputs = await loadProductionInputs(fixtureUrls.valid);
|
||||||
|
expect(Object.isFrozen(inputs.content.projects.selected)).toBe(true);
|
||||||
|
expect(() => (inputs.site.buildId = 'mutated')).toThrow();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Implement `loadProductionInputs()` and `assetUrl()`**
|
||||||
|
|
||||||
|
Use explicit paths, never directory discovery for subject matter:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface ProductionInputLocations {
|
||||||
|
contentJson: URL;
|
||||||
|
assetManifestJson: URL;
|
||||||
|
preparedAssetRoot: URL;
|
||||||
|
siteConfigJson: URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadProductionInputs(
|
||||||
|
locations: ProductionInputLocations
|
||||||
|
): Promise<Readonly<ProductionInputs>>;
|
||||||
|
|
||||||
|
export function assetUrl(
|
||||||
|
manifest: PublicAssetManifest,
|
||||||
|
assetId: string,
|
||||||
|
base = '/assets/'
|
||||||
|
): string;
|
||||||
|
```
|
||||||
|
|
||||||
|
The loader parses JSON with duplicate-key detection, validates every prepared file against the manifest, rejects links/special files/extra members, hashes bytes with SHA-256, and deep-freezes the result. Run both input test files; expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit the input contract**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/content tests/fixtures tests/unit/content-schemas.test.ts tests/unit/production-inputs.test.ts
|
||||||
|
git commit -m "feat: define closed portfolio input contract"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Create the approved public content projection
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `content/public/portfolio-content.json`
|
||||||
|
- Create: `content/public/asset-manifest.json`
|
||||||
|
- Create: `config/site.preview.json`
|
||||||
|
- Create: `scripts/materialize-empty-asset-set.mjs`
|
||||||
|
- Create: `docs/content-approval-checklist.md`
|
||||||
|
- Modify: `package.json`
|
||||||
|
- Test: `tests/integration/public-content.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: the supplied CV, project descriptions, public repository evidence, and Luca’s explicit approval of the public email/social URLs.
|
||||||
|
- Produces: validated public JSON inputs with no evidence IDs, source paths, approval identities, original filenames, or private names.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Record the exact approval checklist before transcribing content**
|
||||||
|
|
||||||
|
The checklist contains one row for identity, each screening fact, each experience entry, systems operations, each infrastructure evidence band, every project, every Toolkit item, contact field, metadata field, and asset record. Each row records `approved`, `omit`, or `needs Luca confirmation`; execution stops and asks Luca about any third state before public JSON is written.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Extract claims from the supplied CV and project evidence into a private scratch document**
|
||||||
|
|
||||||
|
Use the PDF workflow to read `/home/luca/Documents/cv-en-septembrie-2026-np.pdf`. Keep the scratch file outside tracked project paths, compare every claim with the approved design, and do not copy phone, street address, birth information, or identifiers into public content.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Obtain explicit confirmation for the public contact fields**
|
||||||
|
|
||||||
|
Confirm one public email and each optional GitHub, GitLab, and LinkedIn URL. Omit any unconfirmed social link from both visible content and metadata. Do not create `config/site.production.json` in this task.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Write the public content and empty-safe manifest**
|
||||||
|
|
||||||
|
Use the exact generic private titles from the spec. Asset IDs may appear only when their Phase 3 sanitized derivatives exist; until then, use empty `assetIds` arrays and text-first case-study layouts. `config/site.preview.json` is exactly:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"environment": "preview",
|
||||||
|
"canonicalUrl": "https://portfolio.localhost",
|
||||||
|
"buildId": "preview-semantic-v1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`materialize-empty-asset-set.mjs` validates that the current manifest contains zero assets, creates `inputs/prepared-assets/` when absent, and fails if that directory contains any member. Add `prepare:preview-assets` before the build commands in `build:preview`; Phase 3 replaces this command with the real asset preparation workflow.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Verify content rules in an integration test**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
it('contains approved identity and balanced evidence without prohibited surfaces', async () => {
|
||||||
|
const { content } = await loadRepositoryPreviewInputs();
|
||||||
|
expect(content.identity).toEqual(expect.objectContaining({
|
||||||
|
name: 'Luca Pîslaru',
|
||||||
|
role: 'System Administrator & Full-Stack Developer'
|
||||||
|
}));
|
||||||
|
expect(content.projects.selected.filter(p => p.visibility === 'private').every(p => !p.url)).toBe(true);
|
||||||
|
expect(JSON.stringify(content)).not.toMatch(/Arch Linux|CV download|heatmap|expert|percent/i);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `npm run test:unit -- tests/integration/public-content.test.ts`. Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit only the public projection and checklist**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add content/public config/site.preview.json scripts/materialize-empty-asset-set.mjs docs/content-approval-checklist.md tests/integration/public-content.test.ts package.json
|
||||||
|
git commit -m "content: add approved public portfolio projection"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Build the semantic Vue document and section components
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/app.ts`
|
||||||
|
- Create: `src/entry-client.ts`
|
||||||
|
- Create: `src/entry-server.ts`
|
||||||
|
- Create: `src/App.vue`
|
||||||
|
- Create: `src/components/AppShell.vue`
|
||||||
|
- Create: `src/components/SiteNavigation.vue`
|
||||||
|
- Create: `src/components/PortfolioSection.vue`
|
||||||
|
- Create: `src/components/ExperienceTimeline.vue`
|
||||||
|
- Create: `src/components/InfrastructureDiagram.vue`
|
||||||
|
- Create: `src/components/ProjectCaseStudy.vue`
|
||||||
|
- Create: `src/components/ProjectCard.vue`
|
||||||
|
- Create: `src/components/ToolkitGrid.vue`
|
||||||
|
- Create: `src/components/ContactDestination.vue`
|
||||||
|
- Test: `tests/unit/AppShell.test.ts`
|
||||||
|
- Test: `tests/unit/components.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: a readonly `ProductionInputs` injection.
|
||||||
|
- Produces: `createApp(inputs)`, `render(url, inputs)`, and semantic components with typed content props.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing SSR/component tests**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
it('renders one H1 and all canonical section headings', async () => {
|
||||||
|
const html = await render('/', validInputs);
|
||||||
|
expect(count(html, '<h1')).toBe(1);
|
||||||
|
for (const label of ['PROFILE', 'EXPERIENCE', 'INFRASTRUCTURE', 'SELECTED BUILDS', 'ADDITIONAL WORK', 'TOOLKIT', 'CONTACT']) {
|
||||||
|
expect(html).toContain(label);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders native links and a visible email without JavaScript', async () => {
|
||||||
|
const html = await render('/', validInputs);
|
||||||
|
expect(html).toContain('href="#contact"');
|
||||||
|
expect(html).toContain('href="mailto:');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the tests. Expected: FAIL because app modules do not exist.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Implement universal app creation and server rendering**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export function createApp(inputs: Readonly<ProductionInputs>) {
|
||||||
|
const app = createSSRApp(App, { inputs });
|
||||||
|
app.provide(productionInputsKey, inputs);
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function render(url: string, inputs: Readonly<ProductionInputs>) {
|
||||||
|
return renderToString(createApp(inputs));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`entry-client.ts` loads the identical serialized input snapshot and calls `.mount('#app')` to hydrate. It must not fetch copy after load.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement semantic sections**
|
||||||
|
|
||||||
|
Use `<header>`, `<nav aria-label="Portfolio sections">`, `<main>`, seven named content sections plus Start, and `<footer>`. `PortfolioSection` accepts `id`, `index`, `eyebrow`, and `heading`; it renders a real `<section aria-labelledby>` and no subject-matter default strings. The infrastructure diagram is inline semantic SVG with `role="img"`, a visible caption, and a text list equivalent.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Implement accessible project and Toolkit structures**
|
||||||
|
|
||||||
|
Primary builds use `<article>` and real heading levels. Private projects never render link containers. Toolkit icons are `aria-hidden="true"`; each visible label is announced once. Contact renders the visible email plus `mailto:` and verified social links with visible labels and safe `rel` attributes.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run unit tests and type checking**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run typecheck
|
||||||
|
npm run test:unit -- tests/unit/AppShell.test.ts tests/unit/components.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS with no hydration-only branch in server code.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit the semantic application**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/app.ts src/entry-client.ts src/entry-server.ts src/App.vue src/components tests/unit/AppShell.test.ts tests/unit/components.test.ts
|
||||||
|
git commit -m "feat: render semantic portfolio document"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Add the restrained responsive information layer
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/styles/tokens.css`
|
||||||
|
- Create: `src/styles/base.css`
|
||||||
|
- Create: `src/styles/layout.css`
|
||||||
|
- Create: `src/styles/components.css`
|
||||||
|
- Create: `src/styles/print.css`
|
||||||
|
- Modify: `src/App.vue`
|
||||||
|
- Test: `tests/browser/responsive-layout.spec.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: semantic class names and section IDs from Task 4.
|
||||||
|
- Produces: information-layer CSS that remains legible over a static background at 320–2560 CSS pixels.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing geometric browser assertions**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
for (const viewport of [{ width: 320, height: 800 }, { width: 768, height: 1024 }, { width: 1440, height: 900 }]) {
|
||||||
|
test(`no horizontal overflow at ${viewport.width}`, async ({ page }) => {
|
||||||
|
await page.setViewportSize(viewport);
|
||||||
|
await page.goto('/');
|
||||||
|
expect(await page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1)).toBe(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Define design tokens and base typography**
|
||||||
|
|
||||||
|
Use CSS custom properties for deep navy, near-black panels, off-white text, cyan technical accents, amber actions, focus ring, spacing, and type scale. Body copy uses a system/locally hosted neutral sans fallback. Route labels use a restrained monospace stack. Do not add body-text glow, glass blur, or scanlines to content.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement responsive section layouts**
|
||||||
|
|
||||||
|
At 1280px and wider, panels occupy 40–45% and alternate sides while leaving the road center unobstructed. At 768–1279px, panels use a stable two-column/centered rhythm. Below 768px, content becomes one column, decorative HUD density drops, and controls stay at least 44×44 CSS pixels.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add print and no-motion presentation**
|
||||||
|
|
||||||
|
The print stylesheet removes decorative world layers and interactive controls, expands content, preserves URLs where useful, and labels the output as a portfolio—not a résumé. `prefers-reduced-motion` removes non-essential transitions from the information layer.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run layout tests and inspect screenshots**
|
||||||
|
|
||||||
|
Run `npm run test:browser -- tests/browser/responsive-layout.spec.ts`. Expected: all viewports pass and Playwright screenshots show readable panels without horizontal overflow.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit the information layer**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/styles src/App.vue tests/browser/responsive-layout.spec.ts
|
||||||
|
git commit -m "feat: add responsive professional information layer"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Implement exact prerendering, metadata, and crawlable artifacts
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/security/serialize-json-ld.ts`
|
||||||
|
- Create: `src/content/metadata.ts`
|
||||||
|
- Create: `scripts/prerender.mjs`
|
||||||
|
- Create: `scripts/verify-artifacts.mjs`
|
||||||
|
- Modify: `index.html`
|
||||||
|
- Modify: `vite.config.ts`
|
||||||
|
- Test: `tests/unit/metadata.test.ts`
|
||||||
|
- Test: `tests/integration/prerender.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: server render entry, Vite client manifest, validated production inputs.
|
||||||
|
- Produces: `dist/index.html`, `dist/sitemap.xml`, `dist/robots.txt`, `dist/_headers.example`, and `dist/_redirects.example`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing metadata and raw-HTML tests**
|
||||||
|
|
||||||
|
Assert exact H1/role/title identity equality, one canonical URL, escaped JSON-LD, optional `sameAs`, production versus preview robots behavior, and presence of all section content before `<script type="module">`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
expect(html).toContain('<h1');
|
||||||
|
expect(html.indexOf('Luca Pîslaru')).toBeLessThan(html.indexOf('<script'));
|
||||||
|
expect(jsonLd.name).toBe('Luca Pîslaru');
|
||||||
|
expect(jsonLd.jobTitle).toBe('System Administrator & Full-Stack Developer');
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Implement safe metadata generation**
|
||||||
|
|
||||||
|
`serializeJsonLd(value)` uses `JSON.stringify()` then replaces `<` with `\u003c`, `>` with `\u003e`, `&` with `\u0026`, U+2028, and U+2029. Metadata derives name/role/description/social values from `PortfolioContent` and URLs from `SiteConfig`; it never reads environment variables for copy.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the Node ESM prerender script**
|
||||||
|
|
||||||
|
The script accepts exactly `--config <path>`, loads the four explicit input locations, imports `dist-ssr/entry-server.js`, renders `/`, reads `dist/client/.vite/manifest.json`, injects markup/preloads/metadata into the template, computes the JSON-LD CSP hash, and writes final output. Unknown CLI arguments or missing files fail with a non-zero exit.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Implement artifact verification**
|
||||||
|
|
||||||
|
`verify-artifacts.mjs` parses final HTML and fails if identity, any canonical section ID, visible email, infrastructure evidence, selected builds, Toolkit labels, canonical link, JSON-LD, sitemap, or robots file is absent; it also fails on source-map files and prohibited private-name fixtures.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run the complete preview build**
|
||||||
|
|
||||||
|
Run `npm run build:preview`. Expected: PASS and a complete static `dist/index.html` usable when client scripts are blocked.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit the prerender contract**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/security src/content/metadata.ts scripts index.html vite.config.ts tests/unit/metadata.test.ts tests/integration/prerender.test.ts
|
||||||
|
git commit -m "feat: prerender crawlable portfolio artifacts"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: Prove semantic, no-JavaScript, and keyboard usability
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `tests/browser/semantic.spec.ts`
|
||||||
|
- Create: `tests/browser/no-javascript.spec.ts`
|
||||||
|
- Create: `tests/browser/keyboard.spec.ts`
|
||||||
|
- Create: `tests/browser/hydration.spec.ts`
|
||||||
|
- Create: `tests/helpers/serve-dist.ts`
|
||||||
|
- Modify: `playwright.config.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: Phase 1 preview build.
|
||||||
|
- Produces: a release-blocking semantic/browser baseline for later phases.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write raw semantic assertions**
|
||||||
|
|
||||||
|
Verify one H1, logical headings, main/nav/footer landmarks, native anchor destinations, no private-project anchors, visible email, and complete text with JavaScript disabled.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Write keyboard traversal assertions**
|
||||||
|
|
||||||
|
Tab from the skip link through navigation and contact. Each focus indicator must be visible and unobscured. Activating a section link focuses its heading only after explicit navigation; manual page scrolling does not move focus.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Write hydration integrity assertions**
|
||||||
|
|
||||||
|
Capture console errors and DOM text before/after hydration. Fail on hydration mismatch, unhandled rejection, duplicate landmark, or subject-matter text mutation.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run all Phase 1 gates**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run typecheck
|
||||||
|
npm run test:unit
|
||||||
|
npm run build:preview
|
||||||
|
npm run test:browser -- tests/browser/semantic.spec.ts tests/browser/no-javascript.spec.ts tests/browser/keyboard.spec.ts tests/browser/hydration.spec.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: every command passes.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit the browser baseline**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add tests/browser tests/helpers playwright.config.ts
|
||||||
|
git commit -m "test: lock semantic and no-script portfolio behavior"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 1 completion checkpoint
|
||||||
|
|
||||||
|
Open `dist/index.html` with scripts disabled and confirm the full hiring-manager reading path, balanced systems/software evidence, and contact action. Do not begin Phase 2 until Luca approves this text-first build and its content contains no unapproved personal detail.
|
||||||
477
docs/superpowers/plans/2026-09-01-portfolio-phase-2-journey.md
Normal file
477
docs/superpowers/plans/2026-09-01-portfolio-phase-2-journey.md
Normal file
@@ -0,0 +1,477 @@
|
|||||||
|
# Portfolio Phase 2: Journey, Scene, and Audio 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:** Add the persistent chase-view night-drive experience, scroll-reactive route state, section signs, optional audio, and lifecycle-safe controls without changing the semantic reading path.
|
||||||
|
|
||||||
|
**Architecture:** Pure controllers transform native scroll and section geometry into serializable journey snapshots. Vue coordinates state and accessible controls; ordinary Three.js and Web Audio modules consume narrow interfaces and independently own their resources. Reduced motion, Static mode, or initialization failure leaves the Phase 1 document intact.
|
||||||
|
|
||||||
|
**Tech Stack:** Vue 3 Composition API, TypeScript, Three.js, Web Audio API, Vitest, Playwright
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-09-01-portfolio-journey-design.md`
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- Native scrolling, native anchor `href`s, and browser history remain authoritative.
|
||||||
|
- The scene never reads portfolio copy or owns semantic section state.
|
||||||
|
- One canvas, one current scene generation, one RAF loop, and one optional audio graph.
|
||||||
|
- Three.js loads dynamically only when motion policy permits.
|
||||||
|
- The car, road, terrain, sun, signs, particles, and audio are original/procedural.
|
||||||
|
- Section signs appear only for eligible forward passive transitions and are `aria-hidden`.
|
||||||
|
- AudioContext creation occurs only inside a direct visitor activation path.
|
||||||
|
- Every lifecycle edge is a discriminated transition covered by unit tests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Implement the normative scroll-drive controller
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/journey/types.ts`
|
||||||
|
- Create: `src/journey/scroll-drive-controller.ts`
|
||||||
|
- Create: `src/composables/useScrollDrive.ts`
|
||||||
|
- Test: `tests/unit/scroll-drive-controller.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `{ scrollY: number; now: number; maxScrollY: number }`.
|
||||||
|
- Produces: `ScrollDriveSnapshot`, `createScrollDriveController()`, and `useScrollDrive()`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing formula tests**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
it('falls back to cruise for an invalid sample', () => {
|
||||||
|
const controller = createScrollDriveController();
|
||||||
|
expect(controller.sample({ scrollY: NaN, now: 10, maxScrollY: 1000 }))
|
||||||
|
.toMatchObject({ driveSpeed: 0.18, acceleration: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets transient velocity after a gap over 250 ms', () => {
|
||||||
|
const controller = createScrollDriveController();
|
||||||
|
controller.sample({ scrollY: 0, now: 0, maxScrollY: 1000 });
|
||||||
|
controller.sample({ scrollY: 500, now: 100, maxScrollY: 1000 });
|
||||||
|
expect(controller.sample({ scrollY: 500, now: 351, maxScrollY: 1000 }).acceleration).toBe(0);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Add literal cases for `dt=0`, negative `dt`, Infinity, maximum up/down deltas, zero range, and fixed formula values.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify failure**
|
||||||
|
|
||||||
|
Run `npm run test:unit -- tests/unit/scroll-drive-controller.test.ts`. Expected: FAIL because the controller is absent.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the pure controller**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface ScrollDriveSnapshot {
|
||||||
|
scrollY: number;
|
||||||
|
routeProgress: number;
|
||||||
|
direction: -1 | 0 | 1;
|
||||||
|
acceleration: number;
|
||||||
|
driveSpeed: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScrollDriveController {
|
||||||
|
sample(input: ScrollDriveSample): ScrollDriveSnapshot;
|
||||||
|
reset(input?: ScrollDriveSample): ScrollDriveSnapshot;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use constants `2.5`, `90 ms`, `420 ms`, base `0.18`, and gap `>250 ms` exactly. Direction uses signed document delta; acceleration uses its absolute value.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add passive RAF-scheduled Vue integration**
|
||||||
|
|
||||||
|
`useScrollDrive()` attaches passive scroll/resize/visual-viewport/visibility listeners on mount, queues at most one RAF, resets after hidden gaps, and removes every listener/RAF on teardown.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Verify and commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run typecheck
|
||||||
|
npm run test:unit -- tests/unit/scroll-drive-controller.test.ts
|
||||||
|
git add src/journey/types.ts src/journey/scroll-drive-controller.ts src/composables/useScrollDrive.ts tests/unit/scroll-drive-controller.test.ts
|
||||||
|
git commit -m "feat: add deterministic scroll drive controller"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Implement active sections, sign latching, and navigation epochs
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/journey/section-controller.ts`
|
||||||
|
- Create: `src/journey/sign-controller.ts`
|
||||||
|
- Create: `src/journey/navigation-controller.ts`
|
||||||
|
- Create: `src/composables/useJourneySections.ts`
|
||||||
|
- Create: `src/composables/useKeyboardNavigation.ts`
|
||||||
|
- Modify: `src/components/SiteNavigation.vue`
|
||||||
|
- Test: `tests/unit/section-controller.test.ts`
|
||||||
|
- Test: `tests/unit/sign-controller.test.ts`
|
||||||
|
- Test: `tests/unit/navigation-controller.test.ts`
|
||||||
|
- Test: `tests/browser/navigation.spec.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: canonical section geometry, visual viewport height, scroll source/delta, and native link activations.
|
||||||
|
- Produces: `JourneySnapshot`, monotonic navigation epoch, one optional `SignSnapshot`, and exact URL/focus effects.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing section/sign table tests**
|
||||||
|
|
||||||
|
Test first/last boundaries, equal headings, the 35% activation line, document-end forcing, late growth, forward transitions, reverse suppression, multi-section jumps, and exact sign phase boundaries at `1.2vh`, `0.55vh`, `0.15vh`, and `-0.25vh`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
expect(selectActiveSection({ headings, activationY: 350, atDocumentEnd: false })).toBe('experience');
|
||||||
|
expect(nextSign(signState, { source: 'passive-scroll', deltaY: -2, distanceVh: 0.5 })).toBeNull();
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Implement pure section and sign controllers**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface SignSnapshot {
|
||||||
|
targetId: SectionId;
|
||||||
|
phase: 'approach' | 'stabilized' | 'pass';
|
||||||
|
progress: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JourneySnapshot extends ScrollDriveSnapshot {
|
||||||
|
activeSectionId: SectionId;
|
||||||
|
chapter: EnvironmentChapter;
|
||||||
|
sign: SignSnapshot | null;
|
||||||
|
arrived: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnvironmentChapter =
|
||||||
|
| 'deep-night'
|
||||||
|
| 'systems'
|
||||||
|
| 'infrastructure'
|
||||||
|
| 'builds'
|
||||||
|
| 'pre-dawn'
|
||||||
|
| 'destination';
|
||||||
|
```
|
||||||
|
|
||||||
|
Only one latch may exist. Semantic active section and decorative sign target remain separate.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Write failing navigation tests**
|
||||||
|
|
||||||
|
Cover different/current fragments, invalid fragments, exactly one/zero history mutation, wheel cancellation, passive `replaceState`, `popstate`, initial fragments, native `scrollend`, three-frame fallback, 1500 ms alignment, and focus only for a still-current explicit epoch.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Implement navigation ownership**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface NavigationController {
|
||||||
|
navigate(id: SectionId, target: HTMLElement): number;
|
||||||
|
noteUserScrollIntent(): number;
|
||||||
|
restore(id: SectionId, target: HTMLElement): number;
|
||||||
|
dispose(): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep native `href`s. Prevent default only after target validation. Wheel, touchmove, Page Up/Down, Home/End, Space, and arrow-key scroll intent cancel older settle/focus work.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Add measured Vue integration and browser coverage**
|
||||||
|
|
||||||
|
Measure headings after fonts, ResizeObserver, media dimensions, orientation, and visual-viewport changes. Browser tests assert URLs, focus, back/forward, direct fragments, and no focus theft from manual scrolling.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Verify and commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:unit -- tests/unit/section-controller.test.ts tests/unit/sign-controller.test.ts tests/unit/navigation-controller.test.ts
|
||||||
|
npm run test:browser -- tests/browser/navigation.spec.ts
|
||||||
|
git add src/journey src/composables/useJourneySections.ts src/composables/useKeyboardNavigation.ts src/components/SiteNavigation.vue tests/unit tests/browser/navigation.spec.ts
|
||||||
|
git commit -m "feat: coordinate sections signs and native navigation"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Implement motion policy and adaptive quality
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/journey/motion-policy.ts`
|
||||||
|
- Create: `src/journey/adaptive-quality.ts`
|
||||||
|
- Create: `src/composables/useMotionPreferences.ts`
|
||||||
|
- Create: `src/composables/useAdaptiveQuality.ts`
|
||||||
|
- Test: `tests/unit/motion-policy.test.ts`
|
||||||
|
- Test: `tests/unit/adaptive-quality.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: system preference, saved pause/quality, viewport/pointer class, visibility, scene state, and eligible frame intervals.
|
||||||
|
- Produces: `MotionPolicy`, maximum/actual `QualityTier`, and downgrade decisions.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing precedence and initial-tier tests**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
expect(resolveMotionPolicy({ systemReduce: true, savedPause: false, savedStatic: false })).toMatchObject({
|
||||||
|
allowSceneRequest: false,
|
||||||
|
maximumTier: 'static'
|
||||||
|
});
|
||||||
|
expect(initialMaximum({ width: 390, coarsePointer: true })).toBe('low');
|
||||||
|
expect(initialMaximum({ width: 1440, coarsePointer: false })).toBe('high');
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Implement the motion policy**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export type QualityTier = 'high' | 'medium' | 'low' | 'static';
|
||||||
|
export interface MotionPolicy {
|
||||||
|
allowSceneRequest: boolean;
|
||||||
|
allowAutomaticVideo: boolean;
|
||||||
|
maximumTier: QualityTier;
|
||||||
|
reason: 'full' | 'system-reduce' | 'visitor-pause' | 'saved-static' | 'data-saver';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Persist only pause, maximum quality, and volume.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Write failing adaptive epoch tests**
|
||||||
|
|
||||||
|
Cover two-second warm-up, five-second/120-sample windows, nearest-rank p95, `20/25/40 ms` thresholds, two bad Low windows, `>250 ms` same-epoch catastrophic interval, hidden/pause gap exclusion, 30-visible-second cooldown, resize reset, and no automatic upgrade.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Implement the adaptive controller**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface AdaptiveQualityController {
|
||||||
|
enterRunningEpoch(now: number): void;
|
||||||
|
leaveRunningEpoch(): void;
|
||||||
|
sampleFrame(start: number, end: number): QualityDecision | null;
|
||||||
|
setMaximum(tier: QualityTier, reason: 'resize' | 'manual'): QualityDecision | null;
|
||||||
|
snapshot(): AdaptiveQualitySnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QualityDecision {
|
||||||
|
previous: QualityTier;
|
||||||
|
next: QualityTier;
|
||||||
|
reason: 'catastrophic-frame' | 'p95-window' | 'two-bad-low-windows' | 'maximum-change';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdaptiveQualitySnapshot {
|
||||||
|
maximum: QualityTier;
|
||||||
|
actual: QualityTier;
|
||||||
|
epoch: number | null;
|
||||||
|
coolingDownUntil: number | null;
|
||||||
|
eligibleSamples: number;
|
||||||
|
consecutiveBadLowWindows: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Every epoch exit discards its RAF baseline and incomplete window.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Verify and commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:unit -- tests/unit/motion-policy.test.ts tests/unit/adaptive-quality.test.ts
|
||||||
|
git add src/journey/motion-policy.ts src/journey/adaptive-quality.ts src/composables/useMotionPreferences.ts src/composables/useAdaptiveQuality.ts tests/unit
|
||||||
|
git commit -m "feat: add motion policy and adaptive quality"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Build the lifecycle-safe Three.js engine
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/scene/types.ts`
|
||||||
|
- Create: `src/scene/resources.ts`
|
||||||
|
- Create: `src/scene/create-scene-controller.ts`
|
||||||
|
- Create: `src/scene/world/road.ts`
|
||||||
|
- Create: `src/scene/world/terrain.ts`
|
||||||
|
- Create: `src/scene/world/sun.ts`
|
||||||
|
- Create: `src/scene/world/car.ts`
|
||||||
|
- Create: `src/scene/world/sign.ts`
|
||||||
|
- Create: `src/scene/world/particles.ts`
|
||||||
|
- Create: `src/scene/world/environment.ts`
|
||||||
|
- Create: `src/scene/render-quality.ts`
|
||||||
|
- Test: `tests/unit/scene-lifecycle.test.ts`
|
||||||
|
- Test: `tests/unit/scene-resources.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: canvas, `JourneySnapshot`, `QualityTier`, and abort signal.
|
||||||
|
- Produces: `SceneController` and observable `SceneLifecycleState` without exposing Three.js objects.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing lifecycle tests with fake resources**
|
||||||
|
|
||||||
|
Cover every legal/illegal edge, generation invalidation, abort during loading, exactly-once disposal, context loss during loading/running/retained pause, hidden restore, five-second cutoff, bfcache freeze/restore, and terminal disposal.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
expect(() => machine.transition('disposed', 'running')).toThrow(/illegal transition/i);
|
||||||
|
expect(fakeRenderer.dispose).toHaveBeenCalledTimes(1);
|
||||||
|
expect(snapshot.canvasCount).toBe(1);
|
||||||
|
expect(snapshot.rafCount).toBeLessThanOrEqual(1);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Define state and resource ownership**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export type SceneLifecycleState =
|
||||||
|
| { kind: 'idle' }
|
||||||
|
| { kind: 'loading'; generation: number }
|
||||||
|
| { kind: 'running'; generation: number }
|
||||||
|
| { kind: 'paused'; generation: number | null; resumeNeedsLoad: boolean }
|
||||||
|
| { kind: 'lost'; lostAt: number; restorationPending: boolean }
|
||||||
|
| { kind: 'restoring'; generation: number; lostAt: number }
|
||||||
|
| { kind: 'static'; reason: string }
|
||||||
|
| { kind: 'disposed' };
|
||||||
|
```
|
||||||
|
|
||||||
|
`SceneResourceBag.dispose()` owns geometries, materials, textures, targets, passes, renderer, listeners, observers, timers, and RAF exactly once.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the procedural world**
|
||||||
|
|
||||||
|
Road markers scroll from drive distance/speed; restrained wire terrain frames the road; sun/haze evolve by chapter; the original non-branded car uses custom/primitive geometry with a continuous rear light; signs render canonical route labels to internal textures; particles use fixed pools capped by tier.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Implement quality and renderer ownership**
|
||||||
|
|
||||||
|
High/Medium/Low alter particle count, terrain segments, post-processing, and pixel ratio. `createSceneController()` owns one renderer/canvas binding and RAF. `update()` copies a serializable snapshot and never queries content DOM.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Implement context/page lifecycle behavior**
|
||||||
|
|
||||||
|
Attach context loss/restore, visibility, pagehide, and pageshow through the resource bag. Check generation and overlay before every async attach. Late/stale completions dispose themselves.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Verify and commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run typecheck
|
||||||
|
npm run test:unit -- tests/unit/scene-lifecycle.test.ts tests/unit/scene-resources.test.ts
|
||||||
|
git add src/scene tests/unit/scene-lifecycle.test.ts tests/unit/scene-resources.test.ts
|
||||||
|
git commit -m "feat: build procedural lifecycle-safe drive scene"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Integrate scene, HUD, poster, and motion controls
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/components/SceneCanvas.vue`
|
||||||
|
- Create: `src/components/JourneyHud.vue`
|
||||||
|
- Create: `src/components/DriveControls.vue`
|
||||||
|
- Create: `src/composables/useSceneController.ts`
|
||||||
|
- Create: `src/styles/world.css`
|
||||||
|
- Modify: `src/components/AppShell.vue`
|
||||||
|
- Modify: `src/App.vue`
|
||||||
|
- Test: `tests/unit/SceneCanvas.test.ts`
|
||||||
|
- Test: `tests/browser/scene-controls.spec.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: journey snapshot, motion policy, quality controller, scene factory.
|
||||||
|
- Produces: fixed decorative world, accessible controls, poster fallback, status text.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing lazy-load/control tests**
|
||||||
|
|
||||||
|
Assert no Three.js request in reduced motion/Static, exactly one canvas in full motion, visible Pause motion, quality maximum behavior, canvas absent from the accessibility tree, and a usable poster after failure.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Implement dynamic scene ownership**
|
||||||
|
|
||||||
|
`useSceneController()` imports the scene only when `allowSceneRequest` is true, binds abort/generation tokens, and disposes once on unmount, non-persisted pagehide, or Static transition.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement scene host and HUD**
|
||||||
|
|
||||||
|
Expose one visually hidden scene description from content. Canvas, car, signs, and visual HUD are `aria-hidden`. The poster has intrinsic dimensions and empty alt because the adjacent description owns meaning. HUD reads route percentage/current section/quality and `ARRIVED` at Contact.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Implement controls**
|
||||||
|
|
||||||
|
Use real buttons and a named native quality control. Exact names are Pause motion, Resume motion, Enable motion, and Quality. Static/unavailable state uses visible text and not color alone.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Verify and commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:unit -- tests/unit/SceneCanvas.test.ts
|
||||||
|
npm run test:browser -- tests/browser/scene-controls.spec.ts
|
||||||
|
git add src/components src/composables/useSceneController.ts src/styles/world.css src/App.vue tests
|
||||||
|
git commit -m "feat: integrate accessible journey world and controls"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Build the opt-in generated audio engine
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/audio/types.ts`
|
||||||
|
- Create: `src/audio/create-audio-controller.ts`
|
||||||
|
- Create: `src/audio/synth-engine.ts`
|
||||||
|
- Create: `src/composables/useAudioEngine.ts`
|
||||||
|
- Modify: `src/components/DriveControls.vue`
|
||||||
|
- Test: `tests/unit/audio-lifecycle.test.ts`
|
||||||
|
- Test: `tests/browser/audio-controls.spec.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: direct activation, volume/mute input, smoothed drive intensity, visibility/lifecycle events.
|
||||||
|
- Produces: `AudioController` and `AudioUiState`; Vue never receives AudioContext or nodes.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing audio lifecycle/race tests**
|
||||||
|
|
||||||
|
Cover idle/start/running/suspended/user-required/failed/disposed, double activation, quick hide/show while starting, interruption while starting, mute/volume, one graph, rejection recovery, bfcache, and exactly-once close.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
expect(fakeContext.constructed).toBe(0);
|
||||||
|
await controller.startFromGesture();
|
||||||
|
expect(fakeContext.constructed).toBe(1);
|
||||||
|
await controller.startFromGesture();
|
||||||
|
expect(fakeContext.constructed).toBe(1);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Implement idempotent synthesis and state**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export type AudioUiState =
|
||||||
|
| { kind: 'idle' }
|
||||||
|
| { kind: 'starting' }
|
||||||
|
| { kind: 'running'; muted: boolean; volume: number }
|
||||||
|
| { kind: 'suspended'; resumeRequiresGesture: boolean }
|
||||||
|
| { kind: 'failed'; message: string }
|
||||||
|
| { kind: 'disposed' };
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a restrained generated engine/road bed from oscillators, filtered noise, gain, and dynamics. Default volume is `0.4`; clamp `[0,1]`; smooth intensity; create/connect nodes once per generation.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Integrate accessible controls**
|
||||||
|
|
||||||
|
Before activation show START THE DRIVE and Enables the interactive soundtrack. Afterwards show Mute sound/Unmute sound and a visible native range named Sound volume. Failures show concise recovery text and never obscure content.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Verify direct-gesture creation and commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:unit -- tests/unit/audio-lifecycle.test.ts
|
||||||
|
npm run test:browser -- tests/browser/audio-controls.spec.ts
|
||||||
|
git add src/audio src/composables/useAudioEngine.ts src/components/DriveControls.vue tests
|
||||||
|
git commit -m "feat: add opt-in procedural drive audio"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: Lock journey integration and static behavior
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `tests/browser/journey-integration.spec.ts`
|
||||||
|
- Create: `tests/browser/reduced-motion.spec.ts`
|
||||||
|
- Create: `tests/browser/static-fallback.spec.ts`
|
||||||
|
- Create: `tests/browser/bfcache-lifecycle.spec.ts`
|
||||||
|
- Create: `tests/helpers/scene-instrumentation.ts`
|
||||||
|
- Modify: `scripts/verify-artifacts.mjs`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: completed Phase 2 app.
|
||||||
|
- Produces: release-blocking synchronized-state and resource-count assertions.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Test one-source journey synchronization**
|
||||||
|
|
||||||
|
Scroll all sections and assert active navigation, HUD, route progress, environment chapter, and Contact arrival update from one snapshot within one RAF. Visible signs must match the independent latch oracle.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Test reduced motion before boot**
|
||||||
|
|
||||||
|
Emulate reduced motion, clear storage, load, and assert no Three.js request, canvas, automatic video source, or sign; require poster, semantic HUD, navigation, and contact.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Test Static and failure fallback**
|
||||||
|
|
||||||
|
Block the scene chunk and inject shader/context failures. Assert one poster, zero scene RAF, no unhandled rejection, and full navigation/contact. Enable motion retries only when system reduction is absent.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Test freeze/restore counts**
|
||||||
|
|
||||||
|
Exercise persisted pagehide/pageshow across idle/loading/running/paused/lost/restoring/static. Assert at most one current generation, canvas, RAF loop, and audio graph.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run the phase gate and commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run typecheck
|
||||||
|
npm run test:unit
|
||||||
|
npm run build:preview
|
||||||
|
npm run test:browser -- tests/browser/journey-integration.spec.ts tests/browser/reduced-motion.spec.ts tests/browser/static-fallback.spec.ts tests/browser/bfcache-lifecycle.spec.ts
|
||||||
|
git add tests/browser tests/helpers/scene-instrumentation.ts scripts/verify-artifacts.mjs
|
||||||
|
git commit -m "test: lock journey lifecycle and static fallback"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 2 completion checkpoint
|
||||||
|
|
||||||
|
Review desktop, tablet, mobile, reduced-motion, and Static captures. Approve only if the world is memorable while the information layer remains quiet, professional, readable, and dominant enough for hiring decisions.
|
||||||
351
docs/superpowers/plans/2026-09-01-portfolio-phase-3-quality.md
Normal file
351
docs/superpowers/plans/2026-09-01-portfolio-phase-3-quality.md
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
290
docs/superpowers/plans/2026-09-01-portfolio-phase-4-release.md
Normal file
290
docs/superpowers/plans/2026-09-01-portfolio-phase-4-release.md
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
# Portfolio Phase 4: Security, Packaging, and Deployment 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:** Produce a reproducible, privacy-cleared, signed static release and verify an atomic Nginx deployment with correct security headers, caching, metadata, rollback, and post-launch SEO setup.
|
||||||
|
|
||||||
|
**Architecture:** Source control remains private for the initial launch, so the publication set contains one public deployment target rather than a public Git mirror. Canonical digests bind the four inputs and final artifacts; isolated scan/reconciliation jobs produce a signed release manifest. The server stages immutable release directories and atomically switches one `current` symlink only after local and remote verification.
|
||||||
|
|
||||||
|
**Tech Stack:** Node.js crypto, RFC 8785 canonical JSON, npm audit/license tooling, CycloneDX, containerized privacy workers, Nginx, SSH/rsync after explicit deployment authorization, Playwright
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-09-01-portfolio-journey-design.md`
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- No remote, domain, server, DNS, Search Console, or public-repository mutation occurs without Luca’s explicit approval at the deployment checkpoint.
|
||||||
|
- Production configuration contains only `environment`, canonical HTTPS URL, and immutable build ID.
|
||||||
|
- Release/signing keys, issuer state, denylist, approval ledger, original sources, and deployment credentials live outside the repository and build output.
|
||||||
|
- Every signed object uses canonical JSON, lowercase SHA-256, explicit schema version, scoped key ID, and issuer epoch.
|
||||||
|
- A scan or signature failure leaves the previous deployed release untouched.
|
||||||
|
- The launch source repository stays private; no public Git mirror or source map is published.
|
||||||
|
- Production deployment is one atomic Nginx release-pointer change with a recorded predecessor and tested rollback.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Generate and test the static-host security contract
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/security/security-policy.ts`
|
||||||
|
- Create: `scripts/generate-host-config.mjs`
|
||||||
|
- Create: `nginx.portfolio.conf.example`
|
||||||
|
- Create: `tests/unit/security-policy.test.ts`
|
||||||
|
- Create: `tests/integration/nginx-config.test.ts`
|
||||||
|
- Modify: `scripts/prerender.mjs`
|
||||||
|
- Modify: `scripts/verify-artifacts.mjs`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: validated `SiteConfig`, JSON-LD CSP hash, hashed asset manifest.
|
||||||
|
- Produces: exact CSP/header map, Nginx include, redirect rules, cache rules, and preview noindex rules.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing policy tests**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
expect(productionHeaders.csp).toContain("default-src 'self'");
|
||||||
|
expect(productionHeaders.csp).toContain("connect-src 'none'");
|
||||||
|
expect(productionHeaders.csp).toContain("object-src 'none'");
|
||||||
|
expect(productionHeaders.csp).toContain("frame-ancestors 'none'");
|
||||||
|
expect(productionHeaders.referrerPolicy).toBe('no-referrer');
|
||||||
|
expect(previewHeaders.xRobotsTag).toBe('noindex, nofollow, noarchive');
|
||||||
|
```
|
||||||
|
|
||||||
|
Also reject wildcard source expressions, `unsafe-inline`, external scripts/fonts/media, public source maps, and production sitemap output in preview mode.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Implement the exact policy builder**
|
||||||
|
|
||||||
|
Use:
|
||||||
|
|
||||||
|
```text
|
||||||
|
default-src 'self'; script-src 'self' <JSON-LD-hash>; style-src 'self';
|
||||||
|
img-src 'self' data:; media-src 'self'; font-src 'self'; connect-src 'none';
|
||||||
|
object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self';
|
||||||
|
upgrade-insecure-requests
|
||||||
|
```
|
||||||
|
|
||||||
|
Set HSTS `max-age=31536000` after HTTPS verification, `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`, `X-Frame-Options: DENY`, and least-privilege Permissions Policy. Do not add `includeSubDomains` until all subdomains are audited for HTTPS.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Generate Nginx cache/range/redirect rules**
|
||||||
|
|
||||||
|
Hashed JS/CSS/assets receive `public, max-age=31536000, immutable`; HTML/robots/sitemap receive revalidation; correct MIME types and byte ranges remain enabled for MP4/WebM. HTTP and alternate host redirect to the canonical HTTPS host in one permanent hop.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Validate in a local Nginx container**
|
||||||
|
|
||||||
|
Run an unprivileged Nginx container mounting `dist/` read-only and the generated config. Test `nginx -t`, headers, redirect simulation, cache headers, content types, and `Range: bytes=0-1023` video response.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit the hosting contract**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:unit -- tests/unit/security-policy.test.ts tests/integration/nginx-config.test.ts
|
||||||
|
git add src/security scripts/generate-host-config.mjs scripts/prerender.mjs scripts/verify-artifacts.mjs nginx.portfolio.conf.example tests
|
||||||
|
git commit -m "feat: define secure static hosting contract"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Implement canonical provenance and approval verification
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/security/canonical-digest.ts`
|
||||||
|
- Create: `src/security/approval-envelope.ts`
|
||||||
|
- Create: `src/security/approval-closure.ts`
|
||||||
|
- Create: `src/security/production-input-root.ts`
|
||||||
|
- Create: `src/security/release-manifest.ts`
|
||||||
|
- Create: `scripts/release/verify-approvals.mjs`
|
||||||
|
- Test: `tests/unit/canonical-digest.test.ts`
|
||||||
|
- Test: `tests/unit/approval-envelope.test.ts`
|
||||||
|
- Test: `tests/unit/approval-closure.test.ts`
|
||||||
|
- Test: `tests/integration/release-manifest.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: four public inputs, external signed approval envelopes, protected ledger head/revision, scoped public keys.
|
||||||
|
- Produces: `approvedRecordRoot`, `approvedAssetRoot`, `approvalProofSetDigest`, `productionInputRoot`, and `ApprovalProjectionAttestation` verification result.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Install and pin canonical/SBOM tooling**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install canonicalize
|
||||||
|
npm install --save-dev @cyclonedx/cyclonedx-npm license-checker-rseidelsohn
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Write independent canonical vectors first**
|
||||||
|
|
||||||
|
Tests contain fixed canonical bytes and expected SHA-256 values for public records, asset records, signed envelopes, proof set, PreparedAssetSet member index, site config, and four-input root. Reject duplicate JSON keys, unknown fields, unpaired surrogates, non-NFC strings/paths, alternate base64/signature encodings, duplicate IDs, case-fold path collisions, and extra files.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement canonical digest primitives**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export function canonicalJsonDigest(schema: string, value: unknown): Sha256Hex;
|
||||||
|
export function byteDigest(bytes: Uint8Array): Sha256Hex;
|
||||||
|
export function verifyEd25519Envelope<T>(envelope: SignedEnvelope<T>, trust: TrustPolicy): T;
|
||||||
|
```
|
||||||
|
|
||||||
|
Use RFC 8785 bytes and lowercase 64-character hexadecimal SHA-256. Signature payloads include key ID, issuer epoch, scope, issued/expiry timestamps, and policy version.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Implement exact closure/root verification**
|
||||||
|
|
||||||
|
Reproduce the spec’s tuple ordering and schemas for content, assets, proof set, PreparedAssetSet, and four fixed labeled production inputs. Require one current valid approval for every emitted record and asset. Protected ledger equality and trusted current time are mandatory inputs, not advisory metadata.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Implement release-manifest verification**
|
||||||
|
|
||||||
|
The manifest binds all four input digests/root, every built/precompressed artifact hash, source commit/tree when available, exact deploy file set, approval roots/proof, scan evidence, policy/tool versions, predecessor manifest, issue time, and minimum expiry. It cannot name a public Git target in the initial launch mode.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Run and commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:unit -- tests/unit/canonical-digest.test.ts tests/unit/approval-envelope.test.ts tests/unit/approval-closure.test.ts tests/integration/release-manifest.test.ts
|
||||||
|
git add src/security scripts/release package.json package-lock.json tests
|
||||||
|
git commit -m "feat: verify canonical portfolio release provenance"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Build isolated privacy reconciliation and release signing
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `containers/privacy-worker-a/Dockerfile`
|
||||||
|
- Create: `containers/privacy-worker-b/Dockerfile`
|
||||||
|
- Create: `containers/privacy-matcher/Dockerfile`
|
||||||
|
- Create: `containers/privacy-coordinator/Dockerfile`
|
||||||
|
- Create: `scripts/release/enumerate-closure.mjs`
|
||||||
|
- Create: `scripts/release/run-privacy-workers.mjs`
|
||||||
|
- Create: `scripts/release/reconcile-scan.mjs`
|
||||||
|
- Create: `scripts/release/sign-release.mjs`
|
||||||
|
- Create: `scripts/release/trust-policy.schema.json`
|
||||||
|
- Test: `tests/security/privacy-workers.test.ts`
|
||||||
|
- Test: `tests/security/release-signing.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: read-only build/source closure, external denylist capability, external trust policy/keys.
|
||||||
|
- Produces: immutable per-digest worker reports, matcher results, scan-evidence summary, workload attestation, and signed release manifest.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write compromise and closure tests**
|
||||||
|
|
||||||
|
Cover a schema-valid false pass from one authenticated worker, mismatched normalized output, missing/extra/reordered reports, unsupported/encrypted/malformed containers, nested archive limits, archive traversal/symlink/device entries, OCR/QR canaries, stale ledger head, expired approval without ledger append, forged issuer epoch, and substituted artifact.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Implement independent workers and matcher**
|
||||||
|
|
||||||
|
Workers use different images/toolchains, receive one digest/object at a time, run networkless/read-only/non-root with CPU/memory/time limits, and emit immutable typed reports bound to input/check/normalized output/image. The matcher compares agreed normalized evidence and applies rule IDs; disagreement fails.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement trusted reconciliation**
|
||||||
|
|
||||||
|
The coordinator independently enumerates every Git/build/precompressed/release object applicable to the private source deployment, requires both worker reports plus matcher result per check, rejects any coverage mismatch, and emits canonical evidence/candidate objects to content-addressed write-once storage.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Implement scoped signing**
|
||||||
|
|
||||||
|
`sign-release.mjs` receives canonical candidate/evidence bytes through a read-only digest interface, verifies approval projection, privacy workload, policy, tool provenance, ledger, expiry, predecessor, and all semantic fields, then signs exact candidate bytes with an external non-exportable or read-only mounted Ed25519 key. Scanner/coordinator processes never receive the release key.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Test every trust-root compromise response**
|
||||||
|
|
||||||
|
Exercise approval, projection, privacy workload, policy attestation, tool provenance, release signing, and publisher credential epochs. Old epochs fail; approval compromise requires checkpoint audit/reapproval; policy/tool compromise invalidates dependents and forces fresh scan; publisher compromise advances the fence.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Run and commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:unit -- tests/security/privacy-workers.test.ts tests/security/release-signing.test.ts
|
||||||
|
git add containers scripts/release tests/security
|
||||||
|
git commit -m "feat: isolate privacy scanning and release signing"
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not commit keys, attestations containing private evidence, denylist contents, or scanner payload logs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Package an atomic, reversible Nginx release
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `scripts/build-release.mjs`
|
||||||
|
- Create: `scripts/deploy/prepare-release.sh`
|
||||||
|
- Create: `scripts/deploy/activate-release.sh`
|
||||||
|
- Create: `scripts/deploy/rollback-release.sh`
|
||||||
|
- Create: `scripts/deploy/verify-staged-release.mjs`
|
||||||
|
- Create: `docs/deployment/nginx-runbook.md`
|
||||||
|
- Test: `tests/integration/release-package.test.ts`
|
||||||
|
- Test: `tests/integration/atomic-deploy.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: signed manifest, exact `dist/` closure, current release pointer/fence.
|
||||||
|
- Produces: immutable release directory `<buildId>-<manifestDigest>`, readiness receipt, atomic `current` pointer, rollback receipt.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing package and transaction tests**
|
||||||
|
|
||||||
|
Test extra/substituted files, invalid signature, stale predecessor, stale fence, crash before pointer swap, crash after pointer swap, concurrent activation, expiry crossing, rollback with a newly signed manifest, and orphan-stage cleanup.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Build the release package**
|
||||||
|
|
||||||
|
`build-release.mjs` copies only manifest-listed files, verifies hashes, writes the signed manifest and SBOM, creates deterministic tar ordering/timestamps, and verifies the archive by extracting into a temporary read-only test root.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement server-side staging**
|
||||||
|
|
||||||
|
`prepare-release.sh` uses fixed server paths supplied as arguments, creates a new immutable release directory, verifies manifest/signature/hashes/time, applies least-privilege ownership, runs the staged smoke check through an internal Nginx location, and writes a readiness receipt. It never changes `current`.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Implement fenced atomic activation**
|
||||||
|
|
||||||
|
`activate-release.sh` obtains an exclusive lock, compares expected current manifest/fence, rechecks expiry/readiness, increments the fence, creates `current.next` symlink to the staged directory, and atomically renames it over `current`. A failed check leaves the old symlink untouched. Receipts record predecessor/new digest and fence.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Implement signed rollback**
|
||||||
|
|
||||||
|
Rollback selects an existing immutable release but first creates/verifies a new signed rollback manifest against the current approval/ledger state. Activation uses the same fenced symlink swap; an old manifest is never replayed.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Verify locally and commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:unit -- tests/integration/release-package.test.ts tests/integration/atomic-deploy.test.ts
|
||||||
|
git add scripts/build-release.mjs scripts/deploy docs/deployment tests/integration
|
||||||
|
git commit -m "feat: package atomic reversible nginx releases"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Run supply-chain, deployment, and SEO verification
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `scripts/supply-chain-check.mjs`
|
||||||
|
- Create: `scripts/smoke-production.mjs`
|
||||||
|
- Create: `tests/deployment/headers.spec.ts`
|
||||||
|
- Create: `tests/deployment/seo.spec.ts`
|
||||||
|
- Create: `tests/deployment/no-javascript.spec.ts`
|
||||||
|
- Create: `docs/deployment/release-checklist.md`
|
||||||
|
- Create: `docs/deployment/rollback-record-template.md`
|
||||||
|
- Modify: `package.json`
|
||||||
|
- Modify: `.gitignore`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: canonical public URL and deployed signed manifest digest.
|
||||||
|
- Produces: SBOM/license/vulnerability evidence, deployed smoke report, SEO validation, rollback record, and Search Console action record.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Implement clean supply-chain verification**
|
||||||
|
|
||||||
|
Use `npm ci --ignore-scripts` by default, explicitly run only approved package scripts in a credentialless sandbox, verify lockfile cleanliness, run production/development vulnerability and license checks, generate CycloneDX SBOM, and fail on unsigned/unverifiable tool images or Critical/High findings. Medium findings require a committed triage record before release.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Write deployed response tests**
|
||||||
|
|
||||||
|
Assert canonical HTTPS 200, one-hop alternate redirects, CSP/HSTS/nosniff/referrer/permissions/frame headers, HTML revalidation, immutable assets, Brotli/Gzip, MIME, video ranges, canonical/OG/JSON-LD/robots/sitemap consistency, JavaScript-disabled content, and no source maps/private identifiers.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Build and verify the production artifact locally**
|
||||||
|
|
||||||
|
After Luca supplies the approved canonical host, remove `config/site.production.json` from `.gitignore`, create it with exactly `environment`, approved canonical host, and immutable build ID, then run the complete build, privacy scan, signing, package, local Nginx smoke, browser matrix, performance gate, and accessibility gate. Any failure blocks remote work.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Stop for explicit remote-deployment authorization**
|
||||||
|
|
||||||
|
Present the signed manifest digest, release size, local test summary, target server path, planned canonical host, exact activation command, and rollback predecessor. Do not connect to or mutate the server until Luca explicitly authorizes this release.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Stage, verify, and atomically activate after authorization**
|
||||||
|
|
||||||
|
Transfer the immutable package, run server-side preparation/readiness, execute the fenced pointer switch, then run `smoke-production.mjs` against the public URL. If smoke fails, use the newly signed rollback path and record the outcome.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Complete post-launch SEO actions**
|
||||||
|
|
||||||
|
Verify preferred-host redirects and indexability again, add the property in Search Console, submit the exact production sitemap, request indexing for the canonical home page, and record the submission date. Do not promise first-place ranking; monitor branded-name indexing separately.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit non-secret release evidence**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add scripts/supply-chain-check.mjs scripts/smoke-production.mjs tests/deployment docs/deployment package.json package-lock.json config/site.production.json
|
||||||
|
git commit -m "release: verify production portfolio deployment"
|
||||||
|
```
|
||||||
|
|
||||||
|
If `config/site.production.json` contains only the approved public host/build ID, it may be committed; credentials and private server paths remain outside Git.
|
||||||
|
|
||||||
|
## Phase 4 completion checkpoint
|
||||||
|
|
||||||
|
The project is complete only when the signed deployed digest matches the locally approved artifact, the public smoke/SEO/accessibility checks pass, the previous release remains recoverable, Search Console submission is recorded, and no private source or credential has entered the repository or public output.
|
||||||
1070
docs/superpowers/specs/2026-09-01-portfolio-journey-design.md
Normal file
1070
docs/superpowers/specs/2026-09-01-portfolio-journey-design.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user