Skip to content

Show where a research child came from - #381

Merged
MaggieAppleton merged 3 commits into
mainfrom
design/child-document-provenance
Oct 8, 2026
Merged

MaggieAppleton merged 3 commits into
mainfrom
design/child-document-provenance

Conversation

@MaggieAppleton

Copy link
Copy Markdown
Collaborator

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 · by · 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
before desktop child

After, desktop: Chat inherited, empty-state line, provenance with brief open
after desktop child
after desktop child brief

After, desktop: the parent link returns to the focused research card
after desktop returned

Before / after, phone
before phone child
after phone child
after phone chat

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

@MaggieAppleton

Copy link
Copy Markdown
Collaborator 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

@MaggieAppleton

Copy link
Copy Markdown
Collaborator 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.

@MaggieAppleton
MaggieAppleton force-pushed the design/child-document-provenance branch from ffcd97c to 8eca419 Compare October 8, 2026 01:54
@MaggieAppleton

Copy link
Copy Markdown
Collaborator 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).

@MaggieAppleton

Copy link
Copy Markdown
Collaborator Author

Hold lifted.

MaggieAppleton and others added 3 commits October 8, 2026 03:50
A research child now opens with a quiet provenance line naming its parent,
who started it and its source count, with the brief one click away. The
parent name returns to the research card that produced it. The child's
Chat starts in the parent's open or closed state and explains itself when
empty. The server exposes a read-only provenance route for a child.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Verify the parent shares the child's repository node ID before returning
provenance, cover the route's success and denial cases, drop the unused
start time, keep each meta segment on one line, balance the empty Chat
line, and use spacing tokens for the provenance styles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaggieAppleton
MaggieAppleton force-pushed the design/child-document-provenance branch from 8eca419 to 84a80b1 Compare October 8, 2026 02:51
@MaggieAppleton
MaggieAppleton merged commit 6c81f7d into main Oct 8, 2026
3 checks passed
@MaggieAppleton
MaggieAppleton deleted the design/child-document-provenance branch October 8, 2026 03:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant