@itsoltech/basal-client 0.0.0-stage → 0.1.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.
@@ -0,0 +1,164 @@
1
+ function text(value, name) {
2
+ if (typeof value !== 'string' || !value.trim())
3
+ throw new TypeError(`${name} must be a nonempty string`);
4
+ return value;
5
+ }
6
+ function criteria(values, min, max = Infinity) {
7
+ if (!values || typeof values !== 'object' || Array.isArray(values))
8
+ throw new TypeError('criteria must be an object');
9
+ const entries = Object.entries(values);
10
+ if (entries.length < min || entries.length > max)
11
+ throw new TypeError('invalid number of criteria');
12
+ for (const [key, value] of entries) {
13
+ text(key, 'criterion key');
14
+ text(value, 'criterion description');
15
+ }
16
+ return Object.fromEntries(entries);
17
+ }
18
+ function number(value, name, probability = false) {
19
+ if (typeof value !== 'number' || !Number.isFinite(value) || (probability && (value < 0 || value > 1))) {
20
+ throw new TypeError(`invalid ${name}`);
21
+ }
22
+ return value;
23
+ }
24
+ function evidence(options) {
25
+ if (options.evidence !== undefined && typeof options.evidence !== 'boolean')
26
+ throw new TypeError('evidence must be boolean');
27
+ return options.evidence ? { evidence: true } : {};
28
+ }
29
+ /** Immutable snapshot of a typed question. Use factories or the concrete constructors. */
30
+ export class QuestionSpec {
31
+ wire;
32
+ constructor(kind, instructions, fields) {
33
+ this.wire = structuredClone({ ...fields, type: kind, instructions: text(instructions, 'instructions') });
34
+ }
35
+ toJSON() { return structuredClone(this.wire); }
36
+ /** Validate and retrieve an answer with this question's result type. */
37
+ read(response, id) {
38
+ const answer = response.answers?.[id];
39
+ if (!answer || answer.type !== this.wire.type || !answer.probabilities || typeof answer.probabilities !== 'object' || Array.isArray(answer.probabilities)) {
40
+ throw new TypeError('Basal returned a missing or mismatched answer');
41
+ }
42
+ const fields = {
43
+ choice: { choice: 'string', confidence: 'number' }, noul: { noul: 'number', confidence: 'number' },
44
+ score: { score: 'number', legend: 'object', confidence: 'number' },
45
+ multi: { selected: 'object', threshold: 'number', set_confidence: 'number' },
46
+ act: { action: 'string', answer: 'string', expected_costs: 'object', confidence: 'number', calibration: 'string' },
47
+ };
48
+ const data = answer;
49
+ for (const [field, kind] of Object.entries(fields[answer.type])) {
50
+ if (typeof data[field] !== kind || data[field] === null || (kind === 'number' && !Number.isFinite(data[field]))) {
51
+ throw new TypeError('Basal returned an invalid answer field');
52
+ }
53
+ }
54
+ for (const value of Object.values(answer.probabilities))
55
+ number(value, 'probability', true);
56
+ const expectedKeys = this.wire.type === 'noul' || (this.wire.type === 'act' && this.wire.criteria === undefined)
57
+ ? ['true', 'false'] : Array.isArray(this.wire.criteria)
58
+ ? this.wire.criteria.map((_, index) => String(index)) : Object.keys(this.wire.criteria ?? {});
59
+ if (expectedKeys.some(key => !Object.hasOwn(answer.probabilities, key)))
60
+ throw new TypeError('missing outcome probability');
61
+ if (answer.evidence !== undefined) {
62
+ if (!Array.isArray(answer.evidence))
63
+ throw new TypeError('invalid evidence');
64
+ for (const span of answer.evidence) {
65
+ if (!span || typeof span.text !== 'string' || !Number.isSafeInteger(span.start) || !Number.isSafeInteger(span.end)
66
+ || span.start < 0 || span.end < 0)
67
+ throw new TypeError('invalid evidence span');
68
+ number(span.probability, 'evidence probability', true);
69
+ }
70
+ }
71
+ if (answer.type === 'choice' && !Object.hasOwn(this.wire.criteria ?? {}, answer.choice))
72
+ throw new TypeError('unknown choice');
73
+ if (answer.type === 'multi' && (!Array.isArray(answer.selected) || answer.selected.some(k => typeof k !== 'string' || !Object.hasOwn(this.wire.criteria ?? {}, k))))
74
+ throw new TypeError('invalid selected labels');
75
+ if (answer.type === 'score' && (Array.isArray(answer.legend) || Object.values(answer.legend).some(v => typeof v !== 'string')))
76
+ throw new TypeError('invalid legend');
77
+ if (answer.type === 'act') {
78
+ if (!['validated', 'unvalidated'].includes(answer.calibration) || !Object.hasOwn(this.wire.costs ?? {}, answer.action) || !expectedKeys.includes(answer.answer))
79
+ throw new TypeError('invalid action');
80
+ if (Array.isArray(answer.expected_costs) || Object.keys(this.wire.costs ?? {}).some(key => !Object.hasOwn(answer.expected_costs, key)))
81
+ throw new TypeError('invalid expected costs');
82
+ for (const value of Object.values(answer.expected_costs))
83
+ number(value, 'expected cost');
84
+ }
85
+ return { ...answer, ...(answer.type === 'multi' ? {} : { evidence: answer.evidence ?? [] }) };
86
+ }
87
+ }
88
+ export class ChoiceQuestion extends QuestionSpec {
89
+ constructor(instructions, values, options = {}) {
90
+ if (options.option_keys !== undefined && !['show', 'hide'].includes(options.option_keys))
91
+ throw new TypeError('invalid option_keys');
92
+ super('choice', instructions, { criteria: criteria(values, 2, 10), ...evidence(options), ...(options.option_keys === 'hide' ? { option_keys: 'hide' } : {}) });
93
+ }
94
+ }
95
+ export class NoulQuestion extends QuestionSpec {
96
+ constructor(instructions, options = {}) { super('noul', instructions, evidence(options)); }
97
+ }
98
+ export class ScoreQuestion extends QuestionSpec {
99
+ constructor(instructions, levels, options = {}) {
100
+ if (!Array.isArray(levels) || levels.length < 2 || levels.length > 10)
101
+ throw new TypeError('score requires 2–10 ordered levels');
102
+ super('score', instructions, { criteria: levels.map(v => text(v, 'level')), ...evidence(options) });
103
+ }
104
+ }
105
+ export class MultiQuestion extends QuestionSpec {
106
+ constructor(instructions, values, options = {}) {
107
+ if ('evidence' in options)
108
+ throw new TypeError('multi does not support evidence');
109
+ const fields = { criteria: criteria(values, 1) };
110
+ if (options.threshold !== undefined)
111
+ fields.threshold = number(options.threshold, 'threshold', true);
112
+ for (const key of ['min', 'max']) {
113
+ const value = options[key];
114
+ if (value !== undefined) {
115
+ if (!Number.isInteger(value) || value < 0 || value > Object.keys(values).length)
116
+ throw new TypeError(`invalid ${key}`);
117
+ fields[key] = value;
118
+ }
119
+ }
120
+ if (options.min !== undefined && options.max !== undefined && options.min > options.max)
121
+ throw new TypeError('min exceeds max');
122
+ super('multi', instructions, fields);
123
+ }
124
+ }
125
+ export class ActQuestion extends QuestionSpec {
126
+ constructor(instructions, costs, options = {}) {
127
+ const optionsMap = options.criteria === undefined ? undefined : criteria(options.criteria, 2, 10);
128
+ const outcomes = Object.keys(optionsMap ?? { 'true': 'Yes', 'false': 'No' });
129
+ if (!costs || typeof costs !== 'object' || Array.isArray(costs) || !Object.keys(costs).length)
130
+ throw new TypeError('act requires a cost matrix');
131
+ const matrix = {};
132
+ for (const [action, row] of Object.entries(costs)) {
133
+ text(action, 'action');
134
+ if (!row || typeof row !== 'object' || Array.isArray(row) || Object.keys(row).length !== outcomes.length || outcomes.some(k => !Object.hasOwn(row, k)))
135
+ throw new TypeError('each cost row must cover every outcome exactly');
136
+ Object.defineProperty(matrix, action, { value: Object.fromEntries(Object.entries(row).map(([k, v]) => [k, number(v, 'cost')])), enumerable: true });
137
+ }
138
+ if (options.defer_action !== undefined && !Object.hasOwn(matrix, options.defer_action))
139
+ throw new TypeError('unknown defer_action');
140
+ super('act', instructions, { costs: matrix, ...evidence(options), ...(optionsMap ? { criteria: optionsMap } : {}),
141
+ ...(options.defer_action === undefined ? {} : { defer_action: options.defer_action }),
142
+ ...(options.max_error === undefined ? {} : { max_error: number(options.max_error, 'max_error', true) }) });
143
+ }
144
+ }
145
+ export function choice(instructions, values, options) { return new ChoiceQuestion(instructions, values, options); }
146
+ export function noul(instructions, options) { return new NoulQuestion(instructions, options); }
147
+ export function score(instructions, levels, options) { return new ScoreQuestion(instructions, levels, options); }
148
+ export function multi(instructions, values, options) { return new MultiQuestion(instructions, values, options); }
149
+ export function act(instructions, costs, options) { return new ActQuestion(instructions, costs, options); }
150
+ export function buildRequest(state, questions, options) {
151
+ if (state === null || state === undefined)
152
+ throw new TypeError('state must not be null/undefined');
153
+ if (!questions || !Object.keys(questions).length)
154
+ throw new TypeError('questions must not be empty');
155
+ if (Object.getOwnPropertySymbols(questions).length)
156
+ throw new TypeError('question keys must be strings or numbers');
157
+ const entries = Object.entries(questions).map(([id, q]) => { text(id, 'question ID'); if (!(q instanceof QuestionSpec))
158
+ throw new TypeError('expected typed question'); return [id, q.toJSON()]; });
159
+ if (options.model !== undefined)
160
+ text(options.model, 'model');
161
+ if (options.facts !== undefined && !['off', 'auto'].includes(options.facts))
162
+ throw new TypeError('invalid facts');
163
+ return { state, questions: Object.fromEntries(entries), ...(options.model === undefined ? {} : { model: options.model }), ...(options.facts === undefined ? {} : { facts: options.facts }) };
164
+ }
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
2
  "name": "@itsoltech/basal-client",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "TypeScript and JavaScript client for the native Basal HTTP API",
5
+ "type": "module",
6
+ "main": "./dist/cjs/index.cjs",
7
+ "module": "./dist/esm/index.js",
8
+ "types": "./dist/esm/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/esm/index.d.ts",
13
+ "default": "./dist/esm/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/cjs/index.d.cts",
17
+ "default": "./dist/cjs/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "README.md",
24
+ "LICENSE",
25
+ "NOTICE"
26
+ ],
27
+ "sideEffects": false,
28
+ "engines": {
29
+ "node": ">=22"
30
+ },
31
+ "license": "Apache-2.0",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/itsoltech/basal-rs.git",
35
+ "directory": "sdk/typescript"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "scripts": {
41
+ "build": "node build.mjs",
42
+ "prepack": "npm run build"
43
+ },
44
+ "devDependencies": {
45
+ "typescript": "6.0.3"
46
+ }
47
+ }