@more-ink/irt-edge 2.1.3 → 2.2.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 CHANGED
@@ -1,526 +1,530 @@
1
- # @more-ink/irt-edge
2
-
3
- **Two packages in one:**
4
- 1. **SDK** - Lightweight API client for JavaScript/TypeScript frontends (published to npm)
5
- 2. **Backend Server** - Production HTTP API server built with Hono for Aliyun Function Compute (not published)
6
-
7
- ---
8
-
9
- ## Table of Contents
10
-
11
- - [SDK Usage (Frontend)](#sdk-usage-frontend)
12
- - [Installation](#installation)
13
- - [Quick Start](#quick-start)
14
- - [Configuration](#configuration)
15
- - [API Methods](#api-methods)
16
- - [Error Handling](#error-handling)
17
- - [Best Practices](#best-practices)
18
- - [Backend Server Setup](#backend-server-setup)
19
- - [Backend Deployment](#backend-deployment-aliyun-fc)
20
- - [SDK Publishing](#sdk-publishing-npm)
21
- - [Package Scripts](#package-scripts)
22
-
23
- ---
24
-
25
- ## SDK Usage (Frontend)
26
-
27
- ### Installation
28
-
29
- ```bash
30
- npm install @more-ink/irt-edge @more-ink/irt-core
31
- # or
32
- yarn add @more-ink/irt-edge @more-ink/irt-core
33
- ```
34
-
35
- **Note:** `@more-ink/irt-core` is a peer dependency that provides type definitions.
36
-
37
- ### Quick Start
38
-
39
- ```typescript
40
- import { IrtClient } from '@more-ink/irt-edge'
41
-
42
- const client = new IrtClient({
43
- baseUrl: 'https://your-api.example.com',
44
- headers: {
45
- 'Authorization': 'Bearer YOUR_TOKEN' // Optional
46
- },
47
- timeout: 10000 // Optional (default: 10000ms)
48
- })
49
-
50
- // Record an answer and get the next item
51
- const result = await client.recordAnswer({
52
- userId: 'user123',
53
- skillId: 'math',
54
- itemId: 'item456',
55
- score: 0.8,
56
- timestamp: Date.now()
57
- })
58
-
59
- console.log('Updated ability:', result.theta)
60
- console.log('Next item:', result.nextItem)
61
- ```
62
-
63
- ### Configuration
64
-
65
- #### IrtClientConfig
66
-
67
- ```typescript
68
- interface IrtClientConfig {
69
- baseUrl: string // API server base URL (required)
70
- headers?: Record<string, string> // Custom headers (e.g., auth tokens)
71
- timeout?: number // Request timeout in ms (default: 10000)
72
- }
73
- ```
74
-
75
- ### API Methods
76
-
77
- #### recordAnswer()
78
-
79
- Record a user's response and get the next recommended item.
80
-
81
- ```typescript
82
- async recordAnswer(params: RecordAnswerRequest): Promise<RecordAnswerResponse>
83
- ```
84
-
85
- **Parameters:**
86
- ```typescript
87
- interface RecordAnswerRequest {
88
- userId: string // User identifier
89
- skillId: string // Skill identifier
90
- itemId: string // Item identifier
91
- score: number // Response score [0,1] (0=wrong, 1=correct)
92
- timestamp: number // When response occurred (ms)
93
- updateOptions?: Partial<UpdateOptions> // Override learning rates
94
- selectionOptions?: Partial<NextItemOptions> // Override selection behavior
95
- }
96
- ```
97
-
98
- **Returns:** Updated ability (theta), standard error (se), next item, and updated states.
99
-
100
- **Example:**
101
- ```typescript
102
- const result = await client.recordAnswer({
103
- userId: 'alice',
104
- skillId: 'geometry',
105
- itemId: 'q100',
106
- score: 1.0,
107
- timestamp: Date.now()
108
- })
109
-
110
- console.log(`Ability: ${result.theta.toFixed(2)} ± ${result.se.toFixed(2)}`)
111
- if (result.nextItem) {
112
- console.log(`Next question: ${result.nextItem.id}`)
113
- }
114
- ```
115
-
116
- #### recordMultiSkillAnswers()
117
-
118
- Record multiple skill scores that came from a single multi-skill item. This is useful when one interaction yields separate sub-skill scores (e.g., integrated tasks that grade both reading and writing).
119
-
120
- ```typescript
121
- async recordMultiSkillAnswers(
122
- params: RecordMultiSkillAnswersRequest,
123
- ): Promise<RecordMultiSkillAnswersResponse>
124
- ```
125
-
126
- **Parameters:**
127
-
128
- ```typescript
129
- interface RecordMultiSkillAnswersRequest {
130
- userId: string
131
- itemId: string
132
- timestamp?: number
133
- updateOptions?: Partial<UpdateOptions>
134
- skillScores: Array<{
135
- skillId: string
136
- score: number // [0,1]
137
- updateOptions?: Partial<UpdateOptions>
138
- }>
139
- }
140
- ```
141
-
142
- **Returns:** Array of per-skill updates with theta, SE, updated user/item snapshots.
143
-
144
- **Example:**
145
-
146
- ```typescript
147
- const multi = await client.recordMultiSkillAnswers({
148
- userId: 'alice',
149
- itemId: 'essay-22',
150
- skillScores: [
151
- { skillId: 'reading', score: 0.9 },
152
- { skillId: 'writing', score: 0.6 },
153
- ],
154
- })
155
-
156
- multi.results.forEach((entry) => {
157
- console.log(entry.skillId, entry.theta, entry.se)
158
- })
159
- ```
160
-
161
- #### recordAnswerOnly()
162
-
163
- Record a user's response without requesting the next recommended item (useful when you already control sequencing).
164
-
165
- ```typescript
166
- async recordAnswerOnly(params: RecordAnswerRequest): Promise<RecordAnswerOnlyResponse>
167
- ```
168
-
169
- ```typescript
170
- await client.recordAnswerOnly({
171
- userId: 'alice',
172
- skillId: 'geometry',
173
- itemId: 'q100',
174
- score: 1.0,
175
- timestamp: Date.now()
176
- })
177
- ```
178
-
179
- #### selectNextItem()
180
-
181
- Get the next recommended item without recording a response.
182
-
183
- ```typescript
184
- async selectNextItem(params: SelectNextItemRequest): Promise<SelectNextItemResponse>
185
- ```
186
-
187
- #### resetUser() / resetItem()
188
-
189
- Reset all Redis-backed skill state for a user or item.
190
-
191
- ```typescript
192
- async resetUser(userId: string): Promise<ResetUserResponse>
193
- async resetItem(itemId: string): Promise<ResetItemResponse>
194
- ```
195
-
196
- #### bulkUpdateUser() / bulkUpdateItem()
197
-
198
- Bulk cold-start/backfill helpers that update **only** user skills or **only** item parameters.
199
-
200
- ```typescript
201
- async bulkUpdateUser(
202
- userId: string,
203
- events: BulkUpdateEvent[],
204
- updateOptions?: Partial<UpdateOptions>
205
- ): Promise<BulkUpdateResponse>
206
-
207
- async bulkUpdateItem(
208
- itemId: string,
209
- events: BulkUpdateEvent[],
210
- updateOptions?: Partial<UpdateOptions>
211
- ): Promise<BulkUpdateResponse>
212
- ```
213
-
214
- #### getUserState() / getUserStates()
215
-
216
- Get current user ability estimates.
217
-
218
- ```typescript
219
- async getUserState(userId: string, skillId: string): Promise<UserStateResponse>
220
- async getUserStates(userId: string): Promise<UserStatesResponse>
221
- ```
222
-
223
- #### getItemState() / getItemStates()
224
-
225
- Get item parameters and metadata.
226
-
227
- ```typescript
228
- async getItemState(itemId: string, skillId: string): Promise<ItemStateResponse>
229
- async getItemStates(itemId: string): Promise<ItemStatesResponse>
230
- ```
231
-
232
- #### deleteUser() / deleteItem()
233
-
234
- Delete a user (and their skill states) or an item (and all per-skill calibrations). These are mainly for integration tests and fixture cleanup.
235
-
236
- ```typescript
237
- async deleteUser(userId: string): Promise<DeleteUserResponse>
238
- async deleteItem(itemId: string): Promise<DeleteItemResponse>
239
- ```
240
-
241
- #### health()
242
-
243
- Check API server health status.
244
-
245
- ```typescript
246
- async health(): Promise<HealthResponse>
247
- ```
248
-
249
- See `examples/sdk-usage.ts` for complete working examples.
250
-
251
- ### Error Handling
252
-
253
- All methods throw errors for network failures, HTTP errors, timeouts, and invalid responses.
254
-
255
- ```typescript
256
- try {
257
- const result = await client.recordAnswer({...})
258
- // Handle success
259
- } catch (error) {
260
- if (error instanceof Error) {
261
- console.error('API error:', error.message)
262
- if (error.message.includes('timeout')) {
263
- // Retry logic
264
- }
265
- }
266
- }
267
- ```
268
-
269
- ### Best Practices
270
-
271
- 1. **Reuse client instances** - Create one client and reuse it
272
- 2. **Use timestamps** - Always provide client-side timestamps for accurate analytics
273
- 3. **Handle errors gracefully** - Show user-friendly messages on errors
274
- 4. **Check for null items** - The API may return `null` if no suitable item is available
275
-
276
- ```typescript
277
- const result = await client.selectNextItem({...})
278
- if (result.nextItem) {
279
- // Show item to user
280
- } else {
281
- // No more items available
282
- }
283
- ```
284
-
285
- ---
286
-
287
- ## Backend Server Setup
288
-
289
- The backend server is **not published** to npm. It runs as a service on Aliyun Function Compute.
290
-
291
- ### Features
292
-
293
- - RESTful IRT API backed by **Redis (online state)** and **Postgres via Prisma** (durable via `/api/irt/sync`)
294
- - Aliyun FC lifecycle hooks (`/initialize`, `/pre-stop`)
295
- - Graceful shutdown and health checks
296
-
297
- ### Development Setup
298
-
299
- ```bash
300
- # From repo root
301
- npm install
302
-
303
- # Create .env file with:
304
- # REDIS_URL=redis://... (or rediss://...)
305
- # REDIS_DB=2
306
- # DATABASE_URL=postgres://user:pass@host:port/db
307
-
308
- # Run dev server
309
- npm run --workspace @more-ink/irt-edge dev
310
-
311
- # Seed data
312
- npm run --workspace @more-ink/irt-edge seed
313
- ```
314
-
315
- ### Backend Environment Variables
316
-
317
- ```bash
318
- # Aliyun Function Compute
319
- ALIYUN_ACCESS_KEY="YOUR_ALIYUN_ACCESS_KEY"
320
- ALIYUN_SECRET_ACCESS_KEY="YOUR_ALIYUN_SECRET_KEY"
321
-
322
- # Database (PostgreSQL via Prisma)
323
- DATABASE_URL="postgres://user:pass@host:port/db?schema=public"
324
- # If using PgBouncer / pooled connections, also set DIRECT_URL for Prisma CLI commands
325
- # DIRECT_URL="postgres://user:pass@host:port/db?schema=public"
326
-
327
- # Feishu (Lark) Integration
328
- FEISHU_APP_ID="YOUR_FEISHU_APP_ID"
329
- FEISHU_APP_SECRET="YOUR_FEISHU_APP_SECRET"
330
- FEISHU_CHAT_ID="YOUR_FEISHU_CHAT_ID"
331
-
332
- # Redis (primary online store)
333
- REDIS_URL="redis://localhost:6379"
334
- REDIS_DB=2
335
-
336
- # Debug mode for Function Compute
337
- DEBUG_FC="true"
338
-
339
- # Server port (optional, default: 9000)
340
- PORT=9000
341
- ```
342
-
343
- See `src/index.ts` for default IRT engine parameters (learning rates, selection options).
344
-
345
- ### API Endpoints
346
-
347
- **IRT Operations:**
348
- - `POST /api/irt/answer` - Record response and get next item
349
- - `POST /api/irt/answer-multi` - Record multiple skill responses for one item
350
- - `POST /api/irt/sync` - Persist dirty Redis state to Postgres (cron-triggered)
351
- - `POST /api/irt/next-item` - Get next item without recording response
352
- - `POST /api/irt/users/:userId/reset` - Reset user skills in Redis
353
- - `POST /api/irt/items/:itemId/reset` - Reset item skills in Redis
354
- - `POST /api/irt/bulk-update-user/:userId` - Bulk update user skills (user-only)
355
- - `POST /api/irt/bulk-update-item/:itemId` - Bulk update item parameters (item-only)
356
- - `DELETE /api/irt/users/:userId` - Delete a user and all skill states
357
- - `DELETE /api/irt/items/:itemId` - Delete an item and its per-skill calibrations
358
- - `GET /api/irt/health` - Health check
359
-
360
- **Aliyun FC Lifecycle:**
361
- - `POST /initialize` - Instance startup (verifies Prisma DB and Redis connection)
362
- - `GET /pre-stop` - Instance shutdown (closes Prisma DB and Redis)
363
-
364
- **System:**
365
- - `GET /` - Service info
366
-
367
- ---
368
-
369
- ## Cold-start Guidance
370
-
371
- When you have **no prior data** (no user θ, no item a/b), the recommended order for scale is:
372
-
373
- 1) **Item cold-start** using a default θ (e.g., θ=0).
374
- 2) **User cold-start** using the calibrated item a/b.
375
- 3) **Optional**: run item cold-start again to refine a/b.
376
-
377
- Why this order: item sets are typically small and fixed, while user sets are large and segmented. A quick first-pass item calibration (with θ=0) reduces downstream work before user backfill.
378
-
379
- Reset and cold-start operations clear **Redis** only. Postgres is treated as a backup store and may retain stale rows until fresh updates overwrite them.
380
-
381
- ### Bulk Update Constraints
382
-
383
- - Maximum **10k events** per request (over limit returns 400).
384
- - `timestamp` is **required** and events are processed in ascending time order.
385
- - `score` must be in `[0, 1]` (invalid events are skipped with errors).
386
- - Errors are reported per event; valid events still proceed.
387
- - Only a **single global** `updateOptions` is supported per bulk request.
388
-
389
- ---
390
-
391
- ## Backend Deployment (Aliyun FC)
392
-
393
- Deploy the backend server to Aliyun FC:
394
-
395
- ```bash
396
- # From root (recommended)
397
- npm run dp
398
-
399
- # Or from package dir
400
- npm run --workspace @more-ink/irt-edge deploy
401
- ```
402
-
403
- ### How Deployment Works
404
-
405
- Deployment now runs directly from the package root with npm-installed `node_modules` (no flattening step needed):
406
- 1. Run `npm run --workspace @more-ink/irt-edge deploy` (runs `prisma generate`, then `build`, then `fc-deploy` from the package root)
407
- 2. Upload from the package root; `node_modules` already contains real files.
408
-
409
- Ensure `DATABASE_URL` (and `DIRECT_URL` if pooling) is set in the function env.
410
-
411
- ---
412
-
413
- ## SDK Publishing (npm)
414
-
415
- Publish the SDK to npm (backend code stays private):
416
-
417
- ```bash
418
- # From repo root (recommended)
419
- npm run pub
420
-
421
- # Or from package directory
422
- cd packages/irt-edge
423
- npm run publish:sdk
424
-
425
- # Or using npm directly
426
- npm publish --access public
427
- ```
428
-
429
- ### What Gets Published
430
-
431
- **Published to npm:**
432
- - ✅ `sdk/` - Compiled SDK code (~40KB unpacked)
433
- - `README.md`, `CHANGELOG.md` - Documentation
434
- - ✅ `examples/` - Usage examples
435
- - `package.json` - Package metadata
436
-
437
- **NOT published (stays private):**
438
- - Backend server code (`src/index.ts`, `routes.ts`, `storage/`)
439
- - Scripts and tests (`scripts/`, `tests/`)
440
- - ❌ Config files (`.env`, `tsconfig.json`, etc.)
441
- - Backend build output (`dist/`)
442
-
443
- The `.npmignore` file ensures only SDK files are published.
444
-
445
- ### Publishing Checklist
446
-
447
- Before publishing:
448
-
449
- 1. **Version Management**
450
- - Update version in `package.json` (follow [semver](https://semver.org/))
451
- - Update `CHANGELOG.md` with changes
452
-
453
- 2. **Code Quality**
454
- - Build SDK: `npm run --workspace @more-ink/irt-edge build:sdk`
455
- - Run tests: `npm run --workspace @more-ink/irt-edge test`
456
- - Check TypeScript: `tsc -p tsconfig.sdk.json --noEmit`
457
-
458
- 3. **Verify Package**
459
- - Dry-run: `npm pack --dry-run`
460
- - Check package size (<50KB)
461
- - Verify only SDK files included
462
-
463
- 4. **Publish**
464
- - `npm run pub` (auto-builds before publishing)
465
-
466
- 5. **Post-Publishing**
467
- - Verify on npm: https://www.npmjs.com/package/@more-ink/irt-edge
468
- - Tag release: `git tag v1.0.0 && git push origin v1.0.0`
469
- - Create GitHub release
470
-
471
- ### Test Installation
472
-
473
- ```bash
474
- mkdir /tmp/test-irt-sdk && cd /tmp/test-irt-sdk
475
- npm init -y
476
- npm install @more-ink/irt-edge @more-ink/irt-core
477
- node -e "const { IrtClient } = require('@more-ink/irt-edge'); console.log('OK')"
478
- ```
479
-
480
- ---
481
-
482
- ## Package Scripts
483
-
484
- ### Development & Backend
485
- - `npm run --workspace @more-ink/irt-edge dev` - Run dev server with hot reload
486
- - `npm run --workspace @more-ink/irt-edge build` - Build backend server to `dist/`
487
- - `npm run --workspace @more-ink/irt-edge start` - Start production backend server
488
- - `npm run --workspace @more-ink/irt-edge seed` - Seed Redis with test data
489
- - `npm run --workspace @more-ink/irt-edge deploy` - Deploy backend to Aliyun FC
490
-
491
- ### SDK Publishing
492
- - `npm run --workspace @more-ink/irt-edge build:sdk` - Build SDK to `sdk/`
493
- - `npm run --workspace @more-ink/irt-edge publish:sdk` - Build and publish SDK to npm
494
- - `npm run pub` - Shortcut for `npm run --workspace @more-ink/irt-edge publish:sdk` (from root)
495
-
496
- ### Testing
497
- - `npm run --workspace @more-ink/irt-edge test` - Run test suite
498
- - `npm run --workspace @more-ink/irt-edge test:watch` - Run tests in watch mode
499
-
500
- ---
501
-
502
- ## Architecture
503
-
504
- ```
505
-
506
- ---
507
-
508
- irt-edge/
509
- ├── src/
510
- │ ├── sdk/ # ✅ SDK source (published)
511
- │ │ ├── types.ts # Type definitions
512
- │ │ ├── client.ts # IrtClient class
513
- │ │ └── index.ts # Public exports
514
- │ ├── index.ts # Backend server (not published)
515
- │ ├── routes.ts # Backend routes
516
- └── storage/ # Backend storage layer
517
- ├── sdk/ # Compiled SDK (published)
518
- ├── dist/ # ❌ Backend build (not published)
519
- ├── examples/ # SDK examples (published)
520
- ├── tsconfig.json # Backend build config
521
- └── tsconfig.sdk.json # SDK build config
522
- ```
523
-
524
- ## License
525
-
526
- Proprietary - All Rights Reserved
1
+ # @more-ink/irt-edge
2
+
3
+ **Two packages in one:**
4
+ 1. **SDK** - Lightweight API client for JavaScript/TypeScript frontends (published to npm)
5
+ 2. **Backend Server** - Production HTTP API server built with Hono for Aliyun Function Compute (not published)
6
+
7
+ ---
8
+
9
+ ## Table of Contents
10
+
11
+ - [SDK Usage (Frontend)](#sdk-usage-frontend)
12
+ - [Installation](#installation)
13
+ - [Quick Start](#quick-start)
14
+ - [Configuration](#configuration)
15
+ - [API Methods](#api-methods)
16
+ - [Error Handling](#error-handling)
17
+ - [Best Practices](#best-practices)
18
+ - [Backend Server Setup](#backend-server-setup)
19
+ - [Backend Deployment](#backend-deployment-aliyun-fc)
20
+ - [SDK Publishing](#sdk-publishing-npm)
21
+ - [Package Scripts](#package-scripts)
22
+
23
+ ---
24
+
25
+ ## SDK Usage (Frontend)
26
+
27
+ ### Installation
28
+
29
+ ```bash
30
+ npm install @more-ink/irt-edge @more-ink/irt-core
31
+ # or
32
+ yarn add @more-ink/irt-edge @more-ink/irt-core
33
+ ```
34
+
35
+ **Note:** `@more-ink/irt-core` is a peer dependency that provides type definitions.
36
+
37
+ ### Quick Start
38
+
39
+ ```typescript
40
+ import { IrtClient } from '@more-ink/irt-edge'
41
+
42
+ const client = new IrtClient({
43
+ namespace: 'exam-a',
44
+ baseUrl: 'https://your-api.example.com',
45
+ headers: {
46
+ 'Authorization': 'Bearer YOUR_TOKEN' // Optional
47
+ },
48
+ timeout: 10000 // Optional (default: 10000ms)
49
+ })
50
+
51
+ // Record an answer and get the next item
52
+ const result = await client.recordAnswer({
53
+ userId: 'user123',
54
+ skillId: 'math',
55
+ itemId: 'item456',
56
+ score: 0.8,
57
+ timestamp: Date.now()
58
+ })
59
+
60
+ console.log('Updated ability:', result.theta)
61
+ console.log('Next item:', result.nextItem)
62
+ ```
63
+
64
+ ### Configuration
65
+
66
+ #### IrtClientConfig
67
+
68
+ ```typescript
69
+ interface IrtClientConfig {
70
+ namespace: string // Required namespace (exam type)
71
+ baseUrl: string // API server base URL (required)
72
+ headers?: Record<string, string> // Custom headers (e.g., auth tokens)
73
+ timeout?: number // Request timeout in ms (default: 10000)
74
+ }
75
+ ```
76
+
77
+ The SDK sends the namespace as `X-IRT-Namespace` on every request; the server rejects calls without it.
78
+
79
+ ### API Methods
80
+
81
+ #### recordAnswer()
82
+
83
+ Record a user's response and get the next recommended item.
84
+
85
+ ```typescript
86
+ async recordAnswer(params: RecordAnswerRequest): Promise<RecordAnswerResponse>
87
+ ```
88
+
89
+ **Parameters:**
90
+ ```typescript
91
+ interface RecordAnswerRequest {
92
+ userId: string // User identifier
93
+ skillId: string // Skill identifier
94
+ itemId: string // Item identifier
95
+ score: number // Response score [0,1] (0=wrong, 1=correct)
96
+ timestamp: number // When response occurred (ms)
97
+ updateOptions?: Partial<UpdateOptions> // Override learning rates
98
+ selectionOptions?: Partial<NextItemOptions> // Override selection behavior
99
+ }
100
+ ```
101
+
102
+ **Returns:** Updated ability (theta), standard error (se), next item, and updated states.
103
+
104
+ **Example:**
105
+ ```typescript
106
+ const result = await client.recordAnswer({
107
+ userId: 'alice',
108
+ skillId: 'geometry',
109
+ itemId: 'q100',
110
+ score: 1.0,
111
+ timestamp: Date.now()
112
+ })
113
+
114
+ console.log(`Ability: ${result.theta.toFixed(2)} ± ${result.se.toFixed(2)}`)
115
+ if (result.nextItem) {
116
+ console.log(`Next question: ${result.nextItem.id}`)
117
+ }
118
+ ```
119
+
120
+ #### recordMultiSkillAnswers()
121
+
122
+ Record multiple skill scores that came from a single multi-skill item. This is useful when one interaction yields separate sub-skill scores (e.g., integrated tasks that grade both reading and writing).
123
+
124
+ ```typescript
125
+ async recordMultiSkillAnswers(
126
+ params: RecordMultiSkillAnswersRequest,
127
+ ): Promise<RecordMultiSkillAnswersResponse>
128
+ ```
129
+
130
+ **Parameters:**
131
+
132
+ ```typescript
133
+ interface RecordMultiSkillAnswersRequest {
134
+ userId: string
135
+ itemId: string
136
+ timestamp?: number
137
+ updateOptions?: Partial<UpdateOptions>
138
+ skillScores: Array<{
139
+ skillId: string
140
+ score: number // [0,1]
141
+ updateOptions?: Partial<UpdateOptions>
142
+ }>
143
+ }
144
+ ```
145
+
146
+ **Returns:** Array of per-skill updates with theta, SE, updated user/item snapshots.
147
+
148
+ **Example:**
149
+
150
+ ```typescript
151
+ const multi = await client.recordMultiSkillAnswers({
152
+ userId: 'alice',
153
+ itemId: 'essay-22',
154
+ skillScores: [
155
+ { skillId: 'reading', score: 0.9 },
156
+ { skillId: 'writing', score: 0.6 },
157
+ ],
158
+ })
159
+
160
+ multi.results.forEach((entry) => {
161
+ console.log(entry.skillId, entry.theta, entry.se)
162
+ })
163
+ ```
164
+
165
+ #### recordAnswerOnly()
166
+
167
+ Record a user's response without requesting the next recommended item (useful when you already control sequencing).
168
+
169
+ ```typescript
170
+ async recordAnswerOnly(params: RecordAnswerRequest): Promise<RecordAnswerOnlyResponse>
171
+ ```
172
+
173
+ ```typescript
174
+ await client.recordAnswerOnly({
175
+ userId: 'alice',
176
+ skillId: 'geometry',
177
+ itemId: 'q100',
178
+ score: 1.0,
179
+ timestamp: Date.now()
180
+ })
181
+ ```
182
+
183
+ #### selectNextItem()
184
+
185
+ Get the next recommended item without recording a response.
186
+
187
+ ```typescript
188
+ async selectNextItem(params: SelectNextItemRequest): Promise<SelectNextItemResponse>
189
+ ```
190
+
191
+ #### resetUser() / resetItem()
192
+
193
+ Reset all Redis-backed skill state for a user or item.
194
+
195
+ ```typescript
196
+ async resetUser(userId: string): Promise<ResetUserResponse>
197
+ async resetItem(itemId: string): Promise<ResetItemResponse>
198
+ ```
199
+
200
+ #### bulkUpdateUser() / bulkUpdateItem()
201
+
202
+ Bulk cold-start/backfill helpers that update **only** user skills or **only** item parameters.
203
+
204
+ ```typescript
205
+ async bulkUpdateUser(
206
+ userId: string,
207
+ events: BulkUpdateEvent[],
208
+ updateOptions?: Partial<UpdateOptions>
209
+ ): Promise<BulkUpdateResponse>
210
+
211
+ async bulkUpdateItem(
212
+ itemId: string,
213
+ events: BulkUpdateEvent[],
214
+ updateOptions?: Partial<UpdateOptions>
215
+ ): Promise<BulkUpdateResponse>
216
+ ```
217
+
218
+ #### getUserState() / getUserStates()
219
+
220
+ Get current user ability estimates.
221
+
222
+ ```typescript
223
+ async getUserState(userId: string, skillId: string): Promise<UserStateResponse>
224
+ async getUserStates(userId: string): Promise<UserStatesResponse>
225
+ ```
226
+
227
+ #### getItemState() / getItemStates()
228
+
229
+ Get item parameters and metadata.
230
+
231
+ ```typescript
232
+ async getItemState(itemId: string, skillId: string): Promise<ItemStateResponse>
233
+ async getItemStates(itemId: string): Promise<ItemStatesResponse>
234
+ ```
235
+
236
+ #### deleteUser() / deleteItem()
237
+
238
+ Delete a user (and their skill states) or an item (and all per-skill calibrations). These are mainly for integration tests and fixture cleanup.
239
+
240
+ ```typescript
241
+ async deleteUser(userId: string): Promise<DeleteUserResponse>
242
+ async deleteItem(itemId: string): Promise<DeleteItemResponse>
243
+ ```
244
+
245
+ #### health()
246
+
247
+ Check API server health status.
248
+
249
+ ```typescript
250
+ async health(): Promise<HealthResponse>
251
+ ```
252
+
253
+ See `examples/sdk-usage.ts` for complete working examples.
254
+
255
+ ### Error Handling
256
+
257
+ All methods throw errors for network failures, HTTP errors, timeouts, and invalid responses.
258
+
259
+ ```typescript
260
+ try {
261
+ const result = await client.recordAnswer({...})
262
+ // Handle success
263
+ } catch (error) {
264
+ if (error instanceof Error) {
265
+ console.error('API error:', error.message)
266
+ if (error.message.includes('timeout')) {
267
+ // Retry logic
268
+ }
269
+ }
270
+ }
271
+ ```
272
+
273
+ ### Best Practices
274
+
275
+ 1. **Reuse client instances** - Create one client and reuse it
276
+ 2. **Use timestamps** - Always provide client-side timestamps for accurate analytics
277
+ 3. **Handle errors gracefully** - Show user-friendly messages on errors
278
+ 4. **Check for null items** - The API may return `null` if no suitable item is available
279
+
280
+ ```typescript
281
+ const result = await client.selectNextItem({...})
282
+ if (result.nextItem) {
283
+ // Show item to user
284
+ } else {
285
+ // No more items available
286
+ }
287
+ ```
288
+
289
+ ---
290
+
291
+ ## Backend Server Setup
292
+
293
+ The backend server is **not published** to npm. It runs as a service on Aliyun Function Compute.
294
+
295
+ ### Features
296
+
297
+ - RESTful IRT API backed by **Redis (online state)** and **Postgres via Prisma** (durable via `/api/irt/sync`)
298
+ - Aliyun FC lifecycle hooks (`/initialize`, `/pre-stop`)
299
+ - Graceful shutdown and health checks
300
+
301
+ ### Development Setup
302
+
303
+ ```bash
304
+ # From repo root
305
+ npm install
306
+
307
+ # Create .env file with:
308
+ # REDIS_URL=redis://... (or rediss://...)
309
+ # REDIS_DB=2
310
+ # DATABASE_URL=postgres://user:pass@host:port/db
311
+
312
+ # Run dev server
313
+ npm run --workspace @more-ink/irt-edge dev
314
+
315
+ # Seed data
316
+ npm run --workspace @more-ink/irt-edge seed
317
+ ```
318
+
319
+ ### Backend Environment Variables
320
+
321
+ ```bash
322
+ # Aliyun Function Compute
323
+ ALIYUN_ACCESS_KEY="YOUR_ALIYUN_ACCESS_KEY"
324
+ ALIYUN_SECRET_ACCESS_KEY="YOUR_ALIYUN_SECRET_KEY"
325
+
326
+ # Database (PostgreSQL via Prisma)
327
+ DATABASE_URL="postgres://user:pass@host:port/db?schema=public"
328
+ # If using PgBouncer / pooled connections, also set DIRECT_URL for Prisma CLI commands
329
+ # DIRECT_URL="postgres://user:pass@host:port/db?schema=public"
330
+
331
+ # Feishu (Lark) Integration
332
+ FEISHU_APP_ID="YOUR_FEISHU_APP_ID"
333
+ FEISHU_APP_SECRET="YOUR_FEISHU_APP_SECRET"
334
+ FEISHU_CHAT_ID="YOUR_FEISHU_CHAT_ID"
335
+
336
+ # Redis (primary online store)
337
+ REDIS_URL="redis://localhost:6379"
338
+ REDIS_DB=2
339
+
340
+ # Debug mode for Function Compute
341
+ DEBUG_FC="true"
342
+
343
+ # Server port (optional, default: 9000)
344
+ PORT=9000
345
+ ```
346
+
347
+ See `src/index.ts` for default IRT engine parameters (learning rates, selection options).
348
+
349
+ ### API Endpoints
350
+
351
+ **IRT Operations:**
352
+ - `POST /api/irt/answer` - Record response and get next item
353
+ - `POST /api/irt/answer-multi` - Record multiple skill responses for one item
354
+ - `POST /api/irt/sync` - Persist dirty Redis state to Postgres (cron-triggered)
355
+ - `POST /api/irt/next-item` - Get next item without recording response
356
+ - `POST /api/irt/users/:userId/reset` - Reset user skills in Redis
357
+ - `POST /api/irt/items/:itemId/reset` - Reset item skills in Redis
358
+ - `POST /api/irt/bulk-update-user/:userId` - Bulk update user skills (user-only)
359
+ - `POST /api/irt/bulk-update-item/:itemId` - Bulk update item parameters (item-only)
360
+ - `DELETE /api/irt/users/:userId` - Delete a user and all skill states
361
+ - `DELETE /api/irt/items/:itemId` - Delete an item and its per-skill calibrations
362
+ - `GET /api/irt/health` - Health check
363
+
364
+ **Aliyun FC Lifecycle:**
365
+ - `POST /initialize` - Instance startup (verifies Prisma DB and Redis connection)
366
+ - `GET /pre-stop` - Instance shutdown (closes Prisma DB and Redis)
367
+
368
+ **System:**
369
+ - `GET /` - Service info
370
+
371
+ ---
372
+
373
+ ## Cold-start Guidance
374
+
375
+ When you have **no prior data** (no user θ, no item a/b), the recommended order for scale is:
376
+
377
+ 1) **Item cold-start** using a default θ (e.g., θ=0).
378
+ 2) **User cold-start** using the calibrated item a/b.
379
+ 3) **Optional**: run item cold-start again to refine a/b.
380
+
381
+ Why this order: item sets are typically small and fixed, while user sets are large and segmented. A quick first-pass item calibration (with θ=0) reduces downstream work before user backfill.
382
+
383
+ Reset and cold-start operations clear **Redis** only. Postgres is treated as a backup store and may retain stale rows until fresh updates overwrite them.
384
+
385
+ ### Bulk Update Constraints
386
+
387
+ - Maximum **10k events** per request (over limit returns 400).
388
+ - `timestamp` is **required** and events are processed in ascending time order.
389
+ - `score` must be in `[0, 1]` (invalid events are skipped with errors).
390
+ - Errors are reported per event; valid events still proceed.
391
+ - Only a **single global** `updateOptions` is supported per bulk request.
392
+
393
+ ---
394
+
395
+ ## Backend Deployment (Aliyun FC)
396
+
397
+ Deploy the backend server to Aliyun FC:
398
+
399
+ ```bash
400
+ # From root (recommended)
401
+ npm run dp
402
+
403
+ # Or from package dir
404
+ npm run --workspace @more-ink/irt-edge deploy
405
+ ```
406
+
407
+ ### How Deployment Works
408
+
409
+ Deployment now runs directly from the package root with npm-installed `node_modules` (no flattening step needed):
410
+ 1. Run `npm run --workspace @more-ink/irt-edge deploy` (runs `prisma generate`, then `build`, then `fc-deploy` from the package root)
411
+ 2. Upload from the package root; `node_modules` already contains real files.
412
+
413
+ Ensure `DATABASE_URL` (and `DIRECT_URL` if pooling) is set in the function env.
414
+
415
+ ---
416
+
417
+ ## SDK Publishing (npm)
418
+
419
+ Publish the SDK to npm (backend code stays private):
420
+
421
+ ```bash
422
+ # From repo root (recommended)
423
+ npm run pub
424
+
425
+ # Or from package directory
426
+ cd packages/irt-edge
427
+ npm run publish:sdk
428
+
429
+ # Or using npm directly
430
+ npm publish --access public
431
+ ```
432
+
433
+ ### What Gets Published
434
+
435
+ **Published to npm:**
436
+ - ✅ `sdk/` - Compiled SDK code (~40KB unpacked)
437
+ - `README.md`, `CHANGELOG.md` - Documentation
438
+ - `examples/` - Usage examples
439
+ - `package.json` - Package metadata
440
+
441
+ **NOT published (stays private):**
442
+ - ❌ Backend server code (`src/index.ts`, `routes.ts`, `storage/`)
443
+ - Scripts and tests (`scripts/`, `tests/`)
444
+ - ❌ Config files (`.env`, `tsconfig.json`, etc.)
445
+ - Backend build output (`dist/`)
446
+
447
+ The `.npmignore` file ensures only SDK files are published.
448
+
449
+ ### Publishing Checklist
450
+
451
+ Before publishing:
452
+
453
+ 1. **Version Management**
454
+ - Update version in `package.json` (follow [semver](https://semver.org/))
455
+ - Update `CHANGELOG.md` with changes
456
+
457
+ 2. **Code Quality**
458
+ - Build SDK: `npm run --workspace @more-ink/irt-edge build:sdk`
459
+ - Run tests: `npm run --workspace @more-ink/irt-edge test`
460
+ - Check TypeScript: `tsc -p tsconfig.sdk.json --noEmit`
461
+
462
+ 3. **Verify Package**
463
+ - Dry-run: `npm pack --dry-run`
464
+ - Check package size (<50KB)
465
+ - Verify only SDK files included
466
+
467
+ 4. **Publish**
468
+ - `npm run pub` (auto-builds before publishing)
469
+
470
+ 5. **Post-Publishing**
471
+ - Verify on npm: https://www.npmjs.com/package/@more-ink/irt-edge
472
+ - Tag release: `git tag v1.0.0 && git push origin v1.0.0`
473
+ - Create GitHub release
474
+
475
+ ### Test Installation
476
+
477
+ ```bash
478
+ mkdir /tmp/test-irt-sdk && cd /tmp/test-irt-sdk
479
+ npm init -y
480
+ npm install @more-ink/irt-edge @more-ink/irt-core
481
+ node -e "const { IrtClient } = require('@more-ink/irt-edge'); console.log('OK')"
482
+ ```
483
+
484
+ ---
485
+
486
+ ## Package Scripts
487
+
488
+ ### Development & Backend
489
+ - `npm run --workspace @more-ink/irt-edge dev` - Run dev server with hot reload
490
+ - `npm run --workspace @more-ink/irt-edge build` - Build backend server to `dist/`
491
+ - `npm run --workspace @more-ink/irt-edge start` - Start production backend server
492
+ - `npm run --workspace @more-ink/irt-edge seed` - Seed Redis with test data
493
+ - `npm run --workspace @more-ink/irt-edge deploy` - Deploy backend to Aliyun FC
494
+
495
+ ### SDK Publishing
496
+ - `npm run --workspace @more-ink/irt-edge build:sdk` - Build SDK to `sdk/`
497
+ - `npm run --workspace @more-ink/irt-edge publish:sdk` - Build and publish SDK to npm
498
+ - `npm run pub` - Shortcut for `npm run --workspace @more-ink/irt-edge publish:sdk` (from root)
499
+
500
+ ### Testing
501
+ - `npm run --workspace @more-ink/irt-edge test` - Run test suite
502
+ - `npm run --workspace @more-ink/irt-edge test:watch` - Run tests in watch mode
503
+
504
+ ---
505
+
506
+ ## Architecture
507
+
508
+ ```
509
+
510
+ ---
511
+
512
+ irt-edge/
513
+ ├── src/
514
+ │ ├── sdk/ # SDK source (published)
515
+ ├── types.ts # Type definitions
516
+ │ ├── client.ts # IrtClient class
517
+ │ │ └── index.ts # Public exports
518
+ ├── index.ts # ❌ Backend server (not published)
519
+ ├── routes.ts # Backend routes
520
+ │ └── storage/ # Backend storage layer
521
+ ├── sdk/ # ✅ Compiled SDK (published)
522
+ ├── dist/ # ❌ Backend build (not published)
523
+ ├── examples/ # ✅ SDK examples (published)
524
+ ├── tsconfig.json # Backend build config
525
+ └── tsconfig.sdk.json # SDK build config
526
+ ```
527
+
528
+ ## License
529
+
530
+ Proprietary - All Rights Reserved