Add structured review targets to human gates - #671
Conversation
|
Reviewer note: This is intentionally opt-in. A human gate with |
There was a problem hiding this comment.
Pull request overview
Adds an opt-in, typed “review target” contract for human gates (review_target=true) so workflows can attach a validated external document link that is propagated through events/projections/API and rendered consistently across clients (web, Slack, console), with corresponding OpenAPI + docs updates.
Changes:
- Introduces
ReviewTarget/ReviewTargetKindinfabro-typeswith validation + roundtrip tests, and threads it through interview records and workflow events. - Updates the OpenAPI schema and generated Rust/TypeScript API clients to include
review_targeton interview questions. - Renders review targets as external links in the web UI and Slack, and prints explicit URL context in console-based interview flows; documents the workflow contract.
Reviewed changes
Copilot reviewed 41 out of 42 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| lib/packages/fabro-api-client/src/models/review-target.ts | Adds generated TS ReviewTarget model. |
| lib/packages/fabro-api-client/src/models/review-target-kind.ts | Adds generated TS ReviewTargetKind enum. |
| lib/packages/fabro-api-client/src/models/interview-question-record.ts | Adds review_target to interview question record model. |
| lib/packages/fabro-api-client/src/models/index.ts | Re-exports new review target models. |
| lib/packages/fabro-api-client/src/models/api-question.ts | Adds review_target to pending question model. |
| lib/packages/fabro-api-client/src/.openapi-generator/FILES | Tracks new generated files. |
| lib/foundation/fabro-types/src/run_event/misc.rs | Adds review_target to interview started event props. |
| lib/foundation/fabro-types/src/lib.rs | Re-exports review target types/errors from interview. |
| lib/foundation/fabro-types/src/interview.rs | Implements ReviewTarget validation + serde, and wires into InterviewQuestionRecord. |
| lib/foundation/fabro-types/src/graph.rs | Adds Node::review_target() attribute accessor + tests. |
| lib/foundation/fabro-types/Cargo.toml | Adds thiserror dependency for typed validation errors. |
| lib/foundation/fabro-api/tests/pending_interview_record_round_trip.rs | Extends API roundtrip test JSON with review_target. |
| lib/foundation/fabro-api/tests/interview_question_record_round_trip.rs | Asserts canonical-type reuse + JSON parity for review target types. |
| lib/foundation/fabro-api/src/lib.rs | Exposes ReviewTarget* via generated types module. |
| lib/foundation/fabro-api/build.rs | Reuses canonical fabro_types::{ReviewTarget, ReviewTargetKind} in API generation. |
| lib/components/fabro-workflow/src/interview_runtime.rs | Snapshots review_target into interview-started props. |
| lib/components/fabro-workflow/src/handler/human.rs | Opt-in gate behavior: reads context.review_target, validates, sets question text/target, and snapshots into events; adds tests. |
| lib/components/fabro-workflow/src/event/events.rs | Adds review_target field to InterviewStarted event variant. |
| lib/components/fabro-workflow/src/event/convert.rs | Converts review_target through event → event body. |
| lib/components/fabro-workflow/src/context.rs | Adds review_target context key constant and internal-key assertions. |
| lib/components/fabro-validate/src/rules/inert_attribute.rs | Allows review_target attribute only on human nodes + tests. |
| lib/components/fabro-store/tests/serializable_projection.rs | Updates projection test fixtures to include review_target: None. |
| lib/components/fabro-store/src/run_state.rs | Projects review_target into pending interviews; adds coverage in reducer tests. |
| lib/components/fabro-slack/src/blocks.rs | Renders review target as the primary link in Slack question header + tests. |
| lib/components/fabro-interview/src/lib.rs | Adds review_target to runtime Question struct (serde + defaults). |
| lib/components/fabro-interview/src/console.rs | Prints explicit review target line (label + URL) for TTY and non-TTY console flows + tests. |
| lib/apps/fabro-server/src/server/tests.rs | Updates server tests to include/expose review_target through endpoints. |
| lib/apps/fabro-server/src/server.rs | Threads review_target into Slack notifications + API/runtime question projections. |
| lib/apps/fabro-server/src/demo/mod.rs | Updates demo API questions to include review_target: None. |
| lib/apps/fabro-cli/src/commands/run/runner.rs | Updates CLI tests for updated question shape. |
| lib/apps/fabro-cli/src/commands/run/attach.rs | Carries review_target from API to runtime question; prints explicit review target line. |
| docs/public/workflows/human-in-the-loop.mdx | Documents workflow contract and expected question/link behavior. |
| docs/public/reference/dot-language.mdx | Documents review_target DOT attribute on human nodes. |
| docs/public/execution/context.mdx | Documents the review_target context key convention. |
| docs/public/api-reference/fabro-api.yaml | Adds ReviewTarget* schemas and review_target fields to question shapes. |
| Cargo.lock | Adds thiserror to the lockfile. |
| apps/fabro-web/app/components/stage-renderers/human-qa.tsx | Uses ReviewTargetQuestion when a review target is present. |
| apps/fabro-web/app/components/stage-renderers/helpers.ts | Parses review_target from interview.started events into UI model. |
| apps/fabro-web/app/components/stage-renderers/helpers.test.ts | Adds test coverage for preserving review_target in parsed interview pairs. |
| apps/fabro-web/app/components/review-target-question.tsx | New component: safely renders external link for review targets with validation/fallback. |
| apps/fabro-web/app/components/interview-dock.tsx | Renders dock question text via ReviewTargetQuestion with link when safe. |
| apps/fabro-web/app/components/interview-dock.test.tsx | Tests safe linking and unsafe-target fallback behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The review question sentence was written in four places and the URL safety rules in three. Collapse each to one definition. - Add `ReviewTarget::question_text_with_link` as the single definition of the question wording. `question_text()` and the Slack header both use it, so a wording change is now one edit. - Delete `ReviewTargetKind::noun()`. The enum already derives `strum::Display` with the same snake_case output. - Share one `review_target_line` helper between the console interviewer and the CLI attach client, which held a byte-identical copy. Print only the URL: `question.text` already carries the label and the noun. - Trim the web-side check to the URL scheme, host, and credentials, which are what a raw `href` can act on. Label length and control characters cannot affect the DOM and stay server-side. - Split validation from presentation in the web UI. `safeReviewTarget` returns the target or null, and each caller picks its own fallback, so an unsafe target now falls back to the same Markdown rendering as a question with no target. - Derive the resource noun from `kind` in the web UI instead of hardcoding "document". - Use `ReviewTargetKind.DOCUMENT` and the shared `isRecord` guard when parsing events, instead of a raw string and a hand-rolled object check that accepted arrays. - Drop `deny_unknown_fields` from the wire struct. The OpenAPI schema leaves `additionalProperties` permissive, so an added field would otherwise make persisted events unreadable. - Import `ReviewTarget` by name, and stop naming Slack in a fabro-types error message. - Document that `review_target=true` replaces the gate's `label`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Ran a simplification pass over the branch (reuse, quality, and efficiency reviews) and pushed one refactor commit. The main finding was duplication: the review question sentence was written in four places and the URL safety rules in three.
The efficiency review found nothing worth changing: validation costs ~242 ns in Rust and runs about a dozen times per run, and neither web call site is on a hot render path. Three findings I did not act on, for a separate change:
Verification: 7,411 workspace tests pass, clippy and fmt clean, web typecheck and production build pass. The web suite has 13 pre-existing failures in |
`slack_link` builds `<url|label>`, and `escape_slack_controls` covers Slack's documented escapes (`&`, `<`, `>`) but not `|`. Slack has no escape for `|`, so a label containing one splits the markup and can make Slack reject the block. `is_safe_slack_link_url` already guards the URL half against `|`; the label half was unguarded. It did not matter before because the only labels were "Open in Fabro" and a PR number. Review target labels are model-authored, so this is now reachable. Replace `|` inside link labels, which keeps the link working. Plain-text labels are untouched, since `|` is fine outside link markup. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Claude Code Review
Claude Code Review is paused for this repository. To reconnect it, an admin of this repository's GitHub organization (or the account owner, for personal repositories) who can also manage your Claude organization's Code Review settings needs to re-link GitHub in Code Review settings. This is a one-time step.
Tip: disable this comment in your organization's Code Review settings.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 41 out of 42 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
lib/components/fabro-slack/src/blocks.rs:443
slack_link()escapes Slack control characters in the label but interpolates the URL verbatim into<url|label>. Slack’s mrkdwn formatting treats&as an escape character, so a valid review target URL with query params (e.g....?a=1&b=2) can produce malformed markup / rejected blocks. Escaping the URL with the sameescape_slack_controls()used elsewhere keeps the link syntax intact while making&safe.
fn slack_link(url: &str, label: &str) -> String {
if is_safe_slack_link_url(url) {
let label = escape_slack_controls(label).replace('|', "/");
format!("<{url}|{label}>")
} else {
escape_slack_controls(label)
}
Conflict in apps/fabro-web/app/components/interview-dock.tsx. Main moved the dock onto the shared collapsible `RunDockShell` and replaced the local button constants with shared ones. Kept main's structure whole and re-applied the review target rendering onto it: the question paragraph in the shell's `body` becomes the linked `ReviewTargetQuestion` when the target passes `safeReviewTarget`, and plain text otherwise. Both now share main's paragraph classes through `QUESTION_TEXT`, so the two renderings stay visually identical. `peek` keeps using `question.text`, which is the correct plain-text collapsed summary for a review target question. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 41 out of 42 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
apps/fabro-web/app/components/stage-renderers/helpers.ts:93
parseReviewTargetcheckskind !== ReviewTargetKind.DOCUMENT, but then returns{ label, url, kind }wherekindis still typed as a generalstring(fromgetString). That’s not safely assignable to the generatedReviewTarget.kind(a string-literal union), and can cause TypeScript to lose the literal type or fail typechecking depending on settings. Prefer returning the literal constant when the check passes.
const kind = getString(value, "kind");
if (!label || !url || kind !== ReviewTargetKind.DOCUMENT) return null;
return { label, url, kind };
}
lib/foundation/fabro-types/src/interview.rs:74
- The doc comment says
question_text_with_linkis the “single definition” of the review question wording, but the web UI also hardcodes/mirrors this sentence inapps/fabro-web/app/components/review-target-question.tsx. This wording is still canonical server-side, but it’s not literally the only definition in the repo, so the comment is misleading.
Summary
Testing