# Grant Application — Cradle CIP Spark builder

**Nav:** Investor portal → **AI Coach** → the **Grant Application** tab (`/ai/grant-application`).

## What it does

Turns a member's codebase into a complete, editable Cradle CIP Spark grant application.
The member uploads their project as a ZIP; the analyzer reads README / manifests / a sample
of source files; ONE AI pass drafts Section A (proposed name + the three category picks with
reasons), every Section C product field, a TRL assessment with reasoning, and an RM100,000
funding breakdown where every line names the roadmap module it delivers. The member edits each
field in place (live word counts against Cradle's 150-word cap), balances the funding table,
generates Section B from rough experience notes, and copies field-by-field into Cradle's form.
**The applicant is the signed-in member — no name is ever asked for** (2026-08-26, user's call).

Ported from the user's standalone "Cradle CIP Section C AI Generator" (Manus/tRPC app, delivered
as a ZIP via /file/f2421f6a-…). The prompt engineering carried over verbatim: field formulas
(Problem = WHO+PAIN+COST+WHY NOW …), golden-thread consistency, sparse-repo TRL rule,
banned-word list, and the EXACT Cradle dropdown strings (including the form's own
"Encyption" typo — do not fix it, the member copies the value across).

## How it works

- `src/Grant/GrantApplication` — one row per generated application. `content` (JSON) is the
  source of truth; `proposed_name` / `trl_level` / `funding_amount` are promoted by
  `summarize()` for the list strip. Standard key model (uuid + blame + soft delete).
- `src/Grant/CradleOptions` — the three category lists, verbatim.
- `src/Grant/Services/CradleRepoAnalyzer` — PHP ZipArchive port of the Node analyzer
  (root-prefix strip, ignore dirs, tech-stack detection from deps + marker files, README/source
  excerpts, has* flags) + `contextBlock()` for the user message.
- Prompts: `resources/prompts/cradle_application.md` (Section A+C; categories inlined, repo
  context moved to the USER message since our registry's system prompts are static) and
  `cradle_section_b.md`. Keys `AiRequest::PROMPT_CRADLE_APPLICATION` / `PROMPT_CRADLE_SECTION_B`
  in `config/ai_prompts.php` — admin-editable like every prompt.
- `Main\Portal\GrantApplicationController`: index / generate (RateLimiter 10/day +
  route throttle, `set_time_limit(240)` for the AI pass, `sanitize()` clamps off-list
  categories, TRL 1-9, and rebalances the breakdown's last line to the ask) / update
  (debounced autosave) / sectionB (JSON, 20/day) / destroy. Bills the COMPANY key
  (membership includes the tool — same reasoning as the plan reviewer).
- Page `Pages/Main/Portal/Grant/Index.vue` + `Partials/FieldCard.vue` (word-cap + copy) +
  `Partials/FundingTable.vue` (delivers column, live total vs ask). **Three screens in one page**
  (2026-08-26 restructure, user's design): LIST (a table of applications; empty → one create
  CTA), UPLOAD (the single "Step 1" ZIP screen), EDITOR (a stateful TAB STRIP — Section A / TRL /
  Section C with five field sub-tabs / Funding / Section B / Pitch Deck). Tabs follow the wealth
  canvas's colour grammar: green tick = passes, RED ! = needs the member (Section B unwritten,
  deck not generated, field empty/over 150 words, funding unbalanced) — the strip IS the to-do
  list. `?open=` selects an application, ShowTabs-style replaceState.
- **"Ask AI" on every tab** — the grant coach: the SAME `AskPlan` panel (new `askUrl` prop aims
  it at `POST {id}/ask`, `GrantAskController` — an SSE clone of the wealth ask: snapshot in the
  body, refusals as JSON, thread saved to AI Chatbot as "Cradle grant · …"). The snapshot is the
  application AS EDITED NOW plus the active tab + per-tab red/green status, so "is this good
  enough?" is judged against what the member sees. Prompt: `PROMPT_CRADLE_ASK`
  (`resources/prompts/cradle_ask.md`) — evaluator-bar judgement, ONE concrete fix, no invention.
- The uploaded ZIP is analyzed from the temp upload and **never stored**; the stored `analysis`
  drops README/source excerpts.

## Related files

- `database/migrations/2026_08_26_100000_create_grant_applications_table.php`
- `tests/Feature/Main/Portal/Grant/GrantApplicationTest.php` — 6 tests: generation from a real
  in-memory ZIP with a faked AiClient, category clamp + breakdown rebalance, non-zip rejection,
  edit round-trip, ownership, index scoping.

## Pitch deck (added same day)

The source app's deck generator IS ported. `POST {id}/deck` (10/day, `thinking: minimal`) runs
`PROMPT_CRADLE_DECK` over the member's **edited** fields → DeckContent JSON → **rendered
SERVER-side** (2026-08-26, second pass): `Src\Grant\Services\DeckRenderer` runs
`scripts/render-cradle-deck.mjs` (Node + the ONE pptxgenjs renderer,
`resources/js/utils/grant/renderDeck.js`) for the .pptx, then LibreOffice (`soffice --headless`,
per-run `UserInstallation` profile or concurrent runs deadlock) for a PDF. Both files are stored
via `MediaService::storeFromPath` (COLLECTION_UPLOAD → served by `/file/{uuid}`), and their
uuids persisted onto `content.deckPptx` / `content.deckPdf` server-side. The Pitch Deck tab
shows the PDF in an iframe — the ONLY preview that cannot drift from the file (an HTML slide
recreation was shipped first and rejected: "the slide format is out"); downloads are plain file
links, including a "Ready" chip on the list table. Legacy client-rendered decks (deckContent
only, no uuids) get a "regenerate for preview" hint + client re-render fallback for download.
Renderer verified AS www-data (mod_php's user); cleanup uses scandir (glob misses LibreOffice's
dotfile profile).

## Traps learned on day one

- **A silently disabled button reads as "not functioning."** Section B's button was disabled
  until notes ≥ 20 chars with no explanation — zero requests ever reached the endpoint. The
  button is now always enabled and explains itself on a short click.
- **In-request AI calls need `thinking: minimal` + `set_time_limit`** (the AiReview controller's
  documented trap) — Section B and deck both pass it now.
- The tinker HTTP kernel harness currently HANGS on this box even for a plain GET — don't burn
  time on it; the Apache access log answered in seconds what the harness couldn't.
