diff --git a/.github/workflows/pages-smoke.yml b/.github/workflows/pages-smoke.yml index 9aef67e..aefe593 100644 --- a/.github/workflows/pages-smoke.yml +++ b/.github/workflows/pages-smoke.yml @@ -18,3 +18,10 @@ jobs: test -f video-engineer.html grep -qi '' index.html ! grep -R -n -E '^(<<<<<<<|=======|>>>>>>>)' index.html script-writing-machine.html video-engineer.html + + - uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Validate public registry and embedded fallbacks + run: npm run check-manifest diff --git a/CLAUDE.md b/CLAUDE.md index e1b0927..6f48fdf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,7 +21,7 @@ GitHub Pages org site for DaveHomeAssist. Contains the project hub (index.html), ## Manifest Sync -`project-manifest.json` is the single source of truth for the project list. Both `private-hub.html` and `index.html` embed a `FALLBACK_MANIFEST` block so the hubs still render if the fetch fails. After editing `project-manifest.json`, run `npm run sync-manifest` to propagate changes to both hubs' embedded fallbacks. Never edit the `FALLBACK_MANIFEST` blocks directly. +`project-manifest.json` is the single source of truth for the project list. `index.html`, `public-hub.html` and `private-hub.html` each embed a `FALLBACK_MANIFEST` block so the hubs still render if the fetch fails. After editing `project-manifest.json`, run `npm run sync-manifest` to propagate changes to every embedded fallback (the target list lives in `scripts/sync-manifest.mjs`; add any new hub page there). Never edit the `FALLBACK_MANIFEST` blocks directly. `node scripts/sync-manifest.mjs --check` reports drift without writing. ## Documentation Maintenance @@ -36,7 +36,9 @@ GitHub Pages org site for DaveHomeAssist. Contains the project hub (index.html), | 002 | P1 | resolved | Video engineer LinkedIn URL points to generic linkedin.com | Fixed: href updated to https://www.linkedin.com/in/daverobertson93/ | | 003 | P2 | obsolete | Elysium nav links hidden on screens under 480px with no fallback | Elysium pages removed from repo; issue no longer applies | | 004 | P2 | obsolete | Elysium back link uses inline onmouseover/onmouseout | Elysium pages removed from repo; issue no longer applies | -| 005 | P2 | open | Video engineer page has no back link to main portfolio | Users cannot navigate back to the hub | +| 005 | P2 | resolved | Video engineer page has no back link to main portfolio | Fixed 2026-07-11 in 93ca6c6: "Back to Portfolio" link added to video-engineer.html | +| 009 | P0 | resolved | private-hub.html threw ReferenceError on load (loadManifest removed in e21df48) | Fixed 2026-09-10: loader, validateManifest and manifestError restored | +| 010 | P1 | resolved | public-hub.html FALLBACK_MANIFEST was not covered by sync-manifest and drifted to the 2026-04-12 registry | Fixed 2026-09-10: added to sync targets, regenerated, drift check gates CI | | 006 | P1 | resolved | Public manifest exposed 11 more private repo URLs plus localPath/runCommand values | Fixed 2026-07-03: blanked 10 private URLs, repointed BPMDelayCalc to public bpm-delay-calculator, cleared all localPath/runCommand | | 007 | P2 | resolved | private-hub.html was indexable and listed in sitemap.xml | Fixed 2026-07-03: robots meta set to noindex,nofollow; removed from sitemap | | 008 | P2 | resolved | index.html footer linked to missing archives/index-v1.html (live 404) | Fixed 2026-07-03: link removed | diff --git a/package.json b/package.json index cef3b51..68b926a 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,8 @@ "type": "module", "scripts": { "validate-manifest": "node scripts/validate-public-manifest.mjs", - "sync-manifest": "npm run validate-manifest && node scripts/sync-manifest.mjs" + "check-manifest": "npm run validate-manifest && node scripts/sync-manifest.mjs --check", + "sync-manifest": "npm run validate-manifest && node scripts/sync-manifest.mjs", + "test": "npm run check-manifest" } } diff --git a/private-hub.html b/private-hub.html index c9f9798..6b956fa 100644 --- a/private-hub.html +++ b/private-hub.html @@ -947,8 +947,8 @@ <h2 id="drawerTitle">Project details</h2> <script> // ── Manifest loader ── -// In production: fetch('project-manifest.json').then(...) -// For now, inline: +// project-manifest.json is fetched in loadManifest(); this embedded copy is the +// offline fallback and is regenerated by scripts/sync-manifest.mjs. Do not edit it. const FALLBACK_MANIFEST = { "meta": { "owner": "DaveHomeAssist", @@ -2207,6 +2207,72 @@ <h2 id="drawerTitle">Project details</h2> } }; +let manifestData = FALLBACK_MANIFEST; + +let manifestError = null; + +function validateManifest(data) { + const errors = []; + const warnings = []; + if (!data || typeof data !== 'object') { + errors.push('Manifest is not an object'); + return { ok: false, errors, warnings }; + } + if (!data.meta || typeof data.meta !== 'object') { + errors.push('Missing meta object'); + } else if (!data.meta.owner) { + errors.push('Missing meta.owner'); + } + if (!Array.isArray(data.projects)) { + errors.push('projects is not an array'); + } else { + data.projects.forEach((p, i) => { + if (!p || typeof p !== 'object') { + errors.push(`projects[${i}] is not an object`); + return; + } + if (!p.id) errors.push(`projects[${i}] missing id`); + if (!p.name) errors.push(`projects[${i}] missing name`); + if (!p.status) warnings.push(`projects[${i}] (${p.id || '?'}) missing status`); + if (!p.category) warnings.push(`projects[${i}] (${p.id || '?'}) missing category`); + if (!p.description) warnings.push(`projects[${i}] (${p.id || '?'}) missing description`); + if (!Array.isArray(p.tags)) warnings.push(`projects[${i}] (${p.id || '?'}) tags is not an array`); + }); + } + if (!data.categories || typeof data.categories !== 'object') { + warnings.push('Missing categories map'); + } + if (!data.hostingPlatforms || typeof data.hostingPlatforms !== 'object') { + warnings.push('Missing hostingPlatforms map'); + } + return { ok: errors.length === 0, errors, warnings }; +} + +async function loadManifest() { + try { + const response = await fetch('./project-manifest.json', { cache: 'no-store' }); + if (!response.ok) { + throw new Error(`Manifest request failed with ${response.status}`); + } + const data = await response.json(); + const result = validateManifest(data); + if (!result.ok) { + console.error('Manifest validation failed:', result.errors); + manifestError = 'Manifest invalid, using fallback'; + manifestData = FALLBACK_MANIFEST; + return; + } + if (result.warnings.length) { + console.warn('Manifest validation warnings:', result.warnings); + } + manifestData = data; + } catch (error) { + console.warn('Using embedded project manifest fallback.', error); + manifestError = 'Manifest failed to load, using fallback'; + manifestData = FALLBACK_MANIFEST; + } +} + const STORAGE_KEY = 'private-hub-state'; const DEFAULT_STATE = { search: '', diff --git a/public-hub.html b/public-hub.html index 03abbf4..5d6166c 100644 --- a/public-hub.html +++ b/public-hub.html @@ -341,7 +341,7 @@ <h1 class="hero-name">Dave Robertson</h1> "meta": { "owner": "DaveHomeAssist", "github": "https://github.com/DaveHomeAssist", - "lastUpdated": "2026-04-12", + "lastUpdated": "2026-08-23", "hub": "https://davehomeassist.github.io" }, "projects": [ @@ -349,7 +349,7 @@ <h1 class="hero-name">Dave Robertson</h1> "id": "davehomeassist-hub", "name": "DaveHomeAssist Hub", "description": "Central portfolio and project directory", - "category": "infrastructure", + "category": "portfolio-hub", "status": "active", "visibility": "public", "featured": false, @@ -358,28 +358,35 @@ <h1 class="hero-name">Dave Robertson</h1> "repo": "https://github.com/DaveHomeAssist/DaveHomeAssist.github.io", "localPath": "", "runCommand": "", - "tags": ["hub", "portfolio"] + "tags": [ + "hub", + "portfolio" + ] }, { "id": "trailkeeper", "name": "Trailkeeper", "description": "Trail tracking and outdoor activity logger", - "category": "outdoor", + "category": "garden-outdoor", "status": "active", "visibility": "public", - "featured": true, + "featured": false, "hosting": "github-pages", "url": "https://davehomeassist.github.io/trailkeeper", "repo": "https://github.com/DaveHomeAssist/trailkeeper", "localPath": "", "runCommand": "", - "tags": ["hiking", "tracking", "outdoor"] + "tags": [ + "hiking", + "tracking", + "outdoor" + ] }, { "id": "garden-os", "name": "Garden OS", "description": "Garden management OS with Beds, Planner, Doctor, and Journal workflows", - "category": "outdoor", + "category": "garden-outdoor", "status": "active", "visibility": "public", "featured": true, @@ -388,13 +395,17 @@ <h1 class="hero-name">Dave Robertson</h1> "repo": "https://github.com/DaveHomeAssist/garden-os", "localPath": "", "runCommand": "", - "tags": ["garden", "planner", "outdoor"] + "tags": [ + "garden", + "planner", + "outdoor" + ] }, { "id": "garden-league-simulator", "name": "Garden League Simulator", "description": "Competitive garden league game simulator", - "category": "outdoor", + "category": "garden-outdoor", "status": "active", "visibility": "public", "featured": false, @@ -403,14 +414,18 @@ <h1 class="hero-name">Dave Robertson</h1> "repo": "https://github.com/DaveHomeAssist/garden-os", "localPath": "", "runCommand": "", - "tags": ["garden", "game", "simulator"], + "tags": [ + "garden", + "game", + "simulator" + ], "parentProject": "garden-os" }, { "id": "garden-planner-v5", "name": "Garden Planner v5", "description": "Garden OS planner with beds, plant profiles, and crop timelines", - "category": "outdoor", + "category": "garden-outdoor", "status": "active", "visibility": "public", "featured": false, @@ -419,29 +434,36 @@ <h1 class="hero-name">Dave Robertson</h1> "repo": "https://github.com/DaveHomeAssist/garden-os", "localPath": "", "runCommand": "", - "tags": ["garden", "planner"], + "tags": [ + "garden", + "planner" + ], "parentProject": "garden-os" }, { "id": "graph-explorer", "name": "Graph Explorer", "description": "Interactive project ecosystem and dependency graph visualizer", - "category": "dev-tools", + "category": "creative-design-tools", "status": "active", "visibility": "public", - "featured": true, + "featured": false, "hosting": "github-pages", "url": "https://davehomeassist.github.io/graph-explorer", "repo": "https://github.com/DaveHomeAssist/graph-explorer", "localPath": "", "runCommand": "", - "tags": ["visualization", "graph", "ecosystem"] + "tags": [ + "visualization", + "graph", + "ecosystem" + ] }, { "id": "notion-widgets", "name": "NotionWidgets", "description": "Custom Notion embed widgets and components", - "category": "dev-tools", + "category": "command-center-ops", "status": "active", "visibility": "public", "featured": false, @@ -450,13 +472,17 @@ <h1 class="hero-name">Dave Robertson</h1> "repo": "https://github.com/DaveHomeAssist/NotionWidgets", "localPath": "", "runCommand": "", - "tags": ["notion", "widgets", "embeds"] + "tags": [ + "notion", + "widgets", + "embeds" + ] }, { "id": "gemini-api-cost-calculator", "name": "Gemini API Cost Calculator", - "description": "Cost estimation tool for Google Gemini API usage", - "category": "dev-tools", + "description": "Real-time API cost comparison across Gemini, Claude, and competitor models", + "category": "research-developer-utility", "status": "active", "visibility": "public", "featured": false, @@ -465,13 +491,17 @@ <h1 class="hero-name">Dave Robertson</h1> "repo": "https://github.com/DaveHomeAssist/gemini-api-cost-calculator", "localPath": "", "runCommand": "", - "tags": ["api", "calculator", "gemini"] + "tags": [ + "api", + "calculator", + "gemini" + ] }, { "id": "sdlc-tool-stack-map", "name": "SDLC Tool Stack Map", "description": "Interactive map of software development lifecycle tools", - "category": "dev-tools", + "category": "research-developer-utility", "status": "active", "visibility": "public", "featured": false, @@ -480,13 +510,17 @@ <h1 class="hero-name">Dave Robertson</h1> "repo": "https://github.com/DaveHomeAssist/sdlc-tool-stack-map", "localPath": "", "runCommand": "", - "tags": ["sdlc", "devops", "tooling"] + "tags": [ + "sdlc", + "devops", + "tooling" + ] }, { "id": "web-templates", "name": "Web Templates", - "description": "Reusable web template collection", - "category": "dev-tools", + "description": "Reference layout pack — 8 patterns for filterable tables, kanban, roadmaps, and directories", + "category": "creative-design-tools", "status": "active", "visibility": "public", "featured": false, @@ -495,29 +529,37 @@ <h1 class="hero-name">Dave Robertson</h1> "repo": "https://github.com/DaveHomeAssist/web-templates", "localPath": "", "runCommand": "", - "tags": ["templates", "html", "css"] + "tags": [ + "templates", + "html", + "css" + ] }, { - "id": "homelab-os", - "name": "Homelab OS", + "id": "home-os", + "name": "Home OS", "description": "Home lab infrastructure management dashboard", - "category": "infrastructure", - "status": "active", + "category": "home-personal-ops", + "status": "archived", "visibility": "public", "featured": false, "hosting": "github-pages", - "url": "https://davehomeassist.github.io/homelab-os", - "repo": "https://github.com/DaveHomeAssist/DaveHomeAssist.github.io", + "url": "https://davehomeassist.github.io/home-os", + "repo": "https://github.com/DaveHomeAssist/home-os", "localPath": "", "runCommand": "", - "tags": ["homelab", "infrastructure", "dashboard"] + "tags": [ + "homelab", + "infrastructure", + "dashboard" + ] }, { "id": "elysium-landing", "name": "Elysium Landing", "description": "Landing page for Elysium project", - "category": "creative", - "status": "active", + "category": "business-client-sites", + "status": "archived", "visibility": "public", "featured": false, "hosting": "github-pages", @@ -525,103 +567,126 @@ <h1 class="hero-name">Dave Robertson</h1> "repo": "https://github.com/DaveHomeAssist/elysium-landing", "localPath": "", "runCommand": "", - "tags": ["landing-page", "creative"] + "tags": [ + "landing-page", + "creative" + ] }, { "id": "shieldbox", "name": "ShieldBox", - "description": "Insurance quote request system", - "category": "client-work", + "description": "Event security quote request form — gold master demo (shieldboxsecurity.com)", + "category": "business-client-sites", "status": "active", "visibility": "public", "featured": false, - "hosting": "github-pages", - "url": "", - "repo": "https://github.com/DaveHomeAssist/shieldbox-event-quote", + "hosting": "custom-domain", + "url": "https://shieldboxsecurity.com", + "repo": "https://github.com/DaveHomeAssist/shieldbox-security", "localPath": "", "runCommand": "", - "tags": ["insurance", "quotes", "client"] + "tags": [ + "event-security", + "quotes", + "client" + ] }, { "id": "freelance", "name": "Freelance", "description": "Freelance business management site", - "category": "client-work", + "category": "business-client-sites", "status": "active", "visibility": "public", "featured": false, - "hosting": "netlify", - "url": "", + "hosting": "github-pages", + "url": "https://davehomeassist.github.io/freelance", "repo": "https://github.com/DaveHomeAssist/freelance", "localPath": "", "runCommand": "", - "tags": ["freelance", "business"] + "tags": [ + "freelance", + "business" + ] }, { "id": "freelance-landing", "name": "Freelance Landing", "description": "Landing page for freelance services", - "category": "client-work", + "category": "business-client-sites", "status": "active", "visibility": "unlisted", "featured": false, "hosting": "github-pages", - "url": "", + "url": "https://davehomeassist.github.io/freelance-landing", "repo": "https://github.com/DaveHomeAssist/freelance-landing", "localPath": "", "runCommand": "", - "tags": ["freelance", "landing-page"] + "tags": [ + "freelance", + "landing-page" + ] }, { "id": "contractor", "name": "Contractor", "description": "Contractor management and scheduling", - "category": "client-work", + "category": "business-client-sites", "status": "active", "visibility": "public", "featured": false, - "hosting": "netlify", - "url": "", + "hosting": "github-pages", + "url": "https://davehomeassist.github.io/contractor", "repo": "https://github.com/DaveHomeAssist/contractor", "localPath": "", "runCommand": "", - "tags": ["contractor", "scheduling"] + "tags": [ + "contractor", + "scheduling" + ] }, { "id": "act-two-catering", "name": "Act Two Catering", "description": "Catering business website", - "category": "client-work", + "category": "business-client-sites", "status": "active", "visibility": "unlisted", "featured": false, - "hosting": "netlify", - "url": "", + "hosting": "github-pages", + "url": "https://davehomeassist.github.io/act-two-catering", "repo": "https://github.com/DaveHomeAssist/act-two-catering", "localPath": "", "runCommand": "", - "tags": ["catering", "client"] + "tags": [ + "catering", + "client" + ] }, { "id": "curl-plan", "name": "Curl Plan", - "description": "Curling strategy and game planning tool", - "category": "outdoor", + "description": "CurlPlan: curling-native app to map your season and stay close to your circle (CRDT sync, iOS in flight)", + "category": "research-developer-utility", "status": "active", "visibility": "public", - "featured": false, + "featured": true, "hosting": "github-pages", "url": "https://davehomeassist.github.io/curl-plan/", "repo": "https://github.com/DaveHomeAssist/curl-plan", "localPath": "", "runCommand": "", - "tags": ["curling", "strategy", "sports"] + "tags": [ + "curling", + "strategy", + "sports" + ] }, { "id": "mlb-ballparks-quest", "name": "MLB Ballparks Quest", "description": "Track visits to MLB ballparks across the country", - "category": "outdoor", + "category": "games-interactive-play", "status": "active", "visibility": "public", "featured": false, @@ -630,14 +695,20 @@ <h1 class="hero-name">Dave Robertson</h1> "repo": "https://github.com/DaveHomeAssist/mlb-ballparks-quest", "localPath": "", "runCommand": "", - "tags": ["baseball", "travel", "quest"], - "dependencies": ["quest-platform"] + "tags": [ + "baseball", + "travel", + "quest" + ], + "dependencies": [ + "quest-platform" + ] }, { "id": "festival-atlas", "name": "Festival Atlas", "description": "Music festival discovery and tracking map", - "category": "creative", + "category": "events-planning", "status": "active", "visibility": "public", "featured": false, @@ -646,61 +717,84 @@ <h1 class="hero-name">Dave Robertson</h1> "repo": "https://github.com/DaveHomeAssist/festival-atlas", "localPath": "", "runCommand": "", - "tags": ["music", "festivals", "map"], - "dependencies": ["quest-platform"] + "tags": [ + "music", + "festivals", + "map" + ], + "dependencies": [ + "quest-platform" + ] }, { "id": "quest-platform", "name": "Quest Platform", - "description": "Shared runtime library for quest-style tracking apps", - "category": "infrastructure", - "status": "active", + "description": "Shared runtime library for quest-style tracking apps (retired; extraction closed 2026-07-11)", + "category": "games-interactive-play", + "status": "archived", "visibility": "unlisted", "featured": false, - "hosting": "github-pages", + "hosting": "none", "url": "", "repo": "https://github.com/DaveHomeAssist/quest-platform", "localPath": "", "runCommand": "", - "tags": ["library", "shared", "runtime"], - "dependents": ["mlb-ballparks-quest", "festival-atlas"] + "tags": [ + "library", + "shared", + "runtime" + ], + "dependents": [ + "mlb-ballparks-quest", + "festival-atlas" + ] }, { "id": "metagrid", "name": "Metagrid", - "description": "Component library with animation utilities", - "category": "infrastructure", + "description": "Metagrid — wireless power infrastructure concept site (metamaterials, remote AI data centers; patent pending)", + "category": "creative-design-tools", "status": "active", "visibility": "unlisted", "featured": false, - "hosting": "vercel", - "url": "", + "hosting": "github-pages", + "url": "https://davehomeassist.github.io/metagrid", "repo": "https://github.com/DaveHomeAssist/metagrid", "localPath": "", "runCommand": "", - "tags": ["library", "components", "animation"], - "dependents": ["notion-widgets"] + "tags": [ + "concept-site", + "wireless-power", + "landing" + ], + "dependents": [ + "notion-widgets" + ] }, { "id": "prompt-lab", "name": "Prompt Lab", "description": "AI prompt engineering and testing workspace", - "category": "dev-tools", + "category": "ai-agent-systems", "status": "active", "visibility": "public", "featured": true, "hosting": "vercel", "url": "https://promptlab.tools", - "repo": "https://github.com/DaveHomeAssist/new-land-pages", + "repo": "https://github.com/DaveHomeAssist/prompt-lab", "localPath": "", "runCommand": "", - "tags": ["ai", "prompts", "tooling"] + "tags": [ + "ai", + "prompts", + "tooling" + ] }, { "id": "deckforge", "name": "DeckForge", "description": "Stream Deck configuration and action builder", - "category": "dev-tools", + "category": "av-show-tools", "status": "active", "visibility": "public", "featured": false, @@ -709,28 +803,35 @@ <h1 class="hero-name">Dave Robertson</h1> "repo": "https://github.com/DaveHomeAssist/deckforge", "localPath": "", "runCommand": "", - "tags": ["stream-deck", "automation"] + "tags": [ + "stream-deck", + "automation" + ] }, { "id": "sap-music", "name": "SAP Music", - "description": "Music streaming/production application", - "category": "creative", + "description": "SAP (Standard Acid Procedure) — artist site", + "category": "audio-music-tools", "status": "active", "visibility": "public", "featured": false, - "hosting": "vercel", - "url": "", + "hosting": "custom-domain", + "url": "https://standardacidprocedure.com", "repo": "https://github.com/DaveHomeAssist/sap-music", "localPath": "", "runCommand": "", - "tags": ["music", "streaming", "audio"] + "tags": [ + "music", + "artist-site", + "band" + ] }, { "id": "nesy", "name": "NeSy", - "description": "Neural-symbolic AI experimentation", - "category": "dev-tools", + "description": "Interactive research brief on neuro-symbolic AI in 2026: architectures, evidence, and the LLM reliability shift", + "category": "creative-design-tools", "status": "active", "visibility": "unlisted", "featured": false, @@ -739,13 +840,17 @@ <h1 class="hero-name">Dave Robertson</h1> "repo": "https://github.com/DaveHomeAssist/nesy", "localPath": "", "runCommand": "", - "tags": ["ai", "neural-symbolic"] + "tags": [ + "ai", + "neuro-symbolic", + "research-brief" + ] }, { "id": "lansdowne-lighting-plot", "name": "Lansdowne Lighting Plot", "description": "Stage lighting design and plotting tool", - "category": "creative", + "category": "av-show-tools", "status": "active", "visibility": "public", "featured": false, @@ -754,13 +859,17 @@ <h1 class="hero-name">Dave Robertson</h1> "repo": "https://github.com/DaveHomeAssist/lansdowne-lighting-plot", "localPath": "", "runCommand": "", - "tags": ["lighting", "stage", "design"] + "tags": [ + "lighting", + "stage", + "design" + ] }, { "id": "davellm", "name": "DaveLLM", "description": "Local-first LLM development server with OpenAI-compatible API", - "category": "dev-tools", + "category": "ai-agent-systems", "status": "active", "visibility": "public", "featured": false, @@ -769,73 +878,92 @@ <h1 class="hero-name">Dave Robertson</h1> "repo": "https://github.com/DaveHomeAssist/DaveLLM", "localPath": "", "runCommand": "", - "tags": ["llm", "local", "ai"] + "tags": [ + "llm", + "local", + "ai" + ] }, { "id": "readout", "name": "ReadOut", "description": "Standalone desktop reading application", - "category": "dev-tools", + "category": "av-show-tools", "status": "active", - "visibility": "private", + "visibility": "public", "featured": false, - "hosting": "local", - "url": "", - "repo": "https://github.com/DaveHomeAssist/ReadOut", + "hosting": "none", + "url": "https://github.com/DaveHomeAssist/readout", + "repo": "https://github.com/DaveHomeAssist/readout", "localPath": "", "runCommand": "", - "tags": ["desktop", "reading", "app"] + "tags": [ + "desktop", + "reading", + "app" + ] }, { "id": "curling-simulator", "name": "Curling Simulator", "description": "Curling game simulator", - "category": "outdoor", + "category": "games-interactive-play", "status": "wip", - "visibility": "private", + "visibility": "unlisted", "featured": false, - "hosting": "none", - "url": "", + "hosting": "github-pages", + "url": "https://davehomeassist.github.io/curling-simulator", "repo": "https://github.com/DaveHomeAssist/curling-simulator", "localPath": "", "runCommand": "", - "tags": ["curling", "simulator", "game"] + "tags": [ + "curling", + "simulator", + "game" + ] }, { "id": "daily-prophet", "name": "Daily Prophet", "description": "Self-contained daily content generator", - "category": "creative", + "category": "content-signals", "status": "active", - "visibility": "private", + "visibility": "unlisted", "featured": false, - "hosting": "local", - "url": "", + "hosting": "github-pages", + "url": "https://davehomeassist.github.io/daily-prophet", "repo": "https://github.com/DaveHomeAssist/daily-prophet", "localPath": "", "runCommand": "", - "tags": ["content", "daily"] + "tags": [ + "content", + "daily" + ] }, { "id": "ai-agent-personalities", "name": "AI Agent Personalities", "description": "Documentation for AI agent personality profiles", - "category": "dev-tools", + "category": "ai-agent-systems", "status": "active", - "visibility": "private", + "visibility": "unlisted", "featured": false, - "hosting": "none", - "url": "", + "hosting": "github-pages", + "url": "https://davehomeassist.github.io/ai-agent-personalities", "repo": "https://github.com/DaveHomeAssist/ai-agent-personalities", "localPath": "", "runCommand": "", - "tags": ["ai", "agents", "documentation"] + "tags": [ + "ai", + "agents", + "documentation" + ] }, { "id": "av-resume", "name": "AV Resume", "description": "Interactive AV-themed resume", - "category": "creative", + "category": "av-show-tools", "status": "active", "visibility": "unlisted", "featured": false, @@ -844,13 +972,16 @@ <h1 class="hero-name">Dave Robertson</h1> "repo": "https://github.com/DaveHomeAssist/av-resume", "localPath": "", "runCommand": "", - "tags": ["resume", "portfolio"] + "tags": [ + "resume", + "portfolio" + ] }, { "id": "theme-organizer-resource", "name": "Theme Organizer Resource", "description": "Theme and resource organization tool", - "category": "dev-tools", + "category": "creative-design-tools", "status": "active", "visibility": "unlisted", "featured": false, @@ -859,43 +990,54 @@ <h1 class="hero-name">Dave Robertson</h1> "repo": "https://github.com/DaveHomeAssist/theme-organizer-resource", "localPath": "", "runCommand": "", - "tags": ["themes", "resources", "organizer"] + "tags": [ + "themes", + "resources", + "organizer" + ] }, { "id": "codedash-mar24", "name": "CodeDash Mar24", - "description": "Hackathon/code dash project from March 2024", - "category": "creative", + "description": "Historical Code Dashboard snapshot from March 2026", + "category": "command-center-ops", "status": "archived", - "visibility": "private", + "visibility": "unlisted", "featured": false, - "hosting": "none", - "url": "", + "hosting": "github-pages", + "url": "https://davehomeassist.github.io/codedash-mar24", "repo": "https://github.com/DaveHomeAssist/codedash-mar24", "localPath": "", "runCommand": "", - "tags": ["hackathon", "archived"] + "tags": [ + "hackathon", + "archived" + ] }, { "id": "skills", "name": "Skills", "description": "Documentation repo for AI skill definitions", - "category": "infrastructure", + "category": "ai-agent-systems", "status": "active", - "visibility": "private", + "visibility": "unlisted", "featured": false, - "hosting": "none", - "url": "", + "hosting": "github-pages", + "url": "https://davehomeassist.github.io/skills", "repo": "https://github.com/DaveHomeAssist/skills", "localPath": "", "runCommand": "", - "tags": ["documentation", "skills", "ai"] + "tags": [ + "documentation", + "skills", + "ai" + ] }, { "id": "veronica-lucas-wedding", "name": "Veronica & Lucas Wedding", "description": "Wedding website — separate domain, intentionally isolated", - "category": "client-work", + "category": "business-client-sites", "status": "active", "visibility": "isolated", "featured": false, @@ -904,23 +1046,552 @@ <h1 class="hero-name">Dave Robertson</h1> "repo": "https://github.com/DaveHomeAssist/VeronicaLucasWedding", "localPath": "", "runCommand": "", - "tags": ["wedding", "client"] + "tags": [ + "wedding", + "client" + ] + }, + { + "id": "pixelforge", + "name": "PixelForge", + "description": "Lightweight browser-based drawing app with raster and vector layers", + "category": "creative-design-tools", + "status": "active", + "visibility": "public", + "featured": true, + "hosting": "github-pages", + "url": "https://davehomeassist.github.io/PixelForge", + "repo": "https://github.com/DaveHomeAssist/PixelForge", + "localPath": "", + "runCommand": "", + "tags": [ + "drawing", + "canvas", + "editor" + ] + }, + { + "id": "bpm-delay-calculator", + "name": "BPM Delay Calculator", + "description": "Tempo-synced delay and reverb time calculator for music production", + "category": "audio-music-tools", + "status": "active", + "visibility": "public", + "featured": false, + "hosting": "github-pages", + "url": "https://davehomeassist.github.io/bpm-delay-calculator", + "repo": "https://github.com/DaveHomeAssist/bpm-delay-calculator", + "localPath": "", + "runCommand": "", + "tags": [ + "music", + "calculator", + "audio" + ] + }, + { + "id": "stereo-field-compressor", + "name": "Stereo Field Compressor", + "description": "Sidechain-driven spatial compression plugin (public prototype, archived; canonical source pending duplicate-resolution review)", + "category": "audio-music-tools", + "status": "archived", + "visibility": "public", + "featured": false, + "hosting": "none", + "url": "", + "repo": "https://github.com/DaveHomeAssist/stereo-field-compressor", + "localPath": "", + "runCommand": "", + "tags": [ + "music", + "audio", + "plugin", + "juce" + ] + }, + { + "id": "garden-cage-build-guide", + "name": "Garden Cage Build Guide", + "description": "Step-by-step garden cage construction guide", + "category": "garden-outdoor", + "status": "archived", + "visibility": "public", + "featured": false, + "hosting": "github-pages", + "url": "https://davehomeassist.github.io/garden-cage-build-guide", + "repo": "https://github.com/DaveHomeAssist/garden-cage-build-guide", + "localPath": "", + "runCommand": "", + "tags": [ + "garden", + "diy", + "guide" + ] + }, + { + "id": "garden-planner", + "name": "Garden Planner", + "description": "Archived standalone raised-bed planner (lifecycle-historical); superseded by Garden Planner v5 inside Garden OS", + "category": "garden-outdoor", + "status": "archived", + "visibility": "public", + "featured": false, + "hosting": "github-pages", + "url": "https://davehomeassist.github.io/garden-planner/", + "repo": "https://github.com/DaveHomeAssist/garden-planner", + "localPath": "", + "runCommand": "", + "tags": [ + "garden", + "planner", + "archived" + ], + "parentProject": "garden-os" + }, + { + "id": "mixmash", + "name": "MixMash", + "description": "MixMash Studio — hub for games that play in your browser (mixmash.games)", + "category": "games-interactive-play", + "status": "active", + "visibility": "public", + "featured": false, + "hosting": "github-pages", + "url": "https://davehomeassist.github.io/mixmash", + "repo": "https://github.com/DaveHomeAssist/mixmash", + "localPath": "", + "runCommand": "", + "tags": [ + "games", + "browser", + "hub" + ] + }, + { + "id": "park-rave-005", + "name": "Park Rave 005", + "description": "Park rave event page", + "category": "events-planning", + "status": "archived", + "visibility": "public", + "featured": false, + "hosting": "github-pages", + "url": "https://davehomeassist.github.io/park-rave-005", + "repo": "https://github.com/DaveHomeAssist/park-rave-005", + "localPath": "", + "runCommand": "", + "tags": [ + "events", + "music" + ] + }, + { + "id": "phillies-wire", + "name": "Phillies Wire", + "description": "Philadelphia Phillies news and content aggregator", + "category": "content-signals", + "status": "active", + "visibility": "public", + "featured": true, + "hosting": "github-pages", + "url": "https://davehomeassist.github.io/phillies-wire", + "repo": "https://github.com/DaveHomeAssist/phillies-wire", + "localPath": "", + "runCommand": "", + "tags": [ + "baseball", + "phillies", + "sports" + ] + }, + { + "id": "system-by-dave", + "name": "System by Dave", + "description": "Personal system and workflow documentation site", + "category": "ai-agent-systems", + "status": "active", + "visibility": "public", + "featured": true, + "hosting": "custom-domain", + "url": "https://systembydave.com", + "repo": "https://github.com/DaveHomeAssist/system-by-dave", + "localPath": "", + "runCommand": "", + "tags": [ + "system", + "workflow", + "documentation" + ] + }, + { + "id": "repo-favicons", + "name": "Repo Favicons", + "description": "Shared favicon and icon assets for all repos", + "category": "command-center-ops", + "status": "active", + "visibility": "unlisted", + "featured": false, + "hosting": "github-pages", + "url": "https://davehomeassist.github.io/repo-favicons", + "repo": "https://github.com/DaveHomeAssist/repo-favicons", + "localPath": "", + "runCommand": "", + "tags": [ + "assets", + "icons", + "shared" + ] + }, + { + "id": "recipe-tracker", + "name": "Recipe Tracker", + "description": "Offline-capable family recipe journal with search, tags, editing, import/export backups, and PWA install support.", + "category": "home-personal-ops", + "status": "active", + "visibility": "public", + "featured": false, + "hosting": "github-pages", + "url": "https://davehomeassist.github.io/recipe-tracker/", + "repo": "https://github.com/DaveHomeAssist/recipe-tracker", + "localPath": "", + "runCommand": "", + "tags": [ + "recipes", + "pwa", + "family" + ] + }, + { + "id": "code-dashboard", + "name": "Code Dashboard", + "description": "Notion-backed dashboard automation for tracking code projects and workspace status from local scripts.", + "category": "command-center-ops", + "status": "active", + "visibility": "public", + "featured": false, + "hosting": "none", + "url": "https://github.com/DaveHomeAssist/code-dashboard", + "repo": "https://github.com/DaveHomeAssist/code-dashboard", + "localPath": "", + "runCommand": "", + "tags": [ + "notion", + "dashboard", + "automation" + ] + }, + { + "id": "plotforge-candidate", + "name": "PlotForge", + "description": "Lighting plot generator: vector SVG drafting, structured document model, IndexedDB autosave", + "category": "av-show-tools", + "status": "candidate", + "visibility": "public", + "featured": false, + "hosting": "vercel", + "url": "https://plotforge-beta.vercel.app", + "repo": "https://github.com/DaveHomeAssist/plotforge", + "localPath": "", + "runCommand": "", + "tags": [ + "catalog-candidate", + "javascript" + ] + }, + { + "id": "new-land-pages-candidate", + "name": "New Land Pages", + "description": "Standalone Pages deployment of the new-land.html Prompt Lab landing page concept", + "category": "business-client-sites", + "status": "candidate", + "visibility": "public", + "featured": false, + "hosting": "github-pages", + "url": "https://davehomeassist.github.io/new-land-pages/", + "repo": "https://github.com/DaveHomeAssist/new-land-pages", + "localPath": "", + "runCommand": "", + "tags": [ + "catalog-candidate", + "html" + ] + }, + { + "id": "command-center-061eed-candidate", + "name": "Command Center", + "description": "Public operations dashboard for project status, git sync state, and domain health", + "category": "command-center-ops", + "status": "candidate", + "visibility": "public", + "featured": false, + "hosting": "github-pages", + "url": "https://davehomeassist.github.io/command-center-061eed/", + "repo": "https://github.com/DaveHomeAssist/command-center-061eed", + "localPath": "", + "runCommand": "", + "tags": [ + "catalog-candidate", + "html" + ] + }, + { + "id": "hat-in-ring-candidate", + "name": "Hat In Ring Archive", + "description": "Hat-in-Ring Radar — 2028 presidential signal tracker. Auto-ingests FEC + Google News, scores status tier × momentum, daily rebuild to GitHub Pages.", + "category": "content-signals", + "status": "archived", + "visibility": "public", + "featured": false, + "hosting": "github-pages", + "url": "https://davehomeassist.github.io/hat-in-ring/", + "repo": "https://github.com/DaveHomeAssist/hat-in-ring", + "localPath": "", + "runCommand": "", + "tags": [ + "catalog-candidate", + "python", + "archived" + ] + }, + { + "id": "hatinring-candidate", + "name": "Hat In Ring", + "description": "Hat-in-Ring Radar — 2028 presidential signal tracker", + "category": "content-signals", + "status": "candidate", + "visibility": "public", + "featured": false, + "hosting": "custom-domain", + "url": "https://hatinring.com", + "repo": "https://github.com/DaveHomeAssist/hatinring", + "localPath": "", + "runCommand": "", + "tags": [ + "catalog-candidate", + "html" + ] + }, + { + "id": "notion-banner-generator-candidate", + "name": "Notion Banner Generator", + "description": "Local-first generator for Notion page covers. Deterministic Canvas design rules, optional AI enhancement, zero backend.", + "category": "creative-design-tools", + "status": "candidate", + "visibility": "public", + "featured": false, + "hosting": "github-pages", + "url": "https://davehomeassist.github.io/notion-banner-generator/", + "repo": "https://github.com/DaveHomeAssist/notion-banner-generator", + "localPath": "", + "runCommand": "", + "tags": [ + "catalog-candidate", + "typescript" + ] + }, + { + "id": "notion-banners-candidate", + "name": "Notion Banners", + "description": "Sliced banner covers for top-level Notion sidebar pages (16 art tiles, 624x290)", + "category": "creative-design-tools", + "status": "candidate", + "visibility": "public", + "featured": false, + "hosting": "none", + "url": "https://github.com/DaveHomeAssist/notion-banners", + "repo": "https://github.com/DaveHomeAssist/notion-banners", + "localPath": "", + "runCommand": "", + "tags": [ + "catalog-candidate" + ] + }, + { + "id": "clever-paws-candidate", + "name": "Clever Paws", + "description": "Calm, evidence-based cat training reference with local progress tracking", + "category": "home-personal-ops", + "status": "candidate", + "visibility": "public", + "featured": false, + "hosting": "github-pages", + "url": "https://davehomeassist.github.io/clever-paws/", + "repo": "https://github.com/DaveHomeAssist/clever-paws", + "localPath": "", + "runCommand": "", + "tags": [ + "catalog-candidate", + "html" + ] + }, + { + "id": "home-comic-dashboard-candidate", + "name": "Home Comic Dashboard", + "description": "Family-facing Home Assistant dashboard with a pulp comic book aesthetic. Current surface uses React UMD + Babel; future architecture should follow product needs.", + "category": "home-personal-ops", + "status": "candidate", + "visibility": "public", + "featured": false, + "hosting": "github-pages", + "url": "https://davehomeassist.github.io/home-comic-dashboard/", + "repo": "https://github.com/DaveHomeAssist/home-comic-dashboard", + "localPath": "", + "runCommand": "", + "tags": [ + "catalog-candidate", + "javascript" + ] + }, + { + "id": "utility-candidate", + "name": "Utility", + "description": "Grab-bag of standalone scripts, prototypes, and single-file tools", + "category": "research-developer-utility", + "status": "candidate", + "visibility": "public", + "featured": false, + "hosting": "none", + "url": "https://github.com/DaveHomeAssist/Utility", + "repo": "https://github.com/DaveHomeAssist/Utility", + "localPath": "", + "runCommand": "", + "tags": [ + "catalog-candidate", + "html" + ] + }, + { + "id": "groundskeeper", + "name": "The Groundskeeper", + "description": "Weekly gardening dispatch for the raised beds of Oaklyn, NJ (zone 7a/7b) — web and email editions, published Saturday mornings", + "category": "garden-outdoor", + "status": "active", + "visibility": "public", + "featured": false, + "hosting": "github-pages", + "url": "https://davehomeassist.github.io/groundskeeper/", + "repo": "https://github.com/DaveHomeAssist/groundskeeper", + "localPath": "", + "runCommand": "", + "tags": [ + "garden", + "newsletter", + "weekly" + ] + }, + { + "id": "noteforge", + "name": "NoteForge", + "description": "Local-first markdown notes app (Obsidian/Notion-style): wikilinks, backlinks, tags, graph, nested notes, editable tables, PWA. Vanilla JS + Vite.", + "category": "research-developer-utility", + "status": "active", + "visibility": "public", + "featured": false, + "hosting": "github-pages", + "url": "https://davehomeassist.github.io/noteforge/", + "repo": "https://github.com/DaveHomeAssist/noteforge", + "localPath": "", + "runCommand": "", + "tags": [ + "notes", + "markdown", + "local-first", + "pwa" + ] } ], "categories": { - "outdoor": { "label": "Outdoor & Sports", "icon": "🌲", "color": "#2d6a4f" }, - "dev-tools": { "label": "Dev Tools", "icon": "⚙️", "color": "#4361ee" }, - "creative": { "label": "Creative", "icon": "🎨", "color": "#e76f51" }, - "client-work": { "label": "Client Work", "icon": "💼", "color": "#6c757d" }, - "infrastructure": { "label": "Infrastructure", "icon": "🔧", "color": "#7209b7" } + "ai-agent-systems": { + "label": "AI & Agent Systems", + "icon": "AI", + "color": "#4361ee" + }, + "audio-music-tools": { + "label": "Audio & Music Tools", + "icon": "AU", + "color": "#9d4edd" + }, + "av-show-tools": { + "label": "AV & Show Tools", + "icon": "AV", + "color": "#f77f00" + }, + "business-client-sites": { + "label": "Business & Client Sites", + "icon": "BC", + "color": "#6c757d" + }, + "command-center-ops": { + "label": "Command Center & Ops", + "icon": "CC", + "color": "#0f766e" + }, + "content-signals": { + "label": "Content & Signals", + "icon": "CS", + "color": "#d62828" + }, + "creative-design-tools": { + "label": "Creative & Design Tools", + "icon": "CD", + "color": "#e76f51" + }, + "events-planning": { + "label": "Events & Planning", + "icon": "EP", + "color": "#2a9d8f" + }, + "games-interactive-play": { + "label": "Games & Interactive Play", + "icon": "GI", + "color": "#3a0ca3" + }, + "garden-outdoor": { + "label": "Garden & Outdoor", + "icon": "GO", + "color": "#2d6a4f" + }, + "home-personal-ops": { + "label": "Home & Personal Ops", + "icon": "HP", + "color": "#588157" + }, + "portfolio-hub": { + "label": "Portfolio & Hub", + "icon": "PH", + "color": "#18201f" + }, + "research-developer-utility": { + "label": "Research & Developer Utility", + "icon": "RD", + "color": "#457b9d" + } }, "hostingPlatforms": { - "github-pages": { "label": "GitHub Pages", "color": "#24292e" }, - "vercel": { "label": "Vercel", "color": "#000000" }, - "netlify": { "label": "Netlify", "color": "#00ad9f" }, - "local": { "label": "Local", "color": "#e63946" }, - "custom-domain": { "label": "Custom Domain", "color": "#8338ec" }, - "none": { "label": "Not Deployed", "color": "#adb5bd" } + "github-pages": { + "label": "GitHub Pages", + "color": "#24292e" + }, + "vercel": { + "label": "Vercel", + "color": "#000000" + }, + "netlify": { + "label": "Netlify", + "color": "#00ad9f" + }, + "local": { + "label": "Local", + "color": "#e63946" + }, + "custom-domain": { + "label": "Custom Domain", + "color": "#8338ec" + }, + "none": { + "label": "Not Deployed", + "color": "#adb5bd" + } } }; diff --git a/scripts/sync-manifest.mjs b/scripts/sync-manifest.mjs index 62da79e..e1e1e11 100644 --- a/scripts/sync-manifest.mjs +++ b/scripts/sync-manifest.mjs @@ -1,10 +1,11 @@ #!/usr/bin/env node // sync-manifest.mjs -// Propagates project-manifest.json into the FALLBACK_MANIFEST blocks in -// private-hub.html and index.html so the three copies never drift. +// Propagates project-manifest.json into the FALLBACK_MANIFEST blocks embedded +// in every hub page that consumes the manifest, so the copies never drift. // -// Usage: node scripts/sync-manifest.mjs -// (or: npm run sync-manifest) +// Usage: node scripts/sync-manifest.mjs (rewrite stale fallbacks) +// node scripts/sync-manifest.mjs --check (exit 1 on drift, write nothing) +// (or: npm run sync-manifest / npm run check-manifest) import { readFileSync, writeFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; @@ -12,12 +13,16 @@ import { dirname, resolve } from 'node:path'; const __dirname = dirname(fileURLToPath(import.meta.url)); const repoRoot = resolve(__dirname, '..'); +const checkOnly = process.argv.includes('--check'); const manifestPath = resolve(repoRoot, 'project-manifest.json'); -const targets = [ - resolve(repoRoot, 'private-hub.html'), - resolve(repoRoot, 'index.html'), +// Every page that embeds a FALLBACK_MANIFEST block. Add new hub pages here. +const FALLBACK_TARGETS = [ + 'index.html', + 'public-hub.html', + 'private-hub.html', ]; +const targets = FALLBACK_TARGETS.map((file) => resolve(repoRoot, file)); const manifestRaw = readFileSync(manifestPath, 'utf8'); // Validate JSON and re-serialise with 2-space indent for stable diffs. @@ -28,6 +33,7 @@ const blockRegex = /const FALLBACK_MANIFEST = \{[\s\S]*?\n\};/; const replacement = `const FALLBACK_MANIFEST = ${manifestPretty};`; let ok = true; +let drifted = 0; for (const file of targets) { const before = readFileSync(file, 'utf8'); if (!blockRegex.test(before)) { @@ -38,6 +44,9 @@ for (const file of targets) { const after = before.replace(blockRegex, replacement); if (after === before) { console.log(` [ok] ${file} — already in sync`); + } else if (checkOnly) { + console.error(` [drift] ${file} — FALLBACK_MANIFEST differs from project-manifest.json`); + drifted += 1; } else { writeFileSync(file, after, 'utf8'); console.log(` [wrote] ${file}`); @@ -48,4 +57,9 @@ console.log(`\nSource: ${manifestPath}`); console.log(`Projects: ${manifestObj.projects?.length ?? 'n/a'}`); console.log(`Last updated: ${manifestObj.meta?.lastUpdated ?? 'n/a'}`); +if (checkOnly && drifted > 0) { + console.error(`\n${drifted} fallback block(s) out of sync. Run: npm run sync-manifest`); + ok = false; +} + process.exit(ok ? 0 : 1); diff --git a/scripts/validate-public-manifest.mjs b/scripts/validate-public-manifest.mjs index 4d0b118..c27cf44 100644 --- a/scripts/validate-public-manifest.mjs +++ b/scripts/validate-public-manifest.mjs @@ -1,4 +1,9 @@ #!/usr/bin/env node +// validate-public-manifest.mjs +// Gate for project-manifest.json, the public project registry (README.md: +// "project-manifest.json is an API"). Enforces the privacy rules and the +// documented entry contract. Throws (exit 1) on the first violation so it can +// run before sync-manifest and in CI. import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; @@ -9,11 +14,23 @@ const manifestPath = resolve(__dirname, '..', 'project-manifest.json'); const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')); const projects = Array.isArray(manifest.projects) ? manifest.projects : null; const allowedVisibilities = new Set(['public', 'unlisted', 'isolated']); +// README.md entry shape: status is active, wip, archived, or candidate. +const allowedStatuses = new Set(['active', 'wip', 'archived', 'candidate']); +const categories = manifest.categories && typeof manifest.categories === 'object' ? manifest.categories : {}; +const hostingPlatforms = manifest.hostingPlatforms && typeof manifest.hostingPlatforms === 'object' ? manifest.hostingPlatforms : {}; if (!projects) { throw new Error('Public manifest must contain a projects array.'); } +if (!manifest.meta || typeof manifest.meta !== 'object' || !manifest.meta.owner) { + throw new Error('Public manifest must contain meta.owner.'); +} + +if (!/^\d{4}-\d{2}-\d{2}$/.test(manifest.meta.lastUpdated || '')) { + throw new Error('Public manifest meta.lastUpdated must be a YYYY-MM-DD date.'); +} + const ids = new Set(); for (const project of projects) { if (!project.id || ids.has(project.id)) { @@ -21,10 +38,30 @@ for (const project of projects) { } ids.add(project.id); + if (!project.name) { + throw new Error(`Public manifest requires a name for ${project.id}.`); + } + if (!allowedVisibilities.has(project.visibility)) { throw new Error(`Public manifest rejects visibility "${project.visibility}" for ${project.id}.`); } + if (!allowedStatuses.has(project.status)) { + throw new Error(`Public manifest rejects status "${project.status}" for ${project.id}.`); + } + + if (typeof project.featured !== 'boolean') { + throw new Error(`Public manifest requires a boolean featured flag for ${project.id}.`); + } + + if (!Object.hasOwn(categories, project.category)) { + throw new Error(`Public manifest references undefined category "${project.category}" for ${project.id}.`); + } + + if (!Object.hasOwn(hostingPlatforms, project.hosting)) { + throw new Error(`Public manifest references undefined hosting "${project.hosting}" for ${project.id}.`); + } + if (!/^https:\/\/github\.com\/DaveHomeAssist\/[A-Za-z0-9._-]+\/?$/.test(project.repo || '')) { throw new Error(`Public manifest requires a DaveHomeAssist GitHub repository URL for ${project.id}.`); } @@ -34,4 +71,18 @@ for (const project of projects) { } } +// Relationship fields must resolve inside the registry. +for (const project of projects) { + const related = [ + ...(project.parentProject ? [project.parentProject] : []), + ...(Array.isArray(project.dependencies) ? project.dependencies : []), + ...(Array.isArray(project.dependents) ? project.dependents : []), + ]; + for (const id of related) { + if (!ids.has(id)) { + throw new Error(`Public manifest entry ${project.id} references unknown project id "${id}".`); + } + } +} + console.log(`Validated ${projects.length} public-safe projects across ${new Set(projects.map((project) => project.repo)).size} repositories.`);