diff --git a/CHANGELOG.md b/CHANGELOG.md index 639f381..678ab05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 2026-08-18 +- Added `docs/ops-hub-v2/AUDIT-2026-08-18.md` — read-only audit of the historical + cross-project dashboard, command center, Ops Hub, Code Dashboard, project + manifest, and public portfolio surfaces, with each artifact classified + preserve / migrate / archive / unlink / remove-from-public. +- Added `docs/ops-hub-v2/OPS_HUB_V2_SPEC.md` — build spec for the consolidated + replacement: registry schema, `ops-state.json` v2 contract, nine enforced + invariants, build order, and acceptance criteria. +- README: linked both documents. + ## 2026-07-06 - Added LICENSE (explicit all-rights-reserved). diff --git a/README.md b/README.md index b5e44c7..dc9fb18 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,20 @@ node notionDashboardUpdate.js --dry-run # print a sample summary, no API calls In production, the script runs unattended via the `Update Notion Dashboard` GitHub Actions workflow, which needs a `NOTION_TOKEN` secret configured on the repo. +## Related documents + +This repo drives the Notion Code Dashboard page, which makes it one artifact in a +longer lineage of cross-project dashboards. That lineage was audited on +2026-08-18: + +| Document | What it covers | +|---|---| +| [`docs/ops-hub-v2/AUDIT-2026-08-18.md`](docs/ops-hub-v2/AUDIT-2026-08-18.md) | Read-only audit of every historical dashboard, command center, manifest, and public portfolio surface, with each artifact classified preserve / migrate / archive / unlink / remove-from-public | +| [`docs/ops-hub-v2/OPS_HUB_V2_SPEC.md`](docs/ops-hub-v2/OPS_HUB_V2_SPEC.md) | Build spec for the consolidated replacement: registry schema, state contract, enforced invariants, and acceptance criteria | + +Both are public-safe by construction and cite only already-public evidence. The +Ops Hub v2 implementation itself belongs in a private repo — see §11 of the spec. + ## License All rights reserved — see [LICENSE](LICENSE). diff --git a/docs/ops-hub-v2/AUDIT-2026-08-18.md b/docs/ops-hub-v2/AUDIT-2026-08-18.md new file mode 100644 index 0000000..7fc41fe --- /dev/null +++ b/docs/ops-hub-v2/AUDIT-2026-08-18.md @@ -0,0 +1,324 @@ +# Cross-Project Dashboard Surface Audit — 2026-08-18 + +> [!IMPORTANT] +> **Read-only audit.** Nothing was archived, unlinked, deleted, de-indexed, or +> republished. Every remediation below is gated behind explicit approval +> (see [Remediation bundles](#remediation-bundles)). + +## Scope + +Every historical surface that has, at some point, tried to answer *"what projects +exist and what state are they in"*: cross-project dashboards, command centers, +Ops Hub, Code Dashboard, the project manifest, and the public portfolio hub. + +Each artifact is classified as **preserve**, **migrate**, **archive**, **unlink**, +or **remove from public**. + +## Publication boundary applied to this document + +`DaveHomeAssist/code-dashboard` is a **public** repository. Since the central +finding of this audit is that internal operations material leaked onto public +infrastructure, publishing an unredacted inventory here would reproduce the exact +defect being reported. This document therefore contains **only facts already +public**: anonymous HTTP crawl results, the contents of the already-public +`project-manifest.json`, and architecture description. + +Deliberately excluded: workspace page titles/URLs/IDs, local filesystem paths, +private project identifiers, run IDs, and client-engagement detail. Where an +internal source is cited it is referred to by **role**, not by name — e.g. +"the dashboard standards governance page". The unredacted inventory belongs in a +private repository. + +## Method + +| Evidence class | How it was obtained | Trust | +|---|---|---| +| HTTP status, redirects, robots meta, page `` | Anonymous `curl` against the live host, 2026-08-18 | **Confirmed** | +| Sitemap contents | `GET /sitemap.xml` | **Confirmed** | +| Public project inventory | `GET /project-manifest.json` | **Confirmed** | +| Hub rendering logic | Read of the served hub HTML | **Confirmed** | +| Repository visibility | GitHub API | **Confirmed** | +| Dashboard lineage & design intent | Internal workspace records | **Reported** — not independently reproducible here | + +Not available, and therefore **not** asserted anywhere in this document: Search +Console, analytics, CrUX/Lighthouse, indexation state, or traffic outcomes. No +claim below depends on knowing whether a page is actually *indexed* — only on +whether it is *indexable*. + +--- + +## Root cause + +Six dashboard generations were built. All six were rebuilt as a **new surface**; +none established a **contract** underneath. The concrete expression of that today: + +> **`visibility` is decorative metadata. Nothing in any build step converts it +> into a robots directive, a sitemap entry, or a link-rendering decision.** + +Every finding below is a symptom of that single missing invariant. + +### Evidence: `visibility` has no enforcement + +13 projects are marked `visibility: unlisted` in the public manifest. 12 have a +live URL. Their robots directives, fetched anonymously: + +| Live `` | Count | Verdict | +|---|---:|---| +| `noindex,nofollow` | 1 | Matches declared intent | +| *(no robots meta at all)* | 10 | Declared unlisted, fully indexable | +| `index, follow` | 1 | **Explicitly contradicts** its own manifest entry | + +One page carries an affirmative `index, follow` while its manifest row says +`unlisted`. That is not drift — it is two systems asserting opposite things with +no arbiter. + +### Evidence: the sitemap and the manifest are disjoint + +| Measure | Value | +|---|---:| +| On-host project URLs in the manifest | 48 | +| Of those, returning HTTP 200 | **48 / 48** | +| Of those, present in `sitemap.xml` | **1** | +| Sitemap entries that are *not* a manifest project | **8 of 9** | + +The sitemap is not stale — it is an **orphan from a different lineage**. Its 9 +entries are the root hub plus a brand palette, two internal strategy reports, and +four one-off pages. It has never described the project portfolio at all. + +### Evidence: hub links do not exist until JavaScript runs + +The served hub HTML contains no project anchors. The project directory is built +client-side from the manifest, filtered on `visibility === 'public'`. The static +document offers one fallback: a link to the raw JSON. + +This makes the manifest the **sole** discovery path — and it is the same file that +carries the unenforced `visibility` field. + +--- + +## Findings + +### F-001 · Internal operations dashboard is publicly indexable +**Severity: Critical** · **Classification: remove from public** + +`/command-center-061eed/` serves HTTP 200 with a complete internal operations +console: per-project status panels, an attention queue, git sync state, daily run +history, domain status, and a triage lane. + +Verified state of the document ``: + +| Element | Present | +|---|---| +| `` | **No** — the string `noindex` appears **zero** times in the entire 229 KB document | +| `` | No | +| `` | No | +| `

` | No | + +`robots.txt` is `Allow: /` with no exclusions. The manifest entry sets +`visibility: public`, so the hub renders a card linking to it. + +> [!WARNING] +> Internal design records state this surface was intended to be +> **"public (noindex)"**. The published artifact has no robots directive +> whatsoever. The intent was recorded and never implemented — and no check exists +> that would have caught the gap. + +**Two properties make this worse than a normal exposure:** + +1. **It is republished by automation.** Internal run records show recurring + automated passes that regenerate and push this surface. A manual edit to the + published output **will be overwritten**. The fix must land in the generator. +2. **`noindex` requires a crawl.** Blocking in `robots.txt` first would prevent + crawlers from ever *seeing* a `noindex`. Correct order: ship `noindex`, let it + be crawled, *then* consider disallowing. + +Treat this as a disclosure cleanup that happens to have SEO side effects — not as +an SEO defect. + +### F-002 · Public manifest and private ops registry are the same document +**Severity: High** · **Classification: migrate — split the contract** + +One `project-manifest.json` serves both portfolio discovery and internal +inventory. Consequences visible in the public file today: + +- 79 entries, of which **18 are `visibility: private`** and 24 are `candidate`. + Private and candidate work is enumerated by name in a public JSON file. +- The schema carries `localPath` and `runCommand` fields. Both are **empty in all + 79 rows** — the leak has been cleaned, but the *channel* remains. Any future + writer that populates them publishes local filesystem paths. +- The rows most likely to be sensitive are exactly the rows least likely to get a + review pass, because they render nothing. + +Empty-but-present private fields are a latent leak, not a resolved one. + +### F-003 · Sitemap describes an abandoned lineage +**Severity: High** · **Classification: preserve the file, regenerate the contents** + +Quantified above: 1 of 48 live project URLs present; 8 of 9 sitemap entries are +not projects. Two of those 8 are internal strategy reports under +`/assets/reports/`, published and submitted for crawling. + +The sitemap must become a **build artifact of the manifest**, never hand-authored. + +### F-004 · Project discovery requires JavaScript +**Severity: Medium** · **Classification: preserve the hub, fix the build** + +No project anchors in the served HTML. Filtering, search, and presentation can +stay client-side; the `` set must be pre-rendered. + +### F-005 · Ops Hub v0.5 is the correct architecture and should not be rewritten +**Severity: High (opportunity)** · **Classification: migrate** + +The Ops Hub generation lineage already established the right separation: + +``` +per-project state file → registry → generated hub → history + health panel +``` + +with candidate discovery, manual promotion, and a no-auto-registration default. +This is the only generation that treated the dashboard as a *rendered view* rather +than as the source of truth. v2 should be a **hardening** of it, not a rewrite. + +Two known defects to carry forward as requirements: registry paths were fragile +across sync-backed directories, and **two parallel registry files existed in +different locations** — a split-brain the v2 schema must make structurally +impossible (one canonical registry, path resolved relative to the repo root). + +### F-006 · Governance already defines the missing truth model +**Severity: Medium (opportunity)** · **Classification: preserve** + +The dashboard standards governance record already specifies canonical source +hierarchy, conflict badges, freshness SLAs, and the rule that raw capture never +owns verified truth. v2 should **import** these rules rather than reinvent them. + +### F-007 · One page's `noindex` may be correct, and nothing records that +**Severity: Medium** · **Classification: decision required** + +`/act-two-catering/` carries `noindex,nofollow` and is marked `unlisted`. This is +the *only* unlisted page whose live directive matches its declared visibility. + +It cannot be graded without knowing intent. If it is a live commercial page, this +is a serious defect. If it is a client preview, it is the one correctly configured +page on the property. **The audit's real finding is that intent is not recorded +anywhere** — so a correct configuration and a broken one are indistinguishable. + +v2 requires an explicit `indexPolicy` with a reason string, so this question is +answered by the registry rather than by inference. + +### F-008 · Duplicate hub URL +**Severity: Low** · **Classification: unlink or canonicalize** + +`/` and `/public-hub.html` both appear in the sitemap, share an identical +`` (`Dave Robertson — Projects`), serve different bodies (91 KB vs 31 KB), +and **each self-canonicalizes to its own URL**. Two documents assert they are both +the canonical version of the same titled page. + +Pick one canonical; redirect the other or point its canonical at the winner. + +--- + +## Artifact classification + +Grouped by disposition. Internal-only artifacts are described by role. + +### Preserve — active or canonical +| Artifact | Reason | +|---|---| +| Public portfolio hub (site root) | Correct product, defective build. Fix, don't replace. | +| `project-manifest.json` | Correct primitive. Split public/private concerns (F-002). | +| Dashboard standards governance record | Already defines v2's truth model (F-006). | +| Portfolio manifest reconciliation log | Most recent authority on manifest curation rules. | +| Standard audit procedure | Method reference for future passes. | +| Routing matrix for dev-hub surfaces | Routing rules migrate directly into v2. | + +### Migrate — source material for v2 +| Artifact | What survives | +|---|---| +| Ops Hub switchboard + v0.5 hardening + v0.1–v0.8 dev log | **The architecture itself** (F-005) — registry-driven generation, state reads, history, candidate discovery, health panel | +| Per-project `ops-state.json` pattern | The single strongest reusable primitive. Becomes schema v2. | +| Per-project ops-dashboard initializer | The per-project contract. Update for provenance + visibility. | +| `dashboard-registry.json` | Seeds the v2 registry. Must be de-duplicated to one canonical file. | +| Quick Start Command Center spec | **Provenance model only** — `{value, confidence, source, verifiedAt}` and freshness decay. Its published output is F-001. | +| Cross-Project Scan Dashboard | **Taxonomy only** — Security / Drift / Operability / Reuse. Becomes v2's audit lane. | +| Code Dashboard page template | **Layout only** — Top 3, Blocked, Registry, Open Threads. Not its stale content. | +| Operator Dashboard spec | **Five-zone layout and visibility gates only.** Its full integration surface is out of scope for v2 seed. | + +### Archive — freeze as historical, remove from active navigation +Code Dashboard page-breakdown template and its March snapshots; the earlier +unified dashboard sync mechanism; the generated cross-project scan HTML artifact; +the June daily command-center notes; the command-center UI/live-data context +update (*after* its publishing lessons are captured in F-001); the prior +context-bucket audit prompts. + +These are useful provenance and misleading authority. Label them historical. + +### Unlink — remove from public navigation and sitemap, no deletion required +`/public-hub.html` · `/assets/reports/*` · `/assets/brand/brand-palette.html` +(unless deliberately public) · `/codedash-mar24/` · every `candidate` entry not +deliberately public. + +Pending review before they stay linked: `/home-comic-dashboard/`, +`/daily-prophet/`, `/clever-paws/` — all three serve 200 with no robots meta. + +### Remove from public +`/command-center-061eed/` and its published mirror, plus any surface exposing +project queues, audit state, run identifiers, local paths, or client references. +**Fix the generator, not the output** (F-001). + +--- + +## Severity roll-up + +| Severity | Finding | +|---|---| +| 🔴 Critical | F-001 — internal ops console publicly indexable, actively republished | +| 🟠 High | F-002 public/private contract conflation · F-003 orphaned sitemap · F-005 architecture at risk of rewrite | +| 🟡 Medium | F-004 JS-only discovery · F-006 unimported governance · F-007 unrecorded index intent | +| ⚪ Low | F-008 duplicate hub URL | + +--- + +## Remediation bundles + +None of this has been executed. Approve per bundle. + +<details> +<summary><strong>Bundle A — Safety.</strong> Closes the confirmed exposure.</summary> + +1. Add `noindex,nofollow` to the command center **in its generator**, so the next + automated republish preserves it rather than reverting it. +2. Flip its manifest entry off `visibility: public` so the hub stops linking it. +3. Decide whether it should remain on public hosting at all. +4. Review `/home-comic-dashboard/`, `/daily-prophet/`, `/clever-paws/` for + content sensitivity before they stay publicly linked. +5. Only after `noindex` has been crawled, consider a `robots.txt` disallow. + +**Do not reorder step 5 ahead of step 1** — a disallow blocks the crawl that the +`noindex` depends on. +</details> + +<details> +<summary><strong>Bundle B — Archive.</strong> Stops stale surfaces reading as current.</summary> + +Mark the Code Dashboard lineage and the June command-center notes historical; +retain the cross-project scan as a dated snapshot; retain Ops Hub v0.5 as +migration source. +</details> + +<details> +<summary><strong>Bundle C — Build v2.</strong> Per <a href="./OPS_HUB_V2_SPEC.md">the spec</a>.</summary> + +Schemas, registry seed, validators, and a read-only private dashboard skeleton. +Depends on Bundle A only in sequencing — v2 should not be built on top of a live +exposure. +</details> + +## Recommended order + +**Bundle A first.** It is the only confirmed high-severity exposure, it is being +actively re-published by automation, and it is cheap. Bundle C then inherits a +clean boundary instead of encoding the current one. + +Tradeoff: the v2 build waits one pass. Worth it — v2's central invariant is the +public/private boundary, and building it while that boundary is knowingly broken +would bake the defect into the schema. diff --git a/docs/ops-hub-v2/OPS_HUB_V2_SPEC.md b/docs/ops-hub-v2/OPS_HUB_V2_SPEC.md new file mode 100644 index 0000000..614c429 --- /dev/null +++ b/docs/ops-hub-v2/OPS_HUB_V2_SPEC.md @@ -0,0 +1,357 @@ +# Ops Hub v2 — Build Spec + +**Status:** Draft for approval · **Date:** 2026-08-18 +**Companion:** [Cross-Project Dashboard Surface Audit](./AUDIT-2026-08-18.md) + +## 1. Product definition + +Ops Hub v2 is the **private, registry-driven operating view** over Dave's +software, web, AI, and systems work. It answers five questions and nothing else: + +1. What exists? +2. What is active? +3. What is broken or stale? +4. What needs Dave next? +5. What is safe to publish publicly? + +## 2. Non-goals + +Ops Hub v2 is **not** a public portfolio, a project management system, a +replacement for the workspace project hubs, a replacement for GitHub/Vercel/ +Cloudflare consoles, a secrets store, or a public mirror of internal state. + +Explicitly deferred from the seed build: multi-service live integrations, +mutations/write-back, and public/admin dual-mode rendering. The audit found that +every prior generation failed by widening before the contract underneath was +solid. v2 earns scope by first proving the contract. + +## 3. The one rule + +> **The dashboard is a renderer, never an authority.** + +Five previous generations were built as surfaces and each drifted into being +treated as truth. v2 inverts this: registries and state contracts own truth, and +every rendered fact carries its source. + +| Truth | Owning source | +|---|---| +| Project identity | Workspace projects database | +| Verified status | Status-check runs database | +| Tasks and open loops | Action items database | +| Build/deploy evidence | Development logs database | +| Session narrative | Conversation notes database | +| Internal ops state | Private registry + per-project `ops-state.json` | +| Public project listing | Sanitized public manifest | + +If two sources disagree, v2 renders **both with a conflict badge**. It never +silently picks a winner. + +## 4. Architecture + +``` +workspace databases ─┐ +GitHub / deploy ├─→ Ops Registry (private, canonical) +per-project state ───┘ │ + ├─→ Private Ops Hub (full internal detail) + │ + └─→ export-public-manifest (allowlist, fails closed) + │ + ├─→ Public portfolio hub + └─→ sitemap.xml +``` + +The public hub is generated **from the sanitized export**, never from the private +registry. There is no code path from internal state to a public surface that does +not pass through the allowlist. + +## 5. Invariants + +These are the reason v2 exists. Each is a **CI check that fails the build**, not +a review-time convention. Every one maps to a finding in the audit. + +| # | Invariant | Audit finding | +|---|---|---| +| **I-1** | `visibility != "public"` ⟹ the built page carries `<meta name="robots" content="noindex,nofollow">` | F-001, root cause | +| **I-2** | No internal surface is reachable from any public build output | F-001 | +| **I-3** | The public manifest is derived from an **allowlist**. An unlisted field is dropped, never passed through | F-002 | +| **I-4** | `sitemap.xml` is generated from the public manifest. Hand edits fail the build | F-003 | +| **I-5** | Every public project has a real `<a href>` in the **served** HTML, before JavaScript | F-004 | +| **I-6** | Exactly one canonical registry file, path-resolved from the repo root | F-005 | +| **I-7** | Every rendered fact carries `source` + `verifiedAt`. Unsourced facts cannot render green | F-006 | +| **I-8** | Every entry declares `indexPolicy` with a reason. There is no default | F-007 | +| **I-9** | Ambiguous privacy flags **fail the export**. Never publish-by-default | F-002 | + +**I-9 is the load-bearing one.** Every prior generation defaulted to publish and +relied on review to catch mistakes. Review did not catch them. + +### I-1 belongs in the generator + +The audit found the command center is regenerated and republished by recurring +automation. A directive added to published output is reverted on the next pass. +**Every invariant is enforced at generation time.** Verifying the output is a +regression test, not the fix. + +## 6. Repository layout + +Private repository — `ops-hub`. Not this one; see §11. + +``` +ops-hub/ +├── registry/ +│ ├── ops-registry.json # canonical, single file (I-6) +│ ├── ops-registry.schema.json +│ ├── public-export.allowlist.json # field allowlist (I-3) +│ └── visibility-policy.md +├── schemas/ +│ ├── ops-state.schema.json +│ └── audit-finding.schema.json +├── adapters/ # notion/ github/ pages/ local-state/ +├── scripts/ +│ ├── validate-registry.mjs +│ ├── collect-state.mjs +│ ├── build-hub.mjs +│ ├── export-public-manifest.mjs +│ └── audit-public-safety.mjs # enforces I-1, I-2, I-5 +├── dist/ +│ ├── ops-hub.html # private +│ ├── ops-snapshot.json +│ ├── source-health.json +│ └── public-project-manifest.json # sanitized (I-3) +└── docs/ +``` + +## 7. Registry schema + +One row per project. `visibility`, `indexPolicy`, and `privacy` are **required** — +there is no inferred default (I-8, I-9). + +```jsonc +{ + "schemaVersion": 2, + "id": "garden-os", + "name": "Garden OS", + "domain": "software", + + "visibility": "public", // public | unlisted | private | archived + "lifecycle": "active", // active | maintenance | paused | candidate | retired + "tier": 1, + + "indexPolicy": { // REQUIRED — no default (I-8) + "directive": "index,follow", // index,follow | noindex,nofollow + "reason": "Public flagship project page", + "decidedAt": "2026-08-18" + }, + + "canonicalRecord": "<workspace project page>", + "repo": { "provider": "github", "owner": "…", "name": "garden-os", "visibility": "public" }, + "urls": { "public": "https://…/garden-os/", "admin": null, "dashboard": null }, + + "state": { + "statePath": "projects/garden-os/ops-state.json", // repo-relative (I-6) + "staleAfterMinutes": 1440, + "required": true + }, + + "publicExport": { // the ONLY fields that may reach a public surface (I-3) + "allowed": true, + "title": "Garden OS", + "description": "Public-safe description", + "tags": ["game", "simulation"], + "canonicalUrl": "https://…/garden-os/" + }, + + "privacy": { + "containsInternalData": false, + "containsClientData": false, + "publicSafe": true, + "reviewedAt": "2026-08-18" // absent or stale ⟹ export fails (I-9) + } +} +``` + +**Why `publicExport` is a nested object rather than flat fields:** the exporter +copies that subtree and nothing else. Adding a field to the top level can never +accidentally publish it — the failure mode behind F-002, made structurally +impossible. + +## 8. `ops-state.json` schema v2 + +Per-project, machine-written. The durable primitive — a project's dashboard HTML +is an optional drilldown, its state file is not. + +```jsonc +{ + "schemaVersion": 2, + "project": { "id": "garden-os", "visibility": "public", "lifecycle": "active" }, + + "status": { + "light": "yellow", // green | yellow | red | gray + "label": "Needs deploy verification", + "confidence": "likely", // confirmed | likely | unknown + "verifiedAt": "2026-08-18T09:00:00-04:00", + "source": "status-check-runs" // required (I-7) + }, + + "freshness": { + "updatedAt": "2026-08-18T09:00:00-04:00", + "staleAfterMinutes": 1440, + "state": "fresh" // fresh | aging | stale | missing | unknown + }, + + "kpis": [ { "label": "Live URL", "value": "200", "status": "ok", + "confidence": "confirmed", "source": "curl", "verifiedAt": "…" } ], + "issues": [ { "id": "seo-static-content", "severity": "P1", "status": "open", + "title": "…", "source": "…", "recommendedAction": "…" } ], + "links": [ { "label": "Public site", "url": "https://…", "visibility": "public" } ], + "facts": [ { "key": "public_indexable", "value": true, + "confidence": "confirmed", "source": "curl", "verifiedAt": "…" } ], + + "privacy": { "containsInternalData": false, "publicSafe": true }, + "metadata": { "generator": "scripts/collect-state.mjs", "generatedAt": "…" } +} +``` + +### Confidence and freshness rules + +- A fact with `confidence: "unknown"` **may render**, but its tile cannot be green + (I-7). Uncertainty is displayed, not hidden. +- Past `staleAfterMinutes`, status degrades to `aging`, then `stale`. A stale + green renders as gray — **an old success is not a current one**. +- A missing state file renders as `missing`, distinct from `red`. + **Absence of data is not evidence of failure**, and v2 must never conflate them: + conflating them is what trains an operator to ignore the dashboard. + +## 9. UI zones + +Five zones, in priority order. Zone 1 is the reason the product exists. + +**Zone 1 — Safety rail.** Public/private boundary violations, invariant failures, +failed state collection, overdue P0/P1. Empty when healthy; nothing else may +occupy this space. Seeded with the audit's open items. + +**Zone 2 — Top 3.** Maximum three cards. Each shows source, confidence, and one +next action. No hand-written prose — a card that cannot cite a source cannot appear. + +**Zone 3 — Project grid.** Grouped: active production · active development · +watchlist · archive candidates · internal systems · public surfaces. Each tile: +status light, verified timestamp, next action, open P0/P1 count, public-safety +badge, freshness badge. + +**Zone 4 — Audit and drift lane.** Inherits the Cross-Project Scan taxonomy: +Security · Drift · Operability · Public exposure · Registry mismatch · Stale state. + +**Zone 5 — Source health footer.** Always visible: registry freshness, per-source +freshness, last state collection, last public export, invariant check results, +stale-state count, unmapped-project count. + +Zone 5 is permanent and non-collapsible. Every prior generation degraded by +looking authoritative while its inputs went stale; the footer makes staleness +impossible to miss. + +## 10. Public/private field policy + +**Never exported, at any visibility:** local paths · run identifiers · internal +audit state · client names · task queues · blocker text · session notes · secrets +inventory state · device/host names · private record URLs · raw `ops-state.json`. + +**Exportable, only via `publicExport`:** name · public description · public URL · +public repo URL · sanitized tags · category · lifecycle label · screenshot/OG +image · demo link. + +The exporter is an **allowlist copy**, not a denylist filter (I-3). A field that +is not named is not exported, including fields that do not exist yet. + +> [!NOTE] +> The current public manifest carries `localPath` and `runCommand` on all 79 rows. +> Both are empty today. Under I-3 they are not exported at all, so the channel +> closes rather than staying empty-by-luck. + +## 11. Where this lives + +Ops Hub v2 belongs in a **private** repository. + +`DaveHomeAssist/code-dashboard` — this repo — is public, and is itself an artifact +in the audited lineage (it drives the Notion Code Dashboard page). This spec and +the audit are hosted here because both are public-safe by construction: they +describe architecture and cite only already-public evidence. + +**The registry, state files, and rendered private dashboard must not land here.** +Publishing them would reproduce F-001 in a new location. + +## 12. Build order + +Each session ends with something verifiable. No session ships a surface before the +contract under it. + +<details open> +<summary><strong>Session 1 — Foundation</strong></summary> + +Schemas + validator + 5-project registry seed +(hub · Garden OS · PixelForge · MixMash · Ops Hub itself) ++ allowlist + a read-only dashboard skeleton reading real state. + +**Done when:** `validate-registry.mjs` passes on the seed and fails on a row with +a missing `privacy.reviewedAt`. +</details> + +<details> +<summary><strong>Session 2 — Migration</strong></summary> + +Import v0.5 registry entries, de-duplicating to one canonical file (I-6). Convert +existing state files to schema v2. Add `audit-public-safety.mjs`. Wire the safety +rail and source-health footer. + +**Done when:** the safety rail reproduces the audit's open findings from live data, +with no hand-authored entries. +</details> + +<details> +<summary><strong>Session 3 — Public separation</strong></summary> + +Generate `public-project-manifest.json` from the allowlist. **Emit a diff against +the existing manifest — do not write over it.** Add sitemap generation (I-4), +static link pre-rendering (I-5), and the unlisted ⟹ noindex invariant (I-1). + +**Done when:** the diff is reviewed and approved by a human, and a deliberately +mis-flagged fixture row fails the export (I-9). +</details> + +<details> +<summary><strong>Session 4 — Automation</strong></summary> + +Scheduled state collection, history log, stale-source badges. Decide the refresh +owner: scheduled CI, local scheduler, or manual. + +**Done when:** a full unattended refresh runs, and an injected invariant violation +fails it. +</details> + +## 13. Acceptance criteria + +v2 is accepted when **all** hold: + +- [ ] No private or internal surface is reachable from any public build output +- [ ] Every registry row has `visibility`, `lifecycle`, `indexPolicy`, `privacy.publicSafe`, and `canonicalRecord` +- [ ] Every active project has a valid `ops-state.json` v2, or an explicit `state.required: false` +- [ ] Every displayed status shows confidence and freshness +- [ ] The public manifest is allowlist-generated; export **fails closed** on ambiguity +- [ ] `sitemap.xml` is generated and covers every public project URL +- [ ] Every public project has a static `<a href>` before JavaScript runs +- [ ] `visibility != public` ⟹ `noindex,nofollow`, verified against the **built output** +- [ ] Archived Code Dashboard surfaces are labeled historical and out of active navigation +- [ ] `/command-center-061eed/` is no longer publicly indexable internal content + +### Verification + +Each criterion has a check that runs in CI and fails the build: + +| Check | Enforces | +|---|---| +| `validate-registry.mjs` — schema + required fields | I-6, I-8, I-9 | +| `audit-public-safety.mjs` — crawl built output for robots directives, internal links, static anchors | I-1, I-2, I-5 | +| `export-public-manifest.mjs --check` — assert exported keys ⊆ allowlist | I-3 | +| sitemap diff — generated vs committed | I-4 | +| state schema validation across all rows | I-7 | + +An invariant without a failing check is a convention, and conventions are what the +previous five generations already tried.