@hachej/boring-feedback 0.0.0-stage → 0.2.4

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.
Files changed (92) hide show
  1. package/INVARIANTS.md +35 -0
  2. package/LICENSE +21 -0
  3. package/README.md +20 -2
  4. package/dist/agent/capability.d.ts +52 -0
  5. package/dist/agent/capability.js +230 -0
  6. package/dist/agent/index.d.ts +4 -0
  7. package/dist/agent/index.js +5 -0
  8. package/dist/agent/preview.d.ts +10 -0
  9. package/dist/agent/preview.js +27 -0
  10. package/dist/format/index.d.ts +7 -0
  11. package/dist/format/index.js +6 -0
  12. package/dist/format/json.d.ts +30 -0
  13. package/dist/format/json.js +193 -0
  14. package/dist/format/preview.d.ts +28 -0
  15. package/dist/format/preview.js +45 -0
  16. package/dist/format/readable.d.ts +10 -0
  17. package/dist/format/readable.js +19 -0
  18. package/dist/format/report.d.ts +187 -0
  19. package/dist/format/report.js +669 -0
  20. package/dist/format/requests.d.ts +53 -0
  21. package/dist/format/requests.js +69 -0
  22. package/dist/page/app-element.d.ts +79 -0
  23. package/dist/page/app-element.js +451 -0
  24. package/dist/page/canaries.d.ts +46 -0
  25. package/dist/page/canaries.js +142 -0
  26. package/dist/page/index.d.ts +6 -0
  27. package/dist/page/index.js +8 -0
  28. package/dist/page/picker/index.d.ts +4 -0
  29. package/dist/page/picker/index.js +4 -0
  30. package/dist/page/picker/label.d.ts +16 -0
  31. package/dist/page/picker/label.js +58 -0
  32. package/dist/page/picker/overlay.d.ts +42 -0
  33. package/dist/page/picker/overlay.js +126 -0
  34. package/dist/page/picker/picker.d.ts +72 -0
  35. package/dist/page/picker/picker.js +386 -0
  36. package/dist/page/privacy/digest.d.ts +6 -0
  37. package/dist/page/privacy/digest.js +24 -0
  38. package/dist/page/privacy/index.d.ts +6 -0
  39. package/dist/page/privacy/index.js +5 -0
  40. package/dist/page/privacy/name.d.ts +8 -0
  41. package/dist/page/privacy/name.js +106 -0
  42. package/dist/page/privacy/policy.d.ts +65 -0
  43. package/dist/page/privacy/policy.js +125 -0
  44. package/dist/page/privacy/route.d.ts +6 -0
  45. package/dist/page/privacy/route.js +20 -0
  46. package/dist/page/privacy/serialize.d.ts +42 -0
  47. package/dist/page/privacy/serialize.js +90 -0
  48. package/dist/page/voice/align.d.ts +36 -0
  49. package/dist/page/voice/align.js +122 -0
  50. package/dist/page/voice/capture.d.ts +43 -0
  51. package/dist/page/voice/capture.js +186 -0
  52. package/dist/page/voice/index.d.ts +3 -0
  53. package/dist/page/voice/index.js +3 -0
  54. package/dist/preview/agent.d.ts +34 -0
  55. package/dist/preview/agent.js +91 -0
  56. package/dist/preview/index.d.ts +5 -0
  57. package/dist/preview/index.js +6 -0
  58. package/dist/preview/page.d.ts +50 -0
  59. package/dist/preview/page.js +293 -0
  60. package/dist/preview/session.d.ts +63 -0
  61. package/dist/preview/session.js +110 -0
  62. package/dist/source/index.d.ts +24 -0
  63. package/dist/source/index.js +59 -0
  64. package/dist/source/jsx-dev-runtime.d.ts +5 -0
  65. package/dist/source/jsx-dev-runtime.js +29 -0
  66. package/dist/store/index.d.ts +5 -0
  67. package/dist/store/index.js +4 -0
  68. package/dist/store/mentions.d.ts +17 -0
  69. package/dist/store/mentions.js +27 -0
  70. package/dist/store/store.d.ts +121 -0
  71. package/dist/store/store.js +353 -0
  72. package/dist/tickets/index.d.ts +5 -0
  73. package/dist/tickets/index.js +5 -0
  74. package/dist/tickets/sinks.d.ts +27 -0
  75. package/dist/tickets/sinks.js +84 -0
  76. package/dist/tickets/ticket.d.ts +93 -0
  77. package/dist/tickets/ticket.js +148 -0
  78. package/dist/ui/annotate.d.ts +103 -0
  79. package/dist/ui/annotate.js +154 -0
  80. package/dist/ui/index.d.ts +7 -0
  81. package/dist/ui/index.js +8 -0
  82. package/dist/ui/report.d.ts +51 -0
  83. package/dist/ui/report.js +58 -0
  84. package/dist/ui/save.d.ts +45 -0
  85. package/dist/ui/save.js +65 -0
  86. package/dist/ui/session.d.ts +186 -0
  87. package/dist/ui/session.js +762 -0
  88. package/dist/ui/show.d.ts +43 -0
  89. package/dist/ui/show.js +72 -0
  90. package/dist/ui/state.d.ts +11 -0
  91. package/dist/ui/state.js +21 -0
  92. package/package.json +98 -4
@@ -0,0 +1,45 @@
1
+ import { type FeedbackDraft, type FeedbackReport } from '../format/index.js';
2
+ /** What the browser sends. */
3
+ export interface SaveRequest {
4
+ /** Stable across retries of one draft. */
5
+ readonly operationId: string;
6
+ readonly draft: FeedbackDraft;
7
+ }
8
+ /** What the host answers. `unknown` means the outcome is not known: retry later with the same operation id to reconcile. */
9
+ export type SaveResult = {
10
+ readonly kind: 'saved';
11
+ readonly id: string;
12
+ readonly revision: string;
13
+ } | {
14
+ readonly kind: 'denied' | 'unavailable' | 'conflict' | 'unknown' | 'invalid';
15
+ readonly reason: string;
16
+ };
17
+ /** The host's save endpoint, as the annotate state calls it. A throw counts as `unknown`. */
18
+ export type SaveEndpoint = (request: SaveRequest) => Promise<SaveResult>;
19
+ /** A save endpoint over Fetch: POSTs the request as JSON to `url` and reads a `SaveResult` back. Transport failures are `unknown`. */
20
+ export declare function fetchSaveEndpoint(url: string | URL, fetcher?: (request: Request) => Promise<Response>): SaveEndpoint;
21
+ /** A well-formed `SaveResult`, or undefined. */
22
+ export declare function saveResultOf(value: unknown): SaveResult | undefined;
23
+ /** For the host route: the request body as a `SaveRequest`, or the reason it is refused. The draft is fully checked by `store.create`. */
24
+ export declare function parseSaveRequest(value: unknown): {
25
+ readonly ok: true;
26
+ readonly request: SaveRequest;
27
+ } | {
28
+ readonly ok: false;
29
+ readonly reason: string;
30
+ };
31
+ /** The store's create result (`@hachej/boring-feedback/store`), structurally, so this browser module needs no store import. */
32
+ export type StoreCreateResult = {
33
+ readonly kind: 'applied';
34
+ readonly report: FeedbackReport;
35
+ readonly revision: string;
36
+ } | {
37
+ readonly kind: 'denied' | 'unavailable' | 'conflict' | 'unknown';
38
+ readonly reason: string;
39
+ };
40
+ /** For the host route: the store's create result as the `SaveResult` the browser reads, with an HTTP status. */
41
+ export declare function saveResponseOf(result: StoreCreateResult): {
42
+ readonly status: number;
43
+ readonly body: SaveResult;
44
+ };
45
+ //# sourceMappingURL=save.d.ts.map
@@ -0,0 +1,65 @@
1
+ // The human Save path (FEEDBACK.md, "Storage": operations and replay). The browser keeps one operation id per draft across retries and
2
+ // sends it with the draft; the host route admits it with the request's access (`store.operationKey('create', draft, access)`) and calls
3
+ // `store.create`. This module holds the wire shapes on both sides and imports no store code: the route maps the store's result itself.
4
+ import { draftProblems } from '../format/index.js';
5
+ const OPERATION_ID = /^[A-Za-z0-9_.:-]{8,128}$/;
6
+ const REFUSALS = new Set(['denied', 'unavailable', 'conflict', 'unknown', 'invalid']);
7
+ /** A save endpoint over Fetch: POSTs the request as JSON to `url` and reads a `SaveResult` back. Transport failures are `unknown`. */
8
+ export function fetchSaveEndpoint(url, fetcher = request => fetch(request)) {
9
+ return async (request) => {
10
+ let response;
11
+ try {
12
+ response = await fetcher(new Request(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(request) }));
13
+ }
14
+ catch {
15
+ return { kind: 'unknown', reason: 'The save request did not complete; retrying reconciles it.' };
16
+ }
17
+ let body;
18
+ try {
19
+ body = await response.json();
20
+ }
21
+ catch {
22
+ body = undefined;
23
+ }
24
+ return saveResultOf(body) ?? (response.status === 401 || response.status === 403
25
+ ? { kind: 'denied', reason: 'You are not signed in or may not leave feedback here.' }
26
+ : { kind: 'unknown', reason: `The save answered ${response.status}; retrying reconciles it.` });
27
+ };
28
+ }
29
+ /** A well-formed `SaveResult`, or undefined. */
30
+ export function saveResultOf(value) {
31
+ if (typeof value !== 'object' || value === null)
32
+ return undefined;
33
+ const record = value;
34
+ if (record['kind'] === 'saved' && typeof record['id'] === 'string' && typeof record['revision'] === 'string')
35
+ return { kind: 'saved', id: record['id'], revision: record['revision'] };
36
+ if (typeof record['kind'] === 'string' && REFUSALS.has(record['kind']) && typeof record['reason'] === 'string')
37
+ return { kind: record['kind'], reason: record['reason'] };
38
+ return undefined;
39
+ }
40
+ /** For the host route: the request body as a `SaveRequest`, or the reason it is refused. The draft is fully checked by `store.create`. */
41
+ export function parseSaveRequest(value) {
42
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
43
+ return { ok: false, reason: 'The request must be a JSON object' };
44
+ const record = value;
45
+ const extra = Object.keys(record).filter(key => key !== 'operationId' && key !== 'draft');
46
+ if (extra.length)
47
+ return { ok: false, reason: `Unexpected fields: ${extra.join(', ')}` };
48
+ const operationId = record['operationId'];
49
+ if (typeof operationId !== 'string' || !OPERATION_ID.test(operationId))
50
+ return { ok: false, reason: 'operationId must be 8 to 128 letters, digits or _.:-' };
51
+ const problems = draftProblems(record['draft']);
52
+ if (problems.length)
53
+ return { ok: false, reason: problems.map(problem => `${problem.at}: ${problem.message}`).join('; ') };
54
+ return { ok: true, request: { operationId, draft: record['draft'] } };
55
+ }
56
+ /** For the host route: the store's create result as the `SaveResult` the browser reads, with an HTTP status. */
57
+ export function saveResponseOf(result) {
58
+ switch (result.kind) {
59
+ case 'applied': return { status: 200, body: { kind: 'saved', id: result.report.id, revision: result.revision } };
60
+ case 'denied': return { status: 403, body: { kind: 'denied', reason: result.reason } };
61
+ case 'conflict': return { status: 409, body: { kind: 'conflict', reason: result.reason } };
62
+ case 'unavailable': return { status: 503, body: { kind: 'unavailable', reason: result.reason } };
63
+ default: return { status: 202, body: { kind: 'unknown', reason: result.reason } };
64
+ }
65
+ }
@@ -0,0 +1,186 @@
1
+ import { type FeedbackDraft } from '../format/index.js';
2
+ import { type PinMark, type PointerSample, type SpeechSegment, type VoiceCapture, type VoiceRefusal } from '../page/index.js';
3
+ import { type AnnotateHost, type CopyState } from './annotate.js';
4
+ import type { SaveEndpoint, SaveResult } from './save.js';
5
+ import { type ExternalState } from './state.js';
6
+ /**
7
+ * What the host's transcription answers. `segments` (times in seconds from the start of the audio, optionally with word timings)
8
+ * let speech be aligned with the pointer and the pins; without them `text` becomes the general note.
9
+ */
10
+ export interface TranscribeResult {
11
+ readonly text: string;
12
+ readonly segments?: readonly SpeechSegment[];
13
+ }
14
+ /**
15
+ * Transcription is a host capability, not part of this library: the host passes any function from the recorded audio to text
16
+ * (its own server route, a shared service, a provider SDK). The session never checks a provider's schema beyond reading these
17
+ * fields: extra fields are ignored, missing or malformed ones are tolerated, and an answer with neither usable segments nor text
18
+ * is reported as "could not be transcribed" with the pins kept.
19
+ */
20
+ export type Transcribe = (audio: Blob, mimeType: string) => Promise<TranscribeResult>;
21
+ export interface FeedbackSessionOptions extends AnnotateHost {
22
+ /** Optional voice: both a capture (`createVoiceCapture()`) and the host's `transcribe` are needed for the 🎙 toggle; without
23
+ * `transcribe` the Record button is simply not offered. */
24
+ readonly voice?: VoiceCapture;
25
+ readonly transcribe?: Transcribe;
26
+ /** Optional storage: Send saves the report first (one operation id per draft across retries). */
27
+ readonly save?: SaveEndpoint;
28
+ /** With `save`: the `@` mention of a saved report (`id => 'feedback/<id>.md'`), so the host's mention resolver inlines it. */
29
+ readonly mention?: (id: string) => string;
30
+ /** Extra CSS for the overlays' shadow roots (the registry's `pickerOverlayStyles`). */
31
+ readonly styles?: string;
32
+ /** The one clock of pins, pointer samples and recordings. Defaults to `performance.now()`. */
33
+ readonly now?: () => number;
34
+ /** Elements at a viewport point (tests); defaults to `document.elementsFromPoint`. */
35
+ readonly hitTest?: (x: number, y: number) => readonly Element[];
36
+ /** Defaults to `copyToClipboard` from `@hachej/boring-files/platform`. */
37
+ readonly copyText?: (text: string) => Promise<boolean>;
38
+ }
39
+ export type SessionPhase = 'idle' | 'active' | 'processing' | 'review';
40
+ /**
41
+ * `feedback`: the desktop feedback mode, its bar docked in the composer. `point` (phones): the host's panel gets out of the way while the
42
+ * person taps elements in the app (`isPointing`), one tap per pick; the same notes, draft and attachment.
43
+ */
44
+ export type SessionMode = 'feedback' | 'point';
45
+ export interface SessionStartOptions {
46
+ readonly mode?: SessionMode;
47
+ }
48
+ export interface SessionNote {
49
+ /** Stable for the session; never reused. */
50
+ readonly id: number;
51
+ /** Its number on the page and in the review (1-based, in order). */
52
+ readonly number: number;
53
+ readonly text: string;
54
+ /** The privacy-policy label of its element ("SettingsPage · button «Save profile»"), or "the page" for a general note. */
55
+ readonly label: string;
56
+ /** The anchor's fallback, when it has an anchor. */
57
+ readonly fallback?: string;
58
+ /** The anchor's development source location (`src/settings/SaveBar.tsx:42`), when it has one. */
59
+ readonly source?: string;
60
+ readonly from?: 'voice';
61
+ /** Pinned while recording: its note is what was said there (empty until transcribed, or when nothing was). */
62
+ readonly recorded?: true;
63
+ }
64
+ export type VoiceState = {
65
+ readonly kind: 'off';
66
+ } | {
67
+ readonly kind: 'starting';
68
+ } | {
69
+ readonly kind: 'recording';
70
+ }
71
+ /** Stop was pressed; the audio is being handed over. */
72
+ | {
73
+ readonly kind: 'stopping';
74
+ } | {
75
+ readonly kind: 'refused';
76
+ readonly reason: string;
77
+ readonly code?: VoiceRefusal;
78
+ };
79
+ /** The note being typed in the bubble. */
80
+ export interface SessionBubble {
81
+ readonly noteId: number;
82
+ readonly number: number;
83
+ readonly element: Element;
84
+ readonly label: string;
85
+ readonly text: string;
86
+ /** A new pin (Esc removes it) or an existing note being edited (Esc keeps it as it was). */
87
+ readonly fresh: boolean;
88
+ }
89
+ export type SendState = {
90
+ readonly kind: 'idle';
91
+ } | {
92
+ readonly kind: 'sending';
93
+ } | SaveResult;
94
+ export interface FeedbackSessionSnapshot {
95
+ readonly phase: SessionPhase;
96
+ /** How the current or last session was started. */
97
+ readonly mode: SessionMode;
98
+ /** Clicks and keys reach the app (⌥ held or the "Use app" toggle). */
99
+ readonly using: boolean;
100
+ readonly useToggle: boolean;
101
+ readonly notes: readonly SessionNote[];
102
+ readonly bubble?: SessionBubble;
103
+ /** The general note: unaligned speech, what the person types in the review, or a question asked with no element (`askGeneral`). */
104
+ readonly general: string;
105
+ /** The general question field is open (feedback mode, no element attached). */
106
+ readonly generalOpen: boolean;
107
+ /** The route template now (active) or when Done was pressed (review). */
108
+ readonly route: string;
109
+ /** How many steps were recorded between the notes. */
110
+ readonly stepCount: number;
111
+ readonly voice: VoiceState;
112
+ /** Voice can be offered: the host gave both a capture and a transcription. */
113
+ readonly voiceAvailable: boolean;
114
+ /** Done was pressed with a recording: it is being transcribed and merged into the notes. */
115
+ readonly transcribing: boolean;
116
+ /** Something the person should know (a refused pin, a failed transcription), until the next action. */
117
+ readonly problem?: string;
118
+ readonly send: SendState;
119
+ /** The label of the element under the pointer, from the privacy policy (active only). */
120
+ readonly hover?: string;
121
+ readonly copy: CopyState;
122
+ }
123
+ /** What Send puts into the message. */
124
+ export type FeedbackAttachment = {
125
+ readonly kind: 'mention';
126
+ readonly id: string;
127
+ readonly path: string;
128
+ } | {
129
+ readonly kind: 'inline';
130
+ readonly text: string;
131
+ } | {
132
+ readonly kind: 'refused';
133
+ readonly reason: string;
134
+ };
135
+ export interface FeedbackSession extends ExternalState<FeedbackSessionSnapshot> {
136
+ /** Enters feedback mode; `{ mode: 'point' }` for point mode on phones (a tap picks at once). */
137
+ readonly start: (options?: SessionStartOptions) => void;
138
+ /** Done: closes feedback mode, transcribes and aligns voice, and leaves the draft for review. */
139
+ readonly done: () => Promise<void>;
140
+ /** ✕: discards everything (feedback mode or the attached draft). */
141
+ readonly discard: () => void;
142
+ readonly setBubbleText: (text: string) => void;
143
+ /** Enter in the bubble: keeps the note (an empty new note removes its pin). */
144
+ readonly saveBubble: () => void;
145
+ /** Esc in the bubble: removes a new pin, or leaves an edited note as it was. */
146
+ readonly cancelBubble: () => void;
147
+ /**
148
+ * Cancels the selection of the open bubble (the "Cancel selection" button, or clicking the selected zone again): the pin and its
149
+ * note go, the person is back in feedback mode with nothing selected. What was typed is kept for a general question (`askGeneral`).
150
+ */
151
+ readonly cancelSelection: () => void;
152
+ /** "Ask a general question": opens a note with NO element attached (any open bubble closes; text typed there carries over). */
153
+ readonly askGeneral: () => void;
154
+ /** Closes the general question field, keeping what was typed. */
155
+ readonly closeGeneral: () => void;
156
+ /** The "Use app" toggle (touch, no ⌥). */
157
+ readonly setUseToggle: (on: boolean) => void;
158
+ readonly toggleVoice: () => Promise<void>;
159
+ readonly setNoteText: (id: number, text: string) => void;
160
+ readonly removeNote: (id: number) => void;
161
+ readonly setGeneral: (text: string) => void;
162
+ /** Hovering a note in the review: highlights its element in the page, or says it is not on this page. Undefined clears. */
163
+ readonly highlight: (id: number | undefined) => 'shown' | 'not-on-page' | 'none';
164
+ /** The draft the report is made from. Throws when there is nothing to send. */
165
+ readonly draft: () => FeedbackDraft;
166
+ /** The never-stored report text (Copy, inline). */
167
+ readonly reportText: () => string;
168
+ /** Copy report: the never-stored report on the clipboard. Needs no store, route or agent (FEEDBACK-2). */
169
+ readonly copy: () => Promise<CopyState>;
170
+ /** Send: saves first when the host has a store, then says what to put in the message. */
171
+ readonly attach: () => Promise<FeedbackAttachment>;
172
+ /** The message was accepted: the draft is done. */
173
+ readonly sent: () => void;
174
+ /** For journeys and tests: the pointer trail and pin marks so far, on the session clock. */
175
+ readonly trail: () => {
176
+ readonly pointer: readonly PointerSample[];
177
+ readonly pins: readonly PinMark[];
178
+ };
179
+ readonly dispose: () => void;
180
+ }
181
+ /** Point mode is on (or finishing): the host hides its panel while this is true and shows it again when it turns false. */
182
+ export declare function isPointing(state: Pick<FeedbackSessionSnapshot, 'mode' | 'phase'>): boolean;
183
+ /** A pin number as drawn on the page: ①…⑳, then the plain number. */
184
+ export declare function pinMark(number: number): string;
185
+ export declare function createFeedbackSession(options: FeedbackSessionOptions): FeedbackSession;
186
+ //# sourceMappingURL=session.d.ts.map