@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/README.md +142 -0
- package/dist/catalog.d.ts +16 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +253 -0
- package/dist/errors.d.ts +47 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +67 -0
- package/dist/hub.d.ts +142 -0
- package/dist/hub.d.ts.map +1 -0
- package/dist/hub.js +262 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/params.d.ts +31 -0
- package/dist/params.d.ts.map +1 -0
- package/dist/params.js +149 -0
- package/dist/registry.d.ts +42 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +74 -0
- package/dist/schema.d.ts +137 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +71 -0
- package/dist/types.d.ts +84 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +0 -0
- package/examples/decisions.yaml +42 -0
- package/examples/support.yaml +54 -0
- package/package.json +66 -0
- package/schemas/decision-catalog.schema.json +226 -0
- package/src/catalog.ts +359 -0
- package/src/errors.ts +72 -0
- package/src/hub.ts +419 -0
- package/src/index.ts +7 -0
- package/src/params.ts +170 -0
- package/src/registry.ts +99 -0
- package/src/schema.ts +95 -0
- package/src/types.ts +80 -0
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# @gobing-ai/ts-ai-decision
|
|
2
|
+
|
|
3
|
+
Declarative YAML **decision catalogs** over the `DecisionMaker` surface of
|
|
4
|
+
`@gobing-ai/ts-ai-runner`, served through a fallback-guaranteed **`DecisionHub`** (ADR-033). A
|
|
5
|
+
catalog declares named, static decision points whose shape follows TypeSafe AI's Jev question
|
|
6
|
+
spec (`type` / `instructions` / `criteria`), with a required `fallback` so every decision always
|
|
7
|
+
has a concrete answer.
|
|
8
|
+
|
|
9
|
+
The package owns three layers: the strict catalog schema and loader (JSON Schema + zod,
|
|
10
|
+
`${params.*}` template validation/rendering, load-time consistency checks), the
|
|
11
|
+
`DecisionMakerRegistry` (makers picked by name), and the `DecisionHub` (one Jev question per
|
|
12
|
+
`decide`, declared fallback on every backend outcome).
|
|
13
|
+
|
|
14
|
+
## Catalog format
|
|
15
|
+
|
|
16
|
+
```yaml
|
|
17
|
+
# decisions/support.yaml
|
|
18
|
+
$schema: "@gobing-ai/ts-ai-decision/schemas/decision-catalog.schema.json"
|
|
19
|
+
version: 1
|
|
20
|
+
decisions:
|
|
21
|
+
category:
|
|
22
|
+
type: choice
|
|
23
|
+
instructions: "Classify this ${params.channel} ticket: ${params.instructions}"
|
|
24
|
+
parameters:
|
|
25
|
+
channel: { type: enum, values: [email, chat], default: email }
|
|
26
|
+
criteria:
|
|
27
|
+
bug_report: Something is broken or producing errors
|
|
28
|
+
billing: Charges, invoices, refunds, subscriptions
|
|
29
|
+
fallback: billing
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { buildState, loadDecisionCatalog, renderQuestion, resolveDecisionInput } from '@gobing-ai/ts-ai-decision';
|
|
34
|
+
|
|
35
|
+
const catalog = await loadDecisionCatalog('decisions/support.yaml');
|
|
36
|
+
const decision = catalog.decisions.category;
|
|
37
|
+
|
|
38
|
+
const params = resolveDecisionInput(decision, { instructions: ticketText, channel: 'chat' });
|
|
39
|
+
const question = renderQuestion(decision, params); // rendered Jev question
|
|
40
|
+
const state = buildState(decision, params); // declared params, minus instructions/nulls
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Loading is YAML-only (`.yaml` / `.yml`) and reuses ts-runtime's `parseStructuredConfig`, so the
|
|
44
|
+
`$schema` header is honoured when present and no YAML dependency is added here. Inconsistent
|
|
45
|
+
catalogs (fallback outside the answer vocabulary, `${...}` refs to undeclared params or other
|
|
46
|
+
namespaces, a declared reserved `instructions` parameter, mismatched defaults) throw
|
|
47
|
+
`DecisionCatalogError { source, decisionId?, field? }`; input-contract violations throw
|
|
48
|
+
`DecisionInputError { decisionId, param }`.
|
|
49
|
+
|
|
50
|
+
The reserved `instructions` parameter carries caller text to the model: every decision
|
|
51
|
+
implicitly has one (type `string`, default `""`), it cannot be declared under `parameters`, and
|
|
52
|
+
an authored `instructions` template embeds it with `${params.instructions}` beside other
|
|
53
|
+
parameters. Every string leaf of `instructions` and of the criteria descriptions is substituted
|
|
54
|
+
once per `decide`; criteria labels, `fallback`, `maker` and `model` are never templated, so the
|
|
55
|
+
answer vocabulary stays static. `${env.*}` and any other namespace are rejected at load.
|
|
56
|
+
|
|
57
|
+
## DecisionHub
|
|
58
|
+
|
|
59
|
+
`DecisionHub` loads catalogs and serves `decide(id, input)` with a guaranteed concrete answer —
|
|
60
|
+
see `examples/decisions.yaml` for a complete catalog:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { DecisionHub } from '@gobing-ai/ts-ai-decision';
|
|
64
|
+
|
|
65
|
+
const hub = new DecisionHub(); // registry with the built-ins, defaultMaker 'typesafe'
|
|
66
|
+
await hub.loadFile('decisions/support.yaml'); // or hub.load(loadedCatalog)
|
|
67
|
+
|
|
68
|
+
hub.list(); // [{ id, type, description, source }] — pure discovery
|
|
69
|
+
hub.describe('category'); // parameters (incl. instructions), criteria, fallback,
|
|
70
|
+
// effective minConfidence, maker and model
|
|
71
|
+
const r = await hub.decide('category', { instructions: ticketText, channel: 'chat' });
|
|
72
|
+
// { id, type, value, confidence, source, reason, maker, durationMs }
|
|
73
|
+
const r2 = await hub.decide('category', { instructions: ticketText }, { maker: 'fm-local' });
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`load` is all-or-nothing per catalog: a duplicate decision id across catalogs, or a `maker`
|
|
77
|
+
name (decision or `defaults.maker`) that is not registered, throws `DecisionCatalogError` and
|
|
78
|
+
registers nothing from the offending catalog. `list` and `describe` never construct a maker or
|
|
79
|
+
driver.
|
|
80
|
+
|
|
81
|
+
### The fallback contract
|
|
82
|
+
|
|
83
|
+
`decide` never rejects for backend outcomes — only caller mistakes throw (`UnknownDecisionError`
|
|
84
|
+
for an unknown id, `DecisionInputError` for input breaking the parameter contract,
|
|
85
|
+
`UnknownDecisionMakerError` for an unregistered per-call maker; the constructor throws for an
|
|
86
|
+
unregistered `defaultMaker`). Every throw happens before any backend call.
|
|
87
|
+
|
|
88
|
+
Exactly one Jev question keyed by the decision id goes through a single
|
|
89
|
+
`maker.ask({ state, questions, model })`; `model` resolves decision → catalog default → omitted.
|
|
90
|
+
Backend outcomes map to the declared fallback with `source: 'default'`:
|
|
91
|
+
|
|
92
|
+
| Outcome | `reason` |
|
|
93
|
+
|---------|----------|
|
|
94
|
+
| Maker construction failed (factory throw, missing config) | `no-backend` |
|
|
95
|
+
| `DecisionTimeoutError` | `timeout` |
|
|
96
|
+
| Any other backend failure or an invalid answer | `error` |
|
|
97
|
+
| Confidence below the effective floor (`decision → defaults → 0.7`) | `low-confidence` |
|
|
98
|
+
| A confident answer | `accepted` (`source: 'model'`) |
|
|
99
|
+
|
|
100
|
+
Values: choice returns a string label, score the (possibly fractional) expected score, and noul
|
|
101
|
+
`p >= 0.5` with `probability` and confidence `max(p, 1 - p)`. Every result names the selected
|
|
102
|
+
`maker`.
|
|
103
|
+
|
|
104
|
+
## Maker registry
|
|
105
|
+
|
|
106
|
+
`DecisionMakerRegistry` maps plain names to `DecisionMaker`s — the three built-ins (`typesafe`,
|
|
107
|
+
`fm-local`, `laya-local`) pre-registered lazily, plus anything consumers register (ADR-033):
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { createDecisionMaker } from '@gobing-ai/ts-ai-runner';
|
|
111
|
+
import { DecisionMakerRegistry } from '@gobing-ai/ts-ai-decision';
|
|
112
|
+
|
|
113
|
+
const registry = new DecisionMakerRegistry(); // pass `builtins: false` to start empty
|
|
114
|
+
registry.register('scripted-judge', () => createDecisionMaker({ driver: myDriver }));
|
|
115
|
+
|
|
116
|
+
registry.has('fm-local'); // true — nothing was constructed yet
|
|
117
|
+
const maker = await registry.resolve('fm-local'); // factory runs here, once, and is memoised
|
|
118
|
+
registry.names(); // ['typesafe', 'fm-local', 'laya-local', 'scripted-judge']
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Registering builds nothing; the first `resolve` runs a factory and memoises the maker, and a
|
|
122
|
+
factory that throws is not memoised, so a later `resolve` retries. Invalid or duplicate names
|
|
123
|
+
throw `DecisionRegistryError`; unknown names throw `UnknownDecisionMakerError { name }`; factory
|
|
124
|
+
errors propagate unchanged. Pass `builtins: { makerOptions, driverOptions }` to tune the built-ins
|
|
125
|
+
(env/keys/timeouts, or executor/client injection for the local drivers).
|
|
126
|
+
|
|
127
|
+
The hub picks a maker purely by name — no new driver code per routing:
|
|
128
|
+
per-call `options.maker` → the decision's `maker` → the catalog's `defaults.maker` → the hub's
|
|
129
|
+
`defaultMaker` (default `typesafe`). Catalog names are checked at `load`; a misspelled per-call
|
|
130
|
+
name fails loudly instead of falling back. `createDecisionHub` composes the whole thing in one
|
|
131
|
+
call and never returns a partially built hub:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { createDecisionHub } from '@gobing-ai/ts-ai-decision';
|
|
135
|
+
|
|
136
|
+
const hub = await createDecisionHub({
|
|
137
|
+
catalogs: ['decisions/support.yaml'],
|
|
138
|
+
makers: { 'scripted-judge': () => createDecisionMaker({ driver: myDriver }) },
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
See `docs/design/ai-decision-catalog.md` for the full field reference.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { FileSystem } from '@gobing-ai/ts-runtime';
|
|
2
|
+
import type { DecisionCatalog } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Options for {@link loadDecisionCatalog} and {@link parseDecisionCatalog}.
|
|
5
|
+
*/
|
|
6
|
+
export interface CatalogLoadOptions {
|
|
7
|
+
/** Honour the top-level `$schema` when present. Default true. */
|
|
8
|
+
validateSchema?: boolean;
|
|
9
|
+
/** ts-runtime FileSystem used to read the catalog; defaults to `createNodeFileSystem()`. */
|
|
10
|
+
fileSystem?: Pick<FileSystem, 'readFile'>;
|
|
11
|
+
}
|
|
12
|
+
/** Load and parse a YAML catalog file, returning typed decision definitions. */
|
|
13
|
+
export declare function loadDecisionCatalog(path: string, options?: CatalogLoadOptions): Promise<DecisionCatalog>;
|
|
14
|
+
/** Parse a catalog string (YAML) into typed decision definitions. The source must end in .yaml/.yml. */
|
|
15
|
+
export declare function parseDecisionCatalog(content: string, source: string, options?: CatalogLoadOptions): Promise<DecisionCatalog>;
|
|
16
|
+
//# sourceMappingURL=catalog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAKxD,OAAO,KAAK,EAER,eAAe,EAMlB,MAAM,SAAS,CAAC;AAYjB;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAC/B,iEAAiE;IACjE,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,4FAA4F;IAC5F,UAAU,CAAC,EAAE,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;CAC7C;AAaD,gFAAgF;AAChF,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,eAAe,CAAC,CAalH;AAED,wGAAwG;AACxG,wBAAsB,oBAAoB,CACtC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,kBAAuB,GACjC,OAAO,CAAC,eAAe,CAAC,CA+B1B"}
|
package/dist/catalog.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { createNodeFileSystem, parseStructuredConfig } from '@gobing-ai/ts-runtime';
|
|
2
|
+
import { DecisionCatalogError } from './errors.js';
|
|
3
|
+
import { DEFAULT_INSTRUCTIONS_TEMPLATE, INSTRUCTIONS_PARAM, implicitInstructionsParam } from './params.js';
|
|
4
|
+
import { DecisionCatalogSchema } from './schema.js';
|
|
5
|
+
/**
|
|
6
|
+
* Catalog loading (task 0089 R5/R6): a thin layer over ts-runtime's
|
|
7
|
+
* `parseStructuredConfig` (YAML parse + `$schema` validation), then the zod
|
|
8
|
+
* schema, then the cross-field checks. Validation is all-or-nothing per
|
|
9
|
+
* catalog — a catalog that fails any check produces no definitions.
|
|
10
|
+
*/
|
|
11
|
+
/** Same template regex as `ts-dual-workflow-engine`'s variable resolver. */
|
|
12
|
+
const TEMPLATE_REF = /\$\{([^}]+)\}/g;
|
|
13
|
+
const YAML_SOURCE_PATTERN = /\.(yaml|yml)$/;
|
|
14
|
+
function assertYamlSource(source) {
|
|
15
|
+
if (!YAML_SOURCE_PATTERN.test(source)) {
|
|
16
|
+
throw new DecisionCatalogError(`Decision catalogs are YAML only; source "${source}" does not end in .yaml or .yml`, source);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Load and parse a YAML catalog file, returning typed decision definitions. */
|
|
20
|
+
export async function loadDecisionCatalog(path, options = {}) {
|
|
21
|
+
assertYamlSource(path);
|
|
22
|
+
const fileSystem = options.fileSystem ?? createNodeFileSystem();
|
|
23
|
+
let content;
|
|
24
|
+
try {
|
|
25
|
+
content = await fileSystem.readFile(path);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
throw new DecisionCatalogError(`Cannot read decision catalog "${path}": ${error instanceof Error ? error.message : String(error)}`, path);
|
|
29
|
+
}
|
|
30
|
+
return parseDecisionCatalog(content, path, options);
|
|
31
|
+
}
|
|
32
|
+
/** Parse a catalog string (YAML) into typed decision definitions. The source must end in .yaml/.yml. */
|
|
33
|
+
export async function parseDecisionCatalog(content, source, options = {}) {
|
|
34
|
+
assertYamlSource(source);
|
|
35
|
+
let raw;
|
|
36
|
+
try {
|
|
37
|
+
raw = await parseStructuredConfig(content, source, {
|
|
38
|
+
validateSchema: options.validateSchema ?? true,
|
|
39
|
+
fileSystem: options.fileSystem,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
// YAML syntax (including duplicate keys) and $schema failures are load failures:
|
|
44
|
+
// wrap so callers always see DecisionCatalogError naming the source.
|
|
45
|
+
throw new DecisionCatalogError(`Decision catalog "${source}" failed to parse or validate: ` +
|
|
46
|
+
`${error instanceof Error ? error.message : String(error)}`, source);
|
|
47
|
+
}
|
|
48
|
+
const parsed = DecisionCatalogSchema.safeParse(raw);
|
|
49
|
+
if (!parsed.success) {
|
|
50
|
+
const issue = parsed.error.issues[0];
|
|
51
|
+
const fieldPath = issue?.path.map(String).join('.') ?? '';
|
|
52
|
+
const decisionId = fieldPath.startsWith('decisions.') ? fieldPath.split('.')[1] : undefined;
|
|
53
|
+
throw new DecisionCatalogError(`Invalid decision catalog "${source}"${fieldPath ? ` at ${fieldPath}` : ''}: ${issue?.message ?? 'schema mismatch'}`, source, decisionId, fieldPath || undefined);
|
|
54
|
+
}
|
|
55
|
+
return buildCatalog(parsed.data, source);
|
|
56
|
+
}
|
|
57
|
+
function buildCatalog(parsed, source) {
|
|
58
|
+
const decisions = {};
|
|
59
|
+
for (const [id, raw] of Object.entries(parsed.decisions)) {
|
|
60
|
+
decisions[id] = checkDecision(id, raw, source);
|
|
61
|
+
}
|
|
62
|
+
const defaults = parsed.defaults ?? {};
|
|
63
|
+
return { source, defaults, decisions };
|
|
64
|
+
}
|
|
65
|
+
function catalogError(source, decisionId, field, message) {
|
|
66
|
+
return new DecisionCatalogError(`Decision "${decisionId}" in "${source}": ${message}`, source, decisionId, field);
|
|
67
|
+
}
|
|
68
|
+
function normalizeParam(name, rawParam) {
|
|
69
|
+
if (typeof rawParam === 'string')
|
|
70
|
+
return { name, type: rawParam, required: true };
|
|
71
|
+
const hasDefault = 'default' in rawParam;
|
|
72
|
+
return {
|
|
73
|
+
name,
|
|
74
|
+
type: rawParam.type,
|
|
75
|
+
required: !hasDefault,
|
|
76
|
+
...(hasDefault ? { default: rawParam.default } : {}),
|
|
77
|
+
...(rawParam.values !== undefined ? { values: rawParam.values } : {}),
|
|
78
|
+
...(rawParam.description !== undefined ? { description: rawParam.description } : {}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function checkParam(id, param, source) {
|
|
82
|
+
if (param.type === 'enum') {
|
|
83
|
+
if (param.values === undefined || param.values.length < 1) {
|
|
84
|
+
throw catalogError(source, id, `parameters.${param.name}.values`, 'enum parameter requires at least one value');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else if (param.values !== undefined) {
|
|
88
|
+
throw catalogError(source, id, `parameters.${param.name}.values`, `only enum parameters may declare values; parameter type is "${param.type}"`);
|
|
89
|
+
}
|
|
90
|
+
if (!param.required && !defaultMatches(param)) {
|
|
91
|
+
throw catalogError(source, id, `parameters.${param.name}.default`, `default ${JSON.stringify(param.default ?? null)} does not match declared type "${param.type}"` +
|
|
92
|
+
(param.values ? ` with values [${param.values.join(', ')}]` : ''));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function defaultMatches(param) {
|
|
96
|
+
const value = param.default;
|
|
97
|
+
switch (param.type) {
|
|
98
|
+
case 'string':
|
|
99
|
+
return typeof value === 'string';
|
|
100
|
+
case 'number':
|
|
101
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
102
|
+
case 'boolean':
|
|
103
|
+
return typeof value === 'boolean';
|
|
104
|
+
case 'enum':
|
|
105
|
+
return typeof value === 'string' && param.values?.includes(value) === true;
|
|
106
|
+
case 'json':
|
|
107
|
+
return true; // any JSON value, including null
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function normalizeCriteria(id, raw, source) {
|
|
111
|
+
const criteria = raw.criteria;
|
|
112
|
+
if (raw.type === 'choice') {
|
|
113
|
+
if (criteria === undefined || Array.isArray(criteria) || typeof criteria !== 'object' || criteria === null) {
|
|
114
|
+
throw catalogError(source, id, 'criteria', 'choice decision requires a criteria label map');
|
|
115
|
+
}
|
|
116
|
+
const labels = criteria;
|
|
117
|
+
if (Object.keys(labels).length < 2) {
|
|
118
|
+
throw catalogError(source, id, 'criteria', 'choice criteria requires at least two labels');
|
|
119
|
+
}
|
|
120
|
+
return { kind: 'choice', labels };
|
|
121
|
+
}
|
|
122
|
+
if (raw.type === 'score') {
|
|
123
|
+
if (criteria === undefined) {
|
|
124
|
+
throw catalogError(source, id, 'criteria', 'score decision requires a criteria rubric list');
|
|
125
|
+
}
|
|
126
|
+
if (!Array.isArray(criteria)) {
|
|
127
|
+
throw catalogError(source, id, 'criteria', 'score criteria must be a list of level descriptions');
|
|
128
|
+
}
|
|
129
|
+
if (criteria.length < 2) {
|
|
130
|
+
throw catalogError(source, id, 'criteria', 'score criteria requires at least two levels');
|
|
131
|
+
}
|
|
132
|
+
return { kind: 'score', levels: criteria };
|
|
133
|
+
}
|
|
134
|
+
// noul: optional { true?, false? } (R4). The zod union may have matched the open
|
|
135
|
+
// record branch, so unknown outcome keys are rejected here.
|
|
136
|
+
if (criteria === undefined)
|
|
137
|
+
return { kind: 'noul' };
|
|
138
|
+
if (Array.isArray(criteria) || typeof criteria !== 'object' || criteria === null) {
|
|
139
|
+
throw catalogError(source, id, 'criteria', 'noul criteria must be an object with "true"/"false" keys');
|
|
140
|
+
}
|
|
141
|
+
for (const key of Object.keys(criteria)) {
|
|
142
|
+
if (key !== 'true' && key !== 'false') {
|
|
143
|
+
throw catalogError(source, id, 'criteria', `noul criteria accepts only the keys "true" and "false"; got "${key}"`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const outcomes = criteria;
|
|
147
|
+
return {
|
|
148
|
+
kind: 'noul',
|
|
149
|
+
...(outcomes.true !== undefined || outcomes.false !== undefined
|
|
150
|
+
? {
|
|
151
|
+
outcomes: {
|
|
152
|
+
...(outcomes.true !== undefined ? { true: outcomes.true } : {}),
|
|
153
|
+
...(outcomes.false !== undefined ? { false: outcomes.false } : {}),
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
: {}),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function checkFallback(id, type, criteria, fallback, source) {
|
|
160
|
+
if (type === 'choice') {
|
|
161
|
+
// Own-property check: prototype keys like "constructor" must not pass as labels.
|
|
162
|
+
if (criteria.kind !== 'choice' || typeof fallback !== 'string' || !Object.hasOwn(criteria.labels, fallback)) {
|
|
163
|
+
const labels = criteria.kind === 'choice' ? Object.keys(criteria.labels).join(', ') : '';
|
|
164
|
+
throw catalogError(source, id, 'fallback', `fallback ${JSON.stringify(fallback)} is not a declared criteria label (${labels})`);
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (type === 'score') {
|
|
169
|
+
const levelCount = criteria.kind === 'score' ? criteria.levels.length : 0;
|
|
170
|
+
if (criteria.kind !== 'score' ||
|
|
171
|
+
typeof fallback !== 'number' ||
|
|
172
|
+
!Number.isInteger(fallback) ||
|
|
173
|
+
fallback < 0 ||
|
|
174
|
+
fallback >= levelCount) {
|
|
175
|
+
throw catalogError(source, id, 'fallback', `fallback ${JSON.stringify(fallback)} is not an integer level in [0, ${levelCount - 1}]`);
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (typeof fallback !== 'boolean') {
|
|
180
|
+
throw catalogError(source, id, 'fallback', `noul fallback must be a boolean; got ${JSON.stringify(fallback)}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/** Collect every `${...}` reference inside string leaves of a JSON value (recursing objects/arrays). */
|
|
184
|
+
function collectTemplateRefs(value, into) {
|
|
185
|
+
if (typeof value === 'string') {
|
|
186
|
+
for (const match of value.matchAll(TEMPLATE_REF))
|
|
187
|
+
into.add(match[1] ?? '');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (Array.isArray(value)) {
|
|
191
|
+
for (const item of value)
|
|
192
|
+
collectTemplateRefs(item, into);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (value !== null && typeof value === 'object') {
|
|
196
|
+
for (const entry of Object.values(value))
|
|
197
|
+
collectTemplateRefs(entry, into);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function checkTemplateRefs(id, field, entry, declared, source) {
|
|
201
|
+
const refs = new Set();
|
|
202
|
+
collectTemplateRefs(entry, refs);
|
|
203
|
+
for (const ref of refs) {
|
|
204
|
+
if (!ref.startsWith('params.')) {
|
|
205
|
+
throw catalogError(source, id, field, `template ref "\${${ref}}" uses namespace "${ref.split('.')[0] ?? ''}"; only "params" is allowed`);
|
|
206
|
+
}
|
|
207
|
+
const name = ref.slice('params.'.length);
|
|
208
|
+
if (!declared.has(name)) {
|
|
209
|
+
throw catalogError(source, id, field, `template ref "\${${ref}}" names an undeclared parameter`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function checkDecision(id, raw, source) {
|
|
214
|
+
const parameters = {};
|
|
215
|
+
const rawParams = raw.parameters ?? {};
|
|
216
|
+
if (INSTRUCTIONS_PARAM in rawParams) {
|
|
217
|
+
throw catalogError(source, id, 'parameters.instructions', 'the "instructions" parameter is reserved and implicit (string, default ""); it cannot be declared');
|
|
218
|
+
}
|
|
219
|
+
for (const [name, rawParam] of Object.entries(rawParams)) {
|
|
220
|
+
const param = normalizeParam(name, rawParam);
|
|
221
|
+
checkParam(id, param, source);
|
|
222
|
+
parameters[name] = param;
|
|
223
|
+
}
|
|
224
|
+
parameters[INSTRUCTIONS_PARAM] = implicitInstructionsParam();
|
|
225
|
+
const criteria = normalizeCriteria(id, raw, source);
|
|
226
|
+
checkFallback(id, raw.type, criteria, raw.fallback, source);
|
|
227
|
+
const instructions = raw.instructions ?? DEFAULT_INSTRUCTIONS_TEMPLATE;
|
|
228
|
+
const declared = new Set(Object.keys(parameters));
|
|
229
|
+
checkTemplateRefs(id, 'instructions', instructions, declared, source);
|
|
230
|
+
const criteriaEntries = criteria.kind === 'choice'
|
|
231
|
+
? Object.values(criteria.labels)
|
|
232
|
+
: criteria.kind === 'score'
|
|
233
|
+
? [...criteria.levels]
|
|
234
|
+
: criteria.outcomes
|
|
235
|
+
? [criteria.outcomes.true, criteria.outcomes.false].filter((e) => e !== undefined)
|
|
236
|
+
: [];
|
|
237
|
+
for (const entry of criteriaEntries) {
|
|
238
|
+
checkTemplateRefs(id, 'criteria', entry, declared, source);
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
id,
|
|
242
|
+
source,
|
|
243
|
+
type: raw.type,
|
|
244
|
+
...(raw.description !== undefined ? { description: raw.description } : {}),
|
|
245
|
+
instructions,
|
|
246
|
+
parameters,
|
|
247
|
+
criteria,
|
|
248
|
+
fallback: raw.fallback,
|
|
249
|
+
...(raw.minConfidence !== undefined ? { minConfidence: raw.minConfidence } : {}),
|
|
250
|
+
...(raw.maker !== undefined ? { maker: raw.maker } : {}),
|
|
251
|
+
...(raw.model !== undefined ? { model: raw.model } : {}),
|
|
252
|
+
};
|
|
253
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DecisionCatalogError — task 0089 — load-time failure for a decision catalog.
|
|
3
|
+
* Names the source and, when known, the decision id and field, so an
|
|
4
|
+
* inconsistent catalog points straight at the offending key.
|
|
5
|
+
*/
|
|
6
|
+
export declare class DecisionCatalogError extends Error {
|
|
7
|
+
readonly source: string;
|
|
8
|
+
readonly decisionId?: string | undefined;
|
|
9
|
+
readonly field?: string | undefined;
|
|
10
|
+
constructor(message: string, source: string, decisionId?: string | undefined, field?: string | undefined);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* DecisionRegistryError — task 0091 — invalid DecisionMakerRegistry use: a name
|
|
14
|
+
* that fails `^[a-z][a-z0-9-]*$` or a duplicate registration.
|
|
15
|
+
*/
|
|
16
|
+
export declare class DecisionRegistryError extends Error {
|
|
17
|
+
constructor(message: string);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* UnknownDecisionMakerError — task 0091 — resolve of a name that is not
|
|
21
|
+
* registered. The `name` property carries the unknown maker name; the class is
|
|
22
|
+
* identified via `instanceof`, because assigning `this.name` would clobber it.
|
|
23
|
+
*/
|
|
24
|
+
export declare class UnknownDecisionMakerError extends Error {
|
|
25
|
+
readonly name: string;
|
|
26
|
+
constructor(message: string, name: string);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* UnknownDecisionError — task 0090 — decide/describe of a decision id that no
|
|
30
|
+
* loaded catalog declares. A caller mistake: it throws instead of falling
|
|
31
|
+
* back, before any backend call. The `decisionId` property carries the id; the
|
|
32
|
+
* class is identified via `instanceof`, like UnknownDecisionMakerError.
|
|
33
|
+
*/
|
|
34
|
+
export declare class UnknownDecisionError extends Error {
|
|
35
|
+
readonly decisionId: string;
|
|
36
|
+
constructor(message: string, decisionId: string);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* DecisionInputError — task 0089 R7 — violation of one decision's parameter
|
|
40
|
+
* contract: unknown key, missing required parameter, or type mismatch.
|
|
41
|
+
*/
|
|
42
|
+
export declare class DecisionInputError extends Error {
|
|
43
|
+
readonly decisionId: string;
|
|
44
|
+
readonly param: string;
|
|
45
|
+
constructor(message: string, decisionId: string, param: string);
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;IAGvC,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM;gBAHvB,OAAO,EAAE,MAAM,EACN,MAAM,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,MAAM,YAAA,EACnB,KAAK,CAAC,EAAE,MAAM,YAAA;CAK9B;AAED;;;GAGG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI9B;AAED;;;;GAIG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;aAG1B,IAAI,EAAE,MAAM;gBAD9B,OAAO,EAAE,MAAM,EACG,IAAI,EAAE,MAAM;CAIrC;AAED;;;;;GAKG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;IAGvC,QAAQ,CAAC,UAAU,EAAE,MAAM;gBAD3B,OAAO,EAAE,MAAM,EACN,UAAU,EAAE,MAAM;CAKlC;AAED;;;GAGG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAGrC,QAAQ,CAAC,UAAU,EAAE,MAAM;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM;gBAFtB,OAAO,EAAE,MAAM,EACN,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM;CAK7B"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DecisionCatalogError — task 0089 — load-time failure for a decision catalog.
|
|
3
|
+
* Names the source and, when known, the decision id and field, so an
|
|
4
|
+
* inconsistent catalog points straight at the offending key.
|
|
5
|
+
*/
|
|
6
|
+
export class DecisionCatalogError extends Error {
|
|
7
|
+
source;
|
|
8
|
+
decisionId;
|
|
9
|
+
field;
|
|
10
|
+
constructor(message, source, decisionId, field) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.source = source;
|
|
13
|
+
this.decisionId = decisionId;
|
|
14
|
+
this.field = field;
|
|
15
|
+
this.name = 'DecisionCatalogError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* DecisionRegistryError — task 0091 — invalid DecisionMakerRegistry use: a name
|
|
20
|
+
* that fails `^[a-z][a-z0-9-]*$` or a duplicate registration.
|
|
21
|
+
*/
|
|
22
|
+
export class DecisionRegistryError extends Error {
|
|
23
|
+
constructor(message) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = 'DecisionRegistryError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* UnknownDecisionMakerError — task 0091 — resolve of a name that is not
|
|
30
|
+
* registered. The `name` property carries the unknown maker name; the class is
|
|
31
|
+
* identified via `instanceof`, because assigning `this.name` would clobber it.
|
|
32
|
+
*/
|
|
33
|
+
export class UnknownDecisionMakerError extends Error {
|
|
34
|
+
name;
|
|
35
|
+
constructor(message, name) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = name;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* UnknownDecisionError — task 0090 — decide/describe of a decision id that no
|
|
42
|
+
* loaded catalog declares. A caller mistake: it throws instead of falling
|
|
43
|
+
* back, before any backend call. The `decisionId` property carries the id; the
|
|
44
|
+
* class is identified via `instanceof`, like UnknownDecisionMakerError.
|
|
45
|
+
*/
|
|
46
|
+
export class UnknownDecisionError extends Error {
|
|
47
|
+
decisionId;
|
|
48
|
+
constructor(message, decisionId) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.decisionId = decisionId;
|
|
51
|
+
this.name = 'UnknownDecisionError';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* DecisionInputError — task 0089 R7 — violation of one decision's parameter
|
|
56
|
+
* contract: unknown key, missing required parameter, or type mismatch.
|
|
57
|
+
*/
|
|
58
|
+
export class DecisionInputError extends Error {
|
|
59
|
+
decisionId;
|
|
60
|
+
param;
|
|
61
|
+
constructor(message, decisionId, param) {
|
|
62
|
+
super(message);
|
|
63
|
+
this.decisionId = decisionId;
|
|
64
|
+
this.param = param;
|
|
65
|
+
this.name = 'DecisionInputError';
|
|
66
|
+
}
|
|
67
|
+
}
|