Refresh Current state for v0.1.0:82; document render-smoke build gate

Record the v82 vendor+SRI + render-smoke work in durable docs: packaging guide
gains the verified-build gate + re-vendor instructions; Current state rewritten
and compressed for v82; ROADMAP logs the deferred pre-compile-JSX alternative.
This commit is contained in:
Keysat
2026-06-16 16:43:10 -05:00
parent 40a0270a99
commit c29ac2f2ee
3 changed files with 23 additions and 6 deletions
+6 -6
View File
@@ -101,14 +101,14 @@ Subsystem rules live in `docs/guides/` and lazy-load in Claude Code via `.claude
## Current state ## Current state
_Phase 0 substrate + Phase 1 thesis/outreach are built; **box and repo at v0.1.0:81** (latest: **Communications tab is matched-only** — the email-activity panel now surfaces only email linked to a known investor/contact; unmatched cold/unknown-sender email is captured but never shown; prior v80: repurposed the tab into the admin-only captured-Gmail search over the `email_*` tables). **Decision (2026-06-16): the fundraising grid + email capture is the canonical system of record** — vestigial classic-CRM surfaces get pruned or repurposed (see `ROADMAP.md` → "Consolidate on the fundraising grid as canonical"). Longer-term backlog: `ROADMAP.md`._ _Phase 0 substrate + Phase 1 thesis/outreach are built; **box and repo at v0.1.0:82** (latest: **front-end libs vendored + SRI-pinned and a jsdom render smoke check now gates every build** — closes the v78/v79 blank-screen class; prior v81: Communications tab is matched-only; v80: that tab became the admin-only captured-Gmail panel). **Decision (2026-06-16): the fundraising grid + email capture is the canonical system of record** — vestigial classic-CRM surfaces get pruned or repurposed (see `ROADMAP.md` → "Consolidate on the fundraising grid as canonical"). Longer-term backlog: `ROADMAP.md`._
- **Working (all draft-only):** CRM + ingest (chunk→embed→Qdrant + retrieval) + redaction boundary; Gmail capture (DWD) + email-activity propose→approve; Thesis Workshop + Architect (Claude) with dual-approval gate; Outreach Draft Assistant + follow-up radar + per-user voice + Tier-B in-thread Gmail draft creation. - **Working (all draft-only):** CRM + ingest (chunk→embed→Qdrant + retrieval) + redaction boundary; Gmail capture (DWD) + email-activity propose→approve; Thesis Workshop + Architect (Claude) with dual-approval gate; Outreach Draft Assistant + follow-up radar + per-user voice + Tier-B in-thread Gmail draft creation.
- **Deployed & verified live: v0.1.0:81** (box `$START9_BOX_HOST`/immense-voyage.local; `installed-version`→`0.1.0:81`, migration chain `…80→81` clean, server up on `:8080`, schedulers + Gmail integration up). **v0.1.0:81 makes the Communications tab matched-only:** `query_email_activity` now gates on `EXISTS(email_investor_links)`, so the panel surfaces only email linked to a known investor/contact; unmatched cold/unknown-sender email is still captured (metadata-only) and will appear automatically if its sender is later added as an investor — a read-side filter, no schema/capture change. Graveyard investors unaffected (their email has a link), still hidden from the picker but visible/searchable as an audit surface. Backend-only (frontend `index.html` byte-identical to v80, which was render-verified). **Prior — v0.1.0:80 repurposed the Communications tab into the admin-only email-activity panel:** new `GET /api/email/activity` (admin-enforced server-side) over the `email_*` tables, filterable by investor / mailbox / direction + free-text search; soft-delete honored on the per-mailbox sighting; direction decided at the email level (mirrors `digest_builder`); graveyard investors hidden from the picker but their email stays visible + searchable (audit surface). The classic manual "Log Communication" form was retired (the grid context menu remains the manual-log path); nav item + page are admin-only. Query lives in `email_integration/db.py:query_email_activity`; tests in `email_integration/test_email_activity_panel.py`. **Prior — v0.1.0:79 was a P0 hotfix:** the page loaded `@babel/standalone` from unpkg **unpinned**, so the CDN served **Babel 8.0.0**, whose `@babel/preset-react` automatic JSX runtime prepends an ESM `import {jsx} from "react/jsx-runtime"` — illegal in this classic (non-module) inline `<script>`, so the browser rejected the whole bundle and React never mounted → **blank screen for every user**. Fix: pin `@babel/standalone@7.29.7` (classic runtime; verified via headless render locally + on the box). Same release closed **3 server-side admin gaps** from a permissions audit — `GET /api/users`, `/api/email/status`, `/api/email/accounts` were UI-hidden from members but not API-enforced; all now `require_admin` (write endpoints were already gated). **Prior — v0.1.0:78 retired `lp_profiles` + the orphaned LP Tracker** (endpoints/handlers/lp-breakdown report/contact-dossier LP section/frontend component+redirect removed; empty table left in place per never-hard-delete) and **repointed the Dashboard "Total Committed"** onto `fundraising_investors.total_invested` (graveyard-excluded; "Total Funded" dropped — the grid has no funded concept). **Digest is fully live:** capture (DWD) → propose→approve; transport routes Gmail-DWD→SMTP (no app password); and **daily activity digest (Phase B)** — `digest_builder.py` (by-team-member Spark narrative + by-investor section, soft-delete filtered) + always-on `digest_scheduler.py` reading a DB policy + `send-now`. **Auto-send defaults OFF** (env seed unset → `app_settings.digest_policy` off) until Grant enables it in Settings → Admin. Detail: `docs/guides/email.md`. - **Deployed & verified live: v0.1.0:82** (box `$START9_BOX_HOST`/immense-voyage.local; `installed-version``0.1.0:82`, migration chain `…81→82` clean, server up on `:8080`, schedulers + Gmail integration up). **v82 vendored React 18.3.1 / ReactDOM 18.3.1 / @babel/standalone 7.29.7 into `frontend/assets/vendor/`**, served same-origin with `sha384` SRI (no CDN, no outbound-internet dependency to render the UI), and added **`start9/0.4/render-smoke.mjs`** — a jsdom check (shipped-Babel transform asserts classic/non-module + parseable; real mount asserts the login UI renders) wired into the default `make` goal (`verified-build`), so every build is gated on the frontend actually rendering. Closes the v78 (blank screen) + v79 (Babel-8 ESM-import) class structurally. Detail: `docs/guides/packaging.md`. **Prior shipped & live:** v81 Communications-tab matched-only (`query_email_activity` gates on `EXISTS(email_investor_links)`; unmatched email captured but never shown; `docs/guides/email.md`); v80 admin-only email-activity panel (`GET /api/email/activity`); v78 retired `lp_profiles`/LP Tracker + repointed Dashboard "Total Committed" onto the grid (graveyard-excluded). **Digest fully live:** capture (DWD) → propose→approve; Gmail-DWD→SMTP transport; daily Phase-B digest (`digest_builder.py` + always-on `digest_scheduler.py` reading a DB policy + `send-now`); **auto-send defaults OFF** until Grant enables it in Settings → Admin. Detail: `docs/guides/email.md`.
- **Live since v74 (2026-06-13):** login works; `/assets/` traversal 404s (plain + URL-encoded), root health 200. On boot, `ensure_thesis_v2_promoted` makes the v2.0 reserve-asset spine the working *approved* spine (node-level, reversible). Security/privacy hardening (path-traversal close, outreach NER backstop, get-by-id soft-delete) shipped in v74 — detail in `EVALUATION.md`. - **Live since v74 (2026-06-13):** login works; `/assets/` traversal 404s (plain + URL-encoded), root health 200. On boot, `ensure_thesis_v2_promoted` makes the v2.0 reserve-asset spine the working *approved* spine (node-level, reversible). Security/privacy hardening (path-traversal close, outreach NER backstop, get-by-id soft-delete) shipped in v74 — detail in `EVALUATION.md`.
- **Tests (2026-06-16):** **22/22 backend tests green** via `python3 backend/run_tests.py` (`email_integration/test_email_activity_panel.py` updated for v81: matched-only scope — unmatched email never surfaces, not even by free-text search — plus investor/mailbox/search/direction filters, per-sighting soft-delete, email-level direction, mailbox + investor roll-ups, graveyard hidden-from-picker-but-visible, facets, route 401/403 admin enforcement; prior: `test_dashboard_report.py`, `test_digest_builder.py`). `py_compile` clean. Frontend render checked locally (jsdom mount + pinned-Babel transform). The 2 stale thesis tests stay fixed (seed structure in `docs/guides/thesis.md`). - **Tests (2026-06-16):** **22/22 backend tests green** via `python3 backend/run_tests.py`, `py_compile` clean (`test_email_activity_panel.py` covers v81 matched-only scope + the panel filters/facets/admin-enforcement). Frontend now has a **scripted render smoke check**`cd start9/0.4 && make render-smoke` (jsdom mount + shipped-Babel transform; negative-controlled) — that gates the default `make` build. The 2 stale thesis tests stay fixed (seed structure in `docs/guides/thesis.md`).
- **Decided, not yet built (detail in `ROADMAP.md`):** Pipeline adoption + a grid flag that auto-loads flagged investors as opportunities; NL→safe-query feature; CRM as canonical thesis backbone with the signal-engine reading from it (reconciliation unwired); reply-all for Tier-B drafts (currently reply to the LP only). *(Done v80: the admin-only per-investor/per-mailbox email-activity panel; v81: made that panel matched-only.)* - **Decided, not yet built (detail in `ROADMAP.md`):** Pipeline adoption + a grid flag that auto-loads flagged investors as opportunities; NL→safe-query feature; CRM as canonical thesis backbone with the signal-engine reading from it (reconciliation unwired); reply-all for Tier-B drafts (currently reply to the LP only). *(Done this session: v81 matched-only Communications panel; v82 vendor+SRI front-end libs + scripted render-smoke build gate.)*
- **Known debt (P2, not deploy-blocking):** **reports-subsystem soft-delete sweep** — `handle_pipeline_report` + remaining report/aggregate queries over opportunities/communications still count soft-deleted rows (v78 shrank this surface: the `lp_profiles`/lp-breakdown aggregates are gone and the dashboard "Total Committed" is now grid-sourced); needs a pass + report-endpoint tests. Also `?limit=abc` crashes the request thread (authenticated list path); scrub-gateway TLS verify off; `cryptography==42.0.5`; **front-end CDN libs still loaded from unpkg without SRI** — Babel is now version-pinned (v79, after an unpinned auto-upgrade to Babel 8 blanked the whole UI), but React/Babel should be **vendored into the package + SRI-pinned** so a CDN can never swap prod deps again; **deploy verification must include a browser-render smoke check** — v78's blank UI shipped as "verified live" because the checks were server-up/curl only, which can't catch a client render failure; stale user-visible `start9/0.4/assets/ABOUT.md`; hardcoded Spark/Qdrant IPs in the s9pk; the 5.4k-line `server.py` monolith. P3 batch + full list in `EVALUATION.md`. - **Known debt (P2, not deploy-blocking):** **reports-subsystem soft-delete sweep**`handle_pipeline_report` + remaining report/aggregate queries over opportunities/communications still count soft-deleted rows (v78 shrank this surface: the `lp_profiles`/lp-breakdown aggregates are gone and the dashboard "Total Committed" is now grid-sourced); needs a pass + report-endpoint tests. Also `?limit=abc` crashes the request thread (authenticated list path); scrub-gateway TLS verify off; `cryptography==42.0.5`; stale user-visible `start9/0.4/assets/ABOUT.md`; hardcoded Spark/Qdrant IPs in the s9pk; the 5.4k-line `server.py` monolith. P3 batch + full list in `EVALUATION.md`. *(Resolved v82: front-end CDN/SRI risk — libs vendored + SRI-pinned — and the render smoke check is now scripted into the build.)*
- **Doc drift to reconcile:** `crm-overview.md` + `EVALUATION.md` still describe `lp_profiles` as a live model in places — a doc-auditor pass should align them to "grid canonical, `lp_profiles` retired." - **Doc drift to reconcile:** `crm-overview.md` + `EVALUATION.md` still describe `lp_profiles` as a live model in places — a doc-auditor pass should align them to "grid canonical, `lp_profiles` retired."
- **Other gaps:** the v2.0 spine is the *working* spine but **not a canonical `thesis_version`** (needs Grant + Jonathan dual sign-off); Appendix-A conviction/exposure (incl. ~40% Strike) stay Grant's working read, not canonical, not fed to the engine; live features (Claude/Qdrant/Gmail) unverified on the box. - **Other gaps:** the v2.0 spine is the *working* spine but **not a canonical `thesis_version`** (needs Grant + Jonathan dual sign-off); Appendix-A conviction/exposure (incl. ~40% Strike) stay Grant's working read, not canonical, not fed to the engine; live features (Claude/Qdrant/Gmail) unverified on the box.
- **Next:** 1) **Vendor + SRI-pin the front-end libs** (serve React/Babel from the package, integrity-checked) so a CDN can never swap prod deps again, **and script the render smoke check into deploy-verify** — a working jsdom-mount + pinned-Babel-transform check was run manually for v80 (catches the v78/v79 blank-screen class); wire it into the build/install flow; 2) add an **auth regression test** asserting the 3 v79-gated GET endpoints (`/api/users`, `/api/email/status`, `/api/email/accounts`) reject members (v80 added the analogous test for `/api/email/activity`); 3) Grant validates digest Phase B on the box — Settings→Admin **Send Digest Now**, then tick **Send automatically every day**; 4) **reports-subsystem soft-delete sweep** + report-endpoint tests; 5) **Pipeline adoption** — grid flag → auto-load opportunities; 6) `?limit=abc` crash; 7) **NL→safe-query** (separate, larger); 8) Grant + Jonathan freeze v2.0 canonical; 9) build reply-all. - **Next:** 1) add an **auth regression test** asserting the 3 v79-gated GET endpoints (`/api/users`, `/api/email/status`, `/api/email/accounts`) reject members (v80 added the analogous test for `/api/email/activity`); 2) Grant validates digest Phase B on the box — Settings→Admin **Send Digest Now**, then tick **Send automatically every day**; 3) **reports-subsystem soft-delete sweep** + report-endpoint tests; 4) **Pipeline adoption** — grid flag → auto-load opportunities; 5) `?limit=abc` crash; 6) **NL→safe-query** (separate, larger); 7) Grant + Jonathan freeze v2.0 canonical; 8) build reply-all. *(Logged to ROADMAP: a build step that pre-compiles JSX to drop runtime Babel entirely — bigger, contradicts the "no build step" convention.)*
+5
View File
@@ -140,6 +140,11 @@ Open design questions (settled at build time): send time = **6 PM box-local** (c
**Keep the Contacts table — as the read-only per-person directory it already is.** Confirmed 2026-06-16: the grid models **investor entity → many people** correctly today. The grid "contacts" column is a multi-pill editor; each pill syncs to a `fundraising_contacts` row AND its own classic `contacts` row (5-person family office → 1 investor + 5 contacts, linked via `fundraising_contacts.contact_id`, migration 0004). The Contacts page is **read-only for creation** (header: "added from the Fundraising Grid"; no New-Contact button), edit-only via the detail slide-over — the desired flow already holds. Email capture already rolls **multiple people up to one investor** (matcher indexes each pill's email separately, all → same `fundraising_investor_id`; `email_investor_links` records both investor and specific person). No build here — future email-surfacing UI should present comms grouped by investor across all its people. **Keep the Contacts table — as the read-only per-person directory it already is.** Confirmed 2026-06-16: the grid models **investor entity → many people** correctly today. The grid "contacts" column is a multi-pill editor; each pill syncs to a `fundraising_contacts` row AND its own classic `contacts` row (5-person family office → 1 investor + 5 contacts, linked via `fundraising_contacts.contact_id`, migration 0004). The Contacts page is **read-only for creation** (header: "added from the Fundraising Grid"; no New-Contact button), edit-only via the detail slide-over — the desired flow already holds. Email capture already rolls **multiple people up to one investor** (matcher indexes each pill's email separately, all → same `fundraising_investor_id`; `email_investor_links` records both investor and specific person). No build here — future email-surfacing UI should present comms grouped by investor across all its people.
### Front-end: pre-compile JSX, drop runtime Babel (optional, larger)
*Logged 2026-06-16 during the v0.1.0:82 vendor+SRI work. The scoped fix shipped: React/ReactDOM/Babel are now vendored + SRI-pinned and served same-origin, and a jsdom render smoke check gates every build (`docs/guides/packaging.md`). This is the bigger alternative we deliberately deferred.*
Today the app ships `@babel/standalone` (~3 MB) and transforms ~5k lines of inline JSX **in the browser on every page load**. A build step that pre-compiles the JSX to plain JS would (a) eliminate the runtime-transform blank-screen class entirely (no Babel in production), and (b) load much faster. **Cost:** it introduces a build step, which contradicts the current **"No build step"** convention (single `frontend/index.html`, inline-Babel React) — so this is a real architecture change, not a tweak. Weigh only if page-load size/latency or render robustness becomes a felt problem; the render-smoke gate already de-risks the status quo. If taken: keep the source `index.html` editable, emit a compiled artifact into the s9pk, and keep the smoke check pointed at the built output.
## Definition of done for "Airtable substitute" v1 ## Definition of done for "Airtable substitute" v1
- Team can manage all investors in one master table - Team can manage all investors in one master table
- Saved views replicate current Airtable workflows - Saved views replicate current Airtable workflows
+12
View File
@@ -21,6 +21,18 @@ Start9 0.4.x ignores a same-version rebuild (the install silently does nothing).
cd start9/0.4 && make # -> ten-database_x86_64.s9pk cd start9/0.4 && make # -> ten-database_x86_64.s9pk
``` ```
- The default `make` goal is `verified-build`: it runs the **frontend render smoke check**
(`start9/0.4/render-smoke.mjs`, via jsdom) *before* packing, so a build that can't render
fails fast. Run it standalone with `make render-smoke` (or `node render-smoke.mjs`). It
(1) transforms the app's inline JSX with the **shipped** Babel and asserts a classic,
non-module, parseable script — catching the v79 Babel-8 ESM-import regression — and
(2) mounts the app in jsdom and asserts the login UI renders — catching the v78 blank
screen. `jsdom` is a build-time devDependency (not shipped in the image); `npm ci` pulls
it. **Front-end libs are vendored + SRI-pinned** in `frontend/assets/vendor/` (React,
ReactDOM, Babel) and served same-origin — never re-point them at a CDN. If you re-vendor,
regenerate each `integrity="sha384-…"` in `frontend/index.html` with
`openssl dgst -sha384 -binary FILE | openssl base64 -A`.
## Install — PRODUCTION ## Install — PRODUCTION
```bash ```bash