@explorer02/cfm-survey-sdk 0.2.0 → 0.2.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.
Files changed (88) hide show
  1. package/dist/cli/index.js +18 -18
  2. package/dist/cli/index.mjs +14 -14
  3. package/dist/index.d.mts +465 -580
  4. package/dist/index.d.ts +465 -580
  5. package/dist/index.js +1 -1
  6. package/dist/index.mjs +1 -1
  7. package/package.json +6 -4
  8. package/postinstall.js +96 -64
  9. package/templates/AGENT.md +15 -7
  10. package/templates/docs/00-integration/answer-logic-and-navigation.md +50 -0
  11. package/templates/docs/00-integration/aws-deploy.md +281 -0
  12. package/templates/docs/00-integration/client-integration-guide.md +351 -0
  13. package/templates/docs/00-integration/component-checklist.md +84 -0
  14. package/templates/docs/00-integration/constraints.md +65 -3
  15. package/templates/docs/00-integration/custom-field-logic-and-navigation.md +174 -0
  16. package/templates/docs/00-integration/display-logic-and-navigation.md +49 -0
  17. package/templates/docs/00-integration/end-page-logic.md +64 -0
  18. package/templates/docs/00-integration/partial-save-and-recovery.md +74 -0
  19. package/templates/docs/00-integration/placeholders-and-tokens.md +116 -0
  20. package/templates/docs/00-integration/progress.md +61 -0
  21. package/templates/docs/00-integration/question-display-variants.md +65 -0
  22. package/templates/docs/00-integration/question-numbering.md +51 -0
  23. package/templates/docs/00-integration/question-type-sdk-matrix.md +242 -0
  24. package/templates/docs/00-integration/setup.md +84 -0
  25. package/templates/docs/00-integration/skip-logic-and-navigation.md +80 -0
  26. package/templates/docs/00-integration/survey-lifecycle-analytics.md +94 -0
  27. package/templates/docs/00-integration/useSurveySDK.md +151 -23
  28. package/templates/docs/01-components/01-survey-page.md +76 -54
  29. package/templates/docs/01-components/02-question.md +77 -89
  30. package/templates/docs/01-components/03-rating-scale.md +11 -1
  31. package/templates/docs/01-components/04-star-rating-scale.md +45 -0
  32. package/templates/docs/01-components/05-csat-matrix-scale.md +20 -11
  33. package/templates/docs/01-components/06-likert-matrix-scale.md +12 -3
  34. package/templates/docs/01-components/07-slider-matrix-scale.md +11 -2
  35. package/templates/docs/01-components/08-file-upload-scale.md +10 -1
  36. package/templates/docs/01-components/09-custom-slider-track.md +2 -2
  37. package/templates/docs/01-components/11-progress-bar.md +10 -4
  38. package/templates/docs/01-components/13-matrix-dropdown.md +3 -1
  39. package/templates/docs/01-components/14-intro-page.md +34 -0
  40. package/templates/docs/01-components/15-end-page.md +31 -0
  41. package/templates/docs/01-components/16-paused-page.md +27 -0
  42. package/templates/docs/01-components/17-heatmap-scale.md +75 -0
  43. package/templates/docs/01-components/18-rank-order-scale.md +100 -0
  44. package/templates/docs/01-components/README.md +44 -36
  45. package/templates/docs/02-reference/config-field-index.md +141 -139
  46. package/templates/docs/02-reference/exports.md +149 -0
  47. package/templates/docs/02-reference/question-types/01-rating.md +31 -32
  48. package/templates/docs/02-reference/question-types/04-csat.md +51 -42
  49. package/templates/docs/02-reference/question-types/05-rating-scale.md +16 -11
  50. package/templates/docs/02-reference/question-types/06-slider.md +2 -2
  51. package/templates/docs/02-reference/question-types/07-matrix-cfm.md +44 -30
  52. package/templates/docs/02-reference/question-types/08-matrix-csat.md +4 -26
  53. package/templates/docs/02-reference/question-types/09-matrix-rating.md +29 -13
  54. package/templates/docs/02-reference/question-types/10-slider-matrix.md +15 -4
  55. package/templates/docs/02-reference/question-types/11-file-upload.md +13 -2
  56. package/templates/docs/02-reference/question-types/12-heatmap.md +59 -0
  57. package/templates/docs/02-reference/question-types/13-rank-order.md +101 -0
  58. package/templates/docs/02-reference/question-types/README.md +86 -47
  59. package/templates/docs/02-reference/routing-table.md +33 -18
  60. package/templates/docs/02-reference/value-derivation.md +63 -0
  61. package/templates/docs/03-ui-specs/00-question-shell.md +24 -80
  62. package/templates/docs/03-ui-specs/01-rating.md +12 -54
  63. package/templates/docs/03-ui-specs/02-radio.md +21 -33
  64. package/templates/docs/03-ui-specs/03-text.md +2 -2
  65. package/templates/docs/03-ui-specs/04-csat.md +38 -33
  66. package/templates/docs/03-ui-specs/05-rating-scale.md +21 -18
  67. package/templates/docs/03-ui-specs/06-slider.md +3 -1
  68. package/templates/docs/03-ui-specs/07-matrix-cfm.md +25 -22
  69. package/templates/docs/03-ui-specs/08-matrix-csat-rating.md +18 -46
  70. package/templates/docs/03-ui-specs/09-slider-matrix.md +2 -2
  71. package/templates/docs/03-ui-specs/10-file-upload.md +2 -2
  72. package/templates/docs/03-ui-specs/11-text-and-media.md +29 -29
  73. package/templates/docs/03-ui-specs/12-survey-chrome.md +20 -14
  74. package/templates/docs/03-ui-specs/13-heatmap.md +73 -0
  75. package/templates/docs/03-ui-specs/14-rank-order.md +59 -0
  76. package/templates/docs/03-ui-specs/README.md +14 -13
  77. package/templates/docs/03-ui-specs/shared/custom-slider-track.md +3 -3
  78. package/templates/docs/03-ui-specs/shared/matrix-dropdown.md +6 -1
  79. package/templates/docs/MANIFEST.json +454 -67
  80. package/templates/docs/index.md +84 -29
  81. package/templates/docs/templates/Question.tsx +310 -0
  82. package/templates/docs/templates/deploy-to-aws.sh +114 -0
  83. package/templates/docs/templates/heatmapCoords.ts +58 -0
  84. package/templates/docs/templates/implementation_plan.md +115 -49
  85. package/templates/docs/templates/survey-inventory.schema.json +72 -0
  86. package/templates/docs/templates/verify-agent-build.sh +53 -0
  87. package/templates/docs/01-components/04-csat-scale.md +0 -40
  88. package/templates/docs/02-question-types/01-rating.md +0 -44
@@ -0,0 +1,50 @@
1
+ # Answer Logic and Navigation
2
+
3
+ > Integrator-facing behavior contract. Answer logic evaluation is handled inside the SDK — use exported visibility helpers; do not import internal `surveyLogic` paths directly.
4
+
5
+ ## Distinction from display logic
6
+
7
+ Skip logic, display logic, and answer logic share the **same condition tree** and **same evaluator** (`evaluateCondition`). They differ by **what** the condition gates:
8
+
9
+ | Kind | API attachment | SDK field | When true |
10
+ |------|----------------|-----------|-----------|
11
+ | `SKIP` | Question `controlConfig.skipLogics` | `question.skipLogics` | Navigate to skip target or end of survey |
12
+ | `DISPLAY` | Question `controlConfig.displayLogic` | `question.displayLogic` | Show the whole question (dynamic or page-level) |
13
+ | `ANSWER` | Sub-entity `controlConfig.displayLogic` | `answerLogic` on options / rows / columns | Show that option, statement row, or scale column |
14
+
15
+ The API uses `displayLogic` on sub-entities (MCQ `answerDetails`, matrix `childSurveyQuestions`, scale `answerDetails` / `matrixColumnQuestions`). The SDK maps these to `answerLogic` to avoid confusion with question-level display logic.
16
+
17
+ ## Supported question types
18
+
19
+ | Type | Answer logic on | Not supported |
20
+ |------|-----------------|---------------|
21
+ | MCQ | Each option | — |
22
+ | SLIDER_MATRIX | Each statement row | Scale columns |
23
+ | CSAT_MATRIX | Each statement row + each scale column | — |
24
+ | RATING_MATRIX | Each statement row only | Scale columns |
25
+ | CFM_MATRIX | Each statement row + each scale column | — |
26
+ | RANK_ORDER | Each option | — |
27
+ | NPS_SCALE, TEXTFIELD, FILE_UPLOAD, TEXT_AND_MEDIA | — | No answer logic |
28
+
29
+ ## Page breaks
30
+
31
+ Adding answer logic in the survey builder creates a **page break before that question** (backend page structure). The SDK does not split pages at runtime; it filters visible items within the current page.
32
+
33
+ If **every** answer item is hidden (all options, all rows, or all scale columns), the question is treated as not answerable and may disappear from `state.currentQuestions` (same pattern as display logic empty pages).
34
+
35
+ ## Client responsibilities
36
+
37
+ 1. Use `state.currentQuestions` from `useSurveySDK` — question-level display logic is already applied.
38
+ 2. **MCQ:** Filter options with `getVisibleMcqOptionsForAnswers(question, answers, allQuestions, customFieldValues?)`.
39
+ 3. **Rank order:** Filter options with `getVisibleRankOrderOptionsForAnswers(question, answers, allQuestions, customFieldValues?)`.
40
+ 4. **Matrix (CSAT, CFM, RATING, SLIDER):** Filter rows and (for CSAT/CFM) columns with `getVisibleMatrixItemsForAnswers(question, answers, allQuestions, customFieldValues?)`.
41
+ 5. Pass the same `customFieldValues` map used in `useSurveySDK` options when logic rules reference `CUSTOM_FIELD` entities — see [`custom-field-logic-and-navigation.md`](custom-field-logic-and-navigation.md).
42
+ 6. Wire `onAction({ type: 'CHANGE' })` so answer logic re-evaluates when prior answers change.
43
+
44
+ ## Progress and validation
45
+
46
+ - Progress and validation count only **visible** options, statement rows, and scale columns.
47
+ - Hidden selections are stripped automatically on answer change via SDK sanitization.
48
+ - Required questions with zero visible options are not blocked (treated as answered).
49
+
50
+ See also: [`display-logic-and-navigation.md`](display-logic-and-navigation.md), [`skip-logic-and-navigation.md`](skip-logic-and-navigation.md), [`progress.md`](progress.md).
@@ -0,0 +1,281 @@
1
+ # AWS Deploy (EC2 Middle API → S3 → CloudFront)
2
+
3
+ > Deploy a **static** survey UI to Sprinklr AWS using the hosted EC2 deploy API.
4
+ > **Do not** embed AWS credentials in client code — the API uses IAM roles on EC2 to presign S3 URLs.
5
+
6
+ Use this doc when the client prompt mentions **AWS**, **CloudFront**, **S3**, **EC2**, or **Sprinklr hosting**.
7
+ For Vercel deploy, use `npx cfm-sdk deploy` instead (see `index.md` Phase 7).
8
+
9
+ ---
10
+
11
+ ## Architecture — Three-Channel Flow
12
+
13
+ ```mermaid
14
+ sequenceDiagram
15
+ participant Dev as Agent_DevMachine
16
+ participant API as EC2_Deploy_API
17
+ participant S3 as Hosting_Bucket
18
+ participant CF as CloudFront
19
+
20
+ Dev->>Dev: yarn build or npm run build
21
+ Dev->>API: POST /deploy/start + JWT + file manifest
22
+ API-->>Dev: deployId + presigned PUT URLs
23
+ loop each file in out/
24
+ Dev->>S3: PUT file direct
25
+ end
26
+ Dev->>API: POST /deploy/complete
27
+ API->>S3: HeadObject index.html
28
+ API->>CF: CreateInvalidation
29
+ API-->>Dev: public HTTPS URL
30
+ ```
31
+
32
+ | Channel | Direction | Action |
33
+ |---------|-----------|--------|
34
+ | 1 Control | Dev → EC2 API | `POST /deploy/start` with JWT + file manifest |
35
+ | 2 Data | Dev → S3 direct | `PUT` each static file to presigned URLs |
36
+ | 3 Finalize | Dev → EC2 API | `POST /deploy/complete` → CloudFront URL |
37
+
38
+ S3 path shape: `sites/{surveyId}/{deployId}/` — extracted from JWT at deploy time.
39
+
40
+ ---
41
+
42
+ ## Prerequisites (verify before deploy)
43
+
44
+ 1. **Phase 6 passed** — `npm run build` / `yarn build` exits 0
45
+ 2. **JWT alignment** — deploy JWT **must equal** `SURVEY_INSTANCE_ID` in `SurveyPage.tsx`
46
+ 3. **Static output exists** — Next.js: `out/index.html`; Vite: `dist/index.html`
47
+ 4. **File count ≤ 500** (API limit)
48
+
49
+ ### Environment variables (dev machine)
50
+
51
+ ```bash
52
+ export CFM_INSTANCE_ID="<same JWT as SurveyPage SURVEY_INSTANCE_ID>"
53
+ export CFM_DEPLOY_API_BASE="http://43.204.26.213:3000"
54
+ export OUT_DIR="<absolute path to static output folder>"
55
+ # Next.js example:
56
+ # export OUT_DIR="/path/to/project/out"
57
+ # Vite example:
58
+ # export OUT_DIR="/path/to/project/dist"
59
+ ```
60
+
61
+ ### Health check
62
+
63
+ ```bash
64
+ curl -s "$CFM_DEPLOY_API_BASE/health"
65
+ # Expected: {"ok":true,"service":"sprinklr-deploy-api"}
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Build Config (apply during Phase 5 BUILD)
71
+
72
+ ### Next.js — static export (required for AWS)
73
+
74
+ See also [`setup.md#nextjs-aws-static-export`](setup.md#nextjs-aws-static-export).
75
+
76
+ **`next.config.js`:**
77
+
78
+ ```javascript
79
+ /** @type {import('next').NextConfig} */
80
+ const nextConfig = {
81
+ reactStrictMode: true,
82
+ transpilePackages: ['@explorer02/cfm-survey-sdk'], // or '@repo/sdk'
83
+ output: 'export',
84
+ assetPrefix: './', // required for nested CloudFront path
85
+ images: { unoptimized: true },
86
+ };
87
+ module.exports = nextConfig;
88
+ ```
89
+
90
+ **`package.json` build script** — copy RSC payload for `index.html` URLs:
91
+
92
+ ```json
93
+ "build": "next build && cp out/index.txt out/index.html.txt"
94
+ ```
95
+
96
+ | Setting | Why |
97
+ |---------|-----|
98
+ | `output: 'export'` | Produces uploadable `out/` folder (no Node server) |
99
+ | `assetPrefix: './'` | Assets resolve under `/sites/{surveyId}/{deployId}/`, not CloudFront root |
100
+ | `images.unoptimized` | Image optimization needs a server; static export does not |
101
+ | `index.html.txt` copy | Fixes 403 on `index.html.txt?_rsc` when page URL ends with `index.html` |
102
+
103
+ ### Vite
104
+
105
+ Set `OUT_DIR` to `dist/`. Ensure asset paths in built HTML are **relative** (no leading `/`). See Vite `base: './'` if needed.
106
+
107
+ ---
108
+
109
+ ## Static Asset Rules (apply during BUILD)
110
+
111
+ | Rule | Detail |
112
+ |------|--------|
113
+ | Public logos | Use relative paths (`logo.svg`), not `/logo.svg` |
114
+ | Rank-order images | Prefer `option.imageUrl` over `option.previewImageUrl` — preview `*_p.*` URLs often return 403 |
115
+ | Footer hash links | Use plain `<a href="#">`, not `next/link` — avoids RSC prefetch on static export |
116
+ | No AWS keys in code | JWT only in `Authorization: Bearer` header at deploy time |
117
+
118
+ ---
119
+
120
+ ## Deploy Procedure (agent executes in order)
121
+
122
+ ### Step 0 — Build
123
+
124
+ ```bash
125
+ cd <project-root>
126
+ yarn build # or npm run build
127
+ test -f "$OUT_DIR/index.html" || { echo "Missing index.html"; exit 1; }
128
+ find "$OUT_DIR" -type f | wc -l # must be ≤ 500
129
+ ```
130
+
131
+ ### Step 1 — Channel 1: manifest + start
132
+
133
+ ```bash
134
+ python3 <<'PY' > /tmp/deploy-manifest.json
135
+ import json, os, mimetypes
136
+ from pathlib import Path
137
+ out = Path(os.environ["OUT_DIR"])
138
+ files = []
139
+ for path in sorted(out.rglob("*")):
140
+ if not path.is_file():
141
+ continue
142
+ rel = path.relative_to(out).as_posix()
143
+ mime, _ = mimetypes.guess_type(rel)
144
+ files.append({
145
+ "path": rel,
146
+ "contentType": mime or "application/octet-stream",
147
+ "sizeBytes": path.stat().st_size,
148
+ })
149
+ print(json.dumps({"files": files}))
150
+ print(f"files: {len(files)}", file=__import__('sys').stderr)
151
+ PY
152
+
153
+ curl -s -X POST "$CFM_DEPLOY_API_BASE/deploy/start" \
154
+ -H "Authorization: Bearer $CFM_INSTANCE_ID" \
155
+ -H "Content-Type: application/json" \
156
+ -d @/tmp/deploy-manifest.json \
157
+ | tee /tmp/deploy-start.json
158
+ ```
159
+
160
+ **Success:** JSON with `deployId`, `s3Prefix`, `uploads[]`.
161
+
162
+ | Error | Fix |
163
+ |-------|-----|
164
+ | `401 TOKEN_EXPIRED` | Ask client for fresh instanceId JWT |
165
+ | `503 DEPLOY_UNAVAILABLE` | EC2 hosting env misconfigured — report to operator |
166
+ | `400 files must not exceed 500` | Reduce build output |
167
+
168
+ ### Step 2 — Channel 2: upload every file to S3
169
+
170
+ ```bash
171
+ python3 <<'PY'
172
+ import json, os, subprocess
173
+ from pathlib import Path
174
+
175
+ out = Path(os.environ["OUT_DIR"])
176
+ start = json.load(open("/tmp/deploy-start.json"))
177
+ manifest = {f["path"]: f["contentType"] for f in json.load(open("/tmp/deploy-manifest.json"))["files"]}
178
+ failed = []
179
+
180
+ for item in start["uploads"]:
181
+ path = item["path"]
182
+ url = item["uploadUrl"]
183
+ mime = manifest.get(path, "application/octet-stream")
184
+ proc = subprocess.run(
185
+ ["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}",
186
+ "-X", "PUT", url, "-H", f"Content-Type: {mime}",
187
+ "--data-binary", f"@{out / path}"],
188
+ capture_output=True, text=True,
189
+ )
190
+ code = proc.stdout.strip()
191
+ mark = "OK" if code == "200" else "FAIL"
192
+ print(f"{mark} {code} {path}")
193
+ if code != "200":
194
+ failed.append(path)
195
+
196
+ if failed:
197
+ print(f"\n{len(failed)} failed — rerun from Step 1 (presigned URLs expire)")
198
+ raise SystemExit(1)
199
+ print("\nAll uploads succeeded")
200
+ PY
201
+ ```
202
+
203
+ **Success:** every line shows `OK 200`.
204
+
205
+ ### Step 3 — Channel 3: complete + live URL
206
+
207
+ ```bash
208
+ DEPLOY_ID=$(python3 -c "import json; print(json.load(open('/tmp/deploy-start.json'))['deployId'])")
209
+
210
+ curl -s -X POST "$CFM_DEPLOY_API_BASE/deploy/complete" \
211
+ -H "Authorization: Bearer $CFM_INSTANCE_ID" \
212
+ -H "Content-Type: application/json" \
213
+ -d "{\"deployId\":\"$DEPLOY_ID\"}" \
214
+ | tee /tmp/deploy-complete.json
215
+
216
+ python3 -c "import json; print('\nLive URL:\n' + json.load(open('/tmp/deploy-complete.json'))['url'])"
217
+ ```
218
+
219
+ **Success:**
220
+
221
+ ```json
222
+ {
223
+ "url": "https://<cloudfront-domain>/sites/{surveyId}/{deployId}/index.html",
224
+ "deployId": "...",
225
+ "surveyId": "..."
226
+ }
227
+ ```
228
+
229
+ ### Step 4 — Verify
230
+
231
+ ```bash
232
+ LIVE_URL=$(python3 -c "import json; print(json.load(open('/tmp/deploy-complete.json'))['url'])")
233
+ curl -s -o /dev/null -w "index.html: %{http_code}\n" "$LIVE_URL"
234
+ # Optional: grep one CSS href from HTML and curl it under the deploy prefix — expect 200
235
+ ```
236
+
237
+ Tell the client to open the printed URL in a browser.
238
+
239
+ ---
240
+
241
+ ## One-Command Wrapper (optional)
242
+
243
+ After build, from project root:
244
+
245
+ ```bash
246
+ export CFM_INSTANCE_ID="<JWT>"
247
+ export CFM_DEPLOY_API_BASE="http://43.204.26.213:3000"
248
+ export OUT_DIR="<absolute path to out/ or dist/>"
249
+
250
+ bash templates/deploy-to-aws.sh
251
+ # or: bash node_modules/@explorer02/cfm-survey-sdk/templates/docs/templates/deploy-to-aws.sh
252
+ ```
253
+
254
+ ---
255
+
256
+ ## Repeat Deploys
257
+
258
+ Each deploy creates a **new** `deployId` folder under `sites/{surveyId}/`. Old deploys remain in S3.
259
+
260
+ After UI changes: rebuild → re-run Steps 1–3 (same `CFM_INSTANCE_ID` if same survey).
261
+
262
+ ---
263
+
264
+ ## Troubleshooting
265
+
266
+ | Symptom | Cause | Agent fix |
267
+ |---------|-------|-----------|
268
+ | 403 on `/_next/static/...` | Absolute asset paths | Add `assetPrefix: "./"`, rebuild |
269
+ | 403 on `index.html.txt?_rsc` | Missing RSC payload file | Add `cp out/index.txt out/index.html.txt` to build script |
270
+ | 403 on `*_p.png` / `*_p.jpg` images | Preview URL used | Use `option.imageUrl \|\| option.previewImageUrl` in RankOrderScale |
271
+ | 401 `TOKEN_EXPIRED` | JWT expired | Ask client for new instanceId |
272
+ | 503 `DEPLOY_UNAVAILABLE` | EC2 CloudFront/S3 env wrong | Report to operator |
273
+ | Some PUTs 403 | Presigned URLs expired (default 5 min) | Restart from Step 1; complete within window |
274
+ | Blank page, HTML 200 | JS/CSS not uploaded or wrong paths | Re-check Channel 2 logs |
275
+ | Survey API errors in browser | Deploy JWT ≠ SurveyPage JWT | Align `SURVEY_INSTANCE_ID` |
276
+
277
+ ---
278
+
279
+ ## Future: CLI automation (Phase 7b)
280
+
281
+ `npx cfm-sdk deploy --aws` is **not implemented yet**. Use the manual flow above or `templates/deploy-to-aws.sh` until the CLI ships.
@@ -0,0 +1,351 @@
1
+ # Client Integration Guide
2
+
3
+ > **Start here** after Phase 2 (instanceId). Single map of everything the client must wire to integrate all SDK features correctly.
4
+
5
+ Import only from the package root (`@explorer02/cfm-survey-sdk` or `@repo/sdk`). Never from `/src/` paths.
6
+
7
+ ## Architecture
8
+
9
+ ```mermaid
10
+ flowchart TB
11
+ subgraph page [SurveyPage orchestrator]
12
+ Hook["useSurveySDK"]
13
+ PhaseRouter["Phase router\nloading / intro / content / end"]
14
+ Lifecycle["SCROLL + RECORD_QUESTION_VIEW effects"]
15
+ end
16
+ subgraph question [Question.tsx dispatcher]
17
+ Dispatch["QUESTION_TYPE switch"]
18
+ Visibility["getVisible*ForAnswers helpers"]
19
+ Placeholders["resolveSurveyQuestionPlaceholders"]
20
+ end
21
+ subgraph scales [Scale components]
22
+ Matrix["LikertMatrixScale / CsatMatrixScale / …"]
23
+ NPS["RatingScale / FileUploadScale / …"]
24
+ end
25
+ Hook --> PhaseRouter
26
+ PhaseRouter --> question
27
+ Dispatch --> scales
28
+ Visibility --> Dispatch
29
+ ```
30
+
31
+ | Layer | File | Responsibility |
32
+ |-------|------|----------------|
33
+ | Orchestrator | `SurveyPage.tsx` | Hook, phase routing, nav buttons, lifecycle effects, pass props to `Question` |
34
+ | Dispatcher | `Question.tsx` | Route on `question.type`; filter options/rows via visibility helpers; placeholder text |
35
+ | Scales | `*Scale.tsx` | Render one question type; store answers via `onSelect` callback |
36
+
37
+ Reference implementation: `apps/client/src/components/SurveyPage.tsx`.
38
+
39
+ ---
40
+
41
+ ## Feature matrix — who wires what
42
+
43
+ | Feature | Client wires | SDK handles |
44
+ |---------|--------------|-------------|
45
+ | Fetch survey | `instanceId`, loading/error UI | `useFetchSurvey`, survey mapping |
46
+ | Submit | `onAction(NEXT/SUBMIT)`, submit loading UI | Validation, API submit, phase → `end`, end-page selection |
47
+ | End page logic | Render `state.specialPage`; pass `customFieldValues` when end pages reference CRM fields | Display-logic evaluation at submit; see [`end-page-logic.md`](end-page-logic.md) |
48
+ | Answers | `onAction(CHANGE)` with `payload.answerValue` | `state.answers`, sanitization on logic change |
49
+ | Skip logic | Nothing (read doc for UX expectations) | Path simulation, skip jumps, answer clearing |
50
+ | Display logic | Use `state.currentQuestions` (pre-filtered) | Question-level visibility, empty-page skip |
51
+ | Question display variants | Render `question.questionText` from `currentQuestions` (no client variant logic) | Conditional variant text via same logic engine |
52
+ | Answer logic UI | `getVisible*ForAnswers` + pass `customFieldValues` | Condition evaluation engine |
53
+ | Custom fields | Stable `customFieldValues` → hook **and** `Question` | Skip/display/validation/progress evaluation |
54
+ | Progress | Bind `state.progressPercentage` | Active-path denominator + earned credits |
55
+ | Question numbering | Render `question.questionNumber` from `currentQuestions` | Active-path ordinals (same denominator as progress); see [`question-numbering.md`](question-numbering.md) |
56
+ | Placeholders | Stable `placeholders` ref + `language` option | Token replacement at map time |
57
+ | Mark as started | `onAction(SCROLL)` once on first scroll | One-time API call (also fires on CHANGE) |
58
+ | Question activity | `onAction(RECORD_QUESTION_VIEW)` via IntersectionObserver | Deduped VIEWED API calls |
59
+ | Partial save | `savePartialResponse: true` option | Debounced partial POST after user interaction — see [`partial-save-and-recovery.md`](partial-save-and-recovery.md) |
60
+ | Placeholders (detail) | Stable ref + `resolveSurveyQuestionPlaceholders` | CRM tokens at map time; answer tokens at render — see [`placeholders-and-tokens.md`](placeholders-and-tokens.md) |
61
+ | Per-type integration | Read [`question-type-sdk-matrix.md`](question-type-sdk-matrix.md) before building scales | Answer shapes, visibility helpers, performance notes per type |
62
+ | Validation scroll | `id={question.id}` on question wrapper | Scroll-to-first-error on failed NEXT/SUBMIT |
63
+
64
+ Deep dives: [`useSurveySDK.md`](useSurveySDK.md) · [`question-type-sdk-matrix.md`](question-type-sdk-matrix.md) · [`placeholders-and-tokens.md`](placeholders-and-tokens.md) · [`partial-save-and-recovery.md`](partial-save-and-recovery.md) · [`survey-lifecycle-analytics.md`](survey-lifecycle-analytics.md) · [`skip-logic-and-navigation.md`](skip-logic-and-navigation.md) · [`display-logic-and-navigation.md`](display-logic-and-navigation.md) · [`question-display-variants.md`](question-display-variants.md) · [`question-numbering.md`](question-numbering.md) · [`end-page-logic.md`](end-page-logic.md) · [`answer-logic-and-navigation.md`](answer-logic-and-navigation.md) · [`custom-field-logic-and-navigation.md`](custom-field-logic-and-navigation.md) · [`progress.md`](progress.md) · [`constraints.md`](constraints.md)
65
+
66
+ ---
67
+
68
+ ## Stable references (CRITICAL)
69
+
70
+ Inline option objects cause infinite re-fetch loops and React hook dependency churn. Define **outside** the component and wrap hook options in `useMemo`:
71
+
72
+ ```typescript
73
+ const SURVEY_PLACEHOLDERS = { FIRST_NAME: 'Customer' };
74
+
75
+ const CUSTOM_FIELD_VALUES: CustomFieldValueMap = {
76
+ '_c_abc123': 'value',
77
+ };
78
+
79
+ export default function SurveyPage() {
80
+ const [selectedLanguage, setSelectedLanguage] = useState<string | undefined>();
81
+
82
+ const surveyOptions = useMemo(
83
+ () => ({
84
+ instanceId: 'YOUR_JWT_HERE',
85
+ language: selectedLanguage,
86
+ placeholders: SURVEY_PLACEHOLDERS,
87
+ customFieldValues: CUSTOM_FIELD_VALUES,
88
+ savePartialResponse: false as const,
89
+ debug: false,
90
+ }),
91
+ [selectedLanguage]
92
+ );
93
+
94
+ const { surveyQueryResults, submitSurveyResults, state, onAction } = useSurveySDK({
95
+ options: surveyOptions,
96
+ });
97
+ // ...
98
+ }
99
+ ```
100
+
101
+ ---
102
+
103
+ ## Minimal SurveyPage skeleton
104
+
105
+ ```tsx
106
+ 'use client';
107
+
108
+ import { useState, useMemo, useEffect } from 'react';
109
+ import {
110
+ useSurveySDK,
111
+ type CustomFieldValueMap,
112
+ type Survey,
113
+ } from '@explorer02/cfm-survey-sdk';
114
+ import Question from './Question';
115
+
116
+ const SURVEY_PLACEHOLDERS = { FIRST_NAME: 'Customer' };
117
+ const CUSTOM_FIELD_VALUES: CustomFieldValueMap = {}; // fill from CRM when survey uses custom-field logic
118
+
119
+ function SurveyContent({
120
+ survey,
121
+ allQuestions,
122
+ customFieldValues,
123
+ state,
124
+ submitSurveyResults,
125
+ onAction,
126
+ }: {
127
+ survey: Survey;
128
+ allQuestions: Survey['pages'][number]['questions'];
129
+ customFieldValues: CustomFieldValueMap;
130
+ state: ReturnType<typeof useSurveySDK>['state'];
131
+ submitSurveyResults: ReturnType<typeof useSurveySDK>['submitSurveyResults'];
132
+ onAction: ReturnType<typeof useSurveySDK>['onAction'];
133
+ }) {
134
+ // Mark as started on first scroll
135
+ useEffect(() => {
136
+ const onScroll = () => onAction({ type: 'SCROLL' });
137
+ window.addEventListener('scroll', onScroll, { passive: true, once: true });
138
+ return () => window.removeEventListener('scroll', onScroll);
139
+ }, [onAction]);
140
+
141
+ // Question activity (VIEWED)
142
+ useEffect(() => {
143
+ const observer = new IntersectionObserver(
144
+ entries => {
145
+ for (const entry of entries) {
146
+ if (entry.isIntersecting && entry.target.id) {
147
+ onAction({
148
+ type: 'RECORD_QUESTION_VIEW',
149
+ payload: { questionIds: [entry.target.id] },
150
+ });
151
+ }
152
+ }
153
+ },
154
+ { threshold: 0.5 }
155
+ );
156
+ state.currentQuestions.forEach(q => {
157
+ const el = document.getElementById(q.id);
158
+ if (el) observer.observe(el);
159
+ });
160
+ return () => observer.disconnect();
161
+ }, [state.currentQuestions, onAction]);
162
+
163
+ return (
164
+ <main>
165
+ <ProgressBar progressPercentage={state.progressPercentage} />
166
+
167
+ {state.currentQuestions.map(question => (
168
+ <Question
169
+ key={question.id}
170
+ question={question}
171
+ selectedValue={state.answers[question.id]}
172
+ validationError={state.validationErrors[question.id]}
173
+ allAnswers={state.answers}
174
+ allQuestions={allQuestions}
175
+ customFieldValues={customFieldValues}
176
+ onSelect={answerValue =>
177
+ onAction({ type: 'CHANGE', payload: { questionId: question.id, answerValue } })
178
+ }
179
+ />
180
+ ))}
181
+
182
+ {state.canGoBack && (
183
+ <button type="button" onClick={() => onAction({ type: 'PREVIOUS' })}>Back</button>
184
+ )}
185
+ <button
186
+ type="button"
187
+ onClick={() => onAction({ type: 'NEXT' })}
188
+ disabled={submitSurveyResults.isLoading}
189
+ >
190
+ {state.primaryButtonType === 'submit' ? 'Submit' : 'Next'}
191
+ </button>
192
+ </main>
193
+ );
194
+ }
195
+
196
+ export default function SurveyPage() {
197
+ const [selectedLanguage, setSelectedLanguage] = useState<string | undefined>();
198
+
199
+ const surveyOptions = useMemo(
200
+ () => ({
201
+ instanceId: 'YOUR_JWT_HERE',
202
+ language: selectedLanguage,
203
+ placeholders: SURVEY_PLACEHOLDERS,
204
+ customFieldValues: CUSTOM_FIELD_VALUES,
205
+ }),
206
+ [selectedLanguage]
207
+ );
208
+
209
+ const { surveyQueryResults, submitSurveyResults, state, onAction } = useSurveySDK({
210
+ options: surveyOptions,
211
+ });
212
+
213
+ const survey = surveyQueryResults.data;
214
+ const allQuestions = useMemo(
215
+ () => survey?.pages.flatMap(page => page.questions) ?? [],
216
+ [survey]
217
+ );
218
+
219
+ if (surveyQueryResults.isLoading) return <div>Loading…</div>;
220
+ if (surveyQueryResults.error) return <div>Error: {surveyQueryResults.error.message}</div>;
221
+ if (!survey) return <div>No survey data</div>;
222
+
223
+ // Phase router — handle intro / paused / end / terminal before content
224
+ if (state.phase.kind === 'intro') return <IntroPage onStart={() => onAction({ type: 'START' })} />;
225
+ if (state.phase.kind === 'end') return <EndPage page={state.specialPage} />;
226
+ // … paused, submitted, expired, quota_fulfilled
227
+
228
+ if (state.phase.kind === 'content') {
229
+ return (
230
+ <SurveyContent
231
+ survey={survey}
232
+ allQuestions={allQuestions}
233
+ customFieldValues={CUSTOM_FIELD_VALUES}
234
+ state={state}
235
+ submitSurveyResults={submitSurveyResults}
236
+ onAction={onAction}
237
+ />
238
+ );
239
+ }
240
+
241
+ return null;
242
+ }
243
+ ```
244
+
245
+ See [`01-components/01-survey-page.md`](../01-components/01-survey-page.md) for full phase routing.
246
+
247
+ ---
248
+
249
+ ## Question dispatcher essentials
250
+
251
+ Copy [`templates/Question.tsx`](../templates/Question.tsx). Dispatch on flat `question.type` — **11** `QUESTION_TYPE` literals, no `subType`.
252
+
253
+ See [`question-type-sdk-matrix.md`](question-type-sdk-matrix.md) for per-type answer shapes, visibility helpers, and `onAction` patterns.
254
+
255
+ | Type literal | Component | Visibility helper |
256
+ |--------------|-----------|-------------------|
257
+ | `MCQ` | Inline | `getVisibleMcqOptionsForAnswers` |
258
+ | `TEXTFIELD` | Inline | — |
259
+ | `NPS_SCALE` | `RatingScale` | `getVisibleScaleColumnsForAnswers` |
260
+ | `CFM_MATRIX` | `LikertMatrixScale` | `getVisibleMatrixItemsForAnswers` |
261
+ | `CSAT_MATRIX`, `RATING_MATRIX` | `CsatMatrixScale` | `getVisibleMatrixItemsForAnswers` |
262
+ | `SLIDER_MATRIX` | `SliderMatrixScale` | `getVisibleStatementRowsForAnswers` |
263
+ | `FILE_UPLOAD` | `FileUploadScale` | — |
264
+ | `TEXT_AND_MEDIA` | Inline media | — |
265
+ | `HEATMAP` | `HeatmapScale` | — |
266
+ | `RANK_ORDER` | `RankOrderScale` | `getVisibleRankOrderOptionsForAnswers` |
267
+
268
+ **Answer logic:** filter before render:
269
+
270
+ ```typescript
271
+ getVisibleMcqOptionsForAnswers(question, allAnswers, allQuestions, customFieldValues);
272
+ getVisibleMatrixItemsForAnswers(question, allAnswers, allQuestions, customFieldValues);
273
+ ```
274
+
275
+ **Placeholders:** stable `SURVEY_PLACEHOLDERS` ref + `resolveSurveyQuestionPlaceholders(text, allAnswers, questionsById)`. See [`placeholders-and-tokens.md`](placeholders-and-tokens.md).
276
+
277
+ **Wrapper:** every question `<section id={question.id}>` for scroll-to-error.
278
+
279
+ See [`component-checklist.md`](component-checklist.md) and [`answer-logic-and-navigation.md`](answer-logic-and-navigation.md).
280
+
281
+ ---
282
+
283
+ ## Custom fields — two wiring points
284
+
285
+ When survey logic references `conditionEntity.entityType: 'CUSTOM_FIELD'`:
286
+
287
+ 1. **`useSurveySDK` options** — `customFieldValues` map keyed by `_c_…` ids (skip, display, validation, progress).
288
+ 2. **`Question.tsx` visibility helpers** — pass the **same map** as the 4th argument to `getVisible*ForAnswers` (answer-logic UI on options/rows/columns).
289
+
290
+ See [`custom-field-logic-and-navigation.md`](custom-field-logic-and-navigation.md).
291
+
292
+ ---
293
+
294
+ ## Monorepo build prerequisite
295
+
296
+ When using `@repo/sdk` from the monorepo, build the SDK before running the client dev server:
297
+
298
+ ```bash
299
+ cd packages/sdk && npm run build
300
+ ```
301
+
302
+ Without a built `dist/`, the client fails with missing module errors. Rebuild after SDK changes.
303
+
304
+ ---
305
+
306
+ ## Debug logging and analytics
307
+
308
+ Enable debug console logs with `useSurveySDK({ options: { debug: true } })`. Logic match logs appear as `[SDK:INFO]` when conditions **newly become true** on answer change:
309
+
310
+ | Log message | When |
311
+ |-------------|------|
312
+ | `Display logic matched` | A gated question becomes visible |
313
+ | `Answer logic matched` | An option / row / column becomes visible via answer logic |
314
+ | `Question variant matched` | A display variant’s condition matches |
315
+ | `End page logic matched` | The winning end page changes (preview before submit) |
316
+ | `Skip logic matched` | Skip logic newly activates on the current question |
317
+
318
+ Analytics events (`[SDK:ANALYTICS]`) fire **always**, even without `debug: true`:
319
+
320
+ | Event | When |
321
+ |-------|------|
322
+ | `display_logic_matched` | Display logic condition newly true |
323
+ | `answer_logic_matched` | Answer-logic sub-entity newly visible |
324
+ | `question_variant_matched` | Variant text switches to a matching variant |
325
+ | `end_page_logic_matched` | Resolved end page changes |
326
+ | `question_viewed` | Question enters viewport (`RECORD_QUESTION_VIEW`) |
327
+ | `skip_logic_triggered` | Skip logic newly activates on answer change |
328
+ | `logic_session_summary` | On successful submit — rollup table + chronological timeline |
329
+
330
+ The submit-time `logic_session_summary` includes counts per logic kind, viewed vs on-path-not-viewed questions, final end page resolution, and a `logicEventTimeline` audit. It prints via `console.table` alongside `survey_session_summary`.
331
+
332
+ ---
333
+
334
+ ## Pre-flight checklist
335
+
336
+ Before `npm run build`:
337
+
338
+ - [ ] Import from package root only — no `@repo/sdk/src/…` paths
339
+ - [ ] `SURVEY_PLACEHOLDERS` and `CUSTOM_FIELD_VALUES` defined outside component
340
+ - [ ] `surveyOptions` wrapped in `useMemo` (depends on `selectedLanguage` etc.)
341
+ - [ ] `Question.tsx` copied from `docs/templates/Question.tsx` — all **11** type branches, no stub text
342
+ - [ ] `SURVEY_PLACEHOLDERS` defined outside component; `surveyOptions` in `useMemo` (see [`placeholders-and-tokens.md`](placeholders-and-tokens.md))
343
+ - [ ] `id={question.id}` on every question wrapper
344
+ - [ ] `customFieldValues` passed to hook **and** `Question` (when survey uses custom-field logic)
345
+ - [ ] Answer logic: `getVisible*ForAnswers` receives `customFieldValues` 4th arg
346
+ - [ ] `SCROLL` + `RECORD_QUESTION_VIEW` lifecycle effects wired (see [`survey-lifecycle-analytics.md`](survey-lifecycle-analytics.md))
347
+ - [ ] `state.canGoBack` / `state.primaryButtonType` used for nav (not raw page index)
348
+ - [ ] Monorepo: SDK built (`packages/sdk/dist/` exists)
349
+ - [ ] `grep -r "not yet implemented" src/ components/ app/` → zero matches
350
+
351
+ Run `bash templates/verify-agent-build.sh` from project root after Phase 5.