@devaccel-labs/app-comments-toolbar 1.0.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +152 -18
- package/dist/react.js +516 -61
- package/dist/server.js +342 -14
- package/dist/types/capture/api.d.ts +30 -0
- package/dist/types/capture/editor.d.ts +10 -0
- package/dist/types/capture/ignore.d.ts +19 -0
- package/dist/types/capture/index.d.ts +1 -0
- package/dist/types/capture/picker.d.ts +9 -0
- package/dist/types/capture/picture.d.ts +29 -0
- package/dist/types/capture/surface.d.ts +39 -0
- package/dist/types/core/index.d.ts +2 -2
- package/dist/types/core/resolve.d.ts +2 -1
- package/dist/types/core/types.d.ts +11 -0
- package/dist/types/debug-context.d.ts +7 -0
- package/dist/types/dictation.d.ts +9 -0
- package/dist/types/embed/controller.d.ts +61 -0
- package/dist/types/embed/index.d.ts +1 -0
- package/dist/types/embed/options.d.ts +35 -0
- package/dist/types/help/index.d.ts +1 -0
- package/dist/types/help/panel-index.d.ts +1 -0
- package/dist/types/help/panel-styles.d.ts +1 -0
- package/dist/types/help/panel.d.ts +4 -0
- package/dist/types/help/pill-styles.d.ts +11 -0
- package/dist/types/help/pill.d.ts +44 -0
- package/dist/types/help/react.d.ts +12 -0
- package/dist/types/help/requests.d.ts +2 -0
- package/dist/types/help/types.d.ts +119 -0
- package/dist/types/help/wire.d.ts +61 -0
- package/dist/types/native-wire.d.ts +63 -0
- package/dist/types/native.d.ts +43 -0
- package/dist/types/react.d.ts +8 -1
- package/dist/types/replay.d.ts +6 -0
- package/dist/types/runtime/api.d.ts +250 -0
- package/dist/types/runtime/attachment-model.d.ts +16 -0
- package/dist/types/runtime/attachments.d.ts +43 -7
- package/dist/types/runtime/boot.d.ts +39 -0
- package/dist/types/runtime/browser-label.d.ts +5 -0
- package/dist/types/runtime/build-id.d.ts +7 -0
- package/dist/types/runtime/capture-ui.d.ts +45 -21
- package/dist/types/runtime/clarity-model.d.ts +260 -0
- package/dist/types/runtime/clarity-ui.d.ts +62 -0
- package/dist/types/runtime/client.d.ts +6 -197
- package/dist/types/runtime/composer-size.d.ts +30 -0
- package/dist/types/runtime/dashboard.d.ts +17 -2
- package/dist/types/runtime/debug-context-core.d.ts +88 -0
- package/dist/types/runtime/debug-context-host.d.ts +39 -0
- package/dist/types/runtime/debug-context-model.d.ts +352 -0
- package/dist/types/runtime/debug-context-redact.d.ts +48 -0
- package/dist/types/runtime/debug-context.d.ts +36 -0
- package/dist/types/runtime/details-model.d.ts +7 -0
- package/dist/types/runtime/details.d.ts +7 -5
- package/dist/types/runtime/dictation-engine.d.ts +98 -0
- package/dist/types/runtime/dictation-model.d.ts +28 -0
- package/dist/types/runtime/dictation-ui.d.ts +56 -0
- package/dist/types/runtime/dictation.d.ts +66 -0
- package/dist/types/runtime/draft-dock.d.ts +32 -0
- package/dist/types/runtime/draft-model.d.ts +104 -0
- package/dist/types/runtime/draft-store.d.ts +15 -0
- package/dist/types/runtime/edits.d.ts +1 -1
- package/dist/types/runtime/font.d.ts +16 -0
- package/dist/types/runtime/format.d.ts +30 -1
- package/dist/types/runtime/host-watch.d.ts +20 -0
- package/dist/types/runtime/icons.d.ts +16 -2
- package/dist/types/runtime/index.d.ts +3 -1
- package/dist/types/runtime/ink.d.ts +8 -0
- package/dist/types/runtime/kind-model.d.ts +14 -0
- package/dist/types/runtime/live-model.d.ts +25 -0
- package/dist/types/runtime/loader.d.ts +5 -11
- package/dist/types/runtime/markup-editor.d.ts +54 -0
- package/dist/types/runtime/markup-model.d.ts +126 -0
- package/dist/types/runtime/markup.d.ts +69 -0
- package/dist/types/runtime/mention-parse.d.ts +18 -0
- package/dist/types/runtime/mentions.d.ts +4 -4
- package/dist/types/runtime/page-background.d.ts +9 -0
- package/dist/types/runtime/page-image.d.ts +123 -0
- package/dist/types/runtime/placement.d.ts +6 -3
- package/dist/types/runtime/record.d.ts +18 -2
- package/dist/types/runtime/related.d.ts +1 -1
- package/dist/types/runtime/replay-model.d.ts +80 -0
- package/dist/types/runtime/replay.d.ts +5 -38
- package/dist/types/runtime/runtime.d.ts +6 -0
- package/dist/types/runtime/screen.d.ts +47 -0
- package/dist/types/runtime/screenshot.d.ts +60 -6
- package/dist/types/runtime/session-source.d.ts +45 -0
- package/dist/types/runtime/session.d.ts +20 -18
- package/dist/types/runtime/shield.d.ts +9 -0
- package/dist/types/runtime/shot.d.ts +45 -0
- package/dist/types/runtime/sketch.d.ts +26 -0
- package/dist/types/runtime/styles.d.ts +1 -1
- package/dist/types/runtime/sync.d.ts +1 -0
- package/dist/types/runtime/thread-model.d.ts +18 -0
- package/dist/types/runtime/ticket-form-model.d.ts +57 -0
- package/dist/types/runtime/ticket-form.d.ts +9 -0
- package/dist/types/runtime/toolbar.d.ts +37 -1
- package/dist/types/runtime/ui-scale.d.ts +17 -0
- package/dist/types/runtime/update.d.ts +37 -0
- package/dist/types/runtime/use-debug-context.d.ts +27 -0
- package/dist/types/server/index.d.ts +22 -0
- package/dist/types/server/support.d.ts +20 -0
- package/dist/types/server.d.ts +2 -0
- package/dist/types/vite/plugin.d.ts +15 -0
- package/dist/vite.js +121 -41
- package/package.json +26 -13
- package/src/capture/api.ts +128 -0
- package/src/capture/editor.tsx +178 -0
- package/src/capture/ignore.ts +38 -0
- package/src/capture/index.ts +27 -0
- package/src/capture/picker.tsx +171 -0
- package/src/capture/picture.ts +90 -0
- package/src/capture/surface.tsx +185 -0
- package/src/compiler/annotate.ts +202 -0
- package/src/compiler/babel.d.ts +11 -0
- package/src/compiler/enabled.ts +55 -0
- package/src/compiler/index.ts +2 -0
- package/src/core/index.ts +8 -0
- package/src/core/resolve.ts +307 -0
- package/src/core/types.ts +58 -0
- package/src/debug-context.ts +47 -0
- package/src/dictation.ts +17 -0
- package/src/embed/controller.ts +215 -0
- package/src/embed/index.ts +68 -0
- package/src/embed/options.ts +100 -0
- package/src/help/index.ts +38 -0
- package/src/help/panel-index.ts +17 -0
- package/src/help/panel-styles.ts +123 -0
- package/src/help/panel.ts +840 -0
- package/src/help/pill-styles.ts +75 -0
- package/src/help/pill.ts +338 -0
- package/src/help/react.tsx +143 -0
- package/src/help/requests.ts +111 -0
- package/src/help/types.ts +124 -0
- package/src/help/wire.ts +55 -0
- package/src/native-wire.ts +124 -0
- package/src/native.ts +282 -0
- package/src/react.ts +27 -0
- package/src/replay.ts +13 -0
- package/src/runtime/api.ts +531 -0
- package/src/runtime/attachment-model.ts +42 -0
- package/src/runtime/attachments.tsx +1260 -0
- package/src/runtime/boot.ts +95 -0
- package/src/runtime/browser-label.ts +31 -0
- package/src/runtime/build-id.ts +10 -0
- package/src/runtime/capture-ui.tsx +306 -0
- package/src/runtime/capture.ts +508 -0
- package/src/runtime/clarity-model.ts +634 -0
- package/src/runtime/clarity-ui.tsx +454 -0
- package/src/runtime/client.ts +151 -0
- package/src/runtime/composer-size.tsx +146 -0
- package/src/runtime/dashboard.tsx +1130 -0
- package/src/runtime/debug-context-core.ts +681 -0
- package/src/runtime/debug-context-host.ts +56 -0
- package/src/runtime/debug-context-model.ts +1319 -0
- package/src/runtime/debug-context-redact.ts +259 -0
- package/src/runtime/debug-context.ts +613 -0
- package/src/runtime/details-model.ts +30 -0
- package/src/runtime/details.tsx +395 -0
- package/src/runtime/dictation-engine.ts +616 -0
- package/src/runtime/dictation-model.ts +79 -0
- package/src/runtime/dictation-ui.tsx +363 -0
- package/src/runtime/dictation.ts +271 -0
- package/src/runtime/dock.ts +108 -0
- package/src/runtime/draft-dock.tsx +124 -0
- package/src/runtime/draft-model.ts +251 -0
- package/src/runtime/draft-store.ts +149 -0
- package/src/runtime/edits.tsx +165 -0
- package/src/runtime/font.ts +42 -0
- package/src/runtime/format.ts +129 -0
- package/src/runtime/freshness.ts +23 -0
- package/src/runtime/handoff.ts +76 -0
- package/src/runtime/host-watch.ts +64 -0
- package/src/runtime/icons.tsx +295 -0
- package/src/runtime/idle.ts +20 -0
- package/src/runtime/index.ts +11 -0
- package/src/runtime/ink.ts +6 -0
- package/src/runtime/kind-model.ts +20 -0
- package/src/runtime/live-model.ts +32 -0
- package/src/runtime/loader.tsx +87 -0
- package/src/runtime/markup-editor.tsx +1333 -0
- package/src/runtime/markup-model.ts +485 -0
- package/src/runtime/markup.ts +382 -0
- package/src/runtime/members.ts +58 -0
- package/src/runtime/mention-parse.ts +56 -0
- package/src/runtime/mentions.tsx +205 -0
- package/src/runtime/page-background.ts +51 -0
- package/src/runtime/page-image.ts +1592 -0
- package/src/runtime/placement.ts +336 -0
- package/src/runtime/record.ts +402 -0
- package/src/runtime/redact.ts +30 -0
- package/src/runtime/related.ts +34 -0
- package/src/runtime/render-page.ts +2 -0
- package/src/runtime/replay-model.ts +232 -0
- package/src/runtime/replay.ts +173 -0
- package/src/runtime/runtime-version.ts +7 -0
- package/src/runtime/runtime.tsx +51 -0
- package/src/runtime/screen.ts +182 -0
- package/src/runtime/screenshot.ts +260 -0
- package/src/runtime/session-source.ts +112 -0
- package/src/runtime/session.ts +108 -0
- package/src/runtime/shield.ts +34 -0
- package/src/runtime/shot.tsx +422 -0
- package/src/runtime/sketch.ts +139 -0
- package/src/runtime/styles.ts +1541 -0
- package/src/runtime/sync.ts +300 -0
- package/src/runtime/thread-model.ts +71 -0
- package/src/runtime/ticket-form-model.ts +128 -0
- package/src/runtime/ticket-form.tsx +342 -0
- package/src/runtime/toolbar.tsx +4427 -0
- package/src/runtime/ui-scale.ts +75 -0
- package/src/runtime/update.ts +98 -0
- package/src/runtime/use-debug-context.ts +81 -0
- package/src/server/index.ts +133 -0
- package/src/server/support.ts +499 -0
- package/src/server.ts +4 -0
- package/src/vite/plugin.ts +102 -0
- package/src/vite.ts +1 -0
- package/dist/chunks/chunk-CSKXLF33.js +0 -153
- package/dist/chunks/chunk-HUM5FQII.js +0 -160
- package/dist/chunks/render-page-76MLIT6M.js +0 -1421
- package/dist/chunks/runtime-JX75TKER.js +0 -9267
- package/dist/chunks/screenshot-66ARTBGR.js +0 -12
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The clarity check as the composer runs it (docs/adr/0002-clarity-check.md): what Send does before a new
|
|
3
|
+
* comment is posted, what the person is shown when a reader would still have to ask something, and what
|
|
4
|
+
* the comment says when it goes. Every decision is here; `./clarity-ui.tsx` draws it. DOM-free.
|
|
5
|
+
*
|
|
6
|
+
* The check is never the reason a comment is lost or late: no mode, mode `off`, a failed or slow request,
|
|
7
|
+
* or an answer this file cannot read all post the comment exactly as it would have gone without the check.
|
|
8
|
+
*
|
|
9
|
+
* The wire types and bounds mirror `packages/contract/src/clarity.ts` and `limits.ts`; the toolbar bundle
|
|
10
|
+
* does not import the contract package. `tests/clarity-model.spec.ts` holds the two in step.
|
|
11
|
+
*
|
|
12
|
+
* Native clients run the same decisions: `../native.ts` exports this file to `@devaccel-labs/app-comments-native`.
|
|
13
|
+
*/
|
|
14
|
+
import type { TicketDraft } from "./ticket-form-model";
|
|
15
|
+
|
|
16
|
+
export type ClarityMode = "off" | "advise" | "require";
|
|
17
|
+
|
|
18
|
+
export type ClarityGap = "expected" | "actual" | "steps" | "which" | "scope" | "done" | "other";
|
|
19
|
+
|
|
20
|
+
/** Who would have to ask: the person making the change, or whoever checks it afterwards. */
|
|
21
|
+
export type ClarityAudience = "developer" | "tester";
|
|
22
|
+
|
|
23
|
+
export interface ClarityQuestion {
|
|
24
|
+
readonly id: string;
|
|
25
|
+
readonly gap: ClarityGap;
|
|
26
|
+
/** Absent on a report from before questions said who asks: read it as the developer's. */
|
|
27
|
+
readonly audience?: ClarityAudience;
|
|
28
|
+
/** The question, in one sentence. */
|
|
29
|
+
readonly ask: string;
|
|
30
|
+
/** Why a reader needs it, in a few words. */
|
|
31
|
+
readonly why: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ClarityAnswer {
|
|
35
|
+
readonly questionId: string;
|
|
36
|
+
readonly ask: string;
|
|
37
|
+
readonly answer: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ClarityAnswerCheck {
|
|
41
|
+
readonly questionId: string;
|
|
42
|
+
readonly status: "answered" | "partial" | "unrelated";
|
|
43
|
+
readonly note?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The part of the service's report the composer reads. */
|
|
47
|
+
export interface ClarityReport {
|
|
48
|
+
readonly verdict: "clear" | "needs_detail" | "unchecked";
|
|
49
|
+
readonly understood: string;
|
|
50
|
+
/** What the check took as given instead of asking. Absent when it took nothing, or predates the field. */
|
|
51
|
+
readonly assumed?: readonly string[];
|
|
52
|
+
readonly questions: readonly ClarityQuestion[];
|
|
53
|
+
readonly answers: readonly ClarityAnswerCheck[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The body of `POST /v1/clarity`. */
|
|
57
|
+
export interface ClarityDraft {
|
|
58
|
+
readonly body: string;
|
|
59
|
+
readonly url: string;
|
|
60
|
+
readonly route: string;
|
|
61
|
+
readonly target: string;
|
|
62
|
+
readonly attachmentIds: readonly string[];
|
|
63
|
+
/** As a new comment is posted with them: the role, custom field values by id, a tester's ticket answers. */
|
|
64
|
+
readonly role?: string | null;
|
|
65
|
+
readonly fields?: Readonly<Record<string, string>>;
|
|
66
|
+
readonly ticket?: TicketDraft;
|
|
67
|
+
readonly asked?: readonly ClarityQuestion[];
|
|
68
|
+
readonly answers?: readonly ClarityAnswer[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const CLARITY_MAX_QUESTIONS = 3;
|
|
72
|
+
export const CLARITY_MAX_ASSUMED = 3;
|
|
73
|
+
export const CLARITY_MAX_ROUNDS = 2;
|
|
74
|
+
export const CLARITY_ANSWER_MAX_LENGTH = 2000;
|
|
75
|
+
export const COMMENT_MESSAGE_MAX_LENGTH = 4000;
|
|
76
|
+
/** How long Send waits for the check. The service gives its model eight seconds; past nine the comment goes. */
|
|
77
|
+
export const CLARITY_WAIT_MS = 9_000;
|
|
78
|
+
|
|
79
|
+
/** Every word the check puts on screen or into a comment. About the reader, never about the writer. */
|
|
80
|
+
export const CLARITY_WORDS = {
|
|
81
|
+
/** The panel's name, for a screen reader. */
|
|
82
|
+
region: "More detail",
|
|
83
|
+
understood: "What a reader takes from this:",
|
|
84
|
+
/** Over what the check took as given, and the line under it. The console's inbox uses the same words. */
|
|
85
|
+
assumed: "Taken as given",
|
|
86
|
+
assumedNote: "Not asked about. Shown so a wrong one can be put right.",
|
|
87
|
+
/** The lead when every question is a developer's; `whoWouldAsk` names the reader otherwise. */
|
|
88
|
+
lead: "A developer would still have to ask:",
|
|
89
|
+
placeholder: "Add your answer…",
|
|
90
|
+
hint: "Add a little more so nobody has to guess.",
|
|
91
|
+
/** After the second look: nothing holds the comment any more. */
|
|
92
|
+
free: "You can send now. Add more if it helps.",
|
|
93
|
+
/** `require`, first look, Send pressed with nothing added. */
|
|
94
|
+
stay: "Answer a question or add to the comment, then send.",
|
|
95
|
+
sendAsIs: "Send as is",
|
|
96
|
+
working: "One moment…",
|
|
97
|
+
/** Beside a question whose answer the service said falls short without saying how. */
|
|
98
|
+
more: "A reader would still need a little more here.",
|
|
99
|
+
full: "The comment has no room for more.",
|
|
100
|
+
/** The line the answers sit under in the posted comment. */
|
|
101
|
+
detail: "More detail:",
|
|
102
|
+
question: "Q. ",
|
|
103
|
+
answer: "A. ",
|
|
104
|
+
} as const;
|
|
105
|
+
|
|
106
|
+
const GAPS: readonly ClarityGap[] = [
|
|
107
|
+
"expected",
|
|
108
|
+
"actual",
|
|
109
|
+
"steps",
|
|
110
|
+
"which",
|
|
111
|
+
"scope",
|
|
112
|
+
"done",
|
|
113
|
+
"other",
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
type UnknownFields = { readonly [field: string]: unknown };
|
|
117
|
+
|
|
118
|
+
function isRecord(value: unknown): value is UnknownFields {
|
|
119
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function oneLine(value: unknown): string {
|
|
123
|
+
return typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isGap(value: unknown): value is ClarityGap {
|
|
127
|
+
return typeof value === "string" && (GAPS as readonly string[]).includes(value);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The environment's mode as `GET /v1/threads` carries it in `clarity`. Null when the field is missing or is
|
|
132
|
+
* not a mode: a service that predates the check, which is never asked.
|
|
133
|
+
*/
|
|
134
|
+
export function readClarityMode(value: unknown): ClarityMode | null {
|
|
135
|
+
return value === "off" || value === "advise" || value === "require" ? value : null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Whether a new comment is checked before it is posted. */
|
|
139
|
+
export function clarityChecks(mode: ClarityMode | null): mode is "advise" | "require" {
|
|
140
|
+
return mode === "advise" || mode === "require";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function readQuestions(raw: unknown): ClarityQuestion[] {
|
|
144
|
+
if (!Array.isArray(raw)) return [];
|
|
145
|
+
const questions: ClarityQuestion[] = [];
|
|
146
|
+
for (const item of raw) {
|
|
147
|
+
if (!isRecord(item)) continue;
|
|
148
|
+
const id = oneLine(item["id"]);
|
|
149
|
+
const ask = oneLine(item["ask"]);
|
|
150
|
+
if (!id || !ask || questions.some((question) => question.id === id)) continue;
|
|
151
|
+
const audience = item["audience"];
|
|
152
|
+
questions.push({
|
|
153
|
+
id,
|
|
154
|
+
gap: isGap(item["gap"]) ? item["gap"] : "other",
|
|
155
|
+
...(audience === "developer" || audience === "tester" ? { audience } : {}),
|
|
156
|
+
ask,
|
|
157
|
+
why: oneLine(item["why"]),
|
|
158
|
+
});
|
|
159
|
+
if (questions.length === CLARITY_MAX_QUESTIONS) break;
|
|
160
|
+
}
|
|
161
|
+
return questions;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** What was taken as given, as `{ assumed }`, or nothing when it is absent, empty or unreadable. */
|
|
165
|
+
function readAssumed(raw: unknown): { readonly assumed?: readonly string[] } {
|
|
166
|
+
if (!Array.isArray(raw)) return {};
|
|
167
|
+
const assumed: string[] = [];
|
|
168
|
+
for (const item of raw) {
|
|
169
|
+
const sentence = oneLine(item);
|
|
170
|
+
if (!sentence || assumed.includes(sentence)) continue;
|
|
171
|
+
assumed.push(sentence);
|
|
172
|
+
if (assumed.length === CLARITY_MAX_ASSUMED) break;
|
|
173
|
+
}
|
|
174
|
+
return assumed.length ? { assumed } : {};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function readChecks(raw: unknown): ClarityAnswerCheck[] {
|
|
178
|
+
if (!Array.isArray(raw)) return [];
|
|
179
|
+
const checks: ClarityAnswerCheck[] = [];
|
|
180
|
+
for (const item of raw) {
|
|
181
|
+
if (!isRecord(item)) continue;
|
|
182
|
+
const questionId = oneLine(item["questionId"]);
|
|
183
|
+
const status = item["status"];
|
|
184
|
+
if (!questionId || checks.some((check) => check.questionId === questionId)) continue;
|
|
185
|
+
if (status !== "answered" && status !== "partial" && status !== "unrelated") continue;
|
|
186
|
+
const note = oneLine(item["note"]);
|
|
187
|
+
checks.push({ questionId, status, ...(note ? { note } : {}) });
|
|
188
|
+
}
|
|
189
|
+
return checks;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* What `POST /v1/clarity` answered, or null when it is not an answer this build can act on. Null posts the
|
|
194
|
+
* comment: an unreadable answer is the check failing, and the check fails open.
|
|
195
|
+
*/
|
|
196
|
+
export function readClarityResponse(
|
|
197
|
+
payload: unknown,
|
|
198
|
+
): { readonly mode: ClarityMode; readonly report: ClarityReport } | null {
|
|
199
|
+
if (!isRecord(payload) || !isRecord(payload["report"])) return null;
|
|
200
|
+
const mode = readClarityMode(payload["mode"]);
|
|
201
|
+
const report = payload["report"];
|
|
202
|
+
const verdict = report["verdict"];
|
|
203
|
+
if (mode === null) return null;
|
|
204
|
+
if (verdict !== "clear" && verdict !== "needs_detail" && verdict !== "unchecked") return null;
|
|
205
|
+
return {
|
|
206
|
+
mode,
|
|
207
|
+
report: {
|
|
208
|
+
verdict,
|
|
209
|
+
understood: oneLine(report["understood"]),
|
|
210
|
+
...readAssumed(report["assumed"]),
|
|
211
|
+
questions: readQuestions(report["questions"]),
|
|
212
|
+
answers: readChecks(report["answers"]),
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** How one check ended: with the service's answer, or without one. */
|
|
218
|
+
export type ClarityOutcome =
|
|
219
|
+
| { readonly kind: "answered"; readonly mode: ClarityMode; readonly report: ClarityReport }
|
|
220
|
+
| { readonly kind: "failed" };
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Runs one check and never throws. A rejection (a 404 from a service without the route, any other status,
|
|
224
|
+
* the network), an answer that cannot be read, or no answer within `waitMs` are all `failed`. The wait is
|
|
225
|
+
* raced, not only aborted, so a request that ignores its signal still lets Send go on time.
|
|
226
|
+
*/
|
|
227
|
+
export async function askClarity(
|
|
228
|
+
run: (signal: AbortSignal) => Promise<unknown>,
|
|
229
|
+
waitMs: number = CLARITY_WAIT_MS,
|
|
230
|
+
): Promise<ClarityOutcome> {
|
|
231
|
+
const abort = new AbortController();
|
|
232
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
233
|
+
const late = new Promise<"late">((resolve) => {
|
|
234
|
+
timer = setTimeout(() => resolve("late"), waitMs);
|
|
235
|
+
});
|
|
236
|
+
try {
|
|
237
|
+
const payload = await Promise.race([run(abort.signal), late]);
|
|
238
|
+
if (payload === "late") {
|
|
239
|
+
abort.abort();
|
|
240
|
+
return { kind: "failed" };
|
|
241
|
+
}
|
|
242
|
+
const read = readClarityResponse(payload);
|
|
243
|
+
return read ? { kind: "answered", mode: read.mode, report: read.report } : { kind: "failed" };
|
|
244
|
+
} catch {
|
|
245
|
+
return { kind: "failed" };
|
|
246
|
+
} finally {
|
|
247
|
+
clearTimeout(timer);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** The draft at the moment Send was pressed. */
|
|
252
|
+
export interface ClarityFacts {
|
|
253
|
+
readonly body: string;
|
|
254
|
+
/** The page the comment is filed against. */
|
|
255
|
+
readonly url: string;
|
|
256
|
+
readonly route: string;
|
|
257
|
+
/** What it is attached to, in words. */
|
|
258
|
+
readonly target: string;
|
|
259
|
+
/** Its uploaded files, by attachment id. */
|
|
260
|
+
readonly attachmentIds: readonly string[];
|
|
261
|
+
/**
|
|
262
|
+
* What it will be posted with besides its words (the role, custom fields and a tester's ticket answers),
|
|
263
|
+
* exactly as the post sends them, so the check does not ask what one of them already says.
|
|
264
|
+
*/
|
|
265
|
+
readonly posted?: Pick<ClarityDraft, "role" | "fields" | "ticket">;
|
|
266
|
+
/**
|
|
267
|
+
* Its files by their key in the draft. A key outlives a reload (an attachment id does not: a resumed
|
|
268
|
+
* draft uploads its files again), so this is what says whether the files changed.
|
|
269
|
+
*/
|
|
270
|
+
readonly fileKeys: readonly string[];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* A draft the check is holding: a reader would still have to ask something. Kept with the draft in storage
|
|
275
|
+
* (`./draft-model.ts`), so a reload or a minimise brings the questions and the typed answers back.
|
|
276
|
+
*/
|
|
277
|
+
export interface ClarityHold {
|
|
278
|
+
readonly v: 1;
|
|
279
|
+
/** Looks the service has taken at this draft that left a question open. */
|
|
280
|
+
readonly round: 1 | 2;
|
|
281
|
+
/** The environment's mode when it last looked. */
|
|
282
|
+
readonly mode: "advise" | "require";
|
|
283
|
+
readonly understood: string;
|
|
284
|
+
/** What the last look took as given. Absent when it took nothing. */
|
|
285
|
+
readonly assumed?: readonly string[];
|
|
286
|
+
/** Every question asked of this draft, most important first. */
|
|
287
|
+
readonly questions: readonly ClarityQuestion[];
|
|
288
|
+
/** The ones still open, by id. */
|
|
289
|
+
readonly open: readonly string[];
|
|
290
|
+
/** What was typed under each question, by id. An answer that closed its question is still here. */
|
|
291
|
+
readonly answers: Readonly<Record<string, string>>;
|
|
292
|
+
/** What the last look said is still needed, by question id. */
|
|
293
|
+
readonly notes: Readonly<Record<string, string>>;
|
|
294
|
+
/** The draft as it was last looked at (`draftMark`). */
|
|
295
|
+
readonly checked: string;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** The draft's own content: its words and its files. Answers are not part of it. */
|
|
299
|
+
export function draftMark(facts: Pick<ClarityFacts, "body" | "fileKeys">): string {
|
|
300
|
+
return [facts.body.trim(), ...facts.fileKeys].join("\u0000");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** The draft and its answers together: what a look that let the comment through was shown. */
|
|
304
|
+
export function passMark(
|
|
305
|
+
facts: Pick<ClarityFacts, "body" | "fileKeys">,
|
|
306
|
+
hold: ClarityHold | null,
|
|
307
|
+
): string {
|
|
308
|
+
const answers = hold ? givenAnswers(hold, hold.questions) : [];
|
|
309
|
+
return [draftMark(facts), ...answers.map((item) => `${item.questionId}=${item.answer}`)].join(
|
|
310
|
+
"\u0001",
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** The questions still open, in the order they were asked. */
|
|
315
|
+
export function openQuestions(hold: ClarityHold): ClarityQuestion[] {
|
|
316
|
+
return hold.questions.filter((question) => hold.open.includes(question.id));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function givenAnswers(hold: ClarityHold, questions: readonly ClarityQuestion[]): ClarityAnswer[] {
|
|
320
|
+
const answers: ClarityAnswer[] = [];
|
|
321
|
+
for (const question of questions) {
|
|
322
|
+
const answer = (hold.answers[question.id] ?? "").trim();
|
|
323
|
+
if (answer) answers.push({ questionId: question.id, ask: question.ask, answer });
|
|
324
|
+
}
|
|
325
|
+
return answers;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** `hold` with `text` typed under one of its questions. */
|
|
329
|
+
export function withAnswer(hold: ClarityHold, questionId: string, text: string): ClarityHold {
|
|
330
|
+
if (!hold.questions.some((question) => question.id === questionId)) return hold;
|
|
331
|
+
return { ...hold, answers: { ...hold.answers, [questionId]: text } };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Whether the comment may be sent with questions still open. In `advise` always; in `require` only once the
|
|
336
|
+
* service has looked twice, so the first Send with open questions cannot be skipped and the second always can.
|
|
337
|
+
*/
|
|
338
|
+
export function canSendAsIs(hold: ClarityHold): boolean {
|
|
339
|
+
return hold.mode === "advise" || hold.round >= CLARITY_MAX_ROUNDS;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** "send" is the composer's own Send; "as-is" is the panel's "Send as is". */
|
|
343
|
+
export type ClaritySend = "send" | "as-is";
|
|
344
|
+
|
|
345
|
+
export type ClarityStep =
|
|
346
|
+
/** Post now. */
|
|
347
|
+
| { readonly kind: "post" }
|
|
348
|
+
/** Ask the service first. */
|
|
349
|
+
| { readonly kind: "check"; readonly request: ClarityDraft }
|
|
350
|
+
/** `require`, first look, and nothing was added: the questions stay. */
|
|
351
|
+
| { readonly kind: "stay" };
|
|
352
|
+
|
|
353
|
+
function requestFor(
|
|
354
|
+
facts: ClarityFacts,
|
|
355
|
+
asked: readonly ClarityQuestion[],
|
|
356
|
+
answers: readonly ClarityAnswer[],
|
|
357
|
+
): ClarityDraft {
|
|
358
|
+
return {
|
|
359
|
+
body: facts.body,
|
|
360
|
+
url: facts.url,
|
|
361
|
+
route: facts.route,
|
|
362
|
+
target: facts.target,
|
|
363
|
+
attachmentIds: facts.attachmentIds,
|
|
364
|
+
...facts.posted,
|
|
365
|
+
...(asked.length ? { asked, answers } : {}),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* What a press of Send does to a new comment.
|
|
371
|
+
*
|
|
372
|
+
* - No mode (a service without the check) or `off`: post, and nothing is asked.
|
|
373
|
+
* - A draft the service already let through, unchanged since: post (a failed post is retried without asking again).
|
|
374
|
+
* - Not looked at yet: ask.
|
|
375
|
+
* - Held, and skipping is allowed and was chosen, or the service has looked twice: post.
|
|
376
|
+
* - Held after the first look: answers go to be judged against their questions; with none, an edited draft is
|
|
377
|
+
* looked at afresh; with nothing added, `advise` posts and `require` stays.
|
|
378
|
+
*/
|
|
379
|
+
export function clarityStep(input: {
|
|
380
|
+
readonly mode: ClarityMode | null;
|
|
381
|
+
readonly hold: ClarityHold | null;
|
|
382
|
+
readonly draft: ClarityFacts;
|
|
383
|
+
readonly send: ClaritySend;
|
|
384
|
+
/** `passMark` of the draft the service last let through, if it has. */
|
|
385
|
+
readonly passed?: string | null;
|
|
386
|
+
}): ClarityStep {
|
|
387
|
+
const { mode, hold, draft } = input;
|
|
388
|
+
if (!clarityChecks(mode)) return { kind: "post" };
|
|
389
|
+
if (typeof input.passed === "string" && input.passed === passMark(draft, hold))
|
|
390
|
+
return { kind: "post" };
|
|
391
|
+
if (!hold) return { kind: "check", request: requestFor(draft, [], []) };
|
|
392
|
+
if (input.send === "as-is" && canSendAsIs(hold)) return { kind: "post" };
|
|
393
|
+
if (hold.round >= CLARITY_MAX_ROUNDS) return { kind: "post" };
|
|
394
|
+
const asked = openQuestions(hold);
|
|
395
|
+
const answers = givenAnswers(hold, asked);
|
|
396
|
+
if (answers.length > 0) return { kind: "check", request: requestFor(draft, asked, answers) };
|
|
397
|
+
if (draftMark(draft) !== hold.checked)
|
|
398
|
+
return { kind: "check", request: requestFor(draft, [], []) };
|
|
399
|
+
return hold.mode === "advise" ? { kind: "post" } : { kind: "stay" };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* The draft's hold after a check, or null when the comment goes: the check failed, the mode is `off`, the
|
|
404
|
+
* verdict is `clear` or `unchecked`, or it left nothing to ask. A second look at answers opens nothing new:
|
|
405
|
+
* only questions that were already asked can stay open, whatever the service sends.
|
|
406
|
+
*/
|
|
407
|
+
export function clarityAfterCheck(
|
|
408
|
+
hold: ClarityHold | null,
|
|
409
|
+
draft: ClarityFacts,
|
|
410
|
+
request: ClarityDraft,
|
|
411
|
+
outcome: ClarityOutcome,
|
|
412
|
+
): ClarityHold | null {
|
|
413
|
+
if (outcome.kind === "failed") return null;
|
|
414
|
+
const { mode, report } = outcome;
|
|
415
|
+
if (!clarityChecks(mode)) return null;
|
|
416
|
+
switch (report.verdict) {
|
|
417
|
+
case "clear":
|
|
418
|
+
case "unchecked":
|
|
419
|
+
return null;
|
|
420
|
+
case "needs_detail":
|
|
421
|
+
break;
|
|
422
|
+
default: {
|
|
423
|
+
const never: never = report.verdict;
|
|
424
|
+
return never;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const checked = draftMark(draft);
|
|
428
|
+
const judged = hold !== null && (request.asked?.length ?? 0) > 0;
|
|
429
|
+
if (!judged) {
|
|
430
|
+
if (report.questions.length === 0) return null;
|
|
431
|
+
return {
|
|
432
|
+
v: 1,
|
|
433
|
+
round: hold ? 2 : 1,
|
|
434
|
+
mode,
|
|
435
|
+
understood: report.understood,
|
|
436
|
+
...readAssumed(report.assumed),
|
|
437
|
+
questions: report.questions,
|
|
438
|
+
open: report.questions.map((question) => question.id),
|
|
439
|
+
answers: {},
|
|
440
|
+
notes: {},
|
|
441
|
+
checked,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
const still = new Set(report.questions.map((question) => question.id));
|
|
445
|
+
const open = hold.open.filter((id) => still.has(id));
|
|
446
|
+
if (open.length === 0) return null;
|
|
447
|
+
const notes: Record<string, string> = {};
|
|
448
|
+
for (const check of report.answers) {
|
|
449
|
+
if (check.status === "answered" || !open.includes(check.questionId)) continue;
|
|
450
|
+
notes[check.questionId] = check.note ?? CLARITY_WORDS.more;
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
v: 1,
|
|
454
|
+
round: 2,
|
|
455
|
+
mode,
|
|
456
|
+
understood: report.understood || hold.understood,
|
|
457
|
+
...readAssumed(report.assumed?.length ? report.assumed : hold.assumed),
|
|
458
|
+
questions: hold.questions,
|
|
459
|
+
open,
|
|
460
|
+
answers: hold.answers,
|
|
461
|
+
notes,
|
|
462
|
+
checked,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** The lines a question adds to the comment before its answer. */
|
|
467
|
+
function questionHead(ask: string): string {
|
|
468
|
+
return `\n${CLARITY_WORDS.question}${ask}\n${CLARITY_WORDS.answer}`;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** The comment's own words and the blank line before the answers; empty for a comment that is only files. */
|
|
472
|
+
function bodyLead(body: string): string {
|
|
473
|
+
return body.trim() ? `${body.trimEnd()}\n\n` : "";
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** An answer is shortened only when at least this much of it fits; a stub of one says nothing. */
|
|
477
|
+
const CLIPPED_ANSWER_MIN = 20;
|
|
478
|
+
|
|
479
|
+
function clip(text: string, length: number): string {
|
|
480
|
+
let kept = text.slice(0, length - 1);
|
|
481
|
+
// Never half of a surrogate pair.
|
|
482
|
+
const last = kept.charCodeAt(kept.length - 1);
|
|
483
|
+
if (last >= 0xd800 && last <= 0xdbff) kept = kept.slice(0, -1);
|
|
484
|
+
return `${kept.trimEnd()}…`;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* The text the comment is posted with: its own words, then every answered question under "More detail:", as
|
|
489
|
+
* plain text (the same lines the service's `detailText` writes for an inbox). With no answers it is `body`,
|
|
490
|
+
* untouched.
|
|
491
|
+
*
|
|
492
|
+
* It never passes `COMMENT_MESSAGE_MAX_LENGTH`, and the comment's own words are never cut to make room: an
|
|
493
|
+
* answer that does not fit whole is shortened and ends in "…", and one with no room left (and any after it)
|
|
494
|
+
* is left out. A comment already at the limit is posted as it is, with no answers.
|
|
495
|
+
*/
|
|
496
|
+
export function clarityBody(body: string, hold: ClarityHold | null): string {
|
|
497
|
+
if (!hold) return body;
|
|
498
|
+
const answers = givenAnswers(hold, hold.questions);
|
|
499
|
+
if (answers.length === 0) return body;
|
|
500
|
+
const lead = `${bodyLead(body)}${CLARITY_WORDS.detail}`;
|
|
501
|
+
let room = COMMENT_MESSAGE_MAX_LENGTH - lead.length;
|
|
502
|
+
const parts: string[] = [];
|
|
503
|
+
for (const item of answers) {
|
|
504
|
+
const head = questionHead(item.ask);
|
|
505
|
+
const left = room - head.length;
|
|
506
|
+
if (left < 1) break;
|
|
507
|
+
if (item.answer.length <= left) {
|
|
508
|
+
parts.push(`${head}${item.answer}`);
|
|
509
|
+
room -= head.length + item.answer.length;
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
if (left < CLIPPED_ANSWER_MIN) break;
|
|
513
|
+
parts.push(`${head}${clip(item.answer, left)}`);
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
return parts.length ? `${lead}${parts.join("")}` : body;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* How long the answer under `questionId` may be, so that what is typed is what is posted: the smaller of the
|
|
521
|
+
* service's cap for one answer and what is left of the comment's length after its words and the other answers.
|
|
522
|
+
*/
|
|
523
|
+
export function answerRoom(body: string, hold: ClarityHold, questionId: string): number {
|
|
524
|
+
const question = hold.questions.find((item) => item.id === questionId);
|
|
525
|
+
if (!question) return 0;
|
|
526
|
+
let used =
|
|
527
|
+
bodyLead(body).length + CLARITY_WORDS.detail.length + questionHead(question.ask).length;
|
|
528
|
+
for (const other of hold.questions) {
|
|
529
|
+
if (other.id === questionId) continue;
|
|
530
|
+
const answer = (hold.answers[other.id] ?? "").trim();
|
|
531
|
+
if (answer) used += questionHead(other.ask).length + answer.length;
|
|
532
|
+
}
|
|
533
|
+
return Math.max(0, Math.min(CLARITY_ANSWER_MAX_LENGTH, COMMENT_MESSAGE_MAX_LENGTH - used));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* What a screen reader is told when questions appear, change or stay. `who` is the reader the lead names
|
|
538
|
+
* (`whoWouldAsk`); the native package's panel does not pass it yet and keeps "A developer".
|
|
539
|
+
*/
|
|
540
|
+
export function clarityAnnouncement(
|
|
541
|
+
hold: ClarityHold | null,
|
|
542
|
+
stayed: boolean,
|
|
543
|
+
who = "A developer",
|
|
544
|
+
): string {
|
|
545
|
+
if (!hold) return "";
|
|
546
|
+
const count = hold.open.length;
|
|
547
|
+
if (stayed) return CLARITY_WORDS.stay;
|
|
548
|
+
if (hold.round >= CLARITY_MAX_ROUNDS)
|
|
549
|
+
return `${count === 1 ? "1 question is" : `${count} questions are`} still open. ${CLARITY_WORDS.free}`;
|
|
550
|
+
return `${who} would still have to ask ${count === 1 ? "1 question" : `${count} questions`}. ${CLARITY_WORDS.hint}`;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const AUDIENCES: readonly ClarityAudience[] = ["developer", "tester"];
|
|
554
|
+
|
|
555
|
+
/** Who would ask `question`. A question from before questions said so is the developer's. */
|
|
556
|
+
export function questionAudience(question: ClarityQuestion): ClarityAudience {
|
|
557
|
+
return question.audience === "tester" ? "tester" : "developer";
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** The reader who would ask, as the subject of a sentence. The console's inbox uses the same words. */
|
|
561
|
+
export function clarityReader(audience: ClarityAudience): string {
|
|
562
|
+
switch (audience) {
|
|
563
|
+
case "developer":
|
|
564
|
+
return "A developer";
|
|
565
|
+
case "tester":
|
|
566
|
+
return "Whoever tests the fix";
|
|
567
|
+
default: {
|
|
568
|
+
const never: never = audience;
|
|
569
|
+
return never;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** `questions` by who would ask them, the developer's first, leaving out a reader who has none. */
|
|
575
|
+
export function clarityGroups(
|
|
576
|
+
questions: readonly ClarityQuestion[],
|
|
577
|
+
): Array<{ readonly audience: ClarityAudience; readonly questions: ClarityQuestion[] }> {
|
|
578
|
+
return AUDIENCES.map((audience) => ({
|
|
579
|
+
audience,
|
|
580
|
+
questions: questions.filter((question) => questionAudience(question) === audience),
|
|
581
|
+
})).filter((group) => group.questions.length > 0);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Who would ask the open questions, as the subject of "… would still have to ask": the one reader when they
|
|
586
|
+
* are all one reader's, "A reader" when a developer and whoever tests the fix both have some.
|
|
587
|
+
*/
|
|
588
|
+
export function whoWouldAsk(questions: readonly ClarityQuestion[]): string {
|
|
589
|
+
const [only, ...others] = clarityGroups(questions);
|
|
590
|
+
return only && others.length === 0 ? clarityReader(only.audience) : "A reader";
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function readTexts(raw: unknown, ids: ReadonlySet<string>): Record<string, string> {
|
|
594
|
+
const texts: Record<string, string> = {};
|
|
595
|
+
if (!isRecord(raw)) return texts;
|
|
596
|
+
for (const [id, text] of Object.entries(raw)) {
|
|
597
|
+
if (ids.has(id) && typeof text === "string") texts[id] = text;
|
|
598
|
+
}
|
|
599
|
+
return texts;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* `value` as a hold, or null when it is not one this build can show. Storage holds whatever an earlier build
|
|
604
|
+
* wrote; a hold that does not read whole is dropped, and the draft it came with is resumed without it.
|
|
605
|
+
*/
|
|
606
|
+
export function readClarityHold(value: unknown): ClarityHold | null {
|
|
607
|
+
if (!isRecord(value) || value["v"] !== 1) return null;
|
|
608
|
+
const round = value["round"];
|
|
609
|
+
const mode = value["mode"];
|
|
610
|
+
if (round !== 1 && round !== 2) return null;
|
|
611
|
+
if (mode !== "advise" && mode !== "require") return null;
|
|
612
|
+
if (typeof value["checked"] !== "string") return null;
|
|
613
|
+
const questions = readQuestions(value["questions"]);
|
|
614
|
+
const ids = new Set(questions.map((question) => question.id));
|
|
615
|
+
const open = Array.isArray(value["open"])
|
|
616
|
+
? value["open"].filter(
|
|
617
|
+
(id, index, all): id is string =>
|
|
618
|
+
typeof id === "string" && ids.has(id) && all.indexOf(id) === index,
|
|
619
|
+
)
|
|
620
|
+
: [];
|
|
621
|
+
if (open.length === 0) return null;
|
|
622
|
+
return {
|
|
623
|
+
v: 1,
|
|
624
|
+
round,
|
|
625
|
+
mode,
|
|
626
|
+
understood: oneLine(value["understood"]),
|
|
627
|
+
...readAssumed(value["assumed"]),
|
|
628
|
+
questions,
|
|
629
|
+
open,
|
|
630
|
+
answers: readTexts(value["answers"], ids),
|
|
631
|
+
notes: readTexts(value["notes"], ids),
|
|
632
|
+
checked: value["checked"],
|
|
633
|
+
};
|
|
634
|
+
}
|