toolnexus 0.17.0 → 0.18.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/dist/agents/loop.js +10 -0
- package/dist/classifier.d.ts +334 -0
- package/dist/classifier.js +630 -0
- package/dist/client.d.ts +26 -3
- package/dist/client.js +4 -25
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/retry.d.ts +30 -0
- package/dist/retry.js +55 -0
- package/package.json +5 -3
- package/src/agents/loop.ts +12 -0
- package/src/classifier.ts +832 -0
- package/src/client.ts +31 -28
- package/src/index.ts +1 -0
- package/src/retry.ts +57 -0
package/dist/agents/loop.js
CHANGED
|
@@ -13,6 +13,16 @@ export function guardedHooks(guardrails, hooks) {
|
|
|
13
13
|
beforeTool: async (ev) => {
|
|
14
14
|
for (const g of guardrails) {
|
|
15
15
|
const verdict = g(ev);
|
|
16
|
+
// A guardrail is SYNCHRONOUS by contract. An async one returns a Promise,
|
|
17
|
+
// which is truthy and !== "allow", so it would silently DENY every call
|
|
18
|
+
// with `denied: [object Promise]`. Fail loudly instead, and say where
|
|
19
|
+
// asynchronous work belongs.
|
|
20
|
+
if (verdict != null && typeof verdict !== "string") {
|
|
21
|
+
const what = typeof verdict.then === "function" ? "a Promise" : typeof verdict;
|
|
22
|
+
throw new TypeError(`guardrail returned ${what}: a guardrail must return a string synchronously ` +
|
|
23
|
+
`("" or "allow" to permit, any other string to deny). ` +
|
|
24
|
+
`Do asynchronous work in hooks.beforeTool, which is awaited.`);
|
|
25
|
+
}
|
|
16
26
|
if (verdict && verdict !== "allow") {
|
|
17
27
|
return { result: { output: `denied: ${verdict}`, isError: true } };
|
|
18
28
|
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classifier (SPEC.md §8B) — the contract for a JUDGMENT, as `Tool` is the contract for an
|
|
3
|
+
* ACTION. A System One model takes a state plus pre-declared, typed questions and returns
|
|
4
|
+
* calibrated answers with no free text. It has no messages, no tool calling and no streaming,
|
|
5
|
+
* so it never enters the §8 client loop.
|
|
6
|
+
*
|
|
7
|
+
* A classifier INTERPRETS; it never AUTHORISES. Schema validity is not correctness: a decision
|
|
8
|
+
* can be confidently wrong, and "cannot hallucinate" means only that the returned value is in
|
|
9
|
+
* the declared schema. Numeric limits, permission checks and allowlists stay in code. Nothing
|
|
10
|
+
* here is a security control.
|
|
11
|
+
*/
|
|
12
|
+
import type { Client } from "./client.js";
|
|
13
|
+
import { type ErrorInfo, type ErrorTier, type MetricEvent } from "./client.js";
|
|
14
|
+
/** The System One endpoint base. */
|
|
15
|
+
export declare const DEFAULT_CLASSIFIER_BASE_URL = "https://api.typesafe.ai/v1";
|
|
16
|
+
/** The floating model alias. Pin it (e.g. `jev-1.13.0`) once thresholds are tuned. */
|
|
17
|
+
export declare const DEFAULT_CLASSIFIER_MODEL = "jev-latest";
|
|
18
|
+
/** The NAME of the env var holding the credential — never the value. */
|
|
19
|
+
export declare const DEFAULT_CLASSIFIER_API_KEY_ENV = "TYPESAFE_API_KEY";
|
|
20
|
+
/** Bounds ONE request; a classifier has no loop to bound. */
|
|
21
|
+
export declare const DEFAULT_CLASSIFIER_TIMEOUT_MS = 10000;
|
|
22
|
+
/** Client-side cap on a choice's named options (§8B). */
|
|
23
|
+
export declare const MAX_CHOICE_OPTIONS = 255;
|
|
24
|
+
/** Bounds of a score rubric (§8B). */
|
|
25
|
+
export declare const MIN_SCORE_LEVELS = 2;
|
|
26
|
+
export declare const MAX_SCORE_LEVELS = 10;
|
|
27
|
+
/**
|
|
28
|
+
* The ABSOLUTE tolerance on `max|p - 1/n|`, compared INCLUSIVELY (§8B). Pinned across every
|
|
29
|
+
* port; `examples/judge/near-uniform.json` pins both sides at 0.0499 / 0.0501.
|
|
30
|
+
*/
|
|
31
|
+
export declare const NEAR_UNIFORM_TOLERANCE = 0.05;
|
|
32
|
+
/**
|
|
33
|
+
* Labels for the true and false cases of a noul question. Absent and empty are DIFFERENT values
|
|
34
|
+
* and both are preserved on the wire: omit `criteria` entirely and the field is absent; pass
|
|
35
|
+
* `{ true: "", false: "" }` and it is emitted with empty values.
|
|
36
|
+
*/
|
|
37
|
+
export interface NoulCriteria {
|
|
38
|
+
true: string;
|
|
39
|
+
false: string;
|
|
40
|
+
}
|
|
41
|
+
/** The probability that a statement holds, one number in 0..1. It carries NO confidence. */
|
|
42
|
+
export interface NoulQuestion {
|
|
43
|
+
type: "noul";
|
|
44
|
+
instructions: string;
|
|
45
|
+
/** Optional. Omitted ⇒ the field is ABSENT from the request, not null and not {}. */
|
|
46
|
+
criteria?: NoulCriteria;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* One option from a named set, 1..255 options.
|
|
50
|
+
*
|
|
51
|
+
* THE ENCODING OBLIGATION IS THE CALLER'S (§8B, docs/adr/0021 D1): `criteria[id]` is the only
|
|
52
|
+
* thing that differentiates one option from another to the model. Passing the id itself, an
|
|
53
|
+
* empty string, or one value repeated is schema-valid, returns HTTP 200 and a well-formed
|
|
54
|
+
* distribution — and ranks at chance (measured: 17 apples described by consequence, 0/1/0
|
|
55
|
+
* described by id).
|
|
56
|
+
*/
|
|
57
|
+
export interface ChoiceQuestion {
|
|
58
|
+
type: "choice";
|
|
59
|
+
instructions: string;
|
|
60
|
+
/** option id -> what picking it would MEAN. */
|
|
61
|
+
criteria: Record<string, string>;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A rating against an ORDERED rubric of 2..10 levels. The array order IS the level numbering,
|
|
65
|
+
* so it is never sorted — a "sort everything" canonicaliser silently renumbers the rubric.
|
|
66
|
+
*/
|
|
67
|
+
export interface ScoreQuestion {
|
|
68
|
+
type: "score";
|
|
69
|
+
instructions: string;
|
|
70
|
+
criteria: string[];
|
|
71
|
+
}
|
|
72
|
+
/** The closed set of question types, discriminated by `type`. */
|
|
73
|
+
export type Question = NoulQuestion | ChoiceQuestion | ScoreQuestion;
|
|
74
|
+
/** A noul question. Pass `criteria` only when you mean it — absent ≠ empty on the wire. */
|
|
75
|
+
export declare function noul(instructions: string, criteria?: NoulCriteria): NoulQuestion;
|
|
76
|
+
/** A choice question. Every description must say what picking that option would MEAN. */
|
|
77
|
+
export declare function choice(instructions: string, criteria: Record<string, string>): ChoiceQuestion;
|
|
78
|
+
/** A score question. The array order IS the level numbering and is never reordered. */
|
|
79
|
+
export declare function score(instructions: string, criteria: string[]): ScoreQuestion;
|
|
80
|
+
/**
|
|
81
|
+
* Build a choice from any (name, description) pairs — a Tool, a skill, an agent, an A2A card
|
|
82
|
+
* skill. The description must say what picking that option would MEAN; see the encoding
|
|
83
|
+
* obligation on `ChoiceQuestion`.
|
|
84
|
+
*/
|
|
85
|
+
export declare function choiceOver(instructions: string, items: Record<string, string>): ChoiceQuestion;
|
|
86
|
+
/**
|
|
87
|
+
* Canonical JSON: object keys sorted recursively in CODE-UNIT (ASCII) order, arrays NEVER
|
|
88
|
+
* reordered, compact separators, and `<>&'"` plus non-ASCII transmitted raw.
|
|
89
|
+
*
|
|
90
|
+
* Hand-rolled because `JSON.stringify` has no recursive key sort, and its `replacer` ARRAY form
|
|
91
|
+
* applies one key list at EVERY depth — it cannot express "sort whatever keys are here". The
|
|
92
|
+
* per-value escaping and number formatting ARE delegated to `JSON.stringify`, which emits the
|
|
93
|
+
* shortest round-tripping decimal (so 1.21 is "1.21" and integer 0 is "0", not "0.0") and does
|
|
94
|
+
* not escape `<`, `>`, `&` or non-ASCII.
|
|
95
|
+
*
|
|
96
|
+
* `undefined` is ABSENT, never `null`: an object key whose value is undefined is dropped.
|
|
97
|
+
*/
|
|
98
|
+
export declare function canonicalJson(value: unknown): string;
|
|
99
|
+
/**
|
|
100
|
+
* The bytes the byte-identity claim covers: `model` + `questions`.
|
|
101
|
+
*
|
|
102
|
+
* `state` is deliberately NOT here. It is transmitted verbatim as the host supplied it and is
|
|
103
|
+
* OUTSIDE the claim, because numbers do not canonicalise across languages (`-0.0` renders four
|
|
104
|
+
* ways across our own seven runtimes). Do not re-widen this: a caller who needs their state
|
|
105
|
+
* pinned canonicalises it themselves before handing it over.
|
|
106
|
+
*/
|
|
107
|
+
export declare function canonicalRequest(model: string, questions: Record<string, Question>): string;
|
|
108
|
+
/** The probability that a statement holds. NO confidence: the number is the answer. */
|
|
109
|
+
export interface NoulAnswer {
|
|
110
|
+
type: "noul";
|
|
111
|
+
noul: number;
|
|
112
|
+
}
|
|
113
|
+
/** One option from the offered set, with a probability for EVERY offered option. */
|
|
114
|
+
export interface ChoiceAnswer {
|
|
115
|
+
type: "choice";
|
|
116
|
+
choice: string;
|
|
117
|
+
probabilities: Record<string, number>;
|
|
118
|
+
confidence: number;
|
|
119
|
+
/**
|
|
120
|
+
* DERIVED from `probabilities` on decode and never read from the wire (§8B):
|
|
121
|
+
* `max|p - 1/n| <= 0.05`, inclusive, `n` = the number of entries in the map.
|
|
122
|
+
*
|
|
123
|
+
* ADVISORY, NOT a correctness signal. It detects an encoding that gave the model nothing to
|
|
124
|
+
* rank on — it cannot distinguish a good encoding from a subtly wrong one. `calibrated`
|
|
125
|
+
* carries the same caveat.
|
|
126
|
+
*/
|
|
127
|
+
nearUniform: boolean;
|
|
128
|
+
}
|
|
129
|
+
/** A rating against the ordered rubric. `score` MAY fall between levels (1.21 is a real answer). */
|
|
130
|
+
export interface ScoreAnswer {
|
|
131
|
+
type: "score";
|
|
132
|
+
score: number;
|
|
133
|
+
legend: Record<string, string>;
|
|
134
|
+
probabilities: Record<string, number>;
|
|
135
|
+
confidence: number;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* One typed answer, discriminated by `type`. Named `DecisionAnswer` because §10 already owns
|
|
139
|
+
* `Answer` (the suspension resolution) — same idea, different seam.
|
|
140
|
+
*/
|
|
141
|
+
export type DecisionAnswer = NoulAnswer | ChoiceAnswer | ScoreAnswer;
|
|
142
|
+
/** The wire's usage block. `cost` is absent on some backends. */
|
|
143
|
+
export interface ClassifierUsage {
|
|
144
|
+
inputTokens: number;
|
|
145
|
+
outputTokens: number;
|
|
146
|
+
cost?: number;
|
|
147
|
+
}
|
|
148
|
+
/** The legend in LEVEL order, which the map itself loses ("2" sorts before "10" as a level). */
|
|
149
|
+
export declare function levels(answer: ScoreAnswer): string[];
|
|
150
|
+
/**
|
|
151
|
+
* One answer per question, keyed by the CALLER's keys. The keys are addressing, not content:
|
|
152
|
+
* they are never transmitted, so a key may be a tool, skill or agent name verbatim.
|
|
153
|
+
*/
|
|
154
|
+
export declare class Decision {
|
|
155
|
+
/** What actually answered, which may be more specific than the model that was asked for. */
|
|
156
|
+
readonly model: string;
|
|
157
|
+
readonly answers: Record<string, DecisionAnswer>;
|
|
158
|
+
readonly usage: ClassifierUsage;
|
|
159
|
+
/**
|
|
160
|
+
* Whether the probabilities are calibrated. `systemone` reports true; `llm` reports false
|
|
161
|
+
* unless it derived them from provider token probabilities. A THRESHOLD TUNED AGAINST ONE
|
|
162
|
+
* BACKEND DOES NOT TRANSFER TO ANOTHER.
|
|
163
|
+
*/
|
|
164
|
+
readonly calibrated: boolean;
|
|
165
|
+
constructor(
|
|
166
|
+
/** What actually answered, which may be more specific than the model that was asked for. */
|
|
167
|
+
model: string, answers: Record<string, DecisionAnswer>, usage: ClassifierUsage,
|
|
168
|
+
/**
|
|
169
|
+
* Whether the probabilities are calibrated. `systemone` reports true; `llm` reports false
|
|
170
|
+
* unless it derived them from provider token probabilities. A THRESHOLD TUNED AGAINST ONE
|
|
171
|
+
* BACKEND DOES NOT TRANSFER TO ANOTHER.
|
|
172
|
+
*/
|
|
173
|
+
calibrated: boolean);
|
|
174
|
+
/** Typed read. Throws (never returns undefined) on a wrong-type or absent key. */
|
|
175
|
+
noul(key: string): NoulAnswer;
|
|
176
|
+
choice(key: string): ChoiceAnswer;
|
|
177
|
+
score(key: string): ScoreAnswer;
|
|
178
|
+
private typed;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Whether a choice answer's probability map is indistinguishable from flat (§8B):
|
|
182
|
+
*
|
|
183
|
+
* nearUniform ⇔ max over i of |p_i - 1/n| <= 0.05
|
|
184
|
+
*
|
|
185
|
+
* `n` is the number of ENTRIES IN THE MAP and the values are taken AS RETURNED — not
|
|
186
|
+
* renormalised, not sorted, not rounded; an offered option absent from the map counts as 0 by
|
|
187
|
+
* not being an entry. The tolerance is ABSOLUTE (a relative band collapses below the wire's
|
|
188
|
+
* two-decimal rounding on a 255-option roster) and the comparison is INCLUSIVE. `n === 1` is
|
|
189
|
+
* trivially uniform. An empty map has no distribution at all and is reported false.
|
|
190
|
+
*/
|
|
191
|
+
export declare function nearUniform(probabilities: Record<string, number>): boolean;
|
|
192
|
+
/**
|
|
193
|
+
* Decode a backend response into a Decision. The discriminated decode is the single place
|
|
194
|
+
* `nearUniform` is derived. A malformed answer is an error, never a repaired one (ADR 0020).
|
|
195
|
+
*/
|
|
196
|
+
export declare function decodeDecision(raw: unknown, fallbackModel: string): Decision;
|
|
197
|
+
/** `"systemone" | "llm" | "custom" | "static"` — the same slot as the client's `style`. */
|
|
198
|
+
export type ClassifierStyle = "systemone" | "llm" | "custom" | "static";
|
|
199
|
+
/**
|
|
200
|
+
* One entry of the `static` corpus. It is keyed on the canonical request AND the state: several
|
|
201
|
+
* recorded entries legitimately share one questions payload and differ only in state (the three
|
|
202
|
+
* guard bands of `examples/judge/decisions.json` do exactly that), so a corpus keyed on the
|
|
203
|
+
* canonical request alone cannot tell them apart.
|
|
204
|
+
*/
|
|
205
|
+
export interface RecordedDecision {
|
|
206
|
+
state: unknown;
|
|
207
|
+
questions: Record<string, Question>;
|
|
208
|
+
/** The recorded backend response body, verbatim. */
|
|
209
|
+
response: unknown;
|
|
210
|
+
}
|
|
211
|
+
/** The host's own backend (`style: "custom"`). Every wire option is ignored. */
|
|
212
|
+
export type EvaluateFn = (state: unknown, questions: Record<string, Question>, signal?: AbortSignal) => Promise<Decision> | Decision;
|
|
213
|
+
/**
|
|
214
|
+
* Mirrors §8 `ClientOptions` field-for-field wherever a field makes sense, so a host that has
|
|
215
|
+
* configured one has configured the other.
|
|
216
|
+
*/
|
|
217
|
+
export interface ClassifierOptions {
|
|
218
|
+
/** Default `"systemone"`. */
|
|
219
|
+
style?: ClassifierStyle;
|
|
220
|
+
/** Default `https://api.typesafe.ai/v1`. OpenRouter serves this wire today. */
|
|
221
|
+
baseUrl?: string;
|
|
222
|
+
/** Default `"jev-latest"`. Pin it once thresholds are tuned. */
|
|
223
|
+
model?: string;
|
|
224
|
+
/** The NAME of the env var holding the credential, never the value — read at call time and
|
|
225
|
+
* never logged. §8's `apiKey` takes a value; this option deliberately does not. */
|
|
226
|
+
apiKeyEnv?: string;
|
|
227
|
+
/** Extra headers. Values expand `${ENV_VAR}` from the environment AT CALL TIME and are NEVER
|
|
228
|
+
* logged, identically to remote-MCP headers (§2). */
|
|
229
|
+
headers?: Record<string, string>;
|
|
230
|
+
/** Bounds ONE request (a classifier has no loop to bound). Default 10 s. */
|
|
231
|
+
timeoutMs?: number;
|
|
232
|
+
/** §8 Gap 2 injectable transport. Scope is the classifier path only. */
|
|
233
|
+
fetch?: typeof fetch;
|
|
234
|
+
/** Retries on transient errors (`408`/`429`/`500`/`502`/`503`/`504`/`529` + network). Default 2.
|
|
235
|
+
* Widen the status set with `retryableStatuses`. */
|
|
236
|
+
retries?: number;
|
|
237
|
+
/** Base backoff in ms (exponential). Default 500. */
|
|
238
|
+
retryBaseMs?: number;
|
|
239
|
+
/**
|
|
240
|
+
* Extra HTTP statuses to treat as retryable, ADDED to the default set
|
|
241
|
+
* (`429`/`500`/`502`/`503`/`504`/`529` plus `408` here). It can only widen: a host cannot remove `429` and
|
|
242
|
+
* lose `Retry-After` handling with it. This sets the DEFAULT classification; `onError` still
|
|
243
|
+
* runs per attempt and has the final say, so `onError` returning `"fail"` overrides a status
|
|
244
|
+
* listed here. Example: a Cloudflare-fronted origin that answers `520`–`527`.
|
|
245
|
+
*/
|
|
246
|
+
retryableStatuses?: readonly number[];
|
|
247
|
+
/** §8 Resilience. REUSES the client's `ErrorInfo`/`ErrorTier` and the `Retry-After`
|
|
248
|
+
* delay-seconds rule verbatim — there is no second retry policy, and no `"suspend"` tier. */
|
|
249
|
+
onError?: (info: ErrorInfo) => ErrorTier;
|
|
250
|
+
/** §8 Gap 1. Extra top-level keys shallow-merged into the body after the classifier builds its
|
|
251
|
+
* own; a `requestParams` key WINS on collision. Omit ⇒ body byte-identical. */
|
|
252
|
+
requestParams?: Record<string, unknown>;
|
|
253
|
+
/** §8 Gap 1. Receives the assembled body after the `requestParams` merge and returns the body
|
|
254
|
+
* to send. Order: base body → requestParams merge → bodyTransform → marshal, exactly as §8. */
|
|
255
|
+
bodyTransform?: (body: Record<string, any>) => Record<string, any> | undefined | void;
|
|
256
|
+
/** Emits `classifier.evaluate` events into the SAME §8 sink, and carries the
|
|
257
|
+
* degenerate-criteria warning as `classifier.warning`. */
|
|
258
|
+
onMetric?: (ev: MetricEvent) => void;
|
|
259
|
+
/** `style: "llm"` only — the §8 Client to emulate over. */
|
|
260
|
+
client?: Client;
|
|
261
|
+
/** `style: "custom"` only — the host's own function. */
|
|
262
|
+
evaluate?: EvaluateFn;
|
|
263
|
+
/** `style: "static"` only — the recorded corpus. */
|
|
264
|
+
decisions?: RecordedDecision[];
|
|
265
|
+
}
|
|
266
|
+
/** The whole seam: one verb. */
|
|
267
|
+
export declare class Classifier {
|
|
268
|
+
private readonly opts;
|
|
269
|
+
private readonly style;
|
|
270
|
+
private readonly baseUrl;
|
|
271
|
+
private readonly model;
|
|
272
|
+
private readonly apiKeyEnv;
|
|
273
|
+
private readonly timeoutMs;
|
|
274
|
+
private readonly corpus;
|
|
275
|
+
/** The once-per-question-key set for the degenerate warning, so a per-turn judge does not
|
|
276
|
+
* flood the sink. */
|
|
277
|
+
private readonly warned;
|
|
278
|
+
constructor(opts: ClassifierOptions);
|
|
279
|
+
/**
|
|
280
|
+
* The whole contract: a state plus typed questions in, a Decision out. Questions are
|
|
281
|
+
* INDEPENDENT — one answer is never context for another.
|
|
282
|
+
*/
|
|
283
|
+
evaluate(state: unknown, questions: Record<string, Question>, signal?: AbortSignal): Promise<Decision>;
|
|
284
|
+
/**
|
|
285
|
+
* Emits ONE warning per degenerate question key per classifier, naming the key, and changes
|
|
286
|
+
* nothing about the request. Repairing would invent option descriptions the caller did not
|
|
287
|
+
* write, and the library has no way to know what the options mean.
|
|
288
|
+
*/
|
|
289
|
+
private reportDegenerate;
|
|
290
|
+
/**
|
|
291
|
+
* Assembles the request: the canonical model + questions, plus state VERBATIM as the host
|
|
292
|
+
* supplied it, then the §8 Gap 1 pipeline in §8 order (base → requestParams merge →
|
|
293
|
+
* bodyTransform → marshal).
|
|
294
|
+
*/
|
|
295
|
+
private body;
|
|
296
|
+
/** Identifies a recorded decision by the canonical request AND the state. */
|
|
297
|
+
private staticKey;
|
|
298
|
+
private evaluateStatic;
|
|
299
|
+
private evaluateSystemOne;
|
|
300
|
+
/**
|
|
301
|
+
* The one POST, with the §8 retry budget, reusing the client's `ErrorInfo`/`ErrorTier`
|
|
302
|
+
* classifier and the `Retry-After` delay-seconds rule verbatim.
|
|
303
|
+
*
|
|
304
|
+
* NO CREDENTIAL VALUE AND NO EXPANDED HEADER VALUE APPEARS ON ANY PATH OUT OF HERE: an
|
|
305
|
+
* authentication failure names the status and the endpoint, nothing else, and a 401/403 body
|
|
306
|
+
* is never echoed back (a gateway happily reflects a bad Authorization header into its own
|
|
307
|
+
* 401 text).
|
|
308
|
+
*/
|
|
309
|
+
private post;
|
|
310
|
+
/** Credential and `${ENV_VAR}` headers resolve AT CALL TIME. Never logged, never returned. */
|
|
311
|
+
private requestHeaders;
|
|
312
|
+
/**
|
|
313
|
+
* The three question types rendered as ONE structured-output call on any §8 Client — the
|
|
314
|
+
* vendor-neutral fallback, so a host with no System One credential runs the same questions on
|
|
315
|
+
* a cheap chat model. `calibrated` is FALSE: the numbers are the model's self-report, not
|
|
316
|
+
* token probabilities.
|
|
317
|
+
*/
|
|
318
|
+
private evaluateLLM;
|
|
319
|
+
private emit;
|
|
320
|
+
private emitError;
|
|
321
|
+
}
|
|
322
|
+
/** Builds a Classifier, applying the §8B defaults and rejecting a style whose required option
|
|
323
|
+
* is missing before any call is made. */
|
|
324
|
+
export declare function createClassifier(opts?: ClassifierOptions): Classifier;
|
|
325
|
+
/**
|
|
326
|
+
* The §8B predicate. Degenerate ⇔ ANY of:
|
|
327
|
+
* 1. every value is empty (empty string or absent), or
|
|
328
|
+
* 2. every value equals its own key, or
|
|
329
|
+
* 3. every value is identical to every other value (n >= 2).
|
|
330
|
+
*
|
|
331
|
+
* A single-option choice (n === 1) is NEVER reported: there is nothing to differentiate. Returns
|
|
332
|
+
* the reason, or "" when the criteria are fine.
|
|
333
|
+
*/
|
|
334
|
+
export declare function degenerateCriteria(criteria: Record<string, string>): string;
|