# Floom apps audit · actual input/output contracts
_Drafted 2026-04-14 · verifying v10 wireframe against `/root/floom-monorepo`_

## Summary

- Apps audited: **15 / 15** (all in `examples/`, all seeded live at preview.floom.dev)
- All 15 are `type: hosted` Docker images. **Zero `proxied`. Zero use OpenAPI.**
- `bulk-run` is **NOT** in the monorepo. The 15th slot is **`openblog`**.
- Input styles: **2 prompt** (flyfast, openslides.generate is really hybrid), **13 form**, **0 strict hybrid**
- Apps with an internal LLM parser in the container: **1** (flyfast, inferred from reused image)
- Pure deterministic (no LLM anywhere): **5** — blast-radius, dep-check, hook-stats, session-recall, claude-wrapped, plus opengtm
- Gemini-backed business logic (not parsers): **8** — bouncer, openanalytics, openblog, opencontext, opendraft, openkeyword, openslides (plus flyfast internally)
- External API via token (not OpenAPI): **1** — openpaper → api.openpaper.dev

## Contract format: docs lie, reality is Floom v2 manifest

`spec/protocol.md` and `README.md` claim "OpenAPI derives every surface". **Reality**: each app ships `floom.yaml` with `manifest_version: "2.0"` — Floom's own schema. Shape: `{name, description, runtime, python_dependencies, secrets_needed, actions: { <name>: {label, description, inputs[], outputs[]} }}`.

Valid input types (`services/manifest.ts:15`): `text | textarea | url | number | enum | boolean | date | file`. Output types: `text | json | table | number | html | markdown | pdf | image | file`.

`services/openapi-ingest.ts` exists and CAN ingest `apps.yaml` for proxied OpenAPI apps into v2 manifests at boot, but **no live app uses it**. Dormant.

`services/parser.ts` is the **platform LLM parser**: `gpt-4o-mini` + `OPENAI_API_KEY` maps a natural-language prompt to the action's input schema. **Federico's "no platform AI" rule deletes this file and `routes/parse.ts`.**

## Rendering pipeline (today)

1. `/p/:slug` → `AppPermalinkPage.tsx`. Route is `/p/`, **not** `/r/`. Only `/`, `/apps`, `/p/:slug`, `/protocol` exist in `main.tsx`.
2. `GET /api/hub/:slug` returns the normalized manifest from SQLite.
3. Run tab mounts `<FloomApp standalone showSidebar=false>`, which picks the first action only: `Object.keys(actions)[0] || 'run'`. **Extra actions invisible.**
4. `InputField` switches on `spec.type`: textarea→textarea, enum→select, number→number input, boolean→checkbox, url→url input, default→text input. **100% data-driven, zero per-app frontend code.** No OpenAPI parsing in the web app.
5. Run → `POST /api/run {app_slug, action, inputs}` → `runner.ts` dispatches to `runAppContainer()` (hosted) or `runProxied()` (proxied).
6. Python entrypoint in container emits `__FLOOM_RESULT__{json}`. SSE streams to `OutputPanel.tsx`, rendered by `OutputSpec.type`.

**Whether `/p/flyfast` shows one textarea or six fields is 100% decided by the manifest. Nothing in code is app-specific.**

## Per-app audit

| # | Slug | Display | Action(s) | Inputs (first action) | Output types | Style | Backend | Internal LLM? |
|---|------|---------|-----------|----------------------|--------------|-------|---------|---------------|
| 1 | flyfast | FlyFast | `search` | `prompt: textarea*` | json | prompt | Reused `floom-app-flyfast-mkt:v3`, hits api.flyfast.app w/ `FLYFAST_INTERNAL_TOKEN` | **yes** (inferred; Gemini parser baked into the container, not exposed at Floom layer) |
| 2 | openpaper | OpenPaper | `start_paper_generation`, `check_paper_status`, `list_my_papers` | `topic: textarea*, level: text="Graduate", pages: text="15-20", citation_style: text="APA 7th", language: text="English", context_notes: textarea` | text × 3 | form | Python → api.openpaper.dev w/ `OPENPAPER_API_TOKEN` | no |
| 3 | openanalytics | OpenAnalytics | `health_check`, `mentions_check` | `url: url*` | markdown, number, json | form | Python + Gemini (`GEMINI_API_KEY`) | no (Gemini = business logic) |
| 4 | opengtm | OpenGTM | `aeo_health`, `score_lead` | `url: url*, timeout: number=30` | text, number, json × 2 | form | Pure Python (httpx + BS4), no secrets | no |
| 5 | claude-wrapped | Claude Wrapped | `generate` | `jsonl_sessions: textarea*, author: text, project_slug: text` | number, html | form | Pure Python, no deps, no secrets | no |
| 6 | bouncer | Bouncer | `audit` | `assistant_text: textarea*, diff_text: textarea, task_context: textarea, model: text="gemini-3.1-pro-preview"` | number, text × 2, json, text | form | Python → Gemini (`GEMINI_API_KEY`) | no |
| 7 | blast-radius | Blast Radius | `analyze` | `repo_url: url*, base_branch: text="HEAD~5", head_ref: text="HEAD"` | text, json × 3 | form | Pure Python + bash + `apt: jq`, no secrets | no |
| 8 | dep-check | Dep Check | `analyze` | `repo_url: url*, branch: text` | number, json | form | Pure Python + bash + `apt: jq`, no secrets | no |
| 9 | hook-stats | Hook Stats | `analyze` | `log_content: textarea*` | number × 2, json × 3, text | prompt-ish (one big textarea) | Pure Python, no deps | no |
| 10 | session-recall | Session Recall | `search`, `recent`, `report` | `jsonl_session: textarea*, keywords: text*, max_results: number=15` | text | form | Pure Python, no deps | no |
| 11 | opencontext | OpenContext | `analyze` | `url: url*, additional_context: textarea` | text, markdown, json | form | Python → Gemini w/ Search grounding (`GEMINI_API_KEY`) | no |
| 12 | opendraft | OpenDraft | `generate` | `topic: textarea*, level: text="research_paper", style: text="apa", blurb: text` | html, markdown, text, number | form | Python → Gemini (`GOOGLE_API_KEY`) | no |
| 13 | openkeyword | OpenKeyword | `research` | `company_url: url*, company_name: text, target_count: number=50, language: text="en", region: text="us"` | markdown, table, json × 2 | form | Python → Gemini, 5-stage pipeline (`GEMINI_API_KEY`) | no |
| 14 | openslides | OpenSlides | `generate`, `iterate`, `resolve_logo` | `prompt: textarea*, company_url: url, audience: text="vc", deck_type: text="pitch"` | html, number, json, text, text, number | **hybrid** (prompt + 3 knobs) | Python → Gemini + Pillow + PyPDF2 (`GEMINI_API_KEY`) | no |
| 15 | openblog | OpenBlog | `generate`, `refresh` | `url: url*, keywords: textarea*` | html, markdown, json | form | Python → Gemini (`GEMINI_API_KEY`) | no |

`*` = required. Quoted defaults are the literal string defaults in the yaml.

## Mismatches with the v10 wireframe

1. **Route**: wireframe `/r/:slug` → reality `/p/:slug`. `/r/` does not exist in React Router.
2. **OpenAPI story**: both `spec/protocol.md` and `README.md` promise OpenAPI-driven everything. **Zero of 15 launch apps comply.** All use Floom v2 manifests. Either the 15 must be rewritten or the pitch must say "Floom manifest v2 OR OpenAPI".
3. **Platform AI**: `routes/parse.ts` + `services/parser.ts` call `gpt-4o-mini` to pre-fill forms from a prompt. Federico's "no platform AI" rule requires deleting both. The wireframe chat flow cannot assume this pre-fill exists.
4. **FlyFast is the only prompt-shaped app**: everything else is a typed form. Wireframe probably assumed the prompt-box pattern is the default. It isn't.
5. **Multi-action apps render only the first action.** `FloomApp.tsx` line 59 hardcodes `Object.keys(actions)[0]`. Hidden actions today: openpaper × 2, openanalytics × 1, opengtm × 1, session-recall × 2, openslides × 2, openblog × 1. If the wireframe shows an "action switcher", the code doesn't have one.
6. **Enum regression**: `openpaper.{level,pages,citation_style,language}` and `opendraft.{level,style}` should be `enum` with `options`, but are declared as `text` with allowed values only in `description`. They render as freeform text inputs. Manifest bug.
7. **`file` input type is broken**: `openblog.refresh` uses `type: file`, but `InputField` has no `file` case — falls through to default `text`. Latent bug.
8. **PDF output is wrong type**: `openslides.generate.pdf_base64` is declared as `text`, not `pdf`. No download UI. `OutputPanel`'s `pdf` type is never used by any live app.
9. **`bulk-run` does not exist in the monorepo.** Drop it. The 15 are: flyfast, openpaper, openanalytics, opengtm, claude-wrapped, bouncer, blast-radius, dep-check, hook-stats, session-recall, opencontext, opendraft, openkeyword, openslides, **openblog**.
10. **`knowledge_enabled: true` on 4 apps** (openanalytics, opencontext, openkeyword, openblog) is rendered nowhere. Future "upload knowledge" feature that does not exist yet.

## Recommendations for v10 wireframe

- [ ] Rename `/r/:slug` → `/p/:slug` everywhere in the wireframe, or add `/r/` as a router alias.
- [ ] Add an **input-style pill** on every store card: `PROMPT` | `FORM` | `HYBRID`. Only 1 of 15 is pure prompt.
- [ ] Add an **action switcher** on `/p/:slug` for apps with multiple actions (6 of 15 have hidden actions today).
- [ ] Decide the OpenAPI gap publicly: rewrite 15 apps to OpenAPI, or grandfather v2 as "the native format".
- [ ] Remove the `/api/parse` pre-fill from the wireframe chat flow; users fill forms themselves.
- [ ] Convert `level/pages/citation_style/language/style` in openpaper.yaml and opendraft.yaml from `text` to `enum` with `options`.
- [ ] Implement `file` input handler in `FloomApp.tsx` / `AppInputsCard.tsx` before showing `openblog.refresh`.
- [ ] Swap `openslides.generate.pdf_base64` from `text` to `pdf` OutputSpec; `OutputPanel` needs a download button.
- [ ] Drop `bulk-run` from every store list; use `openblog` as #15.
- [ ] Show a "BYO key" badge on store cards when `secrets_needed.length > 0` (true for 10 of 15: 6 GEMINI, 1 GOOGLE, 1 OPENPAPER, 1 FLYFAST, plus bouncer).

## 3 things I'm least confident about

1. **FlyFast's internal parser**: inferred from the reused docker image `floom-app-flyfast-mkt:v3` + the rumor, not from reading the handler. The monorepo only contains `examples/flyfast/floom.yaml`, not the Python code. If the container doesn't actually call Gemini, the "has LLM parser: yes" claim is wrong. Verify by docker-inspecting the image or reading `~/opensky-app/`.
2. **Multi-action UI**: I read `FloomApp.tsx` line 59 and `AppPermalinkPage.tsx` line 156 — both pick `Object.keys(actions)[0] || 'run'`. I did not do a browser check on `/p/openpaper` to confirm only one form is visible. There may be a switcher elsewhere I did not trace.
3. **OpenAPI ingest dormancy**: I assume `openapi-ingest.ts` is dead because no `apps.yaml` is checked in and seed.json is clearly primary. If the server reads `FLOOM_APPS_CONFIG` env at boot in a self-host deployment, proxied apps could appear at runtime. Did not grep for that env var in full.
