Skip to content

Commit b6f073f

Browse files
fix(webapp): preserve complete practice feedback responses (#1698)
1 parent f2dc35a commit b6f073f

41 files changed

Lines changed: 133 additions & 410 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"hephaestus": patch
3+
---
4+
5+
Disputing feedback now waits for the required explanation before saving the response, and selected practices and expanded observations remain available when a practice-group page is refreshed or shared.

.changeset/practice-standing-feedback-api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44

55
The application API can now answer where a developer stands in each practice group: the current standing and its guidance, how the group has developed across recently reviewed work, which kinds of work contributed feedback, a filterable observation history, and the complete review runs behind it. An undecided observation remains visible in that history without being presented as a verdict. Developers can also replace or delete their response to delivered feedback, recording whether it was helpful, how they handled it, and an optional explanation. Every endpoint answers only for the signed-in developer.
66

7-
**Operators:** direct API callers must replace `/practice-areas` with `/practice-groups`, use the corresponding group schema and field names, replace `/practices/developer` with `/practices/reviewed`, and move from the older reaction endpoint to the combined response endpoint. Existing response history is preserved; `MIGRATION.md` lists the contract changes. The generated Hephaestus web client is updated in the same release.
7+
**Operators:** direct API callers must replace `/practice-areas` with `/practice-groups`, use the corresponding group schema and field names, replace `/practices/learner` with `/practices/reviewed`, and move from the older reaction endpoint to the combined response endpoint. Existing response history is preserved; `MIGRATION.md` lists the contract changes. The generated Hephaestus web client is updated in the same release.

.migration/practice-standing-feedback-api.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,21 @@ bundled web client need no changes; it ships updated in this release.
66

77
**Before**: practice groupings were served under `/workspaces/{workspaceSlug}/practice-areas`, with
88
`PracticeArea` schemas and `areaSlug` parameters. A developer's practices came from
9-
`/workspaces/{workspaceSlug}/practices/developer`, and a reaction to delivered feedback was recorded
9+
`/workspaces/{workspaceSlug}/practices/learner`, and a reaction to delivered feedback was recorded
1010
through its own endpoint.
1111

1212
**After**: the same groupings are served under `/workspaces/{workspaceSlug}/practice-groups`, with
13-
`PracticeGroup` schemas and `groupSlug` parameters — *practice area*, `PracticeArea`, `areaSlug` and
13+
`PracticeGroup` schemas and `groupSlug` parameters — _practice area_, `PracticeArea`, `areaSlug` and
1414
`/practice-areas` are retired names, not synonyms, and no alias remains. The developer practice list
1515
is `/workspaces/{workspaceSlug}/practices/reviewed`. A developer's response to delivered feedback —
16-
whether it was helpful, how it was handled, and an optional explanation — is written through one
17-
combined response endpoint that replaces the earlier reaction endpoint. Every one of these answers
18-
only for the signed-in developer.
16+
whether it was helpful, how it was handled, and an optional explanation — is written with `PUT
17+
/workspaces/{workspaceSlug}/practices/feedback/{feedbackId}/response`. Map helpfulness to
18+
`usefulness`, handling to `resolution`, and the optional explanation to `comment`. This combined
19+
response endpoint replaces the earlier reaction endpoint. Every one of these answers only for the
20+
signed-in developer.
1921

2022
**Migration**: update each caller's paths, parameter names and response field names to the group
21-
spelling, move any caller of `/practices/developer` to `/practices/reviewed`, and switch reaction
23+
spelling, move any caller of `/practices/learner` to `/practices/reviewed`, and switch reaction
2224
writes to the combined response endpoint. Response history recorded before the upgrade is preserved
2325
and readable through the new endpoint, so nothing needs re-entering. Regenerate any client
2426
built from `server/openapi.yaml`.

webapp/src/components/practice-vocabulary/feedback-resolution-defs.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,6 @@ import type { PracticeGroupReviewObservation } from "@/api/types.gen";
55
import type { StatusDefs } from "./status-def";
66

77
export type FeedbackResolution = NonNullable<PracticeGroupReviewObservation["feedbackResolution"]>;
8-
9-
/**
10-
* What the developer did about a piece of delivered feedback — the half of a response about the
11-
* work, where `feedback-usefulness-defs` is about the review. The two are independent on the wire
12-
* and stay independent here.
13-
*
14-
* `DISPUTED` is the only value the server demands an explanation for, so its description says so
15-
* rather than leaving the reader to find out when the form refuses.
16-
*/
178
export const FEEDBACK_RESOLUTION_DEFS: StatusDefs<FeedbackResolution> = {
189
ADDRESSED: {
1910
label: "Addressed",

webapp/src/components/practice-vocabulary/feedback-usefulness-defs.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,6 @@ import type { PracticeGroupReviewObservation } from "@/api/types.gen";
55
import type { StatusDefs } from "./status-def";
66

77
export type FeedbackUsefulness = NonNullable<PracticeGroupReviewObservation["feedbackUsefulness"]>;
8-
9-
/**
10-
* Whether delivered feedback was worth receiving — the half of a response about the review, where
11-
* `feedback-resolution-defs` is about the work. Neither derives the other: feedback can be useful
12-
* and still not apply.
13-
*/
148
export const FEEDBACK_USEFULNESS_DEFS: StatusDefs<FeedbackUsefulness> = {
159
HELPFUL: {
1610
label: "Helpful",

webapp/src/components/practice-vocabulary/practice-group-standing-defs.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,6 @@ export type PracticeGroupStandingValue = PracticeGroupStanding["standing"];
1515
export interface PracticeGroupStandingDef extends StatusDef {
1616
shortLabel: string;
1717
}
18-
19-
/**
20-
* Where a developer stands in a practice group, worst first. `DEVELOPING` means problems
21-
* *predominate* — `MIXED` has them too — and `NO_OPPORTUNITY` covers evidence that settled nothing
22-
* as well as work that offered no occasion.
23-
*
24-
* Both silences render outline, so the icon separates them: dashed for "nothing seen yet", a slash
25-
* for "nothing to see". `shortLabel` is the ring legend's wording, where five entries share a row.
26-
*/
2718
export const PRACTICE_GROUP_STANDING_DEFS: Record<
2819
PracticeGroupStandingValue,
2920
PracticeGroupStandingDef

webapp/src/components/practice-vocabulary/practice-trend-defs.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,6 @@ import type { PracticeTrend } from "@/api/types.gen";
55
import type { StatusDefs } from "./status-def";
66

77
export type TrendDirection = PracticeTrend["direction"];
8-
9-
/**
10-
* Which way recent evidence points. `UNCERTAIN` means the comparison was made and did not separate;
11-
* `INSUFFICIENT_EVIDENCE` that none was possible. Both render muted, so the icon separates them.
12-
*/
138
export const PRACTICE_TREND_DEFS: StatusDefs<TrendDirection> = {
149
IMPROVING: {
1510
label: "More positive recently",

webapp/src/components/profile/EvidenceFileBlock.stories.tsx

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,6 @@ export const Default: Story = {
4545
},
4646
},
4747
};
48-
49-
/** A single named line, the shape most observations carry. */
5048
export const SingleLine: Story = {
5149
args: {
5250
location: {
@@ -60,8 +58,6 @@ export const SingleLine: Story = {
6058
},
6159
},
6260
};
63-
64-
/** Collapsed: the second and later files of a observation start closed so the panel stays scannable. */
6561
export const Collapsed: Story = {
6662
args: {
6763
defaultOpen: false,
@@ -76,8 +72,6 @@ export const Collapsed: Story = {
7672
},
7773
},
7874
};
79-
80-
/** The quote was withheld and the app cannot know why: it says only that, and names the place. */
8175
export const Redacted: Story = {
8276
args: {
8377
location: {
@@ -89,11 +83,6 @@ export const Redacted: Story = {
8983
},
9084
},
9185
};
92-
93-
/**
94-
* The secret scanner is the one detector allowed to omit a quote, so here the reason *is* knowable
95-
* and gets said — the text was never stored, and the reader is pointed at the line to read it there.
96-
*/
9786
export const RedactedBySecretScanner: Story = {
9887
args: {
9988
location: {
@@ -107,8 +96,6 @@ export const RedactedBySecretScanner: Story = {
10796
detector: "secret-diff-scanner",
10897
},
10998
};
110-
111-
/** No directory to absorb truncation — the file name carries the whole identity. */
11299
export const BareFileName: Story = {
113100
args: {
114101
location: {
@@ -121,8 +108,6 @@ export const BareFileName: Story = {
121108
},
122109
},
123110
};
124-
125-
/** A line far wider than the block: it scrolls, and the gutter stays pinned to the left edge. */
126111
export const LongLines: Story = {
127112
args: {
128113
location: {
@@ -138,12 +123,6 @@ export const LongLines: Story = {
138123
},
139124
},
140125
};
141-
142-
/**
143-
* A quote from an object source. Its numbers are offsets into a serialised context file, not lines
144-
* of anything the reader could open, so no gutter and no range are shown — only the source and the
145-
* quote. This is the case that used to render a Slack message as though it were code.
146-
*/
147126
export const ObjectSource: Story = {
148127
args: {
149128
location: {
@@ -160,12 +139,6 @@ export const ObjectSource: Story = {
160139
},
161140
},
162141
};
163-
164-
/**
165-
* A quote from the old side of the change: the line is marked, because it is not what the file says
166-
* now. The new side carries no marker — it is where nearly every quote comes from, so labelling it
167-
* would say nothing.
168-
*/
169142
export const QuotedFromBeforeTheChange: Story = {
170143
args: {
171144
location: {

webapp/src/components/profile/EvidenceFileBlock.tsx

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,13 @@ import {
77
import { Badge } from "@/components/ui/badge";
88
import { cn } from "@/lib/utils";
99
import { type EvidenceLocation, evidenceLineRangeLabel, splitPath } from "./evidence";
10-
11-
/** The one detector the server lets omit a quote, so the reason is knowable when it is the one that ran. */
1210
const SECRET_SCANNER = "secret-diff-scanner";
1311

1412
interface EvidenceFileBlockProps {
1513
location: EvidenceLocation;
16-
/**
17-
* Which detector produced the evidence. Only used to explain a withheld quote: for the secret
18-
* scanner the reason can be stated, for anything else the app does not know it and says so.
19-
*/
2014
detector?: string;
2115
defaultOpen?: boolean;
2216
}
23-
24-
/**
25-
* A quoted citation, rendered as what its source is. Only a `code` source is located by line: for an
26-
* `object` source the numbers index a stored copy, not a place the reader could open, so it gets no
27-
* gutter. `evidence-source-defs` owns that ruling.
28-
*/
2917
export function EvidenceFileBlock({
3018
location,
3119
detector,
@@ -64,9 +52,6 @@ export function EvidenceFileBlock({
6452
{evidenceLineRangeLabel(location)}
6553
</span>
6654
)}
67-
{/* Only the old side is marked. Nearly every quote comes from the new one, so saying
68-
"after" on all of them is noise — while "before" is the thing a reader has to know:
69-
the line quoted here is not what the file says now. */}
7055
{locatedByLine && location.side === "OLD" && (
7156
<Badge variant="outline" className="shrink-0">
7257
{DIFF_SIDE_LABELS.OLD}

webapp/src/components/profile/FeedbackComment.stories.tsx

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,24 +29,18 @@ const meta = {
2929

3030
export default meta;
3131
type Story = StoryObj<typeof meta>;
32-
33-
/** Nothing written yet: the field is optional, and there is nothing to save or cancel. */
3432
export const Empty: Story = {
3533
play: async ({ canvas }) => {
3634
await expect(canvas.getByRole("button", { name: "Save comment" })).toBeDisabled();
3735
await expect(canvas.queryByRole("button", { name: "Cancel" })).toBeNull();
3836
},
3937
};
40-
41-
/** A comment already recorded. Saving stays disabled until the text actually differs. */
4238
export const Recorded: Story = {
4339
args: { comment: "Split into two commits so the reasoning reads on its own." },
4440
play: async ({ canvas }) => {
4541
await expect(canvas.getByRole("button", { name: "Save comment" })).toBeDisabled();
4642
},
4743
};
48-
49-
/** Typing offers both actions; cancelling puts the recorded text back rather than clearing it. */
5044
export const Editing: Story = {
5145
args: { comment: "Split into two commits." },
5246
play: async ({ canvas, userEvent }) => {
@@ -58,11 +52,6 @@ export const Editing: Story = {
5852
await expect(field).toHaveValue("Split into two commits.");
5953
},
6054
};
61-
62-
/**
63-
* Disputing an observation: the server rejects an empty explanation, so the field says it is
64-
* required and saving stays out of reach until something is written.
65-
*/
6655
export const RequiredForDispute: Story = {
6756
args: { isRequired: true },
6857
play: async ({ args, canvas, userEvent }) => {
@@ -77,8 +66,6 @@ export const RequiredForDispute: Story = {
7766
);
7867
},
7968
};
80-
81-
/** While the response is in flight every control is out of action, including cancel. */
8269
export const Saving: Story = {
8370
args: { comment: "Handled in the follow-up.", isPending: true },
8471
};

0 commit comments

Comments
 (0)