Files
Portfolio/docs/superpowers/plans/2026-09-01-portfolio-phase-1-foundation.md

577 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.