@dataimago/interview 0.2.0-alpha.0 → 0.2.0-alpha.2

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/dist/index.d.ts CHANGED
@@ -23,38 +23,108 @@ declare function ProgressBar({ current, total, estimatedMinutesRemaining }: Prop
23
23
  * @dataimago/interview — schema contract.
24
24
  *
25
25
  * The types in this file are the package's public contract. Consumers
26
- * (apps/hub during the iteration-3-B.1 transition; dissertation-ai +
27
- * sgpc-ai + rpkg-ai once they consume the package) populate the actual
28
- * question content conforming to these shapes; the components in this
29
- * package render whatever the consumer provides.
26
+ * (Level-2 hubs like dissertation-ai, future sgpc-ai, future rpkg-ai)
27
+ * populate the actual question content conforming to these shapes; the
28
+ * components in this package render whatever the consumer provides.
30
29
  *
31
- * Extracted from `apps/hub/src/lib/interview-questions.ts` (the
32
- * `InterviewQuestion` + `QuestionInputType` definitions) and
33
- * `apps/hub/src/lib/interview-store.ts` (the `InterviewAnswers`
34
- * record) during iteration-3-B.1 (2026-05-07).
30
+ * Originally extracted from `apps/hub/src/lib/interview-questions.ts` +
31
+ * `apps/hub/src/lib/interview-store.ts` during iteration-3-B.1 (2026-05-07).
32
+ * Extended for the dissertation-ai use case during D.2.1.b (2026-06-02)
33
+ * see CHANGELOG for the new question types and the `select` → `single-select`
34
+ * rename.
35
35
  *
36
36
  * Design authority: `dataimago-design/wiki/patterns/onboarding-interview.md`.
37
37
  */
38
38
  /**
39
39
  * The set of input shapes the interview engine knows how to render.
40
40
  *
41
+ * Scalar-answer types (value is a `string` or `string[]`):
41
42
  * - `short-text` — single-line text field
42
43
  * - `long-text` — multi-line textarea
43
- * - `select` — single-choice radio group with `options[]`
44
+ * - `single-select` — single-choice radio group with `options[]`
45
+ * *(Renamed from `select` in v0.3; `select` is no longer recognized.)*
44
46
  * - `multi-select` — multi-choice checkbox group with `options[]`
45
- * - `scale` — numeric slider 1–N (consumer specifies range via `options[]`
46
- * labels; engine renders as a horizontal scale)
47
+ * - `scale` — numeric slider 1–N
47
48
  * - `list` — variable-length list of short-text items, bounded by `listRange`
49
+ * - `date` — single ISO 8601 date string (`<input type="date">`)
50
+ *
51
+ * Compound-answer types (value is a `Record` or `Record[]`):
52
+ * - `composite` — fixed set of sub-questions answered once. Value is
53
+ * `Record<subQuestionId, AnswerValue>`. Sub-questions live on
54
+ * `InterviewQuestion.subQuestions`.
55
+ * - `repeater` — variable-length list of records, each conforming to
56
+ * `itemSchema`. Value is `Array<Record<itemSchemaId, AnswerValue>>`. The
57
+ * user clicks "Add another" to add rows.
58
+ * - `file-upload` — like `repeater` but each row is anchored to an uploaded
59
+ * File. Value is `Array<FileEntry>`. The package's renderer collects
60
+ * `File` objects + per-file metadata; the actual upload-to-server
61
+ * pipeline is the consumer's responsibility (the package only collects
62
+ * answer state).
48
63
  */
49
- type QuestionInputType = 'short-text' | 'long-text' | 'select' | 'multi-select' | 'scale' | 'list';
64
+ type QuestionInputType = 'short-text' | 'long-text' | 'single-select' | 'multi-select' | 'scale' | 'list' | 'date' | 'composite' | 'repeater' | 'file-upload';
65
+ /**
66
+ * The runtime value carried by a single answer. The shape depends on the
67
+ * question's `inputType`:
68
+ *
69
+ * | inputType | Value shape |
70
+ * | ---------------- | -------------------------------------- |
71
+ * | short-text | `string` |
72
+ * | long-text | `string` |
73
+ * | single-select | `string` (the selected option's value) |
74
+ * | multi-select | `string[]` |
75
+ * | scale | `string` (numeric string) |
76
+ * | list | `string[]` |
77
+ * | date | `string` (ISO 8601 date) |
78
+ * | composite | `AnswerComposite` |
79
+ * | repeater | `AnswerRepeater` |
80
+ * | file-upload | `AnswerFileUpload` |
81
+ *
82
+ * `undefined` is used to mean "not yet answered."
83
+ */
84
+ type AnswerValue = string | string[] | AnswerComposite | AnswerRepeater | AnswerFileUpload | undefined;
85
+ /** Composite answer: a record keyed by sub-question id. */
86
+ type AnswerComposite = {
87
+ [subQuestionId: string]: AnswerValue;
88
+ };
89
+ /** Repeater answer: an array of records, each keyed by itemSchema sub-question id. */
90
+ type AnswerRepeater = Array<{
91
+ [itemSchemaId: string]: AnswerValue;
92
+ }>;
93
+ /**
94
+ * A single uploaded file entry for `file-upload` questions.
95
+ *
96
+ * - At interview time, `file` is a browser `File` object (from drag-and-drop
97
+ * or `<input type="file">`).
98
+ * - After the consumer uploads it (out-of-band), `file` may be replaced with
99
+ * a server-assigned descriptor (e.g., `{ name, path, size }`). The
100
+ * package treats anything matching the descriptor shape as "uploaded".
101
+ * - `metadata` carries per-file answers conforming to the question's
102
+ * `itemSchema` (e.g., title, author, description).
103
+ */
104
+ type FileEntry = {
105
+ file: File | {
106
+ name: string;
107
+ size: number;
108
+ path?: string;
109
+ type?: string;
110
+ };
111
+ metadata: {
112
+ [itemSchemaId: string]: AnswerValue;
113
+ };
114
+ };
115
+ type AnswerFileUpload = FileEntry[];
50
116
  /**
51
117
  * A single question in an interview flow. Consumers provide an array of
52
118
  * these to drive the engine.
53
119
  *
54
- * The `generates` field is consumer-specific metadata (e.g.,
55
- * "wiki/overview.md intro" or "CLAUDE.md project description") — the engine
56
- * doesn't interpret it; it's preserved through the answer collection pipeline
57
- * for the consumer's downstream generators (e.g., `@dataimago/onboarding-engine`).
120
+ * Field availability by `inputType`:
121
+ *
122
+ * | Field | Required for |
123
+ * | --------------- | --------------------------------------------- |
124
+ * | options | single-select, multi-select, scale |
125
+ * | listRange | list (optional bounds) |
126
+ * | subQuestions | composite (the inline sub-question fields) |
127
+ * | itemSchema | repeater + file-upload (per-row sub-questions)|
58
128
  */
59
129
  interface InterviewQuestion {
60
130
  /** Stable unique identifier; used as the answer-record key. */
@@ -69,7 +139,7 @@ interface InterviewQuestion {
69
139
  examples?: string[];
70
140
  /** Which input shape to render. */
71
141
  inputType: QuestionInputType;
72
- /** Required for select / multi-select / scale; ignored for other types. */
142
+ /** Required for single-select / multi-select / scale; ignored otherwise. */
73
143
  options?: Array<{
74
144
  value: string;
75
145
  label: string;
@@ -90,17 +160,34 @@ interface InterviewQuestion {
90
160
  min: number;
91
161
  max: number;
92
162
  };
163
+ /**
164
+ * Composite-only: the fixed set of sub-questions that get answered as part
165
+ * of this composite question. Their answers land at
166
+ * `answers[parentId][subQuestionId]`.
167
+ */
168
+ subQuestions?: InterviewQuestion[];
169
+ /**
170
+ * Repeater + file-upload: the per-row sub-question schema. Each row of the
171
+ * answer array is `Record<itemSchemaId, AnswerValue>`.
172
+ */
173
+ itemSchema?: InterviewQuestion[];
93
174
  }
94
175
  /**
95
- * The runtime answer record. Keys are `InterviewQuestion.id`; values are
96
- * either the answer string (for short-text, long-text, select, scale) or
97
- * an array of strings (for multi-select, list).
176
+ * The runtime answer record. Keys are `InterviewQuestion.id`; values
177
+ * conform to the per-`inputType` shape described in `AnswerValue` above.
98
178
  *
99
179
  * Consumers manage their own state container (Zustand, Redux, server-side
100
- * session, etc.) and pass the relevant slice into the components as props.
101
- * The engine is state-management-agnostic.
180
+ * session, useState, etc.) and pass the relevant slice into the components
181
+ * as props. The engine is state-management-agnostic.
182
+ *
183
+ * Consumers may narrow this to their domain-specific answer shape (e.g.,
184
+ * `dissertation-ai/apps/hub/src/lib/interview/answers.ts` defines a strict
185
+ * typed `InterviewAnswers`). This package's type is the loose lowest
186
+ * common denominator.
102
187
  */
103
- type InterviewAnswers = Record<string, string | string[]>;
188
+ type InterviewAnswers = {
189
+ [questionId: string]: AnswerValue;
190
+ };
104
191
 
105
192
  interface Props$2 {
106
193
  question: InterviewQuestion;
@@ -109,14 +196,13 @@ interface Props$2 {
109
196
  * state container). The component initializes its local form state
110
197
  * from this value and re-syncs when the question id changes.
111
198
  */
112
- existingAnswer?: string | string[];
199
+ existingAnswer?: AnswerValue;
113
200
  /**
114
- * Called when the user submits a non-empty answer (or any answer if
115
- * the question is `required: false`). The consumer is expected to
201
+ * Called when the user submits an answer. The consumer is expected to
116
202
  * persist the answer into its state container and then advance via
117
203
  * `onNext`.
118
204
  */
119
- onAnswerSubmit: (questionId: string, value: string | string[]) => void;
205
+ onAnswerSubmit: (questionId: string, value: AnswerValue) => void;
120
206
  /** Called after `onAnswerSubmit` to advance to the next question. */
121
207
  onNext: () => void;
122
208
  /** Called when the user clicks the Back button. */
@@ -126,34 +212,17 @@ interface Props$2 {
126
212
  }
127
213
  /**
128
214
  * Single question renderer. Handles all input types: short-text, long-text,
129
- * select, multi-select, scale, list. Keyboard-navigable and screen-reader
130
- * safe.
215
+ * single-select, multi-select, scale, list, date, composite, repeater,
216
+ * file-upload. Keyboard-navigable and screen-reader safe.
131
217
  *
132
- * **Decoupled from any state-management library.** The original
133
- * apps/hub implementation imported `useInterviewStore` directly; this
134
- * extracted version receives the existing answer + change handler as
135
- * props, leaving the consumer free to use Zustand, Redux, useState,
136
- * server-side session state, or any other approach.
218
+ * **Decoupled from any state-management library.** Consumers receive the
219
+ * existing answer + change handler as props.
137
220
  *
138
- * Iteration-1 visual identity (Phase B.2, 2026-05-06):
139
- * Form input fields use stone-50 elevated surface + stone-200 default border
140
- * + forest-700 focus border (interactive-primary focus indicator). Selected
221
+ * Iteration-1 visual identity: form input fields use stone-50 elevated
222
+ * surface + stone-200 default border + forest-700 focus border. Selected
141
223
  * radio/checkbox option states use forest-700 border + forest-50 background.
142
- * The examples disclosure has its border-l-2 frame preserved as
143
- * border-copper-200 — same decorative-emphasis role as HeroSection's
144
- * etymology box and ReviewSummary's "what happens next" callout, per
145
- * iteration-1 §5.2's preserved-decorative-copper governance.
146
- *
147
- * Iteration-2 role-shift (2026-05-07): copper is no longer the brand-accent
148
- * moral-weight scale (the brand accent moved to sky-blue accent-500 #1f618d).
149
- * Copper survives as the "warm decorative framing on the editorial scale" —
150
- * a distinct decorative palette, not subordinate to the brand accent. The
151
- * border-copper-200 site below continues to serve its visual role (warm
152
- * framing for examples-list editorial content); its semantic relationship
153
- * to the brand accent is now zero. See wiki/decisions/iteration-2-blue-accent.md.
154
- *
155
- * Extracted from `apps/hub/src/components/interview/QuestionCard.tsx`
156
- * during iteration-3-B.1 (2026-05-07). Decoupled from `useInterviewStore`.
224
+ * The examples disclosure has border-l-2 frame in border-copper-200 —
225
+ * preserved-decorative-copper role per iteration-2-blue-accent.md.
157
226
  */
158
227
  declare function QuestionCard({ question, existingAnswer, onAnswerSubmit, onNext, onBack, isFirst, isLast, }: Props$2): react_jsx_runtime.JSX.Element;
159
228
 
@@ -170,9 +239,8 @@ interface Props$1 {
170
239
  * question without losing later answers. Implements the wiki's pattern:
171
240
  * "Edit any prior answer — a sidebar shows all answered questions."
172
241
  *
173
- * **Decoupled from any state-management library.** The original
174
- * apps/hub implementation imported `useInterviewStore` directly; this
175
- * extracted version receives the answers + jump handler as props.
242
+ * **Decoupled from any state-management library.** Consumers receive the
243
+ * answers + jump handler as props.
176
244
  *
177
245
  * Iteration-1 visual identity (Phase B.2, 2026-05-06):
178
246
  * Three item states with distinct token tiers:
@@ -180,8 +248,9 @@ interface Props$1 {
180
248
  * - answered → stone-200 border + stone-50 bg (default, hover stone-400)
181
249
  * - unanswered → stone-200/50 + stone-50/50 (muted via opacity preservation)
182
250
  *
183
- * Extracted from `apps/hub/src/components/interview/AnswerSidebar.tsx`
184
- * during iteration-3-B.1 (2026-05-07). Decoupled from `useInterviewStore`.
251
+ * D.2.1.b extension: previews for composite/repeater/file-upload answers
252
+ * delegated to `summarize.ts` so all eight scalar + three compound types
253
+ * render consistently.
185
254
  */
186
255
  declare function AnswerSidebar({ questions, currentIndex, answers, onSelectQuestion }: Props$1): react_jsx_runtime.JSX.Element;
187
256
 
@@ -198,28 +267,37 @@ interface Props {
198
267
  * Shows all answers so the user can confirm ownership of the output before
199
268
  * committing.
200
269
  *
201
- * **Decoupled from any state-management library.** The original
202
- * apps/hub implementation imported `useInterviewStore` directly; this
203
- * extracted version receives the answer record as a prop.
270
+ * **Decoupled from any state-management library.** Consumers receive the
271
+ * answer record as a prop.
204
272
  *
205
273
  * Iteration-1 visual identity (Phase B.2, 2026-05-06):
206
274
  * Editorial Josefin display family on the heading and the "What happens next"
207
275
  * callout title. Stone-tier surfaces and content text throughout. The
208
276
  * "What happens next" callout box preserves border-copper-300 + bg-copper-50 —
209
- * iteration-1 §5.2's preserved-decorative-copper role applied to the
210
- * informational/forward-looking callout, same pattern as HeroSection's
211
- * etymology box.
212
- *
213
- * Iteration-2 role-shift (2026-05-07): copper is no longer subordinate to
214
- * the brand accent (which moved to sky-blue accent-500 #1f618d). Copper is
215
- * its own "warm decorative framing on the editorial scale" — distinct from
216
- * the brand-accent role. The "What happens next" callout continues to serve
217
- * its visual function (warm forward-looking framing); its semantic relation
218
- * to the brand accent is now zero. See wiki/decisions/iteration-2-blue-accent.md.
219
- *
220
- * Extracted from `apps/hub/src/components/interview/ReviewSummary.tsx`
221
- * during iteration-3-B.1 (2026-05-07). Decoupled from `useInterviewStore`.
277
+ * preserved-decorative-copper role per iteration-2-blue-accent.md.
278
+ *
279
+ * D.2.1.b extension: rendering of composite/repeater/file-upload answers
280
+ * delegated to `summarize.ts` so all answer types display consistently.
222
281
  */
223
282
  declare function ReviewSummary({ questions, answers, onGenerate, onEdit, generating }: Props): react_jsx_runtime.JSX.Element;
224
283
 
225
- export { AnswerSidebar, type InterviewAnswers, type InterviewQuestion, ProgressBar, QuestionCard, type QuestionInputType, ReviewSummary };
284
+ /**
285
+ * Helpers for rendering one-line / summary previews of answer values in
286
+ * `AnswerSidebar` and `ReviewSummary`. Centralized so the two components
287
+ * stay consistent when new answer shapes are added.
288
+ */
289
+
290
+ declare function isAnswered(value: AnswerValue): boolean;
291
+ /**
292
+ * A short, single-line preview suitable for sidebar items. Truncates
293
+ * long content. Returns the empty string for unanswered values.
294
+ */
295
+ declare function summarizeShort(question: InterviewQuestion, value: AnswerValue, maxLen?: number): string;
296
+ /**
297
+ * A richer multi-line summary suitable for the ReviewSummary list. May
298
+ * contain newlines; consumers should render in a `whitespace-pre-wrap`
299
+ * container.
300
+ */
301
+ declare function summarizeLong(question: InterviewQuestion, value: AnswerValue): string;
302
+
303
+ export { type AnswerComposite, type AnswerFileUpload, type AnswerRepeater, AnswerSidebar, type AnswerValue, type FileEntry, type InterviewAnswers, type InterviewQuestion, ProgressBar, QuestionCard, type QuestionInputType, ReviewSummary, isAnswered, summarizeLong, summarizeShort };