githubnext/chopinPublic

Let's compose rich plans together

Show where a research child came from #381

MergedMaggieAppletonmerged 3 commits intomainfromrefs/pull/381/head
  1. 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 ![before desktop parent](https://github.com/user-attachments/assets/6297d949-274a-4ff4-9739-84ada42741db) ![before desktop child](https://github.com/user-attachments/assets/cd5a1ef2-a3fc-4c96-b28c-c0928254b023)

    After, desktop: Chat inherited, empty-state line, provenance with brief open ![after desktop child](https://github.com/user-attachments/assets/1e4d7f43-1657-49d9-b316-93d23e801272) ![after desktop child brief](https://github.com/user-attachments/assets/999ea579-af0e-4184-9d71-103b80994ec0)

    After, desktop: the parent link returns to the focused research card ![after desktop returned](https://github.com/user-attachments/assets/943edfca-addf-4235-98f0-cbb759548067)

    Before / after, phone ![before phone child](https://github.com/user-attachments/assets/03568d16-bb4c-4be4-9829-e261218bb615) ![after phone child](https://github.com/user-attachments/assets/1b3e38be-11e8-4bb6-812c-d7d675b1101b) ![after phone chat](https://github.com/user-attachments/assets/cf22aff8-43a8-4223-957b-3195dde1b967)

    ## 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)

  2. MA
    Author

    **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)

  3. MA
    Author

    **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.

  4. MaggieAppletonhead-ref-force-pushed 8eca419682d1last month on GitHub

  5. MA
    Author

    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).

  6. MA
    Author

    Hold lifted.

  7. MaggieAppletonhead-ref-force-pushed 84a80b1652e3last month on GitHub

  8. MaggieAppletonmerged 6c81f7df48a4last month on GitHub

  9. MaggieAppletonclosedlast month on GitHub

  10. MaggieAppletonhead-ref-deletedlast month on GitHub