@gobing-ai/ts-ai-decision 0.5.13

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/hub.d.ts ADDED
@@ -0,0 +1,142 @@
1
+ import { type Json } from '@gobing-ai/ts-ai-runner';
2
+ import { type CatalogLoadOptions } from './catalog';
3
+ import { type BuiltinMakerOptions, DecisionMakerRegistry, type MakerSource } from './registry';
4
+ import type { DecisionCatalog, DecisionCriteria, DecisionParam, DecisionType } from './types';
5
+ /**
6
+ * DecisionHub (task 0090, ADR-033 § API / § decide algorithm): loads catalogs
7
+ * over the 0089 loader and serves `decide` through makers resolved by name
8
+ * from the 0091 registry. `decide` never rejects for backend construction,
9
+ * transport, timeout, answer validation or low confidence — it maps every such
10
+ * outcome to the declared fallback — and rejects only for caller mistakes
11
+ * (unknown id, invalid input, unregistered per-call maker), always before any
12
+ * backend call. The hub contains no driver code; every maker comes from
13
+ * `registry.resolve(name)`.
14
+ */
15
+ /** Package confidence floor when neither the decision nor the catalog declares one. */
16
+ export declare const DEFAULT_MIN_CONFIDENCE = 0.7;
17
+ /** Hub default when neither the per-call options, the decision nor the catalog names a maker. */
18
+ export declare const DEFAULT_MAKER = "typesafe";
19
+ /** Shared envelope of every {@link DecisionResult} variant. */
20
+ export interface DecisionResultBase {
21
+ readonly id: string;
22
+ /** Observed calibration; null only when no answer was obtained. */
23
+ readonly confidence: number | null;
24
+ /** `model` for an accepted answer, `default` for any fallback. */
25
+ readonly source: 'model' | 'default';
26
+ readonly reason: 'accepted' | 'low-confidence' | 'no-backend' | 'timeout' | 'error';
27
+ /** Registry name of the maker that was selected. */
28
+ readonly maker: string;
29
+ readonly durationMs: number;
30
+ }
31
+ /** The fallback-guaranteed result of one `decide` call, shaped by the decision type. */
32
+ export type DecisionResult = (DecisionResultBase & {
33
+ readonly type: 'choice';
34
+ readonly value: string;
35
+ }) | (DecisionResultBase & {
36
+ readonly type: 'score';
37
+ readonly value: number;
38
+ }) | (DecisionResultBase & {
39
+ readonly type: 'noul';
40
+ readonly value: boolean;
41
+ readonly probability: number | null;
42
+ });
43
+ /** One loaded decision, for downstream discovery (`hub.list()`). */
44
+ export interface DecisionSummary {
45
+ readonly id: string;
46
+ readonly type: DecisionType;
47
+ readonly description?: string;
48
+ readonly source: string;
49
+ }
50
+ /** Full served contract of one decision (`hub.describe(id)`), including the effective defaults. */
51
+ export interface DecisionDescriptor {
52
+ readonly id: string;
53
+ readonly type: DecisionType;
54
+ readonly description?: string;
55
+ readonly source: string;
56
+ /** Declared parameter contract including the reserved `instructions` parameter. */
57
+ readonly parameters: Readonly<Record<string, DecisionParam>>;
58
+ readonly criteria: DecisionCriteria;
59
+ readonly fallback: string | number | boolean;
60
+ /** decision → catalog defaults → {@link DEFAULT_MIN_CONFIDENCE}. */
61
+ readonly minConfidence: number;
62
+ /** decision → catalog defaults → hub defaultMaker. */
63
+ readonly maker: string;
64
+ /** decision → catalog defaults; absent when neither declares one. */
65
+ readonly model?: string;
66
+ }
67
+ /** {@link DecisionHub} construction options. */
68
+ export interface DecisionHubOptions {
69
+ /** Default: a fresh `DecisionMakerRegistry` with the three built-in makers. */
70
+ readonly registry?: DecisionMakerRegistry;
71
+ /** Used when neither the decision nor the catalog names a maker. Default 'typesafe'. */
72
+ readonly defaultMaker?: string;
73
+ /** Clock for `durationMs`; injectable so tests are deterministic. Default `Date.now`. */
74
+ readonly now?: () => number;
75
+ }
76
+ /** Per-call options for {@link DecisionHub.decide}. */
77
+ export interface DecideOptions {
78
+ /** Per-call maker name; wins over the decision's, the catalog's and the hub default. */
79
+ readonly maker?: string;
80
+ }
81
+ /**
82
+ * Hub over loaded catalogs and named makers (task 0090, ADR-033 § API):
83
+ * `decide` serves one Jev question per call and resolves every backend outcome
84
+ * to the declared fallback; only caller mistakes throw, before any backend
85
+ * call. Discovery (`list`/`describe`) and maker construction stay decoupled —
86
+ * makers come only from `registry.resolve(name)`.
87
+ */
88
+ export declare class DecisionHub {
89
+ readonly registry: DecisionMakerRegistry;
90
+ private readonly decisions;
91
+ private readonly defaultMaker;
92
+ private readonly now;
93
+ constructor(options?: DecisionHubOptions);
94
+ /**
95
+ * Register every decision of an already-loaded catalog. All-or-nothing: a
96
+ * duplicate decision id or an unregistered maker name (decision or
97
+ * `defaults.maker`) throws {@link DecisionCatalogError} and registers
98
+ * nothing from the offending catalog.
99
+ */
100
+ load(catalog: DecisionCatalog): void;
101
+ /** Load a catalog file through the 0089 loader, then register it via {@link load}. */
102
+ loadFile(path: string, options?: CatalogLoadOptions): Promise<void>;
103
+ /** Pure read over the loaded decisions; never constructs a maker or driver. */
104
+ list(): DecisionSummary[];
105
+ /** Pure read of one decision's served contract; throws UnknownDecisionError for an unknown id. */
106
+ describe(id: string): DecisionDescriptor;
107
+ /**
108
+ * Serve one decision with a fallback-guaranteed answer. Resolves the input,
109
+ * renders exactly one Jev question keyed by the decision id, selects the
110
+ * maker by name (per call → decision → catalog defaults → hub default) and
111
+ * issues a single `maker.ask`. Backend construction, transport, timeout,
112
+ * invalid answers and low confidence all resolve to the declared fallback;
113
+ * only caller mistakes throw, before any backend call.
114
+ */
115
+ decide(id: string, input?: Record<string, Json>, options?: DecideOptions): Promise<DecisionResult>;
116
+ /** Maker-name precedence for discovery results: decision → catalog defaults → hub default. */
117
+ private effectiveMakerName;
118
+ private mustGet;
119
+ /**
120
+ * Build the declared-fallback envelope: `source: 'default'`, the observed
121
+ * confidence (null when none), always the selected maker name. For noul,
122
+ * `probability` stays null unless the answer itself supplied one.
123
+ */
124
+ private fallbackResult;
125
+ }
126
+ /** Options for {@link createDecisionHub}: one call from options to a ready hub. */
127
+ export interface CreateDecisionHubOptions extends DecisionHubOptions {
128
+ /** Ignored when `registry` is supplied. Default: the three built-in makers. */
129
+ readonly builtins?: BuiltinMakerOptions | false;
130
+ /** Extra makers registered before any catalog loads (catalog checks run at load). */
131
+ readonly makers?: Readonly<Record<string, MakerSource>>;
132
+ /** Catalog files loaded in order with {@link CreateDecisionHubOptions.catalogOptions}. */
133
+ readonly catalogs?: readonly string[];
134
+ /** Forwarded to every `loadFile` call. */
135
+ readonly catalogOptions?: CatalogLoadOptions;
136
+ }
137
+ /**
138
+ * Build the registry, register `makers`, construct the hub and `loadFile` each
139
+ * catalog in order. Any failure rejects; a partially built hub never escapes.
140
+ */
141
+ export declare function createDecisionHub(options?: CreateDecisionHubOptions): Promise<DecisionHub>;
142
+ //# sourceMappingURL=hub.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hub.d.ts","sourceRoot":"","sources":["../src/hub.ts"],"names":[],"mappings":"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"}
package/dist/hub.js ADDED
@@ -0,0 +1,262 @@
1
+ import { DecisionConfigError, DecisionTimeoutError, q, } from '@gobing-ai/ts-ai-runner';
2
+ import { loadDecisionCatalog } from './catalog.js';
3
+ import { DecisionCatalogError, UnknownDecisionError, UnknownDecisionMakerError } from './errors.js';
4
+ import { buildState, renderQuestion, resolveDecisionInput } from './params.js';
5
+ import { DecisionMakerRegistry } from './registry.js';
6
+ /**
7
+ * DecisionHub (task 0090, ADR-033 § API / § decide algorithm): loads catalogs
8
+ * over the 0089 loader and serves `decide` through makers resolved by name
9
+ * from the 0091 registry. `decide` never rejects for backend construction,
10
+ * transport, timeout, answer validation or low confidence — it maps every such
11
+ * outcome to the declared fallback — and rejects only for caller mistakes
12
+ * (unknown id, invalid input, unregistered per-call maker), always before any
13
+ * backend call. The hub contains no driver code; every maker comes from
14
+ * `registry.resolve(name)`.
15
+ */
16
+ /** Package confidence floor when neither the decision nor the catalog declares one. */
17
+ export const DEFAULT_MIN_CONFIDENCE = 0.7;
18
+ /** Hub default when neither the per-call options, the decision nor the catalog names a maker. */
19
+ export const DEFAULT_MAKER = 'typesafe';
20
+ /** Effective confidence floor: decision → catalog defaults → package default. */
21
+ function effectiveMinConfidence(definition, defaults) {
22
+ return definition.minConfidence ?? defaults.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
23
+ }
24
+ /** Confidence observed in one validated answer; noul synthesizes `max(p, 1 - p)`. */
25
+ function observedAnswer(answer) {
26
+ switch (answer.kind) {
27
+ case 'choice':
28
+ return { confidence: answer.confidence };
29
+ case 'score':
30
+ return { confidence: answer.confidence };
31
+ case 'noul':
32
+ return {
33
+ confidence: Math.max(answer.probability, 1 - answer.probability),
34
+ probability: answer.probability,
35
+ };
36
+ }
37
+ }
38
+ /**
39
+ * Map the rendered question onto the neutral Jev builders — exactly one
40
+ * question per `decide` call, keyed by the decision id at the `ask` level.
41
+ */
42
+ function buildJevQuestion(question) {
43
+ switch (question.type) {
44
+ case 'choice':
45
+ return q.choice(question.instructions, { ...question.criteria });
46
+ case 'score':
47
+ // The loader enforces >= 2 levels; the tuple is the builder's shape.
48
+ return q.score(question.instructions, question.criteria);
49
+ case 'noul': {
50
+ const outcomes = question.criteria;
51
+ return q.noul(question.instructions, outcomes !== undefined ? { yes: outcomes.true, no: outcomes.false } : undefined);
52
+ }
53
+ }
54
+ }
55
+ /**
56
+ * Hub over loaded catalogs and named makers (task 0090, ADR-033 § API):
57
+ * `decide` serves one Jev question per call and resolves every backend outcome
58
+ * to the declared fallback; only caller mistakes throw, before any backend
59
+ * call. Discovery (`list`/`describe`) and maker construction stay decoupled —
60
+ * makers come only from `registry.resolve(name)`.
61
+ */
62
+ export class DecisionHub {
63
+ registry;
64
+ decisions = new Map();
65
+ defaultMaker;
66
+ now;
67
+ constructor(options = {}) {
68
+ this.registry = options.registry ?? new DecisionMakerRegistry();
69
+ this.defaultMaker = options.defaultMaker ?? DEFAULT_MAKER;
70
+ this.now = options.now ?? Date.now;
71
+ if (!this.registry.has(this.defaultMaker)) {
72
+ throw new UnknownDecisionMakerError(`Default decision maker '${this.defaultMaker}' is not registered in the hub registry ` +
73
+ `(registered: ${this.registry.names().join(', ') || 'none'})`, this.defaultMaker);
74
+ }
75
+ }
76
+ /**
77
+ * Register every decision of an already-loaded catalog. All-or-nothing: a
78
+ * duplicate decision id or an unregistered maker name (decision or
79
+ * `defaults.maker`) throws {@link DecisionCatalogError} and registers
80
+ * nothing from the offending catalog.
81
+ */
82
+ load(catalog) {
83
+ const defaultsMaker = catalog.defaults.maker;
84
+ if (defaultsMaker !== undefined && !this.registry.has(defaultsMaker)) {
85
+ throw new DecisionCatalogError(`Catalog "${catalog.source}" defaults.maker '${defaultsMaker}' is not registered in the hub registry ` +
86
+ `(registered: ${this.registry.names().join(', ') || 'none'})`, catalog.source, undefined, 'maker');
87
+ }
88
+ // Stage first, commit only when every decision passed the cross-catalog checks.
89
+ const staged = [];
90
+ for (const [id, definition] of Object.entries(catalog.decisions)) {
91
+ const existing = this.decisions.get(id);
92
+ if (existing !== undefined) {
93
+ throw new DecisionCatalogError(`Decision id "${id}" in catalog "${catalog.source}" duplicates the decision loaded from ` +
94
+ `"${existing.definition.source}"`, catalog.source, id, 'id');
95
+ }
96
+ if (definition.maker !== undefined && !this.registry.has(definition.maker)) {
97
+ throw new DecisionCatalogError(`Decision "${id}" in catalog "${catalog.source}" names maker '${definition.maker}' that is not ` +
98
+ `registered in the hub registry (registered: ${this.registry.names().join(', ') || 'none'})`, catalog.source, id, 'maker');
99
+ }
100
+ staged.push({ definition, defaults: catalog.defaults });
101
+ }
102
+ for (const entry of staged)
103
+ this.decisions.set(entry.definition.id, entry);
104
+ }
105
+ /** Load a catalog file through the 0089 loader, then register it via {@link load}. */
106
+ async loadFile(path, options = {}) {
107
+ this.load(await loadDecisionCatalog(path, options));
108
+ }
109
+ /** Pure read over the loaded decisions; never constructs a maker or driver. */
110
+ list() {
111
+ return [...this.decisions.values()].map(({ definition }) => ({
112
+ id: definition.id,
113
+ type: definition.type,
114
+ ...(definition.description !== undefined ? { description: definition.description } : {}),
115
+ source: definition.source,
116
+ }));
117
+ }
118
+ /** Pure read of one decision's served contract; throws UnknownDecisionError for an unknown id. */
119
+ describe(id) {
120
+ const { definition, defaults } = this.mustGet(id);
121
+ const model = definition.model ?? defaults.model;
122
+ return {
123
+ id: definition.id,
124
+ type: definition.type,
125
+ ...(definition.description !== undefined ? { description: definition.description } : {}),
126
+ source: definition.source,
127
+ parameters: definition.parameters,
128
+ criteria: definition.criteria,
129
+ fallback: definition.fallback,
130
+ minConfidence: effectiveMinConfidence(definition, defaults),
131
+ maker: this.effectiveMakerName(definition, defaults),
132
+ ...(model !== undefined ? { model } : {}),
133
+ };
134
+ }
135
+ /**
136
+ * Serve one decision with a fallback-guaranteed answer. Resolves the input,
137
+ * renders exactly one Jev question keyed by the decision id, selects the
138
+ * maker by name (per call → decision → catalog defaults → hub default) and
139
+ * issues a single `maker.ask`. Backend construction, transport, timeout,
140
+ * invalid answers and low confidence all resolve to the declared fallback;
141
+ * only caller mistakes throw, before any backend call.
142
+ */
143
+ async decide(id, input = {}, options = {}) {
144
+ const start = this.now();
145
+ const { definition, defaults } = this.mustGet(id); // UnknownDecisionError — caller error
146
+ const params = resolveDecisionInput(definition, input); // DecisionInputError — caller error
147
+ const makerName = options.maker ?? definition.maker ?? defaults.maker ?? this.defaultMaker;
148
+ if (!this.registry.has(makerName)) {
149
+ // Reachable only for a per-call name: catalog and hub default names are checked at
150
+ // load/construct. It is a caller error, thrown before any backend interaction.
151
+ throw new UnknownDecisionMakerError(`Decision "${id}" was asked on maker '${makerName}' that is not registered in the hub registry ` +
152
+ `(registered: ${this.registry.names().join(', ') || 'none'})`, makerName);
153
+ }
154
+ const state = buildState(definition, params);
155
+ const model = definition.model ?? defaults.model;
156
+ const request = {
157
+ state,
158
+ questions: { [definition.id]: buildJevQuestion(renderQuestion(definition, params)) },
159
+ ...(model !== undefined ? { model } : {}),
160
+ };
161
+ // The one auditable try/catch for steps 4–5: a resolve failure is a construction
162
+ // failure (no-backend); ask failures map per the error taxonomy. Neither rejects.
163
+ let inAsk = false;
164
+ try {
165
+ const maker = await this.registry.resolve(makerName);
166
+ inAsk = true;
167
+ const answers = await maker.ask(request);
168
+ const answer = answers[definition.id];
169
+ if (answer === undefined || answer.kind !== definition.type) {
170
+ return this.fallbackResult(definition, makerName, 'error', null, start);
171
+ }
172
+ const observed = observedAnswer(answer);
173
+ if (observed.confidence < effectiveMinConfidence(definition, defaults)) {
174
+ return this.fallbackResult(definition, makerName, 'low-confidence', observed.confidence, start, observed.probability);
175
+ }
176
+ const base = {
177
+ id: definition.id,
178
+ confidence: observed.confidence,
179
+ source: 'model',
180
+ reason: 'accepted',
181
+ maker: makerName,
182
+ durationMs: this.now() - start,
183
+ };
184
+ switch (answer.kind) {
185
+ case 'choice':
186
+ return { ...base, type: 'choice', value: answer.label };
187
+ case 'score':
188
+ return { ...base, type: 'score', value: answer.score };
189
+ case 'noul':
190
+ return {
191
+ ...base,
192
+ type: 'noul',
193
+ value: answer.probability >= 0.5,
194
+ probability: answer.probability,
195
+ };
196
+ }
197
+ }
198
+ catch (error) {
199
+ const reason = !inAsk || error instanceof DecisionConfigError
200
+ ? 'no-backend'
201
+ : error instanceof DecisionTimeoutError
202
+ ? 'timeout'
203
+ : 'error';
204
+ return this.fallbackResult(definition, makerName, reason, null, start);
205
+ }
206
+ }
207
+ /** Maker-name precedence for discovery results: decision → catalog defaults → hub default. */
208
+ effectiveMakerName(definition, defaults) {
209
+ return definition.maker ?? defaults.maker ?? this.defaultMaker;
210
+ }
211
+ mustGet(id) {
212
+ const registered = this.decisions.get(id);
213
+ if (registered === undefined) {
214
+ const loaded = [...this.decisions.keys()];
215
+ throw new UnknownDecisionError(`Unknown decision "${id}" — loaded decisions: ${loaded.length > 0 ? loaded.join(', ') : '(none)'}`, id);
216
+ }
217
+ return registered;
218
+ }
219
+ /**
220
+ * Build the declared-fallback envelope: `source: 'default'`, the observed
221
+ * confidence (null when none), always the selected maker name. For noul,
222
+ * `probability` stays null unless the answer itself supplied one.
223
+ */
224
+ fallbackResult(definition, makerName, reason, confidence, start, probability) {
225
+ const base = {
226
+ id: definition.id,
227
+ confidence,
228
+ source: 'default',
229
+ reason,
230
+ maker: makerName,
231
+ durationMs: this.now() - start,
232
+ };
233
+ switch (definition.type) {
234
+ case 'choice':
235
+ return { ...base, type: 'choice', value: definition.fallback };
236
+ case 'score':
237
+ return { ...base, type: 'score', value: definition.fallback };
238
+ case 'noul':
239
+ return {
240
+ ...base,
241
+ type: 'noul',
242
+ value: definition.fallback,
243
+ probability: probability ?? null,
244
+ };
245
+ }
246
+ }
247
+ }
248
+ /**
249
+ * Build the registry, register `makers`, construct the hub and `loadFile` each
250
+ * catalog in order. Any failure rejects; a partially built hub never escapes.
251
+ */
252
+ export async function createDecisionHub(options = {}) {
253
+ const registry = options.registry ?? new DecisionMakerRegistry({ builtins: options.builtins });
254
+ for (const [name, source] of Object.entries(options.makers ?? {})) {
255
+ registry.register(name, source);
256
+ }
257
+ const hub = new DecisionHub({ registry, defaultMaker: options.defaultMaker, now: options.now });
258
+ for (const path of options.catalogs ?? []) {
259
+ await hub.loadFile(path, options.catalogOptions);
260
+ }
261
+ return hub;
262
+ }
@@ -0,0 +1,8 @@
1
+ export * from './catalog';
2
+ export * from './errors';
3
+ export * from './hub';
4
+ export * from './params';
5
+ export * from './registry';
6
+ export * from './schema';
7
+ export * from './types';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,OAAO,CAAC;AACtB,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export * from './catalog.js';
2
+ export * from './errors.js';
3
+ export * from './hub.js';
4
+ export * from './params.js';
5
+ export * from './registry.js';
6
+ export * from './schema.js';
7
+ export * from './types.js';
@@ -0,0 +1,31 @@
1
+ import type { Json } from '@gobing-ai/ts-ai-runner';
2
+ import type { DecisionDefinition, DecisionParam, JevQuestion, ResolvedParams } from './types';
3
+ /**
4
+ * Parameter resolution, `${params.*}` rendering and state building
5
+ * (task 0089 R7; design § Parameters, § Variable replacement).
6
+ */
7
+ /** The reserved per-decision parameter carrying the caller's text; authors cannot declare it. */
8
+ export declare const INSTRUCTIONS_PARAM = "instructions";
9
+ /** Authored-instructions default: the caller's text passed through unchanged. */
10
+ export declare const DEFAULT_INSTRUCTIONS_TEMPLATE = "${params.instructions}";
11
+ /** The implicit reserved `instructions` parameter: type string, default "". */
12
+ export declare function implicitInstructionsParam(): DecisionParam;
13
+ /**
14
+ * Resolve one decision's parameters: declared defaults overlaid by input, plus the
15
+ * implicit `instructions` (default ""). Unknown keys, missing required parameters
16
+ * (those without a default) and type mismatches throw {@link DecisionInputError}.
17
+ * An explicitly supplied `null` is a provided value (type-checked like any other).
18
+ */
19
+ export declare function resolveDecisionInput(decision: DecisionDefinition, input?: Record<string, Json>): ResolvedParams;
20
+ /**
21
+ * Render the decision's Jev question: `${params.<name>}` substitution applied to
22
+ * `instructions` and to criteria-description string leaves. Labels, `fallback`,
23
+ * `maker` and `model` are never templated, so the answer vocabulary stays static.
24
+ */
25
+ export declare function renderQuestion(decision: DecisionDefinition, params: ResolvedParams): JevQuestion;
26
+ /**
27
+ * Build the Jev `state` from the resolved declared parameters, excluding
28
+ * `instructions` and null values; null when nothing remains.
29
+ */
30
+ export declare function buildState(decision: DecisionDefinition, params: ResolvedParams): Record<string, Json> | null;
31
+ //# sourceMappingURL=params.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"params.d.ts","sourceRoot":"","sources":["../src/params.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAE,kBAAkB,EAAE,aAAa,EAAY,WAAW,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAExG;;;GAGG;AAEH,iGAAiG;AACjG,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AAEjD,iFAAiF;AACjF,eAAO,MAAM,6BAA6B,2BAA4B,CAAC;AAKvE,+EAA+E;AAC/E,wBAAgB,yBAAyB,IAAI,aAAa,CAEzD;AA8BD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,kBAAkB,EAAE,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAM,GAAG,cAAc,CA2BnH;AAiCD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,kBAAkB,EAAE,MAAM,EAAE,cAAc,GAAG,WAAW,CAgChG;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,kBAAkB,EAAE,MAAM,EAAE,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,IAAI,CAS5G"}
package/dist/params.js ADDED
@@ -0,0 +1,149 @@
1
+ import { DecisionInputError } from './errors.js';
2
+ /**
3
+ * Parameter resolution, `${params.*}` rendering and state building
4
+ * (task 0089 R7; design § Parameters, § Variable replacement).
5
+ */
6
+ /** The reserved per-decision parameter carrying the caller's text; authors cannot declare it. */
7
+ export const INSTRUCTIONS_PARAM = 'instructions';
8
+ /** Authored-instructions default: the caller's text passed through unchanged. */
9
+ export const DEFAULT_INSTRUCTIONS_TEMPLATE = `\${params.instructions}`;
10
+ /** Same regex as `ts-dual-workflow-engine`'s variable resolver (design § Variable replacement). */
11
+ const TEMPLATE_REF = /\$\{([^}]+)\}/g;
12
+ /** The implicit reserved `instructions` parameter: type string, default "". */
13
+ export function implicitInstructionsParam() {
14
+ return { name: INSTRUCTIONS_PARAM, type: 'string', required: false, default: '' };
15
+ }
16
+ function expectTypeMatch(decision, param, value) {
17
+ const expects = param.values !== undefined ? `one of [${param.values.join(', ')}]` : param.type;
18
+ let ok;
19
+ switch (param.type) {
20
+ case 'string':
21
+ ok = typeof value === 'string';
22
+ break;
23
+ case 'number':
24
+ ok = typeof value === 'number' && Number.isFinite(value);
25
+ break;
26
+ case 'boolean':
27
+ ok = typeof value === 'boolean';
28
+ break;
29
+ case 'enum':
30
+ ok = typeof value === 'string' && param.values?.includes(value) === true;
31
+ break;
32
+ case 'json':
33
+ return; // any JSON value is accepted
34
+ }
35
+ if (!ok) {
36
+ throw new DecisionInputError(`Parameter "${param.name}" of decision "${decision.id}" expects ${expects}, got ${JSON.stringify(value)}`, decision.id, param.name);
37
+ }
38
+ }
39
+ /**
40
+ * Resolve one decision's parameters: declared defaults overlaid by input, plus the
41
+ * implicit `instructions` (default ""). Unknown keys, missing required parameters
42
+ * (those without a default) and type mismatches throw {@link DecisionInputError}.
43
+ * An explicitly supplied `null` is a provided value (type-checked like any other).
44
+ */
45
+ export function resolveDecisionInput(decision, input = {}) {
46
+ const params = {};
47
+ for (const key of Object.keys(input)) {
48
+ // Own-property check: input keys like "constructor" must not resolve via the prototype chain.
49
+ if (!Object.hasOwn(decision.parameters, key)) {
50
+ throw new DecisionInputError(`Unknown parameter "${key}" for decision "${decision.id}"; declared parameters: ` +
51
+ `${Object.keys(decision.parameters).join(', ')}`, decision.id, key);
52
+ }
53
+ }
54
+ for (const param of Object.values(decision.parameters)) {
55
+ const supplied = param.name in input ? input[param.name] : undefined;
56
+ if (supplied === undefined && param.required) {
57
+ throw new DecisionInputError(`Missing required parameter "${param.name}" for decision "${decision.id}"`, decision.id, param.name);
58
+ }
59
+ const value = supplied !== undefined ? supplied : (param.default ?? null);
60
+ expectTypeMatch(decision, param, value);
61
+ params[param.name] = value;
62
+ }
63
+ return params;
64
+ }
65
+ function substitute(decisionId, ref, params) {
66
+ if (!ref.startsWith('params.')) {
67
+ throw new DecisionInputError(`Template ref "\${${ref}}" must use the "params" namespace`, decisionId, ref);
68
+ }
69
+ const name = ref.slice('params.'.length);
70
+ const value = params[name];
71
+ // Substitution forms (design § Variable replacement): strings verbatim, numbers and
72
+ // booleans via String, json via JSON.stringify, null as "".
73
+ if (value === undefined || value === null)
74
+ return '';
75
+ if (typeof value === 'string')
76
+ return value;
77
+ if (typeof value === 'number' || typeof value === 'boolean')
78
+ return String(value);
79
+ return JSON.stringify(value);
80
+ }
81
+ function renderEntryValue(decisionId, value, params) {
82
+ if (typeof value === 'string') {
83
+ return value.replace(TEMPLATE_REF, (_match, ref) => substitute(decisionId, ref, params));
84
+ }
85
+ if (Array.isArray(value))
86
+ return value.map((item) => renderEntryValue(decisionId, item, params));
87
+ if (value !== null && typeof value === 'object') {
88
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, renderEntryValue(decisionId, entry, params)]));
89
+ }
90
+ return value;
91
+ }
92
+ function renderEntry(decisionId, entry, params) {
93
+ return renderEntryValue(decisionId, entry, params);
94
+ }
95
+ /**
96
+ * Render the decision's Jev question: `${params.<name>}` substitution applied to
97
+ * `instructions` and to criteria-description string leaves. Labels, `fallback`,
98
+ * `maker` and `model` are never templated, so the answer vocabulary stays static.
99
+ */
100
+ export function renderQuestion(decision, params) {
101
+ const instructions = renderEntry(decision.id, decision.instructions, params);
102
+ const criteria = decision.criteria;
103
+ switch (criteria.kind) {
104
+ case 'choice': {
105
+ const rendered = {};
106
+ for (const [label, entry] of Object.entries(criteria.labels)) {
107
+ rendered[label] = renderEntry(decision.id, entry, params);
108
+ }
109
+ return { type: 'choice', instructions, criteria: rendered };
110
+ }
111
+ case 'score':
112
+ return {
113
+ type: 'score',
114
+ instructions,
115
+ criteria: criteria.levels.map((entry) => renderEntry(decision.id, entry, params)),
116
+ };
117
+ case 'noul': {
118
+ const outcomes = criteria.outcomes;
119
+ if (outcomes === undefined)
120
+ return { type: 'noul', instructions };
121
+ return {
122
+ type: 'noul',
123
+ instructions,
124
+ criteria: {
125
+ ...(outcomes.true !== undefined ? { true: renderEntry(decision.id, outcomes.true, params) } : {}),
126
+ ...(outcomes.false !== undefined
127
+ ? { false: renderEntry(decision.id, outcomes.false, params) }
128
+ : {}),
129
+ },
130
+ };
131
+ }
132
+ }
133
+ }
134
+ /**
135
+ * Build the Jev `state` from the resolved declared parameters, excluding
136
+ * `instructions` and null values; null when nothing remains.
137
+ */
138
+ export function buildState(decision, params) {
139
+ const state = {};
140
+ for (const param of Object.values(decision.parameters)) {
141
+ if (param.name === INSTRUCTIONS_PARAM)
142
+ continue;
143
+ const value = params[param.name];
144
+ if (value === null || value === undefined)
145
+ continue;
146
+ state[param.name] = value;
147
+ }
148
+ return Object.keys(state).length === 0 ? null : state;
149
+ }
@@ -0,0 +1,42 @@
1
+ import { type DecisionMaker, type DecisionMakerOptions } from '@gobing-ai/ts-ai-runner';
2
+ import { type FmDriverOptions } from '@gobing-ai/ts-decision-fm';
3
+ import { type LayaDriverOptions } from '@gobing-ai/ts-laya-mlx';
4
+ /** A maker, or a lazy factory for one (sync or async). Factories run on first `resolve` and are memoised on success. */
5
+ export type MakerSource = DecisionMaker | (() => DecisionMaker | Promise<DecisionMaker>);
6
+ /** Options for the three built-in makers pre-registered by {@link DecisionMakerRegistry} (task 0091 R2). */
7
+ export interface BuiltinMakerOptions {
8
+ /** Forwarded to `createDecisionMaker` for every built-in (env, apiKey, baseURL, timeoutMs, ...). */
9
+ readonly makerOptions?: Omit<DecisionMakerOptions, 'driver' | 'backend' | 'model'>;
10
+ /** Driver options for the bundled local drivers; the fm platform check runs at first resolve, not registration. */
11
+ readonly driverOptions?: {
12
+ readonly 'fm-local'?: FmDriverOptions;
13
+ readonly 'laya-local'?: LayaDriverOptions;
14
+ };
15
+ }
16
+ /**
17
+ * Registry mapping plain names to {@link DecisionMaker}s (task 0091, ADR-033
18
+ * § Maker registry). Built-ins are registered as lazy factories, so
19
+ * `register`, `has` and `names` never construct a maker or driver; the first
20
+ * `resolve` runs the factory and memoises the maker, while a failed factory
21
+ * stays un-memoised so a later resolve retries (e.g. a model not yet
22
+ * downloaded). No catalog dependency — catalog code depends on this, never
23
+ * the reverse.
24
+ */
25
+ export declare class DecisionMakerRegistry {
26
+ private readonly entries;
27
+ /** Pre-registers `typesafe`, `fm-local` and `laya-local` unless `builtins: false`. */
28
+ constructor(options?: {
29
+ builtins?: BuiltinMakerOptions | false;
30
+ });
31
+ /** Register a maker or factory under `name`; chains. Throws on a bad or already-registered name. */
32
+ register(name: string, source: MakerSource): this;
33
+ has(name: string): boolean;
34
+ names(): string[];
35
+ /**
36
+ * Resolve `name` to a maker, running and memoising a factory on first use. Factory errors
37
+ * propagate unchanged. Concurrent resolves of one name share a single in-flight factory run
38
+ * so every caller gets the same instance; a rejected run is cleared so a later resolve retries.
39
+ */
40
+ resolve(name: string): Promise<DecisionMaker>;
41
+ }
42
+ //# sourceMappingURL=registry.d.ts.map