Show where a research child came from #381
- MA
## Why
A research child had no provenance. It opened with Chat collapsed even when the parent had Chat open, its Chat was a blank pane with no explanation, and nothing said where the report came from or linked back to the card that produced it.
## What changed
- **Provenance line** at the top of a research child, in the research card's own vocabulary (sparkle badge, tertiary text): "Research from **<parent>** · by <who> · N sources", with a **Brief** disclosure that reveals the exact brief. - **Back to the card**: the parent name closes the child and returns focus to (and scrolls to) the parent's research card for that request, using the existing child-close focus restoration with an opener override. - **Chat inherits the parent's state**: a child starts with the saved desktop Chat preference (the parent's) but still never saves its own. - **Empty child Chat** shows one calm line: "Discuss this report here. Messages stay with the report." - **Server (read only)**: `GET /api/channels/:childId/research-provenance` returns the brief, source count, starter handle and request id for a published child, via a new `research.findPublished(parentId, childId)` storage lookup (memory + PostgreSQL, covered by the shared publication contract). A child without a research request gets 404 and shows no line. - `PlanEditor` gains a host-owned `preface` slot in the prose column; when it changes height it announces a scroll so comment markers re-measure.
No child research, Background Work, tasks, grandchildren, or research-workspace threads are introduced.
## Screenshots
Before, desktop: parent with Chat open → child opens with Chat collapsed, no provenance  
After, desktop: Chat inherited, empty-state line, provenance with brief open  
After, desktop: the parent link returns to the focused research card 
Before / after, phone   
## Testing
- `bun run types` — pass - `bun test --timeout 30000 apps/web packages/editor apps/server/src/research` — 1070 pass, 1 fail: `placement-close.test.ts` times out identically on `main` (pre-existing, unrelated) - `bun test apps/server/src/storage` — pass (includes the new `findPublished` contract assertions) - `bun run ci` — fails only on the design-contract exceptions below - E2E updated: `research-child-surface.e2e.ts` (child Chat now inherits the parent's open state) and `research-child-recovery.e2e.ts` (new provenance, brief, empty Chat and return-to-card assertions). Local E2E is suspended for this run; relying on CI's browser-integration job.
## Needs a design-contract review
`bun run ci` reports reviewed dynamic owners changed (no new style or class logic; only props/data flow moved):
- `apps/web/src/document-workspace-host.tsx` (`scripts/design-contract/exceptions/dynamic-web.json`): the child `workspaceProps(...)` case now includes the `parent` provenance object; the old exact case is stale. - `apps/web/src/workspace.tsx` lines 150/161/179/188/385 (`dynamic-web.json`): owner hash changed because `initialWorkspaceState` lost its unused profile argument. - `packages/editor/src/plan-editor.tsx` lines 356/367 (`dynamic-editor.json`): owner hash changed because of the new `preface` prop and its resize effect.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
- MA
**Hold — please don't merge yet.** Adds a new read route; an authorization-focused review is in progress. I'll post the verdict here.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
- MA
**Review:** Looks good
I checked the code and ran it on a fake-GitHub server with the seeded parent and research child. I took screenshots at desktop 1440×900 and phone 390×844.
**Security (new `GET /api/channels/:childId/research-provenance`).** No blocking issues. - It uses the same `channelAccess` as the other research routes. That means a session, the App-installation repository lookup, `repository.id === channel.repositoryId` (node ID is authoritative), and `pull`. Probes I ran: - Unauthenticated: 401. - Parent id, random UUID, or malformed id: 404. - Child renamed to a repo the caller can't read: 404. - Child owner/name pointed at another readable repo while its node id stayed `R_score`: 404. - Read-only user: 200. That's expected, since the brief is already visible on the parent card to the same readers. - POST: 405. - `parentChannelId` comes from the server's child record, not from the caller. The lookup is scoped by `(channel_id = parent, published_channel_id = child)`. The Postgres query is a parameterized tagged template, and `published_channel_id` is UNIQUE. The read path (`request` → `#requestView`) has no side effects, so no terminal work gets restarted. - Defence in depth (non-blocking): the route trusts the stored parent link and never checks the parent's repository. When I renamed the *parent* to an unreadable repo, the child still returned the parent's brief. Publication keeps parent and child in one repo, so this only matters for corrupt data. A cheap `parent.repositoryId === access.channel.repositoryId` check in `routes.ts` would close it.
**Tests** - `apps/server/src/research/routes.test.ts:630`: only the top-level 404 is asserted for the new route. Please add a 200 case and the revoked/no-access case to the same block where access is revoked a few lines below, so route-level authorization is pinned rather than inherited by accident. - `Research.Provenance.startedAt` (`packages/protocol/research.d.ts`) is returned but never used by the client. Remove it until something needs it.
**UX** - Desktop is calm and reads like the research card's status line. The Brief disclosure is correct (`aria-expanded`/`aria-controls`, hidden by default), and the brief indents to the text column. - The parent link closes the child and focuses the parent's research card with a visible ring. It scrolls into view on desktop (top 297 of 900) and phone (348 of 844). - The child Chat inherits the open state, and the empty line is quiet. - Phone nit: the meta line wraps mid-phrase ("· by" / "e2e · 1 source"). Wrap each `· by X` and `· N sources` segment in `white-space: nowrap` so it breaks between segments. - Phone nit: the empty Chat line orphans "report." `text-wrap: balance` on `[data-chat-empty]` would fix it. - Minor: `navigation.css` adds raw `0.5rem`/`0.25rem`/`2rem`/`3px`/`1.5`. Use spacing and leading tokens where equivalents exist.
**CI.** `format, lint, types, tests` fails only on the design-contract review hashes already listed in the PR body. e2e and container pass. Locally, `bun test apps/server/src/research` has one failure, the known `placement-close` timeout, which also fails on main.
MaggieAppletonhead-ref-force-pushed 8eca419682d1last month on GitHub
- MA
Thanks for the review. Follow-ups are in ffcd97c (rebased onto main as 8eca419):
1. **Defence in depth:** `research-provenance` now loads the parent channel and returns 404 unless its repository node ID equals the child's, before calling the service. 2. **Route tests:** added coverage for 200, parent-repository mismatch (404), revoked pull access (404), unaffiliated (404) and unauthenticated (401). The test also asserts the service lookup runs only once. 3. **`startedAt`:** removed from `Research.Provenance` and the service. 4. **Phone wrapping:** each meta segment ("· by …", "· N sources") is `white-space: nowrap`, so wrapping happens only between segments. 5. **Empty Chat line:** now uses `text-balance`. 6. **Tokens:** the provenance styles in `navigation.css` now use `--spacing` multiples, `--edge-width` and `--text-sm--line-height` instead of raw values.
Rebasing resolved a conflict with main's new `onState` prop on `PlanEditor`; both props are kept. Checks: e2e and container pass. Validation still fails only on the design-contract exceptions listed in the PR body (line numbers moved after the rebase: workspace.tsx 152/163/181/190/388, plan-editor.tsx 362/374, plus document-workspace-host.tsx).
- MA
Hold lifted.
MaggieAppletonhead-ref-force-pushed 84a80b1652e3last month on GitHub
MaggieAppletonmerged 6c81f7df48a4last month on GitHub
MaggieAppletonhead-ref-deletedlast month on GitHub